use ratatui::Frame;
use ratatui::layout::Rect;
use crate::components::Component;
use crate::components::ask_sources::SourcesPanel;
use crate::components::config_panel::ConfigPanel;
use crate::components::drawer_views::{LinksPanel, OutlinePanel, TagsPanel};
use crate::components::event_state::EventState;
use crate::components::events::{AppTx, InputEvent};
use crate::components::query_panel::QueryPanel;
use crate::components::semantic_search::SemanticPanel;
use crate::components::sidebar::SidebarComponent;
use crate::settings::themes::Theme;
use kimun_core::NoteVault;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DrawerView {
Files,
Find,
Semantic,
Ask,
Tags,
Links,
Outline,
Config,
}
impl DrawerView {
pub fn label(&self) -> &'static str {
match self {
DrawerView::Files => "FILES",
DrawerView::Find => "FIND",
DrawerView::Semantic => "SEMANTIC",
DrawerView::Ask => "ASK",
DrawerView::Tags => "TAGS",
DrawerView::Links => "LINKS",
DrawerView::Outline => "OUTLINE",
DrawerView::Config => "CFG",
}
}
}
pub use crate::components::config_panel::ConfigInfo;
pub struct DrawerHost {
active: DrawerView,
sidebar: SidebarComponent,
query: QueryPanel,
semantic: SemanticPanel,
ask_sources: SourcesPanel,
tags: TagsPanel,
links: LinksPanel,
outline: OutlinePanel,
config: ConfigPanel,
}
impl DrawerHost {
#[allow(clippy::too_many_arguments)]
pub fn new(
vault: std::sync::Arc<NoteVault>,
key_bindings: &crate::keys::KeyBindings,
sidebar: SidebarComponent,
query: QueryPanel,
semantic: SemanticPanel,
tags: TagsPanel,
links: LinksPanel,
outline: OutlinePanel,
) -> Self {
Self {
active: DrawerView::Files,
sidebar,
query,
semantic,
ask_sources: SourcesPanel::new(vault, key_bindings),
tags,
links,
outline,
config: ConfigPanel::default(),
}
}
pub fn set_config_info(&mut self, info: ConfigInfo) {
self.config.set_info(info);
}
pub fn active_view(&self) -> DrawerView {
self.active
}
pub fn is_text_input(&self) -> bool {
matches!(self.active, DrawerView::Find | DrawerView::Semantic)
}
pub fn set_view(&mut self, view: DrawerView) {
self.active = view;
}
pub fn sidebar(&self) -> &SidebarComponent {
&self.sidebar
}
pub fn sidebar_mut(&mut self) -> &mut SidebarComponent {
&mut self.sidebar
}
pub fn query(&self) -> &QueryPanel {
&self.query
}
pub fn query_mut(&mut self) -> &mut QueryPanel {
&mut self.query
}
pub fn semantic_mut(&mut self) -> &mut SemanticPanel {
&mut self.semantic
}
pub fn ask_sources_mut(&mut self) -> &mut SourcesPanel {
&mut self.ask_sources
}
pub fn tags_mut(&mut self) -> &mut TagsPanel {
&mut self.tags
}
pub fn links_mut(&mut self) -> &mut LinksPanel {
&mut self.links
}
pub fn outline_mut(&mut self) -> &mut OutlinePanel {
&mut self.outline
}
pub fn hint_shortcuts(&self) -> Vec<(String, String)> {
match self.active {
DrawerView::Files => self.sidebar.hint_shortcuts(),
DrawerView::Find => self.query.hint_shortcuts(),
DrawerView::Semantic => self.semantic.hint_shortcuts(),
DrawerView::Ask => self.ask_sources.hint_shortcuts(),
DrawerView::Tags => self.tags.hint_shortcuts(),
DrawerView::Links => self.links.hint_shortcuts(),
DrawerView::Outline => self.outline.hint_shortcuts(),
DrawerView::Config => self.config.hint_shortcuts(),
}
}
pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
match self.active {
DrawerView::Files => self.sidebar.handle_input(event, tx),
DrawerView::Find => {
if let InputEvent::Key(key) = event {
self.query.handle_key(key, tx)
} else {
EventState::NotConsumed
}
}
DrawerView::Semantic => self.semantic.handle_input(event, tx),
DrawerView::Ask => self.ask_sources.handle_input(event, tx),
DrawerView::Tags => self.tags.handle_input(event, tx),
DrawerView::Links => self.links.handle_input(event, tx),
DrawerView::Outline => self.outline.handle_input(event, tx),
DrawerView::Config => self.config.handle_input(event, tx),
}
}
pub fn handle_mouse(&mut self, event: &InputEvent, tx: &AppTx) {
let InputEvent::Mouse(mouse) = event else {
return;
};
match self.active {
DrawerView::Find => {
self.query.handle_mouse(mouse, tx);
}
_ => {
self.handle_input(event, tx);
}
}
}
pub fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
match self.active {
DrawerView::Files => self.sidebar.render(f, rect, theme, focused),
DrawerView::Find => self.query.render(f, rect, theme, focused),
DrawerView::Semantic => self.semantic.render(f, rect, theme, focused),
DrawerView::Ask => self.ask_sources.render(f, rect, theme, focused),
DrawerView::Tags => self.tags.render(f, rect, theme, focused),
DrawerView::Links => self.links.render(f, rect, theme, focused),
DrawerView::Outline => self.outline.render(f, rect, theme, focused),
DrawerView::Config => self.config.render(f, rect, theme, focused),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::events::AppEvent;
use crate::settings::AppSettings;
use crate::test_support::temp_vault;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::mpsc::unbounded_channel;
async fn make_host() -> DrawerHost {
let vault = temp_vault("drawerhost").await;
vault.validate_and_init().await.unwrap();
let settings = AppSettings::default();
let sidebar = crate::components::sidebar::SidebarComponent::new(
settings.key_bindings.clone(),
vault.clone(),
settings.icons(),
&settings,
);
let query = crate::components::query_panel::QueryPanel::new(
vault.clone(),
settings.key_bindings.clone(),
settings.icons(),
);
let semantic = crate::components::semantic_search::SemanticPanel::new(
vault.clone(),
std::sync::Arc::new(std::sync::RwLock::new(settings.clone())),
settings.icons(),
);
let tags = crate::components::drawer_views::TagsPanel::new(vault.clone(), settings.icons());
let links =
crate::components::drawer_views::LinksPanel::new(vault.clone(), settings.icons());
let outline =
crate::components::drawer_views::OutlinePanel::new(vault.clone(), settings.icons());
DrawerHost::new(
vault,
&settings.key_bindings,
sidebar,
query,
semantic,
tags,
links,
outline,
)
}
fn key(code: KeyCode) -> InputEvent {
InputEvent::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
#[tokio::test]
async fn find_view_delivers_keys_only() {
let mut host = make_host().await;
let (tx, _rx) = unbounded_channel();
host.set_view(DrawerView::Find);
assert_eq!(
host.handle_input(&InputEvent::Paste("hi".into()), &tx),
EventState::NotConsumed,
"a paste is not a key; FIND must not consume it"
);
}
#[tokio::test]
async fn config_view_routes_to_the_config_panel() {
let mut host = make_host().await;
let (tx, mut rx) = unbounded_channel();
host.set_view(DrawerView::Config);
assert_eq!(
host.handle_input(&key(KeyCode::Char('t')), &tx),
EventState::Consumed
);
assert!(matches!(
rx.try_recv(),
Ok(AppEvent::ExecuteLeaderAction(
crate::keys::leader::LeaderAction::VaultTheme
))
));
assert_eq!(
host.handle_input(&key(KeyCode::Char('x')), &tx),
EventState::NotConsumed
);
}
#[tokio::test]
async fn hints_follow_the_active_view() {
let mut host = make_host().await;
host.set_view(DrawerView::Config);
let cfg_hints = host.hint_shortcuts();
assert!(cfg_hints.iter().any(|(_, h)| h == "Theme picker"));
host.set_view(DrawerView::Files);
assert_ne!(host.hint_shortcuts(), cfg_hints);
}
#[tokio::test]
async fn text_input_views_are_find_and_semantic() {
let mut host = make_host().await;
for (view, expect) in [
(DrawerView::Files, false),
(DrawerView::Find, true),
(DrawerView::Semantic, true),
(DrawerView::Ask, false),
(DrawerView::Tags, false),
(DrawerView::Links, false),
(DrawerView::Outline, false),
(DrawerView::Config, false),
] {
host.set_view(view);
assert_eq!(host.is_text_input(), expect, "{view:?}");
}
}
}