1#[derive(Clone)]
2pub struct ModelEntry {
3 pub provider: String,
4 pub model: String,
5}
6
7pub struct ModelSelector {
8 pub visible: bool,
9 pub entries: Vec<ModelEntry>,
10 pub filtered: Vec<usize>,
11 pub selected: usize,
12 pub query: String,
13 pub current_provider: String,
14 pub current_model: String,
15 pub favorites: Vec<String>,
16}
17
18impl Default for ModelSelector {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl ModelSelector {
25 pub fn new() -> Self {
26 Self {
27 visible: false,
28 entries: Vec::new(),
29 filtered: Vec::new(),
30 selected: 0,
31 query: String::new(),
32 current_provider: String::new(),
33 current_model: String::new(),
34 favorites: Vec::new(),
35 }
36 }
37
38 pub fn open(
39 &mut self,
40 grouped: Vec<(String, Vec<String>)>,
41 current_provider: &str,
42 current_model: &str,
43 ) {
44 self.entries.clear();
45 for (provider, models) in grouped {
46 for model in models {
47 self.entries.push(ModelEntry {
48 provider: provider.clone(),
49 model,
50 });
51 }
52 }
53 self.current_provider = current_provider.to_string();
54 self.current_model = current_model.to_string();
55 self.query.clear();
56 self.visible = true;
57 self.apply_filter();
58 if let Some(pos) = self.filtered.iter().position(|&i| {
59 self.entries[i].provider == current_provider && self.entries[i].model == current_model
60 }) {
61 self.selected = pos;
62 }
63 }
64
65 pub fn toggle_favorite(&mut self) -> Option<String> {
66 let idx = *self.filtered.get(self.selected)?;
67 let model = self.entries[idx].model.clone();
68 if let Some(pos) = self.favorites.iter().position(|f| f == &model) {
69 self.favorites.remove(pos);
70 } else {
71 self.favorites.push(model.clone());
72 }
73 Some(model)
74 }
75
76 pub fn apply_filter(&mut self) {
77 let q = self.query.to_lowercase();
78 self.filtered = self
79 .entries
80 .iter()
81 .enumerate()
82 .filter(|(_, e)| {
83 if q.is_empty() {
84 return true;
85 }
86 e.model.to_lowercase().contains(&q) || e.provider.to_lowercase().contains(&q)
87 })
88 .map(|(i, _)| i)
89 .collect();
90 self.filtered.sort_by(|&a, &b| {
91 let a_fav = self.favorites.contains(&self.entries[a].model);
92 let b_fav = self.favorites.contains(&self.entries[b].model);
93 b_fav.cmp(&a_fav)
94 });
95 if self.selected >= self.filtered.len() {
96 self.selected = self.filtered.len().saturating_sub(1);
97 }
98 }
99
100 pub fn close(&mut self) {
101 self.visible = false;
102 self.query.clear();
103 }
104
105 pub fn up(&mut self) {
106 if self.selected > 0 {
107 self.selected -= 1;
108 }
109 }
110
111 pub fn down(&mut self) {
112 if self.selected + 1 < self.filtered.len() {
113 self.selected += 1;
114 }
115 }
116
117 pub fn confirm(&mut self) -> Option<ModelEntry> {
118 if self.visible && !self.filtered.is_empty() {
119 self.visible = false;
120 let entry = self.entries[self.filtered[self.selected]].clone();
121 self.query.clear();
122 Some(entry)
123 } else {
124 None
125 }
126 }
127}
128
129#[derive(Clone)]
130pub struct AgentEntry {
131 pub name: String,
132 pub description: String,
133}
134
135pub struct AgentSelector {
136 pub visible: bool,
137 pub entries: Vec<AgentEntry>,
138 pub selected: usize,
139 pub current: String,
140}
141
142impl Default for AgentSelector {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148impl AgentSelector {
149 pub fn new() -> Self {
150 Self {
151 visible: false,
152 entries: Vec::new(),
153 selected: 0,
154 current: String::new(),
155 }
156 }
157
158 pub fn open(&mut self, agents: Vec<AgentEntry>, current: &str) {
159 self.entries = agents;
160 self.current = current.to_string();
161 self.visible = true;
162 self.selected = self
163 .entries
164 .iter()
165 .position(|e| e.name == current)
166 .unwrap_or(0);
167 }
168
169 pub fn close(&mut self) {
170 self.visible = false;
171 }
172
173 pub fn up(&mut self) {
174 if self.selected > 0 {
175 self.selected -= 1;
176 }
177 }
178
179 pub fn down(&mut self) {
180 if self.selected + 1 < self.entries.len() {
181 self.selected += 1;
182 }
183 }
184
185 pub fn confirm(&mut self) -> Option<AgentEntry> {
186 if self.visible && !self.entries.is_empty() {
187 self.visible = false;
188 Some(self.entries[self.selected].clone())
189 } else {
190 None
191 }
192 }
193}
194
195use chrono::{DateTime, Utc};
196
197pub struct SlashCommand {
198 pub name: &'static str,
199 pub aliases: &'static [&'static str],
200 pub description: &'static str,
201 pub shortcut: &'static str,
202}
203
204pub const COMMANDS: &[SlashCommand] = &[
205 SlashCommand {
206 name: "model",
207 aliases: &["m"],
208 description: "switch model",
209 shortcut: "",
210 },
211 SlashCommand {
212 name: "agent",
213 aliases: &["a"],
214 description: "switch agent profile",
215 shortcut: "Tab",
216 },
217 SlashCommand {
218 name: "clear",
219 aliases: &["cl"],
220 description: "clear conversation",
221 shortcut: "",
222 },
223 SlashCommand {
224 name: "help",
225 aliases: &["h"],
226 description: "show commands",
227 shortcut: "",
228 },
229 SlashCommand {
230 name: "thinking",
231 aliases: &["t", "think"],
232 description: "set thinking level",
233 shortcut: "^T",
234 },
235 SlashCommand {
236 name: "sessions",
237 aliases: &["s", "sess"],
238 description: "resume a previous session",
239 shortcut: "",
240 },
241 SlashCommand {
242 name: "new",
243 aliases: &["n"],
244 description: "start new conversation",
245 shortcut: "",
246 },
247 SlashCommand {
248 name: "rename",
249 aliases: &["r"],
250 description: "rename this session",
251 shortcut: "^R",
252 },
253 SlashCommand {
254 name: "export",
255 aliases: &["e"],
256 description: "export session to markdown",
257 shortcut: "",
258 },
259 SlashCommand {
260 name: "login",
261 aliases: &["l"],
262 description: "manage provider credentials",
263 shortcut: "",
264 },
265 SlashCommand {
266 name: "aside",
267 aliases: &["btw"],
268 description: "ask a quick side question",
269 shortcut: "",
270 },
271];
272#[derive(Debug, Clone, PartialEq)]
273pub enum PaletteEntryKind {
274 Command,
275 Skill,
276}
277
278#[derive(Debug, Clone)]
279pub struct PaletteEntry {
280 pub name: String,
281 pub description: String,
282 pub shortcut: String,
283 pub kind: PaletteEntryKind,
284}
285
286pub struct CommandPalette {
287 pub visible: bool,
288 pub selected: usize,
289 pub filtered: Vec<usize>,
290 pub entries: Vec<PaletteEntry>,
291}
292
293impl Default for CommandPalette {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299impl CommandPalette {
300 pub fn new() -> Self {
301 Self {
302 visible: false,
303 selected: 0,
304 filtered: Vec::new(),
305 entries: Vec::new(),
306 }
307 }
308
309 pub fn set_skills(&mut self, skills: &[(String, String)]) {
310 self.entries.clear();
311 for cmd in COMMANDS {
312 self.entries.push(PaletteEntry {
313 name: cmd.name.to_string(),
314 description: cmd.description.to_string(),
315 shortcut: cmd.shortcut.to_string(),
316 kind: PaletteEntryKind::Command,
317 });
318 }
319 for (name, desc) in skills {
320 self.entries.push(PaletteEntry {
321 name: name.clone(),
322 description: desc.clone(),
323 shortcut: String::new(),
324 kind: PaletteEntryKind::Skill,
325 });
326 }
327 }
328
329 pub fn add_custom_commands(&mut self, commands: &[(&str, &str)]) {
330 for (name, desc) in commands {
331 self.entries.push(PaletteEntry {
332 name: name.to_string(),
333 description: desc.to_string(),
334 shortcut: String::new(),
335 kind: PaletteEntryKind::Command,
336 });
337 }
338 }
339
340 pub fn update_filter(&mut self, input: &str) {
341 if self.entries.is_empty() {
342 for cmd in COMMANDS {
343 self.entries.push(PaletteEntry {
344 name: cmd.name.to_string(),
345 description: cmd.description.to_string(),
346 shortcut: cmd.shortcut.to_string(),
347 kind: PaletteEntryKind::Command,
348 });
349 }
350 }
351 let query = input.strip_prefix('/').unwrap_or(input).to_lowercase();
352 self.filtered = self
353 .entries
354 .iter()
355 .enumerate()
356 .filter(|(_, e)| {
357 if query.is_empty() {
358 return true;
359 }
360 e.name.to_lowercase().starts_with(&query)
361 || e.description.to_lowercase().contains(&query)
362 })
363 .map(|(i, _)| i)
364 .collect();
365 if self.selected >= self.filtered.len() {
366 self.selected = self.filtered.len().saturating_sub(1);
367 }
368 }
369
370 pub fn open(&mut self, input: &str) {
371 self.visible = true;
372 self.selected = 0;
373 self.update_filter(input);
374 }
375
376 pub fn close(&mut self) {
377 self.visible = false;
378 }
379
380 pub fn up(&mut self) {
381 if self.selected > 0 {
382 self.selected -= 1;
383 }
384 }
385
386 pub fn down(&mut self) {
387 if self.selected + 1 < self.filtered.len() {
388 self.selected += 1;
389 }
390 }
391
392 pub fn confirm(&mut self) -> Option<PaletteEntry> {
393 if self.visible && !self.filtered.is_empty() {
394 self.visible = false;
395 Some(self.entries[self.filtered[self.selected]].clone())
396 } else {
397 None
398 }
399 }
400}
401
402#[derive(Debug, Clone, Copy, PartialEq)]
403pub enum ThinkingLevel {
404 Off,
405 Low,
406 Medium,
407 High,
408}
409
410impl ThinkingLevel {
411 pub fn budget_tokens(self) -> u32 {
412 match self {
413 ThinkingLevel::Off => 0,
414 ThinkingLevel::Low => 1024,
415 ThinkingLevel::Medium => 8192,
416 ThinkingLevel::High => 32768,
417 }
418 }
419
420 pub fn label(self) -> &'static str {
421 match self {
422 ThinkingLevel::Off => "off",
423 ThinkingLevel::Low => "low",
424 ThinkingLevel::Medium => "medium",
425 ThinkingLevel::High => "high",
426 }
427 }
428
429 pub fn description(self) -> &'static str {
430 match self {
431 ThinkingLevel::Off => "no extended thinking",
432 ThinkingLevel::Low => "1k token budget",
433 ThinkingLevel::Medium => "8k token budget",
434 ThinkingLevel::High => "32k token budget",
435 }
436 }
437
438 pub fn all() -> &'static [ThinkingLevel] {
439 &[
440 ThinkingLevel::Off,
441 ThinkingLevel::Low,
442 ThinkingLevel::Medium,
443 ThinkingLevel::High,
444 ]
445 }
446
447 pub fn from_budget(budget: u32) -> Self {
448 match budget {
449 0 => ThinkingLevel::Off,
450 1..=4095 => ThinkingLevel::Low,
451 4096..=16383 => ThinkingLevel::Medium,
452 _ => ThinkingLevel::High,
453 }
454 }
455
456 pub fn next(self) -> Self {
457 let all = Self::all();
458 let idx = all.iter().position(|l| *l == self).unwrap_or(0);
459 all[(idx + 1) % all.len()]
460 }
461}
462
463pub struct ThinkingSelector {
464 pub visible: bool,
465 pub selected: usize,
466 pub current: ThinkingLevel,
467}
468
469impl Default for ThinkingSelector {
470 fn default() -> Self {
471 Self::new()
472 }
473}
474
475impl ThinkingSelector {
476 pub fn new() -> Self {
477 Self {
478 visible: false,
479 selected: 0,
480 current: ThinkingLevel::Off,
481 }
482 }
483
484 pub fn open(&mut self, current: ThinkingLevel) {
485 self.current = current;
486 self.selected = ThinkingLevel::all()
487 .iter()
488 .position(|l| *l == current)
489 .unwrap_or(0);
490 self.visible = true;
491 }
492
493 pub fn close(&mut self) {
494 self.visible = false;
495 }
496
497 pub fn up(&mut self) {
498 if self.selected > 0 {
499 self.selected -= 1;
500 }
501 }
502
503 pub fn down(&mut self) {
504 if self.selected + 1 < ThinkingLevel::all().len() {
505 self.selected += 1;
506 }
507 }
508
509 pub fn confirm(&mut self) -> Option<ThinkingLevel> {
510 if self.visible {
511 self.visible = false;
512 Some(ThinkingLevel::all()[self.selected])
513 } else {
514 None
515 }
516 }
517}
518
519#[derive(Clone)]
520pub struct SessionEntry {
521 pub id: String,
522 pub title: String,
523 pub subtitle: String,
524}
525
526pub struct SessionSelector {
527 pub visible: bool,
528 pub entries: Vec<SessionEntry>,
529 pub filtered: Vec<usize>,
530 pub selected: usize,
531 pub query: String,
532}
533
534impl Default for SessionSelector {
535 fn default() -> Self {
536 Self::new()
537 }
538}
539
540impl SessionSelector {
541 pub fn new() -> Self {
542 Self {
543 visible: false,
544 entries: Vec::new(),
545 filtered: Vec::new(),
546 selected: 0,
547 query: String::new(),
548 }
549 }
550
551 pub fn open(&mut self, entries: Vec<SessionEntry>) {
552 self.entries = entries;
553 self.query.clear();
554 self.visible = true;
555 self.selected = 0;
556 self.apply_filter();
557 }
558
559 pub fn apply_filter(&mut self) {
560 let q = self.query.to_lowercase();
561 self.filtered = self
562 .entries
563 .iter()
564 .enumerate()
565 .filter(|(_, e)| {
566 if q.is_empty() {
567 return true;
568 }
569 e.title.to_lowercase().contains(&q) || e.subtitle.to_lowercase().contains(&q)
570 })
571 .map(|(i, _)| i)
572 .collect();
573 if self.selected >= self.filtered.len() {
574 self.selected = self.filtered.len().saturating_sub(1);
575 }
576 }
577
578 pub fn close(&mut self) {
579 self.visible = false;
580 self.query.clear();
581 }
582
583 pub fn up(&mut self) {
584 if self.selected > 0 {
585 self.selected -= 1;
586 }
587 }
588
589 pub fn down(&mut self) {
590 if self.selected + 1 < self.filtered.len() {
591 self.selected += 1;
592 }
593 }
594
595 pub fn confirm(&mut self) -> Option<String> {
596 if self.visible && !self.filtered.is_empty() {
597 self.visible = false;
598 let id = self.entries[self.filtered[self.selected]].id.clone();
599 self.query.clear();
600 Some(id)
601 } else {
602 None
603 }
604 }
605}
606
607pub struct HelpPopup {
608 pub visible: bool,
609}
610
611impl Default for HelpPopup {
612 fn default() -> Self {
613 Self::new()
614 }
615}
616
617impl HelpPopup {
618 pub fn new() -> Self {
619 Self { visible: false }
620 }
621
622 pub fn open(&mut self) {
623 self.visible = true;
624 }
625
626 pub fn close(&mut self) {
627 self.visible = false;
628 }
629}
630
631pub fn time_ago(iso: &str) -> String {
632 if let Ok(dt) = iso.parse::<DateTime<Utc>>() {
633 let secs = Utc::now().signed_duration_since(dt).num_seconds();
634 if secs < 60 {
635 return "just now".to_string();
636 }
637 if secs < 3600 {
638 return format!("{}m ago", secs / 60);
639 }
640 if secs < 86400 {
641 return format!("{}h ago", secs / 3600);
642 }
643 if secs < 604800 {
644 return format!("{}d ago", secs / 86400);
645 }
646 return format!("{}w ago", secs / 604800);
647 }
648 iso.to_string()
649}
650
651#[derive(Debug, Clone, Copy, PartialEq)]
652pub enum LoginStep {
653 SelectProvider,
654 SelectMethod,
655 EnterApiKey,
656 OAuthWaiting,
657 OAuthExchanging,
658}
659
660pub struct LoginPopup {
661 pub visible: bool,
662 pub step: LoginStep,
663 pub selected: usize,
664 pub provider: Option<String>,
665 pub key_input: String,
666 pub status: Option<String>,
667 pub oauth_url: Option<String>,
668 pub oauth_verifier: Option<String>,
669 pub oauth_create_key: bool,
670 pub code_input: String,
671 pub from_welcome: bool,
672}
673
674impl Default for LoginPopup {
675 fn default() -> Self {
676 Self::new()
677 }
678}
679
680impl LoginPopup {
681 pub fn new() -> Self {
682 Self {
683 visible: false,
684 step: LoginStep::SelectProvider,
685 selected: 0,
686 provider: None,
687 key_input: String::new(),
688 status: None,
689 oauth_url: None,
690 oauth_verifier: None,
691 oauth_create_key: false,
692 code_input: String::new(),
693 from_welcome: false,
694 }
695 }
696
697 pub fn open(&mut self) {
698 self.visible = true;
699 self.step = LoginStep::SelectProvider;
700 self.selected = 0;
701 self.provider = None;
702 self.key_input.clear();
703 self.code_input.clear();
704 self.status = None;
705 self.oauth_url = None;
706 self.oauth_verifier = None;
707 self.oauth_create_key = false;
708 self.from_welcome = false;
709 }
710
711 pub fn close(&mut self) {
712 self.visible = false;
713 self.key_input.clear();
714 self.code_input.clear();
715 self.status = None;
716 self.oauth_url = None;
717 self.oauth_verifier = None;
718 }
719
720 pub fn providers() -> &'static [&'static str] {
721 &["Anthropic", "OpenAI", "GitHub Copilot"]
722 }
723
724 pub fn anthropic_methods() -> &'static [&'static str] {
725 &[
726 "Claude Pro/Max (OAuth)",
727 "Create API Key (OAuth)",
728 "Enter API Key",
729 ]
730 }
731
732 pub fn up(&mut self) {
733 if self.selected > 0 {
734 self.selected -= 1;
735 }
736 }
737
738 pub fn down(&mut self) {
739 let max = match self.step {
740 LoginStep::SelectProvider => Self::providers().len(),
741 LoginStep::SelectMethod => Self::anthropic_methods().len(),
742 LoginStep::EnterApiKey | LoginStep::OAuthWaiting | LoginStep::OAuthExchanging => 0,
743 };
744 if max > 0 && self.selected + 1 < max {
745 self.selected += 1;
746 }
747 }
748}
749
750#[derive(Debug, Clone, Copy, PartialEq)]
751pub enum WelcomeChoice {
752 Login,
753 UseEnvKeys,
754 SetEnvVars,
755}
756
757pub struct WelcomeScreen {
758 pub visible: bool,
759 pub selected: usize,
760}
761
762impl Default for WelcomeScreen {
763 fn default() -> Self {
764 Self::new()
765 }
766}
767
768impl WelcomeScreen {
769 pub fn new() -> Self {
770 Self {
771 visible: false,
772 selected: 0,
773 }
774 }
775
776 pub fn open(&mut self) {
777 self.visible = true;
778 self.selected = 0;
779 }
780
781 pub fn close(&mut self) {
782 self.visible = false;
783 }
784
785 pub fn choices() -> &'static [(&'static str, &'static str)] {
786 &[
787 ("Login", "OAuth or API key"),
788 ("Use env keys", "ANTHROPIC_API_KEY / OPENAI_API_KEY"),
789 ("Set env variables", "configure keys in your shell"),
790 ]
791 }
792
793 pub fn up(&mut self) {
794 if self.selected > 0 {
795 self.selected -= 1;
796 }
797 }
798
799 pub fn down(&mut self) {
800 if self.selected + 1 < Self::choices().len() {
801 self.selected += 1;
802 }
803 }
804
805 pub fn confirm(&mut self) -> Option<WelcomeChoice> {
806 if !self.visible {
807 return None;
808 }
809 self.visible = false;
810 match self.selected {
811 0 => Some(WelcomeChoice::Login),
812 1 => Some(WelcomeChoice::UseEnvKeys),
813 2 => Some(WelcomeChoice::SetEnvVars),
814 _ => None,
815 }
816 }
817}
818
819pub struct MessageContextMenu {
820 pub visible: bool,
821 pub message_index: usize,
822 pub selected: usize,
823 pub screen_x: u16,
824 pub screen_y: u16,
825}
826
827impl Default for MessageContextMenu {
828 fn default() -> Self {
829 Self::new()
830 }
831}
832
833impl MessageContextMenu {
834 pub fn new() -> Self {
835 Self {
836 visible: false,
837 message_index: 0,
838 selected: 0,
839 screen_x: 0,
840 screen_y: 0,
841 }
842 }
843
844 pub fn open(&mut self, message_index: usize, x: u16, y: u16) {
845 self.visible = true;
846 self.message_index = message_index;
847 self.selected = 0;
848 self.screen_x = x;
849 self.screen_y = y;
850 }
851
852 pub fn close(&mut self) {
853 self.visible = false;
854 }
855
856 pub fn up(&mut self) {
857 if self.selected > 0 {
858 self.selected -= 1;
859 }
860 }
861
862 pub fn down(&mut self) {
863 if self.selected < Self::labels().len() - 1 {
864 self.selected += 1;
865 }
866 }
867
868 pub fn confirm(&mut self) -> Option<(usize, usize)> {
869 if self.visible {
870 self.visible = false;
871 Some((self.selected, self.message_index))
872 } else {
873 None
874 }
875 }
876
877 pub fn labels() -> &'static [&'static str] {
878 &["revert to message", "fork from here", "copy"]
879 }
880}
881
882#[derive(Clone)]
883pub struct FilePickerEntry {
884 pub name: String,
885 pub path: String,
886 pub is_dir: bool,
887}
888
889pub struct FilePicker {
890 pub visible: bool,
891 pub entries: Vec<FilePickerEntry>,
892 pub filtered: Vec<usize>,
893 pub selected: usize,
894 pub query: String,
895 pub at_pos: usize,
896 base_dir: String,
897}
898
899impl Default for FilePicker {
900 fn default() -> Self {
901 Self::new()
902 }
903}
904
905impl FilePicker {
906 pub fn new() -> Self {
907 Self {
908 visible: false,
909 entries: Vec::new(),
910 filtered: Vec::new(),
911 selected: 0,
912 query: String::new(),
913 at_pos: 0,
914 base_dir: String::new(),
915 }
916 }
917
918 pub fn open(&mut self, at_pos: usize) {
919 self.visible = true;
920 self.at_pos = at_pos;
921 self.query.clear();
922 self.selected = 0;
923 self.base_dir.clear();
924 self.populate();
925 }
926
927 pub fn close(&mut self) {
928 self.visible = false;
929 self.query.clear();
930 self.entries.clear();
931 self.filtered.clear();
932 }
933
934 pub fn populate(&mut self) {
935 let (dir, _) = self.dir_and_filter();
936 self.base_dir = dir.clone();
937 self.entries.clear();
938
939 let read_path = if dir.is_empty() {
940 ".".to_string()
941 } else {
942 dir.clone()
943 };
944 let Ok(rd) = std::fs::read_dir(&read_path) else {
945 return;
946 };
947
948 let mut dirs = Vec::new();
949 let mut files = Vec::new();
950
951 for entry in rd.flatten() {
952 let name = entry.file_name().to_string_lossy().to_string();
953 if name.starts_with('.') {
954 continue;
955 }
956 let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
957 let rel = if dir.is_empty() {
958 name.clone()
959 } else {
960 format!("{}{}", dir, name)
961 };
962 let e = FilePickerEntry {
963 name,
964 path: rel,
965 is_dir,
966 };
967 if is_dir {
968 dirs.push(e);
969 } else {
970 files.push(e);
971 }
972 }
973
974 dirs.sort_by(|a, b| a.name.cmp(&b.name));
975 files.sort_by(|a, b| a.name.cmp(&b.name));
976 self.entries.extend(dirs);
977 self.entries.extend(files);
978 self.apply_filter();
979 }
980
981 fn dir_and_filter(&self) -> (String, String) {
982 if let Some(pos) = self.query.rfind('/') {
983 (
984 self.query[..=pos].to_string(),
985 self.query[pos + 1..].to_string(),
986 )
987 } else {
988 (String::new(), self.query.clone())
989 }
990 }
991
992 pub fn apply_filter(&mut self) {
993 let (_, filter) = self.dir_and_filter();
994 let q = filter.to_lowercase();
995 self.filtered = self
996 .entries
997 .iter()
998 .enumerate()
999 .filter(|(_, e)| {
1000 if q.is_empty() {
1001 return true;
1002 }
1003 e.name.to_lowercase().starts_with(&q) || e.name.to_lowercase().contains(&q)
1004 })
1005 .map(|(i, _)| i)
1006 .collect();
1007 if self.selected >= self.filtered.len() {
1008 self.selected = self.filtered.len().saturating_sub(1);
1009 }
1010 }
1011
1012 pub fn update_query(&mut self, query: &str) {
1013 let (old_dir, _) = self.dir_and_filter();
1014 self.query = query.to_string();
1015 let (new_dir, _) = self.dir_and_filter();
1016 if new_dir != old_dir {
1017 self.populate();
1018 } else {
1019 self.apply_filter();
1020 }
1021 }
1022
1023 pub fn up(&mut self) {
1024 if self.selected > 0 {
1025 self.selected -= 1;
1026 }
1027 }
1028
1029 pub fn down(&mut self) {
1030 if self.selected + 1 < self.filtered.len() {
1031 self.selected += 1;
1032 }
1033 }
1034
1035 pub fn confirm(&mut self) -> Option<FilePickerEntry> {
1036 if self.visible && !self.filtered.is_empty() {
1037 self.visible = false;
1038 let entry = self.entries[self.filtered[self.selected]].clone();
1039 self.query.clear();
1040 Some(entry)
1041 } else {
1042 None
1043 }
1044 }
1045}
1046
1047pub struct AsidePopup {
1048 pub visible: bool,
1049 pub question: String,
1050 pub response: String,
1051 pub done: bool,
1052 pub scroll_offset: u16,
1053}
1054
1055impl Default for AsidePopup {
1056 fn default() -> Self {
1057 Self::new()
1058 }
1059}
1060
1061impl AsidePopup {
1062 pub fn new() -> Self {
1063 Self {
1064 visible: false,
1065 question: String::new(),
1066 response: String::new(),
1067 done: false,
1068 scroll_offset: 0,
1069 }
1070 }
1071
1072 pub fn open(&mut self, question: String) {
1073 self.visible = true;
1074 self.question = question;
1075 self.response.clear();
1076 self.done = false;
1077 self.scroll_offset = 0;
1078 }
1079
1080 pub fn close(&mut self) {
1081 self.visible = false;
1082 self.question.clear();
1083 self.response.clear();
1084 self.done = false;
1085 self.scroll_offset = 0;
1086 }
1087
1088 pub fn scroll_up(&mut self) {
1089 self.scroll_offset = self.scroll_offset.saturating_sub(1);
1090 }
1091
1092 pub fn scroll_down(&mut self) {
1093 self.scroll_offset = self.scroll_offset.saturating_add(1);
1094 }
1095}