1010import java .nio .file .Files ;
1111import java .nio .file .Path ;
1212import java .util .ArrayList ;
13+ import java .util .HashSet ;
1314import java .util .List ;
15+ import java .util .Set ;
1416import java .util .logging .Logger ;
1517import java .util .stream .Collectors ;
1618import java .util .stream .StreamSupport ;
1719
20+ import org .dflib .jjava .jupyter .kernel .display .Renderer ;
21+ import org .dflib .jjava .jupyter .kernel .display .mime .MIMEType ;
22+
1823public class JBangHelper {
1924
2025 final static Logger logger = Logger .getLogger (JBangHelper .class .getName ());
@@ -24,8 +29,8 @@ public class JBangHelper {
2429 *
2530 * Note: on windows it will look for jbang.cmd not jbang which is a shell script.
2631 *
27- * 1. $JBANG_HOME /bin/jbang
28- * 2. jbang in $PATH
32+ * 1. /bin/jbang
33+ * 2. jbang in
2934 * 3. ~/.jbang/bin/jbang
3035 *
3136 * If none of the above are found, it will return null.
@@ -37,7 +42,7 @@ public static String findJBangExecutable() throws IOException {
3742 boolean isWindows = System .getProperty ("os.name" ).toLowerCase ().contains ("windows" );
3843 String jbangExecutable = isWindows ? "jbang.cmd" : "jbang" ;
3944
40- // 1. $JBANG_HOME /bin/jbang
45+ // 1. /bin/jbang
4146 String jbangHome = System .getenv ("JBANG_HOME" );
4247 if (jbangHome != null ) {
4348 var homePath = Path .of (jbangHome ).resolve ("bin/" + jbangExecutable );
@@ -46,7 +51,7 @@ public static String findJBangExecutable() throws IOException {
4651 }
4752 }
4853
49- // 2. jbang in $PATH
54+ // 2. jbang in
5055 String pathEnv = System .getenv ("PATH" );
5156 if (pathEnv != null ) {
5257 for (String dir : pathEnv .split (File .pathSeparator )) {
@@ -67,7 +72,143 @@ public static String findJBangExecutable() throws IOException {
6772 return null ;
6873 }
6974
70- public static List <String > getJBangResolvedDependencies (String scriptRef , String body , boolean inclAppJar ) throws IOException {
75+ public static class JBangInfo {
76+ List <String > resolvedDependencies ;
77+ List <String > dependencies ;
78+ List <String > newResolvedDependencies ;
79+
80+ public JBangInfo (List <String > resolvedDependencies , List <String > dependencies ) {
81+ this .resolvedDependencies = resolvedDependencies ;
82+ this .dependencies = dependencies ;
83+ }
84+
85+ public void resolve (Set <String > existingClasspath ) {
86+ this .newResolvedDependencies = getNewResolvedDependencies (existingClasspath );
87+ }
88+
89+ public List <String > getNewResolvedDependencies (Set <String > existingClasspath ) {
90+ return resolvedDependencies .stream ()
91+ .filter (dependency -> !existingClasspath .contains (dependency ))
92+ .collect (Collectors .toList ());
93+ }
94+
95+ public List <String > getDependencies () {
96+ return dependencies ;
97+ }
98+ public List <String > getResolvedDependencies () {
99+ return resolvedDependencies ;
100+ }
101+ public List <String > getNewResolvedDependencies () {
102+ return newResolvedDependencies ;
103+ }
104+
105+ public String toString () {
106+ StringBuilder sb = new StringBuilder ("JBangInfo{" );
107+ sb .append ("dependencies=" ).append (dependencies .size ());
108+ sb .append (",resolvedDependencies=" ).append (resolvedDependencies .size ());
109+ if (newResolvedDependencies != null ) {
110+ sb .append (",newResolvedDependencies=" ).append (newResolvedDependencies .size ());
111+ }
112+ sb .append ('}' );
113+ return sb .toString ();
114+ }
115+
116+ public static void registerAllRenderers (Renderer renderer ) {
117+ renderer
118+ .createRegistration (dev .jbang .jupyter .JBangHelper .JBangInfo .class )
119+ .preferring (MIMEType .TEXT_HTML )
120+ .register ((info , ctx ) -> {
121+ ctx .renderIfRequested (MIMEType .TEXT_HTML , () -> {
122+ StringBuilder html = new StringBuilder ();
123+
124+ // Create set of new dependencies for quick lookup
125+ Set <String > newDepsSet = new HashSet <>();
126+ if (info .getNewResolvedDependencies () != null ) {
127+ newDepsSet .addAll (info .getNewResolvedDependencies ());
128+ }
129+
130+ // Summary section
131+ int totalRequested = (info .getDependencies () != null ) ? info .getDependencies ().size () : 0 ;
132+ int totalResolved = (info .getResolvedDependencies () != null ) ? info .getResolvedDependencies ().size () : 0 ;
133+ int totalNew = (info .getNewResolvedDependencies () != null ) ? info .getNewResolvedDependencies ().size () : 0 ;
134+
135+ // Styles
136+ html .append ("<style>\n " );
137+ html .append (".jbang-compact { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-size: 12px; margin: 4px 0; }\n " );
138+ html .append (".jbang-inline { display: inline-block; margin: 0; padding: 0; vertical-align: top; }\n " );
139+ html .append (".jbang-inline summary { display: inline; cursor: pointer; user-select: none; padding: 2px 6px; border-radius: 3px; transition: background 0.15s; }\n " );
140+ html .append (".jbang-inline summary:hover { background: #f1f3f5; }\n " );
141+ html .append (".jbang-inline-content { margin-top: 6px; padding: 8px; background: #f8f9fa; border-radius: 4px; border-left: 2px solid #667eea; }\n " );
142+ html .append (".jbang-inline-new .jbang-inline-content { border-left-color: #10b981; }\n " );
143+ html .append (".jbang-inline-coord .jbang-inline-content { border-left-color: #764ba2; }\n " );
144+ html .append (".jbang-list { list-style: none; padding: 0; margin: 0; font-size: 11px; }\n " );
145+ html .append (".jbang-list-item { padding: 4px 6px; margin: 2px 0; background: white; border-radius: 2px; border-left: 2px solid #667eea; font-family: 'Monaco', 'Menlo', monospace; color: #2d3748; word-break: break-all; cursor: help; }\n " );
146+ html .append (".jbang-list-item:hover { background: #f8f9fa; }\n " );
147+ html .append (".jbang-list-item-new { border-left-color: #10b981; }\n " );
148+ html .append (".jbang-list-item-old { opacity: 0.4; border-left-color: #adb5bd; }\n " );
149+ html .append (".jbang-list-item-old:hover { opacity: 0.6; }\n " );
150+ html .append (".jbang-list-item-coord { border-left-color: #764ba2; cursor: default; }\n " );
151+ html .append ("</style>\n " );
152+
153+ if (totalRequested > 0 || totalResolved > 0 ) {
154+ html .append ("<div class='jbang-compact'>\n " );
155+ html .append (" 🚀 JBang: " );
156+
157+ // Requested dependencies - always show, inline expandable if > 0
158+ if (info .getDependencies () != null && !info .getDependencies ().isEmpty ()) {
159+ html .append ("<details class='jbang-inline jbang-inline-coord'><summary title='Requested Maven dependencies (click to expand)'>📦 <strong>" ).append (totalRequested ).append ("</strong></summary>" );
160+ html .append ("<div class='jbang-inline-content'><ul class='jbang-list'>" );
161+ for (String dep : info .getDependencies ()) {
162+ html .append ("<li class='jbang-list-item jbang-list-item-coord'>" ).append (dep ).append ("</li>" );
163+ }
164+ html .append ("</ul></div></details>" );
165+ } else {
166+ html .append ("<span title='Requested Maven dependencies'>📦 <strong>" ).append (totalRequested ).append ("</strong></span>" );
167+ }
168+
169+ html .append (" | " );
170+
171+ // New dependencies - always show, inline expandable if > 0
172+ if (info .getNewResolvedDependencies () != null && !info .getNewResolvedDependencies ().isEmpty ()) {
173+ html .append ("<details class='jbang-inline jbang-inline-new'><summary title='Newly added to classpath (click to expand)'>✨ <strong>" ).append (totalNew ).append ("</strong></summary>" );
174+ html .append ("<div class='jbang-inline-content'><ul class='jbang-list'>" );
175+ for (String dep : info .getNewResolvedDependencies ()) {
176+ String fileName = dep .substring (dep .lastIndexOf ('/' ) + 1 );
177+ html .append ("<li class='jbang-list-item jbang-list-item-new' title='" ).append (dep ).append ("'>" ).append (fileName ).append ("</li>" );
178+ }
179+ html .append ("</ul></div></details>" );
180+ } else {
181+ html .append ("<span title='Newly added to classpath'>✨ <strong>" ).append (totalNew ).append ("</strong></span>" );
182+ }
183+
184+ html .append (" | " );
185+
186+ // All resolved dependencies - always show, inline expandable if > 0
187+ if (info .getResolvedDependencies () != null && !info .getResolvedDependencies ().isEmpty ()) {
188+ html .append ("<details class='jbang-inline'><summary title='All resolved JARs (click to expand, grayed = cached)'>📚 <strong>" ).append (totalResolved ).append ("</strong></summary>" );
189+ html .append ("<div class='jbang-inline-content'><ul class='jbang-list'>" );
190+ for (String dep : info .getResolvedDependencies ()) {
191+ String fileName = dep .substring (dep .lastIndexOf ('/' ) + 1 );
192+ boolean isNew = newDepsSet .contains (dep );
193+ String itemClass = isNew ? "jbang-list-item jbang-list-item-new" : "jbang-list-item jbang-list-item-old" ;
194+ html .append ("<li class='" ).append (itemClass ).append ("' title='" ).append (dep ).append ("'>" ).append (fileName ).append ("</li>" );
195+ }
196+ html .append ("</ul></div></details>" );
197+ } else {
198+ html .append ("<span title='All resolved JARs'>📚 <strong>" ).append (totalResolved ).append ("</strong></span>" );
199+ }
200+
201+ html .append ("</div>\n " );
202+ } else {
203+ html .append ("<div class='jbang-compact'>🚀 JBang: <em>No dependencies</em></div>\n " );
204+ }
205+
206+ return html .toString ();
207+ });
208+ });
209+ }
210+ }
211+ public static JBangInfo getJBangResolvedDependencies (String scriptRef , String body , boolean inclAppJar ) throws IOException {
71212 try {
72213 String jbangExecutable = findJBangExecutable ();
73214 if (jbangExecutable == null ) {
@@ -109,11 +250,18 @@ public static List<String> getJBangResolvedDependencies(String scriptRef, String
109250 .collect (Collectors .toList ());
110251 }
111252
253+ List <String > dependencies = new ArrayList <>();
254+ if (json .has ("dependencies" ) && json .get ("dependencies" ).isJsonArray ()) {
255+ dependencies = StreamSupport .stream (json .getAsJsonArray ("dependencies" ).spliterator (), false )
256+ .map (JsonElement ::getAsString )
257+ .collect (Collectors .toList ());
258+ }
259+
112260 if (inclAppJar ) {
113261 resolvedDependencies .add (json .get ("applicationJar" ).getAsString ());
114262 }
115263
116- return resolvedDependencies ;
264+ return new JBangInfo ( resolvedDependencies , dependencies ) ;
117265
118266
119267 } catch (Exception e ) {
0 commit comments