diff --git a/shellhist/core/search.py b/shellhist/core/search.py new file mode 100644 index 0000000..1b1b43d --- /dev/null +++ b/shellhist/core/search.py @@ -0,0 +1,96 @@ +"""Search functionality for shell history.""" + +from datetime import datetime +from typing import Optional + +from fuzzywuzzy import fuzz, process + +from shellhist.core import HistoryEntry, HistoryStore + + +def fuzzy_search( + store: HistoryStore, + query: str, + threshold: int = 70, + limit: int = 20, + reverse: bool = False, + recent: bool = False, +) -> list[tuple[HistoryEntry, int]]: + """Search history with fuzzy matching. + + Args: + store: HistoryStore to search. + query: Search query string. + threshold: Minimum similarity score (0-100). + limit: Maximum number of results to return. + reverse: If True, sort by recency (newest first). + recent: If True, boost scores for recent commands. + + Returns: + List of (HistoryEntry, score) tuples sorted by score. + """ + commands = store.get_unique_commands() + + if not commands: + return [] + + results = [] + now = datetime.now() + + for command in commands: + score = fuzz.token_sort_ratio(query.lower(), command.lower()) + + if score >= threshold: + entry = _get_first_entry(store, command) + if entry: + if recent and entry.timestamp: + hours_old = (now - entry.timestamp).total_seconds() / 3600 + if hours_old < 24: + score = min(100, score + 10) + + results.append((entry, score)) + + results.sort(key=lambda x: x[1], reverse=not reverse) + + return results[:limit] + + +def _get_first_entry(store: HistoryStore, command: str) -> Optional[HistoryEntry]: + """Get the first entry for a command from the store.""" + for entry in store.entries: + if entry.command == command: + return entry + return None + + +def rank_by_frequency( + store: HistoryStore, + results: list[tuple[HistoryEntry, int]], + boost_recent: bool = False, +) -> list[tuple[HistoryEntry, int, int]]: + """Rank search results by frequency. + + Args: + store: HistoryStore for frequency lookups. + results: List of (HistoryEntry, score) tuples. + boost_recent: If True, boost scores for recent commands. + + Returns: + List of (HistoryEntry, score, frequency) tuples. + """ + ranked = [] + now = datetime.now() + + for entry, score in results: + freq = store.get_frequency(entry.command) + + if boost_recent and entry.timestamp: + hours_old = (now - entry.timestamp).total_seconds() / 3600 + if hours_old < 24: + freq = freq * 2 + + ranked.append((entry, score, freq)) + + ranked.sort(key=lambda x: (x[1], x[2]), reverse=True) + + return ranked