From 430e701df0a0e1d6c5f94462dd88242804d08f39 Mon Sep 17 00:00:00 2001 From: Kai Schmidt Date: Wed, 20 Nov 2024 12:37:06 -0800 Subject: [PATCH] some window stuff work nicely --- Cargo.lock | 23 ++ Cargo.toml | 5 +- pad/editor/src/utils.rs | 56 +--- src/algorithm/encode.rs | 81 ++++- src/main.rs | 656 ++++++++++++++++++++++------------------ src/sys/native.rs | 18 +- src/window.rs | 350 +++++++++++++++++---- 7 files changed, 768 insertions(+), 421 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fa02b8dc..f1aa00eec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4509,6 +4509,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "ron" version = "0.8.1" @@ -5681,6 +5703,7 @@ dependencies = [ "rawrrr", "rayon", "regex", + "rmp-serde", "rustfft", "rustls", "rustls-pemfile", diff --git a/Cargo.toml b/Cargo.toml index 88feaa1b7..36f515818 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,8 @@ wasm-bindgen = {version = "0.2.92", optional = true} web-sys = {version = "0.3.60", optional = true} # Window dependencies -eframe = {version = "0.29.1", features = ["persistence"], optional = true} +eframe = {version = "0.29.1", optional = true, features = ["persistence"]} +rmp-serde = {version = "1.3.0", optional = true} [features] audio = ["hodaun", "lockfree", "audio_encode"] @@ -152,7 +153,7 @@ terminal_image = ["viuer", "image", "icy_sixel"] tls = ["httparse", "rustls", "webpki-roots", "rustls-pemfile"] web = ["wasm-bindgen", "js-sys", "web-sys"] webcam = ["image", "uiua-nokhwa"] -window = ["eframe"] +window = ["eframe", "rmp-serde"] xlsx = ["calamine", "simple_excel_writer"] # Use system static libraries instead of building them system = ["libffi?/system"] diff --git a/pad/editor/src/utils.rs b/pad/editor/src/utils.rs index 489a7648e..1b1751677 100644 --- a/pad/editor/src/utils.rs +++ b/pad/editor/src/utils.rs @@ -8,15 +8,14 @@ use std::{ }; use base64::engine::{general_purpose::URL_SAFE, Engine}; -use image::ImageOutputFormat; use leptos::*; use uiua::{ ast::Item, - encode::{image_to_bytes, value_to_gif_bytes, value_to_image, value_to_wav_bytes}, + encode::SmartOutput, lsp::{BindingDocsKind, ImportSrc}, Compiler, DiagnosticKind, Inputs, Primitive, Report, ReportFragment, ReportKind, SpanKind, - Spans, SysBackend, Uiua, UiuaError, UiuaResult, Value, + Spans, Uiua, UiuaError, UiuaResult, Value, }; use unicode_segmentation::UnicodeSegmentation; use wasm_bindgen::JsCast; @@ -1005,52 +1004,21 @@ fn run_code_single(code: &str) -> (Vec, Option) { let mut stack = Vec::new(); let value_count = values.len(); for (i, value) in values.into_iter().enumerate() { - // Try to convert the value to audio - if value.shape().last().is_some_and(|&n| n >= 44100 / 4) - && matches!(&value, Value::Num(arr) if arr.elements().all(|x| x.abs() <= 5.0)) - { - if let Ok(bytes) = value_to_wav_bytes(&value, io.audio_sample_rate()) { - let label = value.meta().label.as_ref().map(Into::into); - stack.push(OutputItem::Audio(bytes, label)); + let value = match SmartOutput::from_value(value, io) { + SmartOutput::Png(bytes, label) => { + stack.push(OutputItem::Image(bytes, label)); continue; } - } - // Try to convert the value to an image - const MIN_AUTO_IMAGE_DIM: usize = 30; - if let Ok(image) = value_to_image(&value) { - if image.width() >= MIN_AUTO_IMAGE_DIM as u32 - && image.height() >= MIN_AUTO_IMAGE_DIM as u32 - { - if let Ok(bytes) = image_to_bytes(&image, ImageOutputFormat::Png) { - let label = value.meta().label.as_ref().map(Into::into); - stack.push(OutputItem::Image(bytes, label)); - continue; - } - } - } - // Try to convert the value to a gif - if let Ok(bytes) = value_to_gif_bytes(&value, 16.0) { - match value.shape().dims() { - &[f, h, w] | &[f, h, w, _] - if h >= MIN_AUTO_IMAGE_DIM && w >= MIN_AUTO_IMAGE_DIM && f >= 5 => - { - let label = value.meta().label.as_ref().map(Into::into); - stack.push(OutputItem::Gif(bytes, label)); - continue; - } - _ => {} + SmartOutput::Gif(bytes, label) => { + stack.push(OutputItem::Gif(bytes, label)); + continue; } - } - // Try to convert the value to SVG - if let Ok(mut str) = value.as_string(&rt, "") { - if str.starts_with("") { - if !str.contains("xmlns") { - str = str.replacen(" { + stack.push(OutputItem::Audio(bytes, label)); continue; } - } + SmartOutput::Normal(value) => value, + }; // Otherwise, just show the value let class = if value_count == 1 { "" diff --git a/src/algorithm/encode.rs b/src/algorithm/encode.rs index 3b2ef0c74..c8630b366 100644 --- a/src/algorithm/encode.rs +++ b/src/algorithm/encode.rs @@ -4,10 +4,66 @@ use hound::{SampleFormat, WavReader, WavSpec, WavWriter}; #[cfg(feature = "image")] use image::{DynamicImage, ImageOutputFormat}; +use serde::*; +use crate::SysBackend; #[allow(unused_imports)] use crate::{Array, Uiua, UiuaResult, Value}; +/// Conversion of a value to some media format based on the value's shape +#[derive(Clone, Serialize, Deserialize)] +#[allow(missing_docs)] +pub enum SmartOutput { + Normal(Value), + Png(Vec, Option), + Gif(Vec, Option), + Wav(Vec, Option), +} + +impl SmartOutput { + /// Convert a value to a SmartOutput + pub fn from_value(value: Value, backend: &dyn SysBackend) -> Self { + // Try to convert the value to audio + #[cfg(feature = "audio_encode")] + if value.shape().last().is_some_and(|&n| n >= 44100 / 4) + && matches!(&value, Value::Num(arr) if arr.elements().all(|x| x.abs() <= 5.0)) + { + if let Ok(bytes) = value_to_wav_bytes(&value, backend.audio_sample_rate()) { + let label = value.meta().label.as_ref().map(Into::into); + return Self::Wav(bytes, label); + } + } + // Try to convert the value to an image + #[cfg(feature = "image")] + const MIN_AUTO_IMAGE_DIM: usize = 30; + if let Ok(image) = value_to_image(&value) { + if image.width() >= MIN_AUTO_IMAGE_DIM as u32 + && image.height() >= MIN_AUTO_IMAGE_DIM as u32 + { + if let Ok(bytes) = image_to_bytes(&image, ImageOutputFormat::Png) { + let label = value.meta().label.as_ref().map(Into::into); + return Self::Png(bytes, label); + } + } + } + // Try to convert the value to a gif + #[cfg(feature = "gif")] + if let Ok(bytes) = value_to_gif_bytes(&value, 16.0) { + match value.shape().dims() { + &[f, h, w] | &[f, h, w, _] + if h >= MIN_AUTO_IMAGE_DIM && w >= MIN_AUTO_IMAGE_DIM && f >= 5 => + { + let label = value.meta().label.as_ref().map(Into::into); + return Self::Gif(bytes, label); + } + _ => {} + } + } + // Otherwise, just show the value + Self::Normal(value) + } +} + pub(crate) fn image_encode(env: &mut Uiua) -> UiuaResult { #[cfg(feature = "image")] { @@ -184,6 +240,18 @@ pub fn rgb_image_to_array(image: image::RgbImage) -> Array { ) } +#[doc(hidden)] +#[cfg(feature = "image")] +pub fn rgba_image_to_array(image: image::RgbaImage) -> Array { + let shape = crate::Shape::from([image.height() as usize, image.width() as usize, 4]); + Array::new( + shape, + (image.into_raw().into_iter()) + .map(|b| b as f64 / 255.0) + .collect::>(), + ) +} + #[doc(hidden)] #[cfg(feature = "image")] pub fn image_bytes_to_array(bytes: &[u8], alpha: bool) -> Result, String> { @@ -339,11 +407,11 @@ pub fn value_to_wav_bytes(audio: &Value, sample_rate: u32) -> Result, St if sample_rate == 0 { return Err("Sample rate must not be 0".to_string()); } - + let channels = value_to_audio_channels(audio)?; #[cfg(not(feature = "audio"))] { - value_to_wav_bytes_impl( - audio, + channels_to_wav_bytes_impl( + channels, |f| (f * i16::MAX as f64) as i16, 16, SampleFormat::Int, @@ -352,20 +420,19 @@ pub fn value_to_wav_bytes(audio: &Value, sample_rate: u32) -> Result, St } #[cfg(feature = "audio")] { - value_to_wav_bytes_impl(audio, |f| f as f32, 32, SampleFormat::Float, sample_rate) + channels_to_wav_bytes_impl(channels, |f| f as f32, 32, SampleFormat::Float, sample_rate) } } #[cfg(feature = "audio_encode")] -fn value_to_wav_bytes_impl( - audio: &Value, +fn channels_to_wav_bytes_impl( + channels: Vec>, convert_samples: impl Fn(f64) -> T + Copy, bits_per_sample: u16, sample_format: SampleFormat, sample_rate: u32, ) -> Result, String> { // We use i16 samples for compatibility with Firefox (if I remember correctly) - let channels = value_to_audio_channels(audio)?; let channels: Vec> = channels .into_iter() .map(|c| c.into_iter().map(convert_samples).collect()) diff --git a/src/main.rs b/src/main.rs index 52004c3d2..4202e974b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,13 +16,14 @@ use std::{ time::{Duration, Instant}, }; -use clap::{error::ErrorKind, Parser, Subcommand}; +use clap::{Parser, Subcommand}; use colored::*; use notify::{EventKind, RecursiveMode, Watcher}; use once_cell::sync::Lazy; use parking_lot::Mutex; use rustyline::{error::ReadlineError, DefaultEditor}; use uiua::{ + encode::SmartOutput, format::{format_file, format_str, FormatConfig, FormatConfigSource}, lsp::BindingDocsKind, Assembly, CodeSpan, Compiler, NativeSys, PreEvalMode, PrimClass, PrimDocFragment, PrimDocLine, @@ -38,6 +39,15 @@ fn fail(e: UiuaError) -> T { exit(1) } +fn use_window() -> bool { + #[cfg(feature = "window")] + { + uiua::window::use_window() + } + #[cfg(not(feature = "window"))] + false +} + fn main() { color_backtrace::install(); @@ -49,9 +59,9 @@ fn main() { println!("# Program interrupted"); print_watching(); } else { - match App::try_parse() { - Ok(App::Watch { .. }) | Err(_) => clear_watching_with(" ", ""), - Ok(App::Repl { .. }) => { + match App::try_parse().ok().and_then(|app| app.command) { + Some(Comm::Watch { .. }) | None => clear_watching_with(" ", ""), + Some(Comm::Repl { .. }) => { if !PRESSED_CTRL_C.swap(true, Ordering::Relaxed) { return; } @@ -77,327 +87,331 @@ fn main() { print_stack(&rt.take_stack(), true); return; } - match App::try_parse() { - Ok(app) => match app { - App::Init => { - if let Ok(path) = working_file_path() { - eprintln!("File already exists: {}", path.display()); - } else { - fs::write("main.ua", "\"Hello, World!\"").unwrap(); - } + let app = App::parse(); + match app.command { + Some(Comm::Init) => { + if let Ok(path) = working_file_path() { + eprintln!("File already exists: {}", path.display()); + } else { + fs::write("main.ua", "\"Hello, World!\"").unwrap(); } - App::Fmt { - path, - formatter_options, - io, - } => { - let config = FormatConfig::from_source( - formatter_options.format_config_source, - path.as_deref(), - ) - .unwrap_or_else(fail); + } + Some(Comm::Fmt { + path, + formatter_options, + io, + }) => { + let config = + FormatConfig::from_source(formatter_options.format_config_source, path.as_deref()) + .unwrap_or_else(fail); - if io { - let mut buffer = String::new(); - let mut code = String::new(); - let stdin = stdin(); - let mut stdin = stdin.lock(); - loop { - buffer.clear(); - if stdin.read_line(&mut buffer).is_err() { - break; - } - if buffer.is_empty() { - break; - } - code.push_str(&buffer); + if io { + let mut buffer = String::new(); + let mut code = String::new(); + let stdin = stdin(); + let mut stdin = stdin.lock(); + loop { + buffer.clear(); + if stdin.read_line(&mut buffer).is_err() { + break; } - let formatted = format_str(&code, &config).unwrap_or_else(fail); - print!("{}", formatted.output); - } else if let Some(path) = path { - format_single_file(path, &config).unwrap_or_else(fail); - } else { - format_multi_files(&config).unwrap_or_else(fail); + if buffer.is_empty() { + break; + } + code.push_str(&buffer); } + let formatted = format_str(&code, &config).unwrap_or_else(fail); + print!("{}", formatted.output); + } else if let Some(path) = path { + format_single_file(path, &config).unwrap_or_else(fail); + } else { + format_multi_files(&config).unwrap_or_else(fail); } - App::Run { - path, - no_format, - no_color, - formatter_options, + } + Some(Comm::Run { + path, + no_format, + no_color, + formatter_options, + time_instrs, + limit, + mode, + #[cfg(feature = "audio")] + audio_options, + #[cfg(feature = "window")] + window, + args, + }) => { + let path = if let Some(path) = path { + path + } else { + match working_file_path() { + Ok(path) => path, + Err(e) => { + eprintln!("{}", e); + return; + } + } + }; + #[cfg(feature = "audio")] + setup_audio(audio_options); + #[cfg(feature = "window")] + uiua::window::set_use_window(window); + run( + &path, + args, time_instrs, limit, mode, - #[cfg(feature = "audio")] - audio_options, - args, - } => { - let path = if let Some(path) = path { - path - } else { - match working_file_path() { - Ok(path) => path, - Err(e) => { - eprintln!("{}", e); - return; - } - } - }; - #[cfg(feature = "audio")] - setup_audio(audio_options); - run( - &path, - args, - time_instrs, - limit, - mode, - (!no_format).then_some(formatter_options), - no_color, - ); - } - App::Build { path, output } => { - let path = if let Some(path) = path { - path - } else { - match working_file_path() { - Ok(path) => path, - Err(e) => { - eprintln!("{}", e); - return; - } + (!no_format).then_some(formatter_options), + no_color, + ); + } + Some(Comm::Build { path, output }) => { + let path = if let Some(path) = path { + path + } else { + match working_file_path() { + Ok(path) => path, + Err(e) => { + eprintln!("{}", e); + return; } - }; - let assembly = Compiler::with_backend(NativeSys) - .mode(RunMode::Normal) - .print_diagnostics(true) - .load_file(&path) - .unwrap_or_else(fail) - .finish(); - let output = output.unwrap_or_else(|| path.with_extension("uasm")); - let uasm = assembly.to_uasm(); - if let Err(e) = fs::write(output, uasm) { - eprintln!("Failed to write assembly: {e}"); } + }; + let assembly = Compiler::with_backend(NativeSys) + .mode(RunMode::Normal) + .print_diagnostics(true) + .load_file(&path) + .unwrap_or_else(fail) + .finish(); + let output = output.unwrap_or_else(|| path.with_extension("uasm")); + let uasm = assembly.to_uasm(); + if let Err(e) = fs::write(output, uasm) { + eprintln!("Failed to write assembly: {e}"); } - App::Eval { - code, - no_color, - experimental, - #[cfg(feature = "audio")] - audio_options, - args, - } => { - #[cfg(feature = "audio")] - setup_audio(audio_options); - let mut rt = Uiua::with_native_sys().with_args(args); - rt.compile_run(|comp| { - comp.mode(RunMode::Normal) - .experimental(experimental) - .print_diagnostics(true) - .load_str(&code) - }) - .unwrap_or_else(fail); - print_stack(&rt.take_stack(), !no_color); - } - App::Test { - path, - formatter_options, - args, - } => { - let path = if let Some(path) = path { - path - } else { - match working_file_path() { - Ok(path) => path, - Err(e) => { - eprintln!("{}", e); - return; - } + } + Some(Comm::Eval { + code, + no_color, + experimental, + #[cfg(feature = "audio")] + audio_options, + args, + }) => { + #[cfg(feature = "audio")] + setup_audio(audio_options); + let mut rt = Uiua::with_native_sys().with_args(args); + rt.compile_run(|comp| { + comp.mode(RunMode::Normal) + .experimental(experimental) + .print_diagnostics(true) + .load_str(&code) + }) + .unwrap_or_else(fail); + print_stack(&rt.take_stack(), !no_color); + } + Some(Comm::Test { + path, + formatter_options, + args, + }) => { + let path = if let Some(path) = path { + path + } else { + match working_file_path() { + Ok(path) => path, + Err(e) => { + eprintln!("{}", e); + return; } - }; - let config = - FormatConfig::from_source(formatter_options.format_config_source, Some(&path)) - .unwrap_or_else(fail); - format_file(&path, &config).unwrap_or_else(fail); - let mut rt = Uiua::with_native_sys() - .with_file_path(&path) - .with_args(args); - let res = rt.compile_run(|comp| { - comp.mode(RunMode::Test) - .print_diagnostics(true) - .load_file(path) - }); - if let Err(e) = &res { - println!("{}", e.report()); - } - rt.print_reports(); - if res.is_err() { - exit(1); } + }; + let config = + FormatConfig::from_source(formatter_options.format_config_source, Some(&path)) + .unwrap_or_else(fail); + format_file(&path, &config).unwrap_or_else(fail); + let mut rt = Uiua::with_native_sys() + .with_file_path(&path) + .with_args(args); + let res = rt.compile_run(|comp| { + comp.mode(RunMode::Test) + .print_diagnostics(true) + .load_file(path) + }); + if let Err(e) = &res { + println!("{}", e.report()); } - App::Watch { - no_format, - no_color, - formatter_options, + rt.print_reports(); + if res.is_err() { + exit(1); + } + } + Some(Comm::Watch { + no_format, + no_color, + formatter_options, + clear, + #[cfg(feature = "window")] + window, + args, + stdin_file, + }) => { + #[cfg(feature = "window")] + uiua::window::set_use_window(window); + if let Err(e) = (WatchArgs { + initial_path: working_file_path().ok(), + format: !no_format, + color: !no_color, + format_config_source: formatter_options.format_config_source, clear, args, stdin_file, - } => { - if let Err(e) = (WatchArgs { - initial_path: working_file_path().ok(), - format: !no_format, - color: !no_color, - format_config_source: formatter_options.format_config_source, - clear, - args, - stdin_file, - }) - .watch() - { - eprintln!("Error watching file: {e}"); - } + }) + .watch() + { + eprintln!("Error watching file: {e}"); } - #[cfg(feature = "lsp")] - App::Lsp => uiua::lsp::run_language_server(), - App::Repl { - file, - formatter_options, - #[cfg(feature = "audio")] - audio_options, - stack, - args, - } => { - let config = FormatConfig { - trailing_newline: false, - ..FormatConfig::from_source(formatter_options.format_config_source, None) - .unwrap_or_else(fail) - }; + } + #[cfg(feature = "lsp")] + Some(Comm::Lsp) => uiua::lsp::run_language_server(), + Some(Comm::Repl { + file, + formatter_options, + #[cfg(feature = "audio")] + audio_options, + stack, + args, + }) => { + let config = FormatConfig { + trailing_newline: false, + ..FormatConfig::from_source(formatter_options.format_config_source, None) + .unwrap_or_else(fail) + }; - #[cfg(feature = "audio")] - setup_audio(audio_options); - let mut rt = Uiua::with_native_sys().with_args(args); - let mut compiler = Compiler::with_backend(NativeSys); - compiler.mode(RunMode::Normal).print_diagnostics(true); - if let Some(file) = file { - compiler.load_file(file).unwrap_or_else(fail); - rt.run_compiler(&mut compiler).unwrap_or_else(fail); - } - repl(rt, compiler, true, stack, config); + #[cfg(feature = "audio")] + setup_audio(audio_options); + let mut rt = Uiua::with_native_sys().with_args(args); + let mut compiler = Compiler::with_backend(NativeSys); + compiler.mode(RunMode::Normal).print_diagnostics(true); + if let Some(file) = file { + compiler.load_file(file).unwrap_or_else(fail); + rt.run_compiler(&mut compiler).unwrap_or_else(fail); } - App::Update { main, check } => update(main, check), - App::Module { command } => { - let paths = match list_modules() { - Ok(paths) => paths, - Err(e) => { - eprintln!("Failed to list modules: {e}"); - return; - } - }; - match command.unwrap_or(ModuleCommand::List) { - ModuleCommand::List => { - if let Some(paths) = paths { - for path in paths { - println!("{}", path.display()); - } + repl(rt, compiler, true, stack, config); + } + Some(Comm::Update { main, check }) => update(main, check), + Some(Comm::Module { command }) => { + let paths = match list_modules() { + Ok(paths) => paths, + Err(e) => { + eprintln!("Failed to list modules: {e}"); + return; + } + }; + match command.unwrap_or(ModuleCommand::List) { + ModuleCommand::List => { + if let Some(paths) = paths { + for path in paths { + println!("{}", path.display()); } } - ModuleCommand::Update { module } => { - let modules = if let Some(module) = module { - vec![module] - } else if let Some(paths) = paths { - paths - } else { - eprintln!("No modules to update"); - return; - }; - if let Err(e) = update_modules(&modules) { - eprintln!("Failed to update modules: {e}"); - } + } + ModuleCommand::Update { module } => { + let modules = if let Some(module) = module { + vec![module] + } else if let Some(paths) = paths { + paths + } else { + eprintln!("No modules to update"); + return; + }; + if let Err(e) = update_modules(&modules) { + eprintln!("Failed to update modules: {e}"); } } } - #[cfg(feature = "stand")] - App::Stand { main, name } => { - let main = main.unwrap_or_else(|| "main.ua".into()); - if !main.exists() { - eprintln!("{} does not exist", main.display()); - exit(1); - } - match uiua::stand::build_exe(&main) { - Ok(bytes) => { - let name = name - .or_else(|| { - env::current_dir().ok().and_then(|p| { - p.file_stem().map(|p| p.to_string_lossy().into_owned()) - }) + } + #[cfg(feature = "stand")] + Some(Comm::Stand { main, name }) => { + let main = main.unwrap_or_else(|| "main.ua".into()); + if !main.exists() { + eprintln!("{} does not exist", main.display()); + exit(1); + } + match uiua::stand::build_exe(&main) { + Ok(bytes) => { + let name = name + .or_else(|| { + env::current_dir().ok().and_then(|p| { + p.file_stem().map(|p| p.to_string_lossy().into_owned()) }) - .unwrap_or_else(|| "program".into()); - let path = PathBuf::from(name).with_extension(env::consts::EXE_EXTENSION); - #[allow(clippy::needless_borrows_for_generic_args)] - if let Err(e) = fs::write(&path, bytes) { - eprintln!("Failed to write executable: {e}"); - exit(1); - } - // Set executable permissions on Unix - #[cfg(unix)] - if let Err(e) = (|| { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&path)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&path, perms) - })() { - eprintln!("Failed to set executable permissions: {e}"); - exit(1); - } + }) + .unwrap_or_else(|| "program".into()); + let path = PathBuf::from(name).with_extension(env::consts::EXE_EXTENSION); + #[allow(clippy::needless_borrows_for_generic_args)] + if let Err(e) = fs::write(&path, bytes) { + eprintln!("Failed to write executable: {e}"); + exit(1); } - Err(e) => { - eprintln!("Failed to build executable: {e}"); + // Set executable permissions on Unix + #[cfg(unix)] + if let Err(e) = (|| { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&path, perms) + })() { + eprintln!("Failed to set executable permissions: {e}"); exit(1); } } + Err(e) => { + eprintln!("Failed to build executable: {e}"); + exit(1); + } } - App::Doc { name } => doc(&name), - App::Find { path, text, raw } => find(path, text, raw).unwrap_or_else(fail), - #[cfg(feature = "window")] - App::Window => uiua::window::run_window(), - }, - Err(e) - if e.kind() == ErrorKind::InvalidSubcommand - && env::args() - .nth(1) - .is_some_and(|path| Path::new(&path).exists()) => - { - let mut args: Vec = env::args().skip(1).collect(); - let path = args.remove(0); - run( - path.as_ref(), - args, - false, - None, - Some(RunMode::Normal), - None, - false, - ) } - Err(e) if e.kind() == ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => { - let res = match working_file_path() { - Ok(path) => WatchArgs { - initial_path: Some(path), - ..Default::default() - } - .watch(), - Err(NoWorkingFile::MultipleFiles) => WatchArgs::default().watch(), - Err(nwf) => { - _ = e.print(); - eprintln!("\n{nwf}"); - return; + Some(Comm::Doc { name }) => doc(&name), + Some(Comm::Find { path, text, raw }) => find(path, text, raw).unwrap_or_else(fail), + #[cfg(feature = "window")] + Some(Comm::Window) => uiua::window::run_window(), + None => { + #[cfg(feature = "window")] + uiua::window::set_use_window(app.window); + if let Some(path) = app.file { + let args: Vec = env::args().skip(2).collect(); + run( + path.as_ref(), + args, + false, + None, + Some(RunMode::Normal), + None, + false, + ) + } else { + let res = match working_file_path() { + Ok(path) => WatchArgs { + initial_path: Some(path), + ..Default::default() + } + .watch(), + Err(NoWorkingFile::MultipleFiles) => WatchArgs::default().watch(), + Err(nwf) => { + _ = App::try_parse_from(["uiua", "help"]) + .map(drop) + .unwrap_err() + .print(); + eprintln!("\n{nwf}"); + return; + } + }; + if let Err(e) = res { + eprintln!("Error watching file: {e}"); } - }; - if let Err(e) = res { - eprintln!("Error watching file: {e}"); } } - Err(e) => _ = e.print(), } } @@ -541,7 +555,9 @@ impl WatchArgs { let mut watcher = notify::recommended_watcher(send)?; watcher.watch(Path::new("."), RecursiveMode::Recursive)?; - println!("Watching for changes... (end with ctrl+C, use `uiua help` to see options)"); + if !use_window() { + println!("Watching for changes... (end with ctrl+C, use `uiua help` to see options)"); + } let config = FormatConfig::from_source(format_config_source, initial_path.as_deref()).ok(); #[cfg(feature = "audio")] @@ -592,9 +608,9 @@ impl WatchArgs { let stdin_file = stdin_file.map(fs::File::open).transpose()?; - *WATCH_CHILD.lock() = Some( - Command::new(env::current_exe().unwrap()) - .arg("run") + *WATCH_CHILD.lock() = Some({ + let mut com = Command::new(env::current_exe().unwrap()); + com.arg("run") .arg(path) .args((!color).then_some("--no-color")) .args([ @@ -609,12 +625,16 @@ impl WatchArgs { "--audio-port", #[cfg(feature = "audio")] &audio_port, - ]) - .args(&args) + ]); + #[cfg(feature = "window")] + if use_window() { + com.arg("--window"); + } + com.args(&args) .stdin(stdin_file.map_or_else(Stdio::inherit, Into::into)) .spawn() - .unwrap(), - ); + .unwrap() + }); return Ok(()); } Err(e) => { @@ -679,7 +699,17 @@ impl WatchArgs { #[derive(Parser)] #[clap(version)] -enum App { +struct App { + #[clap(subcommand)] + command: Option, + file: Option, + #[cfg(feature = "window")] + #[clap(short, long, help = "Use a window for output instead of stdout")] + window: bool, +} + +#[derive(Subcommand)] +enum Comm { #[clap(about = "Initialize a new main.ua file")] Init, #[clap(about = "Format and run a file")] @@ -700,6 +730,9 @@ enum App { #[cfg(feature = "audio")] #[clap(flatten)] audio_options: AudioOptions, + #[cfg(feature = "window")] + #[clap(long, help = "Use a window for output instead of stdout")] + window: bool, #[clap(trailing_var_arg = true, help = "Arguments to pass to the program")] args: Vec, }, @@ -740,6 +773,9 @@ enum App { formatter_options: FormatterOptions, #[clap(long, help = "Clear the terminal on file change")] clear: bool, + #[cfg(feature = "window")] + #[clap(short, long, help = "Use a window for output instead of stdout")] + window: bool, #[clap(long, help = "Read stdin from file")] stdin_file: Option, #[clap(trailing_var_arg = true, help = "Arguments to pass to the program")] @@ -872,14 +908,23 @@ const WATCHING: &str = "\x1b[0mwatching for changes..."; fn print_watching() { #[cfg(feature = "raw_mode")] rawrrr::disable_raw(); + if use_window() { + return; + } eprint!("{}", WATCHING); stderr().flush().unwrap(); } fn clear_watching() { + if use_window() { + return; + } clear_watching_with("―", "\n") } fn clear_watching_with(s: &str, end: &str) { + if use_window() { + return; + } print!( "\r{}{}", s.repeat(terminal_size::terminal_size().map_or(10, |(w, _)| w.0 as usize)), @@ -974,6 +1019,19 @@ fn format_multi_files(config: &FormatConfig) -> Result<(), UiuaError> { } fn print_stack(stack: &[Value], color: bool) { + #[cfg(feature = "window")] + if uiua::window::use_window() { + _ = uiua::window::Request::Separator.send(); + _ = uiua::window::Request::ShowAll( + stack + .iter() + .map(|v| SmartOutput::from_value(v.clone(), &NativeSys)) + .collect(), + ) + .send(); + _ = uiua::window::Request::ClearBeforeNext.send(); + return; + } if stack.len() == 1 || !color { for value in stack { println!("{}", value.show()); diff --git a/src/sys/native.rs b/src/sys/native.rs index 193805276..29b9a346e 100644 --- a/src/sys/native.rs +++ b/src/sys/native.rs @@ -572,8 +572,8 @@ impl SysBackend for NativeSys { true } #[cfg(all(feature = "terminal_image", feature = "image"))] - fn show_image(&self, image: image::DynamicImage, _: Option<&str>) -> Result<(), String> { - let (width, height) = if let Some((w, h)) = terminal_size() { + fn show_image(&self, image: image::DynamicImage, label: Option<&str>) -> Result<(), String> { + let (_width, _height) = if let Some((w, h)) = terminal_size() { let (tw, th) = (w as u32, h.saturating_sub(1) as u32); let (iw, ih) = (image.width(), (image.height() / 2).max(1)); let scaled_to_height = (iw * th / ih.max(1), th); @@ -607,11 +607,21 @@ impl SysBackend for NativeSys { print!("{s}"); Ok(()) } else { + #[cfg(feature = "window")] + { + crate::window::Request::Show(crate::encode::SmartOutput::Png( + crate::encode::image_to_bytes(&image, image::ImageOutputFormat::Png) + .map_err(|e| e.to_string())?, + label.map(Into::into), + )) + .send() + } + #[cfg(not(feature = "window"))] viuer::print( &image, &viuer::Config { - width, - height, + width: _width, + height: _height, absolute_offset: false, transparent: true, ..Default::default() diff --git a/src/window.rs b/src/window.rs index b91be048c..3df083428 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,119 +1,339 @@ use std::{ + collections::HashMap, env::current_exe, io::{ErrorKind, Read, Write}, net::{SocketAddr, TcpListener, TcpStream}, process::{exit, Command, Stdio}, - thread::{self, sleep}, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, + thread, time::Duration, }; use crossbeam_channel::Receiver; use eframe::egui::*; +use image::{GenericImageView, ImageFormat}; +use load::SizedTexture; use serde::*; -use crate::Value; +use crate::encode::SmartOutput; + +static USE_WINDOW: AtomicBool = AtomicBool::new(false); + +pub fn use_window() -> bool { + USE_WINDOW.load(atomic::Ordering::Relaxed) +} + +pub fn set_use_window(use_window: bool) { + USE_WINDOW.store(use_window, atomic::Ordering::Relaxed); +} const PORT: u16 = 8482; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub enum Request { - Show(Vec), + ShowText(String), + Show(SmartOutput), + ShowAll(Vec), + Separator, + ClearBeforeNext, } +const RETRIES: usize = 10; + impl Request { - pub fn send(self) { - self.send_impl(true); + pub fn send(self) -> Result<(), String> { + self.send_impl(RETRIES) } - fn send_impl(self, allow_creation: bool) { + fn send_impl(self, retries: usize) -> Result<(), String> { let socket_addr = ([127, 0, 0, 1], PORT).into(); - let mut stream = - match TcpStream::connect_timeout(&socket_addr, Duration::from_secs_f32(0.5)) { - Ok(stream) => stream, - Err(e) if allow_creation && e.kind() == ErrorKind::TimedOut => { - Command::new(current_exe().unwrap()) - .arg("window") - .stdout(if cfg!(debug_assertions) { - Stdio::inherit() - } else { - Stdio::null() - }) - .spawn() - .unwrap(); - sleep(Duration::from_secs_f32(0.5)); - return self.send_impl(false); + let timeout = Duration::from_secs_f32(if retries + 1 == RETRIES { 1.0 } else { 0.1 }); + let mut stream = match TcpStream::connect_timeout(&socket_addr, timeout) { + Ok(stream) => stream, + Err(e) if retries > 0 && e.kind() == ErrorKind::TimedOut => { + if cfg!(debug_assertions) { + eprintln!("Uiua window not found, creating..."); } - Err(e) => { - eprintln!("Failed to connect to window: {e}"); - exit(1); + Command::new(current_exe().unwrap()) + .arg("window") + .stdout(if cfg!(debug_assertions) { + Stdio::inherit() + } else { + Stdio::null() + }) + .spawn() + .unwrap(); + if cfg!(debug_assertions) { + eprintln!("Uiua window created, waiting for connection..."); } - }; - let json = serde_json::to_string(&self).unwrap(); - stream.write_all(json.as_bytes()).unwrap(); - stream.flush().unwrap(); + return self.send_impl(retries - 1); + } + Err(e) => { + return Err(format!("Failed to connect to window: {e}")); + } + }; + let bin = rmp_serde::to_vec(&self).unwrap(); + stream.write_all(&bin).map_err(|e| e.to_string())?; + stream.flush().map_err(|e| e.to_string())?; + Ok(()) } } pub fn run_window() { + let (send, recv) = crossbeam_channel::unbounded(); + thread::spawn(move || { + let addr = SocketAddr::from(([0u8; 4], PORT)); + let listener = match TcpListener::bind(addr) { + Ok(listener) => listener, + Err(e) => { + eprintln!("Failed to bind to port {PORT}: {e}"); + exit(1); + } + }; + if cfg!(debug_assertions) { + eprintln!("Listening on port {PORT}"); + } + loop { + match listener.accept() { + Ok((mut stream, addr)) => { + if cfg!(debug_assertions) { + eprintln!("Accepted connection from {addr}"); + } + let mut buffer = Vec::new(); + stream.read_to_end(&mut buffer).unwrap(); + match rmp_serde::from_slice(&buffer) { + Ok(req) => send.send(req).unwrap(), + Err(e) => { + eprintln!("Failed to decode request: {e}") + } + } + } + // Break if the window is closed + Err(e) if e.to_string().is_empty() => break, + Err(e) => { + eprintln!("Failed to accept connection: {e}") + } + }; + } + }); eframe::run_native( "Uiua", eframe::NativeOptions::default(), Box::new(|cc| { - cc.egui_ctx.set_visuals(Visuals::dark()); - Ok(Box::new(App::default())) + cc.egui_ctx.set_theme(Theme::Dark); + let mut fonts = FontDefinitions::default(); + fonts.font_data.insert( + "Uiua386".into(), + FontData::from_static(include_bytes!("algorithm/Uiua386.ttf")), + ); + fonts + .families + .entry(FontFamily::Monospace) + .or_default() + .insert(0, "Uiua386".into()); + cc.egui_ctx.set_fonts(fonts); + Ok(Box::new(App::new(recv, &cc.egui_ctx))) }), ) .unwrap(); } struct App { - stack: Vec, + items: Vec, recv: Receiver, + next_id: u64, + ppp: f32, + clear: bool, + clear_before_next: bool, + size_map: HashMap<[u32; 2], Vec2>, } -impl Default for App { - fn default() -> Self { - let (send, recv) = crossbeam_channel::unbounded(); - thread::spawn(move || { - let addr = SocketAddr::from(([0u8; 4], PORT)); - let listener = match TcpListener::bind(addr) { - Ok(listener) => listener, - Err(e) => { - eprintln!("Failed to bind to port {PORT}: {e}"); - exit(1); - } - }; - eprintln!("Listening on port {PORT}"); - loop { - match listener.accept() { - Ok((mut stream, _)) => { - let mut buffer = String::new(); - stream.read_to_string(&mut buffer).unwrap(); - let req: Request = serde_json::from_str(&buffer).unwrap(); - send.send(req).unwrap(); - } - // Break if the window is closed - Err(e) if e.kind() == ErrorKind::ConnectionReset => break, - Err(e) => { - eprintln!("Failed to accept connection: {e}"); - continue; - } - }; - } +enum OutputItem { + Text(String), + Code(String), + Error(String), + Image { + id: u64, + tex_id: TextureId, + true_size: [u32; 2], + label: Option, + }, + Separator, +} + +impl App { + fn new(recv: Receiver, ctx: &Context) -> Self { + let (ppp, clear) = ctx.memory_mut(|mem| { + ( + mem.data.get_persisted(Id::new("ppp")).unwrap_or(1.5), + mem.data.get_persisted(Id::new("clear")).unwrap_or(false), + ) }); + ctx.set_pixels_per_point(ppp); App { - stack: Vec::new(), + items: Vec::new(), recv, + next_id: 0, + ppp, + clear, + clear_before_next: false, + size_map: HashMap::new(), } } } impl eframe::App for App { fn update(&mut self, ctx: &Context, _: &mut eframe::Frame) { - for req in self.recv.try_iter() { + while let Ok(req) = self.recv.try_recv() { + if self.clear_before_next { + self.clear_before_next = false; + self.items.clear(); + } match req { - Request::Show(stack) => self.stack = stack, + Request::ShowText(text) => self.items.push(OutputItem::Text(text)), + Request::Show(so) => { + let item = self.convert_smart_output(so, ctx); + self.items.push(item); + self.clear_before_next = false; + } + Request::ShowAll(sos) => { + for so in sos.into_iter().rev() { + let item = self.convert_smart_output(so, ctx); + self.items.push(item); + } + self.clear_before_next = false; + } + Request::Separator => self.items.push(OutputItem::Separator), + Request::ClearBeforeNext => self.clear_before_next = self.clear, + } + } + TopBottomPanel::top("top bar").show(ctx, |ui| { + ui.horizontal(|ui| { + ComboBox::new("ppp", "🔍") + .selected_text(format!("{:.0}%", self.ppp * 100.0)) + .show_ui(ui, |ui| { + for ppp in [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0] { + let label = format!("{:.0}%", ppp * 100.0); + if ui.selectable_value(&mut self.ppp, ppp, label).clicked() { + ui.ctx().set_pixels_per_point(ppp); + } + } + }); + Checkbox::new(&mut self.clear, "Clear") + .ui(ui) + .on_hover_text("Clear on each run"); + if !self.clear && ui.button("Clear All").clicked() { + self.items.clear(); + } + }) + }); + CentralPanel::default().show(ctx, |ui| { + ScrollArea::both() + .auto_shrink([false; 2]) + .show(ui, |ui| self.inner(ui)) + }); + if ctx.input(|input| input.viewport().close_requested()) { + ctx.data_mut(|data| { + data.clear(); + data.insert_persisted(Id::new("ppp"), self.ppp); + data.insert_persisted(Id::new("clear"), self.clear); + }); + } + ctx.request_repaint_after_secs(0.1); + } +} + +impl App { + fn inner(&mut self, ui: &mut Ui) { + for item in self.items.iter_mut().rev() { + match item { + OutputItem::Text(text) => { + ui.label(&*text); + } + OutputItem::Code(code) => { + ui.label(RichText::new(code.as_str()).font(FontId::monospace(14.0))); + } + OutputItem::Error(error) => { + ui.label(RichText::new(error.as_str()).color(Color32::RED)); + } + OutputItem::Separator => { + ui.separator(); + } + OutputItem::Image { + id, + tex_id, + true_size, + label, + } => { + if let Some(label) = label { + ui.code(format!("{label}:")); + } + ui.horizontal(|ui| { + let size = self + .size_map + .get(true_size) + .copied() + .unwrap_or_else(|| vec2(true_size[0] as f32, true_size[1] as f32)); + let resp = + (Resize::default().id_salt(*id).default_size(size)).show(ui, |ui| { + let available_width = ui.available_width(); + let available_height = ui.available_height(); + let aspect_ratio = true_size[0] as f32 / true_size[1] as f32; + let use_height = + (available_width / aspect_ratio).min(available_height); + let use_width = (use_height * aspect_ratio).min(available_width); + ui.image(SizedTexture { + id: *tex_id, + size: vec2(use_width, use_height), + }) + }); + let changed = resp.rect.width() != size.x && resp.rect.height() != size.y; + if changed { + self.size_map.insert(*true_size, resp.rect.size()); + } + if ui.button("↻").on_hover_text("Reset size").clicked() { + *id = self.next_id; + self.next_id += 1; + } + }); + } + } + } + } + fn convert_smart_output(&mut self, output: SmartOutput, ctx: &Context) -> OutputItem { + match output { + SmartOutput::Normal(value) => OutputItem::Code(value.show()), + SmartOutput::Png(bytes, label) => { + let img = image::load_from_memory_with_format(&bytes, ImageFormat::Png).unwrap(); + let (width, height) = img.dimensions(); + let pixels: Vec = img + .into_rgba8() + .into_raw() + .chunks_exact(4) + .map(|w| Color32::from_rgba_unmultiplied(w[0], w[1], w[2], w[3])) + .collect(); + let color_image = ColorImage { + size: [width as usize, height as usize], + pixels, + }; + let text_id = ctx.tex_manager().write().alloc( + String::new(), + ImageData::Color(Arc::new(color_image)), + Default::default(), + ); + let unique = self.next_id; + self.next_id += 1; + OutputItem::Image { + id: unique, + tex_id: text_id, + true_size: [width, height], + label, + } } + SmartOutput::Gif(..) => OutputItem::Error("Gif not yet implemented".into()), + SmartOutput::Wav(..) => OutputItem::Error("Audio not yet implemented".into()), } - CentralPanel::default().show(ctx, |_ui| {}); } }