diff --git a/.gitignore b/.gitignore index ee19741..5cdd539 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /target /src/target -/pkg -*.pkg.* \ No newline at end of file +/pkg \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 62b4b82..8a0e193 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "cfg-if" version = "1.0.0" @@ -28,6 +34,7 @@ dependencies = [ "num", "termion", "toml", + "wasm-bindgen", ] [[package]] @@ -52,6 +59,12 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "memchr" version = "2.5.0" @@ -140,6 +153,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -173,6 +210,17 @@ dependencies = [ "serde", ] +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termion" version = "2.0.1" @@ -219,6 +267,66 @@ dependencies = [ "winnow", ] +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + [[package]] name = "winnow" version = "0.4.6" diff --git a/Cargo.toml b/Cargo.toml index 052610a..941f747 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" [lib] name = "daisycalc" path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] [profile.release] opt-level = 3 @@ -32,10 +33,15 @@ cfg-if = "1.0.0" num = "0.4.1" #astro-float = "0.7.1" +[package.metadata.wasm-pack.profile.release] +wasm-opt = true [target.'cfg(target_family = "unix")'.dependencies] termion = "2.0.1" +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + [build-dependencies] toml = "0.7.4" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1aacafc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM ubuntu AS node +RUN apt-get update +RUN apt-get install cargo npm -y +COPY ./site ./site +RUN cd /site && npm install + +FROM ubuntu +RUN apt-get update +RUN apt-get install nginx -y +COPY --from=node /site /var/www/html +COPY ./pkg /var/www/html/pkg +COPY default /etc/nginx/sites-enabled/default +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md index fe53747..9d7c662 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -![](./misc/banner.png) +![](./site/misc/readme-banner.png) A high-precision scientific calculator with support for units, derivatives, and more. Many features are missing, this is still under development. +**Web demo: [here](daisy.betalupi.com)** + # 📦 Installation - **Cargo:** `cargo install daisycalc` - **Arch:** `yay -S daisy` diff --git a/TODO.md b/TODO.md index 8a7ebba..ef9106d 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,7 @@ - git tag -a v1.0.0 -m "Version 1.0.0" on merge commit - cargo publish - Update packages + - Build wasm & push changes ## Pre-release @@ -24,7 +25,6 @@ - Better tests (assignment, many expressions in one context) - Optional config file - Optional history file - - Compile to WASM, publish a webapp - evaluate straight from command line - Package for debian, nix diff --git a/default b/default new file mode 100644 index 0000000..ec6d36d --- /dev/null +++ b/default @@ -0,0 +1,31 @@ +types { + application/wasm wasm; + application/x-font-ttf ttc; + application/x-font-otf otf; + application/font-woff2 woff2; + font/ttf ttf; +} + + +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /var/www/html; + + # Add index.php to the list if you are using PHP + index index.html index.htm index.nginx-debian.html; + + server_name _; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + } + + location ~* .(js|css|ttf|ttc|otf|eot|woff|woff2)$ { + add_header access-control-allow-origin "*"; + expires max; + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e52a66d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3" + +# You'll need to edit this file if you want to use it. +networks: + reverse_proxy: + external: true + +services: + daisy: + build: ./. + container_name: daisy + restart: unless-stopped + + networks: + - reverse_proxy + + #ports: + # - "80:80" \ No newline at end of file diff --git a/misc/daisy.svg b/misc/daisy.svg deleted file mode 100644 index 4fb6cc0..0000000 --- a/misc/daisy.svg +++ /dev/null @@ -1,240 +0,0 @@ - - - -DaisyDaisyDaisy diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..6bd0e57 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/package-lock.json +/pkg \ No newline at end of file diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..e10b735 --- /dev/null +++ b/site/index.html @@ -0,0 +1,152 @@ + + + + + Daisy Web + + + + + + + + + + + +
+ + + + + + diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..b1c1b1d --- /dev/null +++ b/site/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "xterm": "^5.3.0" + } +} diff --git a/site/resources/Fantasque.ttf b/site/resources/Fantasque.ttf new file mode 100755 index 0000000..6cb7375 Binary files /dev/null and b/site/resources/Fantasque.ttf differ diff --git a/site/resources/banner.svg b/site/resources/banner.svg new file mode 100644 index 0000000..efa19b7 --- /dev/null +++ b/site/resources/banner.svg @@ -0,0 +1,137 @@ + + + + diff --git a/site/resources/daisy-dark.svg b/site/resources/daisy-dark.svg new file mode 100644 index 0000000..2eb030b --- /dev/null +++ b/site/resources/daisy-dark.svg @@ -0,0 +1,137 @@ + + + + diff --git a/site/resources/daisy-icon-dark.svg b/site/resources/daisy-icon-dark.svg new file mode 100644 index 0000000..cae6a08 --- /dev/null +++ b/site/resources/daisy-icon-dark.svg @@ -0,0 +1,133 @@ + + + + diff --git a/site/resources/daisy-icon-light.svg b/site/resources/daisy-icon-light.svg new file mode 100644 index 0000000..9c73081 --- /dev/null +++ b/site/resources/daisy-icon-light.svg @@ -0,0 +1,133 @@ + + + + diff --git a/site/resources/daisy-light.svg b/site/resources/daisy-light.svg new file mode 100644 index 0000000..a4610fb --- /dev/null +++ b/site/resources/daisy-light.svg @@ -0,0 +1,137 @@ + + + + diff --git a/misc/banner.png b/site/resources/readme-banner.png similarity index 100% rename from misc/banner.png rename to site/resources/readme-banner.png diff --git a/src/formattedtext.rs b/src/formattedtext.rs deleted file mode 100644 index ed7c6ef..0000000 --- a/src/formattedtext.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::io::Write; -use termion::raw::RawTerminal; -use termion::color; -use termion::style; -use termion::clear; -use termion::cursor; -use std::ops::Add; -use crate::context::Context; - - -#[derive(Debug)] -#[derive(Clone)] -pub struct FormattedText { - text: String -} - -impl ToString for FormattedText { - fn to_string(&self) -> String { return self.text.clone(); } -} - - -fn format_map_none(c: char) -> Option { - Some(match c { - 'n'|'i'|'t'|'a'| - 'e'|'c'|'s'|'r'| - 'p' - => { "".to_string() }, - _ => { return None } - }) -} - - -fn format_map_ansi(c: char) -> Option { - Some(match c { - 'n' => { // Normal text - format!("{}{}", color::Fg(color::Reset), color::Bg(color::Reset)) - }, - 'i' => { // Normal italic text - format!("{}{}", color::Fg(color::Reset), color::Bg(color::Reset)) - }, - 't' => { // Title text (should be cyan) - format!("{}{}", color::Fg(color::AnsiValue(6)), color::Bg(color::Reset)) - }, - 'a' => { // Colored text (should be pink) - format!("{}{}", color::Fg(color::AnsiValue(5)), color::Bg(color::Reset)) - }, - 'e' => { // Error titles (should be red) - format!("{}{}", color::Fg(color::AnsiValue(1)), color::Bg(color::Reset)) - }, - 'c' => { // Console text (inverted black on white) - format!("{}{}", color::Fg(color::AnsiValue(0)), color::Bg(color::AnsiValue(7))) - }, - 'p' => { // Input prompt (how ==> is styled) (should be blue) - format!("{}{}", color::Fg(color::AnsiValue(4)), color::Bg(color::Reset)) - }, - 's' => { // Repeat prompt (how => is styled) (should be pink) - format!("{}{}", color::Fg(color::AnsiValue(5)), color::Bg(color::Reset)) - }, - 'r' => { // Result prompt (how = is styled) (should be green) - format!("{}{}", color::Fg(color::AnsiValue(2)), color::Bg(color::Reset)) - }, - - _ => { return None } - }) -} - - -// style::reset also resets color. -// Make sure color comes AFTER style reset. -fn format_map_full(c: char) -> Option { - Some(match c { - 'n' => { // Normal text - format!("{}{}", style::Reset, color::Fg(color::Reset)) - }, - 'i' => { // Normal italic text - format!("{}{}", color::Fg(color::Reset), style::Italic) - }, - 't' => { // Title text - format!("{}{}", color::Fg(color::Magenta), style::Bold) - }, - 'a' => { // Colored text - format!("{}{}", style::Reset, color::Fg(color::Magenta)) - }, - 'e' => { // Error titles - format!("{}{}", color::Fg(color::Red), style::Bold) - }, - 'c' => { // Console text - format!("{}{}", color::Fg(color::LightBlack), style::Italic) - }, - 'p' => { // Input prompt (how ==> is styled) - format!("{}{}", color::Fg(color::Blue), style::Bold) - }, - 's' => { // Repeat prompt (how => is styled) - format!("{}{}", color::Fg(color::Magenta), style::Bold) - }, - 'r' => { // Result prompt (how = is styled) - format!("{}{}", color::Fg(color::Green), style::Bold) - }, - - - _ => { return None } - }) -} - - - - -impl FormattedText { - pub fn newline(stdout: &mut RawTerminal) -> Result<(), std::io::Error> { - write!(stdout, "\n")?; - return Ok(()); - } - - pub fn format_map(c: char, context: &Context) -> Option { - match context.config.term_color_type { - 0 => format_map_none(c), - 1 => format_map_ansi(c), - 2 => format_map_full(c), - _ => unreachable!("Invalid term_color_type") - } - } -} - - -impl FormattedText { - pub fn new(s: String) -> FormattedText { - return FormattedText { - text: s - } - } - - pub fn push(&mut self, s: &str) { - self.text.push_str(s); - } - - - pub fn write(&self, context: &Context, stdout: &mut RawTerminal) -> Result<(), std::io::Error> { - - if self.text == "[clear]" { - write!( - stdout, - "{}{}", - clear::All, - cursor::Goto(1, 1) - )?; - return Ok(()); - } - - - let mut s = String::new(); - let mut chars = self.text.chars(); - - while let Some(c) = chars.next() { - match c { - '[' => { - let a = chars.next().unwrap(); - - // Handle double [[ as escaped [ - if a == '[' { s.push('['); } - - let b = chars.next().unwrap(); - - match (a, b) { - (c, ']') => { // Normal text - - let q = Self::format_map(c, context); - - if q.is_some() { - s.push_str(&q.unwrap()); - } else { - s.push('['); - s.push(a); - s.push(b); - } - }, - - _ => { - s.push('['); - s.push(a); - s.push(b); - } - } - }, - '\n' => { s.push_str("\r\n") }, - _ => s.push(c) - } - } - - write!(stdout, "\r{}", s)?; - return Ok(()); - } -} - - -impl Add for FormattedText { - type Output = Self; - - fn add(self, other: Self) -> Self::Output { - return FormattedText::new(format!("{}{}", self.text, other.text)); - } -} \ No newline at end of file diff --git a/src/formattedtext/formattedtext.rs b/src/formattedtext/formattedtext.rs new file mode 100644 index 0000000..53188f3 --- /dev/null +++ b/src/formattedtext/formattedtext.rs @@ -0,0 +1,40 @@ +use std::ops::Add; +use std::ops::AddAssign; + + +#[derive(Debug)] +#[derive(Clone)] +pub struct FormattedText { + pub(super) text: String +} + +impl ToString for FormattedText { + fn to_string(&self) -> String { return self.text.clone(); } +} + +impl FormattedText { + pub fn new(s: String) -> FormattedText { + return FormattedText { + text: s + } + } + + pub fn push(&mut self, s: &str) { + self.text.push_str(s); + } +} + + +impl Add for FormattedText { + type Output = Self; + + fn add(self, other: Self) -> Self::Output { + return FormattedText::new(format!("{}{}", self.text, other.text)); + } +} + +impl AddAssign for FormattedText where { + fn add_assign(&mut self, other: Self) { + self.text.push_str(&other.text); + } +} \ No newline at end of file diff --git a/src/formattedtext/mod.rs b/src/formattedtext/mod.rs new file mode 100644 index 0000000..d30b4f7 --- /dev/null +++ b/src/formattedtext/mod.rs @@ -0,0 +1,12 @@ +mod formattedtext; +pub use formattedtext::FormattedText; + + +// Select write implementation by target system +cfg_if::cfg_if! { + if #[cfg(target_family = "unix")] { + mod unix_backend; + } else if #[cfg(target_arch = "wasm32")] { + mod wasm_backend; + } +} \ No newline at end of file diff --git a/src/formattedtext/unix_backend.rs b/src/formattedtext/unix_backend.rs new file mode 100644 index 0000000..789e14a --- /dev/null +++ b/src/formattedtext/unix_backend.rs @@ -0,0 +1,179 @@ +use super::FormattedText; +use std::io::Write; +use crate::context::Context; + +use termion::raw::RawTerminal; +use termion::color; +use termion::style; +use termion::clear; +use termion::cursor; + +fn format_map_ansi(s: &str) -> Option { + Some(match s { + "n" => { // Normal text + format!("{}{}", color::Fg(color::Reset), color::Bg(color::Reset)) + }, + "i" => { // Normal italic text + format!("{}{}", color::Fg(color::Reset), color::Bg(color::Reset)) + }, + "t" => { // Title text (should be cyan) + format!("{}{}", color::Fg(color::AnsiValue(6)), color::Bg(color::Reset)) + }, + "a" => { // Colored text (should be pink) + format!("{}{}", color::Fg(color::AnsiValue(5)), color::Bg(color::Reset)) + }, + "e" => { // Error titles (should be red) + format!("{}{}", color::Fg(color::AnsiValue(1)), color::Bg(color::Reset)) + }, + "c" => { // Console text (inverted black on white) + format!("{}{}", color::Fg(color::AnsiValue(0)), color::Bg(color::AnsiValue(7))) + }, + "p" => { // Input prompt (how ==> is styled) (should be blue) + format!("{}{}", color::Fg(color::AnsiValue(4)), color::Bg(color::Reset)) + }, + "s" => { // Repeat prompt (how => is styled) (should be pink) + format!("{}{}", color::Fg(color::AnsiValue(5)), color::Bg(color::Reset)) + }, + "r" => { // Result prompt (how = is styled) (should be green) + format!("{}{}", color::Fg(color::AnsiValue(2)), color::Bg(color::Reset)) + }, + + _ => { return None } + }) +} + + + +fn format_map_none(s: &str) -> Option { + Some(match s { + "n"|"i"|"t"|"a"| + "e"|"c"|"s"|"r"| + "p" + => { "".to_string() }, + _ => { return None } + }) +} + +// style::reset also resets color. +// Make sure color comes AFTER style reset. +fn format_map_full(s: &str) -> Option { + Some(match s { + "n" => { // Normal text + format!("{}{}", style::Reset, color::Fg(color::Reset)) + }, + "i" => { // Normal italic text + format!("{}{}", color::Fg(color::Reset), style::Italic) + }, + "t" => { // Title text + format!("{}{}", color::Fg(color::Magenta), style::Bold) + }, + "a" => { // Colored text + format!("{}{}", style::Reset, color::Fg(color::Magenta)) + }, + "e" => { // Error titles + format!("{}{}", color::Fg(color::Red), style::Bold) + }, + "c" => { // Console text + format!("{}{}", color::Fg(color::LightBlack), style::Italic) + }, + "p" => { // Input prompt (how ==> is styled) + format!("{}{}", color::Fg(color::Blue), style::Bold) + }, + "s" => { // Repeat prompt (how => is styled) + format!("{}{}", color::Fg(color::Magenta), style::Bold) + }, + "r" => { // Result prompt (how = is styled) + format!("{}{}", color::Fg(color::Green), style::Bold) + }, + + + _ => { return None } + }) +} + +impl FormattedText { + pub fn newline(stdout: &mut RawTerminal) -> Result<(), std::io::Error> { + write!(stdout, "\n")?; + return Ok(()); + } + + pub fn format_map(s: &str, context: &Context) -> Option { + match context.config.term_color_type { + 0 => format_map_none(s), + 1 => format_map_ansi(s), + 2 => format_map_full(s), + _ => unreachable!("Invalid term_color_type") + } + } + + pub fn write(&self, context: &Context, stdout: &mut RawTerminal) -> Result<(), std::io::Error> { + + let mut word = String::new(); + let mut reading = false; // are we reading a word? + let mut chars = self.text.chars(); + let mut out = String::new(); + + while let Some(c) = chars.next() { + + match c { + '[' => { + if reading { + // Discard old word, start reading again. + out.push_str(&word); + word.clear(); + } + + // Start reading a new word + reading = true; + word.push(c); + }, + + ']' => { + if !reading { + out.push(c); + } else { + word.push(c); + + + let f = Self::format_map(&word[1..word.len()-1], context); + + if f.is_some() { + out.push_str(&f.unwrap()); + } else if word == "[clear]" { + out.push_str(&format!( + "{}{}", + clear::All, + cursor::Goto(1, 1) + )); + } else if word.starts_with("[cursorright") { + let n: u16 = word[12..word.len()-1].parse().unwrap(); + out.push_str(&format!( + "{}", + cursor::Right(n), + )); + } else { + out.push_str(&word); + } + + reading = false; + word.clear(); + } + }, + + '\n' => { + if reading { word.push_str("\r\n"); } + else { out.push_str("\r\n"); } + }, + + _ => { + if reading { word.push(c); } + else { out.push(c); } + } + } + } + + write!(stdout, "\r{}", out)?; + stdout.flush()?; + return Ok(()); + } +} \ No newline at end of file diff --git a/src/formattedtext/wasm_backend.rs b/src/formattedtext/wasm_backend.rs new file mode 100644 index 0000000..61ac800 --- /dev/null +++ b/src/formattedtext/wasm_backend.rs @@ -0,0 +1,80 @@ +use super::FormattedText; + +fn format_map(s: &str) -> Option { + Some(match s { + "n" => {"\x1B[0m"}, + "i" => {"\x1B[3m"}, + "t" => {"\x1B[1;35m"}, + "a" => {"\x1B[0;35m"}, + "e" => {"\x1B[1;31m"}, + "c" => {"\x1B[3;90m"}, + "p" => {"\x1B[1;34m"}, + "s" => {"\x1B[1;35m"}, + "r" => {"\x1B[1;32m"}, + _ => { return None } + }.to_string()) +} + + +impl FormattedText { + pub fn write(&self) -> String { + + let mut word = String::new(); + let mut reading = false; // are we reading a word? + let mut chars = self.text.chars(); + let mut out = String::new(); + + while let Some(c) = chars.next() { + + match c { + '[' => { + if reading { + // Discard old word, start reading again. + out.push_str(&word); + word.clear(); + } + + // Start reading a new word + reading = true; + word.push(c); + }, + + ']' => { + if !reading { + out.push(c); + } else { + word.push(c); + + let f = format_map(&word[1..word.len()-1]); + + if f.is_some() { + out.push_str(&f.unwrap()); + } else if word == "[clear]" { + out.push_str(&format!("\x1B[2J\x1B[H")); + } else if word.starts_with("[cursorright") { + let n: u16 = word[12..word.len()-1].parse().unwrap(); + out.push_str(&format!("\x1B[{n}C")); + } else { + out.push_str(&word); + } + + reading = false; + word.clear(); + } + }, + + '\n' => { + if reading { word.push_str("\r\n"); } + else { out.push_str("\r\n"); } + }, + + _ => { + if reading { word.push(c); } + else { out.push(c); } + } + } + } + + return out; + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 1e85639..ceed6cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,14 +10,96 @@ mod context; mod formattedtext; mod errors; mod evaluate; +mod promptbuffer; pub use crate::formattedtext::FormattedText; pub use crate::context::Context; pub use crate::errors::DaisyError; pub use crate::evaluate::evaluate; +pub use crate::promptbuffer::PromptBuffer; +cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + use wasm_bindgen::prelude::*; + + #[derive(Debug)] + pub struct State { + pub context: Context, + pub promptbuffer: PromptBuffer + } + + #[wasm_bindgen] + pub extern fn daisy_init() -> *mut State { + Box::into_raw(Box::new(State { + context: Context::new(), + promptbuffer: PromptBuffer::new(64) + })) + } + + #[wasm_bindgen] + pub extern fn daisy_free(state: *mut State) { + unsafe { drop(Box::from_raw(state)) }; + } + + #[wasm_bindgen] + pub fn daisy_prompt(state: *mut State) -> String { + let t = unsafe { (*state).promptbuffer.write_prompt(&mut (*state).context) }; + return t.write(); + } + + + #[wasm_bindgen] + pub fn daisy_char(state: *mut State, s: String) -> String { + let mut out = FormattedText::new("".to_string()); + + match &s[..] { + "\r" => { + // Print again without cursor, in case we pressed enter + // while inside a substitution + let t = unsafe { (*state).promptbuffer.write_prompt_nocursor(&mut (*state).context) }; + out += t; + + + let in_str = unsafe { (*state).promptbuffer.enter() }; + out += FormattedText::new("\n".to_string()); + if in_str == "" { + return format!("\r\n{}", daisy_prompt(state)); + } + + if in_str.trim() == "quit" { + return "[quit]".to_string(); + } else { + let r = crate::do_string( unsafe { &mut (*state).context }, &in_str); + + match r { + Ok(t) | Err(t) => { + out += t; + } + } + } + }, + + "\x7F" => { unsafe { (*state).promptbuffer.backspace(); } }, + "\x1B[3~" => { unsafe { (*state).promptbuffer.delete(); } }, + "\x1B[D" => { unsafe { (*state).promptbuffer.cursor_left(); } }, + "\x1B[C" => { unsafe { (*state).promptbuffer.cursor_right(); } }, + "\x1B[A" => { unsafe { (*state).promptbuffer.hist_up(); } }, + "\x1B[B" => { unsafe { (*state).promptbuffer.hist_down(); } }, + + //'\x04' | '\x03' + //=> { break 'outer; }, + + _ => { unsafe { (*state).promptbuffer.add_char(s.chars().next().unwrap()); } }, + }; + + let t = unsafe { (*state).promptbuffer.write_prompt(&mut (*state).context) }; + return (out + t).write(); + } + } +} + #[inline(always)] pub fn do_string( context: &mut Context, diff --git a/src/main.rs b/src/main.rs index 24747aa..94903de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -pub mod promptbuffer; - use std::io::stdout; use std::io::stdin; use std::env; @@ -11,11 +9,10 @@ use termion::{ color::DetectColors }; -use promptbuffer::PromptBuffer; +use daisycalc::PromptBuffer; use daisycalc::command; use daisycalc::Context; use daisycalc::FormattedText; - use daisycalc::do_string; #[cfg(test)] @@ -87,7 +84,8 @@ pub fn main() -> Result<(), std::io::Error> { 'outer: loop { - pb.write_prompt(&mut context, &mut stdout)?; + let t = pb.write_prompt(&mut context); + t.write(&context, &mut stdout)?; let stdin = stdin(); for c in stdin.keys() { @@ -96,7 +94,10 @@ pub fn main() -> Result<(), std::io::Error> { '\n' => { // Print again without cursor, in case we pressed enter // while inside a substitution - pb.write_prompt_nocursor(&mut context, &mut stdout)?; + let t = pb.write_prompt_nocursor(&mut context); + t.write(&context, &mut stdout)?; + + let in_str = pb.enter(); FormattedText::newline(&mut stdout)?; if in_str == "" { break; } @@ -140,7 +141,8 @@ pub fn main() -> Result<(), std::io::Error> { }; }; - pb.write_prompt(&mut context, &mut stdout)?; + let t = pb.write_prompt(&mut context); + t.write(&context, &mut stdout)?; } } diff --git a/src/promptbuffer.rs b/src/promptbuffer.rs index 0b6bc6b..dc9d7d1 100644 --- a/src/promptbuffer.rs +++ b/src/promptbuffer.rs @@ -1,9 +1,7 @@ use std::collections::VecDeque; -use std::io::Write; -use termion::raw::RawTerminal; -use daisycalc::FormattedText; -use daisycalc::parser::substitute_cursor; -use daisycalc::Context; +use crate::FormattedText; +use crate::parser::substitute_cursor; +use crate::Context; const PROMPT_STR: &str = "==> "; @@ -18,12 +16,49 @@ pub struct PromptBuffer { // 1 means "on last item of history" hist_cursor: usize, - buffer: String, + pub buffer: String, buffer_changed: bool, cursor: usize, last_print_len: usize } +impl PromptBuffer { + // Same as write_primpt, but pretends there is no cursor + pub fn write_prompt_nocursor(&mut self, context: &Context) -> FormattedText { + let tmp = self.cursor; + self.cursor = 0; + let r = self.write_prompt(context); + self.cursor = tmp; + return r; + } + + pub fn write_prompt(&mut self, context: &Context) -> FormattedText { + let l = self.buffer.chars().count(); + let i = if l == 0 {0} else {l - self.cursor}; + + // Draw prettyprinted expression + let (display_c, s) = substitute_cursor(context, &self.get_contents(), i); + + let mut tx = FormattedText::new("".to_string()); + + tx.push(&format!("\r[p]{PROMPT_STR}[n]{s}")); + + + // If this string is shorter, clear the remaining old one. + if s.chars().count() < self.last_print_len { + tx.push(&" ".repeat(self.last_print_len - s.chars().count())); + } + + let q = (display_c + PROMPT_STR.chars().count()) as u16; + tx.push(&format!("\r[cursorright{q}]")); + + self.last_print_len = s.chars().count(); + + return tx; + } +} + + impl PromptBuffer { pub fn new(maxlen: usize) -> PromptBuffer { return PromptBuffer { @@ -37,48 +72,6 @@ impl PromptBuffer { }; } - // Same as write_primpt, but pretends there is no cursor - pub fn write_prompt_nocursor(&mut self, context: &Context, stdout: &mut RawTerminal) -> Result<(), std::io::Error> { - let tmp = self.cursor; - self.cursor = 0; - let r = self.write_prompt(context, stdout); - self.cursor = tmp; - return r; - } - - pub fn write_prompt(&mut self, context: &Context, stdout: &mut RawTerminal) -> Result<(), std::io::Error> { - let l = self.buffer.chars().count(); - let i = if l == 0 {0} else {l - self.cursor}; - - // Draw prettyprinted expression - let (display_c, s) = substitute_cursor(context, &self.get_contents(), i); - - write!( - stdout, "\r{}{PROMPT_STR}{}{}", - FormattedText::format_map('p', context).unwrap(), - FormattedText::format_map('n', context).unwrap(), - s - )?; - - // If this string is shorter, clear the remaining old one. - if s.chars().count() < self.last_print_len { - write!( - stdout, "{}", - " ".repeat(self.last_print_len - s.chars().count()), - )?; - } - - write!( - stdout, "\r{}", - termion::cursor::Right((display_c + PROMPT_STR.chars().count()) as u16) - )?; - - stdout.flush()?; - self.last_print_len = s.chars().count(); - - return Ok(()); - } - // Prompt methods pub fn get_contents(&self) -> &String {&self.buffer}