<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roman Kalkowski</title>
    <description>The latest articles on DEV Community by Roman Kalkowski (@spicyromek).</description>
    <link>https://dev.to/spicyromek</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4004022%2F036abd63-9881-48e8-b712-26213f949e34.jpg</url>
      <title>DEV Community: Roman Kalkowski</title>
      <link>https://dev.to/spicyromek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spicyromek"/>
    <language>en</language>
    <item>
      <title>SWT and Eclipse RCP on the Web: What AI Could and Couldn't Port</title>
      <dc:creator>Roman Kalkowski</dc:creator>
      <pubDate>Tue, 11 Aug 2026 14:57:00 +0000</pubDate>
      <link>https://dev.to/vaadin/swt-and-eclipse-rcp-on-the-web-what-ai-could-and-couldnt-port-29p5</link>
      <guid>https://dev.to/vaadin/swt-and-eclipse-rcp-on-the-web-what-ai-could-and-couldnt-port-29p5</guid>
      <description>&lt;p&gt;I came to RSS about twenty years late. When I finally went looking for a reader, one beloved old Eclipse desktop app kept turning up on every "best of" list, and it wouldn't even launch on my Mac. So I pointed Claude Code at it and rebuilt its main screen for the browser in Vaadin 25.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get from this article
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The experiment.&lt;/strong&gt; Port the main screen of RSSOwl, a 20-year-old Eclipse RCP and SWT feed reader, to the web with Vaadin 25 and Claude Code. Java server-side UI, not a JavaScript rewrite. That was the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The result.&lt;/strong&gt; The whole three-pane screen runs in a browser tab, and it took a couple of weeks of focused evenings. Two of RSSOwl's densest classes, 306 and 467 lines, came out around 10 lines each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The finding worth your time.&lt;/strong&gt; The AI reproduced structure fast and under-reported what it skipped. It invented feed categories, quietly shipped fewer feeds than the original, and made up an article retention limit. All of it looked plausible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The code.&lt;/strong&gt; Everything is at &lt;a href="https://github.com/vaadin/modernization-experiments/tree/main/swt-rcp-to-vaadin" rel="noopener noreferrer"&gt;vaadin/modernization-experiments/swt-rcp-to-vaadin&lt;/a&gt;. It needs a Keycloak to log in against, which takes a few minutes to stand up. Setup is at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you maintain an SWT or Eclipse RCP app and you're weighing a web rewrite, this is the reality check I wanted before starting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a 20-year-old Eclipse app won't launch on a modern Mac
&lt;/h2&gt;

&lt;p&gt;My inbox had become a graveyard of unread content. I read a fair amount of developer material, the Vaadin blog, the Spring blog, a couple of Java ones, Hacker News, and half of it only arrives by email now. RSS promised to put it all in one place, newest first, without handing my address to anyone.&lt;/p&gt;

&lt;p&gt;So I looked for something to read it in. One name kept coming up, on nearly every "best RSS reader" list and in half the old forum threads: RSSOwl, an Eclipse desktop reader with a classic three-pane layout. A tree of feeds on the left, a sortable table of headlines top-right, an article reader below. The lists tended to add the same asterisk, needs Java, looks dated, which I ignored. I downloaded it. Nothing happened.&lt;/p&gt;

&lt;p&gt;The official Mac build is a 32-bit binary linked against Carbon, a UI layer Apple killed years ago. Rosetta can't help, because it only translates 64-bit Intel code. The flagship download of the app cannot run on a 2026 machine.&lt;/p&gt;

&lt;p&gt;There are two alternatives, and neither is something a normal user would find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RSSOwlnix&lt;/strong&gt;, a maintained community fork on GitHub, ships a 64-bit Intel binary. It runs on a current Mac, but under Rosetta, which is itself on the way out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building from source&lt;/strong&gt; worked. In just over two minutes I had a native Apple Silicon app with no Rosetta involved, because modern SWT ships a current aarch64 build. It took changing one line in the build config, a Tycho target environment from &lt;code&gt;x86_64&lt;/code&gt; to &lt;code&gt;aarch64&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "you cannot run SWT on Apple Silicon" is false. SWT keeps up fine. Distribution is what fails. The only build a normal person can get is a crumbling 32-bit relic or an Intel one riding Rosetta, and getting a modern native build needs source access, a Maven and Tycho toolchain, the right JDK, and knowing which knob to turn. Nobody downloading a feed reader is going to do that. A web app, you open.&lt;/p&gt;

&lt;p&gt;Which gave me the question I wanted to answer: could an AI take that exact screen, a feeds tree, a sortable headlines table with a right-click menu, and an article reader, and rebuild it in the browser, faithfully enough that I'd use it? And where would it fall apart?&lt;/p&gt;

&lt;h2&gt;
  
  
  What I started with and what I used
&lt;/h2&gt;

&lt;p&gt;The target was the headlines table and then the full three-pane screen. RSSOwl's headlines table is about 2,000 lines of code, and the behaviour is dense: it looks like a plain table but is secretly a tree so it can group rows, with sortable columns, bold unread rows, custom row colours, clickable in-cell icons, and a right-click menu. If you're estimating your own port, remember that ratio. Small widget, heavy behaviour.&lt;/p&gt;

&lt;p&gt;On the tooling side: Claude Code did the typing, Vaadin 25 was the target, and the Vaadin MCP server answered API questions. The AI also drove the running app with Playwright, which turned out to matter more than I expected. Line counts below exclude comments.&lt;/p&gt;

&lt;p&gt;I was not trying to port the application shell. One polished screen is not the same as a whole RCP app, and the difference is the section on effort below.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the port went
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before.&lt;/strong&gt; RSSOwlnix on macOS, the maintained fork running under Rosetta. Feeds tree on the left, sortable headlines table top-right, article reader below. Bold rows are unread.*&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkx5tfykt4qwfgvkqajj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkx5tfykt4qwfgvkqajj.png" alt="RSSOwlnix Eclipse RCP desktop feed reader on macOS, showing the three-pane layout with a feeds tree, a sortable headlines table, and an article reader pane" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After.&lt;/strong&gt; The same screen in Vaadin 25. Sortable Grid columns, unread state as a dot plus an orange title, and per-row mark-read and sticky buttons where the desktop had a toolbar.*&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbztmozj24g917go5b0gh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbztmozj24g917go5b0gh.webp" alt="The same three-pane feed reader rebuilt as a Vaadin 25 web application, with a sortable Grid of headlines, a feeds tree, and a reader pane" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note the feed counts.&lt;/strong&gt; The port carries fewer feeds and fewer articles per feed. That's the completeness gap described below, not a rendering difference.&lt;/p&gt;
&lt;h3&gt;
  
  
  What moved across fast
&lt;/h3&gt;

&lt;p&gt;Selecting a headline updates the reader with no plumbing at all. The AI first wired the master-to-detail link with old-style change listeners, which is stale. Vaadin 25 has Signals: the selected headline lives in a &lt;code&gt;ValueSignal&amp;lt;NewsItem&amp;gt;&lt;/code&gt;, and the reader binds to it reactively through &lt;code&gt;Signal.effect(...)&lt;/code&gt;. There's no manual "when the selection changes, go update the reader." It's better than the desktop original. I only found it because I checked, which becomes a theme.&lt;/p&gt;

&lt;p&gt;Sorting was nearly free. You get click-to-sort headers and the sort arrow, and you give each column a &lt;code&gt;Comparator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The right-click menu rendered faithfully. A &lt;code&gt;GridContextMenu&lt;/code&gt; rebuilt on each open reproduced "Mark read" flipping to "Mark unread," and correctly shows nothing when you right-click a group header.&lt;/p&gt;

&lt;p&gt;Then the thing that surprised me most, which is how much code disappeared. RSSOwl's &lt;code&gt;NewsComparator&lt;/code&gt; went from 306 lines to about 10 lines of Grid column comparators. Its 467-line &lt;code&gt;NewsTableLabelProvider&lt;/code&gt;, which handled bold unread rows, highlighted sticky rows, and label colours, became some 10 lines of CSS. All that imperative desktop ceremony, the comparators and content providers and label providers and hand-drawn row painting, collapses into a bit of declarative Grid config plus CSS. To be fair, part of the shrink is that my version does a little less than the original in the edge cases. Even accounting for that, the difference is enormous.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where the AI let me down
&lt;/h3&gt;

&lt;p&gt;None of the problems below came from anything the AI knew. They came from the compiler, from running the app, and from me comparing against how the original behaved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It confidently wrote APIs that don't exist, because it had to.&lt;/strong&gt; The model has a training cutoff, and Vaadin 25 shipped after it, so the model has never seen this API. Ask anyway and it doesn't say "I don't know." It pattern-matches on the older Vaadin it learned on and returns confident, plausible, wrong code: deprecated constructors, moved methods, the pre-Signals listener style. It looks fine and it will not compile. Without live docs, it answers your Vaadin 25 questions from Vaadin 24-era training data, fluently and incorrectly. The fix is the Vaadin MCP server, which feeds current docs straight to the AI. Treat the model's built-in framework knowledge as a stale cache, and make querying the MCP server for the real 25.x API a required step rather than an afterthought. That one habit is what surfaced Signals, the current security config, and the modern Upload and Download handlers instead of fiction.&lt;/p&gt;

&lt;p&gt;Even with the right API there are sharp edges. A Grid row double-click event hands you the item directly, while the context-menu event hands you an &lt;code&gt;Optional&amp;lt;T&amp;gt;&lt;/code&gt;. Same "get the clicked row," written two different ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A styling trick straight from the docs silently did nothing.&lt;/strong&gt; The docs say to colour a row's background with the &lt;code&gt;--vaadin-grid-cell-background&lt;/code&gt; custom property. I did. It was applied to the cell and the background stayed stubbornly white. Under the current Aura theme the cell background is painted separately, so the property is ignored. The only thing that worked was &lt;code&gt;background-color: … !important&lt;/code&gt; on the cell's &lt;code&gt;::part(...)&lt;/code&gt;. Finding that took inspecting the live shadow DOM. It's a one-line fix and a reminder that even doc-sanctioned advice needs checking against the running app. I'd have spent an hour on it, but the AI caught it with Playwright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signals don't tolerate guessing.&lt;/strong&gt; Reading the selection from a background timer with &lt;code&gt;signal.get()&lt;/code&gt; throws, because &lt;code&gt;get()&lt;/code&gt; sets up reactive dependency tracking and is illegal outside an effect. From a plain callback you want &lt;code&gt;signal.peek()&lt;/code&gt;. Obvious in hindsight, invisible until you run the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sorting subtlety it got quietly wrong.&lt;/strong&gt; RSSOwl always sorts undated items to the bottom, in either direction. Vaadin gives a column one comparator and reverses it for a descending sort, so my &lt;code&gt;nullsLast&lt;/code&gt; flipped to &lt;code&gt;nullsFirst&lt;/code&gt; and empty-date rows leapt to the top of the default newest-first view. Wrong in a way you'd only notice if you knew the original.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The completeness gaps were the real lesson.&lt;/strong&gt; The AI made things up on its own that looked right and weren't. It created a set of feed categories different from the real ones in RSSOwl. It shipped fewer feeds than the original and never mentioned it. It invented an arbitrary limit for how many articles to keep, until I pointed it at RSSOwl's source, where the real default of 200 per feed was sitting in plain sight. Every one of those looked completely plausible.&lt;/p&gt;

&lt;p&gt;The AI got me a faithful skeleton, in hours. It needed someone who could replicate the behaviour of the original to turn it into a faithful app, and this time that was me. The tooling reproduces structure amazingly fast and under-delivers completeness just as reliably. It nails the broad 80% in an afternoon. The last 20%, the fidelity that makes the result the same app rather than a convincing lookalike, needs someone who remembers what the original did or is willing to go find out. That person is what decides whether the result is correct.&lt;/p&gt;
&lt;h2&gt;
  
  
  What came out of it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;RSSOwl (SWT and JFace)&lt;/th&gt;
&lt;th&gt;Vaadin 25&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Column sorting&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NewsComparator&lt;/code&gt;, 306 lines&lt;/td&gt;
&lt;td&gt;~10 lines of Grid column comparators&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold unread, sticky rows, label colours&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NewsTableLabelProvider&lt;/code&gt;, 467 lines&lt;/td&gt;
&lt;td&gt;~10 lines of CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selection drives the reader&lt;/td&gt;
&lt;td&gt;manual change listeners&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ValueSignal&amp;lt;NewsItem&amp;gt;&lt;/code&gt; plus &lt;code&gt;Signal.effect(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right-click menu&lt;/td&gt;
&lt;td&gt;custom JFace menu&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GridContextMenu&lt;/code&gt; rebuilt on open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedded article view&lt;/td&gt;
&lt;td&gt;real embedded browser&lt;/td&gt;
&lt;td&gt;feed HTML inline, sanitized through a jsoup allow-list&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the answer to the question I started with is yes, with a caveat about who has to be in the loop. The screen moves, and much of it moves surprisingly fast.&lt;/p&gt;

&lt;p&gt;I also got a bonus I didn't plan for. I only wanted to prove the main screen could move, but every time I checked whether some other part could move too, logins, full-text search, filters, labels, keyboard shortcuts, it turned into a working feature. I ended up with the multi-user feed reader I'd half-jokingly asked for, one I'd use. With the caveats above firmly attached: I still had to catch every completeness gap myself.&lt;/p&gt;

&lt;p&gt;On effort, if you're sizing your own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The headlines table alone, with sorting and custom rendering and clickable cells and the dynamic menu and multi-select and the timing behaviours, was a solid couple of weeks of focused evenings with the AI helping.&lt;/li&gt;
&lt;li&gt;The entire three-pane screen, done faithfully, was realistic in that same couple-of-weeks window, with the AI, the MCP server answering API questions, and me watching for what it got subtly wrong.&lt;/li&gt;
&lt;li&gt;A full RCP application is a different beast, and most of what's left isn't widgets. The workbench, the perspectives, the command framework, the plugin system. That's the real cost and the part with no clean equivalent. Redrawing screens is the fast, cheap part.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Four rules if you try this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the AI's memory of the framework.&lt;/strong&gt; Point it at the vendor's live docs first. For Vaadin that's the MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan the gnarly parts before writing code&lt;/strong&gt;, so the "there's no equivalent for this" cases surface early instead of halfway through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify by running the app, not by reading the diff.&lt;/strong&gt; The CSS bug, the Signals throw, and the sort flip were all invisible on paper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay in the loop yourself on anything where fidelity to the original matters.&lt;/strong&gt; The AI won't.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  What can't be ported at all
&lt;/h2&gt;

&lt;p&gt;Some things didn't fight me so much as slam a door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eclipse's pluggable menus have no web equivalent.&lt;/strong&gt; In RCP, other plugins can add items to a menu through the Eclipse extension registry. That's an architecture, not a widget, and there's nothing on the web to map it onto. If your app relies on that kind of extensibility, you're looking at a redesign rather than a port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real embedded browser is blocked by the web itself.&lt;/strong&gt; RSSOwl embeds a real browser showing the live page. Drop a news site into an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; today and it renders blank, because sites forbid it with &lt;code&gt;X-Frame-Options&lt;/code&gt; and CSP headers. I render the feed's own article HTML inline instead, first cleaned through a jsoup allow-list, because Vaadin's &lt;code&gt;Html&lt;/code&gt; component doesn't sanitize, a documented footgun. Close in spirit, not the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some features are simply gone.&lt;/strong&gt; RSSOwl syncs with Google Reader, which Google shut down in 2013. When the other half is gone, there's nothing to move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom row colour plus the selection highlight is still awkward.&lt;/strong&gt; An arbitrary per-row colour and the selected-row state don't compose cleanly through CSS parts, so I settled for a compromise rather than a perfect match.&lt;/p&gt;

&lt;p&gt;None of these are the web framework's fault. They're the real edges of dragging a 20-year-old desktop app onto today's platform, and knowing where those edges sit before you start is worth more than any feature list.&lt;/p&gt;
&lt;h2&gt;
  
  
  Clone it and point it at your own screen
&lt;/h2&gt;

&lt;p&gt;Everything is on GitHub, in &lt;a href="https://github.com/vaadin/modernization-experiments/tree/main/swt-rcp-to-vaadin" rel="noopener noreferrer"&gt;vaadin/modernization-experiments/swt-rcp-to-vaadin&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vaadin/modernization-experiments.git
&lt;span class="nb"&gt;cd &lt;/span&gt;modernization-experiments/swt-rcp-to-vaadin/poc/headlines
./mvnw spring-boot:run   &lt;span class="c"&gt;# needs JDK 21+; the first run pulls the frontend toolchain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat, and it's the price of making this multi-user. The app gates every page behind an OIDC login, so it doesn't just open. Out of the box, &lt;code&gt;application.properties&lt;/code&gt; points at my Keycloak, which won't do you any good, so you'll need your own. The repo has a one-shot &lt;code&gt;keycloak/setup-keycloak.sh&lt;/code&gt; that provisions the realm, a confidential client, and two test users (alice/alice, bob/bob) against a local Keycloak. Then point &lt;code&gt;spring.security.oauth2.client.provider.keycloak.issuer-uri&lt;/code&gt; at your instance and pass the client secret through the &lt;code&gt;KEYCLOAK_CLIENT_SECRET&lt;/code&gt; env var (see &lt;code&gt;run.sh&lt;/code&gt;). Once it's up, open &lt;code&gt;http://localhost:8080&lt;/code&gt; and log in as alice. A few minutes of setup, not the one-command kind.&lt;/p&gt;

&lt;p&gt;The prompts I used are in the repo too, and they're the genuinely reusable part. Build your app from source first to prove it still runs. Map its tables and trees to Grid and TreeGrid the same way. Query the framework's live docs instead of trusting the AI's memory. Verify by running. Not a magic recipe, but a starting point for your own attempt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you got your own stranded desktop app?&lt;/strong&gt; Tell me in the comments what it is and which screen you'd port first. I'm curious how common the RSSOwl shape is: dense table, heavy behaviour, no modern build.&lt;/p&gt;

&lt;p&gt;If you hit a wall on a weird framework quirk, drop it in the &lt;a href="https://vaadin.com/forum" rel="noopener noreferrer"&gt;Vaadin Forum&lt;/a&gt;. If you have a large legacy codebase and want an architectural gut check or a full migration strategy, &lt;a href="https://vaadin.com/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt; and we'll work out how to get your app off the desktop.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>swt</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
