ui: split out lots of code into a widets mod
This commit is contained in:
@@ -20,6 +20,7 @@ mod audio;
|
|||||||
mod artifacts;
|
mod artifacts;
|
||||||
mod archive;
|
mod archive;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
// TODO: We should be able to delete entries from the conversation, or at least go back and edit something I said.
|
// TODO: We should be able to delete entries from the conversation, or at least go back and edit something I said.
|
||||||
// TODO: I want a "mark" command or keyboard shortcut, that inserts a marker into the log, so I know where to come back for the next speaking segment.
|
// TODO: I want a "mark" command or keyboard shortcut, that inserts a marker into the log, so I know where to come back for the next speaking segment.
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use crossterm::event::{self, KeyCode, KeyModifiers};
|
use crossterm::event::{self, KeyCode, KeyModifiers, MediaKeyCode::Record};
|
||||||
use ratatui::{Frame, layout::{Direction, Layout, Rect}, style::{self, Style, Stylize}, text::{Line, Span, Text}, widgets::{BorderType, Clear, Gauge, List, ListDirection, ListState, Paragraph, Wrap}};
|
use ratatui::{Frame, layout::{Direction, Layout, Rect}, style::{self, Style, Stylize}, text::{Line, Span, Text}, widgets::{BorderType, Clear, Gauge, List, ListDirection, ListState, Paragraph, StatefulWidget, Widget, Wrap}};
|
||||||
use throbber_widgets_tui::{Throbber, ThrobberState};
|
use throbber_widgets_tui::{Throbber, ThrobberState};
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use tui_input::{Input, backend::crossterm::EventHandler};
|
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||||
use tui_skeleton::{AnimationMode, Block, Constraint, SkeletonText};
|
use tui_skeleton::{AnimationMode, Block, Constraint, SkeletonText};
|
||||||
|
|
||||||
use crate::{audio::AudioInputControl, prediction::{PredictionAction, SessionControl, SessionUpdate}, scene::{Scene, conversation::ConversationEntry}, transcription::TranscriptionControl, tts::TtsControl};
|
use crate::{audio::AudioInputControl, prediction::{PredictionAction, SessionControl, SessionUpdate}, scene::{Scene, conversation::ConversationEntry}, transcription::TranscriptionControl, tts::TtsControl};
|
||||||
|
use crate::widgets::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Ui {
|
pub struct Ui {
|
||||||
@@ -54,47 +55,6 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_line<'a>(entry: &'a ConversationEntry, max_width: usize) -> Text<'a> {
|
|
||||||
let prefix = entry.prefix().unwrap_or_default();
|
|
||||||
|
|
||||||
let style = entry.prefix_style();
|
|
||||||
|
|
||||||
let text_style = entry.text_style();
|
|
||||||
|
|
||||||
let avail_width = max_width - prefix.len();
|
|
||||||
let indent = " ".repeat(prefix.len());
|
|
||||||
let wrap_options = textwrap::Options::new(avail_width).initial_indent(prefix).subsequent_indent(&indent);
|
|
||||||
let wrapped: Vec<Line> = textwrap::wrap(&entry.to_string(), wrap_options)
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(idx, s)| {
|
|
||||||
if idx == 0 {
|
|
||||||
Line::from_iter([Span::from(prefix).style(style), Span::from(s[prefix.len()..].to_string()).style(text_style)])
|
|
||||||
} else {
|
|
||||||
Line::from(s.to_string()).style(text_style)
|
|
||||||
}
|
|
||||||
}).collect();
|
|
||||||
|
|
||||||
Text::from_iter(wrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_conversation(&mut self, frame: &mut Frame, area: Rect) {
|
|
||||||
let width = area.width.into();
|
|
||||||
let items: Vec<Text> = self.scene.conversation().iter().rev().map(|entry| {
|
|
||||||
Self::format_line(entry, width)
|
|
||||||
}).collect();
|
|
||||||
// TODO: Would be nice to be able to scroll a longer conversation with the scroll wheel, or with page up/down
|
|
||||||
frame.render_stateful_widget(
|
|
||||||
List::new(items)
|
|
||||||
.block(Block::bordered().border_style(style::Color::LightYellow).title("Conversation (Press Tab to select and read Eva's lines aloud)"))
|
|
||||||
.direction(ListDirection::BottomToTop)
|
|
||||||
.highlight_symbol("> ")
|
|
||||||
.highlight_style(style::Style::new().bold().fg(style::Color::Cyan)),
|
|
||||||
area,
|
|
||||||
&mut self.conversation_state
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_options(&mut self, frame: &mut Frame, area: Rect) {
|
fn draw_options(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
let borders = Block::bordered().border_style(style::Color::LightGreen).title("Reply Options (Press 'Ctrl+R' to regenerate, Ctrl+Enter to use)");
|
let borders = Block::bordered().border_style(style::Color::LightGreen).title("Reply Options (Press 'Ctrl+R' to regenerate, Ctrl+Enter to use)");
|
||||||
if self.is_requesting {
|
if self.is_requesting {
|
||||||
@@ -105,34 +65,7 @@ impl Ui {
|
|||||||
.block(borders);
|
.block(borders);
|
||||||
frame.render_widget(list, area);
|
frame.render_widget(list, area);
|
||||||
} else {
|
} else {
|
||||||
let wrap_options = textwrap::Options::new(area.width as usize).subsequent_indent("...");
|
frame.render_stateful_widget(Options(self.scene.reply_options()), area, &mut self.reply_state);
|
||||||
let options: Vec<Text> = self.scene.reply_options().iter().map(|option| {
|
|
||||||
let mut contents: Vec<Line> = vec![];
|
|
||||||
|
|
||||||
if let Some(direction) = &option.stage_direction {
|
|
||||||
let padded = format!("({})", direction);
|
|
||||||
let mut wrapped_direction: Vec<Line> = textwrap::wrap(&padded, wrap_options.clone())
|
|
||||||
.iter()
|
|
||||||
.map(|x| { Line::from(x.to_string()).fg(style::Color::Yellow)}).collect();
|
|
||||||
contents.append(&mut wrapped_direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut text: Vec<Line> = textwrap::wrap(&option.text, wrap_options.clone())
|
|
||||||
.iter()
|
|
||||||
.map(|x| { Line::from(x.to_string())}).collect();
|
|
||||||
contents.append(&mut text);
|
|
||||||
Text::from_iter(contents)
|
|
||||||
}).collect();
|
|
||||||
frame.render_stateful_widget(
|
|
||||||
List::new(options)
|
|
||||||
.block(borders)
|
|
||||||
.style(ratatui::style::Color::White)
|
|
||||||
.highlight_symbol("> ".fg(ratatui::style::Color::Cyan))
|
|
||||||
.highlight_style(style::Style::new().bold().bg(style::Color::DarkGray))
|
|
||||||
.repeat_highlight_symbol(true),
|
|
||||||
area,
|
|
||||||
&mut self.reply_state
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,46 +88,6 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status(&self, frame: &mut Frame, area: Rect) {
|
|
||||||
let time_remaining: Duration = self.scene.direction().time_remaining();
|
|
||||||
let minutes_remaining = time_remaining.num_seconds() / 60;
|
|
||||||
|
|
||||||
let negative = time_remaining.abs() != time_remaining;
|
|
||||||
|
|
||||||
let time_style = if minutes_remaining <= 0 || negative {
|
|
||||||
Style::new().fg(ratatui::style::Color::LightRed).bold()
|
|
||||||
} else if minutes_remaining < 5 {
|
|
||||||
Style::new().fg(ratatui::style::Color::LightRed).bold()
|
|
||||||
} else if minutes_remaining < 10 {
|
|
||||||
ratatui::style::Color::Red.into()
|
|
||||||
} else if minutes_remaining < 25 {
|
|
||||||
ratatui::style::Color::Yellow.into()
|
|
||||||
} else if minutes_remaining < 60 {
|
|
||||||
ratatui::style::Color::Green.into()
|
|
||||||
} else {
|
|
||||||
ratatui::style::Color::Blue.into()
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatted_time = if negative {
|
|
||||||
format!("-{:0>2}:{:0>2}:{:0>2}", time_remaining.num_hours().abs(), time_remaining.num_minutes().abs()% 60, time_remaining.num_seconds().abs() % 60)
|
|
||||||
} else {
|
|
||||||
format!("{:0>2}:{:0>2}:{:0>2}", time_remaining.num_hours(), time_remaining.num_minutes() % 60, time_remaining.num_seconds() % 60)
|
|
||||||
};
|
|
||||||
|
|
||||||
let status_line = Line::from_iter([
|
|
||||||
Span::from(format!("Episode {}", self.scene.direction().episode_number)).style(ratatui::style::Color::LightBlue),
|
|
||||||
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
|
||||||
Span::from(format!("{} tracks", self.scene.scenery().current_playlist.len())).style(ratatui::style::Color::LightBlue),
|
|
||||||
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
|
||||||
Span::from(format!("Time Remaining: {}", formatted_time)).style(time_style),
|
|
||||||
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
|
||||||
Span::from(format!("{} artifacts recorded", self.scene.scenery().artifacts.len())).style(ratatui::style::Color::LightBlue),
|
|
||||||
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
|
||||||
Span::from(format!("{} tokens sacrificed", self.scene.tokens_consumed)).style(ratatui::style::Color::LightCyan),
|
|
||||||
]);
|
|
||||||
frame.render_widget(status_line, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_narration(&self, frame: &mut Frame, area: Rect) {
|
fn draw_narration(&self, frame: &mut Frame, area: Rect) {
|
||||||
let narrative_desc = if self.scene.direction().narrative.is_empty() {
|
let narrative_desc = if self.scene.direction().narrative.is_empty() {
|
||||||
Span::from("No narrative available.").style(ratatui::style::Color::DarkGray)
|
Span::from("No narrative available.").style(ratatui::style::Color::DarkGray)
|
||||||
@@ -205,16 +98,6 @@ impl Ui {
|
|||||||
frame.render_widget(setting, area);
|
frame.render_widget(setting, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_event_log(&self, frame: &mut Frame, area: Rect) {
|
|
||||||
let items: Vec<Line> = self.scene.conversation().iter().filter(|entry| { if let ConversationEntry::StageDirection(_) = entry { true } else { false }}).rev().map(|entry| {
|
|
||||||
match entry {
|
|
||||||
ConversationEntry::StageDirection(text) => Line::from_iter([text]).style(ratatui::style::Color::Yellow),
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
}).collect();
|
|
||||||
frame.render_widget(Paragraph::new(items).block(Block::bordered().border_style(style::Color::Red).title("Event Log")).wrap(Wrap { trim: false }), area);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw(&mut self, frame: &mut Frame) {
|
pub fn draw(&mut self, frame: &mut Frame) {
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
@@ -231,14 +114,8 @@ impl Ui {
|
|||||||
.constraints([Constraint::Fill(4), Constraint::Fill(1)])
|
.constraints([Constraint::Fill(4), Constraint::Fill(1)])
|
||||||
.split(layout[0]);
|
.split(layout[0]);
|
||||||
|
|
||||||
let context_layout = Layout::default()
|
frame.render_stateful_widget(Conversation(self.scene.conversation()), scene_layout[0], &mut self.conversation_state);
|
||||||
.direction(Direction::Vertical)
|
self.draw_narration(frame, scene_layout[1]);
|
||||||
.constraints([Constraint::Fill(1), Constraint::Fill(1)])
|
|
||||||
.split(scene_layout[1]);
|
|
||||||
|
|
||||||
self.draw_conversation(frame, scene_layout[0]);
|
|
||||||
self.draw_narration(frame, context_layout[0]);
|
|
||||||
self.draw_event_log(frame, context_layout[1]);
|
|
||||||
self.draw_options(frame, layout[1]);
|
self.draw_options(frame, layout[1]);
|
||||||
|
|
||||||
let status_layout = Layout::default()
|
let status_layout = Layout::default()
|
||||||
@@ -248,51 +125,9 @@ impl Ui {
|
|||||||
|
|
||||||
self.draw_user_input(frame, layout[2]);
|
self.draw_user_input(frame, layout[2]);
|
||||||
self.draw_io_throbber(frame, status_layout[0]);
|
self.draw_io_throbber(frame, status_layout[0]);
|
||||||
self.draw_status(frame, status_layout[1]);
|
frame.render_widget(StatusBar(&self.scene), status_layout[1]);
|
||||||
self.draw_recording_status(frame, status_layout[2]);
|
frame.render_widget(RecordingStatus(self.recording_audio), status_layout[2]);
|
||||||
self.draw_volume(frame, status_layout[3]);
|
frame.render_widget(Volume(self.audio_level, self.recording_audio), status_layout[3]);
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_recording_status(&self, frame: &mut Frame, area: Rect) {
|
|
||||||
frame.render_widget(Line::from_iter(
|
|
||||||
if self.recording_audio {
|
|
||||||
[
|
|
||||||
Span::from(" "),
|
|
||||||
Span::from(" Recording ").bg(style::Color::LightRed).fg(style::Color::White),
|
|
||||||
Span::from(" ")
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
[
|
|
||||||
Span::from(" "),
|
|
||||||
Span::from(" Stopped ").bg(style::Color::DarkGray).fg(style::Color::White),
|
|
||||||
Span::from(" ")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
), area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_volume(&self, frame: &mut Frame, area: Rect) {
|
|
||||||
const NOISE_FLOOR: f64 = 50.;
|
|
||||||
let vu_pct = 1.0 - (self.audio_level.abs().min(NOISE_FLOOR) / NOISE_FLOOR);
|
|
||||||
|
|
||||||
let volume_color = if self.recording_audio {
|
|
||||||
if vu_pct >= 0.85 {
|
|
||||||
style::Color::Red
|
|
||||||
} else if vu_pct >= 0.60 {
|
|
||||||
style::Color::Yellow
|
|
||||||
} else {
|
|
||||||
style::Color::LightGreen
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
style::Color::Gray
|
|
||||||
};
|
|
||||||
|
|
||||||
let gauge = Gauge::default()
|
|
||||||
.ratio(vu_pct)
|
|
||||||
.use_unicode(true)
|
|
||||||
.gauge_style(volume_color)
|
|
||||||
.label(format!("{:.01}dB", self.audio_level));
|
|
||||||
frame.render_widget(gauge, area);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn insert_selected_prompt(&mut self) {
|
async fn insert_selected_prompt(&mut self) {
|
||||||
@@ -313,7 +148,7 @@ impl Ui {
|
|||||||
if let Ok(episode_number) = arg.trim().parse() {
|
if let Ok(episode_number) = arg.trim().parse() {
|
||||||
self.predictions.insert(PredictionAction::SetEpisodeNumber(episode_number)).await;
|
self.predictions.insert(PredictionAction::SetEpisodeNumber(episode_number)).await;
|
||||||
} else {
|
} else {
|
||||||
self.predictions.log("Invalid episode number format. Use /episode [number]".into()).await;
|
log::error!("Invalid episode number format. Use /episode [number]");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -321,9 +156,9 @@ impl Ui {
|
|||||||
if let Ok(minutes) = arg.trim().parse::<i64>() {
|
if let Ok(minutes) = arg.trim().parse::<i64>() {
|
||||||
let end_time = Utc::now() + Duration::minutes(minutes);
|
let end_time = Utc::now() + Duration::minutes(minutes);
|
||||||
self.predictions.insert(PredictionAction::SetShowEndTime(end_time)).await;
|
self.predictions.insert(PredictionAction::SetShowEndTime(end_time)).await;
|
||||||
self.predictions.log(format!("Set timer for {} minutes.", minutes)).await;
|
log::info!("Set timer for {} minutes.", minutes);
|
||||||
} else {
|
} else {
|
||||||
self.predictions.log("Invalid timer format. Use /timer [minutes]".into()).await;
|
log::error!("Invalid timer format. Use /timer [minutes]");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/narrative" => {
|
"/narrative" => {
|
||||||
@@ -336,7 +171,7 @@ impl Ui {
|
|||||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::ShipComputer(arg.to_string()))).await;
|
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::ShipComputer(arg.to_string()))).await;
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
self.predictions.log("Unknown command. Available commands: /episode [number], /narrative [text], /event [text], /computer [text], /timer [minutes]".into()).await;
|
log::error!("Unknown command. Available commands: /episode [number], /narrative [text], /event [text], /computer [text], /timer [minutes]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
use chrono::Duration;
|
||||||
|
use ratatui::{layout::Rect, style::Style, text::{Line, Text}, widgets::*};
|
||||||
|
use tui_skeleton::Block;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
|
||||||
|
use crate::{prediction::PossibleResponse, scene::{Scene, conversation::ConversationEntry}};
|
||||||
|
|
||||||
|
pub struct Options<'a>(pub &'a Vec<PossibleResponse>);
|
||||||
|
|
||||||
|
impl StatefulWidget for Options<'_> {
|
||||||
|
type State = ListState;
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
|
||||||
|
let borders = Block::bordered().border_style(style::Color::LightGreen).title("Reply Options (Press 'Ctrl+R' to regenerate, Ctrl+Enter to use)");
|
||||||
|
let wrap_options = textwrap::Options::new(area.width as usize).subsequent_indent("...");
|
||||||
|
let options: Vec<Text> = self.0.iter().map(|option| {
|
||||||
|
let mut contents: Vec<Line> = vec![];
|
||||||
|
|
||||||
|
if let Some(direction) = &option.stage_direction {
|
||||||
|
let padded = format!("({})", direction);
|
||||||
|
let mut wrapped_direction: Vec<Line> = textwrap::wrap(&padded, wrap_options.clone())
|
||||||
|
.iter()
|
||||||
|
.map(|x| { Line::from(x.to_string()).fg(style::Color::Yellow)}).collect();
|
||||||
|
contents.append(&mut wrapped_direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text: Vec<Line> = textwrap::wrap(&option.text, wrap_options.clone())
|
||||||
|
.iter()
|
||||||
|
.map(|x| { Line::from(x.to_string())}).collect();
|
||||||
|
contents.append(&mut text);
|
||||||
|
Text::from_iter(contents)
|
||||||
|
}).collect();
|
||||||
|
let list = List::new(options)
|
||||||
|
.block(borders)
|
||||||
|
.style(ratatui::style::Color::White)
|
||||||
|
.highlight_symbol("> ".fg(ratatui::style::Color::Cyan))
|
||||||
|
.highlight_style(style::Style::new().bold().bg(style::Color::DarkGray))
|
||||||
|
.repeat_highlight_symbol(true);
|
||||||
|
StatefulWidget::render(list, area, buf, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Conversation<'a>(pub &'a Vec<ConversationEntry>);
|
||||||
|
|
||||||
|
impl Conversation<'_> {
|
||||||
|
fn format_line<'a>(entry: &'a ConversationEntry, max_width: usize) -> Text<'a> {
|
||||||
|
let prefix = entry.prefix().unwrap_or_default();
|
||||||
|
|
||||||
|
let style = entry.prefix_style();
|
||||||
|
|
||||||
|
let text_style = entry.text_style();
|
||||||
|
|
||||||
|
let avail_width = max_width - prefix.len();
|
||||||
|
let indent = " ".repeat(prefix.len());
|
||||||
|
let wrap_options = textwrap::Options::new(avail_width).initial_indent(prefix).subsequent_indent(&indent);
|
||||||
|
let wrapped: Vec<Line> = textwrap::wrap(&entry.to_string(), wrap_options)
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, s)| {
|
||||||
|
if idx == 0 {
|
||||||
|
Line::from_iter([Span::from(prefix).style(style), Span::from(s[prefix.len()..].to_string()).style(text_style)])
|
||||||
|
} else {
|
||||||
|
Line::from(s.to_string()).style(text_style)
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
Text::from_iter(wrapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> StatefulWidget for Conversation<'a> {
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
|
||||||
|
let width = area.width.into();
|
||||||
|
let items: Vec<Text> = self.0.iter().rev().map(|entry| {
|
||||||
|
Self::format_line(entry, width)
|
||||||
|
}).collect();
|
||||||
|
// TODO: Would be nice to be able to scroll a longer conversation with the scroll wheel, or with page up/down
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(Block::bordered().border_style(Color::LightYellow).title("Conversation (Press Tab to select and read Eva's lines aloud)"))
|
||||||
|
.direction(ListDirection::BottomToTop)
|
||||||
|
.highlight_symbol("> ")
|
||||||
|
.highlight_style(Style::new().bold().fg(Color::Cyan));
|
||||||
|
StatefulWidget::render(list, area, buf, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = ListState;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Volume(pub f64, pub bool);
|
||||||
|
|
||||||
|
impl Widget for Volume {
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized
|
||||||
|
{
|
||||||
|
const NOISE_FLOOR: f64 = 50.;
|
||||||
|
let vu_pct = 1.0 - (self.0.abs().min(NOISE_FLOOR) / NOISE_FLOOR);
|
||||||
|
|
||||||
|
let volume_color = if self.1 {
|
||||||
|
if vu_pct >= 0.85 {
|
||||||
|
style::Color::Red
|
||||||
|
} else if vu_pct >= 0.60 {
|
||||||
|
style::Color::Yellow
|
||||||
|
} else {
|
||||||
|
style::Color::LightGreen
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style::Color::Gray
|
||||||
|
};
|
||||||
|
|
||||||
|
let gauge = Gauge::default()
|
||||||
|
.ratio(vu_pct)
|
||||||
|
.use_unicode(true)
|
||||||
|
.gauge_style(volume_color)
|
||||||
|
.label(format!("{:.01}dB", self.0));
|
||||||
|
gauge.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RecordingStatus(pub bool);
|
||||||
|
|
||||||
|
impl Widget for RecordingStatus {
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized
|
||||||
|
{
|
||||||
|
Line::from_iter(
|
||||||
|
if self.0 {
|
||||||
|
[
|
||||||
|
Span::from(" "),
|
||||||
|
Span::from(" Recording ").bg(style::Color::LightRed).fg(style::Color::White),
|
||||||
|
Span::from(" ")
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
Span::from(" "),
|
||||||
|
Span::from(" Stopped ").bg(style::Color::DarkGray).fg(style::Color::White),
|
||||||
|
Span::from(" ")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
).render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StatusBar<'a>(pub &'a Scene);
|
||||||
|
|
||||||
|
impl Widget for StatusBar<'_> {
|
||||||
|
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized {
|
||||||
|
let time_remaining: Duration = self.0.direction().time_remaining();
|
||||||
|
let minutes_remaining = time_remaining.num_seconds() / 60;
|
||||||
|
|
||||||
|
let negative = time_remaining.abs() != time_remaining;
|
||||||
|
|
||||||
|
let time_style = if minutes_remaining <= 0 || negative {
|
||||||
|
Style::new().fg(ratatui::style::Color::LightRed).bold()
|
||||||
|
} else if minutes_remaining < 5 {
|
||||||
|
Style::new().fg(ratatui::style::Color::LightRed).bold()
|
||||||
|
} else if minutes_remaining < 10 {
|
||||||
|
ratatui::style::Color::Red.into()
|
||||||
|
} else if minutes_remaining < 25 {
|
||||||
|
ratatui::style::Color::Yellow.into()
|
||||||
|
} else if minutes_remaining < 60 {
|
||||||
|
ratatui::style::Color::Green.into()
|
||||||
|
} else {
|
||||||
|
ratatui::style::Color::Blue.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted_time = if negative {
|
||||||
|
format!("-{:0>2}:{:0>2}:{:0>2}", time_remaining.num_hours().abs(), time_remaining.num_minutes().abs()% 60, time_remaining.num_seconds().abs() % 60)
|
||||||
|
} else {
|
||||||
|
format!("{:0>2}:{:0>2}:{:0>2}", time_remaining.num_hours(), time_remaining.num_minutes() % 60, time_remaining.num_seconds() % 60)
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_line = Line::from_iter([
|
||||||
|
Span::from(format!("Episode {}", self.0.direction().episode_number)).style(ratatui::style::Color::LightBlue),
|
||||||
|
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
||||||
|
Span::from(format!("{} tracks", self.0.scenery().current_playlist.len())).style(ratatui::style::Color::LightBlue),
|
||||||
|
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
||||||
|
Span::from(format!("Time Remaining: {}", formatted_time)).style(time_style),
|
||||||
|
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
||||||
|
Span::from(format!("{} artifacts recorded", self.0.scenery().artifacts.len())).style(ratatui::style::Color::LightBlue),
|
||||||
|
Span::from(" | ").style(ratatui::style::Color::DarkGray),
|
||||||
|
Span::from(format!("{} tokens sacrificed", self.0.tokens_consumed)).style(ratatui::style::Color::LightCyan),
|
||||||
|
]);
|
||||||
|
status_line.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user