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