diff --git a/.gitignore b/.gitignore index ea8c4bf..c849890 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +dist/ diff --git a/Cargo.lock b/Cargo.lock index f0c9941..44d1da0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,9 +46,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "cassowary" @@ -67,9 +73,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.14" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "shlex", ] @@ -121,6 +127,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "crossterm" version = "0.28.1" @@ -181,11 +197,20 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "either" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" [[package]] name = "engineering-repr" @@ -306,6 +331,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -314,9 +349,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "linux-raw-sys" @@ -336,9 +371,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "lru" @@ -386,6 +421,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -415,6 +456,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -471,6 +521,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.93" @@ -505,23 +561,36 @@ dependencies = [ "lru", "paste", "strum", + "time", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", ] +[[package]] +name = "ratzilla" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501e0c35cbec571237034fd003912e4f63255efb7f3b72e760b432d03833bc1f" +dependencies = [ + "console_error_panic_hook", + "ratatui", + "thiserror", + "web-sys", +] + [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" dependencies = [ "bitflags", ] [[package]] name = "rusistor" -version = "0.3.0" +version = "0.3.1" [[package]] name = "rustc-demangle" @@ -560,6 +629,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -686,6 +775,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "tracing" version = "0.1.41" @@ -747,6 +857,25 @@ dependencies = [ "ratatui", "rusistor", "tui-input", + "tusistor-core", +] + +[[package]] +name = "tusistor-core" +version = "0.1.0" +dependencies = [ + "engineering-repr", + "rusistor", +] + +[[package]] +name = "tusistor-web" +version = "0.1.0" +dependencies = [ + "engineering-repr", + "ratzilla", + "rusistor", + "tusistor-core", ] [[package]] @@ -796,6 +925,74 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index c9fce37..08a2f67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,6 @@ [workspace] +members = ["rusistor", "tusistor", "tusistor-core", "tusistor-web" ] +resolver = "2" [workspace.package] authors = ["dawe "] @@ -6,19 +8,3 @@ license = "MIT" edition = "2024" repository = "https://github.com/dawedawe/tusistor" -[package] -name = "tusistor" -version = "0.6.0" -authors.workspace = true -license.workspace = true -edition.workspace = true -repository.workspace = true -description = "This is a Ratatui app to calculate the color code of electrical resistors." - -[dependencies] -crossterm = "0.28.1" -ratatui = "0.29.0" -color-eyre = "0.6.3" -tui-input = "0.11.1" -rusistor = { path = "rusistor", version = "0.3.0" } -engineering-repr = "1.1.0" diff --git a/rusistor/Cargo.toml b/rusistor/Cargo.toml index 850f378..6b79521 100644 --- a/rusistor/Cargo.toml +++ b/rusistor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rusistor" -version = "0.3.0" +version = "0.3.1" authors.workspace = true license.workspace = true edition.workspace = true diff --git a/rusistor/src/lib.rs b/rusistor/src/lib.rs index 8bc1fe0..59a5609 100644 --- a/rusistor/src/lib.rs +++ b/rusistor/src/lib.rs @@ -147,6 +147,27 @@ impl From for Color { } } +impl From for Color { + fn from(value: usize) -> Self { + match value { + 0 => Color::Black, + 1 => Color::Brown, + 2 => Color::Red, + 3 => Color::Orange, + 4 => Color::Yellow, + 5 => Color::Green, + 6 => Color::Blue, + 7 => Color::Violet, + 8 => Color::Grey, + 9 => Color::White, + 10 => Color::Gold, + 11 => Color::Silver, + 12 => Color::Pink, + _ => panic!("invalid value {} given to Color::from", value), + } + } +} + impl Display for Color { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let s = match self { diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index e27a22b..0000000 --- a/src/app.rs +++ /dev/null @@ -1,990 +0,0 @@ -pub mod model { - use rusistor::Resistor; - use tui_input::Input; - - #[derive(Debug, Default)] - pub enum InputFocus { - #[default] - Resistance, - Tolerance, - Tcr, - } - - impl InputFocus { - pub fn next(&self) -> InputFocus { - match self { - InputFocus::Resistance => InputFocus::Tolerance, - InputFocus::Tolerance => InputFocus::Tcr, - InputFocus::Tcr => InputFocus::Resistance, - } - } - - pub fn prev(&self) -> InputFocus { - match self { - InputFocus::Resistance => InputFocus::Tcr, - InputFocus::Tolerance => InputFocus::Resistance, - InputFocus::Tcr => InputFocus::Tolerance, - } - } - } - - #[derive(Debug, Default, PartialEq)] - pub enum SelectedTab { - #[default] - ColorCodesToSpecs, - SpecsToColorCodes, - } - - impl SelectedTab { - pub fn toggle(&self) -> SelectedTab { - match self { - SelectedTab::ColorCodesToSpecs => SelectedTab::SpecsToColorCodes, - SelectedTab::SpecsToColorCodes => SelectedTab::ColorCodesToSpecs, - } - } - } - - impl From<&SelectedTab> for Option { - fn from(selected_tab: &SelectedTab) -> Self { - match selected_tab { - SelectedTab::ColorCodesToSpecs => Some(0), - SelectedTab::SpecsToColorCodes => Some(1), - } - } - } - - #[derive(Debug, Default)] - pub struct SpecsToColorModel { - pub resistance_input: Input, - pub tolerance_input: Input, - pub tcr_input: Input, - pub focus: InputFocus, - pub resistor: Option, - pub error: Option, - } - - #[derive(Debug)] - pub struct ColorCodesToSpecsModel { - pub selected_band: usize, - pub resistor: Resistor, - } - - impl Default for ColorCodesToSpecsModel { - fn default() -> ColorCodesToSpecsModel { - ColorCodesToSpecsModel { - selected_band: 0, - resistor: Resistor::SixBand { - band1: rusistor::Color::Brown, - band2: rusistor::Color::Black, - band3: rusistor::Color::Black, - band4: rusistor::Color::Black, - band5: rusistor::Color::Brown, - band6: rusistor::Color::Black, - }, - } - } - } - - #[derive(Debug)] - pub struct Model { - pub running: bool, - pub selected_tab: SelectedTab, - pub specs_to_color: SpecsToColorModel, - pub color_codes_to_specs: ColorCodesToSpecsModel, - } - - impl Default for Model { - fn default() -> Model { - Model { - running: true, - selected_tab: SelectedTab::default(), - specs_to_color: SpecsToColorModel::default(), - color_codes_to_specs: ColorCodesToSpecsModel::default(), - } - } - } -} - -pub mod view { - use crate::app::model::{InputFocus, Model, SelectedTab}; - use ratatui::{ - Frame, - layout::{Constraint, Direction, Flex, Layout, Rect}, - style::{Color, Modifier, Style}, - symbols, - text::{Line, Span, Text}, - widgets::{ - Bar, BarChart, BarGroup, Block, Borders, List, ListDirection, ListItem, ListState, - Padding, Paragraph, Tabs, - }, - }; - - const BAR_WIDTH: u16 = 19; - - fn tabs<'a>(selected: &SelectedTab) -> Tabs<'a> { - Tabs::new(vec![" color codes to specs ", " specs to color codes "]) - .padding(" ", " ") - .divider(symbols::DOT) - .select(selected) - } - - fn band_numeric_info(bands: usize, band_idx: usize, color: &rusistor::Color) -> String { - match (bands, band_idx) { - (3, i) | (4, i) if i <= 1 => { - if i == 0 && *color == rusistor::Color::Black { - " ".to_string() - } else { - color.as_digit().map_or(" ".to_string(), |s| s.to_string()) - } - } - (5, i) | (6, i) if i <= 2 => { - if i == 0 && *color == rusistor::Color::Black { - " ".to_string() - } else { - color.as_digit().map_or(" ".to_string(), |s| s.to_string()) - } - } - (3, 2) | (4, 2) | (5, 3) | (6, 3) => { - format!("10^{}", color.as_digit_or_exponent()) - } - (4, 3) | (5, 4) | (6, 4) => color - .as_tolerance() - .map_or(" ".to_string(), |s| format!("{:>4}", (s * 100.0))), - (6, 5) => color - .as_tcr() - .map_or(" ".to_string(), |s| format!("{:>3}", s.to_string())), - _ => "".to_string(), - } - } - - fn band_semantic_info(bands: usize, band_idx: usize) -> String { - match (bands, band_idx) { - (3, i) | (4, i) if i <= 1 => format!("Digit {}", band_idx + 1), - (5, i) | (6, i) if i <= 2 => format!("Digit {}", band_idx + 1), - (3, 2) | (4, 2) | (5, 3) | (6, 3) => "Multiplier".to_string(), - (4, 3) | (5, 4) | (6, 4) => "Tolerance".to_string(), - (6, 5) => "TCR".to_string(), - _ => "".to_string(), - } - } - - fn band_list<'a>(band_idx: usize, bands: usize, is_focused: bool) -> List<'a> { - let items = [ - rusistor::Color::Black, - rusistor::Color::Brown, - rusistor::Color::Red, - rusistor::Color::Orange, - rusistor::Color::Yellow, - rusistor::Color::Green, - rusistor::Color::Blue, - rusistor::Color::Violet, - rusistor::Color::Grey, - rusistor::Color::White, - rusistor::Color::Gold, - rusistor::Color::Silver, - rusistor::Color::Pink, - ] - .iter() - .map(|color| { - let numeric_info = band_numeric_info(bands, band_idx, color); - let (color, name) = rusistor_color_to_ratatui_color(color); - let s = format!(" {numeric_info} {name}"); - let style = if color == Color::Black { - Style::default().bg(color) - } else { - Style::default().bg(color).fg(Color::Black) - }; - ListItem::new(s).style(style) - }); - - let style = if is_focused { - Style::default().add_modifier(Modifier::BOLD) - } else { - Style::default() - }; - - let semantic_info = band_semantic_info(bands, band_idx); - - List::new(items) - .block( - Block::bordered() - .title(format!( - " Band {}: {}{}", - band_idx + 1, - semantic_info, - if is_focused { "* " } else { " " } - )) - .style(style), - ) - .highlight_symbol(">> ") - .repeat_highlight_symbol(true) - .direction(ListDirection::TopToBottom) - } - - pub fn view(model: &Model, frame: &mut Frame) { - fn center_horizontal(area: Rect, width: u16) -> Rect { - let [area] = Layout::horizontal([Constraint::Length(width)]) - .flex(Flex::Center) - .areas(area); - area - } - - let tabs_width = 49; - - match model.selected_tab { - SelectedTab::ColorCodesToSpecs => { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - [ - Constraint::Length(2), - Constraint::Length(3), - Constraint::Length(15), - Constraint::Min(1), - ] - .as_ref(), - ) - .split(frame.area()); - let tabs_rect = center_horizontal(chunks[0], tabs_width); - let help_msg_rect = center_horizontal(chunks[3], 95); - - let spec_chuncks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - ]) - .split(chunks[1]); - - let bands_rect = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - Constraint::Ratio(1, 6), - ]) - .split(chunks[2]); - - let tabs = tabs(&model.selected_tab); - frame.render_widget(tabs, tabs_rect); - - let specs = model.color_codes_to_specs.resistor.specs(); - - let resistance_paragraph = Paragraph::new(specs.ohm.to_string()) - .style(Style::default().fg(Color::Yellow)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" Resistance (Ω) "), - ); - frame.render_widget(resistance_paragraph, spec_chuncks[0]); - - let tolerance_paragraph = Paragraph::new(format!("±{}", (specs.tolerance * 100.0))) - .style(Style::default().fg(Color::Yellow)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" Tolerance (%) "), - ); - frame.render_widget(tolerance_paragraph, spec_chuncks[1]); - - let min_paragraph = Paragraph::new(specs.min_ohm.to_string()) - .style(Style::default().fg(Color::Yellow)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" Minimum (Ω) "), - ); - frame.render_widget(min_paragraph, spec_chuncks[2]); - - let max_paragraph = Paragraph::new(specs.max_ohm.to_string()) - .style(Style::default().fg(Color::Yellow)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" Maximum (Ω) "), - ); - frame.render_widget(max_paragraph, spec_chuncks[3]); - - let tcr_paragraph = - Paragraph::new(specs.tcr.map(|f| f.to_string()).unwrap_or_default()) - .style(Style::default().fg(Color::Yellow)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" TCR (ppm/K) "), - ); - frame.render_widget(tcr_paragraph, spec_chuncks[4]); - - let (msg, style) = ( - vec![ - Span::styled("Tab", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": next band, "), - Span::styled("↑/↓", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": prev/next color, "), - Span::styled("3|4|5|6", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": bands count, "), - Span::styled("Shift ←/→", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": prev/next tab, "), - Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": exit"), - ], - Style::default(), - ); - let text = Text::from(Line::from(msg)).style(style); - let help_message = Paragraph::new(text); - frame.render_widget(help_message, help_msg_rect); - - let bands = model.color_codes_to_specs.resistor.bands(); - for i in 0..bands.len() { - let mut state = ListState::default().with_selected(Some(*bands[i] as usize)); - let is_focused = model.color_codes_to_specs.selected_band == i; - let list = band_list(i, bands.len(), is_focused); - frame.render_stateful_widget(list, bands_rect[i], &mut state); - } - } - SelectedTab::SpecsToColorCodes => { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(2) - .constraints( - [ - Constraint::Length(2), - Constraint::Length(3), - Constraint::Min(1), - Constraint::Length(1), - ] - .as_ref(), - ) - .split(frame.area()); - let input_rects = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - Constraint::Ratio(1, 3), - ]) - .split(chunks[1]); - - let tabs_rect = center_horizontal(chunks[0], tabs_width); - let help_msg_rect = center_horizontal(chunks[3], 82); - let resistance_rect = input_rects[0]; - let tolerance_rect = input_rects[1]; - let tcr_rect = input_rects[2]; - let main_rect = chunks[2]; - - let tabs = tabs(&model.selected_tab); - frame.render_widget(tabs, tabs_rect); - - let (msg, style) = ( - vec![ - Span::styled("Tab", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": next input, "), - Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": calculate color codes, "), - Span::styled("Shift ←/→", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": prev/next tab, "), - Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(": exit"), - ], - Style::default(), - ); - let text = Text::from(Line::from(msg)).style(style); - let help_message = Paragraph::new(text); - frame.render_widget(help_message, help_msg_rect); - - let resistance_width = resistance_rect.width.max(3) - 3; // keep 2 for borders and 1 for cursor - let resistance_scroll = model - .specs_to_color - .resistance_input - .visual_scroll(resistance_width as usize); - let resistance_paragraph = - Paragraph::new(model.specs_to_color.resistance_input.value()) - .style(Style::default().fg(Color::Yellow)) - .scroll((0, resistance_scroll as u16)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" Resistance (Ω) "), - ); - frame.render_widget(resistance_paragraph, resistance_rect); - - let tolerance_width = tolerance_rect.width.max(3) - 3; // keep 2 for borders and 1 for cursor - let tolerance_scroll = model - .specs_to_color - .tolerance_input - .visual_scroll(tolerance_width as usize); - let tolerance_paragraph = - Paragraph::new(model.specs_to_color.tolerance_input.value()) - .style(Style::default().fg(Color::Yellow)) - .scroll((0, tolerance_scroll as u16)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" Tolerance (%) "), - ); - frame.render_widget(tolerance_paragraph, tolerance_rect); - - let tcr_width = tcr_rect.width.max(3) - 3; // keep 2 for borders and 1 for cursor - let tcr_scroll = model - .specs_to_color - .tcr_input - .visual_scroll(tcr_width as usize); - let tcr_paragraph = Paragraph::new(model.specs_to_color.tcr_input.value()) - .style(Style::default().fg(Color::Yellow)) - .scroll((0, tcr_scroll as u16)) - .block( - Block::default() - .borders(Borders::ALL) - .title(" TCR (ppm/K) "), - ); - frame.render_widget(tcr_paragraph, tcr_rect); - - // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering - let (rect, input, scroll) = match model.specs_to_color.focus { - InputFocus::Resistance => ( - resistance_rect, - &model.specs_to_color.resistance_input, - resistance_scroll, - ), - InputFocus::Tolerance => ( - tolerance_rect, - &model.specs_to_color.tolerance_input, - tolerance_scroll, - ), - InputFocus::Tcr => (tcr_rect, &model.specs_to_color.tcr_input, tcr_scroll), - }; - frame.set_cursor_position(( - // Put cursor past the end of the input text - rect.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 1, - // Move one line down, from the border to the input line - rect.y + 1, - )); - - if let Some(resistor) = &model.specs_to_color.resistor { - let bands = resistor.bands(); - let band_infos = bands - .iter() - .enumerate() - .map(|(idx, c)| { - let sem_info = band_semantic_info(bands.len(), idx); - let num_info = band_numeric_info(bands.len(), idx, c); - let (color, name) = rusistor_color_to_ratatui_color(c); - (sem_info, num_info, color, name) - }) - .collect::>(); - let specs = resistor.specs(); - let chart = barchart(&band_infos, specs.ohm, specs.tolerance, specs.tcr); - let chart_length: u16 = { - let bands_len: u16 = bands.len() as u16; - let bands_widths = bands_len * BAR_WIDTH; - let bands_gaps = bands_len - 1; - let border_plus_margin = 4; - bands_widths + bands_gaps + border_plus_margin - }; - let centered_main_rect = center_horizontal(main_rect, chart_length); - frame.render_widget(chart, centered_main_rect); - } - if let Some(e) = &model.specs_to_color.error { - let text = Text::from(e.to_string()); - let error_message = Paragraph::new(text).style(Style::default().fg(Color::Red)); - frame.render_widget(error_message, main_rect); - } - } - } - } - - fn barchart( - band_infos: &[(String, String, Color, String)], - ohm: f64, - tolerance: f64, - tcr: Option, - ) -> BarChart { - let bars: Vec = band_infos.iter().map(|i| bar(i)).collect(); - let tcr = if let Some(tcr) = tcr { - format!(" - TCR: {}(ppm/K)", tcr) - } else { - String::from("") - }; - let title = format!( - "Resistance: {}Ω - Tolerance: ±{}%{}", - ohm, - tolerance * 100.0, - tcr - ); - let title = Line::from(title).centered(); - BarChart::default() - .data(BarGroup::default().bars(&bars)) - .block( - Block::new() - .padding(Padding::new(1, 1, 1, 1)) - .title(title) - .borders(Borders::all()), - ) - .bar_width(BAR_WIDTH) - .bar_gap(1) - } - - fn bar((sem_info, num_info, color, name): &(String, String, Color, String)) -> Bar { - Bar::default() - .value(100) - .text_value(format!(" {} ", name)) - .value_style(Style::default().fg(Color::White).bg(Color::Black)) - .label(Line::from(format!("{}: {}", sem_info, num_info.trim()))) - .style(bar_style(color)) - } - - fn bar_style(color: &Color) -> Style { - Style::new().fg(*color) - } - - fn rusistor_color_to_ratatui_color(color: &rusistor::Color) -> (Color, String) { - match color { - rusistor::Color::Black => (Color::Black, rusistor::Color::Black.to_string()), - rusistor::Color::Brown => (Color::Rgb(165, 42, 42), rusistor::Color::Brown.to_string()), - rusistor::Color::Red => (Color::Red, rusistor::Color::Red.to_string()), - rusistor::Color::Orange => { - (Color::Rgb(255, 165, 0), rusistor::Color::Orange.to_string()) - } - rusistor::Color::Yellow => (Color::Yellow, rusistor::Color::Yellow.to_string()), - rusistor::Color::Green => (Color::Green, rusistor::Color::Green.to_string()), - rusistor::Color::Blue => (Color::Blue, rusistor::Color::Blue.to_string()), - rusistor::Color::Violet => { - (Color::Rgb(148, 0, 211), rusistor::Color::Violet.to_string()) - } - rusistor::Color::Grey => (Color::Gray, rusistor::Color::Grey.to_string()), - rusistor::Color::White => (Color::White, rusistor::Color::White.to_string()), - rusistor::Color::Gold => (Color::Rgb(255, 215, 0), rusistor::Color::Gold.to_string()), - rusistor::Color::Silver => ( - Color::Rgb(192, 192, 192), - rusistor::Color::Silver.to_string(), - ), - rusistor::Color::Pink => (Color::Rgb(255, 105, 180), rusistor::Color::Pink.to_string()), - } - } -} - -pub mod update { - use crate::app::model::{InputFocus, Model, SelectedTab}; - use color_eyre::Result; - use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; - use rusistor::{self, Resistor}; - use std::str::FromStr; - use tui_input::backend::crossterm::EventHandler; - - pub enum ColorCodesMsg { - ThreeBands, - FourBands, - FiveBands, - SixBands, - NextBand, - PrevBand, - NextColor, - PrevColor, - } - - pub enum SpecsMsg { - Determine, - NextSpecInput, - PrevSpecInput, - } - - pub enum Msg { - ToggleTab, - Exit, - SpecsMsg { msg: SpecsMsg }, - ColorCodesMsg { msg: ColorCodesMsg }, - } - - pub fn handle_event(model: &mut Model) -> Result> { - match event::read()? { - // it's important to check KeyEventKind::Press to avoid handling key release events - Event::Key(key) if key.kind == KeyEventKind::Press => { - Result::Ok(on_key_event(model, key)) - } - _ => Result::Ok(None), - } - } - - fn on_key_event(model: &mut Model, key: KeyEvent) -> Option { - match (key.code, &model.selected_tab) { - (KeyCode::Left, _) | (KeyCode::Right, _) if key.modifiers == KeyModifiers::SHIFT => { - Some(Msg::ToggleTab) - } - (KeyCode::Enter, SelectedTab::SpecsToColorCodes) => Some(Msg::SpecsMsg { - msg: SpecsMsg::Determine, - }), - (KeyCode::Tab, SelectedTab::SpecsToColorCodes) => Some(Msg::SpecsMsg { - msg: SpecsMsg::NextSpecInput, - }), - (KeyCode::Tab, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::NextBand, - }), - (KeyCode::BackTab, SelectedTab::SpecsToColorCodes) => Some(Msg::SpecsMsg { - msg: SpecsMsg::PrevSpecInput, - }), - (KeyCode::BackTab, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::PrevBand, - }), - (KeyCode::Up, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::PrevColor, - }), - (KeyCode::Down, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::NextColor, - }), - (KeyCode::Char('3'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::ThreeBands, - }), - (KeyCode::Char('4'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::FourBands, - }), - (KeyCode::Char('5'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::FiveBands, - }), - (KeyCode::Char('6'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { - msg: ColorCodesMsg::SixBands, - }), - (KeyCode::Esc, _) => Some(Msg::Exit), - _ => { - let target_input = match model.specs_to_color.focus { - InputFocus::Resistance => &mut model.specs_to_color.resistance_input, - InputFocus::Tolerance => &mut model.specs_to_color.tolerance_input, - InputFocus::Tcr => &mut model.specs_to_color.tcr_input, - }; - target_input.handle_event(&Event::Key(key)); - None - } - } - } - - pub fn update(model: &mut Model, msg: Msg) { - match msg { - Msg::ToggleTab => model.selected_tab = model.selected_tab.toggle(), - Msg::SpecsMsg { - msg: SpecsMsg::Determine, - } => { - match try_determine_resistor( - model.specs_to_color.resistance_input.value(), - model.specs_to_color.tolerance_input.value(), - model.specs_to_color.tcr_input.value(), - ) { - Ok(resistor) => { - model.specs_to_color.resistor = Some(resistor); - model.specs_to_color.error = None; - } - Err(e) => { - model.specs_to_color.resistor = None; - model.specs_to_color.error = Some(e); - } - } - model.specs_to_color.resistance_input.reset(); - model.specs_to_color.tolerance_input.reset(); - model.specs_to_color.tcr_input.reset(); - model.specs_to_color.focus = InputFocus::Resistance; - } - Msg::SpecsMsg { - msg: SpecsMsg::NextSpecInput, - } - | Msg::SpecsMsg { - msg: SpecsMsg::PrevSpecInput, - } => { - model.specs_to_color.error = match model.specs_to_color.focus { - InputFocus::Resistance => { - let value = model.specs_to_color.resistance_input.value(); - if value.trim().is_empty() { - None - } else { - try_parse_resistance(value).err().map(|err| err.to_string()) - } - } - InputFocus::Tolerance => { - let value = model.specs_to_color.tolerance_input.value(); - if value.trim().is_empty() { - None - } else { - value.parse::().err().map(|err| err.to_string()) - } - } - InputFocus::Tcr => { - let value = model.specs_to_color.tcr_input.value(); - if value.trim().is_empty() { - None - } else { - value.parse::().err().map(|err| err.to_string()) - } - } - }; - if model.specs_to_color.error.is_none() { - model.specs_to_color.focus = match msg { - Msg::SpecsMsg { - msg: SpecsMsg::NextSpecInput, - } => model.specs_to_color.focus.next(), - _ => model.specs_to_color.focus.prev(), - }; - } else { - model.specs_to_color.resistor = None; - } - } - Msg::Exit => { - model.running = false; - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::ThreeBands, - } => { - model.color_codes_to_specs.resistor = Resistor::ThreeBand { - band1: rusistor::Color::Brown, - band2: rusistor::Color::Black, - band3: rusistor::Color::Black, - }; - model.color_codes_to_specs.selected_band = - model.color_codes_to_specs.selected_band.min(2) - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::FourBands, - } => { - model.color_codes_to_specs.resistor = Resistor::FourBand { - band1: rusistor::Color::Brown, - band2: rusistor::Color::Black, - band3: rusistor::Color::Black, - band4: rusistor::Color::Brown, - }; - model.color_codes_to_specs.selected_band = - model.color_codes_to_specs.selected_band.min(3) - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::FiveBands, - } => { - model.color_codes_to_specs.resistor = Resistor::FiveBand { - band1: rusistor::Color::Brown, - band2: rusistor::Color::Black, - band3: rusistor::Color::Black, - band4: rusistor::Color::Black, - band5: rusistor::Color::Brown, - }; - model.color_codes_to_specs.selected_band = - model.color_codes_to_specs.selected_band.min(4) - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::SixBands, - } => { - model.color_codes_to_specs.resistor = Resistor::SixBand { - band1: rusistor::Color::Brown, - band2: rusistor::Color::Black, - band3: rusistor::Color::Black, - band4: rusistor::Color::Black, - band5: rusistor::Color::Brown, - band6: rusistor::Color::Black, - }; - model.color_codes_to_specs.selected_band = - model.color_codes_to_specs.selected_band.min(5) - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::NextBand, - } => { - model.color_codes_to_specs.selected_band = - (model.color_codes_to_specs.selected_band + 1) - % model.color_codes_to_specs.resistor.bands().len() - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::PrevBand, - } => { - let bands_count = model.color_codes_to_specs.resistor.bands().len(); - model.color_codes_to_specs.selected_band = - (model.color_codes_to_specs.selected_band + (bands_count - 1)) % bands_count - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::NextColor, - } => { - let current_idx: usize = *model.color_codes_to_specs.resistor.bands() - [model.color_codes_to_specs.selected_band] - as usize; - let mut i: usize = 0; - let mut resistor = Err("".to_string()); - while resistor.is_err() { - i += 1; - let next_color = index_to_color((current_idx + i) % 13); - resistor = model - .color_codes_to_specs - .resistor - .with_color(next_color, model.color_codes_to_specs.selected_band); - } - model.color_codes_to_specs.resistor = resistor.unwrap(); - } - Msg::ColorCodesMsg { - msg: ColorCodesMsg::PrevColor, - } => { - let current_idx = *model.color_codes_to_specs.resistor.bands() - [model.color_codes_to_specs.selected_band] - as usize; - let mut i: usize = 13; - let mut resistor = Err("".to_string()); - while resistor.is_err() { - i -= 1; - let next_color = index_to_color((current_idx + i) % 13); - resistor = model - .color_codes_to_specs - .resistor - .with_color(next_color, model.color_codes_to_specs.selected_band); - } - model.color_codes_to_specs.resistor = resistor.unwrap(); - } - } - } - - fn try_parse_resistance(input: &str) -> Result { - match input.parse::() { - Ok(t) => Ok(t), - Err(e) => match engineering_repr::EngineeringQuantity::::from_str(input) { - Ok(t) => { - let r: i64 = t.into(); - let r = r as f64; - Ok(r) - } - Err(_) => Err(format!("invalid input for resistance: {}", e)), - }, - } - } - - fn try_determine_resistor( - resistance_input: &str, - tolerance_input: &str, - tcr_input: &str, - ) -> Result { - let resistance = try_parse_resistance(resistance_input); - let tolerance = if tolerance_input.is_empty() { - Ok(None) - } else { - match tolerance_input.parse::() { - Ok(t) => Ok(Some(t)), - Err(e) => Err(format!("invalid input for tolerance: {}", e)), - } - }; - - let tcr = if tcr_input.is_empty() { - Ok(None) - } else { - match tcr_input.parse::() { - Ok(t) => Ok(Some(t)), - Err(e) => Err(format!("invalid input for tcr: {}", e)), - } - }; - - match (resistance, tolerance, tcr) { - (Ok(resistance), Ok(tolerance), Ok(tcr)) => { - match Resistor::determine(resistance, tolerance, tcr) { - Ok(resistor) => Ok(resistor), - Err(e) => Err(format!( - "could not determine a resistor for these inputs: {}", - e - )), - } - } - (res, tol, tcr) => { - let mut error_msg: String = String::from(""); - if let Err(res_error) = res { - error_msg.push_str(res_error.to_string().as_str()); - } - if let Err(tol_error) = tol { - error_msg.push('\n'); - error_msg.push_str(tol_error.to_string().as_str()); - } - if let Err(tcr_error) = tcr { - error_msg.push('\n'); - error_msg.push_str(tcr_error.to_string().as_str()); - } - if error_msg.is_empty() { - panic!("unknown error"); - } else { - Err(error_msg) - } - } - } - } - - fn index_to_color(idx: usize) -> rusistor::Color { - match idx { - 0 => rusistor::Color::Black, - 1 => rusistor::Color::Brown, - 2 => rusistor::Color::Red, - 3 => rusistor::Color::Orange, - 4 => rusistor::Color::Yellow, - 5 => rusistor::Color::Green, - 6 => rusistor::Color::Blue, - 7 => rusistor::Color::Violet, - 8 => rusistor::Color::Grey, - 9 => rusistor::Color::White, - 10 => rusistor::Color::Gold, - 11 => rusistor::Color::Silver, - 12 => rusistor::Color::Pink, - _ => panic!("unknown color"), - } - } -} - -#[cfg(test)] -mod tests { - use super::model::{Model, SelectedTab}; - use super::update::{ColorCodesMsg, Msg, update}; - - #[test] - fn test_exit_msg() { - let mut model = Model::default(); - update(&mut model, Msg::Exit); - assert!(!model.running) - } - - #[test] - fn test_toggletab_msg() { - let mut model = Model::default(); - update(&mut model, Msg::ToggleTab); - assert_eq!(model.selected_tab, SelectedTab::SpecsToColorCodes); - update(&mut model, Msg::ToggleTab); - assert_eq!(model.selected_tab, SelectedTab::ColorCodesToSpecs) - } - - #[test] - fn test_nbands_msg() { - let mut model = Model::default(); - assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 6); - - update( - &mut model, - Msg::ColorCodesMsg { - msg: ColorCodesMsg::ThreeBands, - }, - ); - assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 3); - - update( - &mut model, - Msg::ColorCodesMsg { - msg: ColorCodesMsg::FourBands, - }, - ); - assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 4); - - update( - &mut model, - Msg::ColorCodesMsg { - msg: ColorCodesMsg::FiveBands, - }, - ); - assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 5); - - update( - &mut model, - Msg::ColorCodesMsg { - msg: ColorCodesMsg::SixBands, - }, - ); - assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 6); - } -} diff --git a/tusistor-core/Cargo.toml b/tusistor-core/Cargo.toml new file mode 100644 index 0000000..3342c0f --- /dev/null +++ b/tusistor-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tusistor-core" +version = "0.1.0" +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +description = "This crate provides shared code for tusistor and tusistor-web." + +[dependencies] +rusistor = { path = "../rusistor", version = "0.3.0" } +engineering-repr = "1.1.0" diff --git a/tusistor-core/src/lib.rs b/tusistor-core/src/lib.rs new file mode 100644 index 0000000..01f4f3e --- /dev/null +++ b/tusistor-core/src/lib.rs @@ -0,0 +1,3 @@ +pub mod model; +pub mod update; +pub mod view; diff --git a/tusistor-core/src/model.rs b/tusistor-core/src/model.rs new file mode 100644 index 0000000..085a4b2 --- /dev/null +++ b/tusistor-core/src/model.rs @@ -0,0 +1,23 @@ +use rusistor::Resistor; + +#[derive(Debug)] +pub struct ColorCodesToSpecsModel { + pub selected_band: usize, + pub resistor: Resistor, +} + +impl Default for ColorCodesToSpecsModel { + fn default() -> ColorCodesToSpecsModel { + ColorCodesToSpecsModel { + selected_band: 0, + resistor: Resistor::SixBand { + band1: rusistor::Color::Brown, + band2: rusistor::Color::Black, + band3: rusistor::Color::Black, + band4: rusistor::Color::Black, + band5: rusistor::Color::Brown, + band6: rusistor::Color::Black, + }, + } + } +} diff --git a/tusistor-core/src/update.rs b/tusistor-core/src/update.rs new file mode 100644 index 0000000..7d75710 --- /dev/null +++ b/tusistor-core/src/update.rs @@ -0,0 +1,10 @@ +pub enum ColorCodesMsg { + ThreeBands, + FourBands, + FiveBands, + SixBands, + NextBand, + PrevBand, + NextColor, + PrevColor, +} diff --git a/tusistor-core/src/view.rs b/tusistor-core/src/view.rs new file mode 100644 index 0000000..f61baee --- /dev/null +++ b/tusistor-core/src/view.rs @@ -0,0 +1,39 @@ +pub fn band_numeric_info(bands: usize, band_idx: usize, color: &rusistor::Color) -> String { + match (bands, band_idx) { + (3, i) | (4, i) if i <= 1 => { + if i == 0 && *color == rusistor::Color::Black { + " ".to_string() + } else { + color.as_digit().map_or(" ".to_string(), |s| s.to_string()) + } + } + (5, i) | (6, i) if i <= 2 => { + if i == 0 && *color == rusistor::Color::Black { + " ".to_string() + } else { + color.as_digit().map_or(" ".to_string(), |s| s.to_string()) + } + } + (3, 2) | (4, 2) | (5, 3) | (6, 3) => { + format!("10^{}", color.as_digit_or_exponent()) + } + (4, 3) | (5, 4) | (6, 4) => color + .as_tolerance() + .map_or(" ".to_string(), |s| format!("{:>4}", (s * 100.0))), + (6, 5) => color + .as_tcr() + .map_or(" ".to_string(), |s| format!("{:>3}", s.to_string())), + _ => "".to_string(), + } +} + +pub fn band_semantic_info(bands: usize, band_idx: usize) -> String { + match (bands, band_idx) { + (3, i) | (4, i) if i <= 1 => format!("Digit {}", band_idx + 1), + (5, i) | (6, i) if i <= 2 => format!("Digit {}", band_idx + 1), + (3, 2) | (4, 2) | (5, 3) | (6, 3) => "Multiplier".to_string(), + (4, 3) | (5, 4) | (6, 4) => "Tolerance".to_string(), + (6, 5) => "TCR".to_string(), + _ => "".to_string(), + } +} diff --git a/tusistor-web/Cargo.toml b/tusistor-web/Cargo.toml new file mode 100644 index 0000000..baacc86 --- /dev/null +++ b/tusistor-web/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tusistor-web" +version = "0.1.0" +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +description = "This is a Ratzilla app to calculate the color code of electrical resistors." + +[dependencies] +rusistor = { path = "../rusistor", version = "0.3.1" } +tusistor-core = { path = "../tusistor-core", version = "0.1.0" } +engineering-repr = "1.1.0" +ratzilla = "0.0.2" diff --git a/tusistor-web/index.html b/tusistor-web/index.html new file mode 100644 index 0000000..6599ce5 --- /dev/null +++ b/tusistor-web/index.html @@ -0,0 +1,39 @@ + + + + + + + tusistor + + + + + + diff --git a/tusistor-web/src/main.rs b/tusistor-web/src/main.rs new file mode 100644 index 0000000..d39ba68 --- /dev/null +++ b/tusistor-web/src/main.rs @@ -0,0 +1,25 @@ +pub mod update; +pub mod view; + +use ratzilla::{DomBackend, WebRenderer}; +use std::{cell::RefCell, io, rc::Rc}; +use tusistor_core::model::ColorCodesToSpecsModel; +use update::handle_event; +use view::view; + +fn main() -> io::Result<()> { + let backend = DomBackend::new()?; + let terminal = ratzilla::ratatui::Terminal::new(backend)?; + let model = Rc::new(RefCell::new(ColorCodesToSpecsModel::default())); + + terminal.on_key_event({ + let model = model.clone(); + move |key_event| handle_event(&mut model.borrow_mut(), key_event) + }); + + terminal.draw_web(move |frame| { + view(&model.borrow(), frame); + }); + + Ok(()) +} diff --git a/tusistor-web/src/update.rs b/tusistor-web/src/update.rs new file mode 100644 index 0000000..c2f3de0 --- /dev/null +++ b/tusistor-web/src/update.rs @@ -0,0 +1,114 @@ +use ratzilla::event; +use rusistor::{self, Color, Resistor}; +use tusistor_core::model::ColorCodesToSpecsModel; +use tusistor_core::update::ColorCodesMsg; + +pub fn handle_event(model: &mut ColorCodesToSpecsModel, event: ratzilla::event::KeyEvent) { + match event.code { + event::KeyCode::Char('3') => update(model, ColorCodesMsg::ThreeBands), + event::KeyCode::Char('4') => update(model, ColorCodesMsg::FourBands), + event::KeyCode::Char('5') => update(model, ColorCodesMsg::FiveBands), + event::KeyCode::Char('6') => update(model, ColorCodesMsg::SixBands), + event::KeyCode::Up => update(model, ColorCodesMsg::PrevColor), + event::KeyCode::Down => update(model, ColorCodesMsg::NextColor), + event::KeyCode::Left => update(model, ColorCodesMsg::PrevBand), + event::KeyCode::Right => update(model, ColorCodesMsg::NextBand), + _ => (), + } +} + +pub fn update(model: &mut ColorCodesToSpecsModel, msg: ColorCodesMsg) { + match msg { + ColorCodesMsg::ThreeBands => { + model.resistor = Resistor::ThreeBand { + band1: rusistor::Color::Brown, + band2: rusistor::Color::Black, + band3: rusistor::Color::Black, + }; + model.selected_band = model.selected_band.min(2) + } + ColorCodesMsg::FourBands => { + model.resistor = Resistor::FourBand { + band1: rusistor::Color::Brown, + band2: rusistor::Color::Black, + band3: rusistor::Color::Black, + band4: rusistor::Color::Brown, + }; + model.selected_band = model.selected_band.min(3) + } + ColorCodesMsg::FiveBands => { + model.resistor = Resistor::FiveBand { + band1: rusistor::Color::Brown, + band2: rusistor::Color::Black, + band3: rusistor::Color::Black, + band4: rusistor::Color::Black, + band5: rusistor::Color::Brown, + }; + model.selected_band = model.selected_band.min(4) + } + ColorCodesMsg::SixBands => { + model.resistor = Resistor::SixBand { + band1: rusistor::Color::Brown, + band2: rusistor::Color::Black, + band3: rusistor::Color::Black, + band4: rusistor::Color::Black, + band5: rusistor::Color::Brown, + band6: rusistor::Color::Black, + }; + model.selected_band = model.selected_band.min(5) + } + ColorCodesMsg::NextBand => { + model.selected_band = (model.selected_band + 1) % model.resistor.bands().len() + } + ColorCodesMsg::PrevBand => { + let bands_count = model.resistor.bands().len(); + model.selected_band = (model.selected_band + (bands_count - 1)) % bands_count + } + ColorCodesMsg::NextColor => { + let current_idx: usize = *model.resistor.bands()[model.selected_band] as usize; + let mut i: usize = 0; + let mut resistor = Err("".to_string()); + while resistor.is_err() { + i += 1; + let next_color = Color::from((current_idx + i) % 13); + resistor = model.resistor.with_color(next_color, model.selected_band); + } + model.resistor = resistor.unwrap(); + } + ColorCodesMsg::PrevColor => { + let current_idx = *model.resistor.bands()[model.selected_band] as usize; + let mut i: usize = 13; + let mut resistor = Err("".to_string()); + while resistor.is_err() { + i -= 1; + let next_color = Color::from((current_idx + i) % 13); + resistor = model.resistor.with_color(next_color, model.selected_band); + } + model.resistor = resistor.unwrap(); + } + } +} + +#[cfg(test)] +mod tests { + use super::{ColorCodesMsg, update}; + use tusistor_core::model::ColorCodesToSpecsModel; + + #[test] + fn test_nbands_msg() { + let mut model = ColorCodesToSpecsModel::default(); + assert_eq!(model.resistor.bands().len(), 6); + + update(&mut model, ColorCodesMsg::ThreeBands); + assert_eq!(model.resistor.bands().len(), 3); + + update(&mut model, ColorCodesMsg::FourBands); + assert_eq!(model.resistor.bands().len(), 4); + + update(&mut model, ColorCodesMsg::FiveBands); + assert_eq!(model.resistor.bands().len(), 5); + + update(&mut model, ColorCodesMsg::SixBands); + assert_eq!(model.resistor.bands().len(), 6); + } +} diff --git a/tusistor-web/src/view.rs b/tusistor-web/src/view.rs new file mode 100644 index 0000000..e040a60 --- /dev/null +++ b/tusistor-web/src/view.rs @@ -0,0 +1,202 @@ +use ratzilla::ratatui::{ + Frame, + layout::{Constraint, Direction, Flex, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, List, ListDirection, ListItem, ListState, Paragraph}, +}; +use tusistor_core::{ + model::ColorCodesToSpecsModel, + view::{band_numeric_info, band_semantic_info}, +}; + +fn band_list<'a>(band_idx: usize, bands: usize, is_focused: bool) -> List<'a> { + let items = [ + rusistor::Color::Black, + rusistor::Color::Brown, + rusistor::Color::Red, + rusistor::Color::Orange, + rusistor::Color::Yellow, + rusistor::Color::Green, + rusistor::Color::Blue, + rusistor::Color::Violet, + rusistor::Color::Grey, + rusistor::Color::White, + rusistor::Color::Gold, + rusistor::Color::Silver, + rusistor::Color::Pink, + ] + .iter() + .map(|color| { + let numeric_info = band_numeric_info(bands, band_idx, color); + let (color, name) = rusistor_color_to_ratatui_color(color); + let s = format!(" {numeric_info} {name}"); + let style = if color == Color::Black { + Style::default().bg(color) + } else { + Style::default().bg(color).fg(Color::Black) + }; + ListItem::new(s).style(style) + }); + + let style = if is_focused { + Style::default().add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + + let semantic_info = band_semantic_info(bands, band_idx); + + List::new(items) + .block( + Block::bordered() + .title(format!( + " Band {}: {}{}", + band_idx + 1, + semantic_info, + if is_focused { "* " } else { " " } + )) + .style(style), + ) + .highlight_symbol(">> ") + .repeat_highlight_symbol(true) + .direction(ListDirection::TopToBottom) +} + +pub fn view(model: &ColorCodesToSpecsModel, frame: &mut Frame) { + fn center_horizontal(area: Rect, width: u16) -> Rect { + let [area] = Layout::horizontal([Constraint::Length(width)]) + .flex(Flex::Center) + .areas(area); + area + } + + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(3), + Constraint::Length(15), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(frame.area()); + let help_msg_rect = center_horizontal(chunks[2], 95); + + let spec_chuncks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + ]) + .split(chunks[0]); + + let bands_rect = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + ]) + .split(chunks[1]); + + let specs = model.resistor.specs(); + + let resistance_paragraph = Paragraph::new(specs.ohm.to_string()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Resistance (Ω) "), + ); + frame.render_widget(resistance_paragraph, spec_chuncks[0]); + + let tolerance_paragraph = Paragraph::new(format!("±{}", (specs.tolerance * 100.0))) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Tolerance (%) "), + ); + frame.render_widget(tolerance_paragraph, spec_chuncks[1]); + + let min_paragraph = Paragraph::new(specs.min_ohm.to_string()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Minimum (Ω) "), + ); + frame.render_widget(min_paragraph, spec_chuncks[2]); + + let max_paragraph = Paragraph::new(specs.max_ohm.to_string()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Maximum (Ω) "), + ); + frame.render_widget(max_paragraph, spec_chuncks[3]); + + let tcr_paragraph = Paragraph::new(specs.tcr.map(|f| f.to_string()).unwrap_or_default()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" TCR (ppm/K) "), + ); + frame.render_widget(tcr_paragraph, spec_chuncks[4]); + + let (msg, style) = ( + vec![ + Span::styled("←/→", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": prev/next band, "), + Span::styled("↑/↓", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": prev/next color, "), + Span::styled("3|4|5|6", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": bands count, "), + ], + Style::default(), + ); + let text = Text::from(Line::from(msg)).style(style); + let help_message = Paragraph::new(text); + frame.render_widget(help_message, help_msg_rect); + + let bands = model.resistor.bands(); + for i in 0..bands.len() { + let mut state = ListState::default().with_selected(Some(*bands[i] as usize)); + let is_focused = model.selected_band == i; + let list = band_list(i, bands.len(), is_focused); + frame.render_stateful_widget(list, bands_rect[i], &mut state); + } +} + +fn rusistor_color_to_ratatui_color(color: &rusistor::Color) -> (Color, String) { + match color { + rusistor::Color::Black => (Color::Black, rusistor::Color::Black.to_string()), + rusistor::Color::Brown => (Color::Rgb(165, 42, 42), rusistor::Color::Brown.to_string()), + rusistor::Color::Red => (Color::Red, rusistor::Color::Red.to_string()), + rusistor::Color::Orange => (Color::Rgb(255, 165, 0), rusistor::Color::Orange.to_string()), + rusistor::Color::Yellow => (Color::Yellow, rusistor::Color::Yellow.to_string()), + rusistor::Color::Green => (Color::Green, rusistor::Color::Green.to_string()), + rusistor::Color::Blue => (Color::Blue, rusistor::Color::Blue.to_string()), + rusistor::Color::Violet => (Color::Rgb(148, 0, 211), rusistor::Color::Violet.to_string()), + rusistor::Color::Grey => (Color::Gray, rusistor::Color::Grey.to_string()), + rusistor::Color::White => (Color::White, rusistor::Color::White.to_string()), + rusistor::Color::Gold => (Color::Rgb(255, 215, 0), rusistor::Color::Gold.to_string()), + rusistor::Color::Silver => ( + Color::Rgb(192, 192, 192), + rusistor::Color::Silver.to_string(), + ), + rusistor::Color::Pink => (Color::Rgb(255, 105, 180), rusistor::Color::Pink.to_string()), + } +} diff --git a/tusistor/Cargo.toml b/tusistor/Cargo.toml new file mode 100644 index 0000000..db1d024 --- /dev/null +++ b/tusistor/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tusistor" +version = "0.6.0" +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true +description = "This is a Ratatui app to calculate the color code and specs of electrical resistors." + +[dependencies] +crossterm = "0.28.1" +ratatui = "0.29.0" +color-eyre = "0.6.3" +tui-input = "0.11.1" +rusistor = { path = "../rusistor", version = "0.3.1" } +tusistor-core = { path = "../tusistor-core", version = "0.1.0" } +engineering-repr = "1.1.0" diff --git a/src/main.rs b/tusistor/src/main.rs similarity index 77% rename from src/main.rs rename to tusistor/src/main.rs index 23d8965..5e461d7 100644 --- a/src/main.rs +++ b/tusistor/src/main.rs @@ -1,9 +1,11 @@ -pub mod app; +pub mod model; +pub mod update; +pub mod view; -use app::model::Model; -use app::update::{handle_event, update}; -use app::view::view; use color_eyre::eyre::Ok; +use model::Model; +use update::{handle_event, update}; +use view::view; fn main() -> color_eyre::Result<()> { color_eyre::install()?; diff --git a/tusistor/src/model.rs b/tusistor/src/model.rs new file mode 100644 index 0000000..aaf3f80 --- /dev/null +++ b/tusistor/src/model.rs @@ -0,0 +1,83 @@ +use rusistor::Resistor; +use tui_input::Input; +use tusistor_core::model::ColorCodesToSpecsModel; + +#[derive(Debug, Default)] +pub enum InputFocus { + #[default] + Resistance, + Tolerance, + Tcr, +} + +impl InputFocus { + pub fn next(&self) -> InputFocus { + match self { + InputFocus::Resistance => InputFocus::Tolerance, + InputFocus::Tolerance => InputFocus::Tcr, + InputFocus::Tcr => InputFocus::Resistance, + } + } + + pub fn prev(&self) -> InputFocus { + match self { + InputFocus::Resistance => InputFocus::Tcr, + InputFocus::Tolerance => InputFocus::Resistance, + InputFocus::Tcr => InputFocus::Tolerance, + } + } +} + +#[derive(Debug, Default, PartialEq)] +pub enum SelectedTab { + #[default] + ColorCodesToSpecs, + SpecsToColorCodes, +} + +impl SelectedTab { + pub fn toggle(&self) -> SelectedTab { + match self { + SelectedTab::ColorCodesToSpecs => SelectedTab::SpecsToColorCodes, + SelectedTab::SpecsToColorCodes => SelectedTab::ColorCodesToSpecs, + } + } +} + +impl From<&SelectedTab> for Option { + fn from(selected_tab: &SelectedTab) -> Self { + match selected_tab { + SelectedTab::ColorCodesToSpecs => Some(0), + SelectedTab::SpecsToColorCodes => Some(1), + } + } +} + +#[derive(Debug, Default)] +pub struct SpecsToColorModel { + pub resistance_input: Input, + pub tolerance_input: Input, + pub tcr_input: Input, + pub focus: InputFocus, + pub resistor: Option, + pub error: Option, +} + +#[derive(Debug)] +pub struct Model { + pub running: bool, + pub selected_tab: SelectedTab, + pub specs_to_color: SpecsToColorModel, + pub color_codes_to_specs: ColorCodesToSpecsModel, +} + +impl Default for Model { + fn default() -> Model { + Model { + running: true, + selected_tab: SelectedTab::default(), + specs_to_color: SpecsToColorModel::default(), + color_codes_to_specs: ColorCodesToSpecsModel::default(), + } + } +} diff --git a/tusistor/src/update.rs b/tusistor/src/update.rs new file mode 100644 index 0000000..f1a5df1 --- /dev/null +++ b/tusistor/src/update.rs @@ -0,0 +1,382 @@ +use crate::model::{InputFocus, Model, SelectedTab}; +use color_eyre::Result; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use rusistor::{Color, Resistor}; +use std::str::FromStr; +use tui_input::backend::crossterm::EventHandler; +use tusistor_core::update::ColorCodesMsg; + +pub enum SpecsMsg { + Determine, + NextSpecInput, + PrevSpecInput, +} + +pub enum Msg { + ToggleTab, + Exit, + SpecsMsg { msg: SpecsMsg }, + ColorCodesMsg { msg: ColorCodesMsg }, +} + +pub fn handle_event(model: &mut Model) -> Result> { + match event::read()? { + // it's important to check KeyEventKind::Press to avoid handling key release events + Event::Key(key) if key.kind == KeyEventKind::Press => Result::Ok(on_key_event(model, key)), + _ => Result::Ok(None), + } +} + +fn on_key_event(model: &mut Model, key: KeyEvent) -> Option { + match (key.code, &model.selected_tab) { + (KeyCode::Left, _) | (KeyCode::Right, _) if key.modifiers == KeyModifiers::SHIFT => { + Some(Msg::ToggleTab) + } + (KeyCode::Enter, SelectedTab::SpecsToColorCodes) => Some(Msg::SpecsMsg { + msg: SpecsMsg::Determine, + }), + (KeyCode::Tab, SelectedTab::SpecsToColorCodes) => Some(Msg::SpecsMsg { + msg: SpecsMsg::NextSpecInput, + }), + (KeyCode::Tab, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::NextBand, + }), + (KeyCode::BackTab, SelectedTab::SpecsToColorCodes) => Some(Msg::SpecsMsg { + msg: SpecsMsg::PrevSpecInput, + }), + (KeyCode::BackTab, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::PrevBand, + }), + (KeyCode::Up, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::PrevColor, + }), + (KeyCode::Down, SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::NextColor, + }), + (KeyCode::Char('3'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::ThreeBands, + }), + (KeyCode::Char('4'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::FourBands, + }), + (KeyCode::Char('5'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::FiveBands, + }), + (KeyCode::Char('6'), SelectedTab::ColorCodesToSpecs) => Some(Msg::ColorCodesMsg { + msg: ColorCodesMsg::SixBands, + }), + (KeyCode::Esc, _) => Some(Msg::Exit), + _ => { + let target_input = match model.specs_to_color.focus { + InputFocus::Resistance => &mut model.specs_to_color.resistance_input, + InputFocus::Tolerance => &mut model.specs_to_color.tolerance_input, + InputFocus::Tcr => &mut model.specs_to_color.tcr_input, + }; + target_input.handle_event(&Event::Key(key)); + None + } + } +} + +pub fn update(model: &mut Model, msg: Msg) { + match msg { + Msg::ToggleTab => model.selected_tab = model.selected_tab.toggle(), + Msg::SpecsMsg { + msg: SpecsMsg::Determine, + } => { + match try_determine_resistor( + model.specs_to_color.resistance_input.value(), + model.specs_to_color.tolerance_input.value(), + model.specs_to_color.tcr_input.value(), + ) { + Ok(resistor) => { + model.specs_to_color.resistor = Some(resistor); + model.specs_to_color.error = None; + } + Err(e) => { + model.specs_to_color.resistor = None; + model.specs_to_color.error = Some(e); + } + } + model.specs_to_color.resistance_input.reset(); + model.specs_to_color.tolerance_input.reset(); + model.specs_to_color.tcr_input.reset(); + model.specs_to_color.focus = InputFocus::Resistance; + } + Msg::SpecsMsg { + msg: SpecsMsg::NextSpecInput, + } + | Msg::SpecsMsg { + msg: SpecsMsg::PrevSpecInput, + } => { + model.specs_to_color.error = match model.specs_to_color.focus { + InputFocus::Resistance => { + let value = model.specs_to_color.resistance_input.value(); + if value.trim().is_empty() { + None + } else { + try_parse_resistance(value).err().map(|err| err.to_string()) + } + } + InputFocus::Tolerance => { + let value = model.specs_to_color.tolerance_input.value(); + if value.trim().is_empty() { + None + } else { + value.parse::().err().map(|err| err.to_string()) + } + } + InputFocus::Tcr => { + let value = model.specs_to_color.tcr_input.value(); + if value.trim().is_empty() { + None + } else { + value.parse::().err().map(|err| err.to_string()) + } + } + }; + if model.specs_to_color.error.is_none() { + model.specs_to_color.focus = match msg { + Msg::SpecsMsg { + msg: SpecsMsg::NextSpecInput, + } => model.specs_to_color.focus.next(), + _ => model.specs_to_color.focus.prev(), + }; + } else { + model.specs_to_color.resistor = None; + } + } + Msg::Exit => { + model.running = false; + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::ThreeBands, + } => { + model.color_codes_to_specs.resistor = Resistor::ThreeBand { + band1: Color::Brown, + band2: Color::Black, + band3: Color::Black, + }; + model.color_codes_to_specs.selected_band = + model.color_codes_to_specs.selected_band.min(2) + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::FourBands, + } => { + model.color_codes_to_specs.resistor = Resistor::FourBand { + band1: Color::Brown, + band2: Color::Black, + band3: Color::Black, + band4: Color::Brown, + }; + model.color_codes_to_specs.selected_band = + model.color_codes_to_specs.selected_band.min(3) + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::FiveBands, + } => { + model.color_codes_to_specs.resistor = Resistor::FiveBand { + band1: Color::Brown, + band2: Color::Black, + band3: Color::Black, + band4: Color::Black, + band5: Color::Brown, + }; + model.color_codes_to_specs.selected_band = + model.color_codes_to_specs.selected_band.min(4) + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::SixBands, + } => { + model.color_codes_to_specs.resistor = Resistor::SixBand { + band1: Color::Brown, + band2: Color::Black, + band3: Color::Black, + band4: Color::Black, + band5: Color::Brown, + band6: Color::Black, + }; + model.color_codes_to_specs.selected_band = + model.color_codes_to_specs.selected_band.min(5) + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::NextBand, + } => { + model.color_codes_to_specs.selected_band = (model.color_codes_to_specs.selected_band + + 1) + % model.color_codes_to_specs.resistor.bands().len() + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::PrevBand, + } => { + let bands_count = model.color_codes_to_specs.resistor.bands().len(); + model.color_codes_to_specs.selected_band = + (model.color_codes_to_specs.selected_band + (bands_count - 1)) % bands_count + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::NextColor, + } => { + let current_idx: usize = *model.color_codes_to_specs.resistor.bands() + [model.color_codes_to_specs.selected_band] + as usize; + let mut i: usize = 0; + let mut resistor = Err("".to_string()); + while resistor.is_err() { + i += 1; + let next_color = Color::from((current_idx + i) % 13); + resistor = model + .color_codes_to_specs + .resistor + .with_color(next_color, model.color_codes_to_specs.selected_band); + } + model.color_codes_to_specs.resistor = resistor.unwrap(); + } + Msg::ColorCodesMsg { + msg: ColorCodesMsg::PrevColor, + } => { + let current_idx = *model.color_codes_to_specs.resistor.bands() + [model.color_codes_to_specs.selected_band] as usize; + let mut i: usize = 13; + let mut resistor = Err("".to_string()); + while resistor.is_err() { + i -= 1; + let next_color = Color::from((current_idx + i) % 13); + resistor = model + .color_codes_to_specs + .resistor + .with_color(next_color, model.color_codes_to_specs.selected_band); + } + model.color_codes_to_specs.resistor = resistor.unwrap(); + } + } +} + +fn try_parse_resistance(input: &str) -> Result { + match input.parse::() { + Ok(t) => Ok(t), + Err(e) => match engineering_repr::EngineeringQuantity::::from_str(input) { + Ok(t) => { + let r: i64 = t.into(); + let r = r as f64; + Ok(r) + } + Err(_) => Err(format!("invalid input for resistance: {}", e)), + }, + } +} + +fn try_determine_resistor( + resistance_input: &str, + tolerance_input: &str, + tcr_input: &str, +) -> Result { + let resistance = try_parse_resistance(resistance_input); + let tolerance = if tolerance_input.is_empty() { + Ok(None) + } else { + match tolerance_input.parse::() { + Ok(t) => Ok(Some(t)), + Err(e) => Err(format!("invalid input for tolerance: {}", e)), + } + }; + + let tcr = if tcr_input.is_empty() { + Ok(None) + } else { + match tcr_input.parse::() { + Ok(t) => Ok(Some(t)), + Err(e) => Err(format!("invalid input for tcr: {}", e)), + } + }; + + match (resistance, tolerance, tcr) { + (Ok(resistance), Ok(tolerance), Ok(tcr)) => { + match Resistor::determine(resistance, tolerance, tcr) { + Ok(resistor) => Ok(resistor), + Err(e) => Err(format!( + "could not determine a resistor for these inputs: {}", + e + )), + } + } + (res, tol, tcr) => { + let mut error_msg: String = String::from(""); + if let Err(res_error) = res { + error_msg.push_str(res_error.to_string().as_str()); + } + if let Err(tol_error) = tol { + error_msg.push('\n'); + error_msg.push_str(tol_error.to_string().as_str()); + } + if let Err(tcr_error) = tcr { + error_msg.push('\n'); + error_msg.push_str(tcr_error.to_string().as_str()); + } + if error_msg.is_empty() { + panic!("unknown error"); + } else { + Err(error_msg) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{ColorCodesMsg, Msg, update}; + use crate::model::{Model, SelectedTab}; + + #[test] + fn test_exit_msg() { + let mut model = Model::default(); + update(&mut model, Msg::Exit); + assert!(!model.running) + } + + #[test] + fn test_toggletab_msg() { + let mut model = Model::default(); + update(&mut model, Msg::ToggleTab); + assert_eq!(model.selected_tab, SelectedTab::SpecsToColorCodes); + update(&mut model, Msg::ToggleTab); + assert_eq!(model.selected_tab, SelectedTab::ColorCodesToSpecs) + } + + #[test] + fn test_nbands_msg() { + let mut model = Model::default(); + assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 6); + + update( + &mut model, + Msg::ColorCodesMsg { + msg: ColorCodesMsg::ThreeBands, + }, + ); + assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 3); + + update( + &mut model, + Msg::ColorCodesMsg { + msg: ColorCodesMsg::FourBands, + }, + ); + assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 4); + + update( + &mut model, + Msg::ColorCodesMsg { + msg: ColorCodesMsg::FiveBands, + }, + ); + assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 5); + + update( + &mut model, + Msg::ColorCodesMsg { + msg: ColorCodesMsg::SixBands, + }, + ); + assert_eq!(model.color_codes_to_specs.resistor.bands().len(), 6); + } +} diff --git a/tusistor/src/view.rs b/tusistor/src/view.rs new file mode 100644 index 0000000..d1e49af --- /dev/null +++ b/tusistor/src/view.rs @@ -0,0 +1,420 @@ +use crate::model::{InputFocus, Model, SelectedTab}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Flex, Layout, Rect}, + style::{Color, Modifier, Style}, + symbols, + text::{Line, Span, Text}, + widgets::{ + Bar, BarChart, BarGroup, Block, Borders, List, ListDirection, ListItem, ListState, Padding, + Paragraph, Tabs, + }, +}; +use tusistor_core::view::{band_numeric_info, band_semantic_info}; + +const BAR_WIDTH: u16 = 19; + +fn tabs<'a>(selected: &SelectedTab) -> Tabs<'a> { + Tabs::new(vec![" color codes to specs ", " specs to color codes "]) + .padding(" ", " ") + .divider(symbols::DOT) + .select(selected) +} + +fn band_list<'a>(band_idx: usize, bands: usize, is_focused: bool) -> List<'a> { + let items = [ + rusistor::Color::Black, + rusistor::Color::Brown, + rusistor::Color::Red, + rusistor::Color::Orange, + rusistor::Color::Yellow, + rusistor::Color::Green, + rusistor::Color::Blue, + rusistor::Color::Violet, + rusistor::Color::Grey, + rusistor::Color::White, + rusistor::Color::Gold, + rusistor::Color::Silver, + rusistor::Color::Pink, + ] + .iter() + .map(|color| { + let numeric_info = band_numeric_info(bands, band_idx, color); + let (color, name) = rusistor_color_to_ratatui_color(color); + let s = format!(" {numeric_info} {name}"); + let style = if color == Color::Black { + Style::default().bg(color) + } else { + Style::default().bg(color).fg(Color::Black) + }; + ListItem::new(s).style(style) + }); + + let style = if is_focused { + Style::default().add_modifier(Modifier::BOLD) + } else { + Style::default() + }; + + let semantic_info = band_semantic_info(bands, band_idx); + + List::new(items) + .block( + Block::bordered() + .title(format!( + " Band {}: {}{}", + band_idx + 1, + semantic_info, + if is_focused { "* " } else { " " } + )) + .style(style), + ) + .highlight_symbol(">> ") + .repeat_highlight_symbol(true) + .direction(ListDirection::TopToBottom) +} + +pub fn view(model: &Model, frame: &mut Frame) { + fn center_horizontal(area: Rect, width: u16) -> Rect { + let [area] = Layout::horizontal([Constraint::Length(width)]) + .flex(Flex::Center) + .areas(area); + area + } + + let tabs_width = 49; + + match model.selected_tab { + SelectedTab::ColorCodesToSpecs => { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(2), + Constraint::Length(3), + Constraint::Length(15), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(frame.area()); + let tabs_rect = center_horizontal(chunks[0], tabs_width); + let help_msg_rect = center_horizontal(chunks[3], 95); + + let spec_chuncks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + ]) + .split(chunks[1]); + + let bands_rect = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + Constraint::Ratio(1, 6), + ]) + .split(chunks[2]); + + let tabs = tabs(&model.selected_tab); + frame.render_widget(tabs, tabs_rect); + + let specs = model.color_codes_to_specs.resistor.specs(); + + let resistance_paragraph = Paragraph::new(specs.ohm.to_string()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Resistance (Ω) "), + ); + frame.render_widget(resistance_paragraph, spec_chuncks[0]); + + let tolerance_paragraph = Paragraph::new(format!("±{}", (specs.tolerance * 100.0))) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Tolerance (%) "), + ); + frame.render_widget(tolerance_paragraph, spec_chuncks[1]); + + let min_paragraph = Paragraph::new(specs.min_ohm.to_string()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Minimum (Ω) "), + ); + frame.render_widget(min_paragraph, spec_chuncks[2]); + + let max_paragraph = Paragraph::new(specs.max_ohm.to_string()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Maximum (Ω) "), + ); + frame.render_widget(max_paragraph, spec_chuncks[3]); + + let tcr_paragraph = + Paragraph::new(specs.tcr.map(|f| f.to_string()).unwrap_or_default()) + .style(Style::default().fg(Color::Yellow)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" TCR (ppm/K) "), + ); + frame.render_widget(tcr_paragraph, spec_chuncks[4]); + + let (msg, style) = ( + vec![ + Span::styled("Tab", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": next band, "), + Span::styled("↑/↓", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": prev/next color, "), + Span::styled("3|4|5|6", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": bands count, "), + Span::styled("Shift ←/→", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": prev/next tab, "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": exit"), + ], + Style::default(), + ); + let text = Text::from(Line::from(msg)).style(style); + let help_message = Paragraph::new(text); + frame.render_widget(help_message, help_msg_rect); + + let bands = model.color_codes_to_specs.resistor.bands(); + for i in 0..bands.len() { + let mut state = ListState::default().with_selected(Some(*bands[i] as usize)); + let is_focused = model.color_codes_to_specs.selected_band == i; + let list = band_list(i, bands.len(), is_focused); + frame.render_stateful_widget(list, bands_rect[i], &mut state); + } + } + SelectedTab::SpecsToColorCodes => { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(2) + .constraints( + [ + Constraint::Length(2), + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(1), + ] + .as_ref(), + ) + .split(frame.area()); + let input_rects = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ]) + .split(chunks[1]); + + let tabs_rect = center_horizontal(chunks[0], tabs_width); + let help_msg_rect = center_horizontal(chunks[3], 82); + let resistance_rect = input_rects[0]; + let tolerance_rect = input_rects[1]; + let tcr_rect = input_rects[2]; + let main_rect = chunks[2]; + + let tabs = tabs(&model.selected_tab); + frame.render_widget(tabs, tabs_rect); + + let (msg, style) = ( + vec![ + Span::styled("Tab", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": next input, "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": calculate color codes, "), + Span::styled("Shift ←/→", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": prev/next tab, "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(": exit"), + ], + Style::default(), + ); + let text = Text::from(Line::from(msg)).style(style); + let help_message = Paragraph::new(text); + frame.render_widget(help_message, help_msg_rect); + + let resistance_width = resistance_rect.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let resistance_scroll = model + .specs_to_color + .resistance_input + .visual_scroll(resistance_width as usize); + let resistance_paragraph = + Paragraph::new(model.specs_to_color.resistance_input.value()) + .style(Style::default().fg(Color::Yellow)) + .scroll((0, resistance_scroll as u16)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Resistance (Ω) "), + ); + frame.render_widget(resistance_paragraph, resistance_rect); + + let tolerance_width = tolerance_rect.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let tolerance_scroll = model + .specs_to_color + .tolerance_input + .visual_scroll(tolerance_width as usize); + let tolerance_paragraph = Paragraph::new(model.specs_to_color.tolerance_input.value()) + .style(Style::default().fg(Color::Yellow)) + .scroll((0, tolerance_scroll as u16)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Tolerance (%) "), + ); + frame.render_widget(tolerance_paragraph, tolerance_rect); + + let tcr_width = tcr_rect.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let tcr_scroll = model + .specs_to_color + .tcr_input + .visual_scroll(tcr_width as usize); + let tcr_paragraph = Paragraph::new(model.specs_to_color.tcr_input.value()) + .style(Style::default().fg(Color::Yellow)) + .scroll((0, tcr_scroll as u16)) + .block( + Block::default() + .borders(Borders::ALL) + .title(" TCR (ppm/K) "), + ); + frame.render_widget(tcr_paragraph, tcr_rect); + + // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering + let (rect, input, scroll) = match model.specs_to_color.focus { + InputFocus::Resistance => ( + resistance_rect, + &model.specs_to_color.resistance_input, + resistance_scroll, + ), + InputFocus::Tolerance => ( + tolerance_rect, + &model.specs_to_color.tolerance_input, + tolerance_scroll, + ), + InputFocus::Tcr => (tcr_rect, &model.specs_to_color.tcr_input, tcr_scroll), + }; + frame.set_cursor_position(( + // Put cursor past the end of the input text + rect.x + ((input.visual_cursor()).max(scroll) - scroll) as u16 + 1, + // Move one line down, from the border to the input line + rect.y + 1, + )); + + if let Some(resistor) = &model.specs_to_color.resistor { + let bands = resistor.bands(); + let band_infos = bands + .iter() + .enumerate() + .map(|(idx, c)| { + let sem_info = band_semantic_info(bands.len(), idx); + let num_info = band_numeric_info(bands.len(), idx, c); + let (color, name) = rusistor_color_to_ratatui_color(c); + (sem_info, num_info, color, name) + }) + .collect::>(); + let specs = resistor.specs(); + let chart = barchart(&band_infos, specs.ohm, specs.tolerance, specs.tcr); + let chart_length: u16 = { + let bands_len: u16 = bands.len() as u16; + let bands_widths = bands_len * BAR_WIDTH; + let bands_gaps = bands_len - 1; + let border_plus_margin = 4; + bands_widths + bands_gaps + border_plus_margin + }; + let centered_main_rect = center_horizontal(main_rect, chart_length); + frame.render_widget(chart, centered_main_rect); + } + if let Some(e) = &model.specs_to_color.error { + let text = Text::from(e.to_string()); + let error_message = Paragraph::new(text).style(Style::default().fg(Color::Red)); + frame.render_widget(error_message, main_rect); + } + } + } +} + +fn barchart( + band_infos: &[(String, String, Color, String)], + ohm: f64, + tolerance: f64, + tcr: Option, +) -> BarChart { + let bars: Vec = band_infos.iter().map(|i| bar(i)).collect(); + let tcr = if let Some(tcr) = tcr { + format!(" - TCR: {}(ppm/K)", tcr) + } else { + String::from("") + }; + let title = format!( + "Resistance: {}Ω - Tolerance: ±{}%{}", + ohm, + tolerance * 100.0, + tcr + ); + let title = Line::from(title).centered(); + BarChart::default() + .data(BarGroup::default().bars(&bars)) + .block( + Block::new() + .padding(Padding::new(1, 1, 1, 1)) + .title(title) + .borders(Borders::all()), + ) + .bar_width(BAR_WIDTH) + .bar_gap(1) +} + +fn bar((sem_info, num_info, color, name): &(String, String, Color, String)) -> Bar { + Bar::default() + .value(100) + .text_value(format!(" {} ", name)) + .value_style(Style::default().fg(Color::White).bg(Color::Black)) + .label(Line::from(format!("{}: {}", sem_info, num_info.trim()))) + .style(bar_style(color)) +} + +fn bar_style(color: &Color) -> Style { + Style::new().fg(*color) +} + +fn rusistor_color_to_ratatui_color(color: &rusistor::Color) -> (Color, String) { + match color { + rusistor::Color::Black => (Color::Black, rusistor::Color::Black.to_string()), + rusistor::Color::Brown => (Color::Rgb(165, 42, 42), rusistor::Color::Brown.to_string()), + rusistor::Color::Red => (Color::Red, rusistor::Color::Red.to_string()), + rusistor::Color::Orange => (Color::Rgb(255, 165, 0), rusistor::Color::Orange.to_string()), + rusistor::Color::Yellow => (Color::Yellow, rusistor::Color::Yellow.to_string()), + rusistor::Color::Green => (Color::Green, rusistor::Color::Green.to_string()), + rusistor::Color::Blue => (Color::Blue, rusistor::Color::Blue.to_string()), + rusistor::Color::Violet => (Color::Rgb(148, 0, 211), rusistor::Color::Violet.to_string()), + rusistor::Color::Grey => (Color::Gray, rusistor::Color::Grey.to_string()), + rusistor::Color::White => (Color::White, rusistor::Color::White.to_string()), + rusistor::Color::Gold => (Color::Rgb(255, 215, 0), rusistor::Color::Gold.to_string()), + rusistor::Color::Silver => ( + Color::Rgb(192, 192, 192), + rusistor::Color::Silver.to_string(), + ), + rusistor::Color::Pink => (Color::Rgb(255, 105, 180), rusistor::Color::Pink.to_string()), + } +}