YRGDFHABL6BRX55ZWIBGXX3ZX2R4WUV4BELP7JMW5AZX54P5BBIQC 6YZAVBWU6E5FYOI5JGEIPXGZLIKAW6LS2AOFIQWEE5DMOPPCD5PQC IQDCHWCP47LL46EXQLQGHQPGFYIHQLMQBHA57RWJCIOX5UEUIQAQC WT3GA27PQ2AOAIGK65O3Q4DMX4AZDVNULBLRL6GF4QW6QCASUEAAC UB2ITZJSDADVINSQEZ3HA6PVGA7OA6JYFG5GMSO7Y7LOXJC4FI7AC EC3TVL4X6VZZVLOKUN63LC73ADPHBHMZO7QMDXGX2ZPURVI4B4XQC S2NVIFXRFER4SRA37WCT5XTXHDHAL5WIGGKY4A4XOTPLTKTZSRGQC YBJRDOTCX3ZRDB5EVXJBR55FX3CADCSIGMYWNYVC2PD5W3GXR3DQC KM5PSZ4A2FJOPHJA6RC7LHZAUXLQDZDQC2DVSE5YUORLFIPZO74QC 2VUX5BTDKHX3TJ677NW34H5WLSWH35C3PU46C7MXCN5O7PAZVXNQC A5YBC77VWH2LXCZJOPZORQJI5ZYABSCHJWVX5HVNWPM5RABXESLQC D7A7MSIHJS3IAOLEPK52M4CZLDPLO7JB3Y62XACT2AM6UUCPQ6BAC 4WO3ZJM2RNYZCBPS7FGYAEBELYD57OSS7LEUYCWGZBCAY272SNQQC AMPZ2BXK4IGUZO3OPBRSJ6Z4GI5K4PRFMLUGTR6AP4FKKRWQG7LQC V55EAIWQXWER2HWKZHPJBV7DDJMSPSPWSO3FSSAYODJHVDBHUN6QC 6SW7UVSHRWJYE2PWVXULTUGEGD432T775EX6EKVEFRO3MDVVAG3AC HOJZI52YIXKAYF766WR3SAOIFZH6YRMDOUE23VWEYNBZRBGEU25AC ZVI4AWERNOTDJ3765HJXRBZT57XPNKVONQ6TGOGNPOL2VN42KMJQC JE44NYHM4QORCRKOF33QM42EDT7SBCPTULWGT6IVDL3D5LUHQXLAC HC7ROIBC66IBYFED4ZZM7RXGSNC2CCBWBI36RKM2G5FD5DKVEYMQC BFN2VHZS7VCBUHQ4S3CQ3LFQV2V4M6VANNAF32XMRFQVWRGYSZ6AC 3SYSJKYLVCXR54LRUPL6GOQISSJS6XWK4M6PRQRCKZN7F23NNVEAC 23SFYK4Q5NKBPJG53PQNPWQH6UOUU2YKJEL7RLXYBRLJOJYV7AWQC OPXFZKEBDHZZLXEJ2JRDYBOJH6YIN7UZNZYHVHMWMQVDTE2ZD53QC 3QVNMRNMI63L2VOFVTMPCVPXH3J4JXLXVTIIPNOMACQCPCAPWILQC MYGIBRRHHXPKVRAMQQRJTZH74L2XOK3SF7J57JPCRKSVRLZ2D6NQC PKJCFSBMXXA2H3US47IJEB7QMIYLEKTLGWQUYEZSKCDODDQTD6HQC WGID4LS4EISIOXB5Y5SOFGEF5PLBJSCPFCETH2CGRTFN3NC4WGJQC VCNKFNUF7OWVSWC6I5D25KUZ3XZZICZ3LHWVPF2N5ZSP7LQ2JOUQC 6F7Q4ZLR5DGYT557MYMSHMZGQ7EVEB3LZGLZFCWHGIOI66STIANQC ACDXXAX26ZJJFKJDGRC2GOSJY5JHQWCSTP55SYI6D6LH5UIRYUBAC FVA36HBVXZCYW7FMQLST63Q6IDGLJ23OIHORF67BUIO2GXYNBW2QC I56UGW7UUKLSR4753EYRGNROZB5PD522REEOGHVAQOZZTSVRUEEQC X6AK4QPXKTGTWIMJ5CIR46CVIXVUXV5WKTP73CNQOIRANQN4MD5QC YYKXNBFL44LLOBABLXBKOF7IFUIGIEL2SYIPLGDH6UOEY5EZZZSQC 5CYU7UT74NXJWCC36GNQGVBXH676BHBXWZQVIINRMPDEJ27SBRGAC ESMM3FELOBYIX7FUNOU37FYKRJHFU2IMX6LY6EGJTVPTBDU3SEEQC TSFQFCB2NXDOBLBRUSAT63VJIXLPPTJGSTIDNOTLGHVVWSHITRNQC I2AG42PAVOII4V4TWDJV5ZVNDIHKBRDT254BFQLFUIY723TW6CCQC SASAN2XCWDQ2VEHZ7TAQEN2R3Y7AG7JUGEFVRL4DZAGHXDFEZFRQC YKHE3XMWOWPGOWYSISF73MIAKN7WB3AHCV2OA4ECAFPF47YHUXEAC KWTBNTO3QUUE2YADF6SYW6G6ZOKYEWRJQKIWDGZXR33S3YNDVIZQC 5MUEECMJHU44FL5RDUR3VFBIWK3H4X2L5MVJ73J37PYHZWLUKU2AC 3TLPJ57B2OD5OWJN5WMS7A4W7IGFUWJJHVIXRM34VT6KUN6R4YSAC WXQBBQ2ACNPKCTDF7OTBLP342324ZIOJK42PUO2KT2IYVJ2ETCMAC PTWZYQFRWWUOE2WMQT26CKZKFSHAIJVJS3QWHJFYUFDRRTVPHSUAC RDRBP7AL74NBFNZSQFTU7VQCMWTGJO5RZWGPCWVVS5WRTXJ77DFAC UR4J677RWA3OFG6HQTD46BUUE5YFPSBEFCJAEM5OMT4V5A7SBNNQC A6Z4O6RC33HYWP7JIVQ6FDWE4EOCQWQTIGENK2WAHUGSHDDLSA7QC JZXYSIYDPBWQZCAMGDZ5BFMN6SU73EVVDIYEGTDJN6DVOSBNHN4QC 5ZRDYL6KIQPUI3ZZETH5KJ64N6RUF7KYM3P6Q6HER5XVJZ7GZ4WQC BAUK5BONEFQ3KIQPFLM7MGNCS5GWBILBXZMTIGN5LWTYTNNNNSPQC OJPGHVC3RFBQ7TTSCZH6URSSATII3TESD74EISDNOTNXXSX7PQMAC 7MJOO4E2VGNT7FKBOJUX6JDG4OET6V7DH3JIERUQXXJPM2AJQCNQC FJSVMFB4FRZV6VXQTQ3FWY3GRHSM5RLYNCZ67JIDN254CVY7QFOQC 3XRG4BB6V5V4DICZCMOZMLQNTANWKPO7BBRATTXOZLRNSEUQIA5AC WAOGSCOJ5A372BZKHEYD2BCDBCENNVLFYW3INKUOOAZMDADDIFIQC WH57EHNML4OTGQQZBT2SG6SOFTBOD6OJPJYHJVGPH22CSSOE25AAC EJPSD5XO43DWUBBZGNQMY4TMCAXL5EWCGX3OEHUERQ5GRASGWQLQC YK3MOJJLRYEKZ4FUCNJ3YKMTKOINWIYOJKR3ER7IRSGTC7O6FJZQC TQEZQJV4G4OXAYDLTMJW3NCRQV6W7I3MUBOKNCSOSXCSVLABW4QQC 7WCB5YQJJZIPUAFHTCQBWNI6ZM5XMIQJAKTLYTR7NOR5NKESRMDQC AZ5D2LQUSYVWVEP7ISFDSZTMZ65UEHZATILMDQ4TYLCKJH4Q3TIAC PKLUHYE4BGIMJKU6VKGBGSHEB2ZT53OYMTFBYCZYCO4J3RVTRXSAC CULHFNIVQ3ATML2W3Z45RARZ2LHGXONYTGGN2ETWAAMV7R3Y67AQC U3EAZKHRN3DBOOYM4GDEVJK2DJ6ULHOPOHJLCCHYV2EEPUBMYX5QC KF2LDB5YIXMXBZK6KJWJOLJL66TN2KDXPH3NKEGGCQ5EVOZB77BQC IFQPVMBD552DZ3B5HCM6W6MI2SB6576ZYJNU5KVA3O4YPZAUEFHAC IN2JREDBRA56UC3H2PGS2DSLCAHGIHLE47JZF33AEID4EIZICDSAC LFEMJYYDO45ASMQSOJ3TNID7B5UZXDHB3NWFZJXWOAWNBS6GMDEAC HPSOAD4RXHXU7TSVX2AD5ZDGHMS7HLYNRARWHJCXGGH5RWW7LH6QC N256FH74YJDO7OYYVIHMV54IP7XA23UKVNMBDZ66LT3DRNEXUGYAC 2SLTGWP6FTM7C7BMSYEI2EBD4YTVO2XCIVRPHBJH5XHVLLVR76TAC FU6P5QLG4GVLHVB4O5TCEPJF4X4FGDUBONQFRYP4U5KEPIYLUWJQC LNAL3372UEPJAO2OMU7V6AIYROA4HDWWFF7FMQBAHUSUMQDRJZGQC YGZ3VCW4OAJYPI2CYK3MTABNFY7Y2ENSSTFE5ZZ4K6HK57FCU3XQC UTDTZCTXAAP6AHENYQP7MOQ5QNIKKXN34NV2ONWEGM4HA4FU637AC C3OS2JJ6Q7SEFKJKYQHEIBPUKGYUAIKFO5T5UQFSZUVRW4VI24GAC XQTT6NDF6KRXHUABU45HB2TDSQQXYUF7S66PGMM7TVKE3OFBTVAQC G5WLRXODOQR3PLO7G4RS2T3COBA3TXGOJJQ6DG5T2HA7C5K4APHAC OLT666N4VXRYJAVF4ZBYL3FDCQB5N42BFUCMVFWN4LP5AANKWGPQC TEDT26JQBWGATVTY6HZTIOGFR6BXW2BHSUKUTXTA7HOXARRQ5D6AC BJ3CYLUTYL3ODCU7XIQ2YIBQ6GMHP4IQ7HYMD4YCOPFRYEIYNWKQC IQHXLIIU5M4H6YRKICFPJXDNTLZTUAUWIYTEII6LDUROCZHQ76RQC YTOYRQ2MPUW3FJK64HPML5KQIB66G6CJJFQ4BGOAHT4MSAQAKFIAC EH7FHUXXZ2WEY6LLQOTUCVW6LW5LGTGRC3PMC4PCUVCDHVXLPUTAC FTRAPDYAI3KKJDIGZX6PX3HZBYKLPRD5VZ2P5MZSAP2246OPWUOAC EIHMXSDRPLRGGSOE54HTK3GIESHIVJHODYV72PLVZZDXT6NUO4YQC match sub {model::SubState::PickingRepoDir(state) => {picking_repo(state, *window_size, allowed_actions, report)}model::SubState::ManagingRepo(state) => managing_repo(state,get_diff,*window_size,allowed_actions,report,),}}fn picking_repo<'a>(state: &'a model::PickingRepoDir,window_size: iced::Size,allowed_actions: &'a [action::Binding],report: &'a report::Container,) -> Element<'a, Msg, Theme> {let model::PickingRepoDir { picker } = state;let main = el(column([el(text("Select project directory:")),dir_picker::view(picker, theme::Container::Bordered).map(Msg::PickingRepo),]).spacing(SPACING).width(Length::Fill).height(Length::Fill));add_actions_and_report(None, main, window_size, allowed_actions, report)}fn managing_repo<'a, F>(state: &'a model::ManagingRepo,get_diff: F,window_size: iced::Size,allowed_actions: &'a [action::Binding],report: &'a report::Container,) -> Element<'a, Msg, Theme>whereF: Fn(file::IdHash) -> Option<&'a diff::File>,{let model::ManagingRepo { repo_path, sub } = state;
let actions_inner = column([]).spacing(4);let actions_inner =actions_inner.push(view_actions(allowed_actions));let actions = el(container(actions_inner).class(theme::Container::ActionsBg).width(Length::Fill).height(Length::Shrink).padding(Padding::from([2, 5])));let main = overlay_report(main, report, window_size);el(column([main, actions]))
add_actions_and_report(None,main,window_size,allowed_actions,report,)
fn add_actions_and_report<'a>(above_actions: Option<Column<'a, Msg, Theme>>,main: Element<'a, Msg, Theme>,window_size: iced::Size,allowed_actions: &'a [action::Binding],report: &'a report::Container,) -> Element<'a, Msg, Theme> {let actions_inner = above_actions.unwrap_or_else(|| column([]).spacing(4));let actions_inner = actions_inner.push(view_actions(allowed_actions));let actions = el(container(actions_inner).class(theme::Container::ActionsBg).width(Length::Fill).height(Length::Shrink).padding(Padding::from([2, 5])));let main = overlay_report(main, report, &window_size);el(column([main, actions]).spacing(SPACING))}
let actions_inner = actions_inner.push(view_actions(allowed_actions));let actions = el(container(actions_inner).class(theme::Container::ActionsBg).width(Length::Fill).height(Length::Shrink).padding(Padding::from([2, 5])));// Overlay the main view with reportslet main = overlay_report(main, report, &window_size);el(column([main, actions]).spacing(SPACING))
add_actions_and_report(Some(actions_inner),main,window_size,allowed_actions,report,)
let allowed_actions = action::get_allowed(&sub, &None, &report);let state = State {window_size: WINDOW_SIZE,window_scale: WINDOW_SCALE,
let state = ManagingRepo {
let allowed_actions = action::get_allowed(&sub, &None, &report);let state = State {window_size: WINDOW_SIZE,window_scale: WINDOW_SCALE,
let state = ManagingRepo {
let allowed_actions = action::get_allowed(&sub, &None, &report);let state = State {
let state = ManagingRepo {repo_path: repo_path.clone(),sub,};let sub = SubState::ManagingRepo(state);let mut state = State {sub,
let allowed_actions = action::get_allowed(&sub, &sub_menu, &report);let state = State {
let state = ManagingRepo {repo_path: repo_path.clone(),sub,};let sub = SubState::ManagingRepo(state);let mut state = State {sub,
#[allow(clippy::large_enum_variant)]#[derive(Debug)]pub enum SubState {PickingRepoDir(PickingRepoDir),ManagingRepo(ManagingRepo),}#[derive(Debug)]pub struct PickingRepoDir {pub picker: dir_picker::State,}#[derive(Debug)]pub struct ManagingRepo {pub repo_path: PathBuf,pub sub: ManagingRepoSubState,}
pub fn get_allowed(state: &model::SubState,sub_menu: &Option<model::SubMenu>,report: &report::Container,) -> Vec<Binding> {let mut bindings = if let Some(sub_menu) = sub_menu {
pub fn update_allowed_actions(state: &mut model::State) {let mut bindings = if let Some(sub_menu) = state.sub_menu {
match state {model::SubState::Loading { .. } => vec![],model::SubState::SelectingId { .. } => todo!(),model::SubState::Ready(ready_state) => {get_ready_allowed(ready_state)
match &state.sub {model::SubState::PickingRepoDir(sub) => {get_allowed_in_picking_repo(sub)}model::SubState::ManagingRepo(sub) => {get_allowed_in_managing_repo(&sub.sub)
bindings
fn get_allowed_in_picking_repo(_state: &model::PickingRepoDir) -> Vec<Binding> {vec![down(), up()]}fn get_allowed_in_managing_repo(state: &model::ManagingRepoSubState,) -> Vec<Binding> {match state {model::ManagingRepoSubState::Loading { .. } => vec![],model::ManagingRepoSubState::SelectingId { .. } => todo!(),model::ManagingRepoSubState::Ready(ready_state) => {get_ready_allowed(ready_state)}}
))),};let down = || Binding {key: "↓| j",label: "",msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(selection::Dir::Down,))),};let up = || Binding {key: "↑| k",label: "",msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(selection::Dir::Up,
fn down() -> Binding {Binding {key: "↓| j",label: "",msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(selection::Dir::Down,))),}}fn up() -> Binding {Binding {key: "↑| k",label: "",msg: Some(FilteredMsg::Selection(selection::Msg::PressDir(selection::Dir::Up,))),}}
//! A file-system directory pickeruse async_stream::try_stream;use iced::widget::{button, column, container, row, text, text_input};use iced::{Element, Font, Length, Padding, Pixels};use iced_utils::{task, Task};use normpath::{BasePathBuf, PathExt};use nucleo_matcher::pattern::{Atom, AtomKind, CaseMatching, Normalization};use nucleo_matcher::Matcher;use tokio::fs;use std::borrow::Cow;use std::cmp;use std::collections::VecDeque;use std::path::{self, Path, PathBuf};use crate::nav_scrollable;const PIJUL_DIR: &str = ".pijul";const GIT_DIR: &str = ".git";#[derive(Debug)]pub struct State {left_nav: nav_scrollable::State,right_nav: nav_scrollable::State,input: String,current_dir: BasePathBuf,child_dirs: Vec<PathBuf>,matched_child_dirs: Vec<String>,found_repos_dirs_pijul: Vec<PathBuf>,found_repos_dirs_git: Vec<PathBuf>,find_child_dirs_handle: Option<task::Handle>,find_repos_handle: Option<task::Handle>,/// Fuzzy matcher state that avoid allocations during matchingmatcher: Matcher,}#[derive(Debug, Clone, Copy)]pub enum RepoKind {Pijul,Git,}#[derive(Debug, Clone)]pub enum Msg {Input(String),SubmitInput,SelectChildDir(PathBuf),EditSegment { ix: usize },FindChildDirsSuccess(Vec<PathBuf>),FindChildDirsFailed(String),FindReposDirsSuccess((PathBuf, RepoKind)),FindReposDirsFailed(String),SelectRepo(PathBuf),}pub fn init(start_path: impl Into<PathBuf>) -> (State, Task<Msg>) {let mut start_path = start_path.into();let current_dir = loop {match PathExt::normalize(start_path.as_path()) {Ok(path) => break path,Err(_) => {if let Some(path) = start_path.parent() {start_path = path.to_path_buf();} else {break BasePathBuf::new(PathBuf::from_iter([path::Component::RootDir,])).expect("Must be able to construct root dir");}}}};let (find_child_dirs_task, find_child_dirs_handle) =find_child_dirs(current_dir.as_path());let find_child_dirs_handle = Some(find_child_dirs_handle);let (find_repos_task, find_repos_handle) =find_repos_dirs(current_dir.as_path());let find_repos_handle = Some(find_repos_handle);(State {left_nav: nav_scrollable::State::default(),right_nav: nav_scrollable::State::default(),input: String::default(),current_dir,child_dirs: vec![],matched_child_dirs: vec![],found_repos_dirs_pijul: vec![],found_repos_dirs_git: vec![],find_child_dirs_handle,find_repos_handle,matcher: Matcher::default(),},Task::batch([find_child_dirs_task,find_repos_task,// Focus the inputtask::widget_focus_next(),]),)}#[derive(Debug)]pub enum Action {Picked(PathBuf),}//pub fn update(state: &mut State, msg: Msg) -> (Task<Msg>, Option<Action>) {let mut action = None;let task = match msg {Msg::Input(input) => {if input.is_empty() && state.input.is_empty() {// When there's an empty input and nothing is typed in (i.e.// indempotent delete), go up a dirif let Ok(Some(parent)) = state.current_dir.parent() {change_dir(state, parent.to_owned())} else {Task::none()}} else {if input.is_empty() {state.matched_child_dirs = vec![];state.input = input;Task::none()} else {let mut matches = if input.ends_with(PATH_SEP) {// If the path ends with separator accept it only if// there's an exact matchlet (exact, _) = input.split_at(input.len() - 1);if state.child_dirs.iter().any(|dir| {dir.file_name().map(|name| name.to_string_lossy())== Some(Cow::Borrowed(exact))}) {vec![input.to_string()]} else {vec![]}} else {match_child_dirs(state, &input)};// Change dir if there is only one matchif matches.len() == 1 {let matched =state.current_dir.join(matches.pop().unwrap());state.input = String::default();change_dir(state, matched)} else {state.matched_child_dirs = matches;state.input = input;Task::none()}}}}Msg::SubmitInput => {if state.input.is_empty() {action = Some(Action::Picked(state.current_dir.as_path().to_path_buf(),));Task::none()} else {// Accept input only if there's an exact matchlet mut matches = if state.child_dirs.iter().any(|dir| {dir.file_name().map(|name| name.to_string_lossy())== Some(Cow::Borrowed(&state.input))}) {vec![state.input.clone()]} else {vec![]};// Change dir if there is only one matchif matches.len() == 1 {let matched =state.current_dir.join(matches.pop().unwrap());state.input = String::default();change_dir(state, matched)} else {Task::none()}}}Msg::SelectChildDir(dir) => {state.input = String::default();let path = PathExt::normalize(dir.as_path()).unwrap();change_dir(state, path)}Msg::EditSegment { ix } => {let ix = cmp::max(ix, 1);let path: PathBuf =state.current_dir.as_path().iter().take(ix).collect();let path = PathExt::normalize(path.as_path()).unwrap();change_dir(state, path)}Msg::FindChildDirsSuccess(dirs) => {state.find_child_dirs_handle = None;state.child_dirs = dirs;// If the last char isn't path separator, update matches.// This is needed when `Msg::DeleteChar` deletes a path separator to// go up a dirif state.input.len() > 1 {let (_, last_char) =state.input.split_at(state.input.len() - 1);if last_char != PATH_SEP_STR {let input = PathBuf::from(&state.input);let needle = input.file_name().unwrap().to_string_lossy();let matches = match_child_dirs(state, &needle);state.matched_child_dirs = matches;}}Task::none()}Msg::FindChildDirsFailed(_err) => {// TODO report errorTask::none()}Msg::FindReposDirsSuccess((dir, kind)) => {match kind {RepoKind::Pijul => state.found_repos_dirs_pijul.push(dir),RepoKind::Git => state.found_repos_dirs_git.push(dir),}Task::none()}Msg::FindReposDirsFailed(_err) => {// TODO report errorTask::none()}Msg::SelectRepo(dir) => {action = Some(Action::Picked(dir));Task::none()}};(task, action)}pub fn view<'a, Theme, Renderer>(state: &'a State,bordered_container_class: <Theme as container::Catalog>::Class<'a>,) -> Element<'a, Msg, Theme, Renderer>whereTheme: 'a+ button::Catalog+ container::Catalog+ nav_scrollable::Catalog+ text::Catalog+ text_input::Catalog,Renderer: 'a + iced::advanced::Renderer + iced::advanced::text::Renderer,<Renderer as iced_core::text::Renderer>::Font: From<iced::Font>,{let State {left_nav,right_nav,input,current_dir,child_dirs,matched_child_dirs,found_repos_dirs_pijul,found_repos_dirs_git,find_child_dirs_handle: _,find_repos_handle,matcher: _,} = state;const CORNER: &str = "├─";const LAST_CORNER: &str = "└─";const CORNER_SIZE: Pixels = Pixels(24.0);let mut left_children =Vec::with_capacity(if state.find_child_dirs_handle.is_some() {1} else {if !matched_child_dirs.is_empty() {matched_child_dirs.len()} else {child_dirs.len()}});// Display path in segments so that changing level forces element tree// updates to avoid the annoying Iced state cache that prevents text_input// from updatinglet left_header = Element::new(container(row([Element::new(row(itertools::intersperse_with(current_dir.as_path().iter().enumerate().map(|(ix, comp)| {Element::new(button(text(comp.to_string_lossy())).on_press(Msg::EditSegment { ix }),)}),|| {Element::new(container(text(PATH_SEP_STR)).padding(text_input::DEFAULT_PADDING.left(0).right(0),),)},))),Element::new(container(text(PATH_SEP_STR)).padding(text_input::DEFAULT_PADDING.left(0).right(0)),),Element::new(text_input("", input).on_input(Msg::Input).on_submit(Msg::SubmitInput),),])).class(bordered_container_class).padding(button::DEFAULT_PADDING),);const CHILDREN_PADDING: Padding = Padding {left: 20.0,..Padding::ZERO};if state.find_child_dirs_handle.is_some() {left_children.push(Element::new(text("Loading child directories...")));} else if !matched_child_dirs.is_empty() {let last_matched_child_ix = matched_child_dirs.len().saturating_sub(1);left_children.extend(matched_child_dirs.iter().enumerate().map(|(ix, dir)| {Element::new(row([Element::new(text(if ix == last_matched_child_ix {LAST_CORNER} else {CORNER}).size(CORNER_SIZE).font(Font::MONOSPACE).shaping(text::Shaping::Advanced),),Element::new(button(text(dir)).on_press_with(move || {Msg::SelectChildDir(current_dir.as_path().join(dir),)},)),]).padding(CHILDREN_PADDING),)},));} else {let last_child_ix = child_dirs.len().saturating_sub(1);left_children.extend(child_dirs.iter().enumerate().map(|(ix, dir)| {let dir_str = dir.file_name().unwrap().to_string_lossy();Element::new(row([Element::new(text(if ix == last_child_ix {LAST_CORNER} else {CORNER}).size(CORNER_SIZE).shaping(text::Shaping::Advanced),),Element::new(button(text(dir_str)).on_press_with(|| Msg::SelectChildDir(dir.clone())),),]).padding(CHILDREN_PADDING),)}));}let right_header = Element::new(text(if !found_repos_dirs_pijul.is_empty()|| !found_repos_dirs_git.is_empty(){"Found repositories:"} else if find_repos_handle.is_none() {"No repositories found"} else {"Searching for repositories"},));let right_children = found_repos_dirs_pijul.iter().map(|dir| {Element::new(button(text(dir.to_string_lossy())).on_press_with(|| Msg::SelectRepo(dir.clone())),)}).chain(found_repos_dirs_git.iter().map(|dir| {Element::new(button(text(format!("{} (git)", dir.to_string_lossy()))).on_press_with(|| Msg::SelectRepo(dir.clone())),)})).collect::<Vec<_>>();// TODO make both scrollablelet left = Element::new(container(column([left_header,Element::new(nav_scrollable(left_nav, left_children).height(Length::Fill).width(Length::Fill),),])).height(Length::Fill).width(Length::FillPortion(1)),);let right = Element::new(container(column([right_header,Element::new(nav_scrollable(right_nav, right_children).height(Length::Fill).width(Length::Fill),),])).height(Length::Fill).width(Length::FillPortion(1)),);Element::new(row([left, right]).spacing(SPACING).width(Length::Fill).height(Length::Fill),)}const SPACING: u32 = 10;fn change_dir(state: &mut State, path: BasePathBuf) -> Task<Msg> {state.current_dir = path;state.found_repos_dirs_pijul = vec![];state.found_repos_dirs_git = vec![];state.matched_child_dirs = vec![];state.child_dirs = vec![];let (find_child_dirs_task, find_child_dirs_handle) =find_child_dirs(state.current_dir.as_path());state.find_child_dirs_handle = Some(find_child_dirs_handle);let (find_repos_dirs_task, find_repos_dirs_handle) =find_repos_dirs(state.current_dir.as_path());state.find_repos_handle = Some(find_repos_dirs_handle);Task::batch([find_child_dirs_task, find_repos_dirs_task])}fn find_child_dirs(dir: &Path) -> (Task<Msg>, task::Handle) {let current_dir = dir.to_owned();let (task, handle) = Task::perform(async move {let mut read_dir = fs::read_dir(¤t_dir).await?;let mut child_dirs = vec![];while let Some(entry) = read_dir.next_entry().await? {let path = entry.path();let metadata = fs::metadata(&path).await?;if metadata.is_dir() && path.file_name().is_some() {child_dirs.push(path);}}Ok(child_dirs)},|result: std::io::Result<_>| match result {Ok(dirs) => Msg::FindChildDirsSuccess(dirs),Err(error) => Msg::FindChildDirsFailed(error.to_string()),},).abortable();let handle = handle.abort_on_drop();(task, handle)}fn find_repos_dirs(dir: &Path) -> (Task<Msg>, task::Handle) {// Breadth-first search for pijul reposlet dir = dir.to_path_buf();let stream = try_stream! {let mut to_try = VecDeque::from([dir.to_path_buf()]);while let Some(path) = to_try.pop_front() {let mut reader = fs::read_dir(&path).await.map_err(|err| format!("Cannot read dir {} ({err})", path.to_string_lossy()))?;// Ignore errors in accessing entrieswhile let Ok(Some(entry)) = reader.next_entry().await {let metadata = fs::metadata(entry.path()).await;if metadata.map(|meta| meta.is_dir()).unwrap_or(false) {let path = entry.path();if let Ok(true) = fs::try_exists(path.join(PIJUL_DIR)).await {yield (path, RepoKind::Pijul);}else if let Ok(true) = fs::try_exists(path.join(GIT_DIR)).await {yield (path, RepoKind::Git);} else {to_try.push_back(path);}}}}};let (task, handle) =Task::run(stream, |result: Result<_, String>| match result {Ok((dir, kind)) => Msg::FindReposDirsSuccess((dir, kind)),Err(error) => Msg::FindReposDirsFailed(error.to_string()),}).abortable();let handle = handle.abort_on_drop();(task, handle)}fn match_child_dirs(state: &mut State, needle: &str) -> Vec<String> {let atom = Atom::new(needle,CaseMatching::Ignore,Normalization::Never,AtomKind::Fuzzy,false,);itertools::Itertools::sorted_by(atom.match_list(state.child_dirs.iter().filter_map(|dir| {dir.file_name().map(|name| name.to_string_lossy())}),&mut state.matcher,).into_iter(),// Sort by score descending|(_, left), (_, right)| Ord::cmp(right, left),).map(|(dir, _score)| dir.to_string()).collect()}const PATH_SEP: char = path::MAIN_SEPARATOR;const PATH_SEP_STR: &str = path::MAIN_SEPARATOR_STR;
let SubState::Ready(ready_state) = &mut state.model.sub else {panic!("Unexpected state: {:?}", state.model)
let SubState::ManagingRepo(sub) = &mut state.model.sub else {panic!("Unexpected state: {:?}", state.model.sub)};let ManagingRepoSubState::Ready(ready_state) = &mut sub.sub else {panic!("Unexpected state: {:?}", sub.sub)
state.files.diffs_cache.inner.peek(&file::id_parts_hash(&to_path("untracked_1.rs"),file::Kind::Untracked)),
state.managing_repo.as_ref().unwrap().files.diffs_cache.inner.peek(&file::id_parts_hash(&to_path("untracked_1.rs"),file::Kind::Untracked)),
state.files.diffs_cache.inner.peek(&file::id_parts_hash(&to_path("changed_0.rs"),file::Kind::Changed)),
state.managing_repo.as_ref().unwrap().files.diffs_cache.inner.peek(&file::id_parts_hash(&to_path("changed_0.rs"),file::Kind::Changed)),
state.files.diffs_cache.inner.peek(&file::id_parts_hash(&to_path("changed_1.rs"),file::Kind::Changed)),
state.managing_repo.as_ref().unwrap().files.diffs_cache.inner.peek(&file::id_parts_hash(&to_path("changed_1.rs"),file::Kind::Changed)),
let task = repo_got_change_diffs(&mut state, change_hash_0, diffs);
let task = {let model::SubState::ManagingRepo(model) = &mut state.model.sub else {panic!("Unexpected state {:?}", state.model.sub)};repo_got_change_diffs(model, change_hash_0, diffs)};
let task = repo_got_change_diffs(&mut state, change_hash_1, diffs);
let task = {let model::SubState::ManagingRepo(model) = &mut state.model.sub else {panic!("Unexpected state {:?}", state.model.sub)};repo_got_change_diffs(model, change_hash_1, diffs)};
let task = repo_got_change_diffs(&mut state, change_hash_1, diffs);
let task = {let model::SubState::ManagingRepo(model) = &mut state.model.sub else {panic!("Unexpected state {:?}", state.model.sub)};repo_got_change_diffs(model, change_hash_1, diffs)};
let is_init = matches!(&msg, Msg::FromRepo(repo::MsgOut::Init(_))); // Upate state with the msg received from task to init the repo
let is_init = matches!(&msg,Msg::ManagingRepo(ManagingRepoMsg::FromRepo(repo::MsgOut::Init(_)))); // Upate state with the msg received from task to init the repo
let SubState::Ready(ready_state) = &mut state.model.sub else {
let SubState::ManagingRepo(state) = &mut state.model.sub else {panic!("Unexpected state: {state_dbg}")};let state_dbg = format!("{:?}", state.sub);let ManagingRepoSubState::Ready(ready_state) = &mut state.sub else {
let (selection, task): (Option<Status>, Task<crate::Msg>) =match ctx.state.status.take() {Some(Status::LogChange(LogChange {ix,hash,message,file:Some(LogChangeFileSelection {ix: file_ix,path,diff_selected,}),})) => {if diff_selected {(Some(Status::LogChange(LogChange {ix,hash,message,file: Some(LogChangeFileSelection {ix: file_ix,path,diff_selected: false,}),})),Task::none(),)} else {let selection = Status::LogChange(LogChange {
let (selection, task): (Option<Status>,Task<crate::ManagingRepoMsg>,) = match ctx.state.status.take() {Some(Status::LogChange(LogChange {ix,hash,message,file:Some(LogChangeFileSelection {ix: file_ix,path,diff_selected,}),})) => {if diff_selected {(Some(Status::LogChange(LogChange {
file: None,});(Some(selection), Task::none())}
file: Some(LogChangeFileSelection {ix: file_ix,path,diff_selected: false,}),})),Task::none(),)} else {let selection = Status::LogChange(LogChange {ix,hash,message,file: None,});(Some(selection), Task::none())
diff_selected: true,}) => (Some(Status::UntrackedFile {ix,path,diff_selected: false,}),Task::none(),),
diff_selected: false,}),Task::none(),),Some(Status::ChangedFile {ix,path,diff_selected: true,}) => (
diff_selected: true,}) => (Some(Status::ChangedFile {ix,path,diff_selected: false,}),Task::none(),),selection @ (Some(Status::UntrackedFile { .. })| Some(Status::ChangedFile { .. })| Some(Status::LogChange(LogChange {file: None,..}))| None) => (selection, Task::none()),};
diff_selected: false,}),Task::none(),),selection @ (Some(Status::UntrackedFile { .. })| Some(Status::ChangedFile { .. })| Some(Status::LogChange(LogChange {file: None,..}))| None) => (selection, Task::none()),};
fn select_right_status(ctx: &mut Ctx<'_>) -> Task<crate::Msg> {let (selection, task): (Option<Status>, Task<crate::Msg>) = match ctx.state.status.take(){Some(Status::UntrackedFile {ix,path,diff_selected: false,}) => {let diff_selected =diff::file_diff_needs_scrolling(&ctx.navigation.files_diffs);(Some(Status::UntrackedFile {ix,path,diff_selected,}),Task::none(),)}Some(Status::ChangedFile {ix,path,diff_selected: false,}) => {// Always allow right move for `to_record` selectionlet diff_selected = true;(Some(Status::ChangedFile {ix,path,diff_selected,}),Task::none(),)}Some(Status::LogChange(LogChange {ix,hash,message,file: None,})) => {let log_entry = ctx.repo.short_log.get(ix).unwrap();let (file, task) = if let Some(path) = log_entry.file_paths.first(){let file_id = file::log_id_parts_hash(log_entry.hash, path);// If the log is not loaded yet, the nav will be initialized// once it's loaded (`repo::MsgOut::GotChangeDiffs`)if let Some(log) = ctx.navigation.log_diffs.diffs.get(&file_id)
fn select_right_status(ctx: &mut Ctx<'_>) -> Task<crate::ManagingRepoMsg> {let (selection, task): (Option<Status>, Task<crate::ManagingRepoMsg>) =match ctx.state.status.take() {Some(Status::UntrackedFile {ix,path,diff_selected: false,}) => {let diff_selected = diff::file_diff_needs_scrolling(&ctx.navigation.files_diffs,);(Some(Status::UntrackedFile {ix,path,diff_selected,}),Task::none(),)}Some(Status::ChangedFile {ix,path,diff_selected: false,}) => {// Always allow right move for `to_record` selectionlet diff_selected = true;(Some(Status::ChangedFile {ix,path,diff_selected,}),Task::none(),)}Some(Status::LogChange(LogChange {ix,hash,message,file: None,})) => {let log_entry = ctx.repo.short_log.get(ix).unwrap();let (file, task) = if let Some(path) =log_entry.file_paths.first()
// Init log diffs navlet unchanged_sections =diff::unchanged_sections(&log.file);log::init_diffs_nav(&mut ctx.navigation.status_logs_navs,file_id,).set_skip_sections(unchanged_sections);};
let file_id = file::log_id_parts_hash(log_entry.hash, path);// If the log is not loaded yet, the nav will be initialized// once it's loaded (`repo::MsgOut::GotChangeDiffs`)if let Some(log) =ctx.navigation.log_diffs.diffs.get(&file_id){// Init log diffs navlet unchanged_sections =diff::unchanged_sections(&log.file);log::init_diffs_nav(&mut ctx.navigation.status_logs_navs,file_id,).set_skip_sections(unchanged_sections);};
let (selection, task) = status_log_file_selection(0,hash,VDir::Down,ctx.navigation,log_entry,);(Some(selection), task)} else {(None, Task::none())};(Some(Status::LogChange(LogChange {ix,hash,message,file,})),task,)}Some(Status::LogChange(LogChange {ix,hash,message,file:Some(LogChangeFileSelection {ix: file_ix,path,diff_selected: false,}),})) => {let is_diff_scrollable =log::diff_needs_scrolling(&ctx.navigation.status_logs_navs);(Some(Status::LogChange(LogChange {ix,hash,message,file: Some(LogChangeFileSelection {
let (selection, task) = status_log_file_selection(0,hash,VDir::Down,ctx.navigation,log_entry,);(Some(selection), task)} else {(None, Task::none())};(Some(Status::LogChange(LogChange {ix,hash,message,file,})),task,)}Some(Status::LogChange(LogChange {ix,hash,message,file:Some(LogChangeFileSelection {
})),Task::none(),)}selection => (selection, Task::none()),};
})) => {let is_diff_scrollable =log::diff_needs_scrolling(&ctx.navigation.status_logs_navs);(Some(Status::LogChange(LogChange {ix,hash,message,file: Some(LogChangeFileSelection {ix: file_ix,path,diff_selected: is_diff_scrollable,}),})),Task::none(),)}selection => (selection, Task::none()),};
Task::done(crate::Msg::ToRepo(repo::MsgIn::LoadOtherChannelLog(name.clone(),)))
Task::done(crate::ManagingRepoMsg::ToRepo(repo::MsgIn::LoadOtherChannelLog(name.clone()),))
let window_scale = 1.0;let allowed_actions = default();let sub_menu = None;let report = report::Container::default();if let Some(repo_path) = repo_path {let (sub, managing_repo, managing_repo_task) =init_managing_repo(repo_path);let tasks = 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),},tasks,)} else {let start_path = env::home_dir().unwrap_or_else(|| PathBuf::from("/"));let (picker, picker_task) = dir_picker::init(start_path);let tasks = Task::batch([open_window_task.map(|_id| Msg::NoOp),set_icon_task,picker_task.map(view::Msg::PickingRepo).map(Msg::View),]);let state = model::PickingRepoDir { picker };let sub = model::SubState::PickingRepoDir(state);let model = model::State {sub,window_size,window_scale,allowed_actions,sub_menu,report,};(State {model,managing_repo: None,},tasks,)}}fn init_managing_repo(repo_path: PathBuf,) -> (model::ManagingRepo, ManagingRepo, Task<Msg>) {
open_window_task.map(|_id| Msg::NoOp),set_icon_task,repo_task,load_id_task,files_task.map(Msg::File),
repo_task.map(Msg::ManagingRepo),load_id_task.map(Msg::ManagingRepo),files_task.map(ManagingRepoMsg::File).map(Msg::ManagingRepo),
allowed_actions: default(),sub_menu: None,report: report::Container::default(),
};let managing_repo = ManagingRepo {repo_fs_watch: None,repo_tx_in,files,_repo_thread: repo_thread,
(State {repo_fs_watch: None,repo_tx_in,files,model,_repo_thread: repo_thread,},tasks,)
(sub, managing_repo, tasks)
Msg::View(msg) => update_from_view(state, msg),Msg::Window(event) => update_from_window_event(state, event),Msg::LoadedId(id) => {match &mut state.model.sub {model::SubState::Loading { user_ids, repo } => {
Msg::View(msg) => match &mut state.model.sub {model::SubState::PickingRepoDir(model) => {let (task, new_sub_state) =update_picking_repo_from_view(model, 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,).map(Msg::ManagingRepo)}},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();update_managing_repo(sub, model, &mut state.model.report, msg).map(Msg::ManagingRepo)} else {Task::none()}}};action::update_allowed_actions(&mut state.model);task}fn update_managing_repo(state: &mut ManagingRepo,model: &mut model::ManagingRepo,report: &mut report::Container,msg: ManagingRepoMsg,) -> Task<ManagingRepoMsg> {match msg {ManagingRepoMsg::LoadedId(id) => {match &mut model.sub {model::ManagingRepoSubState::Loading { user_ids, repo } => {
state.model.sub = model::SubState::Ready(ReadyState {user_id: *id,repo,selection: default(),navigation: default(),record_changes: default(),forking_channel_name: default(),logs: default(),to_record: default(),jobs: default(),})
model.sub =model::ManagingRepoSubState::Ready(ReadyState {user_id: *id,repo,selection: default(),navigation: default(),record_changes: default(),forking_channel_name: default(),logs: default(),to_record: default(),jobs: default(),})
Msg::File(msg) => update_file(state, msg),};state.model.allowed_actions = action::get_allowed(&state.model.sub,&state.model.sub_menu,&state.model.report,);task
ManagingRepoMsg::File(msg) => update_file(state, model, msg),}
fn update_from_view(state: &mut State, msg: view::Msg) -> Task<Msg> {
fn update_picking_repo_from_view(state: &mut model::PickingRepoDir,msg: view::Msg,) -> (Task<Msg>, Option<(model::ManagingRepo, ManagingRepo)>) {let mut new_state = None;let model::PickingRepoDir { picker } = state;let task = match msg {view::Msg::Action(msg) => {dbg!(msg);Task::none()}view::Msg::UnfilteredSelection(msg) => {dbg!(msg);Task::none()}view::Msg::PickingRepo(msg) => {let (task, action) = dir_picker::update(picker, msg);let task = task.map(view::Msg::PickingRepo).map(Msg::View);if let Some(dir_picker::Action::Picked(dir)) = action {// If it contains Pijul repo, init ManagingRepo state// TODO: If it contains Git, offer to migrate it// TODO: Otherwise, offer to init Pijul from scratchlet (sub, managing_repo, managing_repo_task) =init_managing_repo(dir);new_state = Some((sub, managing_repo));Task::batch([task, managing_repo_task])} else {task}}view::Msg::EditRecordMsg(_)| view::Msg::EditRecordDesc(_)| view::Msg::EditForkChannelName(_)| view::Msg::ToRecord(_) => Task::none(),};(task, new_state)}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<ManagingRepoMsg> {
view::Msg::Action(msg) => update_from_action(state, msg),view::Msg::EditRecordMsg(action) => edit_record_msg(state, action),view::Msg::EditRecordDesc(action) => edit_record_desc(state, action),
view::Msg::Action(msg) => {update_from_action(state, model, sub_menu, report, msg)}view::Msg::EditRecordMsg(action) => edit_record_msg(model, action),view::Msg::EditRecordDesc(action) => edit_record_desc(model, action),
if let Some(sub_menu) = &state.model.sub_menu {match sub_menu {model::SubMenu::Push => push(state),model::SubMenu::Pull => pull(state),model::SubMenu::ResetChange => reset_change(state),
if let Some(menu) = &sub_menu {match menu {model::SubMenu::Push => push(state, model, sub_menu),model::SubMenu::Pull => pull(state, model, sub_menu),model::SubMenu::ResetChange => {reset_change(state, model, sub_menu, report)}
action::FilteredMsg::SaveRecord => save_record(state),action::FilteredMsg::PostponeRecord => defer_record(state),action::FilteredMsg::DiscardRecord => abandon_record(state),action::FilteredMsg::FocusNext => focus_next(state),action::FilteredMsg::FocusPrev => focus_prev(state),action::FilteredMsg::AddUntrackedFile => add_untracked_file(state),action::FilteredMsg::RmChange => rm_change(state),action::FilteredMsg::StartRecord => start_record(state),
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::AddUntrackedFile => {add_untracked_file(state, model)}action::FilteredMsg::RmChange => rm_change(state, model, sub_menu),action::FilteredMsg::StartRecord => start_record(model),
fn push(state: &mut State) -> Task<Msg> {if let Some(ReadyState { repo, jobs, .. }) =model::is_ready_mut(&mut state.model){
fn push(state: &mut ManagingRepo,model: &mut model::ManagingRepo,sub_menu: &mut Option<model::SubMenu>,) -> Task<ManagingRepoMsg> {if let Some(ReadyState { repo, jobs, .. }) = model::is_ready_mut(model) {
fn pull(state: &mut State) -> Task<Msg> {if let Some(ReadyState { repo, jobs, .. }) =model::is_ready_mut(&mut state.model){
fn pull(state: &mut ManagingRepo,model: &mut model::ManagingRepo,sub_menu: &mut Option<model::SubMenu>,) -> Task<ManagingRepoMsg> {if let Some(ReadyState { repo, jobs, .. }) = model::is_ready_mut(model) {
fn reset_change(state: &mut State) -> Task<Msg> {if let Some(ReadyState { selection, .. }) =model::is_ready_mut(&mut state.model)
fn reset_change(state: &mut ManagingRepo,model: &mut model::ManagingRepo,sub_menu: &mut Option<model::SubMenu>,report: &mut report::Container,) -> Task<ManagingRepoMsg> {if let Some(ReadyState { selection, .. }) = model::is_ready_mut(model)
fn clipboard_copy(state: &mut State) -> Task<Msg> {if let Some(ReadyState { selection, .. }) = model::is_ready(&state.model) {
fn clipboard_copy(model: &mut model::ManagingRepo) -> Task<ManagingRepoMsg> {if let Some(ReadyState { selection, .. }) = model::is_ready(model) {
fn update_from_repo(state: &mut State, msg: repo::MsgOut) -> Task<Msg> {let report_info = |state: &mut State, err: String| {report::show_info(&mut state.model.report, err);
fn update_from_repo(state: &mut ManagingRepo,model: &mut model::ManagingRepo,report: &mut report::Container,msg: repo::MsgOut,) -> Task<ManagingRepoMsg> {let report_info = |report: &mut report::Container, err: String| {report::show_info(report, err);
repo::MsgOut::Init(repo) => repo_init(state, repo),repo::MsgOut::InitFailed(err) => report_err(state, err.to_string()),
repo::MsgOut::Init(repo) => repo_init(state, model, report, repo),repo::MsgOut::InitFailed(err) => report_err(report, err.to_string()),
Ok(()) => report_info(state, format!("Pushed to {channel}")),Err(repo::PushError::Empty) => report_info(state, format!("Nothing to push to {channel}")),Err(err) => report_err(state, err.to_string()),
Ok(()) => report_info(report, format!("Pushed to {channel}")),Err(repo::PushError::Empty) => report_info(report, format!("Nothing to push to {channel}")),Err(err) => report_err(report, err.to_string()),
Ok(()) => report_info(state, format!("Pulled from {channel}")),Err(repo::PullError::Empty) => report_info(state, format!("Nothing to pull from {channel}")),Err(err) => report_err(state, err.to_string()),
Ok(()) => report_info(report, format!("Pulled from {channel}")),Err(repo::PullError::Empty) => report_info(report, format!("Nothing to pull from {channel}")),Err(err) => report_err(report, err.to_string()),
fn defer_record(state: &mut State) -> Task<Msg> {if let Some(ReadyState { record_changes, .. }) =model::is_ready_mut(&mut state.model)
fn defer_record(model: &mut model::ManagingRepo) -> Task<ManagingRepoMsg> {if let Some(ReadyState { record_changes, .. }) = model::is_ready_mut(model)
fn repo_init(state: &mut State, repo_state: repo::State) -> Task<Msg> {match &mut state.model.sub {model::SubState::Loading { user_ids, repo: _ } => {state.model.sub = if user_ids.len() == 1 {
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: _ } => {model.sub = if user_ids.len() == 1 {
match &mut state.model.sub {model::SubState::Loading { user_ids: _, repo } => {
match &mut model.sub {model::ManagingRepoSubState::Loading { user_ids: _, repo } => {
fn got_entire_log(state: &mut State, log: repo::Log) -> Task<Msg> {if let Some(ReadyState { logs, .. }) = model::is_ready_mut(&mut state.model){
fn got_entire_log(model: &mut model::ManagingRepo,log: repo::Log,) -> Task<ManagingRepoMsg> {if let Some(ReadyState { logs, .. }) = model::is_ready_mut(model) {
fn subs(_state: &State) -> Subscription<Msg> {
fn subs(state: &State) -> Subscription<Msg> {let key_subs = match &state.model.sub {model::SubState::PickingRepoDir(_picking_repo) => {Subscription::none()// dir_picker::subs(&picking_repo.picker)// .map(view::Msg::PickingRepo)// .map(Msg::View)}model::SubState::ManagingRepo(model) => subs_managing_repo(model),};let window_subs = window::events().map(|(_id, event)| Msg::Window(event));Subscription::batch([key_subs, window_subs])}fn subs_managing_repo(_model: &model::ManagingRepo) -> Subscription<Msg> {
let key_subs =keyboard::listen().filter_map(|event| {match event {keyboard::Event::KeyPressed {key,modifiers: mods,..} => {let action = |msg| Some(Msg::View(view::Msg::Action(msg)));let selection = |selection| {action(action::FilteredMsg::Selection(selection))};
keyboard::listen().filter_map(|event| {let action = |msg| Some(Msg::View(view::Msg::Action(msg)));let selection =|selection| action(action::FilteredMsg::Selection(selection));let unfiltered = |selection| {Some(Msg::View(view::Msg::UnfilteredSelection(selection)))};
if mods.is_empty() {match key {Key::Character(c) => match c.as_str() {// _________________________________________________________// Directions"j" => selection(selection::Msg::PressDir(selection::Dir::Down,)),"k" => {selection(selection::Msg::PressDir(selection::Dir::Up))}"h" => selection(selection::Msg::PressDir(selection::Dir::Left,)),"l" => selection(selection::Msg::PressDir(selection::Dir::Right,)),// _________________________________________________________// Other keys (sort alphabetically)"a" => action(action::FilteredMsg::AddUntrackedFile),"c" => action(action::FilteredMsg::SelectChannel),"e" => action(action::FilteredMsg::ShowEntireLog),"f" => action(action::FilteredMsg::ForkChannel),"t" => action(action::FilteredMsg::ToRecordToggleSelectedFileOrChange,),"r" => action(action::FilteredMsg::StartRecord),"x" => action(action::FilteredMsg::RmChange),_ => None,},Key::Named(key::Named::Enter) => {action(action::FilteredMsg::Confirm)
match event {keyboard::Event::KeyPressed {key,modifiers: mods,..} => {if mods.is_empty() {match key {Key::Character(c) => match c.as_str() {// _____________________________________________// Directions"j" => selection(selection::Msg::PressDir(selection::Dir::Down,)),"k" => {selection(selection::Msg::PressDir(selection::Dir::Up))
Key::Named(key::Named::Escape) => {action(action::FilteredMsg::Cancel)
"h" => selection(selection::Msg::PressDir(selection::Dir::Left,)),"l" => selection(selection::Msg::PressDir(selection::Dir::Right,)),// _____________________________________________// Other keys (sort alphabetically)"a" => action(action::FilteredMsg::AddUntrackedFile),"c" => action(action::FilteredMsg::SelectChannel),"e" => action(action::FilteredMsg::ShowEntireLog),"f" => action(action::FilteredMsg::ForkChannel),"t" => action(action::FilteredMsg::ToRecordToggleSelectedFileOrChange,),"r" => action(action::FilteredMsg::StartRecord),"x" => action(action::FilteredMsg::RmChange),_ => None,},Key::Named(key::Named::Enter) => {action(action::FilteredMsg::Confirm)}Key::Named(key::Named::Escape) => {action(action::FilteredMsg::Cancel)}Key::Named(key::Named::ArrowDown) => {selection(selection::Msg::PressDir(selection::Dir::Down))}Key::Named(key::Named::ArrowUp) => {selection(selection::Msg::PressDir(selection::Dir::Up))}Key::Named(key::Named::ArrowLeft) => {selection(selection::Msg::PressDir(selection::Dir::Left))}Key::Named(key::Named::ArrowRight) => {selection(selection::Msg::PressDir(selection::Dir::Right))}Key::Named(key::Named::Tab) => {action(action::FilteredMsg::FocusNext)}Key::Named(_) | Key::Unidentified => None,}} else {match key {Key::Character(c) => match c.as_str() {"c" if mods == Modifiers::CTRL => {action(action::FilteredMsg::ClipboardCopy)
Key::Named(key::Named::ArrowDown) => {selection(selection::Msg::PressDir(selection::Dir::Down))
"c" if mods == Modifiers::SHIFT | Modifiers::CTRL => {action(action::FilteredMsg::ClipboardCopyReports)}"d" if mods == Modifiers::CTRL => {action(action::FilteredMsg::DiscardRecord)
Key::Named(_) | Key::Unidentified => None,
"f" if mods == Modifiers::SHIFT => action(action::FilteredMsg::EnterSubMenu(model::SubMenu::Pull),),"j" if mods == Modifiers::SHIFT => selection(selection::Msg::AltPressDir(selection::Dir::Down),),"k" if mods == Modifiers::SHIFT => selection(selection::Msg::AltPressDir(selection::Dir::Up),),"h" if mods == Modifiers::SHIFT => selection(selection::Msg::AltPressDir(selection::Dir::Left),),"l" if mods == Modifiers::SHIFT => selection(selection::Msg::AltPressDir(selection::Dir::Right),),"p" if mods == Modifiers::SHIFT => action(action::FilteredMsg::EnterSubMenu(model::SubMenu::Push),),"t" if mods == Modifiers::SHIFT => {action(action::FilteredMsg::ToRecord(to_record::Msg::ToggleOverall,))}_ => None,},Key::Named(key::Named::ArrowDown)if mods == Modifiers::SHIFT =>{selection(selection::Msg::AltPressDir(selection::Dir::Down))
} else {match key {Key::Character(c) => match c.as_str() {"c" if mods == Modifiers::CTRL => {action(action::FilteredMsg::ClipboardCopy)}"c" if mods == Modifiers::SHIFT | Modifiers::CTRL => {action(action::FilteredMsg::ClipboardCopyReports)}"d" if mods == Modifiers::CTRL => {action(action::FilteredMsg::DiscardRecord)}"p" if mods == Modifiers::CTRL => {action(action::FilteredMsg::PostponeRecord)}"r" if mods == Modifiers::CTRL => {action(action::FilteredMsg::RefreshRepo)}"r" if mods == Modifiers::SHIFT => {action(action::FilteredMsg::ToggleReports)}"s" if mods == Modifiers::CTRL => {action(action::FilteredMsg::SaveRecord)}"f" if mods == Modifiers::SHIFT => action(action::FilteredMsg::EnterSubMenu(model::SubMenu::Pull),
Key::Named(key::Named::ArrowUp) if mods == Modifiers::SHIFT => {selection(selection::Msg::AltPressDir(selection::Dir::Up))}Key::Named(key::Named::ArrowLeft)if mods == Modifiers::SHIFT =>{selection(selection::Msg::AltPressDir(selection::Dir::Left))}Key::Named(key::Named::ArrowRight)if mods == Modifiers::SHIFT =>{selection(selection::Msg::AltPressDir(selection::Dir::Right,))}Key::Named(key::Named::Tab) if mods == Modifiers::SHIFT => {action(action::FilteredMsg::FocusPrev)}Key::Named(_) | Key::Unidentified => None,}}}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,),
"t" if mods == Modifiers::SHIFT => {action(action::FilteredMsg::ToRecord(to_record::Msg::ToggleOverall,))}_ => None,},Key::Named(key::Named::ArrowDown)if mods == Modifiers::SHIFT =>{selection(selection::Msg::AltPressDir(selection::Dir::Down))}Key::Named(key::Named::ArrowUp) if mods == Modifiers::SHIFT => {selection(selection::Msg::AltPressDir(selection::Dir::Up))}Key::Named(key::Named::ArrowLeft)if mods == Modifiers::SHIFT =>{selection(selection::Msg::AltPressDir(selection::Dir::Left))}Key::Named(key::Named::ArrowRight)if mods == Modifiers::SHIFT =>{selection(selection::Msg::AltPressDir(
)}Key::Named(key::Named::ArrowRight) => {unfiltered(selection::UnfilteredMsg::ReleaseDir(
}}keyboard::Event::KeyReleased {key,modifiers: mods,..} => {if mods.is_empty() {match key {Key::Character(c) => match c.as_str() {"j" => Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Down,),),)),"k" => Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Up,),),)),"h" => Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Left,),),)),"l" => Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Right,),),)),_ => None,},Key::Named(key::Named::ArrowDown) => {Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Down,),)))}Key::Named(key::Named::ArrowUp) => {Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Up,),)))}Key::Named(key::Named::ArrowLeft) => {Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Left,),)))}Key::Named(key::Named::ArrowRight) => {Some(Msg::View(view::Msg::UnfilteredSelection(selection::UnfilteredMsg::ReleaseDir(selection::Dir::Right,),)))}Key::Named(_) | Key::Unidentified => None,}} else {None}}keyboard::Event::ModifiersChanged(_) => None,
Key::Named(_) | Key::Unidentified => None,}} else {None}
});let window_subs = window::events().map(|(_id, event)| Msg::Window(event));Subscription::batch([key_subs, window_subs])
keyboard::Event::ModifiersChanged(_) => None,}})
view::main(&state.model,|id_hash| file::try_get_src_file(&state.files, id_hash),window_id,).map(Msg::View)
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)
let icon_bytes = tokio::fs::read(path).await.unwrap();window::icon::from_file_data(&icon_bytes, Some(ImageFormat::Png)).unwrap()},|msg| msg,)
let icon_bytes = tokio::fs::read(path).await.unwrap();window::icon::from_file_data(&icon_bytes, Some(ImageFormat::Png)).unwrap()})
}}pub fn abortable(self) -> (Self, Handle)whereT: 'static,{let (stream, handle) = match self.stream {Some(stream) => {let (stream, handle) = iced::futures::stream::abortable(stream);(Some(iced_runtime::futures::boxed_stream(stream)),InternalHandle::Manual(handle),)}None => (None,InternalHandle::Manual(iced::futures::stream::AbortHandle::new_pair().0,),),};(Self { stream }, Handle { internal: handle })}}#[cfg(any(test, feature = "testing"))]impl<T> std::fmt::Debug for Task<T> {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {f.debug_struct(&format!("Task<{}>", std::any::type_name::<T>())).finish()}}/// A handle to a [`Task`] that can be used for aborting it.#[cfg(any(test, feature = "testing"))]#[derive(Debug, Clone)]pub struct Handle {internal: InternalHandle,}#[cfg(any(test, feature = "testing"))]#[derive(Debug, Clone)]enum InternalHandle {Manual(iced::futures::stream::AbortHandle),AbortOnDrop(std::sync::Arc<iced::futures::stream::AbortHandle>),}#[cfg(any(test, feature = "testing"))]impl InternalHandle {pub fn as_ref(&self) -> &iced::futures::stream::AbortHandle {match self {InternalHandle::Manual(handle) => handle,InternalHandle::AbortOnDrop(handle) => handle.as_ref(),}}}#[cfg(any(test, feature = "testing"))]impl Handle {pub fn abort(&self) {self.internal.as_ref().abort();}pub fn abort_on_drop(self) -> Self {match &self.internal {InternalHandle::Manual(handle) => Self {internal: InternalHandle::AbortOnDrop(std::sync::Arc::new(handle.clone(),)),},InternalHandle::AbortOnDrop(_) => self,
#[cfg(any(test, feature = "testing"))]impl Drop for Handle {fn drop(&mut self) {if let InternalHandle::AbortOnDrop(handle) = &mut self.internal {let handle = std::mem::replace(handle,std::sync::Arc::new(iced::futures::stream::AbortHandle::new_pair().0,),);if let Some(handle) = std::sync::Arc::into_inner(handle) {handle.abort();}}}}