├── .gitignore ├── LICENSE ├── README.org ├── doc └── intro.md ├── examples ├── .gitignore ├── clipper │ └── src │ │ └── clipper │ │ └── main.cljs └── kitchen_sink │ └── src │ └── kitchen_sink │ └── main.cljs ├── project.clj ├── src ├── lib │ └── i9n │ │ ├── core.clj │ │ ├── core.cljs │ │ ├── ext.clj │ │ ├── ext.cljs │ │ ├── keymap.cljs │ │ ├── more.cljs │ │ ├── nav_entry.cljs │ │ ├── op │ │ ├── fix.cljs │ │ ├── hop.cljs │ │ └── state.cljs │ │ ├── properties.cljs │ │ └── ui.cljs ├── os │ └── i9n │ │ └── node │ │ └── os.cljs └── plugins │ └── i9n │ └── ext │ ├── log.cljs │ └── text.cljs └── test └── i9n ├── more_test.cljs └── nav_entry_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | /node_modules 5 | /resources/public 6 | /.gut 7 | pom.xml 8 | pom.xml.asc 9 | *.jar 10 | *.class 11 | /.lein-* 12 | /.nrepl-port 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * i9n 2 | 3 | A ClojureScript library for creating terminal-based, curses 4 | interfaces. Being built off Node.js allows applications made with 5 | =i9n= to be snappy, light and mostly instant to boot up. Don't worry 6 | much if you're not into node, most of the little beast is half-hidden 7 | behind bars. 8 | 9 | This project's main goal is to simultaneously allow simple interfaces 10 | to be done declaratively--hopefully within the grasp of someone who 11 | isn't a programmer (though don't ask me why this someone would want a 12 | terminal UI)--and provide a simple, solid backbone for complex curses 13 | applications like file managers, music players, ascii-based games and 14 | even text editors, all the while remaining a lib and not a framework, 15 | by having little enough opinion on the rest of your code except for 16 | the use of =core.async=. 17 | 18 | The way =i9n= (standing for /interaction/) lays out interfaces is 19 | through a navigation abstraction, composed of screens--think like 20 | pages in a website, though any given screen can be highly interactive 21 | much like a Single Page Application in web dev. There's a special 22 | declarative ease reserved for list-based navigation, because it's such 23 | a common use case (e.g. menus), but screens can contain any number of 24 | UI widgets and interactions in addition to lists. 25 | 26 | These widgets, interactions and the navigation hierarchy are all 27 | represented declaratively by plain Clojure data structures like 28 | vectors and maps, though of course "declarative" ends whenever you 29 | plug in functions to trigger custom actions (which you are very much 30 | encouraged to do all the time.) Here's a full interface's navigation 31 | hierarchy: 32 | 33 | #+begin_src clojure 34 | [["my german vocab" ["review words" :review 35 | "add new word" :add 36 | "quit" i9n-os/exit]] 37 | [:review "review" ["ich" (fn [] "I") 38 | "bin" (fn [] "am") 39 | "praktisch" (fn [] "practical") 40 | "perfekt" (constantly "perfect")]] 41 | [:add "add word" "not implemented (or really necessary) yet."]] 42 | #+end_src 43 | 44 | The example above fully describes a flashcard (memorization) program 45 | which lets you achieve a four-word strong German vocabulary and form a 46 | full sentence. Of course, a more expansive vocabulary should probably 47 | be backed by reading word/translation pairs from a text file. The 48 | first screen, entitled "my german vocab", is where the application 49 | will start, and is implicitly given the id =:root=. The others have 50 | ids =:review= and =:add= which we use as /keyword actions/ on the root 51 | menu screen to link them up. Actions can also be arbitrary functions, 52 | as shown by the especially useful functions that return a string, 53 | which declaratively generates an action to add that string as an 54 | alert-like message beside the menu option selected (in these cases the 55 | German words you're memorizing.) 56 | 57 | Interfaces in =i9n= are meant to be asynchronous, lazy and responsive 58 | through the use of =core.async= and an input channel where you can put 59 | messages. This channel then serves as the backbone of the navigation's 60 | loop, as every operation message (adding and modifying screens, moving 61 | from screen to screen, among others) is processed sequentially and 62 | synchronously, even as they're accepted asynchronously. This 63 | guarantees, for instance, that you can send an operation message to 64 | add a new screen, then another message to go to this new screen, and 65 | be sure the screen exists in the navigation's hierarchy. 66 | 67 | ** Usage 68 | 69 | *** Navigation 70 | 71 | **** Operations 72 | 73 | ***** :next 74 | 75 | Takes as first argument either the id of the screen to go next, or the 76 | full nav-entry--that is, a vector with three elements: the id, the 77 | title and a body. In this latter case, the nav-entry is added to the 78 | hierarchy (or updated, if the id already exists.) 79 | 80 | =:next= messages are the standard way to navigate the interface, 81 | because it saves a link so you can go back to the previous screen. It 82 | also optionally takes the list position in the current screen (default 83 | to =0= if not supplied.), so that when going back you end up in the 84 | position you left, which is very natural and even expected behavior in 85 | navigation screens. 86 | 87 | The third--also optional--argument is the list position to go to in 88 | the next screen, defaulting to =0= as well. Going to a specific 89 | position might be useful e.g. you're jumping to some search result. 90 | 91 | ***** :hop 92 | 93 | Like =:next=, except it doesn't save a link to go back to, and so it 94 | doesn't take =:next='s current position argument, instead taking an 95 | optional position to go to as second and last argument. This also 96 | makes it useful for hopping to another position within the same 97 | screen. 98 | 99 | Note that when using =:next= the new link created overwrites the 100 | former link, since there can only be one meaning when you 101 | press *back*. Here there's no overwriting, so if you hop to a random 102 | screen, you might be able to back into a former connection that was 103 | made to it when =:next= was used. This is precisely what happens in 104 | the action of going back itself, which uses =:hop= for its internal 105 | implementation. This allows you to go next many screens and then be 106 | able to go all the way back. 107 | 108 | ***** :set 109 | 110 | Like =:hop=, except that it must always take a full nav-entry as first 111 | argument, never a screen id. A =:set= can be useful for setting the 112 | interface to a temporary screen with a dummy id (say, =:temp= or 113 | really anything, though not =nil=) that won't go into the hierarchy. 114 | 115 | In reality I was lying and =:hop= is not used internally to implement 116 | the action of going back screens. It's =:set= that's used, because 117 | when a link is saved, the whole nav-entry is copied there, which 118 | allows local, temporary state to be preserved for when you go back. 119 | So this operation can be used for similar effect. 120 | 121 | ***** :fix 122 | 123 | Updates nav-entries as surgically as you need, taking care to apply 124 | your fixes both in the hierarchy (i.e. persist the changes) and in any 125 | temporary state such as the user's current screen. It can be used to 126 | replace, add or remove an option or part of it. Takes the id of the 127 | screen to change as first argument, the =place= (where to change) as 128 | second, and what to change as rest arguments. 129 | 130 | ****** =place= syntax 131 | 132 | The most basic =place= designation is an integer (equal or greater 133 | than zero--with a negative number the fix operation will be ignored), 134 | which signifies a fix *starting* from that index. That means if you 135 | give it =4= as the place and then supply four following arguments in 136 | the =:fix= operation, the option starting at index =4= as well as the 137 | one starting at index =6= will be changed--or added if those indexes 138 | didn't exist. 139 | 140 | The only way to change the title of an entry, as opposed to the 141 | options of its body, is to use =:title= as the place. The table below 142 | features other keywords accepted as a place. Note that *option* means 143 | a pair of elements (label and action) from the body's vector, so 144 | /option 3/ would start at index 6 and end at index 7. 145 | 146 | | keyword | place meaning | 147 | |----------------+------------------------------------------------------------| 148 | | =:last= | Last option's label, i.e. last /even/ index | 149 | | =:last-action= | Last option's action, i.e. very last index | 150 | | =:pop= | Removes the last option, takes no argument after the place | 151 | | =:append= | Adds after the last option | 152 | | =:prepend= | Adds before the first option | 153 | 154 | The append and prepend keywords above are useful in that they always 155 | add new stuff, but what about adding to the middle of the list? The 156 | next table shows vector-based place syntax, allowing you to do that 157 | and other hopefully helpful things. 158 | 159 | | syntax | place meaning | 160 | |-------------------------+----------------------------------------------------| 161 | | =[:insert n]= | Adds before the start of option =n= | 162 | | =[:insert-after & xs]= | Adds after option matching any of elements =xs= | 163 | | =[:insert-before & xs]= | Adds before option matching any of elements =xs= | 164 | | =[:action n]= | Changes action(s) of option(s) starting from /n/ | 165 | | =[:label n]= | Changes label(s) of option(s) starting from /n/ | 166 | | =[:action-find & xs]= | Like =:action= but from first matching any of =xs= | 167 | | =[:label-find & xs]= | Like =:label= but from first matching any of =xs= | 168 | | =[:from & xs]= | Replaces from first option matching any of =xs= | 169 | | =[:after n & xs]= | Like =:from= but starts /n/ options after | 170 | | =[:before n & xs]= | Like =:from= but starts /n/ options before | 171 | | =[:shrink n start]= | Remove /n/ options from option /start/ | 172 | | =[:remove n & xs]= | Remove /n/ options from first matching any of =xs= | 173 | 174 | When there are several needles =xs= from which to find a matching 175 | result in the haystack, each needle is first searched over the whole 176 | haystack before trying the next one. Trying needles is less dangerous 177 | than it sounds. 178 | 179 | ***** :put 180 | 181 | Takes all the same arguments as =:fix=, but doesn't persist the fix 182 | into the hierarchy. Thus, the fix is only applied to temporary state 183 | nav-entries, such as the one representing the user's current screen. 184 | If there is no such temporary state target where to apply the fix, 185 | nothing is done by the =:put= operation. 186 | 187 | ***** :select 188 | 189 | Example: =(a/put! in [:select 2])= 190 | 191 | Select option at index /n/ in whatever screen is the current. Accepts 192 | =:last= as an index. 193 | 194 | ***** :dirty 195 | 196 | Example: =(a/put! in [:dirty :screen1 :screen4])= 197 | 198 | Takes the id(s) of the screen(s) to be made dirty. Dirtying is 199 | exclusively for the library user, to facilitate his keeping track of 200 | which parts of the hierarchy will have to be lazily recomputed, if at 201 | all, when they're finally accessed--this is coupled with passing a 202 | =:flush= channel inside the =cfg= parameter's =:watches= property, on 203 | which to listen to flush messages. 204 | 205 | Even though dirtying could be managed externally by the user, building 206 | it into the navigation loop takes care of a few things for you: 207 | 208 | 1. a flush notification is sent out when a dirty screen is finally 209 | accessed, after first clearing the screen's dirty status; 210 | 211 | 2. you can send in =:stub= messages, which are just like =:add= 212 | messages, except that the screen is created dirty, which means you 213 | lazily create just a stub, and wait for a flush message to finish 214 | building the screen only when it's first needed. 215 | 216 | ***** :state 217 | 218 | Example: =(a/put! in [:state :user-setting1 :foo :user-setting2 :bar])= 219 | 220 | Use for any application-specific state that you need to keep between 221 | screens--globally in fact, stored within the =nav= object as a map 222 | under the =:state= key. 223 | 224 | Built-in user-facing facilities may interact with state to make the 225 | use of state easier and more high-level than sending =:state= 226 | messages; see =enlightened.os.navigation/pick-option= for example. 227 | 228 | ***** :column 229 | 230 | Allows setting properties of columnar data in a given screen. Takes 231 | the id of the screen as first argument and the integer index of the 232 | column as second. The rest of the arguments should consist of pairs of 233 | keyword and value (keyword arguments). All arguments are optional. The 234 | keywords accepted are: 235 | 236 | | keyword | arguments type | description | 237 | |----------+----------------+-------------------------------------------------| 238 | | =:width= | int | Always keep col width at this character length. | 239 | | =:sort= | (fn [a b]) | Sort table by this col, using supplied fn. | 240 | 241 | ** License 242 | 243 | Copyright © 2014 Vic Goldfeld 244 | 245 | Distributed under the Eclipse Public License either version 1.0 or (at 246 | your option) any later version. 247 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to enlightened 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/great-documentation/what-to-write/) 4 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.js -------------------------------------------------------------------------------- /examples/clipper/src/clipper/main.cljs: -------------------------------------------------------------------------------- 1 | (ns clipper.main 2 | (:require [cljs.nodejs :as node] 3 | [cljs.core.async :as a] 4 | [claude.process :as proc] 5 | [i9n.node.os :as os] 6 | [i9n.core :as i9n])) 7 | 8 | (defn nav [] 9 | [["clippings" ["quit" #(proc/exit)]]]) 10 | 11 | (defn make-paster [chan mode] 12 | #(a/pipe (os/paste) 13 | (a/map> (fn [p] [:fix [nil mode p nil]]) 14 | chan))) 15 | 16 | (defn -main [& input] 17 | (let [chan (a/chan)] 18 | (os/bind-global-keys 19 | ["C-c" "q"] #(proc/exit) 20 | "p" (make-paster chan :insert-after) 21 | "S-p" (make-paster chan :insert) 22 | "x" #(a/put! chan [:fix [nil :remove]]) 23 | "y" #(os/copy "copied text")) 24 | (let [[cmd & args] input] 25 | (i9n/navigation-view (nav) {:chan chan})))) 26 | 27 | (set! *main-cli-fn* -main) 28 | -------------------------------------------------------------------------------- /examples/kitchen_sink/src/kitchen_sink/main.cljs: -------------------------------------------------------------------------------- 1 | (ns kitchen-sink.main 2 | (:require [cljs.nodejs :as node] 3 | [cljs.core.async :as a] 4 | [claude.process :as proc] 5 | [i9n.core :as i9n :include-macros true] 6 | [i9n.node.os :as os] 7 | [i9n.ext.log])) 8 | 9 | (defn sad-async [] 10 | (let [chan (a/chan)] 11 | (doto chan 12 | (a/put! [:fix [:hip :title "Hop!"]]) 13 | (a/put! [:fix [:hip 0 "to non-async"]]) 14 | (a/put! [:fix [:hip 1 :ho]]) 15 | (a/put! [:add [:here "Here" ["text" :ho "book" :Book]]]) 16 | (a/put! [:fix [:hip 2 "hooooo"]]) 17 | (a/put! [:fix [:hip 3 :here]]) 18 | (a/put! [:fix [:here 0 "ho"]]) 19 | (a/put! [:next :hip]) 20 | (a/close!)) 21 | (os/render-deferred) 22 | chan)) 23 | 24 | (def lorem 25 | (str "Lorem ipsum dolores siamet" 26 | (clojure.string/join (repeat 888 " lorem ipsum dolores siamet")) 27 | ". Lorem ipsum dolores siamet.")) 28 | 29 | (defn nav [chan] 30 | [["kitchen sink" ["view text" :Text 31 | "view book" :Book 32 | "edit items" :edit 33 | "log hey" #(cljs.core.async/put! chan [:log "heeee"]) 34 | "test" :test 35 | #_"n-fn" #_(n [nav] (js/setTimeout #(.log js/console (clj->js nav)) 100) 36 | :Text) 37 | "quit" #(proc/exit)]] 38 | [:Text "Text" lorem] 39 | [:Book "Book" ["ch.1" lorem "ch.2" "Second part" "ch.3" "The End"]] 40 | (i9n/editable :edit "edit these items" 41 | (vec (interleave ["one" "two" "three" "four" "five" "six"] 42 | (repeat nil)))) 43 | [:test "focus" ["sadly asynchronous" sad-async 44 | "also sad but hip" :hip 45 | "sysiphus" #(i9n/navigation-view (nav)) 46 | "jit" (fn [] [[:new "Menu" ["a" :ho "d" :d]] 47 | [:d "d" ["a" :ho]]])]] 48 | [:hip "" sad-async] 49 | [:ho "go" ["hum" (constantly "sad")]]]) 50 | 51 | (defn -main [& input] 52 | (os/bind-global-keys ["C-c"] #(proc/exit)) 53 | (let [chan (a/chan) 54 | [cmd & args] input] 55 | (i9n/navigation-view (nav chan) 56 | {:chan chan}))) 57 | 58 | (set! *main-cli-fn* -main) 59 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject i9n "0.2.15" 2 | :description "Fast declarative terminal (curses) UIs with cljs and nodejs." 3 | :url "https://github.com/goldfeld/i9n" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :scm {:name "git" 7 | :url "https://github.com/goldfeld/i9n.git"} 8 | :aliases {"cleantest" ["do" "clean," "cljsbuild" "once," "test,"] 9 | "autotest" ["do" "clean," "cljsbuild" "auto" "test"]} 10 | :source-paths ["src/lib" "src/os" "src/plugins"] 11 | :dependencies [[org.clojure/clojure "1.6.0"] 12 | [org.clojure/clojurescript "0.0-3165"] 13 | [org.clojure/core.async "0.1.346.0-17112a-alpha"] 14 | [longstorm/claude "0.1.10"] 15 | [secretary "1.2.3"] 16 | [flow "0.1.6"]] 17 | :cljsbuild 18 | {:builds [{:id "example-clipper" 19 | :source-paths ["src/lib" "src/os" "src/plugins" 20 | "examples/clipper/src"] 21 | :compiler {:target :nodejs :optimizations :simple 22 | :output-to "examples/clipper/clipper.js"}} 23 | {:id "example-kitchen-sink" 24 | :source-paths ["src/lib" "src/os" "src/plugins" 25 | "examples/kitchen_sink/src"] 26 | :compiler {:target :nodejs :optimizations :simple 27 | :output-to "examples/kitchen_sink/kitchen_sink.js"}} 28 | {:id "test" 29 | :source-paths ["src/lib" "test"] 30 | :compiler {:output-to "target/test.js" 31 | :optimizations :simple 32 | :pretty-print true}}] 33 | :test-commands {"gen-tests" ["node" :node-runner 34 | "target/test.js"]}} 35 | :profiles 36 | {:dev {:dependencies [[com.cemerick/double-check "0.6.1"] 37 | [com.cemerick/piggieback "0.2.0"] 38 | [org.clojure/tools.nrepl "0.2.10"]] 39 | :node-dependencies [[blessed "0.0.29"] 40 | [blessed-contrib "0.0.8"] 41 | [copy-paste "0.3.0"]] 42 | :plugins [[lein-cljsbuild "1.0.5"] 43 | [lein-npm "0.5.0"] 44 | [com.cemerick/clojurescript.test "0.3.3"]] 45 | :repl-options {:nrepl-middleware 46 | [cemerick.piggieback/wrap-cljs-repl]}}}) 47 | -------------------------------------------------------------------------------- /src/lib/i9n/core.clj: -------------------------------------------------------------------------------- 1 | (ns i9n.core) 2 | 3 | (defmacro n [arglist & body] 4 | `{:i9n-action :n-fn 5 | :action (fn ~arglist ~@body)}) 6 | 7 | (defmacro route [r params & body] 8 | (let [r' (clojure.string/replace (subs (str r) 1) "?" ":") 9 | id-symb 'id] 10 | `{:i9n-step :route 11 | :route ~r' 12 | :dispatch 13 | (fn [{:keys ~params :as m#}] 14 | (let [id# (i9n.more/route->keyword (secretary.core/render-route ~r' m#)) 15 | ~'id (when (some #{'~id-symb} '~params) id#) 16 | entry# (do ~@body)] 17 | (if (= 3 (count entry#)) entry# (apply vector id# entry#))))})) 18 | -------------------------------------------------------------------------------- /src/lib/i9n/core.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.core 2 | (:require-macros [i9n.ext :refer [defop]]) 3 | (:require [cljs.core.async :as a] 4 | [secretary.core :as secretary] 5 | [flow.datetime :as dt] 6 | [i9n.op.state :as op-state] 7 | [i9n.op.fix :as op-fix] 8 | [i9n.op.hop :as op-hop] 9 | [i9n.keymap :as keymap] 10 | [i9n.ui :as ui] 11 | [i9n.ext :as ext] 12 | [i9n.nav-entry :as nav-entry] 13 | [i9n.more :refer [index-of]])) 14 | 15 | ;; set-impl! is called from os-specific code (e.g. node.js) 16 | (def impl (atom nil)) 17 | (defn set-impl! [impl-map] (reset! impl impl-map)) 18 | (def create-pane (ui/make-create-pane-impl impl)) 19 | (def navigation (ui/make-navigation-impl impl create-pane)) 20 | (def navigation-view (ui/make-navigation-view-impl impl navigation)) 21 | 22 | ;;; UI HELPERS 23 | 24 | (defn editable [& nav-entry] 25 | {:i9n-step :editable 26 | :entry nav-entry}) 27 | 28 | (defn pick-option 29 | ([id title options settings] 30 | [id title (pick-option id options settings)]) 31 | ([state-id options settings] 32 | (let [action (or (:action settings) 33 | (constantly (or (:next settings) 34 | (when-let [hop (:hop settings)] 35 | {:i9n-action :hop :action hop}))))] 36 | (->> 37 | options 38 | (map (fn [option] 39 | (let [i (index-of options option)] 40 | {:i9n-action :pick-option 41 | :action action 42 | :args {:pick (if-let [handles (:handles settings)] 43 | (nth handles i) 44 | option) 45 | :pick-i i 46 | :state-id state-id 47 | :options options}}))) 48 | (interleave options) 49 | (#(concat % (:more settings))))))) 50 | 51 | ;;; ACTIONS 52 | 53 | (defmethod ext/custom-i9n-action :pick-option 54 | [{:keys [action args]} {:keys [selected channels]}] 55 | (doto (:in channels) 56 | (a/put! [:state (:state-id args) (:pick args)]) 57 | (a/put! [:handle-returned-action action args selected]))) 58 | 59 | (defmethod ext/custom-i9n-action :hop 60 | [{:keys [action]} {{in :in} :channels}] 61 | (a/put! in [:hop action])) 62 | 63 | (defmethod ext/custom-i9n-action :n-fn 64 | [{:keys [action requested-args]} {:keys [nav handle-returned-action] :as more}] 65 | (handle-returned-action (action nav))) 66 | 67 | ;;; OPERATIONS 68 | 69 | (defop add [[cmd & args] nav more] {} 70 | (nav-entry/add-to-hierarchy nav args)) 71 | 72 | (defop stub [[cmd & args] nav more] {} 73 | (nav-entry/add-to-hierarchy 74 | nav args #(assoc-in %1 [:hierarchy %2 :dirty] true))) 75 | 76 | (defn select-option 77 | ([widget render! select! nav i relative-fn] 78 | (select-option widget render! select! nav (relative-fn (:pos nav) i))) 79 | ([widget render! select! {last :last :as nav} i] 80 | (if-let [k (cond (= :last i) last 81 | (integer? i) (cond (> i last) last 82 | (< i 0) 0 83 | :else i))] 84 | (do (select! widget k) 85 | (render!) 86 | (assoc nav :pos k)) 87 | nav))) 88 | 89 | (defop select [[cmd & args] nav {:keys [widget render! select!]}] {} 90 | (apply select-option widget render! select! nav 91 | (if (= 1 (count args)) args (reverse args)))) 92 | 93 | (defop dirty [[cmd & args] nav more] {} 94 | (reduce (fn [n id] (assoc-in nav [:hierarchy id :dirty] true)) 95 | nav args)) 96 | 97 | (defop fix [[cmd & args] nav more] {} 98 | (op-fix/change nav args (:refresh more) :persist false)) 99 | 100 | (defop user-fix [[cmd & args] nav more] {} 101 | (op-fix/change nav args (:refresh more) :persist :user-made)) 102 | 103 | (defop put [[cmd & args] nav more] {} 104 | (op-fix/change nav args (:refresh more) false false)) 105 | 106 | (defop toggle-editable [[cmd id toggle] nav more] {} 107 | (let [target (or id (-> nav :current first))] 108 | (if (nil? toggle) 109 | (update-in nav [:hierarchy id :editable] not) 110 | (assoc-in nav [:hierarchy id :editable] toggle)))) 111 | 112 | (defop state [[cmd & args] nav more] {} 113 | (apply op-state/update-states nav args)) 114 | 115 | (defop history [[cmd & args] nav {{in :in} :channels}] {} 116 | (a/put! 117 | in [:next [:i9n-history "Navigation history" 118 | (-> (reduce-kv (fn [labels timestamp {:keys [prev op n]}] 119 | (if (some #{:next :hop :set} [op]) 120 | (conj labels 121 | (str (dt/time-display timestamp) 122 | " - " (first (:current n)))) 123 | labels)) 124 | {} (:history nav)) 125 | (interleave (repeat nil)))]]) 126 | nav) 127 | 128 | (defop undo 129 | [[cmd & args] {:keys [last-op history] :as nav} {:keys [refresh]}] {} 130 | (let [undo-candidates (get-in history [last-op :prev]) 131 | prev-nav (get-in history [(apply max undo-candidates) :nav])] 132 | (refresh (assoc prev-nav :history history)))) 133 | 134 | ;; {{{ 135 | ;; }}} ;;;;;;;;;;;;;;;;;; 136 | ;; {{{ :next operation ;; 137 | ;;;;;;;;;;;;;;;;;;;;;;;;; 138 | 139 | (defop next [[cmd & args] nav more] {} 140 | (let [create-link (fn [n id] (assoc-in n [:hierarchy id :link] 141 | {:nav-entry (:current nav) 142 | :pos (nth args 1 0)}))] 143 | (op-hop/may-hop (first args) nav 144 | {:may-create-link create-link 145 | :do-hop #(op-hop/hop %1 (nth args 2 0) %2 more) 146 | :may-abort 147 | (fn [id n do-hop] 148 | (if (not= id (first (:current n))) (do-hop) n))}))) 149 | 150 | ;; }}} ;;;;;;;;;;;;;;;;; 151 | ;; {{{ :hop operation ;; 152 | ;;;;;;;;;;;;;;;;;;;;;;;; 153 | 154 | (defop hop [[cmd & args] nav more] {} 155 | (op-hop/may-hop (first args) nav 156 | {:may-create-link (fn [n id] n) 157 | :do-hop #(op-hop/hop %1 (nth args 1 0) %2 more) 158 | :may-abort (fn [id _ do-hop] (do-hop))})) 159 | 160 | (defop set [[cmd nav-entry go-to] nav more] {} 161 | (op-hop/hop nav-entry (or go-to 0) nav more)) 162 | 163 | ;; }}} ;;;;;;;;;;;;;; 164 | ;; :back operation ;; 165 | ;;;;;;;;;;;;;;;;;;;;; 166 | 167 | (defop back 168 | [[cmd times] {back :back :as nav} {{in :in} :channels}] {} 169 | (when back 170 | (let [x (or times 1)] 171 | (cond 172 | (= 1 x) (back) 173 | (and (integer? x) (> x 0)) (do (back) 174 | (a/put! in [:back (dec x)])) 175 | :else (throw (js/Error. (str "custom-i9n-op :back takes only" 176 | "an optional positive integer.")))))) 177 | nav) 178 | 179 | (defop pick 180 | [[cmd i] {:keys [pick pos] :as nav} {:keys [channels]}] {} 181 | (when pick (pick (or i pos) nav)) 182 | nav) 183 | 184 | (defop key [[cmd & args] nav {:keys [channels]}] {} 185 | (let [[action keystate] (keymap/handle-key (first args) 186 | (:keystate nav) 187 | (:keymap nav))] 188 | (when action 189 | (doseq [act (filter identity (action nav))] 190 | (a/put! (:in channels) act))) 191 | (assoc nav :keystate keystate))) 192 | 193 | (defop bind [[cmd kstr action] nav more] {} 194 | (update-in nav [:keymap] keymap/bind (keymap/str->kseq kstr) action)) 195 | 196 | (defn create-quick-fix-fn [in persist?] 197 | (fn 198 | ([body] (a/put! in [(if persist? :fix :put) [nil :body body]])) 199 | ([title body] 200 | (doto #(a/put! in [(if persist? :fix :put) [nil %1 %2]]) 201 | (apply [:title title]) 202 | (apply [:body body]))))) 203 | 204 | (defop i9n-action 205 | [[cmd & args] n {hra :handle-returned-action :keys [widget cfg channels]}] {} 206 | (let [[action-map i] args 207 | in (:in channels)] 208 | (ext/custom-i9n-action 209 | action-map {:selected i :nav n :channels channels 210 | :put (create-quick-fix-fn in false) 211 | :fix (create-quick-fix-fn in :persist) 212 | :handle-returned-action #(hra % i hra n)}) 213 | n)) 214 | 215 | (defop i9n 216 | [[cmd & args] nav {:keys [widget cfg impl]}] {} 217 | (do (ext/custom-i9n (first args) 218 | {:parent widget :nav nav :cfg cfg} 219 | impl) 220 | nav)) 221 | 222 | (defop handle-returned-action 223 | [[cmd & args] nav {hra :handle-returned-action}] {} 224 | (let [[action action-args i] args] 225 | (hra (action (assoc action-args :state (:state nav))) i hra nav) 226 | nav)) 227 | 228 | ;;; STEPS 229 | 230 | (defmethod ext/custom-i9n-step :route 231 | [{:keys [route dispatch]} nav more] 232 | (secretary/add-route! (str "/" route) dispatch) 233 | nav) 234 | 235 | (defmethod ext/custom-i9n-step :editable 236 | [{entry :entry} nav {assoc-entry :assoc-entry}] 237 | (assoc-entry nav entry (fn [n id] (assoc-in n [:hierarchy id :editable] 238 | true)))) 239 | -------------------------------------------------------------------------------- /src/lib/i9n/ext.clj: -------------------------------------------------------------------------------- 1 | (ns i9n.ext) 2 | 3 | (defmacro defop [name arglist conditions & body] 4 | (let [op (keyword name)] 5 | `(defmethod i9n.ext/custom-i9n-op ~op ~arglist 6 | ~@body))) 7 | -------------------------------------------------------------------------------- /src/lib/i9n/ext.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.ext) 2 | 3 | (defmulti custom-i9n-op (fn [op nav more] (first op))) 4 | (defmethod custom-i9n-op :default [op nav more] nav) 5 | 6 | (defmulti custom-i9n (fn [i9n-map more] (:i9n i9n-map))) 7 | (defmethod custom-i9n :default [i9n-map more] nil) 8 | 9 | (defmulti custom-i9n-action (fn [action-map more] (:i9n-action action-map))) 10 | (defmethod custom-i9n-action :default [action-map more] nil) 11 | 12 | (defmulti custom-i9n-step (fn [step-map nav more] (:i9n-step step-map))) 13 | (defmethod custom-i9n-step :default [step-map nav more] nav) 14 | -------------------------------------------------------------------------------- /src/lib/i9n/keymap.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.keymap 2 | (:require [clojure.string :as strng])) 3 | 4 | (defn handle-key [k keystate km] 5 | "Returns a 2-element vector containing an action and a new keystate; 6 | an action is the bound value in a keymap km, and the keystate is the 7 | current value of a state machine keeping track of keys pressed 8 | sequentially to trigger a binding." 9 | (if (= k "escape") 10 | [nil []] 11 | (let [ks (conj keystate k) 12 | action (get km ks)] 13 | (condp apply [action] 14 | nil? [nil []] 15 | true? [nil ks] 16 | [action []])))) 17 | 18 | (defn str-chars [s] 19 | (rest (strng/split s #""))) 20 | 21 | (def uppercase 22 | (set (str-chars "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) 23 | 24 | (def special 25 | #{"enter" "escape" 26 | "left" "right" "down" "up"}) 27 | 28 | (defn str->kseq [s] 29 | (let [result 30 | (-> (fn [{:keys [escape-seq escaping?] :as res} c] 31 | (cond 32 | escaping? (let [esc-seq (strng/lower-case 33 | (str escape-seq c))] 34 | (if (or (some special [esc-seq]) 35 | (re-find #"^[csm]-.$" esc-seq)) 36 | (-> (update-in res [:kseq] conj esc-seq) 37 | (assoc :escaping? false 38 | :escape-seq "")) 39 | (update-in res [:escape-seq] str c))) 40 | (= "\\" c) (-> (assoc res :escaping? true)) 41 | :else (update-in res [:kseq] conj 42 | (if (some uppercase [c]) 43 | (str "s-" (strng/lower-case c)) 44 | c)))) 45 | (reduce {:kseq [] :escape-seq "" :escaping? false} 46 | (str-chars s)))] 47 | (if (:escaping? result) 48 | (throw (js/Error. (str "Invalid escape sequence in binding '" s "'"))) 49 | (:kseq result)))) 50 | 51 | (def bound? (every-pred identity (complement true?))) 52 | 53 | (defn bind 54 | ([km kseq action] (bind km kseq action kseq)) 55 | ([km kseq action prefix] 56 | (if (seq prefix) 57 | (if (bound? (get km prefix)) 58 | (throw (js/Error. (str "Can't bind key sequence '" (apply str kseq) 59 | "', '" (apply str prefix) "' already bound."))) 60 | (bind km kseq action (butlast prefix))) 61 | (apply assoc km kseq action 62 | (interleave (iterate butlast (butlast kseq)) 63 | (repeat (dec (count kseq)) true)))))) 64 | 65 | (defn make-keymap [action-map action-defs] 66 | (reduce-kv (fn [km kstr action] 67 | (bind km (str->kseq kstr) 68 | (condp apply [action] 69 | keyword? (or (:fn (get action-defs action)) 70 | (fn [n] [[action]])) 71 | map? (:fn action) 72 | (fn [n] [])))) 73 | {} action-map)) 74 | 75 | (def action-defs 76 | {:down {:type :move :fn (fn [n] [[:select + 1]])} 77 | :up {:type :move :fn (fn [n] [[:select - 1]])} 78 | :top {:type :move :fn (fn [n] [[:select 0]])} 79 | :bottom {:type :move :fn (fn [n] [[:select (:last n)]])} 80 | :back {:type :hop :fn (fn [n] [[:back]])} 81 | :pick {:type :control :fn (fn [n] [[:pick]])} 82 | :undo {:type :control :fn (fn [n] [[:undo]])} 83 | :remove {:fn (fn [n] [[:user-fix [nil :remove]]])}}) 84 | 85 | (def vi-actions 86 | {"j" :down, "\\DOWN" :down, "k" :up, "\\UP" :up 87 | "gg" :top, "G" :bottom 88 | "h" :back, "\\LEFT" :back 89 | "l" :pick, "\\RIGHT" :pick, "\\ENTER" :pick 90 | "dd" :remove 91 | "dj" {:type :move :fn (fn [n] [[:user-fix [nil [:remove 2]]]])} 92 | "u" :undo 93 | "H" :history}) 94 | 95 | (def vi (make-keymap vi-actions action-defs)) 96 | -------------------------------------------------------------------------------- /src/lib/i9n/more.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.more 2 | (:require [cljs.core.async.impl.protocols :refer [Channel]] 3 | [clojure.string :as strng])) 4 | 5 | (defn index-of 6 | ([coll item] (.indexOf (clj->js coll) (clj->js item))) 7 | ([coll item & items] 8 | (let [jscoll (clj->js coll)] 9 | (some #(let [i (.indexOf jscoll %)] (and (not= -1 i) i)) 10 | (clj->js (cons item items)))))) 11 | 12 | (defn splice [coll idx cnt & items] 13 | (let [[a b] (split-at idx coll)] 14 | (vec (concat a items (drop cnt b))))) 15 | 16 | (defn replace-at-indexes 17 | "Replaces elements at coll at the given indexes with the given 18 | replacements, and coll must be a vector. If indexes and replacements 19 | don't have the same length, replacement will stop as soon as the 20 | shorter one is exhausted." 21 | [coll indexes replacements] 22 | (if (and (seq indexes) (seq replacements)) 23 | (replace-at-indexes (assoc coll (first indexes) (first replacements)) 24 | (rest indexes) (rest replacements)) 25 | coll)) 26 | 27 | (defn channel? [x] 28 | (satisfies? Channel x)) 29 | 30 | (defn widget? [x] 31 | (.hasOwnProperty x "screen")) 32 | 33 | (defn route->keyword [s] 34 | (->> (strng/split s #"[\\/]" 2) 35 | (map #(strng/replace % "%20" "_+_")) 36 | (apply keyword))) 37 | 38 | (defn encode-query-params [params] 39 | (if (seq params) 40 | (-> (reduce-kv (fn [s k v] (str s "&" (name k) "=" v)) (str) params) 41 | (subs 1) 42 | (#(str "?" %))) 43 | (str))) 44 | 45 | (defn encode-keyword 46 | ([s] (encode-keyword s {})) 47 | ([s query-params] 48 | (->> (strng/split (str s (encode-query-params query-params)) 49 | #"[\\/]" 2) 50 | (map #(strng/replace % #" " "_+_")) 51 | (apply keyword)))) 52 | 53 | (defn decode-keyword [k] 54 | (strng/replace (subs (str k) 1) "_+_" " ")) 55 | -------------------------------------------------------------------------------- /src/lib/i9n/nav_entry.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.nav-entry 2 | "Pure, testable helpers for i9n.navigation & co." 3 | (:require [i9n.ext :as ext])) 4 | 5 | (defn assoc-entry [nav nav-entry update-in-entry] 6 | (let [[id title body] nav-entry 7 | has-trigger (and (vector? body) (odd? (count body))) 8 | n (if has-trigger 9 | (assoc-in nav [:hierarchy id :trigger] (last body)) 10 | nav)] 11 | (-> (if update-in-entry (update-in-entry n id) n) 12 | (assoc-in 13 | [:hierarchy id :data] 14 | [title (if has-trigger (vec (butlast body)) body)])))) 15 | 16 | (defn add-to-hierarchy 17 | ([nav nav-entries] (add-to-hierarchy nav nav-entries nil)) 18 | ([nav nav-entries update-in-entry] 19 | (reduce (fn [n entry] 20 | (condp apply [entry] 21 | vector? (assoc-entry n entry update-in-entry) 22 | map? (if (contains? entry :i9n-step) 23 | (ext/custom-i9n-step entry n {:assoc-entry assoc-entry}) 24 | n) 25 | n)) 26 | nav nav-entries))) 27 | 28 | (defn create-nav [root-item nav-entries] 29 | (add-to-hierarchy {:hierarchy {:root {:data root-item}}} 30 | nav-entries)) 31 | 32 | (defn fill-body 33 | "Prepares an entry's body for a fix by filling with nils as 34 | needed. If body is not a vector, return a vector full of nil 35 | elements." 36 | [body place fix-length] 37 | (if (vector? body) 38 | (let [elements-to-add (- (+ place fix-length) (count body))] 39 | (if (> elements-to-add 0) 40 | (into body (repeat elements-to-add nil)) 41 | body)) 42 | (vec (repeat (+ place fix-length) nil)))) 43 | 44 | (defn set-last [nav i] 45 | (assoc nav :last (-> i (/ 2) int dec))) 46 | -------------------------------------------------------------------------------- /src/lib/i9n/op/fix.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.op.fix 2 | (:require [i9n.more :refer [index-of splice replace-at-indexes]] 3 | [i9n.nav-entry :as nav-entry])) 4 | 5 | (defn noop-fix [nav _ _] nav) 6 | 7 | (defn fix-at 8 | [place fix replace? nav _ body-path] 9 | (update-in nav body-path 10 | #(let [fix-length (if replace? (count fix) 0)] 11 | (vec (apply splice (nav-entry/fill-body % place fix-length) 12 | place fix-length fix))))) 13 | 14 | (defn create-fix-search 15 | "Returns a function applying a fix within some integer distance 16 | (determined by offset-fn) of the first item within search-items 17 | found in the body of the nav-entry designated by body-path. See 18 | create-fix." 19 | [search-items offset-fn fix replace?] 20 | (fn [nav _ body-path] 21 | (if-let [i (apply index-of (get-in nav body-path) search-items)] 22 | (fix-at (offset-fn i) fix replace? nav _ body-path) 23 | nav))) 24 | 25 | (def x2 (partial * 2)) 26 | 27 | (defn create-replacements-fix 28 | "Returns a function applying the fix vector over a range starting at 29 | i, and skipping every other item. This allows a fix consisting 30 | solely of either labels or actions to be applied over an entry's 31 | body." 32 | [i fix] 33 | (fn [nav _ body-path] 34 | (let [flength (x2 (count fix))] 35 | (replace-at-indexes (nav-entry/fill-body (get nav body-path) i flength) 36 | (range i (+ i flength 2)) 37 | fix)))) 38 | 39 | (defn create-replacements-fix-search 40 | [search-items offset-fn fix] 41 | (fn [nav _ body-path] 42 | (if-let [i (apply index-of (get-in nav body-path) search-items)] 43 | ((create-replacements-fix (offset-fn i) fix) nav _ body-path) 44 | nav))) 45 | 46 | (def current-label #(if (even? %) % (dec %))) 47 | (def current-action #(if (odd? %) % (inc %))) 48 | (def next-label #(inc (if (even? %) (inc %) %))) 49 | 50 | (defn create-fix 51 | "Returns a function which applies a surgical fix to a nav-entry or 52 | hierarchy entry, where the distinction between the two is made by 53 | supplying the proper paths to title & body within the entry 54 | vector. It also may target either the title or the body, making use 55 | of such paths. When the target is a body and it is not a vector, 56 | assume that whatever originates the fix has made use of the 57 | non-vector body first, if applicable, and so overwrite it with a 58 | sparse vector (containing the fix and filled with nils.)" 59 | [place fix] 60 | (condp apply [place] 61 | integer? (if (>= place 0) (partial fix-at place fix :replace) noop-fix) 62 | keyword? 63 | (case place 64 | :title (fn [nav tpath _] (assoc-in nav tpath (first fix))) 65 | :body (fn [nav _ bpath] (assoc-in nav bpath (first fix))) 66 | :append (fn [nav _ bpath] 67 | (update-in nav bpath #(into (or (and (vector? %) %) []) fix))) 68 | :prepend (fn [nav _ bpath] 69 | (update-in nav bpath #(into (vec fix) 70 | (or (and (vector? %) %) [])))) 71 | :last (fn [nav _ bpath] 72 | (update-in nav bpath 73 | #(fix-at (-> % count dec dec) fix true nav _ bpath))) 74 | :last-action (fn [nav _ bpath] 75 | (update-in nav bpath 76 | #(fix-at (dec (count %)) fix true nav _ bpath))) 77 | :pop (fn [nav _ bpath] 78 | (update-in nav bpath #(if (and (vector? %) (seq %)) 79 | (pop (if (odd? (count %)) % (pop %))) 80 | []))) 81 | :remove (fn [nav _ bpath] 82 | (update-in nav bpath #(splice % (x2 (:pos nav)) 2))) 83 | :insert (fn [nav _ bpath] 84 | (fix-at (x2 (:pos nav)) fix false nav _ bpath)) 85 | :insert-after (fn [nav _ bpath] 86 | (fix-at (x2 (inc (:pos nav))) fix false nav _ bpath)) 87 | noop-fix) 88 | vector? 89 | (let [[cmd & [arg & more :as args]] place] 90 | (case cmd 91 | :insert (partial fix-at (x2 arg) fix false) 92 | :insert-after (create-fix-search args next-label fix false) 93 | :insert-before (create-fix-search args current-label fix false) 94 | :action (create-replacements-fix (inc (x2 arg)) fix) 95 | :label (create-replacements-fix (x2 arg) fix) 96 | :action-find (create-replacements-fix-search args current-action fix) 97 | :label-find (create-replacements-fix-search args current-label fix) 98 | :from (create-fix-search args current-label fix :replace) 99 | :after (create-fix-search more #(+ % (x2 arg)) fix :replace) 100 | :before (create-fix-search more #(- % (x2 arg)) fix :replace) 101 | :shrink (fn [nav _ bpath] 102 | (update-in nav bpath #(splice % (x2 (second args)) 103 | (x2 arg)))) 104 | :remove (fn [nav _ bpath] 105 | (when-let [i (if more 106 | (apply index-of (get-in nav bpath) more) 107 | (x2 (:pos nav)))] 108 | (update-in nav bpath #(splice % (current-label i) 109 | (x2 arg))))) 110 | noop-fix)) 111 | noop-fix)) 112 | 113 | (defn change 114 | "Applies :fix or :put operations into a nav. The option between :fix 115 | and :put is made by the persist? parameter, which means that a :fix 116 | is persisted into the hierarchy, whereas a :put is not. Meanwhile, 117 | operations targeting the current nav-entry will effect the 118 | temporary-state nav-entry copy, regardless of the persist? param. As 119 | such, each operation may in fact be applied to both hierarchy and 120 | current entry, to one of the two, or to none at all." 121 | [nav args refresh persist? user-made?] 122 | (let [current-id (-> nav :current first) 123 | new-nav (reduce 124 | (fn [n [target place & fix]] 125 | (let [id (or target current-id)] 126 | (if (and user-made? (not (-> n :hierarchy id :editable))) 127 | n 128 | (let [apply-fix (create-fix place fix) 129 | n' (if persist? 130 | (apply-fix n [:hierarchy id :data 0] 131 | [:hierarchy id :data 1]) 132 | n)] 133 | (if (= id current-id) 134 | (-> (apply-fix n' [:current 1] [:current 2]) 135 | (assoc :current-is-dirty true)) 136 | n'))))) 137 | nav args) 138 | n (dissoc new-nav :current-is-dirty)] 139 | (if (:current-is-dirty new-nav) 140 | (refresh (nav-entry/set-last n (let [body (-> n :current (nth 2))] 141 | (if (vector? body) (count body) 0)))) 142 | n))) 143 | -------------------------------------------------------------------------------- /src/lib/i9n/op/hop.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.op.hop 2 | (:require [cljs.core.async :as a] 3 | [secretary.core :as secretary] 4 | [i9n.more :as more] 5 | [i9n.nav-entry :as nav-entry])) 6 | 7 | (defn may-flush [nav id chan] 8 | (if (and chan (get-in nav [:hierarchy id :dirty])) 9 | (do (a/put! chan id) 10 | (assoc-in nav [:hierarchy id :dirty] false)) 11 | nav)) 12 | 13 | (defn may-trigger [nav id in hra] 14 | (if-let [trigger (get-in nav [:hierarchy id :trigger])] 15 | (do (hra (trigger id in) nil hra nav) 16 | (update-in nav [:hierarchy id] dissoc :trigger)) 17 | nav)) 18 | 19 | (defn hop [[id _ body :as entry] pos nav {:keys [refresh channels] :as more}] 20 | (-> (if (vector? body) (nav-entry/set-last nav (count body)) nav) 21 | (may-flush id (:flush channels)) 22 | (may-trigger id (:in channels) (:handle-returned-action more)) 23 | (assoc :pos pos 24 | :current entry 25 | :rm-back (:back nav) 26 | :back (when-let [parent (get-in nav [:hierarchy id :link])] 27 | #(a/put! (:in channels) 28 | [:set (:nav-entry parent) (:pos parent)]))) 29 | refresh)) 30 | 31 | (defn may-hop [target nav {:keys [may-create-link may-abort do-hop]}] 32 | (condp apply [target] 33 | vector? (do-hop target (-> (may-create-link nav (first target)) 34 | (nav-entry/add-to-hierarchy [target]))) 35 | keyword? 36 | (if-let [dest (get-in nav [:hierarchy target :data])] 37 | (may-abort target nav #(do-hop (into [target] dest) 38 | (may-create-link nav target))) 39 | (if-let [routed (secretary/dispatch! 40 | (str "/" (more/decode-keyword target)))] 41 | (let [n (nav-entry/add-to-hierarchy nav [routed]) 42 | id (first routed)] 43 | (may-abort id n #(do-hop (into [id] (get-in n [:hierarchy id :data])) 44 | (may-create-link n id)))) 45 | nav)))) 46 | -------------------------------------------------------------------------------- /src/lib/i9n/op/state.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.op.state 2 | (:require [i9n.nav-entry :as nav-entry])) 3 | 4 | (defn update-state [nav state-id state-val] 5 | (if-let [{:keys [set deps]} (get-in nav [:state state-id])] 6 | (update-in nav [:state state-id :val] 7 | (fn [old-val] 8 | (set (if (fn? state-val) (state-val old-val) state-val) 9 | old-val 10 | (select-keys (:state nav) deps)))) 11 | (assoc-in nav [:state state-id] 12 | {:val state-val 13 | :get (fn [val] val) 14 | :set (fn [val _ _] val)}))) 15 | 16 | (defn update-states [nav & id-state-pairs] 17 | (reduce (fn [n [state-id state-val]] 18 | (if-let [dpdnts (get-in n [:state state-id :dependents])] 19 | (reduce (fn [n' dpdt] 20 | (condp apply [dpdt] 21 | fn? (do (dpdt) n') 22 | keyword? (let [d (get-in n' [:state dpdt])] 23 | (or (and (map? d) (= :eager (:computed d)) 24 | (update-state n' dpdt (:val d))) 25 | n')))) 26 | (update-state n state-id state-val) 27 | dpdnts) 28 | (update-state n state-id state-val))) 29 | nav (partition 2 id-state-pairs))) 30 | -------------------------------------------------------------------------------- /src/lib/i9n/properties.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.properties) 2 | 3 | (defn centered [props] 4 | (assoc props :top "center" :left "center")) 5 | 6 | (defn halved [props] 7 | (assoc props :width "50%" :height "50%")) 8 | 9 | (defn half-width [props] 10 | (assoc props :width "50%")) 11 | 12 | (defn half-height [props] 13 | (assoc props :height "50%")) 14 | 15 | (defn line-bordered [props] 16 | (assoc props :border {:type "line"})) 17 | 18 | (defn clickable [props] 19 | (assoc props :mouse true)) 20 | 21 | (defn interactive [props] 22 | (assoc props :mouse true :keys true)) 23 | 24 | (defn interactive-vi [props] 25 | (assoc props :mouse true :keys true :vi true)) 26 | 27 | (defn vi [props] 28 | (assoc props :vi true)) 29 | -------------------------------------------------------------------------------- /src/lib/i9n/ui.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.ui 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require [cljs.core.async :as a] 4 | [secretary.core :as secretary] 5 | [i9n.ext :as ext] 6 | [i9n.nav-entry :as nav-entry] 7 | [i9n.keymap :as keymap] 8 | [i9n.more :refer [channel? widget?]])) 9 | 10 | (defn handle-map-action 11 | [action i {in :in}] 12 | (condp #(contains? %2 %1) action 13 | :i9n-action (a/put! in [:i9n-action action i]) 14 | :i9n (a/put! in [:i9n action]) 15 | nil) 16 | nil) 17 | 18 | (defn go-to-book-part [i chan book km keybinds-pager impl titles parts] 19 | (apply (:unset-keys impl) book @km) 20 | ((:set-content impl) book (nth parts i)) 21 | ((:render impl)) 22 | (let [quit (fn [] (a/put! chan [:pos i]) (a/close! chan)) 23 | kmaps [(:quit keybinds-pager) quit 24 | (:left keybinds-pager) 25 | (if (> i 0) 26 | #(go-to-book-part (dec i) chan book km 27 | keybinds-pager impl titles parts) 28 | quit) 29 | (:right keybinds-pager) 30 | (if (< i (dec (count parts))) 31 | #(go-to-book-part (inc i) chan book km 32 | keybinds-pager impl titles parts) 33 | (constantly nil))]] 34 | (reset! km kmaps) 35 | (apply (:set-keys impl) book kmaps) 36 | chan)) 37 | 38 | (defn create-action! [body selection restore-state pane binds impl] 39 | (let [actions (take-nth 2 (rest body))] 40 | (if (every? string? actions) 41 | (let [book ((:create-text-viewer impl) pane) 42 | book-chan (a/chan)] 43 | (go (let [res ( cfg :keybinds-pager :left) 111 | parse-body #(parse-body % widget l-binds back channels impl) 112 | options (parse-body bd)] 113 | (when rm-back ((:unset-key impl) widget l-binds rm-back)) 114 | (when back ((:set-key-once impl) widget l-binds back)) 115 | ((:set-content impl) title-widget (if (string? title) title "[untitled]")) 116 | ((:remove-all-listeners impl) widget "select") 117 | (let [n (if options 118 | (do ((:set-items impl) widget (map #(if (vector? %) (first %) %) 119 | (take-nth 2 options))) 120 | ((:select impl) widget (:pos nav)) 121 | (nav-entry/set-last nav (count options))) 122 | nav)] 123 | ((:render impl)) 124 | (-> (assoc n :pick (create-pick-fn widget hra channels cfg impl)) 125 | (dissoc :rm-back)))))) 126 | 127 | (def config-default 128 | {:keymap keymap/vi 129 | :keybinds-pager {:quit ["q" "escape"] 130 | :left ["h" "left"] 131 | :right ["l" "right"] 132 | :page-up ["C-p"] 133 | :page-down ["C-n"] 134 | :cut-item ["d"] 135 | :paste-below ["p"] 136 | :paste-above ["P"]}}) 137 | 138 | (defn make-create-pane-impl [impl] 139 | (fn create-pane 140 | ([current initial-nav widget] 141 | (create-pane current initial-nav widget config-default)) 142 | ([[id title body :as current] initial-nav widget cfg] 143 | (let [in (or (:chan cfg) (a/chan)) 144 | mult (or (:mult cfg) (a/mult in)) 145 | channels (assoc (:watches cfg) :in in :mult mult) 146 | title-widget ((:create-text @impl) {:left 2 :content title}) 147 | hra (create-handle-returned-action channels widget cfg @impl) 148 | other {:widget widget :cfg cfg :impl @impl 149 | :render! (:render @impl) :select! (:select @impl) 150 | :channels channels :handle-returned-action hra 151 | :refresh (create-refresh-fn widget title-widget 152 | hra channels cfg @impl)}] 153 | (doto widget 154 | ((:on @impl) "keypress" 155 | (fn [ch keydata] 156 | (a/put! in [:key (clojure.string/lower-case 157 | (aget keydata "full"))]))) 158 | ((:prepend @impl) title-widget)) 159 | (a/reduce 160 | (fn [{last-op :last-op :as nav} op] 161 | (let [timestamp (.getTime (js/Date.))] 162 | (-> (ext/custom-i9n-op op nav other) 163 | (assoc-in [:history timestamp] 164 | {:op op :nav (dissoc nav :history) 165 | :next [] :prev [last-op]}) 166 | (update-in [:history last-op :next] conj timestamp) 167 | (assoc :last-op timestamp)))) 168 | (assoc initial-nav :pos 0, :last-op :start, :current current 169 | :keystate [], :keymap (:keymap cfg) 170 | :history {:start {:next []}}) 171 | (a/tap mult (a/chan))) 172 | (a/put! in [:hop id])) 173 | widget))) 174 | 175 | (defn make-navigation-impl [impl create-pane-impl] 176 | (fn navigation 177 | ([nav-entries] (navigation nav-entries {})) 178 | ([nav-entries cfg] (navigation nav-entries cfg ((:create @impl) :list))) 179 | ([nav-entries cfg list] 180 | (let [{entries :xs 181 | root :x} (group-by #(if (and (vector? %) (= 2 (count %))) :x :xs) 182 | nav-entries)] 183 | (create-pane-impl (cons :root (first root)) 184 | (merge 185 | (nav-entry/create-nav (first root) entries) 186 | (:nav cfg)) 187 | list (let [keymap (merge (:keymap config-default) 188 | (:keybinds cfg))] 189 | (-> (merge config-default cfg) 190 | (assoc :keymap keymap)))))))) 191 | 192 | (defn make-navigation-view-impl [impl navigation-impl] 193 | (fn navigation-view 194 | ([nav-entries] (navigation-view nav-entries {})) 195 | ([nav-entries cfg] 196 | (navigation-impl nav-entries cfg ((:list-view @impl)))))) 197 | -------------------------------------------------------------------------------- /src/os/i9n/node/os.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.node.os 2 | (:refer-clojure :exclude [list]) 3 | (:require [cljs.nodejs :as node] 4 | [cljs.core.async :as a] 5 | [claude.process :as proc] 6 | [i9n.core :as core] 7 | [i9n.properties :as p])) 8 | 9 | (declare create-args set-args) 10 | 11 | (def get-blessed (memoize #(node/require "blessed"))) 12 | (def get-screen (memoize #(.screen (get-blessed)))) 13 | 14 | (defn render [] 15 | (.render (get-screen))) 16 | 17 | (defn render-deferred [] 18 | (js/setTimeout render 0)) 19 | 20 | (def exit proc/exit) 21 | 22 | (defn bind-global-keys 23 | ([key-arg fn] 24 | (.key (get-screen) (clj->js key-arg) fn)) 25 | ([key-arg fn & args] 26 | (let [screen (get-screen)] 27 | (.key screen (clj->js key-arg) fn) 28 | (doseq [[k f] (partition 2 args)] 29 | (.key screen (clj->js k) f))))) 30 | 31 | (def ^:private *active* (atom nil)) 32 | 33 | (defn switch-active-view 34 | ([new-active main-widget] 35 | (.focus main-widget) 36 | (switch-active-view new-active) 37 | main-widget) 38 | ([new-active] 39 | (let [active (deref *active*)] 40 | (reset! *active* new-active) 41 | (when active (.setBack active)) 42 | (.setFront new-active) 43 | (render) 44 | new-active))) 45 | 46 | (defn create-widget 47 | [widget-fn mods props overrides] 48 | ;(let [mod->fn (comp resolve (partial symbol "framework") name) 49 | ;modify (apply comp (map mod->fn mods))] 50 | (let [modify (apply comp mods) 51 | override #(apply assoc % overrides)] 52 | (widget-fn (-> props modify override)))) 53 | 54 | (defn create-text [props] 55 | (.text (get-blessed) (clj->js props))) 56 | 57 | (defn create-form 58 | ([props] (.form (get-blessed) (clj->js props))) 59 | ([mods props] (create-form [] mods props)) 60 | ([overrides mods props] (create-widget create-form mods props overrides))) 61 | 62 | (defn create-box 63 | ([props] (.box (get-blessed) (clj->js props))) 64 | ([mods props] (create-box [] mods props)) 65 | ([overrides mods props] (create-widget create-box mods props overrides))) 66 | 67 | (defn create-list 68 | ([props] (.list (get-blessed) (clj->js props))) 69 | ([mods props] (create-list [] mods props)) 70 | ([overrides mods props] (create-widget create-list mods props overrides))) 71 | 72 | (defn set-items [widget items] 73 | (.setItems widget (clj->js items)) 74 | items) 75 | 76 | (defn set-key [widget key-or-keys action] 77 | (.key widget (clj->js key-or-keys) action)) 78 | 79 | (defn unset-key [widget key-or-keys action] 80 | (.unkey widget (clj->js key-or-keys) action)) 81 | 82 | (defn set-key-once [widget key-or-keys action] 83 | (.onceKey widget (clj->js key-or-keys) action)) 84 | 85 | (defn ->js [key-or-keys] 86 | (if (vector? key-or-keys) (clj->js key-or-keys) key-or-keys)) 87 | 88 | (defn set-keys [widget & keymaps] 89 | (doseq [[key fn] (partition 2 keymaps)] 90 | (.key widget (->js key) fn))) 91 | 92 | (defn unset-keys [widget & keymaps] 93 | (doseq [[key fn] (partition 2 keymaps)] 94 | (.unkey widget (->js key) fn))) 95 | 96 | (defn copy [text] 97 | (let [chan (a/chan) 98 | clipboard (.. (node/require "copy-paste") noConflict silent)] 99 | (.copy clipboard text (fn [err res] (a/put! chan (or err res)))) 100 | chan)) 101 | 102 | (defn paste [] 103 | (let [chan (a/chan) 104 | clipboard (.. (node/require "copy-paste") noConflict silent)] 105 | (.paste clipboard (fn [err res] (a/put! chan res))) 106 | chan)) 107 | 108 | (defn types [] 109 | [:list :text :preview :row :wrapper :commandline]) 110 | 111 | (defn create 112 | "Creates a widget. The overrides parameters are key-value pairs of a 113 | property and the desired override value. 114 | (See i9n.os.ui/types for valid type values.)" 115 | [type & overrides] 116 | (let [[create-fn mods props] (create-args type)] 117 | (create-fn overrides mods props))) 118 | 119 | (def set-title identity) 120 | 121 | (defn set-content 122 | "Sets the content of a widget. Any content parameters will be 123 | concat'ed together to make up a list of the lines of the widget's 124 | body content. 125 | (See i9n.os.ui/types for valid type values.)" 126 | ([widget type title content1 content2 & contents] 127 | (set-content widget type title (apply concat content1 content2 contents))) 128 | ([widget type title content] 129 | (let [set-fn (type set-args)] 130 | (set-fn widget content) 131 | (set-title widget title)))) 132 | 133 | (defn alert 134 | ([content] (alert [] content)) 135 | ([overrides content] (alert 2000 overrides content)) 136 | ([timeout overrides content] 137 | (js/setTimeout #(.log js/console content) 100))) 138 | 139 | (defn list 140 | ([title content on-select] 141 | (let [lst (list title content)] 142 | (.on lst "select" #(on-select %2 content)) 143 | lst)) 144 | ([title content] 145 | (let [lst (create :list)] 146 | (set-content lst :list title content) 147 | lst))) 148 | 149 | (defn list-view 150 | ([title content on-select] 151 | (let [widget (list-view title content)] 152 | (.on widget "select" #(on-select %2 content)) 153 | widget)) 154 | ([title content] 155 | (doto (list-view) 156 | (set-content :list title content))) 157 | ([] 158 | (let [view (create :wrapper :height "75%") 159 | widget (create :list)] 160 | (.append view widget) 161 | (switch-active-view view widget) 162 | widget))) 163 | 164 | (defn text 165 | ([title txt] 166 | (let [widget (text txt)] 167 | (set-title widget title) 168 | widget)) 169 | ([txt] 170 | (let [widget (text)] 171 | (.setContent widget txt) 172 | widget)) 173 | ([] (create :text))) 174 | 175 | (defn text-view [title text] 176 | (let [view (create :wrapper :height "75%") 177 | widget (create :text)] 178 | (.append view widget) 179 | (.setContent widget text) 180 | (set-title widget title) 181 | (switch-active-view view widget) 182 | widget)) 183 | 184 | (defn create-text-viewer! 185 | ([pane text] 186 | (let [viewer (create-text-viewer! pane)] 187 | (.setContent viewer text) 188 | viewer)) 189 | ([pane] 190 | (let [viewer (text)] 191 | (set-items pane []) 192 | (.append pane viewer) 193 | (.focus viewer) 194 | viewer))) 195 | 196 | (def create-textbox identity) 197 | 198 | (defn create-args [type] 199 | (case type 200 | :list [create-list 201 | [p/centered p/half-height p/line-bordered] 202 | {:align "left" 203 | :width "75%" 204 | :fg "blue" 205 | :selectedBg "blue" 206 | :selectedFg "white"}] 207 | :text [create-box 208 | [p/interactive-vi] 209 | {:scrollable true 210 | :alwaysScroll true 211 | :width "75%" 212 | :height "80%" 213 | :top "center" 214 | :left 5}] 215 | :preview [create-box 216 | [] 217 | {:width "75%" 218 | :height "25%" 219 | :bottom 0 220 | :left "center"}] 221 | :row [create-box [] {:width "100%"}] 222 | :wrapper [create-box 223 | [] 224 | {:parent (get-screen) 225 | :width "100%" 226 | :height "100%"}] 227 | :commandline [create-textbox 228 | [] 229 | {:width "100%" 230 | :bottom 0 231 | :fg "yellow"}])) 232 | 233 | (def set-args 234 | {:list set-items}) 235 | 236 | (def impl-map 237 | {:render render 238 | :render-deferred render-deferred 239 | :select (fn [widget i] (.select widget i)) 240 | :prepend (fn [parent widget] (.prepend parent widget)) 241 | :detach (fn [widget] (.detach widget)) 242 | :set-content (fn [widget content] (.setContent widget content)) 243 | :bind-global-keys bind-global-keys 244 | :set-items set-items 245 | :set-key-once set-key-once 246 | :set-keys set-keys 247 | :unset-key unset-key 248 | :unset-keys unset-keys 249 | :remove-all-listeners (fn [widget type] (.removeAllListeners widget type)) 250 | :on (fn [widget type listener] (.on widget type listener)) 251 | :create create 252 | :list-view list-view 253 | :create-text create-text 254 | :create-text-viewer create-text-viewer!}) 255 | 256 | (core/set-impl! impl-map) 257 | -------------------------------------------------------------------------------- /src/plugins/i9n/ext/log.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.ext.log 2 | (:require [i9n.ext :refer [custom-i9n-op]] 3 | [flow.datetime :as dt] 4 | [claude.etc :refer [home]] 5 | [claude.fs :refer [append-file-and-forget]])) 6 | 7 | (defmethod custom-i9n-op :log 8 | [[cmd & msg] nav {:keys [cfg]}] 9 | (let [now (dt/now)] 10 | (append-file-and-forget (str "[" (dt/date-display now) 11 | "-" (dt/time-display now) "] " 12 | (clojure.string/join " " msg) "\n") 13 | (or (:logfile cfg) (str (home) "/.i9n-log")))) 14 | nav) 15 | -------------------------------------------------------------------------------- /src/plugins/i9n/ext/text.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.ext.text 2 | (:require [i9n.ext :refer [custom-i9n]])) 3 | 4 | (defmethod custom-i9n :text 5 | [{:keys [content keybinds]} {:keys [parent nav cfg]} 6 | {:keys [render set-key-once create-text-viewer!]}] 7 | (let [t (create-text-viewer! parent content) 8 | back #(do (.detach t) (when-let [b (:back nav)] (b)) (render))] 9 | (set-key-once t (-> cfg :keybinds :left) back) 10 | (doseq [[kb trigger] (partition 2 keybinds)] 11 | (set-key-once t kb #(trigger 12 | {:detach (fn [] (.detach t) (render)) 13 | :back back}))) 14 | (render))) 15 | -------------------------------------------------------------------------------- /test/i9n/more_test.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.more-test 2 | (:require-macros [cemerick.cljs.test :refer [deftest is testing]]) 3 | (:require [i9n.more :as more])) 4 | 5 | (deftest index-of-test 6 | (is (= 4 (more/index-of "hey you" "you"))) 7 | (is (= 2 (more/index-of "hey you" "your" "a" "b" "y" "o")))) 8 | 9 | (deftest splice-test 10 | (is (= [1 2 3 4 5] (more/splice [1 2 5] 2 0 3 4))) 11 | (is (= [1 5] (more/splice [1 2 3 4 5] 1 3))) 12 | (is (= [1 2 :a :b 5] (more/splice [1 2 3 4 5] 2 2 :a :b)))) 13 | -------------------------------------------------------------------------------- /test/i9n/nav_entry_test.cljs: -------------------------------------------------------------------------------- 1 | (ns i9n.nav-entry-test 2 | (:require-macros [cemerick.cljs.test :refer [deftest is testing]]) 3 | (:require [i9n.nav-entry :as n])) 4 | 5 | (deftest create-nav-simple-test 6 | (is (= (n/create-nav ["menu" ["a" :a "b" :b]] 7 | [[:a "a" "some text"] 8 | [:b "b" ["c" :c]] 9 | [:c "c" "other text"]]) 10 | {:hierarchy 11 | {:root {:data ["menu" ["a" :a "b" :b]]} 12 | :a {:data ["a" "some text"]} 13 | :b {:data ["b" ["c" :c]]} 14 | :c {:data ["c" "other text"]}}}))) 15 | 16 | (deftest fill-body-test 17 | (is (= [1 2 3 nil nil nil] (n/fill-body [1 2 3] 2 4)))) 18 | --------------------------------------------------------------------------------