dir picker scrollables handling + confirmation

tzemanovic
Feb 6, 2026, 2:31 PM
TMDH7GPVR3J7OEIELEE7RM7KXBA5IMFBCCVJTEGGMLYJZZCOLJ6QC

Dependencies

  • [2] EJPSD5XO shared allowed actions conditions between update and view
  • [3] PKLUHYE4 allow to copy change hash
  • [4] LFEMJYYD start of to_record selection
  • [5] YGZ3VCW4 add push
  • [6] YRGDFHAB project dir picker
  • [7] LPSUBGUB add projects picker
  • [8] MORKDJUE use allowed actions binding for key subs
  • [9] OLDN7R34 retry upsert project
  • [10] IMJQ4PML fix and refactor action bindings
  • [11] 5BAPU7K6 dir picker key navigation
  • [12] TEDT26JQ add push and pull sub-menus
  • [13] AZ5D2LQU allow to set record description
  • [14] UTDTZCTX pull+push status, add info reports
  • [15] ODCT4QJN add pull
  • [*] 6YZAVBWU Initial commit

Change contents

  • edit in inflorescence_model/src/action.rs at line 359
    [8.172698]
    [11.4611]
    push_if(
    matches!(selection, dir_picker::Selection::Input),
    || confirm("Confirm input"),
    ma,
    );
    push_if(
    matches!(selection, dir_picker::Selection::SubDir(_)),
    || confirm("Confirm dir selection"),
    ma,
    );
    push_if(
    matches!(
    selection,
    dir_picker::Selection::ProjectPijul(_)
    | dir_picker::Selection::ProjectGit(_)
    ),
    || confirm("Confirm project selection"),
    ma,
    );
  • replacement in inflorescence_model/src/action.rs at line 649
    [3.445][10.130:569]()
    let push_sub_menu = |can_push: bool| {
    move || -> Binding {
    Binding {
    keys_str: "S-p",
    keys: ModKeys::One(ModKey {
    key: Key::Character("p".into()),
    mods: Mods::SHIFT,
    }),
    label: "push",
    msg: can_push
    .then_some(FilteredMsg::EnterSubMenu(model::SubMenu::Push)),
    }
    [3.445]
    [10.569]
    let push_sub_menu = |can_push: bool| -> Binding {
    Binding {
    keys_str: "S-p",
    keys: ModKeys::One(ModKey {
    key: Key::Character("p".into()),
    mods: Mods::SHIFT,
    }),
    label: "push",
    msg: can_push
    .then_some(FilteredMsg::EnterSubMenu(model::SubMenu::Push)),
  • replacement in inflorescence_model/src/action.rs at line 661
    [5.4995][10.580:1019]()
    let pull_sub_menu = |can_pull: bool| {
    move || -> Binding {
    Binding {
    keys_str: "S-f",
    keys: ModKeys::One(ModKey {
    key: Key::Character("f".into()),
    mods: Mods::SHIFT,
    }),
    label: "pull",
    msg: can_pull
    .then_some(FilteredMsg::EnterSubMenu(model::SubMenu::Pull)),
    }
    [5.4995]
    [10.1019]
    let pull_sub_menu = |can_pull: bool| -> Binding {
    Binding {
    keys_str: "S-f",
    keys: ModKeys::One(ModKey {
    key: Key::Character("f".into()),
    mods: Mods::SHIFT,
    }),
    label: "pull",
    msg: can_pull
    .then_some(FilteredMsg::EnterSubMenu(model::SubMenu::Pull)),
  • replacement in inflorescence_model/src/action.rs at line 715
    [4.14738][10.1457:1561]()
    push(push_sub_menu(can_push_pull), ma);
    push(pull_sub_menu(can_push_pull), ma);
    [4.14738]
    [2.10048]
    push(|| push_sub_menu(can_push_pull), ma);
    push(|| pull_sub_menu(can_push_pull), ma);
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 54
    [6.1206][6.1206:1223]()
    SubmitInput,
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 121
    [6.3304]
    [6.3304]
    state.selection = Selection::Input;
    nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
    nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 171
    [6.5187][6.5187:6245]()
    }
    }
    }
    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 match
    let 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 match
    if matches.len() == 1 {
    let matched =
    state.current_dir.join(matches.pop().unwrap());
    state.input = String::default();
    change_dir(state, matched)
    } else {
    Task::none()
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 174
    [6.6287]
    [6.6287]
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 247
    [11.6945][11.6945:7000]()
    *selection = Selection::SubDir(0);
    [11.6945]
    [11.7000]
    let new_ix = 0;
    *selection = Selection::SubDir(new_ix);
    nav_scrollable::scroll_up_to_section(
    &mut state.left_nav,
    new_ix,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 263
    [11.7427]
    [11.7427]
    nav_scrollable::scroll_down_to_section(
    &mut state.left_nav,
    new_ix,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 269
    [11.7503]
    [11.7503]
    nav_scrollable::scroll_up_to_section(
    &mut state.left_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 280
    [11.7828]
    [11.7828]
    nav_scrollable::scroll_down_to_section(
    &mut state.right_nav,
    new_ix,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 286
    [11.7962]
    [11.7962]
    nav_scrollable::scroll_down_to_section(
    &mut state.right_nav,
    state
    .found_repos_dirs_pijul
    .len()
    .saturating_sub(1),
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 295
    [11.8056]
    [11.8056]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    0,
    );
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 301
    [11.8145][11.8145:8441]()
    if state.found_repos_dirs_git.len() - 1 > *ix {
    let new_ix = *ix + 1;
    *selection = Selection::ProjectGit(new_ix);
    } else {
    *selection = Selection::ProjectGit(0);
    }
    [11.8145]
    [11.8441]
    *selection = Selection::ProjectGit(0);
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    state.found_repos_dirs_pijul.len().saturating_sub(1),
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 308
    [11.8517]
    [11.8517]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 319
    [11.8834]
    [11.8834]
    nav_scrollable::scroll_down_to_section(
    &mut state.right_nav,
    state
    .found_repos_dirs_pijul
    .len()
    .saturating_sub(1)
    + new_ix,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 329
    [11.8972]
    [11.8972]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 335
    [11.9064]
    [11.9064]
    nav_scrollable::scroll_down_to_section(
    &mut state.right_nav,
    state
    .found_repos_dirs_pijul
    .len()
    .saturating_sub(1),
    );
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 344
    [11.9155][11.9155:9457]()
    if state.found_repos_dirs_pijul.len() - 1 > *ix {
    let new_ix = *ix + 1;
    *selection = Selection::ProjectPijul(new_ix);
    } else {
    *selection = Selection::ProjectPijul(0);
    }
    [11.9155]
    [11.9457]
    *selection = Selection::ProjectPijul(0);
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 351
    [11.9533]
    [11.9533]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 363
    [11.9843]
    [11.9843]
    nav_scrollable::scroll_down_to_section(
    &mut state.left_nav,
    new_ix,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 370
    [11.10021]
    [11.10021]
    nav_scrollable::scroll_down_to_section(
    &mut state.left_nav,
    new_ix,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 383
    [11.10357]
    [11.10357]
    nav_scrollable::scroll_up_to_section(
    &mut state.left_nav,
    new_ix,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 389
    [11.10433]
    [11.10433]
    nav_scrollable::scroll_up_to_section(
    &mut state.left_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 400
    [11.10721]
    [11.10721]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    new_ix,
    );
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 405
    [11.10792][11.10792:10918]()
    *selection = Selection::ProjectGit(
    state.found_repos_dirs_git.len() - 1,
    [11.10792]
    [11.10918]
    let new_ix = state.found_repos_dirs_git.len() - 1;
    *selection = Selection::ProjectGit(new_ix);
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    state
    .found_repos_dirs_pijul
    .len()
    .saturating_sub(1)
    + new_ix,
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 416
    [11.10974][11.10974:11104]()
    *selection = Selection::ProjectPijul(
    state.found_repos_dirs_pijul.len() - 1,
    [11.10974]
    [11.11104]
    let new_ix = state.found_repos_dirs_pijul.len() - 1;
    *selection = Selection::ProjectPijul(new_ix);
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    new_ix,
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 427
    [11.11367]
    [11.11367]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    new_ix,
    );
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 432
    [11.11396][11.11396:11522]()
    *selection = Selection::ProjectGit(
    state.found_repos_dirs_git.len() - 1,
    [11.11396]
    [11.11522]
    let new_ix = state.found_repos_dirs_git.len() - 1;
    *selection = Selection::ProjectGit(new_ix);
    nav_scrollable::scroll_down_to_section(
    &mut state.right_nav,
    new_ix,
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 441
    [11.11647]
    [11.11647]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 452
    [11.11929]
    [11.11929]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    state
    .found_repos_dirs_pijul
    .len()
    .saturating_sub(1)
    + new_ix,
    );
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 461
    [11.12002][11.12002:12132]()
    *selection = Selection::ProjectPijul(
    state.found_repos_dirs_pijul.len() - 1,
    [11.12002]
    [11.12132]
    let new_ix = state.found_repos_dirs_pijul.len() - 1;
    *selection = Selection::ProjectPijul(new_ix);
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    new_ix,
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 468
    [11.12188][11.12188:12314]()
    *selection = Selection::ProjectGit(
    state.found_repos_dirs_git.len() - 1,
    [11.12188]
    [11.12314]
    let new_ix = state.found_repos_dirs_git.len() - 1;
    *selection = Selection::ProjectGit(new_ix);
    nav_scrollable::scroll_down_to_section(
    &mut state.right_nav,
    state
    .found_repos_dirs_pijul
    .len()
    .saturating_sub(1)
    + new_ix,
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 483
    [11.12581]
    [11.12581]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    new_ix,
    );
  • replacement in inflorescence_iced_widget/src/dir_picker.rs at line 488
    [11.12610][11.12610:12740]()
    *selection = Selection::ProjectPijul(
    state.found_repos_dirs_pijul.len() - 1,
    [11.12610]
    [11.12740]
    let new_ix = state.found_repos_dirs_pijul.len() - 1;
    *selection = Selection::ProjectPijul(new_ix);
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    new_ix,
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 497
    [11.12865]
    [11.12865]
    nav_scrollable::scroll_up_to_section(
    &mut state.right_nav,
    0,
    );
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 511
    [11.13302]
    [11.13302]
    nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 521
    [11.13635]
    [11.13635]
    nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 531
    [11.13966]
    [11.13966]
    nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 537
    [6.8248]
    [6.8248]
    pub fn confirm_input_or_selection(
    state: &mut State,
    ) -> (Task<Msg>, Option<Action>) {
    let mut action = None;
    let task = match state.selection {
    Selection::Input => {
    if state.input.is_empty() {
    action = Some(Action::Picked(
    state.current_dir.as_path().to_path_buf(),
    ));
    Task::none()
    } else {
    nav_scrollable::scroll_up_to_section(&mut state.right_nav, 0);
    nav_scrollable::scroll_up_to_section(&mut state.left_nav, 0);
    // Accept input only if there's an exact match
    let 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 match
    if matches.len() == 1 {
    let matched =
    state.current_dir.join(matches.pop().unwrap());
    state.input = String::default();
    change_dir(state, matched)
    } else {
    Task::none()
    }
    }
    }
    Selection::SubDir(ix) => {
    if !state.matched_child_dirs.is_empty() {
    if let Some(dir) = state.matched_child_dirs.get(ix) {
    let path = state.current_dir.join(dir);
    change_dir(state, path)
    } else {
    Task::none()
    }
    } else if !state.child_dirs.is_empty() {
    if let Some(dir) = state.child_dirs.get(ix) {
    let path = state.current_dir.join(dir);
    change_dir(state, path)
    } else {
    Task::none()
    }
    } else {
    Task::none()
    }
    }
    Selection::ProjectPijul(ix) => {
    if let Some(path) = state.found_repos_dirs_pijul.get(ix) {
    action = Some(Action::Picked(path.clone()));
    }
    Task::none()
    }
    Selection::ProjectGit(ix) => {
    if let Some(path) = state.found_repos_dirs_git.get(ix) {
    action = Some(Action::Picked(path.clone()));
    }
    Task::none()
    }
    };
    state.selection = Selection::Input;
    (task, action)
    }
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 682
    [6.10442][6.10442:10613]()
    ),
    Element::new(
    text_input("", input)
    .on_input(Msg::Input)
    .on_submit(Msg::SubmitInput),
  • edit in inflorescence_iced_widget/src/dir_picker.rs at line 683
    [6.10628]
    [6.10628]
    Element::new(text_input("", input).on_input(Msg::Input)),
  • edit in inflorescence/src/main.rs at line 555
    [6.3615521]
    [6.3615521]
    let mut handle_action = |action| {
    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 scratch
    let project = store::Project {
    last_closed_time: cmp::Reverse(None),
    path: dir.clone(),
    };
    let store_project_task = if repo::is_pijul(&project.path) {
    Task::perform(
    async move { store::upsert_project(project).await },
    Msg::UpsertProjectResult,
    )
    } else {
    Task::none()
    };
    let (sub, managing_repo, managing_repo_task) =
    init_managing_repo(dir);
    new_state = Some((sub, managing_repo));
    Task::batch([managing_repo_task, store_project_task])
    } else {
    Task::none()
    }
    };
  • replacement in inflorescence/src/main.rs at line 586
    [11.17884][11.17884:17959]()
    inflorescence_model::selection::Msg::AltPressDir(dir) => {
    [11.17884]
    [11.17959]
    selection::Msg::AltPressDir(dir) => {
  • replacement in inflorescence/src/main.rs at line 592
    [11.18160][11.18160:18243]()
    action::FilteredMsg::Confirm
    | action::FilteredMsg::Cancel
    [11.18160]
    [11.18243]
    action::FilteredMsg::Confirm => {
    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(action);
    Task::batch([dir_picker_task, action_task])
    }
    action::FilteredMsg::Cancel
  • replacement in inflorescence/src/main.rs at line 624
    [7.22469][6.3615928:3616200](),[6.3615928][6.3615928:3616200](),[6.3616200][7.22470:22821](),[7.22821][9.1171:1221](),[9.1221][7.22866:22965](),[7.22866][7.22866:22965](),[7.22965][6.3616200:3616364](),[6.3616200][6.3616200:3616364](),[6.3616364][7.22966:23132](),[7.23132][6.3616420:3616441](),[6.3616420][6.3616420:3616441](),[6.3616441][7.23133:23165](),[7.23165][6.3616462:3616476](),[6.3616462][6.3616462:3616476]()
    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 scratch
    let project = store::Project {
    last_closed_time: cmp::Reverse(None),
    path: dir.clone(),
    };
    let store_project_task = if repo::is_pijul(&project.path) {
    Task::perform(
    async move { store::upsert_project(project).await },
    Msg::UpsertProjectResult,
    )
    } else {
    Task::none()
    };
    let (sub, managing_repo, managing_repo_task) =
    init_managing_repo(dir);
    new_state = Some((sub, managing_repo));
    Task::batch([
    dir_picker_task,
    managing_repo_task,
    store_project_task,
    ])
    } else {
    dir_picker_task
    }
    [7.22469]
    [6.3616476]
    let action_task = handle_action(action);
    Task::batch([dir_picker_task, action_task])