├── .github └── FUNDING.yml ├── LICENSE ├── README.org ├── dwim-shell-command.el ├── dwim-shell-commands.el └── images ├── apple.gif ├── apple.mov ├── apple.webp ├── apple_x0.50.mov ├── blur.png ├── couldnt.png ├── diredmark.gif ├── diredmark_x0.50.mov ├── diredmark_x0.50.webp ├── history.png ├── progress.gif ├── progress.mov ├── progress.webp ├── progress_x0.50.mov ├── showme.png ├── template.png ├── togif.mov ├── togif.webp ├── togif_x0.50.gif ├── togif_x0.50.mov ├── togif_x0.50_x1.5.gif ├── togif_x0.50_x2.mov ├── togif_x1.5.gif └── togif_x1.5.mov /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [xenodium] 4 | -------------------------------------------------------------------------------- /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 | . 675 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | 👉 [[https://github.com/sponsors/xenodium][Support this work via GitHub Sponsors]] 2 | 3 | * Bring command-line utilities to your Emacs workflow 4 | 5 | Use =dwim-shell-command-on-marked-files= to define new functions that apply command-line utilities to current buffer or [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html][dired]] files. 6 | 7 | For example: 8 | 9 | #+begin_src emacs-lisp :lexical no 10 | (defun my/dwim-shell-command-convert-to-gif () 11 | "Convert all marked videos to optimized gif(s)." 12 | (interactive) 13 | (dwim-shell-command-on-marked-files 14 | "Convert to gif" 15 | "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif'" 16 | :utils "ffmpeg")) 17 | #+end_src 18 | 19 | Can be applied as: 20 | 21 | #+HTML: 22 | 23 | This makes wrapping one-liners a breeze, so let's do some more... 24 | 25 | ** One-liners 26 | 27 | #+begin_src emacs-lisp :lexical no 28 | (defun my/dwim-shell-command-convert-image-to-jpg () 29 | "Convert all marked images to jpg(s)." 30 | (interactive) 31 | (dwim-shell-command-on-marked-files 32 | "Convert to jpg" 33 | "convert -verbose '<>' '<>.jpg'" 34 | :utils "convert")) 35 | 36 | (defun my/dwim-shell-command-convert-audio-to-mp3 () 37 | "Convert all marked audio to mp3(s)." 38 | (interactive) 39 | (dwim-shell-command-on-marked-files 40 | "Convert to mp3" 41 | "ffmpeg -stats -n -i '<>' -acodec libmp3lame '<>.mp3'" 42 | :utils "ffmpeg")) 43 | 44 | (defun dwim-shell-commands-http-serve-dir () 45 | "HTTP serve current directory." 46 | (interactive) 47 | (dwim-shell-command-on-marked-files 48 | "HTTP serve current dir" 49 | "python3 -m http.server" 50 | :utils "python3" 51 | :focus-now t 52 | :no-progress t)) 53 | #+end_src 54 | 55 | ** Multi-line scripts 56 | 57 | #+begin_src emacs-lisp :lexical no 58 | (defun dwim-shell-commands-image-view-location-in-openstreetmap () 59 | "Open image(s) location in map/browser." 60 | (interactive) 61 | (dwim-shell-command-on-marked-files 62 | "Browse location" 63 | "lat=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f2-2)\" 64 | if [ -z \"$lat\" ]; then 65 | echo \"no latitude\" 66 | exit 1 67 | fi 68 | lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f3-3)\" 69 | if [ -z \"$lon\" ]; then 70 | echo \"no longitude\" 71 | exit 1 72 | fi 73 | if [[ $OSTYPE == darwin* ]]; then 74 | open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" 75 | else 76 | xdg-open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" 77 | fi" 78 | :utils "exiftool" 79 | :error-autofocus t 80 | :silent-success t)) 81 | #+end_src 82 | 83 | ** Pick your language 84 | 85 | #+begin_src emacs-lisp :lexical no 86 | (defun dwim-shell-command-csv-to-json-via-python () 87 | "Convert csv file to json (via Python)." 88 | (interactive) 89 | (dwim-shell-command-on-marked-files 90 | "Convert csv file to json (via Python)." 91 | " 92 | import csv 93 | import json 94 | text = json.dumps({ \"values\": list(csv.reader(open('<>')))}) 95 | fpath = '<>.json' 96 | with open(fpath , 'w') as f: 97 | f.write(text)" 98 | :shell-util "python" 99 | :shell-args "-c")) 100 | 101 | (defun dwim-shell-command-csv-to-json-via-swift () 102 | "Convert csv file to json (via Swift)." 103 | (interactive) 104 | (dwim-shell-command-on-marked-files 105 | "Convert csv file to json (via Swift)." 106 | "import Foundation 107 | import TabularData 108 | let filePath = \"<>\" 109 | print(\"reading \\(filePath)\") 110 | let content = try String(contentsOfFile: filePath).trimmingCharacters(in: .whitespacesAndNewlines) 111 | let parsedCSV = content.components(separatedBy: CSVWritingOptions().newline).map{ 112 | $0.components(separatedBy: \",\") 113 | } 114 | let jsonEncoder = JSONEncoder() 115 | let jsonData = try jsonEncoder.encode([\"value\": parsedCSV]) 116 | let json = String(data: jsonData, encoding: String.Encoding.utf8) 117 | let outURL = URL(fileURLWithPath:\"<>.json\") 118 | try json!.write(to: outURL, atomically: true, encoding: String.Encoding.utf8) 119 | print(\"wrote \\(outURL)\")" 120 | :shell-pipe "swift -")) 121 | #+end_src 122 | * Build a rich toolbox (or use mine) 123 | 124 | While you may want to build your own command toolbox over time, I've also [[#my-toolbox][shared my toolbox]] (close to 100 commands). 125 | 126 | If you create new command not found in my list, I'd love to hear about it. File an [[https://github.com/xenodium/dwim-shell-command/issues/new][issue]] or just ping me ([[https://indieweb.social/@xenodium][Mastodon]] / [[https://twitter.com/xenodium][Twitter]] / [[https://www.reddit.com/user/xenodium][Reddit]] / [[mailto:me__AT__xenodium.com][Email]]). 127 | 128 | * A =shell-command=, =async-shell-command=, and =dired-do-shell-command= alternative 129 | 130 | #+HTML: 131 | 132 | ** Run M-x =dwim-shell-command= to execute disposable [[https://en.wikipedia.org/wiki/DWIM][DWIM]] shell commands 133 | - Asynchronously. 134 | - Using noweb templates. 135 | - Automatically injecting files (from [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html][dired]] or other buffers) or kill ring. 136 | - Managing buffer focus with heuristics. 137 | - Showing progress bar. 138 | - Quick buffer exit. 139 | - More reusable history. 140 | 141 | * Which files 142 | 143 | =dwim-shell-command= determines which file(s) you want the command to operate on. 144 | 145 | If visiting a [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html][dired]] buffer, draw the marked file(s). 146 | 147 | #+HTML: 148 | 149 | If visiting a buffer with an associated file, use that. 150 | 151 | #+HTML: 152 | 153 | * noweb templates 154 | 155 | Operate on drawn files using either the following: 156 | 157 | - =<>= (file path) 158 | - =<>= (file path without extension) 159 | - =<>= (extension) 160 | - =<>= (generate a temporary directory) 161 | - =<<*>>= (all files joined) 162 | - =<>= (clipboard) 163 | 164 | For example: 165 | 166 | With drawn files =path/to/image1.png= and =path/to/image2.png= 167 | 168 | =convert <> <>.jpg= expands to 169 | 170 | #+begin_src sh 171 | convert path/to/image1.png path/to/image1.jpg 172 | convert path/to/image2.png path/to/image2.jpg 173 | #+end_src 174 | 175 | while =ls -lh <<*>>= expands to 176 | 177 | #+begin_src sh 178 | ls -lh path/to/image1.png path/to/image2.png 179 | #+end_src 180 | 181 | * Focus 182 | 183 | =dwim-shell-command= creates a process buffer to capture command output, but neither displays nor focuses on it by default. Instead, it tries to guess what's more convenient to focus on. 184 | 185 | While the process is busy, show a spinner in the minibuffer. No focus changes. 186 | 187 | #+HTML: 188 | 189 | After process is finished: 190 | 191 | If there were any files created in the =default-directory=, jump to a [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html][dired]] buffer and move point to the new file (via [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired-Enter.html][dired-jump]]). 192 | 193 | [[file:images/showme.png]] 194 | 195 | 196 | If no new files were created, automatically switch focus to the process buffer and display its output. 197 | 198 | #+HTML: 199 | 200 | Note: You can prevent this automatic focus by prepending your command with whitespace. 201 | 202 | " convert '<>' '<>.jpg'" 203 | 204 | If the shell command caused any errors, offer to focus the process buffer and display its output. 205 | 206 | #+HTML: 207 | 208 | *** Easily create utilities 209 | 210 | Command-line utilities like [[https://ffmpeg.org/][ffmpeg]] can be easily integrated into Emacs flows (without the need to remember any flags or parameters) by wrapping command invocations into functions and invoking via =M-x= (or your favorite binding). Same DWIM behavior from =dwim-shell-command= is inherited. 211 | 212 | * Quick exit 213 | 214 | Process buffers are read-only and can be quickly closed by pressing =q=. 215 | * More reusable history 216 | Because of templates, command history becomes automatically reusable in other contexts. 217 | 218 | #+HTML: 219 | 220 | * Install 221 | 222 | =dwim-shell-command= is available on [[https://melpa.org/#/dwim-shell-command][MELPA]]. 223 | 224 | [[https://melpa.org/#/dwim-shell-command][file:https://melpa.org/packages/dwim-shell-command.svg]] 225 | 226 | 1. Install via M-x /package-install/. 227 | 2. Require, set edit style, and add company backend: 228 | 229 | #+begin_src emacs-lisp 230 | (require 'dwim-shell-command) 231 | #+end_src 232 | 233 | Now you're ready to run 234 | 235 | M-x =dwim-shell-command= 236 | 237 | ** use-package 238 | 239 | Alternatively, can also install via [[https://github.com/jwiegley/use-package][use-package]], define your own commands and remap to =shell-command='s existing binding using something like: 240 | 241 | #+begin_src emacs-lisp :lexical no 242 | (use-package dwim-shell-command 243 | :ensure t 244 | :bind (([remap shell-command] . dwim-shell-command) 245 | :map dired-mode-map 246 | ([remap dired-do-async-shell-command] . dwim-shell-command) 247 | ([remap dired-do-shell-command] . dwim-shell-command) 248 | ([remap dired-smart-shell-command] . dwim-shell-command)) 249 | :config 250 | (defun my/dwim-shell-command-convert-to-gif () 251 | "Convert all marked videos to optimized gif(s)." 252 | (interactive) 253 | (dwim-shell-command-on-marked-files 254 | "Convert to gif" 255 | "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif'" 256 | :utils "ffmpeg"))) 257 | #+end_src 258 | * My toolbox 259 | 260 | I'm including an optional package ([[https://github.com/xenodium/dwim-shell-command/blob/main/dwim-shell-commands.el][dwim-shell-commands.el]]), with all the command line utilities I've brought in over time. You can load this optional package via: 261 | 262 | #+begin_src emacs-lisp :lexical no 263 | (require 'dwim-shell-commands) 264 | #+end_src 265 | 266 | Note: =dwim-shell-command(s).el= gives you all commands, while =dwim-shell-command.el= provides only the building blocks. 267 | 268 | Here are all the commands I've added so far... 269 | 270 | #+BEGIN_SRC emacs-lisp :results table :colnames '("Command" "Description") :exports results 271 | (let ((rows)) 272 | (mapatoms 273 | (lambda (symbol) 274 | (when (and (string-match "^dwim-shell-commands" 275 | (symbol-name symbol)) 276 | (not (string-match "git-set-author-name-and-email-credentials" 277 | (symbol-name symbol))) 278 | (commandp symbol)) 279 | (push `(,(symbol-name symbol) 280 | ,(car 281 | (split-string 282 | (or (documentation symbol t) "") 283 | "\n"))) 284 | rows)))) 285 | (seq-sort (lambda (row1 row2) 286 | (string-greaterp (seq-elt row2 0) (seq-elt row1 0))) 287 | rows)) 288 | #+END_SRC 289 | 290 | #+RESULTS: 291 | | Command | Description | 292 | |--------------------------------------------------------------+---------------------------------------------------------------------------| 293 | | dwim-shell-commands-audio-to-mp3 | Convert all marked audio to mp3(s). | 294 | | dwim-shell-commands-clip-round-rect-gif | Clip gif(s) with round rectangle. | 295 | | dwim-shell-commands-clipboard-to-qr | Generate a QR code from clipboard. | 296 | | dwim-shell-commands-copy-to-desktop | Copy file to ~/Desktop. | 297 | | dwim-shell-commands-copy-to-downloads | Copy file to ~/Downloads. | 298 | | dwim-shell-commands-docx-to-pdf | Convert docx(s) to pdf (via latex). | 299 | | dwim-shell-commands-download-clipboard-stream-url | Download clipboard URL. | 300 | | dwim-shell-commands-drop-video-audio | Drop audio from all marked videos. | 301 | | dwim-shell-commands-duplicate | Duplicate file. | 302 | | dwim-shell-commands-epub-to-org | Convert epub(s) to org. | 303 | | dwim-shell-commands-external-ip | Copy external IP to kill ring. | 304 | | dwim-shell-commands-files-combined-size | Get files combined file size. | 305 | | dwim-shell-commands-gif-to-video | Convert all marked gif(s) to video(s). | 306 | | dwim-shell-commands-git-clone-clipboard-url | Clone git URL in clipboard to `default-directory'. | 307 | | dwim-shell-commands-git-clone-clipboard-url-to-downloads | Clone git URL in clipboard to "~/Downloads/". | 308 | | dwim-shell-commands-git-delete-untracked-files | Delete untracked git files in `default-directory'. | 309 | | dwim-shell-commands-git-list-untracked-files | List untracked git files in `default-directory'. | 310 | | dwim-shell-commands-http-serve-dir | HTTP serve current directory. | 311 | | dwim-shell-commands-image-add-drop-shadow | Add a drop shadow. | 312 | | dwim-shell-commands-image-apply-ios-round-corners | Apply iOS round corners to image(s). | 313 | | dwim-shell-commands-image-clear-exif-metadata | Clear EXIF metadata in image(s). | 314 | | dwim-shell-commands-image-exif-metadata | View EXIF metadata in image(s). | 315 | | dwim-shell-commands-image-horizontal-flip | Horizontally flip image(s). | 316 | | dwim-shell-commands-image-reverse-geocode-location | Reverse geocode image(s) location. | 317 | | dwim-shell-commands-image-scan-code | Scan any code (ie. qr, bar, etc) from image(s). | 318 | | dwim-shell-commands-image-to-grayscale | Convert all marked images to grayscale. | 319 | | dwim-shell-commands-image-to-icns | Convert png to icns icon. | 320 | | dwim-shell-commands-image-to-jpg | Convert all marked images to jpg(s). | 321 | | dwim-shell-commands-image-to-png | Convert all marked images to png(s). | 322 | | dwim-shell-commands-image-trim-borders | Trim image(s) border (useful for video screenshots). | 323 | | dwim-shell-commands-image-vertical-flip | Horizontally flip image(s). | 324 | | dwim-shell-commands-image-view-location-in-openstreetmap | Open image(s) location in map/browser. | 325 | | dwim-shell-commands-join-as-pdf | Join all marked images as a single pdf. | 326 | | dwim-shell-commands-join-images-horizontally | Join all marked images horizontally as a single image. | 327 | | dwim-shell-commands-join-images-vertically | Join all marked images vertically as a single image. | 328 | | dwim-shell-commands-keep-pdf-page | Keep a page from pdf. | 329 | | dwim-shell-commands-kill-gpg-agent | Kill (thus restart) gpg agent. | 330 | | dwim-shell-commands-kill-process | Select and kill process. | 331 | | dwim-shell-commands-macos-abort-recording-window | Stop recording a macOS window. | 332 | | dwim-shell-commands-macos-add-to-photos | Add to Photos.app. | 333 | | dwim-shell-commands-macos-bin-plist-to-xml | Convert binary plist to xml. | 334 | | dwim-shell-commands-macos-caffeinate | Invoke caffeinate to prevent mac from sleeping. | 335 | | dwim-shell-commands-macos-convert-to-mp4 | Convert to mov to mp4 | 336 | | dwim-shell-commands-macos-empty-trash | Empty macOS trash. | 337 | | dwim-shell-commands-macos-end-recording-window | Stop recording a macOS window. | 338 | | dwim-shell-commands-macos-install-iphone-device-ipa | Install iPhone device .ipa. | 339 | | dwim-shell-commands-macos-make-finder-alias | Make macOS Finder alias. | 340 | | dwim-shell-commands-macos-ocr-text-from-desktop-region | Select a macOS desktop area to OCR and copy recognized text to kill ring. | 341 | | dwim-shell-commands-macos-ocr-text-from-image | OCR file and copy recognized text to kill ring. | 342 | | dwim-shell-commands-macos-open-with | Open file(s) with specific external app. | 343 | | dwim-shell-commands-macos-open-with-firefox | Open file(s) in Firefox. | 344 | | dwim-shell-commands-macos-open-with-safari | Open file(s) in Safari. | 345 | | dwim-shell-commands-macos-reveal-in-finder | Reveal selected files in macOS Finder. | 346 | | dwim-shell-commands-macos-screenshot-window | Select and screenshot macOS window. | 347 | | dwim-shell-commands-macos-set-default-app | Set default app for file(s). | 348 | | dwim-shell-commands-macos-share | Share selected files from macOS. | 349 | | dwim-shell-commands-macos-start-recording-window | Select and start recording a macOS window. | 350 | | dwim-shell-commands-macos-toggle-bluetooth-device-connection | Toggle Bluetooth device connection. | 351 | | dwim-shell-commands-macos-toggle-dark-mode | Toggle macOS dark mode. | 352 | | dwim-shell-commands-macos-toggle-display-rotation | Rotate display. | 353 | | dwim-shell-commands-macos-version-and-hardware-overview-info | View macOS version and hardware overview info. | 354 | | dwim-shell-commands-make-swift-package-executable | Create a swift package executable | 355 | | dwim-shell-commands-make-swift-package-library | Create a swift package library | 356 | | dwim-shell-commands-make-transparent-png | Create a transparent png. | 357 | | dwim-shell-commands-move-to-desktop | Move file to ~/Desktop. | 358 | | dwim-shell-commands-move-to-downloads | Move file to ~/Downloads. | 359 | | dwim-shell-commands-ndjson-to-org | Convert ndjson file to org. | 360 | | dwim-shell-commands-open-clipboard-url | Open clipboard URL. Offer to stream if possible. | 361 | | dwim-shell-commands-open-externally | Open file(s) externally. | 362 | | dwim-shell-commands-optimize-gif | Convert all marked videos to optimized gif(s). | 363 | | dwim-shell-commands-pass-git-pull | Pass git pull. | 364 | | dwim-shell-commands-pdf-password-protect | Add a password to pdf(s). | 365 | | dwim-shell-commands-pdf-password-unprotect | Remove a password from pdf(s). | 366 | | dwim-shell-commands-pdf-to-txt | Convert pdf to txt. | 367 | | dwim-shell-commands-ping-google | Ping google.com. | 368 | | dwim-shell-commands-rename-all | Rename all marked file(s). | 369 | | dwim-shell-commands-reorient-image | Reorient images. | 370 | | dwim-shell-commands-resize-gif | Resize marked gif(s). | 371 | | dwim-shell-commands-resize-image-by-factor | Resize marked image(s) by factor. | 372 | | dwim-shell-commands-resize-image-in-pixels | Resize marked image(s) in pixels. | 373 | | dwim-shell-commands-resize-video | Resize marked images. | 374 | | dwim-shell-commands-set-media-artwork-image-metadata | Set image artwork metadata for media file(s). | 375 | | dwim-shell-commands-sha-256-hash-file-at-clipboard-url | Download file at clipboard URL and generate SHA-256 hash. | 376 | | dwim-shell-commands-speed-up-gif | Speeds up gif(s). | 377 | | dwim-shell-commands-speed-up-video | Speed up video(s). | 378 | | dwim-shell-commands-speed-up-video-fragment | Speed up fragment in video(s). | 379 | | dwim-shell-commands-stream-clipboard-url | Stream clipboard URL using mpv. | 380 | | dwim-shell-commands-svg-to-favicons | Convert svg to common favicons. | 381 | | dwim-shell-commands-svg-to-png | Convert all marked svg(s) to png(s). | 382 | | dwim-shell-commands-tesseract-ocr-text-from-image | Extract text from image via tesseract. | 383 | | dwim-shell-commands-unzip | Unzip all marked archives (of any kind) using `atool'. | 384 | | dwim-shell-commands-upload-to-0x0 | Upload the marked files to 0x0.st | 385 | | dwim-shell-commands-video-to-gif | Convert all marked videos to gif(s). | 386 | | dwim-shell-commands-video-to-hevc-mkv | Convert all marked videos to hevc mkv. | 387 | | dwim-shell-commands-video-to-mp3 | Convert video(s) to mp3. | 388 | | dwim-shell-commands-video-to-mp3-with-artwork | Convert video(s) to mp3 (keep frame as artwork). | 389 | | dwim-shell-commands-video-to-optimized-gif | Convert all marked videos to optimized gif(s). | 390 | | dwim-shell-commands-video-to-thumbnail | Generate a thumbnail for marked video(s). | 391 | | dwim-shell-commands-video-to-webp | Convert all marked videos to webp(s). | 392 | | dwim-shell-commands-video-trim-beginning | Drop audio from all marked videos. | 393 | | dwim-shell-commands-video-trim-end | Drop audio from all marked videos. | 394 | | dwim-shell-commands-view-sqlite-schema-diagram | View sqlite schema diagram. | 395 | | dwim-shell-commands-webp-to-gif | Convert all marked webp(s) to gif(s). | 396 | | dwim-shell-commands-webp-to-video | Convert all marked webp(s) to video(s). | 397 | | dwim-shell-commands-zip | Zip all marked files into archive.zip. | 398 | | dwim-shell-commands-zip-password-protect | Protect/encrypt zip file(s) with password. | 399 | 400 | * Evaluating elisp functions 401 | 402 | This can be done with either of the following: 403 | 404 | #+begin_src emacs-lisp :lexical no 405 | emacs --quick --batch --eval '(message "<>")' 406 | #+end_src 407 | 408 | #+begin_src emacs-lisp :lexical no 409 | emacsclient --eval '(message "<>")' 410 | #+end_src 411 | 412 | * Support this work 413 | 414 | 👉 [[https://github.com/sponsors/xenodium][Support my work via GitHub Sponsors]] 415 | -------------------------------------------------------------------------------- /dwim-shell-command.el: -------------------------------------------------------------------------------- 1 | ;;; dwim-shell-command.el --- Shell commands with DWIM behaviour -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2022 Alvaro Ramirez https://xenodium.com 4 | 5 | ;; Author: Alvaro Ramirez 6 | ;; Package-Requires: ((emacs "28.1")) 7 | ;; URL: https://github.com/xenodium/dwim-shell-command 8 | ;; Version: 0.63.2 9 | 10 | ;; This package is free software; you can redistribute it and/or modify 11 | ;; it under the terms of the GNU General Public License as published by 12 | ;; the Free Software Foundation; either version 3, or (at your option) 13 | ;; any later version. 14 | 15 | ;; This package is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with GNU Emacs. If not, see . 22 | 23 | ;;; Commentary: 24 | 25 | ;; Provides `dwim-shell-command' as an opinionated DWIM alternative to 26 | ;; `shell-command'. 27 | ;; 28 | ;; Use `dwim-shell-command-on-marked-files' to create your own command 29 | ;; line utilities, invoked via M-x. 30 | ;; 31 | ;; See examples at https://github.com/xenodium/dwim-shell-command/blob/main/dwim-shell-commands.el 32 | 33 | ;;; Code: 34 | 35 | (require 'cl-lib) 36 | (require 'comint) 37 | (require 'dired) 38 | (require 'dired-aux) 39 | (require 'map) 40 | (require 'seq) 41 | (require 'simple) 42 | (require 'subr-x) 43 | (require 'view) 44 | 45 | (defcustom dwim-shell-command-prompt 46 | "DWIM shell command (<> <> <>): " 47 | "`dwim-shell-command' prompt. Modify if shorter is preferred." 48 | :type 'string 49 | :group 'dwim-shell-command) 50 | 51 | (defcustom dwim-shell-command-default-command 52 | " '<>'" 53 | "Set to nil if no default shell command wanted." 54 | :type 'string 55 | :group 'dwim-shell-command) 56 | 57 | (defcustom dwim-shell-command-buffer-name 58 | "DWIM shell command" 59 | "`dwim-shell-command' buffer name. Modify if shorter is preferred." 60 | :type 'string 61 | :group 'dwim-shell-command) 62 | 63 | (defcustom dwim-shell-command-prompt-on-error nil 64 | "If t, prompt user to focus buffer on process error. 65 | Otherwise, automatically focus buffer on process error." 66 | :type 'boolean 67 | :group 'dwim-shell-command) 68 | 69 | (defcustom dwim-shell-command-use-absolute-paths nil 70 | "If t, generate absolute paths in templates. Relative otherwise." 71 | :type 'boolean 72 | :group 'dwim-shell-command) 73 | 74 | (defcustom dwim-shell-command-shell-util nil 75 | "Shell util, for example: \"zsh\" or \"bash\". 76 | Set to nil to use `shell-file-name'." 77 | :type 'string 78 | :group 'dwim-shell-command) 79 | 80 | (defcustom dwim-shell-command-shell-args nil 81 | "Shell util, for example: '(\"-x\" \"-c\"). 82 | Set to nil to use `shell-command-switch'." 83 | :type '(repeat string) 84 | :group 'dwim-shell-command) 85 | 86 | (defcustom dwim-shell-command-shell-trace nil 87 | "Attempt to add --xtrace to shell command to debug." 88 | :type 'boolean 89 | :group 'dwim-shell-command) 90 | 91 | (defcustom dwim-shell-command-done-buffer-name 92 | (lambda (name) 93 | (format "%s %s" name (propertize "done" 'face 'success))) 94 | "Function to format buffer name on success. 95 | Use `identify' to remove formatting." 96 | :type 'function 97 | :group 'dwim-shell-command) 98 | 99 | (defcustom dwim-shell-command-error-buffer-name 100 | (lambda (name) 101 | (format "%s %s" name (propertize "error" 'face 'error))) 102 | "Function to format buffer name on error. 103 | Use `identify' to remove formatting." 104 | :type 'function 105 | :group 'dwim-shell-command) 106 | 107 | (defvar dwim-shell-command--commands nil "All commands in progress.") 108 | 109 | (cl-defstruct 110 | dwim-shell-command--command 111 | "Describes a command in progress." 112 | script 113 | process 114 | name 115 | calling-buffer 116 | reporter 117 | on-completion 118 | files-before 119 | silent-success 120 | error-autofocus 121 | monitor-directory) 122 | 123 | ;;;###autoload 124 | (defun dwim-shell-command (prefix) 125 | "Execute DWIM shell command asynchronously using noweb templates. 126 | 127 | Which files 128 | 129 | `dwim-shell-command' attempts to guess which file(s) you may want 130 | the command to operate on. 131 | 132 | 1. If visiting a `dired' buffer, draw the marked file(s). 133 | 2. If visiting any other buffer with an associated file, use that. 134 | 135 | Templates 136 | 137 | Operate on drawn files using either the following: 138 | 139 | <> (file path) 140 | <> (file path without extension) 141 | <> (extension) 142 | <> (generate a temporary directory) 143 | <<*>> (all files joined) 144 | <> (clipboard) 145 | <>, <<1n>>, or <> (for current iteration) 146 | 147 | For example: 148 | 149 | With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") 150 | 151 | \"convert '<>' '<>.jpg'\" expands to 152 | 153 | \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" 154 | \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" 155 | 156 | while \"ls -lh <<*>>\" expands to 157 | 158 | \"ls -lh path/to/image1.png path/to/image2.png\" 159 | 160 | Focus 161 | 162 | `dwim-shell-command' creates a process buffer to capture command 163 | output, but doesn't display or focus on it by default. Instead, 164 | it tries to guess what's more convenient to focus on. 165 | 166 | While the process is busy, show a spinner in the minibuffer. No 167 | focus changes. 168 | 169 | After process is finished: 170 | 171 | 1. If there were any files created in the `default-directory', 172 | jump to a `dired' buffer and move point to the new file (via 173 | `dired-jump'). 174 | 175 | 2. If no new files were created, automatically switch focus to the 176 | process buffer and display its output. 177 | 178 | Note: You can prevent this automatic focus by prepending your 179 | command with whitespace. 180 | 181 | | 182 | V 183 | \" convert '<>' '<>.jpg'\" 184 | 185 | 3. If the shell command caused any errors, offer to focus the 186 | process buffer and display its output. 187 | 188 | Quick exit 189 | 190 | Process buffers are read-only and can be quickly closed by 191 | pressing `q'. 192 | 193 | Prefix 194 | 195 | With PREFIX, execute command that number of times." 196 | (interactive "p") 197 | (let ((script (dwim-shell-command--read-shell-command))) 198 | (dwim-shell-command-on-marked-files 199 | dwim-shell-command-buffer-name script 200 | :repeat prefix 201 | :shell-util dwim-shell-command-shell-util 202 | :shell-args dwim-shell-command-shell-args 203 | :silent-success (string-prefix-p " " script) 204 | :error-autofocus (not dwim-shell-command-prompt-on-error)))) 205 | 206 | (defun dwim-shell-command--read-shell-command () 207 | "Read a shell command from the minibuffer, using `shell-command-history'." 208 | (minibuffer-with-setup-hook 209 | (lambda () 210 | (beginning-of-line) 211 | (setq-local minibuffer-default-add-function 212 | #'minibuffer-default-add-shell-commands)) 213 | (read-from-minibuffer dwim-shell-command-prompt dwim-shell-command-default-command nil nil 'shell-command-history))) 214 | 215 | (cl-defun dwim-shell-command-on-marked-files (buffer-name script &key utils extensions shell-util shell-args shell-trace shell-pipe post-process-template on-completion repeat silent-success no-progress error-autofocus monitor-directory focus-now join-separator temp-dir) 216 | "Create DWIM utilities executing templated SCRIPT on given files. 217 | 218 | Here's a simple utility invoking SCRIPT to convert image files to jpg. 219 | 220 | (defun dwim-shell-command-convert-image-to-jpg () 221 | \"Convert all marked images to jpg(s).\" 222 | (interactive) 223 | (dwim-shell-command-on-marked-files 224 | \"Convert to jpg\" 225 | \"convert -verbose '<>' '<>.jpg'\" 226 | :utils \"convert\")) 227 | 228 | Check `dwim-shell-command-commands.el' for more examples. 229 | 230 | All command process output is written to a buffer with BUFFER-NAME. 231 | 232 | All params explained in `dwim-shell-command-execute-script'. 233 | 234 | Which files 235 | 236 | `dwim-shell-command-on-marked-files' attempts to guess which file(s) 237 | you may want the command to operate on. 238 | 239 | 1. If visiting a `dired' buffer, draw the marked file(s). 240 | 2. If visiting any other buffer with an associated file, use that. 241 | 242 | Templates 243 | 244 | Operate on drawn files using either the following: 245 | 246 | <> (file path) 247 | <> (file path without extension) 248 | <> (extension) 249 | <> (generate a temporary directory) 250 | <<*>> (all files joined) 251 | <> (clipboard) 252 | <>, <<1n>>, or <> (for current iteration) 253 | 254 | For example: 255 | 256 | With drawn files '(\"path/to/image1.png\" \"path/to/image2.png\") 257 | 258 | \"convert '<>' '<>.jpg'\" expands to 259 | 260 | \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" 261 | \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" 262 | 263 | while \"ls -lh <<*>>\" expands to 264 | 265 | \"ls -lh path/to/image1.png path/to/image2.png\" 266 | 267 | Focus 268 | 269 | `dwim-shell-command-on-marked-files' creates a process buffer to 270 | capture command output, but doesn't display or focus on it by 271 | default. Instead, it tries to guess what's more convenient to focus 272 | on. 273 | 274 | While the process is busy, show a spinner in the minibuffer. No 275 | focus changes. 276 | 277 | After process is finished: 278 | 279 | 1. If there were any files created in the `default-directory', 280 | jump to a `dired' buffer and move point to the new file (via 281 | `dired-jump'). 282 | 283 | 2. If no new files were created, automatically switch focus to the 284 | process buffer and display its output. 285 | 286 | Note: You can prevent this automatic focus by prepending your 287 | command with whitespace. 288 | 289 | | 290 | V 291 | \" convert '<>' '<>.jpg'\" 292 | 293 | 3. If the shell command caused any errors, offer to focus the 294 | process buffer and display its output. 295 | 296 | Quick exit 297 | 298 | Process buffers are read-only and can be quickly closed by 299 | pressing `q'." 300 | (dwim-shell-command-execute-script buffer-name script 301 | :files (dwim-shell-command--files) 302 | :utils utils 303 | :extensions extensions 304 | :shell-util shell-util 305 | :shell-args shell-args 306 | :shell-trace shell-trace 307 | :shell-pipe shell-pipe 308 | :post-process-template post-process-template 309 | :on-completion on-completion 310 | :silent-success silent-success 311 | :no-progress no-progress 312 | :repeat repeat 313 | :error-autofocus error-autofocus 314 | :monitor-directory monitor-directory 315 | :focus-now focus-now 316 | :join-separator join-separator 317 | :temp-dir temp-dir)) 318 | 319 | (cl-defun dwim-shell-command-execute-script (buffer-name script &key files extensions shell-util shell-args shell-trace shell-pipe utils post-process-template on-completion silent-success temp-dir repeat no-progress error-autofocus monitor-directory focus-now join-separator) 320 | "Execute a script asynchronously, DWIM style with SCRIPT and BUFFER-NAME. 321 | 322 | :FILES are used to instantiate SCRIPT as a noweb template. 323 | 324 | The following are supported: 325 | 326 | <> (file path) 327 | <> (file path without extension) 328 | <> (extension) 329 | <> (generate a temporary directory) 330 | <<*>> (all files joined) 331 | <> (clipboard) 332 | <>, <<1n>>, or <> (for current iteration) 333 | 334 | For example: 335 | 336 | Given :FILES '(\"path/to/image1.png\" \"path/to/image2.png\") 337 | 338 | \"convert '<>' '<>.jpg'\" expands to 339 | 340 | \"convert 'path/to/image1.png' 'path/to/image1.jpg'\" 341 | \"convert 'path/to/image2.png' 'path/to/image2.jpg'\" 342 | 343 | and \"ls -lh <<*>>\" expands to 344 | 345 | \"ls -lh path/to/image1.png path/to/image2.png\" 346 | 347 | :EXTENSIONS ensures that all files in :FILES have the given 348 | extensions. Can be either single string \"png\" or a list '(\"png\" \"jpg\"). 349 | 350 | :SHELL-UTIL and :SHELL-ARGS can be used to specify SCRIPT interpreter. 351 | 352 | For python, use: 353 | 354 | (dwim-shell-command-execute-script 355 | \"Print Pi\" 356 | \"import math 357 | print math.pi\" 358 | :shell-util \"python\" 359 | :shell-args \"-c\") 360 | 361 | :SHELL-PIPE can be used to pipe SCRIPT to it 362 | 363 | For swift, use: 364 | 365 | (dwim-shell-command-on-marked-files 366 | \"Print Pi\" 367 | \"print(Double.pi)\" 368 | :shell-pipe \"swift -\") 369 | 370 | :UTILS ensures that all needed command line utilities are installed. 371 | Can be either a single string \"ffmpeg\" or a list '(\"ffmpet\" \"convert\"). 372 | 373 | :POST-PROCESS-TEMPLATE enables processing template further after noweb 374 | instantiation. 375 | 376 | :ON-COMPLETION is invoked after SCRIPT executes (disabling DWIM 377 | internal behavior). 378 | 379 | :SILENT-SUCCESS to avoid jumping to process buffer if neither error 380 | nor file generated. 381 | 382 | :TEMP-DIR to generate a temporary directory for this command. 383 | This is implied when <> appears in the script. 384 | 385 | :REPEAT Use to repeat script N number of times. 386 | 387 | :NO-PROGRESS Suppress progress reporting. 388 | 389 | :ERROR-AUTOFOCUS Automatically focus process buffer on error. 390 | 391 | :MONITOR-DIRECTORY Monitor this directory for new files. 392 | 393 | :FOCUS-NOW Immediately focus process buffer once started." 394 | (cl-assert buffer-name nil "Script must have a buffer name") 395 | (cl-assert (not (string-empty-p script)) nil "Script must not be empty") 396 | (when (stringp extensions) 397 | (setq extensions (list extensions))) 398 | (when (and shell-util (stringp shell-util)) 399 | (setq shell-util (list shell-util))) 400 | (setq shell-util (or shell-util 401 | (when shell-file-name 402 | (list shell-file-name)) 403 | '("zsh"))) 404 | (setq shell-args (or shell-args 405 | (when shell-command-switch 406 | (list shell-command-switch)) 407 | '("-x" "-c"))) 408 | (when (and shell-args (stringp shell-args)) 409 | (setq shell-args (list shell-args))) 410 | ;; See if -x can be prepended. 411 | (when (and (not (seq-contains-p shell-args "-x")) 412 | (or shell-trace dwim-shell-command-shell-trace) 413 | (apply #'dwim-shell-command--program-test 414 | (seq-concatenate 415 | 'list shell-util '("-x") shell-args (list "echo")))) 416 | (setq shell-args (seq-concatenate 'list '("-x") shell-args))) 417 | (when (stringp utils) 418 | (setq utils (list utils))) 419 | (when (and (string-match-p "\<\\>" script 0) (not temp-dir)) 420 | (setq temp-dir (make-temp-file "dwim-shell-command-" t))) 421 | (when (and repeat (> repeat 1)) 422 | (cl-assert (<= (length files) 1) nil 423 | "Must not repeat when multiple files are selected.") 424 | (setq files (make-list repeat (or (seq-first files) "_no_file_selected_")))) 425 | (when (seq-empty-p files) 426 | (cl-assert (not (or (dwim-shell-command--contains-multi-file-ref script) 427 | (dwim-shell-command--contains-single-file-ref script))) 428 | nil "No files found to expand %s" 429 | (or (dwim-shell-command--contains-multi-file-ref script) 430 | (dwim-shell-command--contains-single-file-ref script)))) 431 | (when extensions 432 | (seq-do (lambda (file) 433 | (cl-assert (seq-contains-p extensions (downcase (file-name-extension file))) 434 | nil "Not a .%s file" (string-join extensions " ."))) 435 | files)) 436 | (seq-do (lambda (util) 437 | (cl-assert (executable-find util) nil (format "%s not installed" util))) 438 | utils) 439 | (let* ((replacements (dwim-shell-command--extract-queries script)) 440 | (proc-buffer (generate-new-buffer (format "*%s*" buffer-name))) 441 | (template script) 442 | (script "") 443 | (files-before) 444 | (proc) 445 | (progress-reporter) 446 | (padding (dwim-shell-command--digits (length files))) 447 | (n (or (dwim-shell-command--n-start-value template padding) "1"))) 448 | (if (seq-empty-p files) 449 | (setq script (dwim-shell-command--expand-file-template template nil post-process-template temp-dir n replacements)) 450 | (if (dwim-shell-command--contains-multi-file-ref template) 451 | (setq script (dwim-shell-command--expand-files-template template files post-process-template temp-dir replacements join-separator)) 452 | (seq-do (lambda (file) 453 | (setq script 454 | (concat script "\n" 455 | (dwim-shell-command--expand-file-template template file post-process-template temp-dir n replacements))) 456 | (setq n (dwim-shell-command--increment-string n padding))) 457 | files))) 458 | (setq script (string-trim script)) 459 | (with-current-buffer proc-buffer 460 | (let ((inhibit-message t)) 461 | ;; Silence noise of entering shell-mode. 462 | (comint-mode)) 463 | (setq default-directory default-directory) 464 | (shell-command-save-pos-or-erase) 465 | (view-mode +1) 466 | (setq view-exit-action 'kill-buffer)) 467 | (setq files-before (dwim-shell-command--default-directory-files monitor-directory)) 468 | (setq proc (apply #'start-process (seq-concatenate 'list 469 | (list (buffer-name proc-buffer) proc-buffer) 470 | shell-util 471 | shell-args 472 | (if shell-pipe 473 | (list (format "echo '%s' | %s" script shell-pipe)) 474 | (list script))))) 475 | (set-process-query-on-exit-flag proc nil) 476 | (if no-progress 477 | (dwim-shell-command--message "%s started" (process-name proc)) 478 | (setq progress-reporter (make-progress-reporter 479 | ;; Append space so "done" is spaced when 480 | ;; progress reporter is finished: 481 | ;; 482 | ;; *DWIM shell command* done 483 | (concat (process-name proc) " "))) 484 | (progress-reporter-update progress-reporter)) 485 | ;; Momentarily set buffer to same window, so it's next in recent stack. 486 | ;; Makes finding the shell command buffer a lot easier. 487 | (let ((current (current-buffer))) 488 | (pop-to-buffer-same-window proc-buffer) 489 | (pop-to-buffer-same-window current)) 490 | (when focus-now 491 | (switch-to-buffer proc-buffer)) 492 | (if (equal (process-status proc) 'exit) 493 | (dwim-shell-command--finalize (current-buffer) 494 | files-before 495 | proc 496 | progress-reporter 497 | on-completion 498 | silent-success 499 | error-autofocus 500 | monitor-directory) 501 | (setq dwim-shell-command--commands 502 | (push (cons (process-name proc) 503 | (make-dwim-shell-command--command :script script 504 | :process proc 505 | :name (process-name proc) 506 | :calling-buffer (current-buffer) 507 | :files-before files-before 508 | :reporter progress-reporter 509 | :on-completion on-completion 510 | :silent-success silent-success 511 | :error-autofocus error-autofocus 512 | :monitor-directory monitor-directory)) 513 | dwim-shell-command--commands)) 514 | (set-process-sentinel proc #'dwim-shell-command--sentinel) 515 | (set-process-filter proc #'dwim-shell-command--filter)))) 516 | 517 | (cl-defun dwim-shell-command-read-file-name (prompt &key extension default) 518 | "Invoke `read-string' with PROMPT. 519 | Validates :EXTENSION and returns :DEFAULT if empty input." 520 | (let ((file-name (read-string prompt))) 521 | (cond ((string-empty-p (string-trim file-name)) 522 | default) 523 | ((and extension 524 | (string-equal (file-name-extension file-name) extension)) 525 | file-name) 526 | ((and extension 527 | (not (string-equal (file-name-extension file-name) extension))) 528 | (user-error "Name must end in .%s" extension)) 529 | (t 530 | file-name)))) 531 | 532 | (defun dwim-shell-command--message (message &rest args) 533 | "Like `dwim-shell-command--message' but non-blocking. 534 | MESSAGE and ARGS same as `dwim-shell-command--message'." 535 | (let ((message-id (random))) 536 | (message (propertize (apply #'format message args) 'message-id message-id)) 537 | (run-with-timer 3 nil 538 | (lambda () 539 | (when (and (current-message) 540 | (eq (get-text-property 0 'message-id (current-message)) 541 | message-id)) 542 | (message nil)))))) 543 | 544 | (defun dwim-shell-command--extract-queries (template) 545 | "Extract queries from TEMPLATE. 546 | 547 | For all queries, request a value from the user. 548 | 549 | For example: 550 | 551 | \"Hello <> world <>\" => 552 | 553 | ((\"<>\" . \"100\") 554 | (\"<>\" . \"200\"))" 555 | (let ((matches) 556 | (pos 0)) 557 | (while (and (< pos (length template)) 558 | (string-match "<<\\([[:alpha:]]\\|[[:blank:]]\\)+:\\([[:alnum:]]\\|[.]\\)*>>" template pos)) 559 | (setq pos (1+ (match-beginning 0))) 560 | (let ((match 0)) 561 | (push (match-string match template) matches) 562 | (setq match (1+ match)))) 563 | (seq-map (lambda (match) 564 | (let* ((query (split-string (string-remove-suffix ">>" (string-remove-prefix "<<" match)) ":")) 565 | (prompt (nth 0 query)) 566 | (default-value (if (string-empty-p (nth 1 query)) 567 | nil 568 | (nth 1 query))) 569 | (value (if (string-match-p "^\\([[:digit:]]\\|[.]\\)+$" default-value) 570 | (number-to-string (read-number (format "%s: " prompt) 571 | (string-to-number default-value))) 572 | (string-trim (read-string (concat prompt 573 | (if default-value 574 | (format " (default %s): " default-value) 575 | ": ")))))) 576 | (result (cons match (if (string-empty-p value) 577 | default-value 578 | value)))) 579 | (cl-assert (cdr result) nil "Must have a value")result)) 580 | (seq-uniq (nreverse matches))))) 581 | 582 | (defun dwim-shell-command--digits (n) 583 | "Return the number of digits in N." 584 | (let ((count 0)) 585 | (while (> n 0) 586 | (setq n (/ n 10)) 587 | (setq count (1+ count))) 588 | count)) 589 | 590 | (defun dwim-shell-command--number-to-string (n padding) 591 | "Convert N to string using PADDING for number of digits." 592 | (format (format "%%0%dd" padding) n)) 593 | 594 | (defun dwim-shell-command--expand-files-template (template files &optional post-process-template temp-dir replacements join-separator) 595 | "Expand TEMPLATE using FILES. 596 | 597 | Expand using <<*>> for FILES. 598 | 599 | Note: This expander cannot be used to expand <>, <>, or <>. 600 | 601 | For example: 602 | 603 | Given FILES '(\"path/to/image1.png\" \"path/to/image2.png\") 604 | 605 | \"du -csh '<<*>>'\" expands to 606 | 607 | \"du -csh 'path/to/image1.png' 'path/to/image2.png'\" 608 | 609 | Use POST-PROCESS-TEMPLATE to further expand template given own logic. 610 | 611 | Set TEMP-DIR to a unique temp directory to this template. 612 | 613 | REPLACEMENTS is a cons list of literals to replace with values. 614 | 615 | JOIN-SEPARATOR is used to join files from <<*>>." 616 | (cl-assert (not (and (dwim-shell-command--contains-multi-file-ref template) 617 | (dwim-shell-command--contains-single-file-ref template))) 618 | nil "Must not have %s and %s in the same template" 619 | (dwim-shell-command--contains-multi-file-ref template) 620 | (dwim-shell-command--contains-single-file-ref template)) 621 | (setq files (seq-map (lambda (file) 622 | (if dwim-shell-command-use-absolute-paths 623 | (expand-file-name file) 624 | (file-relative-name (expand-file-name file) default-directory))) 625 | files)) 626 | 627 | (mapc (lambda (replacement) 628 | (setq template 629 | (string-replace (car replacement) (cdr replacement) template))) 630 | replacements) 631 | 632 | ;; Try to use quotes surrounding <<*>> in each path. 633 | ;; "'<<*>>'" with '("path/to/image1.png" "path/to/image2.png") -> "'path/to/image1.png' 'path/to/image2.png'" 634 | (when-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\<\\*\>\>" template)) 635 | (unescaped-quote (nth 0 quoting)) 636 | (escaped-quote (nth 1 quoting))) 637 | (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\<\\*\>\>\\)\\([^ ]\\)" 638 | (string-join (seq-map (lambda (file) 639 | (concat unescaped-quote 640 | (string-replace unescaped-quote 641 | escaped-quote file) unescaped-quote)) 642 | files) 643 | (or join-separator " ")) 644 | template nil nil 0))) 645 | 646 | ;; "<>" -> some.txt (if unique) 647 | ;; -> some(1).txt (if it exist) 648 | (when-let* ((found (string-match "\<\<\\([^ ]?+\\)(u)\>\>" template)) 649 | (name (match-string 1 template))) 650 | (setq template (replace-regexp-in-string "\<\<\\([^ ]?+\\)(u)\>\>" 651 | (dwim-shell-command--unique-new-file-path name) 652 | template nil nil 0))) 653 | 654 | ;; "<<~>>" -> "/home/user" (or equivalent). 655 | (when (string-match "\<\<~\>\>" template) 656 | (setq template (replace-regexp-in-string "\<\<~\>\>" 657 | (expand-file-name "~") 658 | template nil nil 0))) 659 | 660 | ;; "<<*>>" with '("path/to/image1.png" "path/to/image2.png") -> "path/to/image1.png path/to/image2.png" 661 | (setq template (replace-regexp-in-string "\\(\<\<\\*\>\>\\)" (string-join files (or join-separator " ")) template nil nil 1)) 662 | 663 | ;; "<>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5" 664 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" temp-dir template nil nil 1)) 665 | 666 | ;; "<>" with (current-kill 0) -> "whatever was in kill ring" 667 | (when (string-match "\<\\>" template) 668 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" (current-kill 0) 669 | template nil nil 1))) 670 | 671 | (when post-process-template 672 | (setq template (funcall post-process-template template files))) 673 | template) 674 | 675 | (defun dwim-shell-command--escaped-quote-around (needle haystack &optional unbalanced) 676 | "Find NEEDLE in HAYSTACK that's surrounded by either ' or \". 677 | 678 | Set UNBALANCED to t if NEEDLE isn't surrounded by quotes on both sides. 679 | 680 | For example: 681 | 682 | \"\<\\>\" \"before \"<>\" after\" => (\"\"\" \"\\\\\"\") 683 | 684 | \"\<\\>\" \"before '<>' after\" => (\"'\" \"'\"'\"'\")" 685 | (when-let ((found (string-match (format "\\([^ ]\\)\\(%s\\)\\([^ ]\\)" needle) haystack)) 686 | (unescaped-quote (if unbalanced 687 | (or (match-string 1 haystack) 688 | (match-string 3 haystack)) 689 | (cl-assert (string-equal (match-string 1 haystack) 690 | (match-string 3 haystack)) nil 691 | "%s must match %s" 692 | (match-string 1 haystack) 693 | (match-string 3 haystack)) 694 | (match-string 1 haystack))) 695 | (escaped-quote "'")) 696 | ;; Known quoted quotes. 697 | (cond 698 | ((string-equal unescaped-quote "\"") 699 | (setq escaped-quote "\\\\\"")) 700 | ((string-equal unescaped-quote "'") 701 | (setq escaped-quote "'\"'\"'")) 702 | ;; Ignore slashes as user may be joining paths. 703 | ((string-equal unescaped-quote "/") 704 | (setq escaped-quote "/")) 705 | (t 706 | (error "Couldn't figure out how to quote for \"%s\" using %s and %s" 707 | haystack 708 | (match-string 1 haystack) 709 | (match-string 3 haystack)))) 710 | (list unescaped-quote escaped-quote))) 711 | 712 | (defun dwim-shell-command--expand-file-template (template file &optional post-process-template temp-dir current replacements) 713 | "Expand TEMPLATE using FILE. 714 | 715 | Expand using <> for FILE, <> for FILE without extension, and 716 | <> for FILE extension. <>, <<1n>>, or <> is replaced with 717 | CURRENT. <> expands to unique \"some(1).txt\". 718 | 719 | Note: This expander cannot be used to expand <<*>>. 720 | 721 | For example: 722 | 723 | Given FILE \"path/to/image.png\" 724 | 725 | \"convert '<>' '<>.jpg'\" expands to 726 | 727 | \"convert 'path/to/image.png' 'path/to/image.jpg'\" 728 | 729 | Use POST-PROCESS-TEMPLATE to further expand template given own logic. 730 | 731 | Set TEMP-DIR to a unique temp directory to this template. 732 | 733 | REPLACEMENTS is a cons list of literals to replace with values." 734 | (cl-assert (not (and (dwim-shell-command--contains-multi-file-ref template) 735 | (dwim-shell-command--contains-single-file-ref template))) 736 | nil "Must not have %s and %s in the same template" 737 | (dwim-shell-command--contains-multi-file-ref template) 738 | (dwim-shell-command--contains-single-file-ref template)) 739 | 740 | (mapc (lambda (replacement) 741 | (setq template 742 | (string-replace (car replacement) (cdr replacement) template))) 743 | replacements) 744 | 745 | (when file 746 | (setq file (if dwim-shell-command-use-absolute-paths 747 | (expand-file-name file) 748 | (file-relative-name (expand-file-name file) default-directory))) 749 | ;; "<>" with "/path/tmp.txt" -> "/path/tmp" 750 | (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) 751 | (unescaped-quote (nth 0 quoting)) 752 | (escaped-quote (nth 1 quoting))) 753 | (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)" 754 | (string-replace unescaped-quote escaped-quote (file-name-sans-extension file)) 755 | template nil nil 2)) 756 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-sans-extension file) template nil nil 1))) 757 | 758 | ;; "<>" with "/path/tmp.txt" -> "tmp.txt" 759 | (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) 760 | (unescaped-quote (nth 0 quoting)) 761 | (escaped-quote (nth 1 quoting))) 762 | (setq template (replace-regexp-in-string "\\(\<\\>\\)\\([^ ]\\)" 763 | (string-replace unescaped-quote escaped-quote (file-name-nondirectory file)) 764 | template nil nil 1)) 765 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-nondirectory file) template nil nil 1))) 766 | 767 | ;; "<>" with "/path/tmp.txt" -> "tmp" 768 | (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template t)) 769 | (unescaped-quote (nth 0 quoting)) 770 | (escaped-quote (nth 1 quoting))) 771 | (setq template (replace-regexp-in-string "\\(\<\\>\\)\\([^ ]\\)" 772 | (string-replace unescaped-quote escaped-quote 773 | (file-name-sans-extension (file-name-nondirectory file))) 774 | template nil nil 1)) 775 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-sans-extension 776 | (file-name-nondirectory file)) template nil nil 1))) 777 | 778 | ;; "<>" with "/path/tmp.txt" -> "txt" 779 | (if (file-name-extension file) 780 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" (file-name-extension file) template nil nil 1)) 781 | ;; File had no extension. Attempt to remove .<>. 782 | (setq template (replace-regexp-in-string "\\(\.\<\\>\\)" "" template nil nil 1))) 783 | 784 | ;; "<>" with "/path/file.jpg" -> "/path/file.jpg" 785 | (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template)) 786 | (unescaped-quote (nth 0 quoting)) 787 | (escaped-quote (nth 1 quoting))) 788 | (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)\\([^ ]\\)" 789 | (string-replace unescaped-quote escaped-quote file) 790 | template nil nil 2)) 791 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" file template nil nil 1))) 792 | 793 | ;; "<>" with "/path/file.jpg" -> "/path/file(1).jpg" 794 | (if-let* ((quoting (dwim-shell-command--escaped-quote-around "\<\\>" template)) 795 | (unescaped-quote (nth 0 quoting)) 796 | (escaped-quote (nth 1 quoting))) 797 | (setq template (replace-regexp-in-string "\\([^ ]\\)\\(\<\\>\\)\\([^ ]\\)" 798 | (string-replace unescaped-quote escaped-quote 799 | (dwim-shell-command--unique-new-file-path file)) 800 | template nil nil 2)) 801 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" 802 | (dwim-shell-command--unique-new-file-path file) template nil nil 1)))) 803 | 804 | ;; "<>" -> some.txt (if unique) 805 | ;; -> some(1).txt (if it exist) 806 | (when-let* ((found (string-match "\<\<\\([^ ]?+\\)(u)\>\>" template)) 807 | (name (match-string 1 template))) 808 | (setq template (replace-regexp-in-string "\<\<\\([^ ]?+\\)(u)\>\>" 809 | (dwim-shell-command--unique-new-file-path name) 810 | template nil nil 0))) 811 | 812 | ;; "<<~>>" -> "/home/user" (or equivalent). 813 | (when (string-match "\<\<~\>\>" template) 814 | (setq template (replace-regexp-in-string "\<\<~\>\>" 815 | (expand-file-name "~") 816 | template nil nil 0))) 817 | 818 | ;; "<>" with TEMP-DIR -> "/var/folders/m7/ky091cp56d5g68nyhl4y7frc0000gn/T/dwim-shell-command-JNK4V5" 819 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" temp-dir template nil nil 1)) 820 | 821 | ;; "<>" with (current-kill 0) -> "whatever was in kill ring" 822 | (when (string-match "\<\\>" template) 823 | (setq template (replace-regexp-in-string "\\(\<\\>\\)" (current-kill 0) 824 | template nil nil 1))) 825 | 826 | ;; "<>" or "<" or "<<1n>" with current. 827 | (setq template (replace-regexp-in-string "\\(\<\<[[:alnum:]]?+n\>\>\\)" current 828 | template nil nil 1)) 829 | 830 | (when post-process-template 831 | (setq template (funcall post-process-template template file))) 832 | template) 833 | 834 | (defun dwim-shell-command--contains-single-file-ref (template) 835 | "Check for <>, <>, or <> in TEMPLATE." 836 | (cond ((string-match "\<\\>" template) 837 | "<>") 838 | ((string-match "\<\\>" template) 839 | "<>") 840 | ((string-match "\<\\>" template) 841 | "<>") 842 | ((string-match "\<\\>" template) 843 | "<>") 844 | ((string-match "\<\\>" template) 845 | "<>"))) 846 | 847 | (defun dwim-shell-command--contains-multi-file-ref (template) 848 | "Check for <<*>> in TEMPLATE." 849 | (when (string-match "\<\<\\*\>\>" template) 850 | "<<*>>")) 851 | 852 | (defun dwim-shell-command--default-directory-files (override) 853 | "List of files in current buffer's `default-directory'. 854 | Use OVERRIDE to override `default-directory'." 855 | (when-let ((default-directory (or override default-directory))) 856 | (seq-map (lambda (filename) 857 | (file-name-concat default-directory filename)) 858 | (cl-remove-if 859 | (lambda (e) (member e '("." ".."))) 860 | (directory-files default-directory))))) 861 | 862 | (defun dwim-shell-command--last-modified-between (before after) 863 | "Compare files in BEFORE and AFTER and return oldest file in diff." 864 | (car (last (seq-sort #'file-newer-than-file-p 865 | (seq-difference after before))))) 866 | 867 | (defun dwim-shell-command--finalize (calling-buffer files-before process progress-reporter on-completion silent-success error-autofocus monitor-directory) 868 | "Finalize script execution. 869 | 870 | CALLING-BUFFER, FILES-BEFORE, PROCESS, PROGRESS-REPORTER, 871 | ERROR-AUTOFOCUS, ON-COMPLETION, SILENT-SUCCESS, and MONITOR-DIRECTORY are 872 | all needed to finalize processing." 873 | (let ((oldest-new-file) 874 | (files-after)) 875 | (when progress-reporter 876 | (progress-reporter-done progress-reporter)) 877 | (if (= (process-exit-status process) 0) 878 | (progn 879 | (dwim-shell-command--message (funcall dwim-shell-command-done-buffer-name (process-name process))) 880 | (with-current-buffer (process-buffer process) 881 | (rename-buffer (generate-new-buffer-name (funcall dwim-shell-command-done-buffer-name (process-name process))))) 882 | (if on-completion 883 | (funcall on-completion (process-buffer process) process) 884 | (with-current-buffer calling-buffer 885 | (if (equal major-mode 'dired-mode) 886 | (progn (when revert-buffer-function 887 | (funcall revert-buffer-function nil t)) 888 | ;; Region is not accurate if new files added. Wipe it. 889 | (when (use-region-p) 890 | (deactivate-mark))) 891 | (when (and (or buffer-auto-save-file-name 892 | buffer-file-name) 893 | (not (verify-visited-file-modtime))) 894 | ;; Already visiting a file. Revert if modified by command. 895 | (revert-buffer :ignore-auto :noconfirm))) 896 | (with-current-buffer (process-buffer process) 897 | (setq files-after (dwim-shell-command--default-directory-files monitor-directory))) 898 | (setq oldest-new-file 899 | (dwim-shell-command--last-modified-between 900 | files-before 901 | files-after)) 902 | ;; There's at least one new file. Show that. 903 | (if oldest-new-file 904 | (dired-jump nil oldest-new-file) 905 | ;; Files may have been deleted but harder to track. 906 | ;; Open dired and refresh to show files are gone. 907 | (unless (equal (length files-after) 908 | (length files-before)) 909 | (dired monitor-directory) 910 | (when revert-buffer-function 911 | (funcall revert-buffer-function nil t))))) 912 | (unless (equal (process-buffer process) 913 | (window-buffer (selected-window))) 914 | (if (or oldest-new-file silent-success) 915 | (kill-buffer (process-buffer process)) 916 | (unless silent-success 917 | (switch-to-buffer (process-buffer process))))))) 918 | (if on-completion 919 | (funcall on-completion (process-buffer process) process) 920 | (if (and (buffer-name (process-buffer process)) 921 | (or error-autofocus 922 | ;; Buffer already selected. Don't ask. 923 | (equal (process-buffer process) 924 | (window-buffer (selected-window))) 925 | (ignore-error quit 926 | (y-or-n-p (format "%s error, see output? " 927 | (buffer-name (process-buffer process))))))) 928 | (progn 929 | (with-current-buffer (process-buffer process) 930 | (rename-buffer (generate-new-buffer-name (funcall dwim-shell-command-error-buffer-name (process-name process))))) 931 | (when (or error-autofocus 932 | (equal (process-buffer process) 933 | (window-buffer (selected-window)))) 934 | (dwim-shell-command--message (funcall dwim-shell-command-error-buffer-name (process-name process)))) 935 | (switch-to-buffer (process-buffer process))) 936 | (kill-buffer (process-buffer process))))) 937 | (setq dwim-shell-command--commands 938 | (map-delete dwim-shell-command--commands (process-name process))))) 939 | 940 | (defun dwim-shell-command--unique-new-file-path (file-path) 941 | "Return a unique FILE-PATH. 942 | 943 | If FILE-PATH already contains a number in the format (n), set counter to n. 944 | 945 | \"/tmp/blah.txt\" -> \"/tmp/blah(1).txt\" 946 | \"/tmp/blah(2).txt\" -> \"/tmp/blah(3).txt\"" 947 | (let* ((name (file-name-sans-extension file-path)) 948 | (extension (file-name-extension file-path)) 949 | (counter (if (string-match "(\\([0-9]+\\))$" name) 950 | (string-to-number (match-string 1 name)) 951 | 0))) 952 | (when (string-match "(\\([0-9]+\\))$" name) 953 | (setq name(replace-match "" t t name))) 954 | (while (file-exists-p file-path) 955 | (setq counter (1+ counter)) 956 | (setq file-path (if extension 957 | (format "%s(%d).%s" name counter extension) 958 | (format "%s(%d)" name counter)))) 959 | file-path)) 960 | 961 | (defun dwim-shell-command--sentinel (process _) 962 | "Handles PROCESS sentinel and STATE." 963 | (let ((exec (map-elt dwim-shell-command--commands (process-name process)))) 964 | (dwim-shell-command--finalize (dwim-shell-command--command-calling-buffer exec) 965 | (dwim-shell-command--command-files-before exec) 966 | process 967 | (dwim-shell-command--command-reporter exec) 968 | (dwim-shell-command--command-on-completion exec) 969 | (dwim-shell-command--command-silent-success exec) 970 | (dwim-shell-command--command-error-autofocus exec) 971 | (dwim-shell-command--command-monitor-directory exec)))) 972 | 973 | (defun dwim-shell-command--filter (process output) 974 | "Handles PROCESS filtering and STATE and OUTPUT." 975 | (when-let* ((exec (map-elt dwim-shell-command--commands (process-name process))) 976 | (reporter (dwim-shell-command--command-reporter exec))) 977 | (progress-reporter-update reporter)) 978 | (comint-output-filter process output)) 979 | 980 | (defun dwim-shell-command--file-extensions () 981 | "Return buffer file extension or marked/region extensions for a `dired' buffer." 982 | (seq-uniq 983 | (seq-map 984 | #'file-name-extension 985 | (seq-filter 986 | #'file-name-extension 987 | (dwim-shell-command--files))))) 988 | 989 | (defun dwim-shell-command--files () 990 | "Return buffer file (if available) or marked/region files for a `dired' buffer." 991 | (cl-assert (not (and (use-region-p) (let ((files (dired-get-marked-files nil nil nil t))) 992 | ;; Based on `dired-number-of-marked-files'. 993 | (cond ((null (cdr files)) 994 | nil) 995 | ((and (= (length files) 2) 996 | (eq (car files) t)) 997 | t) 998 | (t 999 | (not (seq-empty-p files))))))) 1000 | nil "Region and marked files both active. Choose one only.") 1001 | (if (buffer-file-name) 1002 | (list (buffer-file-name)) 1003 | (or 1004 | (dwim-shell-command--dired-paths-in-region) 1005 | (dired-get-marked-files)))) 1006 | 1007 | (defun dwim-shell-command--dired-paths-in-region () 1008 | "If `dired' buffer, return region files. nil otherwise." 1009 | (when (and (equal major-mode 'dired-mode) 1010 | (use-region-p)) 1011 | (let ((start (region-beginning)) 1012 | (end (region-end)) 1013 | (paths)) 1014 | (save-excursion 1015 | (save-restriction 1016 | (goto-char start) 1017 | (while (< (point) end) 1018 | ;; Skip non-file lines. 1019 | (while (and (< (point) end) (dired-between-files)) 1020 | (forward-line 1)) 1021 | (when (dired-get-filename nil t) 1022 | (setq paths (append paths (list (dired-get-filename nil t))))) 1023 | (forward-line 1)))) 1024 | paths))) 1025 | 1026 | (defun dwim-shell-command--n-start-value (template padding) 1027 | "Extract n start value from TEMPLATE using PADDING. 1028 | Falls back to \"1\"." 1029 | (when (string-match "\<\<\\([[:alnum:]]?+\\)n\>\>" template) 1030 | (if (string-empty-p (match-string 1 template)) 1031 | (dwim-shell-command--increment-string (number-to-string 0) padding) 1032 | (if-let* ((start-string (match-string 1 template)) 1033 | (start-n (string-to-number start-string))) 1034 | (dwim-shell-command--increment-string (number-to-string (1- start-n)) padding) 1035 | (match-string 1 template))))) 1036 | 1037 | (defun dwim-shell-command--increment-string (text padding) 1038 | "Increment TEXT using PADDING. 1039 | \"a\" -> \"b\" 1040 | \"1\" -> \"2\"" 1041 | (cond ((string-match "^[[:alpha:]]$" text) ;; char 1042 | (char-to-string (1+ (string-to-char (match-string 0 text))))) 1043 | ((string-match "^[[:digit:]]+$" text) ;; char 1044 | (dwim-shell-command--number-to-string (1+ (string-to-number (match-string 0 text))) padding)))) 1045 | 1046 | (defun dwim-shell-command--program-test (program &rest args) 1047 | "Test that running PROGRAM with ARGS is successful." 1048 | (eq 0 (apply #'call-process program nil nil nil args))) 1049 | 1050 | (cl-defun dwim-shell-command-foreach (fun &key monitor-directory) 1051 | "Execute FUN for each file. 1052 | Monitor :MONITOR-DIRECTORY for new file and `dired-jump' to it." 1053 | (let ((files (dwim-shell-command--files)) 1054 | (files-before (dwim-shell-command--default-directory-files monitor-directory)) 1055 | (oldest-new-file) 1056 | (created-file) 1057 | (jump-to)) 1058 | (mapc (lambda (file-path) 1059 | (setq created-file (or (funcall fun file-path) 1060 | created-file))) 1061 | files) 1062 | (setq oldest-new-file (dwim-shell-command--last-modified-between 1063 | files-before 1064 | (dwim-shell-command--default-directory-files monitor-directory))) 1065 | (setq jump-to (or oldest-new-file created-file)) 1066 | (when jump-to 1067 | (dired-jump nil jump-to)))) 1068 | 1069 | (provide 'dwim-shell-command) 1070 | 1071 | ;;; dwim-shell-command.el ends here 1072 | -------------------------------------------------------------------------------- /dwim-shell-commands.el: -------------------------------------------------------------------------------- 1 | ;;; dwim-shell-commands.el --- Useful commands -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2022 Alvaro Ramirez 4 | 5 | ;; Author: Alvaro Ramirez 6 | ;; URL: https://github.com/xenodium/dwim-shell-command 7 | 8 | ;; This package is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation; either version 3, or (at your option) 11 | ;; any later version. 12 | 13 | ;; This package is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with GNU Emacs. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; A collection of useful commands created via 24 | ;; `dwim-shell-command-on-marked-files'. 25 | 26 | ;;; Code: 27 | 28 | (require 'browse-url) 29 | (require 'cl-lib) 30 | (require 'dwim-shell-command) 31 | (require 'files) 32 | (require 'proced) 33 | (require 'seq) 34 | (require 'subr-x) 35 | 36 | ;;;###autoload 37 | (defun dwim-shell-commands-audio-to-mp3 () 38 | "Convert all marked audio to mp3(s)." 39 | (interactive) 40 | (dwim-shell-command-on-marked-files 41 | "Convert to mp3" 42 | "ffmpeg -stats -n -i '<>' -acodec libmp3lame '<>.mp3'" 43 | :utils "ffmpeg")) 44 | 45 | ;;;###autoload 46 | (defun dwim-shell-extract-har-content (prefix) 47 | "Extract all har content fields to files." 48 | (interactive "P") 49 | (when prefix 50 | (setq prefix (string-trim (read-string "Transcription locale: " "ja-JP"))) 51 | (when (string-empty-p prefix) 52 | (error "No locale given"))) 53 | (dwim-shell-command-on-marked-files 54 | "Extract har response content" 55 | (format "declare -A mime_map=( \ 56 | [\"audio/mpeg\"]=\"mp3\" \ 57 | [\"image/jpeg\"]=\"jpg\" \ 58 | [\"text/plain\"]=\"txt\" \ 59 | [\"application/json\"]=\"json\" \ 60 | # TODO: Add more mappings if needed. 61 | ) 62 | outdir='<>' 63 | mkdir -p \"${outdir}\" 64 | : > \"${outdir}/<>.org\" 65 | jq -r '.log.entries[] | @base64' '<>' | while read -r entry; do 66 | url=$(echo \"$entry\" | base64 --decode | jq -r '.request.url') 67 | basename=$(echo \"$entry\" | base64 --decode | jq -r '.request.url | capture(\"(?<=//)[^/]+/(?.*)\") | .path | gsub(\"[^a-zA-Z0-9]\"; \"_\")') 68 | mime=$(echo \"$entry\" | base64 --decode | jq -r '.response.content.mimeType') 69 | extension=${mime_map[$mime]:-\"bin\"} 70 | name=\"${outdir}/${basename:0:255}.${extension}\" 71 | echo \"${name}\" 72 | content=$(echo \"$entry\" | base64 --decode | jq -r '.response.content.text') 73 | echo \"$content\" | base64 --decode > \"${name}\" 74 | if [ -f \"${name}\" ] && %s; then 75 | transcription=$(macosrec --speech-to-text --locale %s --input \"${name}\") 76 | if [ $? -eq 0 ]; then 77 | transcribed_name=\"${outdir}/${transcription}.${extension}\" 78 | mv \"${name}\" \"${transcribed_name}\" 79 | basename=$(basename \"${transcribed_name}\") 80 | echo \"[[file:${basename}][${transcription}]] [[${url}][remote]]\" >> \"${outdir}/<>.org\" 81 | fi 82 | fi 83 | done" 84 | (if prefix 85 | "true" 86 | "false") 87 | prefix 88 | (if prefix 89 | "true" 90 | "false")) 91 | :utils "jq" 92 | :extensions "har")) 93 | 94 | ;;;###autoload 95 | (defun dwim-shell-extract-har-urls () 96 | "Get all request URLs." 97 | (interactive) 98 | (dwim-shell-command-on-marked-files 99 | "Extract har request URLs" 100 | "jq -r '.log.entries[].request.url' '<>'" 101 | :utils "jq")) 102 | 103 | ;;;###autoload 104 | (defun dwim-shell-view-open-ports-per-app () 105 | "View open ports per app" 106 | (interactive) 107 | (dwim-shell-command-on-marked-files 108 | "Ports per app" 109 | ;; https://x.com/nurmiwilliam/status/1823228630664634695 110 | "sudo lsof -iTCP -sTCP:LISTEN -n -P | awk 'NR>1 {print $9, $1, $2}' | sed 's/.*://' | sort -u | while read port process pid; do echo \"Port $port: $(ps -p $pid -o command= | sed 's/^-//') (PID: $pid)\"; done | sort -n" 111 | :utils "jq")) 112 | 113 | ;;;###autoload 114 | (defun dwim-shell-commands-open-clipboard-url () 115 | "Open clipboard URL. Offer to stream if possible." 116 | (interactive) 117 | (let ((url (or (current-kill 0) 118 | (user-error "Nothing in clipboard")))) 119 | (dwim-shell-commands-url-browse url))) 120 | 121 | (defun dwim-shell-commands-url-browse (url &rest args) 122 | "If URL is playable media, offer to open in mpv. Else browser. 123 | Optional argument ARGS as per `browse-url-default-browser'" 124 | (if (and (or (string-match-p "^http[s]?://.*youtube.com" url) 125 | (string-match-p "^http[s]?://.*m.youtube.com" url) 126 | (string-match-p "^http[s]?://.*youtu.be" url) 127 | (string-match-p "^http[s]?://.*soundcloud.com" url) 128 | (string-match-p "^http[s]?://.*redditmedia.com" url) 129 | (string-match-p "^http[s]?://.*reddit.com" url) 130 | (string-match-p "^http[s]?://.*bandcamp.com" url)) 131 | (y-or-n-p "Stream from mpv? ")) 132 | (dwim-shell-command-on-marked-files 133 | "Streaming" 134 | (format "mpv --geometry=30%%x30%%+100%%+0%% '%s'" url) 135 | :utils "mpv" 136 | :no-progress t 137 | :error-autofocus t 138 | :silent-success t) 139 | (funcall #'browse-url-default-browser url args))) 140 | 141 | ;;;###autoload 142 | (defun dwim-shell-commands-stream-clipboard-url () 143 | "Stream clipboard URL using mpv." 144 | (interactive) 145 | (cl-assert (string-match-p "^http[s]?://" (current-kill 0)) nil "Not a URL") 146 | (dwim-shell-command-on-marked-files 147 | "Streaming" 148 | "mpv --geometry=30%x30%+100%+0% \"<>\"" 149 | :utils "mpv" 150 | :no-progress t 151 | :error-autofocus t 152 | :silent-success t)) 153 | 154 | ;;;###autoload 155 | (defun dwim-shell-commands-download-clipboard-stream-url () 156 | "Download clipboard URL." 157 | (interactive) 158 | (cl-assert (string-match-p "^http[s]?://" (current-kill 0)) nil "Not a URL") 159 | (dwim-shell-command-on-marked-files 160 | "Downloading" 161 | "youtube-dl --newline -o \"~/Downloads/%(title)s.%(ext)s\" \"<>\"" 162 | :utils "youtube-dl" 163 | :no-progress t 164 | :error-autofocus t 165 | :monitor-directory "~/Downloads" 166 | :silent-success t)) 167 | 168 | ;;;###autoload 169 | (defun dwim-shell-commands-image-clear-exif-metadata () 170 | "Clear EXIF metadata in image(s)." 171 | (interactive) 172 | (dwim-shell-command-on-marked-files 173 | "View EXIF" 174 | "cp '<>' '<>_cleared.<>' 175 | exiftool -all:all= -overwrite_original '<>_cleared.<>'" 176 | :utils "exiftool")) 177 | 178 | ;;;###autoload 179 | (defun dwim-shell-commands-image-scan-code () 180 | "Scan any code (ie. qr, bar, etc) from image(s)." 181 | (interactive) 182 | (dwim-shell-command-on-marked-files 183 | "Scan code" 184 | "zbarimg --quiet '<>'" 185 | :utils "zbarimg")) 186 | 187 | ;;;###autoload 188 | (defun dwim-shell-commands-image-exif-metadata () 189 | "View EXIF metadata in image(s)." 190 | (interactive) 191 | (dwim-shell-command-on-marked-files 192 | "View EXIF" 193 | "exiftool '<>'" 194 | :utils "exiftool")) 195 | 196 | ;;;###autoload 197 | (defun dwim-shell-commands-tesseract-ocr-text-from-image () 198 | "Extract text from image via tesseract." 199 | (interactive) 200 | (dwim-shell-command-on-marked-files 201 | "Extract text from image via tesseract." 202 | "tesseract '<>' -" 203 | :utils "tesseract")) 204 | 205 | ;;;###autoload 206 | (defun dwim-shell-commands-image-view-location-in-openstreetmap () 207 | "Open image(s) location in map/browser." 208 | (interactive) 209 | (dwim-shell-command-on-marked-files 210 | "Browse location" 211 | "lat=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f2-2)\" 212 | if [ -z \"$lat\" ]; then 213 | echo \"no latitude\" 214 | exit 1 215 | fi 216 | lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f3-3)\" 217 | if [ -z \"$lon\" ]; then 218 | echo \"no longitude\" 219 | exit 1 220 | fi 221 | if [[ $OSTYPE == darwin* ]]; then 222 | open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" 223 | else 224 | xdg-open \"http://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&layers=C\" 225 | fi" 226 | :utils "exiftool" 227 | :error-autofocus t 228 | :silent-success t)) 229 | 230 | ;;;###autoload 231 | (defun dwim-shell-commands-image-reverse-geocode-location () 232 | "Reverse geocode image(s) location." 233 | (interactive) 234 | (dwim-shell-command-on-marked-files 235 | "Reverse geocode" 236 | "lat=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f2-2)\" 237 | if [ -z \"$lat\" ]; then 238 | echo \"no latitude\" 239 | exit 1 240 | fi 241 | lon=\"$(exiftool -csv -n -gpslatitude -gpslongitude '<>' | tail -n 1 | cut -s -d',' -f3-3)\" 242 | if [ -z \"$lon\" ]; then 243 | echo \"no longitude\" 244 | exit 1 245 | fi 246 | json=$(curl \"https://nominatim.openstreetmap.org/reverse?format=json&accept-language=en&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1\") 247 | echo \"json_start $json json_end\"" 248 | :utils '("exiftool" "curl") 249 | :silent-success t 250 | :error-autofocus t 251 | :on-completion (lambda (buffer _process) 252 | (with-current-buffer buffer 253 | (goto-char (point-min)) 254 | (let ((matches '())) 255 | (while (re-search-forward "^json_start\\(.*?\\)json_end" nil t) 256 | (push (match-string 1) matches)) 257 | (message "%s" (string-join (seq-map (lambda (json) 258 | (map-elt (json-parse-string json :object-type 'alist) 'display_name)) 259 | matches) 260 | "\n"))) 261 | (kill-buffer buffer))))) 262 | 263 | ;;;###autoload 264 | (defun dwim-shell-commands-image-horizontal-flip () 265 | "Horizontally flip image(s)." 266 | (interactive) 267 | (dwim-shell-command-on-marked-files 268 | "Image horizontal flip" 269 | "convert -verbose -flop '<>' '<>_h_flipped.<>'" 270 | :utils "convert")) 271 | 272 | ;;;###autoload 273 | (defun dwim-shell-commands-image-vertical-flip () 274 | "Horizontally flip image(s)." 275 | (interactive) 276 | (dwim-shell-command-on-marked-files 277 | "Image vertical flip" 278 | "convert -verbose -flip '<>' '<>_v_flipped.<>'" 279 | :utils "convert")) 280 | 281 | ;;;###autoload 282 | (defun dwim-shell-commands-image-to-jpg () 283 | "Convert all marked images to jpg(s)." 284 | (interactive) 285 | (dwim-shell-command-on-marked-files 286 | "Convert to jpg" 287 | "convert -verbose '<>' '<>.jpg'" 288 | :utils "convert")) 289 | 290 | ;;;###autoload 291 | (defun dwim-shell-commands-image-to-png () 292 | "Convert all marked images to png(s)." 293 | (interactive) 294 | (dwim-shell-command-on-marked-files 295 | "Convert to png" 296 | "convert -verbose '<>' '<>.png'" 297 | :utils "convert")) 298 | 299 | ;;;###autoload 300 | (defun dwim-shell-commands-svg-to-png () 301 | "Convert all marked svg(s) to png(s)." 302 | (interactive) 303 | (dwim-shell-command-on-marked-files 304 | "Convert to png" 305 | "rsvg-convert -b white '<>' -f png -o '<>.png'" 306 | :utils "rsvg-convert")) 307 | 308 | ;;;###autoload 309 | (defun dwim-shell-commands-svg-to-favicons () 310 | "Convert svg to common favicons." 311 | (interactive) 312 | (dwim-shell-command-on-marked-files 313 | "Create favicons" 314 | "echo '>\">' 315 | rsvg-convert -o '<>-16.png' -w 16 -h 16 '<>' 316 | echo '>-16.png\" sizes=\"16x16\">' 317 | rsvg-convert -o '<>-32.png' -w 32 -h 32 '<>' 318 | echo '>-32.png\" sizes=\"32x32\">' 319 | rsvg-convert -o '<>-48.png' -w 48 -h 48 '<>' 320 | echo '>-48.png\" sizes=\"48x48\">' 321 | convert '<>-16.png' '<>-32.png' '<>-48.png' '<>.ico' 322 | echo '>.ico'\">' 323 | rsvg-convert -o '<>-64.png' -w 64 -h 64 '<>' 324 | echo '>-64.png\" sizes=\"64x64\">' 325 | rsvg-convert -o '<>-180.png' -w 180 -h 180 '<>' 326 | echo '>-180.png\" sizes=\"180x180\">' 327 | read -n 1 -s -r" 328 | :extensions "svg" 329 | :utils '("convert" "rsvg-convert") 330 | :focus-now t)) 331 | 332 | ;;;###autoload 333 | (defun dwim-shell-commands-make-transparent-png () 334 | "Create a transparent png." 335 | (interactive) 336 | (dwim-shell-command-on-marked-files 337 | "Create transparent png" 338 | "convert -verbose -size <>x<> xc:none '<>x<>.png(u)>>'" 339 | :utils "convert")) 340 | 341 | ;;;###autoload 342 | (defun dwim-shell-commands-join-as-pdf () 343 | "Join all marked images as a single pdf." 344 | (interactive) 345 | (dwim-shell-command-on-marked-files 346 | "Join as pdf" 347 | (format "convert -verbose '<<*>>' '<<%s(u)>>'" 348 | (dwim-shell-command-read-file-name 349 | "Join as pdf named (default \"joined.pdf\"): " 350 | :extension "pdf" 351 | :default "joined.pdf")) 352 | :utils "convert")) 353 | 354 | ;;;###autoload 355 | (defun dwim-shell-commands-keep-pdf-page () 356 | "Keep a page from pdf." 357 | (interactive) 358 | (let ((page-num (read-number "Keep page number: " 1))) 359 | (dwim-shell-command-on-marked-files 360 | "Keep pdf page" 361 | (format "qpdf '<>' --pages . %d -- '<>_%d.<>'" page-num page-num) 362 | :utils "qpdf"))) 363 | 364 | ;;;###autoload 365 | (defun dwim-shell-commands-join-images-horizontally () 366 | "Join all marked images horizontally as a single image." 367 | (interactive) 368 | (let ((filename (format "joined.%s" 369 | (or (seq-first (dwim-shell-command--file-extensions)) "png")))) 370 | (dwim-shell-command-on-marked-files 371 | "Join images horizontally" 372 | (format "convert -verbose '<<*>>' +append '<<%s(u)>>'" 373 | (dwim-shell-command-read-file-name 374 | (format "Join as image named (default \"%s\"): " filename) 375 | :default filename)) 376 | :utils "convert"))) 377 | 378 | ;;;###autoload 379 | (defun dwim-shell-commands-join-images-vertically () 380 | "Join all marked images vertically as a single image." 381 | (interactive) 382 | (let ((filename (format "joined.%s" 383 | (or (seq-first (dwim-shell-command--file-extensions)) "png")))) 384 | (dwim-shell-command-on-marked-files 385 | "Join images vertically" 386 | (format "convert -verbose '<<*>>' -gravity center -append '<<%s(u)>>'" 387 | (dwim-shell-command-read-file-name 388 | (format "Join as image named (default \"%s\"): " filename) 389 | :default filename)) 390 | :utils "convert"))) 391 | 392 | ;;;###autoload 393 | (defun dwim-shell-commands-image-to-grayscale () 394 | "Convert all marked images to grayscale." 395 | (interactive) 396 | (dwim-shell-command-on-marked-files 397 | "Convert image to grayscale" 398 | "convert -verbose -type Grayscale '<>' '<>_grayscale.<>'" 399 | :utils "convert")) 400 | 401 | ;;;###autoload 402 | (defun dwim-shell-commands-reorient-image () 403 | "Reorient images." 404 | (interactive) 405 | (dwim-shell-command-on-marked-files 406 | "Reorient image" 407 | "convert -verbose -auto-orient '<>' '<>_reoriented.<>'" 408 | :utils "convert")) 409 | 410 | ;;;###autoload 411 | (defun dwim-shell-commands-gif-to-video () 412 | "Convert all marked gif(s) to video(s)." 413 | (interactive) 414 | (dwim-shell-command-on-marked-files 415 | "Convert to gif" 416 | "ffmpeg -i '<>' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<>.mp4'" 417 | :utils "ffmpeg")) 418 | 419 | ;;;###autoload 420 | (defun dwim-shell-commands-macos-empty-trash () 421 | "Empty macOS trash." 422 | (interactive) 423 | (when (y-or-n-p "Empty macOS trash? ") 424 | (dwim-shell-command-on-marked-files 425 | "Empty macOS trash" 426 | "trash -e -y" 427 | :silent-success t 428 | :utils "trash"))) 429 | 430 | ;;;###autoload 431 | (defun dwim-shell-commands-macos-ocr-text-from-desktop-region () 432 | "Select a macOS desktop area to OCR and copy recognized text to kill ring." 433 | (interactive) 434 | (dwim-shell-command-on-marked-files 435 | "OCR area" 436 | "macosrec --ocr" 437 | ;; brew install xenodium/macosrec/macosrec 438 | :utils "macosrec" 439 | :on-completion 440 | (lambda (buffer process) 441 | (when-let ((success (= (process-exit-status process) 0)) 442 | (text (with-current-buffer buffer 443 | (string-trim (buffer-string))))) 444 | (progn 445 | (kill-new text) 446 | (switch-to-buffer buffer) 447 | (goto-char (point-min)) 448 | (message "OCR copied to clipboard")))))) 449 | 450 | ;;;###autoload 451 | (defun dwim-shell-commands-macos-ocr-text-from-image () 452 | "OCR file and copy recognized text to kill ring." 453 | (interactive) 454 | (dwim-shell-command-on-marked-files 455 | "OCR area" 456 | "macosrec --ocr --clipboard --input '<>'" 457 | ;; brew install xenodium/macosrec/macosrec 458 | :utils "macosrec" 459 | :on-completion 460 | (lambda (buffer process) 461 | (when-let ((success (= (process-exit-status process) 0)) 462 | (text (with-current-buffer buffer 463 | (string-trim (buffer-string))))) 464 | (progn 465 | (kill-new text) 466 | (switch-to-buffer buffer) 467 | (goto-char (point-min)) 468 | (message "OCR copied to clipboard")))))) 469 | 470 | ;;;###autoload 471 | (defun dwim-shell-commands-macos-convert-to-mp4 () 472 | "Convert to mov to mp4" 473 | (interactive) 474 | (dwim-shell-command-on-marked-files 475 | "Convert to mov to mp4" 476 | ;; "ffmpeg -loglevel quiet -stats -y -i <>.mov -vcodec h264 -acodec copy <>.mp4" 477 | ;; Found the encoder via ffmpeg -encoders | grep videotoolbox, 478 | ;; source https://www.reddit.com/r/ffmpeg/comments/14pqeex/getting_0_gpu_utilization_with_apple_silicons/ 479 | "ffmpeg -i '<>' -map_metadata 0 \ 480 | -c:v hevc_videotoolbox -q:v 35 -preset fast -c:a aac -b:a 128k -tag:v hvc1 '<>'.mp4" 481 | :utils "ffmpeg")) 482 | 483 | ;;;###autoload 484 | (defun dwim-shell-commands-video-to-gif () 485 | "Convert all marked videos to gif(s)." 486 | (interactive) 487 | (dwim-shell-command-on-marked-files 488 | "Convert to gif" 489 | "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif'" 490 | :utils "ffmpeg")) 491 | 492 | ;;;###autoload 493 | (defun dwim-shell-commands-video-to-mov () 494 | "Convert all marked videos to mov(s)." 495 | (interactive) 496 | (dwim-shell-command-on-marked-files 497 | "Convert to mov" 498 | "ffmpeg -i '<>' -c:v libx264 -c:a aac '<>.mov'" 499 | :utils "ffmpeg")) 500 | 501 | ;;;###autoload 502 | (defun dwim-shell-commands-video-to-webp () 503 | "Convert all marked videos to webp(s)." 504 | (interactive) 505 | (dwim-shell-command-on-marked-files 506 | "Convert to webp" 507 | "ffmpeg -i '<>' -vcodec libwebp -filter:v fps=fps=10 -compression_level 3 -lossless 1 -loop 0 -preset default -an -vsync 0 '<>'.webp" 508 | :utils "ffmpeg")) 509 | 510 | ;;;###autoload 511 | (defun dwim-shell-commands-webp-to-video () 512 | "Convert all marked webp(s) to video(s)." 513 | (interactive) 514 | (dwim-shell-command-on-marked-files 515 | "Convert webp to video" 516 | "convert '<>' '<>/<>.gif' 517 | ffmpeg -i '<>/<>.gif' -movflags faststart -pix_fmt yuv420p -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2' '<>.mp4'" 518 | :utils '("ffmpeg" "convert") 519 | :extensions "webp")) 520 | 521 | ;;;###autoload 522 | (defun dwim-shell-commands-webp-to-gif () 523 | "Convert all marked webp(s) to gif(s)." 524 | (interactive) 525 | (dwim-shell-command-on-marked-files 526 | "Convert webp to video" 527 | "convert '<>' '<>.gif'" 528 | :utils '("convert") 529 | :extensions "webp")) 530 | 531 | ;;;###autoload 532 | (defun dwim-shell-commands-video-to-hevc-mkv () 533 | "Convert all marked videos to hevc mkv." 534 | (interactive) 535 | (dwim-shell-command-on-marked-files 536 | "Convert video to h265 " 537 | "REPO_DIR=/tmp/other_video_transcoding 538 | if ! [ -d \"$REPO_DIR\" ] 539 | then 540 | git clone https://github.com/donmelton/other_video_transcoding.git $REPO_DIR 541 | fi 542 | pushd $REPO_DIR 543 | git pull origin master || echo \"skipping repo update...\" 544 | popd 545 | ruby $REPO_DIR/bin/other-transcode --hevc '<>'" 546 | :utils '("git" "ffmpeg" "mkvtoolnix" "mpv"))) 547 | 548 | ;;;###autoload 549 | (defun dwim-shell-commands-video-to-optimized-gif () 550 | "Convert all marked videos to optimized gif(s)." 551 | (interactive) 552 | (dwim-shell-command-on-marked-files 553 | "Convert to optimized gif" 554 | "ffmpeg -loglevel quiet -stats -y -i '<>' -pix_fmt rgb24 -r 15 '<>.gif' 555 | gifsicle -O3 '<>.gif' --lossy=80 -o '<>.gif'" 556 | :utils '("ffmpeg" "gifsicle"))) 557 | 558 | ;;;###autoload 559 | (defun dwim-shell-commands-unzip () 560 | "Unzip all marked archives (of any kind) using `atool'." 561 | (interactive) 562 | (dwim-shell-command-on-marked-files 563 | "Unzip" "atool --extract --explain '<>'" 564 | :utils "atool")) 565 | 566 | ;;;###autoload 567 | (defun dwim-shell-commands-zip () 568 | "Zip all marked files into archive.zip." 569 | (interactive) 570 | (dwim-shell-command-on-marked-files 571 | "Zip" (if (eq 1 (seq-length (dwim-shell-command--files))) 572 | "zip -r '<>.<>' '<>'" 573 | "zip -r '<>' '<<*>>'") 574 | :utils "zip")) 575 | 576 | ;;;###autoload 577 | (defun dwim-shell-commands-zip-password-protect () 578 | "Protect/encrypt zip file(s) with password." 579 | (interactive) 580 | (dwim-shell-command-on-marked-files 581 | "Add zip password" "zipcloak --output-file '<>_protected.<>' '<>'" 582 | :extensions "zip" 583 | :utils "zipcloak")) 584 | 585 | ;;;###autoload 586 | (defun dwim-shell-commands-optimize-gif () 587 | "Convert all marked videos to optimized gif(s)." 588 | (interactive) 589 | (dwim-shell-command-on-marked-files 590 | "Convert to optimized gif" 591 | "gifsicle -O3 '<>' --lossy=90 -o '<>_optimized.gif'" 592 | :utils '("ffmpeg" "gifsicle"))) 593 | 594 | ;;;###autoload 595 | (defun dwim-shell-commands-speed-up-gif () 596 | "Speeds up gif(s)." 597 | (interactive) 598 | (let ((factor (string-to-number 599 | (completing-read "Speed up x times: " '("1" "1.5" "2" "2.5" "3" "4"))))) 600 | (dwim-shell-command-on-marked-files 601 | "Speed up gif" 602 | (format "gifsicle -U '<>' <> -O2 -o '<>_x%s.<>'" factor) 603 | :extensions "gif" :utils '("gifsicle" "identify") 604 | :post-process-template (lambda (script file) 605 | (string-replace "<>" (dwim-shell-commands--gifsicle-frames-every factor file) script))))) 606 | 607 | ;;;###autoload 608 | (defun dwim-shell-commands-image-apply-ios-round-corners () 609 | "Apply iOS round corners to image(s)." 610 | (interactive) 611 | (dwim-shell-command-on-marked-files 612 | "Speed up gif" 613 | "set -o xtrace 614 | width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 '<>') 615 | height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<>') 616 | corner=$((${width}/4)) 617 | echo ${corner} 618 | convert -size ${width}x${height} xc:none -fill white -draw \"roundRectangle 0,0 ${width},${height} ${corner},${corner}\" '<>' -compose SrcIn -composite '<>_ios_round.<>'" 619 | :utils '("ffprobe" "convert"))) 620 | 621 | ;;;###autoload 622 | (defun dwim-shell-commands-clip-round-rect-gif () 623 | "Clip gif(s) with round rectangle." 624 | (interactive) 625 | (dwim-shell-command-on-marked-files 626 | "Clip round rect gif(s)" 627 | "width=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 '<>') 628 | height=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 '<>') 629 | convert -quiet -size \"${width}x${height}\" xc:none -fill black -draw \"roundRectangle 0,0,${width},${height} <>,<>\" '<>/mask.png' 630 | convert '<>' -coalesce -background black -alpha remove -alpha off '<>/no_alpha.<>' 631 | # https://stackoverflow.com/a/66990135 632 | convert '<>/no_alpha.<>' -quiet -coalesce -alpha extract null: \\( '<>/mask.png' -alpha extract \\) -compose multiply -layers composite '<>/alpha.gif' 633 | convert '<>/no_alpha.<>' null: '<>/alpha.gif' -quiet -alpha off -compose copy_opacity -layers composite '<>_rounded.<>' 634 | # Turn looping on. 635 | mogrify -loop 0 '<>_rounded.<>' 636 | gifsicle -O3 '<>_rounded.<>' --lossy=80 -o '<>_rounded.<>'" 637 | :extensions "gif" 638 | :utils '("ffprobe" "convert"))) 639 | 640 | ;;;###autoload 641 | (defun dwim-shell-commands-resize-gif () 642 | "Resize marked gif(s)." 643 | (interactive) 644 | (dwim-shell-command-on-marked-files 645 | "Resize marked gif(s)" 646 | "gifsicle --scale <> '<>' -o '<>_x<>.gif'" 647 | :extensions "gif" 648 | :utils "gifsicle")) 649 | 650 | ;;;###autoload 651 | (defun dwim-shell-commands-epub-to-org () 652 | "Convert epub(s) to org." 653 | (interactive) 654 | (dwim-shell-command-on-marked-files 655 | "epub to org" 656 | "pandoc --from=epub --to=org '<>' > '<>.org'" 657 | :extensions "epub" 658 | :utils "pandoc")) 659 | 660 | ;;;###autoload 661 | (defun dwim-shell-commands-docx-to-pdf () 662 | "Convert docx(s) to pdf (via latex)." 663 | (interactive) 664 | (dwim-shell-command-on-marked-files 665 | "docx to pdf (via latex)" 666 | "pandoc -t latex '<>' -o '<>.pdf'" 667 | :extensions "docx" ;; brew install mactex 668 | :utils "pdflatex")) 669 | 670 | ;;;###autoload 671 | (defun dwim-shell-commands-kill-process () 672 | "Select and kill process." 673 | (interactive) 674 | (let* ((pid-width 5) 675 | (comm-width 25) 676 | (user-width 10) 677 | (processes (proced-process-attributes)) 678 | (candidates 679 | (mapcar (lambda (attributes) 680 | (let* ((process (cdr attributes)) 681 | (pid (format (format "%%%ds" pid-width) (map-elt process 'pid))) 682 | (user (format (format "%%-%ds" user-width) 683 | (truncate-string-to-width 684 | (map-elt process 'user) user-width nil nil t))) 685 | (comm (format (format "%%-%ds" comm-width) 686 | (truncate-string-to-width 687 | (map-elt process 'comm) comm-width nil nil t))) 688 | (args-width (- (window-width) (+ pid-width user-width comm-width 3))) 689 | (args (map-elt process 'args))) 690 | (cons (if args 691 | (format "%s %s %s %s" pid user comm (truncate-string-to-width args args-width nil nil t)) 692 | (format "%s %s %s" pid user comm)) 693 | process))) 694 | processes)) 695 | (selection (map-elt candidates 696 | (completing-read "kill process: " 697 | (seq-sort 698 | (lambda (p1 p2) 699 | (string-lessp (nth 2 (split-string (string-trim (car p1)))) 700 | (nth 2 (split-string (string-trim (car p2)))))) 701 | candidates) nil t))) 702 | (prompt-title (format "%s %s %s" 703 | (map-elt selection 'pid) 704 | (map-elt selection 'user) 705 | (map-elt selection 'comm)))) 706 | (when (y-or-n-p (format "Kill %s?" prompt-title)) 707 | (dwim-shell-command-on-marked-files 708 | (format "Kill %s" prompt-title) 709 | (format "kill -9 %d" (map-elt selection 'pid)) 710 | :utils "kill" 711 | :error-autofocus t 712 | :silent-success t)))) 713 | 714 | ;;;###autoload 715 | (defun dwim-shell-commands-macos-add-to-photos () 716 | "Add to Photos.app." 717 | (interactive) 718 | (dwim-shell-command-on-marked-files 719 | "Add to Photos.app" 720 | "osascript <>\" 723 | end tell 724 | EOF" 725 | :silent-success t 726 | :utils "osascript" 727 | :on-completion (lambda (buffer process) 728 | (if-let ((success (= (process-exit-status process) 0))) 729 | (progn 730 | (kill-buffer buffer) 731 | (start-process "Open Photos" nil "open" "-a" "Photos")) 732 | (switch-to-buffer buffer))))) 733 | 734 | ;;;###autoload 735 | (defun dwim-shell-commands-macos-toggle-bluetooth-device-connection () 736 | "Toggle Bluetooth device connection." 737 | (interactive) 738 | (let* ((devices (seq-filter 739 | (lambda (line) 740 | ;; Keep lines like: af-8c-3b-b1-99-af - Device name 741 | (string-match-p "^[0-9a-f]\\{2\\}" line)) 742 | (with-current-buffer (get-buffer-create "*BluetoothConnector*") 743 | (erase-buffer) 744 | ;; BluetoothConnector exits with 64 if no param is given. 745 | ;; Invoke with no params to get a list of devices. 746 | (unless (eq 64 (call-process "BluetoothConnector" nil (current-buffer))) 747 | (kill-buffer (current-buffer)) 748 | (error (buffer-string))) 749 | (let ((lines (split-string (buffer-string) "\n"))) 750 | (kill-buffer (current-buffer)) 751 | lines)))) 752 | (candidates (mapcar (lambda (device) 753 | ;; key (device name) : value (address) 754 | (cons (nth 1 (split-string device " - ")) 755 | (nth 0 (split-string device " - ")))) 756 | devices)) 757 | (selected-name (completing-read "Toggle connection: " 758 | (seq-sort #'string-lessp candidates) nil t)) 759 | (address (map-elt candidates selected-name))) 760 | (dwim-shell-command-on-marked-files 761 | (format "Toggle %s" selected-name) 762 | (format "BluetoothConnector %s --notify" address) 763 | :utils "BluetoothConnector" 764 | ;; :error-autofocus t 765 | ;; :silent-success t 766 | ))) 767 | 768 | ;;;###autoload 769 | (defun dwim-shell-commands-macos-bin-plist-to-xml () 770 | "Convert binary plist to xml." 771 | (interactive) 772 | (dwim-shell-command-on-marked-files 773 | "Convert binary plist to xml" 774 | "plutil -convert xml1 -o '<>.xml' '<>'" 775 | :utils "plutil")) 776 | 777 | ;;;###autoload 778 | (defun dwim-shell-commands-macos-toggle-dark-mode () 779 | "Toggle macOS dark mode." 780 | (interactive) 781 | (dwim-shell-command-on-marked-files 782 | "Toggle dark mode" 783 | "dark-mode" 784 | :utils "dark-mode" ;; brew install dark-mode 785 | :silent-success t)) 786 | 787 | ;;;###autoload 788 | (defun dwim-shell-commands-macos-toggle-menu-bar-autohide () 789 | "Toggle macOS dark mode." 790 | (interactive) 791 | (dwim-shell-command-on-marked-files 792 | "Toggle menu bar auto-hide." 793 | "current_status=$(osascript -e 'tell application \"System Events\" to get autohide menu bar of dock preferences') 794 | 795 | if [ \"$current_status\" = \"true\" ]; then 796 | osascript -e 'tell application \"System Events\" to set autohide menu bar of dock preferences to false' 797 | echo \"Auto-hide disabled.\" 798 | else 799 | osascript -e 'tell application \"System Events\" to set autohide menu bar of dock preferences to true' 800 | echo \"Auto-hide enabled.\" 801 | fi" 802 | :utils "osascript" 803 | :shell-util "zsh" 804 | :silent-success t)) 805 | 806 | ;;;###autoload 807 | (defun dwim-shell-commands-pdf-to-txt () 808 | "Convert pdf to txt." 809 | (interactive) 810 | (dwim-shell-command-on-marked-files 811 | "pdf to txt" 812 | "pdftotext -layout '<>' '<>.txt'" 813 | :utils "pdftotext")) 814 | 815 | ;;;###autoload 816 | (defun dwim-shell-commands-resize-image-by-factor () 817 | "Resize marked image(s) by factor." 818 | (interactive) 819 | (dwim-shell-command-on-marked-files 820 | "Resize image" 821 | (let ((factor (read-number "Resize scaling factor: " 0.5))) 822 | (format "convert -resize %%%d '<>' '<>_x%.2f.<>'" 823 | (* 100 factor) factor)) 824 | :utils "convert")) 825 | 826 | ;;;###autoload 827 | (defun dwim-shell-commands-resize-image-in-pixels () 828 | "Resize marked image(s) in pixels." 829 | (interactive) 830 | (dwim-shell-command-on-marked-files 831 | "Resize image" 832 | (let ((width (read-number "Resize width (pixels): " 500))) 833 | (format "convert -resize %dx '<>' '<>_x%d.<>'" width width)) 834 | :utils "convert")) 835 | 836 | ;;;###autoload 837 | (defun dwim-shell-commands-pdf-password-protect () 838 | "Add a password to pdf(s)." 839 | (interactive) 840 | (dwim-shell-command-on-marked-files 841 | "Password protect pdf" 842 | (format "qpdf --verbose --encrypt '%s' '%s' 256 -- '<>' '<>_protected.<>'" 843 | (read-passwd "user-password: ") 844 | (read-passwd "owner-password: ")) 845 | :utils "qpdf" 846 | :extensions "pdf")) 847 | 848 | ;;;###autoload 849 | (defun dwim-shell-commands-pdf-password-unprotect () 850 | "Remove a password from pdf(s)." 851 | (interactive) 852 | (dwim-shell-command-on-marked-files 853 | "Remove protection from pdf" 854 | (format "qpdf --verbose --decrypt --password='%s' -- '<>' '<>_unprotected.<>'" 855 | (read-passwd "password: ")) 856 | :utils "qpdf" 857 | :extensions "pdf")) 858 | 859 | (defun dwim-shell-commands--gifsicle-frames-every (skipping-every file) 860 | "Generate frames SKIPPING-EVERY count for video FILE." 861 | (string-join 862 | (seq-map (lambda (n) (format "'#%d'" n)) 863 | (number-sequence 0 (string-to-number 864 | ;; Get total frames count. 865 | (seq-first (process-lines "identify" "-format" "%n\n" file))) 866 | skipping-every)) " ")) 867 | 868 | ;;;###autoload 869 | (defun dwim-shell-commands-video-to-mp3 () 870 | "Convert video(s) to mp3." 871 | (interactive) 872 | (dwim-shell-command-on-marked-files 873 | "Convert to mp3" 874 | "ffmpeg -i '<>' -vn -ab 128k -ar 44100 -y '<>.mp3'" 875 | :utils "ffmpeg")) 876 | 877 | ;;;###autoload 878 | (defun dwim-shell-commands-video-to-mp3-with-artwork () 879 | "Convert video(s) to mp3 (keep frame as artwork)." 880 | (interactive) 881 | (dwim-shell-command-on-marked-files 882 | "Convert to mp3" 883 | "ffmpeg -i '<>' -vf 'select=eq(n\\,0)' -q:v 3 cover.jpg -i '<>' -vn -ab 128k -ar 44100 -y -map_metadata 0 -id3v2_version 3 -write_id3v1 1 -metadata:s:v title='Album cover' -metadata:s:v comment='Cover (front)' '<>.mp3'" 884 | :utils "ffmpeg")) 885 | 886 | ;;;###autoload 887 | (defun dwim-shell-commands-ndjson-to-org () 888 | "Convert ndjson file to org." 889 | (interactive) 890 | (unless (eq (length (dwim-shell-command--files)) 1) 891 | (error "Only 1 file supported")) 892 | (let* ((emacs-bin (file-truename (expand-file-name invocation-name 893 | invocation-directory))) 894 | (source (nth 0 (dwim-shell-command--files))) 895 | (destination (concat (file-name-sans-extension 896 | source) ".org")) 897 | (fields (with-temp-buffer 898 | (insert-file-contents source) 899 | (buffer-substring-no-properties (point-min) (line-end-position)) 900 | (read-string "Fields: " (mapconcat 'identity (mapcar (lambda (item) 901 | (symbol-name (car item))) 902 | (json-read-from-string 903 | (buffer-substring-no-properties 904 | (point-min) (line-end-position)))) 905 | " "))))) 906 | (dwim-shell-command-on-marked-files 907 | "Convert ndjson to org" 908 | (format "%s --quick --batch --eval \"%s\"" emacs-bin 909 | (replace-regexp-in-string 910 | "\"" "\\\\\"" 911 | (prin1-to-string 912 | `(progn 913 | (require 'org) 914 | (require 'json) 915 | (defun convert-to-org-table (ndjson) 916 | (let ((rows (mapcar #'json-read-from-string 917 | (split-string ndjson "\n" t)))) 918 | (orgtbl-to-orgtbl 919 | (append 920 | (list (split-string ,fields)) 921 | '(hline) 922 | (mapcar (lambda (obj) 923 | (mapcar (lambda (key) 924 | (or (alist-get (intern key) obj) "")) 925 | (split-string ,fields))) 926 | rows)) nil))) 927 | (with-temp-buffer 928 | (insert-file-contents ,source) 929 | (let ((org (convert-to-org-table (buffer-string)))) 930 | (with-temp-file ,destination 931 | (insert org)))))))) 932 | :extensions "ndjson"))) 933 | 934 | ;;;###autoload 935 | (defun dwim-shell-commands-set-media-artwork-image-metadata () 936 | "Set image artwork metadata for media file(s)." 937 | (interactive) 938 | (let ((artwork-file (file-name-unquote 939 | (read-file-name "Select artwork image: " 940 | nil nil t))) 941 | (should-backup (y-or-n-p "Create backup files? "))) 942 | (unless (file-regular-p artwork-file) 943 | (user-error "Not a file")) 944 | (unless should-backup 945 | (unless (y-or-n-p "Override file(s)? ") 946 | (user-error "Aborted"))) 947 | (dwim-shell-command-on-marked-files 948 | "Set album artwork" 949 | (format (if should-backup 950 | "ffmpeg -i '<>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<>.tmp.<>' && mv -f '<>' '<>.bak' && mv '<>.tmp.<>' '<>'" 951 | "ffmpeg -i '<>' -i '%s' -map_metadata 0 -map 0:a -map 1 -c copy -disposition:v:0 attached_pic '<>.tmp.<>' && mv -f '<>.tmp.<>' '<>'") 952 | artwork-file) 953 | :utils "AtomicParsley" 954 | :silent-success t))) 955 | 956 | ;;;###autoload 957 | (defun dwim-shell-commands-video-trim-beginning () 958 | "Drop audio from all marked videos." 959 | (interactive) 960 | (dwim-shell-command-on-marked-files 961 | "Trim beginning" 962 | "ffmpeg -i '<>' -y -ss <> -c:v copy -c:a copy '<>_trimmed.<>'" 963 | :silent-success t 964 | :utils "ffmpeg")) 965 | 966 | ;;;###autoload 967 | (defun dwim-shell-commands-video-trim-end () 968 | "Drop audio from all marked videos." 969 | (interactive) 970 | (dwim-shell-command-on-marked-files 971 | "Trim beginning" 972 | "ffmpeg -sseof -<> -i '<>' -y -c:v copy -c:a copy '<>_trimmed.<>'" 973 | :silent-success t 974 | :utils "ffmpeg")) 975 | 976 | ;;;###autoload 977 | (defun dwim-shell-commands-video-to-thumbnail () 978 | "Generate a thumbnail for marked video(s)." 979 | (interactive) 980 | (let ((temp-dir (make-temp-file "thumbnails-" t))) 981 | (dwim-shell-command-on-marked-files 982 | "Thumbnail with ffmpeg" 983 | "ffmpeg -i '<>' -ss 00:00:01.000 -vframes 1 '<>.jpg'" 984 | :utils "ffmpeg"))) 985 | 986 | ;;;###autoload 987 | (defun dwim-shell-commands-drop-video-audio () 988 | "Drop audio from all marked videos." 989 | (interactive) 990 | (dwim-shell-command-on-marked-files 991 | "Drop audio" 992 | "ffmpeg -i '<>' -c copy -an '<>_no_audio.<>'" 993 | :utils "ffmpeg")) 994 | 995 | ;;;###autoload 996 | (defun dwim-shell-commands-ping-google () 997 | "Ping google.com." 998 | (interactive) 999 | (dwim-shell-command-on-marked-files 1000 | "Ping google.com" 1001 | "ping -c 3 google.com" 1002 | :utils "ping" 1003 | :focus-now t)) 1004 | 1005 | ;;;###autoload 1006 | (defun dwim-shell-commands-speed-up-video () 1007 | "Speed up video(s)." 1008 | (interactive) 1009 | (dwim-shell-command-on-marked-files 1010 | "Speed up video" 1011 | (let ((factor (read-number "Resize scaling factor: " 2))) 1012 | (format "ffmpeg -i '<>' -an -filter:v 'setpts=%s*PTS' '<>_x%s.<>'" 1013 | (/ 1 (float factor)) factor)) 1014 | :utils "ffmpeg")) 1015 | 1016 | ;;;###autoload 1017 | (defun dwim-shell-commands-speed-up-video-fragment () 1018 | "Speed up fragment in video(s)." 1019 | (interactive) 1020 | (let ((start (read-number "Start (seconds): ")) 1021 | (end (read-number "End (seconds): ")) 1022 | (factor (read-number "Speed up factor: " 2))) 1023 | (dwim-shell-command-on-marked-files 1024 | "Speed up fragment in video" 1025 | (format "ffmpeg -i '<>' -filter_complex '[0:v]trim=start=0:end=%d,setpts=PTS-STARTPTS[v0];[0:v]trim=start=%d:end=%d,setpts=(PTS-1)/%d[v1];[0:v]trim=start=%d,setpts=PTS-STARTPTS[v2];[v0][v1][v2]concat=n=3:v=1:a=0' -preset fast '<>_%d:%dx%d.<>'" start start end factor end start end factor) 1026 | :utils "ffmpeg"))) 1027 | 1028 | (defun dwim-shell-commands-set-song-title () 1029 | "Set song(s) title." 1030 | (interactive) 1031 | (let ((title (replace-regexp-in-string "'" "\'" (read-string "New title: ")))) 1032 | (dwim-shell-command-on-marked-files 1033 | "Set song(s) title" 1034 | (format "ffmpeg -i '<>' -metadata title='%s' -codec copy '<>/<>_temp.<>' && mv -f '<>/<>_temp.<>' '<>'" 1035 | title) 1036 | :utils "ffmpeg"))) 1037 | 1038 | ;;;###autoload 1039 | (defun dwim-shell-commands-resize-video () 1040 | "Resize marked images." 1041 | (interactive) 1042 | (dwim-shell-command-on-marked-files 1043 | "Resize video" 1044 | " 1045 | eval $(ffprobe -v quiet -show_format -of flat=s=_ -show_entries stream=width '<>'); 1046 | width=${streams_stream_0_width}; 1047 | zmodload zsh/mathfunc 1048 | width=$((rint($width * <>))); 1049 | # Make it even or face 'not divisible by 2' errors. 1050 | if [[ $((width%2)) -ne 0 ]] then 1051 | width=$(($width - 1)) 1052 | fi 1053 | ffmpeg -n -i '<>' -vf \"scale=$width:-2\" '<>_x<>.<>'" 1054 | :utils "ffmpeg")) 1055 | 1056 | ;;;###autoload 1057 | (defun dwim-shell-commands-clipboard-to-qr () 1058 | "Generate a QR code from clipboard." 1059 | (interactive) 1060 | (let ((temp-file (concat (temporary-file-directory) "qr-code"))) 1061 | (dwim-shell-command-on-marked-files 1062 | "Generate a QR code from clipboard" 1063 | (format "qrencode -s10 -o %s %s" temp-file (shell-quote-argument (current-kill 0))) 1064 | :utils "qrencode" 1065 | :on-completion (lambda (buffer _process) 1066 | (kill-buffer buffer) 1067 | (switch-to-buffer (find-file-noselect temp-file t)))))) 1068 | 1069 | ;;;###autoload 1070 | (defun dwim-shell-commands-sha-256-hash-file-at-clipboard-url () 1071 | "Download file at clipboard URL and generate SHA-256 hash." 1072 | (interactive) 1073 | (unless (string-match-p "^http[s]?://" (current-kill 0)) 1074 | (user-error "No URL in clipboard")) 1075 | (dwim-shell-command-on-marked-files 1076 | "Generate SHA-256 hash from clipboard URL." 1077 | "curl -s '<>' | sha256sum - | cut -d ' ' -f1" 1078 | :utils '("curl" "sha256sum") 1079 | :on-completion 1080 | (lambda (buffer process) 1081 | (if-let ((success (= (process-exit-status process) 0)) 1082 | (hash (with-current-buffer buffer 1083 | (string-trim (buffer-string))))) 1084 | (progn 1085 | (kill-buffer buffer) 1086 | (kill-new hash) 1087 | (message "Copied %s to clipboard" 1088 | (propertize hash 'face 'font-lock-string-face))) 1089 | (switch-to-buffer buffer))))) 1090 | 1091 | ;;;###autoload 1092 | (defun dwim-shell-commands-view-sqlite-schema-diagram () 1093 | "View sqlite schema diagram." 1094 | (interactive) 1095 | (dwim-shell-command-on-marked-files 1096 | "View sqlite schema" 1097 | "set -e 1098 | temp_dir=\"${TMPDIR:-/tmp/}\" 1099 | file_name=\"sqlite-schema-diagram.sql\" 1100 | file_path=\"${temp_dir}/${file_name}\" 1101 | url=\"https://gitlab.com/Screwtapello/sqlite-schema-diagram/-/raw/main/sqlite-schema-diagram.sql\" 1102 | 1103 | if [[ ! -f \"$file_path\" ]]; then 1104 | curl -o \"$file_path\" \"$url\" 1105 | fi 1106 | 1107 | sqlite3 -list \"<>\" < $file_path > \"<>.dot\" 1108 | dot -Tsvg \"<>.dot\" > \"<>.svg\" 1109 | echo \"<>.svg\" 1110 | " 1111 | :utils '("dot" "sqlite3") 1112 | :on-completion 1113 | (lambda (buffer process) 1114 | (if (= (process-exit-status process) 0) 1115 | (with-current-buffer buffer 1116 | (let ((svg-file (string-trim (buffer-string)))) 1117 | (if (string-suffix-p "svg" svg-file) 1118 | (progn 1119 | (find-file svg-file) 1120 | (kill-buffer buffer)) 1121 | (switch-to-buffer buffer)))) 1122 | (switch-to-buffer buffer))))) 1123 | 1124 | ;;;###autoload 1125 | (defun dwim-shell-commands-open-externally () 1126 | "Open file(s) externally." 1127 | (interactive) 1128 | (dwim-shell-command-on-marked-files 1129 | "Open externally" 1130 | (if (eq system-type 'darwin) 1131 | (if (derived-mode-p 'prog-mode) 1132 | (format "xed --line %d '<>'" 1133 | (line-number-at-pos (point))) 1134 | "open '<>'") 1135 | "xdg-open '<>'") 1136 | :shell-util "zsh" 1137 | :shell-args '("-x" "-c") 1138 | :silent-success t 1139 | :utils (if (eq system-type 'darwin) 1140 | "open" 1141 | "xdg-open"))) 1142 | 1143 | ;;;###autoload 1144 | (defun dwim-shell-commands-macos-caffeinate () 1145 | "Invoke caffeinate to prevent mac from sleeping." 1146 | (interactive) 1147 | (dwim-shell-command-on-marked-files 1148 | "Caffeinate" 1149 | "caffeinate" 1150 | :utils "caffeinate" 1151 | :no-progress t 1152 | :focus-now t)) 1153 | 1154 | ;;;###autoload 1155 | (defun dwim-shell-commands-macos-make-finder-alias () 1156 | "Make macOS Finder alias." 1157 | (interactive) 1158 | (let ((files (dwim-shell-command--files)) 1159 | (target-dir (read-directory-name "Select target dir: " "/Applications" nil t))) 1160 | (dwim-shell-command-on-marked-files 1161 | "Make macOS alias" 1162 | (format "osascript -e 'tell application \"Finder\" to make alias file to POSIX file \"<>\" at POSIX file \"%s\"'" 1163 | target-dir) 1164 | :utils "osascript" 1165 | :no-progress t 1166 | :silent-success t 1167 | :on-completion (lambda (buffer _process) 1168 | (kill-buffer buffer) 1169 | (dired-jump nil (file-name-concat target-dir (file-name-nondirectory (nth 0 files)))))))) 1170 | 1171 | ;;;###autoload 1172 | (defun dwim-shell-commands-macos-version-and-hardware-overview-info () 1173 | "View macOS version and hardware overview info." 1174 | (interactive) 1175 | (dwim-shell-command-on-marked-files 1176 | "macOS hardware overview" 1177 | "sw_vers; system_profiler SPHardwareDataType" 1178 | :utils '("sw_vers" "system_profiler"))) 1179 | 1180 | ;;;###autoload 1181 | (defun dwim-shell-commands-macos-reveal-in-finder () 1182 | "Reveal selected files in macOS Finder." 1183 | (interactive) 1184 | (dwim-shell-command-on-marked-files 1185 | "Reveal in Finder" 1186 | "import AppKit 1187 | NSWorkspace.shared.activateFileViewerSelecting([\"<<*>>\"].map{URL(fileURLWithPath:$0)})" 1188 | :silent-success t 1189 | :shell-pipe "swift -" 1190 | :join-separator ", " 1191 | :utils "swift")) 1192 | 1193 | (defun dwim-shell-commands--macos-sharing-services () 1194 | "Return a list of sharing services." 1195 | (let* ((source (format "import AppKit 1196 | NSSharingService.sharingServices(forItems: [ 1197 | %s 1198 | ]).forEach { 1199 | print(\"\\($0.title)\") 1200 | }" 1201 | (string-join (mapcar (lambda (file) 1202 | (format "URL(fileURLWithPath: \"%s\")" file)) 1203 | (dwim-shell-command--files)) 1204 | ", "))) 1205 | (services (split-string 1206 | (string-trim 1207 | ;; Remove anything that doesn't start with alpha characters 1208 | ;; There may be compilation warnings. 1209 | (replace-regexp-in-string "^[^[:alpha:]].*\n" "" 1210 | (shell-command-to-string (format "echo '%s' | swift -" source)))) 1211 | "\n"))) 1212 | (when (seq-empty-p services) 1213 | (error "No sharing services available")) 1214 | services)) 1215 | 1216 | ;;;###autoload 1217 | (defun dwim-shell-commands-macos-share () 1218 | "Share selected files from macOS." 1219 | (interactive) 1220 | (let* ((services (dwim-shell-commands--macos-sharing-services)) 1221 | (service-name (completing-read "Share via: " services)) 1222 | (selection (seq-position services service-name #'string-equal))) 1223 | (dwim-shell-command-on-marked-files 1224 | "Share" 1225 | (format 1226 | "import AppKit 1227 | 1228 | _ = NSApplication.shared 1229 | 1230 | NSApp.setActivationPolicy(.regular) 1231 | 1232 | class MyWindow: NSWindow, NSSharingServiceDelegate { 1233 | func sharingService( 1234 | _ sharingService: NSSharingService, 1235 | didShareItems items: [Any] 1236 | ) { 1237 | NSApplication.shared.terminate(nil) 1238 | } 1239 | 1240 | func sharingService( 1241 | _ sharingService: NSSharingService, didFailToShareItems items: [Any], error: Error 1242 | ) { 1243 | let error = error as NSError 1244 | if error.domain == NSCocoaErrorDomain && error.code == NSUserCancelledError { 1245 | NSApplication.shared.terminate(nil) 1246 | } 1247 | exit(1) 1248 | } 1249 | } 1250 | 1251 | let window = MyWindow( 1252 | contentRect: NSRect(x: 0, y: 0, width: 0, height: 0), 1253 | styleMask: [], 1254 | backing: .buffered, 1255 | defer: false) 1256 | 1257 | let services = NSSharingService.sharingServices(forItems: [\"<<*>>\"].map{URL(fileURLWithPath:$0)}) 1258 | let service = services[%s] 1259 | service.delegate = window 1260 | service.perform(withItems: [\"<<*>>\"].map{URL(fileURLWithPath:$0)}) 1261 | 1262 | NSApp.run()" selection) 1263 | :silent-success t 1264 | :shell-pipe "swift -" 1265 | :join-separator ", " 1266 | :no-progress t 1267 | :utils "swift"))) 1268 | 1269 | ;;;###autoload 1270 | (defun dwim-shell-commands-macos-toggle-display-rotation () 1271 | "Rotate display." 1272 | (interactive) 1273 | ;; # Display_ID Resolution ____Display_Bounds____ Rotation 1274 | ;; 2 0x2b347692 1440x2560 0 0 1440 2560 270 [main] 1275 | ;; From fb-rotate output, get the `current-rotation' from Column 7, row 1 zero-based. 1276 | (let ((current-rotation (nth 7 (split-string (nth 1 (process-lines "fb-rotate" "-i")))))) 1277 | (dwim-shell-command-on-marked-files 1278 | "macOS hardware overview" 1279 | (format "fb-rotate -d 1 -r %s" (if (equal current-rotation "270") "0" "270")) 1280 | :utils "fb-rotate"))) 1281 | 1282 | ;;;###autoload 1283 | (defun dwim-shell-commands-make-swift-package-library () 1284 | "Create a swift package library" 1285 | (interactive) 1286 | (dwim-shell-command-on-marked-files 1287 | "Create a swift package library" 1288 | "swift package init --type library" 1289 | :utils "swift")) 1290 | 1291 | ;;;###autoload 1292 | (defun dwim-shell-commands-make-swift-package-executable () 1293 | "Create a swift package executable" 1294 | (interactive) 1295 | (dwim-shell-command-on-marked-files 1296 | "Create a swift package executable" 1297 | "swift package init --type executable" 1298 | :utils "swift")) 1299 | 1300 | (defun dwim-shell-commands--macos-apps () 1301 | "Return alist of macOS apps (\"Emacs\" . \"/Applications/Emacs.app\")." 1302 | (mapcar (lambda (path) 1303 | (cons (file-name-base path) path)) 1304 | (seq-sort 1305 | #'string-lessp 1306 | (seq-mapcat (lambda (paths) 1307 | (directory-files-recursively 1308 | paths "\\.app$" t (lambda (path) 1309 | (not (string-suffix-p ".app" path))))) 1310 | '("/Applications" "~/Applications" "/System/Applications"))))) 1311 | 1312 | ;;;###autoload 1313 | (defun dwim-shell-commands-macos-set-default-app () 1314 | "Set default app for file(s)." 1315 | (interactive) 1316 | (let* ((apps (dwim-shell-commands--macos-apps)) 1317 | (selection (progn 1318 | (cl-assert apps nil "No apps found") 1319 | (completing-read "Set default app: " apps nil t)))) 1320 | (dwim-shell-command-on-marked-files 1321 | "Set default app" 1322 | (format "duti -s \"%s\" '<>' all" 1323 | (string-trim 1324 | (shell-command-to-string (format "defaults read '%s/Contents/Info.plist' CFBundleIdentifier" 1325 | (map-elt apps selection))))) 1326 | :silent-success t 1327 | :no-progress t 1328 | :utils "duti"))) 1329 | 1330 | ;;;###autoload 1331 | (defun dwim-shell-commands-macos-open-with () 1332 | "Open file(s) with specific external app." 1333 | (interactive) 1334 | (let* ((apps (dwim-shell-commands--macos-apps)) 1335 | (selection (progn 1336 | (cl-assert apps nil "No apps found") 1337 | (completing-read "Open with: " apps nil t)))) 1338 | (dwim-shell-command-on-marked-files 1339 | "Open with" 1340 | (format "open -a '%s' '<<*>>'" (map-elt apps selection)) 1341 | :silent-success t 1342 | :no-progress t 1343 | :utils "open"))) 1344 | 1345 | ;;;###autoload 1346 | (defun dwim-shell-commands-macos-open-with-firefox () 1347 | "Open file(s) in Firefox." 1348 | (interactive) 1349 | (dwim-shell-command-on-marked-files 1350 | "Open in Firefox" 1351 | "open -a Firefox '<<*>>'" 1352 | :silent-success t 1353 | :no-progress t 1354 | :utils "open")) 1355 | 1356 | ;;;###autoload 1357 | (defun dwim-shell-commands-macos-open-with-safari () 1358 | "Open file(s) in Safari." 1359 | (interactive) 1360 | (dwim-shell-command-on-marked-files 1361 | "Open in Safari" 1362 | "open -a Safari '<<*>>'" 1363 | :silent-success t 1364 | :no-progress t 1365 | :utils "open")) 1366 | 1367 | ;;;###autoload 1368 | (defun dwim-shell-commands-macos-start-recording-window () 1369 | "Select and start recording a macOS window." 1370 | (interactive) 1371 | (let* ((window (dwim-shell-commands--macos-select-window)) 1372 | (path (dwim-shell-commands--generate-path "~/Screenshots" (car window) ".gif")) 1373 | (buffer-file-name path) ;; override so <> picks it up 1374 | (inhibit-message t)) 1375 | ;; Silence echo to avoid unrelated messages making into animation. 1376 | (cl-letf (((symbol-function 'dwim-shell-command--message) 1377 | (lambda (fmt &rest args) nil))) 1378 | (dwim-shell-command-on-marked-files 1379 | "Start recording a macOS window." 1380 | (format 1381 | "macosrec --record '%s' --gif --output '<>'" 1382 | (cdr window)) 1383 | :silent-success t 1384 | :monitor-directory "~/Screenshots" 1385 | :no-progress t 1386 | :utils '("ffmpeg" "macosrec") 1387 | :on-completion 1388 | (lambda (buffer process) 1389 | (if (= (process-exit-status process) 0) 1390 | (progn 1391 | "Saved recording" 1392 | (dired-jump nil path) 1393 | (kill-buffer buffer)) 1394 | (with-current-buffer buffer 1395 | (goto-char (point-min)) 1396 | (if (search-forward "Aborted" nil t) 1397 | (progn 1398 | (message "Aborted recording") 1399 | (kill-buffer buffer)) 1400 | (switch-to-buffer buffer))))))))) 1401 | 1402 | (defun dwim-shell-commands--generate-path (dir name ext) 1403 | "Generate a timestamped path with DIR, NAME, and EXT." 1404 | (concat (file-name-as-directory (expand-file-name dir)) 1405 | (format-time-string "%Y-%m-%d-%H:%M:%S-") 1406 | name ext)) 1407 | 1408 | (defun dwim-shell-commands--macos-select-window () 1409 | "Return a list of macOS windows." 1410 | (if-let* ((line (completing-read 1411 | "Select: " 1412 | (process-lines "macosrec" "--list") nil t)) 1413 | (window-info (split-string line " ")) 1414 | (window-number (string-to-number (nth 0 window-info))) 1415 | (window-app (nth 1 window-info)) 1416 | (valid (> window-number 0))) 1417 | (cons window-app window-number) 1418 | (user-error "No window found"))) 1419 | 1420 | ;;;###autoload 1421 | (defun dwim-shell-commands-macos-end-recording-window () 1422 | "Stop recording a macOS window." 1423 | (interactive) 1424 | (let ((inhibit-message t)) 1425 | (cl-letf (((symbol-function 'dwim-shell-command--message) 1426 | (lambda (fmt &rest args) nil))) 1427 | (dwim-shell-command-on-marked-files 1428 | "End recording macOS window." 1429 | "macosrec --save" 1430 | :silent-success t 1431 | :no-progress t 1432 | :error-autofocus t 1433 | :utils "macosrec")))) 1434 | 1435 | ;;;###autoload 1436 | (defun dwim-shell-commands-macos-abort-recording-window () 1437 | "Stop recording a macOS window." 1438 | (interactive) 1439 | (let ((inhibit-message t)) 1440 | (cl-letf (((symbol-function 'dwim-shell-command--message) 1441 | (lambda (fmt &rest args) nil))) 1442 | (dwim-shell-command-on-marked-files 1443 | "Abort recording macOS window." 1444 | "macosrec --abort" 1445 | :silent-success t 1446 | :no-progress t 1447 | :utils "macosrec")))) 1448 | 1449 | ;;;###autoload 1450 | (defun dwim-shell-commands-macos-screenshot-window () 1451 | "Select and screenshot macOS window." 1452 | (interactive) 1453 | ;; Silence echo to avoid unrelated messages making into screenshot. 1454 | (let ((window (dwim-shell-commands--macos-select-window)) 1455 | (inhibit-message t)) 1456 | (dwim-shell-command-on-marked-files 1457 | "Start recording a macOS window." 1458 | (format "macosrec --screenshot %s" (cdr window)) 1459 | :silent-success t 1460 | :monitor-directory "~/Screenshots" 1461 | :no-progress t 1462 | :utils "macosrec"))) 1463 | 1464 | ;;;###autoload 1465 | (defun dwim-shell-commands-files-combined-size () 1466 | "Get files combined file size." 1467 | (interactive) 1468 | (dwim-shell-command-on-marked-files 1469 | "Get files combined file size" 1470 | "du -csh '<<*>>'" 1471 | :utils "du" 1472 | :on-completion (lambda (buffer _process) 1473 | (with-current-buffer buffer 1474 | (message "Total size: %s" 1475 | (progn 1476 | (re-search-backward "\\(^[ 0-9.,]+[A-Za-z]+\\).*total$") 1477 | (match-string 1)))) 1478 | (kill-buffer buffer)))) 1479 | 1480 | ;;;###autoload 1481 | (defun dwim-shell-commands-image-to-icns () 1482 | "Convert png to icns icon." 1483 | (interactive) 1484 | (dwim-shell-command-on-marked-files 1485 | "Convert png to icns icon." 1486 | " 1487 | # Based on http://stackoverflow.com/questions/12306223/how-to-manually-create-icns-files-using-iconutil 1488 | # Note: png must be 1024x1024 1489 | mkdir '<>.iconset' 1490 | sips -z 16 16 '<>' --out '<>.iconset/icon_16x16.png' 1491 | sips -z 32 32 '<>' --out '<>.iconset/icon_16x16@2x.png' 1492 | sips -z 32 32 '<>' --out '<>.iconset/icon_32x32.png' 1493 | sips -z 64 64 '<>' --out '<>.iconset/icon_32x32@2x.png' 1494 | sips -z 128 128 '<>' --out '<>.iconset/icon_128x128.png' 1495 | sips -z 256 256 '<>' --out '<>.iconset/icon_128x128@2x.png' 1496 | sips -z 256 256 '<>' --out '<>.iconset/icon_256x256@2x.png' 1497 | sips -z 512 512 '<>' --out '<>.iconset/icon_512x512.png' 1498 | sips -z 512 512 '<>' --out '<>.iconset/icon_256x256@2x.png' 1499 | sips -z 1024 1024 '<>' --out '<>.iconset/icon_512x512@2x.png' 1500 | iconutil -c icns '<>.iconset'" 1501 | :utils '("sips" "iconutil") 1502 | :extensions "png")) 1503 | 1504 | ;;;###autoload 1505 | (defun dwim-shell-commands-image-add-drop-shadow () 1506 | "Add a drop shadow." 1507 | (interactive) 1508 | (dwim-shell-command-on-marked-files 1509 | "Add a drop shadow." 1510 | "convert <> -bordercolor white -border 13 \\( +clone -background black -shadow 80x3+2+2 \\) +swap -background white -layers merge +repage <>-shadow.<>" 1511 | :utils "convert")) 1512 | 1513 | ;;;###autoload 1514 | (defun dwim-shell-commands-image-trim-borders () 1515 | "Trim image(s) border (useful for video screenshots)." 1516 | (interactive) 1517 | (dwim-shell-command-on-marked-files 1518 | "Trim image border" 1519 | "magick convert -fuzz 3% -define trim:percent-background=0% -trim +repage '<>' '<>_trimmed.<>'" 1520 | :utils "magick")) 1521 | 1522 | ;;;###autoload 1523 | (defun dwim-shell-commands-git-clone-clipboard-url-to-downloads () 1524 | "Clone git URL in clipboard to \"~/Downloads/\"." 1525 | (interactive) 1526 | (cl-assert (or (string-match-p "^\\(http\\|https\\|ssh\\)://" (current-kill 0)) 1527 | (string-match-p "^git@" (current-kill 0))) nil "No URL in clipboard") 1528 | (let* ((url (current-kill 0)) 1529 | (download-dir (expand-file-name "~/Downloads/")) 1530 | (project-dir (concat download-dir (file-name-base url))) 1531 | (default-directory download-dir)) 1532 | (when (or (not (file-exists-p project-dir)) 1533 | (when (y-or-n-p (format "%s exists. delete?" (file-name-base url))) 1534 | (delete-directory project-dir t) 1535 | t)) 1536 | (dwim-shell-command-on-marked-files 1537 | (format "Clone %s" (file-name-base url)) 1538 | (format "git clone %s" url) 1539 | :utils "git" 1540 | :on-completion (lambda (buffer _process) 1541 | (kill-buffer buffer) 1542 | (dired project-dir)))))) 1543 | 1544 | ;;;###autoload 1545 | (defun dwim-shell-commands-http-serve-dir () 1546 | "HTTP serve current directory." 1547 | (interactive) 1548 | (cond ((executable-find "python3") 1549 | (dwim-shell-command-on-marked-files 1550 | "HTTP serve current dir" 1551 | "python3 -m http.server" 1552 | :utils "python3" 1553 | :focus-now t 1554 | :no-progress t)) 1555 | ((executable-find "python2") 1556 | (dwim-shell-command-on-marked-files 1557 | "HTTP serve current dir" 1558 | "python2 -m SimpleHTTPServer" 1559 | :utils "python2" 1560 | :focus-now t 1561 | :no-progress t)) 1562 | ((executable-find "python") 1563 | (dwim-shell-command-on-marked-files 1564 | "HTTP serve current dir" 1565 | "python -m SimpleHTTPServer" 1566 | :utils "python" 1567 | :focus-now t 1568 | :no-progress t)) 1569 | (t 1570 | (error "No python found")))) 1571 | 1572 | ;;;###autoload 1573 | (defun dwim-shell-commands-git-clone-clipboard-url () 1574 | "Clone git URL in clipboard to `default-directory'." 1575 | (interactive) 1576 | (dwim-shell-command-on-marked-files 1577 | (format "Clone %s" (file-name-base (current-kill 0))) 1578 | "git clone <>" 1579 | :utils "git")) 1580 | 1581 | ;;;###autoload 1582 | (defun dwim-shell-commands-pass-git-pull () 1583 | "Pass git pull." 1584 | (interactive) 1585 | (dwim-shell-command-on-marked-files 1586 | "pass git pull" 1587 | "pass git pull" 1588 | :utils '("pass" "git") 1589 | :silent-success t)) 1590 | 1591 | ;;;###autoload 1592 | (defun dwim-shell-commands-git-list-untracked-files () 1593 | "List untracked git files in `default-directory'." 1594 | (interactive) 1595 | (dwim-shell-command-on-marked-files 1596 | "List untracked" 1597 | "git ls-files --others ." 1598 | :utils "git" 1599 | :focus-now t)) 1600 | 1601 | ;;;###autoload 1602 | (defun dwim-shell-commands-git-delete-untracked-files () 1603 | "Delete untracked git files in `default-directory'." 1604 | (interactive) 1605 | (when (y-or-n-p (format "Clean '%s'? \n\n%s\n...\n\n" 1606 | default-directory 1607 | (string-join 1608 | (seq-take (process-lines "git" "ls-files" "--others" ".") 3) 1609 | "\n"))) 1610 | (dwim-shell-command-on-marked-files 1611 | "Clean untracked" 1612 | "git clean -f ." 1613 | :utils "git" 1614 | :silent-success t))) 1615 | 1616 | ;;;###autoload 1617 | (defun dwim-shell-commands-external-ip () 1618 | "Copy external IP to kill ring." 1619 | (interactive) 1620 | (let ((ip (car (last (process-lines "curl" "ifconfig.me"))))) 1621 | (kill-new ip) 1622 | (message "Copied %s" ip))) 1623 | 1624 | ;;;###autoload 1625 | (defun dwim-shell-commands-macos-install-iphone-device-ipa () 1626 | "Install iPhone device .ipa. 1627 | Needs ideviceinstaller and libmobiledevice installed." 1628 | (interactive) 1629 | (dwim-shell-command-on-marked-files 1630 | "Install .ipa" 1631 | "ideviceinstaller -i '<>'" 1632 | :utils "ideviceinstaller")) 1633 | 1634 | ;;;###autoload 1635 | (defun dwim-shell-commands-copy-to-downloads () 1636 | "Copy file to ~/Downloads." 1637 | (interactive) 1638 | (dwim-shell-command-foreach 1639 | (lambda (file) 1640 | (copy-file file "~/Downloads/" 1) 1641 | (file-name-concat "~/Downloads" (file-name-nondirectory file))) 1642 | :monitor-directory "~/Downloads")) 1643 | 1644 | ;;;###autoload 1645 | (defun dwim-shell-commands-duplicate () 1646 | "Duplicate file." 1647 | (interactive) 1648 | (dwim-shell-command-on-marked-files 1649 | "Duplicate file(s)." 1650 | "cp -R '<>' '<>'" 1651 | :utils "cp")) 1652 | 1653 | ;;;###autoload 1654 | (defun dwim-shell-commands-rename-all () 1655 | "Rename all marked file(s)." 1656 | (interactive) 1657 | (dwim-shell-command-on-marked-files 1658 | "Rename all" 1659 | "mv '<>' '<>(<>).<>'" 1660 | :utils "mv")) 1661 | 1662 | ;;;###autoload 1663 | (defun dwim-shell-commands-move-to-downloads () 1664 | "Move file to ~/Downloads." 1665 | (interactive) 1666 | (dwim-shell-command-foreach 1667 | (lambda (file) 1668 | (rename-file file "~/Downloads/" 1) 1669 | (when (buffer-file-name) 1670 | (rename-buffer (file-name-nondirectory file)) 1671 | (set-visited-file-name 1672 | (file-name-concat "~/Downloads" (file-name-nondirectory file))) 1673 | (set-buffer-modified-p nil)) 1674 | (file-name-concat "~/Downloads" (file-name-nondirectory file))) 1675 | :monitor-directory "~/Downloads")) 1676 | 1677 | ;;;###autoload 1678 | (defun dwim-shell-commands-copy-to-desktop () 1679 | "Copy file to ~/Desktop." 1680 | (interactive) 1681 | (dwim-shell-command-foreach 1682 | (lambda (file) 1683 | (copy-file file "~/Desktop/" 1) 1684 | (file-name-concat "~/Desktop" (file-name-nondirectory file))) 1685 | :monitor-directory "~/Desktop")) 1686 | 1687 | ;;;###autoload 1688 | (defun dwim-shell-commands-move-to-desktop () 1689 | "Move file to ~/Desktop." 1690 | (interactive) 1691 | (dwim-shell-command-foreach 1692 | (lambda (file) 1693 | (rename-file file "~/Desktop/" 1) 1694 | (when (buffer-file-name) 1695 | (rename-buffer (file-name-nondirectory file)) 1696 | (set-visited-file-name 1697 | (file-name-concat "~/Desktop" (file-name-nondirectory file))) 1698 | (set-buffer-modified-p nil)) 1699 | (file-name-concat "~/Desktop" (file-name-nondirectory file))) 1700 | :monitor-directory "~/Desktop")) 1701 | 1702 | ;;;###autoload 1703 | (defun dwim-shell-commands-kill-gpg-agent () 1704 | "Kill (thus restart) gpg agent. 1705 | 1706 | Useful for when you get this error: 1707 | 1708 | gpg: public key decryption failed: No pinentry 1709 | gpg: decryption failed: No pinentry" 1710 | (interactive) 1711 | (dwim-shell-command-on-marked-files 1712 | "Kill gpg agent" 1713 | "gpgconf --kill gpg-agent" 1714 | :utils "gpgconf" 1715 | :silent-success t)) 1716 | 1717 | ;; Based on 1718 | ;; https://apps.bram85.nl/git/bram/gists/src/commit/31ac3363da925daafa2420b7f96c67612ca28241/gists/dwim-0x0-upload.el 1719 | ;;;###autoload 1720 | (defun dwim-shell-commands-upload-to-0x0 () 1721 | "Upload the marked files to 0x0.st" 1722 | (interactive) 1723 | (dwim-shell-command-on-marked-files 1724 | "0x0 upload" 1725 | "curl -Ffile=@<> -Fsecret= https://0x0.st" 1726 | :utils "curl" 1727 | :post-process-template 1728 | ;; Insert the single quotes at the appropriate place according to 1729 | ;; 0x0.st example online: 1730 | ;; curl -F'file=@yourfile.png' -Fsecret= https://0x0.st 1731 | ;; 1732 | ;; The placement of these single quotes confuse the escaping 1733 | ;; mechanisms of dwim-shell-command, as it considers @ as the 1734 | ;; opening 'quote' as it appears right in front of <>. 1735 | (lambda (template path) 1736 | (string-replace "-Ffile" "-F'file" 1737 | (string-replace path (concat path "'") template))) 1738 | :on-completion 1739 | (lambda (buffer process) 1740 | (if (= (process-exit-status process) 0) 1741 | (with-current-buffer buffer 1742 | (let ((url (car (last (split-string (string-trim (buffer-string)) "\n"))))) 1743 | (eww url) 1744 | (kill-new url) 1745 | (message "Copied: %s" (current-kill 0))) 1746 | (kill-buffer buffer)) 1747 | (switch-to-buffer buffer))))) 1748 | 1749 | (provide 'dwim-shell-commands) 1750 | 1751 | ;;; dwim-shell-commands.el ends here 1752 | -------------------------------------------------------------------------------- /images/apple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/apple.gif -------------------------------------------------------------------------------- /images/apple.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/apple.mov -------------------------------------------------------------------------------- /images/apple.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/apple.webp -------------------------------------------------------------------------------- /images/apple_x0.50.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/apple_x0.50.mov -------------------------------------------------------------------------------- /images/blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/blur.png -------------------------------------------------------------------------------- /images/couldnt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/couldnt.png -------------------------------------------------------------------------------- /images/diredmark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/diredmark.gif -------------------------------------------------------------------------------- /images/diredmark_x0.50.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/diredmark_x0.50.mov -------------------------------------------------------------------------------- /images/diredmark_x0.50.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/diredmark_x0.50.webp -------------------------------------------------------------------------------- /images/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/history.png -------------------------------------------------------------------------------- /images/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/progress.gif -------------------------------------------------------------------------------- /images/progress.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/progress.mov -------------------------------------------------------------------------------- /images/progress.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/progress.webp -------------------------------------------------------------------------------- /images/progress_x0.50.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/progress_x0.50.mov -------------------------------------------------------------------------------- /images/showme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/showme.png -------------------------------------------------------------------------------- /images/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/template.png -------------------------------------------------------------------------------- /images/togif.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif.mov -------------------------------------------------------------------------------- /images/togif.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif.webp -------------------------------------------------------------------------------- /images/togif_x0.50.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif_x0.50.gif -------------------------------------------------------------------------------- /images/togif_x0.50.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif_x0.50.mov -------------------------------------------------------------------------------- /images/togif_x0.50_x1.5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif_x0.50_x1.5.gif -------------------------------------------------------------------------------- /images/togif_x0.50_x2.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif_x0.50_x2.mov -------------------------------------------------------------------------------- /images/togif_x1.5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif_x1.5.gif -------------------------------------------------------------------------------- /images/togif_x1.5.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenodium/dwim-shell-command/4b077432a94873e5f505c8f569743cfd984eebb1/images/togif_x1.5.mov --------------------------------------------------------------------------------