use crate::model::{ReadyState, RecordChanges};
use crate::{model, selection, to_record};
use iced::keyboard::key::Named;
use inflorescence_iced_widget::{dir_picker, nav_scrollable};
use libflorescence::{diff, repo};
use iced::keyboard::{Key, Modifiers as Mods};
#[derive(Debug, Clone, Hash)]
pub struct Binding {
pub keys_str: &'static str,
pub keys: ModKeys,
pub label: &'static str,
pub msg: Option<FilteredMsg>,
}
#[derive(Debug, Clone, Hash)]
pub enum ModKeys {
One(ModKey),
Two(ModKey, ModKey),
}
#[derive(Debug, Clone, Hash)]
pub struct ModKey {
pub key: Key,
pub mods: Mods,
}
#[derive(Debug, Clone, Hash)]
pub enum FilteredMsg {
Confirm,
Cancel,
Selection(selection::Msg),
PostponeRecord,
SaveRecord,
DiscardRecord,
ToggleRecursive,
RmChange,
StartRecord,
SelectChannel,
ForkChannel,
RefreshRepo,
ShowEntireLog,
FocusNext,
FocusPrev,
ClipboardCopy,
ToggleReports,
ClipboardCopyReports,
ToRecord(to_record::Msg),
ToRecordToggleSelectedFileOrChange,
EnterSubMenu(model::SubMenu),
SubMenuPushOption(model::PushOption),
SubMenuPullOption(model::PullOption),
SubMenuCompareRemoteOption(model::CompareRemoteOption),
ReloadIdentity,
}
pub fn is_allowed(allowed_actions: &[Binding], msg: &FilteredMsg) -> bool {
allowed_actions.iter().any(
|Binding {
msg: allowed_msg, ..
}| {
if let Some(allowed_msg) = allowed_msg {
is_same_msg(msg, allowed_msg)
} else {
false
}
},
)
}
pub fn is_same_msg(left: &FilteredMsg, right: &FilteredMsg) -> bool {
use FilteredMsg::*;
match (left, right) {
(Confirm, Confirm) => true,
(Cancel, Cancel) => true,
(Selection(left), Selection(right)) => left == right,
(PostponeRecord, PostponeRecord) => true,
(SaveRecord, SaveRecord) => true,
(DiscardRecord, DiscardRecord) => true,
(ToggleRecursive, ToggleRecursive) => true,
(RmChange, RmChange) => true,
(StartRecord, StartRecord) => true,
(SelectChannel, SelectChannel) => true,
(ForkChannel, ForkChannel) => true,
(RefreshRepo, RefreshRepo) => true,
(ShowEntireLog, ShowEntireLog) => true,
(FocusNext, FocusNext) => true,
(FocusPrev, FocusPrev) => true,
(ClipboardCopy, ClipboardCopy) => true,
(ToggleReports, ToggleReports) => true,
(ClipboardCopyReports, ClipboardCopyReports) => true,
(ToRecord(left), ToRecord(right)) => left == right,
(
ToRecordToggleSelectedFileOrChange,
ToRecordToggleSelectedFileOrChange,
) => true,
(
ToRecord(to_record::Msg::ToggleFile { path: _ })
| ToRecord(to_record::Msg::ToggleChange {
path: _,
diff_id: _,
}),
ToRecordToggleSelectedFileOrChange,
) => true,
(
ToRecordToggleSelectedFileOrChange,
ToRecord(to_record::Msg::ToggleFile { path: _ })
| ToRecord(to_record::Msg::ToggleChange {
path: _,
diff_id: _,
}),
) => true,
(EnterSubMenu(left), EnterSubMenu(right)) => {
core::mem::discriminant(left) == core::mem::discriminant(right)
}
(SubMenuPushOption(left), SubMenuPushOption(right)) => {
core::mem::discriminant(left) == core::mem::discriminant(right)
}
(SubMenuPullOption(left), SubMenuPullOption(right)) => {
core::mem::discriminant(left) == core::mem::discriminant(right)
}
(
SubMenuCompareRemoteOption(left),
SubMenuCompareRemoteOption(right),
) => core::mem::discriminant(left) == core::mem::discriminant(right),
(ReloadIdentity, ReloadIdentity) => true,
(Confirm, _) => false,
(Cancel, _) => false,
(Selection(_), _) => false,
(PostponeRecord, _) => false,
(SaveRecord, _) => false,
(DiscardRecord, _) => false,
(ToggleRecursive, _) => false,
(RmChange, _) => false,
(StartRecord, _) => false,
(SelectChannel, _) => false,
(ForkChannel, _) => false,
(RefreshRepo, _) => false,
(ShowEntireLog, _) => false,
(FocusNext, _) => false,
(FocusPrev, _) => false,
(ClipboardCopy, _) => false,
(ToggleReports, _) => false,
(ClipboardCopyReports, _) => false,
(ToRecord(_), _) => false,
(ToRecordToggleSelectedFileOrChange, _) => false,
(EnterSubMenu(_), _) => false,
(SubMenuPushOption(_), _) => false,
(SubMenuPullOption(_), _) => false,
(SubMenuCompareRemoteOption(_), _) => false,
(ReloadIdentity, _) => false,
}
}
impl ModKeys {
pub fn iter(&self) -> ModKeysIter<'_> {
ModKeysIter { keys: self, ix: 0 }
}
}
pub struct ModKeysIter<'a> {
keys: &'a ModKeys,
ix: usize,
}
impl<'a> Iterator for ModKeysIter<'a> {
type Item = &'a ModKey;
fn next(&mut self) -> Option<Self::Item> {
let Self { keys, ix } = self;
match keys {
ModKeys::One(mod_key) => {
if *ix == 0 {
*ix += 1;
Some(mod_key)
} else {
None
}
}
ModKeys::Two(mod_key_0, mod_key_1) => {
if *ix == 0 {
*ix += 1;
Some(mod_key_0)
} else if *ix == 1 {
*ix += 1;
Some(mod_key_1)
} else {
None
}
}
}
}
}
pub fn update_allowed_actions(state: &mut model::State) {
let mut bindings = if let Some(sub_menu) = &state.sub_menu {
match sub_menu {
model::SubMenu::Push { remote: _, opt } => {
if opt.is_none() {
vec![
Binding {
keys_str: "Enter | S-p",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::Enter),
mods: Mods::NONE,
},
ModKey {
key: Key::Character("p".into()),
mods: Mods::SHIFT,
},
),
label: "confirm push",
msg: Some(FilteredMsg::Confirm),
},
select_remote(Some(FilteredMsg::SubMenuPushOption(
model::PushOption::SelectingRemote { remote: None },
))),
cancel(),
]
} else {
vec![
confirm("confirm remote selection"),
down(),
up(),
cancel(),
]
}
}
model::SubMenu::Pull { remote: _, opt } => {
if opt.is_none() {
vec![
Binding {
keys_str: "Enter | S-f",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::Enter),
mods: Mods::NONE,
},
ModKey {
key: Key::Character("f".into()),
mods: Mods::SHIFT,
},
),
label: "confirm pull",
msg: Some(FilteredMsg::Confirm),
},
select_remote(Some(FilteredMsg::SubMenuPullOption(
model::PullOption::SelectingRemote { remote: None },
))),
cancel(),
]
} else {
vec![
confirm("confirm remote selection"),
down(),
up(),
cancel(),
]
}
}
model::SubMenu::ResetChange => {
vec![confirm("confirm reset selection"), cancel()]
}
model::SubMenu::InitRepo { path: _ } => {
vec![confirm("confirm init repo"), cancel()]
}
model::SubMenu::ImportFromGit { path: _ } => {
vec![confirm("confirm import Git repo"), cancel()]
}
model::SubMenu::Add { recursive } => {
vec![
confirm("confirm add"),
toggle_recursive(*recursive),
cancel(),
]
}
model::SubMenu::CompareRemote {
remote: _,
remote_channel: _,
opt,
} => {
if let Some(opt) = opt {
let confirm_label = match opt {
model::CompareRemoteOption::SelectingRemote {
..
} => "confirm remote selection",
model::CompareRemoteOption::InputingRemoteChannel {
..
} => "confirm channel input",
};
vec![confirm(confirm_label), down(), up(), cancel()]
} else {
vec![
Binding {
keys_str: "Enter | S-c",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::Enter),
mods: Mods::NONE,
},
ModKey {
key: Key::Character("c".into()),
mods: Mods::SHIFT,
},
),
label: "compare remote",
msg: Some(FilteredMsg::Confirm),
},
select_remote(Some(
FilteredMsg::SubMenuCompareRemoteOption(
model::CompareRemoteOption::SelectingRemote {
remote: None,
},
),
)),
select_remote_channel(Some(
FilteredMsg::SubMenuCompareRemoteOption(
model::CompareRemoteOption::InputingRemoteChannel {
channel: None,
},
),
)),
cancel(),
]
}
}
}
} else {
match &state.sub {
model::SubState::PickingRepoDir(sub) => {
get_allowed_in_picking_repo_dir(sub)
}
model::SubState::ManagingRepo(sub) => {
get_allowed_in_managing_repo(&sub.sub)
}
model::SubState::PickingProject(sub) => {
get_allowed_in_picking_project(sub)
}
}
};
if !state.report.entries.is_empty() {
let label = if !state.report.hidden {
"hide reports"
} else {
"show reports"
};
bindings.push(Binding {
keys_str: "S-r",
keys: ModKeys::One(ModKey {
key: Key::Character("r".into()),
mods: Mods::SHIFT,
}),
label,
msg: Some(FilteredMsg::ToggleReports),
});
if !state.report.hidden {
bindings.push(Binding {
keys_str: "S-C-c",
keys: ModKeys::One(ModKey {
key: Key::Character("c".into()),
mods: Mods::SHIFT.union(Mods::CTRL),
}),
label: "copy reports",
msg: Some(FilteredMsg::ClipboardCopyReports),
});
}
}
state.allowed_actions = bindings;
}
fn get_allowed_in_picking_project(
state: &model::PickingProject,
) -> Vec<Binding> {
let model::PickingProject {
projects,
is_blocking: _,
selection: _,
projects_nav: _,
} = state;
let mut actions = vec![];
let ma = &mut actions;
if projects.is_some() {
push(|| confirm("confirm selection"), ma);
push(down, ma);
push(up, ma);
}
actions
}
fn get_allowed_in_picking_repo_dir(
state: &model::PickingRepoDir,
) -> Vec<Binding> {
let model::PickingRepoDir {
picker:
dir_picker::State {
current_kind,
matched_child_dirs,
child_dirs,
found_repos_dirs_pijul,
found_repos_dirs_git,
selection,
..
},
waiting_to_init,
} = state;
let down = || Binding {
keys_str: "C-(↓| j)",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowDown),
mods: Mods::CTRL,
},
ModKey {
key: Key::Character("j".into()),
mods: Mods::CTRL,
},
),
label: "down",
msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
selection::Dir::Down,
))),
};
let up = || Binding {
keys_str: "C-(↑| k)",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowUp),
mods: Mods::CTRL,
},
ModKey {
key: Key::Character("k".into()),
mods: Mods::CTRL,
},
),
label: "up",
msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
selection::Dir::Up,
))),
};
let left = || Binding {
keys_str: "C-(←| h)",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowLeft),
mods: Mods::CTRL,
},
ModKey {
key: Key::Character("h".into()),
mods: Mods::CTRL,
},
),
label: "left",
msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
selection::Dir::Left,
))),
};
let right = || Binding {
keys_str: "C-(→| l)",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowRight),
mods: Mods::CTRL,
},
ModKey {
key: Key::Character("l".into()),
mods: Mods::CTRL,
},
),
label: "right",
msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
selection::Dir::Right,
))),
};
let mut actions = vec![];
let ma = &mut actions;
if waiting_to_init.is_none() {
let can_down_or_up = match selection {
dir_picker::Selection::Input => {
!matched_child_dirs.is_empty() || !child_dirs.is_empty()
}
dir_picker::Selection::SubDir(_) => true,
dir_picker::Selection::ProjectPijul(_)
| dir_picker::Selection::ProjectGit(_) => {
!found_repos_dirs_pijul.is_empty()
|| !found_repos_dirs_git.is_empty()
}
};
push(
|| {
confirm(match current_kind {
Some(dir_picker::RepoKind::Pijul) => {
"open Pijul repository"
}
Some(dir_picker::RepoKind::Git) => "import from Git",
None => "initialize new repository",
})
},
ma,
);
push_if(can_down_or_up, down, ma);
push_if(can_down_or_up, up, ma);
push_if(
matches!(
selection,
dir_picker::Selection::ProjectPijul(_)
| dir_picker::Selection::ProjectGit(_)
),
left,
ma,
);
push_if(
matches!(
selection,
dir_picker::Selection::Input | dir_picker::Selection::SubDir(_)
),
right,
ma,
);
push(focus_next, ma);
}
actions
}
fn get_allowed_in_managing_repo(
state: &model::ManagingRepoSubState,
) -> Vec<Binding> {
match state {
model::ManagingRepoSubState::Loading { .. } => vec![],
model::ManagingRepoSubState::SelectingIdentity { .. } => {
vec![confirm("confirm"), down(), up()]
}
model::ManagingRepoSubState::Ready(ready_state) => {
get_ready_allowed(ready_state)
}
model::ManagingRepoSubState::NoIdFound { .. } => vec![Binding {
keys_str: "r",
keys: ModKeys::One(ModKey {
key: Key::Character("r".into()),
mods: Mods::NONE,
}),
label: "Reload identity",
msg: Some(FilteredMsg::ReloadIdentity),
}],
}
}
fn get_ready_allowed(state: &ReadyState) -> Vec<Binding> {
let State {
has_other_channels,
has_default_remote,
sub,
} = derive_state(state);
let down_no_skip = || Binding {
keys_str: "C-(↓| j)",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowDown),
mods: Mods::CTRL,
},
ModKey {
key: Key::Character("j".into()),
mods: Mods::CTRL,
},
),
label: "no skip",
msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
selection::Dir::Down,
))),
};
let up_no_skip = || Binding {
keys_str: "C-(↑| k)",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowUp),
mods: Mods::CTRL,
},
ModKey {
key: Key::Character("k".into()),
mods: Mods::CTRL,
},
),
label: "no skip",
msg: Some(FilteredMsg::Selection(selection::Msg::AltPressDir(
selection::Dir::Up,
))),
};
let add_untracked = || Binding {
keys_str: "a",
keys: ModKeys::One(ModKey {
key: Key::Character("a".into()),
mods: Mods::NONE,
}),
label: "track file",
msg: Some(FilteredMsg::EnterSubMenu(model::SubMenu::Add {
recursive: false,
})),
};
let reset_changed_file = || Binding {
keys_str: "x",
keys: ModKeys::One(ModKey {
key: Key::Character("x".into()),
mods: Mods::NONE,
}),
label: "reset file",
msg: Some(FilteredMsg::RmChange),
};
let reset_changed_hunk = || Binding {
keys_str: "x",
keys: ModKeys::One(ModKey {
key: Key::Character("x".into()),
mods: Mods::NONE,
}),
label: "reset hunk",
msg: Some(FilteredMsg::RmChange),
};
let rm_added_file = || Binding {
keys_str: "x",
keys: ModKeys::One(ModKey {
key: Key::Character("x".into()),
mods: Mods::NONE,
}),
label: "untrack file",
msg: Some(FilteredMsg::RmChange),
};
let start_record = || Binding {
keys_str: "r",
keys: ModKeys::One(ModKey {
key: Key::Character("r".into()),
mods: Mods::NONE,
}),
label: "record",
msg: Some(FilteredMsg::StartRecord),
};
let save_record = || Binding {
keys_str: "C-s",
keys: ModKeys::One(ModKey {
key: Key::Character("s".into()),
mods: Mods::CTRL,
}),
label: "save record",
msg: Some(FilteredMsg::SaveRecord),
};
let postpone_record = || Binding {
keys_str: "C-p",
keys: ModKeys::One(ModKey {
key: Key::Character("p".into()),
mods: Mods::CTRL,
}),
label: "postpone record",
msg: Some(FilteredMsg::PostponeRecord),
};
let discard_record = || Binding {
keys_str: "C-d",
keys: ModKeys::One(ModKey {
key: Key::Character("d".into()),
mods: Mods::CTRL,
}),
label: "discard record",
msg: Some(FilteredMsg::DiscardRecord),
};
let select_channel = || Binding {
keys_str: "c",
keys: ModKeys::One(ModKey {
key: Key::Character("c".into()),
mods: Mods::NONE,
}),
label: "select channel",
msg: Some(FilteredMsg::SelectChannel),
};
let fork_channel = || Binding {
keys_str: "f",
keys: ModKeys::One(ModKey {
key: Key::Character("f".into()),
mods: Mods::NONE,
}),
label: "fork channel",
msg: Some(FilteredMsg::ForkChannel),
};
let refresh_repo = || Binding {
keys_str: "C-r",
keys: ModKeys::One(ModKey {
key: Key::Character("r".into()),
mods: Mods::CTRL,
}),
label: "refresh repo",
msg: Some(FilteredMsg::RefreshRepo),
};
let cant_confirm = |label: &'static str| Binding {
keys_str: "Enter",
keys: ModKeys::One(ModKey {
key: Key::Named(Named::Enter),
mods: Mods::NONE,
}),
label,
msg: None,
};
let exit_entire_log = || Binding {
keys_str: "Esc",
keys: ModKeys::One(ModKey {
key: Key::Named(Named::Escape),
mods: Mods::NONE,
}),
label: "exit entire log",
msg: Some(FilteredMsg::Cancel),
};
let exit_other_channels = || Binding {
keys_str: "Esc",
keys: ModKeys::One(ModKey {
key: Key::Named(Named::Escape),
mods: Mods::NONE,
}),
label: "exit channel selection",
msg: Some(FilteredMsg::Cancel),
};
let show_entire_log = || Binding {
keys_str: "e",
keys: ModKeys::One(ModKey {
key: Key::Character("e".into()),
mods: Mods::NONE,
}),
label: "entire log",
msg: Some(FilteredMsg::ShowEntireLog),
};
let clipboard_copy_change_hash = || Binding {
keys_str: "C-c",
keys: ModKeys::One(ModKey {
key: Key::Character("c".into()),
mods: Mods::CTRL,
}),
label: "copy hash",
msg: Some(FilteredMsg::ClipboardCopy),
};
let to_record_toggle_overall = |next: to_record::PickSet| {
let label = match next {
to_record::PickSet::Include => "include all files",
to_record::PickSet::Exclude => "exclude all files",
to_record::PickSet::Partial => "partial files",
};
Binding {
keys_str: "S-t",
keys: ModKeys::One(ModKey {
key: Key::Character("t".into()),
mods: Mods::SHIFT,
}),
label,
msg: Some(FilteredMsg::ToRecord(to_record::Msg::ToggleOverall)),
}
};
let to_record_toggle_file = |next: to_record::PickSet| {
let label = match next {
to_record::PickSet::Include => "include selected file",
to_record::PickSet::Exclude => "exclude selected file",
to_record::PickSet::Partial => "partial selected file",
};
Binding {
keys_str: "t",
keys: ModKeys::One(ModKey {
key: Key::Character("t".into()),
mods: Mods::NONE,
}),
label,
msg: Some(FilteredMsg::ToRecordToggleSelectedFileOrChange),
}
};
let to_record_toggle_file_change = |next: to_record::Pick| {
let label = match next {
to_record::Pick::Include => "include selected change",
to_record::Pick::Exclude => "exclude selected change",
};
Binding {
keys_str: "t",
keys: ModKeys::One(ModKey {
key: Key::Character("t".into()),
mods: Mods::NONE,
}),
label,
msg: Some(FilteredMsg::ToRecordToggleSelectedFileOrChange),
}
};
let push_sub_menu = |can_push: bool| -> Binding {
Binding {
keys_str: "S-p",
keys: ModKeys::One(ModKey {
key: Key::Character("p".into()),
mods: Mods::SHIFT,
}),
label: "push",
msg: can_push.then_some(FilteredMsg::EnterSubMenu(
model::SubMenu::Push {
remote: None,
opt: None,
},
)),
}
};
let pull_sub_menu = |can_pull: bool| -> Binding {
Binding {
keys_str: "S-f",
keys: ModKeys::One(ModKey {
key: Key::Character("f".into()),
mods: Mods::SHIFT,
}),
label: "pull",
msg: can_pull.then_some(FilteredMsg::EnterSubMenu(
model::SubMenu::Pull {
remote: None,
opt: None,
},
)),
}
};
let compare_remote = || -> Binding {
Binding {
keys_str: "S-c",
keys: ModKeys::One(ModKey {
key: Key::Character("c".into()),
mods: Mods::SHIFT,
}),
label: "compare remote",
msg: Some(FilteredMsg::EnterSubMenu(
model::SubMenu::CompareRemote {
remote: None,
remote_channel: None,
opt: None,
},
)),
}
};
let mut actions = vec![];
let ma = &mut actions;
match sub {
SubState::Main {
selection,
can_select_right,
can_record,
next_to_record_pick,
next_to_record_file_pick,
can_push_pull,
} => {
push(down, ma);
push(up, ma);
if let Some(selection) = selection {
match selection {
StatusSelection::Untracked => {
push(add_untracked, ma);
push_if(can_select_right, right, ma);
}
StatusSelection::Changed => {
push(right, ma);
push(reset_changed_file, ma);
}
StatusSelection::AddedFromUntracked => {
push(rm_added_file, ma);
push_if(can_select_right, right, ma);
}
StatusSelection::LogChange => {
push_if(can_select_right, right, ma);
push(clipboard_copy_change_hash, ma);
}
}
}
push_if(can_record, start_record, ma);
push(show_entire_log, ma);
push_if(has_other_channels, select_channel, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
push_if_some(next_to_record_pick, to_record_toggle_overall, ma);
push_if_some(next_to_record_file_pick, to_record_toggle_file, ma);
push(|| push_sub_menu(can_push_pull), ma);
push(|| pull_sub_menu(can_push_pull), ma);
push_if(has_default_remote, compare_remote, ma);
}
SubState::StatusLogChange {
can_select_right,
can_record,
} => {
push(left, ma);
push(down, ma);
push(up, ma);
push_if(can_select_right, right, ma);
push_if(can_record, start_record, ma);
push(clipboard_copy_change_hash, ma);
push(show_entire_log, ma);
push_if(has_other_channels, select_channel, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
push_if(has_default_remote, compare_remote, ma);
}
SubState::StatusLogDiff {
can_record,
can_skip_navigate,
can_reset_hunk,
next_to_record_file_pick,
next_to_record_file_change_pick,
} => {
push(left, ma);
push(down, ma);
push(up, ma);
push_if(can_skip_navigate, down_no_skip, ma);
push_if(can_skip_navigate, up_no_skip, ma);
{
let _ = (can_reset_hunk, reset_changed_hunk);
}
push(clipboard_copy_change_hash, ma);
push_if(can_record, start_record, ma);
push(show_entire_log, ma);
push_if(has_other_channels, select_channel, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
let _ = next_to_record_file_pick;
push_if_some(
next_to_record_file_change_pick,
to_record_toggle_file_change,
ma,
);
push_if(has_default_remote, compare_remote, ma);
}
SubState::Recording => {
push(focus_next, ma);
push(focus_prev, ma);
push(save_record, ma);
push(postpone_record, ma);
push(discard_record, ma);
push_if(has_default_remote, compare_remote, ma);
}
SubState::SelectingChannel(state) => {
match state {
SelectingChannelState::NoOtherChannels => push(cancel, ma),
SelectingChannelState::NothingSelected => {
push(down, ma);
push(up, ma);
push(cancel, ma);
}
SelectingChannelState::SomethingSelected {
can_switch,
can_select_right,
} => {
push(down, ma);
push(up, ma);
push_if(can_select_right, right, ma);
push(cancel, ma);
push_if(
!can_switch,
|| {
cant_confirm(
"cannot switch with unrecorded changes",
)
},
ma,
);
push_if(can_switch, || confirm("switch channel"), ma);
}
};
push(show_entire_log, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
}
SubState::OtherChannelLog { can_select_right } => {
push(left, ma);
push(down, ma);
push(up, ma);
push_if(can_select_right, right, ma);
push(clipboard_copy_change_hash, ma);
push(exit_other_channels, ma);
push(show_entire_log, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
}
SubState::OtherChannelLogChange { can_select_right } => {
push(left, ma);
push(down, ma);
push(up, ma);
push_if(can_select_right, right, ma);
push(clipboard_copy_change_hash, ma);
push(exit_other_channels, ma);
push(show_entire_log, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
}
SubState::OtherChannelLogChangeDiff => {
push(left, ma);
push(down, ma);
push(up, ma);
push(down_no_skip, ma);
push(up_no_skip, ma);
push(clipboard_copy_change_hash, ma);
push(exit_other_channels, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
}
SubState::ForkingChannel { empty, unique } => {
push_if(!empty && unique, || confirm("confirm fork"), ma);
push_if(!unique, || cant_confirm("channel already exists"), ma);
push(cancel, ma);
}
SubState::EntireLog { can_select_right } => {
push(down, ma);
push(up, ma);
push_if(can_select_right, right, ma);
push(clipboard_copy_change_hash, ma);
push(exit_entire_log, ma);
push_if(has_other_channels, select_channel, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
}
SubState::EntireLogChange { can_select_right } => {
push(left, ma);
push(down, ma);
push(up, ma);
push_if(can_select_right, right, ma);
push(clipboard_copy_change_hash, ma);
push(exit_entire_log, ma);
push_if(has_other_channels, select_channel, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
}
SubState::EntireLogChangeDiff => {
push(left, ma);
push(down, ma);
push(up, ma);
push(down_no_skip, ma);
push(up_no_skip, ma);
push(clipboard_copy_change_hash, ma);
push(exit_entire_log, ma);
push(fork_channel, ma);
push(refresh_repo, ma);
}
SubState::CompareRemote {
can_select_right,
has_any_diff,
} => {
push_if(has_any_diff, down, ma);
push_if(has_any_diff, up, ma);
push_if(has_any_diff && can_select_right, right, ma);
push_if(has_any_diff, clipboard_copy_change_hash, ma);
push(cancel, ma);
}
SubState::CompareRemoteChange { can_select_right } => {
push(left, ma);
push(down, ma);
push(up, ma);
push_if(can_select_right, right, ma);
push(clipboard_copy_change_hash, ma);
push(cancel, ma);
}
SubState::CompareRemoteChangeDiff => {
push(left, ma);
push(down, ma);
push(up, ma);
push(clipboard_copy_change_hash, ma);
push(cancel, ma);
}
};
actions
}
fn push<F>(to_add: F, actions: &mut Vec<Binding>)
where
F: Fn() -> Binding,
{
actions.push(to_add())
}
fn push_if<F>(predicate: bool, to_add: F, actions: &mut Vec<Binding>)
where
F: Fn() -> Binding,
{
if predicate {
actions.push(to_add())
}
}
fn push_if_some<T, F>(
predicate: Option<T>,
to_add: F,
actions: &mut Vec<Binding>,
) where
F: Fn(T) -> Binding,
{
if let Some(value) = predicate {
actions.push(to_add(value))
}
}
#[derive(Debug, Clone, Copy)]
struct State {
has_other_channels: bool,
has_default_remote: bool,
sub: SubState,
}
#[derive(Debug, Clone, Copy)]
enum SubState {
Main {
selection: Option<StatusSelection>,
can_select_right: bool,
can_record: bool,
next_to_record_pick: Option<to_record::PickSet>,
next_to_record_file_pick: Option<to_record::PickSet>,
can_push_pull: bool,
},
StatusLogChange {
can_select_right: bool,
can_record: bool,
},
StatusLogDiff {
can_record: bool,
can_skip_navigate: bool,
can_reset_hunk: bool,
next_to_record_file_pick: Option<to_record::PickSet>,
next_to_record_file_change_pick: Option<to_record::Pick>,
},
Recording,
SelectingChannel(SelectingChannelState),
OtherChannelLog {
can_select_right: bool,
},
OtherChannelLogChange {
can_select_right: bool,
},
OtherChannelLogChangeDiff,
ForkingChannel {
empty: bool,
unique: bool,
},
EntireLog {
can_select_right: bool,
},
EntireLogChange {
can_select_right: bool,
},
EntireLogChangeDiff,
CompareRemote {
can_select_right: bool,
has_any_diff: bool,
},
CompareRemoteChange {
can_select_right: bool,
},
CompareRemoteChangeDiff,
}
#[derive(Debug, Clone, Copy)]
enum StatusSelection {
Untracked,
AddedFromUntracked,
LogChange,
Changed,
}
#[derive(Debug, Clone, Copy)]
enum SelectingChannelState {
NoOtherChannels,
NothingSelected,
SomethingSelected {
can_switch: bool,
can_select_right: bool,
},
}
fn derive_state(state: &ReadyState) -> State {
let has_other_channels = !state.repo.other_channels.is_empty();
let has_default_remote = state.repo.remotes.default.is_some();
let sub = derive_sub_state(state);
State {
has_other_channels,
has_default_remote,
sub,
}
}
fn derive_sub_state(state: &ReadyState) -> SubState {
let ReadyState {
user_id: _,
repo:
repo::State {
dir_name: _,
channel,
other_channels,
untracked_files: _,
changed_files,
short_log: _,
remotes: _,
},
selection,
navigation,
record_changes,
forking_channel_name,
logs,
to_record,
jobs,
record_dichotomy,
} = state;
let selection::State {
primary,
status: status_selection,
channel: channel_selection,
entire_log: entire_log_selection,
compare_remote: compare_remote_selection,
held_key: _,
} = selection;
match record_changes {
Some(RecordChanges::Typing { .. }) => return SubState::Recording,
Some(RecordChanges::Canceled { .. }) | None => {
if let Some(name) = forking_channel_name.as_ref() {
let name = name.trim();
let empty = name.is_empty();
let unique = channel != name
&& !other_channels.iter().any(|n| n == name);
return SubState::ForkingChannel { empty, unique };
}
}
}
let can_push_pull = jobs.iter().all(|job| match job {
model::Job::Pull {
remote: _,
channel: c,
}
| model::Job::Push {
remote: _,
channel: c,
} => c != channel,
model::Job::CompareRemote { .. } => true,
});
match primary {
selection::Primary::Status => {
let can_record = !changed_files.is_empty();
let next_to_record_pick = can_record.then(|| {
to_record::next_overall_pick(to_record, changed_files)
});
match status_selection {
Some(selection::Status::UntrackedFile {
ix: _,
path: _,
diff_selected,
}) => {
let next_to_record_file_pick = None;
if *diff_selected {
SubState::StatusLogDiff {
can_record,
can_skip_navigate: false,
can_reset_hunk: false,
next_to_record_file_pick,
next_to_record_file_change_pick: None,
}
} else {
let diffs_nav = &navigation.files_diffs.diffs_nav;
let can_select_right =
nav_scrollable::needs_scrolling(diffs_nav);
SubState::Main {
selection: Some(StatusSelection::Untracked),
can_select_right,
can_record,
next_to_record_pick,
next_to_record_file_pick,
can_push_pull,
}
}
}
Some(selection::Status::ChangedFile {
ix: _,
path,
diff_selected,
}) => {
let is_added_from_untracked = changed_files
.get(path)
.map(|diffs| {
diffs.iter().any(|diff| {
matches!(
diff,
repo::ChangedFileDiff::Add { .. }
)
})
})
.unwrap_or_default();
let main_selection = || {
if is_added_from_untracked {
StatusSelection::AddedFromUntracked
} else {
StatusSelection::Changed
}
};
let next_to_record_file_pick =
Some(to_record::next_file_pick(path, to_record));
if *diff_selected {
let diff_ix = navigation
.files_diffs
.diffs_nav
.get_selected_section_ix();
let selected_diff = diff_ix.and_then(|diff_ix| {
changed_files
.get(path)
.and_then(|diffs| diffs.iter().nth(diff_ix))
});
let next_to_record_file_change_pick = selected_diff
.map(|selected_diff| {
to_record::next_file_change_pick(
path,
diff::id_parts_hash(selected_diff),
to_record,
)
});
SubState::StatusLogDiff {
can_record,
can_skip_navigate: true,
can_reset_hunk: true,
next_to_record_file_pick,
next_to_record_file_change_pick,
}
} else {
let diffs_nav = &navigation.files_diffs.diffs_nav;
let can_select_right =
nav_scrollable::needs_scrolling(diffs_nav);
SubState::Main {
selection: Some(main_selection()),
can_select_right,
can_record,
next_to_record_pick,
next_to_record_file_pick,
can_push_pull,
}
}
}
Some(selection::Status::LogChange(selection::LogChange {
ix: _,
hash: _,
message: _,
file,
})) => match file {
Some(selection::LogChangeFileSelection {
ix: _,
path: _,
diff_selected,
}) => {
if *diff_selected {
let next_to_record_file_pick = None;
SubState::StatusLogDiff {
can_record,
can_skip_navigate: true,
can_reset_hunk: false,
next_to_record_file_pick,
next_to_record_file_change_pick: None,
}
} else {
let diffs_nav =
&navigation.status_logs_navs.diffs_nav;
let can_select_right =
nav_scrollable::needs_scrolling(diffs_nav);
SubState::StatusLogChange {
can_record,
can_select_right,
}
}
}
None => {
let files_nav = &navigation.status_logs_navs.files_nav;
let can_select_right =
nav_scrollable::has_sections(files_nav);
SubState::Main {
selection: Some(StatusSelection::LogChange),
can_select_right,
can_record,
next_to_record_pick,
next_to_record_file_pick: None,
can_push_pull,
}
}
},
None => SubState::Main {
selection: None,
can_select_right: false,
can_record,
next_to_record_pick,
next_to_record_file_pick: None,
can_push_pull,
},
}
}
selection::Primary::Channel => match channel_selection {
Some(selection::Channel {
log: None, name, ..
}) => {
let sub_state = if other_channels.is_empty() {
SelectingChannelState::NoOtherChannels
} else {
let log = logs.other_channels_logs.get(name);
let can_switch = changed_files.is_empty();
let can_select_right = log.is_some();
SelectingChannelState::SomethingSelected {
can_switch,
can_select_right,
}
};
SubState::SelectingChannel(sub_state)
}
Some(selection::Channel {
log: Some(selection::LogChange { file: None, .. }),
..
}) => {
let files_nav = &navigation.other_channel_logs_navs.files_nav;
let can_select_right = nav_scrollable::has_sections(files_nav);
SubState::OtherChannelLog { can_select_right }
}
Some(selection::Channel {
log:
Some(selection::LogChange {
file:
Some(selection::LogChangeFileSelection {
diff_selected,
..
}),
..
}),
..
}) => {
if *diff_selected {
SubState::OtherChannelLogChangeDiff
} else {
let diffs_nav =
&navigation.other_channel_logs_navs.diffs_nav;
let can_select_right =
nav_scrollable::needs_scrolling(diffs_nav);
SubState::OtherChannelLogChange { can_select_right }
}
}
None => SubState::SelectingChannel(
SelectingChannelState::NothingSelected,
),
},
selection::Primary::EntireLog => {
if let Some(selection::LogChange { file, .. }) =
entire_log_selection
{
match file {
Some(selection::LogChangeFileSelection {
diff_selected,
..
}) => {
if *diff_selected {
SubState::EntireLogChangeDiff
} else {
let diffs_nav =
&navigation.entire_logs_navs.diffs_nav;
let can_select_right =
nav_scrollable::needs_scrolling(diffs_nav);
SubState::EntireLogChange { can_select_right }
}
}
None => {
let files_nav = &navigation.entire_logs_navs.files_nav;
let can_select_right =
nav_scrollable::has_sections(files_nav);
SubState::EntireLog { can_select_right }
}
}
} else {
SubState::EntireLog {
can_select_right: false,
}
}
}
selection::Primary::CompareRemote => {
if let Some(selection::CompareRemote {
ix: _,
hash: _,
file,
remote,
remote_channel,
}) = compare_remote_selection
{
let has_any_diff = model::get_record_dichotomy(
record_dichotomy,
remote,
remote_channel,
)
.map(|d| !d.is_empty())
.unwrap_or(false);
match file {
Some(selection::LogChangeFileSelection {
diff_selected,
..
}) => {
if *diff_selected {
SubState::CompareRemoteChangeDiff
} else {
let diffs_nav =
&navigation.compare_remote_navs.diffs_nav;
let can_select_right =
nav_scrollable::needs_scrolling(diffs_nav);
SubState::CompareRemoteChange { can_select_right }
}
}
None => {
let files_nav =
&navigation.compare_remote_navs.files_nav;
let can_select_right =
nav_scrollable::has_sections(files_nav);
SubState::CompareRemote {
can_select_right,
has_any_diff,
}
}
}
} else {
SubState::CompareRemote {
has_any_diff: false,
can_select_right: false,
}
}
}
}
}
fn left() -> Binding {
Binding {
keys_str: "←| h",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowLeft),
mods: Mods::NONE,
},
ModKey {
key: Key::Character("h".into()),
mods: Mods::NONE,
},
),
label: "",
msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
selection::Dir::Left,
))),
}
}
fn right() -> Binding {
Binding {
keys_str: "→| l",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowRight),
mods: Mods::NONE,
},
ModKey {
key: Key::Character("l".into()),
mods: Mods::NONE,
},
),
label: "",
msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
selection::Dir::Right,
))),
}
}
fn down() -> Binding {
Binding {
keys_str: "↓| j",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowDown),
mods: Mods::NONE,
},
ModKey {
key: Key::Character("j".into()),
mods: Mods::NONE,
},
),
label: "",
msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
selection::Dir::Down,
))),
}
}
fn up() -> Binding {
Binding {
keys_str: "↑| k",
keys: ModKeys::Two(
ModKey {
key: Key::Named(Named::ArrowUp),
mods: Mods::NONE,
},
ModKey {
key: Key::Character("k".into()),
mods: Mods::NONE,
},
),
label: "",
msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(
selection::Dir::Up,
))),
}
}
fn confirm(label: &'static str) -> Binding {
Binding {
keys_str: "Enter",
keys: ModKeys::One(ModKey {
key: Key::Named(Named::Enter),
mods: Mods::NONE,
}),
label,
msg: Some(FilteredMsg::Confirm),
}
}
fn toggle_recursive(is_recursive: bool) -> Binding {
Binding {
keys_str: "r",
keys: ModKeys::One(ModKey {
key: Key::Character("r".into()),
mods: Mods::NONE,
}),
label: if is_recursive {
"add non-recursive"
} else {
"add recursive"
},
msg: Some(FilteredMsg::ToggleRecursive),
}
}
fn cancel() -> Binding {
Binding {
keys_str: "Esc",
keys: ModKeys::One(ModKey {
key: Key::Named(Named::Escape),
mods: Mods::NONE,
}),
label: "cancel",
msg: Some(FilteredMsg::Cancel),
}
}
fn focus_next() -> Binding {
Binding {
keys_str: "Tab",
keys: ModKeys::One(ModKey {
key: Key::Named(Named::Tab),
mods: Mods::NONE,
}),
label: "focus next",
msg: Some(FilteredMsg::FocusNext),
}
}
fn focus_prev() -> Binding {
Binding {
keys_str: "S-Tab",
keys: ModKeys::One(ModKey {
key: Key::Named(Named::Tab),
mods: Mods::SHIFT,
}),
label: "focus previous",
msg: Some(FilteredMsg::FocusPrev),
}
}
fn select_remote(msg: Option<FilteredMsg>) -> Binding {
Binding {
keys_str: "r",
keys: ModKeys::One(ModKey {
key: Key::Character("r".into()),
mods: Mods::NONE,
}),
label: "select remote",
msg,
}
}
fn select_remote_channel(msg: Option<FilteredMsg>) -> Binding {
Binding {
keys_str: "c",
keys: ModKeys::One(ModKey {
key: Key::Character("c".into()),
mods: Mods::NONE,
}),
label: "select remote channel",
msg,
}
}