Skip to content

Commit 5800131

Browse files
authored
Merge pull request #7 from wasabeef/feat-themes
feat(ui): add default selection to menus and unify ESC cancellation b…
2 parents 2eebbb1 + 01cef5a commit 5800131

20 files changed

+795
-173
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,5 @@ IMPLEMENTATION_COMPLETE.md
6464
analysis_results/
6565
backup_phase*/
6666
scripts/
67+
68+
worktrees/

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "git-workers"
3-
version = "0.5.1"
3+
version = "0.6.0"
44
edition = "2021"
55
authors = ["Daichi Furiya"]
66
description = "Interactive Git worktree manager with shell integration"

src/commands.rs

Lines changed: 99 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,23 @@ use crate::config::Config;
2929
use crate::constants::{
3030
section_header, ACTION_CHANGE_BRANCH_NAME, ACTION_CREATE_NEW_BRANCH, ACTION_USE_LOCAL_BRANCH,
3131
ACTION_USE_WORKTREE_NAME, BRANCH_OPTION_SELECT_BRANCH, BRANCH_OPTION_SELECT_TAG, CHAR_DOT,
32-
CONFIG_FILE_NAME, DEFAULT_EMPTY_STRING, DEFAULT_REPO_NAME, DEFAULT_WORKTREE_CLEANUP_DAYS,
32+
CONFIG_FILE_NAME, DEFAULT_BRANCH_DETACHED, DEFAULT_BRANCH_UNKNOWN, DEFAULT_EDITOR_UNIX,
33+
DEFAULT_EDITOR_WINDOWS, DEFAULT_EMPTY_STRING, DEFAULT_MENU_SELECTION, DEFAULT_REPO_NAME,
34+
DEFAULT_WORKTREE_CLEANUP_DAYS, EMOJI_DETACHED, EMOJI_FOLDER, EMOJI_HOME, EMOJI_LOCKED,
35+
ENV_EDITOR, ENV_VISUAL, ERROR_CUSTOM_PATH_EMPTY, ERROR_WORKTREE_NAME_EMPTY,
3336
FUZZY_SEARCH_THRESHOLD, GIT_CRITICAL_DIRS, GIT_DIR, GIT_REMOTE_PREFIX, GIT_RESERVED_NAMES,
34-
HOOK_POST_CREATE, HOOK_POST_SWITCH, HOOK_PRE_REMOVE, ICON_ARROW, ICON_LIST, ICON_LOCAL_BRANCH,
37+
HEADER_CREATE_WORKTREE, HEADER_SEARCH_WORKTREES, HEADER_WORKTREES, HOOK_POST_CREATE,
38+
HOOK_POST_SWITCH, HOOK_PRE_REMOVE, ICON_ARROW, ICON_LIST, ICON_LOCAL_BRANCH,
3539
ICON_REMOTE_BRANCH, ICON_SWITCH, ICON_TAG_INDICATOR, INFO_TIP, INFO_USE_CREATE,
3640
INVALID_FILESYSTEM_CHARS, LABEL_BRANCH, LABEL_MODIFIED, LABEL_NAME, LABEL_NO, LABEL_PATH,
37-
LABEL_YES, MAX_WORKTREE_NAME_LENGTH, MIN_PATH_COMPONENTS_FOR_SUBDIR,
38-
PATH_COMPONENT_SECOND_INDEX, PROGRESS_BAR_TICK_MILLIS, TAG_MESSAGE_TRUNCATE_LENGTH,
39-
UI_BRANCH_COL_EXTRA_WIDTH, UI_FOOTER_LINES, UI_HEADER_LINES, UI_MIN_ITEMS_PER_PAGE,
40-
UI_MODIFIED_COL_WIDTH, UI_NAME_COL_MIN_WIDTH, UI_PATH_COL_WIDTH, WARNING_NO_WORKTREES,
41-
WINDOWS_RESERVED_CHARS, WORKTREES_SUBDIR, WORKTREE_LOCATION_CUSTOM_PATH,
41+
LABEL_YES, MAX_WORKTREE_NAME_LENGTH, MIN_PATH_COMPONENTS_FOR_SUBDIR, MSG_ALREADY_IN_WORKTREE,
42+
MSG_NO_WORKTREES_TO_SEARCH, MSG_SEARCH_FUZZY_ENABLED, PATH_COMPONENT_SECOND_INDEX,
43+
PROGRESS_BAR_TICK_MILLIS, PROMPT_CONFLICT_ACTION, PROMPT_CUSTOM_PATH, PROMPT_SELECT_BRANCH,
44+
PROMPT_SELECT_BRANCH_OPTION, PROMPT_SELECT_TAG, PROMPT_SELECT_WORKTREE_LOCATION,
45+
PROMPT_SELECT_WORKTREE_SWITCH, PROMPT_WORKTREE_NAME, SEARCH_CURRENT_INDICATOR,
46+
TAG_MESSAGE_TRUNCATE_LENGTH, UI_BRANCH_COL_EXTRA_WIDTH, UI_FOOTER_LINES, UI_HEADER_LINES,
47+
UI_MIN_ITEMS_PER_PAGE, UI_MODIFIED_COL_WIDTH, UI_NAME_COL_MIN_WIDTH, UI_PATH_COL_WIDTH,
48+
WARNING_NO_WORKTREES, WINDOWS_RESERVED_CHARS, WORKTREES_SUBDIR, WORKTREE_LOCATION_CUSTOM_PATH,
4249
WORKTREE_LOCATION_SAME_LEVEL, WORKTREE_LOCATION_SUBDIRECTORY,
4350
};
4451
use crate::file_copy;
@@ -84,13 +91,13 @@ fn get_worktree_icon_internal(is_current: bool) -> colored::ColoredString {
8491
#[allow(dead_code)]
8592
pub fn get_worktree_icon(worktree: &WorktreeInfo) -> &'static str {
8693
if worktree.is_current {
87-
"🏠"
94+
EMOJI_HOME
8895
} else if worktree.is_locked {
89-
"🔒"
90-
} else if worktree.branch == "detached" {
91-
"🔗"
96+
EMOJI_LOCKED
97+
} else if worktree.branch == DEFAULT_BRANCH_DETACHED {
98+
EMOJI_DETACHED
9299
} else {
93-
"📁"
100+
EMOJI_FOLDER
94101
}
95102
}
96103

@@ -202,7 +209,7 @@ pub fn list_worktrees_with_git(git_ops: &dyn GitReadOperations) -> Result<()> {
202209

203210
// Print header
204211
println!();
205-
let header = section_header("Worktrees");
212+
let header = section_header(HEADER_WORKTREES);
206213
println!("{header}");
207214

208215
let start_idx = current_page * items_per_page;
@@ -354,15 +361,15 @@ fn search_worktrees_internal(manager: &GitWorktreeManager) -> Result<bool> {
354361

355362
if worktrees.is_empty() {
356363
println!();
357-
let msg = "• No worktrees to search.".yellow();
364+
let msg = MSG_NO_WORKTREES_TO_SEARCH.yellow();
358365
println!("{msg}");
359366
println!();
360367
press_any_key_to_continue()?;
361368
return Ok(false);
362369
}
363370

364371
println!();
365-
let header = section_header("Search Worktrees");
372+
let header = section_header(HEADER_SEARCH_WORKTREES);
366373
println!("{header}");
367374
println!();
368375

@@ -372,16 +379,16 @@ fn search_worktrees_internal(manager: &GitWorktreeManager) -> Result<bool> {
372379
.map(|wt| {
373380
let mut item = format!("{} ({})", wt.name, wt.branch);
374381
if wt.is_current {
375-
item.push_str(" (current)");
382+
item.push_str(SEARCH_CURRENT_INDICATOR);
376383
}
377384
item
378385
})
379386
.collect();
380387

381388
// Use FuzzySelect for interactive search
382-
println!("Type to search worktrees (fuzzy search enabled):");
389+
println!("{MSG_SEARCH_FUZZY_ENABLED}");
383390
let selection = match FuzzySelect::with_theme(&get_theme())
384-
.with_prompt("Select a worktree to switch to")
391+
.with_prompt(PROMPT_SELECT_WORKTREE_SWITCH)
385392
.items(&items)
386393
.interact_opt()?
387394
{
@@ -393,7 +400,7 @@ fn search_worktrees_internal(manager: &GitWorktreeManager) -> Result<bool> {
393400

394401
if selected_worktree.is_current {
395402
println!();
396-
let msg = "• Already in this worktree.".yellow();
403+
let msg = MSG_ALREADY_IN_WORKTREE.yellow();
397404
println!("{msg}");
398405
println!();
399406
press_any_key_to_continue()?;
@@ -529,7 +536,7 @@ pub fn create_worktree_with_ui(
529536
ui: &dyn UserInterface,
530537
) -> Result<bool> {
531538
println!();
532-
let header = section_header("Create New Worktree");
539+
let header = section_header(HEADER_CREATE_WORKTREE);
533540
println!("{header}");
534541
println!();
535542

@@ -538,13 +545,13 @@ pub fn create_worktree_with_ui(
538545
let has_worktrees = !existing_worktrees.is_empty();
539546

540547
// Get worktree name
541-
let name = match ui.input("Enter worktree name") {
548+
let name = match ui.input(PROMPT_WORKTREE_NAME) {
542549
Ok(name) => name.trim().to_string(),
543550
Err(_) => return Ok(false),
544551
};
545552

546553
if name.is_empty() {
547-
utils::print_error("Worktree name cannot be empty");
554+
utils::print_error(ERROR_WORKTREE_NAME_EMPTY);
548555
return Ok(false);
549556
}
550557

@@ -577,7 +584,11 @@ pub fn create_worktree_with_ui(
577584
"Custom path (specify relative to project root)".to_string(),
578585
];
579586

580-
let selection = match ui.select("Select worktree location pattern", &options) {
587+
let selection = match ui.select_with_default(
588+
PROMPT_SELECT_WORKTREE_LOCATION,
589+
&options,
590+
DEFAULT_MENU_SELECTION,
591+
) {
581592
Ok(selection) => selection,
582593
Err(_) => return Ok(false),
583594
};
@@ -594,13 +605,13 @@ pub fn create_worktree_with_ui(
594605
"Examples: ../custom-dir/worktree-name, temp/worktrees/name".dimmed();
595606
println!("{examples}");
596607

597-
let custom_path = match ui.input("Custom path") {
608+
let custom_path = match ui.input(PROMPT_CUSTOM_PATH) {
598609
Ok(path) => path.trim().to_string(),
599610
Err(_) => return Ok(false),
600611
};
601612

602613
if custom_path.is_empty() {
603-
utils::print_error("Custom path cannot be empty");
614+
utils::print_error(ERROR_CUSTOM_PATH_EMPTY);
604615
return Ok(false);
605616
}
606617

@@ -626,7 +637,11 @@ pub fn create_worktree_with_ui(
626637
"Select tag".to_string(),
627638
];
628639

629-
let branch_choice = match ui.select("Select branch option", &branch_options) {
640+
let branch_choice = match ui.select_with_default(
641+
PROMPT_SELECT_BRANCH_OPTION,
642+
&branch_options,
643+
DEFAULT_MENU_SELECTION,
644+
) {
630645
Ok(choice) => choice,
631646
Err(_) => return Ok(false),
632647
};
@@ -677,9 +692,13 @@ pub fn create_worktree_with_ui(
677692
// Use FuzzySelect for better search experience when there are many branches
678693
let selection_result = if branch_items.len() > FUZZY_SEARCH_THRESHOLD {
679694
println!("Type to search branches (fuzzy search enabled):");
680-
ui.fuzzy_select("Select a branch", &branch_items)
695+
ui.fuzzy_select(PROMPT_SELECT_BRANCH, &branch_items)
681696
} else {
682-
ui.select("Select a branch", &branch_items)
697+
ui.select_with_default(
698+
PROMPT_SELECT_BRANCH,
699+
&branch_items,
700+
DEFAULT_MENU_SELECTION,
701+
)
683702
};
684703
let selection_result = selection_result.ok();
685704

@@ -709,7 +728,11 @@ pub fn create_worktree_with_ui(
709728
"Cancel".to_string(),
710729
];
711730

712-
match ui.select("What would you like to do?", &action_options) {
731+
match ui.select_with_default(
732+
PROMPT_CONFLICT_ACTION,
733+
&action_options,
734+
DEFAULT_MENU_SELECTION,
735+
) {
713736
Ok(ACTION_USE_WORKTREE_NAME) => {
714737
// Use worktree name as new branch name
715738
(Some(selected_branch.clone()), Some(name.clone()))
@@ -779,7 +802,11 @@ pub fn create_worktree_with_ui(
779802
"Cancel".to_string(),
780803
];
781804

782-
match ui.select("What would you like to do?", &action_options) {
805+
match ui.select_with_default(
806+
PROMPT_CONFLICT_ACTION,
807+
&action_options,
808+
DEFAULT_MENU_SELECTION,
809+
) {
783810
Ok(ACTION_CREATE_NEW_BRANCH) => {
784811
// Create new branch with worktree name
785812
(
@@ -846,9 +873,9 @@ pub fn create_worktree_with_ui(
846873
// Use FuzzySelect for better search experience when there are many tags
847874
let selection_result = if tag_items.len() > FUZZY_SEARCH_THRESHOLD {
848875
println!("Type to search tags (fuzzy search enabled):");
849-
ui.fuzzy_select("Select a tag", &tag_items)
876+
ui.fuzzy_select(PROMPT_SELECT_TAG, &tag_items)
850877
} else {
851-
ui.select("Select a tag", &tag_items)
878+
ui.select_with_default(PROMPT_SELECT_TAG, &tag_items, DEFAULT_MENU_SELECTION)
852879
};
853880
let selection_result = selection_result.ok();
854881

@@ -1091,7 +1118,11 @@ pub fn delete_worktree_with_ui(manager: &GitWorktreeManager, ui: &dyn UserInterf
10911118
.map(|w| format!("{} ({})", w.name, w.branch))
10921119
.collect();
10931120

1094-
let selection = match ui.select("Select a worktree to delete (ESC to cancel)", &items) {
1121+
let selection = match ui.select_with_default(
1122+
"Select a worktree to delete (ESC to cancel)",
1123+
&items,
1124+
DEFAULT_MENU_SELECTION,
1125+
) {
10951126
Ok(selection) => selection,
10961127
Err(_) => return Ok(()),
10971128
};
@@ -1261,13 +1292,20 @@ pub fn switch_worktree_with_ui(
12611292
})
12621293
.collect();
12631294

1264-
let selection = ui.select("Select a worktree to switch to (ESC to cancel)", &items)?;
1295+
let selection = match ui.select_with_default(
1296+
"Select a worktree to switch to (ESC to cancel)",
1297+
&items,
1298+
DEFAULT_MENU_SELECTION,
1299+
) {
1300+
Ok(selection) => selection,
1301+
Err(_) => return Ok(false),
1302+
};
12651303

12661304
let selected_worktree = &sorted_worktrees[selection];
12671305

12681306
if selected_worktree.is_current {
12691307
println!();
1270-
let msg = "• Already in this worktree.".yellow();
1308+
let msg = MSG_ALREADY_IN_WORKTREE.yellow();
12711309
println!("{msg}");
12721310
println!();
12731311
press_any_key_to_continue()?;
@@ -1741,16 +1779,23 @@ pub fn rename_worktree_with_ui(manager: &GitWorktreeManager, ui: &dyn UserInterf
17411779
.map(|w| format!("{} ({})", w.name, w.branch))
17421780
.collect();
17431781

1744-
let selection = ui.select("Select a worktree to rename (ESC to cancel)", &items)?;
1782+
let selection = match ui.select_with_default(
1783+
"Select a worktree to rename (ESC to cancel)",
1784+
&items,
1785+
DEFAULT_MENU_SELECTION,
1786+
) {
1787+
Ok(selection) => selection,
1788+
Err(_) => return Ok(()),
1789+
};
17451790

17461791
let worktree = renameable_worktrees[selection];
17471792

17481793
// Get new name
17491794
println!();
1750-
let new_name = ui
1751-
.input(&format!("New name for '{}' (ESC to cancel)", worktree.name))?
1752-
.trim()
1753-
.to_string();
1795+
let new_name = match ui.input(&format!("New name for '{}' (ESC to cancel)", worktree.name)) {
1796+
Ok(name) => name.trim().to_string(),
1797+
Err(_) => return Ok(()),
1798+
};
17541799

17551800
if new_name.is_empty() {
17561801
utils::print_error("Name cannot be empty");
@@ -1774,13 +1819,16 @@ pub fn rename_worktree_with_ui(manager: &GitWorktreeManager, ui: &dyn UserInterf
17741819
}
17751820

17761821
// Check if the worktree has a branch that could be renamed
1777-
let rename_branch = if worktree.branch != "detached"
1778-
&& worktree.branch != "unknown"
1822+
let rename_branch = if worktree.branch != DEFAULT_BRANCH_DETACHED
1823+
&& worktree.branch != DEFAULT_BRANCH_UNKNOWN
17791824
&& (worktree.branch == worktree.name
17801825
|| worktree.branch == format!("feature/{}", worktree.name))
17811826
{
17821827
println!();
1783-
ui.confirm_with_default("Also rename the associated branch?", true)?
1828+
match ui.confirm_with_default("Also rename the associated branch?", true) {
1829+
Ok(confirm) => confirm,
1830+
Err(_) => return Ok(()),
1831+
}
17841832
} else {
17851833
false
17861834
};
@@ -1813,7 +1861,10 @@ pub fn rename_worktree_with_ui(manager: &GitWorktreeManager, ui: &dyn UserInterf
18131861
}
18141862

18151863
println!();
1816-
let confirm = ui.confirm_with_default("Proceed with rename?", false)?;
1864+
let confirm = match ui.confirm_with_default("Proceed with rename?", false) {
1865+
Ok(confirm) => confirm,
1866+
Err(_) => return Ok(()),
1867+
};
18171868

18181869
if !confirm {
18191870
return Ok(());
@@ -2505,13 +2556,13 @@ copy = [
25052556
}
25062557

25072558
// Get the user's preferred editor
2508-
let editor = std::env::var("EDITOR")
2509-
.or_else(|_| std::env::var("VISUAL"))
2559+
let editor = std::env::var(ENV_EDITOR)
2560+
.or_else(|_| std::env::var(ENV_VISUAL))
25102561
.unwrap_or_else(|_| {
25112562
if cfg!(target_os = "windows") {
2512-
"notepad".to_string()
2563+
DEFAULT_EDITOR_WINDOWS.to_string()
25132564
} else {
2514-
"vi".to_string()
2565+
DEFAULT_EDITOR_UNIX.to_string()
25152566
}
25162567
});
25172568

0 commit comments

Comments
 (0)