use std::borrow::Cow;
use crate::menu::Menu;
use crate::menu::arg::Arg;
use crate::style::Style;
use crate::ui::layout::opts;
use crate::ui::{self, UiTree, layout_line, layout_span, repeat_chars};
use crate::{
app::State,
config::{Config, ReadOnly},
ops::Op,
};
use itertools::Itertools;
use unicode_width::UnicodeWidthStr;
enum MenuValue<'a> {
Text(Cow<'a, str>),
Arg(&'a Arg),
}
pub(crate) fn layout_menu<'a>(layout: &mut UiTree<'a>, state: &'a State, width: usize) {
let Some(ref pending) = state.pending_menu else {
return;
};
if pending.is_hidden {
return;
}
if state.picker.is_some() {
return;
}
let config = &state.config;
let item = state.screens.last().unwrap().get_selected_item();
let style = &config.style;
let arg_binds = config.bindings.arg_list(pending).collect::<Vec<_>>();
let (target_binds, non_target_binds): (Vec<_>, Vec<_>) = config
.bindings
.list(&pending.menu)
.filter(|keybind| !(state.read_only && keybind.op.writes()))
.partition(|keybind| keybind.op.clone().implementation().is_target_op());
let target_binds: Vec<_> = target_binds
.into_iter()
.filter(|keybind| {
keybind
.op
.clone()
.implementation()
.get_action(&item.data)
.is_some()
})
.collect();
let (menu_binds, non_menu_binds): (Vec<_>, Vec<_>) = non_target_binds
.into_iter()
.chunk_by(|bind| &bind.op)
.into_iter()
.map(|(op, binds)| {
let binds: Vec<_> = binds.collect();
(op, binds)
})
.partition(|(op, _binds)| matches!(op, Op::OpenMenu(_)));
let feature_binds: Vec<_> = if matches!(pending.menu, Menu::Root | Menu::Help) {
state
.feature_keys
.iter()
.map(|(feature, key)| {
(
key.to_string().into(),
MenuValue::Text(feature.as_str().into()),
)
})
.collect()
} else {
vec![]
};
let separator_style = Style::from(&style.separator);
let read_only_notice = (state.read_only && matches!(pending.menu, Menu::Root | Menu::Help))
.then(|| {
let first = match config.general.read_only {
ReadOnly::Auto => "Read-only (.jj found):",
ReadOnly::On | ReadOnly::Off => "Read-only:",
};
format!("{first} commands that change the repository are hidden")
});
layout.col(opts(), |layout| {
ui::repeat_chars(layout, width, ui::DASHES, separator_style);
layout.row(opts().gap(3).pad(1), |layout| {
if !non_menu_binds.is_empty() {
layout.col(opts(), |layout| {
let heading = match pending.menu {
Menu::Root | Menu::Help => "Commands".into(),
menu => menu.to_string(),
};
layout_line(layout, heading.into(), Style::from(&style.menu.heading));
layout_keybinds_table(
layout,
config,
non_menu_binds
.into_iter()
.map(|(op, binds)| {
(
binds.iter().map(|bind| bind.raw.as_str()).join("/").into(),
MenuValue::Text(
op.clone().implementation().display(state).into(),
),
)
})
.collect(),
);
});
}
if !menu_binds.is_empty() || !feature_binds.is_empty() {
let has_submenus = !menu_binds.is_empty();
layout.col(opts(), |layout| {
if has_submenus {
layout_line(layout, "Submenus".into(), Style::from(&style.menu.heading));
layout_keybinds_table(
layout,
config,
menu_binds
.into_iter()
.map(|(op, binds)| {
let Op::OpenMenu(menu) = op else {
unreachable!();
};
(
binds.iter().map(|bind| bind.raw.as_str()).join("/").into(),
MenuValue::Text(menu.to_string().into()),
)
})
.collect(),
);
}
if !feature_binds.is_empty() {
if has_submenus {
layout_line(layout, " ".into(), Style::new());
}
layout_line(layout, "Features".into(), Style::from(&style.menu.heading));
layout_keybinds_table(layout, config, feature_binds);
}
});
}
layout.col(opts(), |layout| {
let target_binds_empty = target_binds.is_empty();
if !target_binds_empty {
layout_line(
layout,
item.data.kind().into(),
Style::from(&style.menu.heading),
);
layout_keybinds_table(
layout,
config,
target_binds
.into_iter()
.map(|bind| {
(
bind.raw.as_str().into(),
MenuValue::Text(
bind.op.clone().implementation().display(state).into(),
),
)
})
.collect(),
);
}
if let Some(notice) = read_only_notice {
if !target_binds_empty {
layout_line(layout, " ".into(), Style::new());
}
layout_line(layout, notice.into(), Style::from(&style.error_msg));
}
if !arg_binds.is_empty() {
layout_line(layout, "Arguments".into(), Style::from(&style.menu.heading));
layout_keybinds_table(
layout,
config,
arg_binds
.into_iter()
.map(|bind| {
let Op::ToggleArg(name) = &bind.op else {
unreachable!();
};
let arg = pending.args.get(name.as_str()).unwrap();
(bind.raw.as_str().into(), MenuValue::Arg(arg))
})
.collect(),
);
}
});
});
});
}
fn layout_keybinds_table<'a>(
layout: &mut UiTree<'a>,
config: &Config,
rows: Vec<(Cow<'a, str>, MenuValue<'a>)>,
) {
const SPACES: &str = " ";
let key_style = Style::from(&config.style.menu.key);
let max_width = rows
.iter()
.map(|(key, _)| UnicodeWidthStr::width(key.as_ref()))
.max()
.unwrap_or(0)
+ 1;
layout.col(opts(), |layout| {
for (key, value) in rows {
let padding = max_width - UnicodeWidthStr::width(key.as_ref());
layout.row(opts(), |layout| {
layout_line(layout, key, key_style);
repeat_chars(layout, padding, SPACES, Style::new());
layout.row(opts(), |layout| match value {
MenuValue::Text(text) => layout_span(layout, (text, Style::new())),
MenuValue::Arg(arg) => {
layout_span(layout, (arg.display.into(), Style::new()));
layout_span(layout, (" (".into(), Style::new()));
layout_span(
layout,
(
arg.get_cli_token().into(),
if arg.is_active() {
Style::from(&config.style.menu.active_arg)
} else {
Style::from(&config.style.menu.inactive_arg)
},
),
);
layout_span(layout, (")".into(), Style::new()));
}
});
});
}
});
}