001/************************* PROJECT RON *************************/ 002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved. */ 003/* Use of this source code is governed by an MIT-style license */ 004/* that can be found in the repository LICENSE file. */ 005/***************************************************************/ 006package tools.PathplannerSearch; 007 008import java.io.File; 009import java.io.IOException; 010import java.nio.file.Files; 011import java.util.ArrayList; 012import java.util.List; 013import java.util.Map; 014import java.util.regex.Matcher; 015import java.util.regex.Pattern; 016 017import tools.PathplannerSearch.Main.ARGUMENTS; 018import tools.ToolClasses.ArgumentedTool; 019 020import static tools.util.AnsiColors.*; 021 022/** 023 * 024 * 025 * <h2>Utility class that gives search features that Pathplanner lacks</h2> 026 * 027 * <p> 028 * This class is meant to help search for where paths and linked variables are 029 * used so you can 030 * clean up unused ones. 031 */ 032public final class PathplannerSearch extends ArgumentedTool<ARGUMENTS> { 033 public enum SearchType { 034 LINKED_WAYPOINT, 035 PATH 036 } 037 038 public PathplannerSearch() { 039 super(Main.ARGUMENTS.class); 040 } 041 042 /** 043 * 044 * 045 * <h4>Functionality</h4> 046 * 047 * <p> 048 * Searches through .path files in <code>src/main/deploy/pathplanner</code> and 049 * logs files that 050 * match a certain search term based on the search type. 051 * 052 * @param parsedArgs the parsed arguments 053 */ 054 @Override 055 protected void execute(Map<ARGUMENTS, Object> parsedArgs) { 056 String searchTerm = ((String) parsedArgs.get(ARGUMENTS.SEARCH_TERM)).toLowerCase(); 057 SearchType searchType = ((SearchType) parsedArgs.get(ARGUMENTS.SEARCH_TYPE)); 058 059 switch (searchType) { 060 case LINKED_WAYPOINT -> { 061 final File[] files = new File("./src/main/deploy/pathplanner/paths/").listFiles(); 062 if (files == null) 063 break; 064 065 final List<String> matches = new ArrayList<>(); 066 for (final File file : files) { 067 try { 068 final String content = Files.readString(file.toPath()).toLowerCase(); 069 if (content.isBlank()) 070 continue; 071 if (content.contains("\"linkedname\"") && content.contains("\"" + searchTerm + "\"")) { 072 final String folderName = parseFolderKeyFromFileContent(content); 073 matches.add(folderName + "/" + file.getName()); 074 } 075 } catch (IOException e) { 076 System.out.println("Error reading file: " + file.getName()); 077 e.printStackTrace(); 078 } 079 } 080 081 logFiles(searchTerm, searchType, matches); 082 } 083 case PATH -> { 084 final File[] files = new File("./src/main/deploy/pathplanner/autos/").listFiles(); 085 if (files == null) 086 break; 087 088 final List<String> matches = new ArrayList<>(); 089 for (final File file : files) { 090 try { 091 final String content = Files.readString(file.toPath()).toLowerCase(); 092 if (!content.contains("\"sequential\"")) 093 continue; 094 if (content.contains("\"path\"") 095 && content.contains("\"pathname\"") 096 && content.contains("\"" + searchTerm + "\"")) { 097 098 final String folderName = parseFolderKeyFromFileContent(content); 099 matches.add(folderName + "/" + file.getName()); 100 } 101 } catch (IOException e) { 102 System.out.println("Error reading file: " + file.getName()); 103 e.printStackTrace(); 104 } 105 } 106 107 logFiles(searchTerm, searchType, matches); 108 } 109 default -> { 110 System.out.println("Specify a valid search type by editing the file."); 111 System.exit(1); 112 } 113 } 114 } 115 116 /** 117 * Parses the "folder" key from the content of a .path file. 118 * 119 * <p> 120 * As Java doesn't have a built in JSON parser and for only having to do this 121 * once, it would be 122 * overkill to add a dependency. 123 * 124 * <p> 125 * This method uses regex to parse the "folder" key from the .path file. 126 * <code>.path</code> 127 * files are essentially just JSON files but with a different extension. 128 * 129 * @param content the content of the .path file 130 * @return the value of the "folder" key in the .path file, or an empty string 131 * if it doesn't exist 132 */ 133 public static String parseFolderKeyFromFileContent(String content) { 134 final String folderName; 135 Pattern regex = Pattern.compile( 136 "\"folder\"\\s*:\\s*\"([^\"]*)\"", 137 Pattern.CASE_INSENSITIVE); // ima be honest i used ai for this regex 🤤 138 Matcher match = regex.matcher(content); 139 if (match.find()) { 140 folderName = match.group(1); 141 } else { 142 folderName = ""; 143 } 144 145 return folderName; 146 } 147 148 /** 149 * Logs the result of the search to the console with some formatting. 150 * 151 * <p> 152 * Uses ANSI color codes for coloring the output. 153 * 154 * <p> 155 * If no matches are found, it will log "None were found." in red. 156 * 157 * <p> 158 * Otherwise, it will log the list of matches in green on separate lines. 159 * 160 * <p> 161 * <b>Example input and output:</b> 162 * 163 * <pre> 164 * ./gradlew runPathPlannersearch -Pargs="disrupt path" 165 * </pre> 166 * 167 * <pre> 168 * Autons that use the path 'disrupt': 169 * disrupt/LB Disrupt.auto 170 * disrupt/LT Disrupt.auto 171 * disrupt/RB Disrupt.auto 172 * disrupt/RT Disrupt.auto 173 * </pre> 174 * 175 * @param searchTerm 176 * @param searchType 177 * @param matches 178 */ 179 public static void logFiles(String searchTerm, SearchType searchType, List<String> matches) { 180 String stem = switch (searchType) { 181 case LINKED_WAYPOINT -> "Paths that use the linked waypoint '"; 182 case PATH -> "Autons that use the path '"; 183 default -> ""; 184 }; 185 186 System.out.println(stem + YELLOW + searchTerm + "'" + RESET + ":"); 187 if (matches.isEmpty()) { 188 System.out.println(RED + "None were found." + RESET); 189 return; 190 } 191 192 for (String match : matches) { 193 System.out.println(GREEN + match + RESET); 194 } 195 } 196}