diff --git a/icons/mycmd.ico b/icons/mycmd.ico index 428cfee..888a474 100644 Binary files a/icons/mycmd.ico and b/icons/mycmd.ico differ diff --git a/src/main/java/com/mycmd/App.java b/src/main/java/com/mycmd/App.java index bc99d3f..97cf691 100644 --- a/src/main/java/com/mycmd/App.java +++ b/src/main/java/com/mycmd/App.java @@ -16,6 +16,7 @@ public static void main(String[] args) { System.out.println("(c) 2025 MyCMD Organization. All rights reserved."); Scanner sc = new Scanner(System.in); + context.setScanner(sc); // ← Added this line while (true) { System.out.print(context.getCurrentDir().getAbsolutePath() + ">"); @@ -153,6 +154,8 @@ private CommandNames() {} private static final String WHOAMI = "whoami"; private static final String WMIC = "wmic"; private static final String XCOPY = "xcopy"; + private static final String SEARCHHISTORY = "searchhistory"; + private static final String ISEARCH = "isearch"; } private static void registerCommands(Map commands) { @@ -230,5 +233,7 @@ private static void registerCommands(Map commands) { commands.put(CommandNames.WHOAMI, new WhoamiCommand()); commands.put(CommandNames.WMIC, new WmicCommand()); commands.put(CommandNames.XCOPY, new XcopyCommand()); + commands.put(CommandNames.SEARCHHISTORY, new SearchHistoryCommand()); + commands.put(CommandNames.ISEARCH, new InteractiveSearchCommand()); } } diff --git a/src/main/java/com/mycmd/ShellContext.java b/src/main/java/com/mycmd/ShellContext.java index 502e59c..8816f41 100644 --- a/src/main/java/com/mycmd/ShellContext.java +++ b/src/main/java/com/mycmd/ShellContext.java @@ -23,15 +23,47 @@ public class ShellContext { private final Map envVars = new HashMap<>(); + private Scanner scanner; + public ShellContext() { this.currentDir = new File(System.getProperty("user.dir")); this.history = new ArrayList<>(); this.aliases = new HashMap<>(); this.commandHistory = new ArrayList<>(); this.startTime = Instant.now(); + this.scanner = null; // Will be set by App.java loadAliases(); } + // ==================== Scanner Management ==================== + + /** + * Set the shared Scanner instance for all commands to use. + * Should only be called once by App.java during initialization. + */ + public void setScanner(Scanner scanner) { + if (this.scanner != null) { + throw new IllegalStateException("Scanner already initialized"); + } + if (scanner == null) { + throw new IllegalArgumentException("Scanner cannot be null"); + } + this.scanner = scanner; + } + + /** + * Get the shared Scanner instance. + * All commands should use this instead of creating their own Scanner. + * @return the shared Scanner instance + * @throws IllegalStateException if Scanner hasn't been initialized + */ + public Scanner getScanner() { + if (scanner == null) { + throw new IllegalStateException("Scanner not initialized in ShellContext"); + } + return scanner; + } + public void addToHistory(String command) { history.add(command); commandHistory.add(command); diff --git a/src/main/java/com/mycmd/commands/InteractiveSearchCommand.java b/src/main/java/com/mycmd/commands/InteractiveSearchCommand.java new file mode 100644 index 0000000..fd69622 --- /dev/null +++ b/src/main/java/com/mycmd/commands/InteractiveSearchCommand.java @@ -0,0 +1,127 @@ +package com.mycmd.commands; + +import com.mycmd.Command; +import com.mycmd.ShellContext; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +/** + * Interactive history search command (similar to Ctrl+R in bash). + * Usage: isearch + * + * Provides an interactive prompt where users can: + * - Type to filter history in real-time + * - Navigate through matches with numbers + * - Execute a selected command + */ +public class InteractiveSearchCommand implements Command { + + @Override + public void execute(String[] args, ShellContext context) { + List history = context.getHistory(); + + if (history.isEmpty()) { + System.out.println("No command history available."); + return; + } + + // ✅ Use shared scanner from ShellContext instead of creating a new one + Scanner scanner = context.getScanner(); + + System.out.println("=== Interactive History Search ==="); + System.out.println("Type to search, 'q' to quit"); + System.out.println(); + + while (true) { + System.out.print("search> "); + String searchTerm = scanner.nextLine().trim(); + + if (searchTerm.equalsIgnoreCase("q") || searchTerm.equalsIgnoreCase("quit")) { + System.out.println("Search cancelled."); + break; + } + + if (searchTerm.isEmpty()) { + System.out.println("Enter a search term or 'q' to quit."); + continue; + } + + // Search and display results + List matches = searchHistory(history, searchTerm); + + if (matches.isEmpty()) { + System.out.println("No matches found for: '" + searchTerm + "'"); + System.out.println(); + continue; + } + + // Display matches with numbers + System.out.println("\nMatches for '" + searchTerm + "':"); + for (int i = 0; i < Math.min(matches.size(), 10); i++) { + System.out.println(" " + (i + 1) + ". " + matches.get(i)); + } + + if (matches.size() > 10) { + System.out.println(" ... and " + (matches.size() - 10) + " more"); + } + + // Ask user to select or refine + System.out.println(); + System.out.print("Select number to copy (1-" + Math.min(matches.size(), 10) + + "), or press Enter to search again: "); + String selection = scanner.nextLine().trim(); + + if (selection.isEmpty()) { + continue; + } + + try { + int index = Integer.parseInt(selection) - 1; + if (index >= 0 && index < Math.min(matches.size(), 10)) { + String selectedCommand = matches.get(index); + System.out.println("\nSelected command: " + selectedCommand); + System.out.println("(You can now run this command by typing it at the prompt)"); + break; + } else { + System.out.println("Invalid selection. Try again."); + } + } catch (NumberFormatException e) { + System.out.println("Invalid input. Enter a number or press Enter."); + } + + System.out.println(); + } + } + + /** + * Search history for commands containing the search term. + * Returns matches in reverse order (most recent first). + */ + private List searchHistory(List history, String searchTerm) { + String lowerSearch = searchTerm.toLowerCase(); + + // Search in reverse order (most recent first) + return history.stream() + .filter(cmd -> cmd.toLowerCase().contains(lowerSearch)) + .collect(Collectors.collectingAndThen( + Collectors.toList(), + list -> { + java.util.Collections.reverse(list); + return list; + } + )); + } + + @Override + public String description() { + return "Interactive history search (like Ctrl+R)"; + } + + @Override + public String usage() { + return "isearch\n" + + " Opens an interactive prompt to search command history.\n" + + " Type your search term and select from matching commands."; + } +} diff --git a/src/main/java/com/mycmd/commands/SearchHistoryCommand.java b/src/main/java/com/mycmd/commands/SearchHistoryCommand.java new file mode 100644 index 0000000..e0542e6 --- /dev/null +++ b/src/main/java/com/mycmd/commands/SearchHistoryCommand.java @@ -0,0 +1,114 @@ +package com.mycmd.commands; + +import com.mycmd.Command; +import com.mycmd.ShellContext; +import java.util.List; + +/** + * Command to search through command history. + * Usage: searchhistory [search_term] + * + * If no search term is provided, shows all history. + * If search term is provided, filters history to matching commands. + */ +public class SearchHistoryCommand implements Command { + + @Override + public void execute(String[] args, ShellContext context) { + List history = context.getHistory(); + + if (history.isEmpty()) { + System.out.println("No command history available."); + return; + } + + // If no search term provided, show all history + if (args.length == 0) { + System.out.println("Command History (use 'searchhistory ' to filter):"); + displayHistory(history, null); + return; + } + + // Join all args as the search term (supports multi-word searches) + String searchTerm = String.join(" ", args).toLowerCase(); + + System.out.println("Searching history for: '" + searchTerm + "'"); + System.out.println(); + + // Filter history + List matches = history.stream() + .filter(cmd -> cmd.toLowerCase().contains(searchTerm)) + .toList(); + + if (matches.isEmpty()) { + System.out.println("No matching commands found."); + System.out.println("Tip: Search is case-insensitive and matches partial text."); + } else { + displayHistory(matches, searchTerm); + System.out.println(); + System.out.println("Found " + matches.size() + " matching command(s)."); + } + } + + /** + * Display history entries with line numbers. + * Optionally highlights the search term if provided. + */ + private void displayHistory(List commands, String searchTerm) { + int maxDigits = String.valueOf(commands.size()).length(); + + for (int i = 0; i < commands.size(); i++) { + String lineNum = String.format("%" + maxDigits + "d", i + 1); + String command = commands.get(i); + + // Highlight search term if provided (simple uppercase for visibility) + if (searchTerm != null && !searchTerm.isEmpty()) { + command = highlightTerm(command, searchTerm); + } + + System.out.println(" " + lineNum + " " + command); + } + } + + /** + * Simple highlighting by surrounding the search term with markers. + * For terminal with color support, you could use ANSI codes instead. + */ + private String highlightTerm(String text, String term) { + // Case-insensitive highlight + int index = text.toLowerCase().indexOf(term.toLowerCase()); + if (index == -1) { + return text; + } + + StringBuilder result = new StringBuilder(); + int lastIndex = 0; + + while (index >= 0) { + result.append(text, lastIndex, index); + result.append("["); + result.append(text, index, index + term.length()); + result.append("]"); + + lastIndex = index + term.length(); + index = text.toLowerCase().indexOf(term.toLowerCase(), lastIndex); + } + + result.append(text.substring(lastIndex)); + return result.toString(); + } + + @Override + public String description() { + return "Search through command history"; + } + + @Override + public String usage() { + return "searchhistory [search_term]\n" + + " Examples:\n" + + " searchhistory - Show all history\n" + + " searchhistory dir - Find all 'dir' commands\n" + + " searchhistory cd .. - Find all 'cd ..' commands"; + } +}