use std::time::Duration;
use abstracttui::app::use_key_state;
use abstracttui::prelude::*;
use abstracttui::reactive::{bounded_source, interval, IntervalHandle, OverflowPolicy};
use abstracttui::widgets::{Feed, FeedItem, FeedState};
const SCOPE_WINDOW: usize = 128;
const CHUNK: Duration = Duration::from_millis(30);
const WORDS: &[&str] = &[
"the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", "while", "the", "meter",
"watches", "every", "word", "land",
];
fn main() -> abstracttui::base::Result<()> {
if !abstracttui::term::have_tty() {
println!("voice_mock: needs an interactive terminal — skipping cleanly");
return Ok(());
}
if let Ok(id) = std::env::var("ABSTRACTTUI_THEME") {
set_theme_by_id(&id);
}
let mut app = App::new(Size::new(90, 26));
let quitter = app.quitter();
app.mount(move |cx| {
let keys = use_key_state(cx);
let caps = use_caps(cx);
let level = cx.signal(0.0f32);
let bands = cx.signal(vec![0.0f32; 8]);
let (scope_tx, scope_window, _stats) =
bounded_source::<f32>(cx, SCOPE_WINDOW, OverflowPolicy::DropOldest);
let feed = FeedState::new(cx);
feed.push(
"intro",
FeedItem::markdown("**voice mock** — no audio, no network; everything below is synthesized from the frame clock."),
);
let utterance = cx.signal(0u32);
let synth: Signal<Option<IntervalHandle>> = cx.signal(None);
let ptt = abstracttui::app::PushToTalk::bind(cx, KeyChord::plain(Key::Char(' ')));
let start_synth = {
let scope_tx = scope_tx.clone();
let feed = feed.clone();
move || {
let n = {
utterance.update(|u| *u += 1);
utterance.get_untracked()
};
let item_key = format!("say-{n}");
feed.push_stream(&item_key);
feed.stream_append(&item_key, "🎙 ");
let scope_tx = scope_tx.clone();
let feed = feed.clone();
let mut tick = 0u64;
let handle = interval(cx, CHUNK, move || {
tick += 1;
let ts = tick as f32 * 0.03;
let envelope = 0.55 + 0.35 * (ts * 1.7).sin();
let noise = (hash01(tick) - 0.5) * 0.30;
let v = (envelope + noise).clamp(0.0, 1.0);
level.set(v);
scope_tx.send(v);
bands.set(
(0..8)
.map(|b| {
let phase = ts * (1.1 + b as f32 * 0.6) + b as f32;
(v * (0.45 + 0.55 * phase.sin().abs())).clamp(0.0, 1.0)
})
.collect(),
);
if tick.is_multiple_of(12) {
let word = WORDS[(tick as usize / 12) % WORDS.len()];
feed.stream_append(&item_key, &format!("{word} "));
}
});
synth.set(Some(handle));
}
};
let stop_synth = {
let feed = feed.clone();
move |reason: abstracttui::app::StopReason| {
if let Some(handle) = synth.get_untracked() {
handle.cancel();
}
synth.set(None);
level.set(0.0);
bands.set(vec![0.0; 8]);
let n = utterance.get_untracked();
let key = format!("say-{n}");
feed.stream_append(&key, &format!(" — ({reason:?})"));
feed.stream_finish(&key);
}
};
let ptt = ptt.on_start(start_synth).on_stop(stop_synth);
let mono = Meter::new(level)
.db_floor(-60.0)
.layout(LayoutStyle::line(1))
.view(cx);
let spectrum = Meter::bands(bands).bar(3, 1).view(cx);
let scope = AudioScope::new(scope_window).range(0.0, 1.0).view(cx);
let feed_clear = feed.clone();
let quit = quitter.clone();
Element::new()
.style(LayoutStyle::column().padding(Edges::all(1)).gap(1))
.shortcut(KeyChord::plain(Key::Char('q')), move |_| quit.quit())
.shortcut(KeyChord::plain(Key::Char('c')), move |_| feed_clear.clear())
.child({
let ptt = ptt.clone();
dyn_view(LayoutStyle::line(1), move || {
let talking = ptt.state().get().is_talking();
text(format!(
"VOICE MOCK — {}",
if talking { "● capturing" } else { "○ idle" }
))
})
})
.child(
Element::new()
.style(LayoutStyle::row().gap(2).h(11).shrink(0.0))
.child(
Element::new()
.style(LayoutStyle::column().gap(1).grow(1.0))
.child(text("mic level (dB, peak hold)"))
.child(mono)
.child(text("waveform (rolling window)"))
.child(scope)
.build(),
)
.child(
Element::new()
.style(LayoutStyle::column().gap(1).w(34))
.child(text("bands"))
.child(spectrum)
.build(),
)
.build(),
)
.child(
Element::new()
.style(LayoutStyle::column().grow(1.0))
.child(Scroll::new(Feed::new(&feed).view(cx)).view(cx))
.build(),
)
.child({
let ptt = ptt.clone();
dyn_view(LayoutStyle::line(1), move || {
let fidelity = keys.fidelity();
let kitty = caps.get().kitty_keyboard;
text(format!(
"talk: {} · key state: {:?} (kitty keyboard: {}) · c clear · q quit",
ptt.gesture_label(),
fidelity,
if kitty { "live" } else { "off" },
))
})
})
.build()
})?;
app.run()
}
fn hash01(x: u64) -> f32 {
let mut z = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^= z >> 31;
(z & 0xFFFF) as f32 / 65535.0
}