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..9cf2ae1 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" @@ -22,12 +28,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "daisycalc" -version = "1.1.0" +version = "1.1.1" dependencies = [ "cfg-if", "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 327e09e..ab23e32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "daisycalc" -version = "1.1.0" +version = "1.1.1" edition = "2021" build = "buildscript/main.rs" license = "GPL-3.0-only" @@ -13,6 +13,11 @@ readme = "README.md" name = "daisy" path = "src/main.rs" +[lib] +name = "daisycalc" +path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] + [profile.release] opt-level = 3 debug = 0 @@ -25,11 +30,18 @@ panic = "abort" [dependencies] cfg-if = "1.0.0" - -[target.'cfg(target_family = "unix")'.dependencies] -termion = "2.0.1" 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/command/mod.rs b/src/command/mod.rs index ebc774c..39a0096 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -16,6 +16,7 @@ pub fn is_command( | "vars" | "consts" | "constants" | "del" | "delete" + | "flags" => true, _ => false } @@ -66,6 +67,7 @@ pub fn do_command( "\n", "╞═══════════════ [t]Commands[n] ═══════════════╡\n", " [c]help[n] Show this help\n", + " [c]flags[n] Show command-line options\n", " [c]clear[n] Clear the terminal\n", " [c]quit[n] Exit daisy\n", //" [c]units[n] List available units\n", @@ -81,6 +83,27 @@ pub fn do_command( return t; }, + "flags" => { + return FormattedText::new( + concat!( + "\n", + "A list of command-line arguments is below\n", + "\n", + "╞════ [t]Flag[n] ════╪════════════════ [t]Function[n] ════════════════╡\n", + " [c]--help[n] Show help\n", + " [c]--version[n] Show version\n", + " [c]--info[n] Show system information\n", + " [c]--256color[n] Use full color support (default)\n", + " [c]--8color[n] Use reduced colors (ANSI, no styling)\n", + " [c]--nocolor[n] Do not use colors and styling\n", + " [c]--nosub[n] Disable inline substitution\n", + " [c]--nosuper[n] Disable superscript powers\n", + " [c]--nooneover[n] Disable \"one-over\" fractions as -1 power\n", + "\n\n" + ).to_string() + ); + }, + "clear" => { return FormattedText::new("[clear]".to_string()); }, diff --git a/src/entrypoint/mod.rs b/src/entrypoint/mod.rs deleted file mode 100644 index 54ff10f..0000000 --- a/src/entrypoint/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ - - -// Select main script for target system -cfg_if::cfg_if! { - if #[cfg(target_family = "unix")] { - mod unix; - pub use unix::main as main_e; - } else { - pub fn main_e() -> Result<(), std::io::Error> { - unimplemented!("Not yet implemented."); - } - } -} \ No newline at end of file diff --git a/src/entrypoint/unix/mod.rs b/src/entrypoint/unix/mod.rs deleted file mode 100644 index bca160f..0000000 --- a/src/entrypoint/unix/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod unix; -mod promptbuffer; -pub use self::unix::main; \ No newline at end of file diff --git a/src/entrypoint/unix/unix.rs b/src/entrypoint/unix/unix.rs deleted file mode 100644 index 104a60a..0000000 --- a/src/entrypoint/unix/unix.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::io::stdout; -use std::io::stdin; -use std::env; - -use termion::{ - event::Key, - input::TermRead, - raw::IntoRawMode, - color::DetectColors -}; - -use super::promptbuffer::PromptBuffer; -use crate::command; -use crate::context::Context; -use crate::FormattedText; - -#[inline(always)] -pub fn main() -> Result<(), std::io::Error> { - let mut stdout = stdout().into_raw_mode().unwrap(); - let mut pb: PromptBuffer = PromptBuffer::new(64); - let mut context = Context::new(); - - // Set color compatibilty - let term_colors = stdout.available_colors().unwrap_or(0); - if term_colors >= 256 { - context.config.term_color_type = 2; - } else if term_colors >= 8 { - context.config.term_color_type = 1; - } else { - context.config.term_color_type = 0; - } - - context.config.check(); - - - - // Handle command-line arguments - let args: Vec = env::args().collect(); - if args.iter().any(|s| s == "--help") { - let t = command::do_command(&mut context, &String::from("help")); - t.write(&context, &mut stdout)?; - return Ok(()); - } else if args.iter().any(|s| s == "--version") { - let t = FormattedText::new(format!( - "Daisy v{}\n", env!("CARGO_PKG_VERSION") - )); - t.write(&context, &mut stdout)?; - return Ok(()); - } else if args.iter().any(|s| s == "--debug") { - let t = FormattedText::new(format!( - concat!( - "Daisy v{}\n", - "Your terminal supports {} colors.\n" - ), - env!("CARGO_PKG_VERSION"), - term_colors - )); - t.write(&context, &mut stdout)?; - return Ok(()); - } - - 'outer: loop { - - pb.write_prompt(&mut context, &mut stdout)?; - - let stdin = stdin(); - for c in stdin.keys() { - if let Key::Char(q) = c.as_ref().unwrap() { - match q { - '\n' => { - // Print again without cursor, in case we pressed enter - // while inside a substitution - pb.write_prompt_nocursor(&mut context, &mut stdout)?; - let in_str = pb.enter(); - FormattedText::newline(&mut stdout)?; - if in_str == "" { break; } - - if in_str.trim() == "quit" { - break 'outer; - } else { - let r = crate::do_string(&mut context, &in_str); - - match r { - Ok(t) | Err(t) => { - t.write(&context, &mut stdout).unwrap(); - } - } - } - - break; - }, - _ => { pb.add_char(*q); } - }; - } else { - match c.unwrap() { - Key::Backspace => { pb.backspace(); }, - Key::Delete => { pb.delete(); }, - Key::Left => { pb.cursor_left(); }, - Key::Right => { pb.cursor_right(); }, - Key::Up => { pb.hist_up(); }, - Key::Down => { pb.hist_down(); }, - - Key::Ctrl('d') | - Key::Ctrl('c') => { break 'outer; }, - _ => {} - }; - }; - - pb.write_prompt(&mut context, &mut stdout)?; - } - } - - FormattedText::newline(&mut stdout)?; - return Ok(()); -} \ No newline at end of file diff --git a/src/formattedtext.rs b/src/formattedtext.rs deleted file mode 100644 index dfcfc55..0000000 --- a/src/formattedtext.rs +++ /dev/null @@ -1,199 +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 } - }) -} - -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 newline(stdout: &mut RawTerminal) -> Result<(), std::io::Error> { - write!(stdout, "\n")?; - return Ok(()); - } -} - - -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 = 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 new file mode 100644 index 0000000..ceed6cc --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,365 @@ +pub mod parser; +pub mod command; +pub mod quantity; + +use crate::parser::substitute; +use crate::parser::LineLocation; + + +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, + s: &String +) -> Result { + + let r: (LineLocation, DaisyError); + if command::is_command(s) { + return Ok(command::do_command(context, s)); + } else if s.contains("=") { + let x = do_assignment(context, s); + match x { + Ok(t) => { return Ok(t) }, + Err(t) => { r = t } + }; + } else { + let x = do_expression(context, s); + match x { + Ok((t, e)) => { context.push_hist(e); return Ok(t) }, + Err(t) => { r = t } + }; + } + + let (l, e) = r; + let mut t = FormattedText::new("".to_string()); + if l.zero() { + t.push(&format!( + "\n {}\n\n", + e.text().to_string(), + )); + } else { + t.push(&format!( + concat!( + "{}[e]{}[n]\n", + " {}\n\n" + ), + " ".repeat(l.pos + 4), + "^".repeat(l.len), + e.text().to_string(), + )); + } + + return Err(t); +} + +// Handle a simple evaluation string. +// Returns a FormattedText with output that should be printed. +#[inline(always)] +fn do_expression( + context: &mut Context, + s: &String +) -> Result<(FormattedText, parser::Expression), (LineLocation, DaisyError)> { + + let mut output = FormattedText::new("".to_string()); + + let g = parser::parse(context, &s)?; + let g_evaluated = evaluate::evaluate(context, &g)?; + + // Display parsed string + output.push(&format!( + " [s]=>[n] {}\n\n", + g.display(context) + )); + + // Display result + output.push(&format!( + " [r]=[n] {}\n\n", + g_evaluated.display_outer(context), + )); + + return Ok((output, g_evaluated)); +} + + +// Handle a variable or function definition string. +// Returns a FormattedText with output that should be printed. +#[inline(always)] +fn do_assignment( + context: &mut Context, + s: &String +) -> Result { + + let mut output = FormattedText::new("".to_string()); + + let parts = s.split("=").collect::>(); + if parts.len() != 2 { + return Err(( + LineLocation::new_zero(), + DaisyError::Syntax + )); + } + + // Index of first non-whitespace character in left + // (relative to whole prompt) + let starting_left = parts[0] + .char_indices() + .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) + .map(|(i, _)| i) + .unwrap_or_else(|| parts[0].len()); + + // Index of first non-whitespace character in right + // (relative to whole prompt) + // +1 accounts for equals sign + let starting_right = parts[0].chars().count() + 1 + + parts[1] + .char_indices() + .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) + .map(|(i, _)| i) + .unwrap_or_else(|| parts[0].len()); + + + let left = substitute(context, &parts[0].trim().to_string()); + let right = substitute(context, &parts[1].trim().to_string()); + let is_function = left.contains("("); + + // The order of methods below is a bit odd. + // This is intentional, since we want to check a definition's + // variable name before even attempting to parse its content. + if is_function { + let mut mode = 0; + let mut name = String::new(); + let mut args = String::new(); + for c in left.chars() { + match mode { + + // Mode 0: reading function name + 0 => { + if c == '(' { + mode = 1; continue; + } else { name.push(c); } + }, + + // Mode 1: reading arguments + 1 => { + if c == ')' { + mode = 2; continue; + } else { args.push(c); } + }, + + // Mode 2: we should be done by now. + // That close paren should've been the last character. + 2 => { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::Syntax + )); + }, + + _ => unreachable!() + } + } + + + let args = args + .split(",").collect::>() + .iter().map(|x| x.trim().to_string()).collect::>(); + + if name.len() == 0 { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::Syntax + )); + }; + + if !context.valid_function(&name) { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::BadFunction + )); + }; + + if args.iter().find(|x| &x[..] == "").is_some() { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::Syntax + )); + }; + + for a in &args { + if !context.valid_varible(a) { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::BadVariable + )); + } + } + + // Parse right hand side + let g = parser::parse(context, &right); + let Ok(g) = g else { + let Err((l, e)) = g else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + // Display parsed string + output.push(&format!( + " [s]=>[n] {left} = {}\n\n", + g.display(context) + )); + + // Evaluate expression with shadow variables + for a in &args { context.add_shadow(a.to_string(), None);} + let g_evaluated = evaluate::evaluate(context, &g); + context.clear_shadow(); + let Ok(_g_evaluated) = g_evaluated else { + let Err((l, e)) = g_evaluated else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + // We could push g_evaluated instead, but an un-evaluated string + // makes the 'vars' command prettier. + // + // We still need to evaluate g above, though, to make sure it works. + context.push_function(name, args, g).unwrap(); + } else { + + if !context.valid_varible(&left) { + return Err(( + LineLocation{ pos: starting_left, len: left.chars().count() }, + DaisyError::BadVariable + )); + } + + // Parse right hand side + let g = parser::parse(context, &right); + let Ok(g) = g else { + let Err((l, e)) = g else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + // Display parsed string + output.push(&format!( + " [t]=>[n] {left} = {}\n\n", + g.display(context) + )); + + // Evaluate expression + let g_evaluated = evaluate::evaluate(context, &g); + let Ok(g_evaluated) = g_evaluated else { + let Err((l, e)) = g_evaluated else { unreachable!() }; + return Err(( + LineLocation{ pos: l.pos + starting_right, len: l.len}, + e + )); + }; + + context.push_variable(left.to_string(), g_evaluated).unwrap(); + } + + return Ok(output); + +} + + diff --git a/src/main.rs b/src/main.rs index 0841f07..94903de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,289 +1,151 @@ -pub mod parser; -pub mod command; -pub mod quantity; -pub mod evaluate; -pub mod context; -pub mod errors; -pub mod formattedtext; +use std::io::stdout; +use std::io::stdin; +use std::env; -use crate::parser::substitute; -use crate::errors::DaisyError; -use crate::formattedtext::FormattedText; -use crate::context::Context; -use crate::parser::LineLocation; +use termion::{ + event::Key, + input::TermRead, + raw::IntoRawMode, + color::DetectColors +}; - -// Run main script for target system -mod entrypoint; -use crate::entrypoint::main_e; +use daisycalc::PromptBuffer; +use daisycalc::command; +use daisycalc::Context; +use daisycalc::FormattedText; +use daisycalc::do_string; #[cfg(test)] mod tests; -fn main() -> Result<(), std::io::Error> { - return main_e(); -} + #[inline(always)] -pub fn do_string( - context: &mut Context, - s: &String -) -> Result { +pub fn main() -> Result<(), std::io::Error> { + let mut stdout = stdout().into_raw_mode().unwrap(); + let mut pb: PromptBuffer = PromptBuffer::new(64); + let mut context = Context::new(); - let r: (LineLocation, DaisyError); - if command::is_command(s) { - return Ok(command::do_command(context, s)); - } else if s.contains("=") { - let x = do_assignment(context, s); - match x { - Ok(t) => { return Ok(t) }, - Err(t) => { r = t } - }; + // Detect color compatibilty + // Currently unused, this is slow. + /* + let term_colors = stdout.available_colors().unwrap_or(0); + if term_colors >= 256 { + context.config.term_color_type = 2; + } else if term_colors >= 8 { + context.config.term_color_type = 1; } else { - let x = do_expression(context, s); - match x { - Ok((t, e)) => { context.push_hist(e); return Ok(t) }, - Err(t) => { r = t } - }; + context.config.term_color_type = 0; } + */ - let (l, e) = r; - let mut t = FormattedText::new("".to_string()); - if l.zero() { - t.push(&format!( - "\n {}\n\n", - e.text().to_string(), + + // Handle command-line arguments + let args: Vec = env::args().collect(); + if args.iter().any(|s| s == "--help") { + let t = command::do_command(&mut context, &String::from("help")); + t.write(&context, &mut stdout)?; + let t = command::do_command(&mut context, &String::from("flags")); + t.write(&context, &mut stdout)?; + return Ok(()); + } else if args.iter().any(|s| s == "--version") { + let t = FormattedText::new(format!( + "Daisy v{}\n", env!("CARGO_PKG_VERSION") )); - } else { - t.push(&format!( + t.write(&context, &mut stdout)?; + return Ok(()); + } else if args.iter().any(|s| s == "--info") { + let t = FormattedText::new(format!( concat!( - "{}[e]{}[n]\n", - " {}\n\n" + "Daisy v{}\n", + "Your terminal supports {} colors.\n" ), - " ".repeat(l.pos + 4), - "^".repeat(l.len), - e.text().to_string(), + env!("CARGO_PKG_VERSION"), + stdout.available_colors().unwrap_or(0) )); + t.write(&context, &mut stdout)?; + return Ok(()); + } else if args.iter().any(|s| s == "--256color") { + context.config.term_color_type = 2; + } else if args.iter().any(|s| s == "--8color") { + context.config.term_color_type = 1; + } else if args.iter().any(|s| s == "--0color") { + context.config.term_color_type = 0; + } else if args.iter().any(|s| s == "--nosub") { + context.config.enable_substituion = false; + } else if args.iter().any(|s| s == "--nosuper") { + context.config.enable_super_powers = false; + } else if args.iter().any(|s| s == "--nooneover") { + context.config.enable_one_over_power = false; } - return Err(t); -} - -// Handle a simple evaluation string. -// Returns a FormattedText with output that should be printed. -#[inline(always)] -fn do_expression( - context: &mut Context, - s: &String -) -> Result<(FormattedText, parser::Expression), (LineLocation, DaisyError)> { - - let mut output = FormattedText::new("".to_string()); - - let g = parser::parse(context, &s)?; - let g_evaluated = evaluate::evaluate(context, &g)?; - - // Display parsed string - output.push(&format!( - " [s]=>[n] {}\n\n", - g.display(context) - )); - - // Display result - output.push(&format!( - " [r]=[n] {}\n\n", - g_evaluated.display_outer(context), - )); - - return Ok((output, g_evaluated)); -} + context.config.check(); -// Handle a variable or function definition string. -// Returns a FormattedText with output that should be printed. -#[inline(always)] -fn do_assignment( - context: &mut Context, - s: &String -) -> Result { + 'outer: loop { - let mut output = FormattedText::new("".to_string()); + let t = pb.write_prompt(&mut context); + t.write(&context, &mut stdout)?; - let parts = s.split("=").collect::>(); - if parts.len() != 2 { - return Err(( - LineLocation::new_zero(), - DaisyError::Syntax - )); + let stdin = stdin(); + for c in stdin.keys() { + if let Key::Char(q) = c.as_ref().unwrap() { + match q { + '\n' => { + // Print again without cursor, in case we pressed enter + // while inside a substitution + 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; } + + if in_str.trim() == "quit" { + break 'outer; + } else { + let r = crate::do_string(&mut context, &in_str); + + match r { + Ok(t) | Err(t) => { + t.write(&context, &mut stdout).unwrap(); + } + } + } + + break; + }, + + // Only process sane characters + 'a'..='z' | 'A'..='Z' | '0'..='9' + |'!'|'@'|'#'|'$'|'%'|'^'|'&'|'*'|'('|')' + |'?'|'~'|','|'.'|'['|']' + |'<'|'>'|'/'|'_'|'-'|':'|'|'|'='|'+'|';' + => { pb.add_char(*q); }, + + _ => {} + }; + } else { + match c.unwrap() { + Key::Backspace => { pb.backspace(); }, + Key::Delete => { pb.delete(); }, + Key::Left => { pb.cursor_left(); }, + Key::Right => { pb.cursor_right(); }, + Key::Up => { pb.hist_up(); }, + Key::Down => { pb.hist_down(); }, + + Key::Ctrl('d') | + Key::Ctrl('c') => { break 'outer; }, + _ => {} + }; + }; + + let t = pb.write_prompt(&mut context); + t.write(&context, &mut stdout)?; + } } - // Index of first non-whitespace character in left - // (relative to whole prompt) - let starting_left = parts[0] - .char_indices() - .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) - .map(|(i, _)| i) - .unwrap_or_else(|| parts[0].len()); - - // Index of first non-whitespace character in right - // (relative to whole prompt) - // +1 accounts for equals sign - let starting_right = parts[0].chars().count() + 1 + - parts[1] - .char_indices() - .find(|(_, ch)| !(ch.is_whitespace() && *ch != '\n')) - .map(|(i, _)| i) - .unwrap_or_else(|| parts[0].len()); - - - let left = substitute(context, &parts[0].trim().to_string()); - let right = substitute(context, &parts[1].trim().to_string()); - let is_function = left.contains("("); - - // The order of methods below is a bit odd. - // This is intentional, since we want to check a definition's - // variable name before even attempting to parse its content. - if is_function { - let mut mode = 0; - let mut name = String::new(); - let mut args = String::new(); - for c in left.chars() { - match mode { - - // Mode 0: reading function name - 0 => { - if c == '(' { - mode = 1; continue; - } else { name.push(c); } - }, - - // Mode 1: reading arguments - 1 => { - if c == ')' { - mode = 2; continue; - } else { args.push(c); } - }, - - // Mode 2: we should be done by now. - // That close paren should've been the last character. - 2 => { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::Syntax - )); - }, - - _ => unreachable!() - } - } - - - let args = args - .split(",").collect::>() - .iter().map(|x| x.trim().to_string()).collect::>(); - - if name.len() == 0 { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::Syntax - )); - }; - - if !context.valid_function(&name) { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::BadFunction - )); - }; - - if args.iter().find(|x| &x[..] == "").is_some() { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::Syntax - )); - }; - - for a in &args { - if !context.valid_varible(a) { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::BadVariable - )); - } - } - - // Parse right hand side - let g = parser::parse(context, &right); - let Ok(g) = g else { - let Err((l, e)) = g else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - // Display parsed string - output.push(&format!( - " [s]=>[n] {left} = {}\n\n", - g.display(context) - )); - - // Evaluate expression with shadow variables - for a in &args { context.add_shadow(a.to_string(), None);} - let g_evaluated = evaluate::evaluate(context, &g); - context.clear_shadow(); - let Ok(_g_evaluated) = g_evaluated else { - let Err((l, e)) = g_evaluated else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - // We could push g_evaluated instead, but an un-evaluated string - // makes the 'vars' command prettier. - // - // We still need to evaluate g above, though, to make sure it works. - context.push_function(name, args, g).unwrap(); - } else { - - if !context.valid_varible(&left) { - return Err(( - LineLocation{ pos: starting_left, len: left.chars().count() }, - DaisyError::BadVariable - )); - } - - // Parse right hand side - let g = parser::parse(context, &right); - let Ok(g) = g else { - let Err((l, e)) = g else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - // Display parsed string - output.push(&format!( - " [t]=>[n] {left} = {}\n\n", - g.display(context) - )); - - // Evaluate expression - let g_evaluated = evaluate::evaluate(context, &g); - let Ok(g_evaluated) = g_evaluated else { - let Err((l, e)) = g_evaluated else { unreachable!() }; - return Err(( - LineLocation{ pos: l.pos + starting_right, len: l.len}, - e - )); - }; - - context.push_variable(left.to_string(), g_evaluated).unwrap(); - } - - return Ok(output); - -} - - + FormattedText::newline(&mut stdout)?; + return Ok(()); +} \ No newline at end of file diff --git a/src/entrypoint/unix/promptbuffer.rs b/src/promptbuffer.rs similarity index 80% rename from src/entrypoint/unix/promptbuffer.rs rename to src/promptbuffer.rs index 619f759..dc9d7d1 100644 --- a/src/entrypoint/unix/promptbuffer.rs +++ b/src/promptbuffer.rs @@ -1,9 +1,7 @@ use std::collections::VecDeque; -use std::io::Write; -use termion::raw::RawTerminal; -use crate::formattedtext; +use crate::FormattedText; use crate::parser::substitute_cursor; -use crate::context::Context; +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} diff --git a/src/tests.rs b/src/tests.rs index d54f10f..e765a57 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,7 +1,7 @@ // Many of these have been borrowed from insect. -use crate::parser; -use crate::evaluate::evaluate; -use crate::context::Context; +use daisycalc::parser; +use daisycalc::evaluate; +use daisycalc::Context; fn eval_to_str(s: &str) -> Result { let g = match parser::parse_no_context(&String::from(s)) {