Overnight churnings
Build and Test j3270 / Build JAR & Run Tests (push) Failing after 1m23s
Release j3270 / Build & Publish Release (push) Failing after 1m15s

This commit is contained in:
2026-09-01 09:45:40 -04:00
parent a56aa5f059
commit 161364a95f
15 changed files with 1489 additions and 33 deletions
@@ -145,12 +145,12 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// 3. View menu // 3. View menu
JMenu viewMenu = createMenu("View"); JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2))); viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2), true));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2))); viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2), true));
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> { viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
terminalPanel.setFontSize(16); terminalPanel.setFontSize(16);
terminalPanel.guardedPack(); terminalPanel.guardedPack();
})); }, true));
viewMenu.addSeparator(); viewMenu.addSeparator();
// UI Theme Submenu // UI Theme Submenu
@@ -234,17 +234,17 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.sendEnter(); client.sendEnter();
terminalPanel.repaint(); terminalPanel.repaint();
} }
})); }, true));
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> { actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
if (client != null) if (client != null)
client.sendClear(); client.sendClear();
terminalPanel.repaint(); terminalPanel.repaint();
})); }, true));
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> { actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
if (client != null) if (client != null)
client.reset(); client.reset();
terminalPanel.repaint(); terminalPanel.repaint();
})); }, true));
actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> { actionsMenu.add(createMenuItem("Erase Input", KeyEvent.VK_E, () -> {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
client.eraseInput(); client.eraseInput();
@@ -272,7 +272,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
}, true)); }, true));
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog)); actionsMenu.add(createMenuItem("Run Keystrokes / Script...", -1, this::showScriptDialog));
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog, true));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
// 5. Help menu // 5. Help menu
@@ -706,18 +706,18 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
"Insert — Toggle insert mode\n" + "Insert — Toggle insert mode\n" +
"Escape — Reset\n" + "Escape — Reset\n" +
"PageUp/Down — PF7/PF8\n" + "PageUp/Down — PF7/PF8\n" +
"Alt+C / Ctrl+K — Clear\n" + "Alt+C / Alt+K — Clear\n" +
"Alt+E — Erase Input\n" + "Alt+E — Erase Input\n" +
"Alt+A — Attention\n" + "Alt+A — Attention\n" +
"Alt+S — System Request\n" + "Alt+S — System Request\n" +
"Alt+Q — Cursor Select\n" + "Alt+Q — Cursor Select\n" +
"Alt+L — Toggle Light Pen\n" + "Alt+L — Toggle Light Pen\n" +
"Alt+T — File Transfer\n" +
"Alt+=/-/0 — Font size +/-/reset\n" +
"Cmd/Ctrl+F — Find on Screen\n" + "Cmd/Ctrl+F — Find on Screen\n" +
"Cmd/Ctrl+G / F3— Find Next\n" + "Cmd/Ctrl+G / F3— Find Next\n" +
"Cmd/Ctrl+T — File Transfer\n" +
"Cmd/Ctrl+D — Disconnect\n" + "Cmd/Ctrl+D — Disconnect\n" +
"Cmd/Ctrl+Q — Quit\n" + "Cmd/Ctrl+Q — Quit";
"Cmd/Ctrl+=/-/0 — Font size +/-/reset";
JTextArea area = new JTextArea(text); JTextArea area = new JTextArea(text);
area.setEditable(false); area.setEditable(false);
@@ -0,0 +1,85 @@
package haus.nightmare.j3270.ui;
import haus.nightmare.j3270.J3270App;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
public class MenuBarShortcutsTest {
private Map<String, JMenuItem> collectMenuItems(JMenu menu) {
Map<String, JMenuItem> map = new HashMap<>();
for (int i = 0; i < menu.getItemCount(); i++) {
JMenuItem item = menu.getItem(i);
if (item != null) {
map.put(item.getText(), item);
}
}
return map;
}
@Test
public void testActionAndViewShortcutsUseAlt() {
J3270App app = new J3270App();
JMenuBar mb = app.getJMenuBar();
assertNotNull(mb, "JMenuBar should be present");
JMenu viewMenu = null;
JMenu actionsMenu = null;
JMenu fileMenu = null;
JMenu editMenu = null;
for (int i = 0; i < mb.getMenuCount(); i++) {
JMenu m = mb.getMenu(i);
if (m != null) {
if ("View".equals(m.getText())) viewMenu = m;
else if ("Actions".equals(m.getText())) actionsMenu = m;
else if ("File".equals(m.getText())) fileMenu = m;
else if ("Edit".equals(m.getText())) editMenu = m;
}
}
assertNotNull(viewMenu, "View menu should exist");
assertNotNull(actionsMenu, "Actions menu should exist");
assertNotNull(fileMenu, "File menu should exist");
assertNotNull(editMenu, "Edit menu should exist");
// Verify View Menu items use ALT_DOWN_MASK
Map<String, JMenuItem> viewItems = collectMenuItems(viewMenu);
assertAcceleratorUsesAlt(viewItems.get("Font Size +"), KeyEvent.VK_EQUALS);
assertAcceleratorUsesAlt(viewItems.get("Font Size -"), KeyEvent.VK_MINUS);
assertAcceleratorUsesAlt(viewItems.get("Reset Font"), KeyEvent.VK_0);
// Verify Actions Menu items use ALT_DOWN_MASK
Map<String, JMenuItem> actionItems = collectMenuItems(actionsMenu);
assertAcceleratorUsesAlt(actionItems.get("Send Enter"), KeyEvent.VK_ENTER);
assertAcceleratorUsesAlt(actionItems.get("Clear"), KeyEvent.VK_K);
assertAcceleratorUsesAlt(actionItems.get("Reset"), KeyEvent.VK_R);
assertAcceleratorUsesAlt(actionItems.get("Erase Input"), KeyEvent.VK_E);
assertAcceleratorUsesAlt(actionItems.get("Attention"), KeyEvent.VK_A);
assertAcceleratorUsesAlt(actionItems.get("System Request"), KeyEvent.VK_S);
assertAcceleratorUsesAlt(actionItems.get("Cursor Select"), KeyEvent.VK_Q);
assertAcceleratorUsesAlt(actionItems.get("Toggle Light Pen (Alt+L)"), KeyEvent.VK_L);
assertAcceleratorUsesAlt(actionItems.get("File Transfer..."), KeyEvent.VK_T);
// Dispose frame
app.dispose();
}
private void assertAcceleratorUsesAlt(JMenuItem item, int expectedKeyCode) {
assertNotNull(item, "Menu item must exist");
KeyStroke ks = item.getAccelerator();
assertNotNull(ks, "Menu item " + item.getText() + " should have an accelerator");
assertEquals(expectedKeyCode, ks.getKeyCode(), "Key code mismatch for " + item.getText());
int mods = ks.getModifiers();
assertTrue((mods & (KeyEvent.ALT_DOWN_MASK | KeyEvent.ALT_MASK)) != 0,
"Menu item " + item.getText() + " accelerator must have ALT modifier");
assertEquals(0, mods & (KeyEvent.CTRL_DOWN_MASK | KeyEvent.CTRL_MASK | KeyEvent.META_DOWN_MASK | KeyEvent.META_MASK),
"Menu item " + item.getText() + " accelerator must NOT have CTRL or META modifiers");
}
}
@@ -40,6 +40,8 @@ public interface ECLConstants {
// Search directions // Search directions
int SEARCH_FORWARD = 1; int SEARCH_FORWARD = 1;
int SEARCH_BACKWARD = 2; int SEARCH_BACKWARD = 2;
int DIR_FORWARD = 1;
int DIR_BACKWARD = 2;
// OIA Input Inhibited Reason Codes // OIA Input Inhibited Reason Codes
int INHIBIT_NOT_INHIBITED = 0; int INHIBIT_NOT_INHIBITED = 0;
@@ -28,35 +28,43 @@ public class ECLField {
/** Buffer address of the field attribute character. */ /** Buffer address of the field attribute character. */
public int getStart() { return startPos; } public int getStart() { return startPos; }
public int GetStart() { return getStart(); }
/** First buffer address of the field data (start + 1). */ /** First buffer address of the field data (start + 1). */
public int getDataStart() { return dataStart; } public int getDataStart() { return dataStart; }
public int GetDataStart() { return getDataStart(); }
/** Last buffer address of the field data inclusive. */ /** Last buffer address of the field data inclusive. */
public int getEnd() { return endPos; } public int getEnd() { return endPos; }
public int GetEnd() { return getEnd(); }
/** Number of data characters in the field. */ /** Number of data characters in the field. */
public int getLength() { return length; } public int getLength() { return length; }
public int GetLength() { return getLength(); }
public int getStartRow() { public int getStartRow() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? startPos / cols : 0; return cols > 0 ? startPos / cols : 0;
} }
public int GetStartRow() { return getStartRow(); }
public int getStartCol() { public int getStartCol() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? startPos % cols : 0; return cols > 0 ? startPos % cols : 0;
} }
public int GetStartCol() { return getStartCol(); }
public int getEndRow() { public int getEndRow() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? endPos / cols : 0; return cols > 0 ? endPos / cols : 0;
} }
public int GetEndRow() { return getEndRow(); }
public int getEndCol() { public int getEndCol() {
int cols = ps.getCols(); int cols = ps.getCols();
return cols > 0 ? endPos % cols : 0; return cols > 0 ? endPos % cols : 0;
} }
public int GetEndCol() { return getEndCol(); }
private byte getLiveAttribute() { private byte getLiveAttribute() {
if (ps != null && ps.getScreenBuffer() != null) { if (ps != null && ps.getScreenBuffer() != null) {
@@ -71,34 +79,42 @@ public class ECLField {
public boolean isModified() { public boolean isModified() {
return faIsModified(getLiveAttribute() & 0xFF); return faIsModified(getLiveAttribute() & 0xFF);
} }
public boolean IsModified() { return isModified(); }
public boolean isProtected() { public boolean isProtected() {
return faIsProtected(getLiveAttribute() & 0xFF); return faIsProtected(getLiveAttribute() & 0xFF);
} }
public boolean IsProtected() { return isProtected(); }
public boolean isNumeric() { public boolean isNumeric() {
return faIsNumeric(getLiveAttribute() & 0xFF); return faIsNumeric(getLiveAttribute() & 0xFF);
} }
public boolean IsNumeric() { return isNumeric(); }
public boolean isHighIntensity() { public boolean isHighIntensity() {
return faIsHigh(getLiveAttribute() & 0xFF); return faIsHigh(getLiveAttribute() & 0xFF);
} }
public boolean IsHighIntensity() { return isHighIntensity(); }
public boolean isHidden() { public boolean isHidden() {
return faIsZero(getLiveAttribute() & 0xFF); return faIsZero(getLiveAttribute() & 0xFF);
} }
public boolean IsHidden() { return isHidden(); }
public boolean isDisplay() { public boolean isDisplay() {
return !isHidden(); return !isHidden();
} }
public boolean IsDisplay() { return isDisplay(); }
public boolean isPenSelectable() { public boolean isPenSelectable() {
return faIsSelectable(getLiveAttribute() & 0xFF); return faIsSelectable(getLiveAttribute() & 0xFF);
} }
public boolean IsPenSelectable() { return isPenSelectable(); }
public short getAttribute() { public short getAttribute() {
return (short) (getLiveAttribute() & 0xFF); return (short) (getLiveAttribute() & 0xFF);
} }
public short GetAttribute() { return getAttribute(); }
/** /**
* Get the text contents of this field as a String. * Get the text contents of this field as a String.
@@ -107,6 +123,7 @@ public class ECLField {
if (length <= 0) return ""; if (length <= 0) return "";
return ps.getString(dataStart, length); return ps.getString(dataStart, length);
} }
public String GetText() { return getText(); }
/** /**
* Set the text contents of this field. * Set the text contents of this field.
@@ -115,6 +132,7 @@ public class ECLField {
if (isProtected() || length <= 0) return; if (isProtected() || length <= 0) return;
ps.setText(text, dataStart); ps.setText(text, dataStart);
} }
public void SetText(String text) { setText(text); }
/** /**
* Get selector light pen type. * Get selector light pen type.
@@ -129,6 +147,7 @@ public class ECLField {
} }
return ' '; return ' ';
} }
public char GetSelectorPenType() { return getSelectorPenType(); }
/** /**
* Actuate lightpen selection on this field ('?' -> '>'). * Actuate lightpen selection on this field ('?' -> '>').
@@ -140,6 +159,7 @@ public class ECLField {
setText(">" + (t.length() > 1 ? t.substring(1) : "")); setText(">" + (t.length() > 1 ? t.substring(1) : ""));
} }
} }
public void SelectField() { selectField(); }
/** /**
* Deselect lightpen selection on this field ('>' -> '?'). * Deselect lightpen selection on this field ('>' -> '?').
@@ -151,16 +171,19 @@ public class ECLField {
setText("?" + (t.length() > 1 ? t.substring(1) : "")); setText("?" + (t.length() > 1 ? t.substring(1) : ""));
} }
} }
public void DeSelectField() { deSelectField(); }
/** Return true if this field wraps from bottom of screen to top. */ /** Return true if this field wraps from bottom of screen to top. */
public boolean isWrapped() { public boolean isWrapped() {
return startPos > endPos; return startPos > endPos;
} }
public boolean IsWrapped() { return isWrapped(); }
/** Last data buffer address (same as getEnd). */ /** Last data buffer address (same as getEnd). */
public int getDataEnd() { public int getDataEnd() {
return endPos; return endPos;
} }
public int GetDataEnd() { return getDataEnd(); }
/** /**
* Check if the specified buffer address is contained within this field (including its FA). * Check if the specified buffer address is contained within this field (including its FA).
@@ -178,6 +201,7 @@ public class ECLField {
return pos >= startPos || pos <= endPos; return pos >= startPos || pos <= endPos;
} }
} }
public boolean Contains(int pos) { return contains(pos); }
/** /**
* Check if the specified row and column is contained within this field. * Check if the specified row and column is contained within this field.
@@ -187,6 +211,7 @@ public class ECLField {
int cols = ps.getCols(); int cols = ps.getCols();
return contains(row * cols + col); return contains(row * cols + col);
} }
public boolean Contains(int row, int col) { return contains(row, col); }
/** /**
* Set the Modified Data Tag (MDT) for this field. * Set the Modified Data Tag (MDT) for this field.
@@ -205,6 +230,7 @@ public class ECLField {
} }
} }
} }
public void SetModified(boolean modified) { setModified(modified); }
/** /**
* Erase all character data within this field to nulls. * Erase all character data within this field to nulls.
@@ -225,6 +251,20 @@ public class ECLField {
sb.markAllChanged(); sb.markAllChanged();
sb.updateDisplaySnapshot(); sb.updateDisplaySnapshot();
} }
public void Erase() { erase(); }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ECLField)) return false;
ECLField other = (ECLField) o;
return this.startPos == other.startPos && this.endPos == other.endPos && this.length == other.length;
}
@Override
public int hashCode() {
return java.util.Objects.hash(startPos, endPos, length);
}
@Override @Override
public String toString() { public String toString() {
@@ -70,27 +70,38 @@ public class ECLFieldList {
} }
} }
public void Refresh() { refresh(); }
public synchronized int getFieldCount() { public synchronized int getFieldCount() {
return fields.size(); return fields.size();
} }
public int GetFieldCount() { return getFieldCount(); }
public synchronized List<ECLField> getFields() { public synchronized List<ECLField> getFields() {
return Collections.unmodifiableList(new ArrayList<>(fields)); return Collections.unmodifiableList(new ArrayList<>(fields));
} }
public List<ECLField> GetFields() { return getFields(); }
public synchronized ECLField getFirstField() { public synchronized ECLField getFirstField() {
if (fields.isEmpty()) return null; if (fields.isEmpty()) return null;
return fields.get(0); return fields.get(0);
} }
public ECLField GetFirstField() { return getFirstField(); }
public synchronized ECLField getNextField(ECLField prev) { public synchronized ECLField getNextField(ECLField prev) {
if (prev == null || fields.isEmpty()) return getFirstField(); if (prev == null || fields.isEmpty()) return getFirstField();
int idx = fields.indexOf(prev); for (int i = 0; i < fields.size(); i++) {
if (idx >= 0 && idx + 1 < fields.size()) { ECLField f = fields.get(i);
return fields.get(idx + 1); if (f.equals(prev) || f.getStart() == prev.getStart()) {
if (i + 1 < fields.size()) {
return fields.get(i + 1);
} }
return null; return null;
} }
}
return null;
}
public ECLField GetNextField(ECLField prev) { return getNextField(prev); }
/** /**
* Find the field that contains the specified buffer position. * Find the field that contains the specified buffer position.
@@ -113,6 +124,7 @@ public class ECLFieldList {
} }
return null; return null;
} }
public ECLField FindField(int pos) { return findField(pos); }
/** /**
* Find the field at the specified 0-indexed row and column. * Find the field at the specified 0-indexed row and column.
@@ -122,20 +134,26 @@ public class ECLFieldList {
int cols = screen.getCols(); int cols = screen.getCols();
return findField(row * cols + col); return findField(row * cols + col);
} }
public ECLField FindField(int row, int col) { return findField(row, col); }
/** /**
* Get the field preceding the given field in the field list. * Get the field preceding the given field in the field list.
*/ */
public synchronized ECLField getPreviousField(ECLField next) { public synchronized ECLField getPreviousField(ECLField next) {
if (next == null || fields.isEmpty()) return null; if (next == null || fields.isEmpty()) return null;
int idx = fields.indexOf(next); for (int i = 0; i < fields.size(); i++) {
if (idx > 0) { ECLField f = fields.get(i);
return fields.get(idx - 1); if (f.equals(next) || f.getStart() == next.getStart()) {
} else if (idx == 0) { if (i > 0) {
return fields.get(i - 1);
} else {
return fields.get(fields.size() - 1); return fields.get(fields.size() - 1);
} }
}
}
return null; return null;
} }
public ECLField GetPreviousField(ECLField next) { return getPreviousField(next); }
/** /**
* Find the field at the given buffer position (alias for findField). * Find the field at the given buffer position (alias for findField).
@@ -143,6 +161,7 @@ public class ECLFieldList {
public ECLField findFieldAt(int pos) { public ECLField findFieldAt(int pos) {
return findField(pos); return findField(pos);
} }
public ECLField FindFieldAt(int pos) { return findFieldAt(pos); }
/** /**
* Find the field at the given row and column. * Find the field at the given row and column.
@@ -150,6 +169,7 @@ public class ECLFieldList {
public ECLField findFieldAt(int row, int col) { public ECLField findFieldAt(int row, int col) {
return findField(row, col); return findField(row, col);
} }
public ECLField FindFieldAt(int row, int col) { return findFieldAt(row, col); }
/** /**
* Find the field preceding the one at the given buffer position. * Find the field preceding the one at the given buffer position.
@@ -159,6 +179,7 @@ public class ECLFieldList {
if (curr == null) return null; if (curr == null) return null;
return getPreviousField(curr); return getPreviousField(curr);
} }
public ECLField FindPrevField(int pos) { return findPrevField(pos); }
/** /**
* Find the field succeeding the one at the given buffer position. * Find the field succeeding the one at the given buffer position.
@@ -168,6 +189,7 @@ public class ECLFieldList {
if (curr == null) return null; if (curr == null) return null;
return getNextField(curr); return getNextField(curr);
} }
public ECLField FindNextField(int pos) { return findNextField(pos); }
/** /**
* Find field containing the given text string. * Find field containing the given text string.
@@ -202,4 +224,5 @@ public class ECLFieldList {
} }
return null; return null;
} }
public ECLField FindField(String text, int startPos) { return findField(text, startPos); }
} }
@@ -85,24 +85,52 @@ public class ECLOIA implements ECLConstants {
return inputProcessor != null && inputProcessor.isInsertMode(); return inputProcessor != null && inputProcessor.isInsertMode();
} }
public boolean IsInsertMode() {
return isInsertMode();
}
public boolean isNumeric() { public boolean isNumeric() {
if (screen == null || !screen.isFormatted()) return false; if (screen == null || !screen.isFormatted()) return false;
byte fa = screen.getFieldAttributeAt(screen.getCursorAddress()); byte fa = screen.getFieldAttributeAt(screen.getCursorAddress());
return faIsNumeric(fa & 0xFF); return faIsNumeric(fa & 0xFF);
} }
public boolean IsNumeric() {
return isNumeric();
}
public boolean isAlphanumeric() { public boolean isAlphanumeric() {
return !isNumeric(); return !isNumeric();
} }
public boolean IsAlphanumeric() {
return isAlphanumeric();
}
public boolean isDBCS() {
return getAlphanumericType() == TYPE_DBCS;
}
public boolean IsDBCS() {
return isDBCS();
}
public boolean isMessageWaiting() { public boolean isMessageWaiting() {
return false; return false;
} }
public boolean IsMessageWaiting() {
return isMessageWaiting();
}
public boolean isCommError() { public boolean isCommError() {
return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected(); return fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isConnected();
} }
public boolean IsCommError() {
return isCommError();
}
private int inhibitOverride = -1; private int inhibitOverride = -1;
public void setInputInhibited(int reason) { public void setInputInhibited(int reason) {
@@ -112,6 +140,10 @@ public class ECLOIA implements ECLConstants {
} }
} }
public void SetInputInhibited(int reason) {
setInputInhibited(reason);
}
/** /**
* Get the alphanumeric character entry type allowed at current cursor position. * Get the alphanumeric character entry type allowed at current cursor position.
* Returns TYPE_ALPHANUMERIC (0), TYPE_NUMERIC (1), or TYPE_DBCS (2). * Returns TYPE_ALPHANUMERIC (0), TYPE_NUMERIC (1), or TYPE_DBCS (2).
@@ -132,6 +164,10 @@ public class ECLOIA implements ECLConstants {
return TYPE_ALPHANUMERIC; return TYPE_ALPHANUMERIC;
} }
public int GetAlphanumericType() {
return getAlphanumericType();
}
public String getAlphanumericTypeString() { public String getAlphanumericTypeString() {
switch (getAlphanumericType()) { switch (getAlphanumericType()) {
case TYPE_NUMERIC: return "N"; case TYPE_NUMERIC: return "N";
@@ -141,38 +177,74 @@ public class ECLOIA implements ECLConstants {
} }
} }
public String GetAlphanumericTypeString() {
return getAlphanumericTypeString();
}
public boolean isXSystem() { public boolean isXSystem() {
return getInputInhibited() == INHIBIT_SYSTEM_LOCK; return getInputInhibited() == INHIBIT_SYSTEM_LOCK;
} }
public boolean IsXSystem() {
return isXSystem();
}
public boolean isXProt() { public boolean isXProt() {
return getInputInhibited() == INHIBIT_PROTECTED_FIELD; return getInputInhibited() == INHIBIT_PROTECTED_FIELD;
} }
public boolean IsXProt() {
return isXProt();
}
public boolean isXNum() { public boolean isXNum() {
return getInputInhibited() == INHIBIT_NUMERIC_ONLY; return getInputInhibited() == INHIBIT_NUMERIC_ONLY;
} }
public boolean IsXNum() {
return isXNum();
}
public boolean isXWait() { public boolean isXWait() {
return isXSystem(); return isXSystem();
} }
public boolean IsXWait() {
return isXWait();
}
public boolean isXInsert() { public boolean isXInsert() {
return isInsertMode(); return isInsertMode();
} }
public boolean IsXInsert() {
return isXInsert();
}
public boolean isXComm() { public boolean isXComm() {
return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK; return isCommError() || getInputInhibited() == INHIBIT_COMM_CHECK;
} }
public boolean IsXComm() {
return isXComm();
}
public boolean isXOverflow() { public boolean isXOverflow() {
return getInputInhibited() == INHIBIT_OVERFLOW; return getInputInhibited() == INHIBIT_OVERFLOW;
} }
public boolean IsXOverflow() {
return isXOverflow();
}
public boolean isXOperatorDue() { public boolean isXOperatorDue() {
return getInputInhibited() == INHIBIT_OPERATOR_DUE; return getInputInhibited() == INHIBIT_OPERATOR_DUE;
} }
public boolean IsXOperatorDue() {
return isXOperatorDue();
}
public String getStatusString() { public String getStatusString() {
int inhibit = getInputInhibited(); int inhibit = getInputInhibited();
switch (inhibit) { switch (inhibit) {
@@ -188,6 +260,10 @@ public class ECLOIA implements ECLConstants {
} }
} }
public String GetStatusString() {
return getStatusString();
}
/** /**
* Get the current Input Inhibited code. * Get the current Input Inhibited code.
* Returns one of INHIBIT_* constants from ECLConstants. * Returns one of INHIBIT_* constants from ECLConstants.
@@ -205,6 +281,18 @@ public class ECLOIA implements ECLConstants {
return INHIBIT_NOT_INHIBITED; return INHIBIT_NOT_INHIBITED;
} }
public int GetInputInhibited() {
return getInputInhibited();
}
public int getInputInhibitedType() {
return getInputInhibited();
}
public int GetInputInhibitedType() {
return getInputInhibited();
}
/** /**
* Block until the keyboard becomes unlocked and ready for user input, or timeout expires. * Block until the keyboard becomes unlocked and ready for user input, or timeout expires.
* @return true if keyboard unlocked, false if timeout occurred. * @return true if keyboard unlocked, false if timeout occurred.
@@ -205,21 +205,75 @@ public class ECLPS implements ECLConstants {
return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true); return SearchPSExt(text, 1, getSize(), SEARCH_FORWARD, false, true);
} }
public int searchPS(String text) {
return SearchPS(text);
}
public int SearchPS(String text, int startPos) {
return SearchPSExt(text, startPos, getSize(), SEARCH_FORWARD, false, true);
}
public int searchPS(String text, int startPos) {
return SearchPS(text, startPos);
}
public int SearchPS(String text, int startRow, int startCol) { public int SearchPS(String text, int startRow, int startCol) {
int pos = (startRow - 1) * getCols() + startCol; int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), SEARCH_FORWARD, false, true); return SearchPSExt(text, pos, getSize(), SEARCH_FORWARD, false, true);
} }
public int searchPS(String text, int startRow, int startCol) {
return SearchPS(text, startRow, startCol);
}
public int SearchPS(String text, int startRow, int startCol, int dir) { public int SearchPS(String text, int startRow, int startCol, int dir) {
int pos = (startRow - 1) * getCols() + startCol; int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), dir, false, true); return SearchPSExt(text, pos, getSize(), dir, false, true);
} }
public int searchPS(String text, int startRow, int startCol, int dir) {
return SearchPS(text, startRow, startCol, dir);
}
public int SearchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) { public int SearchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) {
int pos = (startRow - 1) * getCols() + startCol; int pos = (startRow - 1) * getCols() + startCol;
return SearchPSExt(text, pos, getSize(), dir, ignoreCase, true); return SearchPSExt(text, pos, getSize(), dir, ignoreCase, true);
} }
public int searchPS(String text, int startRow, int startCol, int dir, boolean ignoreCase) {
return SearchPS(text, startRow, startCol, dir, ignoreCase);
}
public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol) {
int sPos = (startRow - 1) * getCols() + startCol;
int ePos = (endRow - 1) * getCols() + endCol;
return SearchPSExt(text, sPos, ePos, SEARCH_FORWARD, false, false);
}
public int searchPS(String text, int startRow, int startCol, int endRow, int endCol) {
return SearchPS(text, startRow, startCol, endRow, endCol);
}
public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir) {
int sPos = (startRow - 1) * getCols() + startCol;
int ePos = (endRow - 1) * getCols() + endCol;
return SearchPSExt(text, sPos, ePos, dir, false, false);
}
public int searchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir) {
return SearchPS(text, startRow, startCol, endRow, endCol, dir);
}
public int SearchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase) {
int sPos = (startRow - 1) * getCols() + startCol;
int ePos = (endRow - 1) * getCols() + endCol;
return SearchPSExt(text, sPos, ePos, dir, ignoreCase, false);
}
public int searchPS(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase) {
return SearchPS(text, startRow, startCol, endRow, endCol, dir, ignoreCase);
}
/** /**
* SearchPS extended method conforming to IBM ECL specification. * SearchPS extended method conforming to IBM ECL specification.
* Uses 1-based positions and returns 1-based index (or 0 if not found). * Uses 1-based positions and returns 1-based index (or 0 if not found).
@@ -263,6 +317,21 @@ public class ECLPS implements ECLConstants {
return 0; return 0;
} }
public int searchPSExt(String text, int startPos, int endPos, int dir, boolean ignoreCase, boolean wrap) {
return SearchPSExt(text, startPos, endPos, dir, ignoreCase, wrap);
}
public int SearchPSExt(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase, boolean wrap) {
int cols = getCols();
int sPos = (startRow - 1) * cols + startCol;
int ePos = (endRow - 1) * cols + endCol;
return SearchPSExt(text, sPos, ePos, dir, ignoreCase, wrap);
}
public int searchPSExt(String text, int startRow, int startCol, int endRow, int endCol, int dir, boolean ignoreCase, boolean wrap) {
return SearchPSExt(text, startRow, startCol, endRow, endCol, dir, ignoreCase, wrap);
}
private boolean matchesAt(String screenText, String target, int pos, int size) { private boolean matchesAt(String screenText, String target, int pos, int size) {
int len = target.length(); int len = target.length();
for (int j = 0; j < len; j++) { for (int j = 0; j < len; j++) {
@@ -303,6 +372,10 @@ public class ECLPS implements ECLConstants {
return sb.toString(); return sb.toString();
} }
public String CopyString(int sRow, int sCol, int eRow, int eCol) {
return copyString(sRow, sCol, eRow, eCol);
}
/** /**
* Paste a multi-line rectangular block of text starting at (row, col). * Paste a multi-line rectangular block of text starting at (row, col).
*/ */
@@ -320,6 +393,12 @@ public class ECLPS implements ECLConstants {
String line = lines[i]; String line = lines[i];
for (int c = 0; c < line.length() && (col + c) < cols; c++) { for (int c = 0; c < line.length() && (col + c) < cols; c++) {
int pos = targetRow * cols + (col + c); int pos = targetRow * cols + (col + c);
if (screen.isFormatted()) {
byte fa = screen.getFieldAttributeAt(pos);
if (faIsProtected(fa & 0xFF) || screen.getCell(pos).isFieldAttribute()) {
continue;
}
}
setCursorPos(pos); setCursorPos(pos);
if (inputProcessor != null) { if (inputProcessor != null) {
inputProcessor.typeCharacter(line.charAt(c)); inputProcessor.typeCharacter(line.charAt(c));
@@ -330,6 +409,18 @@ public class ECLPS implements ECLConstants {
return count; return count;
} }
public int PasteString(String text, int row, int col) {
return pasteString(text, row, col);
}
public int pasteRectangular(String text, int row, int col) {
return pasteString(text, row, col);
}
public int PasteRectangular(String text, int row, int col) {
return pasteString(text, row, col);
}
/** /**
* Paste text with line wrapping across unprotected fields. * Paste text with line wrapping across unprotected fields.
*/ */
@@ -365,6 +456,108 @@ public class ECLPS implements ECLConstants {
return charsPasted; return charsPasted;
} }
public int PasteLineWrap(String text, int startPos, int endCol, boolean wordWrap) {
return pasteLineWrap(text, startPos, endCol, wordWrap);
}
// ========== Entry Assist & DOC Mode Operations ==========
public boolean isEntryAssistDOCmode() { return screen != null && screen.isEntryAssistDOCmode(); }
public boolean IsEntryAssistDOCmode() { return isEntryAssistDOCmode(); }
public void setEntryAssistDOCmode(boolean bl) { if (screen != null) screen.setEntryAssistDOCmode(bl); }
public void SetEntryAssistDOCmode(boolean bl) { setEntryAssistDOCmode(bl); }
public boolean isEntryAssistWordWrap() { return screen != null && screen.isEntryAssistWordWrap(); }
public boolean IsEntryAssistWordWrap() { return isEntryAssistWordWrap(); }
public void setEntryAssistWordWrap(boolean bl) { if (screen != null) screen.setEntryAssistWordWrap(bl); }
public void SetEntryAssistWordWrap(boolean bl) { setEntryAssistWordWrap(bl); }
public int getEntryAssistStartColumn() { return screen != null ? screen.getEntryAssistStartColumn() : 0; }
public int GetEntryAssistStartColumn() { return getEntryAssistStartColumn(); }
public void setEntryAssistStartColumn(int n) { if (screen != null) screen.setEntryAssistStartColumn(n); }
public void SetEntryAssistStartColumn(int n) { setEntryAssistStartColumn(n); }
public int getEntryAssistEndColumn() { return screen != null ? screen.getEntryAssistEndColumn() : 0; }
public int GetEntryAssistEndColumn() { return getEntryAssistEndColumn(); }
public void setEntryAssistEndColumn(int n) { if (screen != null) screen.setEntryAssistEndColumn(n); }
public void SetEntryAssistEndColumn(int n) { setEntryAssistEndColumn(n); }
public int[] getEntryAssistTabStops() { return screen != null ? screen.getEntryAssistTabStops() : null; }
public int[] GetEntryAssistTabStops() { return getEntryAssistTabStops(); }
public void setEntryAssistTabStops(int[] stops) { if (screen != null) screen.setEntryAssistTabStops(stops); }
public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); }
public void processWordTab(boolean forward) { if (inputProcessor != null) inputProcessor.processWordTab(forward); else if (screen != null) screen.processWordTab(forward); }
public void ProcessWordTab(boolean forward) { processWordTab(forward); }
public void wordTab(boolean forward) { processWordTab(forward); }
public void WordTab(boolean forward) { processWordTab(forward); }
public void processDeleteWord() { if (inputProcessor != null) inputProcessor.processDeleteWord(); else if (screen != null) screen.processDeleteWord(); }
public void ProcessDeleteWord() { processDeleteWord(); }
public void deleteWord() { processDeleteWord(); }
public void DeleteWord() { processDeleteWord(); }
public void processWordLeft() { if (inputProcessor != null) inputProcessor.processWordLeft(); }
public void ProcessWordLeft() { processWordLeft(); }
public void wordLeft() { processWordLeft(); }
public void WordLeft() { processWordLeft(); }
public void processWordRight() { if (inputProcessor != null) inputProcessor.processWordRight(); }
public void ProcessWordRight() { processWordRight(); }
public void wordRight() { processWordRight(); }
public void WordRight() { processWordRight(); }
public void processFieldEnd() { if (inputProcessor != null) inputProcessor.processFieldEnd(); }
public void ProcessFieldEnd() { processFieldEnd(); }
public void fieldEnd() { processFieldEnd(); }
public void FieldEnd() { processFieldEnd(); }
// ========== Field Management Accessors ==========
public ECLField getField(int pos) {
return getFieldList().findField(pos);
}
public ECLField GetField(int pos) {
return getField(pos);
}
public ECLField getField(int row, int col) {
return getFieldList().findField(row, col);
}
public ECLField GetField(int row, int col) {
return getField(row, col);
}
public ECLField getFirstField() {
return getFieldList().getFirstField();
}
public ECLField GetFirstField() {
return getFirstField();
}
public ECLField getNextField(ECLField prev) {
return getFieldList().getNextField(prev);
}
public ECLField GetNextField(ECLField prev) {
return getNextField(prev);
}
public ECLField getPreviousField(ECLField next) {
return getFieldList().getPreviousField(next);
}
public ECLField GetPreviousField(ECLField next) {
return getPreviousField(next);
}
public ECLFieldList GetFieldList() {
return getFieldList();
}
// ========== ECLPS Event Listener Management ========== // ========== ECLPS Event Listener Management ==========
private final java.util.List<ECLPSListener> psListeners = new java.util.concurrent.CopyOnWriteArrayList<>(); private final java.util.List<ECLPSListener> psListeners = new java.util.concurrent.CopyOnWriteArrayList<>();
@@ -455,6 +648,29 @@ public class ECLPS implements ECLConstants {
/** /**
* Send keystrokes with configurable inter-character or inter-mnemonic delay. * Send keystrokes with configurable inter-character or inter-mnemonic delay.
*/ */
public void sendCharacters(String keys) {
sendCharacters(keys, 0);
}
public void SendCharacters(String keys) {
sendCharacters(keys, 0);
}
public void SendCharacters(String keys, int delayMs) {
sendCharacters(keys, delayMs);
}
public void sendCharacters(String keys, int row, int col, int delayMs) {
if (row > 0 && col > 0) {
setCursorPos(row - 1, col - 1);
}
sendCharacters(keys, delayMs);
}
public void SendCharacters(String keys, int row, int col, int delayMs) {
sendCharacters(keys, row, col, delayMs);
}
public void sendCharacters(String keys, int delayMs) { public void sendCharacters(String keys, int delayMs) {
if (keys == null || keys.isEmpty()) return; if (keys == null || keys.isEmpty()) return;
if (delayMs <= 0) { if (delayMs <= 0) {
@@ -466,6 +682,11 @@ public class ECLPS implements ECLConstants {
int len = keys.length(); int len = keys.length();
while (i < len) { while (i < len) {
if (keys.charAt(i) == '[') { if (keys.charAt(i) == '[') {
if (i + 1 < len && keys.charAt(i + 1) == '[') {
// Escaped bracket "[["
sendKeys("[");
i += 2;
} else {
int close = keys.indexOf(']', i); int close = keys.indexOf(']', i);
if (close > i) { if (close > i) {
String mnemonic = keys.substring(i, close + 1); String mnemonic = keys.substring(i, close + 1);
@@ -475,6 +696,7 @@ public class ECLPS implements ECLConstants {
sendKeys(keys.substring(i, i + 1)); sendKeys(keys.substring(i, i + 1));
i++; i++;
} }
}
} else { } else {
sendKeys(keys.substring(i, i + 1)); sendKeys(keys.substring(i, i + 1));
i++; i++;
@@ -131,6 +131,22 @@ public class InputProcessor {
int baddr = screen.getCursorAddress(); int baddr = screen.getCursorAddress();
baddr = ((baddr % size) + size) % size; baddr = ((baddr % size) + size) % size;
// Entry Assist DOC mode / Word Wrap handling
if ((screen.isEntryAssistDOCmode() || screen.isEntryAssistWordWrap()) && !isNvtMode()) {
int curCol = baddr % screen.getCols();
int endCol = screen.getEntryAssistEndColumn();
int startCol = screen.getEntryAssistStartColumn();
if (curCol >= endCol) {
screen.handleWordWrap(baddr, ch);
baddr = screen.getCursorAddress();
if (ch == ' ' && (baddr % screen.getCols()) == startCol) {
screen.markAllChanged();
screen.updateDisplaySnapshot();
return;
}
}
}
if (screen.isFormatted()) { if (screen.isFormatted()) {
// Check if cursor is at a field attribute // Check if cursor is at a field attribute
ExtendedAttribute ea = screen.getCell(baddr); ExtendedAttribute ea = screen.getCell(baddr);
@@ -909,19 +909,108 @@ public class ScreenBuffer {
// ========== Entry Assist & DOC Mode Operations ========== // ========== Entry Assist & DOC Mode Operations ==========
public boolean isEntryAssistDOCmode() { return docMode; } public boolean isEntryAssistDOCmode() { return docMode; }
public boolean IsEntryAssistDOCmode() { return isEntryAssistDOCmode(); }
public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; } public void setEntryAssistDOCmode(boolean bl) { this.docMode = bl; }
public void SetEntryAssistDOCmode(boolean bl) { setEntryAssistDOCmode(bl); }
public boolean isEntryAssistWordWrap() { return wordWrap; } public boolean isEntryAssistWordWrap() { return wordWrap; }
public boolean IsEntryAssistWordWrap() { return isEntryAssistWordWrap(); }
public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; } public void setEntryAssistWordWrap(boolean bl) { this.wordWrap = bl; }
public void SetEntryAssistWordWrap(boolean bl) { setEntryAssistWordWrap(bl); }
public int getEntryAssistStartColumn() { return docStartCol; } public int getEntryAssistStartColumn() { return docStartCol; }
public int GetEntryAssistStartColumn() { return getEntryAssistStartColumn(); }
public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); } public void setEntryAssistStartColumn(int n) { this.docStartCol = Math.max(0, n); }
public void SetEntryAssistStartColumn(int n) { setEntryAssistStartColumn(n); }
public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); } public int getEntryAssistEndColumn() { return docEndCol >= 0 ? docEndCol : (cols - 1); }
public int GetEntryAssistEndColumn() { return getEntryAssistEndColumn(); }
public void setEntryAssistEndColumn(int n) { this.docEndCol = n; } public void setEntryAssistEndColumn(int n) { this.docEndCol = n; }
public void SetEntryAssistEndColumn(int n) { setEntryAssistEndColumn(n); }
public int[] getEntryAssistTabStops() { return tabStops; } public int[] getEntryAssistTabStops() { return tabStops; }
public int[] GetEntryAssistTabStops() { return getEntryAssistTabStops(); }
public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; } public void setEntryAssistTabStops(int[] stops) { this.tabStops = stops; }
public void SetEntryAssistTabStops(int[] stops) { setEntryAssistTabStops(stops); }
/**
* Perform Entry Assist word wrap if typing near/past end margin.
* Moves any partial word typed on the current line to the beginning of the next line (docStartCol).
*/
public synchronized boolean handleWordWrap(int curAddr, char typedChar) {
if (!docMode && !wordWrap) return false;
int size = rows * cols;
if (size <= 0) return false;
curAddr = ((curAddr % size) + size) % size;
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int endCol = getEntryAssistEndColumn();
int startCol = getEntryAssistStartColumn();
if (curCol < endCol) return false;
if (typedChar == ' ') {
int nextRow = (curRow + 1) % rows;
setCursorPosition(nextRow, startCol);
return true;
}
// Scan backwards to find the start of the current word on this row
int rowStartAddr = curRow * cols + startCol;
int scan = curAddr - 1;
while (scan >= rowStartAddr) {
ExtendedAttribute ea = getCell(scan);
if (ea.isFieldAttribute()) break;
int ec = ea.ec & 0xFF;
if (ec == 0 || ec == 0x40 || ea.ucs4 == ' ' || ea.ucs4 == 0) {
break;
}
scan--;
}
int wordStartAddr = scan + 1;
int wordStartCol = wordStartAddr % cols;
if (wordStartCol > startCol && wordStartAddr < curAddr) {
int wordLen = curAddr - wordStartAddr;
ExtendedAttribute[] wordCells = new ExtendedAttribute[wordLen];
for (int i = 0; i < wordLen; i++) {
wordCells[i] = new ExtendedAttribute();
wordCells[i].copyFrom(getCell(wordStartAddr + i));
getCell(wordStartAddr + i).clear();
}
int nextRow = (curRow + 1) % rows;
int targetAddr = nextRow * cols + startCol;
if (formatted) {
targetAddr = findNextUnprotected(targetAddr - 1);
}
for (int i = 0; i < wordLen; i++) {
int dst = (targetAddr + i) % size;
if (!getCell(dst).isFieldAttribute()) {
getCell(dst).copyFrom(wordCells[i]);
}
}
setCursorAddress((targetAddr + wordLen) % size);
screenChanged = true;
updateDisplaySnapshot();
return true;
} else {
// Word spans entire line or starts at startCol, wrap to next line
int nextRow = (curRow + 1) % rows;
int targetAddr = nextRow * cols + startCol;
if (formatted) {
targetAddr = findNextUnprotected(targetAddr - 1);
}
setCursorAddress(targetAddr);
screenChanged = true;
updateDisplaySnapshot();
return true;
}
}
public synchronized void processWordTab(boolean forward) { public synchronized void processWordTab(boolean forward) {
int size = rows * cols; int size = rows * cols;
@@ -1060,4 +1149,12 @@ public class ScreenBuffer {
screenChanged = true; screenChanged = true;
updateDisplaySnapshot(); updateDisplaySnapshot();
} }
/**
* Inspect and balance Shift-Out (0x0E) and Shift-In (0x0F) markers, ensuring DBCS integrity.
*/
public synchronized void balanceSOSI() {
cleanAdjacentSISO(0);
processSOSI();
}
} }
@@ -44,11 +44,41 @@ public class TelnetConnection {
*/ */
public void connect() throws IOException { public void connect() throws IOException {
ConnectionConfig.ProxyType proxyType = config.getProxyType(); ConnectionConfig.ProxyType proxyType = config.getProxyType();
boolean hasProxy = proxyType != null && proxyType != ConnectionConfig.ProxyType.NONE && String proxyHost = config.getProxyHost();
config.getProxyHost() != null && !config.getProxyHost().trim().isEmpty(); int proxyPort = config.getProxyPort();
String proxyUser = config.getProxyUsername();
String proxyPass = config.getProxyPassword();
String connectHost = hasProxy ? config.getProxyHost().trim() : config.getHost(); if ((proxyType == null || proxyType == ConnectionConfig.ProxyType.NONE) && (proxyHost == null || proxyHost.trim().isEmpty())) {
int connectPort = hasProxy ? (config.getProxyPort() > 0 ? config.getProxyPort() : (proxyType == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080)) : config.getPort(); // Check JVM system properties (matching IBM HoD browser/system default fallback)
String sysSocks = System.getProperty("socksProxyHost");
String sysHttp = System.getProperty("http.proxyHost");
if (sysSocks != null && !sysSocks.trim().isEmpty()) {
proxyType = ConnectionConfig.ProxyType.SOCKS5;
proxyHost = sysSocks.trim();
String portStr = System.getProperty("socksProxyPort");
if (portStr != null) {
try { proxyPort = Integer.parseInt(portStr.trim()); } catch (NumberFormatException ignored) {}
}
proxyUser = System.getProperty("java.net.socks.username");
proxyPass = System.getProperty("java.net.socks.password");
} else if (sysHttp != null && !sysHttp.trim().isEmpty()) {
proxyType = ConnectionConfig.ProxyType.HTTP;
proxyHost = sysHttp.trim();
String portStr = System.getProperty("http.proxyPort");
if (portStr != null) {
try { proxyPort = Integer.parseInt(portStr.trim()); } catch (NumberFormatException ignored) {}
}
proxyUser = System.getProperty("http.proxyUser");
proxyPass = System.getProperty("http.proxyPassword");
}
}
boolean hasProxy = proxyType != null && proxyType != ConnectionConfig.ProxyType.NONE &&
proxyHost != null && !proxyHost.trim().isEmpty();
String connectHost = hasProxy ? proxyHost.trim() : config.getHost();
int connectPort = hasProxy ? (proxyPort > 0 ? proxyPort : (proxyType == ConnectionConfig.ProxyType.HTTP ? 8080 : 1080)) : config.getPort();
log.info("Connecting TCP socket to " + connectHost + ":" + connectPort + log.info("Connecting TCP socket to " + connectHost + ":" + connectPort +
(hasProxy ? " (via " + proxyType + " proxy for " + config.getHost() + ":" + config.getPort() + ")" : "") + (hasProxy ? " (via " + proxyType + " proxy for " + config.getHost() + ":" + config.getPort() + ")" : "") +
@@ -67,13 +97,13 @@ public class TelnetConnection {
if (hasProxy) { if (hasProxy) {
switch (proxyType) { switch (proxyType) {
case HTTP: case HTTP:
establishHttpProxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword()); establishHttpProxy(rawSocket, config.getHost(), config.getPort(), proxyUser, proxyPass);
break; break;
case SOCKS4: case SOCKS4:
establishSocks4Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername()); establishSocks4Proxy(rawSocket, config.getHost(), config.getPort(), proxyUser);
break; break;
case SOCKS5: case SOCKS5:
establishSocks5Proxy(rawSocket, config.getHost(), config.getPort(), config.getProxyUsername(), config.getProxyPassword()); establishSocks5Proxy(rawSocket, config.getHost(), config.getPort(), proxyUser, proxyPass);
break; break;
default: default:
break; break;
@@ -0,0 +1,113 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ExtendedAttribute;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLOIAPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLOIA oia;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
oia = new ECLOIA(screen, input, null);
}
@Test
public void testInhibitionCodesAndStatusStrings() {
screen.erase(false);
assertEquals(INHIBIT_NOT_INHIBITED, oia.GetInputInhibited());
assertEquals(INHIBIT_NOT_INHIBITED, oia.GetInputInhibitedType());
assertEquals("READY", oia.GetStatusString());
// Test X-SYSTEM
oia.SetInputInhibited(INHIBIT_SYSTEM_LOCK);
assertTrue(oia.IsXSystem());
assertTrue(oia.IsXWait());
assertEquals("X-SYSTEM", oia.GetStatusString());
// Test X-NUM
oia.SetInputInhibited(INHIBIT_NUMERIC_ONLY);
assertTrue(oia.IsXNum());
assertEquals("X-NUM", oia.GetStatusString());
// Test X-PROT
oia.SetInputInhibited(INHIBIT_PROTECTED_FIELD);
assertTrue(oia.IsXProt());
assertEquals("X-PROT", oia.GetStatusString());
// Test X-OVERFLOW
oia.SetInputInhibited(INHIBIT_OVERFLOW);
assertTrue(oia.IsXOverflow());
assertEquals("X-OVERFLOW", oia.GetStatusString());
// Test X-COMM
oia.SetInputInhibited(INHIBIT_COMM_CHECK);
assertTrue(oia.IsXComm());
assertEquals("X-COMM", oia.GetStatusString());
// Test X-OP
oia.SetInputInhibited(INHIBIT_OPERATOR_DUE);
assertTrue(oia.IsXOperatorDue());
assertEquals("X-OP", oia.GetStatusString());
// Test X-INSERT
oia.SetInputInhibited(INHIBIT_NOT_INHIBITED);
input.setInsertMode(true);
assertTrue(oia.IsInsertMode());
assertTrue(oia.isXInsert());
assertEquals("X-INSERT", oia.GetStatusString());
}
@Test
public void testAlphanumericTypeGetters() {
screen.erase(false);
assertEquals(TYPE_ALPHANUMERIC, oia.GetAlphanumericType());
assertEquals("A", oia.GetAlphanumericTypeString());
assertTrue(oia.IsAlphanumeric());
assertFalse(oia.IsNumeric());
assertFalse(oia.IsDBCS());
// Numeric field
screen.setFieldAttribute(0, (byte) (FA_PRINTABLE | FA_NUMERIC));
screen.setCursorAddress(1);
assertEquals(TYPE_NUMERIC, oia.GetAlphanumericType());
assertEquals("N", oia.GetAlphanumericTypeString());
assertTrue(oia.IsNumeric());
assertFalse(oia.IsAlphanumeric());
// DBCS cell
ExtendedAttribute ea = screen.getCell(1);
ea.cs = ExtendedAttribute.CS_DBCS;
assertEquals(TYPE_DBCS, oia.GetAlphanumericType());
assertEquals("D", oia.GetAlphanumericTypeString());
assertTrue(oia.IsDBCS());
}
@Test
public void testSynchronizationPrimitives() {
screen.erase(false);
assertTrue(oia.WaitForInput(100));
assertTrue(oia.WaitForSystemAvailable(100));
assertTrue(oia.WaitForAppAvailable(100));
// Lock keyboard, wait should timeout
input.setKeyboardLocked(true);
assertFalse(oia.WaitForInput(50));
input.setKeyboardLocked(false);
}
}
@@ -0,0 +1,166 @@
package haus.nightmare.lib3270j.ecl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.input.InputProcessor;
import haus.nightmare.lib3270j.screen.ScreenBuffer;
import static haus.nightmare.lib3270j.ecl.ECLConstants.*;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ECLPSPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
private ECLPS ps;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
ps = new ECLPS(screen, input, translator);
}
@Test
public void testSearchPSComprehensiveOverloads() {
screen.erase(false);
screen.setText("IBM Host On-Demand ECL Automation Presentation Space");
// 1-based indexing overloads
assertEquals(5, ps.SearchPS("Host"));
assertEquals(5, ps.searchPS("Host"));
assertEquals(5, ps.SearchPS("Host", 1));
assertEquals(5, ps.SearchPS("Host", 1, SEARCH_FORWARD));
assertEquals(5, ps.SearchPS("Host", 1, 1));
assertEquals(5, ps.SearchPS("host", 1, 1, SEARCH_FORWARD, true));
assertEquals(0, ps.SearchPS("host", 1, 1, SEARCH_FORWARD, false));
// Bounded range searches
assertEquals(5, ps.SearchPS("Host", 1, 1, 1, 30));
assertEquals(0, ps.SearchPS("Host", 1, 10, 1, 30)); // starts after "Host"
assertEquals(5, ps.SearchPS("host", 1, 1, 1, 30, DIR_FORWARD, true));
// SearchPSExt
int pos1 = ps.SearchPSExt("ECL", 1, 50, SEARCH_FORWARD, false, false);
assertEquals(20, pos1);
int pos2 = ps.SearchPSExt("ecl", 1, 1, 1, 50, DIR_FORWARD, true, false);
assertEquals(20, pos2);
// Backward search
screen.setChar(1, 10, 'Z');
screen.setChar(1, 11, 'O');
screen.setChar(1, 12, 'O');
int backFound = ps.SearchPSExt("ZOO", 200, 1, SEARCH_BACKWARD, false, false);
assertEquals(91, backFound); // row 1 col 10 is 80 + 10 + 1 = 91
}
@Test
public void testRectangularCopyAndPaste() {
screen.erase(false);
// Fill a small grid across rows 0, 1, 2
String r0 = "ROW0_DATA_TEST";
for (int c = 0; c < r0.length(); c++) screen.setChar(0, c, r0.charAt(c));
String r1 = "ROW1_DATA_TEST";
for (int c = 0; c < r1.length(); c++) screen.setChar(1, c, r1.charAt(c));
String r2 = "ROW2_DATA_TEST";
for (int c = 0; c < r2.length(); c++) screen.setChar(2, c, r2.charAt(c));
// Copy rectangular region rows 0..2, cols 0..3
String copied = ps.copyString(0, 0, 2, 3);
assertEquals("ROW0\nROW1\nROW2", copied);
// Test inverted coordinate normalization
String inverted = ps.CopyString(2, 3, 0, 0);
assertEquals("ROW0\nROW1\nROW2", inverted);
// Paste rectangular text
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(80, (byte) FA_PRINTABLE);
screen.setFieldAttribute(160, (byte) FA_PRINTABLE);
int pasted = ps.PasteString("ABC\nDEF\nGHI", 0, 1);
assertEquals(9, pasted);
assertEquals("ABC", ps.getString(1, 3));
assertEquals("DEF", ps.getString(81, 3));
assertEquals("GHI", ps.getString(161, 3));
}
@Test
public void testSendCharactersWithMnemonicsAndEscaping() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(20, (byte) FA_PRINTABLE);
screen.setCursorAddress(1);
// Test mnemonic [tab] and escaped bracket [[
ps.sendCharacters("A[[B[tab]C", 1);
assertEquals('A', screen.getChar(0, 1));
assertEquals('[', screen.getChar(0, 2));
assertEquals('B', screen.getChar(0, 3));
// After tab, at pos 21
assertEquals('C', screen.getChar(0, 21));
}
@Test
public void testEntryAssistPassThroughMethods() {
screen.erase(false);
ps.SetEntryAssistDOCmode(true);
ps.SetEntryAssistWordWrap(true);
ps.SetEntryAssistStartColumn(4);
ps.SetEntryAssistEndColumn(72);
ps.SetEntryAssistTabStops(new int[]{ 8, 16, 24, 32 });
assertTrue(ps.IsEntryAssistDOCmode());
assertTrue(ps.IsEntryAssistWordWrap());
assertEquals(4, ps.GetEntryAssistStartColumn());
assertEquals(72, ps.GetEntryAssistEndColumn());
assertArrayEquals(new int[]{ 8, 16, 24, 32 }, ps.GetEntryAssistTabStops());
ps.setCursorPos(0, 0);
ps.WordTab(true);
assertEquals(8, ps.getCursorCol());
ps.WordTab(true);
assertEquals(16, ps.getCursorCol());
ps.WordTab(false);
assertEquals(8, ps.getCursorCol());
}
@Test
public void testFieldManagementAccessors() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(50, (byte) (FA_PRINTABLE | FA_PROTECT));
assertNotNull(ps.GetFieldList());
assertEquals(2, ps.GetFieldList().GetFieldCount());
ECLField f0 = ps.GetFirstField();
assertNotNull(f0);
assertEquals(0, f0.GetStart());
assertEquals(1, f0.GetDataStart());
assertEquals(49, f0.GetEnd());
assertEquals(49, f0.GetLength());
assertFalse(f0.IsProtected());
ECLField f1 = ps.GetNextField(f0);
assertNotNull(f1);
assertEquals(50, f1.GetStart());
assertTrue(f1.IsProtected());
assertEquals(f0.GetStart(), ps.GetPreviousField(f1).GetStart());
assertEquals(f0.GetStart(), ps.GetField(25).GetStart());
assertEquals(f1.GetStart(), ps.GetField(60).GetStart());
}
}
@@ -0,0 +1,304 @@
package haus.nightmare.lib3270j.graphics;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.awt.Point;
import static org.junit.jupiter.api.Assertions.*;
/**
* Verification test suite for Phase 3: Host Graphics (GOCA / 3179G / Programmed Symbols) (GDDM).
* Covers items 3.1 through 3.6 of the PhasedUpdates specification.
*/
public class GocaPhase3Test {
// ====================================================================================================
// ITEM 3.1: GOCA Order 0x23 / 0x27 - Viewing Window (Clipping Viewport)
// ====================================================================================================
@Test
@DisplayName("Item 3.1: Viewing Window (0x23 / 0x27) clipping bounds and raster isolation")
public void testItem3_1_ViewingWindowClipping() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
assertFalse(plane.isViewingWindowActive());
// Define Viewing Window: x in [-100, 100], y in [-100, 100]
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSVW_DEF); // 0x23
out.write(0x08); // 8-byte payload
out.write((byte) 0xFF); out.write((byte) 0x9C); // xMin = -100
out.write((byte) 0xFF); out.write((byte) 0x9C); // yMin = -100
out.write(0x00); out.write(100); // xMax = 100
out.write(0x00); out.write(100); // yMax = 100
byte[] vwOrder = out.toByteArray();
decoder.decodeGoca(vwOrder, 0, vwOrder.length);
assertTrue(plane.isViewingWindowActive());
assertEquals(-100, plane.getViewingWindowXMin());
assertEquals(-100, plane.getViewingWindowYMin());
assertEquals(100, plane.getViewingWindowXMax());
assertEquals(100, plane.getViewingWindowYMax());
// 1. Draw a line entirely outside viewing window: (-300, -150) to (-200, -150)
ByteArrayOutputStream lineOutside = new ByteArrayOutputStream();
lineOutside.write(GocaConstants.G_GSCOL); lineOutside.write(0x02); // Red
lineOutside.write(GocaConstants.G_GLINE); lineOutside.write(0x08);
lineOutside.write((byte) 0xFE); lineOutside.write((byte) 0xD4); // -300
lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x6A); // -150
lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x38); // -200
lineOutside.write((byte) 0xFF); lineOutside.write((byte) 0x6A); // -150
byte[] lineOutBytes = lineOutside.toByteArray();
decoder.decodeGoca(lineOutBytes, 0, lineOutBytes.length);
int pxClipped = plane.mapX(-300);
int pyClipped = plane.mapY(-150);
int pixelVal = plane.getRgbBuffer()[pyClipped * plane.getCanvasWidth() + pxClipped];
assertEquals(0, pixelVal, "Pixel outside viewing window must be clipped (0)");
// 2. Draw a line inside viewing window: (0, 0) to (50, 50)
ByteArrayOutputStream lineInside = new ByteArrayOutputStream();
lineInside.write(GocaConstants.G_GSCOL); lineInside.write(0x04); // Green
lineInside.write(GocaConstants.G_GLINE); lineInside.write(0x08);
lineInside.write(0x00); lineInside.write(0); lineInside.write(0x00); lineInside.write(0);
lineInside.write(0x00); lineInside.write(50); lineInside.write(0x00); lineInside.write(50);
byte[] lineInBytes = lineInside.toByteArray();
decoder.decodeGoca(lineInBytes, 0, lineInBytes.length);
int pxIn = plane.mapX(0);
int pyIn = plane.mapY(0);
int pixelInVal = plane.getRgbBuffer()[pyIn * plane.getCanvasWidth() + pxIn];
assertNotEquals(0, pixelInVal, "Pixel inside viewing window must be rendered");
// 3. Clear viewing window via zero-length payload
ByteArrayOutputStream clearVw = new ByteArrayOutputStream();
clearVw.write(GocaConstants.G_GSVW);
clearVw.write(0x00);
byte[] clearVwBytes = clearVw.toByteArray();
decoder.decodeGoca(clearVwBytes, 0, clearVwBytes.length);
assertFalse(plane.isViewingWindowActive());
}
// ====================================================================================================
// ITEM 3.2: GOCA Order 0x22 - Segment Characteristics
// ====================================================================================================
@Test
@DisplayName("Item 3.2: Segment Characteristics (0x22) flags decoding")
public void testItem3_2_SegmentCharacteristics() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// Chained (0x80) | Dynamic (0x40) | Visible (0x00) -> 0xC0
decoder.processSegmentCharacteristics(new byte[]{(byte) 0xC0});
assertTrue(decoder.isSegChained());
assertTrue(decoder.isSegDynamic());
assertTrue(decoder.isSegVisible());
// Non-chained (0x00) | Static (0x00) | Invisible (0x20) -> 0x20
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x20});
assertFalse(decoder.isSegChained());
assertFalse(decoder.isSegDynamic());
assertFalse(decoder.isSegVisible());
// Unchained (0x00) | Dynamic (0x40) | Visible (0x00) -> 0x40
decoder.processSegmentCharacteristics(new byte[]{(byte) 0x40});
assertFalse(decoder.isSegChained());
assertTrue(decoder.isSegDynamic());
assertTrue(decoder.isSegVisible());
}
// ====================================================================================================
// ITEM 3.3: GOCA Order 0x11 - Fractional Line Width Calculation
// ====================================================================================================
@Test
@DisplayName("Item 3.3: Fractional Line Width (0x11) fixed-point ratio and plane synchronization")
public void testItem3_3_FractionalLineWidth() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// 1. Fixed point: integer = 3, fraction = 128/256 (0.5) -> 3.5
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSFLW); // 0x11
out.write(0x03); // Integer part
out.write(0x80); // Fractional part (128 / 256 = 0.5)
byte[] flwBytes = out.toByteArray();
decoder.decodeGoca(flwBytes, 0, flwBytes.length);
assertEquals(3.5, decoder.getFractionalLineWidth(), 0.001);
assertEquals(3.5, plane.getFractionalLineWidth(), 0.001);
// 2. Single byte operand: integer = 2, frac = 0 -> 2.0
ByteArrayOutputStream out2 = new ByteArrayOutputStream();
out2.write(GocaConstants.G_GSFLW);
out2.write(0x02);
byte[] flwBytes2 = out2.toByteArray();
decoder.decodeGoca(flwBytes2, 0, flwBytes2.length);
assertEquals(2.0, decoder.getFractionalLineWidth(), 0.001);
assertEquals(2.0, plane.getFractionalLineWidth(), 0.001);
// 3. Reset defaults restores 1.0
decoder.resetDefaults();
assertEquals(1.0, decoder.getFractionalLineWidth(), 0.001);
assertEquals(1.0, plane.getFractionalLineWidth(), 0.001);
}
// ====================================================================================================
// ITEM 3.4: GCS Character Baseline Angle and Shear Vector Transforms
// ====================================================================================================
@Test
@DisplayName("Item 3.4: Character Baseline Angle (GSCA 0x34) and Shear (GSCR 0x35) transforms")
public void testItem3_4_CharacterAngleAndShear() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// 1. GSCA 0x34: Character Angle vector (ax=0, ay=10) -> 90 degrees vertical
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(GocaConstants.G_GSCA);
out.write(0x04);
out.write(0x00); out.write(0); // ax = 0
out.write(0x00); out.write(10); // ay = 10
// GSCR 0x35: Character Shear vector (sx=10, sy=10) -> 45 degrees slant
out.write(GocaConstants.G_GSCR);
out.write(0x04);
out.write(0x00); out.write(10); // sx = 10
out.write(0x00); out.write(10); // sy = 10
byte[] stream = out.toByteArray();
decoder.decodeGoca(stream, 0, stream.length);
assertEquals(90.0, decoder.getCharAngle(), 0.01);
assertEquals(45.0, decoder.getCharShear(), 0.01);
// 2. Draw stroked vector text with active angle and shear
ByteArrayOutputStream textStream = new ByteArrayOutputStream();
textStream.write(GocaConstants.G_GSCS); textStream.write(0xF8); // Vector font
textStream.write(GocaConstants.G_GCHST); // Character String Absolute
textStream.write(0x05); // 4 bytes coord + 1 byte text 'A' (EBCDIC 0xC1)
textStream.write(0x00); textStream.write(50);
textStream.write(0x00); textStream.write(50);
textStream.write((byte) 0xC1); // EBCDIC 'A'
byte[] textBytes = textStream.toByteArray();
decoder.decodeGoca(textBytes, 0, textBytes.length);
assertTrue(plane.hasContent(), "Vector text with angle/shear must render to GraphicsPlane");
}
// ====================================================================================================
// ITEM 3.5: Multi-Plane Programmed Symbols (RWS 4..7) & Multi-Color PS Cells
// ====================================================================================================
@Test
@DisplayName("Item 3.5: Multi-Plane Programmed Symbols (RWS 4..7) & Triple-Plane Composite Color Cells")
public void testItem3_5_ProgrammedSymbolsTriplePlane() {
ProgramSymbolManager psm = new ProgramSymbolManager(9, 16);
// 1. Single-plane set (RWS 2)
byte[] loadpsSingle = new byte[4 + 18];
loadpsSingle[0] = 0x01; // Format 1
loadpsSingle[1] = 0x41; // LCID 0x41
loadpsSingle[2] = 0x40; // Start codepoint
loadpsSingle[3] = 0x02; // RWS 2
for (int i = 0; i < 18; i++) loadpsSingle[4 + i] = (byte) 0x55;
psm.loadps(loadpsSingle);
ProgramSymbolSet singleSet = psm.getSymbolSet(0x41);
assertNotNull(singleSet);
assertFalse(singleSet.isTriplePlane());
// 2. Triple-plane set (RWS 4, LCID 0x42) loaded with all 3 planes simultaneously (54 bytes)
byte[] loadpsTriple = new byte[4 + 54];
loadpsTriple[0] = 0x01; // Format 1
loadpsTriple[1] = 0x42; // LCID 0x42
loadpsTriple[2] = 0x40; // Start codepoint
loadpsTriple[3] = 0x04; // RWS 4 (Triple Plane)
// Red slice: plane 1 (0x01)
for (int i = 0; i < 18; i++) loadpsTriple[4 + i] = (byte) 0xFF;
// Green slice: plane 2 (0x02)
for (int i = 0; i < 18; i++) loadpsTriple[4 + 18 + i] = (byte) 0xFF;
// Blue slice: plane 4 (0x04)
for (int i = 0; i < 18; i++) loadpsTriple[4 + 36 + i] = (byte) 0xFF;
psm.loadps(loadpsTriple);
ProgramSymbolSet tripleSet = psm.getSymbolSet(0x42);
assertNotNull(tripleSet);
assertTrue(tripleSet.isTriplePlane());
ProgramSymbolSet.SymbolSlot slot = psm.getSymbol(0x42, 0x40);
assertNotNull(slot);
assertTrue(slot.isTriplePlane());
// Verify multi-color RGB mapping: composite plane (1|2|4 = 7 -> White)
int[] rgb = slot.getRgbPixels(GocaConstants.GOCA_COLORS[0], 0);
assertNotNull(rgb);
assertEquals(9 * 16, rgb.length);
assertEquals(0xFFFFFFFF, rgb[0]); // White pixel (all 3 planes set)
}
// ====================================================================================================
// ITEM 3.6: Pick Correlation for Graphical Segment Selection
// ====================================================================================================
@Test
@DisplayName("Item 3.6: Pick Correlation & 56-Byte Graphic Input Structured Field generation")
public void testItem3_6_PickCorrelationAndGraphicInput() {
GraphicsPlane plane = new GraphicsPlane(400, 300);
GocaDecoder decoder = new GocaDecoder(plane);
// 1. Define Segment 100: drawing a line from (-50, -50) to (50, 50)
ByteArrayOutputStream segStream = new ByteArrayOutputStream();
segStream.write(GocaConstants.G_BEGSEGM);
segStream.write(0x0C);
segStream.write(0x00); segStream.write(0x00); segStream.write(0x00); segStream.write(100); // Seg ID 100
segStream.write(0x00); segStream.write(0x00);
segStream.write(0x00); segStream.write(0x00);
segStream.write(0x00); segStream.write(0x00); segStream.write(0x00); segStream.write(0x00);
segStream.write(GocaConstants.G_GSCOL); segStream.write(0x06); // Yellow
segStream.write(GocaConstants.G_GLINE); segStream.write(0x08);
segStream.write((byte) 0xFF); segStream.write((byte) 0xCE); // -50
segStream.write((byte) 0xFF); segStream.write((byte) 0xCE); // -50
segStream.write(0x00); segStream.write(50);
segStream.write(0x00); segStream.write(50);
segStream.write(GocaConstants.G_ENDSEGM); segStream.write(0x00);
byte[] segBytes = segStream.toByteArray();
decoder.decodeGoca(segBytes, 0, segBytes.length);
// Hit test inside segment bounds: (0, 0)
int pickedSeg = decoder.findPickedSegment(0, 0);
assertEquals(100, pickedSeg, "Segment 100 should be hit at (0, 0)");
// Hit test outside segment bounds: (200, 200)
int pickedMiss = decoder.findPickedSegment(200, 200);
assertEquals(0, pickedMiss, "Missed click should default to segId 0");
// 2. Build 56-byte Graphic Input Structured Field via GraphicInputBuilder
byte[] inputSF = GraphicInputBuilder.buildPickCorrelation(25, -25, 5);
assertEquals(56, inputSF.length);
// Header check: 0x0034 (52 byte payload length per IBM HOD)
assertEquals(0x00, inputSF[0]);
assertEquals(0x34, inputSF[1]);
assertEquals(0x0F, inputSF[2]);
assertEquals(0x0F, inputSF[3]);
// Coordinate checks: X = 25, Y = -25
int gX = ((inputSF[24] & 0xFF) << 8) | (inputSF[25] & 0xFF);
int gY = (short) (((inputSF[26] & 0xFF) << 8) | (inputSF[27] & 0xFF));
assertEquals(25, gX);
assertEquals(-25, gY);
// Mouse action flags: trigger class = 4, correlation class = 4, Button 1 = 0x01
assertEquals(0x04, inputSF[31]);
assertEquals(0x04, inputSF[33]);
assertEquals(0x01, inputSF[35]);
}
}
@@ -0,0 +1,223 @@
package haus.nightmare.lib3270j.screen;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import haus.nightmare.lib3270j.TerminalModel;
import haus.nightmare.lib3270j.charset.AbstractDBCSCodePage;
import haus.nightmare.lib3270j.charset.EbcdicTranslator;
import haus.nightmare.lib3270j.ecl.ECLField;
import haus.nightmare.lib3270j.ecl.ECLFieldList;
import haus.nightmare.lib3270j.input.InputProcessor;
import static haus.nightmare.lib3270j.protocol.DS3270Constants.*;
public class ScreenBufferPhase4Test {
private ScreenBuffer screen;
private EbcdicTranslator translator;
private InputProcessor input;
@BeforeEach
public void setup() {
translator = new EbcdicTranslator();
screen = new ScreenBuffer(TerminalModel.IBM_3278_2, translator);
input = new InputProcessor(screen, translator, null);
}
@Test
public void testEntryAssistDOCModeAndWordWrap() {
screen.erase(false);
screen.setEntryAssistDOCmode(true);
screen.setEntryAssistWordWrap(true);
screen.setEntryAssistStartColumn(0);
screen.setEntryAssistEndColumn(20);
assertTrue(screen.isEntryAssistDOCmode());
assertTrue(screen.isEntryAssistWordWrap());
assertEquals(0, screen.getEntryAssistStartColumn());
assertEquals(20, screen.getEntryAssistEndColumn());
// Type "HELLO WORLD " near column 20
screen.setCursorPosition(0, 10);
String s1 = "HELLO ";
for (int i = 0; i < s1.length(); i++) {
input.typeCharacter(s1.charAt(i));
}
assertEquals(16, screen.getCursorCol());
assertEquals(0, screen.getCursorRow());
// Now type "TESTING" which starts at col 16 and crosses col 20
String s2 = "TESTING";
for (int i = 0; i < s2.length(); i++) {
input.typeCharacter(s2.charAt(i));
}
// "HELLO " should remain on row 0 (cols 10..15)
assertEquals("HELLO ", screen.getString(10, 6));
// "TESTING" should be soft-wrapped to row 1 (cols 0..6)
assertEquals("TESTING", screen.getString(80, 7));
assertEquals(1, screen.getCursorRow());
assertEquals(7, screen.getCursorCol());
}
@Test
public void testEntryAssistWordWrapTypingSpaceAtEndMargin() {
screen.erase(false);
screen.setEntryAssistWordWrap(true);
screen.setEntryAssistStartColumn(5);
screen.setEntryAssistEndColumn(20);
screen.setCursorPosition(0, 20);
input.typeCharacter(' ');
// Cursor should wrap to next line at startCol 5
assertEquals(1, screen.getCursorRow());
assertEquals(5, screen.getCursorCol());
}
@Test
public void testEntryAssistTabStopsAndNavigation() {
screen.erase(false);
screen.setEntryAssistTabStops(new int[]{ 10, 25, 50, 70 });
assertArrayEquals(new int[]{ 10, 25, 50, 70 }, screen.getEntryAssistTabStops());
screen.setCursorPosition(0, 0);
screen.processWordTab(true);
assertEquals(10, screen.getCursorCol());
screen.processWordTab(true);
assertEquals(25, screen.getCursorCol());
screen.processWordTab(true);
assertEquals(50, screen.getCursorCol());
screen.processWordTab(false);
assertEquals(25, screen.getCursorCol());
screen.processWordTab(false);
assertEquals(10, screen.getCursorCol());
}
@Test
public void testProcessDeleteWord() {
screen.erase(false);
screen.setFieldAttribute(0, (byte) FA_PRINTABLE);
screen.setFieldAttribute(40, (byte) (FA_PRINTABLE | FA_PROTECT));
// Field contains "ALPHA BRAVO CHARLIE"
String text = "ALPHA BRAVO CHARLIE";
for (int i = 0; i < text.length(); i++) {
screen.setChar(0, 1 + i, text.charAt(i));
}
// Place cursor at "BRAVO" (col 7)
screen.setCursorPosition(0, 7);
screen.processDeleteWord();
// "BRAVO " should be deleted, leaving "ALPHA CHARLIE"
assertEquals("ALPHA CHARLIE", screen.getString(1, 13));
assertTrue(screen.isModified(1));
}
@Test
public void testDBCSSOSIPairBalancing() {
EbcdicTranslator dbcsTranslator = new EbcdicTranslator("930");
AbstractDBCSCodePage cp = (AbstractDBCSCodePage) dbcsTranslator.getCodePage();
cp.registerDbcsPair(0x4341, '\u6771');
ScreenBuffer dbcsScreen = new ScreenBuffer(TerminalModel.IBM_3279_2, dbcsTranslator);
dbcsScreen.erase(false);
dbcsScreen.setFieldAttribute(0, (byte) FA_PRINTABLE);
dbcsScreen.setFieldAttribute(20, (byte) (FA_PRINTABLE | FA_PROTECT));
// Insert DBCS character
boolean inserted = dbcsScreen.insertChar(1, '\u6771');
assertTrue(inserted);
assertEquals(ExtendedAttribute.CS_DBCS, dbcsScreen.getCell(1).cs);
assertEquals(ExtendedAttribute.DB_LEFT, dbcsScreen.getCell(1).db);
assertEquals(ExtendedAttribute.DB_RIGHT, dbcsScreen.getCell(2).db);
// Delete DBCS character at left half
boolean deleted = dbcsScreen.deleteChar(1);
assertTrue(deleted);
assertEquals(0, dbcsScreen.getCell(1).ec);
assertEquals(0, dbcsScreen.getCell(2).ec);
// Test balanceSOSI removes adjacent empty SO SI
dbcsScreen.setCell(3, 0x0E); // SO
dbcsScreen.setCell(4, 0x0F); // SI
dbcsScreen.balanceSOSI();
assertEquals(0, dbcsScreen.getCell(3).ec);
assertEquals(0, dbcsScreen.getCell(4).ec);
}
@Test
public void testWrappedFieldListHandling() {
screen.erase(false);
// Field 1 at pos 100..499
screen.setFieldAttribute(100, (byte) FA_PRINTABLE);
// Field 2 at pos 500..99 (wrapping around end of buffer)
screen.setFieldAttribute(500, (byte) (FA_PRINTABLE | FA_PROTECT));
ECLFieldList list = screen.buildFieldList();
assertEquals(2, list.getFieldCount());
ECLField f1 = list.getFirstField();
assertNotNull(f1);
assertEquals(100, f1.getStart());
assertEquals(101, f1.getDataStart());
assertEquals(499, f1.getEnd());
assertEquals(399, f1.getLength());
assertFalse(f1.isWrapped());
assertFalse(f1.isProtected());
ECLField f2 = list.getNextField(f1);
assertNotNull(f2);
assertEquals(500, f2.getStart());
assertEquals(501, f2.getDataStart());
assertEquals(99, f2.getEnd());
assertEquals(1920 - 500 - 1 + 100, f2.getLength());
assertTrue(f2.isWrapped());
assertTrue(f2.isProtected());
// Test containment across wrap
assertTrue(f2.contains(500));
assertTrue(f2.contains(501));
assertTrue(f2.contains(1919));
assertTrue(f2.contains(0));
assertTrue(f2.contains(99));
assertFalse(f2.contains(100));
assertFalse(f2.contains(200));
// Test field lookup by position
assertEquals(f2.getStart(), list.findField(50).getStart());
assertEquals(f1.getStart(), list.findField(200).getStart());
assertEquals(f2.getStart(), list.findField(600).getStart());
// Test navigation
assertEquals(f2.getStart(), list.getPreviousField(f1).getStart());
assertNull(list.getNextField(f2));
}
@Test
public void testSingleFieldFullBufferWrap() {
screen.erase(false);
screen.setFieldAttribute(250, (byte) FA_PRINTABLE);
ECLFieldList list = screen.buildFieldList();
assertEquals(1, list.getFieldCount());
ECLField f = list.getFirstField();
assertNotNull(f);
assertEquals(250, f.getStart());
assertEquals(251, f.getDataStart());
assertEquals(249, f.getEnd());
assertEquals(1919, f.getLength());
assertTrue(f.isWrapped());
assertTrue(f.contains(0));
assertTrue(f.contains(250));
assertTrue(f.contains(1919));
}
}
@@ -241,4 +241,51 @@ public class ProxyConnectionTest {
connection.disconnect(); connection.disconnect();
} }
} }
@Test
public void testSystemPropertiesProxyFallback() throws Exception {
try (ServerSocket proxyServer = new ServerSocket(0)) {
int proxyPort = proxyServer.getLocalPort();
CountDownLatch serverHandshakeDone = new CountDownLatch(1);
Thread serverThread = new Thread(() -> {
try (Socket clientSock = proxyServer.accept()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSock.getInputStream(), StandardCharsets.US_ASCII));
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) break;
}
OutputStream out = clientSock.getOutputStream();
out.write("HTTP/1.1 200 OK\r\n\r\n".getBytes(StandardCharsets.US_ASCII));
out.flush();
serverHandshakeDone.countDown();
} catch (Exception ignored) {}
});
serverThread.setDaemon(true);
serverThread.start();
String origHost = System.getProperty("http.proxyHost");
String origPort = System.getProperty("http.proxyPort");
try {
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", String.valueOf(proxyPort));
ConnectionConfig config = new ConnectionConfig("test.fallback.org", 23);
config.setConnectTimeoutMs(5000);
TelnetFSM fsm = new TelnetFSM(config, new haus.nightmare.lib3270j.screen.ScreenBuffer(TerminalModel.IBM_3279_4, new haus.nightmare.lib3270j.charset.EbcdicTranslator()), null);
TelnetConnection connection = new TelnetConnection(config, fsm);
connection.connect();
assertTrue(connection.isConnected());
assertTrue(serverHandshakeDone.await(3, TimeUnit.SECONDS));
connection.disconnect();
} finally {
if (origHost != null) System.setProperty("http.proxyHost", origHost);
else System.clearProperty("http.proxyHost");
if (origPort != null) System.setProperty("http.proxyPort", origPort);
else System.clearProperty("http.proxyPort");
}
}
}
} }