use ratatui::Frame;
use ratatui::crossterm::event::KeyEvent;
use uuid::Uuid;
use crate::entities::chat::ChatSummary;
use crate::features::chat_search_sort::SortMode;
use crate::features::spellcheck::SpellChecker;
use crate::screens::awaited_chat::AwaitedChat;
use crate::shared::i18n::Locale;
use crate::shared::theme::Palette;
use crate::widgets::chat_list::{ChatListAction, ChatListState};
#[derive(Debug, Clone, PartialEq)]
pub enum ChatListIntent {
Close,
Quit,
Switch(Uuid),
NewChat,
Clone(Uuid),
Copy(Uuid),
Delete(Uuid),
Rename { id: Uuid, title: String },
AutoRename(Uuid),
SearchContent(String),
SearchMessages { query: String, sort: SortMode },
OpenFirstMatch { chat: Uuid, query: String },
SetChildrenExpanded { id: Uuid, expanded: bool },
}
pub struct ChatListScreen {
state: ChatListState,
active: Option<Uuid>,
palette: Palette,
loc: &'static Locale,
awaited: AwaitedChat,
}
impl ChatListScreen {
pub fn new(
chats: Vec<ChatSummary>,
active: Option<Uuid>,
palette: Palette,
loc: &'static Locale,
) -> Self {
Self {
state: ChatListState::new(chats, active),
active,
palette,
loc,
awaited: AwaitedChat::Nothing,
}
}
pub fn await_chat(&mut self, awaited: AwaitedChat) {
self.awaited = awaited;
}
pub fn take_awaited(&mut self, id: Uuid) -> bool {
self.awaited.take_if_arrived(id)
}
pub fn stop_awaiting(&mut self) {
self.awaited.clear();
}
pub fn set_chats(&mut self, chats: Vec<ChatSummary>) {
self.state.set_chats(chats);
}
pub fn set_active(&mut self, active: Option<Uuid>) {
self.active = active;
}
pub fn set_palette(&mut self, palette: Palette) {
self.palette = palette;
}
pub fn set_loc(&mut self, loc: &'static Locale) {
self.loc = loc;
}
pub fn set_error(&mut self, message: String) {
self.state.set_error(message);
}
pub fn set_notice(&mut self, message: String) {
self.state.set_notice(message);
}
pub fn handle_key(&mut self, key: KeyEvent) -> Option<ChatListIntent> {
self.awaited.clear();
match self.state.on_key(key) {
ChatListAction::None => None,
ChatListAction::Close => Some(ChatListIntent::Close),
ChatListAction::Quit => Some(ChatListIntent::Quit),
ChatListAction::Switch(id) => Some(ChatListIntent::Switch(id)),
ChatListAction::New => Some(ChatListIntent::NewChat),
ChatListAction::Clone(id) => Some(ChatListIntent::Clone(id)),
ChatListAction::Copy(id) => Some(ChatListIntent::Copy(id)),
ChatListAction::Delete(id) => Some(ChatListIntent::Delete(id)),
ChatListAction::Rename { id, title } => Some(ChatListIntent::Rename { id, title }),
ChatListAction::AutoRename(id) => Some(ChatListIntent::AutoRename(id)),
ChatListAction::SearchContent(query) => Some(ChatListIntent::SearchContent(query)),
ChatListAction::SearchMessages { query, sort } => {
Some(ChatListIntent::SearchMessages { query, sort })
}
ChatListAction::OpenFirstMatch { chat, query } => {
Some(ChatListIntent::OpenFirstMatch { chat, query })
}
ChatListAction::SetChildrenExpanded { id, expanded } => {
Some(ChatListIntent::SetChildrenExpanded { id, expanded })
}
}
}
pub fn restore_content_query(&mut self, query: String) {
self.state.restore_content_query(query);
}
pub fn set_search_results(&mut self, query: String, chat_ids: Option<Vec<Uuid>>) {
self.state.set_search_results(query, chat_ids);
}
pub fn handle_paste(&mut self, text: &str) {
self.state.handle_paste(text);
}
pub fn recheck_spelling(&mut self, spell: &SpellChecker) -> bool {
self.state.recheck_rename_spelling(spell)
}
pub fn render(&mut self, frame: &mut Frame) {
self.state
.render(frame, frame.area(), self.active, &self.palette, self.loc);
}
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::crossterm::event::{KeyCode, KeyModifiers};
fn ru() -> &'static Locale {
crate::shared::i18n::locale(crate::shared::i18n::Lang::Ru)
}
fn chat(title: &str) -> ChatSummary {
ChatSummary::fixture(title)
}
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn ctrl(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::CONTROL)
}
#[test]
fn esc_maps_to_close() {
let mut s = ChatListScreen::new(vec![chat("A")], None, Palette::default(), ru());
assert_eq!(s.handle_key(key(KeyCode::Esc)), Some(ChatListIntent::Close));
}
#[test]
fn ctrl_q_and_f10_map_to_quit() {
let mut s = ChatListScreen::new(vec![chat("A")], None, Palette::default(), ru());
assert_eq!(
s.handle_key(ctrl(KeyCode::Char('q'))),
Some(ChatListIntent::Quit)
);
assert_eq!(
s.handle_key(key(KeyCode::F(10))),
Some(ChatListIntent::Quit)
);
}
#[test]
fn enter_maps_to_switch_of_selected() {
let chats = vec![chat("A")];
let id = chats[0].id;
let mut s = ChatListScreen::new(chats, Some(id), Palette::default(), ru());
assert_eq!(
s.handle_key(key(KeyCode::Enter)),
Some(ChatListIntent::Switch(id))
);
}
#[test]
fn ctrl_n_maps_to_new_chat() {
let mut s = ChatListScreen::new(vec![chat("A")], None, Palette::default(), ru());
assert_eq!(
s.handle_key(ctrl(KeyCode::Char('n'))),
Some(ChatListIntent::NewChat)
);
}
#[test]
fn typing_filters_and_returns_none() {
let mut s = ChatListScreen::new(
vec![chat("Альфа"), chat("Бета")],
None,
Palette::default(),
ru(),
);
assert_eq!(s.handle_key(key(KeyCode::Char('Б'))), None);
}
#[test]
fn an_awaited_chat_is_taken_once() {
let mut s = ChatListScreen::new(vec![chat("A")], None, Palette::default(), ru());
let id = Uuid::new_v4();
assert!(!s.take_awaited(id));
s.await_chat(AwaitedChat::Created);
assert!(s.take_awaited(id));
assert!(!s.take_awaited(id));
}
#[test]
fn a_key_or_a_refusal_ends_the_wait() {
let mut s = ChatListScreen::new(vec![chat("A")], None, Palette::default(), ru());
let id = Uuid::new_v4();
s.await_chat(AwaitedChat::Chat(id));
s.handle_key(key(KeyCode::Down));
assert!(!s.take_awaited(id), "the user moved on");
s.await_chat(AwaitedChat::Created);
s.stop_awaiting();
assert!(!s.take_awaited(id), "the request was refused");
}
#[test]
fn render_does_not_panic() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let mut s = ChatListScreen::new(vec![chat("Альфа")], None, Palette::default(), ru());
let mut term = Terminal::new(TestBackend::new(80, 24)).unwrap();
term.draw(|f| s.render(f)).unwrap();
}
}