The Screen Handlers section covers wiring a screen handler into a program's own main() with --screen. This section is the distinct embed-and-drive story: running a compiled program from your own Java code and reading the data its screens present - without a 5250 terminal and without editing the RPG.

Much RPG business logic is fused into a display-file program: the query and the screen paint happen together, so there is no callable entry point that returns the data - the only way to run the logic is to run the screen. A compiled program's screens are available to your Java code as typed, named fields rather than rendered characters, so your Java code can run such a program headless and read its records directly. The API lives in the package ltd.whitehorn.rpg.host (in triton-rpg.jar, on your program's classpath already).

RpgHost - the runner

An RpgHost runs a compiled program and hands each screen it presents to a capture you supply. Build one host and reuse it; it is thread-safe, and every run is self-contained - it binds the program to a fresh activation group, borrows a JDBC connection from the DataSource you configured (if any), runs the program, then returns the connection and clears the job's state.

import ltd.whitehorn.rpg.host.*;
import ltd.whitehorn.rpg.runtime.RpgProgram;

RpgHost host = new RpgHost(dataSource);          // omit dataSource for non-SQL programs
RpgProgram program = new CUSTBRW();              // your compiled program class

MyCapture capture = new MyCapture();
RpgRunResult result = host.run(program, capture);

A program is run on the calling thread. RpgRunResult reports how the run went - completed(), interactions() (screens driven), messages() (every DSPLY seen), and lastScreen().

Reading a screen with a capture

Implement RpgCapture to observe each screen and decide what to do next. For every screen the program presents, onScreen receives an RpgScreenEvent and returns an Action:

class CustomerList implements RpgCapture {
    final List<String> names = new ArrayList<>();

    public Action onScreen(RpgScreenEvent screen) {
        for (RpgRecord row : screen.records()) {   // subfile detail rows
            names.add(row.getString("CUSTNAME").trim());
        }
        return Action.exit();                       // F3 - end the program's loop
    }
}

A capture is stateful, single-use, and caller-owned: create it, pass it to one run, then read its own accumulated data (names above) after run returns. Passing the same capture to a second run throws IllegalStateException. It is not thread-safe and must not be shared across concurrent runs.

List and detail captures

Two ready-made captures cover the common read cases, so you do not have to implement RpgCapture by hand. Each reads its data and drives the program to a clean exit; you read the result from a typed accessor afterward.

ListCapture reads a subfile list as a List<RpgRecord>:

ListCapture cap = new ListCapture();
RpgRunResult run = host.run(new DEPTS(), cap);
List<RpgRecord> depts = cap.rows();          // rows().get(0).getString("XID") / ("XNAME")

RecordCapture snapshots one named record format's output fields as a single RpgRecord:

RecordCapture cap = new RecordCapture("DETAIL");   // target format is required
host.run(new CUSTINQ(), cap);
RpgRecord detail = cap.record();                   // detail.getInt("CUSTNO"), getString("CUSTNAME")

The first matching screen is captured; the exit key defaults to F3 (exitKey(n) to change it).

If the target format never appears before the program ends, both captures return empty by default (rows() empty, record() null). Call strict() to instead raise FormatNotSeenException - useful when a missing target is a real error rather than an expected "no data" case.

Create or update: driving a data-entry screen

FormSubmit is the write-side capture: it seeds a data-entry format's input fields, submits, and reports whether the create/update was accepted - so a maintenance program performs its insert or update as a side effect with no edits to the RPG.

FormSubmit form = FormSubmit.into("DETAIL")
        .set("XID", new BigDecimal(201))
        .set("XFIRST", "Grace").set("XLAST", "Hopper")
        .set("XSAL", new BigDecimal("88000"))
        .submitOn(FormSubmit.ENTER)          // default is Enter
        .messageField("XERR")                // field whose text becomes the result message
        .successWhen(fields -> fields.getString("XERR").isBlank());
host.run(new NEWEMP(), form);

SubmitResult r = form.submitResult();
// r.accepted() - true when the outcome signal indicates success
// r.message() - the status/validation text (trimmed)
// r.fields() - the field state the program showed after the submit

Seed values must match the field's type - a String for an alpha field, a number (e.g. BigDecimal) for a numeric field.

You must declare the success signal - a screen program returns no status, so FormSubmit cannot guess whether the write was accepted. Choose exactly one, matching the app's convention (omitting it fails the run):

On a rejection (validation failure), accepted() is false, message() carries the validation text, and a well-behaved program writes nothing. As with every capture, the run is bounded (see Bounding a run): a data-entry loop that never accepts aborts with RpgHostException rather than hanging.

Driving a data-entry screen is inherently program-specific - you must know the input field names, the submit key, and the app's success convention. That is expected for zero-edit exposure; the durable alternative is below.

Captures vs. a nomain adapter

A capture reads data or performs a write by running the program, so it is coupled to the screen flow: which format is shown, in what order, which fields to seed, and which key exits. That is the price of zero-edit exposure - you read the data or drive the create/update without touching the RPG. It is the right tool for using an existing screen program as-is.

When you control the source and want a durable, flow-independent entry point, add a nomain module with an exported procedure that returns the data - or performs the create/update - directly (see Procedure Names). A nomain export is a plain method call - no screen to drive, no submit or exit key, nothing to rebind if the panel layout changes - and is the better long-term integration point where editing the source is an option.

Running without a capture

Call run(program) with no capture to run a batch or DSPLY-only program to completion; its messages are collected on the result:

RpgRunResult result = host.run(batchProgram);
for (String msg : result.messages()) {
    System.out.println(msg);
}

Passing *ENTRY parameters

A program that takes *ENTRY parameters usually needs them to decide what it does - a data-entry program scoped to one department, say. Pass them with ProgramArgs, which any run overload accepts:

RpgRunResult result = host.run(program, capture, ProgramArgs.of("SALES", 42));

The values are seeded exactly as a compiled CALL seeds them, so the program cannot tell a Java caller from an RPG one. That includes %PARMS, which reports the number you supplied rather than the number declared - so a program with trailing OPTIONS(*NOPASS) parameters can still tell that they were omitted.

Parameters bind by position, not by name. This mirrors RPG: program parameters are matched positionally and the names on either side are documentation. There is deliberately no name-keyed form, because it would suggest the names are checked when they are not - renaming a parameter in the RPG source would keep working while reordering two would silently swap them.

Values convert where the mapping is unambiguous: a String for a character parameter, an int, long, BigDecimal or numeric String for a decimal one. A double or float is rejected for a packed or zoned parameter rather than rounded - binary floating point cannot represent every decimal exactly, and silently changing your number is worse than an error. Pass a BigDecimal or a String.

Passing arguments to a program that declares none, or passing more than it declares, raises RpgHostException rather than being ignored.

Values the program hands back

RPG passes a program parameter by reference unless its interface says otherwise, so a program can return values through one. Read them positionally after the run:

RpgRunResult result = host.run(program, ProgramArgs.of(customerId, "      "));
String status = String.valueOf(result.outputArgs().get(1)).trim();

Every parameter is reported, not only the ones a prototype would have marked by-reference - a Java caller has no prototype, so the host does not guess which are outputs. outputArgs() is empty when the run passed no arguments.

Bounding a run

A capture that never returns an exit action would otherwise drive the program forever. RpgHost bounds every run with an interaction count and a wall-clock deadline; exceeding either raises RpgHostException rather than hanging the thread. Configure the limits with the builder:

RpgHost host = RpgHost.builder()
        .dataSource(dataSource)
        .maxInteractions(500)
        .timeout(java.time.Duration.ofSeconds(30))
        .build();