├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SCRIPTS.md ├── TODO ├── crates ├── irust │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── args.rs │ │ ├── dependencies.rs │ │ ├── irust.rs │ │ ├── irust │ │ ├── art.rs │ │ ├── engine.rs │ │ ├── format.rs │ │ ├── help.rs │ │ ├── highlight.rs │ │ ├── highlight │ │ │ └── theme.rs │ │ ├── history.rs │ │ ├── options.rs │ │ ├── parser.rs │ │ ├── ra.rs │ │ ├── ra │ │ │ └── rust_analyzer.rs │ │ └── script │ │ │ ├── mod.rs │ │ │ └── script_manager.rs │ │ ├── main.rs │ │ └── utils.rs ├── irust_api │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── irust_repl │ ├── Cargo.toml │ ├── README.md │ ├── examples │ │ └── re │ │ │ ├── log.rs │ │ │ └── main.rs │ ├── irust_kernel │ │ ├── evcxr.ipynb │ │ ├── irust.ipynb │ │ ├── irust_kernel │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ ├── install.py │ │ │ ├── kernel.py │ │ │ └── resources │ │ │ │ ├── __init__.py │ │ │ │ └── logo-svg.svg │ │ └── pyproject.toml │ ├── src │ │ ├── cargo_cmds.rs │ │ ├── compile_mode.rs │ │ ├── edition.rs │ │ ├── executor.rs │ │ ├── lib.rs │ │ ├── main_result.rs │ │ ├── toolchain.rs │ │ └── utils.rs │ └── tests │ │ └── repl.rs └── printer │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── benches │ └── printer.rs │ ├── examples │ └── shell.rs │ └── src │ ├── buffer.rs │ ├── lib.rs │ ├── printer.rs │ └── printer │ ├── cursor.rs │ ├── cursor │ ├── bound.rs │ └── raw.rs │ ├── tests.rs │ ├── writer.rs │ └── writer │ └── raw.rs ├── distro ├── io.github.sigmasd.IRust.desktop ├── io.github.sigmasd.IRust.metainfo.xml └── io.github.sigmasd.IRust.svg ├── irust.png ├── script_examples ├── Cargo.lock ├── Cargo.toml ├── fun │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── ipython │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── irust_animation │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── irust_prompt │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── irust_vim_dylib │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── script.rs └── mixed_cmds │ ├── Cargo.toml │ └── src │ └── main.rs └── tests ├── deno_bot_test.ts └── irust_bot_test.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | #Copy paste from crossterm 2 | name: irust ci 3 | 4 | on: 5 | # Build master branch only 6 | push: 7 | branches: 8 | - master 9 | - dev 10 | # Build pull requests targeting master branch only 11 | pull_request: 12 | branches: 13 | - master 14 | 15 | jobs: 16 | test: 17 | name: ${{matrix.rust}} on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, windows-latest, macOS-latest] 22 | rust: [stable, nightly] 23 | # Allow failures on nightly, it's just informative 24 | include: 25 | - rust: stable 26 | can-fail: false 27 | - rust: nightly 28 | can-fail: true 29 | steps: 30 | - name: Checkout Repository 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 1 34 | - name: Install Rust 35 | uses: dtolnay/rust-toolchain@master 36 | with: 37 | toolchain: ${{ matrix.rust }} 38 | components: rustfmt,clippy 39 | - name: Toolchain Information 40 | run: | 41 | rustc --version 42 | rustfmt --version 43 | rustup --version 44 | cargo --version 45 | - name: Check Formatting 46 | if: matrix.rust == 'stable' 47 | run: cargo fmt --all -- --check 48 | continue-on-error: ${{ matrix.can-fail }} 49 | - name: Clippy 50 | run: cargo clippy -- -D clippy::all 51 | continue-on-error: ${{ matrix.can-fail }} 52 | - name: Test Build 53 | run: cargo build 54 | continue-on-error: ${{ matrix.can-fail }} 55 | 56 | # TODO: figure out how to unflake this 57 | # - name: Install deno 58 | # uses: denoland/setup-deno@v2 59 | 60 | # - name: End to End test with deno 61 | # run: deno -A ./tests/deno_bot_test.ts 62 | 63 | # - name: Test Packaging 64 | # if: matrix.rust == 'stable' 65 | # run: cargo package --manifest-path crates/irust/Cargo.toml 66 | # continue-on-error: ${{ matrix.can-fail }} 67 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Binary 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'irust@[0-9]+.[0-9]+.[0-9]+' 7 | # Manual trigger 8 | workflow_dispatch: 9 | inputs: 10 | tag: 11 | description: 'Tag for release (e.g., irust@0.1.0)' 12 | required: true 13 | default: 'test-release' 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | build: 20 | name: Release libs 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest, windows-latest, macos-13, macos-latest] 25 | 26 | steps: 27 | - name: Checkout Repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Install Rust toolchain 31 | uses: dtolnay/rust-toolchain@master 32 | with: 33 | toolchain: stable 34 | 35 | # Build for Musl 36 | - if: runner.os == 'Linux' 37 | name: Build Linux musl binary 38 | run: | 39 | rustup target add x86_64-unknown-linux-musl 40 | cargo build --release --target=x86_64-unknown-linux-musl 41 | #################################### 42 | 43 | - name: Build 44 | if: runner.os != 'Linux' 45 | run: cargo build --release 46 | 47 | - if: matrix.os == 'macos-13' 48 | name: Upload MacOS x86_64 Binary 49 | uses: svenstaro/upload-release-action@v2 50 | with: 51 | file: target/release/irust 52 | asset_name: irust-x86_64-apple-darwin 53 | tag: ${{ github.event.inputs.tag || github.ref }} 54 | overwrite: true 55 | 56 | - if: matrix.os == 'macos-latest' 57 | name: Upload MacOS aarch64 Binary 58 | uses: svenstaro/upload-release-action@v2 59 | with: 60 | file: target/release/irust 61 | asset_name: irust-aarch64-apple-darwin 62 | tag: ${{ github.event.inputs.tag || github.ref }} 63 | overwrite: true 64 | 65 | - if: runner.os == 'Linux' 66 | name: Upload Linux binary 67 | uses: svenstaro/upload-release-action@v2 68 | with: 69 | file: target/x86_64-unknown-linux-musl/release/irust 70 | asset_name: irust-x86_64-unknown-linux-musl 71 | tag: ${{ github.event.inputs.tag || github.ref }} 72 | overwrite: true 73 | 74 | - if: runner.os == 'Windows' 75 | name: Upload Windows binary 76 | uses: svenstaro/upload-release-action@v2 77 | with: 78 | file: target/release/irust.exe 79 | asset_name: irust-x86_64-pc-windows-msvc.exe 80 | tag: ${{ github.event.inputs.tag || github.ref }} 81 | overwrite: true 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | target/ 3 | crates/irust_repl/.ipynb_checkpoints 4 | crates/irust_repl/irust_kernel/dist/ 5 | crates/irust_repl/irust_kernel/irust_kernel/__pycache__/ 6 | crates/irust_repl/irust_kernel/irust_kernel/resources/__pycache__/ 7 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: Preload 3 | before: | 4 | rustup update stable 5 | cargo install --path crates/irust 6 | cargo install cargo-edit cargo-show-asm cargo-expand 7 | rustup component add rust-analyzer 8 | echo 'export PATH="$PATH:$(rustc --print sysroot)/bin"' >> ~/.bashrc 9 | - name: IRust 10 | command: | 11 | echo "Welcome to IRust. Just type 'irust' to begin." 12 | echo "Once the REPL starts, type ':help' for a list of commands." 13 | echo "Learn more at https://github.com/sigmaSd/IRust" 14 | github: 15 | prebuilds: 16 | master: true 17 | branches: true 18 | pullRequestsFromForks: true 19 | addLabel: prebuilt-in-gitpod 20 | vscode: 21 | extensions: 22 | - belfz.search-crates-io 23 | - serayuzgur.crates 24 | - bungcip.better-toml 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "crates/irust", 6 | "crates/printer", 7 | "crates/irust_api", 8 | "crates/irust_repl", 9 | ] 10 | 11 | [profile.release] 12 | strip = true 13 | lto = true 14 | 15 | 16 | # flamegraph 17 | # [profile.release] 18 | # debug = true 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 sigmaSd 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 | crates/irust/README.md -------------------------------------------------------------------------------- /SCRIPTS.md: -------------------------------------------------------------------------------- 1 | # Scripts 2 | **Release `1.30.0`:** 3 | 4 | Script v4 is now the only scripting interface available. (uses [rscript](https://github.com/sigmaSd/Rscript))\ 5 | The API is here https://github.com/sigmaSd/IRust/blob/master/crates/irust_api/src/lib.rs \ 6 | Scripts should depend on `irust_api` and `rscript` crates 7 | 8 | Script examples are located here https://github.com/sigmaSd/IRust/tree/master/script_examples 9 | 10 | ## Usage: 11 | - Set `activate_scripting` to `true` in config file. 12 | - Compile a script (it can be oneshot/daemon/dylib(unsafe)), see examples 13 | - Copy it to ~/.config/irust/script/ 14 | 15 | That's it you can verify that scripts are detected with `:scripts`\ 16 | You can activate/deactivate scripts with `:script $myscript activate` (or deactivate) 17 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | ------------------------------------------------ 2 | TODO 3 | ------------------------------------------------ 4 | - switch to glibc from musl (https://nickb.dev/blog/default-musl-allocator-considered-harmful-to-performance/) 5 | - use rust 2024 for prelude 6 | - add debug: false in the repl, to optimze compile speed 7 | 8 | 9 | 10 | ------------------------------------------------ 11 | // Old stuff ahead, I don't remember what these are 12 | ------------------------------------------------ 13 | 14 | // TODO 15 | Off screen rendering 16 | Actual unicde support 17 | 18 | 19 | Cranelift support 20 | Make all options modifiable at runtime 21 | Racer autocomplete commands arguments 22 | Script register action 23 | Add &Buffer to rust api 24 | 25 | Investigate how `git tag` can use `vscode` synchronously 26 | Rustc explain errors 27 | 28 | 29 | //new 30 | Reports scripts errors 31 | 32 | 33 | windows cmds , utf8 34 | -------------------------------------------------------------------------------- /crates/irust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "irust" 3 | version = "1.73.0" 4 | authors = ["Nbiba Bedis "] 5 | edition = "2024" 6 | readme = "README.md" 7 | description = "Cross Platform Rust Repl" 8 | repository = "https://github.com/sigmaSd/IRust" 9 | license = "MIT" 10 | 11 | [package.metadata.binstall] 12 | pkg-url = "{ repo }/releases/download/irust@{ version }/{ name }-{ target }{ binary-ext }" 13 | pkg-fmt = "bin" 14 | 15 | [dependencies] 16 | crossterm = { version = "0.27.0", features = ["serde", "use-dev-tty"] } 17 | dirs = "5.0.1" 18 | toml = "0.7.6" 19 | serde = { version = "1.0.188", features = ["derive"] } 20 | printer = { path = "../printer/", version = "0.7.0" } 21 | irust_api = { path = "../irust_api/", version = "0.31.0" } 22 | irust_repl = { path = "../irust_repl", version = "0.24.0", features = [ 23 | "serde", 24 | ] } 25 | rscript = "0.17.0" 26 | rustc_lexer = { version = "727.0.0", package = "rustc-ap-rustc_lexer" } 27 | serde_json = "1.0.105" 28 | 29 | [target.'cfg(unix)'.dependencies] 30 | libc = "0.2.147" 31 | 32 | [features] 33 | default = [] 34 | no-welcome-screen = [] 35 | 36 | # flamegraph 37 | # [profile.release] 38 | # debug = true 39 | -------------------------------------------------------------------------------- /crates/irust/README.md: -------------------------------------------------------------------------------- 1 | # IRust 2 | Cross Platform Rust Repl 3 | 4 | You can try out IRust with no installation or setup (via Gitpod.io) by visiting https://gitpod.io/#https://github.com/sigmaSd/IRust 5 | 6 | ## Keywords / Tips & Tricks 7 | 8 | **:help** => print help, use `:help full` for the full version 9 | 10 | **:reset** => reset repl 11 | 12 | **:show** => show repl current code (optionally depends on [rustfmt](https://github.com/rust-lang/rustfmt) to format output) 13 | 14 | **:add** ** => add dependencies also it accepts most `cargo add` arguments, for example you can import local dependencies with `:add --path path_to_crate` 15 | 16 | **:type** *\* => shows the expression type, example `:type vec!(5)` 17 | 18 | **:time** *\* => return the amount of time the expression took to execute. example: `:time 5+4` `:time my_fun(arg1,arg2)` 19 | 20 | **:time_release** *\* => same as `time` command but with release mode 21 | 22 | **:load** => load a rust file into the repl 23 | 24 | **:reload** => reload the last specified file 25 | 26 | **:pop** => remove last repl code line 27 | 28 | **:del** ** => remove a specific line from repl code (line count starts at 1 from the first expression statement) 29 | 30 | **:edit** *[editor]* => edit internal buffer using an external editor, example: `:edit micro`. If no editor is specified then the one from the EDITOR environment variable is used (if set). Note some gui terminal requires using `:sync` command after the edit (vscode) 31 | 32 | **:sync** sync the changes written after using :edit with a gui editor (vscode) to the repl 33 | 34 | **:cd** => change current working directory 35 | 36 | **:color** *\* *\* => change token highlight color at runtime, for the token list and value representation check the Theme section, exp: `:color function red` `:color macro #ff12ab` `:color reset` 37 | 38 | **:toolchain** *\* => switch between toolchains, supported value are: `stable`, `beta`, `nightly`, `default` 39 | 40 | **:theme** *\* => if used without arguments list currently installed themes, otherwise set irust to the given theme, see Themes section for more info 41 | 42 | **:check_statements** *true*/*false* => If its set to true, irust will check each statemnt (input that ends with ;) with cargo_check before inserting it to the repl 43 | 44 | **:bench** => run `cargo bench` 45 | 46 | **:asm** *\* => shows assembly of the specified function, note that the function needs to be public, and there has to be no free standing statements/expressions (requires [cargo-show-asm](https://github.com/pacak/cargo-show-asm)) 47 | 48 | **:executor** *\* => set the executor to be used by IRust, available options are: `sync` `tokio` `async_std`, by using an async executor, `await` becomes usable with no other modifications for async executors) 49 | 50 | **:evaluator** *\\>* => set the evaluator statement, exmaple: `:evaluator println!("{}",{$$})` the `$$` 51 | will be replaced by IRust by the input code (the default evaluator uses debug formatting). To reset the evaluator to default you can use `:evaluator reset` 52 | 53 | **:scripts:** => if invoked with no arguments it prints a list of detected scripts, if invoked with on argument it print that script info if it exits, if invoked with 2 arguments, it tries to activate/deactivate a script, example: `:scripts Vim deactivate` 54 | 55 | **:compile_time** *\* => if set to on, IRust will print compiling time on each input, compile time includes rustc compiling + some IRust code (should be marginal) 56 | 57 | **:compile_mode** *\* => Sets how cargo will compile the code in release or debug mode 58 | 59 | **:main_result** *\* => Change main result type, available options are `Unit` and `Result` (which is Result\<(), Box\>), Using `Result` as type allows to use `?` in the repl without any boilerplate 60 | 61 | **:dbg** *\* => Spawn rust-lldb/rust-gdb with (an optional expression), example: `:dbg` or `:dbg fact(12)`, The debugger can be specified in the config file 62 | 63 | **:expand** *\[function\]* => Shows the result of macro expansion, requires https://github.com/dtolnay/cargo-expand, function is optional, example `fn b() { println!("42"); }` then `:expand b` 64 | 65 | **:exit** | **:quit** => Exit IRust immediately 66 | 67 | **$$** => Shell commands can be interpolated with rust code with '$$', for example: `let a = $$ls -l$$;`, this feature can be [en/dis]abled via the config file 68 | 69 | **::** => run a shell command, example `::ls` 70 | 71 | You can use arrow keys to cycle through commands history. 72 | 73 | You can disable all colors by setting `NO_COLOR` env variable. 74 | 75 | To enable completion with tab via rust-analyzer, set `enable_rust_analyzer` to true in the config. 76 | 77 | ## Keybindings 78 | 79 | **ctrl-l** clear screen 80 | 81 | **ctrl-c** clear line 82 | 83 | **ctrl-d** exit if buffer is empty 84 | 85 | **ctrl-z** [unix only] send IRust to the background 86 | 87 | **ctrl-r** search history, hitting **ctrl-r** again continues searching the history backward, hitting **ctrl-s** searches the history forward 88 | 89 | **ctrl-left/right** jump through words 90 | 91 | **HOME/END** go to line start / line end 92 | 93 | **Tab/ShiftTab** cycle through completion suggestions 94 | 95 | **Alt-Enter | ctrl-s** add line break 96 | 97 | **ctrl-e** force evaluation 98 | 99 | **ctrl-o**->**[+-]key** Start recording a macro and saved on the specified key, if **ctrl-o** is clicked again the recording is stopped 100 | 101 | **ctrl-p**->**key** Play a macro saved on the specified key 102 | 103 | **ctrl-u** Undo 104 | 105 | **ctrl-y** Redo 106 | 107 | **ctrl-x** Delete current line 108 | 109 | 110 | 111 | ## Cli commands 112 | 113 | **--help** prints help message 114 | 115 | **--reset-config** reset IRust configuration to default 116 | 117 | If input is piped to IRust then it will evaluate it and exit, example: `echo '"hello".chars()' | irust` 118 | 119 | ## Configuration 120 | 121 | IRust config file is located in: 122 | 123 | **Linux**: */home/$USER/.config/irust/config.toml* 124 | 125 | **Win**: *C:\Users\\$USER\AppData\Roaming/irust/config.toml* 126 | 127 | **Mac**: */Users/$USER/Library/Application Support/irust/config.toml* 128 | 129 | *default config:* 130 | ```toml 131 | # history 132 | add_irust_cmd_to_history = true 133 | add_shell_cmd_to_history = false 134 | 135 | # colors 136 | ok_color = "Blue" 137 | eval_color = "White" 138 | irust_color = "DarkBlue" 139 | irust_warn_color = "Cyan" 140 | out_color = "Red" 141 | shell_color = "DarkYellow" 142 | err_color = "DarkRed" 143 | input_color = "Green" 144 | insert_color = "White" 145 | welcome_msg = "" 146 | welcome_color = "DarkBlue" 147 | 148 | # Rust analyzer 149 | ra_inline_suggestion_color = "Cyan" 150 | ra_suggestions_table_color = "Green" 151 | ra_selected_suggestion_color = "DarkRed" 152 | ra_max_suggestions = 5 153 | enable_rust_analyzer = false 154 | 155 | # other 156 | first_irust_run = false 157 | toolchain = "stable" 158 | check_statements = true 159 | auto_insert_semicolon = true 160 | 161 | #use last output by replacing the specified marker 162 | replace_marker = "$out" 163 | replace_output_with_marker = false 164 | 165 | # modify input prmopt 166 | input_prompt = "In: " 167 | output_prompt = "Out: " 168 | 169 | # activate scripting feature 170 | activate_scripting = false 171 | 172 | # select executor (Sync, Tokio, Asyncstd) 173 | executor = "Sync" 174 | evaluator = ["println!(\"{:?}\", {\n", "\n});"] 175 | compile_time = false 176 | main_result = "Unit" 177 | show_warnings = false 178 | edition = "E2021" 179 | debugger = "LLDB" 180 | shell_interpolate = true 181 | local_server = false 182 | local_server_adress = "127.0.0.1:9000" 183 | theme = "default" 184 | ``` 185 | 186 | ## Theme 187 | Since release `1.66.0` `IRust` can now parse any theme file located under `$config_dir/irust/themes` and use it for the highlighting colors. 188 | 189 | To select a theme, set its name in the irust config. for example to set `themes/mytheme.toml` set `theme = "mytheme"` 190 | 191 | Colors can be specified as names ("red") or as hex representation ("#ff12ab"). 192 | 193 | Default theme file (default.toml): 194 | 195 | ```toml 196 | keyword = "magenta" 197 | keyword2 = "dark_red" 198 | function = "blue" 199 | type = "cyan" 200 | symbol = "red" 201 | macro = "dark_yellow" 202 | literal = "yellow" 203 | lifetime = "dark_magenta" 204 | comment = "dark_grey" 205 | const = "dark_green" 206 | ident = "white" 207 | ``` 208 | 209 | ## Prelude 210 | IRust automatically creates `irust_prelude` crate at `xdg_data_dir/irust/irust_prelude`, this crate is imported at startup, any changes to it (that are marked with `pub`) will be immediately reflected on the repl after saving. 211 | 212 | ## Scripts 213 | IRust supports scripting, all over the code base there are hooks that scripts can react to and usually answer back to IRust with a command.\ 214 | Check out [SCRIPTS.md](https://github.com/sigmaSd/IRust/blob/master/SCRIPTS.md) for more info. 215 | 216 | ## Vim Plugin 217 | For nvim you can use https://github.com/hkupty/iron.nvim (needs irust 1.67.4) 218 | 219 | **Old method:** 220 | 221 | Since version `1.60.0` IRust supports spawning a local server, by changing `local_server` to `true` in the configuration file.\ 222 | This allows it to be controlled programmatically, which in turns allows writing vim plugins that uses this, see https://github.com/sigmaSd/irust-vim-plugin 223 | 224 | ## Jupyter Notebook 225 | A Jupyter Kernel is available, see https://github.com/sigmaSd/IRust/blob/master/crates/irust_repl/README.md#jupyter-kernel for instructions 226 | 227 | ## Book 228 | `The IRust Book` is intended to document a couple of tips and tricks https://sigmasd.github.io/irust_book 229 | 230 | ## Releases 231 | Automatic releases by github actions are uploaded here https://github.com/sigmaSd/irust/releases 232 | 233 | ## Install 234 | 235 | - `cargo install irust` 236 | - `cargo binstall irust` (using [cargo-binstall](https://github.com/cargo-bins/cargo-binstall)) 237 | 238 | ## Building 239 | cargo b --release 240 | 241 | ## How It Works (random drawing ahead) 242 | ![irust](https://github.com/sigmaSd/IRust/assets/22427111/867b4a7c-2f47-4756-bc45-5967448358b1) 243 | 244 | 245 | ## FAQ 246 | 247 | **1- I want to hack on irust but `dbg!` overlaps with the output!!** 248 | 249 | Personaly I do this: 250 | - Run 2 terminals side by side 251 | - run `tty` in the first which should output something like `/dev/pts/4` 252 | - run `cargo r 2>/dev/pts4` in the second 253 | 254 | Now the `dbg!` statements are printed on the second terminal and the output in the first terminal is not messed up. 255 | 256 | ## [Changelog](./CHANGELOG.md) 257 | -------------------------------------------------------------------------------- /crates/irust/src/args.rs: -------------------------------------------------------------------------------- 1 | use crate::irust::options::Options; 2 | 3 | use std::{ 4 | env, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 9 | 10 | pub enum ArgsResult { 11 | Exit, 12 | Proceed, 13 | ProceedWithScriptPath(PathBuf), 14 | ProceedWithDefaultConfig, 15 | } 16 | 17 | pub fn handle_args(args: &[String], options: &mut Options) -> ArgsResult { 18 | match args[0].as_str() { 19 | "-h" | "--help" => { 20 | println!( 21 | "IRust: Cross Platform Rust REPL 22 | version: {}\n 23 | config file is in {}\n 24 | irust {{path_to_rust_file}} will start IRust with the file loaded in the repl 25 | --help => shows this message 26 | --reset-config => reset IRust configuration to default 27 | --default-config => uses the default configuration for this run (it will not be saved)", 28 | VERSION, 29 | Options::config_path() 30 | .map(|p| p.to_string_lossy().to_string()) 31 | .unwrap_or_else(|| "??".into()) 32 | ); 33 | ArgsResult::Exit 34 | } 35 | 36 | "-v" | "--version" => { 37 | println!("{VERSION}"); 38 | ArgsResult::Exit 39 | } 40 | 41 | "--reset-config" => { 42 | options.reset(); 43 | ArgsResult::Proceed 44 | } 45 | "--default-config" => ArgsResult::ProceedWithDefaultConfig, 46 | maybe_path => { 47 | let path = Path::new(&maybe_path); 48 | if path.exists() { 49 | ArgsResult::ProceedWithScriptPath(path.to_path_buf()) 50 | } else { 51 | eprintln!("Unknown argument: {maybe_path}"); 52 | ArgsResult::Proceed 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/irust/src/dependencies.rs: -------------------------------------------------------------------------------- 1 | use crossterm::style::Stylize; 2 | use std::io; 3 | use std::process; 4 | 5 | use crate::irust::options::Options; 6 | 7 | struct Dep { 8 | name: &'static str, 9 | cmd: &'static str, 10 | function: &'static str, 11 | install: &'static dyn Fn() -> io::Result>, 12 | } 13 | impl Dep { 14 | fn new( 15 | name: &'static str, 16 | cmd: &'static str, 17 | function: &'static str, 18 | install: &'static dyn Fn() -> io::Result>, 19 | ) -> Self { 20 | Dep { 21 | name, 22 | cmd, 23 | function, 24 | install, 25 | } 26 | } 27 | } 28 | 29 | pub fn check_required_deps() -> bool { 30 | const REQUIRED_DEPS: &[&str] = &["cargo"]; 31 | for dep in REQUIRED_DEPS { 32 | if !dep_installed(dep) { 33 | eprintln!("{dep} is not insalled!\n{dep} is required for IRust to work."); 34 | return false; 35 | } 36 | } 37 | true 38 | } 39 | 40 | pub fn warn_about_opt_deps(options: &mut Options) { 41 | //TODO: add rust-analyzer 42 | let opt_deps: [Dep; 3] = [ 43 | Dep::new("rustfmt", "rustfmt", "beautifying repl code", &|| { 44 | if !dep_installed("rustup") { 45 | println!( 46 | "{}", 47 | "rustup is not installed.\nrustup is required to install rustfmt".red() 48 | ); 49 | return Err(io::Error::other("rustup is not installed")); 50 | } 51 | let cmd = ["rustup", "component", "add", "rustfmt"]; 52 | println!("{}", format!("Running: {cmd:?}").magenta()); 53 | 54 | Ok(vec![ 55 | process::Command::new(cmd[0]).args(&cmd[1..]).status()?, 56 | ]) 57 | }), 58 | Dep::new( 59 | "cargo-show-asm", 60 | "cargo-asm", 61 | "viewing functions assembly", 62 | &|| { 63 | let cmd = ["cargo", "install", "cargo-show-asm"]; 64 | println!("{}", format!("Running: {cmd:?}").magenta()); 65 | 66 | Ok(vec![ 67 | process::Command::new(cmd[0]).args(&cmd[1..]).status()?, 68 | ]) 69 | }, 70 | ), 71 | Dep::new( 72 | "cargo-expand", 73 | "cargo-expand", 74 | "showing the result of macro expansion", 75 | &|| { 76 | let cmd = ["cargo", "install", "cargo-expand"]; 77 | println!("{}", format!("Running: {cmd:?}").magenta()); 78 | 79 | Ok(vec![ 80 | process::Command::new(cmd[0]).args(&cmd[1..]).status()?, 81 | ]) 82 | }, 83 | ), 84 | ]; 85 | 86 | // only warn when irust is first used 87 | if !options.first_irust_run { 88 | return; 89 | } 90 | 91 | println!( 92 | "{}", 93 | "Hi and Welcome to IRust!\n\ 94 | This is a one time message\n\ 95 | IRust will check now for optional dependencies and offer to install them\n\ 96 | " 97 | .dark_blue() 98 | ); 99 | 100 | let mut installed_something = false; 101 | for dep in &opt_deps { 102 | if !dep_installed(dep.cmd) { 103 | println!(); 104 | println!( 105 | "{}", 106 | format!( 107 | "{} is not installed, it's required for {}\n\ 108 | Do you want IRust to install it? [Y/n]: ", 109 | dep.name, dep.function 110 | ) 111 | .yellow() 112 | ); 113 | let answer = { 114 | let mut a = String::new(); 115 | std::io::stdin() 116 | .read_line(&mut a) 117 | .expect("failed to read stdin"); 118 | a.trim().to_string() 119 | }; 120 | 121 | if answer.is_empty() || answer == "y" || answer == "Y" { 122 | match (dep.install)() { 123 | Ok(status) if status.iter().all(process::ExitStatus::success) => { 124 | println!( 125 | "{}", 126 | format!("{} sucessfully installed!\n", dep.name).green() 127 | ); 128 | installed_something = true; 129 | } 130 | _ => println!("{}", format!("error while installing {}", dep.name).red()), 131 | }; 132 | } 133 | } 134 | } 135 | options.first_irust_run = false; 136 | 137 | if installed_something { 138 | println!( 139 | "{}", 140 | "You might need to reload the shell inorder to update $PATH".yellow() 141 | ); 142 | } 143 | println!("{}", "Everything is set!".green()); 144 | } 145 | 146 | fn dep_installed(d: &str) -> bool { 147 | if let Err(e) = std::process::Command::new(d) 148 | .arg("-h") 149 | .stdout(std::process::Stdio::null()) 150 | .stderr(std::process::Stdio::null()) 151 | .spawn() 152 | { 153 | if e.kind() == std::io::ErrorKind::NotFound { 154 | return false; 155 | } 156 | } 157 | true 158 | } 159 | -------------------------------------------------------------------------------- /crates/irust/src/irust/art.rs: -------------------------------------------------------------------------------- 1 | use crate::irust::{IRust, Result}; 2 | use crossterm::style::Color; 3 | 4 | impl IRust { 5 | pub fn wait_add(&mut self, mut add_cmd: std::process::Child, msg: &str) -> Result<()> { 6 | self.printer.cursor.save_position(); 7 | self.printer.cursor.hide(); 8 | self.printer.writer.raw.set_fg(Color::Cyan)?; 9 | 10 | match self.wait_add_inner(&mut add_cmd, msg) { 11 | Ok(()) => { 12 | self.clean_art()?; 13 | 14 | use std::io::Read; 15 | if let Some(stderr) = add_cmd.stderr.as_mut() { 16 | let mut error = String::new(); 17 | stderr.read_to_string(&mut error)?; 18 | if !error.is_empty() { 19 | // show cargo-add stderr 20 | // it have useful info 21 | return Err(error.into()); 22 | } 23 | } 24 | Ok(()) 25 | } 26 | Err(e) => { 27 | self.clean_art()?; 28 | Err(e) 29 | } 30 | } 31 | } 32 | 33 | fn wait_add_inner(&mut self, add_cmd: &mut std::process::Child, msg: &str) -> Result<()> { 34 | self.printer.write_at( 35 | &format!(" {msg}ing dep [\\]"), 36 | 0, 37 | self.printer.cursor.current_pos().1, 38 | )?; 39 | loop { 40 | match add_cmd.try_wait() { 41 | Ok(None) => { 42 | self.printer.write_at( 43 | "\\", 44 | msg.len() + 10, 45 | self.printer.cursor.current_pos().1, 46 | )?; 47 | self.printer.write_at( 48 | "|", 49 | msg.len() + 10, 50 | self.printer.cursor.current_pos().1, 51 | )?; 52 | self.printer.write_at( 53 | "/", 54 | msg.len() + 10, 55 | self.printer.cursor.current_pos().1, 56 | )?; 57 | self.printer.write_at( 58 | "-", 59 | msg.len() + 10, 60 | self.printer.cursor.current_pos().1, 61 | )?; 62 | self.printer.write_at( 63 | "\\", 64 | msg.len() + 10, 65 | self.printer.cursor.current_pos().1, 66 | )?; 67 | self.printer.write_at( 68 | "|", 69 | msg.len() + 10, 70 | self.printer.cursor.current_pos().1, 71 | )?; 72 | self.printer.write_at( 73 | "/", 74 | msg.len() + 10, 75 | self.printer.cursor.current_pos().1, 76 | )?; 77 | self.printer.write_at( 78 | "-", 79 | msg.len() + 10, 80 | self.printer.cursor.current_pos().1, 81 | )?; 82 | } 83 | Err(e) => { 84 | return Err(e.into()); 85 | } 86 | Ok(Some(_)) => return Ok(()), 87 | } 88 | std::thread::sleep(std::time::Duration::from_millis(100)); 89 | } 90 | } 91 | 92 | fn clean_art(&mut self) -> Result<()> { 93 | self.printer.cursor.restore_position(); 94 | self.printer.write_newline(&self.buffer); 95 | self.printer.cursor.show(); 96 | self.printer.writer.raw.reset_color()?; 97 | Ok(()) 98 | } 99 | 100 | pub fn welcome(&mut self) -> Result<()> { 101 | let default_msg = "Welcome to IRust (type ':help' for more information)".to_string(); 102 | let msg = (|| { 103 | if let Some(msg) = self.trigger_set_msg_hook() { 104 | return self.fit_msg(&msg); 105 | } 106 | 107 | if !self.options.welcome_msg.is_empty() { 108 | self.fit_msg(&self.options.welcome_msg.clone()) 109 | } else { 110 | self.fit_msg(&default_msg) 111 | } 112 | })(); 113 | 114 | self.printer.writer.raw.set_fg(self.options.welcome_color)?; 115 | self.printer.writer.raw.write(&msg)?; 116 | self.printer.writer.raw.reset_color()?; 117 | 118 | self.printer.write_newline(&self.buffer); 119 | self.printer.write_newline(&self.buffer); 120 | 121 | Ok(()) 122 | } 123 | 124 | pub fn ferris(&mut self) -> String { 125 | r" 126 | _~^~^~_ 127 | \) / o o \ (/ 128 | '_ ¬ _' 129 | / '-----' \ 130 | " 131 | .lines() 132 | .skip(1) 133 | .map(|l| l.to_string() + "\n") 134 | .collect() 135 | } 136 | 137 | fn fit_msg(&mut self, msg: &str) -> String { 138 | let slash_num = self.printer.cursor.width() - msg.len(); 139 | let slash = "-".repeat(slash_num / 2); 140 | 141 | format!("{slash}{msg}{slash}") 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /crates/irust/src/irust/format.rs: -------------------------------------------------------------------------------- 1 | use crate::Options; 2 | use crossterm::style::Color; 3 | use printer::printer::{PrintQueue, PrinterItem}; 4 | use std::sync::OnceLock; 5 | 6 | static NO_COLOR: OnceLock = OnceLock::new(); 7 | /// Have the top precedence 8 | fn no_color() -> bool { 9 | *NO_COLOR.get_or_init(|| std::env::var("NO_COLOR").is_ok()) 10 | } 11 | 12 | pub fn format_err<'a>(original_output: &'a str, show_warnings: bool, repl_name: &str) -> String { 13 | const BEFORE_2021_END_TAG: &str = ": aborting due to "; 14 | // Relies on --color=always 15 | const ERROR_TAG: &str = "\u{1b}[0m\u{1b}[1m\u{1b}[38;5;9merror"; 16 | const WARNING_TAG: &str = "\u{1b}[0m\u{1b}[1m\u{1b}[33mwarning"; 17 | 18 | // These are more fragile, should be only used when NO_COLOR is on 19 | const ERROR_TAG_NO_COLOR: &str = "error["; 20 | const WARNING_TAG_NO_COLOR: &str = "warning: "; 21 | 22 | let go_to_start = |output: &'a str| -> Vec<&'a str> { 23 | if show_warnings { 24 | output 25 | .lines() 26 | .skip_while(|line| !line.contains(&format!("{repl_name} v0.1.0"))) 27 | .skip(1) 28 | .collect() 29 | } else { 30 | output 31 | .lines() 32 | .skip_while(|line| { 33 | if no_color() { 34 | !line.starts_with(ERROR_TAG_NO_COLOR) 35 | } else { 36 | !line.starts_with(ERROR_TAG) 37 | } 38 | }) 39 | .collect() 40 | } 41 | }; 42 | let go_to_end = |output: Box>| -> String { 43 | if show_warnings { 44 | output 45 | } else { 46 | Box::new(output.take_while(|line| { 47 | if no_color() { 48 | !line.starts_with(WARNING_TAG_NO_COLOR) 49 | } else { 50 | !line.starts_with(WARNING_TAG) 51 | } 52 | })) 53 | } 54 | .collect::>() 55 | .join("\n") 56 | }; 57 | 58 | let handle_error = |output: &'a str| { 59 | go_to_start(output) 60 | .into_iter() 61 | .take_while(|line| !line.contains(BEFORE_2021_END_TAG)) 62 | }; 63 | let handle_error_2021 = |output: &'a str| { 64 | go_to_start(output) 65 | .into_iter() 66 | .rev() 67 | .skip_while(|line| !line.is_empty()) 68 | .collect::>() 69 | .into_iter() 70 | .rev() 71 | }; 72 | 73 | let output: Box> = if original_output.contains(BEFORE_2021_END_TAG) { 74 | Box::new(handle_error(original_output)) 75 | } else { 76 | Box::new(handle_error_2021(original_output)) 77 | }; 78 | 79 | let formatted_error = go_to_end(output); 80 | // The formatting logic is ad-hoc, there will always be a chance of failure with a rust update 81 | // 82 | // So we do a sanity check here, if the formatted_error is empty (which means we failed to 83 | // format the output), ask the user to open a bug report with the original_output 84 | let irust_version = env!("CARGO_PKG_VERSION"); 85 | if !formatted_error.is_empty() { 86 | formatted_error 87 | } else { 88 | format!( 89 | "IRust {irust_version}: failed to format the error output.\nThis is a bug in IRust.\nFeel free to open a bug-report at https://github.com/sigmaSd/IRust/issues/new with the next text:\n\noriginal_output:\n{original_output}" 90 | ) 91 | } 92 | } 93 | 94 | pub fn format_err_printqueue(output: &str, show_warnings: bool, repl_name: &str) -> PrintQueue { 95 | PrinterItem::String(format_err(output, show_warnings, repl_name), Color::Red).into() 96 | } 97 | 98 | pub fn format_eval_output( 99 | options: &Options, 100 | status: std::process::ExitStatus, 101 | output: String, 102 | prompt: String, 103 | show_warnings: bool, 104 | repl_name: &str, 105 | new_lines_after_output: usize, 106 | ) -> Option { 107 | if !status.success() { 108 | return Some(format_err_printqueue(&output, show_warnings, repl_name)); 109 | } 110 | if output.trim() == "()" { 111 | return None; 112 | } 113 | 114 | let mut eval_output = PrintQueue::default(); 115 | eval_output.push(PrinterItem::String(prompt, options.out_color)); 116 | eval_output.push(PrinterItem::String(output, options.eval_color)); 117 | eval_output.add_new_line(new_lines_after_output); 118 | Some(eval_output) 119 | } 120 | 121 | fn check_is_err(s: &str) -> bool { 122 | !s.contains("[unoptimized + debuginfo]") 123 | } 124 | 125 | pub fn format_check_output( 126 | output: String, 127 | show_warnings: bool, 128 | repl_name: &str, 129 | ) -> Option { 130 | if check_is_err(&output) { 131 | Some(format_err_printqueue(&output, show_warnings, repl_name)) 132 | } else { 133 | None 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/irust/src/irust/help.rs: -------------------------------------------------------------------------------- 1 | use super::highlight::{highlight, theme::Theme}; 2 | use crate::irust::{IRust, Result}; 3 | use crossterm::style::Color; 4 | use printer::{ 5 | buffer::Buffer, 6 | printer::{PrintQueue, PrinterItem}, 7 | }; 8 | 9 | impl IRust { 10 | pub fn help(&mut self, buffer: String) -> Result { 11 | #[cfg(unix)] 12 | let readme = include_str!("../../README.md"); 13 | #[cfg(windows)] 14 | let readme = include_str!("..\\..\\README.md"); 15 | 16 | let compact = !buffer.contains("full"); 17 | 18 | Ok(parse_markdown(&readme.into(), &self.theme, compact)) 19 | } 20 | } 21 | 22 | fn parse_markdown(buffer: &Buffer, theme: &Theme, compact: bool) -> PrintQueue { 23 | let mut queue = PrintQueue::default(); 24 | 25 | let buffer = buffer.to_string(); 26 | let mut buffer = buffer.lines(); 27 | 28 | (|| -> Option<()> { 29 | loop { 30 | let line = buffer.next()?; 31 | if compact && line.starts_with(">() 54 | .join("\n"); 55 | 56 | for _ in 0..skipped_lines { 57 | let _ = buffer.next(); 58 | } 59 | 60 | queue.append(&mut highlight(&code.into(), theme)); 61 | } else { 62 | let mut line = line.chars().peekable(); 63 | 64 | (|| -> Option<()> { 65 | loop { 66 | let c = line.next()?; 67 | match c { 68 | '*' => { 69 | let mut star = String::new(); 70 | star.push('*'); 71 | 72 | let mut pending = None; 73 | let mut post_start_count = 0; 74 | 75 | while line.peek().is_some() { 76 | let c = line.next().unwrap(); 77 | if pending.is_none() && c != '*' { 78 | pending = Some(star.len()); 79 | } 80 | star.push(c); 81 | 82 | if let Some(pending) = pending { 83 | if c == '*' { 84 | post_start_count += 1; 85 | if pending == post_start_count { 86 | break; 87 | } 88 | } else { 89 | post_start_count = post_start_count.saturating_sub(1); 90 | } 91 | } 92 | } 93 | queue.push(PrinterItem::String(star, Color::Magenta)); 94 | } 95 | '`' => { 96 | let mut quoted = String::new(); 97 | quoted.push('`'); 98 | 99 | while line.peek().is_some() && line.peek() != Some(&'`') { 100 | quoted.push(line.next().unwrap()); 101 | } 102 | //push the closing quote 103 | if line.peek().is_some() { 104 | quoted.push(line.next().unwrap()); 105 | } 106 | queue.push(PrinterItem::String(quoted, Color::DarkGreen)); 107 | } 108 | '=' | '>' | '(' | ')' | '-' | '|' => { 109 | queue.push(PrinterItem::Char(c, Color::DarkRed)) 110 | } 111 | c => queue.push(PrinterItem::Char(c, Color::White)), 112 | } 113 | } 114 | })(); 115 | } 116 | queue.add_new_line(1); 117 | } 118 | })(); 119 | queue 120 | } 121 | -------------------------------------------------------------------------------- /crates/irust/src/irust/highlight.rs: -------------------------------------------------------------------------------- 1 | use crossterm::style::Color; 2 | use printer::buffer::Buffer; 3 | use printer::printer::{PrintQueue, PrinterItem}; 4 | use theme::Theme; 5 | pub mod theme; 6 | 7 | const PAREN_COLORS: [&str; 4] = ["red", "yellow", "green", "blue"]; 8 | 9 | pub fn highlight(buffer: &Buffer, theme: &Theme) -> PrintQueue { 10 | let mut print_queue = PrintQueue::default(); 11 | 12 | let buffer = buffer.to_string(); 13 | let rc_buf = std::rc::Rc::new(buffer.clone()); 14 | 15 | let mut token_range = 0..0; 16 | let tokens: Vec<_> = rustc_lexer::tokenize(&buffer).collect(); 17 | let mut paren_idx = 0_isize; 18 | 19 | macro_rules! push_to_printer { 20 | ($color: expr) => {{ 21 | let color = theme::theme_color_to_term_color($color).unwrap_or(Color::White); 22 | print_queue.push(PrinterItem::RcString( 23 | rc_buf.clone(), 24 | token_range.clone(), 25 | color, 26 | )); 27 | }}; 28 | } 29 | 30 | for (idx, token) in tokens.iter().enumerate() { 31 | token_range.start = token_range.end; 32 | token_range.end += token.len; 33 | let text = &buffer[token_range.clone()]; 34 | 35 | use rustc_lexer::TokenKind::*; 36 | match token.kind { 37 | Ident if KEYWORDS.contains(&text) => { 38 | push_to_printer!(&theme.keyword[..]); 39 | } 40 | Ident if KEYWORDS2.contains(&text) => { 41 | push_to_printer!(&theme.keyword2[..]); 42 | } 43 | Ident if TYPES.contains(&text) => { 44 | push_to_printer!(&theme.r#type[..]); 45 | } 46 | // const 47 | Ident if text.chars().all(char::is_uppercase) => { 48 | push_to_printer!(&theme.r#const[..]); 49 | } 50 | // macro 51 | Ident 52 | if matches!( 53 | peek_first_non_white_sapce(&tokens[idx + 1..]).map(|(_, k)| k), 54 | Some(Bang) 55 | ) => 56 | { 57 | push_to_printer!(&theme.r#macro[..]); 58 | } 59 | // function 60 | Ident if is_function(&tokens[idx + 1..]) => { 61 | push_to_printer!(&theme.function[..]); 62 | } 63 | UnknownPrefix | Unknown | Ident | RawIdent | Whitespace => { 64 | push_to_printer!(&theme.ident[..]); 65 | } 66 | LineComment { .. } | BlockComment { .. } => { 67 | push_to_printer!(&theme.comment[..]); 68 | } 69 | Literal { .. } => { 70 | push_to_printer!(&theme.literal[..]) 71 | } 72 | Lifetime { .. } => { 73 | push_to_printer!(&theme.lifetime[..]) 74 | } 75 | Colon | At | Pound | Tilde | Question | Dollar | Semi | Comma | Dot | Eq | Bang 76 | | Lt | Gt | Minus | And | Or | Plus | Star | Slash | Caret | Percent | OpenBrace 77 | | OpenBracket | CloseBrace | CloseBracket => { 78 | push_to_printer!(&theme.symbol[..]); 79 | } 80 | OpenParen => { 81 | if theme.paren_rainbow { 82 | push_to_printer!(PAREN_COLORS[paren_idx.unsigned_abs() % 4]); 83 | } else { 84 | print_queue.push(PrinterItem::Char('(', Color::White)); 85 | } 86 | paren_idx += 1; 87 | } 88 | CloseParen => { 89 | paren_idx -= 1; 90 | if theme.paren_rainbow { 91 | push_to_printer!(PAREN_COLORS[paren_idx.unsigned_abs() % 4]); 92 | } else { 93 | print_queue.push(PrinterItem::Char(')', Color::White)); 94 | } 95 | } 96 | }; 97 | } 98 | print_queue 99 | } 100 | 101 | fn peek_first_non_white_sapce( 102 | tokens: &[rustc_lexer::Token], 103 | ) -> Option<(usize, rustc_lexer::TokenKind)> { 104 | for (idx, token) in tokens.iter().enumerate() { 105 | if token.kind != rustc_lexer::TokenKind::Whitespace { 106 | return Some((idx, token.kind)); 107 | } 108 | } 109 | None 110 | } 111 | 112 | fn is_function(tokens: &[rustc_lexer::Token]) -> bool { 113 | let (idx, kind) = match peek_first_non_white_sapce(tokens) { 114 | Some((i, k)) => (i, k), 115 | None => return false, 116 | }; 117 | use rustc_lexer::TokenKind::*; 118 | match kind { 119 | OpenParen => true, 120 | Lt => true, 121 | Colon => is_function(&tokens[idx + 1..]), 122 | _ => false, 123 | } 124 | } 125 | 126 | // Splitting keywords for a nicer coloring 127 | // red blue green blue red white 128 | // exp: pub fn hello() let mut var 129 | const KEYWORDS: &[&str] = &[ 130 | "async", "await", "while", "use", "super", "self", "Self", "for", "impl", "trait", "type", 131 | "pub", "in", "const", "static", "match", "use", "mut", "continue", "loop", "break", "if", 132 | "else", "macro", 133 | ]; 134 | const KEYWORDS2: &[&str] = &["unsafe", "move", "fn", "let", "struct", "enum", "dyn"]; 135 | 136 | const TYPES: &[&str] = &[ 137 | "bool", "char", "usize", "isize", "u8", "i8", "u32", "i32", "u64", "i64", "u128", "i128", 138 | "str", "String", 139 | ]; 140 | -------------------------------------------------------------------------------- /crates/irust/src/irust/highlight/theme.rs: -------------------------------------------------------------------------------- 1 | use std::fs::DirEntry; 2 | 3 | use crate::irust::Result; 4 | use crossterm::style::Color; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | fn themes_path() -> Result { 8 | Ok(crate::utils::irust_dirs::config_dir() 9 | .ok_or("Error accessing config_dir")? 10 | .join("irust") 11 | .join("themes")) 12 | } 13 | 14 | pub fn theme(name: String) -> Result { 15 | let selected_theme_path = themes_path()?.join(name + ".toml"); 16 | 17 | let data = std::fs::read_to_string(selected_theme_path)?; 18 | 19 | Ok(toml::from_str(&data)?) 20 | } 21 | 22 | pub fn theme_or_create_default(name: String) -> Theme { 23 | let maybe_theme = theme(name); 24 | if let Ok(theme) = maybe_theme { 25 | return theme; 26 | } 27 | 28 | let _ = (|| -> Result<()> { 29 | std::fs::create_dir_all(themes_path()?)?; 30 | std::fs::write( 31 | themes_path()?.join("default.toml"), 32 | toml::to_string(&Theme::default())?, 33 | )?; 34 | Ok(()) 35 | })(); 36 | Theme::default() 37 | } 38 | 39 | pub fn installed_themes() -> Result> { 40 | Ok(std::fs::read_dir( 41 | crate::utils::irust_dirs::config_dir() 42 | .ok_or("Error accessing config_dir")? 43 | .join("irust") 44 | .join("themes"), 45 | )? 46 | .collect::>>()?) 47 | } 48 | 49 | #[derive(Deserialize, Serialize, Debug)] 50 | pub struct Theme { 51 | pub keyword: String, 52 | pub keyword2: String, 53 | pub function: String, 54 | pub r#type: String, 55 | pub symbol: String, 56 | pub r#macro: String, 57 | pub literal: String, 58 | pub lifetime: String, 59 | pub comment: String, 60 | pub r#const: String, 61 | pub ident: String, 62 | pub paren_rainbow: bool, 63 | } 64 | 65 | impl Theme { 66 | pub fn reset(&mut self) { 67 | *self = Self::default(); 68 | } 69 | } 70 | 71 | impl Default for Theme { 72 | fn default() -> Self { 73 | Self { 74 | keyword: "magenta".into(), 75 | keyword2: "dark_red".into(), 76 | function: "blue".into(), 77 | r#type: "cyan".into(), 78 | symbol: "red".into(), 79 | r#macro: "dark_yellow".into(), 80 | literal: "yellow".into(), 81 | lifetime: "dark_magenta".into(), 82 | comment: "dark_grey".into(), 83 | r#const: "dark_green".into(), 84 | ident: "white".into(), 85 | paren_rainbow: true, 86 | } 87 | } 88 | } 89 | 90 | pub fn theme_color_to_term_color(color: &str) -> Option { 91 | if color.starts_with('#') { 92 | if color.len() != 7 { 93 | return None; 94 | } 95 | // Hex color name 96 | let parse = || -> Option { 97 | let color = &color[1..]; 98 | let r = u8::from_str_radix(&color[0..2], 16).ok()?; 99 | let g = u8::from_str_radix(&color[2..4], 16).ok()?; 100 | let b = u8::from_str_radix(&color[4..], 16).ok()?; 101 | Some(Color::Rgb { r, g, b }) 102 | }; 103 | parse() 104 | } else { 105 | // we only support lowercase for performance 106 | // because this is a hot path 107 | match color { 108 | "black" => Some(Color::Black), 109 | "dark_grey" => Some(Color::DarkGrey), 110 | "red" => Some(Color::Red), 111 | "dark_red" => Some(Color::DarkRed), 112 | "green" => Some(Color::Green), 113 | "dark_green" => Some(Color::DarkGreen), 114 | "yellow" => Some(Color::Yellow), 115 | "dark_yellow" => Some(Color::DarkYellow), 116 | "blue" => Some(Color::Blue), 117 | "dark_blue" => Some(Color::DarkBlue), 118 | "magenta" => Some(Color::Magenta), 119 | "dark_magenta" => Some(Color::DarkMagenta), 120 | "cyan" => Some(Color::Cyan), 121 | "dark_cyan" => Some(Color::DarkCyan), 122 | "white" => Some(Color::White), 123 | "grey" => Some(Color::Grey), 124 | _ => None, 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /crates/irust/src/irust/history.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use std::fs; 3 | use std::path; 4 | use std::path::PathBuf; 5 | 6 | /// Mark to keep backward-compatibility with the old way of saving history 7 | const NEW_HISTORY_MARK: &str = "##NewHistoryMark##\n//\n"; 8 | 9 | #[derive(Default)] 10 | pub struct History { 11 | history: Vec, 12 | cursor: usize, 13 | history_file_path: path::PathBuf, 14 | pub lock: bool, 15 | last_buffer: Vec, 16 | } 17 | 18 | impl History { 19 | pub fn new(irust_dir: PathBuf) -> Result { 20 | let history_file_path = if let Some(cache_dir) = crate::utils::irust_dirs::cache_dir() { 21 | let irust_cache = cache_dir.join("irust"); 22 | let _ = std::fs::create_dir_all(&irust_cache); 23 | irust_cache.join("history") 24 | } else { 25 | // If we can't acess the cache, we use irust_repl::IRUST_DIR which is located in tmp and is already created 26 | irust_dir.join("history") 27 | }; 28 | 29 | if !history_file_path.exists() { 30 | fs::File::create(&history_file_path)?; 31 | } 32 | 33 | let history: String = fs::read_to_string(&history_file_path)?; 34 | 35 | let history: Vec = if history.starts_with(NEW_HISTORY_MARK) { 36 | history 37 | .split("\n//\n") 38 | .skip(1) 39 | .map(ToOwned::to_owned) 40 | .collect() 41 | } else { 42 | history.lines().map(ToOwned::to_owned).collect() 43 | }; 44 | 45 | let cursor = 0; 46 | 47 | Ok(Self { 48 | history, 49 | cursor, 50 | history_file_path, 51 | lock: false, 52 | last_buffer: Vec::new(), 53 | }) 54 | } 55 | pub fn down(&mut self, buffer: &[char]) -> Option { 56 | if !self.lock { 57 | buffer.clone_into(&mut self.last_buffer); 58 | self.cursor = 1; 59 | } 60 | 61 | self.cursor = self.cursor.saturating_sub(1); 62 | if self.cursor == 0 { 63 | return Some(self.last_buffer.iter().copied().collect()); 64 | } 65 | 66 | let (filtered, _filtered_len) = self.filter(&self.last_buffer); 67 | 68 | filtered.map(ToOwned::to_owned) 69 | } 70 | 71 | pub fn up(&mut self, buffer: &[char]) -> Option { 72 | if !self.lock { 73 | buffer.clone_into(&mut self.last_buffer); 74 | self.cursor = 0; 75 | } 76 | self.cursor += 1; 77 | 78 | let (filtered, filtered_len) = self.filter(&self.last_buffer); 79 | let res = filtered.map(ToOwned::to_owned); 80 | 81 | if self.cursor + 1 >= filtered_len { 82 | self.cursor = filtered_len; 83 | } 84 | 85 | res 86 | } 87 | 88 | pub fn push(&mut self, buffer: String) { 89 | if !buffer.is_empty() && Some(&buffer) != self.history.last() { 90 | self.history.push(buffer); 91 | self.go_to_last(); 92 | } 93 | } 94 | 95 | pub fn save(&self) -> Result<()> { 96 | let is_comment = |s: &str| -> bool { s.trim_start().starts_with("//") }; 97 | let mut history = self.history.clone(); 98 | 99 | if history.is_empty() || history[0] != NEW_HISTORY_MARK { 100 | history.insert(0, NEW_HISTORY_MARK.to_string()); 101 | } 102 | 103 | let history: Vec = history 104 | .into_iter() 105 | .map(|e| { 106 | let e: Vec = e 107 | .lines() 108 | .filter(|l| !is_comment(l)) 109 | .map(ToOwned::to_owned) 110 | .collect(); 111 | e.join("\n") 112 | }) 113 | .collect(); 114 | let history = history.join("\n//\n"); 115 | 116 | fs::write(&self.history_file_path, history)?; 117 | Ok(()) 118 | } 119 | 120 | fn filter(&self, buffer: &[char]) -> (Option<&String>, usize) { 121 | let mut f: Vec<&String> = self 122 | .history 123 | .iter() 124 | .filter(|h| h.contains(&buffer.iter().collect::())) 125 | .rev() 126 | .collect(); 127 | f.dedup(); 128 | 129 | let len = f.len(); 130 | ( 131 | f.get(self.cursor.saturating_sub(1)).map(ToOwned::to_owned), 132 | len, 133 | ) 134 | } 135 | 136 | fn go_to_last(&mut self) { 137 | if !self.history.is_empty() { 138 | self.cursor = 0; 139 | } 140 | } 141 | 142 | pub fn reverse_find_nth(&self, needle: &str, n: usize) -> Option { 143 | let mut history = self.history.iter().rev().collect::>(); 144 | history.dedup(); 145 | history 146 | .iter() 147 | .filter(|h| h.contains(needle)) 148 | .nth(n) 149 | .map(|e| e.to_owned().to_owned()) 150 | } 151 | 152 | pub fn lock(&mut self) { 153 | self.lock = true; 154 | } 155 | 156 | pub fn unlock(&mut self) { 157 | self.lock = false; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /crates/irust/src/irust/options.rs: -------------------------------------------------------------------------------- 1 | use crate::irust::{IRust, Result}; 2 | use crossterm::style::Color; 3 | use irust_repl::{CompileMode, DEFAULT_EVALUATOR, Edition, Executor, MainResult, ToolChain}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::io::{Read, Write}; 6 | 7 | #[derive(Deserialize, Serialize, Clone, Debug)] 8 | #[serde(default)] 9 | pub struct Options { 10 | add_irust_cmd_to_history: bool, 11 | add_shell_cmd_to_history: bool, 12 | pub ok_color: Color, 13 | pub eval_color: Color, 14 | pub irust_color: Color, 15 | pub irust_warn_color: Color, 16 | pub out_color: Color, 17 | pub shell_color: Color, 18 | pub err_color: Color, 19 | pub input_color: Color, 20 | pub insert_color: Color, 21 | pub welcome_msg: String, 22 | pub welcome_color: Color, 23 | pub ra_inline_suggestion_color: Color, 24 | pub ra_suggestions_table_color: Color, 25 | pub ra_selected_suggestion_color: Color, 26 | pub ra_max_suggestions: usize, 27 | pub first_irust_run: bool, 28 | pub enable_rust_analyzer: bool, 29 | pub toolchain: ToolChain, 30 | pub check_statements: bool, 31 | pub auto_insert_semicolon: bool, 32 | pub replace_marker: String, 33 | pub replace_output_with_marker: bool, 34 | pub input_prompt: String, 35 | pub output_prompt: String, 36 | pub activate_scripting: bool, 37 | pub executor: Executor, 38 | pub evaluator: Vec, 39 | pub compile_time: bool, 40 | pub main_result: MainResult, 41 | pub show_warnings: bool, 42 | pub edition: Edition, 43 | pub debugger: Debugger, 44 | pub shell_interpolate: bool, 45 | pub local_server: bool, 46 | pub local_server_adress: std::net::SocketAddrV4, 47 | pub theme: String, 48 | pub compile_mode: CompileMode, 49 | pub new_lines_after_output: usize, 50 | 51 | #[serde(skip)] 52 | config_load_time: Option, 53 | } 54 | 55 | impl Default for Options { 56 | fn default() -> Self { 57 | Self { 58 | // [Histroy] 59 | add_irust_cmd_to_history: true, 60 | add_shell_cmd_to_history: false, 61 | 62 | // [Colors] 63 | ok_color: Color::Blue, 64 | eval_color: Color::White, 65 | irust_color: Color::DarkBlue, 66 | irust_warn_color: Color::Cyan, 67 | out_color: Color::Red, 68 | shell_color: Color::DarkYellow, 69 | err_color: Color::DarkRed, 70 | input_color: Color::Yellow, 71 | insert_color: Color::White, 72 | 73 | // [Welcome] 74 | welcome_msg: String::new(), 75 | welcome_color: Color::DarkBlue, 76 | 77 | // [Rust Analyzer] 78 | enable_rust_analyzer: false, 79 | ra_inline_suggestion_color: Color::Cyan, 80 | ra_suggestions_table_color: Color::Green, 81 | ra_selected_suggestion_color: Color::DarkRed, 82 | ra_max_suggestions: 5, 83 | 84 | //other 85 | first_irust_run: true, 86 | toolchain: ToolChain::Default, 87 | check_statements: true, 88 | auto_insert_semicolon: true, 89 | 90 | // replace output 91 | replace_marker: "$out".into(), 92 | replace_output_with_marker: false, 93 | 94 | input_prompt: "In: ".to_string(), 95 | output_prompt: "Out: ".to_string(), 96 | activate_scripting: false, 97 | executor: Executor::Sync, 98 | evaluator: DEFAULT_EVALUATOR 99 | .iter() 100 | .map(|part| part.to_string()) 101 | .collect(), 102 | compile_time: false, 103 | main_result: MainResult::Unit, 104 | show_warnings: false, 105 | edition: Edition::E2021, 106 | debugger: Debugger::LLDB, 107 | shell_interpolate: true, 108 | local_server: false, 109 | local_server_adress: "127.0.0.1:9000".parse().expect("correct"), 110 | theme: "default".into(), 111 | compile_mode: CompileMode::Debug, 112 | new_lines_after_output: 1, 113 | config_load_time: None, 114 | } 115 | } 116 | } 117 | 118 | impl Options { 119 | pub fn save(&mut self) -> Result<()> { 120 | if let Some(path) = Self::config_path() { 121 | // Check if the file has been modified since we loaded it 122 | if let Ok(metadata) = std::fs::metadata(&path) { 123 | if let Ok(modified_time) = metadata.modified() { 124 | // If the config file has been modified since startup, don't overwrite it 125 | if let Some(load_time) = self.config_load_time { 126 | if modified_time > load_time { 127 | // File was modified after we loaded it, don't overwrite 128 | return Ok(()); 129 | } 130 | } 131 | } 132 | } 133 | Self::write_config_file(path, self)?; 134 | } 135 | Ok(()) 136 | } 137 | 138 | pub fn new() -> Result { 139 | if let Some(config_path) = Options::config_path() { 140 | let mut config_file = std::fs::File::open(&config_path)?; 141 | let mut config_data = String::new(); 142 | config_file.read_to_string(&mut config_data)?; 143 | 144 | // Get the file's last modified time 145 | let modified_time = std::fs::metadata(&config_path) 146 | .ok() 147 | .and_then(|m| m.modified().ok()); 148 | 149 | // Parse the config and store the load time 150 | let mut options: Options = toml::from_str(&config_data)?; 151 | options.config_load_time = modified_time; 152 | 153 | Ok(options) 154 | } else { 155 | Ok(Options::default()) 156 | } 157 | } 158 | 159 | pub fn reset(&mut self) { 160 | *self = Self::default(); 161 | } 162 | 163 | pub fn reset_evaluator(&mut self) { 164 | self.evaluator = DEFAULT_EVALUATOR 165 | .iter() 166 | .map(|part| part.to_string()) 167 | .collect(); 168 | } 169 | 170 | pub fn config_path() -> Option { 171 | let config_dir = match crate::utils::irust_dirs::config_dir() { 172 | Some(dir) => dir.join("irust"), 173 | None => return None, 174 | }; 175 | 176 | // Ignore directory exists error 177 | let _ = std::fs::create_dir_all(&config_dir); 178 | let config_path = config_dir.join("config.toml"); 179 | 180 | Some(config_path) 181 | } 182 | 183 | fn write_config_file(config_path: std::path::PathBuf, options: &Options) -> Result<()> { 184 | let config = toml::to_string(options)?; 185 | 186 | let mut config_file = std::fs::File::create(config_path)?; 187 | 188 | write!(config_file, "{config}")?; 189 | Ok(()) 190 | } 191 | } 192 | 193 | impl IRust { 194 | pub fn should_push_to_history(&self, buffer: &str) -> bool { 195 | let buffer: Vec = buffer.chars().collect(); 196 | 197 | if buffer.is_empty() { 198 | return false; 199 | } 200 | if buffer.len() == 1 { 201 | return buffer[0] != ':'; 202 | } 203 | 204 | let irust_cmd = buffer[0] == ':' && buffer[1] != ':'; 205 | let shell_cmd = buffer[0] == ':' && buffer[1] == ':'; 206 | 207 | (irust_cmd && self.options.add_irust_cmd_to_history) 208 | || (shell_cmd && self.options.add_shell_cmd_to_history) 209 | || (!irust_cmd && !shell_cmd) 210 | } 211 | pub fn dont_save_options(&mut self) { 212 | self.engine.dont_save_options = true; 213 | } 214 | } 215 | 216 | #[allow(clippy::upper_case_acronyms)] 217 | #[derive(Deserialize, Serialize, Clone, Debug)] 218 | pub enum Debugger { 219 | LLDB, 220 | GDB, 221 | } 222 | -------------------------------------------------------------------------------- /crates/irust/src/irust/ra/rust_analyzer.rs: -------------------------------------------------------------------------------- 1 | use crate::irust::Result; 2 | use serde_json::{Value, json}; 3 | use std::io::Write; 4 | use std::io::{BufRead, Read}; 5 | use std::process::{Child, ChildStdin, ChildStdout}; 6 | use std::sync::atomic::AtomicUsize; 7 | use std::sync::atomic::Ordering; 8 | use std::time::Duration; 9 | use std::{ 10 | io::BufReader, 11 | path::Path, 12 | process::{Command, Stdio}, 13 | }; 14 | 15 | static ID: AtomicUsize = AtomicUsize::new(1); 16 | 17 | pub struct RustAnalyzer { 18 | _process: Child, 19 | stdin: ChildStdin, 20 | stdout: BufReader, 21 | } 22 | 23 | impl RustAnalyzer { 24 | pub fn start(root_uri: &Path, uri: &Path, text: String) -> Result { 25 | let mut process = Command::new("rust-analyzer") 26 | .stdin(Stdio::piped()) 27 | .stdout(Stdio::piped()) 28 | .stderr(Stdio::null()) // comment out to debug lsp 29 | .spawn()?; 30 | let mut stdin = process.stdin.take().expect("piped"); 31 | let mut stdout = BufReader::new(process.stdout.take().expect("piped")); 32 | 33 | // Send a "initialize" request to the language server 34 | let initialize_request = json!({ 35 | "jsonrpc": "2.0", 36 | "id": ID.fetch_add(1, Ordering::SeqCst), 37 | "method": "initialize", 38 | "params": { 39 | // TODO: make this configurable in irust config 40 | "initializationOptions": { 41 | "checkOnSave": false, 42 | "diagnostics": { 43 | "enable": false 44 | }, 45 | "completion": { 46 | "privateEditable": { 47 | "enable": true 48 | } 49 | } 50 | }, 51 | "processId": std::process::id(), 52 | "rootUri": format!("file://{}",root_uri.display()), 53 | "capabilities": { 54 | "textDocument": { 55 | "completion": { 56 | "completionItem": { 57 | "documentationFormat": ["plaintext"] 58 | }, 59 | "completionItemKind": { 60 | "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35] 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | }); 67 | send_request(&mut stdin, &initialize_request)?; 68 | 69 | // Send an "initialized" notification to the language server 70 | let initialized_notification = json!({ 71 | "jsonrpc": "2.0", 72 | "method": "initialized", 73 | "params": {}, 74 | }); 75 | send_request(&mut stdin, &initialized_notification)?; 76 | 77 | // Wait for "initialize" response 78 | let _initialize_response = read_response(&mut stdout)?; 79 | 80 | // Send a "textDocument/didOpen" notification to the language server 81 | let did_open_notification = json!({ 82 | "jsonrpc": "2.0", 83 | "method": "textDocument/didOpen", 84 | "params": { 85 | "textDocument": { 86 | "uri": format!("file://{}",uri.display()), 87 | "languageId": "rust", 88 | "version": 1, 89 | "text": text, 90 | }, 91 | }, 92 | }); 93 | send_request(&mut stdin, &did_open_notification)?; 94 | 95 | Ok(RustAnalyzer { 96 | _process: process, 97 | stdin, 98 | stdout, 99 | }) 100 | } 101 | 102 | pub fn document_did_change(&mut self, uri: &Path, text: String) -> Result<()> { 103 | let did_change_notification = json!({ 104 | "jsonrpc": "2.0", 105 | "method": "textDocument/didChange", 106 | "params": { 107 | "textDocument": { 108 | "uri": format!("file://{}",uri.display()), 109 | "version": 2, 110 | }, 111 | "contentChanges": [ 112 | { 113 | "text":text, 114 | } 115 | ] 116 | }, 117 | }); 118 | send_request(&mut self.stdin, &did_change_notification)?; 119 | Ok(()) 120 | } 121 | 122 | pub fn reload_workspace(&mut self) -> Result<()> { 123 | let reload_msg = json!({ 124 | "jsonrpc": "2.0", 125 | "id": ID.fetch_add(1, Ordering::SeqCst), 126 | "method": "rust-analyzer/reloadWorkspace", 127 | }); 128 | send_request(&mut self.stdin, &reload_msg)?; 129 | read_response(&mut self.stdout)?; 130 | Ok(()) 131 | } 132 | 133 | //TODO: use insertText ; it has the exact text position 134 | pub fn document_completion( 135 | &mut self, 136 | uri: &Path, 137 | (line, character): (usize, usize), 138 | ) -> Result> { 139 | // Send a "textDocument/completion" request to the language server 140 | let completion_request = json!({ 141 | "jsonrpc": "2.0", 142 | "id": ID.fetch_add(1, Ordering::SeqCst), 143 | "method": "textDocument/completion", 144 | "params": { 145 | "textDocument": { 146 | "uri": format!("file://{}",uri.display()), 147 | }, 148 | "position": { 149 | "line": line, 150 | "character": character 151 | }, 152 | }, 153 | }); 154 | send_request(&mut self.stdin, &completion_request)?; 155 | 156 | let completion_response = loop { 157 | let completion_response = read_response(&mut self.stdout)?; 158 | if completion_response.get("result").is_some() { 159 | break completion_response; 160 | } 161 | // NOTE: we block until we get a completion 162 | std::thread::sleep(Duration::from_millis(100)); 163 | }; 164 | if let Some(result) = completion_response.get("result") { 165 | if let Some(items) = result.get("items") { 166 | return Ok(items 167 | .as_array() 168 | .ok_or("ra items is not an array")? 169 | .iter() 170 | .filter_map(|item| item.get("filterText")) 171 | .map(|item| item.to_string()) 172 | // remove quotes 173 | .map(|item| item[1..item.len() - 1].to_owned()) 174 | .collect()); 175 | } 176 | } 177 | 178 | Ok(vec![]) 179 | } 180 | } 181 | 182 | fn send_request(stdin: &mut std::process::ChildStdin, request: &Value) -> Result<()> { 183 | let request_str = serde_json::to_string(request)?; 184 | let content_length = request_str.len(); 185 | writeln!(stdin, "Content-Length: {content_length}\r")?; 186 | writeln!(stdin, "\r")?; 187 | write!(stdin, "{request_str}")?; 188 | stdin.flush()?; 189 | Ok(()) 190 | } 191 | 192 | fn read_response(reader: &mut BufReader) -> Result { 193 | let content_length = get_content_length(reader)?; 194 | let mut content = vec![0; content_length]; 195 | 196 | reader.read_exact(&mut content)?; 197 | let json_string = String::from_utf8(content)?; 198 | let message = serde_json::from_str(&json_string)?; 199 | Ok(message) 200 | } 201 | 202 | fn get_content_length(reader: &mut BufReader) -> Result { 203 | let mut line = String::new(); 204 | let mut blank_line = String::new(); 205 | 206 | let mut _bytes_read = reader.read_line(&mut line)?; 207 | let mut split = line.trim().split(": "); 208 | 209 | if split.next() == Some("Content-Length") { 210 | _bytes_read = reader.read_line(&mut blank_line)?; 211 | Ok(split 212 | .next() 213 | .and_then(|value_string| value_string.parse().ok()) 214 | .ok_or("malformed rpc message")?) 215 | } else { 216 | Err("malformed rpc message".into()) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /crates/irust/src/irust/script/mod.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::Event; 2 | use irust_api::{Command, GlobalVariables}; 3 | 4 | use self::script_manager::ScriptManager; 5 | 6 | use super::options::Options; 7 | 8 | pub mod script_manager; 9 | 10 | pub trait Script { 11 | fn input_prompt(&mut self, _global_variables: &GlobalVariables) -> Option; 12 | fn get_output_prompt(&mut self, _global_variables: &GlobalVariables) -> Option; 13 | fn before_compiling(&mut self, _global_variables: &GlobalVariables) -> Option<()>; 14 | fn input_event_hook( 15 | &mut self, 16 | _global_variables: &GlobalVariables, 17 | _event: Event, 18 | ) -> Option; 19 | fn after_compiling(&mut self, _global_variables: &GlobalVariables) -> Option<()>; 20 | fn output_event_hook( 21 | &mut self, 22 | _input: &str, 23 | _global_variables: &GlobalVariables, 24 | ) -> Option; 25 | fn list(&self) -> Option; 26 | fn activate(&mut self, _script: &str) -> Result, &'static str>; 27 | fn deactivate(&mut self, _script: &str) -> Result, &'static str>; 28 | fn trigger_set_title_hook(&mut self) -> Option; 29 | fn trigger_set_msg_hook(&mut self) -> Option; 30 | fn startup_cmds(&mut self) -> Vec, rscript::Error>>; 31 | fn shutdown_cmds(&mut self) -> Vec, rscript::Error>>; 32 | } 33 | 34 | // Scripts 35 | impl super::IRust { 36 | pub fn update_input_prompt(&mut self) { 37 | if let Some(ref mut script_mg) = self.script_mg { 38 | if let Some(prompt) = script_mg.input_prompt(&self.global_variables) { 39 | self.printer.set_prompt(prompt); 40 | } 41 | } 42 | } 43 | pub fn get_output_prompt(&mut self) -> String { 44 | if let Some(ref mut script_mg) = self.script_mg { 45 | if let Some(prompt) = script_mg.get_output_prompt(&self.global_variables) { 46 | return prompt; 47 | } 48 | } 49 | //Default 50 | self.options.output_prompt.clone() 51 | } 52 | pub fn before_compiling_hook(&mut self) { 53 | if let Some(ref mut script_mg) = self.script_mg { 54 | script_mg.before_compiling(&self.global_variables); 55 | } 56 | } 57 | pub fn input_event_hook(&mut self, event: Event) -> Option { 58 | if let Some(ref mut script_mg) = self.script_mg { 59 | return script_mg.input_event_hook(&self.global_variables, event); 60 | } 61 | None 62 | } 63 | pub fn after_compiling_hook(&mut self) { 64 | if let Some(ref mut script_mg) = self.script_mg { 65 | script_mg.after_compiling(&self.global_variables); 66 | } 67 | } 68 | 69 | pub fn output_event_hook(&mut self, input: &str) -> Option { 70 | if let Some(ref mut script_mg) = self.script_mg { 71 | return script_mg.output_event_hook(input, &self.global_variables); 72 | } 73 | None 74 | } 75 | 76 | pub fn trigger_set_title_hook(&mut self) -> Option { 77 | if let Some(ref mut script_mg) = self.script_mg { 78 | return script_mg.trigger_set_title_hook(); 79 | } 80 | None 81 | } 82 | pub fn trigger_set_msg_hook(&mut self) -> Option { 83 | if let Some(ref mut script_mg) = self.script_mg { 84 | return script_mg.trigger_set_msg_hook(); 85 | } 86 | None 87 | } 88 | 89 | pub fn scripts_list(&self) -> Option { 90 | if let Some(ref script_mg) = self.script_mg { 91 | return script_mg.list(); 92 | } 93 | None 94 | } 95 | pub fn activate_script(&mut self, script: &str) -> Result, &'static str> { 96 | if let Some(ref mut script_mg) = self.script_mg { 97 | return script_mg.activate(script); 98 | } 99 | Ok(None) 100 | } 101 | pub fn deactivate_script(&mut self, script: &str) -> Result, &'static str> { 102 | if let Some(ref mut script_mg) = self.script_mg { 103 | return script_mg.deactivate(script); 104 | } 105 | Ok(None) 106 | } 107 | 108 | // internal 109 | /////////// 110 | pub fn choose_script_mg(options: &Options) -> Option> { 111 | if options.activate_scripting { 112 | ScriptManager::new().map(|script_mg| Box::new(script_mg) as Box) 113 | } else { 114 | None 115 | } 116 | } 117 | 118 | pub fn update_script_state(&mut self) { 119 | self.global_variables.prompt_position = self.printer.cursor.starting_pos(); 120 | self.global_variables.cursor_position = self.printer.cursor.current_pos(); 121 | self.global_variables.is_ra_suggestion_active = self 122 | .completer 123 | .as_ref() 124 | .and_then(|r| r.active_suggestion.as_ref()) 125 | .is_some(); 126 | } 127 | 128 | pub fn run_scripts_startup_cmds(&mut self) -> super::Result<()> { 129 | if let Some(ref mut script_mg) = self.script_mg { 130 | for cmd in script_mg.startup_cmds() { 131 | if let Some(cmd) = cmd? { 132 | self.execute(cmd)?; 133 | } 134 | } 135 | } 136 | Ok(()) 137 | } 138 | pub fn run_scripts_shutdown_cmds(&mut self) -> super::Result<()> { 139 | if let Some(ref mut script_mg) = self.script_mg { 140 | for cmd in script_mg.shutdown_cmds() { 141 | if let Some(cmd) = cmd? { 142 | self.execute(cmd)?; 143 | } 144 | } 145 | } 146 | Ok(()) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /crates/irust/src/irust/script/script_manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crossterm::event::Event; 4 | use irust_api::{Command, GlobalVariables}; 5 | 6 | use super::Script; 7 | 8 | const SCRIPT_CONFIG_NAME: &str = "script.toml"; 9 | 10 | pub struct ScriptManager { 11 | sm: rscript::ScriptManager, 12 | startup_cmds: Vec, rscript::Error>>, 13 | } 14 | 15 | macro_rules! mtry { 16 | ($e: expr) => { 17 | (|| -> Result<_, Box> { Ok($e) })() 18 | }; 19 | } 20 | impl ScriptManager { 21 | pub fn new() -> Option { 22 | let mut sm = rscript::ScriptManager::default(); 23 | let script_path = crate::utils::irust_dirs::config_dir()? 24 | .join("irust") 25 | .join("script"); 26 | sm.add_scripts_by_path( 27 | &script_path, 28 | rscript::Version::parse(crate::args::VERSION).expect("correct version"), 29 | ) 30 | .ok()?; 31 | unsafe { 32 | sm.add_dynamic_scripts_by_path( 33 | script_path, 34 | rscript::Version::parse(crate::args::VERSION).expect("correct version"), 35 | ) 36 | .ok()?; 37 | } 38 | 39 | // read conf if available 40 | let script_conf_path = crate::utils::irust_dirs::config_dir()? 41 | .join("irust") 42 | .join(SCRIPT_CONFIG_NAME); 43 | 44 | // ignore any error that happens while trying to read conf 45 | // If an error happens, a new configuration will be written anyway when ScriptManager is 46 | // dropped 47 | 48 | let mut startup_cmds = vec![]; 49 | if let Ok(script_state) = 50 | mtry!(toml::from_str(&std::fs::read_to_string(script_conf_path)?)?) 51 | { 52 | // type inference 53 | let script_state: HashMap = script_state; 54 | 55 | sm.scripts_mut().iter_mut().for_each(|script| { 56 | let script_name = &script.metadata().name; 57 | if let Some(state) = script_state.get(script_name) { 58 | if *state { 59 | script.activate(); 60 | // Trigger startup hook, in case the script needs to be aware of it 61 | if script.is_listening_for::() { 62 | startup_cmds.push(script.trigger(&irust_api::Startup())); 63 | } 64 | } else { 65 | script.deactivate(); 66 | } 67 | } 68 | }) 69 | } 70 | 71 | Some(ScriptManager { sm, startup_cmds }) 72 | } 73 | } 74 | impl Drop for ScriptManager { 75 | fn drop(&mut self) { 76 | let mut script_state = HashMap::new(); 77 | for script in self.sm.scripts() { 78 | script_state.insert(script.metadata().name.clone(), script.is_active()); 79 | } 80 | // Ignore errors on drop 81 | let _ = mtry!({ 82 | let script_conf_path = crate::utils::irust_dirs::config_dir() 83 | .ok_or("could not find config directory")? 84 | .join("irust") 85 | .join(SCRIPT_CONFIG_NAME); 86 | std::fs::write(script_conf_path, toml::to_string(&script_state)?) 87 | }); 88 | } 89 | } 90 | 91 | /* NOTE: Toml: serilizing tuple struct is not working? 92 | #[derive(Serialize, Deserialize, Debug)] 93 | struct ScriptState(HashMap); 94 | */ 95 | 96 | impl Script for ScriptManager { 97 | fn input_prompt(&mut self, global_variables: &GlobalVariables) -> Option { 98 | self.sm 99 | .trigger(irust_api::SetInputPrompt(global_variables.clone())) 100 | .next()? 101 | .ok() 102 | } 103 | fn get_output_prompt(&mut self, global_variables: &GlobalVariables) -> Option { 104 | self.sm 105 | .trigger(irust_api::SetOutputPrompt(global_variables.clone())) 106 | .next()? 107 | .ok() 108 | } 109 | fn before_compiling(&mut self, global_variables: &GlobalVariables) -> Option<()> { 110 | self.sm 111 | .trigger(irust_api::BeforeCompiling(global_variables.clone())) 112 | .collect::>() 113 | .ok() 114 | } 115 | fn after_compiling(&mut self, global_variables: &GlobalVariables) -> Option<()> { 116 | self.sm 117 | .trigger(irust_api::AfterCompiling(global_variables.clone())) 118 | .collect::>() 119 | .ok() 120 | } 121 | fn input_event_hook( 122 | &mut self, 123 | global_variables: &GlobalVariables, 124 | event: Event, 125 | ) -> Option { 126 | self.sm 127 | .trigger(irust_api::InputEvent(global_variables.clone(), event)) 128 | .next()? 129 | .ok()? 130 | } 131 | fn output_event_hook( 132 | &mut self, 133 | input: &str, 134 | global_variables: &GlobalVariables, 135 | ) -> Option { 136 | self.sm 137 | .trigger(irust_api::OutputEvent( 138 | global_variables.clone(), 139 | input.to_string(), 140 | )) 141 | .next()? 142 | .ok()? 143 | } 144 | fn trigger_set_title_hook(&mut self) -> Option { 145 | self.sm.trigger(irust_api::SetTitle()).next()?.ok()? 146 | } 147 | 148 | fn trigger_set_msg_hook(&mut self) -> Option { 149 | self.sm.trigger(irust_api::SetWelcomeMsg()).next()?.ok()? 150 | } 151 | 152 | fn list(&self) -> Option { 153 | let mut scripts: Vec = self 154 | .sm 155 | .scripts() 156 | .iter() 157 | .map(|script| { 158 | let meta = script.metadata(); 159 | format!( 160 | "{}\t{:?}\t{:?}\t{}", 161 | &meta.name, 162 | &meta.script_type, 163 | &meta.hooks, 164 | script.is_active() 165 | ) 166 | }) 167 | .collect(); 168 | //header 169 | scripts.insert(0, "Name\tScriptType\tHooks\tState".into()); 170 | 171 | Some(scripts.join("\n")) 172 | } 173 | 174 | fn activate(&mut self, script_name: &str) -> Result, &'static str> { 175 | if let Some(script) = self 176 | .sm 177 | .scripts_mut() 178 | .iter_mut() 179 | .find(|script| script.metadata().name == script_name) 180 | { 181 | script.activate(); 182 | // We send a startup message in case the script is listening for one 183 | if let Ok(maybe_command) = script.trigger(&irust_api::Startup()) { 184 | Ok(maybe_command) 185 | } else { 186 | Ok(None) 187 | } 188 | } else { 189 | Err("Script not found") 190 | } 191 | } 192 | 193 | fn deactivate(&mut self, script_name: &str) -> Result, &'static str> { 194 | if let Some(script) = self 195 | .sm 196 | .scripts_mut() 197 | .iter_mut() 198 | .find(|script| script.metadata().name == script_name) 199 | { 200 | script.deactivate(); 201 | // We send a shutdown message in case the script is listening for one 202 | if let Ok(maybe_command) = script.trigger(&irust_api::Shutdown()) { 203 | Ok(maybe_command) 204 | } else { 205 | Ok(None) 206 | } 207 | } else { 208 | Err("Script not found") 209 | } 210 | } 211 | 212 | fn startup_cmds(&mut self) -> Vec, rscript::Error>> { 213 | self.startup_cmds.drain(..).collect() 214 | } 215 | 216 | fn shutdown_cmds(&mut self) -> Vec, rscript::Error>> { 217 | self.sm 218 | .scripts_mut() 219 | .iter_mut() 220 | .filter(|script| script.is_listening_for::()) 221 | .map(|script| script.trigger(&irust_api::Shutdown())) 222 | .collect() 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /crates/irust/src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod dependencies; 3 | mod irust; 4 | mod utils; 5 | use crate::irust::IRust; 6 | use crate::{ 7 | args::{ArgsResult, handle_args}, 8 | irust::options::Options, 9 | }; 10 | use crossterm::{style::Stylize, tty::IsTty}; 11 | use dependencies::{check_required_deps, warn_about_opt_deps}; 12 | use irust_repl::CompileMode; 13 | use std::process::exit; 14 | 15 | fn main() { 16 | let mut options = Options::new().unwrap_or_default(); 17 | 18 | // Handle args 19 | let args: Vec = std::env::args().skip(1).collect(); 20 | let args_result = if args.is_empty() { 21 | ArgsResult::Proceed 22 | } else { 23 | handle_args(&args, &mut options) 24 | }; 25 | 26 | // Exit if there is nothing more todo 27 | if matches!(args_result, ArgsResult::Exit) { 28 | exit(0) 29 | } 30 | 31 | // If no argument are provided, check stdin for some oneshot usage 32 | if args.is_empty() { 33 | let mut stdin = std::io::stdin(); 34 | if !stdin.is_tty() { 35 | // Something was piped to stdin 36 | // The users wants a oneshot evaluation 37 | use irust_repl::{DEFAULT_EVALUATOR, EvalConfig, EvalResult, Repl}; 38 | use std::io::Read; 39 | 40 | let mut repl = Repl::default(); 41 | #[allow(clippy::blocks_in_conditions)] 42 | match (|| -> irust::Result { 43 | let mut input = String::new(); 44 | stdin.read_to_string(&mut input)?; 45 | let result = repl.eval_with_configuration(EvalConfig { 46 | input, 47 | interactive_function: None, 48 | color: true, 49 | evaluator: &*DEFAULT_EVALUATOR, 50 | compile_mode: CompileMode::Debug, 51 | })?; 52 | Ok(result) 53 | })() { 54 | Ok(result) => { 55 | if result.status.success() { 56 | println!("{}", result.output); 57 | } else { 58 | println!( 59 | "{}", 60 | irust::format_err(&result.output, false, &repl.cargo.name) 61 | ); 62 | } 63 | exit(0) 64 | } 65 | Err(e) => { 66 | eprintln!("failed to evaluate input, error: {e}"); 67 | exit(1) 68 | } 69 | } 70 | } 71 | } 72 | 73 | // Check required dependencies and exit if they're not present 74 | if !check_required_deps() { 75 | exit(1); 76 | } 77 | 78 | // Create main IRust interface 79 | let mut irust = if matches!(args_result, ArgsResult::ProceedWithDefaultConfig) { 80 | let mut irust = IRust::new(Options::default()); 81 | irust.dont_save_options(); 82 | irust 83 | } else { 84 | // Check optional dependencies and warn if they're not present 85 | if !cfg!(feature = "no-welcome-screen") { 86 | warn_about_opt_deps(&mut options); 87 | } 88 | IRust::new(options) 89 | }; 90 | 91 | // If a script path was provided try to load it 92 | if let ArgsResult::ProceedWithScriptPath(script) = args_result { 93 | // Ignore if it fails 94 | let _ = irust.load_inner(script); 95 | } 96 | 97 | // Start IRust 98 | let err = irust.run().err(); 99 | 100 | // Now IRust has been dropped we can safely print to stderr 101 | if let Some(err) = err { 102 | eprintln!("{}", format!("\r\nIRust exited with error: {err}").red()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/irust_api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "irust_api" 3 | version = "0.31.9" 4 | authors = ["Nbiba Bedis "] 5 | edition = "2024" 6 | description = "IRust API" 7 | repository = "https://github.com/sigmaSd/IRust/tree/master/crates/irust_api" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | crossterm = { version = "0.27.0", features = ["serde", "use-dev-tty"] } 12 | rscript = "0.17.0" 13 | serde = { version = "1.0.188", features = ["derive"] } 14 | 15 | [package.metadata.workspaces] 16 | independent = true 17 | -------------------------------------------------------------------------------- /crates/irust_api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use rscript::Hook; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | // Reexport crossterm event types 7 | pub mod event { 8 | pub use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 9 | } 10 | pub mod color { 11 | pub use crossterm::style::Color; 12 | } 13 | 14 | macro_rules! hookit { 15 | (Hook => $hook: ident, 16 | Input => ($($input: ty $(,)?)*), 17 | Output => $output: ty) => ( 18 | 19 | #[derive(Serialize, Deserialize)] 20 | pub struct $hook($(pub $input,)*); 21 | 22 | impl Hook for $hook { 23 | const NAME: &'static str = stringify!($hook); 24 | type Output = $output; 25 | } 26 | )} 27 | 28 | hookit!( 29 | Hook => InputEvent, 30 | Input => (GlobalVariables, event::Event), 31 | Output => Option 32 | ); 33 | hookit!( 34 | Hook => OutputEvent, 35 | Input => (GlobalVariables, String), 36 | Output => Option 37 | ); 38 | hookit!( 39 | Hook => SetTitle, 40 | Input => (), 41 | Output => Option 42 | ); 43 | hookit!( 44 | Hook => SetWelcomeMsg, 45 | Input => (), 46 | Output => Option 47 | ); 48 | hookit!( 49 | Hook => Shutdown, 50 | Input => (), 51 | Output => Option 52 | ); 53 | hookit!( 54 | Hook => Startup, 55 | Input => (), 56 | Output => Option 57 | ); 58 | hookit!( 59 | Hook => SetInputPrompt, 60 | Input => (GlobalVariables), 61 | Output => String 62 | ); 63 | hookit!( 64 | Hook => SetOutputPrompt, 65 | Input => (GlobalVariables), 66 | Output => String 67 | ); 68 | hookit!( 69 | Hook => BeforeCompiling, 70 | Input => (GlobalVariables), 71 | Output => () 72 | ); 73 | hookit!( 74 | Hook => AfterCompiling, 75 | Input => (GlobalVariables), 76 | Output => () 77 | ); 78 | 79 | #[derive(Clone, Debug, Serialize, Deserialize)] 80 | pub enum Command { 81 | AcceptSuggestion, 82 | Continue, 83 | DeleteNextWord, 84 | DeleteTillEnd, 85 | DeleteUntilChar(char, bool), 86 | MoveForwardTillChar(char), 87 | MoveBackwardTillChar(char), 88 | Parse(String), 89 | PrintInput, 90 | PrintOutput(String, color::Color), 91 | MacroRecordToggle, 92 | MacroPlay, 93 | Multiple(Vec), 94 | SetThinCursor, 95 | SetWideCursor, 96 | HandleCharacter(char), 97 | HandleEnter(bool), 98 | HandleAltEnter, 99 | HandleTab, 100 | HandleBackTab, 101 | HandleRight, 102 | HandleLeft, 103 | GoToLastRow, 104 | HandleBackSpace, 105 | HandleDelete, 106 | HandleCtrlC, 107 | HandleCtrlD, 108 | HandleCtrlE, 109 | HandleCtrlL, 110 | HandleCtrlR, 111 | HandleCtrlZ, 112 | HandleUp, 113 | HandleDown, 114 | HandleCtrlRight, 115 | HandleCtrlLeft, 116 | HandleHome, 117 | HandleEnd, 118 | Redo, 119 | RemoveRASugesstion, 120 | ResetPrompt, 121 | Undo, 122 | Exit, 123 | } 124 | 125 | #[derive(Debug, Clone, Serialize, Deserialize)] 126 | pub struct GlobalVariables { 127 | current_working_dir: PathBuf, 128 | previous_working_dir: PathBuf, 129 | last_loaded_code_path: Option, 130 | /// last successful output 131 | last_output: Option, 132 | pub operation_number: usize, 133 | 134 | pub prompt_position: (usize, usize), // (row, col) 135 | pub cursor_position: (usize, usize), // (row, col) 136 | pub prompt_len: usize, 137 | pub pid: u32, 138 | pub is_ra_suggestion_active: bool, 139 | } 140 | 141 | impl Default for GlobalVariables { 142 | fn default() -> Self { 143 | Self::new() 144 | } 145 | } 146 | 147 | impl GlobalVariables { 148 | pub fn new() -> Self { 149 | let cwd = std::env::current_dir().expect("Error getting current working directory"); 150 | 151 | Self { 152 | current_working_dir: cwd.clone(), 153 | previous_working_dir: cwd, 154 | last_loaded_code_path: None, 155 | last_output: None, 156 | operation_number: 1, 157 | prompt_position: (0, 0), // (row, col) 158 | cursor_position: (0, 0), // (row, col) 159 | prompt_len: 0, 160 | pid: std::process::id(), 161 | is_ra_suggestion_active: false, 162 | } 163 | } 164 | 165 | pub fn update_cwd(&mut self, cwd: PathBuf) { 166 | self.previous_working_dir 167 | .clone_from(&self.current_working_dir); 168 | self.current_working_dir = cwd; 169 | } 170 | 171 | pub fn get_cwd(&self) -> PathBuf { 172 | self.current_working_dir.clone() 173 | } 174 | 175 | pub fn get_pwd(&self) -> PathBuf { 176 | self.previous_working_dir.clone() 177 | } 178 | 179 | pub fn set_last_loaded_coded_path(&mut self, path: PathBuf) { 180 | self.last_loaded_code_path = Some(path); 181 | } 182 | 183 | pub fn get_last_loaded_coded_path(&self) -> Option { 184 | self.last_loaded_code_path.clone() 185 | } 186 | 187 | pub fn get_last_output(&self) -> Option<&String> { 188 | self.last_output.as_ref() 189 | } 190 | 191 | pub fn set_last_output(&mut self, out: String) { 192 | self.last_output = Some(out); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /crates/irust_repl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "irust_repl" 3 | version = "0.24.11" 4 | authors = ["Nbiba Bedis "] 5 | edition = "2024" 6 | readme = "README.md" 7 | description = "IRust Repl abstraction" 8 | repository = "https://github.com/sigmaSd/IRust/tree/master/crates/irust_repl" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | serde = { version = "1.0.188", features = ["derive"], optional = true } 13 | uuid = { version = "1.4.1", features = ["v4"] } 14 | 15 | [target.'cfg(target_os = "macos")'.dependencies] 16 | dirs = "5.0.1" 17 | 18 | [dev-dependencies] 19 | serde = { version = "1.0.188", features = ["derive"] } 20 | serde_json = "1.0.105" 21 | 22 | [package.metadata.workspaces] 23 | independent = true 24 | -------------------------------------------------------------------------------- /crates/irust_repl/README.md: -------------------------------------------------------------------------------- 1 | # IRust Repl 2 | 3 | Repl engine used by IRust to interpret rust code 4 | 5 | The core is `println!("{:?}", expression)` with tricks to conserve variables and context 6 | 7 | Example: 8 | 9 | ```rust 10 | use irust_repl::{Repl, ToolChain}; 11 | 12 | let mut repl = Repl::new(ToolChain::Stable).unwrap(); 13 | repl.insert("let a = 5"); 14 | assert_eq!(repl.eval("a+a").unwrap().output, "10"); 15 | ``` 16 | Checkout the examples and tests folders for more info. 17 | 18 | 19 | ## Jupyter Kernel 20 | A Jupyter Kernel is provided https://github.com/sigmaSd/IRust/tree/master/crates/irust_repl/irust_kernel, to use it: 21 | 22 | Installation 23 | ------------ 24 | 25 | This requires IPython 3. 26 | 27 | pip install irust_kernel 28 | python -m irust_kernel.install 29 | 30 | To use it, run one of: 31 | 32 | code # vscode have the best implementation 33 | zed # zed implementation is nice as well 34 | jupyter notebook 35 | # In the notebook interface, select IRust from the 'New' menu 36 | jupyter qtconsole --kernel irust 37 | jupyter console --kernel irust 38 | 39 | 40 | Developement 41 | ------------ 42 | 43 | This requires https://github.com/pypa/flit 44 | 45 | To start developping locally use `flint install --symlink` optionally followed by `python -m irust_kernel.install --local-build` if there are changes to `Re` executable 46 | 47 | Examples 48 | ---------- 49 | 50 | irust.ipynb (simple showcase) and evcxr.ipynb (showcase of evcxr protocol) are provided as an example 51 | -------------------------------------------------------------------------------- /crates/irust_repl/examples/re/log.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | use std::io::Result; 3 | use std::io::prelude::*; 4 | use std::path::PathBuf; 5 | use std::sync::Mutex; 6 | use std::sync::atomic::AtomicBool; 7 | 8 | pub static ACTIVE_LOGGER: AtomicBool = AtomicBool::new(false); 9 | static LOG_FILE_PATH: Mutex> = Mutex::new(None); 10 | 11 | pub fn init_log(file_path: impl Into, env: &str) { 12 | if std::env::var(env).is_err() { 13 | return; 14 | } 15 | let mut log_file_path = LOG_FILE_PATH.lock().unwrap(); 16 | *log_file_path = Some(file_path.into()); 17 | ACTIVE_LOGGER.store(true, std::sync::atomic::Ordering::Relaxed); 18 | } 19 | 20 | pub fn log_to_file(message: &str) -> Result<()> { 21 | let file_path = LOG_FILE_PATH.lock().unwrap(); 22 | if let Some(ref path) = *file_path { 23 | let mut file = OpenOptions::new().create(true).append(true).open(path)?; 24 | writeln!(file, "{}", message)?; 25 | } 26 | Ok(()) 27 | } 28 | 29 | #[macro_export] 30 | macro_rules! log { 31 | ($($arg:tt)*) => {{ 32 | use std::fmt::Write as FmtWrite; 33 | if crate::log::ACTIVE_LOGGER.load(std::sync::atomic::Ordering::Relaxed) { 34 | let mut message = String::new(); 35 | write!(&mut message, $($arg)*).unwrap(); 36 | crate::log::log_to_file(&message).unwrap(); 37 | } 38 | }}; 39 | } 40 | -------------------------------------------------------------------------------- /crates/irust_repl/examples/re/main.rs: -------------------------------------------------------------------------------- 1 | use irust_repl::{DEFAULT_EVALUATOR, EvalConfig, EvalResult, Repl}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::Deserializer; 4 | use std::{ 5 | io::{self, Read}, 6 | sync::OnceLock, 7 | }; 8 | 9 | mod log; 10 | use log::init_log; 11 | 12 | #[derive(Serialize, Deserialize, Debug)] 13 | enum Message { 14 | Execute { code: String }, 15 | Complete { code: String, cursor_pos: usize }, 16 | } 17 | 18 | #[derive(Serialize, Deserialize)] 19 | enum Action { 20 | Eval { value: String, mime_type: MimeType }, 21 | Insert, 22 | AddDependencyStream { output_chunk: String }, 23 | AddDependencyEnd, 24 | } 25 | #[derive(Debug, Serialize, Deserialize)] 26 | enum MimeType { 27 | #[serde(rename = "text/plain")] 28 | PlainText, 29 | #[serde(rename = "text/html")] 30 | Html, 31 | #[serde(rename = "image/png")] 32 | Png, 33 | #[serde(rename = "image/jpeg")] 34 | Jpeg, 35 | } 36 | impl MimeType { 37 | fn from_str(mime_type: &str) -> Self { 38 | match mime_type { 39 | "text/plain" => Self::PlainText, 40 | "text/html" => Self::Html, 41 | "image/png" => Self::Png, 42 | "image/jpeg" => Self::Jpeg, 43 | //NOTE: we should warn here 44 | _ => Self::PlainText, 45 | } 46 | } 47 | } 48 | 49 | type Result = std::result::Result>; 50 | 51 | fn main() -> Result<()> { 52 | init_log( 53 | std::env::temp_dir().to_path_buf().join("irust-kernel.log"), 54 | "IRUST_KERNEL_DEBUG", 55 | ); 56 | let stdin = io::stdin(); 57 | let reader = stdin.lock(); 58 | let deserializer = Deserializer::from_reader(reader).into_iter::(); 59 | 60 | let mut repl = Repl::default(); 61 | 62 | // NOTE: errors should not exit this loop 63 | // In case of an error we log it and continue 64 | log!("Starting REPL"); 65 | for json in deserializer { 66 | let result = (|| -> Result<()> { 67 | log!("Received message: {:?}", json); 68 | let message = json?; 69 | match message { 70 | Message::Execute { code } => execute(&mut repl, code), 71 | Message::Complete { code, cursor_pos } => complete(&mut repl, code, cursor_pos), 72 | } 73 | })(); 74 | if result.is_err() { 75 | eprintln!("An error occurred: {result:?}"); 76 | println!("{{}}"); // We still need to send a response so we send an empty object 77 | } 78 | } 79 | 80 | Ok(()) 81 | } 82 | 83 | fn complete(_repl: &mut Repl, _code: String, _cursor_poss: usize) -> Result<()> { 84 | //TODO 85 | Ok(()) 86 | } 87 | 88 | fn execute(repl: &mut Repl, code: String) -> Result<()> { 89 | let mut code = code.trim(); 90 | // detect `!irust` special comment 91 | if code.starts_with("//") && code.contains("!irust") { 92 | code = code 93 | .split_once("!irust") 94 | .map(|x| x.1) 95 | .expect("checked") 96 | .trim(); 97 | } 98 | if code.ends_with(';') || is_a_statement(code) { 99 | let EvalResult { output, status } = repl.eval_check(code.to_owned())?; 100 | if !status.success() { 101 | let output = serde_json::to_string(&Action::Eval { 102 | // NOTE: make show warnings configurable 103 | value: format_err(&output, false, &repl.cargo.name), 104 | mime_type: MimeType::PlainText, 105 | })?; 106 | println!("{output}"); 107 | return Ok(()); 108 | } 109 | // No error, insert the code 110 | repl.insert(code); 111 | let output = serde_json::to_string(&Action::Insert)?; 112 | println!("{output}"); 113 | } else if code.starts_with(":add") { 114 | let cargo_add_arg = code 115 | .strip_prefix(":add") 116 | .expect("checked") 117 | .split_whitespace() 118 | .map(ToOwned::to_owned) 119 | .collect::>(); 120 | { 121 | let mut process = repl.cargo.cargo_add(&cargo_add_arg)?; 122 | let mut stderr = process.stderr.take().expect("piped"); 123 | let mut buf = [0; 512]; 124 | log!("Adding dependencies"); 125 | loop { 126 | let n = stderr.read(&mut buf)?; 127 | log!("Read {n} bytes"); 128 | if n == 0 { 129 | break; 130 | } 131 | let output = serde_json::to_string(&Action::AddDependencyStream { 132 | output_chunk: String::from_utf8_lossy(&buf[..n]).to_string(), 133 | })?; 134 | println!("{output}"); 135 | } 136 | } 137 | log!("Dependencies added"); 138 | // start building the dependencies as soon as possible 139 | repl.cargo.cargo_build(repl.toolchain())?; 140 | 141 | let output = serde_json::to_string(&Action::AddDependencyEnd)?; 142 | println!("{output}"); 143 | return Ok(()); 144 | } else { 145 | // eval here 146 | let EvalResult { 147 | output: value, 148 | status, 149 | } = repl.eval_with_configuration(EvalConfig { 150 | input: code, 151 | interactive_function: None, 152 | color: true, 153 | evaluator: &*DEFAULT_EVALUATOR, 154 | compile_mode: irust_repl::CompileMode::Debug, 155 | })?; 156 | 157 | // It errored, format the error and send it 158 | if !status.success() { 159 | let output = serde_json::to_string(&Action::Eval { 160 | // NOTE: make show warnings configurable 161 | value: format_err(&value, false, &repl.cargo.name), 162 | mime_type: MimeType::PlainText, 163 | })?; 164 | println!("{output}"); 165 | return Ok(()); 166 | } 167 | 168 | // EVCXR 169 | if value.starts_with("EVCXR_BEGIN_CONTENT") { 170 | let data = value.strip_prefix("EVCXR_BEGIN_CONTENT").expect("checked"); 171 | let data = &data[..data.find("EVCXR_END_CONTENT").ok_or("malformed content")?]; 172 | let mut data = data.chars(); 173 | // mime_type = Regex::new("EVCXR_BEGIN_CONTENT ([^ ]+)") 174 | let mime_type = data 175 | .by_ref() 176 | .skip_while(|c| c.is_whitespace()) 177 | .take_while(|c| !c.is_whitespace()) 178 | .collect::(); 179 | 180 | let output = serde_json::to_string(&Action::Eval { 181 | value: data.collect(), 182 | mime_type: MimeType::from_str(&mime_type), 183 | })?; 184 | println!("{output}"); 185 | return Ok(()); 186 | } 187 | 188 | let output = serde_json::to_string(&Action::Eval { 189 | value, 190 | mime_type: MimeType::PlainText, 191 | })?; 192 | println!("{output}"); 193 | } 194 | Ok(()) 195 | } 196 | 197 | // The next functions are extracted from irust 198 | // They should be extracted to a separate common crate 199 | 200 | pub fn is_a_statement(buffer_trimmed: &str) -> bool { 201 | match buffer_trimmed 202 | .split_whitespace() 203 | .collect::>() 204 | .as_slice() 205 | { 206 | // async fn|const fn|unsafe fn 207 | [_, "fn", ..] 208 | | ["fn", ..] 209 | | ["enum", ..] 210 | | ["struct", ..] 211 | | ["trait", ..] 212 | | ["impl", ..] 213 | | ["pub", ..] 214 | | ["extern", ..] 215 | | ["macro", ..] => true, 216 | ["macro_rules!", ..] => true, 217 | // attribute exp: 218 | // #[derive(Debug)] 219 | // struct B{} 220 | [tag, ..] if tag.starts_with('#') => true, 221 | _ => false, 222 | } 223 | } 224 | 225 | static NO_COLOR: OnceLock = OnceLock::new(); 226 | /// Have the top precedence 227 | fn no_color() -> bool { 228 | *NO_COLOR.get_or_init(|| std::env::var("NO_COLOR").is_ok()) 229 | } 230 | pub fn format_err<'a>(original_output: &'a str, show_warnings: bool, repl_name: &str) -> String { 231 | const BEFORE_2021_END_TAG: &str = ": aborting due to "; 232 | // Relies on --color=always 233 | const ERROR_TAG: &str = "\u{1b}[0m\u{1b}[1m\u{1b}[38;5;9merror"; 234 | const WARNING_TAG: &str = "\u{1b}[0m\u{1b}[1m\u{1b}[33mwarning"; 235 | 236 | // These are more fragile, should be only used when NO_COLOR is on 237 | const ERROR_TAG_NO_COLOR: &str = "error["; 238 | const WARNING_TAG_NO_COLOR: &str = "warning: "; 239 | 240 | let go_to_start = |output: &'a str| -> Vec<&'a str> { 241 | if show_warnings { 242 | output 243 | .lines() 244 | .skip_while(|line| !line.contains(&format!("{repl_name} v0.1.0"))) 245 | .skip(1) 246 | .collect() 247 | } else { 248 | output 249 | .lines() 250 | .skip_while(|line| { 251 | if no_color() { 252 | !line.starts_with(ERROR_TAG_NO_COLOR) 253 | } else { 254 | !line.starts_with(ERROR_TAG) 255 | } 256 | }) 257 | .collect() 258 | } 259 | }; 260 | let go_to_end = |output: Box>| -> String { 261 | if show_warnings { 262 | output 263 | } else { 264 | Box::new(output.take_while(|line| { 265 | if no_color() { 266 | !line.starts_with(WARNING_TAG_NO_COLOR) 267 | } else { 268 | !line.starts_with(WARNING_TAG) 269 | } 270 | })) 271 | } 272 | .collect::>() 273 | .join("\n") 274 | }; 275 | 276 | let handle_error = |output: &'a str| { 277 | go_to_start(output) 278 | .into_iter() 279 | .take_while(|line| !line.contains(BEFORE_2021_END_TAG)) 280 | }; 281 | let handle_error_2021 = |output: &'a str| { 282 | go_to_start(output) 283 | .into_iter() 284 | .rev() 285 | .skip_while(|line| !line.is_empty()) 286 | .collect::>() 287 | .into_iter() 288 | .rev() 289 | }; 290 | 291 | let output: Box> = if original_output.contains(BEFORE_2021_END_TAG) { 292 | Box::new(handle_error(original_output)) 293 | } else { 294 | Box::new(handle_error_2021(original_output)) 295 | }; 296 | 297 | let formatted_error = go_to_end(output); 298 | // The formatting logic is ad-hoc, there will always be a chance of failure with a rust update 299 | // 300 | // So we do a sanity check here, if the formatted_error is empty (which means we failed to 301 | // format the output), ask the user to open a bug report with the original_output 302 | if !formatted_error.is_empty() { 303 | formatted_error 304 | } else { 305 | format!( 306 | "IRust: failed to format the error output.\nThis is a bug in IRust.\nFeel free to open a bug-report at https://github.com/sigmaSd/IRust/issues/new with the next text:\n\noriginal_output:\n{original_output}" 307 | ) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /crates/irust_repl/irust_kernel/irust.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "590f518b-4353-45e8-ae65-355db93810a2", 7 | "metadata": { 8 | "vscode": { 9 | "languageId": "rust" 10 | } 11 | }, 12 | "outputs": [ 13 | { 14 | "data": { 15 | "text/plain": [ 16 | "9" 17 | ] 18 | }, 19 | "metadata": {}, 20 | "output_type": "display_data" 21 | } 22 | ], 23 | "source": [ 24 | "5+4" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "id": "926722e2-df56-4547-adf3-916ec5bc2c50", 31 | "metadata": { 32 | "vscode": { 33 | "languageId": "rust" 34 | } 35 | }, 36 | "outputs": [], 37 | "source": [ 38 | "let a = \"hello\";" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 3, 44 | "id": "dae7e4f4-1c40-448c-af79-bf9497e5294b", 45 | "metadata": { 46 | "vscode": { 47 | "languageId": "rust" 48 | } 49 | }, 50 | "outputs": [ 51 | { 52 | "data": { 53 | "text/plain": [ 54 | "Chars(['h', 'e', 'l', 'l', 'o'])" 55 | ] 56 | }, 57 | "metadata": {}, 58 | "output_type": "display_data" 59 | } 60 | ], 61 | "source": [ 62 | "a.chars()" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 4, 68 | "id": "b25222a2-c755-4d5a-b5c5-21dc4c65e7f8", 69 | "metadata": { 70 | "vscode": { 71 | "languageId": "rust" 72 | } 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | ":add regex" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 5, 82 | "id": "c2c8b56a-e2d1-42a0-8572-3d0d1503c7ab", 83 | "metadata": { 84 | "vscode": { 85 | "languageId": "rust" 86 | } 87 | }, 88 | "outputs": [ 89 | { 90 | "data": { 91 | "text/plain": [ 92 | "true" 93 | ] 94 | }, 95 | "metadata": {}, 96 | "output_type": "display_data" 97 | } 98 | ], 99 | "source": [ 100 | "regex::Regex::new(\"lo\").unwrap().is_match(a)" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 6, 106 | "id": "c658b87b-7a97-44a6-ac6f-7eb47243bdd4", 107 | "metadata": { 108 | "vscode": { 109 | "languageId": "rust" 110 | } 111 | }, 112 | "outputs": [ 113 | { 114 | "data": { 115 | "text/plain": [ 116 | "\u001b[0m\u001b[1m\u001b[38;5;9merror[E0369]\u001b[0m\u001b[0m\u001b[1m: cannot add `{integer}` to `&str`\u001b[0m\n", 117 | "\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m--> \u001b[0m\u001b[0msrc/main.rs:4:3\u001b[0m\n", 118 | "\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\n", 119 | "\u001b[0m\u001b[1m\u001b[38;5;12m4\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\u001b[0m \u001b[0m\u001b[0ma + 1\u001b[0m\n", 120 | "\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m-\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;9m^\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m-\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m{integer}\u001b[0m\n", 121 | "\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\n", 122 | "\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m&str\u001b[0m\n" 123 | ] 124 | }, 125 | "metadata": {}, 126 | "output_type": "display_data" 127 | } 128 | ], 129 | "source": [ 130 | "a + 1" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": 7, 136 | "id": "0d15ad22", 137 | "metadata": { 138 | "vscode": { 139 | "languageId": "rust" 140 | } 141 | }, 142 | "outputs": [ 143 | { 144 | "data": { 145 | "text/plain": [ 146 | "Ok(Regex(\"aze\"))" 147 | ] 148 | }, 149 | "metadata": {}, 150 | "output_type": "display_data" 151 | } 152 | ], 153 | "source": [ 154 | "regex::Regex::new(\"aze\")" 155 | ] 156 | } 157 | ], 158 | "metadata": { 159 | "kernelspec": { 160 | "display_name": "IRust", 161 | "language": "rust", 162 | "name": "irust" 163 | }, 164 | "language_info": { 165 | "file_extension": ".rs", 166 | "mimetype": "text/x-rust", 167 | "name": "IRust" 168 | } 169 | }, 170 | "nbformat": 4, 171 | "nbformat_minor": 5 172 | } 173 | -------------------------------------------------------------------------------- /crates/irust_repl/irust_kernel/irust_kernel/__init__.py: -------------------------------------------------------------------------------- 1 | """A bash kernel for Jupyter""" 2 | 3 | from .kernel import __version__ 4 | -------------------------------------------------------------------------------- /crates/irust_repl/irust_kernel/irust_kernel/__main__.py: -------------------------------------------------------------------------------- 1 | from ipykernel.kernelapp import IPKernelApp 2 | from .kernel import IRustKernel 3 | IPKernelApp.launch_instance(kernel_class=IRustKernel) 4 | -------------------------------------------------------------------------------- /crates/irust_repl/irust_kernel/irust_kernel/install.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import pathlib 5 | import subprocess 6 | import shutil 7 | import sys 8 | 9 | from jupyter_client.kernelspec import KernelSpecManager 10 | from IPython.utils.tempdir import TemporaryDirectory 11 | 12 | from .resources import LOGO_PATH 13 | 14 | 15 | kernel_json = { 16 | "argv": [sys.executable, "-m", "irust_kernel", "-f", "{connection_file}"], 17 | "display_name": "IRust", 18 | "language":"rust", 19 | } 20 | 21 | def install_my_kernel_spec(user=True, prefix=None, local_build=False): 22 | def get_cargo_target_dir(): 23 | target = "target" 24 | if 'CARGO_TARGET_DIR' in os.environ: 25 | target = os.environ['CARGO_TARGET_DIR'] 26 | return target 27 | 28 | 29 | with TemporaryDirectory() as td: 30 | cargo_target_dir = get_cargo_target_dir() 31 | os.chmod(td, 0o755) # Starts off as 700, not user readable 32 | with open(os.path.join(td, 'kernel.json'), 'w') as f: 33 | json.dump(kernel_json, f, sort_keys=True) 34 | shutil.copyfile(LOGO_PATH, pathlib.Path(td) / LOGO_PATH.name) 35 | 36 | if local_build: 37 | print('Building `Re` executable') 38 | try: 39 | subprocess.run(["cargo", "b", "--example", "re", "--target-dir", cargo_target_dir],check=True) 40 | except: 41 | print('--local-build needs to be used inside irust repo') 42 | exit(1) 43 | 44 | src = os.path.join(cargo_target_dir, "debug", "examples", "re") 45 | dst = os.path.join(td, "re") 46 | os.symlink(src, dst) 47 | 48 | else: 49 | print('Fetching `irust` repo and compiling `Re` executable') 50 | subprocess.run(["git", "clone","--depth","1", "https://github.com/sigmasd/irust"],cwd=td) 51 | irust_repl_dir = os.path.join(td,"irust", "crates", "irust_repl") 52 | subprocess.run( 53 | [ 54 | "cargo", 55 | "b", 56 | "--release", 57 | "--example", 58 | "re", 59 | "--target-dir", 60 | cargo_target_dir, 61 | "--manifest-path", 62 | os.path.join(irust_repl_dir, "Cargo.toml"), 63 | ] 64 | ) 65 | 66 | src = os.path.join(cargo_target_dir, "release", "examples", "re") 67 | dst = os.path.join(td, "re") 68 | shutil.copy2(src, dst) 69 | shutil.rmtree(os.path.join(td,"irust")) 70 | 71 | print('Installing IRust kernel spec') 72 | KernelSpecManager().install_kernel_spec(td, 'irust', user=user, prefix=prefix) 73 | print('done') 74 | 75 | def _is_root(): 76 | try: 77 | return os.geteuid() == 0 78 | except AttributeError: 79 | return False # assume not an admin on non-Unix platforms 80 | 81 | def main(argv=None): 82 | parser = argparse.ArgumentParser( 83 | description='Install KernelSpec for IRust Kernel' 84 | ) 85 | prefix_locations = parser.add_mutually_exclusive_group() 86 | 87 | prefix_locations.add_argument( 88 | '--user', 89 | help='Install KernelSpec in user\'s home directory', 90 | action='store_true' 91 | ) 92 | prefix_locations.add_argument( 93 | '--sys-prefix', 94 | help='Install KernelSpec in sys.prefix. Useful in conda / virtualenv', 95 | action='store_true', 96 | dest='sys_prefix' 97 | ) 98 | prefix_locations.add_argument( 99 | '--prefix', 100 | help='Install KernelSpec in this prefix', 101 | default=None 102 | ) 103 | 104 | parser.add_argument('--local-build', 105 | help = "Build `Re` locally and copy it to the kernel location", 106 | default = False, 107 | action='store_true' 108 | ) 109 | 110 | args = parser.parse_args(argv) 111 | 112 | user = False 113 | prefix = None 114 | if args.sys_prefix: 115 | prefix = sys.prefix 116 | elif args.prefix: 117 | prefix = args.prefix 118 | elif args.user or not _is_root(): 119 | user = True 120 | 121 | install_my_kernel_spec(user=user, prefix=prefix, local_build=args.local_build) 122 | 123 | if __name__ == '__main__': 124 | main() 125 | -------------------------------------------------------------------------------- /crates/irust_repl/irust_kernel/irust_kernel/kernel.py: -------------------------------------------------------------------------------- 1 | """IRust repl jupyter kernel """ 2 | 3 | __version__ = '0.6.0' 4 | 5 | from ipykernel.kernelbase import Kernel 6 | from jupyter_client.kernelspec import KernelSpecManager 7 | import os 8 | import json 9 | import subprocess 10 | 11 | def log(msg): 12 | # with open("/tmp/irust-kernel-py.log", 'a') as f: 13 | # f.write(msg) 14 | pass 15 | 16 | class IRustKernel(Kernel): 17 | implementation = 'IRust' 18 | implementation_version = '1.0' 19 | language = 'rust' 20 | language_version = '1' 21 | language_info = { 22 | 'name': 'IRust', 23 | 'mimetype': 'text/x-rust', 24 | 'file_extension': '.rs', 25 | } 26 | banner = "IRust" 27 | kernel_name = "irust" 28 | 29 | def __init__(self, **kwargs): 30 | cmd = ["cmd","/c", os.path.join(self.get_kernel_location(),"re")] if os.name == 'nt' else [os.path.join(self._get_kernel_location(),"re"),] 31 | self.re = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 32 | super().__init__(**kwargs) 33 | 34 | 35 | def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): 36 | # Send the first JSON object to the process's standard input 37 | code_object = {"Execute": {"code": code}} 38 | json_code = json.dumps(code_object) 39 | self.re.stdin.write(json_code.encode("utf-8")) 40 | self.re.stdin.write(b"\n") 41 | self.re.stdin.flush() 42 | 43 | # Read the first JSON object from the process's standard output 44 | json_result = self.re.stdout.readline().decode("utf-8") 45 | action_type = json.loads(json_result) 46 | 47 | # Handle Stream response 48 | if "AddDependencyStream" in action_type: 49 | log("AddDependencyStream") 50 | while True: 51 | action = action_type["AddDependencyStream"] 52 | self.send_response(self.iopub_socket, 'stream', { 53 | 'name': 'stderr', 54 | 'text': action["output_chunk"] 55 | }) 56 | log(action["output_chunk"]) 57 | json_result = self.re.stdout.readline().decode("utf-8") 58 | action_type = json.loads(json_result) 59 | if "AddDependencyEnd" in action_type: 60 | log("AddDependencyEnd") 61 | break 62 | 63 | # Handle Eval 64 | if "Eval" in action_type: 65 | action = action_type["Eval"] 66 | self.send_response(self.iopub_socket, 'display_data', { 67 | 'metadata': {}, 68 | 'data': { 69 | action["mime_type"]: action["value"] 70 | } 71 | }) 72 | 73 | return {'status': 'ok', 74 | 'execution_count': self.execution_count, 75 | 'payload': [], 76 | 'user_expressions': {}, 77 | } 78 | 79 | 80 | def _get_kernel_location(self): 81 | kernel_spec_manager = KernelSpecManager() 82 | kernel_dir = os.path.join(str(kernel_spec_manager.user_kernel_dir),self.kernel_name) 83 | return kernel_dir 84 | 85 | 86 | 87 | if __name__ == '__main__': 88 | from ipykernel.kernelapp import IPKernelApp 89 | IPKernelApp.launch_instance(kernel_class=IRustKernel) 90 | -------------------------------------------------------------------------------- /crates/irust_repl/irust_kernel/irust_kernel/resources/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | 4 | LOGO_PATH = pathlib.Path(__file__).resolve().parent / "logo-svg.svg" 5 | -------------------------------------------------------------------------------- /crates/irust_repl/irust_kernel/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "irust_kernel" 7 | authors = [ 8 | {name = "Bedis Nbiba", email = "bedisnbiba@gmail.com"}, 9 | ] 10 | dependencies = ["ipykernel"] 11 | classifiers = [ 12 | "Framework :: Jupyter", 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3", 15 | ] 16 | dynamic = ["version", "description"] 17 | 18 | [project.urls] 19 | Source = "https://github.com/sigmasd/irust/" 20 | -------------------------------------------------------------------------------- /crates/irust_repl/src/compile_mode.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | use std::str::FromStr; 4 | 5 | #[derive(Debug, Clone, Copy)] 6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 7 | pub enum CompileMode { 8 | Debug, 9 | Release, 10 | } 11 | impl CompileMode { 12 | pub fn is_release(&self) -> bool { 13 | matches!(self, Self::Release) 14 | } 15 | } 16 | 17 | impl FromStr for CompileMode { 18 | type Err = Box; 19 | fn from_str(s: &str) -> std::result::Result { 20 | match s.to_lowercase().as_ref() { 21 | "debug" => Ok(CompileMode::Debug), 22 | "release" => Ok(CompileMode::Release), 23 | _ => Err("Unknown compile mode".into()), 24 | } 25 | } 26 | } 27 | 28 | impl std::fmt::Display for CompileMode { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | match self { 31 | CompileMode::Debug => write!(f, "Debug"), 32 | CompileMode::Release => write!(f, "Release"), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/irust_repl/src/edition.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | use std::str::FromStr; 4 | 5 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum Edition { 8 | E2015, 9 | E2018, 10 | E2021, 11 | #[default] 12 | E2024, 13 | } 14 | 15 | impl FromStr for Edition { 16 | type Err = Box; 17 | fn from_str(s: &str) -> std::result::Result { 18 | match s.to_lowercase().as_str() { 19 | "2015" => Ok(Edition::E2015), 20 | "2018" => Ok(Edition::E2018), 21 | "2021" => Ok(Edition::E2021), 22 | "2024" => Ok(Edition::E2024), 23 | _ => Err("Unknown edition".into()), 24 | } 25 | } 26 | } 27 | impl std::fmt::Display for Edition { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | match self { 30 | Edition::E2015 => write!(f, "2015"), 31 | Edition::E2018 => write!(f, "2018"), 32 | Edition::E2021 => write!(f, "2021"), 33 | Edition::E2024 => write!(f, "2024"), 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/irust_repl/src/executor.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | use std::str::FromStr; 4 | 5 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum Executor { 8 | #[default] 9 | Sync, 10 | Tokio, 11 | AsyncStd, 12 | } 13 | 14 | impl Executor { 15 | pub(crate) fn main(&self) -> String { 16 | match self { 17 | Executor::Sync => "fn main()".into(), 18 | Executor::Tokio => "#[tokio::main]async fn main()".into(), 19 | Executor::AsyncStd => "#[async_std::main]async fn main()".into(), 20 | } 21 | } 22 | /// Invokation that can be used with cargo-add 23 | /// The first argument is the crate name, it should be used with cargo-rm 24 | pub(crate) fn dependecy(&self) -> Option> { 25 | match self { 26 | Executor::Sync => None, 27 | Executor::Tokio => Some(vec![ 28 | "tokio".into(), 29 | "--features".into(), 30 | "macros rt-multi-thread".into(), 31 | ]), 32 | Executor::AsyncStd => Some(vec![ 33 | "async_std".into(), 34 | "--features".into(), 35 | "attributes".into(), 36 | ]), 37 | } 38 | } 39 | } 40 | impl FromStr for Executor { 41 | type Err = Box; 42 | fn from_str(s: &str) -> std::result::Result { 43 | match s { 44 | "sync" => Ok(Executor::Sync), 45 | "tokio" => Ok(Executor::Tokio), 46 | "async_std" => Ok(Executor::AsyncStd), 47 | _ => Err("Unknown executor".into()), 48 | } 49 | } 50 | } 51 | 52 | impl std::fmt::Display for Executor { 53 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 54 | match self { 55 | Executor::Sync => write!(f, "sync"), 56 | Executor::Tokio => write!(f, "tokio"), 57 | Executor::AsyncStd => write!(f, "async_std"), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/irust_repl/src/main_result.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, str::FromStr}; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 7 | #[derive(Debug, Clone, Copy, Default)] 8 | pub enum MainResult { 9 | /// fn main() -> () {()} 10 | #[default] 11 | Unit, 12 | /// fn main() -> Result<(), Box> {Ok(())} 13 | /// allows using `?` with no boilerplate 14 | Result, 15 | } 16 | 17 | impl MainResult { 18 | pub(crate) fn ttype(&self) -> &'static str { 19 | match self { 20 | Self::Unit => "()", 21 | Self::Result => "Result<(), Box>", 22 | } 23 | } 24 | pub(crate) fn instance(&self) -> &'static str { 25 | match self { 26 | Self::Unit => "()", 27 | Self::Result => "Ok(())", 28 | } 29 | } 30 | } 31 | 32 | impl FromStr for MainResult { 33 | type Err = Box; 34 | fn from_str(s: &str) -> std::result::Result { 35 | match s.to_lowercase().as_str() { 36 | "unit" => Ok(MainResult::Unit), 37 | "result" => Ok(MainResult::Result), 38 | _ => Err("Unknown main result type".into()), 39 | } 40 | } 41 | } 42 | 43 | impl Display for MainResult { 44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 45 | match self { 46 | MainResult::Unit => write!(f, "Unit"), 47 | MainResult::Result => write!(f, "Result<(), Box>"), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/irust_repl/src/toolchain.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | use std::{fmt::Display, str::FromStr}; 4 | 5 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub enum ToolChain { 8 | Stable, 9 | Beta, 10 | Nightly, 11 | // cargo with no +argument, it can be different from the above 12 | #[default] 13 | Default, 14 | } 15 | 16 | impl FromStr for ToolChain { 17 | type Err = Box; 18 | fn from_str(s: &str) -> std::result::Result { 19 | match s.to_lowercase().as_str() { 20 | "stable" => Ok(ToolChain::Stable), 21 | "beta" => Ok(ToolChain::Beta), 22 | "nightly" => Ok(ToolChain::Nightly), 23 | "default" => Ok(ToolChain::Default), 24 | _ => Err("Unknown toolchain".into()), 25 | } 26 | } 27 | } 28 | 29 | impl ToolChain { 30 | pub(crate) fn as_arg(&self) -> &str { 31 | match self { 32 | ToolChain::Stable => "+stable", 33 | ToolChain::Beta => "+beta", 34 | ToolChain::Nightly => "+nightly", 35 | // The caller should not call as_arg for the default toolchain 36 | ToolChain::Default => unreachable!(), 37 | } 38 | } 39 | } 40 | 41 | impl Display for ToolChain { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | match self { 44 | ToolChain::Stable => write!(f, "stable"), 45 | ToolChain::Beta => write!(f, "beta"), 46 | ToolChain::Nightly => write!(f, "nightly"), 47 | ToolChain::Default => write!(f, "default"), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/irust_repl/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use std::{ 3 | io::Read, 4 | process::{Child, Output}, 5 | sync::mpsc, 6 | }; 7 | 8 | pub fn stdout_and_stderr(out: Output) -> String { 9 | let out = if !out.stdout.is_empty() { 10 | out.stdout 11 | } else { 12 | out.stderr 13 | }; 14 | 15 | String::from_utf8(out).unwrap_or_default() 16 | } 17 | 18 | pub trait ProcessUtils { 19 | fn interactive_output(self, function: Option Result<()>>) -> Result; 20 | } 21 | 22 | impl ProcessUtils for Child { 23 | fn interactive_output( 24 | mut self, 25 | function: Option Result<()>>, 26 | ) -> Result { 27 | let mut stdout = self.stdout.take().expect("stdout is piped"); 28 | let mut stderr = self.stderr.take().expect("stderr is piped"); 29 | 30 | let (tx_out, rx) = mpsc::channel(); 31 | let tx_err = tx_out.clone(); 32 | enum OutType { 33 | Stdout(Vec), 34 | Stderr(Vec), 35 | } 36 | 37 | std::thread::spawn(move || { 38 | let mut out = Vec::new(); 39 | let _ = stdout.read_to_end(&mut out); 40 | let _ = tx_out.send(OutType::Stdout(out)); 41 | }); 42 | 43 | std::thread::spawn(move || { 44 | let mut err = Vec::new(); 45 | let _ = stderr.read_to_end(&mut err); 46 | let _ = tx_err.send(OutType::Stderr(err)); 47 | }); 48 | 49 | while self.try_wait()?.is_none() { 50 | if let Some(ref function) = function { 51 | function(&mut self)?; 52 | } 53 | } 54 | let mut stdout = None; 55 | let mut stderr = None; 56 | for _ in 0..2 { 57 | match rx.recv()? { 58 | OutType::Stdout(out) => stdout = Some(out), 59 | OutType::Stderr(err) => stderr = Some(err), 60 | } 61 | } 62 | 63 | Ok(Output { 64 | status: self.wait()?, 65 | stdout: stdout.unwrap(), 66 | stderr: stderr.unwrap(), 67 | }) 68 | } 69 | } 70 | 71 | pub fn _is_allowed_in_lib(s: &str) -> bool { 72 | match s.split_whitespace().collect::>().as_slice() { 73 | // async fn|const fn|unsafe fn 74 | [_, "fn", ..] 75 | | ["fn", ..] 76 | | [_, "use", ..] 77 | | ["use", ..] 78 | | ["enum", ..] 79 | | ["struct", ..] 80 | | ["trait", ..] 81 | | ["impl", ..] 82 | | ["pub", ..] 83 | | ["extern", ..] 84 | | ["macro", ..] => true, 85 | ["macro_rules!", ..] => true, 86 | // attribute exp: 87 | // #[derive(Debug)] 88 | // struct B{} 89 | [tag, ..] if tag.starts_with('#') => true, 90 | _ => false, 91 | } 92 | } 93 | 94 | pub fn _remove_semi_col_if_exists(mut s: String) -> String { 95 | if !s.ends_with(';') { 96 | return s; 97 | } 98 | s.pop(); 99 | s 100 | } 101 | 102 | pub fn _is_use_stmt(l: &str) -> bool { 103 | let l = l.trim_start(); 104 | l.starts_with("use") || l.starts_with("#[allow(unused_imports)]use") 105 | } 106 | -------------------------------------------------------------------------------- /crates/irust_repl/tests/repl.rs: -------------------------------------------------------------------------------- 1 | use irust_repl::*; 2 | 3 | #[test] 4 | fn repl() { 5 | let mut repl = Repl::default(); 6 | repl.insert("let a = 4;"); 7 | repl.insert("let b = 6;"); 8 | assert_eq!(repl.eval("a+b").unwrap().output, "10"); 9 | 10 | repl.insert(r#"let c = "hello";"#); 11 | assert_eq!(repl.eval("c.chars().count() < a+b").unwrap().output, "true"); 12 | 13 | repl.set_executor(Executor::AsyncStd).unwrap(); 14 | repl.insert("async fn d() -> usize {4}"); 15 | assert_eq!(repl.eval("d().await").unwrap().output, "4"); 16 | } 17 | 18 | #[test] 19 | fn two_repls_at_the_same_time() { 20 | let mut repl1 = Repl::default(); 21 | let mut repl2 = Repl::default(); 22 | repl1.insert("let a = 4;"); 23 | repl2.insert("let a = 5;"); 24 | 25 | let a1_thread = 26 | std::thread::spawn(move || repl1.eval("a").unwrap().output.parse::().unwrap()); 27 | let a2_thread = 28 | std::thread::spawn(move || repl2.eval("a").unwrap().output.parse::().unwrap()); 29 | 30 | assert_eq!(a1_thread.join().unwrap() + a2_thread.join().unwrap(), 9) 31 | } 32 | -------------------------------------------------------------------------------- /crates/printer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "printer" 3 | version = "0.7.4" 4 | authors = ["Nbiba Bedis "] 5 | edition = "2024" 6 | readme = "README.md" 7 | description = "Abstraction over terminal manipulation" 8 | repository = "https://github.com/sigmaSd/IRust/tree/master/crates/printer" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | crossterm = { version = "0.27.0", features = ["use-dev-tty"] } 13 | unicode-width = "0.1.10" 14 | 15 | [package.metadata.workspaces] 16 | independent = true 17 | -------------------------------------------------------------------------------- /crates/printer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 sigmaSd 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 | -------------------------------------------------------------------------------- /crates/printer/README.md: -------------------------------------------------------------------------------- 1 | # Printer 2 | 3 | Abstraction over terminal manipulations 4 | 5 | Used by [irust](https://github.com/sigmaSd/IRust) 6 | -------------------------------------------------------------------------------- /crates/printer/benches/printer.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | use test::Bencher; 4 | 5 | use printer::printer::{Printer, default_process_fn}; 6 | 7 | // last run 45ns/iter 8 | #[bench] 9 | fn bench_print_input(b: &mut Bencher) { 10 | let buffer = r#"\ 11 | fn default() -> Self { 12 | crossterm::terminal::enable_raw_mode().expect("failed to enable raw_mode"); 13 | let raw = Rc::new(RefCell::new(std::io::stdout())); 14 | Self { 15 | printer: Default::default(), 16 | writer: writer::Writer::new(raw.clone()), 17 | cursor: cursor::Cursor::new(raw), 18 | } 19 | "# 20 | .into(); 21 | 22 | let mut printer = Printer::new(std::io::sink(), "".to_string()); 23 | b.iter(|| printer.print_input(&default_process_fn, &buffer)); 24 | } 25 | -------------------------------------------------------------------------------- /crates/printer/examples/shell.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; 2 | use crossterm::style::Color; 3 | use printer::{ 4 | Result, 5 | buffer::Buffer, 6 | printer::{PrintQueue, Printer, PrinterItem, default_process_fn}, 7 | }; 8 | 9 | fn main() -> Result<()> { 10 | let mut printer = Printer::new(std::io::stdout(), "In: ".into()); 11 | printer.print_prompt_if_set()?; 12 | std::io::Write::flush(&mut printer.writer.raw)?; 13 | 14 | let mut buffer = Buffer::new(); 15 | 16 | loop { 17 | let inp = crossterm::event::read()?; 18 | if let crossterm::event::Event::Key(key) = inp { 19 | match key { 20 | KeyEvent { 21 | kind: KeyEventKind::Release, 22 | .. 23 | } => (), 24 | KeyEvent { 25 | code: KeyCode::Char(c), 26 | modifiers: KeyModifiers::NONE, 27 | .. 28 | } => { 29 | buffer.insert(c); 30 | printer.print_input(&default_process_fn, &buffer)?; 31 | printer.cursor.move_right_unbounded(); 32 | } 33 | KeyEvent { 34 | code: KeyCode::Backspace, 35 | .. 36 | } => { 37 | if !buffer.is_at_start() { 38 | buffer.move_backward(); 39 | printer.cursor.move_left(); 40 | buffer.remove_current_char(); 41 | printer.print_input(&default_process_fn, &buffer)?; 42 | } 43 | } 44 | KeyEvent { 45 | code: KeyCode::Enter, 46 | .. 47 | } => { 48 | if let Some(mut output) = eval(buffer.to_string()) { 49 | output.push_front(PrinterItem::NewLine); 50 | 51 | printer.print_output(output)?; 52 | } 53 | buffer.clear(); 54 | printer.print_prompt_if_set()?; 55 | } 56 | KeyEvent { 57 | code: KeyCode::Char('c'), 58 | modifiers: KeyModifiers::CONTROL, 59 | .. 60 | } => break, 61 | _ => (), 62 | } 63 | } 64 | std::io::Write::flush(&mut printer.writer.raw)?; 65 | } 66 | Ok(()) 67 | } 68 | 69 | fn eval(buffer: String) -> Option { 70 | let mut buffer = buffer.split_whitespace(); 71 | let cmd = buffer.next()?; 72 | let args: Vec<&str> = buffer.collect(); 73 | 74 | #[allow(clippy::blocks_in_conditions)] 75 | match (|| -> Result { 76 | let output = std::process::Command::new(cmd).args(args).output()?; 77 | if output.status.success() { 78 | Ok(PrinterItem::String( 79 | String::from_utf8(output.stdout)?, 80 | Color::Blue, 81 | )) 82 | } else { 83 | Ok(PrinterItem::String( 84 | String::from_utf8(output.stderr)?, 85 | Color::Red, 86 | )) 87 | } 88 | })() { 89 | Ok(result) => Some(result.into()), 90 | Err(e) => Some(PrinterItem::String(e.to_string(), Color::Red).into()), 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /crates/printer/src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | #[derive(Clone, Default)] 4 | pub struct Buffer { 5 | pub buffer: Vec, 6 | pub buffer_pos: usize, 7 | } 8 | 9 | impl Buffer { 10 | pub fn new() -> Self { 11 | Self { ..Self::default() } 12 | } 13 | 14 | pub fn insert(&mut self, c: char) { 15 | self.buffer.insert(self.buffer_pos, c); 16 | self.move_forward(); 17 | } 18 | 19 | pub fn insert_str(&mut self, s: &str) { 20 | s.chars().for_each(|c| self.insert(c)); 21 | } 22 | 23 | pub fn set_buffer_pos(&mut self, pos: usize) { 24 | self.buffer_pos = pos; 25 | } 26 | 27 | pub fn remove_current_char(&mut self) -> Option { 28 | if !self.is_empty() && self.buffer_pos < self.buffer.len() { 29 | let character = self.buffer.remove(self.buffer_pos); 30 | Some(character) 31 | } else { 32 | None 33 | } 34 | } 35 | 36 | pub fn next_char(&self) -> Option<&char> { 37 | self.buffer.get(self.buffer_pos + 1) 38 | } 39 | 40 | pub fn current_char(&self) -> Option<&char> { 41 | self.buffer.get(self.buffer_pos) 42 | } 43 | 44 | pub fn previous_char(&self) -> Option<&char> { 45 | if self.buffer_pos > 0 { 46 | self.buffer.get(self.buffer_pos - 1) 47 | } else { 48 | None 49 | } 50 | } 51 | 52 | pub fn move_forward(&mut self) { 53 | self.buffer_pos += 1; 54 | } 55 | 56 | pub fn move_backward(&mut self) { 57 | if self.buffer_pos != 0 { 58 | self.buffer_pos -= 1; 59 | } 60 | } 61 | 62 | pub fn clear(&mut self) { 63 | self.buffer.clear(); 64 | self.buffer_pos = 0; 65 | } 66 | 67 | pub fn len(&self) -> usize { 68 | self.buffer.len() 69 | } 70 | 71 | pub fn is_empty(&self) -> bool { 72 | self.len() == 0 73 | } 74 | 75 | pub fn is_at_string_line_start(&self) -> bool { 76 | self.is_empty() 77 | || self.buffer[..self.buffer_pos] 78 | .rsplitn(2, |d| d == &'\n') 79 | .next() 80 | .unwrap_or_default() 81 | .iter() 82 | .all(|c| c.is_whitespace()) 83 | } 84 | 85 | pub fn is_at_start(&self) -> bool { 86 | self.buffer_pos == 0 87 | } 88 | 89 | pub fn is_at_end(&self) -> bool { 90 | self.buffer_pos == self.buffer.len() 91 | } 92 | 93 | pub fn goto_start(&mut self) { 94 | self.buffer_pos = 0; 95 | } 96 | 97 | pub fn goto_end(&mut self) { 98 | self.buffer_pos = self.buffer.len(); 99 | } 100 | 101 | pub fn _push_str(&mut self, str: &str) { 102 | self.buffer.extend(str.chars()); 103 | self.buffer_pos = self.buffer.len(); 104 | } 105 | 106 | pub fn get(&self, idx: usize) -> Option<&char> { 107 | self.buffer.get(idx) 108 | } 109 | 110 | pub fn _last(&self) -> Option<&char> { 111 | self.buffer.last() 112 | } 113 | 114 | pub fn iter(&self) -> impl Iterator { 115 | self.buffer.iter() 116 | } 117 | 118 | pub fn take(&mut self) -> Vec { 119 | let buffer = std::mem::take(&mut self.buffer); 120 | self.clear(); 121 | self.goto_start(); 122 | buffer 123 | } 124 | } 125 | 126 | impl std::fmt::Display for Buffer { 127 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 128 | for c in self.buffer.iter() { 129 | f.write_char(*c)?; 130 | } 131 | Ok(()) 132 | } 133 | } 134 | 135 | impl From<&str> for Buffer { 136 | fn from(string: &str) -> Self { 137 | Self { 138 | buffer: string.chars().collect(), 139 | buffer_pos: 0, 140 | } 141 | } 142 | } 143 | impl From for Buffer { 144 | fn from(string: String) -> Self { 145 | Self { 146 | buffer: string.chars().collect(), 147 | buffer_pos: 0, 148 | } 149 | } 150 | } 151 | impl From> for Buffer { 152 | fn from(buffer: Vec) -> Self { 153 | Self { 154 | buffer, 155 | buffer_pos: 0, 156 | } 157 | } 158 | } 159 | impl FromIterator for Buffer { 160 | fn from_iter>(iter: I) -> Buffer { 161 | let mut buffer = Buffer::new(); 162 | for c in iter { 163 | buffer.buffer.push(c); 164 | } 165 | buffer 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /crates/printer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod buffer; 2 | pub mod printer; 3 | 4 | pub type Result = std::result::Result>; 5 | -------------------------------------------------------------------------------- /crates/printer/src/printer/cursor.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | mod bound; 3 | pub use bound::Bound; 4 | mod raw; 5 | use raw::Raw; 6 | 7 | use crate::buffer::Buffer; 8 | /// input is shown with x in this example 9 | /// |In: x 10 | /// | x 11 | /// | x 12 | 13 | #[derive(Debug, Clone, Copy)] 14 | pub struct CursorPosition { 15 | pub current_pos: (usize, usize), 16 | pub starting_pos: (usize, usize), 17 | } 18 | 19 | #[derive(Debug, Clone)] 20 | pub struct Cursor { 21 | //pub for tests only 22 | #[cfg(test)] 23 | pub(super) pos: CursorPosition, 24 | #[cfg(test)] 25 | pub(super) bound: Bound, 26 | 27 | #[cfg(not(test))] 28 | pos: CursorPosition, 29 | #[cfg(not(test))] 30 | bound: Bound, 31 | 32 | pub prompt_len: usize, 33 | pub raw: Raw, 34 | 35 | copy: CursorPosition, 36 | } 37 | 38 | impl Cursor { 39 | pub fn new(raw: Rc>, prompt_len: usize) -> Self { 40 | let mut raw = Raw { raw }; 41 | let (width, height) = raw.size().unwrap_or((400, 400)); 42 | let current_pos = raw.get_current_pos().unwrap_or((0, 0)); 43 | 44 | let pos = CursorPosition { 45 | current_pos, 46 | starting_pos: (0, current_pos.1), 47 | }; 48 | Self { 49 | pos, 50 | copy: pos, 51 | bound: Bound::new(width, height), 52 | raw, 53 | prompt_len, 54 | } 55 | } 56 | 57 | pub fn width(&self) -> usize { 58 | self.bound.width 59 | } 60 | 61 | pub fn height(&self) -> usize { 62 | self.bound.height 63 | } 64 | 65 | pub fn current_pos(&self) -> (usize, usize) { 66 | self.pos.current_pos 67 | } 68 | 69 | pub fn set_current_pos(&mut self, xpos: usize, ypos: usize) { 70 | self.pos.current_pos = (xpos, ypos); 71 | } 72 | 73 | pub fn starting_pos(&self) -> (usize, usize) { 74 | self.pos.starting_pos 75 | } 76 | 77 | pub fn set_starting_pos(&mut self, xpos: usize, ypos: usize) { 78 | self.pos.starting_pos = (xpos, ypos); 79 | } 80 | 81 | pub fn save_position(&mut self) { 82 | self.copy = self.pos; 83 | self.raw 84 | .save_position() 85 | .expect("failed to save cursor position"); 86 | } 87 | 88 | pub fn move_right_unbounded(&mut self) { 89 | self.move_right_inner(self.bound.width - 1); 90 | } 91 | 92 | pub fn move_right(&mut self) { 93 | self.move_right_inner(self.current_row_bound()); 94 | } 95 | 96 | pub fn move_right_inner_optimized(&mut self) { 97 | // Performance: Make sure to not move the cursor if cursor_pos = last_cursor_pos+1 because it moves automatically 98 | if self.pos.current_pos.0 == self.bound.width - 1 { 99 | self.pos.current_pos.0 = self.prompt_len; 100 | self.pos.current_pos.1 += 1; 101 | self.goto_internal_pos(); 102 | } else { 103 | self.pos.current_pos.0 += 1; 104 | } 105 | } 106 | fn move_right_inner(&mut self, bound: usize) { 107 | if self.pos.current_pos.0 == bound { 108 | self.pos.current_pos.0 = self.prompt_len; 109 | self.pos.current_pos.1 += 1; 110 | } else { 111 | self.pos.current_pos.0 += 1; 112 | } 113 | self.goto_internal_pos(); 114 | } 115 | 116 | pub fn move_left(&mut self) { 117 | if self.pos.current_pos.0 == self.prompt_len { 118 | self.pos.current_pos.0 = self.previous_row_bound(); 119 | self.pos.current_pos.1 -= 1; 120 | } else { 121 | self.pos.current_pos.0 -= 1; 122 | } 123 | self.goto_internal_pos(); 124 | } 125 | 126 | pub fn move_up_bounded(&mut self, count: u16) { 127 | self.move_up(count); 128 | self.pos.current_pos.0 = std::cmp::min(self.pos.current_pos.0, self.current_row_bound()); 129 | self.goto_internal_pos(); 130 | } 131 | 132 | pub fn move_up(&mut self, count: u16) { 133 | self.pos.current_pos.1 = self.pos.current_pos.1.saturating_sub(count as usize); 134 | self.raw.move_up(count).expect("failed to move cursor up"); 135 | } 136 | 137 | pub fn move_down_bounded(&mut self, count: u16, buffer: &Buffer) { 138 | self.move_down(count); 139 | // check if we're out of bound 140 | self.pos.current_pos.0 = std::cmp::min(self.pos.current_pos.0, self.current_row_bound()); 141 | // check if we passed the buffer 142 | let last_pos = self.input_last_pos(buffer); 143 | if self.pos.current_pos.1 >= last_pos.1 && self.pos.current_pos.0 >= last_pos.0 { 144 | self.pos.current_pos = last_pos; 145 | } 146 | self.goto_internal_pos(); 147 | } 148 | 149 | pub fn move_down(&mut self, count: u16) { 150 | self.pos.current_pos.1 += count as usize; 151 | self.raw 152 | .move_down(count) 153 | .expect("failed to move cursor down"); 154 | } 155 | 156 | pub fn use_current_row_as_starting_row(&mut self) { 157 | self.pos.starting_pos.1 = self.pos.current_pos.1; 158 | } 159 | 160 | pub fn previous_row_bound(&self) -> usize { 161 | self.bound.get_bound(self.pos.current_pos.1 - 1) 162 | } 163 | 164 | pub fn current_row_bound(&self) -> usize { 165 | self.bound.get_bound(self.pos.current_pos.1) 166 | } 167 | 168 | pub fn reset_bound(&mut self) { 169 | self.bound.reset(); 170 | } 171 | 172 | pub fn bound_current_row_at_current_col(&mut self) { 173 | self.bound 174 | .set_bound(self.pos.current_pos.1, self.pos.current_pos.0); 175 | } 176 | 177 | /// Check if adding new_lines to the buffer will make it overflow the screen height and return 178 | /// that amount if so (0 if not) 179 | pub fn screen_height_overflow_by_new_lines(&self, buffer: &Buffer, new_lines: usize) -> usize { 180 | // if current row + new lines < self.raw..bound.height there is no overflow so unwrap to 0 181 | (new_lines + self.input_last_pos(buffer).1).saturating_sub(self.bound.height - 1) 182 | } 183 | 184 | pub fn restore_position(&mut self) { 185 | self.pos = self.copy; 186 | self.raw 187 | .restore_position() 188 | .expect("failed to restore cursor position"); 189 | } 190 | 191 | pub fn goto_internal_pos(&mut self) { 192 | self.raw 193 | .goto(self.pos.current_pos.0 as u16, self.pos.current_pos.1 as u16) 194 | .expect("failed to move cursor"); 195 | } 196 | 197 | pub fn goto(&mut self, x: usize, y: usize) { 198 | self.pos.current_pos.0 = x; 199 | self.pos.current_pos.1 = y; 200 | 201 | self.goto_internal_pos(); 202 | } 203 | 204 | pub fn hide(&mut self) { 205 | self.raw.hide().expect("failed to hide cursor"); 206 | } 207 | 208 | pub fn show(&mut self) { 209 | self.raw.show().expect("failed to show cursor"); 210 | } 211 | 212 | pub fn goto_start(&mut self) { 213 | self.pos.current_pos.0 = self.pos.starting_pos.0; 214 | self.pos.current_pos.1 = self.pos.starting_pos.1; 215 | self.goto_internal_pos(); 216 | } 217 | 218 | pub fn goto_input_start_col(&mut self) { 219 | self.pos.current_pos.0 = self.pos.starting_pos.0 + self.prompt_len; 220 | self.pos.current_pos.1 = self.pos.starting_pos.1; 221 | self.goto_internal_pos(); 222 | } 223 | 224 | pub fn is_at_last_terminal_col(&self) -> bool { 225 | self.pos.current_pos.0 == self.bound.width - 1 226 | } 227 | 228 | pub fn is_at_last_terminal_row(&self) -> bool { 229 | self.pos.current_pos.1 == self.bound.height - 1 230 | } 231 | 232 | pub fn is_at_line_end(&self) -> bool { 233 | self.pos.current_pos.0 == self.current_row_bound() 234 | } 235 | 236 | pub fn is_at_line_start(&self) -> bool { 237 | self.pos.current_pos.0 == self.prompt_len 238 | } 239 | 240 | pub fn is_at_col(&self, col: usize) -> bool { 241 | self.pos.current_pos.0 == col 242 | } 243 | 244 | pub fn buffer_pos_to_cursor_pos(&self, buffer: &Buffer) -> (usize, usize) { 245 | let last_buffer_pos = buffer.len(); 246 | let max_line_chars = self.bound.width - self.prompt_len; 247 | 248 | let mut y = buffer 249 | .iter() 250 | .take(last_buffer_pos) 251 | .filter(|c| **c == '\n') 252 | .count(); 253 | 254 | let mut x = 0; 255 | for i in 0..last_buffer_pos { 256 | match buffer.get(i) { 257 | Some('\n') => x = 0, 258 | _ => x += 1, 259 | }; 260 | if x == max_line_chars { 261 | x = 0; 262 | y += 1; 263 | } 264 | } 265 | 266 | (x, y) 267 | } 268 | 269 | pub fn input_last_pos(&self, buffer: &Buffer) -> (usize, usize) { 270 | let relative_pos = self.buffer_pos_to_cursor_pos(buffer); 271 | //let relative_pos = buffer.last_buffer_pos_to_relative_cursor_pos(self.bound.width); 272 | let x = relative_pos.0 + self.prompt_len; 273 | let y = relative_pos.1 + self.pos.starting_pos.1; 274 | 275 | (x, y) 276 | } 277 | 278 | pub fn move_to_input_last_row(&mut self, buffer: &Buffer) { 279 | let input_last_row = self.input_last_pos(buffer).1; 280 | self.goto(0, input_last_row); 281 | } 282 | pub fn goto_last_row(&mut self, buffer: &Buffer) { 283 | self.pos.current_pos.1 = self.input_last_pos(buffer).1; 284 | self.pos.current_pos.0 = std::cmp::min(self.pos.current_pos.0, self.current_row_bound()); 285 | self.goto_internal_pos(); 286 | } 287 | 288 | pub fn is_at_first_input_line(&self) -> bool { 289 | self.pos.current_pos.1 == self.pos.starting_pos.1 290 | } 291 | 292 | pub fn is_at_last_input_line(&self, buffer: &Buffer) -> bool { 293 | self.pos.current_pos.1 == self.input_last_pos(buffer).1 294 | } 295 | 296 | pub fn cursor_pos_to_buffer_pos(&self) -> usize { 297 | self.pos.current_pos.0 - self.prompt_len 298 | + self.bound.bounds_sum( 299 | self.pos.starting_pos.1, 300 | self.pos.current_pos.1, 301 | self.prompt_len, 302 | ) 303 | } 304 | 305 | pub fn goto_next_row_terminal_start(&mut self) { 306 | self.goto(0, self.pos.current_pos.1 + 1); 307 | } 308 | 309 | pub fn update_dimensions(&mut self, width: u16, height: u16) { 310 | self.bound = Bound::new(width as usize, height as usize); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /crates/printer/src/printer/cursor/bound.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default, Clone)] 2 | pub struct Bound { 3 | pub bound: Vec, 4 | pub width: usize, 5 | pub height: usize, 6 | } 7 | 8 | impl Bound { 9 | pub fn new(width: usize, height: usize) -> Self { 10 | Self { 11 | bound: vec![width - 1; height], 12 | width, 13 | height, 14 | } 15 | } 16 | 17 | pub fn reset(&mut self) { 18 | *self = Self::new(self.width, self.height); 19 | } 20 | 21 | pub fn get_bound(&self, row: usize) -> usize { 22 | self.bound.get(row).copied().unwrap_or(self.width - 1) 23 | } 24 | pub fn _get_mut_bound(&mut self, row: usize) -> &mut usize { 25 | self.bound.get_mut(row).unwrap() 26 | } 27 | 28 | pub fn set_bound(&mut self, row: usize, col: usize) { 29 | self.bound[row] = col; 30 | } 31 | 32 | pub fn _insert_bound(&mut self, row: usize, col: usize) { 33 | // circular buffer 34 | self.bound.insert(row, col); 35 | self.bound[0] = self.bound.pop().unwrap(); 36 | } 37 | 38 | pub fn bounds_sum(&self, start_row: usize, end_row: usize, prompt_len: usize) -> usize { 39 | self.bound 40 | .iter() 41 | .take(end_row) 42 | .skip(start_row) 43 | .map(|b| b + 1 - prompt_len) 44 | .sum() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/printer/src/printer/cursor/raw.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use crossterm::cursor::*; 3 | use crossterm::queue; 4 | use std::{cell::RefCell, rc::Rc}; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct Raw { 8 | pub raw: Rc>, 9 | } 10 | impl std::io::Write for Raw { 11 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 12 | self.raw.borrow_mut().write(buf) 13 | } 14 | fn flush(&mut self) -> std::io::Result<()> { 15 | self.raw.borrow_mut().flush() 16 | } 17 | } 18 | 19 | impl Raw { 20 | pub fn restore_position(&mut self) -> Result<()> { 21 | queue!(self, RestorePosition)?; 22 | Ok(()) 23 | } 24 | 25 | pub fn save_position(&mut self) -> Result<()> { 26 | queue!(self, SavePosition)?; 27 | Ok(()) 28 | } 29 | 30 | pub fn move_down(&mut self, n: u16) -> Result<()> { 31 | queue!(self, MoveDown(n))?; 32 | Ok(()) 33 | } 34 | 35 | pub fn move_up(&mut self, n: u16) -> Result<()> { 36 | queue!(self, MoveUp(n))?; 37 | Ok(()) 38 | } 39 | 40 | pub fn show(&mut self) -> Result<()> { 41 | queue!(self, Show)?; 42 | Ok(()) 43 | } 44 | 45 | pub fn hide(&mut self) -> Result<()> { 46 | queue!(self, Hide)?; 47 | Ok(()) 48 | } 49 | 50 | pub fn goto(&mut self, x: u16, y: u16) -> Result<()> { 51 | queue!(self, MoveTo(x, y))?; 52 | Ok(()) 53 | } 54 | 55 | pub fn size(&self) -> Result<(usize, usize)> { 56 | Ok(crossterm::terminal::size().map(|(w, h)| (w as usize, h as usize))?) 57 | } 58 | 59 | pub fn get_current_pos(&mut self) -> Result<(usize, usize)> { 60 | // position only uses stdout() 61 | Ok(crossterm::cursor::position().map(|(w, h)| (w as usize, h as usize))?) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/printer/src/printer/tests.rs: -------------------------------------------------------------------------------- 1 | use super::Printer; 2 | use super::default_process_fn; 3 | use crossterm::style::Color; 4 | use std::io::Write; 5 | 6 | type Result = std::result::Result>; 7 | #[test] 8 | fn write_from_terminal_start_cursor_pos_correct() -> Result<()> { 9 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 10 | 11 | let origin_pos = p.cursor.pos; 12 | p.write_from_terminal_start("hello", Color::Red)?; 13 | assert_eq!(p.cursor.pos.current_pos.0, 5); 14 | assert_eq!(p.cursor.pos.current_pos.1, origin_pos.current_pos.1); 15 | 16 | Ok(()) 17 | } 18 | 19 | #[test] 20 | fn writenew_line_no_scroll() { 21 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 22 | 23 | let b = "Hello world".into(); 24 | 25 | p.cursor.pos.starting_pos.0 = 0; 26 | p.cursor.pos.starting_pos.1 = 0; 27 | p.cursor.goto_start(); 28 | assert_eq!(p.cursor.pos.current_pos, p.cursor.pos.starting_pos); 29 | 30 | let origin_pos = p.cursor.pos; 31 | p.write_newline(&b); 32 | 33 | assert_eq!(origin_pos.starting_pos.1 + 1, p.cursor.pos.starting_pos.1); 34 | assert_eq!(origin_pos.current_pos.1 + 1, p.cursor.pos.current_pos.1); 35 | } 36 | 37 | #[test] 38 | fn writenew_line_with_scroll() { 39 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 40 | let b = "Hello world".into(); 41 | 42 | p.cursor.pos.starting_pos.0 = 0; 43 | p.cursor.pos.starting_pos.1 = p.cursor.bound.height - 1; 44 | p.cursor.goto_start(); 45 | 46 | assert_eq!(p.cursor.pos.current_pos, p.cursor.pos.starting_pos); 47 | 48 | let origin_pos = p.cursor.pos; 49 | p.write_newline(&b); 50 | 51 | assert_eq!(origin_pos.starting_pos.1, p.cursor.pos.starting_pos.1); 52 | assert_eq!(origin_pos.current_pos.1, p.cursor.pos.current_pos.1); 53 | } 54 | 55 | #[test] 56 | fn scroll_up() -> Result<()> { 57 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 58 | 59 | let origin_pos = p.cursor.pos; 60 | p.scroll_up(3); 61 | 62 | assert_eq!( 63 | origin_pos.starting_pos.1.saturating_sub(3), 64 | p.cursor.pos.starting_pos.1 65 | ); 66 | assert_eq!( 67 | origin_pos.current_pos.1.saturating_sub(3), 68 | p.cursor.pos.current_pos.1 69 | ); 70 | 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | fn scroll_because_input_needs_scroll() -> Result<()> { 76 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 77 | let b = "\n\n\n".into(); 78 | 79 | p.cursor.pos.starting_pos.0 = 0; 80 | p.cursor.pos.starting_pos.1 = p.cursor.bound.height - 1; 81 | p.cursor.goto_start(); 82 | 83 | let original_pos = p.cursor.pos; 84 | p.scroll_if_needed_for_input(&b); 85 | 86 | assert_eq!(original_pos.starting_pos.1 - 3, p.cursor.pos.starting_pos.1); 87 | Ok(()) 88 | } 89 | 90 | #[test] 91 | fn dont_scroll_because_input_doesent_need_scroll() -> Result<()> { 92 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 93 | let b = "\n\n\n".into(); 94 | 95 | p.cursor.pos.starting_pos.0 = 0; 96 | p.cursor.pos.starting_pos.1 = 0; 97 | p.cursor.goto_start(); 98 | 99 | let original_pos = p.cursor.pos; 100 | p.scroll_if_needed_for_input(&b); 101 | 102 | assert_eq!(original_pos.starting_pos.1, p.cursor.pos.starting_pos.1); 103 | Ok(()) 104 | } 105 | 106 | #[test] 107 | fn calculate_bounds_correctly() -> Result<()> { 108 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 109 | let width = p.cursor.bound.width; 110 | let height = p.cursor.bound.height; 111 | let queue = default_process_fn(&"alloc\nprint".into()); 112 | 113 | // 1 114 | move_to_and_modify_start(&mut p, 0, 0); 115 | p.recalculate_bounds(queue)?; 116 | 117 | let expected_bound = { 118 | let mut v = vec![width - 1; height]; 119 | v[0] = 9; 120 | v[1] = 9; 121 | v 122 | }; 123 | assert_eq!(expected_bound, p.cursor.bound.bound); 124 | Ok(()) 125 | } 126 | 127 | #[test] 128 | pub fn calculate_bounds_correctly2() -> Result<()> { 129 | let mut p = Printer::new(std::io::sink(), "".to_owned()); 130 | let width = p.cursor.bound.width; 131 | let height = p.cursor.bound.height; 132 | let queue = default_process_fn(&"A\tz\nBC\n".into()); 133 | // 2 134 | move_to_and_modify_start(&mut p, 0, height - 5); 135 | p.recalculate_bounds(queue)?; 136 | 137 | let expected_bound = { 138 | let mut v = vec![width - 1; height]; 139 | v[height - 5] = 7; 140 | v[height - 4] = 6; 141 | v[height - 3] = 4; 142 | v 143 | }; 144 | assert_eq!(expected_bound, p.cursor.bound.bound); 145 | 146 | Ok(()) 147 | } 148 | 149 | // helper 150 | fn move_to_and_modify_start(printer: &mut Printer, x: usize, y: usize) { 151 | printer.cursor.pos.starting_pos.0 = x; 152 | printer.cursor.pos.starting_pos.1 = y; 153 | printer.cursor.goto_start(); 154 | } 155 | -------------------------------------------------------------------------------- /crates/printer/src/printer/writer.rs: -------------------------------------------------------------------------------- 1 | use crate::{Result, buffer::Buffer}; 2 | use crossterm::{style::Color, terminal::ClearType}; 3 | mod raw; 4 | use raw::Raw; 5 | use std::{cell::RefCell, rc::Rc}; 6 | 7 | use super::cursor::Cursor; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Writer { 11 | pub last_color: Option, 12 | pub raw: Raw, 13 | } 14 | 15 | impl Writer { 16 | pub(super) fn new(raw: Rc>) -> Self { 17 | let raw = Raw { raw }; 18 | Self { 19 | last_color: None, 20 | raw, 21 | } 22 | } 23 | 24 | pub(super) fn write( 25 | &mut self, 26 | out: &str, 27 | color: Color, 28 | cursor: &mut super::cursor::Cursor, 29 | ) -> Result<()> { 30 | // Performance: set_fg only when needed 31 | 32 | if self.last_color != Some(color) { 33 | self.raw.set_fg(color)?; 34 | } 35 | 36 | for c in out.chars() { 37 | self.write_char(c, cursor)?; 38 | } 39 | 40 | self.last_color = Some(color); 41 | Ok(()) 42 | } 43 | 44 | pub(super) fn write_char_with_color( 45 | &mut self, 46 | c: char, 47 | color: Color, 48 | cursor: &mut super::cursor::Cursor, 49 | ) -> Result<()> { 50 | // Performance: set_fg only when needed 51 | if self.last_color != Some(color) { 52 | self.raw.set_fg(color)?; 53 | } 54 | self.write_char(c, cursor)?; 55 | self.last_color = Some(color); 56 | Ok(()) 57 | } 58 | 59 | pub(super) fn write_char( 60 | &mut self, 61 | c: char, 62 | cursor: &mut super::cursor::Cursor, 63 | ) -> Result<()> { 64 | let c = Self::validate_char(c); 65 | if c == '\t' { 66 | self.handle_tab(cursor)?; 67 | return Ok(()); 68 | } 69 | self.raw.write(c)?; 70 | // Performance: Make sure to not move the cursor if cursor_pos = last_cursor_pos+1 because it moves automatically 71 | cursor.move_right_inner_optimized(); 72 | Ok(()) 73 | } 74 | 75 | fn validate_char(c: char) -> char { 76 | // HACK: only accept chars with witdh == 1 77 | // if c_w == witdh=0 or if c_w > witdh=1 replace c with '�' 78 | let width = unicode_width::UnicodeWidthChar::width(c).unwrap_or(1); 79 | if width == 0 || width > 1 { '�' } else { c } 80 | } 81 | 82 | fn handle_tab(&mut self, cursor: &mut super::cursor::Cursor) -> Result<()> { 83 | //Tab hack 84 | for _ in 0..4 { 85 | self.raw.write(' ')?; 86 | cursor.move_right_inner_optimized(); 87 | } 88 | Ok(()) 89 | } 90 | 91 | pub(super) fn write_at( 92 | &mut self, 93 | s: &str, 94 | x: usize, 95 | y: usize, 96 | cursor: &mut super::cursor::Cursor, 97 | ) -> Result<()> { 98 | cursor.goto(x, y); 99 | self.raw.write(s)?; 100 | Ok(()) 101 | } 102 | 103 | pub(super) fn write_at_no_cursor( 104 | &mut self, 105 | s: &str, 106 | color: Color, 107 | x: usize, 108 | y: usize, 109 | cursor: &mut super::cursor::Cursor, 110 | ) -> Result<()> { 111 | self.raw.set_fg(color)?; 112 | let origin_pos = cursor.current_pos(); 113 | self.write_at(s, x, y, cursor)?; 114 | cursor.goto(origin_pos.0, origin_pos.1); 115 | self.raw.reset_color()?; 116 | Ok(()) 117 | } 118 | 119 | pub(super) fn write_from_terminal_start( 120 | &mut self, 121 | out: &str, 122 | color: Color, 123 | cursor: &mut super::cursor::Cursor, 124 | ) -> Result<()> { 125 | cursor.goto(0, cursor.current_pos().1); 126 | self.write(out, color, cursor)?; 127 | Ok(()) 128 | } 129 | 130 | pub(super) fn write_newline(&mut self, cursor: &mut Cursor, buffer: &Buffer) { 131 | cursor.move_to_input_last_row(buffer); 132 | 133 | // check for scroll 134 | if cursor.is_at_last_terminal_row() { 135 | self.scroll_up(1, cursor); 136 | } 137 | 138 | cursor.move_down(1); 139 | cursor.use_current_row_as_starting_row(); 140 | } 141 | 142 | pub(super) fn clear(&mut self, cursor: &mut super::cursor::Cursor) -> Result<()> { 143 | self.raw.clear(ClearType::All)?; 144 | 145 | cursor.set_starting_pos(0, 0); 146 | cursor.goto_start(); 147 | cursor.reset_bound(); 148 | Ok(()) 149 | } 150 | 151 | pub(super) fn clear_last_line(&mut self, cursor: &mut super::cursor::Cursor) -> Result<()> { 152 | let origin_pos = cursor.current_pos(); 153 | cursor.goto(0, cursor.height() - 1); 154 | self.raw.clear(ClearType::CurrentLine)?; 155 | cursor.goto(origin_pos.0, origin_pos.1); 156 | Ok(()) 157 | } 158 | 159 | pub(super) fn scroll_up(&mut self, n: usize, cursor: &mut super::cursor::Cursor) { 160 | self.raw.scroll_up(n as u16).expect("failed to scroll-up"); 161 | cursor.move_up(n as u16); 162 | let original_starting_pos = cursor.starting_pos(); 163 | cursor.set_starting_pos( 164 | original_starting_pos.0, 165 | original_starting_pos.1.saturating_sub(n), 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /crates/printer/src/printer/writer/raw.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use crossterm::{queue, style::*, terminal::*}; 3 | use std::{cell::RefCell, fmt::Display, rc::Rc, sync::OnceLock}; 4 | 5 | static NO_COLOR: OnceLock = OnceLock::new(); 6 | fn no_color() -> bool { 7 | *NO_COLOR.get_or_init(|| std::env::var("NO_COLOR").is_ok()) 8 | } 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct Raw { 12 | pub raw: Rc>, 13 | } 14 | 15 | impl std::io::Write for Raw { 16 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 17 | self.raw.borrow_mut().write(buf) 18 | } 19 | fn flush(&mut self) -> std::io::Result<()> { 20 | self.raw.borrow_mut().flush() 21 | } 22 | } 23 | 24 | impl Raw { 25 | pub fn scroll_up(&mut self, n: u16) -> Result<()> { 26 | queue!(self, ScrollUp(n))?; 27 | Ok(()) 28 | } 29 | 30 | pub fn clear(&mut self, clear_type: ClearType) -> Result<()> { 31 | queue!(self, Clear(clear_type))?; 32 | Ok(()) 33 | } 34 | 35 | pub fn _write(&mut self, value: D) -> Result<()> { 36 | queue!(self, Print(value))?; 37 | Ok(()) 38 | } 39 | 40 | pub fn write(&mut self, value: D) -> Result<()> { 41 | self._write(value) 42 | } 43 | 44 | pub fn write_with_color(&mut self, value: D, color: Color) -> Result<()> { 45 | self.set_fg(color)?; 46 | self.write(value)?; 47 | self.reset_color()?; 48 | Ok(()) 49 | } 50 | 51 | pub fn set_title(&mut self, title: &str) -> Result<()> { 52 | queue!(self, SetTitle(title))?; 53 | Ok(()) 54 | } 55 | 56 | // color commands 57 | 58 | pub fn reset_color(&mut self) -> Result<()> { 59 | if no_color() { 60 | return Ok(()); 61 | } 62 | queue!(self, ResetColor)?; 63 | Ok(()) 64 | } 65 | 66 | pub fn set_fg(&mut self, color: Color) -> Result<()> { 67 | if no_color() { 68 | return Ok(()); 69 | } 70 | queue!(self, SetForegroundColor(color))?; 71 | Ok(()) 72 | } 73 | 74 | pub fn set_bg(&mut self, color: Color) -> Result<()> { 75 | if no_color() { 76 | return Ok(()); 77 | } 78 | queue!(self, SetBackgroundColor(color))?; 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /distro/io.github.sigmasd.IRust.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=IRust 3 | TryExec=irust 4 | Exec=irust %F 5 | Terminal=true 6 | Type=Application 7 | Keywords=Rust;REPL;development;programming; 8 | Categories=Development;IDE; 9 | Icon=io.github.sigmasd.IRust 10 | -------------------------------------------------------------------------------- /distro/io.github.sigmasd.IRust.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.sigmasd.IRust 4 | IRust 5 | Rust Repl 6 | Bedis Nbiba 7 | 8 | MIT 9 | MIT 10 | 11 | 12 |

13 | IRust is a repl for the rust language. 14 |

15 |
16 | 17 | io.github.sigmasd.IRust.desktop 18 | 19 | 20 | 21 | IRust screenshot 22 | https://raw.githubusercontent.com/sigmaSd/IRust/13e0f71ec4c3e765ecff3a2e6d1ed900ea0b69f9/irust.png 23 | 24 | 25 | 26 | https://github.com/sigmaSd/IRust 27 | https://github.com/sigmaSd/IRust/issues 28 | 29 | 30 | 31 | 32 | 33 | 34 |
    35 |
  • General cleanup and deps update
  • 36 |
37 |
38 |
39 | 40 | 41 |
    42 |
  • Fix formating of output on newer version of rust
  • 43 |
44 |
45 |
46 | 47 | 48 |
    49 |
  • Switch metadata to desktop-application to make it appear in stores
  • 50 |
51 |
52 |
53 | 54 | 55 |
    56 |
  • Make :add command more robust
  • 57 |
58 |
59 |
60 | 61 | 62 |
    63 |
  • Fix `:edit` command
  • 64 |
65 |
66 |
67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | rust 75 | repl 76 | development 77 | programming 78 | 79 | 80 | 81 | Development 82 | IDE 83 | 84 | 85 | 86 | irust 87 | 88 |
89 | -------------------------------------------------------------------------------- /distro/io.github.sigmasd.IRust.svg: -------------------------------------------------------------------------------- 1 | 2 | IRUST 102 | -------------------------------------------------------------------------------- /irust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigmaSd/IRust/e7da74d6a9a821a762a8c4a368acefa8bbde6ad2/irust.png -------------------------------------------------------------------------------- /script_examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "./irust_prompt", 6 | "./irust_vim_dylib", 7 | "./ipython", 8 | "./fun", 9 | "./irust_animation", 10 | "./mixed_cmds" 11 | ] 12 | 13 | [workspace.dependencies] 14 | irust_api = { path = "../crates/irust_api/" } 15 | rscript = "0.17.0" 16 | -------------------------------------------------------------------------------- /script_examples/fun/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fun" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | dirs = "5.0.1" 10 | serde = { version = "1.0.188", features = ["derive"] } 11 | toml = "0.7.6" 12 | 13 | irust_api.workspace = true 14 | rscript.workspace = true 15 | -------------------------------------------------------------------------------- /script_examples/fun/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use irust_api::{Command, OutputEvent, Shutdown, color}; 4 | use rscript::scripting::Scripter; 5 | use rscript::{Hook, VersionReq}; 6 | 7 | type Result = std::result::Result>; 8 | 9 | #[derive(Debug)] 10 | struct Fun { 11 | functions: HashMap, 12 | } 13 | 14 | impl Scripter for Fun { 15 | fn name() -> &'static str { 16 | "Fun" 17 | } 18 | 19 | fn script_type() -> rscript::ScriptType { 20 | rscript::ScriptType::Daemon 21 | } 22 | 23 | fn hooks() -> &'static [&'static str] { 24 | &[OutputEvent::NAME, Shutdown::NAME] 25 | } 26 | 27 | fn version_requirement() -> rscript::VersionReq { 28 | VersionReq::parse(">=1.50.0").expect("correct version requirement") 29 | } 30 | } 31 | 32 | fn main() { 33 | let mut fun = Fun::new(); 34 | let _ = Fun::execute(&mut |hook_name| Fun::run(&mut fun, hook_name)); 35 | } 36 | 37 | impl Fun { 38 | fn run(&mut self, hook_name: &str) { 39 | match hook_name { 40 | OutputEvent::NAME => { 41 | let hook: OutputEvent = Self::read(); 42 | let output = match self.handle_output_event(hook) { 43 | Ok(out) => out, 44 | Err(e) => Some(Command::PrintOutput( 45 | e.to_string() + "\n", 46 | color::Color::Red, 47 | )), 48 | }; 49 | Self::write::(&output); 50 | } 51 | Shutdown::NAME => { 52 | let hook: Shutdown = Self::read(); 53 | let output = self.clean_up(hook); 54 | Self::write::(&output); 55 | } 56 | _ => unreachable!(), 57 | } 58 | } 59 | fn new() -> Self { 60 | let functions = (|| { 61 | let fns = 62 | std::fs::read_to_string(dirs::config_dir()?.join("irust/functions.toml")).ok()?; 63 | toml::from_str(&fns).ok() 64 | })() 65 | .unwrap_or_default(); 66 | 67 | Self { functions } 68 | } 69 | fn handle_output_event(&mut self, hook: OutputEvent) -> Result<::Output> { 70 | let input = hook.1; 71 | if !(input.starts_with(":fun") || input.starts_with(":f")) { 72 | return Ok(None); 73 | } 74 | 75 | let buffer = input; 76 | match buffer.splitn(4, ' ').collect::>().as_slice() { 77 | [_, "def" | "d", name, fun] => { 78 | self.functions.insert(name.to_string(), fun.to_string()); 79 | Ok(Some(Command::PrintOutput("Ok!".into(), color::Color::Blue))) 80 | } 81 | [_, name, invoke_arg @ ..] => { 82 | let mut function = self 83 | .functions 84 | .get(*name) 85 | .ok_or(format!("function: `{name}` is not defined"))? 86 | .to_owned(); 87 | 88 | for (idx, arg) in invoke_arg.iter().enumerate() { 89 | let arg_tag = "$arg".to_string() + &idx.to_string(); 90 | function = function.replacen(&arg_tag, arg, 1); 91 | } 92 | 93 | Ok(Some(Command::Parse(function))) 94 | } 95 | _ => Err("Incorrect usage of `fun`".into()), 96 | } 97 | } 98 | fn clean_up(&self, _hook: Shutdown) -> Option { 99 | (|| -> Option<()> { 100 | std::fs::write( 101 | dirs::config_dir()?.join("irust/functions.toml"), 102 | toml::to_string(&self.functions).ok()?, 103 | ) 104 | .ok() 105 | })(); 106 | None 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /script_examples/ipython/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ipython" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | irust_api.workspace = true 10 | rscript.workspace = true 11 | -------------------------------------------------------------------------------- /script_examples/ipython/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | process::{ChildStdin, Command, Stdio}, 4 | sync::mpsc, 5 | }; 6 | 7 | use irust_api::color; 8 | use rscript::{Hook, ScriptType, VersionReq, scripting::Scripter}; 9 | 10 | #[derive(Default)] 11 | struct IPython { 12 | stdin: Option, 13 | stdout: Option>, 14 | } 15 | 16 | impl Scripter for IPython { 17 | fn script_type() -> ScriptType { 18 | ScriptType::Daemon 19 | } 20 | 21 | fn name() -> &'static str { 22 | "IPython" 23 | } 24 | 25 | fn hooks() -> &'static [&'static str] { 26 | &[ 27 | irust_api::SetTitle::NAME, 28 | irust_api::SetWelcomeMsg::NAME, 29 | irust_api::OutputEvent::NAME, 30 | irust_api::Startup::NAME, 31 | irust_api::Shutdown::NAME, 32 | ] 33 | } 34 | fn version_requirement() -> VersionReq { 35 | VersionReq::parse(">=1.50.0").expect("correct version requirement") 36 | } 37 | } 38 | 39 | fn main() { 40 | let mut ipython = IPython::default(); 41 | let _ = IPython::execute(&mut |hook_name| IPython::run(&mut ipython, hook_name)); 42 | } 43 | 44 | impl IPython { 45 | fn run(&mut self, hook_name: &str) { 46 | match hook_name { 47 | irust_api::OutputEvent::NAME => { 48 | let hook: irust_api::OutputEvent = Self::read(); 49 | let output = self.handle_output_event(hook); 50 | Self::write::(&output); 51 | } 52 | irust_api::SetTitle::NAME => { 53 | let _hook: irust_api::SetTitle = Self::read(); 54 | Self::write::(&Some("IPython".to_string())); 55 | } 56 | irust_api::SetWelcomeMsg::NAME => { 57 | let _hook: irust_api::SetWelcomeMsg = Self::read(); 58 | Self::write::(&Some("IPython".to_string())); 59 | } 60 | irust_api::Startup::NAME => { 61 | let _hook: irust_api::Startup = Self::read(); 62 | self.clean_up(); 63 | *self = Self::start(); 64 | Self::write::(&None); 65 | } 66 | irust_api::Shutdown::NAME => { 67 | let _hook: irust_api::Shutdown = Self::read(); 68 | self.clean_up(); 69 | Self::write::(&None); 70 | } 71 | _ => unreachable!(), 72 | } 73 | } 74 | 75 | pub(crate) fn handle_output_event( 76 | &mut self, 77 | hook: irust_api::OutputEvent, 78 | ) -> ::Output { 79 | if hook.1.starts_with(':') { 80 | return None; 81 | } 82 | 83 | if self.stdin.is_none() { 84 | *self = Self::start(); 85 | } 86 | 87 | let stdin = self.stdin.as_mut().unwrap(); 88 | let stdout = self.stdout.as_mut().unwrap(); 89 | 90 | let input = hook.1 + "\n"; 91 | stdin.write_all(input.as_bytes()).unwrap(); 92 | stdin.flush().unwrap(); 93 | let now = std::time::Instant::now(); 94 | while now.elapsed() < std::time::Duration::from_millis(200) { 95 | if let Ok(out) = stdout.try_recv() { 96 | // Expression Or Statement 97 | if out.is_empty() { 98 | return Some(irust_api::Command::PrintOutput( 99 | "()\n".to_string(), 100 | color::Color::Blue, 101 | )); 102 | } else { 103 | return Some(irust_api::Command::PrintOutput( 104 | out + "\n", 105 | color::Color::Blue, 106 | )); 107 | } 108 | } 109 | } 110 | // Statement 111 | Some(irust_api::Command::PrintOutput( 112 | "()\n".to_string(), 113 | color::Color::Blue, 114 | )) 115 | } 116 | 117 | pub(crate) fn clean_up(&mut self) { 118 | // IPython could have already exited 119 | // So we ignore errors 120 | let _ = self.stdin.as_mut().map(|stdin| stdin.write_all(b"exit\n")); 121 | let _ = self.stdin.as_mut().map(|stdin| stdin.flush()); 122 | } 123 | 124 | fn start() -> IPython { 125 | #[expect(clippy::zombie_processes)] 126 | let mut p = Command::new("ipython") 127 | .stdin(Stdio::piped()) 128 | .stdout(Stdio::piped()) 129 | .spawn() 130 | .unwrap(); 131 | 132 | let stdin = p.stdin.take().unwrap(); 133 | let mut stdout = p.stdout.take().unwrap(); 134 | 135 | let (tx, rx) = mpsc::channel(); 136 | 137 | std::thread::spawn(move || { 138 | let mut buf = [0; 512]; 139 | let _ = stdout.read(&mut buf).unwrap(); 140 | let _ = stdout.read(&mut buf).unwrap(); 141 | tx.send(String::new()).unwrap(); 142 | 143 | let mut out = String::new(); 144 | 145 | loop { 146 | let n = stdout.read(&mut buf).unwrap(); 147 | if n == 0 { 148 | break; 149 | } 150 | 151 | let o = String::from_utf8(buf[..n].to_vec()).unwrap(); 152 | out += &o; 153 | 154 | // Use prompt as delimiter 155 | if !out.contains("\nIn ") { 156 | continue; 157 | } 158 | 159 | // Post Process 160 | let o = { 161 | let mut o: Vec<_> = out.lines().collect(); 162 | o.pop(); 163 | let mut o = o.join("\n"); 164 | if o.contains("...:") { 165 | o = o.rsplit("...:").next().unwrap().to_owned(); 166 | } 167 | o 168 | }; 169 | 170 | // Send output and clear it for the next read 171 | tx.send(o.trim().to_owned()).unwrap(); 172 | out.clear(); 173 | } 174 | }); 175 | // Wait for IPython to start 176 | rx.recv().unwrap(); 177 | 178 | IPython { 179 | stdin: Some(stdin), 180 | stdout: Some(rx), 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /script_examples/irust_animation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "irust_animation" 3 | version = "0.1.0" 4 | authors = ["Nbiba Bedis "] 5 | edition = "2024" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | crossterm = "0.27.0" 11 | irust_api.workspace = true 12 | rscript.workspace = true 13 | 14 | [lib] 15 | crate-type = ["cdylib"] 16 | -------------------------------------------------------------------------------- /script_examples/irust_animation/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::let_unit_value)] 2 | use std::{sync::atomic::AtomicBool, thread}; 3 | 4 | use crossterm::{ 5 | cursor::{Hide, MoveTo, MoveToColumn, RestorePosition, SavePosition, Show}, 6 | style::Print, 7 | }; 8 | use rscript::{ 9 | Hook, ScriptInfo, ScriptType, VersionReq, 10 | scripting::{DynamicScript, FFiData, FFiStr}, 11 | }; 12 | 13 | #[unsafe(no_mangle)] 14 | pub static SCRIPT: DynamicScript = DynamicScript { 15 | script, 16 | script_info, 17 | }; 18 | 19 | static ANIMATION_FLAG: AtomicBool = AtomicBool::new(false); 20 | 21 | extern "C" fn script_info() -> FFiData { 22 | let info = ScriptInfo::new( 23 | "Animation", 24 | ScriptType::DynamicLib, 25 | &[ 26 | irust_api::BeforeCompiling::NAME, 27 | irust_api::AfterCompiling::NAME, 28 | ], 29 | VersionReq::parse(">=1.50.0").expect("correct version requirement"), 30 | ); 31 | info.into_ffi_data() 32 | } 33 | 34 | extern "C" fn script(name: FFiStr, hook: FFiData) -> FFiData { 35 | match name.as_str() { 36 | irust_api::BeforeCompiling::NAME => { 37 | let hook: irust_api::BeforeCompiling = DynamicScript::read(hook); 38 | let output = start(hook); 39 | DynamicScript::write::(&output) 40 | } 41 | irust_api::AfterCompiling::NAME => { 42 | let hook: irust_api::AfterCompiling = DynamicScript::read(hook); 43 | let output = end(hook); 44 | DynamicScript::write::(&output) 45 | } 46 | _ => unreachable!(), 47 | } 48 | } 49 | 50 | fn start(hook: irust_api::BeforeCompiling) { 51 | thread::spawn(move || { 52 | ANIMATION_FLAG.store(true, std::sync::atomic::Ordering::Relaxed); 53 | use crossterm::execute; 54 | use std::io::stdout; 55 | let globals = hook.0; 56 | let mut tick = 0; 57 | const STATUS: &[&str] = &["-", "/", "-", "\\"]; 58 | 59 | while ANIMATION_FLAG.load(std::sync::atomic::Ordering::Relaxed) { 60 | let msg = format!("In [{}]: ", STATUS[tick % STATUS.len()]); 61 | execute!( 62 | stdout(), 63 | SavePosition, 64 | Hide, 65 | MoveTo( 66 | globals.prompt_position.0 as u16, 67 | globals.prompt_position.1 as u16 68 | ), 69 | Print(" ".repeat(globals.prompt_len)), 70 | MoveToColumn(0), 71 | Print(msg), 72 | Show, 73 | RestorePosition 74 | ) 75 | .unwrap(); 76 | 77 | tick += 1; 78 | std::thread::sleep(std::time::Duration::from_millis(100)); 79 | } 80 | }); 81 | } 82 | 83 | fn end(_hook: irust_api::AfterCompiling) { 84 | ANIMATION_FLAG.store(false, std::sync::atomic::Ordering::Relaxed); 85 | } 86 | -------------------------------------------------------------------------------- /script_examples/irust_prompt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "irust_prompt" 3 | version = "0.1.0" 4 | authors = ["Nbiba Bedis "] 5 | edition = "2024" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | irust_api.workspace = true 11 | rscript.workspace = true 12 | -------------------------------------------------------------------------------- /script_examples/irust_prompt/src/main.rs: -------------------------------------------------------------------------------- 1 | use irust_api::GlobalVariables; 2 | use rscript::{Hook, ScriptType, VersionReq, scripting::Scripter}; 3 | 4 | struct Prompt; 5 | 6 | impl Scripter for Prompt { 7 | fn script_type() -> ScriptType { 8 | ScriptType::OneShot 9 | } 10 | 11 | fn name() -> &'static str { 12 | "prompt" 13 | } 14 | 15 | fn hooks() -> &'static [&'static str] { 16 | &[ 17 | irust_api::SetInputPrompt::NAME, 18 | irust_api::SetOutputPrompt::NAME, 19 | irust_api::Shutdown::NAME, 20 | ] 21 | } 22 | fn version_requirement() -> VersionReq { 23 | VersionReq::parse(">=1.50.0").expect("correct version requirement") 24 | } 25 | } 26 | 27 | enum PType { 28 | In, 29 | Out, 30 | } 31 | impl std::fmt::Display for PType { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | match self { 34 | PType::In => write!(f, "In"), 35 | PType::Out => write!(f, "Out"), 36 | } 37 | } 38 | } 39 | 40 | impl Prompt { 41 | fn prompt(global: GlobalVariables, ptype: PType) -> String { 42 | format!("{} [{}]: ", ptype, global.operation_number) 43 | } 44 | fn run(hook_name: &str) { 45 | match hook_name { 46 | irust_api::SetInputPrompt::NAME => { 47 | let irust_api::SetInputPrompt(global) = Self::read(); 48 | let output = Self::prompt(global, PType::In); 49 | Self::write::(&output); 50 | } 51 | irust_api::SetOutputPrompt::NAME => { 52 | let irust_api::SetOutputPrompt(global) = Self::read(); 53 | let output = Self::prompt(global, PType::Out); 54 | Self::write::(&output); 55 | } 56 | irust_api::Shutdown::NAME => { 57 | let _hook: irust_api::Shutdown = Self::read(); 58 | let output = Self::clean_up(); 59 | Self::write::(&output); 60 | } 61 | _ => unreachable!(), 62 | } 63 | } 64 | fn clean_up() -> Option { 65 | Some(irust_api::Command::ResetPrompt) 66 | } 67 | } 68 | 69 | fn main() { 70 | let _ = Prompt::execute(&mut Prompt::run); 71 | } 72 | -------------------------------------------------------------------------------- /script_examples/irust_vim_dylib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "irust_vim_dylib" 3 | version = "0.1.0" 4 | authors = ["Nbiba Bedis "] 5 | edition = "2024" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | irust_api.workspace = true 11 | rscript.workspace = true 12 | 13 | [lib] 14 | crate-type = ["cdylib"] 15 | -------------------------------------------------------------------------------- /script_examples/irust_vim_dylib/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rscript::{ 2 | scripting::{DynamicScript, FFiData, FFiStr}, 3 | Hook, ScriptInfo, ScriptType, VersionReq, 4 | }; 5 | use std::sync::{LazyLock, Mutex}; 6 | mod script; 7 | 8 | #[unsafe(no_mangle)] 9 | pub static SCRIPT: DynamicScript = DynamicScript { 10 | script, 11 | script_info, 12 | }; 13 | 14 | static VIM: LazyLock> = LazyLock::new(|| Mutex::new(Vim::new())); 15 | fn vim() -> std::sync::MutexGuard<'static, Vim> { 16 | VIM.lock().unwrap() 17 | } 18 | 19 | extern "C" fn script_info() -> FFiData { 20 | let info = ScriptInfo::new( 21 | "VimDylib", 22 | ScriptType::DynamicLib, 23 | &[ 24 | irust_api::InputEvent::NAME, 25 | irust_api::Shutdown::NAME, 26 | irust_api::Startup::NAME, 27 | ], 28 | VersionReq::parse(">=1.67.3").expect("correct version requirement"), 29 | ); 30 | info.into_ffi_data() 31 | } 32 | 33 | extern "C" fn script(name: FFiStr, hook: FFiData) -> FFiData { 34 | match name.as_str() { 35 | irust_api::InputEvent::NAME => { 36 | let hook: irust_api::InputEvent = DynamicScript::read(hook); 37 | let output = vim().handle_input_event(hook); 38 | DynamicScript::write::(&output) 39 | } 40 | irust_api::Shutdown::NAME => { 41 | let hook: irust_api::Shutdown = DynamicScript::read(hook); 42 | let output = vim().clean_up(hook); 43 | DynamicScript::write::(&output) 44 | } 45 | irust_api::Startup::NAME => { 46 | let hook: irust_api::Startup = DynamicScript::read(hook); 47 | let output = vim().start_up(hook); 48 | DynamicScript::write::(&output) 49 | } 50 | _ => unreachable!(), 51 | } 52 | } 53 | 54 | struct Vim { 55 | state: State, 56 | mode: Mode, 57 | } 58 | 59 | #[allow(non_camel_case_types)] 60 | #[derive(Debug, PartialEq)] 61 | enum State { 62 | Empty, 63 | c, 64 | ci, 65 | d, 66 | di, 67 | g, 68 | f, 69 | F, 70 | r, 71 | } 72 | 73 | #[derive(PartialEq)] 74 | enum Mode { 75 | Normal, 76 | Insert, 77 | } 78 | -------------------------------------------------------------------------------- /script_examples/mixed_cmds/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mixed_cmds" 3 | version = "0.1.0" 4 | authors = ["Light Ning "] 5 | edition = "2024" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | irust_api.workspace = true 11 | rscript.workspace = true 12 | -------------------------------------------------------------------------------- /script_examples/mixed_cmds/src/main.rs: -------------------------------------------------------------------------------- 1 | use irust_api::{Command, OutputEvent, Shutdown}; 2 | use rscript::scripting::Scripter; 3 | use rscript::{Hook, VersionReq}; 4 | 5 | // need sync from crates/irust/src/irust/parser.rs 6 | const COMMANDS: [&str; 32] = [ 7 | ":reset", 8 | ":show", 9 | ":pop", 10 | ":irust", 11 | ":sync", 12 | ":exit", 13 | ":quit", 14 | ":help", 15 | "::", 16 | ":edit", 17 | ":add", 18 | ":hard_load_crate", 19 | ":hard_load", 20 | ":load", 21 | ":reload", 22 | ":type", 23 | ":del", 24 | ":dbg", 25 | ":color", 26 | ":cd", 27 | ":toolchain", 28 | ":main_result", 29 | ":check_statements", 30 | ":time_release", 31 | ":time", 32 | ":bench", 33 | ":asm", 34 | ":executor", 35 | ":evaluator", 36 | ":scripts", 37 | ":compile_time", 38 | ":expand", 39 | ]; 40 | 41 | fn split_cmds(buffer: String) -> Vec { 42 | let mut new_buf = vec![]; 43 | let mut tmp_vec = vec![]; 44 | for line in buffer.lines() { 45 | if line.is_empty() { 46 | continue; 47 | } 48 | if COMMANDS.iter().any(|c| line.trim().starts_with(c)) { 49 | new_buf.push(std::mem::take(&mut tmp_vec).join("")); 50 | new_buf.push(line.trim().to_owned()); 51 | } else { 52 | tmp_vec.push(line); 53 | } 54 | } 55 | if !tmp_vec.is_empty() { 56 | new_buf.push(std::mem::take(&mut tmp_vec).join("")); 57 | } 58 | new_buf 59 | } 60 | 61 | #[derive(Debug, Default)] 62 | struct MixedCmds {} 63 | 64 | impl Scripter for MixedCmds { 65 | fn name() -> &'static str { 66 | "MixedCmds" 67 | } 68 | 69 | fn script_type() -> rscript::ScriptType { 70 | rscript::ScriptType::Daemon 71 | } 72 | 73 | fn hooks() -> &'static [&'static str] { 74 | &[OutputEvent::NAME, Shutdown::NAME] 75 | } 76 | 77 | fn version_requirement() -> rscript::VersionReq { 78 | VersionReq::parse(">=1.50.0").expect("correct version requirement") 79 | } 80 | } 81 | 82 | fn main() { 83 | let _ = MixedCmds::execute(&mut |hook_name| MixedCmds::run(&mut MixedCmds {}, hook_name)); 84 | } 85 | 86 | impl MixedCmds { 87 | fn run(&mut self, hook_name: &str) { 88 | match hook_name { 89 | OutputEvent::NAME => { 90 | let hook: OutputEvent = Self::read(); 91 | let input = hook.1; 92 | let buffers = split_cmds(input); 93 | let cmds: Vec<_> = buffers.into_iter().map(Command::Parse).collect(); 94 | let output = Some(Command::Multiple(cmds)); 95 | Self::write::(&output); 96 | } 97 | Shutdown::NAME => { 98 | let hook: Shutdown = Self::read(); 99 | let output = self.clean_up(hook); 100 | Self::write::(&output); 101 | } 102 | _ => unreachable!(), 103 | } 104 | } 105 | 106 | fn clean_up(&self, _hook: Shutdown) -> Option { 107 | None 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/deno_bot_test.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --unstable-ffi --allow-all 2 | import { Pty } from "jsr:@sigma/pty-ffi@0.36.0"; 3 | import { stripAnsiCode } from "jsr:@std/fmt@1.0.7/colors"; 4 | import { assertEquals, assertMatch } from "jsr:@std/assert@1.0.13"; 5 | 6 | const ENCODER = new TextEncoder(); 7 | 8 | if (import.meta.main) { 9 | const pty = new Pty("cargo", { 10 | args: ["run", "--", "--default-config"], 11 | env: { NO_COLOR: "1" }, 12 | }); 13 | 14 | while (true) { 15 | let { data: input, done } = pty.read(); 16 | if (done) break; 17 | input = stripAnsiCode(input); 18 | 19 | if (input.includes("In:")) break; 20 | await sleep(100); 21 | } 22 | 23 | const write = (input: string) => pty.write(`${input}\n\r`); 24 | const evalRs = async (input: string) => { 25 | write(input); 26 | // detect output 27 | // the plan is: 28 | // TODO 29 | let lastResult = ""; 30 | let idx = 0; 31 | let start = 0; 32 | while (true) { 33 | let { data: output, done } = pty.read(); 34 | if (done) break; 35 | output = stripAnsiCode(output).trim(); 36 | if (output && output !== "In:") lastResult = output; 37 | 38 | if (output && start === 0) { 39 | start = 1; 40 | } 41 | if (!output && start === 1) { 42 | start = 2; 43 | } 44 | if (output && start === 2) { 45 | start = 3; 46 | } 47 | 48 | if (start === 3 && !output && lastResult) { 49 | idx++; 50 | } else { 51 | idx = 0; 52 | } 53 | 54 | if (idx === 5) { 55 | const result = lastResult.replace(/^Out:/, "").trim(); 56 | return result; 57 | } 58 | await sleep(100); 59 | } 60 | // not really needed 61 | return ""; 62 | }; 63 | 64 | const test = async ( 65 | input: string, 66 | expected: string | RegExp, 67 | ) => { 68 | Deno.stdout.write(ENCODER.encode(`eval: ${input}`)); 69 | const output = await evalRs(input); 70 | // try catch just to add a new line before the error 71 | try { 72 | if (typeof expected === "string") { 73 | assertEquals( 74 | output, 75 | expected, 76 | ); 77 | } // exepected is a regex 78 | else { 79 | assertMatch(output, expected); 80 | } 81 | } catch (e) { 82 | console.log(); 83 | throw e; 84 | } 85 | console.log(" [OK]"); 86 | }; 87 | 88 | write('let a = "hello";'); 89 | await test(":type a", "`&str`"); 90 | 91 | write(`fn fact(n: usize) -> usize { 92 | match n { 93 | 1 => 1, 94 | n => n * fact(n-1) 95 | } 96 | }`); 97 | await test("fact(4)", "24"); 98 | 99 | await test("5+4", "9"); 100 | await test("z", /cannot find value `z`/); 101 | await test("let a = 2; a + a", "4"); 102 | // NOTE: this requires network, is it a good idea to enable it ? 103 | // await evalRs(":add regex"); 104 | // await test('regex::Regex::new("a.*a")', 'Ok(Regex("a.*a"))'); 105 | } 106 | 107 | async function sleep(ms: number) { 108 | await new Promise((r) => setTimeout(r, ms)); 109 | } 110 | -------------------------------------------------------------------------------- /tests/irust_bot_test.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | # type: ignore 3 | 4 | #!!! This test is deprecated, the work continues on `deno_bot_test.ts` 5 | 6 | # This test works probably only on unix, and there are some race conditions so sometimes it needs to be rerun 7 | 8 | import pexpect 9 | import sys 10 | 11 | sys.tracebacklimit = 0 12 | 13 | child = pexpect.spawn("cargo run", timeout=240) 14 | 15 | 16 | def sink(n): 17 | for _ in range(n): 18 | child.readline() 19 | 20 | 21 | def send(op): 22 | child.send(f"{op}\r") 23 | 24 | 25 | def assert_eq(op, val): 26 | print(f"[testing] `{op}` => `{val}`") 27 | send(op) 28 | out = child.readline().strip().decode("utf-8") 29 | if out == op: 30 | print("Race condition detected, test needs to be restarted (a couple of retries might be needed)") 31 | exit(1) 32 | 33 | out = out[len(out)-len(val):] 34 | assert out == val, f"got: `{out}` expected: `{val}`" 35 | print("success!") 36 | 37 | 38 | # prelude 39 | sink(2) 40 | 41 | assert_eq(":compile_time off", "Ok!") 42 | assert_eq(":add regex", "Ok!") 43 | assert_eq('regex::Regex::new("a.*a").unwrap()', "a.*a") 44 | 45 | assert_eq("z", "cannot find value `z` in this scope\x1b[0m") 46 | sink(4) 47 | 48 | assert_eq("1+2", "3") 49 | 50 | send('let a = "hello";') 51 | assert_eq(':type a', "`&str`") 52 | 53 | send("""fn fact(n: usize) -> usize { 54 | match n { 55 | 1 => 1, 56 | n => n * fact(n-1) 57 | } 58 | }""") 59 | assert_eq("fact(4)", "24") 60 | 61 | assert_eq(':toolchain nightly', "Ok!") 62 | send('#![feature(decl_macro)]') 63 | send('macro inc($n: expr) {$n + 1}') 64 | assert_eq('inc!({1+1})', '3') 65 | 66 | 67 | print("All tests passed!") 68 | --------------------------------------------------------------------------------