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(name) = feature {
502                    if self.state.config.general.commands.contains_key(&name) {
503                        crate::ops::command::run(self, term, &name)?;
504                    } else if self.state.config.general.views.contains_key(&name) {
505                        crate::ops::command::open_view(self, term, &name)?;
506                    } else {
507                        crate::ops::editor::toggle_feature(self, &name)?;
508                    }
509                }
510            }
511            [_, ..] => (),
512        }
513
514        Ok(())
515    }
516
517    fn handle_mouse_input(&mut self, term: &mut Term, mouse: &MouseEvent) -> Res<bool> {
518        match mouse.kind {
519            MouseEventKind::Down(MouseButton::Left) => {
520                let click_y = mouse.row as usize;
521                if self.screen().is_valid_screen_line(click_y) {
522                    let old_selected_item_id = self.screen().get_selected_item().id;
523                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
524                    let new_selected_item = self.screen().get_selected_item();
525
526                    if old_selected_item_id == new_selected_item.id {
527                        // If the item clicked was already the current item, then try to
528                        // toggle it if it's a section or show it.
529                        if new_selected_item.data.is_section() {
530                            self.handle_op(Op::ToggleSection, term)?;
531                        } else {
532                            self.handle_op(Op::Show, term)?;
533                        }
534                    }
535                }
536            }
537            MouseEventKind::Down(MouseButton::Right) => {
538                let click_y = mouse.row as usize;
539                if self.screen().is_valid_screen_line(click_y) {
540                    self.handle_op(Op::MoveToScreenLine(click_y), term)?;
541                    self.handle_op(Op::Show, term)?;
542                }
543            }
544            MouseEventKind::ScrollUp => {
545                let scroll_lines = self.state.config.general.mouse_scroll_lines;
546                if scroll_lines > 0 {
547                    self.screen_mut().scroll_view_up(scroll_lines);
548                }
549            }
550            MouseEventKind::ScrollDown => {
551                let scroll_lines = self.state.config.general.mouse_scroll_lines;
552                if scroll_lines > 0 {
553                    self.screen_mut().scroll_view_down(scroll_lines);
554                }
555            }
556            _ => return Ok(false),
557        }
558
559        Ok(true)
560    }
561
562    pub(crate) fn handle_op(&mut self, op: Op, term: &mut Term) -> Res<()> {
563        if self.state.read_only && op.writes() {
564            return Ok(());
565        }
566        let item_data = self.screen().selected_target();
567        let implementation = op.clone().implementation();
568
569        if let Some(mut action) = implementation.get_action(&item_data) {
570            let result = Rc::get_mut(&mut action).unwrap()(self, term);
571            self.handle_result(result)?;
572            if !self.state.inhibit_close_menu {
573                self.close_menu();
574            }
575            self.state.inhibit_close_menu = false;
576        }
577
578        Ok(())
579    }
580
581    fn handle_result<T>(&mut self, result: Res<T>) -> Res<()> {
582        match result {
583            Ok(_) => Ok(()),
584            Err(Error::NoMoreEvents) => Err(Error::NoMoreEvents),
585            Err(Error::PromptAborted) => Ok(()),
586            Err(error) => {
587                self.state
588                    .current_cmd_log
589                    .push(CmdLogEntry::Error(error.to_string()));
590
591                Ok(())
592            }
593        }
594    }
595
596    /// What a screen opened now should render with: the viewport it will get,
597    /// and the features currently selected.
598    pub(crate) fn render_params(&self, size: (u16, u16)) -> RenderParams {
599        RenderParams {
600            size,
601            features: self.overlay(),
602            context: self.state.context.clone(),
603            streaming: self.state.enable_async_cmds,
604        }
605    }
606
607    pub fn close_menu(&mut self) {
608        let hide = self.screen().menu.is_some() && !self.screen().show_menu;
609        self.state.pending_menu = self.base_menu().map(|menu| PendingMenu {
610            is_hidden: hide,
611            ..PendingMenu::init(menu)
612        });
613    }
614
615    /// The menu to fall back to once a pending one closes: a screen with its own
616    /// keymap (the interactive rebase todo) imposes it for as long as it is up.
617    pub(crate) fn base_menu(&self) -> Option<Menu> {
618        self.screen().menu.or_else(|| root_menu(&self.state.config))
619    }
620
621    pub fn inhibit_close_menu(&mut self) {
622        self.state.inhibit_close_menu = true;
623    }
624
625    pub(crate) fn screen_mut(&mut self) -> &mut Screen {
626        self.state.screens.last_mut().expect("No screen")
627    }
628
629    pub(crate) fn screen(&self) -> &Screen {
630        self.state.screens.last().expect("No screen")
631    }
632
633    /// Displays an `Info` message to the CmdLog.
634    pub fn display_info<S: Into<Cow<'static, str>>>(&mut self, message: S) {
635        self.state
636            .current_cmd_log
637            .push(CmdLogEntry::Info(message.into().into_owned()));
638    }
639
640    /// Displays an `Error` message to the CmdLog.
641    pub fn display_error<S: Into<Cow<'static, str>>>(&mut self, message: S) {
642        self.state
643            .current_cmd_log
644            .push(CmdLogEntry::Error(message.into().into_owned()));
645    }
646
647    /// Runs a `Command` and handles its output.
648    /// Will block awaiting its completion.
649    pub fn run_cmd(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
650        self.run_cmd_async(term, input, cmd)?;
651        self.await_pending_cmd()?;
652        self.handle_pending_cmd()?;
653        Ok(())
654    }
655
656    /// Runs a `Command` and handles its output asynchronously (if async commands are enabled).
657    /// Will return `Ok(())` if one is already running.
658    pub fn run_cmd_async(&mut self, term: &mut Term, input: &[u8], cmd: Command) -> Res<()> {
659        let shown = crate::cmd_log::command_args(&cmd);
660        self.run_cmd_async_shown(term, input, cmd, shown)
661    }
662
663    /// Run `command` with `sh -c` and `env` set, as [`Self::run_cmd_async`]
664    /// runs a command, shown as it is written rather than as `sh -c` and it.
665    pub(crate) fn run_shell_async(
666        &mut self,
667        term: &mut Term,
668        command: &str,
669        env: Vec<(&'static str, String)>,
670    ) -> Res<()> {
671        let mut cmd = Command::new("sh");
672        cmd.args(["-c", command]).envs(env);
673        self.run_cmd_async_shown(term, &[], cmd, command.to_owned().into())
674    }
675
676    fn run_cmd_async_shown(
677        &mut self,
678        term: &mut Term,
679        input: &[u8],
680        mut cmd: Command,
681        shown: Cow<'static, str>,
682    ) -> Res<()> {
683        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
684
685        if self.state.pending_cmd.is_some() {
686            return Err(Error::CmdAlreadyRunning);
687        }
688
689        cmd.current_dir(self.state.repo.workdir().expect("No workdir"));
690
691        cmd.stdin(Stdio::piped());
692        cmd.stdout(Stdio::piped());
693        cmd.stderr(Stdio::piped());
694
695        let log_entry = self.state.current_cmd_log.push_cmd(shown);
696
697        ui::ui(term, &mut self.state)?;
698
699        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
700
701        use std::io::Write;
702        child
703            .stdin
704            .take()
705            .unwrap()
706            .write_all(input)
707            .map_err(Error::Term)?;
708
709        self.state.pending_cmd = Some((child, log_entry));
710
711        if !self.state.enable_async_cmds {
712            self.await_pending_cmd()?;
713        }
714
715        Ok(())
716    }
717
718    fn await_pending_cmd(&mut self) -> Res<()> {
719        if let Some((child, _)) = &mut self.state.pending_cmd {
720            child.wait().map_err(Error::CouldntAwaitCmd)?;
721        }
722        Ok(())
723    }
724
725    /// Handles any pending_cmd in State without blocking. Returns `true` if a cmd was handled.
726    fn handle_pending_cmd(&mut self) -> Res<()> {
727        let Some((ref mut child, ref mut log_rwlock)) = self.state.pending_cmd else {
728            return Ok(());
729        };
730
731        let Some(status) = child.try_wait().map_err(Error::CouldntAwaitCmd)? else {
732            return Ok(());
733        };
734
735        log::debug!("pending cmd finished with {status:?}");
736
737        let result = write_child_output_to_log(log_rwlock, child, status);
738        self.state.pending_cmd = None;
739        self.update_screens()?;
740        result?;
741
742        Ok(())
743    }
744
745    pub fn run_cmd_interactive(&mut self, term: &mut Term, mut cmd: Command) -> Res<()> {
746        cmd.env("CLICOLOR_FORCE", "1"); // No guarantee, but modern tools seem to implement this
747
748        if self.state.pending_cmd.is_some() {
749            return Err(Error::CmdAlreadyRunning);
750        }
751
752        cmd.current_dir(self.state.repo.workdir().ok_or(Error::NoRepoWorkdir)?);
753
754        self.state
755            .current_cmd_log
756            .push_cmd_with_output(&cmd, "\n".into());
757        self.redraw_now(term)?;
758
759        eprint!("\r");
760
761        // Redirect stderr so we can capture it via `Child::wait_with_output()`
762        cmd.stderr(Stdio::piped());
763
764        term.reset_term_stay_on_alt_screeen(&self.state.config)
765            .map_err(Error::Term)?;
766
767        let mut child = cmd.spawn().map_err(Error::SpawnCmd)?;
768
769        // Drop stdin as `Child::wait_with_output` would
770        drop(child.stdin.take());
771
772        let mut stderr = Vec::new();
773
774        tee(
775            child.stderr.as_mut(),
776            &mut [&mut std::io::stderr(), &mut stderr],
777        )
778        .map_err(Error::Term)?;
779
780        let status = child.wait().map_err(Error::CouldntAwaitCmd)?;
781
782        term.setup_term(&self.state.config).map_err(Error::Term)?;
783
784        let out_utf8 = String::from_utf8(strip_ansi_escapes::strip(stderr.clone()))
785            .expect("Error turning command output to String")
786            .into();
787
788        self.state.current_cmd_log.clear();
789        self.state
790            .current_cmd_log
791            .push_cmd_with_output(&cmd, out_utf8);
792
793        term.clear().map_err(Error::Term)?;
794        self.update_screens()?;
795
796        if !status.success() {
797            // Its stderr went to the terminal.
798            return Err(Error::CmdBadExit(
799                crate::cmd_log::command_args(&cmd).into_owned(),
800                status.code(),
801                String::new(),
802            ));
803        }
804
805        Ok(())
806    }
807
808    pub fn hide_menu(&mut self) {
809        if let Some(ref mut menu) = self.state.pending_menu {
810            menu.is_hidden = true;
811        }
812    }
813
814    fn unhide_menu(&mut self) {
815        if let Some(ref mut menu) = self.state.pending_menu {
816            menu.is_hidden = false;
817        }
818    }
819
820    pub(crate) fn selected_rev(&self) -> Option<Rev> {
821        self.screen().get_selected_item().data.rev()
822    }
823
824    pub(crate) fn prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
825        let default = (params.create_default_value)(self);
826
827        // A prompt with nothing to say says nothing: the key that opened it
828        // already said what is being answered. Prefilled, the default is the
829        // answer being edited rather than the one an empty answer falls back
830        // to — it is on the line, so naming it again says nothing either.
831        let prompt_text = match (params.prompt, &default) {
832            ("", _) => Cow::Borrowed(""),
833            (prompt, _) if params.bare => Cow::Borrowed(prompt),
834            (prompt, Some(default)) if !params.prefill => {
835                format!("{prompt} (default {default}):").into()
836            }
837            (prompt, _) => format!("{prompt}:").into(),
838        };
839
840        if params.hide_menu {
841            self.hide_menu();
842        }
843
844        self.state.prompt.set(prompt::PromptData {
845            prompt_text,
846            bare: params.bare,
847        });
848
849        if params.prefill
850            && let Some(default) = default
851        {
852            self.state.prompt.state.value = default;
853            self.state.prompt.state.move_to(usize::MAX);
854        }
855        self.state.prompt.start_history(params.history)?;
856
857        let result = self.handle_prompt(term, params);
858
859        self.unhide_menu();
860        self.state.prompt.reset();
861
862        result
863    }
864
865    fn handle_prompt(&mut self, term: &mut Term, params: &PromptParams) -> Res<String> {
866        self.redraw_now(term)?;
867
868        loop {
869            let event = term.read_event()?;
870            let was = self.state.prompt.state.value.clone();
871            self.handle_event(term, event)?;
872
873            if self.state.prompt.state.status == text_input::Status::Done {
874                let value = get_prompt_result(params, self)?;
875                if let Some(kind) = params.history {
876                    self.state.prompt.remember(kind, &value)?;
877                }
878                return Ok(value);
879            } else if self.state.prompt.state.status == text_input::Status::Aborted {
880                return Err(Error::PromptAborted);
881            }
882            if let Some(on_edit) = &params.on_edit
883                && self.state.prompt.state.value != was
884            {
885                let value = self.state.prompt.state.value.clone();
886                on_edit(self, &value);
887            }
888
889            self.redraw_now(term)?;
890        }
891    }
892
893    pub fn confirm(&mut self, term: &mut Term, prompt: &'static str) -> Res<()> {
894        self.hide_menu();
895        self.state.prompt.set(prompt::PromptData {
896            prompt_text: prompt.into(),
897            bare: false,
898        });
899
900        let result = self.handle_confirm(term);
901
902        self.unhide_menu();
903        self.state.prompt.reset();
904
905        result
906    }
907
908    fn handle_confirm(&mut self, term: &mut Term) -> Res<()> {
909        self.redraw_now(term)?;
910        loop {
911            let event = term.read_event()?;
912            self.handle_event(term, event)?;
913
914            match self.state.prompt.state.value.as_str() {
915                "y" => {
916                    return Ok(());
917                }
918                "" => (),
919                _ => {
920                    return Err(Error::PromptAborted);
921                }
922            }
923
924            self.redraw_now(term)?;
925        }
926    }
927
928    /// Show a picker and wait for user to select an item or cancel.
929    ///
930    /// Returns:
931    /// - `Ok(Some(data))` - User selected an item
932    /// - `Ok(None)` - User cancelled (Esc or Ctrl-C)
933    /// - `Err(e)` - An error occurred
934    ///
935    /// # Example
936    /// ```ignore
937    /// let items = vec![
938    ///     PickerItem::new("main", PickerData::Item("main".to_string())),
939    ///     PickerItem::new("develop", PickerData::Item("develop".to_string())),
940    /// ];
941    /// let picker = PickerState::new("Select branch", items, false);
942    ///
943    /// match app.pick(term, picker)? {
944    ///     Some(PickerData::Item(name)) => {
945    ///         // User selected a branch
946    ///         println!("Selected: {}", name);
947    ///     }
948    ///     Some(PickerData::CustomInput(_)) => {
949    ///         // Should not happen when allow_custom_input is false
950    ///     }
951    ///     None => {
952    ///         // User cancelled
953    ///         println!("Cancelled");
954    ///     }
955    /// }
956    /// ```
957    pub fn pick(&mut self, term: &mut Term, picker_state: PickerState) -> Res<Option<PickerData>> {
958        Ok(self
959            .pick_or_set_key(term, picker_state)?
960            .map(|picked| match picked {
961                Picked::Item(data) => data,
962                Picked::KeySet(..) => unreachable!("only a picker assigning keys sets them"),
963            }))
964    }
965
966    /// [`App::pick`], for a picker whose items can be given keys.
967    pub(crate) fn pick_or_set_key(
968        &mut self,
969        term: &mut Term,
970        picker_state: PickerState,
971    ) -> Res<Option<Picked>> {
972        self.state.picker = Some(picker_state);
973        let result = self.handle_picker(term, &mut |_, _| Ok(()));
974
975        self.state.picker = None;
976
977        result
978    }
979
980    /// [`App::pick`], calling `preview` with each item the cursor stops on.
981    pub(crate) fn pick_previewing(
982        &mut self,
983        term: &mut Term,
984        picker_state: PickerState,
985        preview: &mut dyn FnMut(&mut App, &PickerData) -> Res<()>,
986    ) -> Res<Option<PickerData>> {
987        self.state.picker = Some(picker_state);
988        let result = self.handle_picker(term, preview);
989
990        self.state.picker = None;
991
992        Ok(result?.map(|picked| match picked {
993            Picked::Item(data) => data,
994            Picked::KeySet(..) => unreachable!("only a picker assigning keys sets them"),
995        }))
996    }
997
998    /// Choose a commit by moving around a log view: the screen is pushed with
999    /// its own keymap, and the loop runs until `select_commit` names one or the
1000    /// screen is closed.
1001    pub fn pick_commit(&mut self, term: &mut Term) -> Res<Option<String>> {
1002        let params = self.render_params(self.screen().size);
1003        let selected = match &self.screen().get_selected_item().data {
1004            crate::item_data::ItemData::Commit { oid, .. } => Some(oid.clone()),
1005            _ => None,
1006        };
1007        // The selected commit has to be found among the rows, so they are all
1008        // drawn first.
1009        let params = RenderParams {
1010            streaming: params.streaming && selected.is_none(),
1011            ..params
1012        };
1013        self.state.screens.push(screen::pager::log(
1014            Arc::clone(&self.state.config),
1015            Rc::clone(&self.state.repo),
1016            params,
1017            COMMIT_PICK_LIMIT,
1018            None,
1019            None,
1020        )?);
1021        self.screen_mut().menu = Some(Menu::CommitPicker);
1022        self.close_menu();
1023
1024        // Start on the commit that was already selected, if there was one.
1025        if let Some(selected) = selected {
1026            self.screen_mut().select_matching(|data| {
1027                matches!(data, crate::item_data::ItemData::Commit { oid, .. } if *oid == selected)
1028            });
1029        }
1030
1031        let depth = self.state.screens.len();
1032        self.redraw_now(term)?;
1033
1034        loop {
1035            let event = term.read_event()?;
1036            self.handle_event(term, event)?;
1037
1038            if let Some(oid) = self.state.picked_commit.take() {
1039                self.state.screens.truncate(depth - 1);
1040                self.close_menu();
1041                return Ok(Some(oid));
1042            }
1043            if self.state.screens.len() < depth {
1044                return Ok(None); // The screen was closed: nothing picked.
1045            }
1046
1047            self.redraw_now(term)?;
1048        }
1049    }
1050
1051    fn handle_picker(
1052        &mut self,
1053        term: &mut Term,
1054        preview: &mut dyn FnMut(&mut App, &PickerData) -> Res<()>,
1055    ) -> Res<Option<Picked>> {
1056        self.redraw_now(term)?;
1057        let selected = |app: &App| {
1058            app.state
1059                .picker
1060                .as_ref()
1061                .and_then(|picker| picker.selected())
1062                .map(|item| item.data.clone())
1063        };
1064        let mut previewed = selected(self);
1065
1066        loop {
1067            let event = term.read_event()?;
1068            self.handle_event(term, event)?;
1069
1070            if let Some(ref picker) = self.state.picker {
1071                let selected = || picker.selected().map(|item| item.data.clone());
1072                match *picker.status() {
1073                    PickerStatus::Done => return Ok(selected().map(Picked::Item)),
1074                    PickerStatus::KeySet(key) => {
1075                        return Ok(selected().map(|data| Picked::KeySet(key, data)));
1076                    }
1077                    // User cancelled - this is not an error
1078                    PickerStatus::Cancelled => return Ok(None),
1079                    PickerStatus::Active => (),
1080                }
1081            }
1082
1083            // Only once the keys that came together are handled, so that an
1084            // arrow held down does not preview every item it passes.
1085            let now = selected(self);
1086            if now != previewed && !matches!(term.poll_event(Duration::ZERO), Ok(true)) {
1087                if let Some(data) = &now {
1088                    preview(self, data)?;
1089                }
1090                previewed = now;
1091            }
1092
1093            self.redraw_now(term)?;
1094        }
1095    }
1096
1097    /// The feature that `key` toggles in the picker's one row, if any. A key
1098    /// that runs a command or opens a view does nothing there.
1099    fn feature_with_key_in_one_row(&self, key: event::KeyEvent) -> Option<String> {
1100        let picker = self.state.picker.as_ref()?;
1101        let quit = &self.state.config.picker_bindings.quit;
1102        match (key.code, key.modifiers) {
1103            (event::KeyCode::Char(c), event::KeyModifiers::NONE)
1104                if picker.is_one_row()
1105                    && !c.is_ascii_digit()
1106                    && !quit.iter().any(|b| b == &vec![(key.modifiers, key.code)]) =>
1107            {
1108                self.feature_with_key(c).filter(|name| {
1109                    let general = &self.state.config.general;
1110                    !general.commands.contains_key(name) && !general.views.contains_key(name)
1111                })
1112            }
1113            _ => None,
1114        }
1115    }
1116
1117    /// The feature that `key` toggles, the command that it runs or the view
1118    /// that it opens, by name, if any.
1119    fn feature_with_key(&self, key: char) -> Option<String> {
1120        self.state
1121            .feature_keys
1122            .iter()
1123            .find(|(_, held)| **held == key)
1124            .map(|(feature, _)| feature.clone())
1125    }
1126
1127    fn handle_picker_input(&mut self, key: event::KeyEvent) -> Res<()> {
1128        if key.kind != event::KeyEventKind::Press {
1129            return Ok(());
1130        }
1131
1132        // Nothing is typed in theme mode's one row, so a feature key toggles its
1133        // feature there as at the top level, and themes can be judged with it on
1134        // and off. A digit there selects a theme, and `picker.quit` quits.
1135        if let Some(feature) = self.feature_with_key_in_one_row(key) {
1136            return crate::ops::editor::toggle_feature(self, &feature);
1137        }
1138
1139        if let Some(ref mut picker) = self.state.picker {
1140            let bindings = &self.state.config.picker_bindings;
1141            let key_combo = vec![(key.modifiers, key.code)];
1142            let typed = match (key.code, key.modifiers) {
1143                (
1144                    event::KeyCode::Char(c),
1145                    event::KeyModifiers::NONE | event::KeyModifiers::SHIFT,
1146                ) => Some(c),
1147                _ => None,
1148            };
1149
1150            if bindings.next.iter().any(|b| b == &key_combo) {
1151                picker.next();
1152            } else if bindings.previous.iter().any(|b| b == &key_combo) {
1153                picker.previous();
1154            } else if bindings.done.iter().any(|b| b == &key_combo) {
1155                picker.done();
1156            } else if bindings.cancel.iter().any(|b| b == &key_combo) {
1157                picker.cancel();
1158            } else if bindings.toggle_list.iter().any(|b| b == &key_combo) {
1159                picker.toggle_one_row();
1160            } else if picker.is_one_row() {
1161                // Nothing is typed here, so a letter can quit and a digit select.
1162                if bindings.quit.iter().any(|b| b == &key_combo) {
1163                    picker.cancel();
1164                } else if let Some(digit) = typed.and_then(|c| c.to_digit(10)) {
1165                    picker.select_number(digit as usize);
1166                }
1167            } else if picker.assigns_keys() {
1168                // A letter sets a key here, but not the quit letter, which is
1169                // bound at the top level and so could not be set anyway. In
1170                // columns, left and right move between them.
1171                if picker.grid().is_some() && key.code == event::KeyCode::Left {
1172                    picker.move_column(-1);
1173                } else if picker.grid().is_some() && key.code == event::KeyCode::Right {
1174                    picker.move_column(1);
1175                } else if bindings.quit.iter().any(|b| b == &key_combo) {
1176                    picker.cancel();
1177                } else if let Some(c) = typed {
1178                    picker.set_key(c);
1179                }
1180            } else {
1181                // Text input - delegate to text state
1182                picker.input_state.handle_key_event(key);
1183                picker.update_filter();
1184            }
1185        }
1186        Ok(())
1187    }
1188}
1189
1190/// `features`, then `theme`: last, so that it has its way over a feature that
1191/// sets the same keys, as delta resolves a conflict between features in favour
1192/// of the later one.
1193fn overlay(features: &[String], theme: Option<&str>) -> Rc<[String]> {
1194    features
1195        .iter()
1196        .cloned()
1197        .chain(theme.map(str::to_owned))
1198        .collect()
1199}
1200
1201fn get_prompt_result(params: &PromptParams, app: &mut App) -> Res<String> {
1202    let input = app.state.prompt.state.value.as_str();
1203    if params.prefill {
1204        return Ok(input.to_owned());
1205    }
1206
1207    let default_value = (params.create_default_value)(app);
1208
1209    let value = match (input, &default_value) {
1210        ("", None) => "",
1211        ("", Some(selected)) => selected,
1212        (value, _) => value,
1213    };
1214
1215    Ok(value.to_string())
1216}
1217
1218fn tee(maybe_input: Option<&mut impl Read>, outputs: &mut [&mut dyn Write]) -> std::io::Result<()> {
1219    let Some(input) = maybe_input else {
1220        return Ok(());
1221    };
1222
1223    let mut buf = [0u8; 1024];
1224
1225    loop {
1226        let num_read = input.read(&mut buf)?;
1227        if num_read == 0 {
1228            break;
1229        }
1230
1231        let buf = &buf[..num_read];
1232        for output in &mut *outputs {
1233            output.write_all(buf)?;
1234        }
1235    }
1236
1237    Ok(())
1238}
1239
1240fn rebase_todo_screen(
1241    config: &Arc<Config>,
1242    repo: &Rc<Repository>,
1243    params: RenderParams,
1244    todo: RebaseTodo,
1245) -> Res<Screen> {
1246    screen::rebase_todo::create(
1247        Arc::clone(config),
1248        Rc::clone(repo),
1249        params,
1250        Rc::new(RefCell::new(todo)),
1251    )
1252}
1253
1254/// How far back [`App::pick_commit`] lists commits.
1255const COMMIT_PICK_LIMIT: usize = 256;
1256
1257/// How long the terminal's width must hold before the screens are rendered again at it, so that
1258/// a burst of resizes runs the renderer once. Terminals delay their own resize signals too: kitty
1259/// by 100ms where the system does not report the end of a resize, Ghostty by 25ms.
1260const RESIZE_RENDER_DELAY: Duration = Duration::from_millis(50);
1261
1262pub(crate) fn root_menu(config: &Config) -> Option<Menu> {
1263    if config.general.always_show_help.enabled {
1264        Some(Menu::Help)
1265    } else {
1266        None
1267    }
1268}
1269
1270fn write_child_output_to_log(
1271    log_rwlock: &mut Arc<RwLock<CmdLogEntry>>,
1272    child: &mut Child,
1273    status: std::process::ExitStatus,
1274) -> Res<()> {
1275    let mut log = log_rwlock.write().unwrap();
1276
1277    let CmdLogEntry::Cmd { args, out: out_log } = log.deref_mut() else {
1278        unreachable!("pending_cmd is always CmdLogEntry::Cmd variant");
1279    };
1280
1281    drop(child.stdin.take());
1282
1283    let mut err_bytes = vec![];
1284    log::debug!("Reading stderr");
1285
1286    child
1287        .stderr
1288        .take()
1289        .unwrap()
1290        .read_to_end(&mut err_bytes)
1291        .map_err(Error::CouldntReadCmdOutput)?;
1292
1293    let mut out_bytes = err_bytes.clone();
1294    child
1295        .stdout
1296        .take()
1297        .unwrap()
1298        .read_to_end(&mut out_bytes)
1299        .map_err(Error::CouldntReadCmdOutput)?;
1300
1301    let out_string = String::from_utf8_lossy(&out_bytes).to_string();
1302    *out_log = Some(out_string.into());
1303
1304    if !status.success() {
1305        return Err(Error::CmdBadExit(
1306            args.to_string(),
1307            status.code(),
1308            String::from_utf8_lossy(&err_bytes).into_owned(),
1309        ));
1310    }
1311
1312    Ok(())
1313}
1314
1315type DefaultFn = Box<dyn Fn(&App) -> Option<String>>;
1316type EditFn = Box<dyn Fn(&mut App, &str)>;
1317
1318pub(crate) struct PromptParams {
1319    pub prompt: &'static str,
1320    pub create_default_value: DefaultFn,
1321    pub hide_menu: bool,
1322    /// Put the default value in the prompt for the user to edit, rather than
1323    /// keeping it as what an empty answer means.
1324    pub prefill: bool,
1325    pub history: Option<prompt::HistoryKind>,
1326    /// Draw `prompt` alone in front of the line typed on, as less and vim draw
1327    /// `/` for a search, rather than as a question.
1328    pub bare: bool,
1329    /// Called with the answer each time it is edited, before it is given.
1330    pub on_edit: Option<EditFn>,
1331}
1332
1333impl Default for PromptParams {
1334    fn default() -> Self {
1335        Self {
1336            prompt: "",
1337            create_default_value: Box::new(|_| None),
1338            hide_menu: true,
1339            prefill: false,
1340            history: None,
1341            bare: false,
1342            on_edit: None,
1343        }
1344    }
1345}