2 Commits

Author SHA1 Message Date
rudi f0fc1b6e70 Fix YMS bug
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 43s
Release j3270 / Build & Publish Release (push) Successful in 43s
2026-08-20 19:58:52 -04:00
rudi 10a31476c2 Update README.md
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m8s
2026-08-20 23:31:46 +00:00
14 changed files with 133 additions and 66 deletions
+1
View File
@@ -1,3 +1,4 @@
*.log
.DS_Store
*.jar
build
-23
View File
@@ -73,29 +73,6 @@ The resulting standalone JAR is created at `build/j3270.jar`.
---
## 🏷️ How to Tag & Publish Releases
Releases can be published automatically to Gitea via Git tags:
```bash
# 1. Create a version tag
git tag -a v0.1.0 -m "Release v0.1.0"
# 2. Push the tag to Gitea
git push origin v0.1.0
```
Pushing any `v*` tag triggers the automated release workflow (`.gitea/workflows/release.yaml`), which runs tests, builds the JAR, and publishes a new Gitea Release with `j3270.jar` attached automatically.
---
## 🔍 Topics & Discoverability
Suggested keywords/topics for repository indexing:
`tn3270` · `tn3270e` · `ibm-3270` · `mainframe` · `terminal-emulator` · `x3270` · `ind-file` · `file-transfer` · `ebcdic` · `z-vm` · `z-os` · `cms` · `tso` · `cics` · `java` · `desktop-app`
---
## 🤝 Contributing
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
Binary file not shown.
Binary file not shown.
@@ -438,16 +438,37 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}
}
// Configure logging over the absolute root logger
// Configure logging for application packages
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
}
globalRoot.setLevel(debug ? Level.ALL : Level.WARNING);
globalRoot.setLevel(Level.WARNING);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
handler.setLevel(debug ? Level.ALL : Level.INFO);
handler.setFormatter(new SimpleFormatter());
globalRoot.addHandler(handler);
Logger appLogger = Logger.getLogger("org.pubvm.j3270");
appLogger.setLevel(Level.ALL);
appLogger.addHandler(handler);
appLogger.setUseParentHandlers(false);
Logger libLogger = Logger.getLogger("org.lib3270j");
libLogger.setLevel(Level.ALL);
libLogger.addHandler(handler);
libLogger.setUseParentHandlers(false);
try {
java.util.logging.FileHandler fileHandler = new java.util.logging.FileHandler("j3270.log", 10 * 1024 * 1024, 1, false);
fileHandler.setLevel(Level.ALL);
fileHandler.setFormatter(new SimpleFormatter());
appLogger.addHandler(fileHandler);
libLogger.addHandler(fileHandler);
log.info("Logging protocol trace to j3270.log");
} catch (Exception e) {
System.err.println("Could not create j3270.log: " + e.getMessage());
}
// Load INI config file if specified
if (configFile != null) {
@@ -889,10 +889,20 @@ public class TerminalPanel extends JPanel {
if (faIsHigh(currentFA & 0xFF))
bold = true;
// Handle invisible fields (zero intensity)
// Handle invisible fields (zero intensity / password fields)
// Modern UX: render '*' for typed characters so user sees length/digit count
if (faIsZero(currentFA & 0xFF)) {
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
char ch = ea.ucs4;
if (ch > 0x20 && ch != 0xFF) {
Font f = bold ? terminalFont.deriveFont(Font.BOLD) : terminalFont;
g2.setFont(f);
g2.setColor(fgColor);
g2.drawString("*", x, y + fontAscent);
}
if (isCellSelected(row, col)) {
g2.setColor(SELECTION_COLOR);
g2.fillRect(x, y, cellWidth, cellHeight);
@@ -6,6 +6,7 @@ import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.listener.ScreenUpdateListener;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
@@ -451,8 +452,8 @@ public class DataStreamProcessor {
pos += 4;
}
// Fill from current position to target
while (baddr != toAddr) {
// Fill from current position to target (do-while wraps entire buffer if baddr == toAddr)
do {
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar;
@@ -461,7 +462,7 @@ public class DataStreamProcessor {
ea.gr = (byte) currentGr;
ea.cs = fillCs;
baddr = (baddr + 1) % size;
}
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
lastWasOrder = true;
break;
@@ -476,7 +477,7 @@ public class DataStreamProcessor {
if (toAddr >= size)
toAddr = toAddr % size;
while (baddr != toAddr) {
do {
ExtendedAttribute ea = screen.getCell(baddr);
if (!ea.isFieldAttribute()) {
int faAddr = screen.findFieldAttribute(baddr);
@@ -491,7 +492,7 @@ public class DataStreamProcessor {
}
}
baddr = (baddr + 1) % size;
}
} while (baddr != toAddr);
screen.setBufferAddress(baddr);
pos += 3;
lastWasOrder = true;
@@ -637,7 +638,13 @@ public class DataStreamProcessor {
outputPos = 0;
int aid = AID_NO; // Last AID
int aid = (inputProcessor != null && inputProcessor.getLastAid() != 0)
? inputProcessor.getLastAid()
: AID_NO;
if (inputProcessor != null) {
inputProcessor.setLastAid(AID_NO);
}
int size = screen.getRows() * screen.getCols();
// AID byte
@@ -649,31 +656,56 @@ public class DataStreamProcessor {
outputWrite(caddr[1] & 0xFF);
if (!screen.isFormatted()) {
// Unformatted: send everything
if (all) {
for (int i = 0; i < size; i++) {
outputWrite(screen.getCell(i).ec & 0xFF);
// Unformatted screen: send data up to last non-null
int lastNonNull = -1;
for (int i = size - 1; i >= 0; i--) {
if (screen.getCell(i).ec != 0) {
lastNonNull = i;
break;
}
}
if (lastNonNull >= 0) {
for (int i = 0; i <= lastNonNull; i++) {
int b = screen.getCell(i).ec & 0xFF;
outputWrite(b != 0 ? b : 0x40);
}
}
} else {
// Formatted: send modified fields
// Formatted screen: send modified fields with trailing NULLs stripped
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && (all || faIsModified(ea.fa & 0xFF))) {
int fieldStart = (i + 1) % size;
// Send SBA for field start
// Collect field data and find last non-null byte
ByteArrayOutputStream fieldData = new ByteArrayOutputStream();
int pos = fieldStart;
int lastNonNull = -1;
int fieldLen = 0;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
fieldData.write(b);
if (b != 0x00) {
lastNonNull = fieldLen;
}
fieldLen++;
pos = (pos + 1) % size;
if (pos == fieldStart)
break;
}
// Always send SBA and address
outputWrite(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
outputWrite(addr[0] & 0xFF);
outputWrite(addr[1] & 0xFF);
// Send field contents until next FA
int pos = fieldStart;
while (pos < size && !screen.getCell(pos).isFieldAttribute()) {
outputWrite(screen.getCell(pos).ec & 0xFF);
pos = (pos + 1) % size;
if (pos == fieldStart)
break;
// Send field data (strip trailing nulls)
if (lastNonNull >= 0) {
byte[] allData = fieldData.toByteArray();
for (int k = 0; k <= lastNonNull; k++) {
outputWrite(allData[k] & 0xFF);
}
}
}
}
@@ -265,20 +265,21 @@ public class QueryReplyBuilder {
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters
// Implicit partition sizes SDP (Self-Defining Parameter)
// SDP 1: Default screen size
out.write(0x00); // flags
out.write(0x00); // reserved
out.write(0x0b); // SDP length
// SDP 1: Implicit partition sizes
out.write(0x00); // SDP length high byte
out.write(0x0b); // SDP length low byte (11 bytes: 2 len + 1 type + 1 res + 8 dims)
out.write(0x01); // SDP type: implicit partition sizes
out.write(0x00); // reserved
// Default
// Default dimensions (Model 2: 80x24)
out.write((MODEL_2_COLS >> 8) & 0xFF);
out.write(MODEL_2_COLS & 0xFF);
out.write((MODEL_2_ROWS >> 8) & 0xFF);
out.write(MODEL_2_ROWS & 0xFF);
// Alternate
// Alternate dimensions
out.write((maxCols >> 8) & 0xFF);
out.write(maxCols & 0xFF);
out.write((maxRows >> 8) & 0xFF);
@@ -138,9 +138,10 @@ public class InputProcessor {
}
// Advance cursor
int startAdvance = baddr;
baddr = (baddr + 1) % size;
// Skip over field attributes
while (screen.getCell(baddr).isFieldAttribute()) {
// Skip over field attributes safely
while (screen.getCell(baddr).isFieldAttribute() && baddr != startAdvance) {
baddr = (baddr + 1) % size;
}
screen.setCursorAddress(baddr);
@@ -255,18 +256,26 @@ public class InputProcessor {
}
}
} else {
// Unformatted screen in 3270 mode:
// Send character data from address 0 up to last non-null character
int size = screen.getRows() * screen.getCols();
int lastNonNull = -1;
for (int i = size - 1; i >= 0; i--) {
if (screen.getCell(i).ec != 0) {
lastNonNull = i;
// Unformatted screen in 3270 mode (e.g. line-mode console):
// Send AID + cursor address + only the active input line (row containing cursor)
int cols = screen.getCols();
int curAddr = screen.getCursorAddress();
int row = curAddr / cols;
int rowStart = row * cols;
int rowEnd = rowStart + cols;
// Find last non-null, non-blank character on the current line
int lastChar = rowStart - 1;
for (int i = rowEnd - 1; i >= rowStart; i--) {
int ec = screen.getCell(i).ec & 0xFF;
if (ec != 0x00 && ec != 0x40) {
lastChar = i;
break;
}
}
if (lastNonNull >= 0) {
for (int i = 0; i <= lastNonNull; i++) {
if (lastChar >= rowStart) {
for (int i = rowStart; i <= lastChar; i++) {
int b = screen.getCell(i).ec & 0xFF;
out.write(b != 0 ? b : 0x40);
}
@@ -325,6 +334,13 @@ public class InputProcessor {
screen.setCursorAddress(addr);
}
public int getLastAid() { return lastAid; }
public void setLastAid(int aid) { this.lastAid = aid; }
public void setCursorAddress(int baddr) {
screen.setCursorAddress(baddr);
}
public void backTab() {
// Find previous unprotected field
int addr = screen.getCursorAddress();
@@ -132,10 +132,14 @@ public class TelnetConnection {
if (log.isLoggable(Level.FINE)) {
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
}
for (int i = 0; i < n; i++) {
fsm.feedByte(buf[i] & 0xFF);
try {
for (int i = 0; i < n; i++) {
fsm.feedByte(buf[i] & 0xFF);
}
fsm.endOfNetworkData();
} catch (Throwable t) {
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
}
fsm.endOfNetworkData();
}
}
} catch (SocketException e) {
@@ -148,6 +152,11 @@ public class TelnetConnection {
log.log(Level.WARNING, "Read error", e);
fsm.onError("Read error: " + e.getMessage());
}
} catch (Throwable t) {
if (running) {
log.log(Level.SEVERE, "Unexpected fatal error in readLoop", t);
fsm.onError("Network loop error: " + t.getMessage());
}
}
}