kimun_notes/components/
mod.rs1pub mod activity_rail;
2pub mod ask_sources;
3pub mod ask_thread;
4pub mod attachment_view;
5pub mod autocomplete;
6pub mod autosave_timer;
7pub mod command_palette;
8pub mod config_panel;
9pub mod dialogs;
10pub mod dir_browser;
11pub mod drawer;
12pub mod drawer_views;
13pub mod event_state;
14pub mod events;
15pub mod file_list;
16pub mod footer_bar;
17pub mod hints;
18pub mod indexing;
19pub mod markdown_lines;
20pub mod note_browser;
21pub mod overlay;
22pub mod panel;
23pub mod preferences;
24pub mod preview_highlight;
25pub mod preview_pane;
26pub mod query_highlight;
27pub mod query_list_panel;
28pub mod query_panel;
29pub mod query_vars;
30pub mod rich_row;
31pub mod saved_search_breadcrumb;
32pub mod saved_searches_modal;
33pub mod search_list;
34pub mod semantic_search;
35pub mod sidebar;
36pub mod single_line_input;
37pub mod text_editor;
38pub mod which_key;
39
40use ratatui::Frame;
41use ratatui::layout::Rect;
42
43use crate::components::event_state::EventState;
44use crate::components::events::{AppEvent, AppTx, InputEvent};
45use crate::settings::themes::Theme;
46
47static CLIPBOARD: std::sync::OnceLock<std::sync::Mutex<Option<arboard::Clipboard>>> =
64 std::sync::OnceLock::new();
65
66pub(crate) fn with_clipboard<T>(
69 f: impl FnOnce(&mut arboard::Clipboard) -> Result<T, arboard::Error>,
70) -> Result<T, arboard::Error> {
71 let cell = CLIPBOARD.get_or_init(|| std::sync::Mutex::new(None));
72 let mut guard = cell.lock().unwrap_or_else(|e| {
75 let mut g = e.into_inner();
76 *g = None;
77 g
78 });
79 if guard.is_none() {
80 *guard = Some(arboard::Clipboard::new()?);
81 }
82 let result = f(guard.as_mut().expect("just opened"));
83 if matches!(&result, Err(e) if !matches!(e, arboard::Error::ContentNotAvailable)) {
91 *guard = None;
92 }
93 result
94}
95
96pub fn yank(text: String, done_msg: impl Into<String>, tx: &AppTx) {
101 let msg = match with_clipboard(|c| c.set_text(text)) {
102 Ok(()) => done_msg.into(),
103 Err(e) => format!("clipboard: {e}"),
104 };
105 tx.send(AppEvent::FlashMessage(msg)).ok();
106}
107
108pub fn yank_row(target: Option<search_list::YankTarget>, tx: &AppTx) {
116 match target {
117 Some(t) => yank(t.text, format!("{} copied", t.noun), tx),
118 None => {
119 tx.send(AppEvent::FlashMessage("nothing to copy".into()))
120 .ok();
121 }
122 }
123}
124
125pub fn fixed_centered_rect(width: u16, height: u16, r: Rect) -> Rect {
129 let width = width.min(r.width);
130 let height = height.min(r.height);
131 Rect {
132 x: r.x + (r.width - width) / 2,
133 y: r.y + (r.height - height) / 2,
134 width,
135 height,
136 }
137}
138
139pub fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
140 let popup_height = (area.height as u32 * percent_y as u32 / 100) as u16;
141 let popup_width = (area.width as u32 * percent_x as u32 / 100) as u16;
142 Rect {
143 x: area.x + (area.width.saturating_sub(popup_width)) / 2,
144 y: area.y + (area.height.saturating_sub(popup_height)) / 2,
145 width: popup_width,
146 height: popup_height,
147 }
148}
149
150pub trait Component {
151 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
154 let _ = (event, tx);
155 EventState::NotConsumed
156 }
157
158 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool);
159
160 fn hint_shortcuts(&self) -> Vec<(String, String)> {
163 vec![]
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn yank_flashes_outcome_on_tx() {
173 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
177 yank("hello".to_string(), "hello copied", &tx);
178 let ev = rx.try_recv().expect("yank sends exactly one event");
179 match ev {
180 AppEvent::FlashMessage(msg) => {
181 assert!(
182 msg == "hello copied" || msg.starts_with("clipboard: "),
183 "unexpected flash message: {msg}"
184 );
185 }
186 other => panic!("expected FlashMessage, got {other:?}"),
187 }
188 }
189
190 #[test]
191 fn centered_rect_is_centered() {
192 let area = Rect {
193 x: 0,
194 y: 0,
195 width: 100,
196 height: 40,
197 };
198 let r = centered_rect(80, 75, area);
199 assert_eq!(r.width, 80);
200 assert_eq!(r.height, 30);
201 assert_eq!(r.x, 10); assert_eq!(r.y, 5); }
204
205 #[test]
206 fn centered_rect_does_not_underflow() {
207 let area = Rect {
209 x: 0,
210 y: 0,
211 width: 5,
212 height: 5,
213 };
214 let _ = centered_rect(80, 75, area);
215 }
216}