mod diff;
mod file;
mod log;
mod selection;
#[cfg(test)]
mod test;
mod window;
use iced_utils::{task, Task};
use ignore::gitignore::Gitignore;
use inflorescence_iced_widget::{dir_picker, nav_scrollable, report};
use inflorescence_model::model::{
Job, Log, Logs, PickingProjectSelection, ReadyState, RecordChanges,
};
use inflorescence_model::{action, model, to_record};
use inflorescence_view::{view, Theme};
use libflorescence::identity::Id;
use libflorescence::prelude::*;
use libflorescence::{identity, repo, store, terrors, PijulConfig};
use iced::widget::text_editor;
use iced::{Element, Subscription};
use notify_debouncer_full::notify::{self, RecommendedWatcher, RecursiveMode};
use notify_debouncer_full::{
new_debouncer, DebounceEventResult, Debouncer, RecommendedCache,
};
use tokio::sync::{mpsc, watch};
use tokio::task::spawn_blocking;
use tokio::time::Duration;
use tokio_stream::wrappers::{UnboundedReceiverStream, WatchStream};
use std::path::PathBuf;
use std::sync::Arc;
use std::{cmp, env, mem};
pub fn main() -> iced::Result {
use core::str;
use std::str::FromStr;
use tracing_subscriber::EnvFilter;
const LOG_ENV: &str = "RUST_LOG";
let filter = std::env::var(LOG_ENV)
.map(|env| {
EnvFilter::from_str(&env).unwrap_or_else(|err| {
panic!("invalid `{LOG_ENV}` environment variable {err:?}")
})
})
.unwrap_or_else(|_| {
EnvFilter::new("inflorescence=info,libflorescence=info")
});
tracing_subscriber::fmt().with_env_filter(filter).init();
#[cfg(not(test))]
{
const PATH_ARG: &str = "repository_path";
let cli = clap::Command::new("Inflorescence")
.bin_name("inflorescence")
.version(clap::crate_version!())
.about("GUI for version control backed by Pijul")
.arg(clap::Arg::new(PATH_ARG).required(false).help(
"Repository path to manage, absolute or relative (optional)",
));
let matches = cli.get_matches();
let repo_path = matches
.get_one::<String>(PATH_ARG)
.map(|path| std::fs::canonicalize(path).unwrap());
iced_utils::daemon(move || init(repo_path.clone()), update, view)
.subscription(subs)
.theme(inflorescence_view::theme)
.title(title)
.run()
}
#[cfg(test)]
{
let _ = (update_managing_repo, view, subs, title);
Ok(())
}
}
#[derive(Debug)]
struct State {
model: model::State,
managing_repo: Option<ManagingRepo>,
}
#[derive(Debug)]
struct ManagingRepo {
repo_fs_watch: Option<Debouncer<RecommendedWatcher, RecommendedCache>>,
repo_tx_in: mpsc::UnboundedSender<repo::MsgIn>,
files: file::State,
ignore: Gitignore,
_repo_thread: std::thread::JoinHandle<()>,
}
#[derive(Debug)]
enum Msg {
View(view::Msg),
NoOp,
Window(window::Event),
ManagingRepo(ManagingRepoMsg),
ReadProjectsResult(Result<store::Projects, store::ProjectsFileErr>),
UpsertProjectResult(Result<(), (store::ProjectsFileErr, store::Project)>),
InitedRepo(anyhow::Result<PathBuf>),
}
#[derive(Debug)]
enum ManagingRepoMsg {
LoadedIdentities(anyhow::Result<Vec<Id>>),
ToRepo(repo::MsgIn),
FromRepo(repo::MsgOut),
File(file::Msg),
WatchedFileChange(PathBuf),
}
fn init(repo_path: Option<PathBuf>) -> (State, Task<Msg>) {
let window_settings = window::settings();
let window_size = window_settings.size;
let (window_id, open_window_task) = task::window_open(window_settings);
let set_icon_task = task::window_set_icon(
window_id,
include_bytes!("../../assets/icon.png").to_vec(),
);
let window_scale = 1.0;
let allowed_actions = default();
let sub_menu = None;
let mut report = report::Container::default();
if let Some(repo_path) = repo_path {
let (sub, managing_repo, managing_repo_task) =
init_managing_repo(repo_path, &mut report);
let task = Task::batch([
open_window_task.map(|_id| Msg::NoOp),
set_icon_task,
managing_repo_task,
]);
let model = model::State {
sub: model::SubState::ManagingRepo(sub),
window_size,
window_scale,
allowed_actions,
sub_menu,
report,
};
(
State {
model,
managing_repo: Some(managing_repo),
},
task,
)
} else {
let (sub, projects_task) = init_picking_project();
let model = model::State {
sub,
window_size,
window_scale,
allowed_actions,
sub_menu,
report,
};
let task = Task::batch([
open_window_task.map(|_id| Msg::NoOp),
set_icon_task,
projects_task,
]);
(
State {
model,
managing_repo: None,
},
task,
)
}
}
fn init_picking_project() -> (model::SubState, Task<Msg>) {
let read_projects_task = Task::perform(
async { store::read_projects().await },
Msg::ReadProjectsResult,
);
let state = model::PickingProject {
is_blocking: false,
projects: None,
selection: default(),
projects_nav: nav_scrollable::State::default(),
};
let sub = model::SubState::PickingProject(state);
(sub, read_projects_task)
}
fn init_picking_repo_dir() -> (model::PickingRepoDir, Task<Msg>) {
let start_path = env::home_dir().unwrap_or_else(|| PathBuf::from("/"));
let (picker, picker_task) = dir_picker::init(start_path);
let model = model::PickingRepoDir {
picker,
waiting_to_init: None,
};
let task = Task::batch([picker_task
.map(view::Msg::PickingRepoDir)
.map(Msg::View)]);
(model, task)
}
fn init_managing_repo(
repo_path: PathBuf,
report: &mut report::Container,
) -> (model::ManagingRepo, ManagingRepo, Task<Msg>) {
let (repo_thread, repo_task, repo_tx_in) =
start_thread_to_manage_repo(repo_path.clone());
let load_ids_task = load_identities_task();
let (files, files_task) = file::init(repo_path.clone());
let tasks = Task::batch([
repo_task.map(Msg::ManagingRepo),
load_ids_task.map(Msg::ManagingRepo),
files_task.map(ManagingRepoMsg::File).map(Msg::ManagingRepo),
]);
let (ignore, ignore_err) =
Gitignore::new(repo_path.join(repo::IGNORE_FILE));
if let Some(err) = ignore_err {
let msg = format!("Error loading .ignore file {err:?}");
error!("{msg}");
report::show_err(report, msg);
}
let sub = model::ManagingRepo {
repo_path,
sub: model::ManagingRepoSubState::Loading {
user_ids: None,
repo: None,
},
};
let managing_repo = ManagingRepo {
repo_fs_watch: None,
repo_tx_in,
files,
ignore,
_repo_thread: repo_thread,
};
(sub, managing_repo, tasks)
}
fn update(state: &mut State, msg: Msg) -> Task<Msg> {
let task = match msg {
Msg::NoOp => Task::none(),
Msg::View(msg) => match &mut state.model.sub {
model::SubState::PickingRepoDir(model) => {
let (task, new_sub_state) = update_picking_repo_dir_from_view(
model,
&mut state.model.sub_menu,
&mut state.model.report,
msg,
);
if let Some((new_sub, managing_repo)) = new_sub_state {
state.model.sub = model::SubState::ManagingRepo(new_sub);
state.managing_repo = Some(managing_repo);
}
task
}
model::SubState::ManagingRepo(model) => {
let sub = state.managing_repo.as_mut().unwrap();
update_managing_repo_from_view(
sub,
model,
&mut state.model.sub_menu,
&mut state.model.report,
&state.model.allowed_actions,
msg,
)
}
model::SubState::PickingProject(model) => {
let (task, new_sub_state, managing_repo) =
update_picking_project_from_view(
model,
msg,
&mut state.model.report,
);
if let Some(new_sub) = new_sub_state {
if let model::SubState::ManagingRepo(_) = new_sub {
state.managing_repo = Some(
managing_repo.expect("managing_repo is required"),
);
}
state.model.sub = new_sub;
}
task
}
},
Msg::Window(event) => update_from_window_event(&mut state.model, event)
.map(Msg::ManagingRepo),
Msg::ManagingRepo(msg) => {
if let model::SubState::ManagingRepo(model) = &mut state.model.sub {
let sub = state.managing_repo.as_mut().unwrap();
let (task, new_state) = update_managing_repo(
sub,
model,
&mut state.model.report,
msg,
);
if let Some(new_state) = new_state {
state.model.sub = new_state;
}
task
} else {
Task::none()
}
}
Msg::ReadProjectsResult(result) => match result {
Ok(projects) => {
if projects.is_empty() {
let (model, task) = init_picking_repo_dir();
let sub = model::SubState::PickingRepoDir(model);
state.model.sub = sub;
task
} else {
if let model::SubState::PickingProject(sub) =
&mut state.model.sub
{
sub.projects = Some(projects);
sub.is_blocking = false;
}
Task::none()
}
}
Err(err) => {
if let model::SubState::PickingProject(sub) =
&mut state.model.sub
{
match err.as_enum() {
terrors::E2::A(store::BlockingLockError) => {
sub.is_blocking = true;
Task::perform(
async { store::read_projects().await },
Msg::ReadProjectsResult,
)
}
_ => {
let msg = format!(
"Failed to read projects from store: {err:?}"
);
report::show_err(&mut state.model.report, msg);
Task::none()
}
}
} else {
Task::none()
}
}
},
Msg::UpsertProjectResult(result) => match result {
Ok(()) => Task::none(),
Err((err, project)) => match err.as_enum() {
terrors::E2::A(store::BlockingLockError) => Task::perform(
async move { store::upsert_project(project).await },
Msg::UpsertProjectResult,
),
_ => {
let msg =
format!("Failed to write projects to store: {err:?}");
report::show_err(&mut state.model.report, msg);
Task::none()
}
},
},
Msg::InitedRepo(result) => {
if let model::SubState::PickingRepoDir(state) = &mut state.model.sub
{
state.waiting_to_init = None;
}
let task = match result {
Ok(path) => {
let (sub, managing_repo, managing_repo_task) =
init_managing_repo(
path.clone(),
&mut state.model.report,
);
state.model.sub = model::SubState::ManagingRepo(sub);
state.managing_repo = Some(managing_repo);
managing_repo_task
}
Err(err) => {
let msg =
format!("Failed to initialize a new repo: {err:?}");
report::show_err(&mut state.model.report, msg);
Task::none()
}
};
state.model.sub_menu = None;
task
}
};
action::update_allowed_actions(&mut state.model);
task
}
fn update_picking_project_from_view(
model: &mut model::PickingProject,
msg: view::Msg,
report: &mut report::Container,
) -> (Task<Msg>, Option<model::SubState>, Option<ManagingRepo>) {
let mut new_state = None;
let mut new_managing_repo = None;
let task = match msg {
view::Msg::Action(msg) => match msg {
action::FilteredMsg::Confirm => match model.selection {
PickingProjectSelection::FindOrCreate => {
let (model, task) = init_picking_repo_dir();
new_state = Some(model::SubState::PickingRepoDir(model));
task
}
PickingProjectSelection::Existing { ix } => {
if let Some(store::Project {
last_closed_time: _,
path,
}) = model
.projects
.as_ref()
.and_then(|projects| projects.iter().nth(ix))
{
let (sub, managing_repo, managing_repo_task) =
init_managing_repo(path.clone(), report);
new_state = Some(model::SubState::ManagingRepo(sub));
new_managing_repo = Some(managing_repo);
managing_repo_task
} else {
Task::none()
}
}
},
action::FilteredMsg::Selection(msg) => {
picking_project_selection(model, msg)
}
action::FilteredMsg::Cancel
| action::FilteredMsg::PostponeRecord
| action::FilteredMsg::SaveRecord
| action::FilteredMsg::DiscardRecord
| action::FilteredMsg::ToggleRecursive
| action::FilteredMsg::RmChange
| action::FilteredMsg::StartRecord
| action::FilteredMsg::SelectChannel
| action::FilteredMsg::ForkChannel
| action::FilteredMsg::RefreshRepo
| action::FilteredMsg::ShowEntireLog
| action::FilteredMsg::FocusNext
| action::FilteredMsg::FocusPrev
| action::FilteredMsg::ClipboardCopy
| action::FilteredMsg::ToggleReports
| action::FilteredMsg::ClipboardCopyReports
| action::FilteredMsg::ToRecord(_)
| action::FilteredMsg::ToRecordToggleSelectedFileOrChange
| action::FilteredMsg::EnterSubMenu(_)
| action::FilteredMsg::ReloadIdentity
| action::FilteredMsg::SubMenuPushOption(_)
| action::FilteredMsg::SubMenuPullOption(_)
| action::FilteredMsg::SubMenuCompareRemoteOption(_) => {
Task::none()
}
},
view::Msg::OpenProject(dir) => {
let (sub, managing_repo, managing_repo_task) =
init_managing_repo(dir, report);
new_state = Some(model::SubState::ManagingRepo(sub));
new_managing_repo = Some(managing_repo);
managing_repo_task
}
view::Msg::PickNewProject => {
let (model, task) = init_picking_repo_dir();
new_state = Some(model::SubState::PickingRepoDir(model));
task
}
view::Msg::PickingRepoDir(_)
| view::Msg::EditRecordMsg(_)
| view::Msg::EditRecordDesc(_)
| view::Msg::EditForkChannelName(_)
| view::Msg::UnfilteredSelection(_)
| view::Msg::ToRecord(_)
| view::Msg::SelectIdentity(_)
| view::Msg::SubMenuPushSelectRemote(_)
| view::Msg::SubMenuPullSelectRemote(_)
| view::Msg::SubMenuCompareRemoteSelectRemote(_)
| view::Msg::SubMenuCompareRemoteInputRemoteChannel(_) => Task::none(),
};
(task, new_state, new_managing_repo)
}
fn picking_project_selection(
model: &mut model::PickingProject,
msg: inflorescence_model::selection::Msg,
) -> Task<Msg> {
match msg {
selection::Msg::PressDir(dir) => {
if let Some(projects) = model.projects.as_mut()
&& !projects.is_empty()
{
match model.selection {
PickingProjectSelection::FindOrCreate => match dir {
selection::Dir::Down => {
model.selection =
PickingProjectSelection::Existing { ix: 0 };
}
selection::Dir::Up => {
model.selection =
PickingProjectSelection::Existing {
ix: projects.len() - 1,
};
}
selection::Dir::Right | selection::Dir::Left => {}
},
PickingProjectSelection::Existing { ix } => match dir {
selection::Dir::Down => {
if ix < projects.len() - 1 {
let ix = ix + 1;
model.selection =
PickingProjectSelection::Existing { ix };
nav_scrollable::scroll_down_to_section(
&mut model.projects_nav,
ix,
);
} else {
model.selection =
PickingProjectSelection::FindOrCreate;
nav_scrollable::scroll_up_to_section(
&mut model.projects_nav,
0,
);
}
}
selection::Dir::Up => {
if ix > 0 {
let ix = ix - 1;
model.selection =
PickingProjectSelection::Existing { ix };
nav_scrollable::scroll_up_to_section(
&mut model.projects_nav,
ix,
);
} else {
model.selection =
PickingProjectSelection::FindOrCreate;
nav_scrollable::scroll_up_to_section(
&mut model.projects_nav,
0,
);
}
}
selection::Dir::Right | selection::Dir::Left => {}
},
}
}
Task::none()
}
selection::Msg::AltPressDir(_) => Task::none(),
}
}
fn update_managing_repo(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
report: &mut report::Container,
msg: ManagingRepoMsg,
) -> (Task<Msg>, Option<model::SubState>) {
let mut new_state = None;
let task = match msg {
ManagingRepoMsg::LoadedIdentities(ids) => {
match ids {
Ok(mut user_ids) => {
let get_repo = |sub: &mut model::ManagingRepoSubState| {
match sub {
model::ManagingRepoSubState::Loading {
repo,
..
} => repo.take(),
model::ManagingRepoSubState::SelectingIdentity {
repo,
..
} => repo.take(),
model::ManagingRepoSubState::NoIdFound { repo } => {
repo.take()
}
model::ManagingRepoSubState::Ready(_) => {
unreachable!()
}
}
};
if user_ids.is_empty() {
let repo = get_repo(&mut model.sub);
model.sub =
model::ManagingRepoSubState::NoIdFound { repo }
} else if user_ids.len() == 1 {
let user_id = user_ids.pop().unwrap();
match &mut model.sub {
model::ManagingRepoSubState::Loading {
user_ids,
repo,
} => {
if let Some(repo) = repo.take() {
model.sub =
model::ManagingRepoSubState::Ready(
ReadyState {
user_id,
repo,
selection: default(),
navigation: default(),
record_changes: default(),
forking_channel_name: default(),
logs: default(),
to_record: default(),
jobs: default(),
record_dichotomy: default(),
},
)
} else {
*user_ids = Some(vec![user_id]);
}
}
model::ManagingRepoSubState::SelectingIdentity {
..
} => {
unreachable!()
}
model::ManagingRepoSubState::Ready(..) => {
unreachable!()
}
model::ManagingRepoSubState::NoIdFound {
repo,
} => {
if let Some(repo) = repo.take() {
model.sub =
model::ManagingRepoSubState::Ready(
ReadyState {
user_id,
repo,
selection: default(),
navigation: default(),
record_changes: default(),
forking_channel_name: default(),
logs: default(),
to_record: default(),
jobs: default(),
record_dichotomy: default(),
},
)
} else {
model.sub = model::ManagingRepoSubState::SelectingIdentity {
ids: user_ids,
selection_ix: default(),
selection_nav: default(),
confirmed_selection_ix: default(),
repo: default(),
};
}
}
}
} else {
let repo = get_repo(&mut model.sub);
model.sub =
model::ManagingRepoSubState::SelectingIdentity {
ids: user_ids,
selection_ix: default(),
selection_nav: default(),
confirmed_selection_ix: default(),
repo,
}
}
}
Err(err) => {
let msg =
format!("Failed to load Pijul identity with {err:#?}");
report::show_err(report, msg);
}
}
Task::none()
}
ManagingRepoMsg::ToRepo(msg) => {
state.repo_tx_in.send(msg).unwrap();
Task::none()
}
ManagingRepoMsg::FromRepo(msg) => {
info!("Repo sent msg {msg}");
let (task, state) = update_from_repo(state, model, report, msg);
new_state = state;
task
}
ManagingRepoMsg::File(msg) => {
update_file(state, model, msg).map(Msg::ManagingRepo)
}
ManagingRepoMsg::WatchedFileChange(path) => {
let repo_ignore_file = model.repo_path.join(repo::IGNORE_FILE);
if path == repo_ignore_file {
let (ignore, ignore_err) = Gitignore::new(repo_ignore_file);
if let Some(err) = ignore_err {
let msg = format!("Error loading .ignore file {err:?}");
error!("{msg}");
report::show_err(report, msg);
}
state.ignore = ignore;
}
let is_dir = path.metadata().map(|md| md.is_dir()).unwrap_or(false);
if !state
.ignore
.matched_path_or_any_parents(&path, is_dir)
.is_ignore()
&& let Some(fs_watch) = state.repo_fs_watch.as_mut()
{
state
.repo_tx_in
.send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
.unwrap();
if let Err(err) =
fs_watch.watch(&path, RecursiveMode::NonRecursive)
{
match &err.kind {
notify::ErrorKind::PathNotFound => {}
notify::ErrorKind::Generic(_)
| notify::ErrorKind::Io(_)
| notify::ErrorKind::WatchNotFound
| notify::ErrorKind::InvalidConfig(_)
| notify::ErrorKind::MaxFilesWatch => {
let msg = format!(
"Error setting up file watch for path {}: {err:?}",
path.to_string_lossy()
);
error!("{msg}");
report::show_err(report, msg)
}
}
}
}
Task::none()
}
};
(task, new_state)
}
fn update_file(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
msg: file::Msg,
) -> Task<ManagingRepoMsg> {
if let Some(ReadyState {
repo,
navigation,
selection,
..
}) = model::is_ready_mut(model)
{
let loaded = file::update(&mut state.files, repo, msg);
if let Some(file::Loaded {
file_id,
unchanged_sections,
}) = loaded
{
let is_selected = match selection.status.as_ref() {
Some(selection::Status::UntrackedFile {
ix: _,
path,
diff_selected: _,
}) => {
let selected_file_id =
file::id_parts_hash(path, file::Kind::Untracked);
selected_file_id == file_id
}
Some(selection::Status::ChangedFile {
ix: _,
path,
diff_selected: _,
}) => {
let selected_file_id =
file::id_parts_hash(path, file::Kind::Changed);
selected_file_id == file_id
}
_ => false,
};
if is_selected {
diff::init_diffs_nav(
&mut navigation.files_diffs,
#[cfg(debug_assertions)]
file_id,
)
.set_skip_sections(unchanged_sections);
}
navigation.files_diffs.diffs.entry(file_id).or_default();
}
}
Task::none()
}
fn update_from_window_event(
model: &mut model::State,
event: window::Event,
) -> Task<ManagingRepoMsg> {
match event {
window::Event::Opened { position: _, size }
| window::Event::Resized(size) => {
model.window_size = size;
}
window::Event::Rescaled(scale) => {
model.window_scale = scale;
}
window::Event::Closed => {
if let model::SubState::ManagingRepo(sub) = &model.sub
&& let model::ManagingRepoSubState::Ready(_) = &sub.sub
{
let _ =
store::updated_closed_time_blocking(sub.repo_path.clone());
}
}
window::Event::Moved(_)
| window::Event::RedrawRequested(_)
| window::Event::CloseRequested
| window::Event::Focused
| window::Event::Unfocused
| window::Event::FileHovered(_)
| window::Event::FileDropped(_)
| window::Event::FilesHoveredLeft => {}
}
Task::none()
}
fn start_thread_to_manage_repo(
repo_path: PathBuf,
) -> (
std::thread::JoinHandle<()>,
Task<ManagingRepoMsg>,
mpsc::UnboundedSender<repo::MsgIn>,
) {
let (repo_tx_in, repo_rx_in) = mpsc::unbounded_channel::<repo::MsgIn>();
let (repo_tx_out, repo_rx_out) = mpsc::unbounded_channel::<repo::MsgOut>();
let repo_task = repo::manage(repo_path, repo_rx_in, repo_tx_out);
let repo_rx_out = UnboundedReceiverStream::new(repo_rx_out);
let repo_msg_out_task = Task::run(repo_rx_out, ManagingRepoMsg::FromRepo);
(repo_task, repo_msg_out_task, repo_tx_in)
}
fn update_picking_repo_dir_from_view(
state: &mut model::PickingRepoDir,
sub_menu: &mut Option<model::SubMenu>,
report: &mut report::Container,
msg: view::Msg,
) -> (Task<Msg>, Option<(model::ManagingRepo, ManagingRepo)>) {
let mut new_state = None;
let model::PickingRepoDir {
picker,
waiting_to_init: _,
} = state;
let mut handle_action =
|sub_menu: &mut Option<model::SubMenu>,
action: Option<dir_picker::Action>| {
if let Some(action) = action {
match action {
dir_picker::Action::Picked(dir) => {
if repo::is_pijul(&dir) {
let project = store::Project {
last_closed_time: cmp::Reverse(None),
path: dir.clone(),
};
let store_project_task = Task::perform(
async move { store::upsert_project(project).await },
Msg::UpsertProjectResult,
);
let (sub, managing_repo, managing_repo_task) =
init_managing_repo(dir, report);
new_state = Some((sub, managing_repo));
Task::batch([
managing_repo_task,
store_project_task,
])
} else {
if repo::is_git(&dir) {
*sub_menu =
Some(model::SubMenu::ImportFromGit {
path: dir.clone(),
});
} else {
*sub_menu = Some(model::SubMenu::InitRepo {
path: dir.clone(),
});
}
Task::none()
}
}
dir_picker::Action::CreateNew(path) => {
*sub_menu = Some(model::SubMenu::InitRepo { path });
Task::none()
}
}
} else {
Task::none()
}
};
let task = match msg {
view::Msg::Action(msg) => match msg {
action::FilteredMsg::Selection(msg) => match msg {
selection::Msg::PressDir(_) => Task::none(),
selection::Msg::AltPressDir(dir) => {
update_dir_picker_selection(picker, dir);
Task::none()
}
},
action::FilteredMsg::FocusNext => task::widget_focus_next(),
action::FilteredMsg::Confirm => {
let (task, clear_sub_menu) = match &sub_menu {
Some(model::SubMenu::InitRepo { path }) => {
state.waiting_to_init =
Some(model::ProjectInitKind::New);
let path = path.clone();
(
Task::perform(
async move { repo::init(path).await },
Msg::InitedRepo,
),
true,
)
}
Some(model::SubMenu::ImportFromGit { path }) => {
state.waiting_to_init =
Some(model::ProjectInitKind::ImportFromGit);
let path = path.clone();
(
Task::perform(
async move { repo::git::import(path).await },
Msg::InitedRepo,
),
true,
)
}
Some(
model::SubMenu::Push { .. }
| model::SubMenu::Pull { .. }
| model::SubMenu::ResetChange
| model::SubMenu::Add { .. }
| model::SubMenu::CompareRemote { .. },
)
| None => {
let (task, action) =
dir_picker::confirm_input_or_selection(picker);
let dir_picker_task =
task.map(view::Msg::PickingRepoDir).map(Msg::View);
let action_task = handle_action(sub_menu, action);
(Task::batch([dir_picker_task, action_task]), false)
}
};
if clear_sub_menu {
*sub_menu = None;
}
task
}
action::FilteredMsg::Cancel => {
*sub_menu = None;
Task::none()
}
action::FilteredMsg::PostponeRecord
| action::FilteredMsg::SaveRecord
| action::FilteredMsg::DiscardRecord
| action::FilteredMsg::ToggleRecursive
| action::FilteredMsg::RmChange
| action::FilteredMsg::StartRecord
| action::FilteredMsg::SelectChannel
| action::FilteredMsg::ForkChannel
| action::FilteredMsg::RefreshRepo
| action::FilteredMsg::ShowEntireLog
| action::FilteredMsg::FocusPrev
| action::FilteredMsg::ClipboardCopy
| action::FilteredMsg::ToggleReports
| action::FilteredMsg::ClipboardCopyReports
| action::FilteredMsg::ToRecord(_)
| action::FilteredMsg::ToRecordToggleSelectedFileOrChange
| action::FilteredMsg::EnterSubMenu(_)
| action::FilteredMsg::ReloadIdentity
| action::FilteredMsg::SubMenuPushOption(_)
| action::FilteredMsg::SubMenuPullOption(_)
| action::FilteredMsg::SubMenuCompareRemoteOption(_) => {
Task::none()
}
},
view::Msg::UnfilteredSelection(_msg) => Task::none(),
view::Msg::PickingRepoDir(msg) => {
let (task, action) = dir_picker::update(picker, msg);
let dir_picker_task =
task.map(view::Msg::PickingRepoDir).map(Msg::View);
let action_task = handle_action(sub_menu, action);
Task::batch([dir_picker_task, action_task])
}
view::Msg::EditRecordMsg(_)
| view::Msg::EditRecordDesc(_)
| view::Msg::EditForkChannelName(_)
| view::Msg::ToRecord(_)
| view::Msg::OpenProject(_)
| view::Msg::PickNewProject
| view::Msg::SelectIdentity(_)
| view::Msg::SubMenuPushSelectRemote(_)
| view::Msg::SubMenuPullSelectRemote(_)
| view::Msg::SubMenuCompareRemoteSelectRemote(_)
| view::Msg::SubMenuCompareRemoteInputRemoteChannel(_) => Task::none(),
};
(task, new_state)
}
fn update_dir_picker_selection(
picker: &mut dir_picker::State,
dir: selection::Dir,
) {
let dir = match dir {
selection::Dir::Down => dir_picker::SelectionDir::Down,
selection::Dir::Up => dir_picker::SelectionDir::Up,
selection::Dir::Right => dir_picker::SelectionDir::Right,
selection::Dir::Left => dir_picker::SelectionDir::Left,
};
dir_picker::move_selection(picker, dir);
}
fn update_managing_repo_from_view(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
sub_menu: &mut Option<model::SubMenu>,
report: &mut report::Container,
allowed_actions: &[action::Binding],
msg: view::Msg,
) -> Task<Msg> {
if let view::Msg::Action(msg) = &msg
&& !action::is_allowed(allowed_actions, msg)
{
info!("Action not allowed: {msg:?}");
return Task::none();
}
match msg {
view::Msg::Action(msg) => {
update_from_action(state, model, sub_menu, report, msg)
}
view::Msg::EditRecordMsg(action) => {
edit_record_msg(model, action).map(Msg::ManagingRepo)
}
view::Msg::EditRecordDesc(action) => {
edit_record_desc(model, action).map(Msg::ManagingRepo)
}
view::Msg::EditForkChannelName(name) => {
if let Some(ReadyState {
forking_channel_name,
..
}) = model::is_ready_mut(model)
{
*forking_channel_name = Some(name);
}
Task::none()
}
view::Msg::UnfilteredSelection(msg) => {
if let Some(ReadyState {
repo,
selection,
navigation,
logs,
record_dichotomy,
..
}) = model::is_ready_mut(model)
{
let record_dichotomy =
selection.compare_remote.as_ref().and_then(
|selection::CompareRemote {
ix: _,
hash: _,
file: _,
remote,
remote_channel,
}| {
model::get_record_dichotomy(
record_dichotomy,
remote,
remote_channel,
)
},
);
selection::update_unfiltered(
msg,
selection,
&mut state.files,
navigation,
repo,
logs,
record_dichotomy,
)
.map(Msg::ManagingRepo)
} else {
Task::none()
}
}
view::Msg::ToRecord(msg) => {
if let Some(ReadyState {
to_record, repo, ..
}) = model::is_ready_mut(model)
{
to_record::update(to_record, msg, &repo.changed_files);
}
Task::none()
}
view::Msg::SelectIdentity(ix) => {
if let model::ManagingRepoSubState::SelectingIdentity {
ids,
repo,
confirmed_selection_ix,
..
} = &mut model.sub
{
if let Some(repo) = repo.take() {
let user_id = ids.get(ix).unwrap().clone();
model.sub =
model::ManagingRepoSubState::Ready(ReadyState {
user_id,
repo,
selection: default(),
navigation: default(),
record_changes: default(),
forking_channel_name: default(),
logs: default(),
to_record: default(),
jobs: default(),
record_dichotomy: default(),
});
} else {
*confirmed_selection_ix = Some(ix);
}
}
Task::none()
}
view::Msg::SubMenuPushSelectRemote(selection) => {
if let Some(model::SubMenu::Push {
opt: Some(model::PushOption::SelectingRemote { remote }),
..
}) = sub_menu
{
*remote = Some(selection);
}
Task::none()
}
view::Msg::SubMenuPullSelectRemote(selection) => {
if let Some(model::SubMenu::Pull {
opt: Some(model::PullOption::SelectingRemote { remote }),
..
}) = sub_menu
{
*remote = Some(selection);
}
Task::none()
}
view::Msg::SubMenuCompareRemoteSelectRemote(selection) => {
if let Some(model::SubMenu::CompareRemote {
opt:
Some(model::CompareRemoteOption::SelectingRemote { remote }),
..
}) = sub_menu
{
*remote = Some(selection);
}
Task::none()
}
view::Msg::SubMenuCompareRemoteInputRemoteChannel(input) => {
if let Some(model::SubMenu::CompareRemote {
opt:
Some(model::CompareRemoteOption::InputingRemoteChannel {
channel,
}),
..
}) = sub_menu
{
*channel = Some(input);
}
Task::none()
}
view::Msg::PickingRepoDir(_)
| view::Msg::OpenProject(_)
| view::Msg::PickNewProject => Task::none(),
}
}
fn update_from_action(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
sub_menu: &mut Option<model::SubMenu>,
report: &mut report::Container,
msg: action::FilteredMsg,
) -> Task<Msg> {
let task = match msg {
action::FilteredMsg::Confirm => match &sub_menu {
Some(model::SubMenu::Push { remote, opt }) => {
if let Some(model::PushOption::SelectingRemote { remote }) = opt
{
*sub_menu = Some(model::SubMenu::Push {
remote: remote.clone(),
opt: None,
});
Task::none()
} else {
push(state, model, sub_menu, report, remote.clone())
}
}
Some(model::SubMenu::Pull { remote, opt }) => {
if let Some(model::PullOption::SelectingRemote { remote }) = opt
{
*sub_menu = Some(model::SubMenu::Pull {
remote: remote.clone(),
opt: None,
});
Task::none()
} else {
pull(state, model, sub_menu, report, remote.clone())
}
}
Some(model::SubMenu::ResetChange) => {
reset_change(state, model, sub_menu, report)
}
Some(model::SubMenu::Add { recursive }) => {
let task = add_untracked_file(state, model, *recursive);
*sub_menu = None;
task
}
Some(model::SubMenu::CompareRemote {
remote,
remote_channel,
opt,
}) => {
if let Some(opt) = opt {
match opt {
model::CompareRemoteOption::SelectingRemote {
remote,
} => {
*sub_menu = Some(model::SubMenu::CompareRemote {
remote: remote.clone(),
remote_channel: remote_channel.clone(),
opt: None,
});
Task::none()
}
model::CompareRemoteOption::InputingRemoteChannel {
channel,
} => {
*sub_menu = Some(model::SubMenu::CompareRemote {
remote: remote.clone(),
remote_channel: channel.clone(),
opt: None,
});
Task::none()
}
}
} else if let Some(ReadyState {
repo,
selection,
jobs,
..
}) = model::is_ready_mut(model)
{
let remote = remote
.as_ref()
.or(repo.remotes.default.as_ref())
.cloned();
if let Some(remote) = remote {
let remote_channel = remote_channel
.clone()
.unwrap_or_else(|| repo.channel.clone());
jobs.insert(model::Job::CompareRemote {
remote: remote.clone(),
remote_channel: remote_channel.clone(),
});
selection.primary = selection::Primary::CompareRemote;
selection.compare_remote =
Some(selection::CompareRemote {
ix: None,
hash: None,
file: None,
remote: remote.clone(),
remote_channel: remote_channel.clone(),
});
state
.repo_tx_in
.send(repo::MsgIn::CompareRemote {
remote,
remote_channel,
})
.unwrap();
} else {
report::show_err(
report,
"Missing remote configuration (check your `.pijul/config.toml`)".to_string(),
);
}
*sub_menu = None;
Task::none()
} else {
Task::none()
}
}
Some(
model::SubMenu::InitRepo { .. }
| model::SubMenu::ImportFromGit { .. },
)
| None => {
if let Some(ReadyState {
repo,
navigation: _,
selection,
forking_channel_name,
..
}) = model::is_ready_mut(model)
{
#[allow(clippy::collapsible_if)]
if matches!(selection.primary, selection::Primary::Channel)
{
if let Some(selection::Channel {
ix: _,
name,
log: _,
}) = selection.channel.take()
{
state
.repo_tx_in
.send(repo::MsgIn::SwitchToChannel(name))
.unwrap();
selection.primary = selection::Primary::default();
}
} else if let Some(name) =
forking_channel_name.take_if(|name| {
let name = name.trim();
let empty = name.is_empty();
let unique = || {
repo.channel != name
&& !repo
.other_channels
.iter()
.any(|n| n == name)
};
!empty && unique()
})
{
state
.repo_tx_in
.send(repo::MsgIn::ForkChannel(name))
.unwrap();
}
Task::none()
} else if let model::ManagingRepoSubState::SelectingIdentity {
ids,
selection_ix,
selection_nav: _,
confirmed_selection_ix,
repo,
} = &mut model.sub
{
if let Some(repo) = repo.take() {
let user_id = ids.get(*selection_ix).unwrap().clone();
model.sub =
model::ManagingRepoSubState::Ready(ReadyState {
user_id,
repo,
selection: default(),
navigation: default(),
record_changes: default(),
forking_channel_name: default(),
logs: default(),
to_record: default(),
jobs: default(),
record_dichotomy: default(),
});
} else {
*confirmed_selection_ix = Some(*selection_ix);
}
Task::none()
} else {
Task::none()
}
}
},
action::FilteredMsg::Cancel => {
if let Some(sub) = sub_menu.take() {
match sub {
model::SubMenu::Push { remote, opt } => {
if opt.is_some() {
*sub_menu = Some(model::SubMenu::Push {
remote,
opt: None,
});
return Task::none();
}
}
model::SubMenu::Pull { remote, opt } => {
if opt.is_some() {
*sub_menu = Some(model::SubMenu::Pull {
remote,
opt: None,
});
return Task::none();
}
}
model::SubMenu::CompareRemote {
remote,
remote_channel,
opt,
} => {
if opt.is_some() {
*sub_menu = Some(model::SubMenu::CompareRemote {
remote,
remote_channel,
opt: None,
});
return Task::none();
}
}
model::SubMenu::ResetChange
| model::SubMenu::InitRepo { path: _ }
| model::SubMenu::ImportFromGit { path: _ }
| model::SubMenu::Add { recursive: _ } => {}
}
} else if let Some(ReadyState {
selection,
record_changes: record_msg,
forking_channel_name,
..
}) = model::is_ready_mut(model)
{
if let Some(RecordChanges::Typing { msg, desc }) =
record_msg.as_ref()
{
*record_msg = Some(RecordChanges::Canceled {
old_msg: msg.clone(),
old_desc: desc.text(),
});
}
match selection.primary {
selection::Primary::Status => {
if forking_channel_name.is_some() {
*forking_channel_name = None;
}
}
selection::Primary::Channel
| selection::Primary::EntireLog
| selection::Primary::CompareRemote => {
selection.primary = selection::Primary::default();
}
}
}
Task::none()
}
action::FilteredMsg::Selection(msg) => {
if let Some(ReadyState {
repo,
selection,
navigation,
logs,
record_dichotomy,
..
}) = model::is_ready_mut(model)
{
if let Some(sub) = sub_menu {
match sub {
model::SubMenu::Push { opt: Some(opt), .. } => {
return sub_menu_push_opt_selection(
msg,
&mut navigation.status_nav,
opt,
repo,
);
}
model::SubMenu::Pull { opt: Some(opt), .. } => {
return sub_menu_pull_opt_selection(
msg,
&mut navigation.status_nav,
opt,
repo,
);
}
model::SubMenu::CompareRemote {
opt: Some(opt),
..
} => {
return sub_menu_compare_remote_opt_selection(
msg,
&mut navigation.status_nav,
opt,
repo,
);
}
model::SubMenu::Push { opt: _, remote: _ }
| model::SubMenu::Pull { opt: _, remote: _ }
| model::SubMenu::CompareRemote {
opt: _,
remote: _,
remote_channel: _,
}
| model::SubMenu::ResetChange
| model::SubMenu::InitRepo { path: _ }
| model::SubMenu::ImportFromGit { path: _ }
| model::SubMenu::Add { recursive: _ } => {}
}
}
let record_dichotomy =
selection.compare_remote.as_ref().and_then(
|selection::CompareRemote {
ix: _,
hash: _,
file: _,
remote,
remote_channel,
}| {
model::get_record_dichotomy(
record_dichotomy,
remote,
remote_channel,
)
},
);
selection::update(
msg,
selection,
&mut state.files,
navigation,
repo,
logs,
record_dichotomy,
)
.map(Msg::ManagingRepo)
} else if let model::ManagingRepoSubState::SelectingIdentity {
ids,
selection_ix,
selection_nav: _,
confirmed_selection_ix,
repo: _,
} = &mut model.sub
{
match msg {
selection::Msg::PressDir(dir) => match dir {
inflorescence_model::selection::Dir::Down => {
*confirmed_selection_ix = None;
if *selection_ix == ids.len().saturating_sub(1) {
*selection_ix = 0;
} else {
*selection_ix += 1;
}
}
inflorescence_model::selection::Dir::Up => {
*confirmed_selection_ix = None;
if *selection_ix == 0 {
*selection_ix = ids.len().saturating_sub(1);
} else {
*selection_ix -= 1;
}
}
inflorescence_model::selection::Dir::Right
| inflorescence_model::selection::Dir::Left => {}
},
selection::Msg::AltPressDir(_) => {}
}
Task::none()
} else {
Task::none()
}
}
action::FilteredMsg::SaveRecord => save_record(state, model),
action::FilteredMsg::PostponeRecord => defer_record(model),
action::FilteredMsg::DiscardRecord => abandon_record(model),
action::FilteredMsg::FocusNext => focus_next(model),
action::FilteredMsg::FocusPrev => focus_prev(model),
action::FilteredMsg::ToggleRecursive => toggle_recursive(sub_menu),
action::FilteredMsg::RmChange => rm_change(state, model, sub_menu),
action::FilteredMsg::StartRecord => start_record(model),
action::FilteredMsg::SelectChannel => {
if let Some(ReadyState {
selection, repo, ..
}) = model::is_ready_mut(model)
&& !repo.other_channels.is_empty()
{
selection.primary = selection::Primary::Channel;
}
Task::none()
}
action::FilteredMsg::ForkChannel => {
if let Some(ReadyState {
forking_channel_name,
..
}) = model::is_ready_mut(model)
{
*forking_channel_name = Some(String::new());
task::widget_focus_next()
} else {
Task::none()
}
}
action::FilteredMsg::RefreshRepo => {
state
.repo_tx_in
.send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
.unwrap();
Task::none()
}
action::FilteredMsg::ShowEntireLog => {
if let Some(ReadyState {
selection, logs, ..
}) = model::is_ready_mut(model)
{
selection.primary = selection::Primary::EntireLog;
match logs.entire_log.as_ref() {
None => {
state
.repo_tx_in
.send(repo::MsgIn::LoadEntireLog)
.unwrap();
logs.entire_log = Some(Log::Loading);
}
Some(Log::Loading | Log::Loaded { .. }) => {}
}
}
Task::none()
}
action::FilteredMsg::ClipboardCopy => clipboard_copy(model),
action::FilteredMsg::ToggleReports => {
if !report.hidden {
report
.entries
.iter_mut()
.for_each(|entry| entry.is_read = true);
}
report.hidden = !report.hidden;
Task::none()
}
action::FilteredMsg::ClipboardCopyReports => {
let to_copy = report::entries_to_string(report);
task::clipboard_write(to_copy)
}
action::FilteredMsg::ToRecord(msg) => {
if let Some(ReadyState {
to_record, repo, ..
}) = model::is_ready_mut(model)
{
to_record::update(to_record, msg, &repo.changed_files);
}
Task::none()
}
action::FilteredMsg::ToRecordToggleSelectedFileOrChange => {
if let Some(ReadyState {
to_record,
repo,
selection,
navigation,
..
}) = model::is_ready_mut(model)
&& let Some(selection::Status::ChangedFile {
ix: _,
path,
diff_selected,
}) = selection.status.as_ref()
{
if *diff_selected {
let diff_ix = navigation
.files_diffs
.diffs_nav
.get_selected_section_ix()
.unwrap();
let selected_diff = repo
.changed_files
.get(path)
.and_then(|diffs| diffs.iter().nth(diff_ix));
if let Some(diff) = selected_diff {
let msg = to_record::Msg::ToggleChange {
path: path.clone(),
diff_id: diff::id_parts_hash(diff),
};
to_record::update(to_record, msg, &repo.changed_files);
}
} else {
let msg = to_record::Msg::ToggleFile { path: path.clone() };
to_record::update(to_record, msg, &repo.changed_files);
}
}
Task::none()
}
action::FilteredMsg::EnterSubMenu(new_sub_menu) => {
*sub_menu = Some(new_sub_menu);
Task::none()
}
action::FilteredMsg::SubMenuPushOption(mut new_opt) => {
if let Some(model::SubMenu::Push { opt, remote }) = sub_menu {
match &mut new_opt {
model::PushOption::SelectingRemote {
remote: selecting_remote,
} => {
*selecting_remote = remote.clone();
}
}
*opt = Some(new_opt);
}
Task::none()
}
action::FilteredMsg::SubMenuPullOption(mut new_opt) => {
if let Some(model::SubMenu::Pull { opt, remote }) = sub_menu {
match &mut new_opt {
model::PullOption::SelectingRemote {
remote: selecting_remote,
} => {
*selecting_remote = remote.clone();
}
}
*opt = Some(new_opt);
}
Task::none()
}
action::FilteredMsg::SubMenuCompareRemoteOption(mut new_opt) => {
if let Some(model::SubMenu::CompareRemote {
opt,
remote,
remote_channel,
}) = sub_menu
{
let task = match &mut new_opt {
model::CompareRemoteOption::SelectingRemote {
remote: selecting_remote,
} => {
*selecting_remote = remote.clone();
Task::none()
}
model::CompareRemoteOption::InputingRemoteChannel {
channel: selecting_channel,
} => {
*selecting_channel = remote_channel.clone();
task::widget_focus_next()
}
};
*opt = Some(new_opt);
return task;
}
Task::none()
}
action::FilteredMsg::ReloadIdentity => {
load_identities_task().map(Msg::ManagingRepo)
}
};
#[cfg(debug_assertions)]
{
if let Some(ReadyState {
selection,
navigation,
..
}) = model::is_ready(model)
{
let selection::State {
primary: _,
status,
channel,
entire_log,
compare_remote,
held_key: _,
} = selection;
if let Some(selection) = status {
match selection {
selection::Status::UntrackedFile { .. } => {}
selection::Status::ChangedFile { .. } => {}
selection::Status::LogChange(selection::LogChange {
hash,
file,
..
}) => {
if let Some(selection::LogChangeFileSelection {
path,
..
}) = file
{
assert_eq!(
navigation.status_logs_navs.change_hash,
Some(*hash)
);
assert_eq!(
navigation.status_logs_navs.file_id,
Some(file::log_id_parts_hash(*hash, path))
);
} else {
assert_eq!(
navigation.status_logs_navs.change_hash,
Some(*hash)
);
}
}
}
}
if let Some(selection::Channel { name, log, .. }) = channel {
assert_eq!(navigation.other_channel_name.as_ref(), Some(name));
match log {
None => {}
Some(selection::LogChange {
hash, file: None, ..
}) => {
assert_eq!(
navigation.other_channel_logs_navs.change_hash,
Some(*hash)
);
}
Some(selection::LogChange {
hash,
file:
Some(selection::LogChangeFileSelection { path, .. }),
..
}) => {
assert_eq!(
navigation.other_channel_logs_navs.change_hash,
Some(*hash)
);
assert_eq!(
navigation.other_channel_logs_navs.file_id,
Some(file::log_id_parts_hash(*hash, path))
);
}
}
}
if let Some(selection::LogChange { hash, file, .. }) = entire_log {
assert_eq!(
navigation.entire_logs_navs.change_hash,
Some(*hash)
);
if let Some(selection::LogChangeFileSelection {
path, ..
}) = file
{
assert_eq!(
navigation.entire_logs_navs.change_hash,
Some(*hash)
);
let file_id = file::log_id_parts_hash(*hash, path);
if navigation.log_diffs.diffs.contains_key(&file_id) {
assert_eq!(
navigation.entire_logs_navs.file_id,
Some(file_id)
);
}
} else {
assert_eq!(
navigation.entire_logs_navs.change_hash,
Some(*hash)
);
}
}
if let Some(selection::CompareRemote {
ix: _,
hash: Some(hash),
file,
remote: _,
remote_channel: _,
}) = compare_remote
{
assert_eq!(
navigation.compare_remote_navs.change_hash,
Some(*hash)
);
if let Some(selection::LogChangeFileSelection {
path, ..
}) = file
{
assert_eq!(
navigation.compare_remote_navs.change_hash,
Some(*hash)
);
let file_id = file::log_id_parts_hash(*hash, path);
if navigation.log_diffs.diffs.contains_key(&file_id) {
assert_eq!(
navigation.compare_remote_navs.file_id,
Some(file_id)
);
}
} else {
assert_eq!(
navigation.compare_remote_navs.change_hash,
Some(*hash)
);
}
}
}
}
task
}
fn sub_menu_push_opt_selection(
msg: selection::Msg,
nav: &mut nav_scrollable::State,
opt: &mut model::PushOption,
repo: &mut repo::State,
) -> Task<Msg> {
let repo::Remotes { default, other } = &repo.remotes;
let remote_names: Vec<_> = default.iter().chain(other).collect();
match opt {
model::PushOption::SelectingRemote { remote } => {
remote_selection(msg, nav, remote, remote_names)
}
}
}
fn sub_menu_pull_opt_selection(
msg: selection::Msg,
nav: &mut nav_scrollable::State,
opt: &mut model::PullOption,
repo: &mut repo::State,
) -> Task<Msg> {
let repo::Remotes { default, other } = &repo.remotes;
let remote_names: Vec<_> = default.iter().chain(other).collect();
match opt {
model::PullOption::SelectingRemote { remote } => {
remote_selection(msg, nav, remote, remote_names)
}
}
}
fn sub_menu_compare_remote_opt_selection(
msg: selection::Msg,
nav: &mut nav_scrollable::State,
opt: &mut model::CompareRemoteOption,
repo: &mut repo::State,
) -> Task<Msg> {
match opt {
model::CompareRemoteOption::SelectingRemote { remote } => {
let repo::Remotes { default, other } = &repo.remotes;
let remote_names: Vec<_> = default.iter().chain(other).collect();
remote_selection(msg, nav, remote, remote_names)
}
model::CompareRemoteOption::InputingRemoteChannel { channel: _ } => {
Task::none()
}
}
}
fn remote_selection(
msg: selection::Msg,
nav: &mut nav_scrollable::State,
remote: &mut Option<String>,
remote_names: Vec<&String>,
) -> Task<Msg> {
match msg {
selection::Msg::PressDir(dir) => match dir {
selection::Dir::Down => {
if let Some(current) = remote {
let ix = remote_names.iter().enumerate().find_map(
|(ix, name)| (*name == current).then_some(ix),
);
if let Some(ix) = ix
&& ix < remote_names.len().saturating_sub(1)
{
let ix = ix + 1;
nav_scrollable::scroll_down_to_section(nav, ix);
*remote = remote_names.into_iter().nth(ix).cloned();
return Task::none();
}
}
nav_scrollable::scroll_up_to_section(nav, 0);
*remote = remote_names.into_iter().next().cloned();
}
selection::Dir::Up => {
if let Some(current) = remote {
let ix = remote_names.iter().enumerate().find_map(
|(ix, name)| (*name == current).then_some(ix),
);
if let Some(ix) = ix
&& ix > 0
{
let ix = ix - 1;
nav_scrollable::scroll_up_to_section(nav, ix);
*remote = remote_names.into_iter().nth(ix).cloned();
return Task::none();
}
}
nav_scrollable::scroll_down_to_section(
nav,
remote_names.len().saturating_sub(1),
);
*remote = remote_names.into_iter().next_back().cloned();
}
selection::Dir::Right | selection::Dir::Left => {}
},
selection::Msg::AltPressDir(_) => {}
}
Task::none()
}
fn load_identities_task() -> Task<ManagingRepoMsg> {
Task::future(async {
let ids = spawn_blocking(identity::load).await.unwrap();
ManagingRepoMsg::LoadedIdentities(ids)
})
}
fn toggle_recursive(sub_menu: &mut Option<model::SubMenu>) -> Task<Msg> {
if let Some(model::SubMenu::Add { recursive }) = sub_menu.as_mut() {
*recursive = !*recursive;
}
Task::none()
}
fn push(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
sub_menu: &mut Option<model::SubMenu>,
report: &mut report::Container,
remote: Option<String>,
) -> Task<Msg> {
if let Some(ReadyState { repo, jobs, .. }) = model::is_ready_mut(model) {
if let Some(remote) =
remote.clone().or_else(|| repo.remotes.default.clone())
{
jobs.insert(model::Job::Push {
remote: remote.clone(),
channel: repo.channel.clone(),
});
state
.repo_tx_in
.send(repo::MsgIn::Push {
remote,
channel: repo.channel.clone(),
})
.unwrap();
} else {
report::show_err(
report,
"Cannot push as there is no default remote configured."
.to_string(),
);
}
}
*sub_menu = None;
Task::none()
}
fn pull(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
sub_menu: &mut Option<model::SubMenu>,
report: &mut report::Container,
remote: Option<String>,
) -> Task<Msg> {
if let Some(ReadyState { repo, jobs, .. }) = model::is_ready_mut(model) {
if let Some(remote) =
remote.clone().or_else(|| repo.remotes.default.clone())
{
jobs.insert(model::Job::Pull {
remote: remote.clone(),
channel: repo.channel.clone(),
});
state
.repo_tx_in
.send(repo::MsgIn::Pull {
remote,
channel: repo.channel.clone(),
})
.unwrap();
} else {
report::show_err(
report,
"Cannot pull as there is no default remote configured."
.to_string(),
);
}
}
*sub_menu = None;
Task::none()
}
fn reset_change(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
sub_menu: &mut Option<model::SubMenu>,
report: &mut report::Container,
) -> Task<Msg> {
if let Some(ReadyState { selection, .. }) = model::is_ready_mut(model)
&& let Some(selection::Status::ChangedFile {
ix: _,
path,
diff_selected,
}) = selection.status.as_ref()
{
if !diff_selected {
state
.repo_tx_in
.send(repo::MsgIn::ResetFile {
path: path.raw.clone(),
})
.unwrap();
} else {
todo!("reset selected hunk")
}
} else {
report::show_err(
report,
"Cannot reset the current selection. This should never happen, please report it! State: {state:?}"
.to_string(),
);
}
*sub_menu = None;
Task::none()
}
fn clipboard_copy(model: &mut model::ManagingRepo) -> Task<Msg> {
if let Some(ReadyState { selection, .. }) = model::is_ready(model) {
let to_copy = match selection::unify(selection) {
selection::Unified::Status(Some(
inflorescence_model::selection::Status::LogChange(
selection::LogChange { hash, .. },
),
)) => Some(repo::hash_to_string(hash)),
selection::Unified::Channel(Some(selection::Channel {
log: Some(selection::LogChange { hash, .. }),
..
})) => Some(repo::hash_to_string(hash)),
selection::Unified::EntireLog(Some(selection::LogChange {
hash,
..
})) => Some(repo::hash_to_string(hash)),
selection::Unified::CompareRemote(Some(
selection::CompareRemote {
hash: Some(hash), ..
},
)) => Some(repo::hash_to_string(hash)),
selection::Unified::Status(_)
| selection::Unified::Channel(_)
| selection::Unified::EntireLog(_)
| selection::Unified::CompareRemote(_) => None,
};
if let Some(to_copy) = to_copy {
task::clipboard_write(to_copy)
} else {
Task::none()
}
} else {
Task::none()
}
}
fn update_from_repo(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
report: &mut report::Container,
msg: repo::MsgOut,
) -> (Task<Msg>, Option<model::SubState>) {
let mut new_state = None;
let task = match msg {
repo::MsgOut::Init(repo) => {
repo_init(state, model, report, repo).map(Msg::ManagingRepo)
}
repo::MsgOut::InitFailed {
err,
switch_to_project_picker,
} => {
report::show_err(report, err);
if switch_to_project_picker {
let (sub, projects_task) = init_picking_project();
new_state = Some(sub);
projects_task
} else {
Task::none()
}
}
repo::MsgOut::RepoTaskExited => {
report::show_err(report, "Task managing repo has crashed. This shouldn't happen, please report what happened!".to_string());
Task::none()
}
repo::MsgOut::Refreshed {
state: repo_state,
invalidate_logs,
} => match repo_state {
Ok(repo_state) => {
repo_refreshed(state, model, repo_state, invalidate_logs)
.map(Msg::ManagingRepo)
}
Err(err) => {
let msg = format!("Failed to refresh repository with {err:?}");
report::show_err(report, msg);
Task::none()
}
},
repo::MsgOut::AddedUntrackedFile { result, path: _ } => match result {
Ok(()) => Task::none(),
Err(err) => {
let msg = format!("Failed to add untracked file with {err:?}");
report::show_err(report, msg);
Task::none()
}
},
repo::MsgOut::RmedAddedFile { result, path: _ } => match result {
Ok(()) => Task::none(),
Err(err) => {
let msg = format!("Failed to remove added file with {err:?}");
report::show_err(report, msg);
Task::none()
}
},
repo::MsgOut::GotChangeDiffs { hash, diffs } => match diffs {
Ok(diffs) => {
repo_got_change_diffs(model, hash, diffs).map(Msg::ManagingRepo)
}
Err(err) => {
let msg = format!(
"Failed to get diff of change {} with {err:?}",
repo::hash_to_string(&hash)
);
report::show_err(report, msg);
Task::none()
}
},
repo::MsgOut::LoadedEntireLog(log) => match log {
Ok(log) => got_entire_log(model, log).map(Msg::ManagingRepo),
Err(err) => {
let msg = format!("Failed to load a log with {err:?}");
report::show_err(report, msg);
Task::none()
}
},
repo::MsgOut::LoadedOtherChannelLog { channel, log } => match log {
Ok(log) => loaded_other_channel_log(model, channel, log)
.map(Msg::ManagingRepo),
Err(err) => {
let msg = format!(
"Failed to load a log of channel {channel} with {err:?}"
);
report::show_err(report, msg);
Task::none()
}
},
repo::MsgOut::Pushed {
remote,
channel,
result,
} => {
match result {
Ok(()) => {
if let Some(ReadyState {
record_dichotomy, ..
}) = model::is_ready_mut(model)
&& let Some(record_dichotomy) =
model::get_record_dichotomy_mut(
record_dichotomy,
&remote,
&channel,
)
{
let repo::RecordDichotomy {
local_records,
remote_records,
remote_unrecords,
} = record_dichotomy;
remote_unrecords.retain(|entry| {
local_records
.iter()
.all(|local| local.hash != entry.hash)
});
remote_records.extend(mem::take(local_records));
}
report::show_info(
report,
format!("Pushed to {remote}, channel {channel}"),
)
}
Err(repo::PushError::Empty) => report::show_info(
report,
format!("Nothing to push to {remote}, channel {channel}"),
),
Err(err) => {
let msg = format!(
"Failed to push to {remote}, channel {channel} with {err:?}"
);
report::show_err(report, msg);
}
};
if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {
jobs.swap_remove(&Job::Push { remote, channel });
}
Task::none()
}
repo::MsgOut::Pulled {
remote,
channel,
result,
} => {
match result {
Ok(()) => {
if let Some(ReadyState {
record_dichotomy, ..
}) = model::is_ready_mut(model)
&& let Some(record_dichotomy) =
model::get_record_dichotomy_mut(
record_dichotomy,
&remote,
&channel,
)
{
let repo::RecordDichotomy {
local_records: _,
remote_records,
remote_unrecords: _,
} = record_dichotomy;
*remote_records = default();
}
report::show_info(
report,
format!("Pulled from {remote}, channel {channel}"),
)
}
Err(repo::PullError::Empty) => report::show_info(
report,
format!("Nothing to pull from {remote}, channel {channel}"),
),
Err(err) => {
let msg = format!(
"Failed to pull from {remote}, channel {channel} with {err:?}"
);
report::show_err(report, msg);
}
};
if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {
jobs.swap_remove(&Job::Pull { remote, channel });
}
Task::none()
}
repo::MsgOut::ComparedRemote {
result,
remote_channel,
remote,
} => {
match result {
Ok(repo::ComparedRemote {
record_dichotomy: d,
remote,
remote_channel,
}) => {
if let Some(ReadyState {
record_dichotomy, ..
}) = model::is_ready_mut(model)
{
record_dichotomy
.entry(remote)
.or_default()
.insert(remote_channel, d);
}
}
Err(err) => {
let msg =
format!("Failed to compare with remote with {err:?}. Remote {remote:?}, channel: {remote_channel:?}");
report::show_err(report, msg);
}
}
if let Some(ReadyState { jobs, .. }) = model::is_ready_mut(model) {
jobs.swap_remove(&Job::CompareRemote {
remote,
remote_channel,
});
}
Task::none()
}
};
(task, new_state)
}
fn add_untracked_file(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
recursive: bool,
) -> Task<Msg> {
if let Some(ReadyState {
repo,
selection,
navigation,
logs,
to_record,
..
}) = model::is_ready_mut(model)
&& let Some(selection::Status::UntrackedFile {
ix,
path,
diff_selected: _,
}) = selection.status.as_ref()
{
state
.repo_tx_in
.send(repo::MsgIn::AddUntrackedFile {
path: path.raw.clone(),
recursive,
})
.unwrap();
if path.is_dir {
state
.repo_tx_in
.send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
.unwrap();
}
let removed = repo.untracked_files.remove(path);
debug_assert!(
removed,
"{:?}, path: {}",
repo.untracked_files, path.raw
);
repo.changed_files
.entry(path.clone())
.or_default()
.insert(repo::ChangedFileDiff::Add { contents: None });
to_record::add_untracked_file_to_record(to_record, path.clone());
if repo.untracked_files.is_empty() {
selection.status = None;
} else {
let ix = cmp::min(*ix, repo.untracked_files.len() - 1);
let (new_selection, selection_task) =
selection::untracked_file_selection(
ix,
selection::VDir::Down,
&mut selection::Ctx {
state: selection,
files: &mut state.files,
navigation,
repo,
logs,
record_dichotomy: None,
},
);
selection.status = Some(new_selection);
return selection_task.map(Msg::ManagingRepo);
};
return Task::none();
}
Task::none()
}
fn rm_change(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
sub_menu: &mut Option<model::SubMenu>,
) -> Task<Msg> {
if let Some(ReadyState {
repo,
selection,
navigation,
logs,
to_record,
..
}) = model::is_ready_mut(model)
&& let Some(selection::Status::ChangedFile {
ix,
path,
diff_selected: _,
}) = selection.status.as_ref()
{
let diffs = repo.changed_files.get(path).unwrap();
if diffs
.iter()
.any(|diff| matches!(diff, repo::ChangedFileDiff::Add { .. }))
{
state
.repo_tx_in
.send(repo::MsgIn::RmAddedFile {
path: path.raw.clone(),
})
.unwrap();
if path.is_dir {
state
.repo_tx_in
.send(repo::MsgIn::RefreshChangedAndUntrackedFiles)
.unwrap();
}
let removed = repo.changed_files.remove(path);
debug_assert!(
removed.is_some(),
"{:?} not found in {:?}",
path.raw,
repo.changed_files
);
repo.untracked_files.insert(path.clone());
to_record::rm_untracked_file_to_record(
to_record,
path,
&repo.changed_files,
);
if repo.changed_files.is_empty() {
selection.status = None
} else {
let ix = cmp::min(*ix, repo.changed_files.len() - 1);
let (new_selection, selection_task) =
selection::changed_file_selection(
ix,
selection::VDir::Down,
&mut selection::Ctx {
state: selection,
files: &mut state.files,
navigation,
repo,
logs,
record_dichotomy: None,
},
);
selection.status = Some(new_selection);
return selection_task.map(Msg::ManagingRepo);
};
} else {
*sub_menu = Some(model::SubMenu::ResetChange);
}
}
Task::none()
}
fn start_record(model: &mut model::ManagingRepo) -> Task<Msg> {
if let Some(ReadyState {
repo,
record_changes,
..
}) = model::is_ready_mut(model)
{
if repo.changed_files.is_empty() {
info!("Trying to record with no changes");
} else if let Some(RecordChanges::Typing { .. }) =
record_changes.as_ref()
{
info!("Requested to record, but already recording");
return Task::none();
} else {
let (msg, desc) = match record_changes.take() {
Some(RecordChanges::Canceled { old_msg, old_desc }) => {
(old_msg, text_editor::Content::with_text(&old_desc))
}
None | Some(RecordChanges::Typing { .. }) => {
(String::new(), text_editor::Content::new())
}
};
*record_changes = Some(RecordChanges::Typing { msg, desc });
return task::widget_focus_next();
}
}
Task::none()
}
fn edit_record_msg(
model: &mut model::ManagingRepo,
new_msg: String,
) -> Task<ManagingRepoMsg> {
if let Some(ReadyState {
record_changes: Some(RecordChanges::Typing { msg, desc: _ }),
..
}) = model::is_ready_mut(model)
{
*msg = new_msg;
}
Task::none()
}
fn edit_record_desc(
model: &mut model::ManagingRepo,
action: text_editor::Action,
) -> Task<ManagingRepoMsg> {
if let Some(ReadyState {
record_changes: Some(RecordChanges::Typing { msg: _, desc }),
..
}) = model::is_ready_mut(model)
{
desc.perform(action);
}
Task::none()
}
fn save_record(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
) -> Task<Msg> {
if let Some(ReadyState {
record_changes,
repo,
user_id,
selection,
to_record,
..
}) = model::is_ready_mut(model)
&& let Some(RecordChanges::Typing { msg, desc }) =
record_changes.as_ref()
{
let msg = msg.trim().to_string();
let desc = desc.text();
let desc = desc.trim();
let desc = if desc.is_empty() {
None
} else {
Some(desc.to_string())
};
if msg.is_empty() {
info!("Cannot record with an empty message");
} else {
let config = PijulConfig::default();
let use_keyring = true;
let (sk, _) = user_id.decrypt(&config, use_keyring).unwrap();
let sk = Arc::new(sk);
let id_pk = user_id.public_key.key.clone();
state
.repo_tx_in
.send(repo::MsgIn::Record {
msg,
desc,
sk,
id_pk,
to_record: to_record.clone(),
})
.unwrap();
*to_record = default();
*selection = selection::State::default();
*record_changes = None;
repo.changed_files = repo::ChangedFiles::default();
file::diffs_cache_clear(&mut state.files.diffs_cache);
}
}
Task::none()
}
fn defer_record(model: &mut model::ManagingRepo) -> Task<Msg> {
if let Some(ReadyState { record_changes, .. }) = model::is_ready_mut(model)
&& let Some(RecordChanges::Typing { msg, desc }) =
record_changes.as_ref()
{
let old_msg = msg.trim();
let old_desc = desc.text();
let old_desc = old_desc.trim();
*record_changes = if !old_msg.is_empty() || !old_desc.is_empty() {
let old_msg = old_msg.to_string();
let old_desc = old_desc.to_string();
Some(RecordChanges::Canceled { old_msg, old_desc })
} else {
None
};
}
Task::none()
}
fn abandon_record(model: &mut model::ManagingRepo) -> Task<Msg> {
if let Some(ReadyState {
record_changes: record_msg,
..
}) = model::is_ready_mut(model)
&& let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
{
*record_msg = None;
}
Task::none()
}
fn focus_next(model: &mut model::ManagingRepo) -> Task<Msg> {
if let Some(ReadyState {
record_changes: record_msg,
..
}) = model::is_ready_mut(model)
&& let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
{
task::widget_focus_next()
} else {
Task::none()
}
}
fn focus_prev(model: &mut model::ManagingRepo) -> Task<Msg> {
if let Some(ReadyState {
record_changes: record_msg,
..
}) = model::is_ready_mut(model)
&& let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
{
task::widget_focus_previous()
} else {
Task::none()
}
}
fn repo_init(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
report: &mut report::Container,
repo_state: repo::State,
) -> Task<ManagingRepoMsg> {
match &mut model.sub {
model::ManagingRepoSubState::Loading { user_ids, repo: _ } => {
if let Some(user_ids) = user_ids {
model.sub = if user_ids.len() == 1 {
let user_id = user_ids.pop().unwrap();
model::ManagingRepoSubState::Ready(ReadyState {
user_id,
repo: repo_state,
selection: default(),
navigation: default(),
record_changes: default(),
forking_channel_name: default(),
logs: default(),
to_record: default(),
jobs: default(),
record_dichotomy: default(),
})
} else {
model::ManagingRepoSubState::SelectingIdentity {
ids: mem::take(user_ids),
selection_ix: default(),
selection_nav: default(),
confirmed_selection_ix: default(),
repo: Some(repo_state),
}
}
}
}
model::ManagingRepoSubState::SelectingIdentity { repo, .. } => {
*repo = Some(repo_state)
}
model::ManagingRepoSubState::Ready(_) => {}
model::ManagingRepoSubState::NoIdFound { repo } => {
*repo = Some(repo_state)
}
};
file::diffs_cache_clear(&mut state.files.diffs_cache);
let (fs_watch_tx, fs_watch_rx) = watch::channel(model.repo_path.clone());
let mut fs_watch = new_debouncer(
Duration::from_secs(1),
None,
move |result: DebounceEventResult| match result {
Ok(events) => events.iter().for_each(|event| {
if event.kind.is_create()
|| event.kind.is_modify()
|| event.kind.is_remove()
{
event.paths.iter().for_each(|path| {
let _ = fs_watch_tx.send(path.clone());
})
}
}),
Err(errors) => {
errors.iter().for_each(|error| eprintln!("{error:?}"))
}
},
)
.unwrap();
let mut already_got_max_files_err = false;
for entry in ignore::WalkBuilder::new(&model.repo_path)
.standard_filters(false) .ignore(true) .build()
.flatten()
{
if let Err(err) =
fs_watch.watch(entry.path(), RecursiveMode::NonRecursive)
{
let msg = if let notify::ErrorKind::MaxFilesWatch = &err.kind {
{
if already_got_max_files_err {
continue;
} else {
already_got_max_files_err = true;
}
}
#[cfg(target_os = "linux")]
const HELP: &str = " Consider increasing the limits (`fs.inotify.max_user_instances` and/or `fs.inotify.max_user_watches`)";
#[cfg(not(target_os = "linux"))]
const MORE: &str = "";
format!("Error setting up file watch. Reached a maximum.{HELP}")
} else {
format!(
"Error setting up file watch for path {}: {err:?}",
entry.path().to_string_lossy()
)
};
error!("{msg}");
report::show_err(report, msg)
}
}
let fs_watch_rx = WatchStream::from_changes(fs_watch_rx);
let watch_task = Task::run(fs_watch_rx, ManagingRepoMsg::WatchedFileChange);
state.repo_fs_watch = Some(fs_watch);
watch_task
}
fn repo_refreshed(
state: &mut ManagingRepo,
model: &mut model::ManagingRepo,
repo_state: repo::State,
invalidate_logs: bool,
) -> Task<ManagingRepoMsg> {
file::diffs_cache_clear(&mut state.files.diffs_cache);
match &mut model.sub {
model::ManagingRepoSubState::Loading { user_ids: _, repo } => {
*repo = Some(repo_state)
}
model::ManagingRepoSubState::SelectingIdentity { repo, .. } => {
*repo = Some(repo_state)
}
model::ManagingRepoSubState::Ready(ReadyState {
repo,
user_id: _,
selection,
navigation: _,
record_changes: _,
forking_channel_name: _,
logs,
to_record: _,
jobs: _,
record_dichotomy: _,
}) => {
*repo = repo_state;
if invalidate_logs {
*logs = default();
};
let selection_task =
reindex_selection(repo, selection, &mut state.files, logs);
return selection_task;
}
model::ManagingRepoSubState::NoIdFound { repo } => {
*repo = Some(repo_state)
}
}
Task::none()
}
fn reindex_selection(
repo: &repo::State,
selection: &mut selection::State,
files: &mut file::State,
logs: &Logs,
) -> Task<ManagingRepoMsg> {
let status_task = if let Some(current_selection) = selection.status.take() {
let (new_selection, task) = match current_selection {
selection::Status::UntrackedFile {
ix: _,
path,
diff_selected,
} => {
let selection = repo
.untracked_files
.iter()
.enumerate()
.find(|(_ix, file_path)| *file_path == &path)
.map(|(ix, _path)| selection::Status::UntrackedFile {
ix,
path: path.clone(),
diff_selected,
});
if selection.is_some() {
file::load_src_file_if_not_cached(
files,
file::Id {
path,
file_kind: file::Kind::Untracked,
},
);
}
(selection, Task::none())
}
selection::Status::ChangedFile {
ix: _,
path,
diff_selected,
} => {
let selection = repo
.changed_files
.iter()
.enumerate()
.find(|(_ix, (file_path, _diffs))| *file_path == &path)
.map(|(ix, (file_path, _diffs))| {
selection::Status::ChangedFile {
ix,
path: file_path.clone(),
diff_selected,
}
});
let task = if selection.is_some()
&& let Some(diffs) = repo.changed_files.get(&path)
{
let id = file::Id {
path: path.clone(),
file_kind: file::Kind::Changed,
};
if diff::should_file_exist(diffs) {
file::load_src_file_if_not_cached(files, id);
Task::none()
} else {
file::src_file_doesnt_exist(files, id, diffs)
.map(ManagingRepoMsg::File)
}
} else {
Task::none()
};
(selection, task)
}
selection::Status::LogChange(selection::LogChange {
ix: _,
hash,
message,
file,
}) => {
let selection = repo
.short_log
.iter()
.enumerate()
.find(|(_ix, entry)| entry.hash == hash)
.map(|(ix, entry)| {
let file = file.and_then(
|selection::LogChangeFileSelection {
ix: _,
path: selected_path,
diff_selected,
}| {
entry
.file_paths
.iter()
.enumerate()
.find(|(_ix, path)| *path == &selected_path)
.map(|(ix, path)| {
selection::LogChangeFileSelection {
ix,
path: path.clone(),
diff_selected,
}
})
},
);
selection::Status::LogChange(selection::LogChange {
ix,
hash: entry.hash,
message,
file,
})
});
let task = if selection.is_some() {
Task::done(ManagingRepoMsg::ToRepo(
repo::MsgIn::GetChangeDiffs { hash },
))
} else {
Task::none()
};
(selection, task)
}
};
selection.status = new_selection;
task
} else {
Task::none()
};
let channel_task = if let Some(selection::Channel { ix: _, name, log }) =
selection.channel.take()
{
let (new_selection, task) = {
let selection = repo
.other_channels
.iter()
.enumerate()
.find(|(_ix, channel_name)| *channel_name == &name)
.map(|(ix, _name)| selection::Channel { ix, name, log });
let task = if let Some(selection::Channel { name, .. }) =
selection.as_ref()
{
Task::done(ManagingRepoMsg::ToRepo(
repo::MsgIn::LoadOtherChannelLog(name.clone()),
))
} else {
Task::none()
};
(selection, task)
};
selection.channel = new_selection;
task
} else {
Task::none()
};
let entire_log_task = if let Some(selection::LogChange {
ix: _,
hash,
message,
file,
}) = selection.entire_log.take()
{
let new_selection =
if let Some(Log::Loaded { log }) = logs.entire_log.as_ref() {
log.iter()
.enumerate()
.find(|(_ix, entry)| entry.hash == hash)
.map(|(ix, entry)| {
let file = file.and_then(
|selection::LogChangeFileSelection {
ix: _,
path: selected_path,
diff_selected,
}| {
entry
.file_paths
.iter()
.enumerate()
.find(|(_ix, path)| *path == &selected_path)
.map(|(ix, path)| {
selection::LogChangeFileSelection {
ix,
path: path.clone(),
diff_selected,
}
})
},
);
selection::LogChange {
ix,
hash: entry.hash,
message,
file,
}
})
} else {
None
};
let task = if new_selection.is_some() {
Task::done(ManagingRepoMsg::ToRepo(repo::MsgIn::LoadEntireLog))
} else {
Task::none()
};
selection.entire_log = new_selection;
task
} else {
Task::none()
};
Task::batch([status_task, channel_task, entire_log_task])
}
fn repo_got_change_diffs(
model: &mut model::ManagingRepo,
hash: repo::ChangeHash,
diffs: repo::ChangedFiles,
) -> Task<ManagingRepoMsg> {
if let Some(ReadyState {
navigation,
selection,
..
}) = model::is_ready_mut(model)
{
navigation.log_diffs.changes_with_loaded_diffs.insert(hash);
diffs.into_iter().for_each(|(path, diffs)| {
let file = diff::init_file(
diff::FileContent::UnknownEncoding,
Some(&diffs),
);
let file_id = file::log_id_parts_hash(hash, &path.raw);
let log_file_diff = diff::FileAndState {
file,
state: diff::State::default(),
};
navigation.log_diffs.diffs.insert(file_id, log_file_diff);
});
let selected_file_and_its_navs = match selection::unify(selection) {
selection::Unified::Status(Some(selection::Status::LogChange(
selection::LogChange {
ix: _,
hash: selected_hash,
message: _,
file,
},
))) if *selected_hash == hash => file
.as_ref()
.map(|file| (file, &mut navigation.status_logs_navs)),
selection::Unified::EntireLog(Some(selection::LogChange {
ix: _,
hash: selected_hash,
message: _,
file,
})) if *selected_hash == hash => file
.as_ref()
.map(|file| (file, &mut navigation.entire_logs_navs)),
selection::Unified::Channel(Some(selection::Channel {
ix: _,
name: _,
log:
Some(selection::LogChange {
ix: _,
hash: selected_hash,
message: _,
file,
}),
})) if *selected_hash == hash => file
.as_ref()
.map(|file| (file, &mut navigation.other_channel_logs_navs)),
selection::Unified::CompareRemote(Some(
selection::CompareRemote {
hash: Some(selected_hash),
file,
..
},
)) if *selected_hash == hash => file
.as_ref()
.map(|file| (file, &mut navigation.compare_remote_navs)),
selection::Unified::Status(_)
| selection::Unified::Channel(_)
| selection::Unified::EntireLog(_)
| selection::Unified::CompareRemote(_) => None,
};
if let Some((file, navs)) = selected_file_and_its_navs {
let file_id = file::log_id_parts_hash(hash, &file.path);
if let Some(log) = navigation.log_diffs.diffs.get(&file_id) {
let unchanged_sections = diff::unchanged_sections(&log.file);
log::init_diffs_nav(navs, file_id)
.set_skip_sections(unchanged_sections);
}
}
}
Task::none()
}
fn got_entire_log(
model: &mut model::ManagingRepo,
log: repo::Log,
) -> Task<ManagingRepoMsg> {
if let Some(ReadyState { logs, .. }) = model::is_ready_mut(model) {
logs.entire_log = Some(Log::Loaded { log });
}
Task::none()
}
fn loaded_other_channel_log(
model: &mut model::ManagingRepo,
channel: String,
log: Vec<repo::LogEntry>,
) -> Task<ManagingRepoMsg> {
if let Some(ReadyState { logs, .. }) = model::is_ready_mut(model) {
logs.other_channels_logs
.insert(channel, Log::Loaded { log });
}
Task::none()
}
fn subs(state: &State) -> Subscription<Msg> {
use iced::keyboard::{self, key, Key};
let allowed_actions = state.model.allowed_actions.clone();
let key_subs = keyboard::listen().with(allowed_actions).filter_map(
|(allowed_actions, event)| {
let unfiltered = |selection| {
Some(Msg::View(view::Msg::UnfilteredSelection(selection)))
};
match event {
keyboard::Event::KeyPressed { key, modifiers, .. } => {
allowed_actions
.iter()
.find_map(|binding| {
binding.msg.as_ref().and_then(|msg| {
if binding.keys.iter().any(|binding| {
binding.key == key
&& binding.mods == modifiers
}) {
Some(msg.clone())
} else {
None
}
})
})
.map(view::Msg::Action)
.map(Msg::View)
}
keyboard::Event::KeyReleased {
key,
modifiers: mods,
..
} => {
if mods.is_empty() {
match key {
Key::Character(c) => match c.as_str() {
"j" => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Down,
),
),
"k" => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Up,
),
),
"h" => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Left,
),
),
"l" => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Right,
),
),
_ => None,
},
Key::Named(key::Named::ArrowDown) => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Down,
),
),
Key::Named(key::Named::ArrowUp) => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Up,
),
),
Key::Named(key::Named::ArrowLeft) => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Left,
),
),
Key::Named(key::Named::ArrowRight) => unfiltered(
selection::UnfilteredMsg::ReleaseDir(
selection::Dir::Right,
),
),
Key::Named(_) | Key::Unidentified => None,
}
} else {
None
}
}
keyboard::Event::ModifiersChanged(_) => None,
}
},
);
let window_subs = window::events().map(|(_id, event)| Msg::Window(event));
Subscription::batch([key_subs, window_subs])
}
fn view(state: &State, window_id: window::Id) -> Element<'_, Msg, Theme> {
let get_diff = |id_hash| {
if let Some(managing_repo) = &state.managing_repo {
file::try_get_src_file(&managing_repo.files, id_hash)
} else {
None
}
};
view::main(&state.model, get_diff, window_id).map(Msg::View)
}
fn title(state: &State, _: window::Id) -> String {
const APP: &str = "Inflorescence";
match &state.model.sub {
model::SubState::PickingProject(_)
| model::SubState::PickingRepoDir(_) => {
format!("{APP}: project picker")
}
model::SubState::ManagingRepo(managing_repo) => {
format!(
"{APP}: {}",
managing_repo
.repo_path
.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_else(|| managing_repo
.repo_path
.to_string_lossy())
)
}
}
}