use std::sync::{Arc, Mutex};
use textual::prelude::*;
const CSS: &str = r#"
#dictionary-search {
dock: top;
margin: 1 0;
width: 100%;
}
#results-container {
width: 100%;
height: 1fr;
background: $surface;
}
#results {
width: 100%;
height: auto;
}
"#;
fn make_word_markdown(word: &str) -> String {
let entries: &[(&str, &str, &[&str])] = &[
(
"hello",
"exclamation",
&[
"Used as a greeting or to begin a telephone conversation.",
"An expression of surprise.",
],
),
(
"world",
"noun",
&[
"The earth, together with all of its countries and peoples.",
"A particular region or group of countries.",
],
),
(
"rust",
"noun",
&[
"A reddish-brown flaky coating of iron oxide formed on iron or steel by oxidation.",
"A programming language focused on safety, speed, and concurrency.",
],
),
(
"textual",
"adjective",
&["Of or relating to a text or texts."],
),
(
"python",
"noun",
&[
"A large heavy-bodied nonvenomous snake.",
"A high-level general-purpose programming language.",
],
),
];
let lower = word.to_lowercase();
let found = entries.iter().find(|(w, _, _)| *w == lower.as_str());
match found {
None => format!(
"# No results for \"{word}\"\n\nNo definition found in the built-in word list.\n\n\
*(Real port would query https://api.dictionaryapi.dev/api/v2/entries/en/{word})*"
),
Some((w, part_of_speech, definitions)) => {
let mut lines = vec![
format!("# {w}"),
String::new(),
format!("_{part_of_speech}_"),
String::new(),
];
for def in *definitions {
lines.push(format!(" - {def}"));
}
lines.push("---".to_string());
lines.join("\n")
}
}
}
struct DictionaryApp {
lookup_result: Arc<Mutex<Option<String>>>,
}
impl DictionaryApp {
fn new() -> Self {
Self {
lookup_result: Arc::new(Mutex::new(None)),
}
}
}
impl TextualApp for DictionaryApp {
fn configure(&mut self, app: &mut App) -> textual::Result<()> {
app.load_stylesheet(CSS);
Ok(())
}
fn compose(&mut self) -> AppRoot {
AppRoot::new()
.with_child(
Node::new(Input::new().with_placeholder("Search for a word"))
.id("dictionary-search"),
)
.with_child(
Node::new(ScrollView::new(Markdown::new("").with_id("results")))
.id("results-container"),
)
}
fn on_input_changed(
&mut self,
value: &str,
_validation: &ValidationResult,
ctx: &mut EventCtx,
) {
let word = value.trim().to_string();
let result_holder = Arc::clone(&self.lookup_result);
if word.is_empty() {
*result_holder.lock().unwrap_or_else(|e| e.into_inner()) = Some(String::new());
ctx.request_exclusive_worker_task("lookup_word", Some("clear"), move |_token| Ok(()));
} else {
ctx.request_exclusive_worker_task("lookup_word", Some("lookup"), move |token| {
std::thread::sleep(std::time::Duration::from_millis(80));
if token.is_cancelled() {
return Ok(());
}
let markdown = make_word_markdown(&word);
*result_holder.lock().unwrap_or_else(|e| e.into_inner()) = Some(markdown);
Ok(())
});
}
ctx.request_repaint();
}
fn on_message_with_app(&mut self, app: &mut App, message: &MessageEvent, ctx: &mut EventCtx) {
if let Message::WorkerStateChanged(w) = &message.message {
if matches!(w.state, WorkerState::Success) {
let markdown = {
let mut guard = self.lookup_result.lock().unwrap_or_else(|e| e.into_inner());
guard.take()
};
let _ = app.with_query_one_mut_as::<Markdown, _>("Markdown", |widget| {
widget.set_markup(markdown.unwrap_or_default());
});
ctx.request_repaint();
}
}
}
}
fn main() -> textual::Result<()> {
run_sync(DictionaryApp::new())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dictionary_app_composes_without_panic() {
let mut app = DictionaryApp::new();
let _root = app.compose();
}
#[test]
fn initial_lookup_result_is_none() {
let app = DictionaryApp::new();
let guard = app.lookup_result.lock().unwrap();
assert!(guard.is_none());
}
#[test]
fn known_word_produces_markdown_with_definition() {
let md = make_word_markdown("rust");
assert!(md.contains("# rust"), "expected heading");
assert!(md.contains("noun"), "expected part of speech");
assert!(
md.contains("oxidation") || md.contains("programming"),
"expected definition"
);
}
#[test]
fn unknown_word_produces_no_results_markdown() {
let md = make_word_markdown("xyzzy");
assert!(md.contains("No results"), "expected no-results message");
assert!(md.contains("xyzzy"), "expected word echoed in output");
}
}