├── .gitignore ├── LICENSE ├── README.md ├── compile_native.sh ├── deps.edn ├── project.clj ├── src └── terminal_todo_mvc │ └── core.clj └── terminal-todo-mvc.gif /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | .cpcache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adrian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terminal-todo-mvc 2 | 3 | An example project using [membrane's](https://github.com/phronmophobic/membrane) terminal backend to create a terminal app and compiling it using GraalVM. 4 | 5 | ## Screenshots 6 | 7 | ![terminal todo example](terminal-todo-mvc.gif?raw=true) 8 | 9 | ## Usage 10 | 11 | ### Running 12 | 13 | ```bash 14 | clojure -M -m terminal-todo-mvc.core 15 | ``` 16 | 17 | ### Graalvm compiled 18 | 19 | 1. Follow the instructions for installing GraalVM. https://github.com/clj-easy/graal-docs/blob/master/doc/hello-world.adoc 20 | 21 | 2. Compile the binary by running `./compile_native.sh` from the project directory. 22 | 23 | 3. Run the app `./target/todoapp` 24 | 25 | ## License 26 | 27 | MIT License 28 | 29 | Copyright (c) 2020 Adrian 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. 48 | -------------------------------------------------------------------------------- /compile_native.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | set -x 3 | 4 | lein do clean, uberjar 5 | 6 | native-image --report-unsupported-elements-at-runtime \ 7 | --initialize-at-build-time \ 8 | --no-server \ 9 | -jar ./target/uberjar/terminal-todo-mvc-0.1.0-SNAPSHOT-standalone.jar \ 10 | -H:Name=./target/todoapp 11 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps { 3 | org.clojure/clojure {:mvn/version "1.11.0-beta1"} 4 | com.phronemophobic/membrane {:mvn/version "0.9.31.8-beta" 5 | ;; :local/root "../membrane2/" 6 | } 7 | com.googlecode.lanterna/lanterna {:mvn/version "3.1.1"} 8 | }} 9 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject terminal-todo-mvc "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :dependencies [[org.clojure/clojure "1.11.0-beta1"] 7 | [com.phronemophobic/membrane "0.9.31.8-beta"] 8 | [com.googlecode.lanterna/lanterna "3.1.1"]] 9 | :jvm-opts ["-Dclojure.compiler.direct-linking=true"] 10 | :main ^:skip-aot terminal-todo-mvc.core 11 | :target-path "target/%s" 12 | :profiles {:uberjar {:aot :all}}) 13 | -------------------------------------------------------------------------------- /src/terminal_todo_mvc/core.clj: -------------------------------------------------------------------------------- 1 | (ns terminal-todo-mvc.core 2 | (:require [membrane.ui :as ui 3 | :refer 4 | [horizontal-layout 5 | vertical-layout 6 | on]] 7 | [clojure.core.async :as async] 8 | 9 | [membrane.lanterna 10 | :refer [textarea checkbox label button rectangle] 11 | :as lanterna] 12 | [membrane.component :as component 13 | :refer [defui defeffect]]) 14 | (:gen-class)) 15 | 16 | ;;; todo app 17 | (defui todo-item [{:keys [todo]}] 18 | (horizontal-layout 19 | (on 20 | :mouse-down 21 | (fn [[mx my]] 22 | [[:delete $todo]]) 23 | (ui/with-color [1 0 0] 24 | (label "X"))) 25 | (checkbox {:checked? (:complete? todo)}) 26 | (ui/wrap-on 27 | :key-press 28 | (fn [default-handler s] 29 | (when (not= s :enter) 30 | (default-handler s))) 31 | (textarea {:text (:description todo)})))) 32 | 33 | (comment 34 | (run-ui #'todo-item {:todo 35 | {:complete? false 36 | :description "fix me"}})) 37 | 38 | 39 | (defui todo-list [{:keys [todos]}] 40 | (apply 41 | vertical-layout 42 | (for [todo todos] 43 | (todo-item {:todo todo})))) 44 | 45 | 46 | 47 | (comment 48 | (run-ui #'todo-list {:todos 49 | [{:complete? false 50 | :description "first"} 51 | {:complete? false 52 | :description "second"} 53 | {:complete? true 54 | :description "third"}]})) 55 | 56 | 57 | 58 | (def filter-fns 59 | {:all (constantly true) 60 | :active (comp not :complete?) 61 | :complete? :complete?}) 62 | 63 | ;; Create a toggle that allows the user 64 | ;; to toggle between options 65 | (defui toggle [{:keys [options selected]}] 66 | (apply 67 | horizontal-layout 68 | (for [option options] 69 | (if (= option selected) 70 | (label (name option)) 71 | (on 72 | :mouse-down 73 | (fn [[mx my]] 74 | [[:set $selected option]]) 75 | (ui/with-color [0.8 0.8 0.8] 76 | (label (name option)))))))) 77 | 78 | (comment 79 | (run-ui #'toggle 80 | {:options [:all :active :complete?] 81 | :selected nil})) 82 | 83 | (defui todo-app [{:keys [todos next-todo-text selected-filter] 84 | :or {selected-filter :all}}] 85 | (vertical-layout 86 | (horizontal-layout 87 | (button "Add Todo" 88 | (fn [] 89 | [[::add-todo $todos next-todo-text] 90 | [:set $next-todo-text ""]])) 91 | (ui/wrap-on 92 | :key-press 93 | (fn [default-handler s] 94 | (let [effects (default-handler s)] 95 | (if (and (seq effects) 96 | (= s :enter)) 97 | [[::add-todo $todos next-todo-text] 98 | [:set $next-todo-text ""]] 99 | effects))) 100 | (textarea {:text next-todo-text}))) 101 | (toggle {:selected selected-filter 102 | :options [:all :active :complete?]}) 103 | (let [filter-fn (get filter-fns selected-filter :all) 104 | visible-todos (filter filter-fn todos)] 105 | (todo-list {:todos visible-todos})))) 106 | 107 | 108 | (def todo-state (atom {:todos 109 | [{:complete? false 110 | :description "first"} 111 | {:complete? false 112 | :description "second"} 113 | {:complete? true 114 | :description "third"}] 115 | :next-todo-text ""})) 116 | 117 | (defeffect ::add-todo [$todos next-todo-text] 118 | (dispatch! :update $todos #(conj % {:description next-todo-text 119 | :complete? false}))) 120 | 121 | (comment 122 | (run-ui #'todo-app todo-state 123 | )) 124 | 125 | (comment 126 | (def todo-state 127 | (run-ui #'todo-app {:todos 128 | [{:complete? false 129 | :description "first"} 130 | {:complete? false 131 | :description "second"} 132 | {:complete? true 133 | :description "third"}] 134 | :next-todo-text ""}))) 135 | 136 | 137 | 138 | (def todo-state (atom {:todos 139 | [{:complete? false 140 | :description "first"} 141 | {:complete? false 142 | :description "second"} 143 | {:complete? true 144 | :description "third"}] 145 | :next-todo-text ""})) 146 | 147 | 148 | (defn bordered-box [title body] 149 | (let [body (ui/padding 1 1 150 | body) 151 | [w h] (ui/bounds body) 152 | w (max (+ 2 (count title)) 153 | w)] 154 | [(rectangle (inc w) (inc h)) 155 | (ui/translate 2 0 (label title)) 156 | body])) 157 | 158 | (comment 159 | (do 160 | (def close-ch (async/chan)) 161 | (lanterna/run (component/make-app #'todo-app todo-state) 162 | {:in membrane.lanterna/in 163 | :out membrane.lanterna/out 164 | :close-ch close-ch 165 | })) 166 | 167 | (async/close! close-ch) 168 | ,) 169 | 170 | (defn -main [& args] 171 | (lanterna/run-sync (component/make-app #'todo-app todo-state)) 172 | ;; (component/run-ui-sync ) 173 | ;; (.close System/in) 174 | ;; (shutdown-agents) 175 | ) 176 | 177 | 178 | -------------------------------------------------------------------------------- /terminal-todo-mvc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phronmophobic/terminal-todo-mvc/2573099b2bd848da085969b02f571891a661e0e8/terminal-todo-mvc.gif --------------------------------------------------------------------------------