├── .gitignore ├── ClojureScript └── replete │ ├── .gitignore │ ├── deps.edn │ ├── project.clj │ ├── script │ ├── build │ ├── build.clj │ ├── clean │ └── get-closure-compiler │ └── src │ └── replete │ └── stub.cljs ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── fikesfarm │ │ └── Replete │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── fonts │ │ │ └── FiraCode-Regular.otf │ ├── java │ │ └── com │ │ │ └── fikesfarm │ │ │ └── Replete │ │ │ ├── HistoryAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ ├── VMHandler.kt │ │ │ ├── ui │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── utils.kt │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ └── list_item.xml │ │ ├── menu │ │ └── menu_actions.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher_foreground.png │ │ ├── values │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── fikesfarm │ └── Replete │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | local.properties 4 | build/ 5 | app/build/ 6 | app/*.iml 7 | app/src/main/assets/out 8 | -------------------------------------------------------------------------------- /ClojureScript/replete/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | main.js 13 | out 14 | .cpcache 15 | compiler 16 | .DS_Store 17 | aot-cache 18 | -------------------------------------------------------------------------------- /ClojureScript/replete/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojurescript {:mvn/version "1.11.60"} 2 | github-replete-repl/replete-shared {:git/url "https://github.com/replete-repl/replete-shared" 3 | :sha "4e3d8affd7131d02fcfb9ad8b02c2addc3dda5d8"}}} 4 | -------------------------------------------------------------------------------- /ClojureScript/replete/project.clj: -------------------------------------------------------------------------------- 1 | (defproject replete "0.1.0" 2 | :dependencies [[cljsjs/parinfer "1.8.1-0"]] 3 | :plugins [[lein-tools-deps "0.4.3"] 4 | [lein-cljsbuild "1.1.7"]] 5 | :middleware [lein-tools-deps.plugin/resolve-dependencies-with-deps-edn] 6 | :lein-tools-deps/config {:config-files [:install :user :project]} 7 | :clean-targets ["out" "target"] 8 | :cljsbuild {:builds {:test {:source-paths ["src" "test"] 9 | :compiler {:output-to "test/resources/compiled.js" 10 | :optimizations :whitespace 11 | :pretty-print true}}} 12 | :test-commands {"test" ["phantomjs" 13 | "test/resources/test.js" 14 | "test/resources/test.html"]}}) 15 | -------------------------------------------------------------------------------- /ClojureScript/replete/script/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Make sure we fail and exit on the command that actually failed. 4 | set -e 5 | set -o pipefail 6 | 7 | # Ensure pkg-config can find libs installed by homebrew. 8 | export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig/:/opt/local/lib/pkgconfig/ 9 | 10 | # Check on some build dependencies: 11 | which pkg-config >/dev/null || $( echo "Error: please \"brew install pkg-config\"" >&2 ; exit 1 ) 12 | which cmake >/dev/null || $( echo "Error: please \"brew install cmake\"" >&2 ; exit 1 ) 13 | pkg-config zlib || $( echo "Error: please \"brew install zlib\"" >&2 ; exit 1 ) 14 | pkg-config libzip || $( echo "Error: please \"brew install libzip\"" >&2 ; exit 1 ) 15 | pkg-config icu-uc || $( echo "Error: please \"brew install icu4c\"" >&2 ; exit 1 ) 16 | 17 | # Parse the command-line options: 18 | FAST_PLANCK='' 19 | while [ $# -gt 0 ]; do 20 | case "$1" in 21 | --fast) 22 | export FAST_PLANCK="--fast" 23 | export FAST_BUILD=1 24 | shift 25 | ;; 26 | esac 27 | done 28 | 29 | # Fetch and build planck: 30 | cd ../../../ 31 | if [ ! -e planck ]; then 32 | echo "Fetching planck" 33 | curl -L https://github.com/planck-repl/planck/archive/master.tar.gz | gunzip | tar x 34 | ln -s planck-master planck 35 | fi 36 | cd - 37 | 38 | cd ../../../planck 39 | if [ ! -e planck-c/build/planck ]; then 40 | echo "Building planck" 41 | script/build $FAST_PLANCK 42 | fi 43 | cd - 44 | 45 | mkdir -p aot-cache 46 | lein deps 47 | M2_REPO=~/.m2/repository 48 | echo "AOT compiling macros" 49 | ../../../planck/planck-c/build/planck -q -k aot-cache -c $M2_REPO/andare/andare/1.1.587/andare-1.1.587.jar:$M2_REPO/org/clojure/test.check/1.1.1/test.check-1.1.1.jar:$M2_REPO/chivorcam/chivorcam/1.0.0/chivorcam-1.0.0.jar < 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/assets/fonts/FiraCode-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replete-repl/replete-android/24dd0b60c99bc9cc9080c8dba8b1095a4430a53f/app/src/main/assets/fonts/FiraCode-Regular.otf -------------------------------------------------------------------------------- /app/src/main/java/com/fikesfarm/Replete/HistoryAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.fikesfarm.Replete 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.text.SpannableString 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.ArrayAdapter 10 | import android.widget.ListView 11 | import android.widget.TextView 12 | import android.graphics.Typeface 13 | 14 | enum class ItemType { 15 | INPUT, OUTPUT, ERROR 16 | } 17 | 18 | data class Item(val text: SpannableString, val type: ItemType) 19 | 20 | fun inflateItem(viewHolder: HistoryAdapter.ViewHolder, item: Item, typeface: Typeface) { 21 | viewHolder.item.text = item.text 22 | viewHolder.item.setTypeface(typeface) 23 | when (item.type) { 24 | ItemType.INPUT -> viewHolder.item.setTextColor(Color.DKGRAY) 25 | ItemType.OUTPUT -> viewHolder.item.setTextColor(Color.BLACK) 26 | ItemType.ERROR -> viewHolder.item.setTextColor(Color.RED) 27 | } 28 | } 29 | 30 | class HistoryAdapter(context: Context, id: Int, val typeface: Typeface, val parent: ListView) : 31 | ArrayAdapter(context, id) { 32 | 33 | fun update(item: Item) { 34 | this.add(item) 35 | parent.post { 36 | parent.smoothScrollToPosition(this.count - 1) 37 | } 38 | } 39 | 40 | class ViewHolder(val item: TextView) 41 | 42 | override fun getView(position: Int, itemView: View?, parent: ViewGroup): View { 43 | 44 | val item = getItem(position) 45 | 46 | if (itemView == null) { 47 | val _itemView = LayoutInflater.from(this.context).inflate(R.layout.list_item, parent, false) 48 | val viewHolder = ViewHolder(_itemView.findViewById(R.id.history_item)) 49 | 50 | _itemView.tag = viewHolder 51 | 52 | if (item != null) { 53 | inflateItem(viewHolder, item, typeface) 54 | } 55 | 56 | return _itemView 57 | } else { 58 | if (item != null) { 59 | inflateItem(itemView.tag as ViewHolder, item, typeface) 60 | } 61 | return itemView 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/fikesfarm/Replete/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.fikesfarm.Replete 2 | 3 | import android.app.Activity 4 | import android.app.AlertDialog 5 | import android.content.Context 6 | import android.graphics.Color 7 | import android.view.* 8 | import android.widget.* 9 | import android.content.ClipData 10 | import android.content.ClipboardManager 11 | import android.content.res.Configuration 12 | import android.graphics.Typeface 13 | import android.os.* 14 | import android.text.* 15 | import android.text.style.ForegroundColorSpan 16 | import android.util.DisplayMetrics 17 | import com.eclipsesource.v8.V8 18 | import com.eclipsesource.v8.V8Array 19 | import java.io.* 20 | import android.os.StrictMode 21 | import java.lang.IndexOutOfBoundsException 22 | 23 | fun setTextSpanColor(s: SpannableString, color: Int, start: Int, end: Int) { 24 | return s.setSpan(ForegroundColorSpan(color), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) 25 | } 26 | 27 | fun markString(s: String): SpannableString { 28 | 29 | var idx = 0 30 | var rs = s as CharSequence 31 | var ps = mutableListOf() 32 | 33 | while (idx != -1) { 34 | idx = rs.indexOfFirst { c -> c == "\u001B"[0] } 35 | if (idx != -1) { 36 | val color = when (rs.substring(idx + 2, idx + 4).toInt()) { 37 | 34 -> Color.BLUE 38 | 32 -> Color.rgb(0, 191, 0) 39 | 35 -> Color.rgb(191, 0, 191) 40 | 31 -> Color.rgb(255, 84, 84) 41 | else -> null 42 | } 43 | rs = rs.substring(0, idx).plus(rs.substring(idx + 5, rs.length)) 44 | 45 | if (color != null) { 46 | ps.add(color) 47 | } 48 | ps.add(idx) 49 | } 50 | } 51 | 52 | val srs = SpannableString(rs) 53 | 54 | while (srs.isNotEmpty() && ps.size >= 3) { 55 | setTextSpanColor(srs, ps[0], ps[1], ps[2]) 56 | ps = ps.subList(3, ps.size) 57 | } 58 | 59 | return srs 60 | } 61 | 62 | enum class Messages(val value: Int) { 63 | INIT_ENV(1), 64 | BOOTSTRAP_ENV(2), 65 | EVAL(3), 66 | ADD_ERROR_ITEM(4), 67 | ADD_OUTPUT_ITEM(5), 68 | ADD_INPUT_ITEM(6), 69 | ENABLE_EVAL(7), 70 | ENABLE_PRINTING(8), 71 | UPDATE_WIDTH(9), 72 | SET_WIDTH(10), 73 | VM_LOADED(11), 74 | CALL_FN(12), 75 | RELEASE_OBJ(13), 76 | NS_LOADED(16), 77 | INIT_FAILED(17), 78 | } 79 | 80 | class MainActivity : Activity() { 81 | 82 | private var isVMLoaded = false 83 | 84 | private var adapter: HistoryAdapter? = null 85 | 86 | private fun bundleGetContents(path: String): String? { 87 | return try { 88 | val reader = assets.open("out/$path").bufferedReader() 89 | val ret = reader.readText() 90 | reader.close() 91 | ret 92 | } catch (e: IOException) { 93 | null 94 | } 95 | } 96 | 97 | private fun getClojureScriptVersion(): String { 98 | val s = bundleGetContents("replete/bundle.js") 99 | return s?.substring(29, s.length)?.takeWhile { c -> c != " ".toCharArray()[0] } ?: "" 100 | } 101 | 102 | private fun runPoorMansParinfer(inputField: EditText, s: Editable) { 103 | val cursorPos = inputField.selectionStart 104 | if (cursorPos == 1) { 105 | when (s.toString()) { 106 | "(" -> s.append(")") 107 | "[" -> s.append("]") 108 | "{" -> s.append("}") 109 | } 110 | inputField.setSelection(cursorPos) 111 | } 112 | } 113 | 114 | private fun applyParinfer(text: String, cursor: Int) { 115 | val s = inputField!!.text 116 | 117 | s.replace(0, s.length, text, 0, text.length) 118 | try { 119 | inputField!!.setSelection(cursor) 120 | } catch (e: IndexOutOfBoundsException) { 121 | } 122 | } 123 | 124 | private fun displayError(error: String) { 125 | adapter!!.update(Item(SpannableString(error), ItemType.ERROR)) 126 | } 127 | 128 | private fun displayInput(input: String) { 129 | adapter!!.update(Item(SpannableString(input), ItemType.INPUT)) 130 | } 131 | 132 | private fun displayOutput(output: SpannableString) { 133 | if (!suppressPrinting) { 134 | adapter!!.update(Item(output, ItemType.OUTPUT)) 135 | } 136 | } 137 | 138 | private fun toAbsolutePath(path: String): File? { 139 | return if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { 140 | val rpath = if (path.startsWith("/")) path.drop(1) else path 141 | val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) 142 | dir.mkdirs() 143 | dir.resolve(rpath) 144 | } else { 145 | null 146 | } 147 | } 148 | 149 | private var selectedPosition = -1 150 | private var selectedView: View? = null 151 | 152 | private fun isRequire(s: String): Boolean { 153 | return s.trimStart().startsWith("(require") 154 | } 155 | 156 | private fun isMacro(s: String): Boolean { 157 | val _s = s.trimStart() 158 | return _s.startsWith("(defmacro") || _s.startsWith("(defmacfn") 159 | } 160 | 161 | private var consentedToChivorcam = false 162 | private var suppressPrinting = false 163 | 164 | private fun defmacroCalled(s: String) { 165 | if (consentedToChivorcam) { 166 | suppressPrinting = true 167 | eval("(require '[chivorcam.core :refer [defmacro defmacfn]])") 168 | eval(s) 169 | } else { 170 | val builder = AlertDialog.Builder(this) 171 | builder.setTitle("Enable REPL\nMacro Definitions?") 172 | builder.setMessage( 173 | "ClojureScript macros must be defined in a separate namespace and required appropriately." + 174 | "\n\nFor didactic purposes, we can support defining macros directly in the Replete REPL. " + 175 | "\n\nAny helper functions called during macroexpansion must be defined using defmacfn in lieu of defn." 176 | ) 177 | builder.setPositiveButton( 178 | "OK" 179 | ) { dialog, id -> 180 | consentedToChivorcam = true 181 | suppressPrinting = true 182 | eval("(require '[chivorcam.core :refer [defmacro defmacfn]])") 183 | eval(s) 184 | } 185 | builder.setNegativeButton( 186 | "Cancel" 187 | ) { dialog, id -> 188 | dialog.cancel() 189 | } 190 | builder.show() 191 | } 192 | } 193 | 194 | var isExecutingTask = false 195 | 196 | private fun disableEvalButton() { 197 | isExecutingTask = true 198 | evalButton!!.isEnabled = false 199 | evalButton!!.setTextColor(Color.GRAY) 200 | } 201 | 202 | private fun enableEvalButton() { 203 | if (inputField!!.text.isNotEmpty()) { 204 | evalButton!!.isEnabled = true 205 | evalButton!!.setTextColor(Color.rgb(0, 153, 204)) 206 | } 207 | isExecutingTask = false 208 | } 209 | 210 | private fun eval(input: String) { 211 | disableEvalButton() 212 | sendThMessage(Messages.EVAL, input) 213 | } 214 | 215 | private fun updateWidth() { 216 | if (isVMLoaded) { 217 | val replHistory: ListView = findViewById(R.id.repl_history) 218 | val width: Double = (replHistory.width / 29).toDouble() 219 | sendThMessage(Messages.SET_WIDTH, width) 220 | } 221 | } 222 | 223 | private var deviceType: String? = null 224 | 225 | private fun setDeviceType() { 226 | val metrics = DisplayMetrics() 227 | windowManager.defaultDisplay.getMetrics(metrics) 228 | 229 | val yInches = metrics.heightPixels / metrics.ydpi; 230 | val xInches = metrics.widthPixels / metrics.xdpi; 231 | val diagonalInches = Math.sqrt((xInches * xInches + yInches * yInches).toDouble()); 232 | deviceType = if (diagonalInches >= 6.5) { 233 | "iPad" 234 | } else { 235 | "iPhone" 236 | } 237 | } 238 | 239 | override fun onConfigurationChanged(cfg: Configuration) { 240 | super.onConfigurationChanged(cfg) 241 | if (resources.configuration.orientation != cfg.orientation) { 242 | updateWidth() 243 | } 244 | } 245 | 246 | var evalButton: Button? = null 247 | var inputField: EditText? = null 248 | 249 | var uiHandler: Handler? = null 250 | var thHandler: Handler? = null 251 | 252 | private fun sendThMessage(what: Messages, obj: Any? = null) { 253 | if (obj != null) { 254 | thHandler!!.sendMessage(thHandler!!.obtainMessage(what.value, obj)) 255 | } else { 256 | thHandler!!.sendMessage(thHandler!!.obtainMessage(what.value)) 257 | } 258 | } 259 | 260 | private fun sendUIMessage(what: Messages, obj: Any? = null) { 261 | if (obj != null) { 262 | uiHandler!!.sendMessage(uiHandler!!.obtainMessage(what.value, obj)) 263 | } else { 264 | uiHandler!!.sendMessage(uiHandler!!.obtainMessage(what.value)) 265 | } 266 | } 267 | 268 | var vm: V8? = null 269 | 270 | private fun initializeVMThread() { 271 | thHandler = VMHandler( 272 | mainLooper, 273 | { what, obj -> sendUIMessage(what, obj) }, 274 | { s -> bundleGetContents(s) }, 275 | { s -> toAbsolutePath(s) } 276 | ) 277 | } 278 | 279 | fun runParinfer(s: String, enterPressed: Boolean, cursorPos: Int) { 280 | val params = V8Array(vm!!).push(s).push(cursorPos).push(enterPressed) 281 | val ret = vm!!.getObject("replete").getObject("repl").executeArrayFunction("format", params) 282 | val text = ret[0] as String 283 | val cursor = ret[1] as Int 284 | 285 | applyParinfer(text, cursor) 286 | 287 | params.release() 288 | ret.release() 289 | } 290 | 291 | inner class CopyActionCallback(val parent: AdapterView<*>, val clipboard: ClipboardManager) : ActionMode.Callback { 292 | 293 | override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { 294 | val inflater = mode.menuInflater 295 | inflater.inflate(R.menu.menu_actions, menu) 296 | return true 297 | } 298 | 299 | override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { 300 | return false 301 | } 302 | 303 | override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { 304 | return when (item.itemId) { 305 | R.id.copy_action -> { 306 | val _item = parent.getItemAtPosition(selectedPosition) 307 | 308 | if (_item != null) { 309 | val sitem = parent.getItemAtPosition(selectedPosition) as Item 310 | clipboard.setPrimaryClip(ClipData.newPlainText("input", sitem.text)) 311 | selectedPosition = -1 312 | (selectedView as View).setBackgroundColor(Color.rgb(255, 255, 255)) 313 | } 314 | 315 | mode.finish() 316 | true 317 | } 318 | else -> false 319 | } 320 | } 321 | 322 | override fun onDestroyActionMode(mode: ActionMode?) { 323 | 324 | } 325 | } 326 | 327 | override fun onCreate(savedInstanceState: Bundle?) { 328 | super.onCreate(savedInstanceState) 329 | 330 | val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() 331 | StrictMode.setThreadPolicy(policy) 332 | 333 | uiHandler = object : Handler(Looper.getMainLooper()) { 334 | override fun handleMessage(msg: Message) { 335 | when (msg.what) { 336 | Messages.ADD_INPUT_ITEM.value -> displayInput(msg.obj as String) 337 | Messages.ADD_OUTPUT_ITEM.value -> displayOutput(msg.obj as SpannableString) 338 | Messages.ADD_ERROR_ITEM.value -> displayError(msg.obj as String) 339 | Messages.ENABLE_EVAL.value -> enableEvalButton() 340 | Messages.ENABLE_PRINTING.value -> suppressPrinting = false 341 | Messages.UPDATE_WIDTH.value -> updateWidth() 342 | Messages.VM_LOADED.value -> { 343 | vm = msg.obj as V8 344 | vm!!.locker.acquire() 345 | isVMLoaded = true 346 | } 347 | Messages.INIT_FAILED.value -> { 348 | val payload = msg.obj as InitFailedPayload 349 | vm = payload.vm 350 | vm!!.locker.acquire() 351 | displayError(payload.message) 352 | } 353 | } 354 | } 355 | } 356 | 357 | initializeVMThread() 358 | setDeviceType() 359 | setContentView(R.layout.activity_main) 360 | 361 | val typeface = Typeface.createFromAsset(getAssets(), "fonts/FiraCode-Regular.otf") 362 | 363 | inputField = findViewById(R.id.input) 364 | val replHistory: ListView = findViewById(R.id.repl_history) 365 | evalButton = findViewById(R.id.eval_button) 366 | 367 | inputField!!.requestFocus() 368 | inputField!!.hint = "Enter form here" 369 | inputField!!.setHintTextColor(Color.GRAY) 370 | inputField!!.setTypeface(typeface) 371 | 372 | evalButton!!.isEnabled = false 373 | evalButton!!.setTextColor(Color.GRAY) 374 | 375 | adapter = HistoryAdapter(this, R.layout.list_item, typeface, replHistory) 376 | 377 | replHistory.adapter = adapter 378 | replHistory.divider = null 379 | 380 | val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 381 | 382 | replHistory.setOnItemClickListener { parent, view, position, id -> 383 | if (position == selectedPosition) { 384 | selectedPosition = -1 385 | view.setBackgroundColor(Color.rgb(255, 255, 255)) 386 | } else { 387 | if (selectedPosition != -1 && selectedView != null) { 388 | (selectedView as View).setBackgroundColor(Color.rgb(255, 255, 255)) 389 | } 390 | selectedPosition = position 391 | view.setBackgroundColor(Color.rgb(219, 220, 255)) 392 | selectedView = view 393 | 394 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 395 | (selectedView as View).startActionMode( 396 | CopyActionCallback(parent, clipboard), 397 | ActionMode.TYPE_FLOATING 398 | ) 399 | } else { 400 | (selectedView as View).startActionMode(CopyActionCallback(parent, clipboard)) 401 | } 402 | } 403 | } 404 | 405 | var isParinferChange = false 406 | var enterPressed = false 407 | 408 | inputField!!.addTextChangedListener(object : TextWatcher { 409 | override fun afterTextChanged(s: Editable?) { 410 | if (s != null) { 411 | evalButton!!.isEnabled = !s.isNullOrEmpty() && isVMLoaded && !isExecutingTask 412 | if (evalButton!!.isEnabled) { 413 | evalButton!!.setTextColor(Color.rgb(0, 153, 204)) 414 | } else { 415 | evalButton!!.setTextColor(Color.GRAY) 416 | } 417 | if (!s.isNullOrEmpty() && !isParinferChange) { 418 | isParinferChange = true 419 | 420 | if (isVMLoaded) { 421 | val cursorPos = inputField!!.selectionStart 422 | runParinfer(s.toString(), enterPressed, cursorPos) 423 | enterPressed = false 424 | } else { 425 | runPoorMansParinfer(inputField!!, s) 426 | } 427 | } else { 428 | isParinferChange = false 429 | } 430 | } 431 | } 432 | 433 | override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { 434 | } 435 | 436 | override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { 437 | if (p0 != null && p0.length > p1 && p0[p1] == "\n"[0]) { 438 | enterPressed = true 439 | } 440 | } 441 | }) 442 | 443 | evalButton!!.setOnClickListener { v -> 444 | val input = inputField!!.text.toString() 445 | inputField!!.text.clear() 446 | sendUIMessage(Messages.ADD_INPUT_ITEM, input) 447 | 448 | uiHandler!!.post { 449 | try { 450 | if (isMacro(input)) { 451 | defmacroCalled(input) 452 | } else { 453 | eval(input) 454 | } 455 | } catch (e: Exception) { 456 | sendUIMessage(Messages.ADD_ERROR_ITEM, e.toString()) 457 | } 458 | } 459 | 460 | } 461 | 462 | sendUIMessage( 463 | Messages.ADD_INPUT_ITEM, "\nClojureScript ${getClojureScriptVersion()}\n" + 464 | " Docs: (doc function-name)\n" + 465 | " (find-doc \"part-of-name\")\n" + 466 | " Source: (source function-name)\n" + 467 | " Results: Stored in *1, *2, *3,\n" + 468 | " an exception in *e\n" 469 | ) 470 | 471 | sendThMessage(Messages.INIT_ENV, deviceType) 472 | } 473 | } 474 | 475 | -------------------------------------------------------------------------------- /app/src/main/java/com/fikesfarm/Replete/VMHandler.kt: -------------------------------------------------------------------------------- 1 | package com.fikesfarm.Replete 2 | 3 | import android.os.* 4 | import com.eclipsesource.v8.* 5 | import com.eclipsesource.v8.utils.V8Runnable 6 | import java.io.* 7 | import java.lang.StringBuilder 8 | import java.net.HttpURLConnection 9 | import java.net.URL 10 | import java.nio.ByteBuffer 11 | import java.nio.charset.StandardCharsets 12 | import kotlin.concurrent.thread 13 | 14 | data class InitFailedPayload(val message: String, val vm: V8) 15 | 16 | class TimeoutThread(val callback: () -> Unit, val t: Long) : Thread() { 17 | var isTimeoutCanceled = false 18 | override fun run() { 19 | Thread.sleep(t) 20 | if (!isTimeoutCanceled) { 21 | callback() 22 | } 23 | } 24 | } 25 | 26 | class IntervalThread(val callback: () -> Unit, val onCanceled: () -> Unit, val t: Long) : Thread() { 27 | var isIntervalCanceled = false 28 | override fun run() { 29 | while (true) { 30 | Thread.sleep(t) 31 | if (isIntervalCanceled) { 32 | onCanceled() 33 | break 34 | } else { 35 | callback() 36 | } 37 | } 38 | } 39 | } 40 | 41 | class VMHandler( 42 | val mainLooper: Looper, 43 | val sendUIMessage: (Messages, Any?) -> Unit, 44 | val bundleGetContents: (String) -> String?, 45 | val toAbsolutePath: (String) -> File? 46 | ) : Handler(mainLooper) { 47 | var vm: V8? = null 48 | 49 | override fun handleMessage(msg: Message) { 50 | when (msg.what) { 51 | Messages.INIT_ENV.value -> _initEnv(msg.obj as String) 52 | Messages.EVAL.value -> eval(msg.obj as String) 53 | Messages.SET_WIDTH.value -> setWidth(msg.obj as Double) 54 | Messages.CALL_FN.value -> callFn(msg.obj as V8Function) 55 | Messages.RELEASE_OBJ.value -> releaseObject(msg.obj as V8Object) 56 | } 57 | } 58 | 59 | private fun _initEnv(deviceType: String) { 60 | thread { 61 | vm = V8.createV8Runtime() 62 | populateEnv(vm!!) 63 | bootstrapEnv(vm!!, deviceType) 64 | } 65 | } 66 | 67 | private fun callFn(fn: V8Function) { 68 | fn.call(fn, V8Array(vm)) 69 | } 70 | 71 | private fun releaseObject(obj: V8Object) { 72 | obj.release() 73 | } 74 | 75 | private fun setWidth(width: Double) { 76 | vm!!.getObject("replete").getObject("repl").executeFunction("set_width", V8Array(vm).push(width)) 77 | } 78 | 79 | private fun bootstrapEnv(vm: V8, deviceType: String) { 80 | val deps_file_path = "main.js" 81 | val goog_base_path = "goog/base.js" 82 | 83 | try { 84 | vm.executeScript("var global = this;") 85 | 86 | vm.executeScript("CLOSURE_IMPORT_SCRIPT = function(src) { AMBLY_IMPORT_SCRIPT('goog/' + src); return true; }") 87 | 88 | val googBaseScript = bundleGetContents(goog_base_path) 89 | val depsScript = bundleGetContents(deps_file_path) 90 | if (googBaseScript != null) { 91 | vm.executeScript(googBaseScript) 92 | if (depsScript != null) { 93 | vm.executeScript(depsScript) 94 | } 95 | } 96 | 97 | vm.executeScript("goog.require('cljs.core');") 98 | vm.executeScript("goog.isProvided_ = function(x) { return false; };") 99 | vm.executeScript( 100 | "goog.require__ = goog.require;\n" + 101 | "goog.require = (src, reload) => {\n" + 102 | " if (reload === \"reload-all\") {\n" + 103 | " goog.cljsReloadAll_ = true;\n" + 104 | " }\n" + 105 | " if (reload || goog.cljsReloadAll_) {\n" + 106 | " if (goog.debugLoader_) {\n" + 107 | " let path = goog.debugLoader_.getPathFromDeps_(src);\n" + 108 | " goog.object.remove(goog.debugLoader_.written_, path);\n" + 109 | " goog.object.remove(goog.debugLoader_.written_, goog.basePath + path);\n" + 110 | " } else {\n" + 111 | " let path = goog.object.get(goog.dependencies_.nameToPath, src);\n" + 112 | " goog.object.remove(goog.dependencies_.visited, path);\n" + 113 | " goog.object.remove(goog.dependencies_.written, path);\n" + 114 | " goog.object.remove(goog.dependencies_.visited, goog.basePath + path);\n" + 115 | " }\n" + 116 | " }\n" + 117 | " let ret = goog.require__(src);\n" + 118 | " if (reload === \"reload-all\") {\n" + 119 | " goog.cljsReloadAll_ = false;\n" + 120 | " }\n" + 121 | " if (goog.isInModuleLoader_()) {\n" + 122 | " return goog.module.getInternal_(src);\n" + 123 | " } else {\n" + 124 | " return ret;\n" + 125 | " }\n" + 126 | "};" 127 | ) 128 | 129 | vm.executeScript("goog.provide('cljs.user');") 130 | vm.executeScript("goog.require('cljs.core');") 131 | vm.executeScript("goog.require('replete.repl');") 132 | vm.executeScript("replete.repl.setup_cljs_user();") 133 | vm.executeScript("replete.repl.init_app_env({'debug-build': false, 'target-simulator': false, 'user-interface-idiom': '$deviceType'});") 134 | vm.executeScript("cljs.core.system_time = REPLETE_HIGH_RES_TIMER;") 135 | vm.executeScript("cljs.core.set_print_fn_BANG_.call(null, REPLETE_PRINT_FN);") 136 | vm.executeScript("cljs.core.set_print_err_fn_BANG_.call(null, REPLETE_PRINT_FN);") 137 | vm.executeScript("var window = global;") 138 | 139 | vm.locker.release() 140 | 141 | sendUIMessage(Messages.VM_LOADED, vm) 142 | sendUIMessage(Messages.UPDATE_WIDTH, null) 143 | sendUIMessage(Messages.ENABLE_EVAL, null) 144 | } catch (e: V8ScriptExecutionException) { 145 | vm.locker.release() 146 | val baos = ByteArrayOutputStream() 147 | e.printStackTrace(PrintStream(baos, true, "UTF-8")) 148 | sendUIMessage( 149 | Messages.INIT_FAILED, 150 | InitFailedPayload( 151 | String( 152 | baos.toByteArray(), 153 | StandardCharsets.UTF_8 154 | ), vm 155 | ) 156 | ) 157 | } 158 | } 159 | 160 | private fun populateEnv(vm: V8) { 161 | vm.registerJavaMethod(repleteLoad, "REPLETE_LOAD") 162 | vm.registerJavaMethod(repletePrintFn, "REPLETE_PRINT_FN") 163 | vm.registerJavaMethod(amblyImportScript, "AMBLY_IMPORT_SCRIPT") 164 | vm.registerJavaMethod(repleteHighResTimer, "REPLETE_HIGH_RES_TIMER") 165 | vm.registerJavaMethod(repleteRequest, "REPLETE_REQUEST") 166 | 167 | vm.registerJavaMethod(repleteWriteStdout, "REPLETE_RAW_WRITE_STDOUT") 168 | vm.registerJavaMethod(repleteFlushStdout, "REPLETE_RAW_FLUSH_STDOUT") 169 | vm.registerJavaMethod(repleteWriteStderr, "REPLETE_RAW_WRITE_STDERR") 170 | vm.registerJavaMethod(repleteFlushStderr, "REPLETE_RAW_FLUSH_STDERR") 171 | 172 | vm.registerJavaMethod(repleteIsDirectory, "REPLETE_IS_DIRECTORY") 173 | vm.registerJavaMethod(repleteListFiles, "REPLETE_LIST_FILES") 174 | vm.registerJavaMethod(repleteDeleteFile, "REPLETE_DELETE") 175 | vm.registerJavaMethod(repleteCopyFile, "REPLETE_COPY") 176 | vm.registerJavaMethod(repleteMakeParentDirectories, "REPLETE_MKDIRS") 177 | 178 | vm.registerJavaMethod(repleteFileReaderOpen, "REPLETE_FILE_READER_OPEN") 179 | vm.registerJavaMethod(repleteFileReaderRead, "REPLETE_FILE_READER_READ") 180 | vm.registerJavaMethod(repleteFileReaderClose, "REPLETE_FILE_READER_CLOSE") 181 | 182 | vm.registerJavaMethod(repleteFileWriterOpen, "REPLETE_FILE_WRITER_OPEN") 183 | vm.registerJavaMethod(repleteFileWriterWrite, "REPLETE_FILE_WRITER_WRITE") 184 | vm.registerJavaMethod(repleteFileWriterFlush, "REPLETE_FILE_WRITER_FLUSH") 185 | vm.registerJavaMethod(repleteFileWriterClose, "REPLETE_FILE_WRITER_CLOSE") 186 | 187 | vm.registerJavaMethod(repleteFileInputStreamOpen, "REPLETE_FILE_INPUT_STREAM_OPEN") 188 | vm.registerJavaMethod(repleteFileInputStreamRead, "REPLETE_FILE_INPUT_STREAM_READ") 189 | vm.registerJavaMethod(repleteFileInputStreamClose, "REPLETE_FILE_INPUT_STREAM_CLOSE") 190 | 191 | vm.registerJavaMethod(repleteFileOutputStreamOpen, "REPLETE_FILE_OUTPUT_STREAM_OPEN") 192 | vm.registerJavaMethod(repleteFileOutputStreamWrite, "REPLETE_FILE_OUTPUT_STREAM_WRITE") 193 | vm.registerJavaMethod(repleteFileOutputStreamFlush, "REPLETE_FILE_OUTPUT_STREAM_FLUSH") 194 | vm.registerJavaMethod(repleteFileOutputStreamClose, "REPLETE_FILE_OUTPUT_STREAM_CLOSE") 195 | 196 | vm.registerJavaMethod(repleteFStat, "REPLETE_FSTAT") 197 | vm.registerJavaMethod(repleteSleep, "REPLETE_SLEEP") 198 | 199 | vm.registerJavaMethod(repleteSetTimeout, "setTimeout") 200 | vm.registerJavaMethod(repleteCancelTimeout, "clearTimeout") 201 | 202 | vm.registerJavaMethod(repleteSetInterval, "setInterval") 203 | vm.registerJavaMethod(repleteCancelInterval, "clearInterval") 204 | } 205 | 206 | private fun eval(s: String) { 207 | vm!!.getObject("replete").getObject("repl").executeFunction("read_eval_print", V8Array(vm).push(s)) 208 | sendUIMessage(Messages.ENABLE_EVAL, null) 209 | sendUIMessage(Messages.ENABLE_PRINTING, null) 210 | } 211 | 212 | private var intervalId: Long = 0 213 | private val intervals: MutableMap = mutableMapOf() 214 | 215 | private fun setInterval(callback: V8Function, t: Long): Long { 216 | 217 | if (intervalId == 9007199254740991) { 218 | intervalId = 0; 219 | } else { 220 | ++intervalId; 221 | } 222 | 223 | val tt = IntervalThread( 224 | { 225 | this.sendMessage(this.obtainMessage(Messages.CALL_FN.value, callback)) 226 | }, 227 | { this.sendMessage(this.obtainMessage(Messages.RELEASE_OBJ.value, callback)) }, 228 | t 229 | ) 230 | intervals[intervalId] = tt 231 | 232 | tt.start() 233 | 234 | return intervalId 235 | } 236 | 237 | private fun cancelInterval(tid: Long) { 238 | if (intervals.contains(tid)) { 239 | intervals[tid]!!.isIntervalCanceled = true 240 | intervals.remove(tid) 241 | } 242 | } 243 | 244 | private val repleteSetInterval = JavaCallback { receiver, parameters -> 245 | if (parameters.length() == 2) { 246 | val callback = parameters.get(0) as V8Function 247 | val timeout = parameters.getDouble(1).toLong() 248 | val tid = setInterval(callback, timeout) 249 | 250 | return@JavaCallback tid.toDouble() 251 | } else { 252 | return@JavaCallback V8.getUndefined() 253 | } 254 | } 255 | 256 | private val repleteCancelInterval = JavaCallback { receiver, parameters -> 257 | if (parameters.length() == 1) { 258 | val tid = parameters.getInteger(0).toLong() 259 | cancelInterval(tid) 260 | } 261 | return@JavaCallback V8.getUndefined() 262 | } 263 | 264 | private var timeoutId: Long = 0 265 | private val timeouts: MutableMap = mutableMapOf() 266 | 267 | private fun setTimeout(callback: V8Function, t: Long): Long { 268 | 269 | if (timeoutId == 9007199254740991) { 270 | timeoutId = 0; 271 | } else { 272 | ++timeoutId; 273 | } 274 | 275 | val tt = 276 | TimeoutThread({ 277 | this.sendMessage(this.obtainMessage(Messages.CALL_FN.value, callback)) 278 | this.sendMessage(this.obtainMessage(Messages.RELEASE_OBJ.value, callback)) 279 | }, t) 280 | timeouts[timeoutId] = tt 281 | 282 | tt.start() 283 | 284 | return timeoutId 285 | } 286 | 287 | private fun cancelTimeout(tid: Long) { 288 | if (timeouts.contains(tid)) { 289 | timeouts[tid]!!.isTimeoutCanceled = true 290 | timeouts.remove(tid) 291 | } 292 | } 293 | 294 | private val repleteSetTimeout = JavaCallback { receiver, parameters -> 295 | if (parameters.length() == 2) { 296 | val callback = parameters.get(0) as V8Function 297 | val timeout = parameters.getDouble(1).toLong() 298 | val tid = setTimeout(callback, timeout) 299 | 300 | return@JavaCallback tid.toDouble() 301 | } else { 302 | return@JavaCallback V8.getUndefined() 303 | } 304 | } 305 | 306 | private val repleteCancelTimeout = JavaCallback { receiver, parameters -> 307 | if (parameters.length() == 1) { 308 | val tid = parameters.getInteger(0).toLong() 309 | cancelTimeout(tid) 310 | } 311 | return@JavaCallback V8.getUndefined() 312 | } 313 | 314 | private val repleteHighResTimer = JavaCallback { receiver, parameters -> 315 | System.nanoTime() / 1e6 316 | } 317 | 318 | private val repleteRequest = JavaCallback { receiver, parameters -> 319 | if (parameters.length() == 1 && parameters.get(0) is V8Object) { 320 | val opts = parameters.getObject(0) 321 | 322 | val url = try { 323 | URL(opts.getString("url")) 324 | } catch (e: V8ResultUndefined) { 325 | null 326 | } 327 | 328 | val timeout = try { 329 | opts.getInteger("timeout") * 1000 330 | } catch (e: V8ResultUndefined) { 331 | 0 332 | } 333 | 334 | val binaryResponse = try { 335 | opts.getBoolean("binary-response") 336 | } catch (e: V8ResultUndefined) { 337 | false 338 | } 339 | 340 | val method = try { 341 | opts.getString("method") 342 | } catch (e: V8ResultUndefined) { 343 | "GET" 344 | } 345 | 346 | val body = try { 347 | opts.getString("body") 348 | } catch (e: V8ResultUndefined) { 349 | null 350 | } 351 | 352 | val headers = try { 353 | opts.getObject("headers") 354 | } catch (e: V8ResultUndefined) { 355 | null 356 | } 357 | 358 | val followRedirects = try { 359 | opts.getBoolean("follow-redirects") 360 | } catch (e: V8ResultUndefined) { 361 | false 362 | } 363 | 364 | val userAgent = try { 365 | opts.getString("user-agent") 366 | } catch (e: V8ResultUndefined) { 367 | null 368 | } 369 | 370 | val insecure = try { 371 | opts.getBoolean("insecure") 372 | } catch (e: V8ResultUndefined) { 373 | false 374 | } 375 | 376 | val socket = try { 377 | opts.getString("socket") 378 | } catch (e: V8ResultUndefined) { 379 | null 380 | } 381 | 382 | opts.release() 383 | 384 | if (url != null) { 385 | val conn = url.openConnection() as HttpURLConnection 386 | 387 | conn.allowUserInteraction = false 388 | conn.requestMethod = method 389 | conn.readTimeout = timeout 390 | conn.connectTimeout = timeout 391 | conn.instanceFollowRedirects = followRedirects 392 | 393 | if (userAgent != null) { 394 | conn.setRequestProperty("User-Agent", userAgent) 395 | } 396 | 397 | if (headers != null) { 398 | for (key in headers.keys) { 399 | val value = headers.getString(key) 400 | conn.setRequestProperty(key, value) 401 | } 402 | } 403 | 404 | if (body != null) { 405 | val ba = body.toByteArray() 406 | conn.setRequestProperty("Content-Length", ba.size.toString()) 407 | conn.doInput = true; 408 | conn.doOutput = true; 409 | conn.useCaches = false; 410 | 411 | val os = conn.outputStream 412 | os.write(body.toByteArray()) 413 | os.close() 414 | } 415 | 416 | try { 417 | conn.connect() 418 | 419 | val result = V8Object(vm) 420 | 421 | val responseBytes = conn.inputStream.readBytes() 422 | val responseCode = conn.responseCode 423 | val responseHeaders = V8Object(vm) 424 | 425 | for (entry in conn.headerFields.entries) { 426 | val values = StringBuilder() 427 | for (value in entry.value) { 428 | values.append(value, ",") 429 | } 430 | if (entry.key != null) { 431 | responseHeaders.add(entry.key, values.toString()) 432 | } 433 | } 434 | 435 | result.add("status", responseCode) 436 | result.add("headers", responseHeaders) 437 | 438 | if (binaryResponse) { 439 | result.add("body", V8ArrayBuffer(vm, ByteBuffer.wrap(responseBytes))) 440 | } else { 441 | result.add("body", String(responseBytes)) 442 | } 443 | 444 | return@JavaCallback result 445 | } catch (e: Exception) { 446 | val result = V8Object(vm) 447 | result.add("error", e.message) 448 | return@JavaCallback result 449 | } 450 | } else { 451 | return@JavaCallback V8.getUndefined() 452 | } 453 | } else { 454 | return@JavaCallback V8.getUndefined() 455 | } 456 | } 457 | 458 | private val repleteLoad = JavaCallback { receiver, parameters -> 459 | if (parameters.length() == 1) { 460 | val path = parameters.getString(0) 461 | return@JavaCallback bundleGetContents(path) 462 | } else { 463 | return@JavaCallback V8.getUndefined() 464 | } 465 | } 466 | 467 | private val loadedLibs = mutableSetOf() 468 | 469 | private val amblyImportScript = JavaCallback { receiver, parameters -> 470 | if (parameters.length() == 1) { 471 | var path = parameters.getString(0) 472 | 473 | if (!loadedLibs.contains(path)) { 474 | 475 | if (path.startsWith("goog/../")) { 476 | path = path.substring(8, path.length) 477 | } 478 | 479 | val script = bundleGetContents(path) 480 | 481 | if (script != null) { 482 | loadedLibs.add(path) 483 | vm!!.executeScript(script) 484 | } 485 | } 486 | } 487 | return@JavaCallback V8.getUndefined() 488 | } 489 | 490 | private val repletePrintFn = JavaCallback { receiver, parameters -> 491 | if (parameters.length() == 1) { 492 | val msg = parameters.getString(0) 493 | 494 | sendUIMessage(Messages.ADD_OUTPUT_ITEM, markString(msg)) 495 | } 496 | return@JavaCallback V8.getUndefined() 497 | } 498 | 499 | private val repleteWriteStdout = JavaCallback { receiver, params -> 500 | if (params.length() == 1) { 501 | val s = params.getString(0) 502 | System.out.write(s.toByteArray()) 503 | } 504 | return@JavaCallback V8.getUndefined() 505 | } 506 | 507 | private val repleteFlushStdout = JavaCallback { receiver, params -> 508 | System.out.flush() 509 | return@JavaCallback V8.getUndefined() 510 | } 511 | 512 | private val repleteWriteStderr = JavaCallback { receiver, params -> 513 | if (params.length() == 1) { 514 | val s = params.getString(0) 515 | System.err.write(s.toByteArray()) 516 | } 517 | return@JavaCallback V8.getUndefined() 518 | } 519 | 520 | private val repleteFlushStderr = JavaCallback { receiver, params -> 521 | System.err.flush() 522 | return@JavaCallback V8.getUndefined() 523 | } 524 | 525 | private val repleteIsDirectory = JavaCallback { receiver, params -> 526 | if (params.length() == 1) { 527 | val path = toAbsolutePath(params.getString(0)) 528 | 529 | if (path != null) { 530 | return@JavaCallback path.isDirectory 531 | } else { 532 | return@JavaCallback V8.getUndefined() 533 | } 534 | 535 | } else { 536 | return@JavaCallback V8.getUndefined() 537 | } 538 | } 539 | 540 | private val repleteListFiles = JavaCallback { receiver, params -> 541 | if (params.length() == 1) { 542 | val path = toAbsolutePath(params.getString(0)) 543 | val ret = V8Array(vm) 544 | 545 | path?.list()?.forEach { p -> ret.push(p.toString()) } 546 | 547 | return@JavaCallback ret 548 | } else { 549 | return@JavaCallback V8.getUndefined() 550 | } 551 | } 552 | 553 | private val repleteDeleteFile = JavaCallback { receiver, params -> 554 | if (params.length() == 1) { 555 | val path = params.getString(0) 556 | 557 | try { 558 | toAbsolutePath(path)?.delete() 559 | } catch (e: IOException) { 560 | sendUIMessage(Messages.ADD_ERROR_ITEM, e.toString()) 561 | } 562 | 563 | } 564 | return@JavaCallback V8.getUndefined() 565 | } 566 | 567 | private val repleteCopyFile = JavaCallback { receiver, params -> 568 | if (params.length() == 2) { 569 | val fromPath = params.getString(0) 570 | val toPath = params.getString(1) 571 | val fromStream = toAbsolutePath(fromPath)?.inputStream() 572 | val toStream = toAbsolutePath(toPath)?.outputStream() 573 | 574 | if (fromStream != null && toStream != null) { 575 | try { 576 | fromStream.copyTo(toStream) 577 | fromStream.close() 578 | toStream.close() 579 | } catch (e: IOException) { 580 | fromStream.close() 581 | toStream.close() 582 | sendUIMessage(Messages.ADD_ERROR_ITEM, e.toString()) 583 | } 584 | } 585 | } 586 | return@JavaCallback V8.getUndefined() 587 | } 588 | 589 | private val repleteMakeParentDirectories = JavaCallback { receiver, params -> 590 | if (params.length() == 1) { 591 | val path = params.getString(0) 592 | val absPath = toAbsolutePath(path) 593 | 594 | try { 595 | if (absPath != null && !absPath.exists()) { 596 | absPath.mkdirs() 597 | } 598 | } catch (e: Exception) { 599 | sendUIMessage(Messages.ADD_ERROR_ITEM, e.toString()) 600 | } 601 | 602 | } 603 | return@JavaCallback V8.getUndefined() 604 | } 605 | 606 | private val openOutputStreams = mutableMapOf() 607 | 608 | private val repleteFileOutputStreamOpen = JavaCallback { receiver, params -> 609 | if (params.length() == 2) { 610 | val path = params.getString(0) 611 | val append = params.getBoolean(1) 612 | 613 | openOutputStreams[path] = FileOutputStream(toAbsolutePath(path), append) 614 | 615 | return@JavaCallback path 616 | } else { 617 | return@JavaCallback "0" 618 | } 619 | } 620 | 621 | private val repleteFileOutputStreamWrite = JavaCallback { receiver, params -> 622 | if (params.length() == 2) { 623 | val path = params.getString(0) 624 | val bytesArray = params.getArray(1) 625 | 626 | try { 627 | val bytes = ByteArray(bytesArray.length()) 628 | for (idx in 0 until bytes.size - 1) { 629 | bytes[idx] = bytesArray[idx] as Byte 630 | } 631 | openOutputStreams[path]!!.write(bytes) 632 | } catch (e: Exception) { 633 | return@JavaCallback e.message 634 | } 635 | return@JavaCallback V8.getUndefined() 636 | } else { 637 | return@JavaCallback "This functions accepts 2 arguments" 638 | } 639 | } 640 | 641 | private val repleteFileOutputStreamFlush = JavaCallback { receiver, params -> 642 | if (params.length() == 1) { 643 | val path = params.getString(0) 644 | 645 | try { 646 | openOutputStreams[path]!!.flush() 647 | } catch (e: Exception) { 648 | return@JavaCallback e.message 649 | } 650 | return@JavaCallback V8.getUndefined() 651 | } else { 652 | return@JavaCallback "This functions accepts 1 argument" 653 | } 654 | } 655 | 656 | private val repleteFileOutputStreamClose = JavaCallback { receiver, params -> 657 | if (params.length() == 1) { 658 | val path = params.getString(0) 659 | 660 | try { 661 | openOutputStreams[path]!!.close() 662 | openOutputStreams.remove(path) 663 | } catch (e: Exception) { 664 | return@JavaCallback e.message 665 | } 666 | 667 | return@JavaCallback V8.getUndefined() 668 | } else { 669 | return@JavaCallback "This functions accepts 1 argument" 670 | } 671 | } 672 | 673 | private val repleteFStat = JavaCallback { receiver, params -> 674 | if (params.length() == 1) { 675 | val path = params.getString(0) 676 | val item = toAbsolutePath(path) 677 | val ret = V8Object(vm) 678 | 679 | if (item != null) { 680 | val itemType = if (item.isFile) "file" else if (item.isDirectory) "directory" else "unknown" 681 | ret.add("type", itemType) 682 | ret.add("modified", item.lastModified().toDouble()) 683 | } 684 | 685 | return@JavaCallback ret 686 | } else { 687 | 688 | } 689 | } 690 | 691 | private val repleteSleep = JavaVoidCallback { receiver, params -> 692 | if (params.length() == 1 || params.length() == 2) { 693 | val ms = params.getDouble(0).toLong() 694 | val ns = if (params.length() == 1) 0 else params.getDouble(1).toInt() 695 | 696 | Thread.sleep(ms, ns) 697 | } 698 | } 699 | 700 | private val openWriteFiles = mutableMapOf() 701 | 702 | private val repleteFileWriterOpen = JavaCallback { receiver, params -> 703 | if (params.length() == 3) { 704 | val path = params.getString(0) 705 | val append = params.getBoolean(1) 706 | val encoding = params.getString(2) 707 | 708 | openWriteFiles[path] = 709 | FileOutputStream(toAbsolutePath(path), append).writer(Charsets.UTF_8) 710 | return@JavaCallback path 711 | } else { 712 | return@JavaCallback "0" 713 | } 714 | } 715 | 716 | private val repleteFileWriterWrite = JavaCallback { receiver, params -> 717 | if (params.length() == 2) { 718 | val path = params.getString(0) 719 | val content = params.getString(1) 720 | 721 | try { 722 | openWriteFiles[path]!!.write(content) 723 | } catch (e: Exception) { 724 | return@JavaCallback e.message 725 | } 726 | return@JavaCallback V8.getUndefined() 727 | } else { 728 | return@JavaCallback "This functions accepts 2 arguments" 729 | } 730 | } 731 | 732 | private val repleteFileWriterFlush = JavaCallback { receiver, params -> 733 | if (params.length() == 1) { 734 | val path = params.getString(0) 735 | 736 | try { 737 | openWriteFiles[path]!!.flush() 738 | } catch (e: Exception) { 739 | return@JavaCallback e.message 740 | } 741 | return@JavaCallback V8.getUndefined() 742 | } else { 743 | return@JavaCallback "This functions accepts 1 argument" 744 | } 745 | } 746 | 747 | private val repleteFileWriterClose = JavaCallback { receiver, params -> 748 | if (params.length() == 1) { 749 | val path = params.getString(0) 750 | 751 | try { 752 | openWriteFiles[path]!!.close() 753 | openWriteFiles.remove(path) 754 | } catch (e: Exception) { 755 | return@JavaCallback e.message 756 | } 757 | 758 | return@JavaCallback V8.getUndefined() 759 | } else { 760 | return@JavaCallback "This functions accepts 1 argument" 761 | } 762 | } 763 | 764 | private val openInputStreams = mutableMapOf() 765 | 766 | private val repleteFileInputStreamOpen = JavaCallback { receiver, params -> 767 | if (params.length() == 1) { 768 | val path = params.getString(0) 769 | val apath = toAbsolutePath(path) 770 | 771 | if (apath != null) { 772 | openInputStreams[path] = apath.inputStream() 773 | return@JavaCallback path 774 | } else { 775 | return@JavaCallback "0" 776 | } 777 | 778 | } else { 779 | return@JavaCallback "0" 780 | } 781 | } 782 | 783 | private val repleteFileInputStreamRead = JavaCallback { receiver, params -> 784 | if (params.length() == 1) { 785 | val path = params.getString(0) 786 | val bytes = ByteArray(1024) 787 | val bytesWritten = openInputStreams[path]!!.read(bytes) 788 | 789 | if (bytesWritten == -1) { 790 | return@JavaCallback V8.getUndefined() 791 | } else { 792 | val ret = V8Array(vm) 793 | bytes.forEach { b -> ret.push(b) } 794 | return@JavaCallback ret 795 | } 796 | } else { 797 | return@JavaCallback V8.getUndefined() 798 | } 799 | } 800 | 801 | private val repleteFileInputStreamClose = JavaCallback { receiver, params -> 802 | if (params.length() == 1) { 803 | val path = params.getString(0) 804 | 805 | openInputStreams[path]!!.close() 806 | openInputStreams.remove(path) 807 | 808 | return@JavaCallback V8.getUndefined() 809 | } else { 810 | return@JavaCallback V8.getUndefined() 811 | } 812 | } 813 | 814 | private val openReadFiles = mutableMapOf() 815 | 816 | private val repleteFileReaderOpen = JavaCallback { receiver, params -> 817 | if (params.length() == 2) { 818 | val path = params.getString(0) 819 | val apath = toAbsolutePath(path) 820 | val encoding = params.getString(1) 821 | 822 | if (apath != null) { 823 | openReadFiles[path] = apath.inputStream().reader(Charsets.UTF_8) 824 | return@JavaCallback path 825 | } else { 826 | return@JavaCallback "0" 827 | } 828 | } else { 829 | return@JavaCallback "0" 830 | } 831 | } 832 | 833 | private val repleteFileReaderRead = JavaCallback { receiver, params -> 834 | if (params.length() == 1) { 835 | val path = params.getString(0) 836 | val content = openReadFiles[path]!!.read() 837 | 838 | if (content == -1) { 839 | return@JavaCallback V8Array(vm).push(V8.getUndefined()).push(V8.getUndefined()) 840 | } else { 841 | return@JavaCallback V8Array(vm).push(content.toChar().toString()).push(V8.getUndefined()) 842 | } 843 | } else { 844 | return@JavaCallback V8.getUndefined() 845 | } 846 | } 847 | 848 | private val repleteFileReaderClose = JavaCallback { receiver, params -> 849 | if (params.length() == 1) { 850 | val path = params.getString(0) 851 | 852 | openReadFiles[path]!!.close() 853 | openReadFiles.remove(path) 854 | 855 | return@JavaCallback V8.getUndefined() 856 | } else { 857 | return@JavaCallback V8.getUndefined() 858 | } 859 | } 860 | } 861 | -------------------------------------------------------------------------------- /app/src/main/java/com/fikesfarm/Replete/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.fikesfarm.Replete.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/fikesfarm/Replete/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.fikesfarm.Replete.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun RepleteREPLTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/fikesfarm/Replete/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.fikesfarm.Replete.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/fikesfarm/Replete/utils.kt: -------------------------------------------------------------------------------- 1 | package com.fikesfarm.Replete 2 | 3 | fun time(msg: String, block: () -> T): T { 4 | val start = System.currentTimeMillis() 5 | val ret = block() 6 | val t = System.currentTimeMillis() - start 7 | println("====== $msg: $t") 8 | return ret 9 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 |