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 button_row;
8pub mod clickable;
9pub mod command_palette;
10pub mod config_panel;
11pub mod dialogs;
12pub mod dir_browser;
13pub mod drawer;
14pub mod drawer_views;
15pub mod event_state;
16pub mod events;
17pub mod file_list;
18pub mod footer_bar;
19pub mod hint_row;
20pub mod hints;
21pub mod indexing;
22pub mod key_picker;
23pub mod markdown_lines;
24pub mod note_browser;
25pub mod overlay;
26pub mod panel;
27pub mod preferences;
28pub mod preview_highlight;
29pub mod preview_pane;
30pub mod query_highlight;
31pub mod query_list_panel;
32pub mod query_panel;
33pub mod query_vars;
34pub mod rich_row;
35pub mod saved_search_breadcrumb;
36pub mod saved_searches_modal;
37pub mod search_list;
38pub mod semantic_search;
39pub mod sidebar;
40pub mod single_line_input;
41pub mod sortable;
42pub mod text_editor;
43pub mod which_key;
44
45use ratatui::Frame;
46use ratatui::layout::Rect;
47
48pub const DOUBLE_CLICK: std::time::Duration = std::time::Duration::from_millis(400);
54
55use crate::components::event_state::EventState;
56use crate::components::events::{AppEvent, AppTx, InputEvent};
57use crate::settings::themes::Theme;
58
59static CLIPBOARD: std::sync::OnceLock<std::sync::Mutex<Option<arboard::Clipboard>>> =
76 std::sync::OnceLock::new();
77
78pub(crate) fn with_clipboard<T>(
81 f: impl FnOnce(&mut arboard::Clipboard) -> Result<T, arboard::Error>,
82) -> Result<T, arboard::Error> {
83 let cell = CLIPBOARD.get_or_init(|| std::sync::Mutex::new(None));
84 let mut guard = cell.lock().unwrap_or_else(|e| {
87 let mut g = e.into_inner();
88 *g = None;
89 g
90 });
91 if guard.is_none() {
92 *guard = Some(arboard::Clipboard::new()?);
93 }
94 let result = f(guard.as_mut().expect("just opened"));
95 if matches!(&result, Err(e) if !matches!(e, arboard::Error::ContentNotAvailable)) {
103 *guard = None;
104 }
105 result
106}
107
108pub fn yank(text: String, done_msg: impl Into<String>, tx: &AppTx) {
113 let msg = match with_clipboard(|c| c.set_text(text)) {
114 Ok(()) => done_msg.into(),
115 Err(e) => format!("clipboard: {e}"),
116 };
117 tx.send(AppEvent::FlashMessage(msg)).ok();
118}
119
120pub fn yank_row(target: Option<search_list::YankTarget>, tx: &AppTx) {
128 match target {
129 Some(t) => yank(t.text, format!("{} copied", t.noun), tx),
130 None => {
131 tx.send(AppEvent::FlashMessage("nothing to copy".into()))
132 .ok();
133 }
134 }
135}
136
137pub fn fixed_centered_rect(width: u16, height: u16, r: Rect) -> Rect {
141 let width = width.min(r.width);
142 let height = height.min(r.height);
143 Rect {
144 x: r.x + (r.width - width) / 2,
145 y: r.y + (r.height - height) / 2,
146 width,
147 height,
148 }
149}
150
151pub fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
152 let popup_height = (area.height as u32 * percent_y as u32 / 100) as u16;
153 let popup_width = (area.width as u32 * percent_x as u32 / 100) as u16;
154 Rect {
155 x: area.x + (area.width.saturating_sub(popup_width)) / 2,
156 y: area.y + (area.height.saturating_sub(popup_height)) / 2,
157 width: popup_width,
158 height: popup_height,
159 }
160}
161
162pub trait Component {
163 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
166 let _ = (event, tx);
167 EventState::NotConsumed
168 }
169
170 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool);
171
172 fn hint_shortcuts(&self) -> Vec<(String, String)> {
175 vec![]
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn yank_flashes_outcome_on_tx() {
185 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
189 yank("hello".to_string(), "hello copied", &tx);
190 let ev = rx.try_recv().expect("yank sends exactly one event");
191 match ev {
192 AppEvent::FlashMessage(msg) => {
193 assert!(
194 msg == "hello copied" || msg.starts_with("clipboard: "),
195 "unexpected flash message: {msg}"
196 );
197 }
198 other => panic!("expected FlashMessage, got {other:?}"),
199 }
200 }
201
202 #[test]
203 fn centered_rect_is_centered() {
204 let area = Rect {
205 x: 0,
206 y: 0,
207 width: 100,
208 height: 40,
209 };
210 let r = centered_rect(80, 75, area);
211 assert_eq!(r.width, 80);
212 assert_eq!(r.height, 30);
213 assert_eq!(r.x, 10); assert_eq!(r.y, 5); }
216
217 #[test]
218 fn centered_rect_does_not_underflow() {
219 let area = Rect {
221 x: 0,
222 y: 0,
223 width: 5,
224 height: 5,
225 };
226 let _ = centered_rect(80, 75, area);
227 }
228}