use super::{Action, OpTrait};
use crate::{
Res,
app::{App, PromptParams, State},
error::Error,
item_data::ItemData,
menu::PendingMenu,
picker::{Picked, PickerData, PickerItem, PickerState},
screen::NavMode,
term::Term,
};
use std::cell::RefCell;
use std::rc::Rc;
pub(crate) struct Quit;
impl OpTrait for Quit {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, term| {
let menu = app
.state
.pending_menu
.as_ref()
.map(|pending_menu| pending_menu.menu);
if menu == app.base_menu() {
if app.state.screens.len() == 1 {
if app.state.config.general.confirm_quit.enabled {
app.confirm(term, "Really quit? (y or n)")?;
};
app.state.quit = true;
} else {
app.state.screens.pop();
}
}
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Quit/Close".into()
}
}
pub(crate) struct OpenMenu(pub crate::menu::Menu);
impl OpTrait for OpenMenu {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
let submenu = self.0;
Some(Rc::new(move |app, _term| {
app.state.pending_menu = Some(PendingMenu::init(submenu));
app.inhibit_close_menu();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Submenu".into()
}
}
pub(crate) struct Suspend;
impl OpTrait for Suspend {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, term| {
term.suspend(&app.state.config).map_err(Error::Term)
}))
}
fn display(&self, _state: &State) -> String {
"Suspend".into()
}
}
pub(crate) struct Refresh;
impl OpTrait for Refresh {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| app.update_screens()))
}
fn display(&self, _state: &State) -> String {
"Refresh".into()
}
}
pub(crate) struct ToggleArg(pub String);
impl OpTrait for ToggleArg {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
let arg_name = self.0.clone();
Some(Rc::new(move |app, term| {
let mut need_prompt = None;
let mut default = None;
let maybe_entry = if let Some(menu) = &mut app.state.pending_menu {
Some(menu.args.entry(arg_name.clone().into()))
} else {
None
};
if let Some(entry) = maybe_entry {
entry.and_modify(|arg| {
if arg.is_active() {
arg.unset();
} else if arg.expects_value() {
default = arg.default_as_string();
need_prompt = Some(arg.display);
} else {
arg.set("").expect("Should succeed");
}
});
}
let arg_name = arg_name.clone();
let parse_and_set_arg =
Box::new(move |app: &mut App, _term: &mut Term, value: &str| {
if let Some(menu) = &mut app.state.pending_menu
&& let Some(entry) = menu.args.get_mut(arg_name.as_str())
{
return entry.set(value);
}
Ok(())
});
if let Some(display) = need_prompt {
let arg = app.prompt(
term,
&PromptParams {
prompt: display,
create_default_value: Box::new(move |_| default.clone()),
hide_menu: false,
prefill: false,
history: None,
},
)?;
parse_and_set_arg(app, term, &arg)?;
}
app.inhibit_close_menu();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
self.0.clone()
}
}
pub(crate) struct ToggleSection;
impl OpTrait for ToggleSection {
fn get_action(&self, target: &ItemData) -> Option<Action> {
if target.is_section() {
Some(Rc::new(|app, _term| {
app.screen_mut().toggle_section()?;
Ok(())
}))
} else {
None
}
}
fn is_target_op(&self) -> bool {
true
}
fn display(&self, state: &State) -> String {
let item = state.screens.last().unwrap().get_selected_item();
if state.screens.last().unwrap().is_collapsed(item) {
"Unfold".into()
} else {
"Fold".into()
}
}
}
pub(crate) struct DiffContext;
impl OpTrait for DiffContext {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app: &mut App, term: &mut Term| {
let current = app.state.context.clone();
let answer = app.prompt(
term,
&PromptParams {
prompt: "",
create_default_value: Box::new(move |_| {
current.as_deref().map(|flag| match flag {
"-W" => "W".to_owned(),
flag => flag.trim_start_matches("-U").to_owned(),
})
}),
hide_menu: false,
prefill: true,
history: None,
},
)?;
let Some(flag) = context_flag(&answer) else {
app.display_error(format!("Not a number of lines, nor W: {answer}"));
return Ok(());
};
app.state.context = flag;
app.rerender_screens()
}))
}
fn display(&self, _state: &State) -> String {
"Diff context".into()
}
}
pub(crate) struct EditGitCommand;
impl OpTrait for EditGitCommand {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app: &mut App, term: &mut Term| {
let Some(git) = app.screen().git_command.clone() else {
app.display_error(NOT_ASKED_FOR);
return Ok(());
};
let line = git.borrow().line(app.state.context.as_deref());
let answer = app.prompt(
term,
&PromptParams {
prompt: "",
create_default_value: Box::new(move |_| Some(line.clone())),
prefill: true,
history: Some(crate::prompt::HistoryKind::GitCommand),
..Default::default()
},
)?;
let edited = git.borrow().edited(&answer);
match edited {
Ok(edited) => *git.borrow_mut() = edited,
Err(err) => {
app.display_error(err.to_string());
return Ok(());
}
}
app.state.context = None;
app.rerender_screens()
}))
}
fn display(&self, _state: &State) -> String {
"Edit git command".into()
}
}
pub(crate) struct FilePatterns;
impl OpTrait for FilePatterns {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app: &mut App, term: &mut Term| {
let Some(git) = app.screen().git_command.clone() else {
app.display_error(NOT_ASKED_FOR);
return Ok(());
};
let patterns = git.borrow().file_patterns().join(" ");
let answer = app.prompt(
term,
&PromptParams {
prompt: "Files",
create_default_value: Box::new(move |_| Some(patterns.clone())),
prefill: true,
history: Some(crate::prompt::HistoryKind::FilePatterns),
..Default::default()
},
)?;
let patterns = match shell_words::split(&answer) {
Ok(patterns) => patterns,
Err(_) => {
app.display_error(Error::EditedCommandQuotes.to_string());
return Ok(());
}
};
ask_for(app, &git, &patterns)
}))
}
fn display(&self, _state: &State) -> String {
"Files".into()
}
}
pub(crate) struct HideFile;
impl OpTrait for HideFile {
fn get_action(&self, target: &ItemData) -> Option<Action> {
let path = file_of(target)?;
Some(Rc::new(move |app: &mut App, _term: &mut Term| {
let Some(git) = app.screen().git_command.clone() else {
app.display_error(NOT_ASKED_FOR);
return Ok(());
};
let mut patterns = git.borrow().file_patterns();
patterns.push(crate::calling_process::excluding(&path));
ask_for(app, &git, &patterns)
}))
}
fn is_target_op(&self) -> bool {
true
}
fn display(&self, _state: &State) -> String {
"Hide file".into()
}
}
const NOT_ASKED_FOR: &str = "This view is not something gitu asked git for";
fn ask_for(
app: &mut App,
git: &Rc<RefCell<crate::calling_process::GitCommand>>,
patterns: &[String],
) -> Res<()> {
let asking_for = git.borrow().asking_for(patterns);
app.state.prompt.remember(
crate::prompt::HistoryKind::GitCommand,
&asking_for.line(app.state.context.as_deref()),
)?;
*git.borrow_mut() = asking_for;
app.rerender_screens()
}
fn file_of(target: &ItemData) -> Option<String> {
let ItemData::Delta { diff, file_i, .. } = target else {
return None;
};
let header = &diff.file_diffs[*file_i].header;
let path = match header.status {
crate::gitu_diff::Status::Deleted => &header.old_file,
_ => &header.new_file,
};
Some(path.fmt(&diff.text).into_owned())
}
fn context_flag(answer: &str) -> Option<Option<Rc<str>>> {
match answer {
"" => Some(None),
"W" | "w" => Some(Some(Rc::from("-W"))),
lines if !lines.is_empty() && lines.chars().all(|c| c.is_ascii_digit()) => {
Some(Some(Rc::from(format!("-U{lines}").as_str())))
}
_ => None,
}
}
pub(crate) struct ToggleAllSections;
impl OpTrait for ToggleAllSections {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().toggle_all_sections();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Fold all".into()
}
}
pub(crate) struct ToggleMenu;
impl OpTrait for ToggleMenu {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app: &mut App, _term: &mut Term| {
let screen = app.screen_mut();
screen.show_menu = !screen.show_menu;
Ok(())
}))
}
fn display(&self, state: &State) -> String {
if state.screens.last().unwrap().show_menu {
"Hide keys".into()
} else {
"Show keys".into()
}
}
}
pub(crate) struct RendererFeatures;
impl OpTrait for RendererFeatures {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app: &mut App, term: &mut Term| {
let offered = crate::diff_renderer::offered_features(
&app.state.config.general.diff_renderer.features,
&app.state.repo.config().map_err(Error::ReadGitConfig)?,
)?;
if offered.is_empty() {
app.display_error("No renderer features are configured");
return Ok(());
}
let mut cursor = 0;
loop {
let picker = PickerState::new("Features", feature_items(app, &offered), false)
.assigning_keys()
.with_cursor(cursor);
match app.pick_or_set_key(term, picker)? {
None => return Ok(()),
Some(Picked::Item(picked)) => return toggle_feature(app, picked.display()),
Some(Picked::KeySet(key, picked)) => {
let feature = picked.display();
cursor = offered
.iter()
.position(|offered| offered == feature)
.unwrap_or(0);
set_feature_key(app, feature, key);
}
}
}
}))
}
fn display(&self, _state: &State) -> String {
"Features".into()
}
}
fn feature_items(app: &App, offered: &[String]) -> Vec<PickerItem> {
offered
.iter()
.map(|feature| {
let mark = if app.state.features.contains(feature) {
'●'
} else {
' '
};
let key = app.state.feature_keys.get(feature).copied().unwrap_or(' ');
PickerItem::new(
format!("{mark} {key} {feature}"),
PickerData::Item(feature.clone()),
)
})
.collect()
}
pub(crate) fn toggle_feature(app: &mut App, feature: &str) -> Res<()> {
app.state.features = toggled(&app.state.features, feature);
app.rerender_screens()
}
fn set_feature_key(app: &mut App, feature: &str, key: char) {
let key = if app.state.feature_keys.get(feature) == Some(&key) {
None
} else if app.state.config.bindings.binds_at_top_level(key) {
app.display_error(format!("{key} is bound already"));
return;
} else {
Some(key)
};
app.state
.feature_keys
.retain(|name, held| name != feature && Some(*held) != key);
if let Some(key) = key {
app.state.feature_keys.insert(feature.to_owned(), key);
}
if let Err(e) = crate::config_edit::set_feature_key(&app.state.config.path, feature, key) {
app.display_error(e.to_string());
}
}
fn toggled(features: &[String], feature: &str) -> Rc<[String]> {
if features.iter().any(|held| held == feature) {
return features
.iter()
.filter(|held| *held != feature)
.cloned()
.collect();
}
features
.iter()
.cloned()
.chain([feature.to_owned()])
.collect()
}
pub(crate) struct SelectCommit;
impl OpTrait for SelectCommit {
fn get_action(&self, target: &ItemData) -> Option<Action> {
let ItemData::Commit { oid, .. } = target else {
return None;
};
let oid = oid.clone();
Some(Rc::new(move |app: &mut App, _term: &mut Term| {
app.state.picked_commit = Some(oid.clone());
Ok(())
}))
}
fn is_target_op(&self) -> bool {
true
}
fn display(&self, _state: &State) -> String {
"Select".into()
}
}
pub(crate) struct MoveUp;
impl OpTrait for MoveUp {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().select_previous(NavMode::Normal);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Up".into()
}
}
pub(crate) struct MoveDown;
impl OpTrait for MoveDown {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().select_next(NavMode::Normal);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Down".into()
}
}
pub(crate) struct MoveDownLine;
impl OpTrait for MoveDownLine {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().select_next(NavMode::IncludeSubLines);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Down line".into()
}
}
pub(crate) struct MoveUpLine;
impl OpTrait for MoveUpLine {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().select_previous(NavMode::IncludeSubLines);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Up line".into()
}
}
pub(crate) struct ExtendSelection(pub bool);
impl OpTrait for ExtendSelection {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
let forwards = self.0;
Some(Rc::new(move |app: &mut App, _term: &mut Term| {
app.screen_mut().extend_selection(forwards);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
if self.0 {
"Select down".into()
} else {
"Select up".into()
}
}
}
pub(crate) struct MoveToScreenLine(pub usize);
impl OpTrait for MoveToScreenLine {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
let screen_line = self.0;
Some(Rc::new(move |app, _term| {
app.screen_mut().move_cursor_to_screen_line(screen_line);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Move to line".into()
}
}
pub(crate) struct MoveNextSection;
impl OpTrait for MoveNextSection {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
let depth = app.screen().get_selected_item().depth;
app.screen_mut().select_next(NavMode::Siblings { depth });
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Next section".into()
}
}
pub(crate) struct MovePrevSection;
impl OpTrait for MovePrevSection {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
let depth = app.screen().get_selected_item().depth;
app.screen_mut()
.select_previous(NavMode::Siblings { depth });
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Prev section".into()
}
}
pub(crate) struct MoveParentSection;
impl OpTrait for MoveParentSection {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
let depth = app.screen().get_selected_item().depth.saturating_sub(1);
app.screen_mut()
.select_previous(NavMode::Siblings { depth });
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Parent section".into()
}
}
pub(crate) struct HalfPageUp;
impl OpTrait for HalfPageUp {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().half_page_up();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Scroll half page up".into()
}
}
pub(crate) struct HalfPageDown;
impl OpTrait for HalfPageDown {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().half_page_down();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Scroll half page down".into()
}
}
pub(crate) struct FullPageUp;
impl OpTrait for FullPageUp {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().full_page_up();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Scroll page up".into()
}
}
pub(crate) struct FullPageDown;
impl OpTrait for FullPageDown {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().full_page_down();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Scroll page down".into()
}
}
pub(crate) struct ScrollViewUp;
impl OpTrait for ScrollViewUp {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.close_menu();
app.screen_mut().scroll_view_up(1);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Scroll view up".into()
}
}
pub(crate) struct ScrollViewDown;
impl OpTrait for ScrollViewDown {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.close_menu();
app.screen_mut().scroll_view_down(1);
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Scroll view down".into()
}
}
pub(crate) struct MoveTop;
impl OpTrait for MoveTop {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().move_cursor_to_top();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Top".into()
}
}
pub(crate) struct MoveBottom;
impl OpTrait for MoveBottom {
fn get_action(&self, _target: &ItemData) -> Option<Action> {
Some(Rc::new(|app, _term| {
app.screen_mut().move_cursor_to_bottom();
Ok(())
}))
}
fn display(&self, _state: &State) -> String {
"Bottom".into()
}
}