ACDXXAX26ZJJFKJDGRC2GOSJY5JHQWCSTP55SYI6D6LH5UIRYUBAC UB2ITZJSDADVINSQEZ3HA6PVGA7OA6JYFG5GMSO7Y7LOXJC4FI7AC EC3TVL4X6VZZVLOKUN63LC73ADPHBHMZO7QMDXGX2ZPURVI4B4XQC KT5UYXGKEEXUHURNOYFVIG7WQ3Y3SJZMM2TP4OSW6NXSXQ5XXRHAC ELG3UDT6OJFEYSJR7HZEC65IUWBMGPPPCXEW3CDW5T74R6KC5LIAC S2NVIFXRFER4SRA37WCT5XTXHDHAL5WIGGKY4A4XOTPLTKTZSRGQC W7IUT3ZVMFH77IGKLAL7WX7IVVTGTY3FKEJ3WHMP3KI37B6NENLQC YBJRDOTCX3ZRDB5EVXJBR55FX3CADCSIGMYWNYVC2PD5W3GXR3DQC KM5PSZ4A2FJOPHJA6RC7LHZAUXLQDZDQC2DVSE5YUORLFIPZO74QC 2VUX5BTDKHX3TJ677NW34H5WLSWH35C3PU46C7MXCN5O7PAZVXNQC A5YBC77VWH2LXCZJOPZORQJI5ZYABSCHJWVX5HVNWPM5RABXESLQC Z2CJPWZECB4PH6DI6Q2DEZIZJ3E2C3IXR3FGBXOLGAP6NVEX2ZDAC D7A7MSIHJS3IAOLEPK52M4CZLDPLO7JB3Y62XACT2AM6UUCPQ6BAC UCBNZULEO6OIEV3RZCAPP6ICALAR7JIS2LLG7IRSX6PVYMFQT5AAC 4WO3ZJM2RNYZCBPS7FGYAEBELYD57OSS7LEUYCWGZBCAY272SNQQC BJXUYQ2YQMVULJITT5FEA6NERJVLWFKEAWSBYZVIB7KAT27KOWBAC W4LFX7IHQ7SDX67ATSGWDB5IN6472ZJDBKY2XZ54SBJEYD5GAT5QC AMPZ2BXK4IGUZO3OPBRSJ6Z4GI5K4PRFMLUGTR6AP4FKKRWQG7LQC AXSXZQDGLPSLBYY3WEI5CCJFWFY33HPRVW6QQFL46OKWZH4G4YSAC V55EAIWQXWER2HWKZHPJBV7DDJMSPSPWSO3FSSAYODJHVDBHUN6QC NRCUG4R2NIM2ANIETSUZ7WZDXFOOCMJ73ROP5MDYJA4RUT4PYA4QC Y5ATDI2HRWTTYJAVUR7SVWQVB4ZKKDZF3UVE4JJQFZ7RX7H7VPJQC ZVI4AWERNOTDJ3765HJXRBZT57XPNKVONQ6TGOGNPOL2VN42KMJQC QMAUTRB6R5R7ABWT2JIDEA7LMILZOS3PGPZIF3YUFKRVLW6HGKTQC OQ6HSAWHIRTAIIWMDGCTIOK47JDY7QVVAHLRDA2R5TTJKNSBFCWQC AHWWRC73FXLSUDAJBU5UU76MZETHD3DSGJ7OLZPFEHXBDJ733QNAC JE44NYHM4QORCRKOF33QM42EDT7SBCPTULWGT6IVDL3D5LUHQXLAC ONRCENKTUB4JJMPXNAQQYEWDYD54TAGOLWH742GF4EH3KTHV7YLQC 4ELJZGRJNL6FXB33QTYDNPY57JA3WZPUXKLQRTGSLDM7W65PD3YQC HC7ROIBC66IBYFED4ZZM7RXGSNC2CCBWBI36RKM2G5FD5DKVEYMQC FR52XEMWD22VH3GKSARXJUJXOGO7ZSQEHWPXFRWHLGRAJU3WRKCAC L6KSEFQIWICZJ6HJUFKLZQDEH6X2QMFM4Z7ZZUGMLDMFF7EHRXWAC BFN2VHZS7VCBUHQ4S3CQ3LFQV2V4M6VANNAF32XMRFQVWRGYSZ6AC 23SFYK4Q5NKBPJG53PQNPWQH6UOUU2YKJEL7RLXYBRLJOJYV7AWQC WGID4LS4EISIOXB5Y5SOFGEF5PLBJSCPFCETH2CGRTFN3NC4WGJQC SWWE2R6MVBX5CNM6X3WLXZTSRTU53PBJL7WJSFVF77XBPXDX4COAC 6YZAVBWU6E5FYOI5JGEIPXGZLIKAW6LS2AOFIQWEE5DMOPPCD5PQC }Msg::AddUntrackedFile => {if let Some(repo) = state.repo.as_mut() {if let Some(cursor::Selection::UntrackedFile { ix, path }) =state.cursor.selection.as_ref(){state.repo_tx_in.send(repo::MsgIn::AddUntrackedFile {path: path.clone(),}).unwrap();let removed = repo.untracked_files.remove(path);debug_assert!(removed,"{:?}, path: {path}",repo.untracked_files);repo.changed_files.entry(path.clone()).or_default().insert(repo::ChangedFileDiff::Add);// Select the next untracked file, if anystate.cursor.selection = if repo.untracked_files.is_empty(){None} else {let ix = cmp::min(*ix, repo.untracked_files.len() - 1);Some(cursor::untracked_file_selection(repo,ix,&mut state.files,))};}}Task::none()}Msg::RmAddedFile => {if let Some(repo) = state.repo.as_mut() {if let Some(cursor::Selection::ChangedFile { ix, path }) =state.cursor.selection.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.clone(),}).unwrap();// Remove from changed fileslet removed = repo.changed_files.remove(path);debug_assert!(removed.is_some(),"{:?} not found in {:?}",path,repo.changed_files);// Update untracked filesrepo.untracked_files.insert(path.clone());// Select the next changed file, if anystate.cursor.selection =if repo.changed_files.is_empty() {None} else {let ix =cmp::min(*ix, repo.changed_files.len() - 1);Some(cursor::changed_file_selection(repo,ix,&mut state.files,))};}}}Task::none()}Msg::Record => {if let Some(repo) = state.repo.as_ref() {if state.id.is_none() {info!("Requested to record, but ID is not yet loaded");}if repo.changed_files.is_empty() {info!("Trying to record with no changed files");} else if let Some(RecordMsg::Typing(_)) =state.record_msg.as_ref(){info!("Requested to record, but already recording");return Task::none();} else {let content = match state.record_msg.take() {Some(RecordMsg::Canceled { old_msg }) => {text_editor::Content::with_text(&old_msg)}None | Some(RecordMsg::Typing(_)) => {text_editor::Content::new()}};state.record_msg = Some(RecordMsg::Typing(content));// TODO: change to use ID once https://github.com/iced-rs/iced/pull/2653 is mergedreturn task::widget_focus_next();}}Task::none()
app::Msg::EditRecordMsg(action) => {if let Some(RecordMsg::Typing(record_msg)) =state.record_msg.as_mut(){record_msg.perform(action);}Task::none()
app::Msg::EditRecordMsg(action) => edit_record_msg(state, action),app::Msg::SaveRecord => save_record(state),app::Msg::DeferRecord => defer_record(state),app::Msg::AbandonRecord => abandon_record(state),app::Msg::FileDiffsContentsAction { id, action } => {file_diffs_contents_action(state, id, action)
app::Msg::SaveRecord => {if let Some(RecordMsg::Typing(record_msg)) =state.record_msg.as_ref(){let msg = record_msg.text();if msg.trim().is_empty() {info!("Cannot record with an empty message");} else {let id = state.id.as_ref().expect("ID must be loaded at this point");// TODO: this call has CLI prompt - replace itlet (sk, _) = id.decrypt().unwrap();
app::Msg::LogChangeFileDiffAction { hash, file, action } => {log_change_file_diff_action(state, hash, file, action)}}}fn update_from_repo(state: &mut State, msg: repo::MsgOut) -> Task<Msg> {match msg {repo::MsgOut::Init(repo) => repo_init(state, repo),repo::MsgOut::Refreshed(repo) => repo_refreshed(state, repo),repo::MsgOut::GotChangeDiffs { hash, diffs } => {repo_got_change_diffs(state, hash, diffs)}}}
state.record_msg = None;state.repo.as_mut().unwrap().changed_files =repo::ChangedFiles::default();
fn add_untracked_file(state: &mut State) -> Task<Msg> {if let Some(repo) = state.repo.as_mut() {if let Some(cursor::Selection::UntrackedFile { ix, path }) =state.cursor.selection.as_ref(){state.repo_tx_in.send(repo::MsgIn::AddUntrackedFile { path: path.clone() }).unwrap();
let sk = Arc::new(sk);state.repo_tx_in.send(repo::MsgIn::Record { msg, sk }).unwrap();}}Task::none()
let removed = repo.untracked_files.remove(path);debug_assert!(removed, "{:?}, path: {path}", repo.untracked_files);repo.changed_files.entry(path.clone()).or_default().insert(repo::ChangedFileDiff::Add);// Select the next untracked file, if anystate.cursor.selection = if repo.untracked_files.is_empty() {None} else {let ix = cmp::min(*ix, repo.untracked_files.len() - 1);Some(cursor::untracked_file_selection(repo,ix,&mut state.files,))};
app::Msg::DeferRecord => {if let Some(RecordMsg::Typing(record_msg)) =state.record_msg.as_ref()
}Task::none()}fn rm_added_file(state: &mut State) -> Task<Msg> {if let Some(repo) = state.repo.as_mut() {if let Some(cursor::Selection::ChangedFile { ix, path }) =state.cursor.selection.as_ref(){let diffs = repo.changed_files.get(path).unwrap();if diffs.iter().any(|diff| matches!(diff, repo::ChangedFileDiff::Add))
let old_msg = record_msg.text();state.record_msg = if !old_msg.trim().is_empty() {Some(RecordMsg::Canceled { old_msg })} else {
state.repo_tx_in.send(repo::MsgIn::RmAddedFile { path: path.clone() }).unwrap();// Remove from changed fileslet removed = repo.changed_files.remove(path);debug_assert!(removed.is_some(),"{:?} not found in {:?}",path,repo.changed_files);// Update untracked filesrepo.untracked_files.insert(path.clone());// Select the next changed file, if anystate.cursor.selection = if repo.changed_files.is_empty() {
app::Msg::AbandonRecord => {if let Some(RecordMsg::Typing(_)) = state.record_msg.as_ref() {state.record_msg = None;}Task::none()
}Task::none()}fn start_record(state: &mut State) -> Task<Msg> {if let Some(repo) = state.repo.as_ref() {if state.id.is_none() {info!("Requested to record, but ID is not yet loaded");
app::Msg::FileDiffsContentsAction { id, action } => {if let Some(selection) = state.cursor.selection.as_mut() {match (selection, id.file_kind) {(cursor::Selection::UntrackedFile { ix: _, path },file::Kind::Untracked,) if path == &id.path => {let diffs = state.diffs_state.entry(id).or_default();diff::update(diffs, action);}(cursor::Selection::ChangedFile { ix: _, path },file::Kind::Changed,) if path == &id.path => {let diffs = state.diffs_state.entry(id).or_default();diff::update(diffs, action);}_ => {// Selection has changed}
if repo.changed_files.is_empty() {info!("Trying to record with no changed files");} else if let Some(RecordMsg::Typing(_)) = state.record_msg.as_ref() {info!("Requested to record, but already recording");return Task::none();} else {let content = match state.record_msg.take() {Some(RecordMsg::Canceled { old_msg }) => {text_editor::Content::with_text(&old_msg)}None | Some(RecordMsg::Typing(_)) => {text_editor::Content::new()
};state.record_msg = Some(RecordMsg::Typing(content));// TODO: change to use ID once https://github.com/iced-rs/iced/pull/2653 is mergedreturn task::widget_focus_next();}}Task::none()}fn edit_record_msg(state: &mut State,action: text_editor::Action,) -> Task<Msg> {if let Some(RecordMsg::Typing(record_msg)) = state.record_msg.as_mut() {record_msg.perform(action);}Task::none()}fn save_record(state: &mut State) -> Task<Msg> {if let Some(RecordMsg::Typing(record_msg)) = state.record_msg.as_ref() {let msg = record_msg.text();if msg.trim().is_empty() {info!("Cannot record with an empty message");} else {let id =state.id.as_ref().expect("ID must be loaded at this point");// TODO: this call has CLI prompt - replace itlet (sk, _) = id.decrypt().unwrap();state.record_msg = None;state.repo.as_mut().unwrap().changed_files =repo::ChangedFiles::default();let sk = Arc::new(sk);state.repo_tx_in.send(repo::MsgIn::Record { msg, sk }).unwrap();}}Task::none()}fn defer_record(state: &mut State) -> Task<Msg> {if let Some(RecordMsg::Typing(record_msg)) = state.record_msg.as_ref() {let old_msg = record_msg.text();state.record_msg = if !old_msg.trim().is_empty() {Some(RecordMsg::Canceled { old_msg })} else {None};}Task::none()}fn abandon_record(state: &mut State) -> Task<Msg> {if let Some(RecordMsg::Typing(_)) = state.record_msg.as_ref() {state.record_msg = None;}Task::none()}fn file_diffs_contents_action(state: &mut State,id: file::Id,action: diff::Msg,) -> Task<Msg> {if let Some(selection) = state.cursor.selection.as_mut() {match (selection, id.file_kind) {(cursor::Selection::UntrackedFile { ix: _, path },file::Kind::Untracked,) if path == &id.path => {let diffs = state.diffs_state.entry(id).or_default();diff::update(diffs, action);
app::Msg::LogChangeFileDiffAction { hash, file, action } => {if let Some(cursor::Selection::LogChange {
}Task::none()}fn log_change_file_diff_action(state: &mut State,hash: pijul::Hash,file: String,action: diff::Msg,) -> Task<Msg> {if let Some(cursor::Selection::LogChange {ix: _,hash: selected_hash,message: _,diffs: Some(diffs),file:Some(cursor::LogChangeFileSelection {
hash: selected_hash,message: _,diffs: Some(diffs),file:Some(cursor::LogChangeFileSelection {ix: _,path: selected_path,}),}) = state.cursor.selection.as_mut(){if *selected_hash == hash && *selected_path == file {let (_file, state) = diffs.get_mut(&file).unwrap();diff::update(state, action);}}Task::none()
path: selected_path,}),}) = state.cursor.selection.as_mut(){if *selected_hash == hash && *selected_path == file {let (_file, state) = diffs.get_mut(&file).unwrap();diff::update(state, action);
fn update_from_repo(state: &mut State, msg: repo::MsgOut) -> Task<Msg> {match msg {repo::MsgOut::Init(repo) => {state.repo = Some(repo);file::diffs_cache_clear(&mut state.files.diffs_cache);
fn repo_init(state: &mut State, repo: repo::State) -> Task<Msg> {state.repo = Some(repo);file::diffs_cache_clear(&mut state.files.diffs_cache);// Start watching the repo's dir for changeslet (fs_watch_tx, fs_watch_rx) = watch::channel(());let mut fs_watch = new_debouncer(Duration::from_secs(1),None,move |result: DebounceEventResult| match result {Ok(events) => events.iter().for_each(|event| {// TODO: distinguish ".pijul" changes// dbg!(event);if event.kind.is_create()|| event.kind.is_modify()|| event.kind.is_remove(){let _ = fs_watch_tx.send(());}}),Err(errors) => {errors.iter().for_each(|error| eprintln!("{error:?}"))}},).unwrap();fs_watch.watch(&state.repo_path, RecursiveMode::Recursive).unwrap();let fs_watch_rx = WatchStream::from_changes(fs_watch_rx);let watch_task = Task::run(fs_watch_rx, |()| {Msg::View(app::Msg::ToRepo(repo::MsgIn::RefreshChangedAndUntrackedFiles,))});state.repo_fs_watch = Some(fs_watch);watch_task}
// Start watching the repo's dir for changeslet (fs_watch_tx, fs_watch_rx) = watch::channel(());let mut fs_watch = new_debouncer(Duration::from_secs(1),None,move |result: DebounceEventResult| match result {Ok(events) => events.iter().for_each(|event| {// TODO: distinguish ".pijul" changes// dbg!(event);if event.kind.is_create()|| event.kind.is_modify()|| event.kind.is_remove(){let _ = fs_watch_tx.send(());}}),Err(errors) => {errors.iter().for_each(|error| eprintln!("{error:?}"))}},).unwrap();fs_watch.watch(&state.repo_path, RecursiveMode::Recursive).unwrap();let fs_watch_rx = WatchStream::from_changes(fs_watch_rx);let watch_task = Task::run(fs_watch_rx, |()| {Msg::View(app::Msg::ToRepo(repo::MsgIn::RefreshChangedAndUntrackedFiles,))});
fn repo_refreshed(state: &mut State, repo: repo::State) -> Task<Msg> {let repo::State {dir_name: _,channel: _,untracked_files,changed_files,log,} = &repo;file::diffs_cache_clear(&mut state.files.diffs_cache);
state.repo_fs_watch = Some(fs_watch);watch_task}repo::MsgOut::RefreshedState(repo_state) => {let repo::State {dir_name: _,channel: _,untracked_files,changed_files,log,} = &repo_state;file::diffs_cache_clear(&mut state.files.diffs_cache);
// Re-index cursor selectionlet task = if let Some(selection) = state.cursor.selection.take() {// Try to find the file with the same name. If not found, remove// selectionlet (selection, task) = match selection {cursor::Selection::UntrackedFile { ix: _, path } => {file::load_src_file_if_not_cached(&mut state.files,file::Id {path: path.clone(),file_kind: file::Kind::Untracked,},);
// Re-index cursor selectionlet task = if let Some(selection) = state.cursor.selection.take() {// Try to find the file with the same name. If not found, remove// selectionlet (selection, task) = match selection {cursor::Selection::UntrackedFile { ix: _, path } => {
let selection = untracked_files.iter().enumerate().find(|(_ix, file_path)| *file_path == &path).map(|(ix, _path)| cursor::Selection::UntrackedFile {ix,path,});(selection, Task::none())}cursor::Selection::ChangedFile { ix: _, path } => {if let Some(diffs) = changed_files.get(&path) {if diff::any_diff_has_contents(diffs) {
let selection = untracked_files.iter().enumerate().find(|(_ix, file_path)| *file_path == &path).map(|(ix, _path)| {cursor::Selection::UntrackedFile { ix, path }});(selection, Task::none())
cursor::Selection::ChangedFile { ix: _, path } => {if let Some(diffs) = changed_files.get(&path) {if diff::any_diff_has_contents(diffs) {file::load_src_file_if_not_cached(&mut state.files,file::Id {
}let selection = changed_files.iter().enumerate().find(|(_ix, (file_path, _diffs))| *file_path == &path).map(|(ix, (file_path, _diffs))| {cursor::Selection::ChangedFile {ix,path: file_path.clone(),}});(selection, Task::none())}cursor::Selection::LogChange {ix: _,hash,message,diffs: _,file,} => {// Request to get the diffslet task = Task::done(Msg::View(app::Msg::ToRepo(repo::MsgIn::GetChangeDiffs { hash },)));let selection = log.iter().enumerate().find(|(_ix, entry)| entry.hash == hash).map(|(ix, entry)| {let file = file.and_then(|file| {entry.file_paths.iter().enumerate().find(|(_ix, path)| *path == &file.path).map(|(ix, path)| {cursor::LogChangeFileSelection {ix,
let selection = changed_files.iter().enumerate().find(|(_ix, (file_path, _diffs))| {*file_path == &path}).map(|(ix, (file_path, _diffs))| {cursor::Selection::ChangedFile {ix,path: file_path.clone(),}});(selection, Task::none())}cursor::Selection::LogChange {ix: _,hash,message,diffs: _,file,} => {// Request to get the diffslet task = Task::done(Msg::View(app::Msg::ToRepo(repo::MsgIn::GetChangeDiffs { hash },)));
(selection, task)}};
let selection = log.iter().enumerate().find(|(_ix, entry)| entry.hash == hash).map(|(ix, entry)| {let file = file.and_then(|file| {entry.file_paths.iter().enumerate().find(|(_ix, path)| *path == &file.path).map(|(ix, path)| {cursor::LogChangeFileSelection {ix,path: path.clone(),}})});cursor::Selection::LogChange {ix,hash: entry.hash,message,diffs: None,file,}});
state.cursor.selection = selection;task} else {Task::none()};
state.repo = Some(repo_state);task}repo::MsgOut::GotChangeDiffs { hash, diffs } => {if let Some(cursor::Selection::LogChange {ix: _,hash: selected_hash,message: _,diffs: selection_diffs @ None,file: _,}) = state.cursor.selection.as_mut(){if *selected_hash == hash {let diffs = diffs.into_iter().map(|(path, diffs)| {// NOTE: using unknown encoding as we don't yet have// the file for past// changeslet file = diff::init_file(diff::FileContent::UnknownEncoding,Some(&diffs),);(path, (file, diff::State::default()))}).collect();*selection_diffs = Some(diffs);}}Task::none()
fn repo_got_change_diffs(state: &mut State,hash: pijul::Hash,diffs: repo::ChangedFiles,) -> Task<Msg> {if let Some(cursor::Selection::LogChange {ix: _,hash: selected_hash,message: _,diffs: selection_diffs @ None,file: _,}) = state.cursor.selection.as_mut(){if *selected_hash == hash {let diffs = diffs.into_iter().map(|(path, diffs)| {// NOTE: using unknown encoding as we don't yet have// the file for past// changeslet file = diff::init_file(diff::FileContent::UnknownEncoding,Some(&diffs),);(path, (file, diff::State::default()))}).collect();*selection_diffs = Some(diffs);