├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .python-version
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── banner.txt
├── commands.toml
├── crates
└── nu_plugin_plotters
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ ├── commands
│ ├── chart_2d.rs
│ ├── draw
│ │ ├── mod.rs
│ │ ├── svg.rs
│ │ └── terminal.rs
│ ├── mod.rs
│ └── series
│ │ ├── bar.rs
│ │ ├── line.rs
│ │ └── mod.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── plugin.rs
│ └── value
│ ├── chart_2d.rs
│ ├── color.rs
│ ├── coords
│ ├── coord_1d.rs
│ ├── coord_2d.rs
│ ├── mod.rs
│ └── range.rs
│ ├── mod.rs
│ └── series_2d.rs
├── examples
├── example.ipynb
├── polars-data.nu
└── polars.ipynb
├── media
├── draw-terminal.png
└── screenshot.png
├── pyproject.toml
├── rustfmt.toml
├── src
├── error.rs
├── handlers
│ ├── control.rs
│ ├── mod.rs
│ ├── shell.rs
│ └── stream.rs
├── jupyter
│ ├── connection_file.rs
│ ├── kernel_info.rs
│ ├── messages
│ │ ├── control.rs
│ │ ├── iopub.rs
│ │ ├── mod.rs
│ │ ├── multipart.rs
│ │ └── shell.rs
│ ├── mod.rs
│ └── register_kernel.rs
├── main.rs
├── nu
│ ├── commands
│ │ ├── command.rs
│ │ ├── display.rs
│ │ ├── external.rs
│ │ ├── mod.rs
│ │ └── print.rs
│ ├── konst.rs
│ ├── mod.rs
│ ├── module.rs
│ └── render.rs
└── util.rs
├── tests
└── test_kernel.py
└── uv.lock
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: continuous-integration
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | env:
8 | RUSTFLAGS: "-D warnings"
9 |
10 | jobs:
11 | cargo:
12 | name: cargo ${{ matrix.job.name }} (${{ matrix.crate }})
13 |
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | crate:
19 | - nu_plugin_plotters
20 | - nu-jupyter-kernel
21 | job:
22 | - name: check
23 | command: cargo check
24 | - name: build
25 | command: cargo build
26 | - name: doc
27 | command: cargo doc
28 | - name: test
29 | command: cargo test
30 |
31 | steps:
32 | - uses: actions/checkout@v4
33 | - uses: ConorMacBride/install-package@v1
34 | with:
35 | apt: libfontconfig1-dev
36 | - uses: actions-rust-lang/setup-rust-toolchain@v1
37 | with:
38 | rustflags: $RUSTFLAGS
39 | - run: ${{ matrix.job.command }} -p ${{ matrix.crate }}
40 |
41 | integration-test:
42 | runs-on: ubuntu-latest
43 |
44 | steps:
45 | - uses: actions/checkout@v4
46 | - uses: ConorMacBride/install-package@v1
47 | with:
48 | apt: libfontconfig1-dev
49 | - uses: actions-rust-lang/setup-rust-toolchain@v1
50 | with:
51 | rustflags: $RUSTFLAGS
52 | - run: cargo run register --user
53 | - name: Read .python-version
54 | id: python-version
55 | run: echo "python-version=$(cat .python-version)" >> $GITHUB_OUTPUT
56 | - uses: actions/setup-python@v5
57 | with:
58 | python-version: ${{ steps.python-version.outputs.python-version }}
59 | - uses: yezz123/setup-uv@v4
60 | - run: uv sync
61 | - uses: actions/cache@v4
62 | with:
63 | path: .venv/
64 | key: ${{ runner.os }}-uv-${{ hashFiles('**/uv.lock') }}
65 | - run: uv run pytest
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 | /target
3 | /examples/.ipynb_checkpoints
4 | /examples/.jupyter
5 | /examples/data
6 | /.venv
7 | __pycache__
8 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nu-jupyter-kernel"
3 | version = "0.1.9+0.104.0"
4 | edition.workspace = true
5 | description = "A jupyter raw kernel for nu"
6 | repository.workspace = true
7 | license.workspace = true
8 | keywords = ["jupyter-kernel", "jupyter", "nushell"]
9 | categories = ["science"]
10 |
11 | [workspace.package]
12 | edition = "2021"
13 | repository = "https://github.com/cptpiepmatz/nu-jupyter-kernel"
14 | license = "MIT"
15 |
16 | [package.metadata.jupyter]
17 | protocol_version = "5.4"
18 |
19 | [features]
20 | default = ["plotting"]
21 | plotting = ["nu-plotters"]
22 |
23 | [workspace]
24 | members = ["crates/nu_plugin_plotters"]
25 |
26 | [workspace.dependencies]
27 | nu-cmd-extra = "0.104.0"
28 | nu-cmd-lang = "0.104.0"
29 | nu-cmd-plugin = "0.104.0"
30 | nu-command = { version = "0.104.0", features = ["plugin"] }
31 | nu-engine = { version = "0.104.0" }
32 | nu-parser = { version = "0.104.0", features = ["plugin"] }
33 | nu-protocol = { version = "0.104.0", features = ["plugin"] }
34 | nu-plugin = "0.104.0"
35 | nuon = "0.104.0"
36 |
37 | # plotting
38 | [dependencies.nu-plotters]
39 | package = "nu_plugin_plotters"
40 | version = "0.1"
41 | path = "crates/nu_plugin_plotters"
42 | optional = true
43 |
44 | [dependencies]
45 | # nu
46 | nu-cmd-extra.workspace = true
47 | nu-cmd-lang.workspace = true
48 | nu-cmd-plugin.workspace = true
49 | nu-command.workspace = true
50 | nu-engine.version = "0.104.0" # cannot publish if this inherits from workspace
51 | nu-parser.workspace = true
52 | nu-protocol.workspace = true
53 |
54 | # Cryptography and Security
55 | hmac = "0.12.1"
56 | sha2 = "0.10.8"
57 |
58 | # Data Handling and Serialization
59 | bytes = "1.6.0"
60 | serde = { version = "1.0", features = ["derive"] }
61 | serde_json = "1.0"
62 | uuid = "1.8.0"
63 |
64 | # Date and Time
65 | chrono = "0.4.38"
66 |
67 | # Derive Macros
68 | clap = { version = "4.5.4", features = ["derive"] }
69 | derive_more = { version = "2", features = ["full"] }
70 | strum = { version = "0.27", features = ["derive"] }
71 |
72 | # Error Handling
73 | thiserror = "2.0.3"
74 | miette = { version = "7.2.0", features = ["fancy"] }
75 |
76 | # Filesystem
77 | dirs = "6"
78 |
79 | # Formatting and Utilities
80 | const_format = "0.2.32"
81 | hex = "0.4.3"
82 | indoc = "2"
83 |
84 | # Media and Configuration Types
85 | mime = "0.3.17"
86 | mime_guess = "2.0.4"
87 | static-toml = "1.2.0"
88 |
89 | # Networking and IPC
90 | os_pipe = { version = "1.1.5", features = ["io_safety"] }
91 | zeromq = "0.4.0"
92 |
93 | # Synchronization and Concurrency
94 | parking_lot = "0.12.2"
95 | tokio = { version = "1.39.2", features = ["rt", "macros", "parking_lot"] }
96 |
97 | # Miscellaneous
98 | atomic_enum = "0.3.0"
99 |
100 | [patch.crates-io]
101 | zip = { git = "https://github.com/zip-rs/zip2", tag = "v2.5.0" }
102 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Tim 'Piepmatz' Hesse
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 |
nu-jupyter-kernel
2 |
3 |
4 | A
5 | jupyter
6 | raw kernel
7 | for nu .
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | [](https://crates.io/crates/nu-jupyter-kernel)
16 | [](https://github.com/nushell/nushell)
17 | 
18 | [](https://github.com/cptpiepmatz/nu-jupyter-kernel/blob/main/LICENSE)
19 |
20 |
21 |
22 |
23 | ## About
24 | `nu-jupyter-kernel` is a [Jupyter](https://jupyter.org) kernel specifically for
25 | executing Nu pipelines.
26 | Unlike most Jupyter kernels that rely on Python, this raw kernel directly
27 | implements the Jupyter messaging protocol, allowing direct communication without
28 | Python intermediaries.
29 | It's designed to work seamlessly with Nu, the language utilized by
30 | [Nushell](https://github.com/nushell/nushell) —
31 | a modern shell that emphasizes structured data.
32 |
33 | 
34 |
35 | ## Features
36 | The `nu-jupyter-kernel` has several features making it a useful kernel for
37 | Jupyter notebooks:
38 |
39 | - **Execution of Nu code:**
40 | Directly run Nu pipeplines within your Jupyter notebook.
41 |
42 | - **State sharing across cells:**
43 | Unlike isolated script execution, the kernel maintains state across different
44 | cells using the `nu-engine`.
45 |
46 | - **Rich Data Rendering:**
47 | Outputs are dynamically rendered in various data types wherever applicable.
48 |
49 | - **Inline Value Printing:**
50 | Easily print values at any point during cell execution.
51 |
52 | - **Controlled External Commands:**
53 | By default, external commands are disabled for reproducibility.
54 | You can enable them as needed, and they will function as they do in Nushell.
55 |
56 | - **Kernel Information:**
57 | Access kernel-specific information via the `$nuju` constant.
58 |
59 | - **Error representation:**
60 | Shell errors are beautifully rendered.
61 |
62 | - **Nushell Plugin Compatibility:**
63 | Supports Nushell plugins within notebooks, allowing them to be loaded and
64 | utilized as in a typical Nushell environment.
65 |
66 | - **Plotting Integration:**
67 | The kernel directly integrates the `nu_plugin_plotters`, making plots easily
68 | accessible.
69 |
70 | ## Examples
71 | In the "examples" directory are some notebooks that show how the kernel works.
72 | Opening the examples on Github also shows a preview of them.
73 |
74 |
75 | ## Design Goals
76 | The design of the `nu-jupyter-kernel` focuses on the following goals:
77 |
78 | - **Reproducibility:**
79 | Notebooks should be as reproducible as possible by default.
80 |
81 | - **Clarity in dependencies:**
82 | Make all dependencies clear and obvious to the user.
83 |
84 | - **Script-like behavior:**
85 | The kernel behaves largely like a regular Nu script to ensure familiarity.
86 |
87 | - **Clear Feature Distinctions:**
88 | Clearly indicating any deviations or limitations compared to standard Nu
89 | script capabilities to avoid confusion during notebook executions.
90 |
91 | ## Installation
92 | To build the kernel you need to have the rust toolchain installed, check the
93 | [installation guide on rust's official website](https://www.rust-lang.org/tools/install).
94 |
95 | Using `cargo install --locked nu-jupyter-kernel` you can install the latest release of
96 | the kernel.
97 | If you want to install the latest version on the git repo, you can install the
98 | kernel via `cargo install --locked nu-jupyter-kernel --git https://github.com/cptpiepmatz/nu-jupyter-kernel.git`
99 |
100 | ## Usage
101 | ### Registering the Kernel
102 | After installation, you must register the kernel to make it available within
103 | Jupyter environments.
104 | This can be done through the command:
105 |
106 | ```sh
107 | nu-jupyter-kernel register
108 | ```
109 |
110 | You can specify the registration scope using `--user` for the current user
111 | (default) or `--system` for system-wide availability.
112 |
113 | ### Using the Kernel
114 |
115 | - **Jupyter Notebook:**
116 | Open Jupyter Notebook, create or open a notebook, and then select "Nushell"
117 | from the kernel options in the top right corner.
118 |
119 | - **Visual Studio Code:**
120 | Ensure you have the
121 | [Jupyter extension by Microsoft](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter)
122 | installed.
123 | Open a `.ipynb` file, click on "Select Kernel", choose "Jupyter Kernel", and
124 | you should see "Nushell" listed.
125 |
126 | Both options may require a restart after registering the kernel.
127 |
128 | ### Note on Updates
129 | Kernel binary updates do not require re-registration unless the binary's
130 | location changes.
131 | For developers, keep in mind that running `cargo run register` and
132 | `cargo run --release register` will result in different binary locations.
133 |
134 | ## Version Scheme
135 | This crate follows the semantic versioning scheme as required by the
136 | [Rust documentation](https://doc.rust-lang.org/cargo/reference/semver.html).
137 | The version number is represented as `x.y.z+a.b.c`, where `x.y.z` is the version
138 | of the crate and `a.b.c` is the version of the `nu-engine` that this crate is
139 | built with.
140 | The `+` symbol is used to separate the two version numbers.
141 |
142 |
143 | ## Contributing
144 | Contributions are welcome!
145 | If you're interested in contributing to the `nu-jupyter-kernel`, you can start
146 | by opening an issue or a pull request.
147 | If you'd like to discuss potential changes or get more involved, join the
148 | Nushell community on Discord.
149 | Invite links are available when you start Nushell or on their GitHub repository.
150 |
151 | ## Testing
152 | This project uses [`uv`](https://github.com/astral-sh/uv) for integration
153 | testing.
154 | Since tools for executing Jupyter notebooks are not currently available in Rust,
155 | the tests are handled via Python.
156 |
157 | To run the tests, follow these steps:
158 | 1. **Register the kernel**:
159 | ```nushell
160 | cargo run register
161 | ```
162 | 2. **Sync Python dependencies:**
163 | ```nushell
164 | uv sync
165 | ```
166 | 3. **Run the tests:**
167 | ```nushell
168 | uv run pytest
169 | ```
170 |
171 | Make sure `uv` is installed before running the commands.
172 |
--------------------------------------------------------------------------------
/banner.txt:
--------------------------------------------------------------------------------
1 | __ ,
2 | .--()°'.' You're running `nu-jupyter-kernel`,
3 | '|, . ,' based on the `nu` language,
4 | !_-(_\ where all data is structured!
5 |
--------------------------------------------------------------------------------
/commands.toml:
--------------------------------------------------------------------------------
1 | # this file holds all the texts that commands use
2 |
3 | # TODO: check out which more should be hidden
4 | incompatible_commands = ["input", "exit", "run-external"]
5 |
6 | [nuju]
7 | name = "nuju"
8 | description = "Control behavior of the kernel."
9 | extra_description = """
10 | You must use one of the following subcommands.
11 | Using this command as-is will only produce this help message.
12 | """
13 |
14 | [display]
15 | name = "nuju display"
16 | description = "Control the rendering of the current cell's output."
17 | extra_description = """
18 | Applies a filter to control how the output of the current cell is displayed.
19 | This command can be positioned anywhere within the cell's code. It passes
20 | through the cell's data, allowing it to be used effectively as the final
21 | command without altering the output content.
22 | """
23 | search_terms = ["jupyter", "display", "cell", "output"]
24 |
25 | [[display.examples]]
26 | example = "{a: 3, b: [1, 2, 2]} | nuju display md"
27 | description = "Force render output to be markdown"
28 |
29 | [[display.examples]]
30 | example = "{a: 3, b: [1, 2, 2]} | nuju display json"
31 | description = "Force render output to be json"
32 |
33 | [[display.examples]]
34 | example = "{a: 3, b: [1, 2, 2]} | table --expand | nuju display txt"
35 | description = "Force render output to be a classic nushell table"
36 |
37 | [external]
38 | name = "nuju external"
39 | description = "Enable external commands for subsequent cells."
40 | extra_description = """
41 | Activates a setting that permits the use of external commands in all subsequent
42 | cell evaluations within the notebook. This irreversible change enhances
43 | flexibility for advanced tasks. By disabling external commands by default,
44 | notebooks become more portable and less likely to encounter failures when run
45 | on different machines."
46 | """
47 | search_terms = ["jupyter", "external", "run"]
48 |
49 | [print]
50 | name = "nuju print"
51 | description = "Display data for this cell."
52 | search_terms = ["jupyter", "print", "display", "cell", "output"]
53 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nu_plugin_plotters"
3 | description = "A nushell plugin for for plotting charts"
4 | version = "0.1.6+0.104.0"
5 | edition.workspace = true
6 | repository.workspace = true
7 | license.workspace = true
8 |
9 | [dependencies]
10 | # nu
11 | nu-plugin.workspace = true
12 | nu-protocol.workspace = true
13 | nu-engine.workspace = true
14 |
15 | # plotting
16 | plotters = "0.3.6"
17 |
18 | # dates
19 | chrono = "0.4"
20 |
21 | # serialization, needed for custom values
22 | serde = { version = "1", features = ["derive"] }
23 | typetag = "0.2"
24 |
25 | # drawing to the terminal
26 | icy_sixel = "0.1"
27 | viuer = "0.9"
28 | image = "0.25"
29 |
30 | # misc
31 | indoc = "2"
32 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/README.md:
--------------------------------------------------------------------------------
1 | nu_plugin_plotters
2 |
3 |
4 | A nushell
5 | plugin for plotting charts.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ## About
24 | `nu_plugin_plotters` is a plugin for [Nushell](https://www.nushell.sh) that
25 | provides easy plotting of data using
26 | [`plotters`](https://github.com/plotters-rs/plotters).
27 |
28 | 
29 |
30 | ## Usage
31 | The plugin provides three main commands:
32 |
33 | - **`series`**:
34 | Use this command to create a dataset from a list of data points.
35 | You can pass in:
36 | - A table with `x` and `y` columns.
37 | - A list of 2-element lists representing `x` and `y` coordinates.
38 | - A plain list of numbers, where the index of each value becomes the `x` value.
39 |
40 | You can also apply custom styling to the series.
41 |
42 | - **`chart`**:
43 | This command creates a chart from one or more series.
44 | You can either pipe the series into the command or pass them as arguments.
45 | Charts can also be extended by adding more series, and you have options to
46 | customize the chart's appearance.
47 |
48 | - **`draw`**:
49 | This renders the chart onto a canvas.
50 | You can output to an SVG file (using the `save` command) or display directly
51 | in the terminal (using iterm, kitty or sixel).
52 |
53 | These commands are modular, allowing you to build and inspect charts step by
54 | step.
55 | Each command's output is a custom value that can be converted into standard Nu
56 | values for further inspection or manipulation.
57 |
58 | ## `nu-jupyter-kernel` Integration
59 | This plugin is directly integrated into the
60 | [`nu-jupyter-kernel`](https://github.com/cptpiepmatz/nu-jupyter-kernel) and
61 | therefore doesn't need to installed separately in order to create charts for the
62 | notebook.
63 |
64 | Also charts are automatically "drawn" and don't need to be called via `draw svg`.
65 | Just output the chart and the kernel will execute the `draw svg` command
66 | automatically (you may need to enforce this using `nuju display svg`).
67 |
68 |
69 | This plugin is integrated directly into the
70 | [`nu-jupyter-kernel`](https://github.com/cptpiepmatz/nu-jupyter-kernel), so
71 | there's no need for separate installation to create charts within Jupyter
72 | notebooks.
73 |
74 | Charts are automatically rendered without the need to explicitly call `draw svg`.
75 | Simply output the chart, and the kernel will handle the `draw svg` command
76 | behind the scenes.
77 | If necessary, you can enforce this behavior by using the
78 | `nuju display svg` command.
79 |
80 | ## Version Scheme
81 | This crate follows the semantic versioning scheme as required by the
82 | [Rust documentation](https://doc.rust-lang.org/cargo/reference/semver.html).
83 | The version number is represented as `x.y.z+a.b.c`, where `x.y.z` is the version
84 | of the crate and `a.b.c` is the version of the `nu-plugin` that this crate is
85 | built with.
86 | The `+` symbol is used to separate the two version numbers.
87 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/chart_2d.rs:
--------------------------------------------------------------------------------
1 | use nu_engine::command_prelude::*;
2 | use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
3 | use nu_protocol::{FromValue, LabeledError};
4 |
5 | use crate::value;
6 |
7 | #[derive(Debug, Clone)]
8 | pub struct Chart2d;
9 |
10 | impl Command for Chart2d {
11 | fn name(&self) -> &str {
12 | "chart 2d"
13 | }
14 |
15 | fn signature(&self) -> Signature {
16 | Signature::new(Command::name(self))
17 | .add_help()
18 | .description(Command::description(self))
19 | .extra_description(Command::extra_description(self))
20 | .search_terms(
21 | Command::search_terms(self)
22 | .into_iter()
23 | .map(ToOwned::to_owned)
24 | .collect(),
25 | )
26 | .optional(
27 | "chart",
28 | value::Chart2d::ty().to_shape(),
29 | "Baseline chart to extend from.",
30 | )
31 | .named(
32 | "width",
33 | SyntaxShape::Int,
34 | "Set the width of the chart in pixels.",
35 | Some('W'),
36 | )
37 | .named(
38 | "height",
39 | SyntaxShape::Int,
40 | "Set the height of the chart in pixels.",
41 | Some('H'),
42 | )
43 | .named(
44 | "background",
45 | SyntaxShape::Any,
46 | "Set the background color of the chart.",
47 | Some('b'),
48 | )
49 | .named(
50 | "caption",
51 | SyntaxShape::String,
52 | "Set a caption for the chart.",
53 | Some('c'),
54 | )
55 | .named(
56 | "margin",
57 | SyntaxShape::List(Box::new(SyntaxShape::Int)),
58 | "Set the margin for the chart, refer to css margin shorthands for setting values.",
59 | Some('m'),
60 | )
61 | .named(
62 | "label-area",
63 | SyntaxShape::List(Box::new(SyntaxShape::Int)),
64 | "Set the area size for the chart, refer to css margin shorthands for setting \
65 | values.",
66 | Some('l'),
67 | )
68 | .named("x-range", SyntaxShape::Range, "Set the x range.", Some('x'))
69 | .named("y-range", SyntaxShape::Range, "Set the y range.", Some('y'))
70 | .switch("disable-mesh", "Disable the background mesh grid.", None)
71 | .switch(
72 | "disable-x-mesh",
73 | "Disable the background mesh for the x axis.",
74 | None,
75 | )
76 | .switch(
77 | "disable-y-mesh",
78 | "Disable the background mesh for the y axis.",
79 | None,
80 | )
81 | .input_output_type(Type::Nothing, value::Chart2d::ty())
82 | .input_output_type(value::Series2d::ty(), value::Chart2d::ty())
83 | .input_output_type(Type::list(value::Series2d::ty()), value::Chart2d::ty())
84 | }
85 |
86 | fn description(&self) -> &str {
87 | "Construct a 2D chart."
88 | }
89 |
90 | fn extra_description(&self) -> &str {
91 | "A chart is a container for a list of series, any `plotters::series-2d` or \
92 | `list` is collected into this container and may be rendered via `draw \
93 | svg` or `draw png`."
94 | }
95 |
96 | fn search_terms(&self) -> Vec<&str> {
97 | vec!["plotters", "chart", "2d"]
98 | }
99 |
100 | fn run(
101 | &self,
102 | engine_state: &EngineState,
103 | stack: &mut Stack,
104 | call: &Call,
105 | input: PipelineData,
106 | ) -> Result {
107 | let span = input.span().unwrap_or(Span::unknown());
108 | let input = input.into_value(span)?;
109 | let extend = call.opt(engine_state, stack, 0)?;
110 | let width = call.get_flag(engine_state, stack, "width")?;
111 | let height = call.get_flag(engine_state, stack, "height")?;
112 | let background = call.get_flag(engine_state, stack, "background")?;
113 | let caption = call.get_flag(engine_state, stack, "caption")?;
114 | let margin = call.get_flag(engine_state, stack, "margin")?;
115 | let label_area = call.get_flag(engine_state, stack, "label-area")?;
116 | let x_range = call.get_flag(engine_state, stack, "x-range")?;
117 | let y_range = call.get_flag(engine_state, stack, "y-range")?;
118 | let disable_mesh = call.get_flag(engine_state, stack, "disable-mesh")?;
119 | let disable_x_mesh = call.get_flag(engine_state, stack, "disable-x-mesh")?;
120 | let disable_y_mesh = call.get_flag(engine_state, stack, "disable-y-mesh")?;
121 | Chart2d::run(self, input, extend, Chart2dOptions {
122 | width,
123 | height,
124 | background,
125 | caption,
126 | margin,
127 | label_area,
128 | x_range,
129 | y_range,
130 | disable_mesh,
131 | disable_x_mesh,
132 | disable_y_mesh,
133 | })
134 | .map(|v| PipelineData::Value(v, None))
135 | }
136 | }
137 |
138 | impl SimplePluginCommand for Chart2d {
139 | type Plugin = crate::plugin::PlottersPlugin;
140 |
141 | fn name(&self) -> &str {
142 | Command::name(self)
143 | }
144 |
145 | fn signature(&self) -> Signature {
146 | Command::signature(self)
147 | }
148 |
149 | fn description(&self) -> &str {
150 | Command::description(self)
151 | }
152 |
153 | fn extra_description(&self) -> &str {
154 | Command::extra_description(self)
155 | }
156 |
157 | fn search_terms(&self) -> Vec<&str> {
158 | Command::search_terms(self)
159 | }
160 |
161 | fn run(
162 | &self,
163 | _: &Self::Plugin,
164 | _: &EngineInterface,
165 | call: &EvaluatedCall,
166 | input: &Value,
167 | ) -> Result {
168 | let input = input.clone();
169 | let extend = call
170 | .positional
171 | .first()
172 | .map(|v| ::from_value(v.clone()))
173 | .transpose()?;
174 |
175 | let mut options = Chart2dOptions::default();
176 | for (name, value) in call.named.clone() {
177 | // TODO: put this function somewhere reusable
178 | fn extract_named(
179 | name: impl ToString,
180 | value: Option,
181 | span: Span,
182 | ) -> Result {
183 | let value = value.ok_or_else(|| ShellError::MissingParameter {
184 | param_name: name.to_string(),
185 | span,
186 | })?;
187 | T::from_value(value)
188 | }
189 |
190 | fn extract_flag(value: Option) -> Result, ShellError> {
191 | Ok(Some(match value {
192 | None => true,
193 | Some(value) => bool::from_value(value)?,
194 | }))
195 | }
196 |
197 | match name.item.as_str() {
198 | "width" => options.width = extract_named("width", value, name.span)?,
199 | "height" => options.height = extract_named("height", value, name.span)?,
200 | "background" => options.background = extract_named("background", value, name.span)?,
201 | "caption" => options.caption = extract_named("caption", value, name.span)?,
202 | "margin" => options.margin = extract_named("margin", value, name.span)?,
203 | "label-area" => options.label_area = extract_named("label-area", value, name.span)?,
204 | "x-range" => options.x_range = extract_named("x-range", value, name.span)?,
205 | "y-range" => options.y_range = extract_named("y-range", value, name.span)?,
206 | "disable-mesh" => options.disable_mesh = extract_flag(value)?,
207 | "disable-x-mesh" => options.disable_x_mesh = extract_flag(value)?,
208 | "disable-y-mesh" => options.disable_y_mesh = extract_flag(value)?,
209 | _ => continue,
210 | }
211 | }
212 |
213 | Chart2d::run(self, input, extend, options).map_err(Into::into)
214 | }
215 | }
216 |
217 | #[derive(Debug, Default)]
218 | struct Chart2dOptions {
219 | pub width: Option,
220 | pub height: Option,
221 | pub background: Option,
222 | pub caption: Option,
223 | pub margin: Option>,
224 | pub label_area: Option>,
225 | pub x_range: Option,
226 | pub y_range: Option,
227 | pub disable_mesh: Option,
228 | pub disable_x_mesh: Option,
229 | pub disable_y_mesh: Option,
230 | }
231 |
232 | impl Chart2d {
233 | fn run(
234 | &self,
235 | input: Value,
236 | extend: Option,
237 | Chart2dOptions {
238 | // unroll here to ensure we use all of them
239 | width,
240 | height,
241 | background,
242 | caption,
243 | margin,
244 | label_area,
245 | x_range,
246 | y_range,
247 | disable_mesh,
248 | disable_x_mesh,
249 | disable_y_mesh,
250 | }: Chart2dOptions,
251 | ) -> Result {
252 | let span = input.span();
253 | let mut input = match input {
254 | v @ Value::Custom { .. } => vec![value::Series2d::from_value(v)?],
255 | v @ Value::List { .. } => Vec::from_value(v)?,
256 | _ => todo!("handle invalid input"),
257 | };
258 |
259 | let mut chart = extend.unwrap_or_default();
260 | chart.series.append(&mut input);
261 | chart.width = width.unwrap_or(chart.width);
262 | chart.height = height.unwrap_or(chart.height);
263 | chart.background = background.or(chart.background);
264 | chart.caption = caption.or(chart.caption);
265 | chart.margin = margin
266 | .map(side_shorthand)
267 | .transpose()?
268 | .unwrap_or(chart.margin);
269 | chart.label_area = label_area
270 | .map(side_shorthand)
271 | .transpose()?
272 | .unwrap_or(chart.label_area);
273 | chart.x_range = x_range.or(chart.x_range);
274 | chart.y_range = y_range.or(chart.y_range);
275 | chart.x_mesh = !(disable_mesh.unwrap_or(false) || disable_x_mesh.unwrap_or(false));
276 | chart.y_mesh = !(disable_mesh.unwrap_or(false) || disable_y_mesh.unwrap_or(false));
277 |
278 | Ok(Value::custom(Box::new(chart), span))
279 | }
280 | }
281 |
282 | fn side_shorthand(input: Vec) -> Result<[T; 4], ShellError> {
283 | let mut iter = input.into_iter();
284 | Ok(match (iter.next(), iter.next(), iter.next(), iter.next()) {
285 | (Some(a), None, None, None) => [a, a, a, a],
286 | (Some(a), Some(b), None, None) => [a, b, a, b],
287 | (Some(a), Some(b), Some(c), None) => [a, b, b, c],
288 | (Some(a), Some(b), Some(c), Some(d)) => [a, b, c, d],
289 | (None, None, None, None) => todo!("throw error for empty list"),
290 | _ => unreachable!("all other variants are not possible with lists"),
291 | })
292 | }
293 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/draw/mod.rs:
--------------------------------------------------------------------------------
1 | use plotters::chart::{ChartBuilder, ChartContext, LabelAreaPosition};
2 | use plotters::coord::Shift;
3 | use plotters::prelude::{Cartesian2d, DrawingArea, DrawingBackend, Rectangle};
4 | use plotters::series::LineSeries;
5 | use plotters::style::{RGBAColor, ShapeStyle, BLACK};
6 |
7 | use crate::value::{
8 | self, Bar2dSeries, Coord1d, Coord2d, Line2dSeries, Range, RangeMetadata, Series2d,
9 | };
10 |
11 | mod svg;
12 | pub use svg::*;
13 |
14 | mod terminal;
15 | pub use terminal::*;
16 |
17 | fn draw(chart: value::Chart2d, drawing_area: DrawingArea) {
18 | if chart.series.is_empty() {
19 | todo!("return some error that empty series do not work")
20 | }
21 |
22 | // TODO: make better error
23 | let mut x_range = chart.x_range().unwrap();
24 | let y_range = chart.y_range().unwrap();
25 |
26 | // bar charts typically want to display all the discrete points
27 | if chart
28 | .series
29 | .iter()
30 | .any(|series| matches!(series, Series2d::Bar(_)))
31 | {
32 | x_range.metadata = Some(RangeMetadata {
33 | discrete_key_points: true,
34 | });
35 | }
36 |
37 | if let Some(color) = chart.background {
38 | let color: RGBAColor = color.into();
39 | drawing_area.fill(&color).unwrap();
40 | }
41 |
42 | let mut chart_builder = ChartBuilder::on(&drawing_area);
43 |
44 | let [top, right, bottom, left] = chart.margin;
45 | chart_builder.margin_top(top);
46 | chart_builder.margin_right(right);
47 | chart_builder.margin_bottom(bottom);
48 | chart_builder.margin_left(left);
49 |
50 | let [top, right, bottom, left] = chart.label_area;
51 | chart_builder.set_label_area_size(LabelAreaPosition::Top, top);
52 | chart_builder.set_label_area_size(LabelAreaPosition::Right, right);
53 | chart_builder.set_label_area_size(LabelAreaPosition::Bottom, bottom);
54 | chart_builder.set_label_area_size(LabelAreaPosition::Left, left);
55 |
56 | if let Some(caption) = chart.caption {
57 | chart_builder.caption(caption, &BLACK);
58 | }
59 |
60 | let mut chart_context = chart_builder.build_cartesian_2d(x_range, y_range).unwrap();
61 |
62 | let mut mesh = chart_context.configure_mesh();
63 | if !chart.x_mesh {
64 | mesh.disable_x_mesh();
65 | };
66 | if !chart.y_mesh {
67 | mesh.disable_y_mesh();
68 | };
69 | mesh.draw().unwrap();
70 |
71 | for series in chart.series {
72 | match series {
73 | value::Series2d::Line(series) => draw_line(&mut chart_context, series),
74 | value::Series2d::Bar(series) => draw_vertical_bar(&mut chart_context, series),
75 | }
76 | }
77 | }
78 |
79 | fn draw_line(
80 | chart_context: &mut ChartContext>,
81 | series: Line2dSeries,
82 | ) {
83 | let Line2dSeries {
84 | series,
85 | color,
86 | filled,
87 | stroke_width,
88 | point_size,
89 | } = series;
90 | let shape_style = ShapeStyle {
91 | color: color.into(),
92 | filled,
93 | stroke_width,
94 | };
95 | let series = LineSeries::new(
96 | series.into_iter().map(|value::Coord2d { x, y }| (x, y)),
97 | shape_style,
98 | )
99 | .point_size(point_size);
100 | chart_context.draw_series(series).unwrap();
101 | }
102 |
103 | fn draw_vertical_bar(
104 | chart_context: &mut ChartContext>,
105 | series: Bar2dSeries,
106 | ) {
107 | let Bar2dSeries {
108 | series,
109 | color,
110 | filled,
111 | stroke_width,
112 | } = series;
113 | let shape_style = ShapeStyle {
114 | color: color.into(),
115 | filled,
116 | stroke_width,
117 | };
118 | let rect_iter = series.into_iter().map(|Coord2d { x, y }| {
119 | let half_width = Coord1d::Float(0.8) / Coord1d::Int(2);
120 | let value_point = (x - half_width, y);
121 | let base_point = (x + half_width, Coord1d::Int(0));
122 | Rectangle::new([value_point, base_point], shape_style)
123 | });
124 | chart_context.draw_series(rect_iter).unwrap();
125 | }
126 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/draw/svg.rs:
--------------------------------------------------------------------------------
1 | use nu_engine::command_prelude::*;
2 | use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
3 | use nu_protocol::{FromValue, LabeledError};
4 | use plotters::prelude::{IntoDrawingArea, SVGBackend};
5 |
6 | use crate::value;
7 |
8 | #[derive(Debug, Clone)]
9 | pub struct DrawSvg;
10 |
11 | impl Command for DrawSvg {
12 | fn name(&self) -> &str {
13 | "draw svg"
14 | }
15 |
16 | fn signature(&self) -> Signature {
17 | Signature::new(Command::name(self))
18 | .add_help()
19 | .description(Command::description(self))
20 | .search_terms(
21 | Command::search_terms(self)
22 | .into_iter()
23 | .map(ToOwned::to_owned)
24 | .collect(),
25 | )
26 | .input_output_type(value::Chart2d::ty(), Type::String)
27 | }
28 |
29 | fn description(&self) -> &str {
30 | "Draws a chart on a SVG string."
31 | }
32 |
33 | fn search_terms(&self) -> Vec<&str> {
34 | vec!["plotters", "chart", "2d", "draw", "svg"]
35 | }
36 |
37 | fn run(
38 | &self,
39 | _: &EngineState,
40 | _: &mut Stack,
41 | _: &Call,
42 | input: PipelineData,
43 | ) -> Result {
44 | let span = input.span().unwrap_or(Span::unknown());
45 | let input = input.into_value(span)?;
46 | DrawSvg::run(self, input).map(|v| PipelineData::Value(v, None))
47 | }
48 | }
49 |
50 | impl SimplePluginCommand for DrawSvg {
51 | type Plugin = crate::plugin::PlottersPlugin;
52 |
53 | fn name(&self) -> &str {
54 | Command::name(self)
55 | }
56 |
57 | fn signature(&self) -> Signature {
58 | Command::signature(self)
59 | }
60 |
61 | fn description(&self) -> &str {
62 | Command::description(self)
63 | }
64 |
65 | fn search_terms(&self) -> Vec<&str> {
66 | Command::search_terms(self)
67 | }
68 |
69 | fn run(
70 | &self,
71 | _: &Self::Plugin,
72 | _: &EngineInterface,
73 | _: &EvaluatedCall,
74 | input: &Value,
75 | ) -> Result {
76 | let input = input.clone();
77 | DrawSvg::run(self, input).map_err(Into::into)
78 | }
79 | }
80 |
81 | impl DrawSvg {
82 | fn run(&self, input: Value) -> Result {
83 | let span = input.span();
84 | let chart = value::Chart2d::from_value(input)?;
85 |
86 | let mut output = String::new();
87 | let drawing_backend = SVGBackend::with_string(&mut output, (chart.width, chart.height));
88 | super::draw(chart, drawing_backend.into_drawing_area());
89 |
90 | Ok(Value::string(output, span))
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/draw/terminal.rs:
--------------------------------------------------------------------------------
1 | use icy_sixel::{DiffusionMethod, MethodForLargest, MethodForRep, PixelFormat, Quality};
2 | use image::{DynamicImage, ImageBuffer, RgbImage};
3 | use nu_engine::command_prelude::*;
4 | use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand};
5 | use nu_protocol::{FromValue, LabeledError};
6 | use plotters::prelude::{BitMapBackend, IntoDrawingArea};
7 | use plotters::style::WHITE;
8 | use viuer::KittySupport;
9 |
10 | use crate::value;
11 |
12 | #[derive(Debug, Clone)]
13 | pub struct DrawTerminal;
14 |
15 | impl Command for DrawTerminal {
16 | fn name(&self) -> &str {
17 | "draw terminal"
18 | }
19 |
20 | fn signature(&self) -> Signature {
21 | Signature::new(Command::name(self))
22 | .add_help()
23 | .description(Command::description(self))
24 | .search_terms(
25 | Command::search_terms(self)
26 | .into_iter()
27 | .map(ToOwned::to_owned)
28 | .collect(),
29 | )
30 | .input_output_type(value::Chart2d::ty(), Type::Nothing)
31 | }
32 |
33 | fn description(&self) -> &str {
34 | "Draws a chart to a sixel string. Compatible terminal emulators may render that."
35 | }
36 |
37 | fn search_terms(&self) -> Vec<&str> {
38 | vec!["plotters", "chart", "2d", "draw", "terminal"]
39 | }
40 |
41 | fn run(
42 | &self,
43 | _: &EngineState,
44 | _: &mut Stack,
45 | _: &Call,
46 | input: PipelineData,
47 | ) -> Result {
48 | let span = input.span().unwrap_or(Span::unknown());
49 | let input = input.into_value(span)?;
50 | DrawTerminal::run(self, input).map(|v| PipelineData::Value(v, None))
51 | }
52 | }
53 |
54 | impl SimplePluginCommand for DrawTerminal {
55 | type Plugin = crate::plugin::PlottersPlugin;
56 |
57 | fn name(&self) -> &str {
58 | Command::name(self)
59 | }
60 |
61 | fn signature(&self) -> Signature {
62 | Command::signature(self)
63 | }
64 |
65 | fn description(&self) -> &str {
66 | Command::description(self)
67 | }
68 |
69 | fn search_terms(&self) -> Vec<&str> {
70 | Command::search_terms(self)
71 | }
72 |
73 | fn run(
74 | &self,
75 | _: &Self::Plugin,
76 | _: &EngineInterface,
77 | _: &EvaluatedCall,
78 | input: &Value,
79 | ) -> Result {
80 | let input = input.clone();
81 | DrawTerminal::run(self, input).map_err(Into::into)
82 | }
83 | }
84 |
85 | impl DrawTerminal {
86 | fn run(&self, input: Value) -> Result {
87 | let span = input.span();
88 | let mut chart = value::Chart2d::from_value(input)?;
89 |
90 | const BYTES_PER_PIXEL: u32 = 3;
91 | let bytes = chart.height * chart.width * BYTES_PER_PIXEL;
92 | let mut buf: Vec = vec![0; bytes as usize];
93 |
94 | let size = (chart.width, chart.height);
95 | let drawing_backend = BitMapBackend::with_buffer(&mut buf, size);
96 |
97 | if chart.background.is_none() {
98 | chart.background = Some(WHITE.into());
99 | }
100 |
101 | super::draw(chart, drawing_backend.into_drawing_area());
102 |
103 | // TODO: convert these errors somehow into shell errors
104 | if viuer::get_kitty_support() != KittySupport::None || viuer::is_iterm_supported() {
105 | let img: RgbImage = ImageBuffer::from_raw(size.0, size.1, buf).unwrap();
106 | let img = DynamicImage::ImageRgb8(img);
107 | viuer::print(&img, &viuer::Config {
108 | absolute_offset: false,
109 | ..Default::default()
110 | }).unwrap();
111 | }
112 | else {
113 | let sixel = icy_sixel::sixel_string(
114 | &buf,
115 | size.0 as i32,
116 | size.1 as i32,
117 | PixelFormat::RGB888,
118 | DiffusionMethod::Stucki,
119 | MethodForLargest::Auto,
120 | MethodForRep::Auto,
121 | Quality::HIGH,
122 | )
123 | .unwrap();
124 | println!("{sixel}");
125 | }
126 |
127 | Ok(Value::nothing(span))
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod series;
2 |
3 | mod chart_2d;
4 | pub use chart_2d::Chart2d;
5 |
6 | pub mod draw;
7 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/series/bar.rs:
--------------------------------------------------------------------------------
1 | use nu_engine::command_prelude::*;
2 | use nu_plugin::{EngineInterface, EvaluatedCall};
3 | use nu_protocol::{FromValue, IntoValue, LabeledError};
4 | use plotters::style::BLUE;
5 |
6 | use crate::value::{Bar2dSeries, Color, Series2d};
7 |
8 | #[derive(Debug, Clone)]
9 | pub struct BarSeries;
10 |
11 | impl Command for BarSeries {
12 | fn name(&self) -> &str {
13 | "series bar"
14 | }
15 |
16 | fn signature(&self) -> Signature {
17 | Signature::new(Command::name(self))
18 | .add_help()
19 | .description(Command::description(self))
20 | .extra_description(Command::extra_description(self))
21 | .search_terms(
22 | Command::search_terms(self)
23 | .into_iter()
24 | .map(ToOwned::to_owned)
25 | .collect(),
26 | )
27 | .named(
28 | "color",
29 | SyntaxShape::Any,
30 | "Define the color of the points and the line. For valid color inputs, refer to \
31 | `plotters colors --help`.",
32 | Some('c'),
33 | )
34 | .named(
35 | "filled",
36 | SyntaxShape::Boolean,
37 | "Define whether the points in the series should be filled.",
38 | Some('f'),
39 | )
40 | .named(
41 | "stroke-width",
42 | SyntaxShape::Int,
43 | "Define the width of the stroke.",
44 | Some('s'),
45 | )
46 | .named(
47 | "horizontal",
48 | SyntaxShape::Boolean,
49 | "Define whether the bars should be horizontal.",
50 | Some('H'),
51 | )
52 | .input_output_type(Type::list(Type::Number), Series2d::ty())
53 | .input_output_type(Type::list(Type::list(Type::Number)), Series2d::ty())
54 | .input_output_type(
55 | Type::list(Type::Record(
56 | vec![
57 | ("x".to_string(), Type::Number),
58 | ("y".to_string(), Type::Number),
59 | ]
60 | .into_boxed_slice(),
61 | )),
62 | Series2d::ty(),
63 | )
64 | }
65 |
66 | fn description(&self) -> &str {
67 | "Create a bar series."
68 | }
69 |
70 | fn extra_description(&self) -> &str {
71 | "This series requires as input a list or stream of value pairs for the x and y axis."
72 | }
73 |
74 | fn search_terms(&self) -> Vec<&str> {
75 | vec!["plotters", "series", "bar", "chart"]
76 | }
77 |
78 | fn run(
79 | &self,
80 | engine_state: &EngineState,
81 | stack: &mut Stack,
82 | call: &Call,
83 | input: PipelineData,
84 | ) -> Result {
85 | let span = input.span().unwrap_or(Span::unknown());
86 | let input = input.into_value(span)?;
87 | let color = call.get_flag(engine_state, stack, "color")?;
88 | let filled = call.get_flag(engine_state, stack, "filled")?;
89 | let stroke_width = call.get_flag(engine_state, stack, "stroke-width")?;
90 | BarSeries::run(self, input, color, filled, stroke_width)
91 | .map(|v| PipelineData::Value(v, None))
92 | }
93 | }
94 |
95 | impl nu_plugin::SimplePluginCommand for BarSeries {
96 | type Plugin = crate::plugin::PlottersPlugin;
97 |
98 | fn name(&self) -> &str {
99 | Command::name(self)
100 | }
101 |
102 | fn signature(&self) -> Signature {
103 | Command::signature(self)
104 | }
105 |
106 | fn description(&self) -> &str {
107 | Command::description(self)
108 | }
109 |
110 | fn extra_description(&self) -> &str {
111 | Command::extra_description(self)
112 | }
113 |
114 | fn search_terms(&self) -> Vec<&str> {
115 | Command::search_terms(self)
116 | }
117 |
118 | fn run(
119 | &self,
120 | _: &Self::Plugin,
121 | _: &EngineInterface,
122 | call: &EvaluatedCall,
123 | input: &Value,
124 | ) -> Result {
125 | let input = input.clone();
126 | let (mut color, mut filled, mut stroke_width) = Default::default();
127 | for (name, value) in call.named.clone() {
128 | fn extract_named(
129 | name: impl ToString,
130 | value: Option,
131 | span: Span,
132 | ) -> Result {
133 | let value = value.ok_or_else(|| ShellError::MissingParameter {
134 | param_name: name.to_string(),
135 | span,
136 | })?;
137 | T::from_value(value)
138 | }
139 |
140 | match name.item.as_str() {
141 | "color" => color = extract_named("color", value, name.span)?,
142 | "filled" => filled = extract_named("filled", value, name.span)?,
143 | "stroke-width" => stroke_width = extract_named("stroke-width", value, name.span)?,
144 | _ => continue,
145 | }
146 | }
147 |
148 | BarSeries::run(self, input, color, filled, stroke_width).map_err(Into::into)
149 | }
150 | }
151 |
152 | impl BarSeries {
153 | fn run(
154 | &self,
155 | input: Value,
156 | color: Option,
157 | filled: Option,
158 | stroke_width: Option,
159 | ) -> Result {
160 | let input_span = input.span();
161 | let series = super::series_from_value(input)?;
162 | let series = Bar2dSeries {
163 | series,
164 | color: color.unwrap_or(BLUE.into()),
165 | filled: filled.unwrap_or(true),
166 | stroke_width: stroke_width.unwrap_or(1),
167 | };
168 |
169 | Ok(Series2d::Bar(series).into_value(input_span))
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/series/line.rs:
--------------------------------------------------------------------------------
1 | use nu_engine::command_prelude::*;
2 | use nu_plugin::{EngineInterface, EvaluatedCall};
3 | use nu_protocol::{FromValue, IntoValue, LabeledError};
4 | use plotters::style::BLUE;
5 |
6 | use crate::value::{Color, Line2dSeries, Series2d};
7 |
8 | #[derive(Debug, Clone)]
9 | pub struct LineSeries;
10 |
11 | impl Command for LineSeries {
12 | fn name(&self) -> &str {
13 | "series line"
14 | }
15 |
16 | fn signature(&self) -> Signature {
17 | Signature::new(Command::name(self))
18 | .add_help()
19 | .description(Command::description(self))
20 | .extra_description(Command::extra_description(self))
21 | .search_terms(
22 | Command::search_terms(self)
23 | .into_iter()
24 | .map(ToOwned::to_owned)
25 | .collect(),
26 | )
27 | .named(
28 | "color",
29 | SyntaxShape::Any,
30 | "Define the color of the points and the line. For valid color inputs, refer to \
31 | `plotters colors --help`.",
32 | Some('c'),
33 | )
34 | .named(
35 | "filled",
36 | SyntaxShape::Boolean,
37 | "Define whether the points in the series should be filled.",
38 | Some('f'),
39 | )
40 | .named(
41 | "stroke-width",
42 | SyntaxShape::Int,
43 | "Define the width of the stroke.",
44 | Some('s'),
45 | )
46 | .named(
47 | "point-size",
48 | SyntaxShape::Int,
49 | "Define the size of the points in pixels.",
50 | Some('p'),
51 | )
52 | .input_output_type(Type::list(Type::Number), Series2d::ty())
53 | .input_output_type(Type::list(Type::list(Type::Number)), Series2d::ty())
54 | .input_output_type(
55 | Type::list(Type::Record(
56 | vec![
57 | ("x".to_string(), Type::Number),
58 | ("y".to_string(), Type::Number),
59 | ]
60 | .into_boxed_slice(),
61 | )),
62 | Series2d::ty(),
63 | )
64 | }
65 |
66 | fn description(&self) -> &str {
67 | "Create a line series."
68 | }
69 |
70 | fn extra_description(&self) -> &str {
71 | "This series requires as input a list or stream of value pairs for the x and y axis."
72 | }
73 |
74 | fn search_terms(&self) -> Vec<&str> {
75 | vec!["plotters", "series", "line", "chart"]
76 | }
77 |
78 | fn run(
79 | &self,
80 | engine_state: &EngineState,
81 | stack: &mut Stack,
82 | call: &Call,
83 | input: PipelineData,
84 | ) -> Result {
85 | let span = input.span().unwrap_or(Span::unknown());
86 | let input = input.into_value(span)?;
87 | let color = call.get_flag(engine_state, stack, "color")?;
88 | let filled = call.get_flag(engine_state, stack, "filled")?;
89 | let stroke_width = call.get_flag(engine_state, stack, "stroke-width")?;
90 | let point_size = call.get_flag(engine_state, stack, "point-size")?;
91 | LineSeries::run(self, input, color, filled, stroke_width, point_size)
92 | .map(|v| PipelineData::Value(v, None))
93 | }
94 | }
95 |
96 | impl nu_plugin::SimplePluginCommand for LineSeries {
97 | type Plugin = crate::plugin::PlottersPlugin;
98 |
99 | fn name(&self) -> &str {
100 | Command::name(self)
101 | }
102 |
103 | fn signature(&self) -> Signature {
104 | Command::signature(self)
105 | }
106 |
107 | fn description(&self) -> &str {
108 | Command::description(self)
109 | }
110 |
111 | fn extra_description(&self) -> &str {
112 | Command::extra_description(self)
113 | }
114 |
115 | fn search_terms(&self) -> Vec<&str> {
116 | Command::search_terms(self)
117 | }
118 |
119 | fn run(
120 | &self,
121 | _: &Self::Plugin,
122 | _: &EngineInterface,
123 | call: &EvaluatedCall,
124 | input: &Value,
125 | ) -> Result {
126 | let input = input.clone();
127 | let (mut color, mut filled, mut stroke_width, mut point_size) = Default::default();
128 | for (name, value) in call.named.clone() {
129 | fn extract_named(
130 | name: impl ToString,
131 | value: Option,
132 | span: Span,
133 | ) -> Result {
134 | let value = value.ok_or_else(|| ShellError::MissingParameter {
135 | param_name: name.to_string(),
136 | span,
137 | })?;
138 | T::from_value(value)
139 | }
140 |
141 | match name.item.as_str() {
142 | "color" => color = extract_named("color", value, name.span)?,
143 | "filled" => filled = extract_named("filled", value, name.span)?,
144 | "stroke-width" => stroke_width = extract_named("stroke-width", value, name.span)?,
145 | "point-size" => point_size = extract_named("point-size", value, name.span)?,
146 | _ => continue,
147 | }
148 | }
149 |
150 | LineSeries::run(self, input, color, filled, stroke_width, point_size).map_err(Into::into)
151 | }
152 | }
153 |
154 | impl LineSeries {
155 | fn run(
156 | &self,
157 | input: Value,
158 | color: Option,
159 | filled: Option,
160 | stroke_width: Option,
161 | point_size: Option,
162 | ) -> Result {
163 | let input_span = input.span();
164 | let series = super::series_from_value(input)?;
165 | let series = Line2dSeries {
166 | series,
167 | color: color.unwrap_or(BLUE.into()),
168 | filled: filled.unwrap_or(false),
169 | stroke_width: stroke_width.unwrap_or(1),
170 | point_size: point_size.unwrap_or(0),
171 | };
172 |
173 | Ok(Series2d::Line(series).into_value(input_span))
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/commands/series/mod.rs:
--------------------------------------------------------------------------------
1 | use nu_protocol::{FromValue, ShellError, Value};
2 |
3 | use crate::value::{Coord1d, Coord2d};
4 |
5 | mod line;
6 | pub use line::*;
7 |
8 | mod bar;
9 | pub use bar::*;
10 |
11 | fn series_from_value(input: Value) -> Result, ShellError> {
12 | let input_span = input.span();
13 | let input = input.into_list()?;
14 | let first = input
15 | .first()
16 | .ok_or_else(|| ShellError::PipelineEmpty {
17 | dst_span: input_span,
18 | })?
19 | .clone();
20 |
21 | let number = Coord1d::from_value(first.clone());
22 | let tuple = <(Coord1d, Coord1d)>::from_value(first.clone());
23 | let coord = Coord2d::from_value(first);
24 |
25 | let mut series: Vec = Vec::with_capacity(input.len());
26 | match (number, tuple, coord) {
27 | (Ok(_), _, _) => {
28 | // input: list
29 | for (i, val) in input.into_iter().enumerate() {
30 | let val = Coord1d::from_value(val)?;
31 | let val = Coord2d {
32 | x: Coord1d::from_int(i as i64),
33 | y: val,
34 | };
35 | series.push(val);
36 | }
37 | }
38 |
39 | (_, Ok(_), _) => {
40 | // input: list>
41 | for val in input {
42 | let (x, y) = <(Coord1d, Coord1d)>::from_value(val)?;
43 | let val = Coord2d { x, y };
44 | series.push(val);
45 | }
46 | }
47 |
48 | (_, _, Ok(_)) => {
49 | // input: list>
50 | for val in input {
51 | let val = Coord2d::from_value(val)?;
52 | series.push(val);
53 | }
54 | }
55 |
56 | (Err(_), Err(_), Err(_)) => todo!("throw explaining error"),
57 | }
58 |
59 | Ok(series)
60 | }
61 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/lib.rs:
--------------------------------------------------------------------------------
1 | use nu_protocol::engine::{EngineState, StateWorkingSet};
2 |
3 | pub mod commands;
4 | pub mod plugin;
5 | pub mod value;
6 |
7 | pub fn add_plotters_command_context(mut engine_state: EngineState) -> EngineState {
8 | let delta = {
9 | let mut working_set = StateWorkingSet::new(&engine_state);
10 |
11 | macro_rules! bind_command {
12 | ( $( $command:expr ),* $(,)? ) => {
13 | $( working_set.add_decl(Box::new($command)); )*
14 | };
15 | }
16 |
17 | bind_command!(
18 | commands::series::BarSeries,
19 | commands::series::LineSeries,
20 | commands::Chart2d,
21 | commands::draw::DrawSvg,
22 | commands::draw::DrawTerminal,
23 | );
24 |
25 | working_set.render()
26 | };
27 |
28 | if let Err(err) = engine_state.merge_delta(delta) {
29 | eprintln!("Error creating default context: {err:?}");
30 | }
31 |
32 | engine_state
33 | }
34 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/main.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | nu_plugin::serve_plugin(
3 | &nu_plugin_plotters::plugin::PlottersPlugin,
4 | nu_plugin::MsgPackSerializer,
5 | )
6 | }
7 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/plugin.rs:
--------------------------------------------------------------------------------
1 | use nu_plugin::{Plugin, PluginCommand};
2 |
3 | use crate::commands;
4 |
5 | pub struct PlottersPlugin;
6 |
7 | impl Plugin for PlottersPlugin {
8 | fn version(&self) -> String {
9 | env!("CARGO_PKG_VERSION").into()
10 | }
11 |
12 | fn commands(&self) -> Vec>> {
13 | vec![
14 | Box::new(commands::series::BarSeries),
15 | Box::new(commands::series::LineSeries),
16 | Box::new(commands::Chart2d),
17 | Box::new(commands::draw::DrawSvg),
18 | Box::new(commands::draw::DrawTerminal),
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/chart_2d.rs:
--------------------------------------------------------------------------------
1 | use std::any::Any;
2 |
3 | use nu_protocol::{CustomValue, FromValue, IntoValue, ShellError, Span, Type, Value};
4 | use serde::{Deserialize, Serialize};
5 |
6 | use super::color::Color;
7 | use super::series_2d::Series2d;
8 | use super::Range;
9 |
10 | #[derive(Debug, Clone, IntoValue, Serialize, Deserialize)]
11 | pub struct Chart2d {
12 | pub series: Vec,
13 | pub width: u32,
14 | pub height: u32,
15 | pub background: Option,
16 | pub caption: Option,
17 | pub margin: [u32; 4], // use css shorthand rotation [top, right, bottom, left]
18 | pub label_area: [u32; 4],
19 | pub x_range: Option,
20 | pub y_range: Option,
21 | pub x_mesh: bool,
22 | pub y_mesh: bool,
23 | }
24 |
25 | impl Default for Chart2d {
26 | fn default() -> Self {
27 | Self {
28 | series: Vec::new(),
29 | width: 600,
30 | height: 400,
31 | background: None,
32 | caption: None,
33 | margin: [5, 5, 5, 5],
34 | label_area: [0, 0, 35, 35],
35 | x_range: None,
36 | y_range: None,
37 | x_mesh: true,
38 | y_mesh: true,
39 | }
40 | }
41 | }
42 |
43 | impl FromValue for Chart2d {
44 | fn from_value(v: Value) -> Result {
45 | let span = v.span();
46 | let v = v.into_custom_value()?;
47 | match v.as_any().downcast_ref::() {
48 | Some(v) => Ok(v.clone()),
49 | None => Err(ShellError::CantConvert {
50 | to_type: Self::ty().to_string(),
51 | from_type: v.type_name(),
52 | span,
53 | help: None,
54 | }),
55 | }
56 | }
57 |
58 | fn expected_type() -> Type {
59 | Self::ty()
60 | }
61 | }
62 |
63 | #[typetag::serde]
64 | impl CustomValue for Chart2d {
65 | fn clone_value(&self, span: Span) -> Value {
66 | Value::custom(Box::new(self.clone()), span)
67 | }
68 |
69 | fn type_name(&self) -> String {
70 | Self::ty().to_string()
71 | }
72 |
73 | fn to_base_value(&self, span: Span) -> Result {
74 | Ok(self.clone().into_value(span))
75 | }
76 |
77 | fn as_any(&self) -> &dyn Any {
78 | self
79 | }
80 |
81 | fn as_mut_any(&mut self) -> &mut dyn Any {
82 | self
83 | }
84 | }
85 |
86 | macro_rules! xy_range {
87 | ($fn_name:ident) => {
88 | pub fn $fn_name(&self) -> Option {
89 | if let Some(range) = self.$fn_name {
90 | return Some(range);
91 | }
92 |
93 | let first = self.series.first()?;
94 | let Range {
95 | mut min, mut max, ..
96 | } = first.$fn_name()?;
97 | for Range {
98 | min: s_min,
99 | max: s_max,
100 | ..
101 | } in self.series.iter().filter_map(|s| s.$fn_name())
102 | {
103 | if s_min < min {
104 | min = s_min
105 | }
106 | if s_max > max {
107 | max = s_max
108 | }
109 | }
110 |
111 | Some(Range {
112 | min,
113 | max,
114 | metadata: None,
115 | })
116 | }
117 | };
118 | }
119 |
120 | impl Chart2d {
121 | xy_range!(x_range);
122 |
123 | xy_range!(y_range);
124 |
125 | pub fn ty() -> Type {
126 | Type::Custom("plotters::chart-2d".to_string().into_boxed_str())
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/color.rs:
--------------------------------------------------------------------------------
1 | use nu_protocol::{FromValue, IntoValue, ShellError, Span, Type, Value};
2 | use plotters::style::*;
3 | use serde::{Deserialize, Serialize};
4 |
5 | #[derive(Debug, Clone, IntoValue, Serialize, Deserialize)]
6 | pub struct Color {
7 | pub r: ColorChannel,
8 | pub g: ColorChannel,
9 | pub b: ColorChannel,
10 | pub a: AlphaChannel,
11 | }
12 |
13 | impl Default for Color {
14 | fn default() -> Self {
15 | Self {
16 | r: ColorChannel(0),
17 | g: ColorChannel(0),
18 | b: ColorChannel(0),
19 | a: AlphaChannel(1.0),
20 | }
21 | }
22 | }
23 |
24 | impl FromValue for Color {
25 | fn from_value(v: Value) -> Result {
26 | match v {
27 | val @ Value::Record { .. } => {
28 | #[derive(FromValue)]
29 | struct ColorDTO {
30 | r: ColorChannel,
31 | g: ColorChannel,
32 | b: ColorChannel,
33 | a: Option,
34 | }
35 |
36 | let color = ColorDTO::from_value(val)?;
37 | Ok(Color {
38 | r: color.r,
39 | g: color.g,
40 | b: color.b,
41 | a: color.a.unwrap_or_default(),
42 | })
43 | }
44 |
45 | list @ Value::List { .. } => {
46 | // TODO: check list length to avoid cloning
47 | let rgba = <(ColorChannel, ColorChannel, ColorChannel, AlphaChannel)>::from_value(
48 | list.clone(),
49 | );
50 | let rgb = <(ColorChannel, ColorChannel, ColorChannel)>::from_value(list);
51 | match (rgba, rgb) {
52 | (Ok((r, g, b, a)), _) => Ok(Color { r, g, b, a }),
53 | (Err(ShellError::CantFindColumn { .. }), Ok((r, g, b))) => Ok(Color {
54 | r,
55 | g,
56 | b,
57 | a: Default::default(),
58 | }),
59 | (Err(e), Ok(_)) => Err(e),
60 | (Err(_), Err(e)) => Err(e),
61 | }
62 | }
63 |
64 | ref v @ Value::String {
65 | ref val,
66 | internal_span: span,
67 | } => match val.to_lowercase().as_str() {
68 | "black" => Ok(BLACK.into()),
69 | "blue" => Ok(BLUE.into()),
70 | "cyan" => Ok(CYAN.into()),
71 | "green" => Ok(GREEN.into()),
72 | "magenta" => Ok(MAGENTA.into()),
73 | "red" => Ok(RED.into()),
74 | "white" => Ok(WHITE.into()),
75 | "yellow" => Ok(YELLOW.into()),
76 | val => {
77 | if let Some(val) = val.strip_prefix("#") {
78 | match val.len() {
79 | 6 => {
80 | let span = |offset| {
81 | Span::new(span.start + 2 + offset, span.start + 4 + offset)
82 | };
83 | let mut color = Color::default();
84 | color.r.0 = u8_from_hex(&val[0..2], span(0))?;
85 | color.g.0 = u8_from_hex(&val[2..4], span(2))?;
86 | color.b.0 = u8_from_hex(&val[4..6], span(4))?;
87 | return Ok(color);
88 | }
89 | 3 => {
90 | let span = |offset| {
91 | Span::new(span.start + 2 + offset, span.start + 3 + offset)
92 | };
93 | let mut color = Color::default();
94 | color.r.0 = u8_from_hex(&val[0..1].repeat(2), span(0))?;
95 | color.g.0 = u8_from_hex(&val[1..2].repeat(2), span(1))?;
96 | color.b.0 = u8_from_hex(&val[2..3].repeat(2), span(2))?;
97 | return Ok(color);
98 | }
99 | _ => (),
100 | }
101 | }
102 |
103 | Err(ShellError::CantConvert {
104 | to_type: Self::expected_type().to_string(),
105 | from_type: v.get_type().to_string(),
106 | span: v.span(),
107 | help: None,
108 | })
109 | }
110 | },
111 |
112 | v => Err(ShellError::CantConvert {
113 | to_type: Self::expected_type().to_string(),
114 | from_type: v.get_type().to_string(),
115 | span: v.span(),
116 | help: None,
117 | }),
118 | }
119 | }
120 |
121 | fn expected_type() -> Type {
122 | Type::Custom("plotters::color".to_string().into_boxed_str())
123 | }
124 | }
125 |
126 | fn u8_from_hex(hex: &str, span: Span) -> Result {
127 | u8::from_str_radix(hex, 16).map_err(|_| ShellError::CantConvert {
128 | to_type: Type::Int.to_string(),
129 | from_type: Type::String.to_string(),
130 | span,
131 | help: None,
132 | })
133 | }
134 |
135 | impl From for Color {
136 | fn from(value: RGBColor) -> Self {
137 | Self {
138 | r: ColorChannel(value.0),
139 | g: ColorChannel(value.1),
140 | b: ColorChannel(value.2),
141 | a: Default::default(),
142 | }
143 | }
144 | }
145 |
146 | impl From for plotters::style::RGBAColor {
147 | fn from(value: Color) -> Self {
148 | Self(value.r.0, value.g.0, value.b.0, value.a.0)
149 | }
150 | }
151 |
152 | #[derive(Debug, Clone, Serialize, Deserialize)]
153 | pub struct ColorChannel(u8);
154 |
155 | impl IntoValue for ColorChannel {
156 | fn into_value(self, span: Span) -> Value {
157 | Value::int(self.0 as i64, span)
158 | }
159 | }
160 |
161 | impl FromValue for ColorChannel {
162 | fn from_value(value: Value) -> Result {
163 | let span = value.span();
164 | let value = i64::from_value(value)?;
165 | const U8MIN: i64 = u8::MIN as i64;
166 | const U8MAX: i64 = u8::MAX as i64;
167 | #[allow(overlapping_range_endpoints)]
168 | #[allow(clippy::match_overlapping_arm)]
169 | match value {
170 | U8MIN..=U8MAX => Ok(ColorChannel(value as u8)),
171 | i64::MIN..U8MIN => Err(ShellError::GenericError {
172 | error: "Integer too small".to_string(),
173 | msg: format!("{value} is smaller than {U8MIN}"),
174 | span: Some(span),
175 | help: None,
176 | inner: vec![],
177 | }),
178 | U8MAX..=i64::MAX => Err(ShellError::GenericError {
179 | error: "Integer too large".to_string(),
180 | msg: format!("{value} is larger than {U8MAX}"),
181 | span: Some(span),
182 | help: None,
183 | inner: vec![],
184 | }),
185 | }
186 | }
187 |
188 | fn expected_type() -> Type {
189 | Type::Int
190 | }
191 | }
192 |
193 | #[derive(Debug, Clone, Serialize, Deserialize)]
194 | pub struct AlphaChannel(f64);
195 |
196 | impl IntoValue for AlphaChannel {
197 | fn into_value(self, span: Span) -> Value {
198 | Value::float(self.0, span)
199 | }
200 | }
201 |
202 | impl FromValue for AlphaChannel {
203 | fn from_value(v: Value) -> Result {
204 | let span = v.span();
205 | let v = f64::from_value(v)?;
206 | match v {
207 | 0.0..=1.0 => Ok(Self(v)),
208 | _ => Err(ShellError::GenericError {
209 | error: "Invalid alpha channel value".to_string(),
210 | msg: format!("{v} is not in range between 0.0 and 1.0"),
211 | span: Some(span),
212 | help: None,
213 | inner: vec![],
214 | }),
215 | }
216 | }
217 |
218 | fn expected_type() -> nu_protocol::Type {
219 | f64::expected_type()
220 | }
221 | }
222 |
223 | impl Default for AlphaChannel {
224 | fn default() -> Self {
225 | Self(1.0)
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/coords/coord_1d.rs:
--------------------------------------------------------------------------------
1 | use std::cmp;
2 | use std::ops::{Add, AddAssign, Div, Sub};
3 |
4 | use nu_protocol::{FromValue, IntoValue, ShellError, Span, Value};
5 | use serde::{Deserialize, Serialize};
6 |
7 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
8 | pub enum Coord1d {
9 | Int(i64),
10 | Float(f64), // we ensure that this is valid number
11 | }
12 |
13 | #[derive(Debug, Clone, Copy)]
14 | pub enum Coord1dFromFloatError {
15 | Nan,
16 | Infinity,
17 | }
18 |
19 | impl Coord1d {
20 | pub fn from_int(int: i64) -> Self {
21 | Coord1d::Int(int)
22 | }
23 |
24 | pub fn from_float(float: f64) -> Result {
25 | match float.is_finite() {
26 | true => Ok(Self::Float(float)),
27 | false => match float.is_nan() {
28 | true => Err(Coord1dFromFloatError::Nan),
29 | false => Err(Coord1dFromFloatError::Infinity),
30 | },
31 | }
32 | }
33 |
34 | pub fn as_float(self) -> f64 {
35 | match self {
36 | Coord1d::Int(int) => int as f64,
37 | Coord1d::Float(float) => float,
38 | }
39 | }
40 |
41 | pub fn floor(self) -> i64 {
42 | match self {
43 | Coord1d::Int(int) => int,
44 | Coord1d::Float(float) => {
45 | let float = float.floor();
46 | debug_assert!(
47 | float < i64::MAX as f64,
48 | "Coord1d::Float was larger than i64::MAX"
49 | );
50 | debug_assert!(
51 | float > i64::MIN as f64,
52 | "Coord1d::Float was smaller than i64::MIN"
53 | );
54 | float as i64
55 | }
56 | }
57 | }
58 |
59 | pub fn round(self) -> i64 {
60 | match self {
61 | Coord1d::Int(int) => int,
62 | Coord1d::Float(float) => {
63 | let float = float.round();
64 | debug_assert!(
65 | float < i64::MAX as f64,
66 | "Coord1d::Float was larger than i64::MAX"
67 | );
68 | debug_assert!(
69 | float > i64::MIN as f64,
70 | "Coord1d::Float was smaller than i64::MIN"
71 | );
72 | float as i64
73 | }
74 | }
75 | }
76 |
77 | pub fn ceil(self) -> i64 {
78 | match self {
79 | Coord1d::Int(int) => int,
80 | Coord1d::Float(float) => {
81 | let float = float.ceil();
82 | debug_assert!(
83 | float < i64::MAX as f64,
84 | "Coord1d::Float was larger than i64::MAX"
85 | );
86 | debug_assert!(
87 | float > i64::MIN as f64,
88 | "Coord1d::Float was smaller than i64::MIN"
89 | );
90 | float as i64
91 | }
92 | }
93 | }
94 | }
95 |
96 | impl Eq for Coord1d {}
97 |
98 | impl PartialEq for Coord1d {
99 | fn eq(&self, other: &Self) -> bool {
100 | let this = (*self).as_float();
101 | let that = (*other).as_float();
102 | this == that
103 | }
104 | }
105 |
106 | impl Ord for Coord1d {
107 | fn cmp(&self, other: &Self) -> cmp::Ordering {
108 | let this = (*self).as_float();
109 | let that = (*other).as_float();
110 | match PartialOrd::partial_cmp(&this, &that) {
111 | Some(ord) => ord,
112 | // we ensure that float is always a valid value
113 | None => panic!("self was NaN"),
114 | }
115 | }
116 | }
117 |
118 | impl PartialOrd for Coord1d {
119 | fn partial_cmp(&self, other: &Self) -> Option {
120 | Some(self.cmp(other))
121 | }
122 | }
123 |
124 | impl IntoValue for Coord1d {
125 | fn into_value(self, span: Span) -> Value {
126 | match self {
127 | Coord1d::Int(int) => Value::int(int, span),
128 | Coord1d::Float(float) => Value::float(float, span),
129 | }
130 | }
131 | }
132 |
133 | impl FromValue for Coord1d {
134 | fn from_value(v: Value) -> Result {
135 | match v {
136 | Value::Int { val, .. } => Ok(Self::Int(val)),
137 | Value::Float { val, internal_span } => Self::from_float(val).map_err(|e| {
138 | let error = match e {
139 | Coord1dFromFloatError::Nan => "Number is not a number",
140 | Coord1dFromFloatError::Infinity => "Number is not finite",
141 | }
142 | .to_string();
143 |
144 | ShellError::GenericError {
145 | error,
146 | msg: "Coordinates need to be a valid number.".to_string(),
147 | span: Some(internal_span),
148 | help: None,
149 | inner: vec![],
150 | }
151 | }),
152 | _ => Err(ShellError::CantConvert {
153 | to_type: Self::expected_type().to_string(),
154 | from_type: v.get_type().to_string(),
155 | span: v.span(),
156 | help: None,
157 | }),
158 | }
159 | }
160 | }
161 |
162 | impl Add for Coord1d {
163 | type Output = Self;
164 |
165 | fn add(self, rhs: Self) -> Self::Output {
166 | match (self, rhs) {
167 | (Coord1d::Int(lhs), Coord1d::Int(rhs)) => Coord1d::Int(lhs + rhs),
168 | (Coord1d::Int(lhs), Coord1d::Float(rhs)) => Coord1d::Float(lhs as f64 + rhs),
169 | (Coord1d::Float(lhs), Coord1d::Int(rhs)) => Coord1d::Float(lhs + rhs as f64),
170 | (Coord1d::Float(lhs), Coord1d::Float(rhs)) => Coord1d::Float(lhs + rhs),
171 | }
172 | }
173 | }
174 |
175 | impl AddAssign for Coord1d {
176 | fn add_assign(&mut self, rhs: Self) {
177 | *self = self.add(rhs);
178 | }
179 | }
180 |
181 | impl Sub for Coord1d {
182 | type Output = Self;
183 |
184 | fn sub(self, rhs: Self) -> Self::Output {
185 | match (self, rhs) {
186 | (Coord1d::Int(lhs), Coord1d::Int(rhs)) => Coord1d::Int(lhs - rhs),
187 | (Coord1d::Int(lhs), Coord1d::Float(rhs)) => Coord1d::Float(lhs as f64 - rhs),
188 | (Coord1d::Float(lhs), Coord1d::Int(rhs)) => Coord1d::Float(lhs - rhs as f64),
189 | (Coord1d::Float(lhs), Coord1d::Float(rhs)) => Coord1d::Float(lhs - rhs),
190 | }
191 | }
192 | }
193 |
194 | impl Div for Coord1d {
195 | type Output = Self;
196 |
197 | fn div(self, rhs: Self) -> Self::Output {
198 | let lhs = self.as_float();
199 | let rhs = rhs.as_float();
200 | Coord1d::Float(lhs / rhs)
201 | }
202 | }
203 |
204 | impl Default for Coord1d {
205 | fn default() -> Self {
206 | Self::Int(0)
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/coords/coord_2d.rs:
--------------------------------------------------------------------------------
1 | use nu_protocol::{FromValue, IntoValue, ShellError, Value};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Coord1d;
5 |
6 | #[derive(Debug, Clone, Copy, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
7 | pub struct Coord2d {
8 | pub x: Coord1d,
9 | pub y: Coord1d,
10 | }
11 |
12 | impl FromValue for Coord2d {
13 | fn from_value(v: Value) -> Result {
14 | #[derive(FromValue)]
15 | struct Coord2dDTO {
16 | x: Coord1d,
17 | y: Coord1d,
18 | }
19 |
20 | let tuple = <(Coord1d, Coord1d)>::from_value(v.clone());
21 | let record = Coord2dDTO::from_value(v);
22 | match (tuple, record) {
23 | (Ok((x, y)), _) => Ok(Self { x, y }),
24 | (_, Ok(Coord2dDTO { x, y })) => Ok(Self { x, y }),
25 | (Err(tuple_e), Err(record_e)) => Err(ShellError::GenericError {
26 | error: "Invalid 2D coordinate.".to_string(),
27 | msg: "A 2D coordinate needs to be either pair or a x-y-record of numbers."
28 | .to_string(),
29 | span: None,
30 | help: None,
31 | inner: vec![tuple_e, record_e],
32 | }),
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/coords/mod.rs:
--------------------------------------------------------------------------------
1 | mod coord_1d;
2 | pub use coord_1d::*;
3 |
4 | mod coord_2d;
5 | pub use coord_2d::*;
6 |
7 | mod range;
8 | pub use range::*;
9 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/coords/range.rs:
--------------------------------------------------------------------------------
1 | use std::ops::{self, Bound};
2 |
3 | use nu_protocol::{FloatRange, FromValue, IntoValue, ShellError, Type, Value};
4 | use plotters::coord::ranged1d::{KeyPointHint, NoDefaultFormatting, ValueFormatter};
5 | use plotters::coord::types::{RangedCoordf64, RangedCoordi64};
6 | use plotters::prelude::{DiscreteRanged, Ranged};
7 | use serde::{Deserialize, Serialize};
8 |
9 | use super::Coord1d;
10 |
11 | // TODO: ensure that ranges are always min to max
12 | #[derive(Debug, Clone, Copy, IntoValue, Serialize, Deserialize)]
13 | pub struct Range {
14 | pub min: Coord1d,
15 | pub max: Coord1d,
16 | pub metadata: Option,
17 | }
18 |
19 | #[derive(Debug, Clone, Copy, IntoValue, Serialize, Deserialize)]
20 | pub struct RangeMetadata {
21 | pub discrete_key_points: bool,
22 | }
23 |
24 | impl FromValue for Range {
25 | fn from_value(v: Value) -> Result {
26 | match v {
27 | Value::Range { val, internal_span } => {
28 | // TODO: try IntRange here first
29 | let range = FloatRange::from(*val);
30 | let min = range.start();
31 | let max = match range.end() {
32 | Bound::Included(max) => max,
33 | Bound::Excluded(max) => max,
34 | Bound::Unbounded => {
35 | return Err(ShellError::CantConvert {
36 | to_type: Self::expected_type().to_string(),
37 | from_type: Type::Range.to_string(),
38 | span: internal_span,
39 | help: Some("Try a bounded range instead.".to_string()),
40 | })
41 | }
42 | };
43 |
44 | let min = Coord1d::from_value(min.into_value(internal_span))?;
45 | let max = Coord1d::from_value(max.into_value(internal_span))?;
46 |
47 | Ok(Self {
48 | min,
49 | max,
50 | metadata: None,
51 | })
52 | }
53 |
54 | v @ Value::List { .. } => {
55 | let [min, max] = <[Coord1d; 2]>::from_value(v)?;
56 | Ok(Self {
57 | min,
58 | max,
59 | metadata: None,
60 | })
61 | }
62 |
63 | v @ Value::Record { .. } => {
64 | #[derive(Debug, FromValue)]
65 | struct RangeDTO {
66 | min: Coord1d,
67 | max: Coord1d,
68 | }
69 |
70 | let RangeDTO { min, max } = RangeDTO::from_value(v)?;
71 | Ok(Self {
72 | min,
73 | max,
74 | metadata: None,
75 | })
76 | }
77 |
78 | v => Err(ShellError::CantConvert {
79 | to_type: Self::expected_type().to_string(),
80 | from_type: v.get_type().to_string(),
81 | span: v.span(),
82 | help: None,
83 | }),
84 | }
85 | }
86 |
87 | fn expected_type() -> Type {
88 | Type::List(Box::new(Coord1d::expected_type()))
89 | }
90 | }
91 |
92 | impl From for RangedCoordf64 {
93 | fn from(value: Range) -> Self {
94 | RangedCoordf64::from(value.min.as_float()..value.max.as_float())
95 | }
96 | }
97 |
98 | impl Ranged for Range {
99 | type FormatOption = NoDefaultFormatting;
100 | type ValueType = Coord1d;
101 |
102 | fn map(&self, value: &Self::ValueType, limit: (i32, i32)) -> i32 {
103 | RangedCoordf64::from(*self).map(&value.as_float(), limit)
104 | }
105 |
106 | fn key_points(&self, hint: Hint) -> Vec {
107 | if let Some(RangeMetadata {
108 | discrete_key_points: true,
109 | }) = self.metadata
110 | {
111 | let lower = self.min.ceil();
112 | let upper = self.max.floor();
113 | return (lower..=upper).map(Coord1d::Int).collect();
114 | }
115 |
116 | match (self.min, self.max) {
117 | (Coord1d::Int(_), Coord1d::Int(_)) => RangedCoordi64::from(*self)
118 | .key_points(hint)
119 | .into_iter()
120 | .map(Coord1d::from_int)
121 | .collect(),
122 |
123 | (Coord1d::Int(_), Coord1d::Float(_)) |
124 | (Coord1d::Float(_), Coord1d::Int(_)) |
125 | (Coord1d::Float(_), Coord1d::Float(_)) => {
126 | // TODO: check here if we want f64 key points and if from_float may expects None
127 | // values
128 | RangedCoordf64::from(*self)
129 | .key_points(hint)
130 | .into_iter()
131 | .map(|float| Coord1d::from_float(float).unwrap())
132 | .collect()
133 | }
134 | }
135 | }
136 |
137 | fn range(&self) -> ops::Range {
138 | self.min..self.max
139 | }
140 | }
141 |
142 | impl From for RangedCoordi64 {
143 | fn from(value: Range) -> Self {
144 | RangedCoordi64::from(value.min.floor()..value.max.ceil())
145 | }
146 | }
147 |
148 | impl DiscreteRanged for Range {
149 | fn size(&self) -> usize {
150 | RangedCoordi64::from(*self).size()
151 | }
152 |
153 | fn index_of(&self, value: &Self::ValueType) -> Option {
154 | RangedCoordi64::from(*self).index_of(&value.round())
155 | }
156 |
157 | fn from_index(&self, index: usize) -> Option {
158 | RangedCoordi64::from(*self)
159 | .from_index(index)
160 | .map(Coord1d::from_int)
161 | }
162 | }
163 |
164 | impl ValueFormatter for Range {
165 | fn format(value: &Coord1d) -> String {
166 | match value {
167 | Coord1d::Int(value) => RangedCoordi64::format(value),
168 | Coord1d::Float(value) => RangedCoordf64::format(value),
169 | }
170 | }
171 |
172 | fn format_ext(&self, value: &Coord1d) -> String {
173 | Self::format(value)
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/mod.rs:
--------------------------------------------------------------------------------
1 | mod chart_2d;
2 | pub use chart_2d::*;
3 |
4 | mod color;
5 | pub use color::*;
6 |
7 | mod series_2d;
8 | pub use series_2d::*;
9 |
10 | mod coords;
11 | pub use coords::*;
12 |
--------------------------------------------------------------------------------
/crates/nu_plugin_plotters/src/value/series_2d.rs:
--------------------------------------------------------------------------------
1 | use std::any::Any;
2 | use std::{cmp, iter};
3 |
4 | use nu_protocol::{CustomValue, FromValue, IntoValue, Record, ShellError, Span, Type, Value};
5 | use serde::{Deserialize, Serialize};
6 |
7 | use super::color::Color;
8 | use super::{Coord1d, Coord2d, Range};
9 |
10 | #[derive(Debug, Clone, Serialize, Deserialize)]
11 | pub enum Series2d {
12 | Line(Line2dSeries),
13 | Bar(Bar2dSeries),
14 | }
15 |
16 | #[derive(Debug, Clone, Serialize, Deserialize, IntoValue)]
17 | pub struct Line2dSeries {
18 | pub series: Vec,
19 | pub color: Color,
20 | pub filled: bool,
21 | pub stroke_width: u32,
22 | pub point_size: u32,
23 | }
24 |
25 | #[derive(Debug, Clone, Serialize, Deserialize, IntoValue)]
26 | pub struct Bar2dSeries {
27 | pub series: Vec,
28 | pub color: Color,
29 | pub filled: bool,
30 | pub stroke_width: u32,
31 | }
32 |
33 | impl Series2d {
34 | fn into_base_value(self, span: Span) -> Value {
35 | let kind = match &self {
36 | Series2d::Line(_) => "line",
37 | Series2d::Bar(_) => "bar",
38 | };
39 | let kind = ("kind".to_string(), Value::string(kind, span));
40 |
41 | let record = match self {
42 | Series2d::Line(line) => line.into_value(span),
43 | Series2d::Bar(bar) => bar.into_value(span),
44 | };
45 | let record = record
46 | .into_record()
47 | .expect("structs derive IntoValue via Value::Record");
48 |
49 | let iter = iter::once(kind).chain(record);
50 | Value::record(Record::from_iter(iter), span)
51 | }
52 | }
53 |
54 | impl IntoValue for Series2d {
55 | fn into_value(self, span: Span) -> Value {
56 | Value::custom(Box::new(self), span)
57 | }
58 | }
59 |
60 | impl FromValue for Series2d {
61 | fn from_value(v: Value) -> Result {
62 | let span = v.span();
63 | let v = v.into_custom_value()?;
64 | match v.as_any().downcast_ref::() {
65 | Some(v) => Ok(v.clone()),
66 | None => Err(ShellError::CantConvert {
67 | to_type: Self::ty().to_string(),
68 | from_type: v.type_name(),
69 | span,
70 | help: None,
71 | }),
72 | }
73 | }
74 |
75 | fn expected_type() -> Type {
76 | Self::ty()
77 | }
78 | }
79 |
80 | #[typetag::serde]
81 | impl CustomValue for Series2d {
82 | fn clone_value(&self, span: Span) -> Value {
83 | Value::custom(Box::new(self.clone()), span)
84 | }
85 |
86 | fn type_name(&self) -> String {
87 | Self::ty().to_string()
88 | }
89 |
90 | fn to_base_value(&self, span: Span) -> Result {
91 | Ok(Series2d::into_base_value(self.clone(), span))
92 | }
93 |
94 | fn as_any(&self) -> &dyn Any {
95 | self
96 | }
97 |
98 | fn as_mut_any(&mut self) -> &mut dyn Any {
99 | self
100 | }
101 | }
102 |
103 | impl Series2d {
104 | pub fn series(&self) -> &[Coord2d] {
105 | match self {
106 | Series2d::Line(line) => &line.series,
107 | Series2d::Bar(bar) => &bar.series,
108 | }
109 | }
110 |
111 | // FIXME: maybe we need to rethink this range function
112 | fn range(&self, axis: A, map: M) -> Option
113 | where
114 | A: Fn(&Coord2d) -> Coord1d,
115 | M: Fn(Coord1d) -> (Coord1d, Coord1d),
116 | {
117 | let first = self.series().first()?;
118 | let (mut min, mut max) = (axis(first), axis(first));
119 | for (lower, upper) in self.series().iter().map(axis).map(map) {
120 | if lower < min {
121 | min = lower;
122 | }
123 |
124 | if upper > max {
125 | max = upper;
126 | }
127 | }
128 |
129 | Some(Range {
130 | min,
131 | max,
132 | metadata: None,
133 | })
134 | }
135 |
136 | pub fn x_range(&self) -> Option {
137 | self.range(
138 | |c| c.x,
139 | |c| match self {
140 | Series2d::Line(_) => (c, c),
141 | Series2d::Bar(_) => (c - Coord1d::Float(0.5), c + Coord1d::Float(0.5)),
142 | },
143 | )
144 | }
145 |
146 | pub fn y_range(&self) -> Option {
147 | self.range(
148 | |c| c.y,
149 | |c| match self {
150 | Series2d::Line(_) => (c, c),
151 | Series2d::Bar(_) => (cmp::min(Coord1d::Int(0), c), cmp::max(Coord1d::Int(0), c)),
152 | },
153 | )
154 | }
155 |
156 | pub fn ty() -> Type {
157 | Type::Custom("plotters::series-2d".to_string().into_boxed_str())
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/examples/example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "e372f727",
6 | "metadata": {},
7 | "source": [
8 | "This is running `nu` code."
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": 1,
14 | "id": "fb2910e8",
15 | "metadata": {},
16 | "outputs": [
17 | {
18 | "data": {
19 | "text/plain": [
20 | " __ ,\r\n",
21 | " .--()°'.' You're running `nu-jupyter-kernel`,\r\n",
22 | "'|, . ,' based on the `nu` language,\r\n",
23 | " !_-(_\\ where all data is structured!"
24 | ]
25 | },
26 | "metadata": {},
27 | "output_type": "display_data"
28 | },
29 | {
30 | "data": {
31 | "application/json": "Welcome to Nushell.",
32 | "text/html": [
33 | "Welcome to Nushell.
"
34 | ],
35 | "text/markdown": [
36 | "Welcome to Nushell.\n"
37 | ],
38 | "text/plain": [
39 | "Welcome to Nushell."
40 | ]
41 | },
42 | "execution_count": 1,
43 | "metadata": {},
44 | "output_type": "execute_result"
45 | }
46 | ],
47 | "source": [
48 | "open ../banner.txt | nuju print -f txt\n",
49 | "help | lines | first"
50 | ]
51 | }
52 | ],
53 | "metadata": {
54 | "kernelspec": {
55 | "display_name": "Nushell",
56 | "language": "nushell",
57 | "name": "nu"
58 | },
59 | "language_info": {
60 | "file_extension": ".nu",
61 | "mimetype": "text/x-nushell",
62 | "name": "nushell",
63 | "version": "0.98.0"
64 | }
65 | },
66 | "nbformat": 4,
67 | "nbformat_minor": 5
68 | }
69 |
--------------------------------------------------------------------------------
/examples/polars-data.nu:
--------------------------------------------------------------------------------
1 | let data_dir = ($env.FILE_PWD | path join data)
2 | mkdir $data_dir
3 | cd $data_dir
4 |
5 | print "Downloading zip..."
6 | http get "https://www.stats.govt.nz/assets/Uploads/New-Zealand-business-demography-statistics/New-Zealand-business-demography-statistics-At-February-2022/Download-data/CSV-data-load_data_metadata.zip"
7 | | save nz-stats.zip
8 |
9 | print "Extracting zip..."
10 | match $nu.os-info.name {
11 | "windows" => {tar -xf nz-stats.zip},
12 | _ => {unzip nz-stats.zip}
13 | }
14 |
15 | print "Done."
16 |
--------------------------------------------------------------------------------
/media/draw-terminal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cptpiepmatz/nu-jupyter-kernel/30a5cf4d3ea11b698697a6caf945f8caa60f3f26/media/draw-terminal.png
--------------------------------------------------------------------------------
/media/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cptpiepmatz/nu-jupyter-kernel/30a5cf4d3ea11b698697a6caf945f8caa60f3f26/media/screenshot.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "nu-jupyter-kernel-test"
3 | version = "0.0.0"
4 | description = "Tests for the nu-jupyter-kernel"
5 | requires-python = ">=3.12"
6 | dependencies = [
7 | "jupyter-client>=8.6.2",
8 | "pytest-timeout>=2.3.1",
9 | "pytest>=8.3.3",
10 | ]
11 |
12 | [tool.pytest.ini_options]
13 | testpaths = ["tests"]
14 | timeout = 30
15 | timeout_method = "thread"
16 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | unstable_features = true
2 | edition = "2021"
3 | binop_separator = "Back"
4 | control_brace_style = "ClosingNextLine"
5 | format_strings = true
6 | hex_literal_case = "Upper"
7 | imports_granularity = "Module"
8 | overflow_delimited_expr = true
9 | reorder_impl_items = true
10 | reorder_imports = true
11 | group_imports = "StdExternalCrate"
12 | use_field_init_shorthand = true
13 | wrap_comments = true
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug)]
2 | pub enum KernelError {
3 | MissingFormatDecls { missing: Vec<&'static str> },
4 | }
5 |
--------------------------------------------------------------------------------
/src/handlers/control.rs:
--------------------------------------------------------------------------------
1 | use std::ops::Deref;
2 | use std::sync::atomic::{AtomicBool, Ordering};
3 | use std::sync::Arc;
4 | use std::time::Duration;
5 |
6 | use tokio::sync::broadcast;
7 |
8 | use crate::jupyter::kernel_info::KernelInfo;
9 | use crate::jupyter::messages::control::{ControlReply, ControlReplyOk, ControlRequest};
10 | use crate::jupyter::messages::{Header, Message, Metadata};
11 | use crate::jupyter::Shutdown;
12 | use crate::ControlSocket;
13 |
14 | pub async fn handle(
15 | mut socket: ControlSocket,
16 | shutdown_sender: broadcast::Sender,
17 | interrupt_signal: Arc,
18 | ) {
19 | loop {
20 | let message = Message::::recv(&mut socket).await.unwrap();
21 | match &message.content {
22 | ControlRequest::KernelInfo => handle_kernel_info_request(&mut socket, &message).await,
23 | ControlRequest::Shutdown(shutdown) => {
24 | handle_shutdown_request(&mut socket, &message, *shutdown, &shutdown_sender).await;
25 | match shutdown.restart {
26 | true => continue,
27 | false => break,
28 | }
29 | }
30 | ControlRequest::Interrupt => {
31 | handle_interrupt_request(&mut socket, &message, interrupt_signal.deref()).await
32 | }
33 | ControlRequest::Debug => todo!(),
34 | }
35 | }
36 | }
37 |
38 | async fn handle_kernel_info_request(socket: &mut ControlSocket, message: &Message) {
39 | let kernel_info = KernelInfo::get();
40 | let reply = ControlReply::Ok(ControlReplyOk::KernelInfo(Box::new(kernel_info)));
41 | let msg_type = ControlReply::msg_type(&message.header.msg_type).unwrap();
42 | let reply = Message {
43 | zmq_identities: message.zmq_identities.clone(),
44 | header: Header::new(msg_type),
45 | parent_header: Some(message.header.clone()),
46 | metadata: Metadata::empty(),
47 | content: reply,
48 | buffers: vec![],
49 | };
50 | reply.into_multipart().unwrap().send(socket).await.unwrap();
51 | }
52 |
53 | async fn handle_shutdown_request(
54 | socket: &mut ControlSocket,
55 | message: &Message,
56 | shutdown: Shutdown,
57 | sender: &broadcast::Sender,
58 | ) {
59 | // according to docs, we first shut our kernel and then reply to the client
60 | sender.send(shutdown).unwrap();
61 |
62 | // TODO: check if application terminated, maybe with broadcast that ensures that
63 | // every subscriber received something
64 | let reply = ControlReply::Ok(ControlReplyOk::Shutdown(shutdown));
65 | let msg_type = ControlReply::msg_type(&message.header.msg_type).unwrap();
66 | let reply = Message {
67 | zmq_identities: message.zmq_identities.clone(),
68 | header: Header::new(msg_type),
69 | parent_header: Some(message.header.clone()),
70 | metadata: Metadata::empty(),
71 | content: reply,
72 | buffers: vec![],
73 | };
74 | reply.into_multipart().unwrap().send(socket).await.unwrap();
75 | }
76 |
77 | async fn handle_interrupt_request(
78 | socket: &mut ControlSocket,
79 | message: &Message,
80 | interrupt_signal: &AtomicBool,
81 | ) {
82 | interrupt_signal.store(true, Ordering::Relaxed);
83 |
84 | while interrupt_signal.load(Ordering::Relaxed) {
85 | // poll the interrupt signal to check when the engine is successfully
86 | // interrupted
87 | tokio::time::sleep(Duration::from_millis(100)).await;
88 | }
89 |
90 | let reply = ControlReply::Ok(ControlReplyOk::Interrupt);
91 | let msg_type = ControlReply::msg_type(&message.header.msg_type).unwrap();
92 | let reply = Message {
93 | zmq_identities: message.zmq_identities.clone(),
94 | header: Header::new(msg_type),
95 | parent_header: Some(message.header.clone()),
96 | metadata: Metadata::empty(),
97 | content: reply,
98 | buffers: vec![],
99 | };
100 | reply.into_multipart().unwrap().send(socket).await.unwrap();
101 | }
102 |
--------------------------------------------------------------------------------
/src/handlers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod iopub {
2 | use tokio::sync::{broadcast, mpsc};
3 |
4 | use crate::jupyter::messages::multipart::Multipart;
5 | use crate::jupyter::Shutdown;
6 | use crate::util::Select;
7 | use crate::IopubSocket;
8 |
9 | pub async fn handle(
10 | mut socket: IopubSocket,
11 | mut shutdown: broadcast::Receiver,
12 | mut iopub_rx: mpsc::Receiver,
13 | ) {
14 | loop {
15 | let next = tokio::select! {
16 | biased;
17 | v = shutdown.recv() => Select::Left(v),
18 | v = iopub_rx.recv() => Select::Right(v.unwrap()),
19 | };
20 |
21 | let multipart = match next {
22 | Select::Left(Ok(Shutdown { restart: false })) => break,
23 | Select::Left(Ok(Shutdown { restart: true })) => continue,
24 | Select::Left(Err(_)) => break,
25 | Select::Right(multipart) => multipart,
26 | };
27 | multipart.send(&mut socket).await.unwrap();
28 | }
29 | }
30 | }
31 |
32 | pub mod heartbeat {
33 | use tokio::sync::broadcast;
34 | use zeromq::{SocketRecv, SocketSend};
35 |
36 | use crate::jupyter::Shutdown;
37 | use crate::util::Select;
38 | use crate::HeartbeatSocket;
39 |
40 | pub async fn handle(mut socket: HeartbeatSocket, mut shutdown: broadcast::Receiver) {
41 | loop {
42 | let next = tokio::select! {
43 | biased;
44 | v = shutdown.recv() => Select::Left(v),
45 | v = socket.recv() => Select::Right(v.unwrap()),
46 | };
47 |
48 | let msg = match next {
49 | Select::Left(Ok(Shutdown { restart: false })) => break,
50 | Select::Left(Ok(Shutdown { restart: true })) => continue,
51 | Select::Left(Err(_)) => break,
52 | Select::Right(msg) => msg,
53 | };
54 | socket.send(msg).await.unwrap();
55 | }
56 | }
57 | }
58 |
59 | pub mod control;
60 | pub mod shell;
61 | pub mod stream;
62 |
--------------------------------------------------------------------------------
/src/handlers/shell.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use mime::Mime;
4 | use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
5 | use nu_protocol::PipelineData;
6 | use parking_lot::Mutex;
7 | use serde_json::json;
8 | use tokio::sync::{broadcast, mpsc};
9 |
10 | use super::stream::StreamHandler;
11 | use crate::jupyter::kernel_info::KernelInfo;
12 | use crate::jupyter::messages::iopub::{self, ExecuteResult, IopubBroacast, Status};
13 | use crate::jupyter::messages::multipart::Multipart;
14 | use crate::jupyter::messages::shell::{
15 | ExecuteReply, ExecuteRequest, IsCompleteReply, IsCompleteRequest, ShellReply, ShellReplyOk,
16 | ShellRequest,
17 | };
18 | use crate::jupyter::messages::{Header, Message, Metadata};
19 | use crate::jupyter::Shutdown;
20 | use crate::nu::commands::external::External;
21 | use crate::nu::konst::Konst;
22 | use crate::nu::module::KernelInternalSpans;
23 | use crate::nu::render::{FormatDeclIds, PipelineRender, StringifiedPipelineRender};
24 | use crate::nu::{self, ExecuteError, ReportExecuteError};
25 | use crate::util::Select;
26 | use crate::ShellSocket;
27 |
28 | // TODO: get rid of this static by passing this into the display command
29 | pub static RENDER_FILTER: Mutex> = Mutex::new(Option::None);
30 |
31 | pub struct HandlerContext {
32 | pub socket: ShellSocket,
33 | pub iopub: mpsc::Sender,
34 | pub stdout_handler: StreamHandler,
35 | pub stderr_handler: StreamHandler,
36 | pub engine_state: EngineState,
37 | pub format_decl_ids: FormatDeclIds,
38 | pub konst: Konst,
39 | pub spans: KernelInternalSpans,
40 | pub stack: Stack,
41 | pub cell: Cell,
42 | }
43 |
44 | pub async fn handle(mut ctx: HandlerContext, mut shutdown: broadcast::Receiver) {
45 | let initial_engine_state = ctx.engine_state.clone();
46 | let initial_stack = ctx.stack.clone();
47 |
48 | loop {
49 | let next = tokio::select! {
50 | biased;
51 | v = shutdown.recv() => Select::Left(v),
52 | v = Message::::recv(&mut ctx.socket) => Select::Right(v),
53 | };
54 |
55 | let message = match next {
56 | Select::Left(Ok(Shutdown { restart: false })) => break,
57 | Select::Left(Ok(Shutdown { restart: true })) => {
58 | ctx.engine_state = initial_engine_state.clone();
59 | ctx.stack = initial_stack.clone();
60 | // TODO: check if cell counter should get a reset too
61 | continue;
62 | }
63 | Select::Left(Err(_)) => break,
64 | Select::Right(Ok(msg)) => msg,
65 | Select::Right(Err(_)) => {
66 | eprintln!("could not recv message");
67 | continue;
68 | }
69 | };
70 |
71 | send_status(&mut ctx, &message, Status::Busy).await;
72 |
73 | match &message.content {
74 | ShellRequest::KernelInfo => handle_kernel_info_request(&mut ctx, &message).await,
75 | ShellRequest::Execute(request) => {
76 | // take the context out temporarily to allow execution on another thread
77 | ctx = handle_execute_request(ctx, &message, request).await;
78 | }
79 | ShellRequest::IsComplete(request) => {
80 | handle_is_complete_request(&mut ctx, &message, request).await
81 | }
82 | }
83 |
84 | send_status(&mut ctx, &message, Status::Idle).await;
85 | }
86 | }
87 |
88 | async fn send_status(ctx: &mut HandlerContext, message: &Message, status: Status) {
89 | ctx.iopub
90 | .send(
91 | status
92 | .into_message(message.header.clone())
93 | .into_multipart()
94 | .unwrap(),
95 | )
96 | .await
97 | .unwrap();
98 | }
99 |
100 | /// Representation of cell execution in Jupyter.
101 | ///
102 | /// Used to keep track of the execution counter and retry attempts for the same
103 | /// cell.
104 | pub struct Cell {
105 | execution_counter: usize,
106 | retry_counter: usize,
107 | }
108 |
109 | impl Cell {
110 | /// Construct a new Cell.
111 | pub const fn new() -> Self {
112 | Cell {
113 | execution_counter: 1,
114 | retry_counter: 1,
115 | }
116 | }
117 |
118 | /// Generate a name for the next retry of the current cell.
119 | ///
120 | /// This method increases the retry counter each time it is called,
121 | /// indicating a new attempt on the same cell.
122 | pub fn next_name(&mut self) -> String {
123 | let name = format!("cell[{}]#{}", self.execution_counter, self.retry_counter);
124 | self.retry_counter += 1;
125 | name
126 | }
127 |
128 | /// Increment the execution counter after a successful execution.
129 | ///
130 | /// Jupyter demands that the execution counter only increases after a
131 | /// successful execution. This function increments the counter and resets
132 | /// the retry counter, indicating a new cell execution. It returns the
133 | /// previous execution counter.
134 | pub fn success(&mut self) -> usize {
135 | let current_execution_counter = self.execution_counter;
136 | self.execution_counter += 1;
137 | self.retry_counter = 1;
138 | current_execution_counter
139 | }
140 | }
141 |
142 | async fn handle_kernel_info_request(ctx: &mut HandlerContext, message: &Message) {
143 | let kernel_info = KernelInfo::get();
144 | let reply = ShellReply::Ok(ShellReplyOk::KernelInfo(kernel_info));
145 | let msg_type = ShellReply::msg_type(&message.header.msg_type).unwrap();
146 | let reply = Message {
147 | zmq_identities: message.zmq_identities.clone(),
148 | header: Header::new(msg_type),
149 | parent_header: Some(message.header.clone()),
150 | metadata: Metadata::empty(),
151 | content: reply,
152 | buffers: vec![],
153 | };
154 | reply
155 | .into_multipart()
156 | .unwrap()
157 | .send(&mut ctx.socket)
158 | .await
159 | .unwrap();
160 | }
161 |
162 | async fn handle_execute_request(
163 | mut ctx: HandlerContext,
164 | message: &Message,
165 | request: &ExecuteRequest,
166 | ) -> HandlerContext {
167 | let ExecuteRequest {
168 | code,
169 | silent,
170 | store_history,
171 | user_expressions,
172 | allow_stdin,
173 | stop_on_error,
174 | } = request;
175 | let msg_type = ShellReply::msg_type(&message.header.msg_type).unwrap();
176 | External::apply(&mut ctx.engine_state).unwrap();
177 |
178 | let cell_name = ctx.cell.next_name();
179 | ctx.konst
180 | .update(&mut ctx.stack, cell_name.clone(), message.clone());
181 | ctx.stdout_handler
182 | .update_reply(message.zmq_identities.clone(), message.header.clone());
183 | ctx.stderr_handler
184 | .update_reply(message.zmq_identities.clone(), message.header.clone());
185 |
186 | // TODO: place coll in cell, then just pass the cell
187 | let code = code.to_owned();
188 | let (executed, mut ctx) = tokio::task::spawn_blocking(move || {
189 | (
190 | nu::execute(&code, &mut ctx.engine_state, &mut ctx.stack, &cell_name),
191 | ctx,
192 | )
193 | })
194 | .await
195 | .unwrap();
196 | match executed {
197 | Ok(data) => handle_execute_results(&mut ctx, message, msg_type, data).await,
198 | Err(error) => handle_execute_error(&mut ctx, message, msg_type, error).await,
199 | };
200 |
201 | // reset interrupt signal after every execution, this also notifies the control
202 | // handler
203 | ctx.engine_state.reset_signals();
204 |
205 | ctx
206 | }
207 |
208 | async fn handle_execute_error(
209 | ctx: &mut HandlerContext,
210 | message: &Message,
211 | msg_type: &str,
212 | error: ExecuteError,
213 | ) {
214 | let mut working_set = StateWorkingSet::new(&ctx.engine_state);
215 | let (name, value) = {
216 | // keeping the report makes the following part not Send
217 | let report = ReportExecuteError::new(error, &mut working_set);
218 | let name = report.code().to_string();
219 | let value = report.fmt();
220 | (name, value)
221 | };
222 | // TODO: for traceback use error source
223 | let traceback = vec![];
224 |
225 | // we send display data to have control over the rendering of the output
226 | let broadcast = IopubBroacast::DisplayData(iopub::DisplayData {
227 | data: HashMap::from([(mime::TEXT_PLAIN.to_string(), value.clone())]),
228 | metadata: HashMap::new(),
229 | transient: HashMap::new(),
230 | });
231 | let broadcast = Message {
232 | zmq_identities: message.zmq_identities.clone(),
233 | header: Header::new(broadcast.msg_type()),
234 | parent_header: Some(message.header.clone()),
235 | metadata: Metadata::empty(),
236 | content: broadcast,
237 | buffers: vec![],
238 | };
239 | ctx.iopub
240 | .send(broadcast.into_multipart().unwrap())
241 | .await
242 | .unwrap();
243 |
244 | let reply = ShellReply::Error {
245 | name,
246 | value,
247 | traceback,
248 | };
249 | let reply = Message {
250 | zmq_identities: message.zmq_identities.clone(),
251 | header: Header::new(msg_type),
252 | parent_header: Some(message.header.clone()),
253 | metadata: Metadata::empty(),
254 | content: reply,
255 | buffers: vec![],
256 | };
257 | reply
258 | .into_multipart()
259 | .unwrap()
260 | .send(&mut ctx.socket)
261 | .await
262 | .unwrap();
263 | }
264 |
265 | async fn handle_execute_results(
266 | ctx: &mut HandlerContext,
267 | message: &Message,
268 | msg_type: &str,
269 | pipeline_data: PipelineData,
270 | ) {
271 | let execution_count = ctx.cell.success();
272 |
273 | if !pipeline_data.is_nothing() {
274 | let render: StringifiedPipelineRender = {
275 | // render filter needs to be dropped until next async yield
276 | let mut render_filter = RENDER_FILTER.lock();
277 | PipelineRender::render(
278 | pipeline_data,
279 | &ctx.engine_state,
280 | &mut ctx.stack,
281 | &ctx.spans,
282 | ctx.format_decl_ids,
283 | render_filter.take(),
284 | )
285 | .unwrap() // TODO: replace this with some actual handling
286 | .into()
287 | };
288 |
289 | let execute_result = ExecuteResult {
290 | execution_count,
291 | data: render.data,
292 | metadata: render.metadata,
293 | };
294 | let broadcast = IopubBroacast::from(execute_result);
295 | let broadcast = Message {
296 | zmq_identities: message.zmq_identities.clone(),
297 | header: Header::new(broadcast.msg_type()),
298 | parent_header: Some(message.header.clone()),
299 | metadata: Metadata::empty(),
300 | content: broadcast,
301 | buffers: vec![],
302 | };
303 | ctx.iopub
304 | .send(broadcast.into_multipart().unwrap())
305 | .await
306 | .unwrap();
307 | }
308 |
309 | let reply = ExecuteReply {
310 | execution_count,
311 | user_expressions: json!({}),
312 | };
313 | let reply = ShellReply::Ok(ShellReplyOk::Execute(reply));
314 | let reply = Message {
315 | zmq_identities: message.zmq_identities.clone(),
316 | header: Header::new(msg_type),
317 | parent_header: Some(message.header.clone()),
318 | metadata: Metadata::empty(),
319 | content: reply,
320 | buffers: vec![],
321 | };
322 | reply
323 | .into_multipart()
324 | .unwrap()
325 | .send(&mut ctx.socket)
326 | .await
327 | .unwrap();
328 | }
329 |
330 | async fn handle_is_complete_request(
331 | ctx: &mut HandlerContext,
332 | message: &Message,
333 | request: &IsCompleteRequest,
334 | ) {
335 | let reply = IsCompleteReply::Unknown;
336 | let reply = ShellReply::Ok(ShellReplyOk::IsComplete(reply));
337 | let msg_type = ShellReply::msg_type(&message.header.msg_type).unwrap();
338 | let reply = Message {
339 | zmq_identities: message.zmq_identities.clone(),
340 | header: Header::new(msg_type),
341 | parent_header: Some(message.header.clone()),
342 | metadata: Metadata::empty(),
343 | content: reply,
344 | buffers: vec![],
345 | };
346 | reply
347 | .into_multipart()
348 | .unwrap()
349 | .send(&mut ctx.socket)
350 | .await
351 | .unwrap();
352 | }
353 |
--------------------------------------------------------------------------------
/src/handlers/stream.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::{self, Read};
3 | use std::os;
4 | use std::sync::Arc;
5 | use std::thread::{self};
6 |
7 | use bytes::Bytes;
8 | use parking_lot::Mutex;
9 | use tokio::sync::mpsc;
10 |
11 | use crate::jupyter::messages::iopub::{self, IopubBroacast};
12 | use crate::jupyter::messages::multipart::Multipart;
13 | use crate::jupyter::messages::{Header, Message, Metadata};
14 |
15 | const BUFFER_SIZE: usize = 8 * 1024;
16 |
17 | pub struct StreamHandler {
18 | message_data: Arc, Option)>>,
19 | stream_name: iopub::StreamName, // iopub_tx: Sender, // moved into the thread
20 | }
21 |
22 | impl StreamHandler {
23 | pub fn start(
24 | stream_name: iopub::StreamName,
25 | iopub_tx: mpsc::Sender,
26 | ) -> io::Result<(Self, File)> {
27 | // TODO: construct a Self, create a pipe, start a reader thread, return the
28 | // writer as a file
29 | let message_data = Arc::new(Mutex::new((vec![], None)));
30 |
31 | let (mut pipe_reader, pipe_writer) = os_pipe::pipe()?;
32 | let t_message_data = message_data.clone();
33 | thread::Builder::new()
34 | .name(format!("{} reader", stream_name.as_ref()))
35 | .spawn(move || {
36 | let mut read_buf = [0u8; BUFFER_SIZE];
37 | loop {
38 | let mut s_buf: Vec = Vec::new();
39 | loop {
40 | match pipe_reader.read(&mut read_buf) {
41 | Err(err) => todo!("handle that error"),
42 | Ok(0) if s_buf.is_empty() => todo!("stream is dead"),
43 | Ok(0) => break,
44 | Ok(BUFFER_SIZE) => s_buf.extend_from_slice(&read_buf),
45 | Ok(n) => {
46 | s_buf.extend_from_slice(&read_buf[..n]);
47 | break;
48 | }
49 | }
50 | }
51 | // TODO: handle this somehow
52 | let s = String::from_utf8(s_buf).unwrap();
53 | let broadcast = IopubBroacast::Stream(iopub::Stream {
54 | name: stream_name,
55 | text: s,
56 | });
57 | let (zmq_identities, parent_header) = t_message_data.lock().clone();
58 | let message = Message {
59 | zmq_identities,
60 | header: Header::new(broadcast.msg_type()),
61 | parent_header,
62 | metadata: Metadata::empty(),
63 | content: broadcast,
64 | buffers: vec![],
65 | };
66 | // TODO: handle this better, also does this need to be blocking?
67 | iopub_tx
68 | .blocking_send(message.into_multipart().unwrap())
69 | .unwrap();
70 | }
71 | })?;
72 |
73 | #[cfg(windows)]
74 | let file: File = os::windows::io::OwnedHandle::from(pipe_writer).into();
75 | #[cfg(unix)]
76 | let file: File = os::unix::io::OwnedFd::from(pipe_writer).into();
77 |
78 | Ok((
79 | Self {
80 | message_data,
81 | stream_name,
82 | },
83 | file,
84 | ))
85 | }
86 |
87 | pub fn update_reply(&mut self, zmq_identities: Vec, parent_header: Header) {
88 | let mut message_data = self.message_data.lock();
89 | message_data.0 = zmq_identities;
90 | message_data.1 = Some(parent_header);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/jupyter/connection_file.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Display;
2 | use std::net::Ipv4Addr;
3 | use std::path::Path;
4 | use std::{fs, io};
5 |
6 | use serde::{Deserialize, Deserializer};
7 | use thiserror::Error;
8 |
9 | #[derive(Debug, Deserialize)]
10 | pub struct ConnectionFile {
11 | pub control_port: PortAddr,
12 | pub shell_port: PortAddr,
13 | pub transport: Transport,
14 | pub signature_scheme: SignatureScheme,
15 | pub stdin_port: PortAddr,
16 | #[serde(alias = "hb_port")]
17 | pub heartbeat_port: PortAddr,
18 | pub ip: Ipv4Addr,
19 | pub iopub_port: PortAddr,
20 | #[serde(deserialize_with = "deserialize_key")]
21 | pub key: Vec,
22 | }
23 |
24 | #[derive(Debug, Error)]
25 | pub enum ReadConnectionFileError {
26 | #[error("could not read connection file")]
27 | ReadFile(#[from] io::Error),
28 | #[error("could not parse connection file")]
29 | Parse(#[from] serde_json::Error),
30 | }
31 |
32 | impl ConnectionFile {
33 | pub fn from_path(path: impl AsRef) -> Result {
34 | let contents = fs::read_to_string(path)?;
35 | let connection_file: ConnectionFile = serde_json::from_str(&contents)?;
36 | Ok(connection_file)
37 | }
38 | }
39 |
40 | #[derive(Debug, Deserialize)]
41 | pub struct PortAddr(u16);
42 |
43 | impl Display for PortAddr {
44 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 | write!(f, "{}", self.0)
46 | }
47 | }
48 |
49 | #[derive(Debug)]
50 | pub struct SignatureScheme {
51 | pub algorithm: SupportedSignatureAlgorithm,
52 | pub hash_fn: SupportedSignatureHashFunction,
53 | }
54 |
55 | impl<'de> Deserialize<'de> for SignatureScheme {
56 | fn deserialize(deserializer: D) -> Result
57 | where
58 | D: serde::Deserializer<'de>,
59 | {
60 | let as_str = String::deserialize(deserializer)?;
61 | let mut split = as_str.split('-');
62 | let algorithm = split
63 | .next()
64 | .ok_or_else(|| serde::de::Error::missing_field("algorithm"))?;
65 | let hash_fn = split
66 | .next()
67 | .ok_or_else(|| serde::de::Error::missing_field("hash_fn"))?;
68 |
69 | let algorithm = match algorithm {
70 | "hmac" => SupportedSignatureAlgorithm::Hmac,
71 | other => return Err(serde::de::Error::unknown_variant(other, &["hmac"])),
72 | };
73 |
74 | let hash_fn = match hash_fn {
75 | "sha256" => SupportedSignatureHashFunction::Sha256,
76 | other => return Err(serde::de::Error::unknown_variant(other, &["sha256"])),
77 | };
78 |
79 | Ok(SignatureScheme { algorithm, hash_fn })
80 | }
81 | }
82 |
83 | #[non_exhaustive]
84 | #[derive(Debug, Clone, Copy)]
85 | pub enum SupportedSignatureAlgorithm {
86 | Hmac,
87 | }
88 |
89 | #[non_exhaustive]
90 | #[derive(Debug, Clone, Copy)]
91 | pub enum SupportedSignatureHashFunction {
92 | Sha256,
93 | }
94 |
95 | #[derive(Debug, Clone, Copy, Deserialize)]
96 | #[serde(rename_all = "snake_case")]
97 | pub enum Transport {
98 | Tcp,
99 | }
100 |
101 | impl Display for Transport {
102 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 | match self {
104 | Transport::Tcp => write!(f, "tcp"),
105 | }
106 | }
107 | }
108 |
109 | fn deserialize_key<'de, D>(deserializer: D) -> Result, D::Error>
110 | where
111 | D: Deserializer<'de>,
112 | {
113 | let as_str = String::deserialize(deserializer)?;
114 | Ok(as_str.into_bytes())
115 | }
116 |
--------------------------------------------------------------------------------
/src/jupyter/kernel_info.rs:
--------------------------------------------------------------------------------
1 | use serde::Serialize;
2 |
3 | use crate::CARGO_TOML;
4 |
5 | #[derive(Debug, Serialize, Clone)]
6 | pub struct KernelInfo {
7 | pub protocol_version: String,
8 | pub implementation: String,
9 | pub implementation_version: String,
10 | pub language_info: LanguageInfo,
11 | pub banner: String,
12 | pub debugger: bool,
13 | pub help_links: Vec,
14 | }
15 |
16 | #[derive(Debug, Serialize, Clone)]
17 | pub struct LanguageInfo {
18 | pub name: String,
19 | pub version: String,
20 | pub mimetype: String,
21 | pub file_extension: String,
22 | }
23 |
24 | #[derive(Debug, Serialize, Clone)]
25 | pub struct HelpLink {
26 | pub text: String,
27 | pub url: String,
28 | }
29 |
30 | impl From<(T, U)> for HelpLink
31 | where
32 | T: Into,
33 | U: Into,
34 | {
35 | fn from(value: (T, U)) -> Self {
36 | HelpLink {
37 | text: value.0.into(),
38 | url: value.1.into(),
39 | }
40 | }
41 | }
42 |
43 | impl KernelInfo {
44 | pub fn get() -> Self {
45 | KernelInfo {
46 | protocol_version: CARGO_TOML
47 | .package
48 | .metadata
49 | .jupyter
50 | .protocol_version
51 | .to_owned(),
52 | implementation: CARGO_TOML.package.name.to_owned(),
53 | implementation_version: CARGO_TOML.package.version.to_owned(),
54 | language_info: LanguageInfo {
55 | name: "nushell".to_owned(),
56 | version: CARGO_TOML.dependencies.nu_engine.version.to_owned(),
57 | mimetype: "text/x-nushell".to_owned(),
58 | file_extension: ".nu".to_owned(),
59 | },
60 | banner: include_str!("../../banner.txt").to_owned(),
61 | debugger: false,
62 | help_links: [
63 | ("Discord", "https://discord.gg/NtAbbGn"),
64 | ("GitHub", "https://github.com/nushell/nushell"),
65 | ]
66 | .into_iter()
67 | .map(|pair| pair.into())
68 | .collect(),
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/jupyter/messages/control.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | use crate::jupyter::kernel_info::KernelInfo;
4 | use crate::jupyter::Shutdown;
5 |
6 | #[derive(Debug, Deserialize, Clone)]
7 | pub enum ControlRequest {
8 | KernelInfo,
9 | Shutdown(Shutdown),
10 | Interrupt,
11 | Debug, // TODO: check if this is reasonable
12 | }
13 |
14 | impl ControlRequest {
15 | pub fn parse_variant(variant: &str, body: &str) -> Result {
16 | match variant {
17 | "kernel_info_request" => Ok(Self::KernelInfo),
18 | "shutdown_request" => Ok(Self::Shutdown(serde_json::from_str(body).unwrap())),
19 | "interrupt_request" => Ok(Self::Interrupt),
20 | "debug_request" => todo!(),
21 | _ => {
22 | eprintln!("found it here: {variant}");
23 |
24 | eprintln!("unknown request {variant}");
25 | Err(())
26 | }
27 | }
28 | }
29 | }
30 |
31 | #[derive(Debug, Serialize, Clone)]
32 | #[serde(rename_all = "snake_case", tag = "status")]
33 | pub enum ControlReply {
34 | Ok(ControlReplyOk),
35 | Error {
36 | #[serde(rename = "ename")]
37 | name: String,
38 | #[serde(rename = "evalue")]
39 | value: String,
40 | traceback: Vec,
41 | },
42 | }
43 |
44 | impl ControlReply {
45 | pub fn msg_type(request_type: &'_ str) -> Result<&'static str, ()> {
46 | Ok(match request_type {
47 | "kernel_info_request" => "kernel_info_reply",
48 | "shutdown_request" => "shutdown_reply",
49 | "interrupt_request" => "interrupt_reply",
50 | "debug_request" => "debug_reply",
51 | _ => todo!("handle unknown requests"),
52 | })
53 | }
54 | }
55 |
56 | #[derive(Debug, Serialize, Clone)]
57 | #[serde(untagged)]
58 | pub enum ControlReplyOk {
59 | KernelInfo(Box),
60 | Shutdown(Shutdown),
61 | Interrupt,
62 | Debug,
63 | }
64 |
--------------------------------------------------------------------------------
/src/jupyter/messages/iopub.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use derive_more::From;
4 | use serde::Serialize;
5 | use strum::AsRefStr;
6 |
7 | use super::Header;
8 | use crate::jupyter::messages::{Message, Metadata};
9 |
10 | #[derive(Debug, Serialize, From, Clone)]
11 | #[serde(untagged)]
12 | pub enum IopubBroacast {
13 | Stream(Stream),
14 | DisplayData(DisplayData),
15 | UpdateDisplayData,
16 | ExecuteInput,
17 | ExecuteResult(ExecuteResult),
18 | Error(Error),
19 | Status(Status),
20 | ClearOutput,
21 | DebugEvent,
22 | }
23 |
24 | impl IopubBroacast {
25 | pub fn msg_type(&self) -> &'static str {
26 | match self {
27 | IopubBroacast::Stream(_) => "stream",
28 | IopubBroacast::DisplayData(_) => "display_data",
29 | IopubBroacast::UpdateDisplayData => "update_display_data",
30 | IopubBroacast::ExecuteInput => "execute_input",
31 | IopubBroacast::ExecuteResult(_) => "execute_result",
32 | IopubBroacast::Error(_) => "error",
33 | IopubBroacast::Status(_) => "status",
34 | IopubBroacast::ClearOutput => "clear_output",
35 | IopubBroacast::DebugEvent => "debug_event",
36 | }
37 | }
38 | }
39 |
40 | #[derive(Debug, Serialize, Clone, Copy, AsRefStr)]
41 | #[serde(rename_all = "snake_case")]
42 | pub enum StreamName {
43 | Stdout,
44 | Stderr,
45 | }
46 |
47 | #[derive(Debug, Serialize, Clone)]
48 | pub struct Stream {
49 | pub name: StreamName,
50 | pub text: String,
51 | }
52 |
53 | #[derive(Debug, Serialize, Clone)]
54 | pub struct DisplayData {
55 | pub data: HashMap,
56 | pub metadata: HashMap,
57 | pub transient: HashMap,
58 | }
59 |
60 | #[derive(Debug, Serialize, Clone)]
61 | pub struct ExecuteResult {
62 | pub execution_count: usize,
63 | pub data: HashMap,
64 | pub metadata: HashMap,
65 | }
66 |
67 | #[derive(Debug, Serialize, Clone)]
68 | pub struct Error {
69 | #[serde(rename = "ename")]
70 | pub name: String,
71 |
72 | #[serde(rename = "evalue")]
73 | pub value: String,
74 |
75 | pub traceback: Vec,
76 | }
77 |
78 | #[derive(Debug, Serialize, Clone, Copy)]
79 | #[serde(tag = "execution_state", rename_all = "snake_case")]
80 | pub enum Status {
81 | Busy,
82 | Idle,
83 | }
84 |
85 | impl Status {
86 | pub fn into_message(self, parent_header: impl Into>) -> Message {
87 | let broadcast = IopubBroacast::Status(self);
88 | let msg_type = broadcast.msg_type();
89 | Message {
90 | zmq_identities: vec![],
91 | header: Header::new(msg_type),
92 | parent_header: parent_header.into(),
93 | metadata: Metadata::empty(),
94 | content: broadcast,
95 | buffers: vec![],
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/jupyter/messages/mod.rs:
--------------------------------------------------------------------------------
1 | use std::ops::Deref;
2 | use std::sync::atomic::{AtomicUsize, Ordering};
3 | use std::sync::OnceLock;
4 |
5 | use bytes::Bytes;
6 | use chrono::Utc;
7 | use control::ControlRequest;
8 | use derive_more::From;
9 | use hmac::{Hmac, Mac};
10 | use nu_protocol::{FromValue, IntoValue};
11 | use serde::{Deserialize, Serialize};
12 | use serde_json::json;
13 | use sha2::digest::InvalidLength;
14 | use sha2::Sha256;
15 | use uuid::Uuid;
16 | use zeromq::SocketRecv;
17 |
18 | use self::shell::ShellRequest;
19 | use crate::{Channel, CARGO_TOML};
20 |
21 | pub mod control;
22 | pub mod iopub;
23 | pub mod multipart;
24 | pub mod shell;
25 |
26 | pub static KERNEL_SESSION: KernelSession = KernelSession::new();
27 | pub static MESSAGE_COUNTER: AtomicUsize = AtomicUsize::new(0);
28 | pub static DIGESTER: Digester = Digester::new();
29 |
30 | pub struct KernelSession(OnceLock);
31 |
32 | impl KernelSession {
33 | pub const fn new() -> Self {
34 | KernelSession(OnceLock::new())
35 | }
36 |
37 | pub fn get(&self) -> &str {
38 | self.0.get_or_init(|| Uuid::new_v4().to_string())
39 | }
40 | }
41 |
42 | pub struct Digester(OnceLock>);
43 |
44 | impl Digester {
45 | pub const fn new() -> Self {
46 | Digester(OnceLock::new())
47 | }
48 |
49 | pub fn key_init(&self, key: &[u8]) -> Result<(), InvalidLength> {
50 | self.0.set(Hmac::new_from_slice(key)?).expect("already set");
51 | Ok(())
52 | }
53 |
54 | pub fn get(&self) -> &Hmac {
55 | match self.0.get() {
56 | None => panic!("hmac not initialized"),
57 | Some(hmac) => hmac,
58 | }
59 | }
60 | }
61 |
62 | #[derive(Debug, Deserialize, Serialize, Clone, IntoValue, FromValue)]
63 | pub struct Header {
64 | pub msg_id: String,
65 | pub session: String,
66 | pub username: String,
67 | pub date: String,
68 | pub msg_type: String, // TODO: make this an enum
69 | pub version: String,
70 | }
71 |
72 | impl Header {
73 | pub fn new(msg_type: impl Into) -> Self {
74 | let session = KERNEL_SESSION.get();
75 | let msg_counter = MESSAGE_COUNTER.fetch_add(1, Ordering::SeqCst);
76 |
77 | Header {
78 | msg_id: format!("{session}:{msg_counter}"),
79 | session: session.to_owned(),
80 | username: "nu_kernel".to_owned(),
81 | date: Utc::now().to_rfc3339(),
82 | msg_type: msg_type.into(),
83 | version: CARGO_TOML
84 | .package
85 | .metadata
86 | .jupyter
87 | .protocol_version
88 | .to_owned(),
89 | }
90 | }
91 | }
92 |
93 | #[derive(Debug, Deserialize, Serialize, Clone)]
94 | pub struct Metadata(serde_json::Value);
95 |
96 | impl Metadata {
97 | pub fn empty() -> Self {
98 | Metadata(json!({}))
99 | }
100 | }
101 |
102 | #[derive(Debug, Deserialize, Clone)]
103 | pub enum IncomingContent {
104 | Shell(shell::ShellRequest),
105 | Control(control::ControlRequest),
106 | }
107 |
108 | #[derive(Debug, Serialize, From, Clone)]
109 | pub enum OutgoingContent {
110 | Shell(shell::ShellReply),
111 | Iopub(iopub::IopubBroacast),
112 | Control(control::ControlReply),
113 | }
114 |
115 | #[derive(Debug, Clone)]
116 | pub struct Message {
117 | pub zmq_identities: Vec,
118 | pub header: Header,
119 | pub parent_header: Option,
120 | pub metadata: Metadata,
121 | pub content: C,
122 | pub buffers: Vec,
123 | }
124 |
125 | static ZMQ_WAIT: i32 = 0;
126 |
127 | impl Message {
128 | // TODO: add a real error type here
129 | async fn recv(socket: &mut S, source: Channel) -> Result {
130 | let mut zmq_message = socket.recv().await.unwrap().into_vec().into_iter();
131 | let zmq_message = &mut zmq_message;
132 |
133 | let mut zmq_identities = Vec::new();
134 | for bytes in zmq_message.by_ref() {
135 | if bytes.deref() == b"" {
136 | break;
137 | }
138 | zmq_identities.push(bytes.to_owned());
139 | }
140 |
141 | // TODO: add error handling for this here
142 | fn next_string(byte_iter: &mut impl Iterator- ) -> String {
143 | String::from_utf8(byte_iter.next().unwrap().to_vec()).unwrap()
144 | }
145 |
146 | let signature = next_string(zmq_message);
147 |
148 | let header = next_string(zmq_message);
149 | let header: Header = serde_json::from_str(&header).unwrap();
150 |
151 | let parent_header = next_string(zmq_message);
152 | let parent_header: Option
= match parent_header.as_str() {
153 | "{}" => None,
154 | ph => serde_json::from_str(ph).unwrap(),
155 | };
156 |
157 | let metadata = next_string(zmq_message);
158 | let metadata: Metadata = serde_json::from_str(&metadata).unwrap();
159 |
160 | let content = next_string(zmq_message);
161 | // FIXME: this is a annoying solution, should be handled somehow by the type
162 | // system better
163 | let content = match source {
164 | Channel::Shell => {
165 | IncomingContent::Shell(ShellRequest::parse_variant(&header.msg_type, &content)?)
166 | }
167 | Channel::Stdin => todo!(),
168 | Channel::Control => {
169 | IncomingContent::Control(ControlRequest::parse_variant(&header.msg_type, &content)?)
170 | }
171 | };
172 |
173 | let buffers: Vec = zmq_message.collect();
174 |
175 | Ok(Message {
176 | zmq_identities,
177 | header,
178 | parent_header,
179 | metadata,
180 | content,
181 | buffers,
182 | })
183 | }
184 | }
185 |
186 | impl Message {
187 | pub async fn recv(socket: &mut S) -> Result {
188 | let msg = Message::::recv(socket, Channel::Shell).await?;
189 | let Message {
190 | zmq_identities,
191 | header,
192 | parent_header,
193 | metadata,
194 | content,
195 | buffers,
196 | } = msg;
197 | let IncomingContent::Shell(content) = content
198 | else {
199 | panic!("unexpected content");
200 | };
201 | Ok(Message {
202 | zmq_identities,
203 | header,
204 | parent_header,
205 | metadata,
206 | content,
207 | buffers,
208 | })
209 | }
210 | }
211 |
212 | impl Message {
213 | pub async fn recv(socket: &mut S) -> Result {
214 | let msg = Message::::recv(socket, Channel::Control).await?;
215 | let Message {
216 | zmq_identities,
217 | header,
218 | parent_header,
219 | metadata,
220 | content,
221 | buffers,
222 | } = msg;
223 | let IncomingContent::Control(content) = content
224 | else {
225 | panic!("unexpected content");
226 | };
227 | Ok(Message {
228 | zmq_identities,
229 | header,
230 | parent_header,
231 | metadata,
232 | content,
233 | buffers,
234 | })
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/src/jupyter/messages/multipart.rs:
--------------------------------------------------------------------------------
1 | use std::iter;
2 |
3 | use bytes::Bytes;
4 | use hmac::Mac;
5 | use zeromq::{SocketSend, ZmqMessage};
6 |
7 | use super::{Message, OutgoingContent, DIGESTER};
8 |
9 | pub struct Multipart(ZmqMessage);
10 |
11 | impl Multipart {
12 | pub async fn send(self, socket: &mut S) -> Result<(), ()> {
13 | socket.send(self.0).await.unwrap();
14 | Ok(())
15 | }
16 | }
17 |
18 | impl Message {
19 | fn into_multipart_impl(self) -> Result {
20 | let zmq_identities = self.zmq_identities;
21 | let header = serde_json::to_string(&self.header).unwrap();
22 | let parent_header = match self.parent_header {
23 | Some(ref parent_header) => serde_json::to_string(parent_header).unwrap(),
24 | None => "{}".to_owned(),
25 | };
26 | let metadata = serde_json::to_string(&self.metadata).unwrap();
27 | let content = match self.content {
28 | OutgoingContent::Shell(ref content) => serde_json::to_string(content).unwrap(),
29 | OutgoingContent::Iopub(ref content) => serde_json::to_string(content).unwrap(),
30 | OutgoingContent::Control(ref content) => serde_json::to_string(content).unwrap(),
31 | };
32 | let buffers = self.buffers;
33 |
34 | let mut digester = DIGESTER.get().clone();
35 | digester.update(header.as_bytes());
36 | digester.update(parent_header.as_bytes());
37 | digester.update(metadata.as_bytes());
38 | digester.update(content.as_bytes());
39 | let signature = digester.finalize().into_bytes();
40 | let signature = hex::encode(signature);
41 |
42 | let frames: Vec = zmq_identities
43 | .into_iter()
44 | .chain(iter::once(Bytes::from_static(b"")))
45 | .chain(
46 | [signature, header, parent_header, metadata, content]
47 | .into_iter()
48 | .map(Bytes::from),
49 | )
50 | .chain(buffers)
51 | .collect();
52 |
53 | Ok(Multipart(ZmqMessage::try_from(frames).unwrap()))
54 | }
55 | }
56 |
57 | impl Message
58 | where
59 | C: Into,
60 | {
61 | pub fn into_multipart(self) -> Result {
62 | let Message {
63 | zmq_identities,
64 | header,
65 | parent_header,
66 | metadata,
67 | content,
68 | buffers,
69 | } = self;
70 | let msg = Message {
71 | zmq_identities,
72 | header,
73 | parent_header,
74 | metadata,
75 | content: content.into(),
76 | buffers,
77 | };
78 | msg.into_multipart_impl()
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/jupyter/messages/shell.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | use crate::jupyter::kernel_info::KernelInfo;
4 |
5 | #[derive(Debug, Deserialize, Clone)]
6 | pub enum ShellRequest {
7 | Execute(ExecuteRequest),
8 | IsComplete(IsCompleteRequest),
9 | KernelInfo,
10 | }
11 |
12 | impl ShellRequest {
13 | pub fn parse_variant(variant: &str, body: &str) -> Result {
14 | match variant {
15 | "execute_request" => Ok(Self::Execute(serde_json::from_str(body).unwrap())),
16 | "is_complete_request" => Ok(Self::IsComplete(serde_json::from_str(body).unwrap())),
17 | "kernel_info_request" => Ok(Self::KernelInfo),
18 | _ => {
19 | eprintln!("unknown request {variant}");
20 | Err(())
21 | }
22 | }
23 | }
24 | }
25 |
26 | #[derive(Debug, Serialize, Clone)]
27 | #[serde(rename_all = "snake_case", tag = "status")]
28 | pub enum ShellReply {
29 | Ok(ShellReplyOk),
30 | Error {
31 | #[serde(rename = "ename")]
32 | name: String,
33 | #[serde(rename = "evalue")]
34 | value: String,
35 | traceback: Vec,
36 | },
37 | }
38 |
39 | impl ShellReply {
40 | pub fn msg_type(request_type: &'_ str) -> Result<&'static str, ()> {
41 | Ok(match request_type {
42 | "kernel_info_request" => "kernel_info_reply",
43 | "execute_request" => "execute_reply",
44 | "is_complete_request" => "is_complete_reply",
45 | _ => todo!("handle unknown requests"),
46 | })
47 | }
48 | }
49 |
50 | #[derive(Debug, Serialize, Clone)]
51 | #[serde(untagged)]
52 | pub enum ShellReplyOk {
53 | KernelInfo(KernelInfo),
54 | Execute(ExecuteReply),
55 | IsComplete(IsCompleteReply),
56 | }
57 |
58 | #[derive(Debug, Deserialize, Clone)]
59 | pub struct ExecuteRequest {
60 | pub code: String,
61 | #[serde(default)]
62 | pub silent: bool,
63 | // TODO: check if this assertion can still be unhold or should be
64 | pub store_history: bool,
65 | // TODO: figure out what to do with this
66 | pub user_expressions: serde_json::Value,
67 | pub allow_stdin: bool,
68 | pub stop_on_error: bool,
69 | }
70 |
71 | #[derive(Debug, Serialize, Clone)]
72 | pub struct ExecuteReply {
73 | pub execution_count: usize,
74 | pub user_expressions: serde_json::Value,
75 | }
76 |
77 | #[derive(Debug, Deserialize, Clone)]
78 | pub struct IsCompleteRequest {
79 | pub code: String,
80 | }
81 |
82 | #[derive(Debug, Serialize, Clone)]
83 | #[serde(tag = "status", rename_all = "snake_case")]
84 | pub enum IsCompleteReply {
85 | Complete,
86 | Incomplete { indent: String },
87 | Invalid,
88 | Unknown,
89 | }
90 |
--------------------------------------------------------------------------------
/src/jupyter/mod.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | pub mod connection_file;
4 | pub mod kernel_info;
5 | pub mod messages;
6 | pub mod register_kernel;
7 |
8 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
9 | pub struct Shutdown {
10 | pub restart: bool,
11 | }
12 |
--------------------------------------------------------------------------------
/src/jupyter/register_kernel.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Path, PathBuf};
2 | use std::{env, fs, io};
3 |
4 | use clap::ValueEnum;
5 | use miette::Diagnostic;
6 | use serde_json::json;
7 | use thiserror::Error;
8 |
9 | #[derive(Debug, ValueEnum, Clone, Copy)]
10 | pub enum RegisterLocation {
11 | User,
12 | System,
13 | }
14 |
15 | #[derive(Debug, Error, Diagnostic)]
16 | pub enum RegisterKernelError {
17 | #[error(transparent)]
18 | Io(#[from] io::Error),
19 |
20 | #[error("could not format kernel manifest")]
21 | Format(#[from] serde_json::Error),
22 | }
23 |
24 | pub fn register_kernel(location: RegisterLocation) -> Result {
25 | let path = kernel_path(location);
26 | let path = path.as_ref();
27 | fs::create_dir_all(path)?;
28 | let mut file_path = PathBuf::from(path);
29 | file_path.push("kernel.json");
30 | let manifest = serde_json::to_string_pretty(&kernel_manifest())?;
31 | fs::write(&file_path, manifest)?;
32 | Ok(file_path)
33 | }
34 |
35 | fn kernel_path(location: RegisterLocation) -> impl AsRef {
36 | let mut path = PathBuf::new();
37 |
38 | #[cfg(target_os = "windows")]
39 | match location {
40 | RegisterLocation::User => {
41 | let appdata = env::var("APPDATA").expect("%APPDATA% not found");
42 | path.push(appdata);
43 | path.push(r"jupyter\kernels");
44 | }
45 | RegisterLocation::System => {
46 | let programdata = env::var("PROGRAMDATA").expect("%PROGRAMDATA% not found");
47 | path = PathBuf::from(programdata);
48 | path.push(r"jupyter\kernels");
49 | }
50 | }
51 |
52 | #[cfg(any(target_os = "linux", target_os = "android"))]
53 | match location {
54 | RegisterLocation::User => {
55 | path.push(dirs::home_dir().expect("defined on linux"));
56 | path.push(".local/share/jupyter/kernels")
57 | }
58 | RegisterLocation::System => path.push("/usr/local/share/jupyter/kernels"),
59 | }
60 |
61 | #[cfg(target_os = "macos")]
62 | match location {
63 | RegisterLocation::User => {
64 | path.push(dirs::home_dir().expect("defined on macos"));
65 | path.push("Library/Jupyter/kernels")
66 | }
67 | RegisterLocation::System => path.push("/usr/local/share/jupyter/kernels"),
68 | }
69 |
70 | if path.to_string_lossy() == "" {
71 | panic!(
72 | "Your target os ({}) doesn't support `register`",
73 | env::consts::OS
74 | );
75 | }
76 |
77 | path.push("nu");
78 |
79 | path
80 | }
81 |
82 | fn kernel_manifest() -> serde_json::Value {
83 | let this_exec = env::current_exe().unwrap();
84 | json!({
85 | "argv": [this_exec, "start", "{connection_file}"],
86 | "display_name": "Nushell",
87 | "language": "nushell",
88 | "interrupt_mode": "message",
89 | "env": {},
90 | "metadata": {}
91 | })
92 | }
93 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | // for now
2 | #![allow(dead_code)]
3 | #![allow(unused_variables)]
4 |
5 | use std::path::{Path, PathBuf};
6 | use std::{panic, process};
7 |
8 | use clap::{Parser, Subcommand};
9 | use const_format::formatcp;
10 | use handlers::shell::Cell;
11 | use handlers::stream::StreamHandler;
12 | use jupyter::connection_file::ConnectionFile;
13 | use jupyter::messages::iopub;
14 | use jupyter::register_kernel::{register_kernel, RegisterLocation};
15 | use nu::commands::{add_jupyter_command_context, JupyterCommandContext};
16 | use nu::konst::Konst;
17 | use nu::render::FormatDeclIds;
18 | use nu_protocol::engine::Stack;
19 | use tokio::sync::{broadcast, mpsc};
20 | use zeromq::{PubSocket, RepSocket, RouterSocket, Socket, ZmqResult};
21 |
22 | use crate::jupyter::messages::DIGESTER;
23 |
24 | mod error;
25 | mod handlers;
26 | mod jupyter;
27 | mod nu;
28 | mod util;
29 |
30 | static_toml::static_toml! {
31 | const CARGO_TOML = include_toml!("Cargo.toml");
32 | }
33 |
34 | #[derive(Debug, Parser)]
35 | #[command(version, long_version = formatcp!(
36 | "{}\nnu-engine {}",
37 | CARGO_TOML.package.version,
38 | CARGO_TOML.dependencies.nu_engine.version
39 | ))]
40 | struct Cli {
41 | #[command(subcommand)]
42 | command: Command,
43 | }
44 |
45 | #[derive(Debug, Subcommand)]
46 | enum Command {
47 | #[command(alias = "install")]
48 | Register {
49 | #[clap(long, group = "location")]
50 | user: bool,
51 |
52 | #[clap(long, group = "location")]
53 | system: bool,
54 | },
55 |
56 | Start {
57 | connection_file_path: PathBuf,
58 | },
59 | }
60 |
61 | type ShellSocket = RouterSocket;
62 | type IopubSocket = PubSocket;
63 | type StdinSocket = RouterSocket;
64 | type ControlSocket = RouterSocket;
65 | type HeartbeatSocket = RepSocket;
66 |
67 | struct Sockets {
68 | pub shell: ShellSocket,
69 | pub iopub: IopubSocket,
70 | pub stdin: StdinSocket,
71 | pub control: ControlSocket,
72 | pub heartbeat: HeartbeatSocket,
73 | }
74 |
75 | impl Sockets {
76 | async fn start(connection_file: &ConnectionFile) -> ZmqResult {
77 | let endpoint = |port| {
78 | format!(
79 | "{}://{}:{}",
80 | connection_file.transport, connection_file.ip, port
81 | )
82 | };
83 |
84 | let mut shell = ShellSocket::new();
85 | shell.bind(&endpoint(&connection_file.shell_port)).await?;
86 |
87 | let mut iopub = IopubSocket::new();
88 | iopub.bind(&endpoint(&connection_file.iopub_port)).await?;
89 |
90 | let mut stdin = StdinSocket::new();
91 | stdin.bind(&endpoint(&connection_file.stdin_port)).await?;
92 |
93 | let mut control = ControlSocket::new();
94 | control
95 | .bind(&endpoint(&connection_file.control_port))
96 | .await?;
97 |
98 | let mut heartbeat = HeartbeatSocket::new();
99 | heartbeat
100 | .bind(&endpoint(&connection_file.heartbeat_port))
101 | .await?;
102 |
103 | Ok(Sockets {
104 | shell,
105 | iopub,
106 | stdin,
107 | control,
108 | heartbeat,
109 | })
110 | }
111 | }
112 |
113 | #[tokio::main]
114 | async fn main() -> miette::Result<()> {
115 | let args = Cli::parse();
116 | match args.command {
117 | Command::Register { user, system } => {
118 | let location = match (user, system) {
119 | (true, true) => unreachable!("handled by clap"),
120 | (false, true) => RegisterLocation::System,
121 | (true, false) => RegisterLocation::User,
122 | (false, false) => RegisterLocation::User, // default case
123 | };
124 | let path = register_kernel(location)?;
125 | println!("Registered kernel to {}", path.display());
126 | }
127 | Command::Start {
128 | connection_file_path,
129 | } => start_kernel(connection_file_path).await,
130 | }
131 | Ok(())
132 | }
133 |
134 | async fn start_kernel(connection_file_path: impl AsRef) {
135 | set_avalanche_panic_hook();
136 |
137 | let connection_file = ConnectionFile::from_path(connection_file_path).unwrap();
138 | let sockets = Sockets::start(&connection_file).await.unwrap();
139 | DIGESTER.key_init(&connection_file.key).unwrap();
140 |
141 | let mut engine_state = nu::initial_engine_state();
142 | let format_decl_ids = FormatDeclIds::find(&engine_state).unwrap();
143 | let spans = nu::module::create_nuju_module(&mut engine_state);
144 | nu::commands::hide_incompatible_commands(&mut engine_state).unwrap();
145 | let konst = Konst::register(&mut engine_state).unwrap();
146 | let (engine_state, interrupt_signal) = nu::add_interrupt_signal(engine_state);
147 |
148 | let (iopub_tx, iopub_rx) = mpsc::channel(1);
149 | let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
150 |
151 | let ctx = JupyterCommandContext {
152 | iopub: iopub_tx.clone(),
153 | format_decl_ids,
154 | konst,
155 | spans: spans.clone(),
156 | };
157 | let engine_state = add_jupyter_command_context(engine_state, ctx);
158 |
159 | let (stdout_handler, stdout_file) =
160 | StreamHandler::start(iopub::StreamName::Stdout, iopub_tx.clone()).unwrap();
161 | let (stderr_handler, stderr_file) =
162 | StreamHandler::start(iopub::StreamName::Stderr, iopub_tx.clone()).unwrap();
163 | let stack = Stack::new()
164 | .stdout_file(stdout_file)
165 | .stderr_file(stderr_file);
166 |
167 | let cell = Cell::new();
168 |
169 | let heartbeat_task = tokio::spawn(handlers::heartbeat::handle(
170 | sockets.heartbeat,
171 | shutdown_rx.resubscribe(),
172 | ));
173 |
174 | let iopub_task = tokio::spawn(handlers::iopub::handle(
175 | sockets.iopub,
176 | shutdown_rx.resubscribe(),
177 | iopub_rx,
178 | ));
179 |
180 | let shell_ctx = handlers::shell::HandlerContext {
181 | socket: sockets.shell,
182 | iopub: iopub_tx,
183 | stdout_handler,
184 | stderr_handler,
185 | engine_state,
186 | format_decl_ids,
187 | konst,
188 | spans,
189 | stack,
190 | cell,
191 | };
192 | let shell_task = tokio::spawn(handlers::shell::handle(
193 | shell_ctx,
194 | shutdown_rx.resubscribe(),
195 | ));
196 |
197 | let control_task = tokio::spawn(handlers::control::handle(
198 | sockets.control,
199 | shutdown_tx,
200 | interrupt_signal,
201 | ));
202 |
203 | heartbeat_task.await.unwrap();
204 | iopub_task.await.unwrap();
205 | shell_task.await.unwrap();
206 | control_task.await.unwrap();
207 | }
208 |
209 | // no heartbeat nor iopub as they are handled differently
210 | #[derive(Debug, Clone, Copy)]
211 | enum Channel {
212 | Shell,
213 | Stdin,
214 | Control,
215 | }
216 |
217 | fn set_avalanche_panic_hook() {
218 | let old_hook = panic::take_hook();
219 | panic::set_hook(Box::new(move |panic_info| {
220 | old_hook(panic_info);
221 | process::exit(1);
222 | }));
223 | }
224 |
--------------------------------------------------------------------------------
/src/nu/commands/command.rs:
--------------------------------------------------------------------------------
1 | use nu_engine::command_prelude::*;
2 | use nu_engine::get_full_help;
3 |
4 | use super::COMMANDS_TOML;
5 |
6 | #[derive(Clone)]
7 | pub struct Nuju;
8 |
9 | impl Command for Nuju {
10 | fn name(&self) -> &str {
11 | COMMANDS_TOML.nuju.name
12 | }
13 |
14 | fn description(&self) -> &str {
15 | COMMANDS_TOML.nuju.description
16 | }
17 |
18 | fn signature(&self) -> nu_protocol::Signature {
19 | Signature::build(Self.name())
20 | .category(super::category())
21 | .input_output_types(vec![(Type::Nothing, Type::String)])
22 | }
23 |
24 | fn extra_description(&self) -> &str {
25 | COMMANDS_TOML.nuju.extra_description
26 | }
27 |
28 | fn run(
29 | &self,
30 | engine_state: &EngineState,
31 | stack: &mut Stack,
32 | call: &Call,
33 | _input: PipelineData,
34 | ) -> Result {
35 | Ok(
36 | Value::string(get_full_help(&Nuju, engine_state, stack), call.head)
37 | .into_pipeline_data(),
38 | )
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/nu/commands/display.rs:
--------------------------------------------------------------------------------
1 | use mime_guess::MimeGuess;
2 | use nu_engine::CallExt;
3 | use nu_protocol::engine::Command;
4 | use nu_protocol::{Example, ShellError, Signature, Spanned, SyntaxShape, Type};
5 |
6 | use super::COMMANDS_TOML;
7 | use crate::handlers::shell::RENDER_FILTER;
8 |
9 | #[derive(Debug, Clone)]
10 | pub struct Display;
11 |
12 | impl Command for Display {
13 | fn name(&self) -> &str {
14 | COMMANDS_TOML.display.name
15 | }
16 |
17 | fn description(&self) -> &str {
18 | COMMANDS_TOML.display.description
19 | }
20 |
21 | fn extra_description(&self) -> &str {
22 | COMMANDS_TOML.display.extra_description
23 | }
24 |
25 | fn search_terms(&self) -> Vec<&str> {
26 | COMMANDS_TOML.display.search_terms.into()
27 | }
28 |
29 | fn signature(&self) -> Signature {
30 | Signature::build(self.name())
31 | .required("format", SyntaxShape::String, "Format to filter for")
32 | .input_output_types(vec![(Type::Any, Type::Any)])
33 | .category(super::category())
34 | }
35 |
36 | fn examples(&self) -> Vec {
37 | COMMANDS_TOML
38 | .display
39 | .examples
40 | .iter()
41 | .map(|eg| Example {
42 | example: eg.example,
43 | description: eg.description,
44 | result: None,
45 | })
46 | .collect()
47 | }
48 |
49 | fn run(
50 | &self,
51 | engine_state: &nu_protocol::engine::EngineState,
52 | stack: &mut nu_protocol::engine::Stack,
53 | call: &nu_protocol::engine::Call,
54 | input: nu_protocol::PipelineData,
55 | ) -> Result {
56 | let format: Spanned = call.req(engine_state, stack, 0)?;
57 |
58 | let mime = MimeGuess::from_ext(&format.item).first().ok_or_else(|| {
59 | ShellError::IncorrectValue {
60 | msg: "cannot guess a mime type".to_owned(),
61 | val_span: format.span,
62 | call_span: call.head,
63 | }
64 | })?;
65 |
66 | RENDER_FILTER.lock().replace(mime);
67 | Ok(input)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/nu/commands/external.rs:
--------------------------------------------------------------------------------
1 | use std::sync::atomic::Ordering;
2 |
3 | use atomic_enum::atomic_enum;
4 | use nu_protocol::engine::{Call, Command, EngineState, Stack, StateWorkingSet};
5 | use nu_protocol::{PipelineData, ShellError, Signature, Span, Type, Value};
6 |
7 | use super::COMMANDS_TOML;
8 |
9 | #[atomic_enum]
10 | #[derive(PartialEq)]
11 | enum ExternalState {
12 | Disabled = 0,
13 | JustEnabled,
14 | AlreadyEnabled,
15 | }
16 |
17 | impl AtomicExternalState {
18 | pub fn fetch_max(&self, val: ExternalState, order: Ordering) -> ExternalState {
19 | match self.0.fetch_max(val as usize, order) {
20 | 0 => ExternalState::Disabled,
21 | 1 => ExternalState::JustEnabled,
22 | 2 => ExternalState::AlreadyEnabled,
23 | _ => unreachable!("ExternalState represents at max 2"),
24 | }
25 | }
26 | }
27 |
28 | static EXTERNAL_STATE: AtomicExternalState = AtomicExternalState::new(ExternalState::Disabled);
29 |
30 | #[derive(Debug, Clone)]
31 | pub struct External;
32 |
33 | impl Command for External {
34 | fn name(&self) -> &str {
35 | COMMANDS_TOML.external.name
36 | }
37 |
38 | fn description(&self) -> &str {
39 | COMMANDS_TOML.external.description
40 | }
41 |
42 | fn extra_description(&self) -> &str {
43 | COMMANDS_TOML.external.extra_description
44 | }
45 |
46 | fn search_terms(&self) -> Vec<&str> {
47 | COMMANDS_TOML.external.search_terms.into()
48 | }
49 |
50 | fn signature(&self) -> Signature {
51 | Signature::build(self.name())
52 | .input_output_types(vec![(Type::Any, Type::Nothing)])
53 | .category(super::category())
54 | }
55 |
56 | fn run(
57 | &self,
58 | _engine_state: &EngineState,
59 | _stack: &mut Stack,
60 | _call: &Call,
61 | _input: PipelineData,
62 | ) -> Result {
63 | // TODO: add some display data iopub here
64 | // update the value to at least `JustEnabled`
65 | EXTERNAL_STATE.fetch_max(ExternalState::JustEnabled, Ordering::SeqCst);
66 | Ok(PipelineData::Value(Value::nothing(Span::unknown()), None))
67 | }
68 | }
69 |
70 | impl External {
71 | /// Apply the `run-external` command to the engine if external commands were
72 | /// just enabled.
73 | pub fn apply(engine_state: &mut EngineState) -> Result<(), ShellError> {
74 | if let ExternalState::JustEnabled = EXTERNAL_STATE.load(Ordering::SeqCst) {
75 | let mut working_set = StateWorkingSet::new(engine_state);
76 | // TODO: add a command that controls the output of external calls
77 | working_set.add_decl(Box::new(nu_command::External));
78 | engine_state.merge_delta(working_set.render())?;
79 | EXTERNAL_STATE.swap(ExternalState::AlreadyEnabled, Ordering::SeqCst);
80 | }
81 | Ok(())
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/nu/commands/mod.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Write;
2 |
3 | use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
4 | use nu_protocol::Category;
5 | use tokio::sync::mpsc;
6 |
7 | use super::konst::Konst;
8 | use super::module::KernelInternalSpans;
9 | use super::render::FormatDeclIds;
10 | use crate::jupyter::messages::multipart::Multipart;
11 |
12 | pub mod command;
13 | pub mod display;
14 | pub mod external;
15 | pub mod print;
16 |
17 | static_toml::static_toml! {
18 | const COMMANDS_TOML = include_toml!("commands.toml");
19 | }
20 |
21 | /// Hide incompatible commands so that users don't accidentally call them.
22 | pub fn hide_incompatible_commands(
23 | engine_state: &mut EngineState,
24 | ) -> Result<(), super::ExecuteError> {
25 | let mut code = String::new();
26 | for command in COMMANDS_TOML.incompatible_commands {
27 | writeln!(code, "hide {command}").expect("String::write is infallible");
28 | }
29 |
30 | let mut stack = Stack::new();
31 | super::execute(&code, engine_state, &mut stack, "hide-initial-commands")?;
32 | Ok(())
33 | }
34 |
35 | pub fn category() -> Category {
36 | Category::Custom("jupyter".to_owned())
37 | }
38 |
39 | #[derive(Debug, Clone)]
40 | pub struct JupyterCommandContext {
41 | pub iopub: mpsc::Sender,
42 | pub format_decl_ids: FormatDeclIds,
43 | pub konst: Konst,
44 | pub spans: KernelInternalSpans,
45 | }
46 |
47 | pub fn add_jupyter_command_context(
48 | mut engine_state: EngineState,
49 | ctx: JupyterCommandContext,
50 | ) -> EngineState {
51 | let delta = {
52 | let mut working_set = StateWorkingSet::new(&engine_state);
53 |
54 | macro_rules! bind_command {
55 | ( $( $command:expr ),* $(,)? ) => {
56 | $( working_set.add_decl(Box::new($command)); )*
57 | };
58 | }
59 |
60 | bind_command! {
61 | command::Nuju,
62 | external::External,
63 | display::Display,
64 | print::Print::new(ctx)
65 | }
66 |
67 | working_set.render()
68 | };
69 |
70 | if let Err(err) = engine_state.merge_delta(delta) {
71 | eprintln!("Error creating jupyter context: {err:?}");
72 | }
73 |
74 | engine_state
75 | }
76 |
--------------------------------------------------------------------------------
/src/nu/commands/print.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use mime_guess::MimeGuess;
4 | use nu_engine::CallExt;
5 | use nu_protocol::engine::Command;
6 | use nu_protocol::{FromValue, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value};
7 |
8 | use super::{JupyterCommandContext, COMMANDS_TOML};
9 | use crate::jupyter::messages::iopub::{DisplayData, IopubBroacast};
10 | use crate::jupyter::messages::{Header, Message, Metadata};
11 | use crate::nu::render::{PipelineRender, StringifiedPipelineRender};
12 |
13 | #[derive(Debug, Clone)]
14 | pub struct Print(JupyterCommandContext);
15 |
16 | impl Print {
17 | pub fn new(ctx: JupyterCommandContext) -> Self {
18 | Self(ctx)
19 | }
20 | }
21 |
22 | impl Command for Print {
23 | fn name(&self) -> &str {
24 | COMMANDS_TOML.print.name
25 | }
26 |
27 | fn description(&self) -> &str {
28 | COMMANDS_TOML.print.description
29 | }
30 |
31 | fn search_terms(&self) -> Vec<&str> {
32 | COMMANDS_TOML.print.search_terms.into()
33 | }
34 |
35 | fn signature(&self) -> Signature {
36 | Signature::build(self.name())
37 | .optional("input", SyntaxShape::Any, "Value to print")
38 | .named(
39 | "format",
40 | SyntaxShape::String,
41 | "Format to filter for",
42 | Some('f'),
43 | )
44 | .input_output_types(vec![
45 | (Type::Any, Type::Nothing),
46 | (Type::Nothing, Type::Nothing),
47 | ])
48 | .category(super::category())
49 | }
50 |
51 | // TODO: split this into multiple parts, this is too much
52 | fn run(
53 | &self,
54 | engine_state: &nu_protocol::engine::EngineState,
55 | stack: &mut nu_protocol::engine::Stack,
56 | call: &nu_protocol::engine::Call,
57 | input: nu_protocol::PipelineData,
58 | ) -> Result {
59 | let arg: Option = call.opt(engine_state, stack, 0)?;
60 | let arg: Option = arg.map(|v| PipelineData::Value(v, None));
61 | let input_span = input.span(); // maybe needed for an error
62 | let to_render = match (input, arg) {
63 | // no data provided, throw error
64 | (PipelineData::Empty, None) => Err(ShellError::GenericError {
65 | error: "No input data".to_string(),
66 | msg: "No data was piped or passed as an argument to the command.".to_string(),
67 | span: Some(call.span()),
68 | help: Some(
69 | "Please provide data through the pipeline or as an argument.".to_string(),
70 | ),
71 | inner: vec![],
72 | }),
73 |
74 | // passed arg has no data, throw error
75 | (_, Some(PipelineData::Empty)) => Err(ShellError::TypeMismatch {
76 | err_message: "Expected non-empty data, but found empty".to_string(),
77 | span: call.arguments_span(),
78 | }),
79 |
80 | // render passed arg
81 | (PipelineData::Empty, Some(data)) => Ok(data),
82 |
83 | // too many inputs, throw error
84 | (_, Some(_)) => Err(ShellError::IncompatibleParameters {
85 | left_message: "Either pass data via pipe".to_string(),
86 | left_span: input_span.unwrap_or(call.head),
87 | right_message: "Or pass data via an argument".to_string(),
88 | right_span: call.arguments_span(),
89 | }),
90 |
91 | // render piped arg
92 | (data, None) => Ok(data),
93 | }?;
94 |
95 | let format: Option = call.get_flag(engine_state, stack, "format")?;
96 | let spanned_format: Option<(Span, Value)> = format.map(|v| (v.span(), v));
97 | let spanned_format: Option<(Span, String)> = spanned_format
98 | .map(|(span, v)| String::from_value(v).map(|s| (span, s)))
99 | .transpose()?;
100 | let mime = spanned_format
101 | .map(|(span, s)| {
102 | MimeGuess::from_ext(&s)
103 | .first()
104 | .ok_or_else(|| ShellError::IncorrectValue {
105 | msg: "Cannot guess a mime type".to_owned(),
106 | val_span: span,
107 | call_span: call.head,
108 | })
109 | })
110 | .transpose()?;
111 |
112 | let render: StringifiedPipelineRender = PipelineRender::render(
113 | to_render,
114 | engine_state,
115 | stack,
116 | &self.0.spans,
117 | self.0.format_decl_ids,
118 | mime,
119 | )
120 | .unwrap() // TODO: handle this better
121 | .into();
122 |
123 | let display_data = DisplayData {
124 | data: render.data,
125 | metadata: render.metadata,
126 | transient: HashMap::new(),
127 | };
128 | let broadcast = IopubBroacast::DisplayData(display_data);
129 |
130 | let konst = self.0.konst.data(stack, call.head)?;
131 | let message = Message {
132 | zmq_identities: konst.message.zmq_identities,
133 | header: Header::new(broadcast.msg_type()),
134 | parent_header: Some(konst.message.header),
135 | metadata: Metadata::empty(),
136 | content: broadcast,
137 | buffers: vec![],
138 | };
139 |
140 | self.0
141 | .iopub
142 | .blocking_send(message.into_multipart().unwrap())
143 | .unwrap();
144 |
145 | Ok(PipelineData::Empty)
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/nu/konst.rs:
--------------------------------------------------------------------------------
1 | use bytes::Bytes;
2 | use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
3 | use nu_protocol::{FromValue, IntoValue, ShellError, Span, Type, VarId};
4 |
5 | use crate::jupyter::messages::{Header, Message};
6 | use crate::CARGO_TOML;
7 |
8 | #[derive(Debug, Clone, Copy)]
9 | pub struct Konst {
10 | var_id: VarId,
11 | }
12 |
13 | impl Konst {
14 | pub const VAR_NAME: &'static str = "nuju";
15 |
16 | pub fn register(engine_state: &mut EngineState) -> Result {
17 | let mut working_set = StateWorkingSet::new(engine_state);
18 | let var_id = working_set.add_variable(
19 | Self::VAR_NAME.as_bytes().to_vec(),
20 | Span::unknown(),
21 | Type::Any,
22 | false,
23 | );
24 | engine_state.merge_delta(working_set.render())?;
25 | Ok(Self { var_id })
26 | }
27 |
28 | pub fn var_id(&self) -> VarId {
29 | self.var_id
30 | }
31 |
32 | pub fn update(&self, stack: &mut Stack, cell_name: String, message: Message) {
33 | let data = KonstData {
34 | version: KonstDataVersion {
35 | kernel: CARGO_TOML.package.version.to_owned(),
36 | nu: CARGO_TOML.dependencies.nu_engine.version.to_owned(),
37 | },
38 | cell: cell_name,
39 | message: KonstDataMessage {
40 | zmq_identities: message.zmq_identities,
41 | header: message.header,
42 | parent_header: message.parent_header,
43 | },
44 | };
45 | stack.add_var(self.var_id, data.into_value(Span::unknown()))
46 | }
47 |
48 | pub fn data(&self, stack: &Stack, span: Span) -> Result {
49 | let value = stack
50 | .get_var(self.var_id, span)
51 | .map_err(|_| ShellError::VariableNotFoundAtRuntime { span })?;
52 | KonstData::from_value(value)
53 | }
54 | }
55 |
56 | #[derive(Debug, Clone, IntoValue, FromValue)]
57 | pub struct KonstData {
58 | pub version: KonstDataVersion,
59 | pub cell: String,
60 | pub message: KonstDataMessage,
61 | }
62 |
63 | #[derive(Debug, Clone, IntoValue, FromValue)]
64 | pub struct KonstDataVersion {
65 | pub kernel: String,
66 | pub nu: String,
67 | }
68 |
69 | #[derive(Debug, Clone, IntoValue, FromValue)]
70 | pub struct KonstDataMessage {
71 | pub zmq_identities: Vec,
72 | pub header: Header,
73 | pub parent_header: Option,
74 | }
75 |
--------------------------------------------------------------------------------
/src/nu/mod.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::env;
3 | use std::fmt::{Debug, Write};
4 | use std::path::PathBuf;
5 | use std::sync::atomic::AtomicBool;
6 | use std::sync::Arc;
7 |
8 | use nu_protocol::debugger::WithoutDebug;
9 | use nu_protocol::engine::{EngineState, Stack, StateDelta, StateWorkingSet};
10 | use nu_protocol::{
11 | CompileError, ParseError, PipelineData, ShellError, Signals, Span, UseAnsiColoring, Value,
12 | NU_VARIABLE_ID,
13 | };
14 | use thiserror::Error;
15 |
16 | pub mod commands;
17 | pub mod konst;
18 | pub mod module;
19 | pub mod render;
20 |
21 | #[allow(clippy::let_and_return)] // i like it here
22 | pub fn initial_engine_state() -> EngineState {
23 | // TODO: compare with nu_cli::get_engine_state for other contexts
24 | let engine_state = nu_cmd_lang::create_default_context();
25 | let engine_state = configure_engine_state(engine_state);
26 | let engine_state = add_env_context(engine_state);
27 |
28 | let engine_state = nu_command::add_shell_command_context(engine_state);
29 | let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
30 | let engine_state = nu_cmd_plugin::add_plugin_command_context(engine_state);
31 |
32 | #[cfg(feature = "nu-plotters")]
33 | let engine_state = nu_plotters::add_plotters_command_context(engine_state);
34 |
35 | // this doesn't add the jupyter context, as they need more context
36 |
37 | engine_state
38 | }
39 |
40 | fn add_env_context(mut engine_state: EngineState) -> EngineState {
41 | let mut env_map = HashMap::new();
42 |
43 | for (key, value) in env::vars() {
44 | env_map.insert(key, value);
45 | }
46 |
47 | if let Ok(current_dir) = env::current_dir() {
48 | env_map.insert("PWD".into(), current_dir.to_string_lossy().into_owned());
49 | }
50 |
51 | let mut toml = String::new();
52 | let mut values = Vec::new();
53 | let mut line_offset = 0;
54 | for (key, value) in env_map {
55 | let line = format!("{key} = {value:?}");
56 | let start = key.len() + " = ".len() + line_offset;
57 | let end = line_offset + line.len();
58 | let span = Span::new(start, end);
59 | line_offset += line.len() + 1;
60 | writeln!(toml, "{line}").expect("infallible");
61 | values.push((key, Value::string(value, span)));
62 | }
63 |
64 | let span_offset = engine_state.next_span_start();
65 | engine_state.add_file(
66 | "Host Environment Variables".into(),
67 | toml.into_bytes().into(),
68 | );
69 | for (key, value) in values {
70 | let span = value.span();
71 | let span = Span::new(span.start + span_offset, span.end + span_offset);
72 | engine_state.add_env_var(key, value.with_span(span));
73 | }
74 |
75 | engine_state
76 | }
77 |
78 | fn configure_engine_state(mut engine_state: EngineState) -> EngineState {
79 | engine_state.history_enabled = false;
80 | engine_state.is_interactive = false;
81 | engine_state.is_login = false;
82 |
83 | // if we cannot access the current dir, we probably also cannot access the
84 | // subdirectories
85 | if let Ok(mut config_dir) = env::current_dir() {
86 | config_dir.push(".nu");
87 | engine_state.set_config_path("config-path", config_dir.join("config.nu"));
88 | engine_state.set_config_path("env-path", config_dir.join("env.nu"));
89 | }
90 |
91 | engine_state.generate_nu_constant();
92 |
93 | if let Some(ref v) = engine_state.get_var(NU_VARIABLE_ID).const_val {
94 | engine_state.plugin_path = v
95 | .get_data_by_key("plugin-path")
96 | .and_then(|v| v.as_str().ok().map(PathBuf::from));
97 | }
98 |
99 | engine_state
100 | }
101 |
102 | pub fn add_interrupt_signal(mut engine_state: EngineState) -> (EngineState, Arc) {
103 | let signal = Arc::new(AtomicBool::new(false));
104 | let signals = Signals::new(signal.clone());
105 | engine_state.set_signals(signals);
106 | (engine_state, signal)
107 | }
108 |
109 | pub fn execute(
110 | code: &str,
111 | engine_state: &mut EngineState,
112 | stack: &mut Stack,
113 | name: &str,
114 | ) -> Result {
115 | let code = code.as_bytes();
116 | let mut working_set = StateWorkingSet::new(engine_state);
117 | let block = nu_parser::parse(&mut working_set, Some(name), code, false);
118 |
119 | // TODO: report parse warnings
120 |
121 | if let Some(error) = working_set.parse_errors.into_iter().next() {
122 | return Err(ExecuteError::Parse {
123 | error,
124 | delta: working_set.delta,
125 | });
126 | }
127 |
128 | if let Some(error) = working_set.compile_errors.into_iter().next() {
129 | return Err(ExecuteError::Compile {
130 | error,
131 | delta: working_set.delta,
132 | });
133 | }
134 |
135 | engine_state.merge_delta(working_set.delta)?;
136 | let res =
137 | nu_engine::eval_block::(engine_state, stack, &block, PipelineData::Empty)?;
138 | Ok(res)
139 | }
140 |
141 | #[derive(Error)]
142 | pub enum ExecuteError {
143 | #[error("{error}")]
144 | Parse {
145 | #[source]
146 | error: ParseError,
147 | /// Delta of the working set.
148 | ///
149 | /// By keeping this delta around we later can update another working
150 | /// set with and with that correctly source the parsing issues.
151 | delta: StateDelta,
152 | },
153 |
154 | #[error("{error}")]
155 | Compile {
156 | #[source]
157 | error: CompileError,
158 | /// Delta of the working set.
159 | ///
160 | /// By keeping this delta around we later can update another working
161 | /// set with and with that correctly source the parsing issues.
162 | delta: StateDelta,
163 | },
164 |
165 | #[error(transparent)]
166 | Shell(#[from] ShellError),
167 | }
168 |
169 | impl Debug for ExecuteError {
170 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 | match self {
172 | Self::Parse { error, delta } => f
173 | .debug_struct("Parse")
174 | .field("error", error)
175 | .field("delta", &format_args!("[StateDelta]"))
176 | .finish(),
177 | Self::Compile { error, delta } => f
178 | .debug_struct("Compile")
179 | .field("error", error)
180 | .field("delta", &format_args!("[StateDelta]"))
181 | .finish(),
182 | Self::Shell(arg0) => f.debug_tuple("Shell").field(arg0).finish(),
183 | }
184 | }
185 | }
186 |
187 | #[derive(Error)]
188 | #[error("{diagnostic}")]
189 | pub struct ReportExecuteError<'s> {
190 | diagnostic: Box,
191 | working_set: &'s StateWorkingSet<'s>,
192 | }
193 |
194 | impl Debug for ReportExecuteError<'_> {
195 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 | // This code is stolen from nu_protocol::errors::cli_error::CliError::Debug impl
197 |
198 | let config = self.working_set.get_config();
199 |
200 | let ansi_support = match config.use_ansi_coloring {
201 | // TODO: design a better auto determination
202 | UseAnsiColoring::Auto => true,
203 | UseAnsiColoring::True => true,
204 | UseAnsiColoring::False => false,
205 | };
206 |
207 | let error_style = &config.error_style;
208 |
209 | let miette_handler: Box = match error_style {
210 | nu_protocol::ErrorStyle::Plain => Box::new(miette::NarratableReportHandler::new()),
211 | nu_protocol::ErrorStyle::Fancy => Box::new(
212 | miette::MietteHandlerOpts::new()
213 | // For better support of terminal themes use the ANSI coloring
214 | .rgb_colors(miette::RgbColors::Never)
215 | // If ansi support is disabled in the config disable the eye-candy
216 | .color(ansi_support)
217 | .unicode(ansi_support)
218 | .terminal_links(ansi_support)
219 | .build(),
220 | ),
221 | };
222 |
223 | // Ignore error to prevent format! panics. This can happen if span points at
224 | // some inaccessible location, for example by calling `report_error()`
225 | // with wrong working set.
226 | let _ = miette_handler.debug(self, f);
227 |
228 | Ok(())
229 | }
230 | }
231 |
232 | impl<'s> ReportExecuteError<'s> {
233 | pub fn new(error: ExecuteError, working_set: &'s mut StateWorkingSet<'s>) -> Self {
234 | let diagnostic = match error {
235 | ExecuteError::Parse { error, delta } => {
236 | working_set.delta = delta;
237 | Box::new(error) as Box
238 | }
239 | ExecuteError::Compile { error, delta } => {
240 | working_set.delta = delta;
241 | Box::new(error) as Box
242 | }
243 | ExecuteError::Shell(error) => Box::new(error) as Box,
244 | };
245 | Self {
246 | diagnostic,
247 | working_set,
248 | }
249 | }
250 |
251 | pub fn code<'a>(&'a self) -> Box {
252 | miette::Diagnostic::code(self)
253 | .unwrap_or_else(|| Box::new(format_args!("nu-jupyter-kernel::unknown-error")))
254 | }
255 |
256 | pub fn fmt(&self) -> String {
257 | format!("Error: {:?}", self)
258 | }
259 | }
260 |
261 | impl<'s> miette::Diagnostic for ReportExecuteError<'s> {
262 | fn code<'a>(&'a self) -> Option> {
263 | self.diagnostic.code()
264 | }
265 |
266 | fn severity(&self) -> Option {
267 | self.diagnostic.severity()
268 | }
269 |
270 | fn help<'a>(&'a self) -> Option> {
271 | self.diagnostic.help()
272 | }
273 |
274 | fn url<'a>(&'a self) -> Option> {
275 | self.diagnostic.url()
276 | }
277 |
278 | fn source_code(&self) -> Option<&dyn miette::SourceCode> {
279 | match self.diagnostic.source_code() {
280 | None => Some(&self.working_set as &dyn miette::SourceCode),
281 | Some(source_code) => Some(source_code),
282 | }
283 | }
284 |
285 | fn labels(&self) -> Option + '_>> {
286 | self.diagnostic.labels()
287 | }
288 |
289 | fn related<'a>(&'a self) -> Option + 'a>> {
290 | self.diagnostic.related()
291 | }
292 |
293 | fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> {
294 | self.diagnostic.diagnostic_source()
295 | }
296 | }
297 |
--------------------------------------------------------------------------------
/src/nu/module.rs:
--------------------------------------------------------------------------------
1 | use indoc::indoc;
2 | use nu_protocol::ast::{Argument, Block, Expr};
3 | use nu_protocol::engine::{EngineState, StateWorkingSet};
4 | use nu_protocol::{BlockId, DeclId, Span};
5 |
6 | const MODULE: &'static str = indoc! {r#"
7 | module nuju {
8 | module render {
9 | @kernel-internal
10 | def "text" []: any -> string {...}
11 |
12 | @kernel-internal
13 | def "csv" []: any -> string {...}
14 |
15 | @kernel-internal
16 | def "json" []: any -> string {...}
17 |
18 | @kernel-internal
19 | def "html" []: any -> string {...}
20 |
21 | @kernel-internal
22 | def "md" []: any -> string {...}
23 |
24 | @kernel-internal
25 | def "svg" []: any -> string {...}
26 | }
27 | }
28 | "#};
29 |
30 | #[derive(Debug, Clone)]
31 | pub struct KernelInternalSpans {
32 | pub render: KernelInternalRenderSpans,
33 | }
34 |
35 | #[derive(Debug, Clone)]
36 | pub struct KernelInternalRenderSpans {
37 | pub text: Span,
38 | pub csv: Span,
39 | pub json: Span,
40 | pub html: Span,
41 | pub md: Span,
42 | pub svg: Span,
43 | }
44 |
45 | pub fn create_nuju_module(engine_state: &mut EngineState) -> KernelInternalSpans {
46 | let mut working_set = StateWorkingSet::new(&engine_state);
47 |
48 | let file_id = working_set.add_file("nuju internals".to_string(), MODULE.as_bytes());
49 | let file_span = working_set.get_span_for_file(file_id);
50 |
51 | let (outer_block, ..) = nu_parser::parse_module_block(&mut working_set, file_span, b"nuju");
52 | let nuju_block_id = find_inner_block(&outer_block, "nuju").expect("find nuju block");
53 | let nuju_block = working_set.get_block(nuju_block_id);
54 | let render_block_id = find_inner_block(&nuju_block, "render").expect("find nuju/render block");
55 | let render_block = working_set.get_block(render_block_id);
56 |
57 | let (_, render_text_span) = find_decl(&render_block, "text").expect("find render/text decl");
58 | let (_, render_csv_span) = find_decl(&render_block, "csv").expect("find render/csv decl");
59 | let (_, render_json_span) = find_decl(&render_block, "json").expect("find render/json decl");
60 | let (_, render_html_span) = find_decl(&render_block, "html").expect("find render/html decl");
61 | let (_, render_md_span) = find_decl(&render_block, "md").expect("find render/md decl");
62 | let (_, render_svg_span) = find_decl(&render_block, "svg").expect("find render/svg decl");
63 |
64 | engine_state
65 | .merge_delta(working_set.delta)
66 | .expect("merge nuju module delta");
67 |
68 | KernelInternalSpans {
69 | render: KernelInternalRenderSpans {
70 | text: render_text_span,
71 | csv: render_csv_span,
72 | json: render_json_span,
73 | html: render_html_span,
74 | md: render_md_span,
75 | svg: render_svg_span,
76 | },
77 | }
78 | }
79 |
80 | fn find_inner_block(block: &Block, name: &str) -> Option {
81 | for pipeline in block.pipelines.iter() {
82 | for element in pipeline.elements.iter() {
83 | if let Expr::Call(call) = &element.expr.expr {
84 | if let Some(Argument::Positional(expr)) = call.arguments.get(0) {
85 | if let Expr::String(positional_name) = &expr.expr {
86 | if positional_name == name {
87 | if let Some(Argument::Positional(expr)) = call.arguments.get(1) {
88 | if let Expr::Block(block_id) = &expr.expr {
89 | return Some(*block_id);
90 | }
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | None
100 | }
101 |
102 | fn find_decl(block: &Block, name: &str) -> Option<(DeclId, Span)> {
103 | for pipeline in block.pipelines.iter() {
104 | for element in pipeline.elements.iter() {
105 | if let Expr::AttributeBlock(attribute_block) = &element.expr.expr {
106 | if let Expr::Call(call) = &attribute_block.item.expr {
107 | if let Some(Argument::Positional(expr)) = call.arguments.get(0) {
108 | if let Expr::String(positional_name) = &expr.expr {
109 | if positional_name == name {
110 | // TODO: return expression span if it includes the `@`
111 | return Some((call.decl_id, attribute_block.item.span));
112 | }
113 | }
114 | }
115 | }
116 | }
117 | }
118 | }
119 |
120 | None
121 | }
122 |
123 | #[cfg(test)]
124 | mod tests {
125 | use super::*;
126 |
127 | #[test]
128 | fn adding_nuju_module_works() {
129 | let engine_state = EngineState::default();
130 | let mut engine_state = nu_cmd_lang::create_default_context();
131 | let spans = create_nuju_module(&mut engine_state);
132 | assert_eq!(
133 | engine_state.get_span_contents(spans.render.json),
134 | br#"def "json" []: any -> string {...}"#
135 | )
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/nu/render.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use mime::Mime;
4 | use nu_command::{ToCsv, ToJson, ToMd};
5 | use nu_plotters::commands::draw::DrawSvg;
6 | use nu_protocol::ast::{Argument, Call};
7 | use nu_protocol::debugger::WithoutDebug;
8 | use nu_protocol::engine::{Command, EngineState, Stack};
9 | use nu_protocol::{DeclId, PipelineData, ShellError, Span, Spanned, Value};
10 | use thiserror::Error;
11 |
12 | use super::module::KernelInternalSpans;
13 | use crate::error::KernelError;
14 |
15 | macro_rules! create_format_decl_ids {
16 | ($($field:ident : $search_str:expr),+ $(,)?) => {
17 | #[derive(Debug, Clone, Copy)]
18 | pub struct FormatDeclIds {
19 | $(pub $field: DeclId,)+
20 | }
21 |
22 | impl FormatDeclIds {
23 | pub fn find(engine_state: &EngineState) -> Result {
24 | $(let mut $field = None;)+
25 |
26 | for (str_bytes, decl_id) in engine_state.get_decls_sorted(false) {
27 | let Ok(s) = String::from_utf8(str_bytes) else { continue };
28 | match s.as_str() {
29 | $($search_str => $field = Some(decl_id),)+
30 | _ => (),
31 | }
32 | }
33 |
34 | if let ($(Some($field),)+) = ($($field,)+) {
35 | return Ok(FormatDeclIds {
36 | $($field,)+
37 | });
38 | }
39 |
40 | let mut missing = Vec::new();
41 | $(if $field.is_none() { missing.push($search_str) })+
42 | Err(KernelError::MissingFormatDecls {missing})
43 | }
44 | }
45 | };
46 | }
47 |
48 | create_format_decl_ids!(
49 | to_text: "to text",
50 | to_csv: "to csv",
51 | to_json: "to json",
52 | to_html: "to html",
53 | to_md: "to md",
54 | table: "table",
55 | // TODO: make this feature flagged
56 | draw_svg: "draw svg"
57 | );
58 |
59 | fn flag(flag: impl Into, span: Span) -> Argument {
60 | Argument::Named((
61 | Spanned {
62 | item: flag.into(),
63 | span,
64 | },
65 | None,
66 | None,
67 | ))
68 | }
69 |
70 | #[derive(Debug, Error)]
71 | pub enum RenderError {
72 | #[error("could not convert pipeline data into value: {0}")]
73 | IntoValue(#[source] ShellError),
74 |
75 | #[error("could not render plain text output: {0}")]
76 | NoText(#[source] ShellError),
77 | }
78 |
79 | #[derive(Debug)]
80 | enum InternalRenderError {
81 | Eval(ShellError),
82 | IntoValue(ShellError),
83 | NoString(ShellError),
84 | }
85 |
86 | #[derive(Debug)]
87 | pub struct PipelineRender {
88 | pub data: HashMap,
89 | pub metadata: HashMap,
90 | }
91 |
92 | impl PipelineRender {
93 | fn render_via_cmd(
94 | value: &Value,
95 | to_cmd: impl Command,
96 | decl_id: DeclId,
97 | engine_state: &EngineState,
98 | span: Span,
99 | stack: &mut Stack,
100 | ) -> Result, InternalRenderError> {
101 | let ty = value.get_type();
102 | let may = to_cmd
103 | .signature()
104 | .input_output_types
105 | .iter()
106 | .map(|(input, _)| input)
107 | .any(|input| ty.is_subtype_of(input));
108 |
109 | match may {
110 | false => Ok(None),
111 | true => {
112 | Self::render_via_call(value.clone(), decl_id, engine_state, stack, span, vec![])
113 | .map(Option::Some)
114 | }
115 | }
116 | }
117 |
118 | fn render_via_call(
119 | value: Value,
120 | decl_id: DeclId,
121 | engine_state: &EngineState,
122 | stack: &mut Stack,
123 | span: Span,
124 | arguments: Vec,
125 | ) -> Result {
126 | let pipeline_data = PipelineData::Value(value, None);
127 | let call = Call {
128 | decl_id,
129 | head: span,
130 | arguments,
131 | parser_info: HashMap::new(),
132 | };
133 | let formatted =
134 | nu_engine::eval_call::(engine_state, stack, &call, pipeline_data)
135 | .map_err(InternalRenderError::Eval)?;
136 | let formatted = formatted
137 | .into_value(Span::unknown())
138 | .map_err(InternalRenderError::IntoValue)?
139 | .into_string()
140 | .map_err(InternalRenderError::NoString)?;
141 | Ok(formatted)
142 | }
143 |
144 | pub fn render(
145 | pipeline_data: PipelineData,
146 | engine_state: &EngineState,
147 | stack: &mut Stack,
148 | spans: &KernelInternalSpans,
149 | format_decl_ids: FormatDeclIds,
150 | filter: Option,
151 | ) -> Result {
152 | let mut data = HashMap::new();
153 | let metadata = HashMap::new();
154 | let value = pipeline_data
155 | .into_value(Span::unknown())
156 | .map_err(RenderError::IntoValue)?;
157 | let ty = value.get_type();
158 |
159 | // `to text` has any input type, no need to check
160 | // also we always need to provide plain text output
161 | match Self::render_via_call(
162 | value.clone(),
163 | format_decl_ids.to_text,
164 | engine_state,
165 | stack,
166 | spans.render.text,
167 | vec![],
168 | ) {
169 | Ok(s) => data.insert(mime::TEXT_PLAIN, s),
170 | Err(
171 | InternalRenderError::Eval(e) |
172 | InternalRenderError::IntoValue(e) |
173 | InternalRenderError::NoString(e),
174 | ) => return Err(RenderError::NoText(e)),
175 | };
176 |
177 | let match_filter = |mime| filter.is_none() || filter == Some(mime);
178 |
179 | // call directly as `ToHtml` is private
180 | if match_filter(mime::TEXT_HTML) {
181 | let span = spans.render.html;
182 | match Self::render_via_call(
183 | value.clone(),
184 | format_decl_ids.to_html,
185 | engine_state,
186 | stack,
187 | span,
188 | vec![flag("partial", span), flag("html-color", span)],
189 | ) {
190 | Ok(s) => data.insert(mime::TEXT_HTML, s),
191 | Err(InternalRenderError::Eval(_)) => None,
192 | Err(_) => None, // TODO: print the error
193 | };
194 | }
195 |
196 | if match_filter(mime::TEXT_CSV) {
197 | match Self::render_via_cmd(
198 | &value,
199 | ToCsv,
200 | format_decl_ids.to_csv,
201 | engine_state,
202 | spans.render.csv,
203 | stack,
204 | ) {
205 | Ok(Some(s)) => data.insert(mime::TEXT_CSV, s),
206 | Ok(None) | Err(InternalRenderError::Eval(_)) => None,
207 | Err(_) => None, // TODO: print the error
208 | };
209 | }
210 |
211 | if match_filter(mime::APPLICATION_JSON) {
212 | match Self::render_via_cmd(
213 | &value,
214 | ToJson,
215 | format_decl_ids.to_json,
216 | engine_state,
217 | spans.render.json,
218 | stack,
219 | ) {
220 | Ok(Some(s)) => data.insert(mime::APPLICATION_JSON, s),
221 | Ok(None) | Err(InternalRenderError::Eval(_)) => None,
222 | Err(_) => None, // TODO: print the error
223 | };
224 | }
225 |
226 | let md_mime: mime::Mime = "text/markdown"
227 | .parse()
228 | .expect("'text/markdown' is valid mime type");
229 | if match_filter(md_mime.clone()) {
230 | match Self::render_via_cmd(
231 | &value,
232 | ToMd,
233 | format_decl_ids.to_md,
234 | engine_state,
235 | spans.render.md,
236 | stack,
237 | ) {
238 | Ok(Some(s)) => data.insert(md_mime, s),
239 | Ok(None) | Err(InternalRenderError::Eval(_)) => None,
240 | Err(_) => None, // TODO: print the error
241 | };
242 | }
243 |
244 | // TODO: feature flag this
245 | if match_filter(mime::IMAGE_SVG) {
246 | match Self::render_via_cmd(
247 | &value,
248 | DrawSvg,
249 | format_decl_ids.draw_svg,
250 | engine_state,
251 | spans.render.svg,
252 | stack,
253 | ) {
254 | Ok(Some(s)) => data.insert(mime::IMAGE_SVG, s),
255 | Ok(None) | Err(InternalRenderError::Eval(_)) => None,
256 | Err(_) => None, // TODO: print the error
257 | };
258 | }
259 |
260 | Ok(PipelineRender { data, metadata })
261 | }
262 | }
263 |
264 | #[derive(Debug)]
265 | pub struct StringifiedPipelineRender {
266 | pub data: HashMap,
267 | pub metadata: HashMap,
268 | }
269 |
270 | impl From for StringifiedPipelineRender {
271 | fn from(render: PipelineRender) -> Self {
272 | Self {
273 | data: render
274 | .data
275 | .into_iter()
276 | .map(|(k, v)| (k.to_string(), v))
277 | .collect(),
278 | metadata: render
279 | .metadata
280 | .into_iter()
281 | .map(|(k, v)| (k.to_string(), v))
282 | .collect(),
283 | }
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/src/util.rs:
--------------------------------------------------------------------------------
1 | #[derive(Debug, Clone)]
2 | pub enum Select {
3 | Left(L),
4 | Right(R),
5 | }
6 |
--------------------------------------------------------------------------------
/tests/test_kernel.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 | import tomllib
5 | from jupyter_client import BlockingKernelClient, KernelManager
6 |
7 |
8 | TIMEOUT = 10
9 |
10 |
11 | @pytest.fixture
12 | def kernel():
13 | km = KernelManager(kernel_name="nu")
14 | km.start_kernel()
15 | yield km.client()
16 | km.shutdown_kernel()
17 |
18 |
19 | def ok(client: BlockingKernelClient, code: str) -> list[dict]:
20 | # wait until client is ready, then send some code
21 | client.wait_for_ready(timeout=TIMEOUT)
22 | client.execute(code)
23 |
24 | # kernel should instantly reply with busy message
25 | busy_status = client.get_iopub_msg(timeout=TIMEOUT)
26 | assert busy_status["content"]["execution_state"] == "busy"
27 |
28 | # check on the iopub channel until we receive an idle message
29 | contents = []
30 | while True:
31 | iopub_reply = client.get_iopub_msg(timeout=TIMEOUT)
32 | if iopub_reply["content"].get("execution_state") == "idle":
33 | break
34 | contents.append(iopub_reply["content"])
35 |
36 | # we should get a ok on the shell channel
37 | shell_reply = client.get_shell_msg(timeout=TIMEOUT)
38 | assert shell_reply["content"]["status"] == "ok"
39 |
40 | return contents
41 |
42 |
43 | def test_kernel_info(kernel: BlockingKernelClient):
44 | kernel.wait_for_ready(timeout=TIMEOUT)
45 | kernel.kernel_info()
46 |
47 | # control channel not used in jupyter_client
48 | # control_kernel_info = kernel.get_control_msg(timeout=TIMEOUT)
49 | shell_kernel_info = kernel.get_shell_msg(timeout=TIMEOUT)
50 |
51 | # assert control_kernel_info["content"] == shell_kernel_info["content"]
52 | kernel_info = shell_kernel_info["content"]
53 |
54 | with open("Cargo.toml", "rb") as cargo_toml_file:
55 | cargo_toml = tomllib.load(cargo_toml_file)
56 | package = cargo_toml["package"]
57 | metadata = package["metadata"]["jupyter"]
58 | nu_engine = cargo_toml["workspace"]["dependencies"]["nu-engine"]
59 | assert kernel_info["protocol_version"] == metadata["protocol_version"]
60 | assert kernel_info["implementation"] == package["name"]
61 | assert kernel_info["implementation_version"] == package["version"]
62 | assert kernel_info["language_info"]["name"] == "nushell"
63 | assert kernel_info["language_info"]["version"] == nu_engine["version"]
64 | assert kernel_info["language_info"]["file_extension"] == ".nu"
65 |
66 |
67 | def test_basic_rendering(kernel: BlockingKernelClient):
68 | contents = ok(kernel, "$nuju")
69 | assert len(contents) == 1
70 | data = contents[0]["data"]
71 | assert "application/json" in data
72 | assert "text/plain" in data
73 | assert "text/html" in data
74 | assert "text/markdown" in data
75 |
76 |
77 | def test_nuju_content(kernel: BlockingKernelClient):
78 | contents = ok(kernel, "$nuju")
79 | assert len(contents) == 1
80 | data = contents[0]["data"]
81 | nuju_constant = json.loads(data["application/json"])
82 | with open("Cargo.toml", "rb") as cargo_toml_file:
83 | cargo_toml = tomllib.load(cargo_toml_file)
84 | assert nuju_constant["version"]["kernel"] == cargo_toml["package"]["version"]
85 |
86 | nu_version = cargo_toml["workspace"]["dependencies"]["nu-engine"]["version"]
87 | assert nuju_constant["version"]["nu"] == nu_version
88 |
89 |
90 | def test_persistence(kernel: BlockingKernelClient):
91 | set_value = ok(kernel, "let foo = 'bar'")
92 | assert len(set_value) == 0
93 |
94 | get_value = ok(kernel, "$foo")
95 | assert len(get_value) == 1
96 |
97 | assert get_value[0]["data"]["text/plain"] == "bar"
98 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | requires-python = ">=3.12"
3 |
4 | [[package]]
5 | name = "cffi"
6 | version = "1.17.1"
7 | source = { registry = "https://pypi.org/simple" }
8 | dependencies = [
9 | { name = "pycparser" },
10 | ]
11 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
12 | wheels = [
13 | { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
14 | { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
15 | { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
16 | { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
17 | { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
18 | { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
19 | { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
20 | { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
21 | { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
22 | { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
23 | { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
24 | { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
25 | { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
26 | { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
27 | { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
28 | { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
29 | { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
30 | { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
31 | { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
32 | { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
33 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
34 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
35 | ]
36 |
37 | [[package]]
38 | name = "colorama"
39 | version = "0.4.6"
40 | source = { registry = "https://pypi.org/simple" }
41 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
42 | wheels = [
43 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
44 | ]
45 |
46 | [[package]]
47 | name = "iniconfig"
48 | version = "2.0.0"
49 | source = { registry = "https://pypi.org/simple" }
50 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
51 | wheels = [
52 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
53 | ]
54 |
55 | [[package]]
56 | name = "jupyter-client"
57 | version = "8.6.2"
58 | source = { registry = "https://pypi.org/simple" }
59 | dependencies = [
60 | { name = "jupyter-core" },
61 | { name = "python-dateutil" },
62 | { name = "pyzmq" },
63 | { name = "tornado" },
64 | { name = "traitlets" },
65 | ]
66 | sdist = { url = "https://files.pythonhosted.org/packages/ff/61/3cd51dea7878691919adc34ff6ad180f13bfe25fb8c7662a9ee6dc64e643/jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df", size = 341102 }
67 | wheels = [
68 | { url = "https://files.pythonhosted.org/packages/cf/d3/c4bb02580bc0db807edb9a29b2d0c56031be1ef0d804336deb2699a470f6/jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f", size = 105901 },
69 | ]
70 |
71 | [[package]]
72 | name = "jupyter-core"
73 | version = "5.7.2"
74 | source = { registry = "https://pypi.org/simple" }
75 | dependencies = [
76 | { name = "platformdirs" },
77 | { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" },
78 | { name = "traitlets" },
79 | ]
80 | sdist = { url = "https://files.pythonhosted.org/packages/00/11/b56381fa6c3f4cc5d2cf54a7dbf98ad9aa0b339ef7a601d6053538b079a7/jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9", size = 87629 }
81 | wheels = [
82 | { url = "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", size = 28965 },
83 | ]
84 |
85 | [[package]]
86 | name = "nu-jupyter-kernel-test"
87 | version = "0.0.0"
88 | source = { virtual = "." }
89 | dependencies = [
90 | { name = "jupyter-client" },
91 | { name = "pytest" },
92 | { name = "pytest-timeout" },
93 | ]
94 |
95 | [package.metadata]
96 | requires-dist = [
97 | { name = "jupyter-client", specifier = ">=8.6.2" },
98 | { name = "pytest", specifier = ">=8.3.3" },
99 | { name = "pytest-timeout", specifier = ">=2.3.1" },
100 | ]
101 |
102 | [[package]]
103 | name = "packaging"
104 | version = "24.1"
105 | source = { registry = "https://pypi.org/simple" }
106 | sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 }
107 | wheels = [
108 | { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
109 | ]
110 |
111 | [[package]]
112 | name = "platformdirs"
113 | version = "4.3.3"
114 | source = { registry = "https://pypi.org/simple" }
115 | sdist = { url = "https://files.pythonhosted.org/packages/f5/19/f7bee3a71decedd8d7bc4d3edb7970b8e899f3caef257b0f0d623f2f7b11/platformdirs-4.3.3.tar.gz", hash = "sha256:d4e0b7d8ec176b341fb03cb11ca12d0276faa8c485f9cd218f613840463fc2c0", size = 21304 }
116 | wheels = [
117 | { url = "https://files.pythonhosted.org/packages/69/e6/7c8e8c326903bd97c6c0c47e0a3c5de815faaae986cab7defdeddf5fddcd/platformdirs-4.3.3-py3-none-any.whl", hash = "sha256:50a5450e2e84f44539718293cbb1da0a0885c9d14adf21b77bae4e66fc99d9b5", size = 18437 },
118 | ]
119 |
120 | [[package]]
121 | name = "pluggy"
122 | version = "1.5.0"
123 | source = { registry = "https://pypi.org/simple" }
124 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
125 | wheels = [
126 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
127 | ]
128 |
129 | [[package]]
130 | name = "pycparser"
131 | version = "2.22"
132 | source = { registry = "https://pypi.org/simple" }
133 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
134 | wheels = [
135 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
136 | ]
137 |
138 | [[package]]
139 | name = "pytest"
140 | version = "8.3.3"
141 | source = { registry = "https://pypi.org/simple" }
142 | dependencies = [
143 | { name = "colorama", marker = "sys_platform == 'win32'" },
144 | { name = "iniconfig" },
145 | { name = "packaging" },
146 | { name = "pluggy" },
147 | ]
148 | sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 }
149 | wheels = [
150 | { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
151 | ]
152 |
153 | [[package]]
154 | name = "pytest-timeout"
155 | version = "2.3.1"
156 | source = { registry = "https://pypi.org/simple" }
157 | dependencies = [
158 | { name = "pytest" },
159 | ]
160 | sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 }
161 | wheels = [
162 | { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 },
163 | ]
164 |
165 | [[package]]
166 | name = "python-dateutil"
167 | version = "2.9.0.post0"
168 | source = { registry = "https://pypi.org/simple" }
169 | dependencies = [
170 | { name = "six" },
171 | ]
172 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
173 | wheels = [
174 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
175 | ]
176 |
177 | [[package]]
178 | name = "pywin32"
179 | version = "306"
180 | source = { registry = "https://pypi.org/simple" }
181 | wheels = [
182 | { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705 },
183 | { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429 },
184 | { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145 },
185 | ]
186 |
187 | [[package]]
188 | name = "pyzmq"
189 | version = "26.2.0"
190 | source = { registry = "https://pypi.org/simple" }
191 | dependencies = [
192 | { name = "cffi", marker = "implementation_name == 'pypy'" },
193 | ]
194 | sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 }
195 | wheels = [
196 | { url = "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", size = 1343105 },
197 | { url = "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", size = 1008365 },
198 | { url = "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", size = 665923 },
199 | { url = "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", size = 903400 },
200 | { url = "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", size = 860034 },
201 | { url = "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", size = 860579 },
202 | { url = "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", size = 1196246 },
203 | { url = "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", size = 1507441 },
204 | { url = "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", size = 1406498 },
205 | { url = "https://files.pythonhosted.org/packages/07/18/907134c85c7152f679ed744e73e645b365f3ad571f38bdb62e36f347699a/pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", size = 575533 },
206 | { url = "https://files.pythonhosted.org/packages/ce/2c/a6f4a20202a4d3c582ad93f95ee78d79bbdc26803495aec2912b17dbbb6c/pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", size = 637768 },
207 | { url = "https://files.pythonhosted.org/packages/5f/0e/eb16ff731632d30554bf5af4dbba3ffcd04518219d82028aea4ae1b02ca5/pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", size = 540675 },
208 | { url = "https://files.pythonhosted.org/packages/04/a7/0f7e2f6c126fe6e62dbae0bc93b1bd3f1099cf7fea47a5468defebe3f39d/pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", size = 1006564 },
209 | { url = "https://files.pythonhosted.org/packages/31/b6/a187165c852c5d49f826a690857684333a6a4a065af0a6015572d2284f6a/pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", size = 1340447 },
210 | { url = "https://files.pythonhosted.org/packages/68/ba/f4280c58ff71f321602a6e24fd19879b7e79793fb8ab14027027c0fb58ef/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", size = 665485 },
211 | { url = "https://files.pythonhosted.org/packages/77/b5/c987a5c53c7d8704216f29fc3d810b32f156bcea488a940e330e1bcbb88d/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", size = 903484 },
212 | { url = "https://files.pythonhosted.org/packages/29/c9/07da157d2db18c72a7eccef8e684cefc155b712a88e3d479d930aa9eceba/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", size = 859981 },
213 | { url = "https://files.pythonhosted.org/packages/43/09/e12501bd0b8394b7d02c41efd35c537a1988da67fc9c745cae9c6c776d31/pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", size = 860334 },
214 | { url = "https://files.pythonhosted.org/packages/eb/ff/f5ec1d455f8f7385cc0a8b2acd8c807d7fade875c14c44b85c1bddabae21/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", size = 1196179 },
215 | { url = "https://files.pythonhosted.org/packages/ec/8a/bb2ac43295b1950fe436a81fc5b298be0b96ac76fb029b514d3ed58f7b27/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", size = 1507668 },
216 | { url = "https://files.pythonhosted.org/packages/a9/49/dbc284ebcfd2dca23f6349227ff1616a7ee2c4a35fe0a5d6c3deff2b4fed/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", size = 1406539 },
217 | { url = "https://files.pythonhosted.org/packages/00/68/093cdce3fe31e30a341d8e52a1ad86392e13c57970d722c1f62a1d1a54b6/pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", size = 575567 },
218 | { url = "https://files.pythonhosted.org/packages/92/ae/6cc4657148143412b5819b05e362ae7dd09fb9fe76e2a539dcff3d0386bc/pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", size = 637551 },
219 | { url = "https://files.pythonhosted.org/packages/6c/67/fbff102e201688f97c8092e4c3445d1c1068c2f27bbd45a578df97ed5f94/pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", size = 540378 },
220 | { url = "https://files.pythonhosted.org/packages/3f/fe/2d998380b6e0122c6c4bdf9b6caf490831e5f5e2d08a203b5adff060c226/pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", size = 1007378 },
221 | { url = "https://files.pythonhosted.org/packages/4a/f4/30d6e7157f12b3a0390bde94d6a8567cdb88846ed068a6e17238a4ccf600/pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", size = 1329532 },
222 | { url = "https://files.pythonhosted.org/packages/82/86/3fe917870e15ee1c3ad48229a2a64458e36036e64b4afa9659045d82bfa8/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", size = 653242 },
223 | { url = "https://files.pythonhosted.org/packages/50/2d/242e7e6ef6c8c19e6cb52d095834508cd581ffb925699fd3c640cdc758f1/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", size = 888404 },
224 | { url = "https://files.pythonhosted.org/packages/ac/11/7270566e1f31e4ea73c81ec821a4b1688fd551009a3d2bab11ec66cb1e8f/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", size = 845858 },
225 | { url = "https://files.pythonhosted.org/packages/91/d5/72b38fbc69867795c8711bdd735312f9fef1e3d9204e2f63ab57085434b9/pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", size = 847375 },
226 | { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 },
227 | { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 },
228 | { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 },
229 | ]
230 |
231 | [[package]]
232 | name = "six"
233 | version = "1.16.0"
234 | source = { registry = "https://pypi.org/simple" }
235 | sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
236 | wheels = [
237 | { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
238 | ]
239 |
240 | [[package]]
241 | name = "tornado"
242 | version = "6.4.1"
243 | source = { registry = "https://pypi.org/simple" }
244 | sdist = { url = "https://files.pythonhosted.org/packages/ee/66/398ac7167f1c7835406888a386f6d0d26ee5dbf197d8a571300be57662d3/tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9", size = 500623 }
245 | wheels = [
246 | { url = "https://files.pythonhosted.org/packages/00/d9/c33be3c1a7564f7d42d87a8d186371a75fd142097076767a5c27da941fef/tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8", size = 435924 },
247 | { url = "https://files.pythonhosted.org/packages/2e/0f/721e113a2fac2f1d7d124b3279a1da4c77622e104084f56119875019ffab/tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14", size = 433883 },
248 | { url = "https://files.pythonhosted.org/packages/13/cf/786b8f1e6fe1c7c675e79657448178ad65e41c1c9765ef82e7f6f765c4c5/tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4", size = 437224 },
249 | { url = "https://files.pythonhosted.org/packages/e4/8e/a6ce4b8d5935558828b0f30f3afcb2d980566718837b3365d98e34f6067e/tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842", size = 436597 },
250 | { url = "https://files.pythonhosted.org/packages/22/d4/54f9d12668b58336bd30defe0307e6c61589a3e687b05c366f804b7faaf0/tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3", size = 436797 },
251 | { url = "https://files.pythonhosted.org/packages/cf/3f/2c792e7afa7dd8b24fad7a2ed3c2f24a5ec5110c7b43a64cb6095cc106b8/tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f", size = 437516 },
252 | { url = "https://files.pythonhosted.org/packages/71/63/c8fc62745e669ac9009044b889fc531b6f88ac0f5f183cac79eaa950bb23/tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4", size = 436958 },
253 | { url = "https://files.pythonhosted.org/packages/94/d4/f8ac1f5bd22c15fad3b527e025ce219bd526acdbd903f52053df2baecc8b/tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698", size = 436882 },
254 | { url = "https://files.pythonhosted.org/packages/4b/3e/a8124c21cc0bbf144d7903d2a0cadab15cadaf683fa39a0f92bc567f0d4d/tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d", size = 438092 },
255 | { url = "https://files.pythonhosted.org/packages/d9/2f/3f2f05e84a7aff787a96d5fb06821323feb370fe0baed4db6ea7b1088f32/tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", size = 438532 },
256 | ]
257 |
258 | [[package]]
259 | name = "traitlets"
260 | version = "5.14.3"
261 | source = { registry = "https://pypi.org/simple" }
262 | sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 }
263 | wheels = [
264 | { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 },
265 | ]
266 |
--------------------------------------------------------------------------------