[
  {
    "path": ".travis.yml",
    "content": "language: go\ngo:\n  - 1.x\n  - master\nenv:\n  - GO111MODULE=on\n"
  },
  {
    "path": "AUTHORS",
    "content": "Please keep the contents of this file sorted alphabetically.\n\nАлександр Крамарев <pochemuto@gmail.com>\nCalum MacRae <calum0macrae@gmail.com>\nGhislain Rodrigues <git@ghislain-rodrigues.fr>\nMateusz Czapliński <czapkofan@gmail.com>\nRohan Verma <hello@rohanverma.net>\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<img align=\"left\" width=\"183\" height=\"154\" src=\"logo.svg\" />\n\n# up - the Ultimate Plumber\n\n**up** is the **Ultimate Plumber**, a tool for writing Linux pipes in a\nterminal-based UI interactively, with instant live preview of command results.\n\nThe main **goal** of the Ultimate Plumber is to help **interactively and\nincrementally explore textual data** in Linux, by making it easier to quickly\nbuild complex pipelines, thanks to a **fast feedback loop**. This is achieved\nby boosting any typical **Linux text-processing utils** such as `grep`, `sort`,\n`cut`, `paste`, `awk`, `wc`, `perl`, etc., etc., by providing a quick,\n**interactive, scrollable preview** of their results.\n\n[![](up.gif)](https://asciinema.org/a/208538)\n\n## Usage\n\n**[Download *up* for Linux](https://github.com/akavel/up/releases/latest/download/up)**\n&nbsp; | &nbsp; [ArchLinux](https://wiki.archlinux.org/index.php/Arch_User_Repository): [`aur/up`](https://aur.archlinux.org/packages/up/)\n&nbsp; | &nbsp; FreeBSD: [`pkg install up`](https://www.freshports.org/textproc/up)\n&nbsp; | &nbsp; macOS: [`brew install up`](https://formulae.brew.sh/formula/up)\n&nbsp; | &nbsp; [Other OSes](https://github.com/akavel/up/releases)\n\nTo start using **up**, redirect any text-emitting command (or pipeline) into it\n— for example:\n\n    $ lshw |& ./up\n\nthen:\n\n- use ***PgUp/PgDn*** and ***Ctrl-[←]/Ctrl-[→]*** for basic browsing through\n  the command output;\n- in the input box at the top of the screen, start **writing any bash\n  pipeline**; then **press Enter to execute the command you typed**,\n  and the Ultimate Plumber will immediately show you the output of\n  the pipeline in the **scrollable window** below (replacing any\n  earlier contents)\n    - For example, you can try writing:\n      `grep network -A2 | grep : | cut -d: -f2- | paste - -`\n      — on my computer, after pressing *Enter*, the screen then shows\n      the pipeline and a scrollable preview of its output like below:\n\n             | grep network -A2 | grep : | cut -d: -f2- | paste - -\n             Wireless interface      Centrino Advanced-N 6235\n             Ethernet interface      RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller\n\n    - **WARNING: Please be careful when using it! It could be dangerous.**\n      In particular, writing \"rm\" or \"dd\" into it could be like running around\n      with a chainsaw. But you'd be careful writing \"rm\" anywhere in Linux\n      anyway, no?\n- when you are satisfied with the result, you can **press *Ctrl-X* to exit**\n  the Ultimate Plumber, and the command you built will be **written into\n  `up1.sh` file** in the current working directory (or, if it already existed,\n  `up2.sh`, etc., until 1000, based on [Shlemiel the Painter's\n  algorithm](https://www.joelonsoftware.com/2001/12/11/back-to-basics/)).\n  Alternatively, you can press ***Ctrl-C*** to quit without saving.\n- If the command you piped into *up* is long-running (in such case you will see\n  a tilde `~` indicator character in the top-left corner of the screen, meaning\n  that *up* is still waiting for more input), you may need to press\n  ***Ctrl-S*** to temporarily freeze *up*'s input buffer (a freeze will be\n  indicated by a `#` character in top-left corner), which will inject a fake\n  EOF into the pipeline; otherwise, some commands in the pipeline may not print\n  anything, waiting for full input (especially commands like `wc` or `sort`,\n  but `grep`, `perl`, etc. may also show incomplete results). To unfreeze back,\n  press ***Ctrl-Q***.\n\n## Additional Notes\n\n- The pipeline is passed verbatim to a `bash -c` command, so any bash-isms should work.\n- The input buffer of the Ultimate Plumber is currently fixed at **40 MB**. If\n  you reach this limit, a `+` character should get displayed in the top-left\n  corner of the screen. (This is intended to be changed to a\n  dynamically/manually growable buffer in a future version of *up*.)\n- **MacOSX support:** I don't have a Mac, thus I have no idea if it works on\n  one. You are welcome to try, and also to send PRs. If you're interested in\n  me providing some kind of official-like support for MacOSX, please consider\n  trying to find a way to send me some usable-enough Mac computer. Please note\n  I'm not trying to \"take advantage\" of you by this, as I'm actually not at all\n  interested in achieving a Mac otherwise. (Also, trying to commit to this kind\n  of support will be an extra burden and obligation on me. Knowing someone out\n  there cares enough to do a fancy physical gesture would really help alleviate\n  this.) If you're serious enough to consider this option, please contact me by\n  email (mailto:czapkofan@gmail.com) or keybase (https://keybase.io/akavel), so\n  that we could try to research possible ways to achieve this.\n  Thanks for understanding!\n- **Prior art:** I was surprised no one seemed to write a similar tool before,\n  that I could find. It should have been possible to write this since the dawn\n  of Unix already, or earlier! And indeed, after I announced *up*, I got enough\n  publicity that my attention was directed to one such earlier project already:\n  **[Pipecut](http://pipecut.org/index.html)**. Looks interesting! You may like\n  to check it too! (Thanks [@TronDD](https://lobste.rs/s/acpz00/up_tool_for_writing_linux_pipes_with#c_qxrgoa).)\n- **Other influences:** I don't remember the fact too well already, but I'm\n  rather sure that this must have been inspired in big part by The Bret Victor's Talk(s).\n\n## Future Ideas\n\n- I have quite a lot of ideas for further experimentation of development of\n  *up*, including but not limited to:\n    - [RIIR](https://rust-lang.org) (once I learn enough of Rust... at some\n      point in future... maybe...) — esp. to hopefully make *up* be a smaller\n      binary (and also to maybe finally learn some Rust); though I'm somewhat\n      afraid if it might ossify the codebase and make harder to develop\n      further..? ...but maybe actually converse?...\n    - Maybe it could be made into an UI-less, RPC/REST/socket/text-driven\n      service, like gocode or [Language Servers](https://langserver.org/), for\n      integration with editors/IDEs (emacs? vim? VSCode?...) I'd be especially\n      interested in eventually merging it into [Luna\n      Studio](https://luna-lang.org/); RIIR may help in this. (Before this, as\n      a simpler approach, multi-line editing may be needed, or at least\n      left&right scrolling of the command editor input box. Also, some kind of\n      jumping between words in the command line; readline's *Alt-b* & *Alt-f*?)\n    - Make it possible to [capture output of already running\n      processes](https://stackoverflow.com/a/19584979/98528)! (But maybe that\n      could be better made as a separate, composable tool! In Rust?)\n    - Adding tests... (ahem; see also\n      [#1](https://github.com/akavel/up/issues/1)) ...also write `--help`...\n    - Making it work on Windows,\n      somehow[?](https://github.com/mattn/go-shellwords) Also, obviously, would\n      be nice to have some CI infrastructure enabling porting it to MacOSX,\n      BSDs, etc., etc...\n    - Integration with [fzf](https://github.com/junegunn/fzf) and other TUI\n      tools? I only have some vague thoughts and ideas about it as of now, not\n      even sure how this could look like.\n    - Adding more previews, for each `|` in the pipeline; also forking of\n      pipelines, merging, feedback loops, and other mixing and matching (though\n      I'd strongly prefer if [Luna](https://luna-lang.org) was to do it\n      eventually).\n- If you are interested in financing my R&D work, contact me by email at:\n  czapkofan@gmail.com, or [on keybase.io as akavel](https://keybase.io/akavel).\n  I suppose I will probably be developing the Ultimate Plumber further anyway,\n  but at this time it's purely a hobby project, with all the fun and risks this\n  entails.\n\n— *Mateusz Czapliński*  \n*October 2018*\n\n*PS. The UP logo was conceived and generously sponsored\nby [Thoai Nguyen](https://github.com/thoaionline)\nand [GPU Exchange](https://gpu.exchange/),\nwith a helping hand from [Many Pixels](https://www.manypixels.co/).*\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/akavel/up\n\ngo 1.14\n\nrequire (\n\tgithub.com/gdamore/tcell v1.4.0\n\tgithub.com/mattn/go-isatty v0.0.3\n\tgithub.com/mattn/go-runewidth v0.0.9 // indirect\n\tgithub.com/spf13/pflag v1.0.3\n\tgolang.org/x/sys v0.0.0-20201029080932-201ba4db2418 // indirect\n\tgolang.org/x/text v0.3.4 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=\ngithub.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=\ngithub.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=\ngithub.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=\ngithub.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=\ngithub.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=\ngithub.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=\ngithub.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=\ngithub.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngolang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=\ngolang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM=\ngolang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\n"
  },
  {
    "path": "up.go",
    "content": "// Copyright 2018 The up AUTHORS\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\n// up is the Ultimate Plumber, a tool for writing Linux pipes in a\n// terminal-based UI interactively, with instant live preview of command\n// results.\npackage main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"sync\"\n\t\"unicode\"\n\n\t\"github.com/gdamore/tcell\"\n\t\"github.com/gdamore/tcell/terminfo\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/spf13/pflag\"\n)\n\nconst version = \"0.4 (2020-10-29)\"\n\n// TODO: in case of error, show it in red (bg?), then below show again initial normal output (see also #4)\n// TODO: F1 should display help, and it should be multi-line, and scrolling licensing credits\n// TODO: some key shortcut to increase stdin capture buffer size (unless EOF already reached)\n// TODO: show status infos:\n//  - red fg + \"up: process returned with error code %d\" -- when subprocess returned an error\n//  - yellow fg -- when process is still not finished\n// TODO: on github: add issues, incl. up-for-grabs / help-wanted\n// TODO: [LATER] make it work on Windows; maybe with mattn/go-shellwords ?\n// TODO: [LATER] Ctrl-O shows input via `less` or $PAGER\n// TODO: properly show all licenses of dependencies on --version\n// TODO: [LATER] on ^X (?), leave TUI and run the command through buffered input, then unpause rest of input\n// TODO: [LATER] allow adding more elements of pipeline (initially, just writing `foo | bar` should work)\n// TODO: [LATER] allow invocation with partial command, like: `up grep -i` (see also #11)\n// TODO: [LATER][MAYBE] allow reading upN.sh scripts (see also #11)\n// TODO: [MUCH LATER] readline-like rich editing support? and completion? (see also #28)\n// TODO: [MUCH LATER] integration with fzf? and pindexis/marker?\n// TODO: [LATER] forking and unforking pipelines (see also #4)\n// TODO: [LATER] capture output of a running process (see: https://stackoverflow.com/q/19584825/98528)\n// TODO: [LATER] richer TUI:\n// - show # of read lines & kbytes\n// - show status (errorlevel) of process, or that it's still running (also with background colors)\n// - allow copying and pasting to/from command line\n// TODO: [LATER] allow connecting external editor (become server/engine via e.g. socket)\n// TODO: [LATER] become pluggable into http://luna-lang.org\n// TODO: [LATER][MAYBE] allow \"plugins\" (\"combos\" - commands with default options) e.g. for Lua `lua -e`+auto-quote, etc.\n// TODO: [LATER] make it more friendly to infrequent Linux users by providing \"descriptive\" commands like \"search\" etc.\n// TODO: [LATER] advertise on some reddits for data exploration / data science\n// TODO: [LATER] undo/redo - history of commands (see also #4)\n// TODO: [LATER] jump between buffers saved from earlier pipe fragments; OR: allow saving/recalling \"snapshots\" of (cmd, results) pairs (see also #4)\n// TODO: [LATER] ^-, U -- to switch to \"unsafe mode\"? -u to switch back? + some visual marker\n\nfunc init() {\n\tpflag.Usage = func() {\n\t\tfmt.Fprint(os.Stderr, `Usage: COMMAND | up [OPTIONS]\n\nup is the Ultimate Plumber, a tool for writing Linux pipes in a terminal-based\nUI interactively, with instant live preview of command results.\n\nTo start using up, redirect any text-emitting command (or pipeline) into it -\nfor example:\n\n    $ lshw |& ./up\n\nUltimate Plumber then opens a full-screen terminal app. The top line of the\nscreen can be edited in order to interactively build a pipeline. Every time you\nhit [Enter], the bottom of the screen will display the results of passing the\nup's standard input through the pipeline (executed using your default $SHELL).\n\nIf a tilde '~' is visible in top-left corner, it indicates that Ultimate\nPlumber did not yet fully consume its input. Some pipelines may not finish with\nincomplete input; use Ctrl-S to freeze reading the input and to inject fake\nEOF; use Ctrl-Q to unfreeze back and continue reading.\n\nIf a plus '+' is visible in top-left corner, the internal buffer limit\n(default: 40MB) was reached and Ultimate Plumber won't read more input.\n\nKEYS\n\n- alphanumeric & symbol keys, Left, Right, Ctrl-A/E/B/F/K/Y/W\n                      - navigate and edit the pipeline command\n- Enter   - execute the pipeline command, updating the pipeline output panel\n- Up, Dn, PgUp, PgDn, Ctrl-Left, Ctrl-Right\n                      - navigate (scroll) the pipeline output panel\n- Ctrl-X  - exit and write the pipeline to up1.sh (or if it exists then to\n            up2.sh, etc. till up1000.sh)\n- Ctrl-C  - quit without saving and emit the pipeline on standard output\n- Ctrl-S  - temporarily freeze a long-running input to Ultimate Plumber,\n            injecting a fake EOF into the buffer (shows '#' indicator in\n            top-left corner)\n- Ctrl-Q  - unfreeze back after Ctrl-S (disables '#' indicator)\n\nOPTIONS\n`)\n\t\tpflag.PrintDefaults()\n\t\tfmt.Fprint(os.Stderr, `\nHOMEPAGE: https://github.com/akavel/up\nVERSION: `+version+`\n`)\n\t}\n\tpflag.ErrHelp = errors.New(\"\") // TODO: or something else?\n}\n\nvar (\n\t// TODO: dangerous? immediate? raw? unsafe? ...\n\t// FIXME(akavel): mark the unsafe mode vs. safe mode with some colour or status; also inform/mark what command's results are displayed...\n\tunsafeMode   = pflag.Bool(\"unsafe-full-throttle\", false, \"enable mode in which pipeline is executed immediately after any change (without pressing Enter)\")\n\toutputScript = pflag.StringP(\"output-script\", \"o\", \"\", \"save the command to specified `file` if Ctrl-X is pressed (default: up<N>.sh)\")\n\tdebugMode    = pflag.Bool(\"debug\", false, \"debug mode\")\n\tnoColors     = pflag.Bool(\"no-colors\", false, \"disable interface colors\")\n\tshellFlag    = pflag.StringArrayP(\"exec\", \"e\", nil, \"`command` to run pipeline with; repeat multiple times to pass multi-word command; defaults to '-e=$SHELL -e=-c'\")\n\tinitialCmd   = pflag.StringP(\"pipeline\", \"c\", \"\", \"initial `commands` to use as pipeline (default empty)\")\n\tbufsize      = pflag.Int(\"buf\", 40, \"input buffer size & pipeline buffer sizes in `megabytes` (MiB)\")\n\tnoinput      = pflag.Bool(\"noinput\", false, \"start with empty buffer regardless if any input was provided\")\n)\n\nfunc main() {\n\t// Handle command-line flags\n\tpflag.Parse()\n\n\tlog.SetOutput(ioutil.Discard)\n\tif *debugMode {\n\t\tdebug, err := os.Create(\"up.debug\")\n\t\tif err != nil {\n\t\t\tdie(err.Error())\n\t\t}\n\t\tlog.SetOutput(debug)\n\t}\n\n\t// Find out what is the user's preferred login shell. This also allows user\n\t// to choose the \"engine\" used for command execution.\n\tshell := *shellFlag\n\tif len(shell) == 0 {\n\t\tlog.Println(\"checking $SHELL...\")\n\t\tsh := os.Getenv(\"SHELL\")\n\t\tif sh != \"\" {\n\t\t\tgoto shell_found\n\t\t}\n\t\tlog.Println(\"checking bash...\")\n\t\tsh, _ = exec.LookPath(\"bash\")\n\t\tif sh != \"\" {\n\t\t\tgoto shell_found\n\t\t}\n\t\tlog.Println(\"checking sh...\")\n\t\tsh, _ = exec.LookPath(\"sh\")\n\t\tif sh != \"\" {\n\t\t\tgoto shell_found\n\t\t}\n\t\tdie(\"cannot find shell: no -e flag, $SHELL is empty, neither bash nor sh are in $PATH\")\n\tshell_found:\n\t\tshell = []string{sh, \"-c\"}\n\t}\n\tlog.Println(\"found shell:\", shell)\n\n\tstdin := io.Reader(os.Stdin)\n\tif *noinput {\n\t\tstdin = bytes.NewReader(nil)\n\t} else if isatty.IsTerminal(os.Stdin.Fd()) {\n\t\t// TODO: Without this block, we'd hang when nothing is piped on input (see\n\t\t// github.com/peco/peco, mattn/gof, fzf, etc.)\n\t\tdie(\"up requires some data piped on standard input, for example try: `echo hello world | up`\")\n\t}\n\n\t// Initialize TUI infrastructure\n\ttui := initTUI()\n\tdefer tui.Fini()\n\n\t// Initialize 3 main UI parts\n\tvar (\n\t\t// The top line of the TUI is an editable command, which will be used\n\t\t// as a pipeline for data we read from stdin\n\t\tcommandEditor = NewEditor(\"| \", *initialCmd)\n\t\t// The rest of the screen is a view of the results of the command\n\t\tcommandOutput = BufView{}\n\t\t// Sometimes, a message may be displayed at the bottom of the screen, with help or other info\n\t\tmessage = `Enter runs  ^X exit (^C nosave)  PgUp/PgDn/Up/Dn/^</^> scroll  ^S pause (^Q end)  [Ultimate Plumber v` + version + ` by akavel et al.]`\n\t)\n\n\t// Initialize main data flow\n\tvar (\n\t\t// We capture data piped to 'up' on standard input into an internal buffer\n\t\t// When some new data shows up on stdin, we raise a custom signal,\n\t\t// so that main loop will refresh the buffers and the output.\n\t\tstdinCapture = NewBuf(*bufsize*1024*1024).\n\t\t\t\tStartCapturing(stdin, func() { triggerRefresh(tui) })\n\t\t// Then, we pass this data as input to a subprocess.\n\t\t// Initially, no subprocess is running, as no command is entered yet\n\t\tcommandSubprocess *Subprocess = nil\n\t)\n\t// Intially, for user's convenience, show the raw input data, as if `cat` command was typed\n\tcommandOutput.Buf = stdinCapture\n\n\t// Main loop\n\tlastCommand := \"\"\n\trestart := false\n\tfor {\n\t\t// If user edited the command, immediately run it in background, and\n\t\t// kill the previously running command.\n\t\tcommand := commandEditor.String()\n\t\tif restart || (*unsafeMode && command != lastCommand) {\n\t\t\tcommandSubprocess.Kill()\n\t\t\tif command != \"\" {\n\t\t\t\tcommandSubprocess = StartSubprocess(shell, command, stdinCapture, func() { triggerRefresh(tui) })\n\t\t\t\tcommandOutput.Buf = commandSubprocess.Buf\n\t\t\t} else {\n\t\t\t\t// If command is empty, show original input data again (~ equivalent of typing `cat`)\n\t\t\t\tcommandSubprocess = nil\n\t\t\t\tcommandOutput.Buf = stdinCapture\n\t\t\t}\n\t\t\trestart = false\n\t\t\tlastCommand = command\n\t\t}\n\n\t\t// Draw UI\n\t\tw, h := tui.Size()\n\t\tstyle := whiteOnBlue\n\t\tif command == lastCommand {\n\t\t\tstyle = whiteOnDBlue\n\t\t}\n\t\tstdinCapture.DrawStatus(TuiRegion(tui, 0, 0, 1, 1), style)\n\t\tcommandEditor.DrawTo(TuiRegion(tui, 1, 0, w-1, 1), style,\n\t\t\tfunc(x, y int) { tui.ShowCursor(x+1, 0) })\n\t\tcommandOutput.DrawTo(TuiRegion(tui, 0, 1, w, h-1))\n\t\tdrawText(TuiRegion(tui, 0, h-1, w, 1), whiteOnBlue, message)\n\t\ttui.Show()\n\n\t\t// Handle UI events\n\t\tswitch ev := tui.PollEvent().(type) {\n\t\t// Key pressed\n\t\tcase *tcell.EventKey:\n\t\t\t// Is it a command editor key?\n\t\t\tif commandEditor.HandleKey(ev) {\n\t\t\t\tmessage = \"\"\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Is it a command output view key?\n\t\t\tif commandOutput.HandleKey(ev, h-1) {\n\t\t\t\tmessage = \"\"\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Some other global key combinations\n\t\t\tswitch getKey(ev) {\n\t\t\tcase key(tcell.KeyEnter):\n\t\t\t\trestart = true\n\t\t\tcase key(tcell.KeyCtrlUnderscore),\n\t\t\t\tctrlKey(tcell.KeyCtrlUnderscore):\n\t\t\t\t// TODO: ask for another character to trigger command-line option, like in `less`\n\n\t\t\tcase key(tcell.KeyCtrlS),\n\t\t\t\tctrlKey(tcell.KeyCtrlS):\n\t\t\t\tstdinCapture.Pause(true)\n\t\t\t\ttriggerRefresh(tui)\n\t\t\tcase key(tcell.KeyCtrlQ),\n\t\t\t\tctrlKey(tcell.KeyCtrlQ):\n\t\t\t\tstdinCapture.Pause(false)\n\t\t\t\trestart = true\n\t\t\tcase key(tcell.KeyCtrlC),\n\t\t\t\tctrlKey(tcell.KeyCtrlC),\n\t\t\t\tkey(tcell.KeyCtrlD),\n\t\t\t\tctrlKey(tcell.KeyCtrlD):\n\t\t\t\t// Quit\n\t\t\t\ttui.Fini()\n\t\t\t\tos.Stderr.WriteString(\"up: Ultimate Plumber v\" + version + \" https://github.com/akavel/up\\n\")\n\t\t\t\tos.Stderr.WriteString(\"up: | \" + commandEditor.String() + \"\\n\")\n\t\t\t\treturn\n\t\t\tcase key(tcell.KeyCtrlX),\n\t\t\t\tctrlKey(tcell.KeyCtrlX):\n\t\t\t\t// Write script 'upN.sh' and quit\n\t\t\t\ttui.Fini()\n\t\t\t\twriteScript(shell, commandEditor.String(), tui)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc initTUI() tcell.Screen {\n\t// TODO: maybe try gocui or termbox?\n\ttui, err := tcell.NewScreen()\n\tif err == terminfo.ErrTermNotFound {\n\t\tterm := os.Getenv(\"TERM\")\n\t\thash := sha1.Sum([]byte(term))\n\t\t// TODO: add a flag which would attempt to perform the download automatically if explicitly requested by user\n\t\tdie(fmt.Sprintf(`%[1]s\nYour terminal code:\n\tTERM=%[2]s\nwas not found in the database provided by tcell library. Please try checking if\na supplemental database is found for your terminal at one of the following URLs:\n\thttps://github.com/gdamore/tcell/raw/master/terminfo/database/%.1[3]x/%.4[3]x\n\thttps://github.com/gdamore/tcell/raw/master/terminfo/database/%.1[3]x/%.4[3]x.gz\nIf yes, download it and save in the following directory:\n\t$HOME/.tcelldb/%.1[3]x/\nthen try running \"up\" again. If that does not work for you, please first consult:\n\thttps://github.com/akavel/up/issues/15\nand if you don't see your terminal code mentioned there, please try asking on:\n\thttps://github.com/gdamore/tcell/issues\nOr, you might try changing TERM temporarily to some other value, for example by\nrunning \"up\" with:\n\tTERM=xterm up\nGood luck!`,\n\t\t\terr, term, hash))\n\t}\n\tif err != nil {\n\t\tdie(err.Error())\n\t}\n\terr = tui.Init()\n\tif err != nil {\n\t\tdie(err.Error())\n\t}\n\treturn tui\n}\n\nfunc triggerRefresh(tui tcell.Screen) {\n\ttui.PostEvent(tcell.NewEventInterrupt(nil))\n}\n\nfunc die(message string) {\n\tos.Stderr.WriteString(\"error: \" + message + \"\\n\")\n\tos.Exit(1)\n}\n\nfunc NewEditor(prompt, value string) *Editor {\n\tv := []rune(value)\n\treturn &Editor{\n\t\tprompt: []rune(prompt),\n\t\tvalue:  v,\n\t\tcursor: len(v),\n\t\tlastw:  len(v),\n\t}\n}\n\ntype Editor struct {\n\t// TODO: make editor multiline. Reuse gocui or something for this?\n\tprompt    []rune\n\tvalue     []rune\n\tkillspace []rune\n\tcursor    int\n\t// lastw is length of value on last Draw; we need it to know how much to erase after backspace\n\tlastw int\n}\n\nfunc (e *Editor) String() string { return string(e.value) }\n\nfunc (e *Editor) DrawTo(region Region, style tcell.Style, setcursor func(x, y int)) {\n\t// Draw prompt & the edited value - use white letters on blue background\n\tfor i, ch := range e.prompt {\n\t\tregion.SetCell(i, 0, style, ch)\n\t}\n\tfor i, ch := range e.value {\n\t\tregion.SetCell(len(e.prompt)+i, 0, style, ch)\n\t}\n\n\t// Clear remains of last value if needed\n\tfor i := len(e.value); i < e.lastw; i++ {\n\t\tregion.SetCell(len(e.prompt)+i, 0, tcell.StyleDefault, ' ')\n\t}\n\te.lastw = len(e.value)\n\n\t// Show cursor if requested\n\tif setcursor != nil {\n\t\tsetcursor(len(e.prompt)+e.cursor, 0)\n\t}\n}\n\nfunc (e *Editor) HandleKey(ev *tcell.EventKey) bool {\n\t// If a character is entered, with no modifiers except maybe shift, then just insert it\n\tif ev.Key() == tcell.KeyRune && ev.Modifiers()&(^tcell.ModShift) == 0 {\n\t\te.insert(ev.Rune())\n\t\treturn true\n\t}\n\t// Handle editing & movement keys\n\tswitch getKey(ev) {\n\tcase key(tcell.KeyBackspace), key(tcell.KeyBackspace2):\n\t\t// See https://github.com/nsf/termbox-go/issues/145\n\t\te.delete(-1)\n\tcase key(tcell.KeyDelete):\n\t\te.delete(0)\n\tcase key(tcell.KeyLeft),\n\t\tkey(tcell.KeyCtrlB),\n\t\tctrlKey(tcell.KeyCtrlB):\n\t\tif e.cursor > 0 {\n\t\t\te.cursor--\n\t\t}\n\tcase key(tcell.KeyRight),\n\t\tkey(tcell.KeyCtrlF),\n\t\tctrlKey(tcell.KeyCtrlF):\n\t\tif e.cursor < len(e.value) {\n\t\t\te.cursor++\n\t\t}\n\tcase key(tcell.KeyCtrlA),\n\t\tctrlKey(tcell.KeyCtrlA):\n\t\te.cursor = 0\n\tcase key(tcell.KeyCtrlE),\n\t\tctrlKey(tcell.KeyCtrlE):\n\t\te.cursor = len(e.value)\n\tcase key(tcell.KeyCtrlK),\n\t\tctrlKey(tcell.KeyCtrlK):\n\t\te.kill()\n\tcase key(tcell.KeyCtrlY),\n\t\tctrlKey(tcell.KeyCtrlY):\n\t\te.insert(e.killspace...)\n\tcase key(tcell.KeyCtrlW),\n\t\tctrlKey(tcell.KeyCtrlW):\n\t\te.unixWordRubout()\n\tdefault:\n\t\t// Unknown key/combination, not handled\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (e *Editor) insert(ch ...rune) {\n\t// Based on https://github.com/golang/go/wiki/SliceTricks#insert\n\te.value = append(e.value, ch...)                     // = PREFIX + SUFFIX + (filler)\n\tcopy(e.value[e.cursor+len(ch):], e.value[e.cursor:]) // = PREFIX + (filler) + SUFFIX\n\tcopy(e.value[e.cursor:], ch)                         // = PREFIX + ch + SUFFIX\n\te.cursor += len(ch)\n}\n\nfunc (e *Editor) delete(dx int) {\n\tpos := e.cursor + dx\n\tif pos < 0 || pos >= len(e.value) {\n\t\treturn\n\t}\n\te.value = append(e.value[:pos], e.value[pos+1:]...)\n\te.cursor = pos\n}\n\nfunc (e *Editor) kill() {\n\tif e.cursor != len(e.value) {\n\t\te.killspace = append(e.killspace[:0], e.value[e.cursor:]...)\n\t}\n\te.value = e.value[:e.cursor]\n}\n\n// unixWordRubout removes the part of the word on the left of the cursor. A word is\n// delimited by whitespaces.\n// The term `unix-word-rubout` comes from `readline` (see `man 3 readline`)\nfunc (e *Editor) unixWordRubout() {\n\tif e.cursor <= 0 {\n\t\treturn\n\t}\n\tpos := e.cursor - 1\n\tfor pos != 0 && (unicode.IsSpace(e.value[pos]) || !unicode.IsSpace(e.value[pos-1])) {\n\t\tpos--\n\t}\n\te.killspace = append(e.killspace[:0], e.value[pos:e.cursor]...)\n\te.value = append(e.value[:pos], e.value[e.cursor:]...)\n\te.cursor = pos\n}\n\ntype BufView struct {\n\t// TODO: Wrap bool\n\tY   int // Y of the view in the Buf, for down/up scrolling\n\tX   int // X of the view in the Buf, for left/right scrolling\n\tBuf *Buf\n}\n\nfunc (v *BufView) DrawTo(region Region) {\n\tr := bufio.NewReader(v.Buf.NewReader(false))\n\n\t// PgDn/PgUp etc. support\n\tfor y := v.Y; y > 0; y-- {\n\t\tline, err := r.ReadBytes('\\n')\n\t\tswitch err {\n\t\tcase nil:\n\t\t\t// skip line\n\t\t\tcontinue\n\t\tcase io.EOF:\n\t\t\tr = bufio.NewReader(bytes.NewReader(line))\n\t\t\ty = 0\n\t\t\tbreak\n\t\tdefault:\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tlclip := false\n\tdrawch := func(x, y int, ch rune) {\n\t\tif x <= v.X && v.X != 0 {\n\t\t\tx, ch = 0, '«'\n\t\t\tlclip = true\n\t\t} else {\n\t\t\tx -= v.X\n\t\t}\n\t\tif x >= region.W {\n\t\t\tx, ch = region.W-1, '»'\n\t\t}\n\t\tregion.SetCell(x, y, tcell.StyleDefault, ch)\n\t}\n\tendline := func(x, y int) {\n\t\tx -= v.X\n\t\tif x < 0 {\n\t\t\tx = 0\n\t\t}\n\t\tif x == 0 && lclip {\n\t\t\tx++\n\t\t}\n\t\tlclip = false\n\t\tfor ; x < region.W; x++ {\n\t\t\tregion.SetCell(x, y, tcell.StyleDefault, ' ')\n\t\t}\n\t}\n\n\tx, y := 0, 0\n\t// TODO: handle runes properly, including their visual width (mattn/go-runewidth)\n\tfor {\n\t\tch, _, err := r.ReadRune()\n\t\tif y >= region.H || err == io.EOF {\n\t\t\tbreak\n\t\t} else if err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tswitch ch {\n\t\tcase '\\n':\n\t\t\tendline(x, y)\n\t\t\tx, y = 0, y+1\n\t\t\tcontinue\n\t\tcase '\\t':\n\t\t\tconst tabwidth = 8\n\t\t\tdrawch(x, y, ' ')\n\t\t\tfor x%tabwidth < (tabwidth - 1) {\n\t\t\t\tx++\n\t\t\t\tif x >= region.W {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdrawch(x, y, ' ')\n\t\t\t}\n\t\tdefault:\n\t\t\tdrawch(x, y, ch)\n\t\t}\n\t\tx++\n\t}\n\tfor ; y < region.H; y++ {\n\t\tendline(x, y)\n\t\tx = 0\n\t}\n}\n\nfunc (v *BufView) HandleKey(ev *tcell.EventKey, scrollY int) bool {\n\tconst scrollX = 8 // When user scrolls horizontally, move by this many characters\n\tswitch getKey(ev) {\n\t//\n\t// Vertical scrolling\n\t//\n\tcase key(tcell.KeyUp):\n\t\tv.Y--\n\t\tv.normalizeY()\n\tcase key(tcell.KeyDown):\n\t\tv.Y++\n\t\tv.normalizeY()\n\tcase key(tcell.KeyPgDn):\n\t\t// TODO: in top-right corner of Buf area, draw current line number & total # of lines\n\t\tv.Y += scrollY\n\t\tv.normalizeY()\n\tcase key(tcell.KeyPgUp):\n\t\tv.Y -= scrollY\n\t\tv.normalizeY()\n\t//\n\t// Horizontal scrolling\n\t//\n\tcase altKey(tcell.KeyLeft),\n\t\tctrlKey(tcell.KeyLeft):\n\t\tv.X -= scrollX\n\t\tif v.X < 0 {\n\t\t\tv.X = 0\n\t\t}\n\tcase altKey(tcell.KeyRight),\n\t\tctrlKey(tcell.KeyRight):\n\t\tv.X += scrollX\n\tcase altKey(tcell.KeyHome),\n\t\tctrlKey(tcell.KeyHome):\n\t\tv.X = 0\n\tdefault:\n\t\t// Unknown key/combination, not handled\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (v *BufView) normalizeY() {\n\tnlines := count(v.Buf.NewReader(false), '\\n') + 1\n\tif v.Y >= nlines {\n\t\tv.Y = nlines - 1\n\t}\n\tif v.Y < 0 {\n\t\tv.Y = 0\n\t}\n}\n\nfunc count(r io.Reader, b byte) (n int) {\n\tbuf := [256]byte{}\n\tfor {\n\t\ti, err := r.Read(buf[:])\n\t\tn += bytes.Count(buf[:i], []byte{b})\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc NewBuf(bufsize int) *Buf {\n\t// TODO: make buffer size dynamic (growable by pressing a key)\n\tbuf := &Buf{bytes: make([]byte, bufsize)}\n\tbuf.cond = sync.NewCond(&buf.mu)\n\treturn buf\n}\n\ntype Buf struct {\n\tbytes []byte\n\n\tmu     sync.Mutex // guards the following fields\n\tcond   *sync.Cond\n\tstatus bufStatus\n\tn      int\n}\n\ntype bufStatus int\n\nconst (\n\tbufReading bufStatus = iota\n\tbufEOF\n\tbufPaused\n)\n\nfunc (b *Buf) StartCapturing(r io.Reader, notify func()) *Buf {\n\tgo b.capture(r, notify)\n\treturn b\n}\n\nfunc (b *Buf) capture(r io.Reader, notify func()) {\n\t// TODO: allow stopping - take context?\n\tfor {\n\t\tn, err := r.Read(b.bytes[b.n:])\n\n\t\tb.mu.Lock()\n\t\tfor b.status == bufPaused {\n\t\t\tb.cond.Wait()\n\t\t}\n\t\tb.n += n\n\t\tif err == io.EOF {\n\t\t\tb.status = bufEOF\n\t\t}\n\t\tif b.n == len(b.bytes) {\n\t\t\t// TODO: remove this when we can grow the buffer\n\t\t\terr = io.EOF\n\t\t}\n\t\tb.cond.Broadcast()\n\t\tb.mu.Unlock()\n\n\t\tgo notify()\n\t\tif err == io.EOF {\n\t\t\tlog.Printf(\"capture EOF after: %q\", b.bytes[:b.n]) // TODO: make sure no race here, and skipped if not debugging\n\t\t\treturn\n\t\t} else if err != nil {\n\t\t\t// TODO: better handling of errors\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc (b *Buf) Pause(pause bool) {\n\tb.mu.Lock()\n\tif pause {\n\t\tif b.status == bufReading {\n\t\t\tb.status = bufPaused\n\t\t\t// trigger all readers to emit fake EOF\n\t\t\tb.cond.Broadcast()\n\t\t}\n\t} else {\n\t\tif b.status == bufPaused {\n\t\t\tb.status = bufReading\n\t\t\t// wake up the capture func\n\t\t\tb.cond.Broadcast()\n\t\t}\n\t}\n\tb.mu.Unlock()\n}\n\nfunc (b *Buf) DrawStatus(region Region, style tcell.Style) {\n\tstatus := '~' // default: still reading input\n\n\tb.mu.Lock()\n\tswitch {\n\tcase b.status == bufPaused:\n\t\tstatus = '#'\n\tcase b.status == bufEOF:\n\t\tstatus = ' ' // all input read, nothing more to do\n\tcase b.n == len(b.bytes):\n\t\tstatus = '+' // buffer full\n\t}\n\tb.mu.Unlock()\n\n\tregion.SetCell(0, 0, style, status)\n}\n\nfunc (b *Buf) NewReader(blocking bool) io.Reader {\n\ti := 0\n\treturn funcReader(func(p []byte) (n int, err error) {\n\t\tb.mu.Lock()\n\t\tend := b.n\n\t\tfor blocking && end == i && b.status == bufReading && end < len(b.bytes) {\n\t\t\tb.cond.Wait()\n\t\t\tend = b.n\n\t\t}\n\t\tb.mu.Unlock()\n\n\t\tn = copy(p, b.bytes[i:end])\n\t\ti += n\n\t\tif n > 0 {\n\t\t\treturn n, nil\n\t\t} else {\n\t\t\tif blocking {\n\t\t\t\tlog.Printf(\"blocking reader emitting EOF after: %q\", b.bytes[:end])\n\t\t\t}\n\t\t\treturn 0, io.EOF\n\t\t}\n\t})\n}\n\ntype funcReader func([]byte) (int, error)\n\nfunc (f funcReader) Read(p []byte) (int, error) { return f(p) }\n\ntype Subprocess struct {\n\tBuf    *Buf\n\tcancel context.CancelFunc\n}\n\nfunc StartSubprocess(shell []string, command string, stdin *Buf, notify func()) *Subprocess {\n\tctx, cancel := context.WithCancel(context.TODO())\n\tr, w := io.Pipe()\n\tp := &Subprocess{\n\t\tBuf:    NewBuf(len(stdin.bytes)).StartCapturing(r, notify),\n\t\tcancel: cancel,\n\t}\n\n\tcmd := exec.CommandContext(ctx, shell[0], append(shell[1:], command)...)\n\tcmd.Stdout = w\n\tcmd.Stderr = w\n\tcmd.Stdin = stdin.NewReader(true)\n\terr := cmd.Start()\n\tif err != nil {\n\t\tfmt.Fprintf(w, \"up: %s\", err)\n\t\tw.Close()\n\t\treturn p\n\t}\n\tlog.Println(cmd.Path)\n\tgo func() {\n\t\terr = cmd.Wait()\n\t\tif err != nil {\n\t\t\tfmt.Fprintf(w, \"up: %s\", err)\n\t\t\tlog.Printf(\"Wait returned error: %s\", err)\n\t\t}\n\t\tw.Close()\n\t}()\n\treturn p\n}\n\nfunc (s *Subprocess) Kill() {\n\tif s == nil {\n\t\treturn\n\t}\n\ts.cancel()\n}\n\ntype key int32\n\nfunc getKey(ev *tcell.EventKey) key { return key(ev.Modifiers())<<16 + key(ev.Key()) }\nfunc altKey(base tcell.Key) key     { return key(tcell.ModAlt)<<16 + key(base) }\nfunc ctrlKey(base tcell.Key) key    { return key(tcell.ModCtrl)<<16 + key(base) }\n\nfunc writeScript(shell []string, command string, tui tcell.Screen) {\n\tos.Stderr.WriteString(\"up: Ultimate Plumber v\" + version + \" https://github.com/akavel/up\\n\")\n\tvar f *os.File\n\tvar err error\n\tif *outputScript != \"\" {\n\t\tos.Stderr.WriteString(\"up: writing \" + *outputScript)\n\t\tf, err = os.OpenFile(*outputScript, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)\n\t\tif err != nil {\n\t\t\tgoto fallback_tmp\n\t\t}\n\t\tgoto try_file\n\t}\n\n\tos.Stderr.WriteString(\"up: writing: .\")\n\tfor i := 1; i < 1000; i++ {\n\t\tf, err = os.OpenFile(fmt.Sprintf(\"up%d.sh\", i), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0755)\n\t\tswitch {\n\t\tcase os.IsExist(err):\n\t\t\tcontinue\n\t\tcase err != nil:\n\t\t\tgoto fallback_tmp\n\t\tdefault:\n\t\t\tos.Stderr.WriteString(\"/\" + f.Name())\n\t\t\tgoto try_file\n\t\t}\n\t}\n\tos.Stderr.WriteString(\" - error: up1.sh-up999.sh already exist\\n\")\n\tgoto fallback_tmp\n\ntry_file:\n\t// NOTE: currently not supporting multi-word shell in upNNN.sh unfortunately :(\n\t_, err = fmt.Fprintf(f, \"#!%s\\n%s\\n\", shell[0], command)\n\tif err != nil {\n\t\tgoto fallback_tmp\n\t}\n\terr = f.Close()\n\tif err != nil {\n\t\tgoto fallback_tmp\n\t}\n\tos.Stderr.WriteString(\" - OK\\n\")\n\treturn\n\nfallback_tmp:\n\t// TODO: test if the fallbacks etc. protections actually work\n\tos.Stderr.WriteString(\" - error: \" + err.Error() + \"\\n\")\n\tf, err = ioutil.TempFile(\"\", \"up-*.sh\")\n\tif err != nil {\n\t\tgoto fallback_print\n\t}\n\t_, err = fmt.Fprintf(f, \"#!%s\\n%s\\n\", shell, command)\n\tif err != nil {\n\t\tgoto fallback_print\n\t}\n\terr = f.Close()\n\tif err != nil {\n\t\tgoto fallback_print\n\t}\n\tos.Stderr.WriteString(\"up: writing: \" + f.Name() + \" - OK\\n\")\n\tos.Chmod(f.Name(), 0755)\n\treturn\n\nfallback_print:\n\tfname := \"TMP\"\n\tif f != nil {\n\t\tfname = f.Name()\n\t}\n\tos.Stderr.WriteString(\"up: writing: \" + fname + \" - error: \" + err.Error() + \"\\n\")\n\tos.Stderr.WriteString(\"up: | \" + command + \"\\n\")\n}\n\ntype Region struct {\n\tW, H    int\n\tSetCell func(x, y int, style tcell.Style, ch rune)\n}\n\nfunc TuiRegion(tui tcell.Screen, x, y, w, h int) Region {\n\treturn Region{\n\t\tW: w, H: h,\n\t\tSetCell: func(dx, dy int, style tcell.Style, ch rune) {\n\t\t\tif dx >= 0 && dx < w && dy >= 0 && dy < h {\n\t\t\t\tif *noColors {\n\t\t\t\t\tstyle = tcell.StyleDefault\n\t\t\t\t}\n\t\t\t\ttui.SetCell(x+dx, y+dy, style, ch)\n\t\t\t}\n\t\t},\n\t}\n}\n\nvar (\n\twhiteOnBlue  = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlue)\n\twhiteOnDBlue = tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorNavy)\n)\n\nfunc drawText(region Region, style tcell.Style, text string) {\n\tfor x, ch := range text {\n\t\tregion.SetCell(x, 0, style, ch)\n\t}\n}\n"
  },
  {
    "path": "up_test.go",
    "content": "package main\n\nimport \"testing\"\n\nfunc Test_Editor_insert(t *testing.T) {\n\ttests := []struct {\n\t\tcomment   string\n\t\te         Editor\n\t\tinsert    []rune\n\t\twantValue []rune\n\t}{\n\t\t{\n\t\t\tcomment: \"prepend ASCII char\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 0,\n\t\t\t},\n\t\t\tinsert:    []rune{'X'},\n\t\t\twantValue: []rune(`Xabc`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"prepend UTF char\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 0,\n\t\t\t},\n\t\t\tinsert:    []rune{'☃'},\n\t\t\twantValue: []rune(`☃abc`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"insert ASCII char\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 1,\n\t\t\t},\n\t\t\tinsert:    []rune{'X'},\n\t\t\twantValue: []rune(`aXbc`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"insert UTF char\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 1,\n\t\t\t},\n\t\t\tinsert:    []rune{'☃'},\n\t\t\twantValue: []rune(`a☃bc`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"append ASCII char\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 3,\n\t\t\t},\n\t\t\tinsert:    []rune{'X'},\n\t\t\twantValue: []rune(`abcX`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"append UTF char\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 3,\n\t\t\t},\n\t\t\tinsert:    []rune{'☃'},\n\t\t\twantValue: []rune(`abc☃`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"insert 2 ASCII chars\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 1,\n\t\t\t},\n\t\t\tinsert:    []rune{'X', 'Y'},\n\t\t\twantValue: []rune(`aXYbc`),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt.e.insert(tt.insert...)\n\t\tif string(tt.e.value) != string(tt.wantValue) {\n\t\t\tt.Errorf(\"%q: bad value\\nwant: %q\\nhave: %q\", tt.comment, tt.wantValue, tt.e.value)\n\t\t}\n\t}\n}\n\nfunc Test_Editor_unix_word_rubout(t *testing.T) {\n\ttests := []struct {\n\t\tcomment       string\n\t\te             Editor\n\t\twantValue     []rune\n\t\twantKillspace []rune\n\t}{\n\t\t{\n\t\t\tcomment: \"unix-word-rubout at beginning of line\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 0,\n\t\t\t},\n\t\t\twantValue:     []rune(`abc`),\n\t\t\twantKillspace: []rune(``),\n\t\t},\n\t\t{\n\t\t\tcomment: \"unix-word-rubout at soft beginning of line\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(` abc`),\n\t\t\t\tcursor: 1,\n\t\t\t},\n\t\t\twantValue:     []rune(`abc`),\n\t\t\twantKillspace: []rune(` `),\n\t\t},\n\t\t{\n\t\t\tcomment: \"unix-word-rubout until soft beginning of line\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(` abc`),\n\t\t\t\tcursor: 2,\n\t\t\t},\n\t\t\twantValue:     []rune(` bc`),\n\t\t\twantKillspace: []rune(`a`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"unix-word-rubout until beginning of line\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`abc`),\n\t\t\t\tcursor: 2,\n\t\t\t},\n\t\t\twantValue:     []rune(`c`),\n\t\t\twantKillspace: []rune(`ab`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"unix-word-rubout in middle of line\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`lorem ipsum dolor`),\n\t\t\t\tcursor: 11,\n\t\t\t},\n\t\t\twantValue:     []rune(`lorem  dolor`),\n\t\t\twantKillspace: []rune(`ipsum`),\n\t\t},\n\t\t{\n\t\t\tcomment: \"unix-word-rubout cursor at beginning of word\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`lorem ipsum dolor`),\n\t\t\t\tcursor: 12,\n\t\t\t},\n\t\t\twantValue:     []rune(`lorem dolor`),\n\t\t\twantKillspace: []rune(`ipsum `),\n\t\t},\n\t\t{\n\t\t\tcomment: \"unix-word-rubout cursor between multiple spaces\",\n\t\t\te: Editor{\n\t\t\t\tvalue:  []rune(`a b   c`),\n\t\t\t\tcursor: 5,\n\t\t\t},\n\t\t\twantValue:     []rune(`a  c`),\n\t\t\twantKillspace: []rune(`b  `),\n\t\t},\n\t\t{\n\t\t\tcomment: \"unix-word-rubout tab as space char (although is it a realistic case in the context of a command line instruction?)\",\n\t\t\te: Editor{\n\t\t\t\tvalue: []rune(`a b\t\tc`),\n\t\t\t\tcursor: 5,\n\t\t\t},\n\t\t\twantValue: []rune(`a c`),\n\t\t\twantKillspace: []rune(`b\t\t`),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt.e.unixWordRubout()\n\t\tif string(tt.e.value) != string(tt.wantValue) {\n\t\t\tt.Errorf(\"%q: bad value\\nwant: %q\\nhave: %q\", tt.comment, tt.wantValue, tt.e.value)\n\t\t}\n\t\tif string(tt.e.killspace) != string(tt.wantKillspace) {\n\t\t\tt.Errorf(\"%q: bad value in killspace\\nwant: %q\\nhave: %q\", tt.comment, tt.wantKillspace, tt.e.value)\n\t\t}\n\t}\n}\n"
  }
]