diff --git a/app.json b/app.json index 69ff15d..46c73e7 100644 --- a/app.json +++ b/app.json @@ -5,6 +5,8 @@ "eu.maveniverse.maven.mima.runtime:standalone-static": "2.4.15", "eu.maveniverse.maven.mima:context": "2.4.15", "info.picocli:picocli": "4.7.6", + "org.jline:jline-console-ui": "3.26.2", + "org.jline:jline-terminal-jni": "3.26.2", "org.slf4j:slf4j-api": "2.0.13", "org.slf4j:slf4j-log4j12": "2.0.13", "org.slf4j:slf4j-simple": "2.0.13" diff --git a/pom.xml b/pom.xml index a1c9331..ac03def 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ 2.4.15 4.7.6 2.11.0 + 3.26.2 2.0.13 2.43.0 1.22.0 @@ -61,6 +62,16 @@ gson ${version.gson} + + org.jline + jline-console-ui + ${version.jline} + + + org.jline + jline-terminal-jni + ${version.jline} + org.slf4j slf4j-api diff --git a/src/main/java/org/codejive/jpm/Main.java b/src/main/java/org/codejive/jpm/Main.java index 4cc8177..9cef57f 100644 --- a/src/main/java/org/codejive/jpm/Main.java +++ b/src/main/java/org/codejive/jpm/Main.java @@ -2,6 +2,7 @@ //DEPS eu.maveniverse.maven.mima:context:2.4.15 eu.maveniverse.maven.mima.runtime:standalone-static:2.4.15 //DEPS info.picocli:picocli:4.7.6 //DEPS com.google.code.gson:gson:2.11.0 +//DEPS org.jline:jline-console-ui:3.26.2 org.jline:jline-terminal-jni:3.26.2 //DEPS org.slf4j:slf4j-api:2.0.13 org.slf4j:slf4j-simple:2.0.13 //SOURCES Jpm.java json/AppInfo.java util/FileUtils.java util/ResolverUtils.java util/SearchUtils.java //SOURCES util/SearchResult.java util/SyncStats.java util/Version.java @@ -16,6 +17,13 @@ import java.util.stream.Collectors; import org.codejive.jpm.util.SyncStats; import org.codejive.jpm.util.Version; +import org.jline.consoleui.prompt.ConsolePrompt; +import org.jline.consoleui.prompt.ListResult; +import org.jline.consoleui.prompt.PromptResultItemIF; +import org.jline.consoleui.prompt.builder.ListPromptBuilder; +import org.jline.consoleui.prompt.builder.PromptBuilder; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; @@ -74,6 +82,12 @@ static class Sync implements Callable { @Mixin QuietMixin quietMixin; @Mixin CopyMixin copyMixin; + @Option( + names = {"-i", "--interactive"}, + description = "Interactively search and select artifacts to install", + defaultValue = "false") + private boolean interactive; + @Option( names = {"-m", "--max"}, description = "Maximum number of results to return", @@ -82,22 +96,135 @@ static class Sync implements Callable { @Parameters( paramLabel = "artifactPattern", - description = "Partial or full artifact name to search for.") + description = "Partial or full artifact name to search for.", + defaultValue = "") private String artifactPattern; @Override public Integer call() throws Exception { - String[] artifactNames = - Jpm.builder() - .directory(copyMixin.directory) - .noLinks(copyMixin.noLinks) - .build() - .search(artifactPattern, Math.min(max, 200)); - if (artifactNames.length > 0) { - Arrays.stream(artifactNames).forEach(System.out::println); + if (interactive || artifactPattern == null || artifactPattern.isEmpty()) { + try (Terminal terminal = TerminalBuilder.builder().build()) { + while (true) { + ConsolePrompt prompt = new ConsolePrompt(terminal); + if (artifactPattern == null || artifactPattern.isEmpty()) { + artifactPattern = askString(prompt, "Search for:"); + } + String[] artifactNames = search(artifactPattern); + PromptBuilder promptBuilder = prompt.getPromptBuilder(); + addSelectItem(promptBuilder, "Select artifact:", artifactNames); + addSelectArtifactAction(promptBuilder); + Map result = + prompt.prompt(promptBuilder.build()); + String selectedArtifact = getSelectedId(result, "item"); + String artifactAction = getSelectedId(result, "action"); + if ("install".equals(artifactAction)) { + SyncStats stats = + Jpm.builder() + .directory(copyMixin.directory) + .noLinks(copyMixin.noLinks) + .build() + .install(new String[] {selectedArtifact}); + if (!quietMixin.quiet) { + printStats(stats); + } + } else if ("copy".equals(artifactAction)) { + SyncStats stats = + Jpm.builder() + .directory(copyMixin.directory) + .noLinks(copyMixin.noLinks) + .build() + .copy(new String[] {selectedArtifact}, false); + if (!quietMixin.quiet) { + printStats(stats); + } + } else if ("version".equals(artifactAction)) { + artifactPattern = selectedArtifact; + continue; + } else { // quit + break; + } + String finalAction = selectFinalAction(prompt); + if ("quit".equals(finalAction)) { + break; + } + artifactPattern = null; + } + } + } else { + String[] artifactNames = search(artifactPattern); + if (artifactNames.length > 0) { + Arrays.stream(artifactNames).forEach(System.out::println); + } } return 0; } + + String[] search(String artifactPattern) throws IOException { + return Jpm.builder() + .directory(copyMixin.directory) + .noLinks(copyMixin.noLinks) + .build() + .search(artifactPattern, Math.min(max, 200)); + } + + String askString(ConsolePrompt prompt, String message) throws IOException { + PromptBuilder promptBuilder = prompt.getPromptBuilder(); + promptBuilder.createInputPrompt().name("input").message(message).addPrompt(); + Map result = prompt.prompt(promptBuilder.build()); + return result.get("input").getResult(); + } + + void addSelectItem(PromptBuilder promptBuilder, String message, String[] items) + throws IOException { + ListPromptBuilder artifactsList = + promptBuilder.createListPrompt().name("item").message(message).pageSize(10); + for (String artifactName : items) { + artifactsList.newItem(artifactName).text(artifactName).add(); + } + artifactsList.addPrompt(); + } + + void addSelectArtifactAction(PromptBuilder promptBuilder) throws IOException { + promptBuilder + .createListPrompt() + .name("action") + .message("What to do:") + .newItem("install") + .text("Install artifact") + .add() + .newItem("copy") + .text("Copy artifact") + .add() + .newItem("version") + .text("Select different version") + .add() + .newItem("quit") + .text("Quit") + .add() + .addPrompt(); + } + + String selectFinalAction(ConsolePrompt prompt) throws IOException { + PromptBuilder promptBuilder = prompt.getPromptBuilder(); + promptBuilder + .createListPrompt() + .name("action") + .message("What to do:") + .newItem("again") + .text("Search again") + .add() + .newItem("quit") + .text("Quit") + .add() + .addPrompt(); + Map result = prompt.prompt(promptBuilder.build()); + return getSelectedId(result, "action"); + } + + private static String getSelectedId( + Map result, String itemName) { + return ((ListResult) result.get(itemName)).getSelectedId(); + } } @Command( diff --git a/src/main/java/org/codejive/jpm/util/SearchUtils.java b/src/main/java/org/codejive/jpm/util/SearchUtils.java index bd44d59..9e07a93 100644 --- a/src/main/java/org/codejive/jpm/util/SearchUtils.java +++ b/src/main/java/org/codejive/jpm/util/SearchUtils.java @@ -67,7 +67,7 @@ private static SearchResult select(String query, int start, int count) throws IO } String searchUrl = String.format( - "https://search.maven.org/solrsearch/select?start=%d&rows=%d&q=%s", + "https://search.maven.org/solrsearch/select?start=%d&rows=%d&q=p:jar+AND+%s", start, count, URLEncoder.encode(finalQuery, "UTF-8")); if (parts.length >= 3) { searchUrl += "&core=gav"; @@ -90,23 +90,23 @@ private static SearchResult select(String query, int start, int count) throws IO } List artifacts = result.response.docs.stream() - .filter( - d -> - parts.length != 2 - || d.g.contains(parts[0]) - && d.a.contains(parts[1])) - .map( - d -> - new DefaultArtifact( - d.g, - d.a, - "", - d.v != null ? d.v : d.latestVersion)) + .filter(d -> acceptDoc(d, parts)) + .map(SearchUtils::toArtifact) .collect(Collectors.toList()); return new SearchResult(artifacts, query, start, count, result.response.numFound); } } } + + private static boolean acceptDoc(MsrDoc d, String[] parts) { + return d.ec != null + && d.ec.contains(".jar") + && (parts.length != 2 || d.g.contains(parts[0]) && d.a.contains(parts[1])); + } + + private static DefaultArtifact toArtifact(MsrDoc d) { + return new DefaultArtifact(d.g, d.a, "", d.v != null ? d.v : d.latestVersion); + } } class MvnSearchResult { @@ -129,4 +129,6 @@ class MsrDoc { public String a; public String v; public String latestVersion; + public String p; + public List ec; }