@@ -1412,21 +1412,167 @@ async def hm_show(
14121412 Plain text with option details (name, type, description) or error with suggestions
14131413 """
14141414 try :
1415- # Search more broadly first
1415+ # First try the basic search to check if option exists
14161416 options = parse_html_options (HOME_MANAGER_URL , name , "" , 100 )
14171417
1418- # Look for exact match
1418+ # Check for exact match in parsed options
1419+ exact_match = None
14191420 for opt in options :
14201421 if opt ["name" ] == name :
1421- info = []
1422- info .append (f"Option: { name } " )
1423- if opt ["type" ]:
1424- info .append (f"Type: { opt ['type' ]} " )
1425- if opt ["description" ]:
1426- info .append (f"Description: { opt ['description' ]} " )
1427- return "\n " .join (info )
1422+ exact_match = opt
1423+ break
14281424
1429- # If not found, check if there are similar options to suggest
1425+ # If found, try enhanced parsing for more details
1426+ if exact_match :
1427+ # Try to get more details from full HTML
1428+ try :
1429+ resp = requests .get (HOME_MANAGER_URL , timeout = 30 )
1430+ resp .raise_for_status ()
1431+ soup = BeautifulSoup (resp .text , "html.parser" )
1432+
1433+ # Find the specific option by its anchor ID
1434+ anchor_id = f"opt-{ name .replace ('<name>' , '_name_' )} "
1435+ anchor = soup .find ("a" , id = anchor_id )
1436+
1437+ if anchor :
1438+ # Found the anchor, get its parent dt and next dd
1439+ dt = anchor .find_parent ("dt" )
1440+ if dt :
1441+ dd = dt .find_next_sibling ("dd" )
1442+ if dd and hasattr (dd , "find_all" ): # Ensure dd is a Tag, not NavigableString
1443+ info = []
1444+ info .append (f"Option: { name } " )
1445+
1446+ # Extract all available information
1447+ # First, clean up any HTML tags from content
1448+ for tag in dd .find_all ("span" , class_ = "filename" ):
1449+ tag .decompose () # Remove file references
1450+ for tag in dd .find_all ("a" , class_ = "filename" ):
1451+ tag .decompose () # Remove file references
1452+
1453+ content = dd .get_text ("\n " , strip = True )
1454+ lines = content .split ("\n " )
1455+
1456+ # Parse structured information
1457+ current_section = None
1458+ type_info = ""
1459+ default_value = ""
1460+ example_value = ""
1461+ description_lines = []
1462+
1463+ for i , line in enumerate (lines ):
1464+ line_stripped = line .strip ()
1465+ if line_stripped .startswith ("Type:" ):
1466+ type_info = line_stripped [5 :].strip ()
1467+ current_section = "type"
1468+ elif line_stripped .startswith ("Default:" ):
1469+ default_value = line_stripped [8 :].strip ()
1470+ current_section = "default"
1471+ # If empty, capture next non-empty line preserving some formatting
1472+ if not default_value and i + 1 < len (lines ):
1473+ for j in range (i + 1 , len (lines )):
1474+ next_line = lines [j ].strip ()
1475+ if next_line and not any (
1476+ next_line .startswith (p )
1477+ for p in ["Type:" , "Default:" , "Example:" , "Declared" ]
1478+ ):
1479+ default_value = next_line
1480+ break
1481+ elif any (
1482+ next_line .startswith (p )
1483+ for p in ["Type:" , "Default:" , "Example:" , "Declared" ]
1484+ ):
1485+ break
1486+ elif line_stripped .startswith ("Example:" ):
1487+ example_value = line_stripped [8 :].strip ()
1488+ current_section = "example"
1489+ # If empty, capture next non-empty line preserving some formatting
1490+ if not example_value and i + 1 < len (lines ):
1491+ for j in range (i + 1 , len (lines )):
1492+ next_line = lines [j ].strip ()
1493+ if next_line and not any (
1494+ next_line .startswith (p )
1495+ for p in ["Type:" , "Default:" , "Example:" , "Declared" ]
1496+ ):
1497+ example_value = next_line
1498+ break
1499+ elif any (
1500+ next_line .startswith (p )
1501+ for p in ["Type:" , "Default:" , "Example:" , "Declared" ]
1502+ ):
1503+ break
1504+ elif line_stripped .startswith ("Declared" ):
1505+ current_section = None # Stop capturing
1506+ elif line_stripped and not any (
1507+ line_stripped .startswith (p ) for p in ["Type:" , "Default:" , "Example:" ]
1508+ ):
1509+ # Handle multiline values - but only continue if we already started capturing
1510+ # Skip if we just captured this line as the initial value
1511+ if (
1512+ current_section == "default"
1513+ and default_value
1514+ and not default_value .endswith (line_stripped )
1515+ ):
1516+ # Only add if it looks like a continuation (e.g., for multi-line JSON)
1517+ if not default_value .endswith ("}" ) and (
1518+ line_stripped .startswith ("{" )
1519+ or line_stripped .startswith ("}" )
1520+ or ":" in line_stripped
1521+ ):
1522+ default_value += " " + line_stripped
1523+ elif (
1524+ current_section == "example"
1525+ and example_value
1526+ and not example_value .endswith (line_stripped )
1527+ ):
1528+ # Only add if it looks like a continuation
1529+ if not example_value .endswith ("}" ) and (
1530+ line_stripped .startswith ("{" )
1531+ or line_stripped .startswith ("}" )
1532+ or ":" in line_stripped
1533+ or "=" in line_stripped
1534+ ):
1535+ example_value += " " + line_stripped
1536+ elif current_section is None or current_section == "description" :
1537+ description_lines .append (line_stripped )
1538+ current_section = "description"
1539+
1540+ # Build formatted output
1541+ if type_info :
1542+ info .append (f"Type: { type_info } " )
1543+
1544+ if description_lines :
1545+ desc = " " .join (description_lines [:3 ]) # First few lines
1546+ # Remove any XML-like tags (except allowed ones)
1547+ import re
1548+
1549+ desc = re .sub (r"<(?!(?:command|package|tool)>)[^>]+>" , "" , desc )
1550+ if len (desc ) > 200 :
1551+ desc = desc [:197 ] + "..."
1552+ info .append (f"Description: { desc } " )
1553+
1554+ if default_value and default_value != "null" :
1555+ info .append (f"Default: { default_value } " )
1556+
1557+ if example_value :
1558+ info .append (f"Example: { example_value } " )
1559+
1560+ return "\n " .join (info )
1561+ except Exception :
1562+ # If enhanced parsing fails, fall through to basic parsing
1563+ pass
1564+
1565+ # If not found by exact match, still show the basic info
1566+ if exact_match :
1567+ info = []
1568+ info .append (f"Option: { name } " )
1569+ if exact_match .get ("type" ):
1570+ info .append (f"Type: { exact_match ['type' ]} " )
1571+ if exact_match .get ("description" ):
1572+ info .append (f"Description: { exact_match ['description' ]} " )
1573+ return "\n " .join (info )
1574+
1575+ # If still not found, check if there are similar options to suggest
14301576 if options :
14311577 suggestions = []
14321578 for opt in options [:5 ]: # Show up to 5 suggestions
@@ -1688,15 +1834,44 @@ async def darwin_search(
16881834 )
16891835
16901836 try :
1691- options = parse_html_options (DARWIN_URL , query , "" , limit )
1837+ # Fetch more results to allow for better sorting
1838+ raw_options = parse_html_options (DARWIN_URL , query , "" , limit * 3 )
16921839
1693- if not options :
1840+ if not raw_options :
16941841 return f"No nix-darwin options found matching '{ query } '"
16951842
1843+ # Sort by relevance for macOS-specific queries
1844+ query_lower = query .lower ()
1845+
1846+ def relevance_score (opt : dict [str , str ]) -> tuple [int , str ]:
1847+ """Score options by relevance, especially for macOS system settings."""
1848+ name = opt ["name" ].lower ()
1849+ score = 0
1850+
1851+ # Exact word match in option path gets highest priority
1852+ parts = name .split ("." )
1853+ if query_lower in parts :
1854+ score += 100
1855+
1856+ # Prioritize system.defaults for macOS settings
1857+ if query_lower == "dock" and name .startswith ("system.defaults.dock" ):
1858+ score += 50
1859+ elif name .startswith ("system.defaults." ) and query_lower in name :
1860+ score += 30
1861+
1862+ # Lower score for partial matches in unrelated contexts
1863+ if query_lower in name :
1864+ score += 10
1865+
1866+ return (- score , name ) # Negative for descending sort
1867+
1868+ # Sort and limit results
1869+ sorted_options = sorted (raw_options , key = relevance_score )[:limit ]
1870+
16961871 results = []
1697- results .append (f"Found { len (options )} nix-darwin options matching '{ query } ':\n " )
1872+ results .append (f"Found { len (sorted_options )} nix-darwin options matching '{ query } ':\n " )
16981873
1699- for opt in options :
1874+ for opt in sorted_options :
17001875 results .append (f"• { opt ['name' ]} " )
17011876 if opt ["type" ]:
17021877 results .append (f" Type: { opt ['type' ]} " )
@@ -1707,8 +1882,8 @@ async def darwin_search(
17071882 # Add helpful next steps
17081883 results .append ("NEXT STEPS:" )
17091884 results .append ("━" * 11 )
1710- if len (options ) > 0 :
1711- first_opt = options [0 ]["name" ]
1885+ if len (sorted_options ) > 0 :
1886+ first_opt = sorted_options [0 ]["name" ]
17121887 results .append (f'• Use darwin_show(name="{ first_opt } ") for full details' )
17131888 # Extract prefix for browsing
17141889 prefix = first_opt .split ("." )[0 ] if "." in first_opt else first_opt
0 commit comments