Skip to main content

max / audiofiles

Dedup the four name-modal handlers behind one outcome helper draw_vfs_create/vfs_rename/dir_create/dir_rename all drew the same name_modal and ran the same outcome match (empty/cancel closes, non-empty commits with an inline error on failure). Extract that shared match into handle_name_modal_outcome taking per-modal close/commit closures; each handler is now the widget call plus its two closures. Ids are captured at draw time (VfsId/NodeId are Copy), which also drops the re-lookup-from-Option dance in the rename paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 20:25 UTC
Commit: 5e3fa8df4f41f647078a092074c5d4434eeec258
Parent: 6ad3ec7
1 file changed, +123 insertions, -141 deletions
@@ -929,105 +929,115 @@ fn draw_bulk_rename_modal(ctx: &egui::Context, state: &mut BrowserState) {
929 929 }
930 930 }
931 931
932 - /// Draw the "New Vault" modal: text input for vault name.
933 - pub fn draw_vfs_create_modal(ctx: &egui::Context, state: &mut BrowserState) {
934 - let hint = "A vault is a separate sample collection \u{2014} like a folder, but with its own tags and analysis. Right-click inside to create sub-folders.";
935 - // C-3: clone the error into a local so the &mut input borrow doesn't
936 - // conflict with the immutable error borrow into name_modal.
937 - let error_owned = state.vfs_modal.name_modal_error.clone();
938 - match widgets::name_modal(
939 - ctx,
940 - "New Vault",
941 - Some(hint),
942 - "Vault name:",
943 - &mut state.vfs_modal.vfs_create_input,
944 - "Create",
945 - error_owned.as_deref(),
946 - ) {
932 + /// Shared outcome handling for the four name modals (create/rename × vault/folder).
933 + ///
934 + /// Each modal draws the same `name_modal` widget and then treats its outcome the
935 + /// same way: an empty or cancelled submit closes the modal; a non-empty submit
936 + /// runs the create/rename op, showing its status line on success or surfacing the
937 + /// error inline (C-3) while leaving the modal open with the typed name intact.
938 + /// Only the static labels, the name buffer, `close`, and `commit` differ per modal.
939 + ///
940 + /// `close` resets the modal (hides it, or clears its rename target); `commit`
941 + /// performs the op and returns the status line to show, or an error string to
942 + /// surface inline.
943 + fn handle_name_modal_outcome(
944 + outcome: NameModalOutcome,
945 + state: &mut BrowserState,
946 + close: impl FnOnce(&mut BrowserState),
947 + commit: impl FnOnce(&mut BrowserState, String) -> Result<String, String>,
948 + ) {
949 + match outcome {
947 950 NameModalOutcome::Submitted(name) => {
948 951 if name.is_empty() {
949 952 // Empty submit = no-op cancel. Close without erroring.
950 - state.vfs_modal.show_vfs_create = false;
953 + close(state);
951 954 state.vfs_modal.name_modal_error = None;
952 955 } else {
953 - match state.backend.create_vfs(&name) {
954 - Ok(_) => {
955 - state.refresh_vfs_list();
956 - state.status = format!("Created vault: {name}");
957 - state.vfs_modal.show_vfs_create = false;
956 + match commit(state, name) {
957 + Ok(status) => {
958 + state.status = status;
959 + close(state);
958 960 state.vfs_modal.name_modal_error = None;
959 961 }
960 - Err(e) => {
961 - // C-3: keep modal open; surface error inline so the
962 - // user can edit and retry without re-typing the name.
963 - state.vfs_modal.name_modal_error = Some(format!("{e}"));
964 - }
962 + // C-3: keep the modal open; surface the error inline so the
963 + // user can edit and retry without re-typing the name.
964 + Err(msg) => state.vfs_modal.name_modal_error = Some(msg),
965 965 }
966 966 }
967 967 }
968 968 NameModalOutcome::Cancelled => {
969 - state.vfs_modal.show_vfs_create = false;
969 + close(state);
970 970 state.vfs_modal.name_modal_error = None;
971 971 }
972 972 NameModalOutcome::None => {}
973 973 }
974 974 }
975 975
976 + /// Draw the "New Vault" modal: text input for vault name.
977 + pub fn draw_vfs_create_modal(ctx: &egui::Context, state: &mut BrowserState) {
978 + let hint = "A vault is a separate sample collection \u{2014} like a folder, but with its own tags and analysis. Right-click inside to create sub-folders.";
979 + // C-3: clone the error into a local so the &mut input borrow doesn't
980 + // conflict with the immutable error borrow into name_modal.
981 + let error_owned = state.vfs_modal.name_modal_error.clone();
982 + let outcome = widgets::name_modal(
983 + ctx,
984 + "New Vault",
985 + Some(hint),
986 + "Vault name:",
987 + &mut state.vfs_modal.vfs_create_input,
988 + "Create",
989 + error_owned.as_deref(),
990 + );
991 + handle_name_modal_outcome(
992 + outcome,
993 + state,
994 + |s| s.vfs_modal.show_vfs_create = false,
995 + |s, name| {
996 + s.backend.create_vfs(&name).map_err(|e| format!("{e}"))?;
997 + s.refresh_vfs_list();
998 + Ok(format!("Created vault: {name}"))
999 + },
1000 + );
1001 + }
1002 +
976 1003 /// Draw the "Rename Vault" modal: text input pre-filled with current name.
977 1004 pub fn draw_vfs_rename_modal(ctx: &egui::Context, state: &mut BrowserState) {
978 1005 let error_owned = state.vfs_modal.name_modal_error.clone();
979 - let outcome = if let Some((_, ref mut name_buf)) = state.vfs_modal.vfs_rename_target {
980 - widgets::name_modal(
981 - ctx,
982 - "Rename Vault",
983 - Some("Vault names can contain spaces."),
984 - "New name:",
985 - name_buf,
986 - "Save",
987 - error_owned.as_deref(),
1006 + let (outcome, vfs_id) = if let Some((id, ref mut name_buf)) = state.vfs_modal.vfs_rename_target
1007 + {
1008 + (
1009 + widgets::name_modal(
1010 + ctx,
1011 + "Rename Vault",
1012 + Some("Vault names can contain spaces."),
1013 + "New name:",
1014 + name_buf,
1015 + "Save",
1016 + error_owned.as_deref(),
1017 + ),
1018 + id,
988 1019 )
989 1020 } else {
990 1021 return;
991 1022 };
992 - match outcome {
993 - NameModalOutcome::Submitted(new_name) => {
994 - if new_name.is_empty() {
995 - state.vfs_modal.vfs_rename_target = None;
996 - state.vfs_modal.name_modal_error = None;
997 - } else {
998 - let vfs_id = state
999 - .vfs_modal
1000 - .vfs_rename_target
1001 - .as_ref()
1002 - .map(|(id, _)| *id);
1003 - if let Some(vfs_id) = vfs_id {
1004 - match state.backend.rename_vfs(vfs_id, &new_name) {
1005 - Ok(()) => {
1006 - state.refresh_vfs_list();
1007 - state.status = format!("Renamed vault to: {new_name}");
1008 - state.vfs_modal.vfs_rename_target = None;
1009 - state.vfs_modal.name_modal_error = None;
1010 - }
1011 - Err(e) => {
1012 - // C-3: keep modal open with the typed name preserved.
1013 - state.vfs_modal.name_modal_error = Some(format!("{e}"));
1014 - }
1015 - }
1016 - }
1017 - }
1018 - }
1019 - NameModalOutcome::Cancelled => {
1020 - state.vfs_modal.vfs_rename_target = None;
1021 - state.vfs_modal.name_modal_error = None;
1022 - }
1023 - NameModalOutcome::None => {}
1024 - }
1023 + handle_name_modal_outcome(
1024 + outcome,
1025 + state,
1026 + |s| s.vfs_modal.vfs_rename_target = None,
1027 + move |s, new_name| {
1028 + s.backend
1029 + .rename_vfs(vfs_id, &new_name)
1030 + .map_err(|e| format!("{e}"))?;
1031 + s.refresh_vfs_list();
1032 + Ok(format!("Renamed vault to: {new_name}"))
1033 + },
1034 + );
1025 1035 }
1026 1036
1027 1037 /// Draw the "New Folder" modal: text input for folder name.
1028 1038 pub fn draw_dir_create_modal(ctx: &egui::Context, state: &mut BrowserState) {
1029 1039 let error_owned = state.vfs_modal.name_modal_error.clone();
1030 - match widgets::name_modal(
1040 + let outcome = widgets::name_modal(
1031 1041 ctx,
1032 1042 "New Folder",
1033 1043 Some("Folder names cannot contain /"),
@@ -1035,86 +1045,58 @@ pub fn draw_dir_create_modal(ctx: &egui::Context, state: &mut BrowserState) {
1035 1045 &mut state.vfs_modal.dir_create_input,
1036 1046 "Create",
1037 1047 error_owned.as_deref(),
1038 - ) {
1039 - NameModalOutcome::Submitted(name) => {
1040 - if name.is_empty() {
1041 - state.vfs_modal.show_dir_create = false;
1042 - state.vfs_modal.name_modal_error = None;
1043 - } else if let Some(vfs_id) = state.current_vfs_id() {
1044 - match state
1045 - .backend
1046 - .create_directory(vfs_id, state.nav.current_dir, &name)
1047 - {
1048 - Ok(_) => {
1049 - state.refresh_contents();
1050 - state.status = format!("Created folder: {name}");
1051 - state.vfs_modal.show_dir_create = false;
1052 - state.vfs_modal.name_modal_error = None;
1053 - }
1054 - Err(e) => {
1055 - // C-3.
1056 - state.vfs_modal.name_modal_error = Some(format!("{e}"));
1057 - }
1058 - }
1059 - }
1060 - }
1061 - NameModalOutcome::Cancelled => {
1062 - state.vfs_modal.show_dir_create = false;
1063 - state.vfs_modal.name_modal_error = None;
1064 - }
1065 - NameModalOutcome::None => {}
1066 - }
1048 + );
1049 + let vfs_id = state.current_vfs_id();
1050 + let current_dir = state.nav.current_dir;
1051 + handle_name_modal_outcome(
1052 + outcome,
1053 + state,
1054 + |s| s.vfs_modal.show_dir_create = false,
1055 + move |s, name| {
1056 + // The New Folder modal is only reachable inside a vault, so this is a
1057 + // defensive guard rather than a real path.
1058 + let vfs_id = vfs_id.ok_or_else(|| "No vault selected".to_string())?;
1059 + s.backend
1060 + .create_directory(vfs_id, current_dir, &name)
1061 + .map_err(|e| format!("{e}"))?;
1062 + s.refresh_contents();
1063 + Ok(format!("Created folder: {name}"))
1064 + },
1065 + );
1067 1066 }
1068 1067
1069 1068 /// Draw the "Rename Folder" modal: text input pre-filled with current name.
1070 1069 pub fn draw_dir_rename_modal(ctx: &egui::Context, state: &mut BrowserState) {
1071 1070 let error_owned = state.vfs_modal.name_modal_error.clone();
1072 - let outcome = if let Some((_, ref mut name_buf)) = state.vfs_modal.dir_rename_target {
1073 - widgets::name_modal(
1074 - ctx,
1075 - "Rename",
1076 - None,
1077 - "New name:",
1078 - name_buf,
1079 - "Save",
1080 - error_owned.as_deref(),
1071 + let (outcome, node_id) = if let Some((id, ref mut name_buf)) = state.vfs_modal.dir_rename_target
1072 + {
1073 + (
1074 + widgets::name_modal(
1075 + ctx,
1076 + "Rename",
1077 + None,
1078 + "New name:",
1079 + name_buf,
1080 + "Save",
1081 + error_owned.as_deref(),
1082 + ),
1083 + id,
1081 1084 )
1082 1085 } else {
1083 1086 return;
1084 1087 };
1085 - match outcome {
1086 - NameModalOutcome::Submitted(new_name) => {
1087 - if new_name.is_empty() {
1088 - state.vfs_modal.dir_rename_target = None;
1089 - state.vfs_modal.name_modal_error = None;
1090 - } else {
1091 - let node_id = state
1092 - .vfs_modal
1093 - .dir_rename_target
1094 - .as_ref()
1095 - .map(|(id, _)| *id);
1096 - if let Some(node_id) = node_id {
1097 - match state.backend.rename_node(node_id, &new_name) {
1098 - Ok(()) => {
1099 - state.refresh_contents();
1100 - state.status = format!("Renamed to: {new_name}");
1101 - state.vfs_modal.dir_rename_target = None;
1102 - state.vfs_modal.name_modal_error = None;
1103 - }
1104 - Err(e) => {
1105 - // C-3.
1106 - state.vfs_modal.name_modal_error = Some(format!("{e}"));
1107 - }
1108 - }
1109 - }
1110 - }
1111 - }
1112 - NameModalOutcome::Cancelled => {
1113 - state.vfs_modal.dir_rename_target = None;
1114 - state.vfs_modal.name_modal_error = None;
1115 - }
1116 - NameModalOutcome::None => {}
1117 - }
1088 + handle_name_modal_outcome(
1089 + outcome,
1090 + state,
1091 + |s| s.vfs_modal.dir_rename_target = None,
1092 + move |s, new_name| {
1093 + s.backend
1094 + .rename_node(node_id, &new_name)
1095 + .map_err(|e| format!("{e}"))?;
1096 + s.refresh_contents();
1097 + Ok(format!("Renamed to: {new_name}"))
1098 + },
1099 + );
1118 1100 }
1119 1101
1120 1102 #[cfg(test)]