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("&lt;test&gt;").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); }); })();