Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:32:52.245Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

picker_prompt.rs

241 lines · 7.1 KB · rust
1use futures::channel::oneshot;
2use fuzzy::{StringMatch, StringMatchCandidate};
3
4use core::cmp;
5use gpui::{
6    App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
7    IntoElement, ParentElement, Render, SharedString, Subscription, Task, WeakEntity, Window, rems,
8};
9use picker::{Picker, PickerDelegate};
10use std::sync::Arc;
11use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
12use util::ResultExt;
13use workspace::{ModalView, Workspace};
14
15pub struct PickerPrompt {
16    pub picker: Entity<Picker<PickerPromptDelegate>>,
17    _subscription: Subscription,
18}
19
20pub fn prompt(
21    prompt: &str,
22    options: Vec<SharedString>,
23    workspace: WeakEntity<Workspace>,
24    window: &mut Window,
25    cx: &mut App,
26) -> Task<Option<usize>> {
27    if options.is_empty() {
28        return Task::ready(None);
29    } else if options.len() == 1 {
30        return Task::ready(Some(0));
31    }
32    let prompt = prompt.to_string().into();
33
34    window.spawn(cx, async move |cx| {
35        // Modal branch picker has a longer trailoff than a popover one.
36        let (tx, rx) = oneshot::channel();
37        let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
38
39        workspace
40            .update_in(cx, |workspace, window, cx| {
41                workspace.toggle_modal(window, cx, |window, cx| {
42                    PickerPrompt::new(delegate, 34., window, cx)
43                })
44            })
45            .ok();
46
47        (rx.await).ok()
48    })
49}
50
51impl PickerPrompt {
52    fn new(
53        delegate: PickerPromptDelegate,
54        rem_width: f32,
55        window: &mut Window,
56        cx: &mut Context<Self>,
57    ) -> Self {
58        let picker =
59            cx.new(|cx| Picker::uniform_list(delegate, window, cx).initial_width(rems(rem_width)));
60        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
61        Self {
62            picker,
63            _subscription,
64        }
65    }
66}
67impl ModalView for PickerPrompt {}
68impl EventEmitter<DismissEvent> for PickerPrompt {}
69
70impl Focusable for PickerPrompt {
71    fn focus_handle(&self, cx: &App) -> FocusHandle {
72        self.picker.focus_handle(cx)
73    }
74}
75
76impl Render for PickerPrompt {
77    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
78        v_flex()
79            .child(self.picker.clone())
80            .on_mouse_down_out(cx.listener(|this, _, window, cx| {
81                this.picker.update(cx, |this, cx| {
82                    this.cancel(&Default::default(), window, cx);
83                })
84            }))
85    }
86}
87
88pub struct PickerPromptDelegate {
89    prompt: Arc<str>,
90    matches: Vec<StringMatch>,
91    all_options: Vec<SharedString>,
92    selected_index: usize,
93    max_match_length: usize,
94    tx: Option<oneshot::Sender<usize>>,
95}
96
97impl PickerPromptDelegate {
98    pub fn new(
99        prompt: Arc<str>,
100        options: Vec<SharedString>,
101        tx: oneshot::Sender<usize>,
102        max_chars: usize,
103    ) -> Self {
104        Self {
105            prompt,
106            all_options: options,
107            matches: vec![],
108            selected_index: 0,
109            max_match_length: max_chars,
110            tx: Some(tx),
111        }
112    }
113}
114
115impl PickerDelegate for PickerPromptDelegate {
116    type ListItem = ListItem;
117
118    fn name() -> &'static str {
119        "picker prompt"
120    }
121
122    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
123        self.prompt.clone()
124    }
125
126    fn match_count(&self) -> usize {
127        self.matches.len()
128    }
129
130    fn selected_index(&self) -> usize {
131        self.selected_index
132    }
133
134    fn set_selected_index(
135        &mut self,
136        ix: usize,
137        _window: &mut Window,
138        _: &mut Context<Picker<Self>>,
139    ) {
140        self.selected_index = ix;
141    }
142
143    fn update_matches(
144        &mut self,
145        query: String,
146        window: &mut Window,
147        cx: &mut Context<Picker<Self>>,
148    ) -> Task<()> {
149        cx.spawn_in(window, async move |picker, cx| {
150            let candidates = picker.read_with(cx, |picker, _| {
151                picker
152                    .delegate
153                    .all_options
154                    .iter()
155                    .enumerate()
156                    .map(|(ix, option)| StringMatchCandidate::new(ix, option))
157                    .collect::<Vec<StringMatchCandidate>>()
158            });
159            let Some(candidates) = candidates.log_err() else {
160                return;
161            };
162            let matches: Vec<StringMatch> = if query.is_empty() {
163                candidates
164                    .into_iter()
165                    .enumerate()
166                    .map(|(index, candidate)| StringMatch {
167                        candidate_id: index,
168                        string: candidate.string,
169                        positions: Vec::new(),
170                        score: 0.0,
171                    })
172                    .collect()
173            } else {
174                fuzzy::match_strings(
175                    &candidates,
176                    &query,
177                    true,
178                    true,
179                    10000,
180                    &Default::default(),
181                    cx.background_executor().clone(),
182                )
183                .await
184            };
185            picker
186                .update(cx, |picker, _| {
187                    let delegate = &mut picker.delegate;
188                    delegate.matches = matches;
189                    if delegate.matches.is_empty() {
190                        delegate.selected_index = 0;
191                    } else {
192                        delegate.selected_index =
193                            cmp::min(delegate.selected_index, delegate.matches.len() - 1);
194                    }
195                })
196                .log_err();
197        })
198    }
199
200    fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
201        let Some(option) = self.matches.get(self.selected_index()) else {
202            return;
203        };
204
205        self.tx.take().map(|tx| tx.send(option.candidate_id));
206        cx.emit(DismissEvent);
207    }
208
209    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
210        cx.emit(DismissEvent);
211    }
212
213    fn render_match(
214        &self,
215        ix: usize,
216        selected: bool,
217        _window: &mut Window,
218        _cx: &mut Context<Picker<Self>>,
219    ) -> Option<Self::ListItem> {
220        let hit = &self.matches.get(ix)?;
221        let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
222
223        Some(
224            ListItem::new(format!("picker-prompt-menu-{ix}"))
225                .inset(true)
226                .spacing(ListItemSpacing::Sparse)
227                .toggle_state(selected)
228                .map(|el| {
229                    let highlights: Vec<_> = hit
230                        .positions
231                        .iter()
232                        .filter(|&&index| index < self.max_match_length)
233                        .copied()
234                        .collect();
235
236                    el.child(HighlightedLabel::new(shortened_option, highlights))
237                }),
238        )
239    }
240}
241
Served at tenant.openagents/omega Member data and write actions are omitted.