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