Skip to main content

gitu/
app.rs

1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::collections::BTreeMap;
4use std::ffi::OsStr;
5use std::io::Read;
6use std::io::Write;
7use std::ops::DerefMut;
8use std::process::Child;
9use std::process::Command;
10use std::process::Stdio;
11use std::rc::Rc;
12use std::sync::Arc;
13use std::sync::RwLock;
14use std::time::Duration;
15
16use arboard::Clipboard;
17use crossterm::event;
18use crossterm::event::Event;
19use crossterm::event::KeyCode;
20use crossterm::event::KeyModifiers;
21use crossterm::event::MouseButton;
22use crossterm::event::MouseEvent;
23use crossterm::event::MouseEventKind;
24use git2::Repository;
25
26use crate::cli;
27use crate::cmd_log::CmdLog;
28use crate::cmd_log::CmdLogEntry;
29use crate::config::Config;
30use crate::error::Error;
31use crate::file_watcher::FileWatcher;
32use crate::item_data::Rev;
33use crate::items::RenderParams;
34use crate::menu::Menu;
35use crate::menu::PendingMenu;
36use crate::ops::Op;
37use crate::picker::PickerData;
38use crate::picker::PickerState;
39use crate::picker::{Picked, PickerStatus};
40use crate::prompt;
41use crate::rebase_todo::RebaseTodo;
42use crate::screen;
43use crate::screen::Screen;
44use crate::screen::pager::Paged;
45use crate::term::Term;
46use crate::text_input;
47use crate::ui;
48
49use super::Res;
50
51pub(crate) struct State {
52    pub repo: Rc<Repository>,
53    pub config: Arc<Config>,
54    pub pending_keys: Vec<(KeyModifiers, KeyCode)>,
55    pub quit: bool,
56    /// What gitu exits with. Non-zero only to tell git, which is waiting on
57    /// gitu as its sequence editor, to call the rebase off: it is until the
58    /// list is accepted.
59    pub exit_code: i32,
60    pub screens: Vec<Screen>,
61    pub pending_menu: Option<PendingMenu>,
62    pending_cmd: Option<(Child, Arc<RwLock<CmdLogEntry>>)>,
63    enable_async_cmds: bool,
64    pub debug: bool,
65    pub current_cmd_log: CmdLog,
66    pub prompt: prompt::Prompt,
67    pub picker: Option<PickerState>,
68    /// The commit `select_commit` named, read by [`App::pick_commit`].
69    pub picked_commit: Option<String>,
70    /// Renderer features chosen in-session, overlaying the user's own
71    /// configuration. Never written to disk: quitting returns them to it.
72    pub features: Rc<[String]>,
73    /// The theme overlaid after `features`, as configured and as chosen since.
74    pub theme: Option<String>,
75    /// How much of a file to ask git for around each change (`-U8`, `-W`),
76    /// chosen in-session. `None` is git's own default.
77    pub context: Option<Rc<str>>,
78    /// The keys that toggle renderer features, as configured and as set since.
79    pub feature_keys: BTreeMap<String, char>,
80    pub clipboard: Option<Clipboard>,
81    /// Whether ops that write are hidden and refused, and nothing chosen
82    /// in-session is saved ([`crate::config::ReadOnly`]).
83    pub read_only: bool,
84    needs_redraw: bool,
85    file_watcher: Option<FileWatcher>,
86    inhibit_close_menu: bool,
87}
88
89pub struct App {
90    pub(crate) state: State,
91}
92
93impl App {
94    pub fn create(
95        repo: Rc<Repository>,
96        size: (u16, u16),
97        args: &cli::Args,
98        config: Arc<Config>,
99        enable_async_cmds: bool,
100        piped: Option<Paged>,
101    ) -> Res<Self> {
102        let params = RenderParams {
103            size,
104            features: overlay(&[], config.general.diff_renderer.theme.as_deref()),
105            context: None,
106            streaming: enable_async_cmds,
107        };
108        let screens = match args.command {
109            // `--pager`: git's output is already in hand, with the command it ran.
110            _ if let Some(paged) = piped => {
111                vec![screen::pager::create(
112                    Arc::clone(&config),
113                    Rc::clone(&repo),
114                    params,
115                    paged,
116                )?]
117            }
118            Some(cli::Commands::Show { ref reference }) => {
119                vec![screen::pager::show(
120                    Arc::clone(&config),
121                    Rc::clone(&repo),
122                    params.clone(),
123                    reference,
124                )?]
125            }
126            Some(cli::Commands::Blame { ref rev, ref file }) => {
127                vec![screen::blame::create(
128                    Arc::clone(&config),
129                    Rc::clone(&repo),
130                    params.clone(),
131                    file.clone(),
132                    rev.clone(),
133                    None,
134                )?]
135            }
136            Some(cli::Commands::Rebase { ref upstream }) => {
137                let todo = RebaseTodo::capture(&repo, OsStr::new(upstream), &[])?;
138                vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
139            }
140            Some(cli::Commands::SequenceEditor { ref file }) => {
141                let todo = RebaseTodo::read(&repo, file)?;
142                vec![rebase_todo_screen(&config, &repo, params.clone(), todo)?]
143            }
144            None => vec![screen::status::create(
145                Arc::clone(&config),
146                Rc::clone(&repo),
147                params,
148            )?],
149        };
150
151        let clipboard = Clipboard::new()
152            .inspect_err(|e| log::warn!("Couldn't initialize clipboard: {e}"))
153            .ok();
154        let read_only = config.general.read_only.is_on(&repo);
155        let prompt = prompt::Prompt::with_history(repo.path().join("gitu"), read_only);
156
157        let mut app = Self {
158            state: State {
159                repo,
160                feature_keys: config.general.diff_renderer.feature_keys.clone(),
161                theme: config.general.diff_renderer.theme.clone(),
162                config,
163                pending_keys: vec![],
164                enable_async_cmds,
165                debug: args.debug,
166                quit: false,
167                exit_code: match args.command {
168                    Some(cli::Commands::SequenceEditor { .. }) => 1,
169                    _ => 0,
170                },
171                screens,
172                pending_cmd: None,
173                pending_menu: None,
174                current_cmd_log: CmdLog::new(),
175                prompt,
176                picker: None,
177                picked_commit: None,
178                features: Rc::from([]),
179                context: None,
180                clipboard,
181                read_only,
182                file_watcher: None,
183                needs_redraw: true,
184                inhibit_close_menu: false,
185            },
186        };
187
188        // The menu a screen imposes is in force from the first key on, not from
189        // the first key that closes a menu.
190        app.close_menu();
191        app.state.file_watcher = app.init_file_watcher()?;
192        Ok(app)
193    }
194
195    fn init_file_watcher(&mut self) -> Res<Option<FileWatcher>> {
196        if !self.state.config.general.refresh_on_file_change.enabled {
197            return Ok(None);
198        }
199
200        let hiding_untracked_files = !self
201            .state
202            .repo
203            .config()
204            .map_err(Error::ReadGitConfig)?
205            .get_bool("status.showUntrackedFiles")
206            .unwrap_or(true);
207
208        if hiding_untracked_files {
209            self.display_info("File watcher disabled (status.showUntrackedFiles is off)");
210
211            return Ok(None);
212        }
213
214        Ok(
215            FileWatcher::new(self.state.repo.workdir().expect("Bare repos unhandled"))
216                .inspect_err(|err| {
217                    self.display_error(err.to_string());
218                    self.display_info("File watcher disabled");
219                })
220                .ok(),
221        )
222    }
223
224    pub fn run(&mut self, term: &mut Term, max_tick_delay: Duration) -> Res<()> {
225        while !self.state.quit {
226            let event = if term.poll_event(max_tick_delay)? {
227                Some(term.read_event()?)
228            } else if term.hung_up() {
229                return Ok(());
230            } else {
231                None
232            };
233
234            self.update(term, event)?;
235        }
236
237        Ok(())
238    }
239
240    pub fn update(&mut self, term: &mut Term, event: Option<Event>) -> Res<()> {
241        if let Some(e) = event {
242            self.handle_event(term, e)?;
243        }
244
245        if let Some(file_watcher) = &mut self.state.file_watcher
246            && file_watcher.pending_updates()
247        {
248            self.update_screens()?;
249        }
250
251        let finished = self.finish_screens();
252        self.handle_result(finished)?;
253
254        let handle_pending_cmd_result = self.handle_pending_cmd();
255        self.handle_result(handle_pending_cmd_result)?;
256
257        if self.state.needs_redraw {
258            self.redraw_now(term)?;
259        }
260
261        Ok(())
262    }
263
264    /// Give each screen whose rows were still being drawn all of them, once
265    /// they are.
266    fn finish_screens(&mut self) -> Res<()> {
267        let mut finished = false;
268        for screen in &mut self.state.screens {
269            finished |= screen.finish()?;
270        }
271        if finished {
272            self.stage_redraw();
273        }
274        Ok(())
275    }
276
277    pub fn update_screens(&mut self) -> Res<()> {
278        self.rebuild_screens(Screen::refresh)
279    }
280
281    /// Rebuild every screen, leaving each cursor where it was in the diff rather
282    /// than on the line number it happened to occupy. For a re-render of the
283    /// same content (a renderer feature turned on or off), the rows move but the
284    /// place in the patch does not.
285    pub fn rerender_screens(&mut self) -> Res<()> {
286        self.rebuild_screens(Screen::refresh_keeping_position)
287    }
288
289    /// [`App::rerender_screens`] for the screen in view only, as a preview: the
290    /// screens under it keep what they were rendered with.
291    pub(crate) fn rerender_screen(&mut self) -> Res<()> {
292        let features = self.overlay();
293        let screen = self.state.screens.last_mut().unwrap();
294        screen.features = features;
295        screen.refresh_keeping_position()?;
296        self.stage_redraw();
297        Ok(())
298    }
299
300    /// The features to render with: those chosen in-session, then the theme.
301    fn overlay(&self) -> Rc<[String]> {
302        overlay(&self.state.features, self.state.theme.as_deref())
303    }
304
305    fn rebuild_screens(&mut self, rebuild: impl Fn(&mut Screen) -> Res<()>) -> Res<()> {
306        let features = self.overlay();
307        for screen in &mut self.state.screens {
308            screen.features = Rc::clone(&features);
309            screen.context = self.state.context.clone();
310            rebuild(screen)?;
311        }
312
313        self.stage_redraw();
314        Ok(())
315    }
316
317    pub fn handle_event(&mut self, term: &mut Term, event: Event) -> Res<()> {
318        match event {
319            Event::Resize(w, h) => {
320                for screen in self.state.screens.iter_mut() {
321                    screen.resize(w, h)?;
322                }
323
324                self.stage_redraw();
325                Ok(())
326            }
327            Event::Mouse(ref mouse) => {
328                if self.state.config.general.mouse_support
329                    && self.handle_mouse_input(term, mouse)?
330                {
331                    self.stage_redraw();
332                }
333                Ok(())
334            }
335            Event::Key(mut key) => {
336                if self.state.pending_cmd.is_none() {
337                    self.state.current_cmd_log.clear();
338                }
339
340                if self.state.pending_cmd.is_none() {
341                    self.state.current_cmd_log.clear();
342                }
343
344                // The key received changes as shift is pressed: '/' becomes '?'
345                // on a US keyboard, and tab becomes backtab. The modifier is
346                // redundant there, and on the other keys it is the only sign
347                // shift was held.
348                if matches!(key.code, KeyCode::Char(_) | KeyCode::BackTab) {
349                    key.modifiers = key.modifiers.difference(KeyModifiers::SHIFT);
350                }
351
352                if self.state.picker.is_some() {
353                    self.handle_picker_input(key)?;
354                } else if self.state.prompt.state.focused {
355                    self.state.prompt.handle_key(key);
356                } else {
357                    self.handle_key_input(term, key)?;
358                }
359
360                self.stage_redraw();
361                Ok(())
362            }
363            _ => Ok(()),
364        }
365    }
366
367    pub fn redraw_now(&mut self, term: &mut Term) -> Res<()> {
368        if self.state.screens.last_mut().is_some() {
369            ui::ui(term, &mut self.state)?;
370            self.state.needs_redraw = false;
371        };
372
373        Ok(())
374    }
375
376    /// Prints the screen, and says so, if it fits on the terminal with a row
377    /// to spare for the shell's prompt: less's `--quit-if-one-screen`. Waits
378    /// for the rest of a screen still being drawn while what there is fits.
379    pub fn print_if_one_screen(&mut self, term: &mut Term) -> Res<bool> {
380        let screen = self.state.screens.last_mut().unwrap();
381        let (_, rows) = term.size().map_err(Error::Term)?;
382        let rows = rows.saturating_sub(1).into();
383        loop {
384            if !screen.fits_in(rows) {
385                return Ok(false);
386            }
387            if !screen.is_drawing() {
388                break;
389            }
390            if !screen.finish()? {
391                std::thread::sleep(Duration::from_millis(10));
392            }
393        }
394
395        screen::print_screen(term, screen)?;
396        Ok(true)
397    }
398
399    pub fn stage_redraw(&mut self) {
400        self.state.needs_redraw = true;
401    }
402
403    fn handle_key_input(&mut self, term: &mut Term, key: event::KeyEvent) -> Res<()> {
404        if key.kind != event::KeyEventKind::Press {
405            return Ok(());
406        }
407
408        let menu = match &self.state.pending_menu {
409            None => self.base_menu().unwrap_or(Menu::Root),
410            Some(menu) if menu.menu == Menu::Help => Menu::Root,
411            Some(menu) => menu.menu,
412        };
413
414        self.state.pending_keys.push((key.modifiers, key.code));
415
416        let matching_bindings = self
417            .state
418            .config
419            .bindings
420            .match_bindings(&menu, &self.state.pending_keys)
421            .collect::<Vec<_>>();
422
423        match matching_bindings[..] {
424            [binding] => {
425                if binding.keys == self.state.pending_keys {
426                    let op = binding.op.clone();
427                    self.state.pending_keys.clear();
428                    self.handle_op(op, term)?;
429                }
430            }
431            [] => {
432                let feature = match self.state.pending_keys[..] {
433                    [(KeyModifiers::NONE, KeyCode::Char(key))] if menu == Menu::Root => {
434                        self.feature_with_key(key)
435                    }
436                    _ => None,
437                };
438                self.state.pending_keys.clear();
439                if let Some(feature) = feature {
440                    crate::ops::editor::toggle_feature(self, &feature)?;
441                }
442            }
443            [_, ..] => (),
444        }
445
446        Ok(())
447    }
448
449    fn handle_mouse_input(&mut self, term: &mut Term, mouse: &MouseEvent) -> Res<bool> {
450        match mouse.kind {
451            MouseEventKind::Down(MouseButton::Left) => {
452                let click_y = mouse.row as usize;
453                if self.screen().is_valid_screen_line(click_y) {
454                    let old_selected_item_id = self.screen().get_selected_item().id;
455                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
456                    let new_selected_item = self.screen().get_selected_item();
457
458                    if old_selected_item_id == new_selected_item.id {
459                        // If the item clicked was already the current item, then try to
460                        // toggle it if it's a section or show it.
461                        if new_selected_item.data.is_section() {
462                            self.handle_op(Op::ToggleSection, term)?;
463                        } else {
464                            self.handle_op(Op::Show, term)?;
465                        }
466                    }
467                }
468            }
469            MouseEventKind::Down(MouseButton::Right) => {
470                let click_y = mouse.row as usize;
471                if self.screen().is_valid_screen_line(click_y) {
472                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
473                    self.handle_op(Op::Show, term)?;
474                }
475            }
476            MouseEventKind::ScrollUp => {
477                let scroll_lines = self.state.config.general.mouse_scroll_lines;
478                if scroll_lines > 0 {
479                    self.screen_mut().scroll_view_up(scroll_lines);
480                }
481            }
482            MouseEventKind::ScrollDown => {
483                let scroll_lines = self.state.config.general.mouse_scroll_lines;
484                if scroll_lines > 0 {
485                    self.screen_mut().scroll_view_down(scroll_lines);
486                }
487            }
488            _ => return Ok(false),
489        }
490
491        Ok(true)
492    }
493
494    pub(crate) fn handle_op(&mut self, op: Op, term: &mut Term) -> Res<()> {
495        if self.state.read_only && op.writes() {
496            return Ok(());
497        }
498        let item_data = self.screen().selected_target();
499        let implementation = op.clone().implementation();
500
501        if let Some(mut action) = implementation.get_action(&item_data) {
502            let result = Rc::get_mut(&mut action).unwrap()(self, term);
503            self.handle_result(result)?;
504            if !self.state.inhibit_close_menu {
505                self.close_menu();
506            }
507            self.state.inhibit_close_menu = false;
508        }
509
510        Ok(())
511    }
512
513    fn handle_result<T>(&mut self, result: Res<T>) -> Res<()> {
514        match result {
515            Ok(_) => Ok(()),
516            Err(Error::NoMoreEvents) => Err(Error::NoMoreEvents),
517            Err(Error::PromptAborted) => Ok(()),
518            Err(error) => {
519                self.state
520                    .current_cmd_log
521                    .push(CmdLogEntry::Error(error.to_string()));
522
523                Ok(())
524            }
525        }
526    }
527
528    /// What a screen opened now should render with: the viewport it will get,
529    /// and the features currently selected.
530    pub(crate) fn render_params(&self, size: (u16, u16)) -> RenderParams {
531        RenderParams {
532            size,
533            features: self.overlay(),
534            context: self.state.context.clone(),
535            streaming: self.state.enable_async_cmds,
536        }
537    }
538
539    pub fn close_menu(&mut self) {
540        let hide = self.screen().menu.is_some() && !self.screen().show_menu;
541        self.state.pending_menu = self.base_menu().map(|menu| PendingMenu {
542            is_hidden: hide,
543            ..PendingMenu::init(menu)
544        });
545    }
546
547    /// The menu to fall back to once a pending one closes: a screen with its own
548    /// keymap (the interactive rebase todo) imposes it for as long as it is up.
549    pub(crate) fn base_menu(&self) -> Option<Menu> {
550        self.screen().menu.or_else(|| root_menu(&self.state.config))
551    }
552
553    pub fn inhibit_close_menu(&mut self) {
554        self.state.inhibit_close_menu = true;
555    }
556
557    pub(crate) fn screen_mut(&mut self) -> &mut Screen {
558        self.state.screens.last_mut().expect("No screen")
559    }
560
561    pub(crate) fn screen(&self) -> &Screen {
562        self.state.screens.last().expect("No screen")
563    }
564
565    /// Displays an `Info` message to the CmdLog.
566    pub fn display_info<S: Into<Cow<'static, str>>>(&mut self, message: S) {
567        self.state
568            .current_cmd_log
569            .push(CmdLogEntry::Info(message.into().into_owned()));
570    }
571
572    /// Displays an `Error` message to the CmdLog.
573    pub fn display_error<S: Into<Cow<'static, str>>>(&mut self, message: S) {
574        self.state
575            .current_cmd_log
576            .push(CmdLogEntry::Error(message.into().into_owned()));
577    }
578
579    /// Runs a `Command` and handles its output.
580    /// Will block awaiting its completion.
581    pub fn run_cmd(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
582        self.run_cmd_async(term, input, cmd)?;
583        self.await_pending_cmd()?;
584        self.handle_pending_cmd()?;
585        Ok(())
586    }
587
588    /// Runs a `Command` and handles its output asynchronously (if async commands are enabled).
589    /// Will return `Ok(())` if one is already running.
590    pub fn run_cmd_async(&mut self, term: &mut Term, input: &[u8], mut cmd: Command) -> Res<()> {
591        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
592
593        if self.state.pending_cmd.is_some() {
594            return Err(Error::CmdAlreadyRunning);
595        }
596
597        cmd.current_dir(self.state.repo.workdir().expect("No workdir"));
598
599        cmd.stdin(Stdio::piped());
600        cmd.stdout(Stdio::piped());
601        cmd.stderr(Stdio::piped());
602
603        let log_entry = self.state.current_cmd_log.push_cmd(&cmd);
604
605        ui::ui(term, &mut self.state)?;
606
607        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
608
609        use std::io::Write;
610        child
611            .stdin
612            .take()
613            .unwrap()
614            .write_all(input)
615            .map_err(Error::Term)?;
616
617        self.state.pending_cmd = Some((child, log_entry));
618
619        if !self.state.enable_async_cmds {
620            self.await_pending_cmd()?;
621        }
622
623        Ok(())
624    }
625
626    fn await_pending_cmd(&mut self) -> Res<()> {
627        if let Some((child, _)) = &mut self.state.pending_cmd {
628            child.wait().map_err(Error::CouldntAwaitCmd)?;
629        }
630        Ok(())
631    }
632
633    /// Handles any pending_cmd in State without blocking. Returns `true` if a cmd was handled.
634    fn handle_pending_cmd(&mut self) -> Res<()> {
635        let Some((ref mut child, ref mut log_rwlock)) = self.state.pending_cmd else {
636            return Ok(());
637        };
638
639        let Some(status) = child.try_wait().map_err(Error::CouldntAwaitCmd)? else {
640            return Ok(());
641        };
642
643        log::debug!("pending cmd finished with {status:?}");
644
645        let result = write_child_output_to_log(log_rwlock, child, status);
646        self.state.pending_cmd = None;
647        self.update_screens()?;
648        result?;
649
650        Ok(())
651    }
652
653    pub fn run_cmd_interactive(&mut self, term: &mut Term, mut cmd: Command) -> Res<()> {
654        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
655
656        if self.state.pending_cmd.is_some() {
657            return Err(Error::CmdAlreadyRunning);
658        }
659
660        cmd.current_dir(self.state.repo.workdir().ok_or(Error::NoRepoWorkdir)?);
661
662        self.state
663            .current_cmd_log
664            .push_cmd_with_output(&cmd, "\n".into());
665        self.redraw_now(term)?;
666
667        eprint!("\r");
668
669        // Redirect stderr so we can capture it via `Child::wait_with_output()`
670        cmd.stderr(Stdio::piped());
671
672        term.reset_term_stay_on_alt_screeen(&self.state.config)
673            .map_err(Error::Term)?;
674
675        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
676
677        // Drop stdin as `Child::wait_with_output` would
678        drop(child.stdin.take());
679
680        let mut stderr = Vec::new();
681
682        tee(
683            child.stderr.as_mut(),
684            &mut [&mut std::io::stderr(), &mut stderr],
685        )
686        .map_err(Error::Term)?;
687
688        let status = child.wait().map_err(Error::CouldntAwaitCmd)?;
689
690        term.setup_term(&self.state.config).map_err(Error::Term)?;
691
692        let out_utf8 = String::from_utf8(strip_ansi_escapes::strip(stderr.clone()))
693            .expect("Error turning command output to String")
694            .into();
695
696        self.state.current_cmd_log.clear();
697        self.state
698            .current_cmd_log
699            .push_cmd_with_output(&cmd, out_utf8);
700
701        term.clear().map_err(Error::Term)?;
702        self.update_screens()?;
703
704        if !status.success() {
705            return Err(Error::CmdBadExit(
706                format!(
707                    "{} {}",
708                    cmd.get_program().to_string_lossy(),
709                    cmd.get_args()
710                        .map(|arg| arg.to_string_lossy())
711                        .collect::<String>()
712                ),
713                status.code(),
714            ));
715        }
716
717        Ok(())
718    }
719
720    pub fn hide_menu(&mut self) {
721        if let Some(ref mut menu) = self.state.pending_menu {
722            menu.is_hidden = true;
723        }
724    }
725
726    fn unhide_menu(&mut self) {
727        if let Some(ref mut menu) = self.state.pending_menu {
728            menu.is_hidden = false;
729        }
730    }
731
732    pub(crate) fn selected_rev(&self) -> Option<Rev> {
733        self.screen().get_selected_item().data.rev()
734    }
735
736    pub(crate) fn prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
737        let default = (params.create_default_value)(self);
738
739        // A prompt with nothing to say says nothing: the key that opened it
740        // already said what is being answered. Prefilled, the default is the
741        // answer being edited rather than the one an empty answer falls back
742        // to — it is on the line, so naming it again says nothing either.
743        let prompt_text = match (params.prompt, &default) {
744            ("", _) => Cow::Borrowed(""),
745            (prompt, _) if params.bare => Cow::Borrowed(prompt),
746            (prompt, Some(default)) if !params.prefill => {
747                format!("{prompt} (default {default}):").into()
748            }
749            (prompt, _) => format!("{prompt}:").into(),
750        };
751
752        if params.hide_menu {
753            self.hide_menu();
754        }
755
756        self.state.prompt.set(prompt::PromptData {
757            prompt_text,
758            bare: params.bare,
759        });
760
761        if params.prefill
762            && let Some(default) = default
763        {
764            self.state.prompt.state.value = default;
765            self.state.prompt.state.move_to(usize::MAX);
766        }
767        self.state.prompt.start_history(params.history)?;
768
769        let result = self.handle_prompt(term, params);
770
771        self.unhide_menu();
772        self.state.prompt.reset();
773
774        result
775    }
776
777    fn handle_prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
778        self.redraw_now(term)?;
779
780        loop {
781            let event = term.read_event()?;
782            self.handle_event(term, event)?;
783
784            if self.state.prompt.state.status == text_input::Status::Done {
785                let value = get_prompt_result(params, self)?;
786                if let Some(kind) = params.history {
787                    self.state.prompt.remember(kind, &value)?;
788                }
789                return Ok(value);
790            } else if self.state.prompt.state.status == text_input::Status::Aborted {
791                return Err(Error::PromptAborted);
792            }
793
794            self.redraw_now(term)?;
795        }
796    }
797
798    pub fn confirm(&mut self, term: &mut Term, prompt: &'static str) -> Res<()> {
799        self.hide_menu();
800        self.state.prompt.set(prompt::PromptData {
801            prompt_text: prompt.into(),
802            bare: false,
803        });
804
805        let result = self.handle_confirm(term);
806
807        self.unhide_menu();
808        self.state.prompt.reset();
809
810        result
811    }
812
813    fn handle_confirm(&mut self, term: &mut Term) -> Res<()> {
814        self.redraw_now(term)?;
815        loop {
816            let event = term.read_event()?;
817            self.handle_event(term, event)?;
818
819            match self.state.prompt.state.value.as_str() {
820                "y" => {
821                    return Ok(());
822                }
823                "" => (),
824                _ => {
825                    return Err(Error::PromptAborted);
826                }
827            }
828
829            self.redraw_now(term)?;
830        }
831    }
832
833    /// Show a picker and wait for user to select an item or cancel.
834    ///
835    /// Returns:
836    /// - `Ok(Some(data))` - User selected an item
837    /// - `Ok(None)` - User cancelled (Esc or Ctrl-C)
838    /// - `Err(e)` - An error occurred
839    ///
840    /// # Example
841    /// ```ignore
842    /// let items = vec![
843    ///     PickerItem::new("main", PickerData::Item("main".to_string())),
844    ///     PickerItem::new("develop", PickerData::Item("develop".to_string())),
845    /// ];
846    /// let picker = PickerState::new("Select branch", items, false);
847    ///
848    /// match app.pick(term, picker)? {
849    ///     Some(PickerData::Item(name)) => {
850    ///         // User selected a branch
851    ///         println!("Selected: {}", name);
852    ///     }
853    ///     Some(PickerData::CustomInput(_)) => {
854    ///         // Should not happen when allow_custom_input is false
855    ///     }
856    ///     None => {
857    ///         // User cancelled
858    ///         println!("Cancelled");
859    ///     }
860    /// }
861    /// ```
862    pub fn pick(&mut self, term: &mut Term, picker_state: PickerState) -> Res<Option<PickerData>> {
863        Ok(self
864            .pick_or_set_key(term, picker_state)?
865            .map(|picked| match picked {
866                Picked::Item(data) => data,
867                Picked::KeySet(..) => unreachable!("only a picker assigning keys sets them"),
868            }))
869    }
870
871    /// [`App::pick`], for a picker whose items can be given keys.
872    pub(crate) fn pick_or_set_key(
873        &mut self,
874        term: &mut Term,
875        picker_state: PickerState,
876    ) -> Res<Option<Picked>> {
877        self.state.picker = Some(picker_state);
878        let result = self.handle_picker(term, &mut |_, _| Ok(()));
879
880        self.state.picker = None;
881
882        result
883    }
884
885    /// [`App::pick`], calling `preview` with each item the cursor stops on.
886    pub(crate) fn pick_previewing(
887        &mut self,
888        term: &mut Term,
889        picker_state: PickerState,
890        preview: &mut dyn FnMut(&mut App, &PickerData) -> Res<()>,
891    ) -> Res<Option<PickerData>> {
892        self.state.picker = Some(picker_state);
893        let result = self.handle_picker(term, preview);
894
895        self.state.picker = None;
896
897        Ok(result?.map(|picked| match picked {
898            Picked::Item(data) => data,
899            Picked::KeySet(..) => unreachable!("only a picker assigning keys sets them"),
900        }))
901    }
902
903    /// Choose a commit by moving around a log view: the screen is pushed with
904    /// its own keymap, and the loop runs until `select_commit` names one or the
905    /// screen is closed.
906    pub fn pick_commit(&mut self, term: &mut Term) -> Res<Option<String>> {
907        let params = self.render_params(self.screen().size);
908        let selected = match &self.screen().get_selected_item().data {
909            crate::item_data::ItemData::Commit { oid, .. } => Some(oid.clone()),
910            _ => None,
911        };
912        // The selected commit has to be found among the rows, so they are all
913        // drawn first.
914        let params = RenderParams {
915            streaming: params.streaming && selected.is_none(),
916            ..params
917        };
918        self.state.screens.push(screen::pager::log(
919            Arc::clone(&self.state.config),
920            Rc::clone(&self.state.repo),
921            params,
922            COMMIT_PICK_LIMIT,
923            None,
924            None,
925        )?);
926        self.screen_mut().menu = Some(Menu::CommitPicker);
927        self.close_menu();
928
929        // Start on the commit that was already selected, if there was one.
930        if let Some(selected) = selected {
931            self.screen_mut().select_matching(|data| {
932                matches!(data, crate::item_data::ItemData::Commit { oid, .. } if *oid == selected)
933            });
934        }
935
936        let depth = self.state.screens.len();
937        self.redraw_now(term)?;
938
939        loop {
940            let event = term.read_event()?;
941            self.handle_event(term, event)?;
942
943            if let Some(oid) = self.state.picked_commit.take() {
944                self.state.screens.truncate(depth - 1);
945                self.close_menu();
946                return Ok(Some(oid));
947            }
948            if self.state.screens.len() < depth {
949                return Ok(None); // The screen was closed: nothing picked.
950            }
951
952            self.redraw_now(term)?;
953        }
954    }
955
956    fn handle_picker(
957        &mut self,
958        term: &mut Term,
959        preview: &mut dyn FnMut(&mut App, &PickerData) -> Res<()>,
960    ) -> Res<Option<Picked>> {
961        self.redraw_now(term)?;
962        let selected = |app: &App| {
963            app.state
964                .picker
965                .as_ref()
966                .and_then(|picker| picker.selected())
967                .map(|item| item.data.clone())
968        };
969        let mut previewed = selected(self);
970
971        loop {
972            let event = term.read_event()?;
973            self.handle_event(term, event)?;
974
975            if let Some(ref picker) = self.state.picker {
976                let selected = || picker.selected().map(|item| item.data.clone());
977                match *picker.status() {
978                    PickerStatus::Done => return Ok(selected().map(Picked::Item)),
979                    PickerStatus::KeySet(key) => {
980                        return Ok(selected().map(|data| Picked::KeySet(key, data)));
981                    }
982                    // User cancelled - this is not an error
983                    PickerStatus::Cancelled => return Ok(None),
984                    PickerStatus::Active => (),
985                }
986            }
987
988            // Only once the keys that came together are handled, so that an
989            // arrow held down does not preview every item it passes.
990            let now = selected(self);
991            if now != previewed && !matches!(term.poll_event(Duration::ZERO), Ok(true)) {
992                if let Some(data) = &now {
993                    preview(self, data)?;
994                }
995                previewed = now;
996            }
997
998            self.redraw_now(term)?;
999        }
1000    }
1001
1002    /// The feature that `key` toggles in the picker's one row, if any.
1003    fn feature_with_key_in_one_row(&self, key: event::KeyEvent) -> Option<String> {
1004        let picker = self.state.picker.as_ref()?;
1005        let quit = &self.state.config.picker_bindings.quit;
1006        match (key.code, key.modifiers) {
1007            (event::KeyCode::Char(c), event::KeyModifiers::NONE)
1008                if picker.is_one_row()
1009                    && !c.is_ascii_digit()
1010                    && !quit.iter().any(|b| b == &vec![(key.modifiers, key.code)]) =>
1011            {
1012                self.feature_with_key(c)
1013            }
1014            _ => None,
1015        }
1016    }
1017
1018    /// The feature that `key` toggles, if any.
1019    fn feature_with_key(&self, key: char) -> Option<String> {
1020        self.state
1021            .feature_keys
1022            .iter()
1023            .find(|(_, held)| **held == key)
1024            .map(|(feature, _)| feature.clone())
1025    }
1026
1027    fn handle_picker_input(&mut self, key: event::KeyEvent) -> Res<()> {
1028        if key.kind != event::KeyEventKind::Press {
1029            return Ok(());
1030        }
1031
1032        // Nothing is typed in theme mode's one row, so a feature key toggles its
1033        // feature there as at the top level, and themes can be judged with it on
1034        // and off. A digit there selects a theme, and `picker.quit` quits.
1035        if let Some(feature) = self.feature_with_key_in_one_row(key) {
1036            return crate::ops::editor::toggle_feature(self, &feature);
1037        }
1038
1039        if let Some(ref mut picker) = self.state.picker {
1040            let bindings = &self.state.config.picker_bindings;
1041            let key_combo = vec![(key.modifiers, key.code)];
1042            let typed = match (key.code, key.modifiers) {
1043                (
1044                    event::KeyCode::Char(c),
1045                    event::KeyModifiers::NONE | event::KeyModifiers::SHIFT,
1046                ) => Some(c),
1047                _ => None,
1048            };
1049
1050            if bindings.next.iter().any(|b| b == &key_combo) {
1051                picker.next();
1052            } else if bindings.previous.iter().any(|b| b == &key_combo) {
1053                picker.previous();
1054            } else if bindings.done.iter().any(|b| b == &key_combo) {
1055                picker.done();
1056            } else if bindings.cancel.iter().any(|b| b == &key_combo) {
1057                picker.cancel();
1058            } else if bindings.toggle_list.iter().any(|b| b == &key_combo) {
1059                picker.toggle_one_row();
1060            } else if picker.is_one_row() {
1061                // Nothing is typed here, so a letter can quit and a digit select.
1062                if bindings.quit.iter().any(|b| b == &key_combo) {
1063                    picker.cancel();
1064                } else if let Some(digit) = typed.and_then(|c| c.to_digit(10)) {
1065                    picker.select_number(digit as usize);
1066                }
1067            } else if picker.assigns_keys() {
1068                // A letter sets a key here, but not the quit letter, which is
1069                // bound at the top level and so could not be set anyway.
1070                if bindings.quit.iter().any(|b| b == &key_combo) {
1071                    picker.cancel();
1072                } else if let Some(c) = typed {
1073                    picker.set_key(c);
1074                }
1075            } else {
1076                // Text input - delegate to text state
1077                picker.input_state.handle_key_event(key);
1078                picker.update_filter();
1079            }
1080        }
1081        Ok(())
1082    }
1083}
1084
1085/// `features`, then `theme`: last, so that it has its way over a feature that
1086/// sets the same keys, as delta resolves a conflict between features in favour
1087/// of the later one.
1088fn overlay(features: &[String], theme: Option<&str>) -> Rc<[String]> {
1089    features
1090        .iter()
1091        .cloned()
1092        .chain(theme.map(str::to_owned))
1093        .collect()
1094}
1095
1096fn get_prompt_result(params: &PromptParams, app: &mut App) -> Res<String> {
1097    let input = app.state.prompt.state.value.as_str();
1098    if params.prefill {
1099        return Ok(input.to_owned());
1100    }
1101
1102    let default_value = (params.create_default_value)(app);
1103
1104    let value = match (input, &default_value) {
1105        ("", None) => "",
1106        ("", Some(selected)) => selected,
1107        (value, _) => value,
1108    };
1109
1110    Ok(value.to_string())
1111}
1112
1113fn tee(maybe_input: Option<&mut impl Read>, outputs: &mut [&mut dyn Write]) -> std::io::Result<()> {
1114    let Some(input) = maybe_input else {
1115        return Ok(());
1116    };
1117
1118    let mut buf = [0u8; 1024];
1119
1120    loop {
1121        let num_read = input.read(&mut buf)?;
1122        if num_read == 0 {
1123            break;
1124        }
1125
1126        let buf = &buf[..num_read];
1127        for output in &mut *outputs {
1128            output.write_all(buf)?;
1129        }
1130    }
1131
1132    Ok(())
1133}
1134
1135fn rebase_todo_screen(
1136    config: &Arc<Config>,
1137    repo: &Rc<Repository>,
1138    params: RenderParams,
1139    todo: RebaseTodo,
1140) -> Res<Screen> {
1141    screen::rebase_todo::create(
1142        Arc::clone(config),
1143        Rc::clone(repo),
1144        params,
1145        Rc::new(RefCell::new(todo)),
1146    )
1147}
1148
1149/// How far back [`App::pick_commit`] lists commits.
1150const COMMIT_PICK_LIMIT: usize = 256;
1151
1152pub(crate) fn root_menu(config: &Config) -> Option<Menu> {
1153    if config.general.always_show_help.enabled {
1154        Some(Menu::Help)
1155    } else {
1156        None
1157    }
1158}
1159
1160fn write_child_output_to_log(
1161    log_rwlock: &mut Arc<RwLock<CmdLogEntry>>,
1162    child: &mut Child,
1163    status: std::process::ExitStatus,
1164) -> Res<()> {
1165    let mut log = log_rwlock.write().unwrap();
1166
1167    let CmdLogEntry::Cmd { args, out: out_log } = log.deref_mut() else {
1168        unreachable!("pending_cmd is always CmdLogEntry::Cmd variant");
1169    };
1170
1171    drop(child.stdin.take());
1172
1173    let mut out_bytes = vec![];
1174    log::debug!("Reading stderr");
1175
1176    child
1177        .stderr
1178        .take()
1179        .unwrap()
1180        .read_to_end(&mut out_bytes)
1181        .map_err(Error::CouldntReadCmdOutput)?;
1182
1183    child
1184        .stdout
1185        .take()
1186        .unwrap()
1187        .read_to_end(&mut out_bytes)
1188        .map_err(Error::CouldntReadCmdOutput)?;
1189
1190    let out_string = String::from_utf8_lossy(&out_bytes).to_string();
1191    *out_log = Some(out_string.into());
1192
1193    if !status.success() {
1194        return Err(Error::CmdBadExit(args.to_string(), status.code()));
1195    }
1196
1197    Ok(())
1198}
1199
1200type DefaultFn = Box<dyn Fn(&App) -> Option<String>>;
1201
1202pub(crate) struct PromptParams {
1203    pub prompt: &'static str,
1204    pub create_default_value: DefaultFn,
1205    pub hide_menu: bool,
1206    /// Put the default value in the prompt for the user to edit, rather than
1207    /// keeping it as what an empty answer means.
1208    pub prefill: bool,
1209    pub history: Option<prompt::HistoryKind>,
1210    /// Draw `prompt` alone in front of the line typed on, as less and vim draw
1211    /// `/` for a search, rather than as a question.
1212    pub bare: bool,
1213}
1214
1215impl Default for PromptParams {
1216    fn default() -> Self {
1217        Self {
1218            prompt: "",
1219            create_default_value: Box::new(|_| None),
1220            hide_menu: true,
1221            prefill: false,
1222            history: None,
1223            bare: false,
1224        }
1225    }
1226}