diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 43fab0c..965b57b 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -51,6 +51,7 @@ jobs:
mkdir -p jb_updater-bundle
cp jb_updater/jb_updater jb_updater-bundle/
cp jb_updater/jb_updater_gui jb_updater-bundle/
+ cp jb_updater/assets/jb_updater.svg jb_updater-bundle/
tar -czf jb_updater-bundle-linux-x86_64.tar.gz -C jb_updater-bundle .
- uses: actions/upload-artifact@v4
with:
@@ -146,13 +147,14 @@ jobs:
cd ..
zip -ry jb_updater-bundle-macos-${ARCH}.zip jb_updater-bundle
- # .app bundle with dylibs inside
+ # .app bundle with dylibs + icon inside
APP_ROOT="JBUpdater.app/Contents"
mkdir -p "$APP_ROOT/MacOS" "$APP_ROOT/Resources"
cp jb_updater/jb_updater_gui "$APP_ROOT/MacOS/"
cp jb_updater/jb_updater "$APP_ROOT/MacOS/"
cp jb_updater/libcrypto.3.dylib "$APP_ROOT/MacOS/"
cp jb_updater/libssl.3.dylib "$APP_ROOT/MacOS/"
+ cp jb_updater/assets/jb_updater.icns "$APP_ROOT/Resources/"
cat > "$APP_ROOT/Info.plist" <<'PLIST'
@@ -162,6 +164,7 @@ jobs:
CFBundleIdentifiercom.example.jb-updater
CFBundleVersion1.0
CFBundleExecutablejb_updater_gui
+ CFBundleIconFilejb_updater
LSMinimumSystemVersion11.0
NSHighResolutionCapable
diff --git a/jb_updater/assets/jb_updater.icns b/jb_updater/assets/jb_updater.icns
new file mode 100644
index 0000000..211120d
Binary files /dev/null and b/jb_updater/assets/jb_updater.icns differ
diff --git a/jb_updater/assets/jb_updater.ico b/jb_updater/assets/jb_updater.ico
new file mode 100644
index 0000000..68a9280
Binary files /dev/null and b/jb_updater/assets/jb_updater.ico differ
diff --git a/jb_updater/assets/jb_updater.svg b/jb_updater/assets/jb_updater.svg
new file mode 100644
index 0000000..98e8514
--- /dev/null
+++ b/jb_updater/assets/jb_updater.svg
@@ -0,0 +1,5 @@
+
diff --git a/jb_updater/assets/jb_updater_128x128.png b/jb_updater/assets/jb_updater_128x128.png
new file mode 100644
index 0000000..9fc8320
Binary files /dev/null and b/jb_updater/assets/jb_updater_128x128.png differ
diff --git a/jb_updater/assets/jb_updater_16x16.png b/jb_updater/assets/jb_updater_16x16.png
new file mode 100644
index 0000000..8e27e2c
Binary files /dev/null and b/jb_updater/assets/jb_updater_16x16.png differ
diff --git a/jb_updater/assets/jb_updater_256x256.png b/jb_updater/assets/jb_updater_256x256.png
new file mode 100644
index 0000000..c738c99
Binary files /dev/null and b/jb_updater/assets/jb_updater_256x256.png differ
diff --git a/jb_updater/assets/jb_updater_32x32.png b/jb_updater/assets/jb_updater_32x32.png
new file mode 100644
index 0000000..eca775a
Binary files /dev/null and b/jb_updater/assets/jb_updater_32x32.png differ
diff --git a/jb_updater/assets/jb_updater_48x48.png b/jb_updater/assets/jb_updater_48x48.png
new file mode 100644
index 0000000..84e1507
Binary files /dev/null and b/jb_updater/assets/jb_updater_48x48.png differ
diff --git a/jb_updater/assets/jb_updater_512x512.png b/jb_updater/assets/jb_updater_512x512.png
new file mode 100644
index 0000000..fbb5a43
Binary files /dev/null and b/jb_updater/assets/jb_updater_512x512.png differ
diff --git a/jb_updater/spec/cli_spec.cr b/jb_updater/spec/cli_spec.cr
new file mode 100644
index 0000000..c37a626
--- /dev/null
+++ b/jb_updater/spec/cli_spec.cr
@@ -0,0 +1,59 @@
+require "./spec_helper"
+include JBUpdater
+
+describe Options do
+ describe "#initialize" do
+ it "defaults all fields to nil/false/empty" do
+ o = Options.new
+ o.plugins_dir.should be_nil
+ o.build.should be_nil
+ o.only.should be_empty
+ o.only_incompatible?.should be_false
+ o.dry_run?.should be_false
+ o.downloads_host.should eq "downloads.marketplace.jetbrains.com"
+ o.pin_versions.should be_empty
+ o.direct_urls.should be_empty
+ o.list?.should be_false
+ o.bin_path.should be_nil
+ o.include_bundled?.should be_false
+ o.install_ids.should be_empty
+ o.product.should be_nil
+ o.ide_path.should be_nil
+ o.brew_patch.should be_false
+ o.upgrade_ide?.should be_false
+ o.ide_downloads_host.should eq "download-cdn.jetbrains.com"
+ o.arch.should be_nil
+ o.list_ide_releases?.should be_false
+ o.no_tty_progress_bar?.should be_false
+ end
+ end
+end
+
+describe JBUpdater do
+ describe ".parse_cli" do
+ it "parses --help and sets list flag" do
+ opts = JBUpdater.parse_cli(["-l"])
+ opts.list?.should be_true
+ end
+
+ it "parses --build" do
+ opts = JBUpdater.parse_cli(["-b", "RM-252"])
+ opts.build.should eq "RM-252"
+ end
+
+ it "parses --dry-run" do
+ opts = JBUpdater.parse_cli(["--dry-run"])
+ opts.dry_run?.should be_true
+ end
+
+ it "parses --plugins-dir" do
+ opts = JBUpdater.parse_cli(["--plugins-dir", "/custom/path"])
+ opts.plugins_dir.should eq "/custom/path"
+ end
+
+ it "parses --install-plugin" do
+ opts = JBUpdater.parse_cli(["-i", "com.example.plugin,org.test"])
+ opts.install_ids.should eq ["com.example.plugin", "org.test"]
+ end
+ end
+end
diff --git a/jb_updater/spec/http_client_spec.cr b/jb_updater/spec/http_client_spec.cr
index d4d8887..c6b9fc5 100644
--- a/jb_updater/spec/http_client_spec.cr
+++ b/jb_updater/spec/http_client_spec.cr
@@ -41,4 +41,35 @@ describe HTTPClient do
new_uri.host.should eq "cdn.jetbrains.io"
new_uri.path.should eq "/files/1234/yaml.zip"
end
+
+ it "overrides IDE host correctly" do
+ uri = URI.parse("https://download.jetbrains.com/ruby/RM-252.0.dmg")
+ new_uri = HTTPClient.override_ide_repo_host(uri, "custom-cdn.example.com")
+ new_uri.host.should eq "custom-cdn.example.com"
+ end
+
+ describe ".no_tty_progress_bar" do
+ it "defaults to false" do
+ HTTPClient.no_tty_progress_bar?.should be_false
+ end
+
+ it "can be set and read back" do
+ HTTPClient.no_tty_progress_bar = true
+ HTTPClient.no_tty_progress_bar?.should be_true
+ HTTPClient.no_tty_progress_bar = false
+ end
+ end
+
+ describe ".override_plugin_repo_host" do
+ it "returns original URI when downloads_host is nil" do
+ uri = URI.parse("https://plugins.jetbrains.com/files/test.zip")
+ HTTPClient.override_plugin_repo_host(uri, nil).should be uri
+ end
+
+ it "returns original URI when host doesn't match" do
+ uri = URI.parse("https://other.host.com/files/test.zip")
+ new_uri = HTTPClient.override_plugin_repo_host(uri, "cdn.example.com")
+ new_uri.should be uri
+ end
+ end
end
diff --git a/jb_updater/spec/plugin_marketplace_spec.cr b/jb_updater/spec/plugin_marketplace_spec.cr
new file mode 100644
index 0000000..2b99e6d
--- /dev/null
+++ b/jb_updater/spec/plugin_marketplace_spec.cr
@@ -0,0 +1,146 @@
+require "./spec_helper"
+include JBUpdater
+
+describe JBUpdater do
+ describe ".html_strip" do
+ it "removes basic HTML tags" do
+ JBUpdater.html_strip("
hello
").should eq "hello"
+ end
+
+ it "decodes HTML entities" do
+ JBUpdater.html_strip("<test>").should eq ""
+ end
+
+ it "decodes numeric entities" do
+ JBUpdater.html_strip("ABC").should eq "ABC"
+ end
+
+ it "collapses whitespace" do
+ JBUpdater.html_strip("a b\n\nc").should eq "a b c"
+ end
+
+ it "handles empty string" do
+ JBUpdater.html_strip("").should eq ""
+ end
+
+ it "handles string with no HTML" do
+ JBUpdater.html_strip("plain text").should eq "plain text"
+ end
+ end
+end
+
+describe PluginInfo do
+ describe ".parse" do
+ it "parses valid XML with one plugin" do
+ xml = <<-XML
+
+
+
+ com.example.plugin
+ Example Plugin
+ Cool plugin
+ Example Inc
+ web, integration,
+
+
+ XML
+ plugins = PluginInfo.parse(xml)
+ plugins.size.should eq 1
+ plugins[0].xml_id.should eq "com.example.plugin"
+ plugins[0].name.should eq "Example Plugin"
+ plugins[0].description.should eq "Cool plugin"
+ plugins[0].downloads.should eq 12_345
+ plugins[0].vendor.should eq "Example Inc"
+ plugins[0].categories.should contain("web")
+ plugins[0].categories.should contain("integration")
+ end
+
+ it "strips HTML from description inside XML entities" do
+ xml = <<-XML
+
+
+
+ test.html
+ Test
+ <b>Bold text</b> description
+
+
+ XML
+ plugins = PluginInfo.parse(xml)
+ plugins.size.should eq 1
+ # The Crystal XML parser may or may not decode entities before passing
+ # to html_strip. Accept both outcomes: either fully stripped or partially.
+ desc = plugins[0].description
+ desc.should_not contain(">")
+ desc.should_not contain("<")
+ end
+
+ it "returns empty array for empty XML" do
+ PluginInfo.parse("").should be_empty
+ end
+
+ it "returns empty array for malformed XML" do
+ PluginInfo.parse("not xml").should be_empty
+ end
+
+ it "strips malformed tags" do
+ xml = <<-XML
+
+
+ test
+ Test
+ baddesc
+
+
+ XML
+ PluginInfo.parse(xml).size.should eq 1
+ end
+ end
+
+ describe "#download_url" do
+ it "builds URL with xml_id and numeric id" do
+ p = PluginInfo.new(id: 999_i64, xml_id: "test.plugin", name: "T", description: "")
+ p.download_url.should eq "https://plugins.jetbrains.com/files/test.plugin/999"
+ end
+ end
+
+ describe "#download_install_url" do
+ it "builds install URL with xml_id" do
+ p = PluginInfo.new(id: 0_i64, xml_id: "test.plugin", name: "T", description: "")
+ p.download_install_url.should eq "https://plugins.jetbrains.com/plugin/download?pluginId=test.plugin"
+ end
+ end
+
+ describe "#download_for_build_url" do
+ it "includes build parameter" do
+ p = PluginInfo.new(id: 0_i64, xml_id: "test.plugin", name: "T", description: "")
+ url = p.download_for_build_url("RM-252")
+ url.should contain("build=RM-252")
+ url.should contain("id=test.plugin")
+ end
+ end
+
+ describe "#formatted_downloads" do
+ it "formats millions" do
+ p = PluginInfo.new(id: 1_i64, xml_id: "x", name: "x", description: "", downloads: 2_500_000_i64)
+ p.formatted_downloads.should eq "2.5M"
+ end
+
+ it "formats thousands" do
+ p = PluginInfo.new(id: 1_i64, xml_id: "x", name: "x", description: "", downloads: 450_000_i64)
+ p.formatted_downloads.should eq "450.0K"
+ end
+
+ it "formats small numbers" do
+ p = PluginInfo.new(id: 1_i64, xml_id: "x", name: "x", description: "", downloads: 123_i64)
+ p.formatted_downloads.should eq "123"
+ end
+ end
+
+ describe "#star_rating" do
+ it "returns five stars" do
+ p = PluginInfo.new(id: 1_i64, xml_id: "x", name: "x", description: "")
+ p.star_rating.should eq "⭐⭐⭐⭐⭐"
+ end
+ end
+end
diff --git a/jb_updater/spec/plugin_meta_spec.cr b/jb_updater/spec/plugin_meta_spec.cr
index a8527b3..4b7508a 100644
--- a/jb_updater/spec/plugin_meta_spec.cr
+++ b/jb_updater/spec/plugin_meta_spec.cr
@@ -2,26 +2,120 @@ require "./spec_helper"
include JBUpdater
describe PluginMeta do
- it "parses valid plugin.xml" do
- xml = <<-XML
-
- org.jetbrains.demo
- 2025.1.0
-
-
- XML
-
- if p = PluginMeta.parse_xml(xml, "/tmp/fake")
- p.id.should eq "org.jetbrains.demo"
- p.version.should eq "2025.1.0"
- p.since.should eq "252.0"
- p.until_build.should eq "260.*"
- else
- fail "expected plugin to be parsed"
+ describe ".parse_xml" do
+ it "parses valid plugin.xml" do
+ xml = <<-XML
+
+ org.jetbrains.demo
+ 2025.1.0
+
+
+ XML
+
+ if p = PluginMeta.parse_xml(xml, "/tmp/fake")
+ p.id.should eq "org.jetbrains.demo"
+ p.version.should eq "2025.1.0"
+ p.since.should eq "252.0"
+ p.until_build.should eq "260.*"
+ p.path.should eq "/tmp/fake"
+ else
+ fail "expected plugin to be parsed"
+ end
+ end
+
+ it "parses plugin.xml with display name" do
+ xml = <<-XML
+
+ My Plugin
+ com.example.my
+ 1.0
+
+ XML
+ p = PluginMeta.parse_xml(xml, "/tmp/fake")
+ p.should_not be_nil
+ p.try(&.name).should eq "My Plugin"
+ end
+
+ it "falls back to name when id is missing" do
+ xml = <<-XML
+
+ Fallback Plugin
+ 1.0
+
+ XML
+ PluginMeta.parse_xml(xml, "/tmp/fake").should_not be_nil
+ end
+
+ it "returns nil for broken xml" do
+ PluginMeta.parse_xml("", "/tmp").should be_nil
end
end
- it "returns nil for broken xml" do
- PluginMeta.parse_xml("", "/tmp").should be_nil
+ describe ".scan_dir" do
+ it "scans plugin directories and returns metadata map" do
+ with_tmpdir do |dir|
+ plugin_dir = File.join(dir, "test-plugin")
+ Dir.mkdir(plugin_dir)
+ meta_dir = File.join(plugin_dir, "META-INF")
+ Dir.mkdir(meta_dir)
+ File.write(File.join(meta_dir, "plugin.xml"), <<-XML)
+
+ com.example.scanned
+ 1.0
+
+ XML
+
+ result = PluginMeta.scan_dir(dir)
+ result.size.should eq 1
+ result["com.example.scanned"].version.should eq "1.0"
+ end
+ end
+
+ it "skips hidden directories" do
+ with_tmpdir do |dir|
+ hidden = File.join(dir, ".hidden")
+ Dir.mkdir(hidden)
+
+ result = PluginMeta.scan_dir(dir)
+ result.should be_empty
+ end
+ end
+
+ it "returns empty hash for empty directory" do
+ with_tmpdir do |dir|
+ PluginMeta.scan_dir(dir).should be_empty
+ end
+ end
+ end
+
+ describe ".parse_from_dir" do
+ it "parses plugin.xml from META-INF directory" do
+ with_tmpdir do |dir|
+ meta_inf = File.join(dir, "META-INF")
+ Dir.mkdir(meta_inf)
+ File.write(File.join(meta_inf, "plugin.xml"), <<-XML)
+
+ com.example.fromdir
+ 2.0
+
+ XML
+
+ p = PluginMeta.parse_from_dir(dir)
+ p.should_not be_nil
+ p.try(&.id).should eq "com.example.fromdir"
+ end
+ end
+
+ it "returns nil when no plugin.xml found" do
+ with_tmpdir do |dir|
+ PluginMeta.parse_from_dir(dir).should be_nil
+ end
+ end
+ end
+
+ describe ".read_text_from_jar" do
+ it "returns nil when jar does not exist" do
+ PluginMeta.read_text_from_jar("/tmp/nonexistent.jar", "META-INF/plugin.xml").should be_nil
+ end
end
end
diff --git a/jb_updater/spec/spec_helper.cr b/jb_updater/spec/spec_helper.cr
index 459baa3..8cf7645 100644
--- a/jb_updater/spec/spec_helper.cr
+++ b/jb_updater/spec/spec_helper.cr
@@ -1,5 +1,7 @@
require "spec"
require "../src/jb_updater"
+require "../src/jb_updater/plugin_marketplace"
+require "./support/tmpdir"
module JBUpdater
class HTTPClient
diff --git a/jb_updater/spec/support/tmpdir.cr b/jb_updater/spec/support/tmpdir.cr
new file mode 100644
index 0000000..a10dd0a
--- /dev/null
+++ b/jb_updater/spec/support/tmpdir.cr
@@ -0,0 +1,9 @@
+require "file_utils"
+
+def with_tmpdir(&)
+ dir = File.join(Dir.tempdir, "jb_updater_test_#{Process.pid}_#{Random.rand(10000)}")
+ Dir.mkdir_p(dir)
+ yield dir
+ensure
+ FileUtils.rm_rf(dir) if dir
+end
diff --git a/jb_updater/src/gui/layout_helper.m b/jb_updater/src/gui/layout_helper.m
index a4023c5..3e0f4d9 100644
--- a/jb_updater/src/gui/layout_helper.m
+++ b/jb_updater/src/gui/layout_helper.m
@@ -1,4 +1,19 @@
#import
+#import
+
+// Forward declaration of libui's uiQuit
+extern void uiQuit(void);
+
+// Small helper class to handle Quit action (avoids libui's terminate: override)
+@interface JBUpdaterHelper : NSObject
+- (void)quitApp:(id)sender;
+@end
+
+@implementation JBUpdaterHelper
+- (void)quitApp:(id)sender {
+ uiQuit();
+}
+@end
void* create_width_constraint(void* item, void* relative_to, double multiplier) {
return (__bridge void*)[NSLayoutConstraint
@@ -14,3 +29,49 @@
void add_constraint_to_view(void* view, void* constraint) {
[(__bridge NSView*)view addConstraint:(__bridge NSLayoutConstraint*)constraint];
}
+
+void set_app_icon(const char* icns_path) {
+ @autoreleasepool {
+ NSString *path = [NSString stringWithUTF8String:icns_path];
+ NSImage *icon = [[NSImage alloc] initWithContentsOfFile:path];
+ if (icon) {
+ [NSApp setApplicationIconImage:icon];
+ }
+ }
+}
+
+void setup_menu_bar(void) {
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ static JBUpdaterHelper *helper = nil;
+ helper = [[JBUpdaterHelper alloc] init];
+
+ NSMenu *mainMenu = [[NSMenu alloc] init];
+
+ // File menu
+ NSMenuItem *fileMenuItem = [[NSMenuItem alloc] initWithTitle:@"File" action:nil keyEquivalent:@""];
+ NSMenu *fileMenu = [[NSMenu alloc] initWithTitle:@"File"];
+ [fileMenuItem setSubmenu:fileMenu];
+ [mainMenu addItem:fileMenuItem];
+
+ // Close Window — cmd+W
+ NSMenuItem *closeItem = [[NSMenuItem alloc] initWithTitle:@"Close Window"
+ action:@selector(performClose:)
+ keyEquivalent:@"w"];
+ [closeItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
+ [closeItem setTarget:nil];
+ [fileMenu addItem:closeItem];
+
+ [fileMenu addItem:[NSMenuItem separatorItem]];
+
+ // Quit — cmd+Q (uses helper to bypass libui's terminate: override)
+ NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
+ action:@selector(quitApp:)
+ keyEquivalent:@"q"];
+ [quitItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand];
+ [quitItem setTarget:helper];
+ [fileMenu addItem:quitItem];
+
+ [NSApp setMainMenu:mainMenu];
+ });
+}
diff --git a/jb_updater/src/gui/main_gui.cr b/jb_updater/src/gui/main_gui.cr
index db28ce6..8b5c839 100644
--- a/jb_updater/src/gui/main_gui.cr
+++ b/jb_updater/src/gui/main_gui.cr
@@ -18,6 +18,8 @@ require "uing"
lib LayoutHelper
fun create_width_constraint(item : Void*, relative_to : Void*, multiplier : Float64) : Void*
fun add_constraint_to_view(view : Void*, constraint : Void*) : Void
+ fun set_app_icon(icns_path : UInt8*) : Void
+ fun setup_menu_bar : Void
end
{% end %}
@@ -588,713 +590,705 @@ private def apply_ide_settings(
end
end
-# ---- UI --------------------------------------------------------------
-UIng.init do
- window = UIng::Window.new("JB Updater — JetBrains IDE & Plugin Manager", 1100, 660)
-
- window.on_closing do
- App.mark_shutting_down
- UIng.quit
- true
+{% if flag?(:darwin) %}
+ private def do_setup_icon_and_keys
+ exe_path = Process.executable_path
+ if exe_path
+ dir = File.dirname(exe_path)
+ icon_path = if dir.ends_with?("/MacOS")
+ File.join(dir, "..", "Resources", "jb_updater.icns")
+ else
+ File.join(dir, "assets", "jb_updater.icns")
+ end
+ LayoutHelper.set_app_icon(icon_path)
+ end
+ LayoutHelper.setup_menu_bar
end
+{% end %}
- root = UIng::Box.new(:vertical)
- root.padded = false
- window.set_child(root)
-
- pb_group = UIng::Group.new("Progress", margined: true)
- pb_inner = UIng::Box.new(:vertical)
- pb_inner.padded = true
-
- overall_label = UIng::Label.new("Overall:")
- overall_bar = UIng::ProgressBar.new
- overall_row = UIng::Box.new(:horizontal)
- overall_row.append(overall_label, false)
- overall_row.append(overall_bar, true)
-
- plugin_label = UIng::Label.new("Current plugin:")
- plugin_bar = UIng::ProgressBar.new
- plugin_row = UIng::Box.new(:horizontal)
- plugin_row.append(plugin_label, false)
- plugin_row.append(plugin_bar, true)
-
- pb_inner.append(overall_row, false)
- pb_inner.append(plugin_row, false)
- pb_group.child = pb_inner
- root.append(pb_group, false)
-
- sep1 = UIng::Separator.new("horizontal")
- root.append(sep1, false)
-
- tabs = UIng::Tab.new
- root.append(tabs, true)
-
- log = UIng::MultilineEntry.new(false, false)
-
- # Log all HTTP requests and Log.* messages to the console
- JBUpdater::HTTPClient.on_request = ->(method : String, url : String) {
- App.push_log("[HTTP] #{method} #{url}")
- }
- JBUpdater::HTTPClient.on_progress = ->(downloaded : Int64, total : Int64) {
- App.update_progress(downloaded, total)
- }
- JBUpdater::Log.listener = ->(msg : String) {
- App.push_log(msg)
- }
-
- {% if flag?(:gui_log) %}
- sep2 = UIng::Separator.new("horizontal")
- root.append(sep2, false)
+# ---- UI --------------------------------------------------------------
+UIng.init
- actions_row = UIng::Box.new(:horizontal)
- actions_row.padded = true
+{% if flag?(:darwin) %}
+ do_setup_icon_and_keys
+{% end %}
- btn_clear_log = UIng::Button.new("Clear console")
- btn_remove_cache = UIng::Button.new("Remove *.bak* backups")
- debug_btn = UIng::Button.new("Debug: Re-enable UI")
+window = UIng::Window.new("JB Updater — JetBrains IDE & Plugin Manager", 1100, 660)
- actions_row.append(btn_clear_log, false)
- actions_row.append(btn_remove_cache, false)
- actions_row.append(debug_btn, false)
- root.append(actions_row, false)
+window.on_closing do
+ App.mark_shutting_down
+ UIng.quit
+ true
+end
- root.append(log, true)
- {% end %}
+root = UIng::Box.new(:vertical)
+root.padded = false
+window.set_child(root)
+
+pb_group = UIng::Group.new("Progress", margined: true)
+pb_inner = UIng::Box.new(:vertical)
+pb_inner.padded = true
+
+overall_label = UIng::Label.new("Overall:")
+overall_bar = UIng::ProgressBar.new
+overall_row = UIng::Box.new(:horizontal)
+overall_row.append(overall_label, false)
+overall_row.append(overall_bar, true)
+
+plugin_label = UIng::Label.new("Current plugin:")
+plugin_bar = UIng::ProgressBar.new
+plugin_row = UIng::Box.new(:horizontal)
+plugin_row.append(plugin_label, false)
+plugin_row.append(plugin_bar, true)
+
+pb_inner.append(overall_row, false)
+pb_inner.append(plugin_row, false)
+pb_group.child = pb_inner
+root.append(pb_group, false)
+
+sep1 = UIng::Separator.new("horizontal")
+root.append(sep1, false)
+
+tabs = UIng::Tab.new
+root.append(tabs, true)
+
+log = UIng::MultilineEntry.new(false, false)
+
+# Log all HTTP requests and Log.* messages to the console
+JBUpdater::HTTPClient.on_request = ->(method : String, url : String) {
+ App.push_log("[HTTP] #{method} #{url}")
+}
+JBUpdater::HTTPClient.on_progress = ->(downloaded : Int64, total : Int64) {
+ App.update_progress(downloaded, total)
+}
+JBUpdater::Log.listener = ->(msg : String) {
+ App.push_log(msg)
+}
+
+{% if flag?(:gui_log) %}
+ sep2 = UIng::Separator.new("horizontal")
+ root.append(sep2, false)
+
+ actions_row = UIng::Box.new(:horizontal)
+ actions_row.padded = true
+
+ btn_clear_log = UIng::Button.new("Clear console")
+ btn_remove_cache = UIng::Button.new("Remove *.bak* backups")
+ debug_btn = UIng::Button.new("Debug: Re-enable UI")
+
+ actions_row.append(btn_clear_log, false)
+ actions_row.append(btn_remove_cache, false)
+ actions_row.append(debug_btn, false)
+ root.append(actions_row, false)
+
+ root.append(log, true)
+{% end %}
- status_label = UIng::Label.new("Ready")
- status_box = UIng::Box.new(:horizontal)
- status_box.padded = true
- status_box.append(status_label, true)
- root.append(status_box, false)
+status_label = UIng::Label.new("Ready")
+status_box = UIng::Box.new(:horizontal)
+status_box.padded = true
+status_box.append(status_label, true)
+root.append(status_box, false)
- {% if flag?(:gui_log) %}
- btn_clear_log.on_clicked do
- UIng.queue_main do
- log.text = ""
- log.append("Console cleared at #{Time.local}\n")
- status_label.text = "Console cleared"
- end
+{% if flag?(:gui_log) %}
+ btn_clear_log.on_clicked do
+ UIng.queue_main do
+ log.text = ""
+ log.append("Console cleared at #{Time.local}\n")
+ status_label.text = "Console cleared"
end
+ end
- debug_btn.on_clicked do
- UIng.queue_main do
- App.debug_reenable
- status_label.text = "UI re-enabled"
- end
+ debug_btn.on_clicked do
+ UIng.queue_main do
+ App.debug_reenable
+ status_label.text = "UI re-enabled"
end
- {% end %}
+ end
+{% end %}
- # --- Plugins tab ----------------------------------------------------
- plugins_tab = UIng::Box.new(:vertical)
- plugins_tab.padded = true
+# --- Plugins tab ----------------------------------------------------
+plugins_tab = UIng::Box.new(:vertical)
+plugins_tab.padded = true
- prod_group = UIng::Group.new("Product Detection", margined: true)
- prod_form = UIng::Form.new
- prod_form.padded = true
+prod_group = UIng::Group.new("Product Detection", margined: true)
+prod_form = UIng::Form.new
+prod_form.padded = true
- combo_products = UIng::Combobox.new
- detected = JBUpdater::DetectProducts.all
- App.detected_products = detected
+combo_products = UIng::Combobox.new
+detected = JBUpdater::DetectProducts.all
+App.detected_products = detected
- detected.sort_by!(&.name)
+detected.sort_by!(&.name)
- combo_products.append("Manual / Custom")
- detected.each do |prod|
- combo_products.append("#{prod.name} (#{prod.build})")
- end
- combo_products.selected = 0
+combo_products.append("Manual / Custom")
+detected.each do |prod|
+ combo_products.append("#{prod.name} (#{prod.build})")
+end
+combo_products.selected = 0
- prod_form.append("IDE / Product", combo_products, false)
- prod_group.child = prod_form
- plugins_tab.append(prod_group, false)
+prod_form.append("IDE / Product", combo_products, false)
+prod_group.child = prod_form
+plugins_tab.append(prod_group, false)
- config_group = UIng::Group.new("Configuration", margined: true)
- config_form = UIng::Form.new
- config_form.padded = true
+config_group = UIng::Group.new("Configuration", margined: true)
+config_form = UIng::Form.new
+config_form.padded = true
- e_plugins_dir = UIng::Entry.new
- e_build = UIng::Entry.new
- e_product = UIng::Entry.new
- e_install_ids = UIng::Entry.new
+e_plugins_dir = UIng::Entry.new
+e_build = UIng::Entry.new
+e_product = UIng::Entry.new
+e_install_ids = UIng::Entry.new
- combo_arch = UIng::Combobox.new
- ["Auto", "arm", "intel"].each { |arch_label| combo_arch.append arch_label }
- combo_arch.selected = 0
+combo_arch = UIng::Combobox.new
+["Auto", "arm", "intel"].each { |arch_label| combo_arch.append arch_label }
+combo_arch.selected = 0
- config_form.append("Plugins dir", e_plugins_dir, true)
- config_form.append("Build", e_build, false)
- config_form.append("Product", e_product, false)
- config_form.append("Install IDs", e_install_ids, false)
- config_form.append("Arch", combo_arch, false)
- config_group.child = config_form
- plugins_tab.append(config_group, false)
+config_form.append("Plugins dir", e_plugins_dir, true)
+config_form.append("Build", e_build, false)
+config_form.append("Product", e_product, false)
+config_form.append("Install IDs", e_install_ids, false)
+config_form.append("Arch", combo_arch, false)
+config_group.child = config_form
+plugins_tab.append(config_group, false)
- chk_dry = UIng::Checkbox.new("Dry run")
- plugins_tab.append(chk_dry, false)
+chk_dry = UIng::Checkbox.new("Dry run")
+plugins_tab.append(chk_dry, false)
- btn_group = UIng::Box.new(:vertical)
- btn_group.padded = true
+btn_group = UIng::Box.new(:vertical)
+btn_group.padded = true
- btn_detect = UIng::Button.new("Detect from Product")
- btn_detect.on_clicked do
- UIng.queue_main do
- product = e_product.text
- if product.nil? || product.empty?
- log.append("ERROR: Enter Product (e.g., RubyMine2025.2) before Detect.\n")
- status_label.text = "Error: missing product"
- else
- begin
- resolved = JBUpdater::Utils.resolve_product_folder(product)
- path = JBUpdater::Utils.expand_jetbrains_plugins_dir(resolved)
- e_plugins_dir.text = path
- log.append("Detected plugins dir: #{path}\n")
- status_label.text = "Detected: #{path}"
- save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
- rescue ex
- log.append("ERROR: #{ex.message}\n")
- status_label.text = "Error: #{ex.message}"
- end
+btn_detect = UIng::Button.new("Detect from Product")
+btn_detect.on_clicked do
+ UIng.queue_main do
+ product = e_product.text
+ if product.nil? || product.empty?
+ log.append("ERROR: Enter Product (e.g., RubyMine2025.2) before Detect.\n")
+ status_label.text = "Error: missing product"
+ else
+ begin
+ resolved = JBUpdater::Utils.resolve_product_folder(product)
+ path = JBUpdater::Utils.expand_jetbrains_plugins_dir(resolved)
+ e_plugins_dir.text = path
+ log.append("Detected plugins dir: #{path}\n")
+ status_label.text = "Detected: #{path}"
+ save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
+ rescue ex
+ log.append("ERROR: #{ex.message}\n")
+ status_label.text = "Error: #{ex.message}"
end
end
end
- btn_group.append(btn_detect, false)
-
- btn_group_sep = UIng::Separator.new("horizontal")
- btn_group.append(btn_group_sep, false)
-
- main_actions = UIng::Box.new(:horizontal)
- main_actions.padded = true
-
- btn_list = UIng::Button.new("List installed")
- btn_install = UIng::Button.new("Install by IDs")
- btn_update = UIng::Button.new("Update all")
-
- main_actions.append(btn_list, false)
- main_actions.append(btn_install, false)
- main_actions.append(btn_update, false)
- btn_group.append(main_actions, false)
-
- plugins_tab.append(btn_group, false)
- tabs.append("Plugins", plugins_tab)
-
- # --- Browse tab -----------------------------------------------------
- browse_tab = UIng::Box.new(:vertical)
- browse_tab.padded = true
-
- browse_content = UIng::Box.new(:horizontal)
- browse_content.padded = true
-
- browse_left = UIng::Box.new(:vertical)
- browse_left.padded = true
-
- browse_header = UIng::Box.new(:horizontal)
- browse_header.padded = true
-
- search_entry = UIng::Entry.new
- search_entry.text = ""
-
- btn_top = UIng::Button.new("Top Downloaded")
- btn_newest = UIng::Button.new("Newest")
- btn_refresh = UIng::Button.new("Refresh")
-
- browse_header.append(search_entry, true)
- browse_header.append(btn_top, false)
- browse_header.append(btn_newest, false)
- browse_header.append(btn_refresh, false)
- browse_left.append(browse_header, false)
-
- browse_model_handler = UIng::Table::Model::Handler.new do
- num_columns { 4 }
- column_type { |_col| UIng::Table::Value::Type::String }
- num_rows { App.browse_plugins.size }
- cell_value { |row, col|
- if row < App.browse_plugins.size
- plugin = App.browse_plugins[row]
- case col
- when 0 then UIng::Table::Value.new(plugin.name)
- when 1
- installed = App.installed_plugins
- value = installed ? (installed.has_key?(plugin.xml_id) ? "✓" : "") : "—"
- UIng::Table::Value.new(value)
- when 2 then UIng::Table::Value.new(plugin.formatted_downloads)
- else UIng::Table::Value.new(plugin.star_rating)
- end
- else
- UIng::Table::Value.new("")
- end
- }
- end
-
- browse_model = UIng::Table::Model.new(browse_model_handler)
- browse_table = UIng::Table.new(browse_model)
- browse_table.header_visible = true
- browse_table.selection_mode = :one
-
- browse_table.append_text_column("Plugin", 0, -1)
- browse_table.append_text_column("Installed", 1, -1)
- browse_table.append_text_column("Downloads", 2, -1)
- browse_table.append_text_column("Rating", 3, -1)
- browse_table.column_set_width(0, 260)
- browse_table.column_set_width(1, 60)
- browse_table.column_set_width(2, 100)
- browse_table.column_set_width(3, 80)
+end
+btn_group.append(btn_detect, false)
- browse_table.on_header_clicked do |column|
- next unless {0, 2, 3}.includes?(column)
+btn_group_sep = UIng::Separator.new("horizontal")
+btn_group.append(btn_group_sep, false)
- (0...4).each do |col|
- browse_table.header_set_sort_indicator(col, :none) if col != column
- end
+main_actions = UIng::Box.new(:horizontal)
+main_actions.padded = true
- current = browse_table.header_sort_indicator(column)
- ascending = current.none? || current.descending?
+btn_list = UIng::Button.new("List installed")
+btn_install = UIng::Button.new("Install by IDs")
+btn_update = UIng::Button.new("Update all")
- plugins = App.browse_plugins
- case column
- when 0 then plugins.sort! { |x, y| ascending ? x.name <=> y.name : y.name <=> x.name }
- when 2 then plugins.sort! { |x, y| ascending ? x.downloads <=> y.downloads : y.downloads <=> x.downloads }
- when 3 then plugins.sort! { |x, y| ascending ? x.rating <=> y.rating : y.rating <=> x.rating }
- end
+main_actions.append(btn_list, false)
+main_actions.append(btn_install, false)
+main_actions.append(btn_update, false)
+btn_group.append(main_actions, false)
- new_indicator = ascending ? UIng::Table::SortIndicator::Ascending : UIng::Table::SortIndicator::Descending
- browse_table.header_set_sort_indicator(column, new_indicator)
+plugins_tab.append(btn_group, false)
+tabs.append("Plugins", plugins_tab)
- plugins.each_with_index { |_, i| browse_model.row_changed(i) }
- end
+# --- Browse tab -----------------------------------------------------
+browse_tab = UIng::Box.new(:vertical)
+browse_tab.padded = true
- browse_left.append(browse_table, true)
+browse_content = UIng::Box.new(:horizontal)
+browse_content.padded = true
- App.browse_table_model = browse_model
- App.browse_handler = browse_model_handler
+browse_left = UIng::Box.new(:vertical)
+browse_left.padded = true
- browse_actions = UIng::Box.new(:horizontal)
- browse_actions.padded = true
+browse_header = UIng::Box.new(:horizontal)
+browse_header.padded = true
- btn_install_browse = UIng::Button.new("Install Selected")
- btn_copy_id = UIng::Button.new("Copy XML ID")
+search_entry = UIng::Entry.new
+search_entry.text = ""
- browse_actions.append(btn_install_browse, false)
- browse_actions.append(btn_copy_id, false)
- browse_left.append(browse_actions, false)
+btn_top = UIng::Button.new("Top Downloaded")
+btn_newest = UIng::Button.new("Newest")
+btn_refresh = UIng::Button.new("Refresh")
- browse_status = UIng::Label.new("Click search or a button to browse plugins")
- browse_status_box = UIng::Box.new(:horizontal)
- browse_status_box.padded = true
- browse_status_box.append(browse_status, true)
- browse_left.append(browse_status_box, false)
+browse_header.append(search_entry, true)
+browse_header.append(btn_top, false)
+browse_header.append(btn_newest, false)
+browse_header.append(btn_refresh, false)
+browse_left.append(browse_header, false)
- browse_detail_box = UIng::Box.new(:vertical)
- browse_detail_box.padded = true
- detail_label = UIng::Label.new("Plugin Details")
- browse_detail = UIng::MultilineEntry.new(true, true)
- App.safe_set_text(browse_detail, "Select a plugin to view details")
- App.browse_detail = browse_detail
+browse_model_handler = UIng::Table::Model::Handler.new do
+ num_columns { 4 }
+ column_type { |_col| UIng::Table::Value::Type::String }
+ num_rows { App.browse_plugins.size }
+ cell_value { |row, col|
+ if row < App.browse_plugins.size
+ plugin = App.browse_plugins[row]
+ case col
+ when 0 then UIng::Table::Value.new(plugin.name)
+ when 1
+ installed = App.installed_plugins
+ value = installed ? (installed.has_key?(plugin.xml_id) ? "✓" : "") : "—"
+ UIng::Table::Value.new(value)
+ when 2 then UIng::Table::Value.new(plugin.formatted_downloads)
+ else UIng::Table::Value.new(plugin.star_rating)
+ end
+ else
+ UIng::Table::Value.new("")
+ end
+ }
+end
- browse_detail_box.append(detail_label, false)
- browse_detail_box.append(browse_detail, true)
- browse_content.append(browse_left, true)
- browse_content.append(browse_detail_box, false)
- browse_tab.append(browse_content, true)
+browse_model = UIng::Table::Model.new(browse_model_handler)
+browse_table = UIng::Table.new(browse_model)
+browse_table.header_visible = true
+browse_table.selection_mode = :one
- tabs.append("Browse", browse_tab)
+browse_table.append_text_column("Plugin", 0, -1)
+browse_table.append_text_column("Installed", 1, -1)
+browse_table.append_text_column("Downloads", 2, -1)
+browse_table.append_text_column("Rating", 3, -1)
+browse_table.column_set_width(0, 260)
+browse_table.column_set_width(1, 60)
+browse_table.column_set_width(2, 100)
+browse_table.column_set_width(3, 80)
- # --- Installed tab ---------------------------------------------------
- installed_tab = UIng::Box.new(:vertical)
- installed_tab.padded = true
+browse_table.on_header_clicked do |column|
+ next unless {0, 2, 3}.includes?(column)
- installed_handler = UIng::Table::Model::Handler.new do
- num_columns { 5 }
- column_type { |_col| UIng::Table::Value::Type::String }
- num_rows { App.installed_plugins_arr.size }
- cell_value { |row, col|
- plugin = App.installed_plugins_arr[row]?
- next UIng::Table::Value.new("") unless plugin
- case col
- when 0 then UIng::Table::Value.new(plugin.name || plugin.id)
- when 1 then UIng::Table::Value.new(plugin.id)
- when 2 then UIng::Table::Value.new(plugin.version)
- when 3 then UIng::Table::Value.new(plugin.since || "—")
- else UIng::Table::Value.new(plugin.until_build || "—")
- end
- }
+ (0...4).each do |col|
+ browse_table.header_set_sort_indicator(col, :none) if col != column
end
- installed_model = UIng::Table::Model.new(installed_handler)
- App.installed_table = installed_table = UIng::Table.new(installed_model)
- App.installed_model = installed_model
- installed_table.header_visible = true
- installed_table.append_text_column("Name", 0, -1)
- installed_table.append_text_column("Plugin ID", 1, -1)
- installed_table.append_text_column("Version", 2, -1)
- installed_table.append_text_column("Since Build", 3, -1)
- installed_table.append_text_column("Until Build", 4, -1)
- installed_table.selection_mode = :one
-
- installed_actions = UIng::Box.new(:horizontal)
- installed_actions.padded = true
-
- btn_scan_installed = UIng::Button.new("Scan")
- btn_uninstall = UIng::Button.new("Uninstall selected")
- btn_uninstall.disable
- installed_actions.append(btn_scan_installed, false)
- installed_actions.append(btn_uninstall, false)
+ current = browse_table.header_sort_indicator(column)
+ ascending = current.none? || current.descending?
- installed_status = UIng::Label.new("Click Scan to list installed plugins")
+ plugins = App.browse_plugins
+ case column
+ when 0 then plugins.sort! { |x, y| ascending ? x.name <=> y.name : y.name <=> x.name }
+ when 2 then plugins.sort! { |x, y| ascending ? x.downloads <=> y.downloads : y.downloads <=> x.downloads }
+ when 3 then plugins.sort! { |x, y| ascending ? x.rating <=> y.rating : y.rating <=> x.rating }
+ end
- installed_tab.append(installed_actions, false)
- installed_tab.append(installed_table, true)
- installed_tab.append(installed_status, false)
+ new_indicator = ascending ? UIng::Table::SortIndicator::Ascending : UIng::Table::SortIndicator::Descending
+ browse_table.header_set_sort_indicator(column, new_indicator)
- tabs.append("Installed", installed_tab)
+ plugins.each_with_index { |_, i| browse_model.row_changed(i) }
+end
- btn_scan_installed.on_clicked do
- dir = expand_tilde(e_plugins_dir.text) || e_plugins_dir.text || ""
- if dir.empty?
- installed_status.text = "Set Plugins Directory first"
- next
+browse_left.append(browse_table, true)
+
+App.browse_table_model = browse_model
+App.browse_handler = browse_model_handler
+
+browse_actions = UIng::Box.new(:horizontal)
+browse_actions.padded = true
+
+btn_install_browse = UIng::Button.new("Install Selected")
+btn_copy_id = UIng::Button.new("Copy XML ID")
+
+browse_actions.append(btn_install_browse, false)
+browse_actions.append(btn_copy_id, false)
+browse_left.append(browse_actions, false)
+
+browse_status = UIng::Label.new("Click search or a button to browse plugins")
+browse_status_box = UIng::Box.new(:horizontal)
+browse_status_box.padded = true
+browse_status_box.append(browse_status, true)
+browse_left.append(browse_status_box, false)
+
+browse_detail_box = UIng::Box.new(:vertical)
+browse_detail_box.padded = true
+detail_label = UIng::Label.new("Plugin Details")
+browse_detail = UIng::MultilineEntry.new(true, true)
+App.safe_set_text(browse_detail, "Select a plugin to view details")
+App.browse_detail = browse_detail
+
+browse_detail_box.append(detail_label, false)
+browse_detail_box.append(browse_detail, true)
+browse_content.append(browse_left, true)
+browse_content.append(browse_detail_box, false)
+browse_tab.append(browse_content, true)
+
+tabs.append("Browse", browse_tab)
+
+# --- Installed tab ---------------------------------------------------
+installed_tab = UIng::Box.new(:vertical)
+installed_tab.padded = true
+
+installed_handler = UIng::Table::Model::Handler.new do
+ num_columns { 5 }
+ column_type { |_col| UIng::Table::Value::Type::String }
+ num_rows { App.installed_plugins_arr.size }
+ cell_value { |row, col|
+ plugin = App.installed_plugins_arr[row]?
+ next UIng::Table::Value.new("") unless plugin
+ case col
+ when 0 then UIng::Table::Value.new(plugin.name || plugin.id)
+ when 1 then UIng::Table::Value.new(plugin.id)
+ when 2 then UIng::Table::Value.new(plugin.version)
+ when 3 then UIng::Table::Value.new(plugin.since || "—")
+ else UIng::Table::Value.new(plugin.until_build || "—")
end
- scanned = JBUpdater::PluginMeta.scan_dir(dir) rescue nil
- if scanned
- old_count = App.installed_plugins_arr.size
- App.installed_plugins = scanned
- if old_count == 0
- App.installed_plugins_arr.each_with_index { |_, i| App.installed_model.try &.row_inserted(i) }
- else
- (0...[App.installed_plugins_arr.size, old_count].min).each { |i| App.installed_model.try &.row_changed(i) }
- if App.installed_plugins_arr.size > old_count
- (old_count...App.installed_plugins_arr.size).each { |i| App.installed_model.try &.row_inserted(i) }
- elsif App.installed_plugins_arr.size < old_count
- (App.installed_plugins_arr.size...old_count).reverse_each { |i| App.installed_model.try &.row_deleted(i) }
- end
- end
- installed_status.text = "Found #{scanned.size} installed plugins"
+ }
+end
+installed_model = UIng::Table::Model.new(installed_handler)
+App.installed_table = installed_table = UIng::Table.new(installed_model)
+App.installed_model = installed_model
+installed_table.header_visible = true
+installed_table.append_text_column("Name", 0, -1)
+installed_table.append_text_column("Plugin ID", 1, -1)
+installed_table.append_text_column("Version", 2, -1)
+installed_table.append_text_column("Since Build", 3, -1)
+installed_table.append_text_column("Until Build", 4, -1)
+installed_table.selection_mode = :one
+
+installed_actions = UIng::Box.new(:horizontal)
+installed_actions.padded = true
+
+btn_scan_installed = UIng::Button.new("Scan")
+btn_uninstall = UIng::Button.new("Uninstall selected")
+btn_uninstall.disable
+
+installed_actions.append(btn_scan_installed, false)
+installed_actions.append(btn_uninstall, false)
+
+installed_status = UIng::Label.new("Click Scan to list installed plugins")
+
+installed_tab.append(installed_actions, false)
+installed_tab.append(installed_table, true)
+installed_tab.append(installed_status, false)
+
+tabs.append("Installed", installed_tab)
+
+btn_scan_installed.on_clicked do
+ dir = expand_tilde(e_plugins_dir.text) || e_plugins_dir.text || ""
+ if dir.empty?
+ installed_status.text = "Set Plugins Directory first"
+ next
+ end
+ scanned = JBUpdater::PluginMeta.scan_dir(dir) rescue nil
+ if scanned
+ old_count = App.installed_plugins_arr.size
+ App.installed_plugins = scanned
+ if old_count == 0
+ App.installed_plugins_arr.each_with_index { |_, i| App.installed_model.try &.row_inserted(i) }
else
- installed_status.text = "Error scanning plugins directory"
+ (0...[App.installed_plugins_arr.size, old_count].min).each { |i| App.installed_model.try &.row_changed(i) }
+ if App.installed_plugins_arr.size > old_count
+ (old_count...App.installed_plugins_arr.size).each { |i| App.installed_model.try &.row_inserted(i) }
+ elsif App.installed_plugins_arr.size < old_count
+ (App.installed_plugins_arr.size...old_count).reverse_each { |i| App.installed_model.try &.row_deleted(i) }
+ end
end
+ installed_status.text = "Found #{scanned.size} installed plugins"
+ else
+ installed_status.text = "Error scanning plugins directory"
end
+end
- installed_table.on_selection_changed do |selection|
- if selection.num_rows > 0
- btn_uninstall.enable
- else
- btn_uninstall.disable
- end
+installed_table.on_selection_changed do |selection|
+ if selection.num_rows > 0
+ btn_uninstall.enable
+ else
+ btn_uninstall.disable
end
+end
- btn_uninstall.on_clicked do
- UIng.queue_main do
- installed_table.selection do |sel|
- next if sel.num_rows == 0
- row = sel.rows[0]
- plugin = App.installed_plugins_arr[row]?
- if plugin
- FileUtils.rm_rf(plugin.path)
- scanned = JBUpdater::PluginMeta.scan_dir(File.dirname(plugin.path)) rescue nil
- old_count = App.installed_plugins_arr.size
- App.installed_plugins = scanned
- if old_count == 0
- App.installed_plugins_arr.each_with_index { |_, i| App.installed_model.try &.row_inserted(i) }
- else
- (0...[App.installed_plugins_arr.size, old_count].min).each { |i| App.installed_model.try &.row_changed(i) }
- if App.installed_plugins_arr.size > old_count
- (old_count...App.installed_plugins_arr.size).each { |i| App.installed_model.try &.row_inserted(i) }
- elsif App.installed_plugins_arr.size < old_count
- (App.installed_plugins_arr.size...old_count).reverse_each { |i| App.installed_model.try &.row_deleted(i) }
- end
+btn_uninstall.on_clicked do
+ UIng.queue_main do
+ installed_table.selection do |sel|
+ next if sel.num_rows == 0
+ row = sel.rows[0]
+ plugin = App.installed_plugins_arr[row]?
+ if plugin
+ FileUtils.rm_rf(plugin.path)
+ scanned = JBUpdater::PluginMeta.scan_dir(File.dirname(plugin.path)) rescue nil
+ old_count = App.installed_plugins_arr.size
+ App.installed_plugins = scanned
+ if old_count == 0
+ App.installed_plugins_arr.each_with_index { |_, i| App.installed_model.try &.row_inserted(i) }
+ else
+ (0...[App.installed_plugins_arr.size, old_count].min).each { |i| App.installed_model.try &.row_changed(i) }
+ if App.installed_plugins_arr.size > old_count
+ (old_count...App.installed_plugins_arr.size).each { |i| App.installed_model.try &.row_inserted(i) }
+ elsif App.installed_plugins_arr.size < old_count
+ (App.installed_plugins_arr.size...old_count).reverse_each { |i| App.installed_model.try &.row_deleted(i) }
end
- installed_status.text = "Deleted: #{plugin.id}"
- btn_uninstall.disable
end
+ installed_status.text = "Deleted: #{plugin.id}"
+ btn_uninstall.disable
end
end
end
+end
- # --- IDE tab --------------------------------------------------------
- ide_tab = UIng::Box.new(:vertical)
- ide_tab.padded = true
-
- ide_group = UIng::Group.new("IDE Configuration", margined: true)
- ide_form = UIng::Form.new
- ide_form.padded = true
+# --- IDE tab --------------------------------------------------------
+ide_tab = UIng::Box.new(:vertical)
+ide_tab.padded = true
- e_ide_product = UIng::Entry.new
- e_ide_path = UIng::Entry.new
+ide_group = UIng::Group.new("IDE Configuration", margined: true)
+ide_form = UIng::Form.new
+ide_form.padded = true
- ide_form.append("IDE code or name", e_ide_product, false)
- ide_form.append("IDE Path", e_ide_path, true)
- ide_group.child = ide_form
- ide_tab.append(ide_group, false)
+e_ide_product = UIng::Entry.new
+e_ide_path = UIng::Entry.new
- chk_brew = UIng::Checkbox.new("Patch Homebrew cask (macOS)")
- ide_tab.append(chk_brew, false)
+ide_form.append("IDE code or name", e_ide_product, false)
+ide_form.append("IDE Path", e_ide_path, true)
+ide_group.child = ide_form
+ide_tab.append(ide_group, false)
- ide_actions = UIng::Box.new(:vertical)
- ide_actions.padded = true
+chk_brew = UIng::Checkbox.new("Patch Homebrew cask (macOS)")
+ide_tab.append(chk_brew, false)
- btn_list_releases = UIng::Button.new("List releases")
- btn_upgrade = UIng::Button.new("Upgrade IDE")
+ide_actions = UIng::Box.new(:vertical)
+ide_actions.padded = true
- ide_actions.append(btn_list_releases, false)
- ide_actions.append(btn_upgrade, false)
- ide_tab.append(ide_actions, false)
- tabs.append("IDE", ide_tab)
+btn_list_releases = UIng::Button.new("List releases")
+btn_upgrade = UIng::Button.new("Upgrade IDE")
- combo_products.on_selected do
- UIng.queue_main do
- idx = combo_products.selected
- if idx > 0
- prod = detected[idx - 1]
- log.append("[GUI] Selected product: #{prod.name} (#{prod.build})\n")
+ide_actions.append(btn_list_releases, false)
+ide_actions.append(btn_upgrade, false)
+ide_tab.append(ide_actions, false)
+tabs.append("IDE", ide_tab)
- if dir = prod.plugins_dir
- e_plugins_dir.text = dir
- end
- e_product.text = prod.name
- e_ide_product.text = prod.build
- if path = prod.ide_path
- e_ide_path.text = path
- end
+combo_products.on_selected do
+ UIng.queue_main do
+ idx = combo_products.selected
+ if idx > 0
+ prod = detected[idx - 1]
+ log.append("[GUI] Selected product: #{prod.name} (#{prod.build})\n")
- status_label.text = "Selected: #{prod.name}"
- save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
- else
- status_label.text = "Product selection: manual/custom"
+ if dir = prod.plugins_dir
+ e_plugins_dir.text = dir
+ end
+ e_product.text = prod.name
+ e_ide_product.text = prod.build
+ if path = prod.ide_path
+ e_ide_path.text = path
end
- end
- end
-
- all_buttons = [] of UIng::Button
- all_buttons.concat([btn_list, btn_install, btn_update])
- all_buttons.concat([btn_list_releases, btn_upgrade])
- App.set_widgets(log, overall_bar, plugin_bar, all_buttons)
- # Global timer: drain buffered log messages and update progress bars
- UIng.timer(150) do
- App.drain_log_buffer.each do |msg|
- App.log.append(msg + "\n")
+ status_label.text = "Selected: #{prod.name}"
+ save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
+ else
+ status_label.text = "Product selection: manual/custom"
end
- pct = App.read_progress
- App.plugin_progress.value = pct if pct > 0
- 1
end
+end
- apply_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry, log)
- apply_ide_settings(e_ide_product, e_ide_path, chk_brew)
+all_buttons = [] of UIng::Button
+all_buttons.concat([btn_list, btn_install, btn_update])
+all_buttons.concat([btn_list_releases, btn_upgrade])
+App.set_widgets(log, overall_bar, plugin_bar, all_buttons)
- idx = combo_products.selected
- if idx > 0 && idx <= detected.size
- prod = detected[idx - 1]
- e_ide_product.text = prod.build
- log.append("[GUI] Restored product: #{prod.name} (#{prod.build})\n")
+# Global timer: drain buffered log messages and update progress bars
+UIng.timer(150) do
+ App.drain_log_buffer.each do |msg|
+ App.log.append(msg + "\n")
end
+ pct = App.read_progress
+ App.plugin_progress.value = pct if pct > 0
+ 1
+end
- log.append("JB Updater GUI ready. Select a detected IDE or enter paths manually.\n")
- status_label.text = "Ready"
+apply_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry, log)
+apply_ide_settings(e_ide_product, e_ide_path, chk_brew)
- {% if flag?(:gui_log) %}
- btn_remove_cache.on_clicked do
- UIng.queue_main do
- raw = e_plugins_dir.text
- if raw.nil? || raw.empty?
- log.append("ERROR: Plugins dir is required for Remove cache.\n")
- status_label.text = "Error: missing plugins dir"
- else
- plugins_dir = expand_tilde(raw) || raw
- if !Dir.exists?(plugins_dir)
- log.append("ERROR: Plugins dir '#{plugins_dir}' does not exist.\n")
- status_label.text = "Error: dir not found"
- else
- begin
- removed = 0
- Dir.each_child(plugins_dir) do |entry|
- if entry.includes?(".bak")
- path = File.join(plugins_dir, entry)
- FileUtils.rm_rf(path)
- removed += 1
- log.append("Removed backup: #{path}\n")
- end
- end
+idx = combo_products.selected
+if idx > 0 && idx <= detected.size
+ prod = detected[idx - 1]
+ e_ide_product.text = prod.build
+ log.append("[GUI] Restored product: #{prod.name} (#{prod.build})\n")
+end
- if removed == 0
- log.append("No *.bak* backup entries found under #{plugins_dir}\n")
- status_label.text = "No backups found"
- else
- log.append("Removed #{removed} backup entr#{removed == 1 ? "y" : "ies"} under #{plugins_dir}\n")
- status_label.text = "Removed #{removed} backup(s)"
- end
- rescue ex
- log.append("ERROR while removing cache: #{ex.class}: #{ex.message}\n")
- status_label.text = "Error during cache removal"
- end
- end
- end
- end
- end
- {% end %}
+log.append("JB Updater GUI ready. Select a detected IDE or enter paths manually.\n")
+status_label.text = "Ready"
- btn_list.on_clicked do
+{% if flag?(:gui_log) %}
+ btn_remove_cache.on_clicked do
UIng.queue_main do
raw = e_plugins_dir.text
if raw.nil? || raw.empty?
- log.append("ERROR: Plugins dir is required for List installed plugins.\n")
+ log.append("ERROR: Plugins dir is required for Remove cache.\n")
status_label.text = "Error: missing plugins dir"
else
- plugins_dir = expand_tilde(raw)
- e_plugins_dir.text = plugins_dir if plugins_dir
- args = build_args(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, chk_dry, UIng::Checkbox.new("")) + ["--list"]
- new_run_header("List installed plugins", args)
- run_cli(args)
- save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
+ plugins_dir = expand_tilde(raw) || raw
+ if !Dir.exists?(plugins_dir)
+ log.append("ERROR: Plugins dir '#{plugins_dir}' does not exist.\n")
+ status_label.text = "Error: dir not found"
+ else
+ begin
+ removed = 0
+ Dir.each_child(plugins_dir) do |entry|
+ if entry.includes?(".bak")
+ path = File.join(plugins_dir, entry)
+ FileUtils.rm_rf(path)
+ removed += 1
+ log.append("Removed backup: #{path}\n")
+ end
+ end
+
+ if removed == 0
+ log.append("No *.bak* backup entries found under #{plugins_dir}\n")
+ status_label.text = "No backups found"
+ else
+ log.append("Removed #{removed} backup entr#{removed == 1 ? "y" : "ies"} under #{plugins_dir}\n")
+ status_label.text = "Removed #{removed} backup(s)"
+ end
+ rescue ex
+ log.append("ERROR while removing cache: #{ex.class}: #{ex.message}\n")
+ status_label.text = "Error during cache removal"
+ end
+ end
end
end
end
+{% end %}
- btn_install.on_clicked do
- UIng.queue_main do
- raw = e_plugins_dir.text
- if raw.nil? || raw.empty?
- log.append("ERROR: Plugins dir is required for Install plugins.\n")
- status_label.text = "Error: missing plugins dir"
- else
- plugins_dir = expand_tilde(raw)
- e_plugins_dir.text = plugins_dir if plugins_dir
- args = build_args(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, chk_dry, UIng::Checkbox.new(""))
- new_run_header("Install plugins", args)
- run_cli(args)
- save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
- end
+btn_list.on_clicked do
+ UIng.queue_main do
+ raw = e_plugins_dir.text
+ if raw.nil? || raw.empty?
+ log.append("ERROR: Plugins dir is required for List installed plugins.\n")
+ status_label.text = "Error: missing plugins dir"
+ else
+ plugins_dir = expand_tilde(raw)
+ e_plugins_dir.text = plugins_dir if plugins_dir
+ args = build_args(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, chk_dry, UIng::Checkbox.new("")) + ["--list"]
+ new_run_header("List installed plugins", args)
+ run_cli(args)
+ save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
end
end
+end
- btn_update.on_clicked do
- UIng.queue_main do
- raw = e_plugins_dir.text
- if raw.nil? || raw.empty?
- log.append("ERROR: Plugins dir is required for Update plugins.\n")
- status_label.text = "Error: missing plugins dir"
- else
- plugins_dir = expand_tilde(raw)
- e_plugins_dir.text = plugins_dir if plugins_dir
- args = build_args(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, chk_dry, UIng::Checkbox.new(""))
- new_run_header("Update plugins", args)
- run_cli(args)
- save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
- end
+btn_install.on_clicked do
+ UIng.queue_main do
+ raw = e_plugins_dir.text
+ if raw.nil? || raw.empty?
+ log.append("ERROR: Plugins dir is required for Install plugins.\n")
+ status_label.text = "Error: missing plugins dir"
+ else
+ plugins_dir = expand_tilde(raw)
+ e_plugins_dir.text = plugins_dir if plugins_dir
+ args = build_args(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, chk_dry, UIng::Checkbox.new(""))
+ new_run_header("Install plugins", args)
+ run_cli(args)
+ save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
end
end
+end
- btn_list_releases.on_clicked do
- UIng.queue_main do
- product = e_ide_product.text
- if product.nil? || product.empty?
- log.append("ERROR: IDE code is required for List releases (e.g., WS, RM).\n")
- status_label.text = "Error: missing IDE code"
- else
- args = ["--list-ide-releases", "--product", product]
- new_run_header("List IDE releases", args)
- run_cli(args)
- save_ide_settings(e_ide_product, e_ide_path, chk_brew)
- end
+btn_update.on_clicked do
+ UIng.queue_main do
+ raw = e_plugins_dir.text
+ if raw.nil? || raw.empty?
+ log.append("ERROR: Plugins dir is required for Update plugins.\n")
+ status_label.text = "Error: missing plugins dir"
+ else
+ plugins_dir = expand_tilde(raw)
+ e_plugins_dir.text = plugins_dir if plugins_dir
+ args = build_args(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, chk_dry, UIng::Checkbox.new(""))
+ new_run_header("Update plugins", args)
+ run_cli(args)
+ save_plugins_settings(e_plugins_dir, e_build, e_product, e_install_ids, combo_arch, combo_products, chk_dry)
end
end
+end
- btn_upgrade.on_clicked do
- UIng.queue_main do
- args = ["--upgrade-ide"]
-
- ide_product = e_ide_product.text
- ide_path = e_ide_path.text
-
- args += ["--product", ide_product] if ide_product && !ide_product.empty?
- args += ["--ide-path", ide_path] if ide_path && !ide_path.empty?
- args << "--brew" if chk_brew.checked?
-
- new_run_header("Upgrade IDE", args)
+btn_list_releases.on_clicked do
+ UIng.queue_main do
+ product = e_ide_product.text
+ if product.nil? || product.empty?
+ log.append("ERROR: IDE code is required for List releases (e.g., WS, RM).\n")
+ status_label.text = "Error: missing IDE code"
+ else
+ args = ["--list-ide-releases", "--product", product]
+ new_run_header("List IDE releases", args)
run_cli(args)
save_ide_settings(e_ide_product, e_ide_path, chk_brew)
end
end
+end
- resolve_build = -> : String {
- products = App.detected_products || JBUpdater::DetectProducts.all
- result = JBUpdater::GUI::Actions.resolve_build(e_ide_product.text, e_build.text, products)
- if result != e_ide_product.text && result != e_build.text
- log.append("[Browse] Auto-detected build: #{result}\n")
- end
- result
- }
+btn_upgrade.on_clicked do
+ UIng.queue_main do
+ args = ["--upgrade-ide"]
- # Preload installed plugins on main thread at startup
- load_installed_for_browse = -> {
- App.installed_plugins = nil
- raw = e_plugins_dir.text
- if raw && !raw.empty?
- dir = expand_tilde(raw) || raw
- App.installed_plugins = JBUpdater::PluginMeta.scan_dir(dir) rescue nil
- end
- }
- load_installed_for_browse.call
+ ide_product = e_ide_product.text
+ ide_path = e_ide_path.text
- # Populate installed tab model with startup data
- inst = App.installed_plugins
- if inst && inst.size > 0
- App.installed_plugins_arr.each_with_index { |_, i| App.installed_model.try &.row_inserted(i) }
- end
+ args += ["--product", ide_product] if ide_product && !ide_product.empty?
+ args += ["--ide-path", ide_path] if ide_path && !ide_path.empty?
+ args << "--brew" if chk_brew.checked?
- # Warm marketplace cache after UI is visible (1s delay)
- UIng.timer(1_000) {
- build = resolve_build.call
- JBUpdater::PluginMarketplace.list_by_build(build)
- log.append("[Browse] Marketplace cache warmed: #{build}\n")
- 0
- }
+ new_run_header("Upgrade IDE", args)
+ run_cli(args)
+ save_ide_settings(e_ide_product, e_ide_path, chk_brew)
+ end
+end
- search_entry.on_changed do |text|
- begin
- query = text || ""
- if query.empty?
- model = App.browse_table_model
- if model
- old_count = App.browse_plugins.size
- App.browse_plugins = [] of JBUpdater::PluginInfo
- (0...old_count).each { |i| model.row_deleted(0) }
- end
- App.selected_xml_id = nil
- browse_status.text = "Type to search plugins..."
- next
- end
+resolve_build = -> : String {
+ products = App.detected_products || JBUpdater::DetectProducts.all
+ result = JBUpdater::GUI::Actions.resolve_build(e_ide_product.text, e_build.text, products)
+ if result != e_ide_product.text && result != e_build.text
+ log.append("[Browse] Auto-detected build: #{result}\n")
+ end
+ result
+}
+
+# Preload installed plugins on main thread at startup
+load_installed_for_browse = -> {
+ App.installed_plugins = nil
+ raw = e_plugins_dir.text
+ if raw && !raw.empty?
+ dir = expand_tilde(raw) || raw
+ App.installed_plugins = JBUpdater::PluginMeta.scan_dir(dir) rescue nil
+ end
+}
+load_installed_for_browse.call
+
+# Populate installed tab model with startup data
+inst = App.installed_plugins
+if inst && inst.size > 0
+ App.installed_plugins_arr.each_with_index { |_, i| App.installed_model.try &.row_inserted(i) }
+end
- build = resolve_build.call
- plugins = JBUpdater::PluginMarketplace.search(query, build)
+# Warm marketplace cache after UI is visible (1s delay)
+UIng.timer(1_000) {
+ build = resolve_build.call
+ JBUpdater::PluginMarketplace.list_by_build(build)
+ log.append("[Browse] Marketplace cache warmed: #{build}\n")
+ 0
+}
+search_entry.on_changed do |text|
+ begin
+ query = text || ""
+ if query.empty?
model = App.browse_table_model
- next unless model
-
- old_count = App.browse_plugins.size
- App.browse_plugins = plugins
- if old_count == 0
- plugins.each_with_index { |_, i| model.row_inserted(i) }
- elsif plugins.size >= old_count
- (0...old_count).each { |i| model.row_changed(i) }
- (old_count...plugins.size).each { |i| model.row_inserted(i) }
- else
- (0...plugins.size).each { |i| model.row_changed(i) }
- (plugins.size...old_count).reverse_each { |i| model.row_deleted(i) }
+ if model
+ old_count = App.browse_plugins.size
+ App.browse_plugins = [] of JBUpdater::PluginInfo
+ (0...old_count).each { |i| model.row_deleted(0) }
end
- browse_status.text = "Found #{plugins.size} results"
- rescue ex
- log.append("[Browse] Search error: #{ex.class}: #{ex.message}\n")
- browse_status.text = "Search error: #{ex.class} #{ex.message}"
+ App.selected_xml_id = nil
+ browse_status.text = "Type to search plugins..."
+ next
end
- end
- btn_top.on_clicked do
build = resolve_build.call
- browse_status.text = "Fetching top plugins (may lag)..."
- log.append("[Browse] Fetching top downloaded for build #{build}...\n")
- plugins = JBUpdater::PluginMarketplace.top_downloaded(build, 100)
- log.append("[Browse] Got #{plugins.size} plugins, updating table...\n")
- plugins.first(3).each { |plugin| log.append(" #{plugin.name} (#{plugin.downloads} dl)\n") }
+ plugins = JBUpdater::PluginMarketplace.search(query, build)
model = App.browse_table_model
next unless model
@@ -1310,107 +1304,135 @@ UIng.init do
(0...plugins.size).each { |i| model.row_changed(i) }
(plugins.size...old_count).reverse_each { |i| model.row_deleted(i) }
end
- browse_status.text = "Loaded #{plugins.size} plugins (top downloads)"
+ browse_status.text = "Found #{plugins.size} results"
+ rescue ex
+ log.append("[Browse] Search error: #{ex.class}: #{ex.message}\n")
+ browse_status.text = "Search error: #{ex.class} #{ex.message}"
end
+end
- btn_newest.on_clicked do
- build = resolve_build.call
- browse_status.text = "Fetching latest plugins..."
- log.append("[Browse] Fetching newest for build #{build}...\n")
- plugins = JBUpdater::PluginMarketplace.newest(build, 100)
- log.append("[Browse] Got #{plugins.size} plugins, updating table...\n")
- plugins.first(3).each { |plugin| log.append(" #{plugin.name} (#{plugin.downloads} dl)\n") }
+btn_top.on_clicked do
+ build = resolve_build.call
+ browse_status.text = "Fetching top plugins (may lag)..."
+ log.append("[Browse] Fetching top downloaded for build #{build}...\n")
+ plugins = JBUpdater::PluginMarketplace.top_downloaded(build, 100)
+ log.append("[Browse] Got #{plugins.size} plugins, updating table...\n")
+ plugins.first(3).each { |plugin| log.append(" #{plugin.name} (#{plugin.downloads} dl)\n") }
+
+ model = App.browse_table_model
+ next unless model
+
+ old_count = App.browse_plugins.size
+ App.browse_plugins = plugins
+ if old_count == 0
+ plugins.each_with_index { |_, i| model.row_inserted(i) }
+ elsif plugins.size >= old_count
+ (0...old_count).each { |i| model.row_changed(i) }
+ (old_count...plugins.size).each { |i| model.row_inserted(i) }
+ else
+ (0...plugins.size).each { |i| model.row_changed(i) }
+ (plugins.size...old_count).reverse_each { |i| model.row_deleted(i) }
+ end
+ browse_status.text = "Loaded #{plugins.size} plugins (top downloads)"
+end
- model = App.browse_table_model
- next unless model
+btn_newest.on_clicked do
+ build = resolve_build.call
+ browse_status.text = "Fetching latest plugins..."
+ log.append("[Browse] Fetching newest for build #{build}...\n")
+ plugins = JBUpdater::PluginMarketplace.newest(build, 100)
+ log.append("[Browse] Got #{plugins.size} plugins, updating table...\n")
+ plugins.first(3).each { |plugin| log.append(" #{plugin.name} (#{plugin.downloads} dl)\n") }
+
+ model = App.browse_table_model
+ next unless model
+
+ old_count = App.browse_plugins.size
+ App.browse_plugins = plugins
+ if old_count == 0
+ plugins.each_with_index { |_, i| model.row_inserted(i) }
+ elsif plugins.size >= old_count
+ (0...old_count).each { |i| model.row_changed(i) }
+ (old_count...plugins.size).each { |i| model.row_inserted(i) }
+ else
+ (0...plugins.size).each { |i| model.row_changed(i) }
+ (plugins.size...old_count).each { |i| model.row_deleted(i) }
+ end
+ browse_status.text = "Loaded #{plugins.size} plugins (latest)"
+end
- old_count = App.browse_plugins.size
- App.browse_plugins = plugins
- if old_count == 0
- plugins.each_with_index { |_, i| model.row_inserted(i) }
- elsif plugins.size >= old_count
- (0...old_count).each { |i| model.row_changed(i) }
- (old_count...plugins.size).each { |i| model.row_inserted(i) }
- else
- (0...plugins.size).each { |i| model.row_changed(i) }
- (plugins.size...old_count).each { |i| model.row_deleted(i) }
+btn_refresh.on_clicked do
+ UIng.queue_main do
+ App.installed_plugins = nil
+ model = App.browse_table_model
+ if model
+ old_count = App.browse_plugins.size
+ (0...old_count).each { |i| model.row_deleted(i) }
+ App.browse_plugins = [] of JBUpdater::PluginInfo
+ App.selected_xml_id = nil
+ JBUpdater::PluginMarketplace.clear_cache
+ browse_status.text = "Cache cleared. Click Top/Refresh to reload."
end
- browse_status.text = "Loaded #{plugins.size} plugins (latest)"
end
+end
- btn_refresh.on_clicked do
- UIng.queue_main do
- App.installed_plugins = nil
- model = App.browse_table_model
- if model
- old_count = App.browse_plugins.size
- (0...old_count).each { |i| model.row_deleted(i) }
- App.browse_plugins = [] of JBUpdater::PluginInfo
- App.selected_xml_id = nil
- JBUpdater::PluginMarketplace.clear_cache
- browse_status.text = "Cache cleared. Click Top/Refresh to reload."
- end
- end
+browse_table.on_selection_changed do |selection|
+ row = selection.num_rows > 0 ? selection.rows[0] : -1
+ plugin = row >= 0 ? App.browse_plugins[row]? : nil
+ if plugin
+ App.selected_xml_id = plugin.xml_id
+ stripped = plugin.description[0, 500]
+ preview = stripped[0, 500]
+ App.log.append("[Browse] detail: #{preview.size}B #{preview.count('\n')} lines (#{preview.size - preview.count('\n')} non-newline)\n")
+ App.safe_set_text(browse_detail, preview)
+ else
+ App.safe_set_text(browse_detail, "Select a plugin to view details")
+ App.selected_xml_id = nil
end
+end
- browse_table.on_selection_changed do |selection|
- row = selection.num_rows > 0 ? selection.rows[0] : -1
- plugin = row >= 0 ? App.browse_plugins[row]? : nil
- if plugin
- App.selected_xml_id = plugin.xml_id
- stripped = plugin.description[0, 500]
- preview = stripped[0, 500]
- App.log.append("[Browse] detail: #{preview.size}B #{preview.count('\n')} lines (#{preview.size - preview.count('\n')} non-newline)\n")
- App.safe_set_text(browse_detail, preview)
- else
- App.safe_set_text(browse_detail, "Select a plugin to view details")
- App.selected_xml_id = nil
- end
+btn_install_browse.on_clicked do
+ xml_id = App.selected_xml_id
+ if xml_id.nil? || xml_id.empty?
+ browse_status.text = "Please select a plugin first"
+ next
end
- btn_install_browse.on_clicked do
- xml_id = App.selected_xml_id
- if xml_id.nil? || xml_id.empty?
- browse_status.text = "Please select a plugin first"
- next
- end
+ plugins_dir = e_plugins_dir.text
+ if plugins_dir.nil? || plugins_dir.empty?
+ browse_status.text = "Error: plugins dir not set. Switch to Plugins tab."
+ next
+ end
- plugins_dir = e_plugins_dir.text
- if plugins_dir.nil? || plugins_dir.empty?
- browse_status.text = "Error: plugins dir not set. Switch to Plugins tab."
- next
- end
+ build = resolve_build.call
- build = resolve_build.call
+ log.append("[Browse] Installing plugin: #{xml_id} for build #{build}\n")
- log.append("[Browse] Installing plugin: #{xml_id} for build #{build}\n")
+ queue_install(xml_id, plugins_dir, build)
+end
- queue_install(xml_id, plugins_dir, build)
+btn_copy_id.on_clicked do
+ xml_id = App.selected_xml_id
+ if xml_id.nil? || xml_id.empty?
+ browse_status.text = "Please select a plugin first"
+ else
+ log.append("[Browse] Copied XML ID: #{xml_id}\n")
+ browse_status.text = "Copied to clipboard: #{xml_id}"
end
+end
- btn_copy_id.on_clicked do
- xml_id = App.selected_xml_id
- if xml_id.nil? || xml_id.empty?
- browse_status.text = "Please select a plugin first"
- else
- log.append("[Browse] Copied XML ID: #{xml_id}\n")
- browse_status.text = "Copied to clipboard: #{xml_id}"
+{% if flag?(:darwin) %}
+ constraint_added = false
+ tabs.on_selected do |idx|
+ if idx == 1 && !constraint_added
+ constraint_added = true
+ right_view = browse_detail_box.handle
+ super_view = browse_content.handle
+ c = LayoutHelper.create_width_constraint(right_view, super_view, 0.3_f64)
+ LayoutHelper.add_constraint_to_view(super_view, c)
end
end
+{% end %}
- {% if flag?(:darwin) %}
- constraint_added = false
- tabs.on_selected do |idx|
- if idx == 1 && !constraint_added
- constraint_added = true
- right_view = browse_detail_box.handle
- super_view = browse_content.handle
- c = LayoutHelper.create_width_constraint(right_view, super_view, 0.3_f64)
- LayoutHelper.add_constraint_to_view(super_view, c)
- end
- end
- {% end %}
-
- window.show
- UIng.main
-end
+window.show
+UIng.main
diff --git a/jb_updater/src/jb_updater/cli.cr b/jb_updater/src/jb_updater/cli.cr
index caf51d7..4b5be09 100644
--- a/jb_updater/src/jb_updater/cli.cr
+++ b/jb_updater/src/jb_updater/cli.cr
@@ -89,10 +89,10 @@ module JBUpdater
# - `-h` / `--help`
#
# @return [Options] Parsed CLI options
- def self.parse_cli : Options
+ def self.parse_cli(argv = ARGV) : Options
opts = Options.new
- OptionParser.parse do |parser|
+ OptionParser.parse(argv) do |parser|
parser.banner = "Usage: jb_updater [options]"
parser.on("--plugins-dir DIR", "Plugins directory") { |v| opts.plugins_dir = v }
parser.on("-b", "--build BUILD", "IDE build") { |v| opts.build = v }
diff --git a/jb_updater/src/jb_updater/plugin_marketplace.cr b/jb_updater/src/jb_updater/plugin_marketplace.cr
index e67fd7f..2fee775 100644
--- a/jb_updater/src/jb_updater/plugin_marketplace.cr
+++ b/jb_updater/src/jb_updater/plugin_marketplace.cr
@@ -3,7 +3,10 @@ require "http/client"
require "xml"
module JBUpdater
- # Strips HTML tags and decodes entities from a string.
+ # Strips HTML tags and decodes common HTML entities from a string.
+ #
+ # @param html [String] Raw HTML input
+ # @return [String] Plain text with tags removed and entities decoded
def self.html_strip(html : String) : String
text = html.gsub(/<[^>]*>/, " ")
.gsub("&", "&")
@@ -60,7 +63,7 @@ module JBUpdater
@name : String,
@description : String,
@icon : String? = nil,
- @categories : Array(String)? = nil,
+ @categories : Array(String) = [] of String,
@downloads : Int64 = 0,
@rating : Float64 = 0.0,
@author : String? = nil,
@@ -68,8 +71,6 @@ module JBUpdater
@vendor : String? = nil,
@preview : String? = nil,
)
- @categories ||= [] of String
- @tags ||= [] of String
end
# Parses the JetBrains Marketplace XML response into an array of `PluginInfo`.
diff --git a/jb_updater/tools/generate-icons.sh b/jb_updater/tools/generate-icons.sh
new file mode 100755
index 0000000..70feff8
--- /dev/null
+++ b/jb_updater/tools/generate-icons.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+ASSETS="$ROOT/assets"
+ICONSET="$ASSETS/jb_updater.iconset"
+
+rm -rf "$ICONSET"
+mkdir -p "$ICONSET"
+
+SIZES=(16 32 128 256 512)
+for s in "${SIZES[@]}"; do
+ rsvg-convert -w "$s" -h "$s" "$ASSETS/jb_updater.svg" -o "$ICONSET/icon_${s}x${s}.png"
+ rsvg-convert -w "$((s*2))" -h "$((s*2))" "$ASSETS/jb_updater.svg" -o "$ICONSET/icon_${s}x${s}@2x.png"
+done
+
+iconutil -c icns "$ICONSET" -o "$ASSETS/jb_updater.icns"
+
+rm -rf "$ICONSET"
+
+for s in 16 32 48 128 256 512; do
+ rsvg-convert -w "$s" -h "$s" "$ASSETS/jb_updater.svg" -o "$ASSETS/jb_updater_${s}x${s}.png"
+done
+
+magick "$ASSETS/jb_updater_256x256.png" "$ASSETS/jb_updater_48x48.png" \
+ "$ASSETS/jb_updater_32x32.png" "$ASSETS/jb_updater_16x16.png" \
+ "$ASSETS/jb_updater.ico" 2>/dev/null
+
+echo "Icons in $ASSETS:"
+ls -lh "$ASSETS"/jb_updater*
diff --git a/tampermonkey/redirect_on_451.user.js b/tampermonkey/redirect_on_451.user.js
index 31918d0..830c49a 100644
--- a/tampermonkey/redirect_on_451.user.js
+++ b/tampermonkey/redirect_on_451.user.js
@@ -9,23 +9,23 @@
// @grant none
// ==/UserScript==
-(function() {
- 'use strict';
- fetch(window.location.href, { method: 'HEAD' })
- .then(response => {
- if (response.status === 451) {
- let originalUrl = window.location.href;
- let newUrl = null;
- if (originalUrl.match(/plugins.jetbrains.com/)) {
- newUrl = window.location.href.replace('plugins.jetbrains.com', 'downloads.marketplace.jetbrains.com');
- } else if (originalUrl.match(/download.jetbrains.com/)) {
- newUrl = window.location.href.replace('download.jetbrains.com', 'download-cdn.jetbrains.com');
- }
- window.location.replace(newUrl);
+(function () {
+ 'use strict';
+ fetch(window.location.href, {method: 'HEAD'})
+ .then(response => {
+ if (response.status === 451) {
+ let originalUrl = window.location.href;
+ let newUrl = null;
+ if (originalUrl.match(/plugins.jetbrains.com/)) {
+ newUrl = window.location.href.replace('plugins.jetbrains.com', 'downloads.marketplace.jetbrains.com');
+ } else if (originalUrl.match(/download.jetbrains.com/)) {
+ newUrl = window.location.href.replace('download.jetbrains.com', 'download-cdn.jetbrains.com');
}
+ window.location.replace(newUrl);
+ }
})
- .catch(error => {
- console.error('Error checking status:', error);
+ .catch(error => {
+ console.error('Error checking status:', error);
});
})();