Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f0fc1b6e70
|
|||
| 10a31476c2 |
@@ -1,3 +1,4 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
*.jar
|
||||
build
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
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));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user