Initial Commit

This commit is contained in:
2026-04-14 09:18:03 -04:00
commit 64d713a283
91 changed files with 6518 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.DS_Store
*.jar
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
@@ -0,0 +1,2 @@
#Wed Mar 18 14:12:38 EDT 2026
gradle.version=8.5
View File
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>
+33
View File
@@ -0,0 +1,33 @@
# j3270
An x3270 based emulator ported to Java using AI
## Acknowledgements
- [x3270](https://awesomeopensource.com/project/elangosundar/awesome-README-templates)
- [RFC ](https://github.com/matiassingers/awesome-readme)
- [Claude](https://bulldogjob.com/news/449-how-to-write-a-good-readme-for-your-github-project)
- [Antigravity]()
- [Gemini ]()
## Deployment
To build your own jar you can read or use run.sh
```bash
./run.sh
```
Or from the jar [with optional config]
```bash
java -jar j3270.jar [-c config.ini]
```
## Contributing
Contributions are welcome!
I have contributed no code to this project, it was entirely written by LLMs with my guidance only.
+29
View File
@@ -0,0 +1,29 @@
plugins {
id 'java'
}
allprojects {
group = 'org.j3270'
version = '0.1.0'
repositories {
mavenCentral()
}
}
subprojects {
apply plugin: 'java'
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
test {
useJUnitPlatform()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
}
+2
View File
@@ -0,0 +1,2 @@
Main-Class: org.pubvm.j3270.J3270App
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+64
View File
@@ -0,0 +1,64 @@
; j3270 Configuration File
; Load with: java -jar j3270.jar -c j3270.ini
; or: ./run.sh -c j3270.ini
[appearance]
; fontFamily = IBM 3270
; fontSize = 18
[behavior]
; startupBehavior = AUTO_CONNECT | SHOW_CONNECT | DO_NOTHING
; autoConnectHost = mainframe.example.com
; autoConnectPort = 23
[colors]
; Host colors 0-15 (hex RGB)
; 0=Neutral Black 1=Blue 2=Red 3=Pink
; 4=Green 5=Turquoise 6=Yellow 7=Neutral White
; 8=Black 9=Deep Blue 10=Orange 11=Purple
; 12=Pale Green 13=Pale Turq 14=Grey 15=White
; color_0 = #000000
; color_4 = #00ff00
; Monochrome / background colors
; mono_NORMAL = #32cd32
; mono_INTENSIFY = #ffffff
; mono_PROTECTED = #5078ff
; mono_PROTECTED_HIGH = #ffffff
; mono_BACKGROUND = #0a0a0a
[keybindings]
; Use comma-separated values for multiple bindings per action.
; Set to UNBOUND to disable an action entirely.
;
; Navigation keys:
; ENTER = ENTER
; TAB = TAB
; shift TAB = shift TAB
; UP = UP
; DOWN = DOWN
; LEFT = LEFT
; RIGHT = RIGHT
; HOME = HOME
; END = END
; PAGE_UP = PAGE_UP
; PAGE_DOWN = PAGE_DOWN
; ESCAPE = ESCAPE
; INSERT = INSERT
; DELETE = DELETE
; BACK_SPACE = BACK_SPACE
; CLEAR = alt C
;
; PF keys (1-24):
; PF1 = F1
; PF2 = F2
; ...
; PF12 = F12, shift F12
; PF13 = shift F1
; ...
; PF24 = UNBOUND
;
; PA keys:
; PA1 = alt 1
; PA2 = alt 2
; PA3 = alt 3
+11
View File
@@ -0,0 +1,11 @@
plugins {
id 'application'
}
dependencies {
implementation project(':lib3270j')
}
application {
mainClass = 'org.j3270.J3270App'
}
@@ -0,0 +1,442 @@
package org.pubvm.j3270;
import org.lib3270j.*;
import org.lib3270j.listener.ConnectionListener;
import org.lib3270j.listener.ScreenUpdateListener;
import org.pubvm.j3270.ui.ConnectDialog;
import org.pubvm.j3270.ui.StatusBar;
import org.pubvm.j3270.ui.TerminalPanel;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.util.logging.*;
/**
* j3270 — Java TN3270 Terminal Emulator.
* Main application class. Creates the main window with terminal display,
* status bar, menu bar, and manages the Telnet3270Client lifecycle.
*/
public class J3270App extends JFrame implements ConnectionListener, ScreenUpdateListener {
private static final Logger log = Logger.getLogger(J3270App.class.getName());
private Telnet3270Client client;
private TerminalPanel terminalPanel;
private StatusBar statusBar;
private Timer refreshTimer;
// Last connection details for reconnect
private String lastHost = "";
private int lastPort = 23;
public J3270App() {
super("j3270 — Java TN3270 Terminal Emulator");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(new Color(10, 10, 10));
buildUI();
buildMenuBar();
pack();
setLocationRelativeTo(null);
setMinimumSize(new Dimension(640, 400));
// Status refresh timer — also ensures focus stays on terminal
refreshTimer = new Timer(100, e -> {
if (client != null) {
statusBar.updateStatus();
// Keep focus on terminal panel when window is active
if (isActive() && !terminalPanel.hasFocus()) {
terminalPanel.requestFocusInWindow();
}
}
});
refreshTimer.start();
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
disconnect();
if (refreshTimer != null)
refreshTimer.stop();
terminalPanel.dispose();
}
@Override
public void windowActivated(WindowEvent e) {
// When window gains focus, push to terminal panel
terminalPanel.requestFocusInWindow();
}
});
}
private void buildUI() {
terminalPanel = new TerminalPanel();
// Status bar
statusBar = new StatusBar();
// Layout
getContentPane().setLayout(new BorderLayout());
getContentPane().setBackground(new Color(10, 10, 10));
getContentPane().add(terminalPanel, BorderLayout.CENTER);
getContentPane().add(statusBar, BorderLayout.SOUTH);
}
private void buildMenuBar() {
JMenuBar menuBar = new JMenuBar();
menuBar.setBackground(new Color(30, 30, 30));
menuBar.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(50, 50, 50)));
// File menu
JMenu fileMenu = createMenu("File");
fileMenu.add(createMenuItem("Connect...", KeyEvent.VK_N, this::showConnectDialog));
fileMenu.add(createMenuItem("Disconnect", KeyEvent.VK_D, this::disconnect));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Settings...", -1, this::showSettingsDialog));
fileMenu.addSeparator();
fileMenu.add(createMenuItem("Quit", KeyEvent.VK_Q, () -> {
disconnect();
System.exit(0);
}));
menuBar.add(fileMenu);
// View menu
JMenu viewMenu = createMenu("View");
viewMenu.add(createMenuItem("Font Size +", KeyEvent.VK_EQUALS, () -> adjustFontSize(2)));
viewMenu.add(createMenuItem("Font Size -", KeyEvent.VK_MINUS, () -> adjustFontSize(-2)));
viewMenu.add(createMenuItem("Reset Font", KeyEvent.VK_0, () -> {
terminalPanel.setFontSize(16);
pack();
}));
menuBar.add(viewMenu);
// Actions menu
JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> {
if (client != null)
client.sendClear();
terminalPanel.repaint();
}));
actionsMenu.add(createMenuItem("Reset", KeyEvent.VK_R, () -> {
if (client != null)
client.reset();
terminalPanel.repaint();
}));
menuBar.add(actionsMenu);
// Help menu
JMenu helpMenu = createMenu("Help");
helpMenu.add(createMenuItem("Key Mappings", -1, this::showKeyMappings));
helpMenu.add(createMenuItem("About", -1, this::showAbout));
menuBar.add(helpMenu);
setJMenuBar(menuBar);
}
private JMenu createMenu(String name) {
JMenu menu = new JMenu(name);
menu.setForeground(new Color(200, 200, 200));
return menu;
}
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) {
JMenuItem item = new JMenuItem(text);
item.setBackground(new Color(40, 40, 40));
item.setForeground(new Color(200, 200, 200));
if (acceleratorKey > 0) {
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()));
}
item.addActionListener(e -> action.run());
return item;
}
// ========== Connection management ==========
private void showConnectDialog() {
ConnectDialog dialog = new ConnectDialog(this);
dialog.setInitialHost(lastHost);
dialog.setInitialPort(lastPort);
dialog.setVisible(true);
if (dialog.isConfirmed()) {
ConnectionConfig config = dialog.getConnectionConfig();
lastHost = config.getHost();
lastPort = config.getPort();
connect(config);
}
}
private void showSettingsDialog() {
org.pubvm.j3270.ui.SettingsDialog dialog = new org.pubvm.j3270.ui.SettingsDialog(this);
dialog.setVisible(true);
}
public TerminalPanel getTerminalPanel() {
return terminalPanel;
}
void connect(ConnectionConfig config) {
lastHost = config.getHost();
lastPort = config.getPort();
// Save for auto-connect
org.pubvm.j3270.config.Settings.setAutoConnectHost(config.getHost());
org.pubvm.j3270.config.Settings.setAutoConnectPort(config.getPort());
// Disconnect existing connection
disconnect();
client = new Telnet3270Client(config);
client.addConnectionListener(this);
client.addScreenUpdateListener(this);
terminalPanel.setClient(client);
statusBar.setClient(client);
setTitle("j3270 — " + config.getHost() + ":" + config.getPort());
// Resize window to match model
terminalPanel.revalidate();
pack();
new Thread(() -> {
try {
client.connect();
} catch (IOException e) {
log.log(Level.WARNING, "Connection failed", e);
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this,
"Connection failed: " + e.getMessage(),
"Connection Error", JOptionPane.ERROR_MESSAGE);
});
}
}, "Connect-Thread").start();
// Focus the terminal panel
terminalPanel.requestFocusInWindow();
}
private void disconnect() {
if (client != null) {
client.disconnect();
client = null;
terminalPanel.setClient(null);
statusBar.setClient(null);
terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator");
}
}
private void adjustFontSize(int delta) {
int current = terminalPanel.getFontSize();
int newSize = Math.max(8, Math.min(36, current + delta));
terminalPanel.setFontSize(newSize);
pack();
}
// ========== ConnectionListener ==========
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
log.info("Connection state: " + oldState + " -> " + newState);
SwingUtilities.invokeLater(() -> {
statusBar.updateStatus();
terminalPanel.repaint();
// Auto-resize and focus on first full session
if (newState.isFullSession() && !oldState.isFullSession()) {
terminalPanel.revalidate();
pack();
terminalPanel.requestFocusInWindow();
}
});
}
@Override
public void onConnectionError(String message) {
log.warning("Connection error: " + message);
SwingUtilities.invokeLater(statusBar::updateStatus);
}
@Override
public void onTN3270ENegotiated(String deviceType, String deviceName) {
log.info("TN3270E negotiated: type=" + deviceType + " name=" + deviceName);
SwingUtilities.invokeLater(() -> {
if (deviceName != null) {
setTitle("j3270 — " + lastHost + ":" + lastPort + " [" + deviceName + "]");
}
});
}
// ========== ScreenUpdateListener ==========
@Override
public void onScreenUpdated() {
SwingUtilities.invokeLater(() -> {
if (client != null) {
client.getInputProcessor().setKeyboardLocked(false);
}
terminalPanel.repaint();
statusBar.updateStatus();
});
}
@Override
public void onSoundAlarm() {
Toolkit.getDefaultToolkit().beep();
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
SwingUtilities.invokeLater(() -> {
terminalPanel.revalidate();
pack();
});
}
// ========== Help dialogs ==========
private void showKeyMappings() {
String text = "Key Mappings:\n\n" +
"Enter — Send Enter (AID)\n" +
"F1-F12 — PF1-PF12\n" +
"Shift+F1-F12 — PF13-PF24\n" +
"Alt+1/2/3 — PA1/PA2/PA3\n" +
"Tab — Tab to next field\n" +
"Shift+Tab — Back-tab\n" +
"Arrow keys — Cursor movement\n" +
"Home — Cursor to home\n" +
"End — Erase to end of field\n" +
"Delete — Delete character\n" +
"Backspace — Backspace\n" +
"Insert — Toggle insert mode\n" +
"Escape — Reset\n" +
"PageUp/Down — PF7/PF8\n" +
"Alt+C — Clear\n" +
"Alt+1/2/3 — PA1/PA2/PA3\n" +
"Cmd+D — Disconnect\n" +
"Cmd+Q — Quit\n" +
"Cmd+=/-/0 — Font size +/-/reset";
JTextArea area = new JTextArea(text);
area.setEditable(false);
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
area.setBackground(new Color(30, 30, 30));
area.setForeground(new Color(200, 200, 200));
JScrollPane sp = new JScrollPane(area);
sp.setPreferredSize(new Dimension(360, 400));
JOptionPane.showMessageDialog(this, sp, "Key Mappings", JOptionPane.INFORMATION_MESSAGE);
}
private void showAbout() {
JOptionPane.showMessageDialog(this,
"j3270 — Java TN3270 Terminal Emulator\n\n" +
"A Java reimplementation of x3270.\n" +
"lib3270j v0.1.0\n\n" +
"Supports: TN3270, TN3270E (RFC 2355)\n" +
"Models: IBM 3278/3279 Models 2-5\n" +
"Colors, Extended Attributes, Query Replies",
"About j3270", JOptionPane.INFORMATION_MESSAGE);
}
// ========== Main ==========
public static void main(String[] args) {
boolean debug = false;
String configFile = null;
java.util.List<String> remainingArgs = new java.util.ArrayList<>();
for (int i = 0; i < args.length; i++) {
if ("--debug".equals(args[i]) || "-d".equals(args[i])) {
debug = true;
} else if (("-c".equals(args[i]) || "--config".equals(args[i])) && i + 1 < args.length) {
configFile = args[++i];
} else {
remainingArgs.add(args[i]);
}
}
// Configure logging over the absolute root logger
Logger globalRoot = Logger.getLogger("");
for (java.util.logging.Handler h : globalRoot.getHandlers()) {
globalRoot.removeHandler(h);
}
globalRoot.setLevel(debug ? Level.ALL : Level.WARNING);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
handler.setFormatter(new SimpleFormatter());
globalRoot.addHandler(handler);
// Load INI config file if specified
if (configFile != null) {
try {
org.pubvm.j3270.config.Settings.loadFromIniFile(configFile);
} catch (Exception e) {
System.err.println("Failed to load config file: " + configFile);
System.err.println(e.getMessage());
System.exit(1);
}
}
// Dark look and feel
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
log.fine("Could not set system look and feel");
}
// Use native macOS menu bar if applicable
System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("apple.awt.application.name", "j3270");
SwingUtilities.invokeLater(() -> {
J3270App app = new J3270App();
app.setVisible(true);
// If host:port given on command line, connect directly
if (!remainingArgs.isEmpty()) {
String host = remainingArgs.get(0);
int port = 23;
if (remainingArgs.size() >= 2) {
try {
port = Integer.parseInt(remainingArgs.get(1));
} catch (NumberFormatException e) {
}
}
TerminalModel model = TerminalModel.IBM_3279_4;
if (remainingArgs.size() >= 3) {
try {
int modelNum = Integer.parseInt(remainingArgs.get(2));
model = TerminalModel.forModel(modelNum, true);
} catch (Exception e) {
}
}
ConnectionConfig config = new ConnectionConfig(host, port, model);
app.connect(config);
} else {
org.pubvm.j3270.config.Settings.StartupBehavior behavior = org.pubvm.j3270.config.Settings
.getStartupBehavior();
switch (behavior) {
case SHOW_CONNECT:
SwingUtilities.invokeLater(app::showConnectDialog);
break;
case AUTO_CONNECT:
String host = org.pubvm.j3270.config.Settings.getAutoConnectHost();
int port = org.pubvm.j3270.config.Settings.getAutoConnectPort();
if (host != null && !host.isEmpty()) {
app.connect(new ConnectionConfig(host, port, TerminalModel.IBM_3279_4));
} else {
SwingUtilities.invokeLater(app::showConnectDialog);
}
break;
case DO_NOTHING:
default:
break;
}
}
});
}
}
@@ -0,0 +1,220 @@
package org.pubvm.j3270.config;
import java.util.prefs.Preferences;
import java.awt.Color;
import java.io.*;
import java.util.logging.Logger;
import java.util.logging.Level;
public class Settings {
private static final Preferences prefs = Preferences.userNodeForPackage(Settings.class);
private static final Logger log = Logger.getLogger(Settings.class.getName());
public enum StartupBehavior {
SHOW_CONNECT,
DO_NOTHING,
AUTO_CONNECT
}
public static String getFontFamily() {
return prefs.get("fontFamily", "Monospaced");
}
public static void setFontFamily(String family) {
prefs.put("fontFamily", family);
}
public static int getFontSize() {
return prefs.getInt("fontSize", 16);
}
public static void setFontSize(int size) {
prefs.putInt("fontSize", size);
}
public static StartupBehavior getStartupBehavior() {
String behaviorStr = prefs.get("startupBehavior", StartupBehavior.SHOW_CONNECT.name());
try {
return StartupBehavior.valueOf(behaviorStr);
} catch (IllegalArgumentException e) {
return StartupBehavior.SHOW_CONNECT;
}
}
public static void setStartupBehavior(StartupBehavior behavior) {
prefs.put("startupBehavior", behavior.name());
}
public static String getAutoConnectHost() {
return prefs.get("autoConnectHost", "");
}
public static void setAutoConnectHost(String host) {
prefs.put("autoConnectHost", host);
}
public static int getAutoConnectPort() {
return prefs.getInt("autoConnectPort", 23);
}
public static void setAutoConnectPort(int port) {
prefs.putInt("autoConnectPort", port);
}
public static Color getColorOverride(int index, Color defaultColor) {
String hex = prefs.get("color_" + index, null);
try {
return hex != null ? Color.decode(hex) : defaultColor;
} catch (NumberFormatException e) {
return defaultColor;
}
}
public static void setColorOverride(int index, Color color) {
prefs.put("color_" + index, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
}
public static Color getMonoColorOverride(String key, Color defaultColor) {
String hex = prefs.get("mono_" + key, null);
try {
return hex != null ? Color.decode(hex) : defaultColor;
} catch (NumberFormatException e) {
return defaultColor;
}
}
public static void setMonoColorOverride(String key, Color color) {
prefs.put("mono_" + key, String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()));
}
public static String getKeyBinding(String action, String defaultBinding) {
return prefs.get("key_" + action, defaultBinding);
}
public static void setKeyBinding(String action, String binding) {
prefs.put("key_" + action, binding);
}
/**
* Load settings from an INI-style configuration file.
* Sections: [appearance], [behavior], [colors], [keybindings]
*
* Example file:
* <pre>
* [appearance]
* fontFamily = IBM 3270
* fontSize = 18
*
* [behavior]
* startupBehavior = AUTO_CONNECT
* autoConnectHost = mainframe.example.com
* autoConnectPort = 23
*
* [colors]
* ; Host colors 0-15
* color_0 = #000000
* color_4 = #00ff00
* ; Monochrome / background
* mono_NORMAL = #32cd32
* mono_BACKGROUND = #0a0a0a
*
* [keybindings]
* ; Comma-separated for multiple bindings
* PF12 = F12, shift F12
* PF24 = UNBOUND
* CLEAR = alt C
* </pre>
*/
public static void loadFromIniFile(String path) throws IOException {
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("Config file not found: " + path);
}
log.info("Loading configuration from: " + path);
String currentSection = "";
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
int lineNum = 0;
while ((line = reader.readLine()) != null) {
lineNum++;
line = line.trim();
// Skip empty lines and comments
if (line.isEmpty() || line.startsWith(";") || line.startsWith("#")) {
continue;
}
// Section header
if (line.startsWith("[") && line.endsWith("]")) {
currentSection = line.substring(1, line.length() - 1).toLowerCase().trim();
continue;
}
// Key = Value
int eq = line.indexOf('=');
if (eq < 0) {
log.warning("Config line " + lineNum + ": no '=' found, skipping: " + line);
continue;
}
String key = line.substring(0, eq).trim();
String value = line.substring(eq + 1).trim();
try {
applyConfigEntry(currentSection, key, value);
} catch (Exception e) {
log.log(Level.WARNING, "Config line " + lineNum + ": error applying " + key + "=" + value, e);
}
}
}
log.info("Configuration loaded successfully.");
}
private static void applyConfigEntry(String section, String key, String value) {
switch (section) {
case "appearance":
switch (key) {
case "fontFamily": setFontFamily(value); break;
case "fontSize": setFontSize(Integer.parseInt(value)); break;
default:
log.warning("Unknown appearance key: " + key);
}
break;
case "behavior":
switch (key) {
case "startupBehavior":
setStartupBehavior(StartupBehavior.valueOf(value.toUpperCase()));
break;
case "autoConnectHost": setAutoConnectHost(value); break;
case "autoConnectPort": setAutoConnectPort(Integer.parseInt(value)); break;
default:
log.warning("Unknown behavior key: " + key);
}
break;
case "colors":
if (key.startsWith("color_")) {
int index = Integer.parseInt(key.substring(6));
setColorOverride(index, Color.decode(value));
} else if (key.startsWith("mono_")) {
String monoKey = key.substring(5);
setMonoColorOverride(monoKey, Color.decode(value));
} else {
log.warning("Unknown colors key: " + key);
}
break;
case "keybindings":
setKeyBinding(key, value);
break;
default:
// Allow bare keys outside any section — treat as raw prefs
log.fine("Setting raw preference: " + key + " = " + value);
prefs.put(key, value);
break;
}
}
}
@@ -0,0 +1,181 @@
package org.pubvm.j3270.ui;
import org.lib3270j.ConnectionConfig;
import org.lib3270j.TerminalModel;
import javax.swing.*;
import java.awt.*;
/**
* Connection dialog for entering host, port, model, and LU name.
*/
public class ConnectDialog extends JDialog {
private JTextField hostField;
private JTextField portField;
private JComboBox<TerminalModel> modelCombo;
private JTextField luField;
private boolean confirmed;
private ConnectionConfig result;
public ConnectDialog(Frame parent) {
super(parent, "Connect to Host", true);
buildUI();
pack();
setLocationRelativeTo(parent);
setResizable(false);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
mainPanel.setBackground(new Color(30, 30, 30));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 4, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
Font labelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 14);
Color fg = new Color(200, 200, 200);
// Host
gbc.gridx = 0;
gbc.gridy = 0;
JLabel hostLabel = new JLabel("Host:");
hostLabel.setForeground(fg);
hostLabel.setFont(labelFont);
mainPanel.add(hostLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
hostField = createDarkField(20);
mainPanel.add(hostField, gbc);
// Port
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 0;
JLabel portLabel = new JLabel("Port:");
portLabel.setForeground(fg);
portLabel.setFont(labelFont);
mainPanel.add(portLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
portField = createDarkField(6);
portField.setText("23");
mainPanel.add(portField, gbc);
// Model
gbc.gridx = 0;
gbc.gridy = 2;
gbc.weightx = 0;
JLabel modelLabel = new JLabel("Model:");
modelLabel.setForeground(fg);
modelLabel.setFont(labelFont);
mainPanel.add(modelLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
modelCombo = new JComboBox<>(TerminalModel.values());
modelCombo.setSelectedItem(TerminalModel.IBM_3279_4);
modelCombo.setBackground(new Color(45, 45, 45));
modelCombo.setForeground(fg);
modelCombo.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 13));
mainPanel.add(modelCombo, gbc);
// LU Name
gbc.gridx = 0;
gbc.gridy = 3;
gbc.weightx = 0;
JLabel luLabel = new JLabel("LU Name:");
luLabel.setForeground(fg);
luLabel.setFont(labelFont);
mainPanel.add(luLabel, gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
luField = createDarkField(12);
mainPanel.add(luField, gbc);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setBackground(new Color(30, 30, 30));
JButton connectBtn = new JButton("Connect");
connectBtn.setBackground(new Color(50, 120, 50));
connectBtn.setForeground(Color.WHITE);
connectBtn.setFont(labelFont);
connectBtn.addActionListener(e -> onConnect());
JButton cancelBtn = new JButton("Cancel");
cancelBtn.setBackground(new Color(60, 60, 60));
cancelBtn.setForeground(fg);
cancelBtn.setFont(labelFont);
cancelBtn.addActionListener(e -> {
confirmed = false;
dispose();
});
buttonPanel.add(cancelBtn);
buttonPanel.add(connectBtn);
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
setContentPane(mainPanel);
// Enter key triggers connect
getRootPane().setDefaultButton(connectBtn);
}
private JTextField createDarkField(int cols) {
JTextField field = new JTextField(cols);
field.setBackground(new Color(45, 45, 45));
field.setForeground(new Color(200, 200, 200));
field.setCaretColor(new Color(200, 200, 200));
field.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
field.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
BorderFactory.createEmptyBorder(4, 6, 4, 6)));
return field;
}
private void onConnect() {
String host = hostField.getText().trim();
if (host.isEmpty()) {
hostField.requestFocus();
return;
}
int port;
try {
port = Integer.parseInt(portField.getText().trim());
} catch (NumberFormatException e) {
portField.requestFocus();
return;
}
result = new ConnectionConfig(host, port, (TerminalModel) modelCombo.getSelectedItem());
String lu = luField.getText().trim();
if (!lu.isEmpty()) {
result.setLuName(lu);
}
confirmed = true;
dispose();
}
public boolean isConfirmed() {
return confirmed;
}
public ConnectionConfig getConnectionConfig() {
return result;
}
/** Set initial values. */
public void setInitialHost(String host) {
hostField.setText(host);
}
public void setInitialPort(int port) {
portField.setText(String.valueOf(port));
}
}
@@ -0,0 +1,490 @@
package org.pubvm.j3270.ui;
import javax.swing.*;
import org.pubvm.j3270.J3270App;
import org.pubvm.j3270.config.Settings;
import java.awt.*;
import java.awt.event.*;
import java.util.HashMap;
import java.util.Map;
import javax.swing.table.DefaultTableModel;
import javax.swing.border.LineBorder;
public class SettingsDialog extends JDialog {
private final J3270App parentApp;
// Appearance tab
private JComboBox<String> fontBox;
private JSpinner fontSizeSpinner;
// Behavior tab
private JComboBox<Settings.StartupBehavior> startupBehaviorBox;
private JPanel autoConnectPanel;
private JTextField hostField;
private JTextField portField;
// Advanced tab state tracking
private final Color[] tempHostColors = new Color[16];
private final Map<String, Color> tempMonoColors = new HashMap<>();
private final Map<String, String> tempKeyBindings = new HashMap<>();
private DefaultTableModel keymapModel;
public SettingsDialog(J3270App parent) {
super(parent, "Settings", true);
this.parentApp = parent;
initComponents();
setSize(550, 450);
setLocationRelativeTo(parent);
}
private void initComponents() {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Appearance", createAppearancePanel());
tabbedPane.addTab("Behavior", createBehaviorPanel());
tabbedPane.addTab("Advanced", createAdvancedPanel());
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnOk = new JButton("OK");
JButton btnApply = new JButton("Apply");
JButton btnCancel = new JButton("Cancel");
btnOk.addActionListener((ActionEvent e) -> {
boolean success = applySettings();
if (success) {
dispose();
}
});
btnApply.addActionListener((ActionEvent e) -> {
applySettings();
});
btnCancel.addActionListener((ActionEvent e) -> {
dispose();
});
buttonPanel.add(btnApply);
buttonPanel.add(btnCancel);
buttonPanel.add(btnOk);
getContentPane().add(tabbedPane, BorderLayout.CENTER);
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
}
private JPanel createAppearancePanel() {
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10);
gbc.anchor = GridBagConstraints.WEST;
// Font family
gbc.gridx = 0;
gbc.gridy = 0;
JLabel fontLabel = new JLabel("Terminal Font:");
panel.add(fontLabel, gbc);
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
fontBox = new JComboBox<>(fonts);
// Find default
String currentFont = Settings.getFontFamily();
for (int i = 0; i < fonts.length; i++) {
if (fonts[i].equalsIgnoreCase(currentFont)) {
fontBox.setSelectedIndex(i);
break;
}
}
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(fontBox, gbc);
// Font size
gbc.gridx = 0;
gbc.gridy = 1;
gbc.fill = GridBagConstraints.NONE;
JLabel sizeLabel = new JLabel("Font Size:");
panel.add(sizeLabel, gbc);
SpinnerNumberModel sizeModel = new SpinnerNumberModel(Settings.getFontSize(), 8, 72, 1);
fontSizeSpinner = new JSpinner(sizeModel);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(fontSizeSpinner, gbc);
// Fill remaining space
gbc.gridy = 2;
gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc);
return panel;
}
private JPanel createBehaviorPanel() {
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10);
gbc.anchor = GridBagConstraints.WEST;
gbc.gridx = 0;
gbc.gridy = 0;
JLabel actionLabel = new JLabel("Startup Action:");
panel.add(actionLabel, gbc);
startupBehaviorBox = new JComboBox<>(Settings.StartupBehavior.values());
startupBehaviorBox.setSelectedItem(Settings.getStartupBehavior());
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
panel.add(startupBehaviorBox, gbc);
// Auto-connect panel
autoConnectPanel = new JPanel(new GridBagLayout());
GridBagConstraints acGbc = new GridBagConstraints();
acGbc.insets = new Insets(5, 5, 5, 5);
acGbc.anchor = GridBagConstraints.WEST;
JLabel hostLabel = new JLabel("Host:");
acGbc.gridx = 0;
acGbc.gridy = 0;
autoConnectPanel.add(hostLabel, acGbc);
hostField = new JTextField(Settings.getAutoConnectHost(), 15);
acGbc.gridx = 1;
autoConnectPanel.add(hostField, acGbc);
JLabel portLabel = new JLabel("Port:");
acGbc.gridx = 2;
autoConnectPanel.add(portLabel, acGbc);
portField = new JTextField(String.valueOf(Settings.getAutoConnectPort()), 4);
acGbc.gridx = 3;
autoConnectPanel.add(portField, acGbc);
gbc.gridx = 0;
gbc.gridy = 1;
gbc.gridwidth = 2;
panel.add(autoConnectPanel, gbc);
// Toggle visibility
startupBehaviorBox.addActionListener(e -> {
autoConnectPanel.setVisible(startupBehaviorBox.getSelectedItem() == Settings.StartupBehavior.AUTO_CONNECT);
});
autoConnectPanel.setVisible(Settings.getStartupBehavior() == Settings.StartupBehavior.AUTO_CONNECT);
// Placeholder for potentially more behavior options below
gbc.gridy = 2;
gbc.weighty = 1.0;
panel.add(Box.createGlue(), gbc);
return panel;
}
private JPanel createAdvancedPanel() {
JPanel panel = new JPanel(new BorderLayout());
JTabbedPane advancedTabs = new JTabbedPane();
advancedTabs.addTab("Colors", createColorsPanel());
advancedTabs.addTab("Keymap", createKeymapPanel());
panel.add(advancedTabs, BorderLayout.CENTER);
return panel;
}
private JPanel createColorsPanel() {
JPanel main = new JPanel(new BorderLayout());
// Host colors grid (16 colors)
JPanel hostPanel = new JPanel(new GridLayout(2, 8, 5, 5));
hostPanel.setBorder(BorderFactory.createTitledBorder("Host Colors"));
for (int i = 0; i < 16; i++) {
Color init = org.pubvm.j3270.config.Settings.getColorOverride(i, TerminalPanel.DEFAULT_HOST_COLORS[i]);
tempHostColors[i] = init;
JPanel cb = new JPanel();
cb.setBackground(init);
cb.setPreferredSize(new Dimension(30, 30));
cb.setBorder(new LineBorder(Color.DARK_GRAY));
final int index = i;
cb.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
Color c = JColorChooser.showDialog(main, "Select Color " + index, tempHostColors[index]);
if (c != null) {
tempHostColors[index] = c;
cb.setBackground(c);
}
}
});
hostPanel.add(cb);
}
// Mono colors
JPanel monoPanel = new JPanel(new GridLayout(2, 3, 5, 5));
monoPanel.setBorder(BorderFactory.createTitledBorder("Monochrome / BG Colors"));
String[] monoKeys = {"NORMAL", "INTENSIFY", "PROTECTED", "PROTECTED_HIGH", "BACKGROUND"};
Color[] monoDefs = {TerminalPanel.DEFAULT_MONO_NORMAL, TerminalPanel.DEFAULT_MONO_INTENSIFY,
TerminalPanel.DEFAULT_MONO_PROTECTED, TerminalPanel.DEFAULT_MONO_PROTECTED_HIGH, TerminalPanel.DEFAULT_BG_COLOR};
for (int i = 0; i < monoKeys.length; i++) {
String mk = monoKeys[i];
Color init = org.pubvm.j3270.config.Settings.getMonoColorOverride(mk, monoDefs[i]);
tempMonoColors.put(mk, init);
JPanel p = new JPanel(new BorderLayout());
p.add(new JLabel(mk, SwingConstants.CENTER), BorderLayout.NORTH);
JPanel cb = new JPanel();
cb.setBackground(init);
cb.setPreferredSize(new Dimension(40, 40));
cb.setBorder(new LineBorder(Color.DARK_GRAY));
cb.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
Color c = JColorChooser.showDialog(main, "Select " + mk, tempMonoColors.get(mk));
if (c != null) {
tempMonoColors.put(mk, c);
cb.setBackground(c);
}
}
});
p.add(cb, BorderLayout.CENTER);
monoPanel.add(p);
}
JPanel resetPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton btnResetColors = new JButton("Reset to Defaults");
btnResetColors.addActionListener(e -> {
for (int i=0; i<16; i++) {
tempHostColors[i] = TerminalPanel.DEFAULT_HOST_COLORS[i];
}
for (int i=0; i<monoKeys.length; i++) {
tempMonoColors.put(monoKeys[i], monoDefs[i]);
}
// Repaint container implicitly handled if we trigger a UI update,
// but for simplicity user can just close/reopen or have it refresh on save
JOptionPane.showMessageDialog(main, "Colors reset. Click Apply to save.");
});
resetPanel.add(btnResetColors);
main.add(hostPanel, BorderLayout.NORTH);
main.add(monoPanel, BorderLayout.CENTER);
main.add(resetPanel, BorderLayout.SOUTH);
return main;
}
private JPanel createKeymapPanel() {
JPanel main = new JPanel(new BorderLayout());
String[] actions = {"ENTER", "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
"HOME", "END", "PAGE_UP", "PAGE_DOWN", "ESCAPE", "INSERT", "DELETE", "BACK_SPACE", "CLEAR"};
keymapModel = new DefaultTableModel(new Object[]{"Action", "Key Binding"}, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false; // read only through UI editor button
}
};
// Populate table from Settings or Defaults
for(String act : actions) {
String def = act;
if(def.equals("PAGE_UP")) def = "PAGE_UP"; // fallback example
String current = org.pubvm.j3270.config.Settings.getKeyBinding(act, def);
tempKeyBindings.put(act, current);
keymapModel.addRow(new Object[]{act, current});
}
for(int i=1;i<=24;i++){
String defaultBinding;
if (i <= 12) {
defaultBinding = "F" + i;
} else {
defaultBinding = "shift F" + (i - 12);
}
String pf = "PF"+i;
String cur = org.pubvm.j3270.config.Settings.getKeyBinding(pf, defaultBinding);
tempKeyBindings.put(pf, cur);
keymapModel.addRow(new Object[]{pf, cur});
}
for(int i=1;i<=3;i++){
String pa = "PA"+i;
String curPa = org.pubvm.j3270.config.Settings.getKeyBinding(pa, "alt "+i);
tempKeyBindings.put(pa, curPa);
keymapModel.addRow(new Object[]{pa, curPa});
}
JTable table = new JTable(keymapModel);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setFillsViewportHeight(true);
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4));
// Helper to open a key-capture dialog and return the keystroke string
// The callback receives the captured string.
java.util.function.BiConsumer<String, java.util.function.Consumer<String>> captureKey = (title, callback) -> {
JDialog captureDialog = new JDialog(this, title, true);
captureDialog.setSize(320, 100);
captureDialog.setLocationRelativeTo(this);
JLabel lbl = new JLabel("Press any key combination now...", SwingConstants.CENTER);
captureDialog.add(lbl);
captureDialog.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
if (code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_CONTROL ||
code == KeyEvent.VK_ALT || code == KeyEvent.VK_META) return;
KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e);
if (ks != null) {
String s = ks.toString().replace("pressed ", "");
callback.accept(s);
captureDialog.dispose();
}
}
});
captureDialog.setVisible(true);
};
// Replace Binding — sets the action to exactly one new key
JButton btnReplace = new JButton("Replace Binding");
btnReplace.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
String action = (String) keymapModel.getValueAt(row, 0);
captureKey.accept("Press key for " + action, s -> {
tempKeyBindings.put(action, s);
keymapModel.setValueAt(s, row, 1);
});
});
btnPanel.add(btnReplace);
// Add Binding — appends an additional key to the existing binding(s)
JButton btnAdd = new JButton("Add Binding");
btnAdd.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
String action = (String) keymapModel.getValueAt(row, 0);
captureKey.accept("Press additional key for " + action, s -> {
String existing = tempKeyBindings.get(action);
String updated;
if (existing == null || existing.isEmpty() || "UNBOUND".equals(existing)) {
updated = s;
} else {
updated = existing + ", " + s;
}
tempKeyBindings.put(action, updated);
keymapModel.setValueAt(updated, row, 1);
});
});
btnPanel.add(btnAdd);
// Remove Last — removes the last comma-separated binding entry
JButton btnRemoveLast = new JButton("Remove Last");
btnRemoveLast.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
String action = (String) keymapModel.getValueAt(row, 0);
String existing = tempKeyBindings.get(action);
if (existing == null || "UNBOUND".equals(existing)) return;
int lastComma = existing.lastIndexOf(',');
String updated;
if (lastComma > 0) {
updated = existing.substring(0, lastComma).trim();
} else {
updated = "UNBOUND";
}
tempKeyBindings.put(action, updated);
keymapModel.setValueAt(updated, row, 1);
});
btnPanel.add(btnRemoveLast);
// Unbind All — clears all bindings for the action
JButton btnUnbind = new JButton("Unbind All");
btnUnbind.addActionListener(e -> {
int row = table.getSelectedRow();
if (row < 0) return;
String action = (String) keymapModel.getValueAt(row, 0);
tempKeyBindings.put(action, "UNBOUND");
keymapModel.setValueAt("UNBOUND", row, 1);
});
btnPanel.add(btnUnbind);
// Reset Keymaps — restore all defaults
JButton btnReset = new JButton("Reset All");
btnReset.addActionListener(e -> {
for (int row = 0; row < keymapModel.getRowCount(); row++) {
String action = (String) keymapModel.getValueAt(row, 0);
String def = getDefaultBinding(action);
tempKeyBindings.put(action, def);
keymapModel.setValueAt(def, row, 1);
}
});
btnPanel.add(btnReset);
main.add(new JScrollPane(table), BorderLayout.CENTER);
main.add(btnPanel, BorderLayout.SOUTH);
return main;
}
/** Returns the factory-default binding for a given action name. */
private String getDefaultBinding(String action) {
if (action.startsWith("PF")) {
int n = Integer.parseInt(action.substring(2));
return n <= 12 ? "F" + n : "shift F" + (n - 12);
}
if (action.startsWith("PA")) return "alt " + action.substring(2);
if (action.equals("CLEAR")) return "alt C";
return action; // nav keys default to their own name
}
private boolean applySettings() {
try {
// Apply Appearance
String fontFam = (String) fontBox.getSelectedItem();
if (fontFam != null) {
Settings.setFontFamily(fontFam);
}
Settings.setFontSize((Integer) fontSizeSpinner.getValue());
// Apply Behavior
Settings.StartupBehavior behavior = (Settings.StartupBehavior) startupBehaviorBox.getSelectedItem();
if (behavior != null) {
Settings.setStartupBehavior(behavior);
}
if (behavior == Settings.StartupBehavior.AUTO_CONNECT) {
Settings.setAutoConnectHost(hostField.getText().trim());
try {
Settings.setAutoConnectPort(Integer.parseInt(portField.getText().trim()));
} catch (NumberFormatException ignored) {
}
}
// Propagate visual changes to the app
// Save Colors
for (int i=0; i<16; i++) {
org.pubvm.j3270.config.Settings.setColorOverride(i, tempHostColors[i]);
}
for (Map.Entry<String, Color> entry : tempMonoColors.entrySet()) {
org.pubvm.j3270.config.Settings.setMonoColorOverride(entry.getKey(), entry.getValue());
}
// Save Keybindings
for (Map.Entry<String, String> entry : tempKeyBindings.entrySet()) {
org.pubvm.j3270.config.Settings.setKeyBinding(entry.getKey(), entry.getValue());
}
parentApp.getTerminalPanel().reloadSettings();
return true;
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "Failed to completely apply settings: " + e.getMessage(), "Error",
JOptionPane.ERROR_MESSAGE);
return false;
}
}
}
@@ -0,0 +1,140 @@
package org.pubvm.j3270.ui;
import org.lib3270j.ConnectionState;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import java.awt.*;
/**
* Status bar displaying connection state, cursor position, timing, and lock
* status.
* Equivalent to the OIA (Operator Information Area) on a real 3270 terminal.
*/
public class StatusBar extends JPanel {
private final JLabel connectionStatus;
private final JLabel cursorPosition;
private final JLabel luName;
private final JLabel lockStatus;
private final JLabel modelInfo;
private Telnet3270Client client;
// OIA colors
private static final Color OIA_BG = new Color(20, 20, 20);
private static final Color OIA_FG = new Color(50, 205, 50);
private static final Color OIA_DIM = new Color(80, 80, 80);
private static final Color OIA_ALERT = new Color(255, 80, 80);
public StatusBar() {
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setBackground(OIA_BG);
setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(40, 40, 40)));
setPreferredSize(new Dimension(800, 22));
Font oiaFont = new Font(Font.MONOSPACED, Font.PLAIN, 12);
connectionStatus = createLabel("Not Connected", oiaFont, OIA_DIM);
luName = createLabel("", oiaFont, OIA_FG);
lockStatus = createLabel("", oiaFont, OIA_ALERT);
modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
add(Box.createHorizontalStrut(6));
add(connectionStatus);
add(Box.createHorizontalStrut(12));
add(luName);
add(Box.createHorizontalStrut(12));
add(lockStatus);
add(Box.createHorizontalGlue());
add(modelInfo);
add(Box.createHorizontalStrut(12));
add(cursorPosition);
add(Box.createHorizontalStrut(6));
}
private JLabel createLabel(String text, Font font, Color fg) {
JLabel label = new JLabel(text);
label.setFont(font);
label.setForeground(fg);
return label;
}
public void setClient(Telnet3270Client client) {
this.client = client;
}
public void updateStatus() {
if (client == null)
return;
// Connection state
ConnectionState state = client.getConnectionState();
switch (state) {
case NOT_CONNECTED:
connectionStatus.setText("Not Connected");
connectionStatus.setForeground(OIA_DIM);
break;
case TCP_PENDING:
case TELNET_PENDING:
connectionStatus.setText("Connecting...");
connectionStatus.setForeground(OIA_ALERT);
break;
case CONNECTED_3270:
connectionStatus.setText("TN3270");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_TN3270E:
connectionStatus.setText("TN3270E");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_SSCP:
connectionStatus.setText("SSCP-LU");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_NVT:
case CONNECTED_NVT_CHAR:
case CONNECTED_E_NVT:
connectionStatus.setText("NVT");
connectionStatus.setForeground(OIA_FG);
break;
case CONNECTED_UNBOUND:
connectionStatus.setText("Unbound");
connectionStatus.setForeground(new Color(255, 255, 80));
break;
default:
connectionStatus.setText(state.name());
connectionStatus.setForeground(OIA_DIM);
break;
}
// LU name
String lu = "";
if (client.getConnectionState().isTn3270e()) {
// lu would come from FSM
}
luName.setText(lu);
// Lock status
if (client.getInputProcessor().isKeyboardLocked()) {
lockStatus.setText("X SYSTEM");
lockStatus.setForeground(OIA_ALERT);
} else if (client.getInputProcessor().isInsertMode()) {
lockStatus.setText("INSERT");
lockStatus.setForeground(OIA_FG);
} else {
lockStatus.setText("");
}
// Model info
modelInfo.setText(client.getConfig().getModel().getTerminalType());
// Cursor position
ScreenBuffer sb = client.getScreenBuffer();
int row = sb.getCursorRow() + 1;
int col = sb.getCursorCol() + 1;
cursorPosition.setText(String.format("%03d/%03d", row, col));
}
}
@@ -0,0 +1,581 @@
package org.pubvm.j3270.ui;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.screen.ScreenBuffer;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import static org.lib3270j.protocol.DS3270Constants.*;
/**
* Custom JPanel that renders the 3270 screen buffer.
* Supports colors, bold, underline, reverse, blink, and all 3278/3279
* attributes.
*/
public class TerminalPanel extends JPanel {
private Telnet3270Client client;
// Font and cell dimensions
private Font terminalFont;
private int cellWidth;
private int cellHeight;
private int fontAscent;
private int fontDescent;
private int padding = 4;
private int currentFontSize = 16;
// Cursor state
private boolean cursorVisible = true;
private Timer blinkTimer;
// Host color mapping
private final Color[] hostColors = new Color[16];
// 3278 monochrome colors (based on field attributes)
private Color monoNormal;
private Color monoIntensify;
private Color monoProtected;
private Color monoProtectedHigh;
// Background
private Color bgColor;
// Default Host color mapping
public static final Color[] DEFAULT_HOST_COLORS = {
new Color(0, 0, 0), // 0: Neutral Black
new Color(80, 120, 255), // 1: Blue
new Color(255, 50, 50), // 2: Red
new Color(255, 130, 180), // 3: Pink
new Color(50, 205, 50), // 4: Green
new Color(64, 224, 208), // 5: Turquoise
new Color(255, 255, 80), // 6: Yellow
new Color(255, 255, 255), // 7: Neutral White
new Color(0, 0, 0), // 8: Black
new Color(30, 60, 180), // 9: Deep Blue
new Color(255, 165, 0), // 10: Orange
new Color(180, 130, 255), // 11: Purple
new Color(144, 238, 144), // 12: Pale Green
new Color(175, 238, 238), // 13: Pale Turquoise
new Color(170, 170, 170), // 14: Grey
new Color(255, 255, 255), // 15: White
};
// Default 3278 monochrome colors
public static final Color DEFAULT_MONO_NORMAL = new Color(50, 205, 50);
public static final Color DEFAULT_MONO_INTENSIFY = new Color(255, 255, 255);
public static final Color DEFAULT_MONO_PROTECTED = new Color(80, 120, 255);
public static final Color DEFAULT_MONO_PROTECTED_HIGH = new Color(255, 255, 255);
// Default Background
public static final Color DEFAULT_BG_COLOR = new Color(10, 10, 10);
public TerminalPanel() {
setupColors();
setBackground(bgColor);
setFocusable(true);
setDoubleBuffered(true);
setFocusTraversalKeysEnabled(false);
// Use key bindings instead of KeyListener for reliable key handling
// This avoids focus/event issues with JScrollPane
setOpaque(true);
setupKeyBindings();
setupFont();
setupCursorBlink();
// Handle mouse clicks to position cursor and grab focus
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
requestFocusInWindow();
if (client != null && client.getConnectionState().isFullSession()) {
int col = (e.getX() - padding) / cellWidth;
int row = (e.getY() - padding) / cellHeight;
ScreenBuffer sb = client.getScreenBuffer();
if (row >= 0 && row < sb.getRows() && col >= 0 && col < sb.getCols()) {
sb.setCursorAddress(row * sb.getCols() + col);
repaint();
}
}
}
});
}
/**
* Use InputMap/ActionMap (key bindings) instead of KeyListener.
* This works reliably even inside a JScrollPane — the WHEN_FOCUSED
* condition ensures our panel receives all key events when focused.
*/
private void bindKeyToMap(InputMap im, String action, String bindingStr) {
if ("UNBOUND".equals(bindingStr) || bindingStr == null || bindingStr.isEmpty()) {
return;
}
// Support multiple bindings separated by commas
String[] bindings = bindingStr.split(",");
for (String binding : bindings) {
binding = binding.trim();
if (!binding.isEmpty() && !"UNBOUND".equals(binding)) {
KeyStroke ks = KeyStroke.getKeyStroke(binding);
if (ks != null) {
im.put(ks, "j3270-" + action);
}
}
}
}
private void setupKeyBindings() {
InputMap im = getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = getActionMap();
im.clear();
am.clear();
// Block the scroll pane from handling Tab, arrows, Page keys
String[] navKeys = { "TAB", "shift TAB", "UP", "DOWN", "LEFT", "RIGHT",
"PAGE_UP", "PAGE_DOWN", "HOME", "END", "ENTER",
"ESCAPE", "INSERT", "DELETE", "BACK_SPACE" };
for (String key : navKeys) {
String binding = org.pubvm.j3270.config.Settings.getKeyBinding(key, key);
bindKeyToMap(im, key, binding);
}
// PF keys 1-24
// PF1-12 default to F1-F12, PF13-24 default to shift F1-shift F12
for (int i = 1; i <= 24; i++) {
String defaultBinding;
if (i <= 12) {
defaultBinding = "F" + i;
} else {
defaultBinding = "shift F" + (i - 12);
}
String binding = org.pubvm.j3270.config.Settings.getKeyBinding("PF" + i, defaultBinding);
bindKeyToMap(im, "PF" + i, binding);
}
// PA keys: Alt+1, Alt+2, Alt+3
String pa1Def = "alt 1";
String pa2Def = "alt 2";
String pa3Def = "alt 3";
bindKeyToMap(im, "PA1", org.pubvm.j3270.config.Settings.getKeyBinding("PA1", pa1Def));
bindKeyToMap(im, "PA2", org.pubvm.j3270.config.Settings.getKeyBinding("PA2", pa2Def));
bindKeyToMap(im, "PA3", org.pubvm.j3270.config.Settings.getKeyBinding("PA3", pa3Def));
// Clear
bindKeyToMap(im, "CLEAR", org.pubvm.j3270.config.Settings.getKeyBinding("CLEAR", "alt C"));
// Create actions for all bound keys
am.put("j3270-ENTER", createAction(this::handleEnter));
am.put("j3270-ESCAPE", createAction(this::handleReset));
am.put("j3270-TAB", createAction(() -> handleTab(false)));
am.put("j3270-shift TAB", createAction(() -> handleTab(true)));
am.put("j3270-UP", createAction(() -> handleCursor("up")));
am.put("j3270-DOWN", createAction(() -> handleCursor("down")));
am.put("j3270-LEFT", createAction(() -> handleCursor("left")));
am.put("j3270-RIGHT", createAction(() -> handleCursor("right")));
am.put("j3270-HOME", createAction(() -> handleCursor("home")));
am.put("j3270-PAGE_UP", createAction(() -> handlePF(7)));
am.put("j3270-PAGE_DOWN", createAction(() -> handlePF(8)));
am.put("j3270-END", createAction(this::handleEraseEOF));
am.put("j3270-DELETE", createAction(this::handleDelete));
am.put("j3270-BACK_SPACE", createAction(this::handleBackspace));
am.put("j3270-INSERT", createAction(this::handleInsert));
am.put("j3270-CLEAR", createAction(this::handleClear));
for (int i = 1; i <= 24; i++) {
final int pf = i;
am.put("j3270-PF" + i, createAction(() -> handlePF(pf)));
}
am.put("j3270-PA1", createAction(() -> handlePA(1)));
am.put("j3270-PA2", createAction(() -> handlePA(2)));
am.put("j3270-PA3", createAction(() -> handlePA(3)));
// For printable character input, we override processKeyEvent
enableEvents(AWTEvent.KEY_EVENT_MASK);
}
@Override
protected void processKeyEvent(KeyEvent e) {
// Handle character typing via processKeyEvent to capture ALL typed chars
if (e.getID() == KeyEvent.KEY_TYPED) {
char ch = e.getKeyChar();
if (ch >= 0x20 && ch != 0x7F && ch != KeyEvent.CHAR_UNDEFINED
&& !e.isControlDown() && !e.isAltDown() && !e.isMetaDown()) {
if (client != null && client.getConnectionState().isFullSession()) {
client.typeCharacter(ch);
refreshScreen();
e.consume();
return;
}
}
}
super.processKeyEvent(e);
}
private AbstractAction createAction(Runnable r) {
return new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
r.run();
}
};
}
// ========== Key action handlers ==========
private void handleEnter() {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendEnter();
refreshScreen();
}
}
private void handleReset() {
if (client != null) {
client.reset();
refreshScreen();
}
}
private void handleTab(boolean shift) {
if (client != null && client.getConnectionState().isFullSession()) {
if (shift)
client.backTab();
else
client.tab();
refreshScreen();
}
}
private void handleCursor(String dir) {
if (client != null && client.getConnectionState().isFullSession()) {
switch (dir) {
case "up":
client.cursorUp();
break;
case "down":
client.cursorDown();
break;
case "left":
client.cursorLeft();
break;
case "right":
client.cursorRight();
break;
case "home":
client.cursorHome();
break;
}
refreshScreen();
}
}
private void handlePF(int n) {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendPF(n);
refreshScreen();
}
}
private void handlePA(int n) {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendPA(n);
refreshScreen();
}
}
private void handleEraseEOF() {
if (client != null && client.getConnectionState().isFullSession()) {
client.getInputProcessor().eraseEof();
refreshScreen();
}
}
private void handleDelete() {
if (client != null && client.getConnectionState().isFullSession()) {
client.getInputProcessor().deleteChar();
refreshScreen();
}
}
private void handleBackspace() {
if (client != null && client.getConnectionState().isFullSession()) {
client.getInputProcessor().backspace();
refreshScreen();
}
}
private void handleInsert() {
if (client != null) {
var ip = client.getInputProcessor();
ip.setInsertMode(!ip.isInsertMode());
refreshScreen();
}
}
private void handleClear() {
if (client != null && client.getConnectionState().isFullSession()) {
client.sendClear();
refreshScreen();
}
}
private void refreshScreen() {
repaint();
// Notify parent to update status bar too
Container parent = getParent();
while (parent != null) {
if (parent instanceof JFrame) {
parent.repaint();
break;
}
parent = parent.getParent();
}
}
private void setupFont() {
currentFontSize = org.pubvm.j3270.config.Settings.getFontSize();
String fontFamily = org.pubvm.j3270.config.Settings.getFontFamily();
terminalFont = new Font(fontFamily, Font.PLAIN, currentFontSize);
if (terminalFont.getFamily().equals("Dialog") && !fontFamily.equals("Dialog")) {
// Fallback
terminalFont = new Font(Font.MONOSPACED, Font.PLAIN, currentFontSize);
}
updateCellSize();
}
private void setupColors() {
for (int i = 0; i < 16; i++) {
hostColors[i] = org.pubvm.j3270.config.Settings.getColorOverride(i, DEFAULT_HOST_COLORS[i]);
}
monoNormal = org.pubvm.j3270.config.Settings.getMonoColorOverride("NORMAL", DEFAULT_MONO_NORMAL);
monoIntensify = org.pubvm.j3270.config.Settings.getMonoColorOverride("INTENSIFY", DEFAULT_MONO_INTENSIFY);
monoProtected = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED", DEFAULT_MONO_PROTECTED);
monoProtectedHigh = org.pubvm.j3270.config.Settings.getMonoColorOverride("PROTECTED_HIGH", DEFAULT_MONO_PROTECTED_HIGH);
bgColor = org.pubvm.j3270.config.Settings.getMonoColorOverride("BACKGROUND", DEFAULT_BG_COLOR);
setBackground(bgColor);
}
public void reloadSettings() {
setupColors();
setupKeyBindings();
setupFont();
revalidate();
repaint();
}
public void setFontSize(int size) {
currentFontSize = size;
org.pubvm.j3270.config.Settings.setFontSize(size);
terminalFont = terminalFont.deriveFont((float) size);
updateCellSize();
revalidate();
repaint();
}
public int getFontSize() {
return currentFontSize;
}
private void updateCellSize() {
FontMetrics fm = getFontMetrics(terminalFont);
cellWidth = fm.charWidth('M');
cellHeight = fm.getHeight();
fontAscent = fm.getAscent();
fontDescent = fm.getDescent();
}
private void setupCursorBlink() {
blinkTimer = new Timer(530, e -> {
cursorVisible = !cursorVisible;
repaint();
});
blinkTimer.start();
}
public void setClient(Telnet3270Client client) {
this.client = client;
}
@Override
public Dimension getPreferredSize() {
if (client != null) {
ScreenBuffer sb = client.getScreenBuffer();
// Always size to the MAXIMUM (alternate) screen dimensions,
// matching how x3270 works — the window shows the full model size
int displayCols = sb.getMaxCols();
int displayRows = sb.getMaxRows();
return new Dimension(displayCols * cellWidth + padding * 2,
displayRows * cellHeight + padding * 2);
}
return new Dimension(80 * cellWidth + padding * 2, 24 * cellHeight + padding * 2);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (client == null)
return;
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
// Manually clear the entire graphics bounds to ensure no ghosting on
// resize/clip
g2.setColor(bgColor);
g2.fillRect(0, 0, getWidth(), getHeight());
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getRows();
int cols = sb.getCols();
boolean isColorModel = client.getConfig().getModel().isColor();
// Track current field attribute for monochrome color decisions
byte currentFA = 0;
ExtendedAttribute currentFieldEa = null;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
int baddr = row * cols + col;
ExtendedAttribute ea = sb.getCell(baddr);
int x = padding + col * cellWidth;
int y = padding + row * cellHeight;
// Determine colors and attributes
Color fgColor;
Color bgColor;
boolean bold = false;
boolean underline = false;
boolean reverse = false;
if (ea.isFieldAttribute()) {
currentFA = ea.fa;
currentFieldEa = ea;
// Field attributes display as blanks
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
continue;
}
// Determine foreground color
if (isColorModel) {
fgColor = getColorForAttribute(ea, currentFieldEa, currentFA);
bgColor = getBackgroundForAttribute(ea, currentFieldEa);
} else {
fgColor = getMonoColor(currentFA);
bgColor = this.bgColor;
}
// Graphics rendition
byte gr = ea.gr != 0 ? ea.gr : (currentFieldEa != null ? currentFieldEa.gr : 0);
if (gr != 0) {
if ((gr & GR_INTENSIFY) != 0)
bold = true;
if ((gr & GR_UNDERLINE) != 0)
underline = true;
if ((gr & GR_REVERSE) != 0)
reverse = true;
}
// Field attribute implicit intensify
if (faIsHigh(currentFA & 0xFF))
bold = true;
// Handle invisible fields (zero intensity)
if (faIsZero(currentFA & 0xFF)) {
g2.setColor(this.bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
continue;
}
// Apply reverse video
if (reverse) {
Color tmp = fgColor;
fgColor = bgColor;
bgColor = tmp;
}
// Draw background
g2.setColor(bgColor);
g2.fillRect(x, y, cellWidth, cellHeight);
// Draw character
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(String.valueOf(ch), x, y + fontAscent);
}
// Draw underline
if (underline) {
g2.setColor(fgColor);
g2.drawLine(x, y + cellHeight - fontDescent,
x + cellWidth - 1, y + cellHeight - fontDescent);
}
}
}
// Draw cursor
if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getCursorAddress();
int curRow = curAddr / cols;
int curCol = curAddr % cols;
int cx = padding + curCol * cellWidth;
int cy = padding + curRow * cellHeight;
g2.setColor(new Color(255, 255, 255, 180));
g2.setXORMode(bgColor);
g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.setPaintMode();
}
}
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
int fg = ea.fg != 0 ? (ea.fg & 0xFF)
: (currentFieldEa != null && currentFieldEa.fg != 0 ? (currentFieldEa.fg & 0xFF) : 0);
if (fg >= 0xf0 && fg <= 0xff) {
return hostColors[fg - 0xf0];
}
if (faIsProtected(currentFA & 0xFF)) {
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_WHITE] : hostColors[HOST_COLOR_BLUE];
}
return faIsHigh(currentFA & 0xFF) ? hostColors[HOST_COLOR_RED] : hostColors[HOST_COLOR_GREEN];
}
private Color getBackgroundForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa) {
int bg = ea.bg != 0 ? (ea.bg & 0xFF)
: (currentFieldEa != null && currentFieldEa.bg != 0 ? (currentFieldEa.bg & 0xFF) : 0);
if (bg >= 0xf0 && bg <= 0xff) {
int idx = bg - 0xf0;
// Neutral black (0xf0) and black (0xf8) should use window bgColor
// to avoid visible seams between field bg and window bg
if (idx == HOST_COLOR_NEUTRAL_BLACK || idx == HOST_COLOR_BLACK) {
return bgColor;
}
return hostColors[idx];
}
return bgColor;
}
private Color getMonoColor(byte fa) {
if (faIsProtected(fa & 0xFF)) {
return faIsHigh(fa & 0xFF) ? monoProtectedHigh : monoProtected;
}
return faIsHigh(fa & 0xFF) ? monoIntensify : monoNormal;
}
public void dispose() {
if (blinkTimer != null)
blinkTimer.stop();
}
}
+13
View File
@@ -0,0 +1,13 @@
plugins {
id 'java-library'
}
dependencies {
}
jar {
manifest {
attributes 'Implementation-Title': 'lib3270j',
'Implementation-Version': archiveVersion
}
}
@@ -0,0 +1,67 @@
package org.lib3270j;
/**
* Configuration for a 3270 terminal connection.
*/
public class ConnectionConfig {
private String host;
private int port = 23;
private TerminalModel model = TerminalModel.IBM_3279_4;
private String luName = null;
private boolean extendedDataStream = true;
private boolean useTls = false;
private int connectTimeoutMs = 15000;
private int nopIntervalSeconds = 0;
private String terminalName = null; // override terminal type string
public ConnectionConfig() {}
public ConnectionConfig(String host, int port) {
this.host = host;
this.port = port;
}
public ConnectionConfig(String host, int port, TerminalModel model) {
this.host = host;
this.port = port;
this.model = model;
}
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public TerminalModel getModel() { return model; }
public void setModel(TerminalModel model) { this.model = model; }
public String getLuName() { return luName; }
public void setLuName(String luName) { this.luName = luName; }
public boolean isExtendedDataStream() { return extendedDataStream; }
public void setExtendedDataStream(boolean ext) { this.extendedDataStream = ext; }
public boolean isUseTls() { return useTls; }
public void setUseTls(boolean useTls) { this.useTls = useTls; }
public int getConnectTimeoutMs() { return connectTimeoutMs; }
public void setConnectTimeoutMs(int ms) { this.connectTimeoutMs = ms; }
public int getNopIntervalSeconds() { return nopIntervalSeconds; }
public void setNopIntervalSeconds(int s) { this.nopIntervalSeconds = s; }
public String getTerminalName() { return terminalName; }
public void setTerminalName(String name) { this.terminalName = name; }
/**
* Get the effective terminal type string to send during negotiation.
*/
public String getEffectiveTerminalType() {
if (terminalName != null) {
return terminalName;
}
return extendedDataStream ? model.getTerminalType() : model.getBaseTerminalType();
}
}
@@ -0,0 +1,62 @@
package org.lib3270j;
/**
* Connection state machine states.
* Mirrors the cstate enum from globals.h in x3270.
*/
public enum ConnectionState {
NOT_CONNECTED, // No socket, unknown mode
RECONNECTING, // Delay before automatic reconnect
RESOLVING, // Resolving hostname
TCP_PENDING, // Socket connection pending
TLS_PENDING, // TLS negotiation pending
PROXY_PENDING, // Proxy negotiation pending
TELNET_PENDING, // Telnet negotiation pending
CONNECTED_NVT, // Connected in NVT line mode
CONNECTED_NVT_CHAR, // Connected in NVT character-at-a-time mode
CONNECTED_3270, // Connected in RFC 1576 TN3270 mode
CONNECTED_UNBOUND, // Connected in TN3270E mode, unbound
CONNECTED_E_NVT, // Connected in TN3270E NVT mode
CONNECTED_SSCP, // Connected in TN3270E SSCP-LU mode
CONNECTED_TN3270E; // Connected in TN3270E 3270 mode
/** True if any kind of connection exists (even half-connected). */
public boolean isConnected() {
return this.ordinal() > NOT_CONNECTED.ordinal();
}
/** True if in a half-connected state (resolving through telnet pending). */
public boolean isHalfConnected() {
return this.ordinal() >= RESOLVING.ordinal() && this.ordinal() < CONNECTED_NVT.ordinal();
}
/** True if fully connected (past TCP pending). */
public boolean isFullyConnected() {
return this.ordinal() > TCP_PENDING.ordinal();
}
/** True if in NVT mode (any flavor). */
public boolean isNvt() {
return this == CONNECTED_NVT || this == CONNECTED_NVT_CHAR || this == CONNECTED_E_NVT;
}
/** True if in 3270 mode (any flavor). */
public boolean is3270() {
return this == CONNECTED_3270 || this == CONNECTED_TN3270E || this == CONNECTED_SSCP;
}
/** True if in SSCP-LU mode. */
public boolean isSscp() {
return this == CONNECTED_SSCP;
}
/** True if in TN3270E mode (any submode). */
public boolean isTn3270e() {
return this.ordinal() >= CONNECTED_UNBOUND.ordinal();
}
/** True if in a full data session (NVT or 3270). */
public boolean isFullSession() {
return isNvt() || is3270();
}
}
@@ -0,0 +1,174 @@
package org.lib3270j;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.listener.*;
import java.util.logging.*;
/**
* Diagnostic: login as guest1/guest and trace the Welcome → MOTD transition.
*/
public class DiagnosticClient {
static volatile int screenUpdateCount = 0;
static Telnet3270Client client;
public static void main(String[] args) throws Exception {
Logger rootLogger = Logger.getLogger("org.lib3270j");
rootLogger.setLevel(Level.ALL);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
handler.setFormatter(new SimpleFormatter());
rootLogger.addHandler(handler);
Logger.getLogger("").setLevel(Level.WARNING);
String host = args.length >= 1 ? args[0] : "192.168.0.30";
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270;
ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_2);
client = new Telnet3270Client(config);
client.addConnectionListener(new ConnectionListener() {
@Override
public void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState) {
System.out.println(">>> STATE: " + oldState + " -> " + newState);
}
@Override
public void onConnectionError(String message) {
System.out.println(">>> ERROR: " + message);
}
@Override
public void onTN3270ENegotiated(String deviceType, String deviceName) {
System.out.println(">>> TN3270E: type=" + deviceType + " name=" + deviceName);
}
});
client.addScreenUpdateListener(new ScreenUpdateListener() {
@Override
public void onScreenUpdated() {
screenUpdateCount++;
System.out.println("\n>>> SCREEN UPDATE #" + screenUpdateCount);
dumpScreen();
client.getInputProcessor().setKeyboardLocked(false);
}
@Override
public void onSoundAlarm() {
System.out.println(">>> ALARM");
}
@Override
public void onScreenSizeChanged(int rows, int cols) {
System.out.println(">>> SCREEN SIZE: " + rows + "x" + cols);
}
});
System.out.println("=== Connecting to " + host + ":" + port + " ===");
client.connect();
// Wait for login screen
waitForUpdates(5000);
// Type guest1 at login field
System.out.println("\n=== Typing 'guest1' ===");
typeAndWait("guest1", 2000);
// Press Enter to submit login
System.out.println("\n=== Pressing Enter (submit login) ===");
client.sendEnter();
waitForUpdates(3000);
// Check if we need to enter password
System.out.println("\n=== Typing password 'guest' ===");
typeAndWait("guest", 2000);
client.sendEnter();
waitForUpdates(5000);
// Now we should see Welcome/allocations screen
// Wait for *** prompt
Thread.sleep(3000);
System.out.println("\n=== Current screen (should be Welcome/allocations with ***) ===");
dumpScreen();
// Press Enter at *** to continue — THIS is where the screen should clear
System.out.println("\n=== Pressing Enter at *** (Welcome → MOTD transition) ===");
client.sendEnter();
waitForUpdates(5000);
// Wait for more updates
Thread.sleep(3000);
System.out.println("\n=== After *** Enter — screen should have been cleared for MOTD ===");
dumpScreen();
// If there's another *** prompt, press Enter again
System.out.println("\n=== Pressing Enter again ===");
client.sendEnter();
waitForUpdates(5000);
Thread.sleep(2000);
dumpScreen();
// One more Enter
System.out.println("\n=== Pressing Enter one more time ===");
client.sendEnter();
waitForUpdates(5000);
Thread.sleep(2000);
dumpScreen();
// Let it sit for any more data
Thread.sleep(3000);
System.out.println("\n=== Final screen ===");
dumpScreen();
client.disconnect();
System.out.println(">>> Disconnected");
}
static void typeAndWait(String text, int waitMs) throws InterruptedException {
for (char c : text.toCharArray()) {
client.typeCharacter(c);
Thread.sleep(30);
}
Thread.sleep(waitMs);
}
static void waitForUpdates(int timeoutMs) throws InterruptedException {
int start = screenUpdateCount;
long deadline = System.currentTimeMillis() + timeoutMs;
while (screenUpdateCount == start && System.currentTimeMillis() < deadline) {
Thread.sleep(100);
}
if (screenUpdateCount == start) {
System.out.println(">>> (no screen update within " + timeoutMs + "ms)");
}
Thread.sleep(500);
}
static void dumpScreen() {
ScreenBuffer sb = client.getScreenBuffer();
int rows = sb.getRows();
int cols = sb.getCols();
System.out.println("--- Screen " + rows + "x" + cols +
" cursor=" + sb.getCursorAddress() +
" formatted=" + sb.isFormatted() +
" state=" + client.getConnectionState() + " ---");
for (int r = 0; r < rows; r++) {
StringBuilder line = new StringBuilder();
boolean hasContent = false;
for (int c = 0; c < cols; c++) {
ExtendedAttribute ea = sb.getCell(r * cols + c);
if (ea.isFieldAttribute()) {
line.append('|');
} else if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
line.append(ea.ucs4);
hasContent = true;
} else {
line.append(' ');
}
}
if (hasContent) {
System.out.println(String.format("%02d: %s", r, line.toString().stripTrailing()));
}
}
System.out.println("---");
}
}
@@ -0,0 +1,192 @@
package org.lib3270j;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.datastream.DataStreamProcessor;
import org.lib3270j.input.InputProcessor;
import org.lib3270j.listener.ConnectionListener;
import org.lib3270j.listener.ScreenUpdateListener;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.telnet.TelnetConnection;
import org.lib3270j.telnet.TelnetFSM;
import java.io.IOException;
import java.util.logging.Logger;
/**
* Main API entry point for lib3270j.
*
* Provides a high-level interface for connecting to a TN3270 host,
* managing the screen buffer, and handling user input.
*
* Usage:
* <pre>
* ConnectionConfig config = new ConnectionConfig("hostname", 23, TerminalModel.IBM_3279_4);
* Telnet3270Client client = new Telnet3270Client(config);
* client.addConnectionListener(myListener);
* client.addScreenUpdateListener(myScreenListener);
* client.connect();
* // ... interact with screen ...
* client.disconnect();
* </pre>
*/
public class Telnet3270Client {
private static final Logger log = Logger.getLogger(Telnet3270Client.class.getName());
private final ConnectionConfig config;
private final EbcdicTranslator translator;
private final ScreenBuffer screenBuffer;
private final DataStreamProcessor dsProcessor;
private final TelnetFSM fsm;
private final InputProcessor inputProcessor;
private TelnetConnection connection;
public Telnet3270Client(ConnectionConfig config) {
this.config = config;
this.translator = new EbcdicTranslator();
this.screenBuffer = new ScreenBuffer(config.getModel(), translator);
this.dsProcessor = new DataStreamProcessor(screenBuffer, translator);
this.fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
this.inputProcessor = new InputProcessor(screenBuffer, translator, fsm);
// Wire up the output sender: DSProcessor -> FSM -> TelnetConnection
dsProcessor.setOutputSender(fsm::send3270Data);
}
/**
* Connect to the configured host.
* This method blocks until the TCP connection is established,
* then returns while telnet/TN3270E negotiation continues asynchronously.
*/
public void connect() throws IOException {
log.info("Connecting to " + config.getHost() + ":" + config.getPort() +
" model=" + config.getModel());
connection = new TelnetConnection(config, fsm);
fsm.setConnection(connection);
// Establish TCP connection
connection.connect();
// Notify FSM that TCP is connected — begins telnet negotiation
fsm.onConnected();
}
/**
* Disconnect from the host.
*/
public void disconnect() {
if (connection != null) {
connection.disconnect();
connection = null;
}
fsm.onDisconnect();
}
/**
* Check if connected (any state past TCP pending).
*/
public boolean isConnected() {
return connection != null && connection.isConnected();
}
// ========== Listener management ==========
public void addConnectionListener(ConnectionListener l) {
fsm.addConnectionListener(l);
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
fsm.addScreenUpdateListener(l);
dsProcessor.addScreenUpdateListener(l);
}
// ========== Screen access ==========
/** Get the screen buffer for rendering. */
public ScreenBuffer getScreenBuffer() { return screenBuffer; }
/** Get the EBCDIC translator. */
public EbcdicTranslator getTranslator() { return translator; }
/** Get the current connection state. */
public ConnectionState getConnectionState() { return fsm.getConnectionState(); }
/** Get the input processor for keyboard operations. */
public InputProcessor getInputProcessor() { return inputProcessor; }
/** Get the connection config. */
public ConnectionConfig getConfig() { return config; }
// ========== Convenience input methods ==========
/** Type a character at the cursor position. */
public void typeCharacter(char ch) { inputProcessor.typeCharacter(ch); }
/** Type a string at the cursor position. */
public void typeString(String s) {
for (char ch : s.toCharArray()) {
inputProcessor.typeCharacter(ch);
}
}
/** Send Enter key. */
public void sendEnter() {
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER);
}
/** Send a PF key (1-24). */
public void sendPF(int number) {
int aid;
switch (number) {
case 1: aid = 0xf1; break; case 2: aid = 0xf2; break;
case 3: aid = 0xf3; break; case 4: aid = 0xf4; break;
case 5: aid = 0xf5; break; case 6: aid = 0xf6; break;
case 7: aid = 0xf7; break; case 8: aid = 0xf8; break;
case 9: aid = 0xf9; break; case 10: aid = 0x7a; break;
case 11: aid = 0x7b; break; case 12: aid = 0x7c; break;
case 13: aid = 0xc1; break; case 14: aid = 0xc2; break;
case 15: aid = 0xc3; break; case 16: aid = 0xc4; break;
case 17: aid = 0xc5; break; case 18: aid = 0xc6; break;
case 19: aid = 0xc7; break; case 20: aid = 0xc8; break;
case 21: aid = 0xc9; break; case 22: aid = 0x4a; break;
case 23: aid = 0x4b; break; case 24: aid = 0x4c; break;
default: return;
}
inputProcessor.sendAid(aid);
}
/** Send a PA key (1-3). */
public void sendPA(int number) {
int aid;
switch (number) {
case 1: aid = 0x6c; break;
case 2: aid = 0x6e; break;
case 3: aid = 0x6b; break;
default: return;
}
inputProcessor.sendAid(aid);
}
/** Send Clear key. */
public void sendClear() {
inputProcessor.sendAid(org.lib3270j.protocol.DS3270Constants.AID_CLEAR);
}
/** Move cursor up. */
public void cursorUp() { inputProcessor.cursorUp(); }
/** Move cursor down. */
public void cursorDown() { inputProcessor.cursorDown(); }
/** Move cursor left. */
public void cursorLeft() { inputProcessor.cursorLeft(); }
/** Move cursor right. */
public void cursorRight() { inputProcessor.cursorRight(); }
/** Move cursor to home position. */
public void cursorHome() { inputProcessor.cursorHome(); }
/** Tab to next unprotected field. */
public void tab() { inputProcessor.tab(); }
/** Back-tab to previous unprotected field. */
public void backTab() { inputProcessor.backTab(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
}
@@ -0,0 +1,75 @@
package org.lib3270j;
import static org.lib3270j.protocol.DS3270Constants.*;
/**
* Terminal model definitions for IBM 3278 and 3279 terminals.
* Models 2-5 are supported, each with default (24x80) and alternate screen sizes.
*/
public enum TerminalModel {
IBM_3278_2(2, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
IBM_3278_3(3, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
IBM_3278_4(4, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
IBM_3278_5(5, false, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS),
IBM_3279_2(2, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_2_ROWS, MODEL_2_COLS),
IBM_3279_3(3, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_3_ROWS, MODEL_3_COLS),
IBM_3279_4(4, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_4_ROWS, MODEL_4_COLS),
IBM_3279_5(5, true, MODEL_2_ROWS, MODEL_2_COLS, MODEL_5_ROWS, MODEL_5_COLS);
private final int modelNumber;
private final boolean color;
private final int defaultRows;
private final int defaultCols;
private final int alternateRows;
private final int alternateCols;
TerminalModel(int modelNumber, boolean color,
int defaultRows, int defaultCols,
int alternateRows, int alternateCols) {
this.modelNumber = modelNumber;
this.color = color;
this.defaultRows = defaultRows;
this.defaultCols = defaultCols;
this.alternateRows = alternateRows;
this.alternateCols = alternateCols;
}
public int getModelNumber() { return modelNumber; }
public boolean isColor() { return color; }
public int getDefaultRows() { return defaultRows; }
public int getDefaultCols() { return defaultCols; }
public int getAlternateRows() { return alternateRows; }
public int getAlternateCols() { return alternateCols; }
/**
* Returns the terminal type string for TN3270E negotiation.
* e.g., "IBM-3279-4-E" for a color model 4 with extended data stream.
*/
public String getTerminalType() {
return String.format("IBM-327%c-%d-E", color ? '9' : '8', modelNumber);
}
/**
* Returns the base terminal type without "-E" suffix (for non-extended mode).
*/
public String getBaseTerminalType() {
return String.format("IBM-327%c-%d", color ? '9' : '8', modelNumber);
}
/**
* Look up a model by number and color mode.
*/
public static TerminalModel forModel(int number, boolean isColor) {
for (TerminalModel m : values()) {
if (m.modelNumber == number && m.color == isColor) {
return m;
}
}
throw new IllegalArgumentException("Unknown model: " + number + " color=" + isColor);
}
@Override
public String toString() {
return getTerminalType();
}
}
@@ -0,0 +1,128 @@
package org.lib3270j.charset;
/**
* EBCDIC ↔ Unicode translator.
* Default: Code Page 037 (US/Canada EBCDIC).
*/
public class EbcdicTranslator {
/**
* EBCDIC Code Page 037 to Unicode mapping.
* Index is the EBCDIC byte value (0x00-0xFF), value is the Unicode codepoint.
*/
private static final int[] CP037_TO_UNICODE = {
// 00-0F
0x0000, 0x0001, 0x0002, 0x0003, 0x009C, 0x0009, 0x0086, 0x007F,
0x0097, 0x008D, 0x008E, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
// 10-1F
0x0010, 0x0011, 0x0012, 0x0013, 0x009D, 0x0085, 0x0008, 0x0087,
0x0018, 0x0019, 0x0092, 0x008F, 0x001C, 0x001D, 0x001E, 0x001F,
// 20-2F
0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x000A, 0x0017, 0x001B,
0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x0005, 0x0006, 0x0007,
// 30-3F
0x0090, 0x0091, 0x0016, 0x0093, 0x0094, 0x0095, 0x0096, 0x0004,
0x0098, 0x0099, 0x009A, 0x009B, 0x0014, 0x0015, 0x009E, 0x001A,
// 40-4F (space, accent chars, punctuation)
0x0020, 0x00A0, 0x00E2, 0x00E4, 0x00E0, 0x00E1, 0x00E3, 0x00E5,
0x00E7, 0x00F1, 0x00A2, 0x002E, 0x003C, 0x0028, 0x002B, 0x007C,
// 50-5F
0x0026, 0x00E9, 0x00EA, 0x00EB, 0x00E8, 0x00ED, 0x00EE, 0x00EF,
0x00EC, 0x00DF, 0x0021, 0x0024, 0x002A, 0x0029, 0x003B, 0x00AC,
// 60-6F
0x002D, 0x002F, 0x00C2, 0x00C4, 0x00C0, 0x00C1, 0x00C3, 0x00C5,
0x00C7, 0x00D1, 0x00A6, 0x002C, 0x0025, 0x005F, 0x003E, 0x003F,
// 70-7F
0x00F8, 0x00C9, 0x00CA, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF,
0x00CC, 0x0060, 0x003A, 0x0023, 0x0040, 0x0027, 0x003D, 0x0022,
// 80-8F (lowercase a-i)
0x00D8, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x00AB, 0x00BB, 0x00F0, 0x00FD, 0x00FE, 0x00B1,
// 90-9F (lowercase j-r)
0x00B0, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, 0x0070,
0x0071, 0x0072, 0x00AA, 0x00BA, 0x00E6, 0x00B8, 0x00C6, 0x00A4,
// A0-AF (lowercase s-z)
0x00B5, 0x007E, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078,
0x0079, 0x007A, 0x00A1, 0x00BF, 0x00D0, 0x00DD, 0x00DE, 0x00AE,
// B0-BF
0x005E, 0x00A3, 0x00A5, 0x00B7, 0x00A9, 0x00A7, 0x00B6, 0x00BC,
0x00BD, 0x00BE, 0x005B, 0x005D, 0x00AF, 0x00A8, 0x00B4, 0x00D7,
// C0-CF (uppercase A-I)
0x007B, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x00AD, 0x00F4, 0x00F6, 0x00F2, 0x00F3, 0x00F5,
// D0-DF (uppercase J-R)
0x007D, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x00B9, 0x00FB, 0x00FC, 0x00F9, 0x00FA, 0x00FF,
// E0-EF (uppercase S-Z)
0x005C, 0x00F7, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A, 0x00B2, 0x00D4, 0x00D6, 0x00D2, 0x00D3, 0x00D5,
// F0-FF (digits 0-9)
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x00B3, 0x00DB, 0x00DC, 0x00D9, 0x00DA, 0x009F,
};
/**
* Unicode to EBCDIC Code Page 037 mapping (for basic Latin + Latin-1).
* Index is the Unicode codepoint (0x00-0xFF), value is the EBCDIC byte (-1 if unmappable).
*/
private static final int[] UNICODE_TO_CP037 = new int[256];
static {
// Build reverse mapping
java.util.Arrays.fill(UNICODE_TO_CP037, -1);
for (int i = 0; i < 256; i++) {
int unicode = CP037_TO_UNICODE[i];
if (unicode < 256) {
UNICODE_TO_CP037[unicode] = i;
}
}
}
/**
* Translate EBCDIC byte to Unicode character.
*/
public char ebcdicToUnicode(int ebc) {
return (char) CP037_TO_UNICODE[ebc & 0xFF];
}
/**
* Translate Unicode character to EBCDIC byte.
* Returns -1 if the character cannot be mapped.
*/
public int unicodeToEbcdic(char unicode) {
if (unicode < 256) {
return UNICODE_TO_CP037[unicode];
}
return -1;
}
/**
* Translate Unicode character to EBCDIC, returning EBCDIC space (0x40) if unmappable.
*/
public byte unicodeToEbcdicSafe(char unicode) {
int ebc = unicodeToEbcdic(unicode);
return (byte) (ebc >= 0 ? ebc : 0x40);
}
/**
* Translate a byte array from EBCDIC to a Unicode string.
*/
public String ebcdicToString(byte[] ebcdic, int offset, int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
sb.append(ebcdicToUnicode(ebcdic[offset + i] & 0xFF));
}
return sb.toString();
}
/**
* Translate a Unicode string to EBCDIC byte array.
*/
public byte[] stringToEbcdic(String s) {
byte[] result = new byte[s.length()];
for (int i = 0; i < s.length(); i++) {
result[i] = unicodeToEbcdicSafe(s.charAt(i));
}
return result;
}
}
@@ -0,0 +1,813 @@
package org.lib3270j.datastream;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.listener.ScreenUpdateListener;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
/**
* Processes inbound 3270 data stream records.
* Handles all 3270 commands: Write, Erase/Write, Read Buffer, WSF, etc.
* Equivalent to process_ds() and ctlr_write() in ctlr.c.
*/
public class DataStreamProcessor {
private static final Logger log = Logger.getLogger(DataStreamProcessor.class.getName());
private final ScreenBuffer screen;
private final EbcdicTranslator translator;
private final QueryReplyBuilder qrBuilder;
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
// Output buffer for responses (read buffer, query replies, etc.)
private byte[] outputBuffer;
private int outputPos;
// Callback for sending data back to host
private OutputSender outputSender;
/** Functional interface for sending output back through the telnet stack. */
@FunctionalInterface
public interface OutputSender {
void send3270Data(byte[] data);
}
public DataStreamProcessor(ScreenBuffer screen, EbcdicTranslator translator) {
this.screen = screen;
this.translator = translator;
this.qrBuilder = new QueryReplyBuilder(screen);
this.outputBuffer = new byte[32768];
this.outputPos = 0;
}
public void setOutputSender(OutputSender sender) {
this.outputSender = sender;
}
public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l);
}
/**
* Process a 3270 data stream record.
*
* @param data Raw data buffer
* @param offset Offset of the first byte (command byte)
* @param length Number of bytes
* @param keyboardRestore Whether to restore keyboard after processing
*/
public void processRecord(byte[] data, int offset, int length, boolean keyboardRestore) {
if (length == 0)
return;
int cmd = data[offset] & 0xFF;
log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
switch (cmd) {
case CMD_W:
case SNA_CMD_W:
processWrite(data, offset, length, false);
break;
case CMD_EW:
case SNA_CMD_EW:
log.info(">>> ERASE/WRITE: clearing screen (default size)"); {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(false);
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
}
break;
case CMD_EWA:
case SNA_CMD_EWA:
log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); {
int oldRows = screen.getRows();
int oldCols = screen.getCols();
screen.erase(true);
processWrite(data, offset, length, true);
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
notifyScreenSizeChanged();
}
}
break;
case CMD_RB:
case SNA_CMD_RB:
processReadBuffer();
break;
case CMD_RM:
case SNA_CMD_RM:
processReadModified(false);
break;
case CMD_RMA:
case SNA_CMD_RMA:
processReadModified(true);
break;
case CMD_EAU:
case SNA_CMD_EAU:
log.info(">>> EAU: erasing all unprotected fields");
screen.eraseAllUnprotected();
break;
case CMD_WSF:
case SNA_CMD_WSF:
processWriteStructuredField(data, offset, length);
break;
case CMD_NOP:
log.info(">>> NOP command");
break;
default:
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
break;
}
// Translate EBCDIC to Unicode for display
screen.translateToUnicode();
screen.markAllChanged();
// Debug: dump non-empty screen lines
if (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW) {
int r = screen.getRows();
int c = screen.getCols();
StringBuilder dump = new StringBuilder();
int linesShown = 0;
for (int row = 0; row < r && linesShown < 5; row++) {
StringBuilder line = new StringBuilder();
boolean hasContent = false;
for (int col = 0; col < c; col++) {
int addr = row * c + col;
ExtendedAttribute ea = screen.getCell(addr);
byte fa = screen.getFieldAttributeAt(addr);
if ((fa & org.lib3270j.protocol.DS3270Constants.FA_MASK) == org.lib3270j.protocol.DS3270Constants.FA_INT_ZERO_NSEL) {
if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
line.append('*');
hasContent = true;
} else {
line.append(' ');
}
} else if (ea.ucs4 > 0x20 && ea.ucs4 != 0xFF) {
line.append(ea.ucs4);
hasContent = true;
} else {
line.append(' ');
}
}
if (hasContent) {
dump.append(String.format(" %02d: %s\n", row, line.toString().stripTrailing()));
linesShown++;
}
}
if (dump.length() > 0) {
log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
}
}
}
/**
* Process SSCP-LU data (TN3270E SSCP-LU mode).
* Data is character data placed starting at cursor position.
* Unlike 3270 mode, SSCP-LU data accumulates — we do NOT clear the screen.
*/
public void processSscpLuData(byte[] data, int offset, int length) {
int size = screen.getRows() * screen.getCols();
int baddr = screen.getCursorAddress();
int cols = screen.getCols();
for (int i = offset; i < offset + length; i++) {
int c = data[i] & 0xFF;
switch (c) {
case 0x15: // NL (new line) — move to column 0 of next row
baddr = ((baddr / cols) + 1) * cols;
if (baddr >= size)
baddr = 0;
break;
case 0x0d: // CR (carriage return) — move to column 0 of current row
baddr = (baddr / cols) * cols;
break;
case 0x0c: // FF (form feed) — clear screen and home cursor
screen.clear();
baddr = 0;
break;
case 0x00: // NULL — skip
break;
default:
ExtendedAttribute ea = screen.getCell(baddr);
ea.ec = (byte) c;
ea.ucs4 = translator.ebcdicToUnicode(c);
baddr = (baddr + 1) % size;
break;
}
}
screen.setCursorAddress(baddr);
screen.translateToUnicode();
screen.markAllChanged();
}
// ========== Write processing ==========
private void processWrite(byte[] data, int offset, int length, boolean eraseFirst) {
if (length < 2)
return;
// WCC is the second byte
int wcc = data[offset + 1] & 0xFF;
boolean alarm = wccSoundAlarm(wcc);
boolean kbdRestore = wccKeyboardRestore(wcc);
boolean resetMdt = wccResetMDT(wcc);
log.fine("WCC: " + String.format("0x%02x", wcc) +
" reset=" + wccReset(wcc) + " alarm=" + alarm + " kbdRestore=" + kbdRestore + " resetMdt=" + resetMdt);
if (resetMdt) {
resetAllMDT();
}
// WCC reset bit — reset partition characteristics
if (wccReset(wcc)) {
// Reset all character attributes to defaults
log.fine("WCC reset: clearing default attributes");
}
if (eraseFirst) {
screen.clear();
log.fine("Cleared screen for Erase/Write");
}
// Process orders and data starting at byte 2
int pos = offset + 2;
int end = offset + length;
int baddr = screen.getBufferAddress();
int size = screen.getRows() * screen.getCols();
// Current SA (set attribute) values for character-mode
byte currentFg = 0, currentBg = 0, currentGr = 0, currentCs = 0;
boolean lastWasOrder = false;
while (pos < end) {
int b = data[pos] & 0xFF;
switch (b) {
case ORDER_SBA: { // Set Buffer Address
if (pos + 2 >= end) {
pos = end;
break;
}
baddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF);
if (baddr >= size)
baddr = baddr % size;
screen.setBufferAddress(baddr);
log.finest("SBA " + baddr + " (row=" + (baddr / screen.getCols()) + " col="
+ (baddr % screen.getCols()) + ")");
pos += 3;
lastWasOrder = true;
break;
}
case ORDER_SF: { // Start Field
if (pos + 1 >= end) {
pos = end;
break;
}
int fa = data[pos + 1] & 0xFF;
ExtendedAttribute ea = screen.getCell(baddr);
ea.clear();
ea.fa = (byte) (fa & FA_MASK);
// FA position is a display position that shows as blank
ea.ec = 0;
ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
screen.setFormatted(true);
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
log.finest("SF fa=" + String.format("0x%02x", fa));
pos += 2;
lastWasOrder = true;
break;
}
case ORDER_SFE: { // Start Field Extended
if (pos + 1 >= end) {
pos = end;
break;
}
int nPairs = data[pos + 1] & 0xFF;
if (pos + 2 + nPairs * 2 > end) {
pos = end;
break;
}
ExtendedAttribute ea = screen.getCell(baddr);
ea.clear();
ea.ec = 0;
ea.ucs4 = ' ';
currentFg = 0;
currentBg = 0;
currentGr = 0;
currentCs = 0;
for (int i = 0; i < nPairs; i++) {
int attrType = data[pos + 2 + i * 2] & 0xFF;
int attrValue = data[pos + 2 + i * 2 + 1] & 0xFF;
applyExtendedAttribute(ea, attrType, attrValue);
}
if (ea.fa == 0) {
// If no 3270 FA was among the pairs, set default
ea.fa = (byte) (FA_PRINTABLE);
}
screen.setFormatted(true);
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 2 + nPairs * 2;
lastWasOrder = true;
break;
}
case ORDER_SA: { // Set Attribute
if (pos + 2 >= end) {
pos = end;
break;
}
int attrType = data[pos + 1] & 0xFF;
int attrValue = data[pos + 2] & 0xFF;
switch (attrType) {
case XA_FOREGROUND:
currentFg = (byte) attrValue;
break;
case XA_BACKGROUND:
currentBg = (byte) attrValue;
break;
case XA_HIGHLIGHTING:
if (attrValue == XAH_DEFAULT)
currentGr = 0;
else if (attrValue == XAH_BLINK)
currentGr = GR_BLINK;
else if (attrValue == XAH_REVERSE)
currentGr = GR_REVERSE;
else if (attrValue == XAH_UNDERSCORE)
currentGr = GR_UNDERLINE;
else if (attrValue == XAH_INTENSIFY)
currentGr = GR_INTENSIFY;
else if (attrValue == XAH_NORMAL)
currentGr = 0;
break;
case XA_CHARSET:
currentCs = (byte) attrValue;
break;
}
pos += 3;
lastWasOrder = true;
break;
}
case ORDER_MF: { // Modify Field
if (pos + 1 >= end) {
pos = end;
break;
}
int nPairs = data[pos + 1] & 0xFF;
if (pos + 2 + nPairs * 2 > end) {
pos = end;
break;
}
// Find the field attribute at or before current position
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
ExtendedAttribute ea = screen.getCell(faAddr);
for (int i = 0; i < nPairs; i++) {
int attrType = data[pos + 2 + i * 2] & 0xFF;
int attrValue = data[pos + 2 + i * 2 + 1] & 0xFF;
applyExtendedAttribute(ea, attrType, attrValue);
}
}
pos += 2 + nPairs * 2;
lastWasOrder = true;
break;
}
case ORDER_IC: { // Insert Cursor
screen.setCursorAddress(baddr);
log.finest("IC at " + baddr);
pos += 1;
lastWasOrder = true;
break;
}
case ORDER_PT: { // Program Tab
// Skip to next unprotected field
baddr = screen.findNextUnprotected(baddr);
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = true;
break;
}
case ORDER_RA: { // Repeat to Address
if (pos + 3 >= end) {
pos = end;
break;
}
int toAddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF);
int fillChar = data[pos + 3] & 0xFF;
if (toAddr >= size)
toAddr = toAddr % size;
// Handle GE (graphic escape) prefix
byte fillCs = currentCs;
if (pos + 4 < end && fillChar == ORDER_GE) {
fillChar = data[pos + 4] & 0xFF;
fillCs = CS_GE;
pos += 5;
} else {
pos += 4;
}
// Fill from current position to target
while (baddr != toAddr) {
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) fillChar;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = fillCs;
baddr = (baddr + 1) % size;
}
screen.setBufferAddress(baddr);
lastWasOrder = true;
break;
}
case ORDER_EUA: { // Erase Unprotected to Address
if (pos + 2 >= end) {
pos = end;
break;
}
int toAddr = decodeAddress(data[pos + 1] & 0xFF, data[pos + 2] & 0xFF);
if (toAddr >= size)
toAddr = toAddr % size;
while (baddr != toAddr) {
ExtendedAttribute ea = screen.getCell(baddr);
if (!ea.isFieldAttribute()) {
int faAddr = screen.findFieldAttribute(baddr);
byte faVal = faAddr >= 0 ? screen.getCell(faAddr).fa : 0;
if (!faIsProtected(faVal & 0xFF)) {
ea.ec = 0;
ea.ucs4 = 0;
}
}
baddr = (baddr + 1) % size;
}
screen.setBufferAddress(baddr);
pos += 3;
lastWasOrder = true;
break;
}
case ORDER_GE: { // Graphic Escape
if (pos + 1 >= end) {
pos = end;
break;
}
int geChar = data[pos + 1] & 0xFF;
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any
ea.ec = (byte) geChar;
ea.cs = CS_GE;
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 2;
lastWasOrder = true;
break;
}
default: {
// Regular data byte (EBCDIC character)
// Format control codes display as nulls/blanks per 3270 spec
boolean isFCOrder = (b == FCORDER_NULL || b == FCORDER_FF ||
b == FCORDER_CR || b == FCORDER_NL || b == FCORDER_EM ||
b == FCORDER_DUP || b == FCORDER_FM || b == FCORDER_SUB ||
b == FCORDER_EO);
ExtendedAttribute ea = screen.getCell(baddr);
ea.fa = 0; // Destroy previous field attribute if any
if (isFCOrder) {
ea.ec = 0; // Display as null/blank
} else {
ea.ec = (byte) b;
}
ea.fg = currentFg;
ea.bg = currentBg;
ea.gr = (byte) currentGr;
ea.cs = currentCs;
baddr = (baddr + 1) % size;
screen.setBufferAddress(baddr);
pos += 1;
lastWasOrder = false;
break;
}
}
}
if (alarm) {
for (ScreenUpdateListener l : screenListeners) {
l.onSoundAlarm();
}
}
screen.updateFormatted();
}
private void applyExtendedAttribute(ExtendedAttribute ea, int type, int value) {
switch (type) {
case XA_3270:
ea.fa = (byte) (value & FA_MASK);
break;
case XA_FOREGROUND:
ea.fg = (byte) value;
break;
case XA_BACKGROUND:
ea.bg = (byte) value;
break;
case XA_HIGHLIGHTING:
if (value == XAH_DEFAULT || value == XAH_NORMAL)
ea.gr = 0;
else if (value == XAH_BLINK)
ea.gr = GR_BLINK;
else if (value == XAH_REVERSE)
ea.gr = GR_REVERSE;
else if (value == XAH_UNDERSCORE)
ea.gr = GR_UNDERLINE;
else if (value == XAH_INTENSIFY)
ea.gr = GR_INTENSIFY;
break;
case XA_CHARSET:
ea.cs = (byte) value;
break;
case XA_VALIDATION:
case XA_OUTLINING:
case XA_INPUT_CONTROL:
// Acknowledged but not visually rendered yet
break;
}
}
private void resetAllMDT() {
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
ea.fa = (byte) (ea.fa & ~FA_MODIFY);
}
}
}
// ========== Read Buffer ==========
private void processReadBuffer() {
outputPos = 0;
int size = screen.getRows() * screen.getCols();
// AID byte
outputWrite(AID_NO);
// Cursor address
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
outputWrite(caddr[0] & 0xFF);
outputWrite(caddr[1] & 0xFF);
// Buffer contents
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute()) {
outputWrite(ORDER_SF);
outputWrite(ea.fa & 0xFF);
} else {
outputWrite(ea.ec & 0xFF);
}
}
sendOutput();
}
// ========== Read Modified ==========
private void processReadModified(boolean all) {
outputPos = 0;
int aid = AID_NO; // Last AID
int size = screen.getRows() * screen.getCols();
// AID byte
outputWrite(aid);
// Cursor address
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
outputWrite(caddr[0] & 0xFF);
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);
}
}
} else {
// Formatted: send modified fields
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
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;
}
}
}
}
sendOutput();
}
// ========== Write Structured Field ==========
private void processWriteStructuredField(byte[] data, int offset, int length) {
int pos = offset + 1; // Skip WSF command byte
int end = offset + length;
while (pos < end) {
// Field length (2 bytes)
if (pos + 2 > end)
break;
int fieldLen = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
if (fieldLen == 0)
fieldLen = end - pos;
if (fieldLen < 3 || pos + fieldLen > end)
break;
int sfId = data[pos + 2] & 0xFF;
log.fine("SF id=" + String.format("0x%02x", sfId) + " len=" + fieldLen);
switch (sfId) {
case SF_READ_PART:
processSFReadPartition(data, pos, fieldLen);
break;
case SF_ERASE_RESET:
if (fieldLen >= 4) {
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT;
screen.erase(alt);
notifyScreenSizeChanged();
}
break;
case SF_SET_REPLY_MODE:
if (fieldLen >= 5) {
screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
}
break;
case SF_CREATE_PART:
// Acknowledged — we use implicit partition
break;
case SF_OUTBOUND_DS:
if (fieldLen > 5) {
// Outbound DS contains another 3270 command
processRecord(data, pos + 4, fieldLen - 4, false);
}
break;
default:
log.fine("Unknown SF id: " + String.format("0x%02x", sfId));
break;
}
pos += fieldLen;
}
}
private void processSFReadPartition(byte[] data, int offset, int fieldLen) {
if (fieldLen < 5)
return;
int partition = data[offset + 3] & 0xFF;
int type = data[offset + 4] & 0xFF;
// Log the incoming ReadPartition request
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fieldLen && i < 32; i++) {
sb.append(String.format("%02x ", data[offset + i] & 0xFF));
}
log.info("ReadPartition raw: " + sb.toString().trim() +
" partition=" + String.format("0x%02x", partition) +
" type=" + String.format("0x%02x", type));
switch (type) {
case SF_RP_QUERY:
log.info("ReadPartition Query — sending all query replies");
sendAllQueryReplies();
break;
case SF_RP_QLIST:
if (fieldLen >= 6) {
int listType = data[offset + 5] & 0xFF;
log.info("ReadPartition QueryList type=" + String.format("0x%02x", listType));
if (listType == SF_RPQ_ALL || listType == SF_RPQ_EQUIV) {
sendAllQueryReplies();
} else if (listType == SF_RPQ_LIST) {
// Send only requested query replies
byte[] requestedCodes = new byte[fieldLen - 6];
System.arraycopy(data, offset + 6, requestedCodes, 0, requestedCodes.length);
StringBuilder reqSb = new StringBuilder();
for (byte c : requestedCodes) {
reqSb.append(String.format("%02x ", c & 0xFF));
}
log.info("Requested QR codes: " + reqSb.toString().trim());
sendRequestedQueryReplies(requestedCodes);
}
} else {
sendAllQueryReplies();
}
break;
case SNA_CMD_RMA:
processReadModified(true);
break;
case SNA_CMD_RB:
processReadBuffer();
break;
case SNA_CMD_RM:
processReadModified(false);
break;
default:
log.fine("Unknown ReadPartition type: " + String.format("0x%02x", type));
break;
}
}
private void sendAllQueryReplies() {
byte[] qr = qrBuilder.buildAllQueryReplies(screen.getMaxCols(), screen.getMaxRows(),
screen.getMaxCols() * screen.getMaxRows());
// Hex dump the query reply for debugging
StringBuilder sb = new StringBuilder();
for (int i = 0; i < qr.length; i++) {
sb.append(String.format("%02x ", qr[i] & 0xFF));
if ((i + 1) % 32 == 0)
sb.append("\n ");
}
log.info("Query reply (" + qr.length + " bytes):\n " + sb.toString().trim());
if (outputSender != null) {
outputSender.send3270Data(qr);
}
}
private void sendRequestedQueryReplies(byte[] codes) {
// For now, send all — refinement can come later
sendAllQueryReplies();
}
// ========== Output helpers ==========
private void outputWrite(int b) {
if (outputPos >= outputBuffer.length) {
byte[] newBuf = new byte[outputBuffer.length * 2];
System.arraycopy(outputBuffer, 0, newBuf, 0, outputPos);
outputBuffer = newBuf;
}
outputBuffer[outputPos++] = (byte) b;
}
private void sendOutput() {
if (outputSender != null && outputPos > 0) {
byte[] data = new byte[outputPos];
System.arraycopy(outputBuffer, 0, data, 0, outputPos);
outputSender.send3270Data(data);
}
outputPos = 0;
}
private void notifyScreenSizeChanged() {
for (ScreenUpdateListener l : screenListeners) {
l.onScreenSizeChanged(screen.getRows(), screen.getCols());
}
}
}
@@ -0,0 +1,215 @@
package org.lib3270j.datastream;
import java.io.ByteArrayOutputStream;
import java.util.logging.Logger;
import org.lib3270j.screen.ScreenBuffer;
import static org.lib3270j.protocol.DS3270Constants.*;
/**
* Builds Query Reply structured fields in response to host Read Partition queries.
* Equivalent to the do_qr_* functions in sf.c.
*/
public class QueryReplyBuilder {
private static final Logger log = Logger.getLogger(QueryReplyBuilder.class.getName());
// Canned values from 3279-2 (matching sf.c)
private static final int SW_3279_2 = 0x09;
private static final int SH_3279_2 = 0x0c;
private static final int Xr_3279_2 = 0x000a02e5;
private static final int Yr_3279_2 = 0x0002006f;
private final ScreenBuffer screen;
// Supported query reply codes (must match what we send in summary)
private static final int[] SUPPORTED_QR = {
QR_SUMMARY, // 0x80 — summary must list itself
QR_USABLE_AREA, // 0x81
QR_ALPHA_PART, // 0x84
QR_CHARSETS, // 0x85
QR_COLOR, // 0x86
QR_HIGHLIGHTING, // 0x87
QR_REPLY_MODES, // 0x88
QR_IMP_PART, // 0xa6
};
public QueryReplyBuilder(ScreenBuffer screen) {
this.screen = screen;
}
/**
* Build all query replies as a single AID_SF + structured field response.
*/
public byte[] buildAllQueryReplies(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(512);
// AID byte for structured field
out.write(AID_SF);
// Summary
appendQueryReply(out, QR_SUMMARY, buildSummary());
// Usable Area
appendQueryReply(out, QR_USABLE_AREA, buildUsableArea(maxCols, maxRows, bufferSize));
// Alpha Partitions
appendQueryReply(out, QR_ALPHA_PART, buildAlphaPartitions(maxRows));
// Character Sets
appendQueryReply(out, QR_CHARSETS, buildCharsets());
// Color
appendQueryReply(out, QR_COLOR, buildColor());
// Highlighting
appendQueryReply(out, QR_HIGHLIGHTING, buildHighlighting());
// Reply Modes
appendQueryReply(out, QR_REPLY_MODES, buildReplyModes());
// Implicit Partition
appendQueryReply(out, QR_IMP_PART, buildImplicitPartition(maxCols, maxRows));
log.info("Built " + out.size() + " bytes of query replies");
return out.toByteArray();
}
private void appendQueryReply(ByteArrayOutputStream out, int code, byte[] data) {
// Length includes the 2-byte length field + SFID_QREPLY + code + data
int len = 4 + data.length;
out.write((len >> 8) & 0xFF);
out.write(len & 0xFF);
out.write(SFID_QREPLY);
out.write(code);
out.write(data, 0, data.length);
}
private byte[] buildSummary() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int code : SUPPORTED_QR) {
out.write(code);
}
return out.toByteArray();
}
private byte[] buildUsableArea(int maxCols, int maxRows, int bufferSize) {
ByteArrayOutputStream out = new ByteArrayOutputStream(19);
out.write(0x01); // 12/14-bit addressing
out.write(0x00); // no special character features
out.write((maxCols >> 8) & 0xFF); // usable width high
out.write(maxCols & 0xFF); // usable width low
out.write((maxRows >> 8) & 0xFF); // usable height high
out.write(maxRows & 0xFF); // usable height low
out.write(0x01); // units (mm)
// Xr (4 bytes) - canned from 3279-2
out.write((Xr_3279_2 >> 24) & 0xFF);
out.write((Xr_3279_2 >> 16) & 0xFF);
out.write((Xr_3279_2 >> 8) & 0xFF);
out.write(Xr_3279_2 & 0xFF);
// Yr (4 bytes) - canned from 3279-2
out.write((Yr_3279_2 >> 24) & 0xFF);
out.write((Yr_3279_2 >> 16) & 0xFF);
out.write((Yr_3279_2 >> 8) & 0xFF);
out.write(Yr_3279_2 & 0xFF);
out.write(SW_3279_2); // AW
out.write(SH_3279_2); // AH
int buf = maxCols * maxRows;
out.write((buf >> 8) & 0xFF); // buffer size high
out.write(buf & 0xFF); // buffer size low
return out.toByteArray();
}
private byte[] buildAlphaPartitions(int maxRows) {
int bufSize = screen.getMaxCols() * screen.getMaxRows();
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
out.write(0x00); // max partitions (1 partition)
out.write((bufSize >> 8) & 0xFF); // total partition storage high
out.write(bufSize & 0xFF); // total partition storage low
out.write(0x00); // no special features
return out.toByteArray();
}
private byte[] buildCharsets() {
ByteArrayOutputStream out = new ByteArrayOutputStream(32);
out.write(0x82); // flags: GE, CGCSGID present
out.write(0x00); // more flags
out.write(SW_3279_2); // SDW - default char width
out.write(SH_3279_2); // SDH - default char height
out.write(0x00); // Load PS format types supported: none
out.write(0x00); // Load PS device type (high)
out.write(0x00); // Load PS device type (low)
out.write(0x00); // reserved
out.write(0x07); // DL = 7 bytes per descriptor (non-DBCS)
// Descriptor 1 (SET 0): default character set
out.write(0x00); // SET 0
out.write(0x10); // FLAGS: non-loadable, single-plane, single-byte, no compare
out.write(0x00); // LCID 0
out.write(0x02); // CGCSGID (4 bytes) = 0x02b90025 (CGEN|CSET)
out.write(0xb9);
out.write(0x00);
out.write(0x25);
// Descriptor 2 (SET 1): APL/GE character set
out.write(0x01); // SET 1
out.write(0x00); // FLAGS: non-loadable, single-plane, single-byte, no compare
out.write(0xf1); // LCID 0xf1
out.write(0x03); // CGCSGID: 3179-style APL2 = 0x03c30136
out.write(0xc3);
out.write(0x01);
out.write(0x36);
return out.toByteArray();
}
private byte[] buildColor() {
ByteArrayOutputStream out = new ByteArrayOutputStream(36);
int colorMax = 16;
out.write(0x00); // no options
out.write(colorMax); // number of colors
out.write(0x00); // default color pair: attribute
out.write(0xf0 + HOST_COLOR_GREEN); // default color: green
for (int i = 0xf1; i < 0xf1 + colorMax - 1; i++) {
out.write(i); // color attribute value
out.write(i); // maps to itself (color mode)
}
return out.toByteArray();
}
private byte[] buildHighlighting() {
ByteArrayOutputStream out = new ByteArrayOutputStream(11);
out.write(5); // 5 pairs
out.write(XAH_DEFAULT); out.write(XAH_NORMAL);
out.write(XAH_BLINK); out.write(XAH_BLINK);
out.write(XAH_REVERSE); out.write(XAH_REVERSE);
out.write(XAH_UNDERSCORE); out.write(XAH_UNDERSCORE);
out.write(XAH_INTENSIFY); out.write(XAH_INTENSIFY);
return out.toByteArray();
}
private byte[] buildReplyModes() {
return new byte[] { SF_SRM_FIELD, SF_SRM_XFIELD, SF_SRM_CHAR };
}
private byte[] buildImplicitPartition(int maxCols, int maxRows) {
ByteArrayOutputStream out = new ByteArrayOutputStream(22);
// Implicit partition sizes, 2 self-defining parameters
// SDP 1: Default screen size
out.write(0x00); // flags
out.write(0x00); // reserved
out.write(0x0b); // SDP length
out.write(0x01); // SDP type: implicit partition sizes
out.write(0x00); // reserved
// Default
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
out.write((maxCols >> 8) & 0xFF);
out.write(maxCols & 0xFF);
out.write((maxRows >> 8) & 0xFF);
out.write(maxRows & 0xFF);
return out.toByteArray();
}
}
@@ -0,0 +1,322 @@
package org.lib3270j.input;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.screen.ExtendedAttribute;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.telnet.TelnetFSM;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.ByteArrayOutputStream;
import java.util.logging.Logger;
/**
* Handles keyboard input and generates 3270 outbound data streams.
* Manages cursor positioning, character entry, and AID key handling.
*/
public class InputProcessor {
private static final Logger log = Logger.getLogger(InputProcessor.class.getName());
private final ScreenBuffer screen;
private final EbcdicTranslator translator;
private final TelnetFSM fsm;
private int lastAid = AID_NO;
private boolean keyboardLocked;
private boolean insertMode;
public InputProcessor(ScreenBuffer screen, EbcdicTranslator translator, TelnetFSM fsm) {
this.screen = screen;
this.translator = translator;
this.fsm = fsm;
}
public boolean isKeyboardLocked() { return keyboardLocked; }
public void setKeyboardLocked(boolean locked) { this.keyboardLocked = locked; }
public boolean isInsertMode() { return insertMode; }
public void setInsertMode(boolean insert) { this.insertMode = insert; }
/**
* Enter a character at the current cursor position.
*/
public void typeCharacter(char ch) {
if (keyboardLocked) return;
int baddr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
// Check if cursor is at a field attribute or in a protected field
ExtendedAttribute ea = screen.getCell(baddr);
if (ea.isFieldAttribute()) {
// Move to next position
baddr = (baddr + 1) % size;
ea = screen.getCell(baddr);
}
byte faVal = screen.getFieldAttributeAt(baddr);
if (faIsProtected(faVal & 0xFF)) {
// Protected field — can't type here
return;
}
// Translate character to EBCDIC
int ebc = translator.unicodeToEbcdic(ch);
if (ebc < 0) return;
if (insertMode) {
// Insert mode: shift characters right from cursor to end of field
// Find end of field
int endAddr = baddr;
while (!screen.getCell(screen.incrementAddress(endAddr)).isFieldAttribute()) {
endAddr = screen.incrementAddress(endAddr);
if (endAddr == baddr) break; // wrapped around (unformatted)
}
// Check if last position is non-null (field overflow)
if (screen.getCell(endAddr).ec != 0) {
// Field overflow — can't insert
return;
}
// Shift right from endAddr-1 down to baddr
int dst = endAddr;
while (dst != baddr) {
int src = screen.decrementAddress(dst);
screen.getCell(dst).ec = screen.getCell(src).ec;
screen.getCell(dst).ucs4 = screen.getCell(src).ucs4;
dst = src;
}
}
// Write character — preserve existing fg/bg/gr/cs attributes
// so the character inherits the field's color scheme
ea = screen.getCell(baddr);
ea.ec = (byte) ebc;
ea.ucs4 = ch;
// Set MDT on field attribute
int faAddr = screen.findFieldAttribute(baddr);
if (faAddr >= 0) {
ExtendedAttribute faEa = screen.getCell(faAddr);
faEa.fa = (byte) (faEa.fa | FA_MODIFY);
}
// Advance cursor
baddr = (baddr + 1) % size;
// Skip over field attributes
while (screen.getCell(baddr).isFieldAttribute()) {
baddr = (baddr + 1) % size;
}
screen.setCursorAddress(baddr);
screen.markAllChanged();
}
/**
* Send an AID key (Enter, PF1-24, PA1-3, Clear).
*/
public void sendAid(int aidCode) {
if (keyboardLocked && aidCode != AID_CLEAR) return;
lastAid = aidCode;
keyboardLocked = true;
if (aidCode == AID_CLEAR) {
screen.clear();
screen.markAllChanged();
// Send just the AID
byte[] data = new byte[] { (byte) aidCode };
sendAidResponse(data);
return;
}
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) {
// PA keys: send AID + cursor address only (no modified data)
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] };
sendAidResponse(data);
return;
}
// Enter, PF keys: send AID + cursor address + modified field data
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
if (screen.isFormatted()) {
// Send modified fields with SBA
// Per 3270 Data Stream Architecture: strip trailing NULLs (0x00) from field data
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
if (faIsProtected(ea.fa & 0xFF)) continue;
int fieldStart = (i + 1) % size;
// First, 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;
}
// Only send if there's actual data (strip trailing nulls)
if (lastNonNull >= 0) {
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
// Write only up to the last non-null byte
byte[] allData = fieldData.toByteArray();
out.write(allData, 0, lastNonNull + 1);
}
}
}
} else {
// Unformatted screen: send all data
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
out.write(screen.getCell(i).ec & 0xFF);
}
}
sendAidResponse(out.toByteArray());
}
private void sendAidResponse(byte[] data) {
if (fsm.getConnectionState().isSscp()) {
fsm.sendSscpLuData(data);
} else {
fsm.send3270Data(data);
}
}
// ========== Cursor movement ==========
public void cursorUp() {
int addr = screen.getCursorAddress();
addr -= screen.getCols();
if (addr < 0) addr += screen.getRows() * screen.getCols();
screen.setCursorAddress(addr);
}
public void cursorDown() {
int addr = screen.getCursorAddress();
addr += screen.getCols();
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
screen.setCursorAddress(addr);
}
public void cursorLeft() {
int addr = screen.getCursorAddress();
addr = screen.decrementAddress(addr);
screen.setCursorAddress(addr);
}
public void cursorRight() {
int addr = screen.getCursorAddress();
addr = screen.incrementAddress(addr);
screen.setCursorAddress(addr);
}
public void cursorHome() {
if (screen.isFormatted()) {
screen.setCursorAddress(screen.findNextUnprotected(0));
} else {
screen.setCursorAddress(0);
}
}
public void tab() {
int addr = screen.findNextUnprotected(screen.getCursorAddress());
screen.setCursorAddress(addr);
}
public void backTab() {
// Find previous unprotected field
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
int start = screen.decrementAddress(addr);
addr = start;
do {
addr = screen.decrementAddress(addr);
if (screen.getCell(addr).isFieldAttribute()) {
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
screen.setCursorAddress(screen.incrementAddress(addr));
return;
}
}
} while (addr != start);
}
public void eraseEof() {
int addr = screen.getCursorAddress();
int size = screen.getRows() * screen.getCols();
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
// Erase from cursor to end of field
while (!screen.getCell(addr).isFieldAttribute()) {
ExtendedAttribute ea = screen.getCell(addr);
ea.ec = 0;
ea.ucs4 = 0;
addr = screen.incrementAddress(addr);
}
// Set MDT
int faAddr = screen.findFieldAttribute(screen.getCursorAddress());
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
}
public void deleteChar() {
int addr = screen.getCursorAddress();
byte faVal = screen.getFieldAttributeAt(addr);
if (faIsProtected(faVal & 0xFF)) return;
// Shift characters left within the field
int shiftAddr = addr;
int size = screen.getRows() * screen.getCols();
while (true) {
int next = screen.incrementAddress(shiftAddr);
if (screen.getCell(next).isFieldAttribute()) {
screen.getCell(shiftAddr).ec = 0;
screen.getCell(shiftAddr).ucs4 = 0;
break;
}
screen.getCell(shiftAddr).ec = screen.getCell(next).ec;
screen.getCell(shiftAddr).ucs4 = screen.getCell(next).ucs4;
shiftAddr = next;
}
int faAddr = screen.findFieldAttribute(addr);
if (faAddr >= 0) {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
}
screen.markAllChanged();
}
public void backspace() {
if (screen.getCursorAddress() == 0) return;
cursorLeft();
deleteChar();
}
/** Reset (unlock keyboard, cancel insert mode). */
public void reset() {
keyboardLocked = false;
insertMode = false;
}
}
@@ -0,0 +1,17 @@
package org.lib3270j.listener;
import org.lib3270j.ConnectionState;
/**
* Listener for connection state changes and errors.
*/
public interface ConnectionListener {
/** Called when the connection state changes. */
void onConnectionStateChanged(ConnectionState oldState, ConnectionState newState);
/** Called when a connection error occurs. */
void onConnectionError(String message);
/** Called when TN3270E negotiation completes. */
default void onTN3270ENegotiated(String deviceType, String deviceName) {}
}
@@ -0,0 +1,18 @@
package org.lib3270j.listener;
/**
* Listener for screen buffer changes.
*/
public interface ScreenUpdateListener {
/** Called when the screen buffer has been updated. */
void onScreenUpdated();
/** Called when the cursor position changes. */
default void onCursorMoved(int oldAddress, int newAddress) {}
/** Called when the host sends a sound alarm (WCC bit). */
default void onSoundAlarm() {}
/** Called when the screen size changes (erase/write vs erase/write alternate). */
default void onScreenSizeChanged(int rows, int cols) {}
}
@@ -0,0 +1,376 @@
package org.lib3270j.protocol;
/**
* 3270 Data Stream protocol constants.
* Derived from 3270ds.h in x3270.
*/
public final class DS3270Constants {
private DS3270Constants() {}
// ========== 3270 Commands ==========
public static final int CMD_W = 0x01; // Write
public static final int CMD_RB = 0x02; // Read Buffer
public static final int CMD_NOP = 0x03; // No-Op
public static final int CMD_EW = 0x05; // Erase/Write
public static final int CMD_RM = 0x06; // Read Modified
public static final int CMD_EWA = 0x0d; // Erase/Write Alternate
public static final int CMD_RMA = 0x0e; // Read Modified All
public static final int CMD_EAU = 0x0f; // Erase All Unprotected
public static final int CMD_WSF = 0x11; // Write Structured Field
// SNA 3270 Commands
public static final int SNA_CMD_RMA = 0x6e; // Read Modified All
public static final int SNA_CMD_EAU = 0x6f; // Erase All Unprotected
public static final int SNA_CMD_EWA = 0x7e; // Erase/Write Alternate
public static final int SNA_CMD_W = 0xf1; // Write
public static final int SNA_CMD_RB = 0xf2; // Read Buffer
public static final int SNA_CMD_WSF = 0xf3; // Write Structured Field
public static final int SNA_CMD_EW = 0xf5; // Erase/Write
public static final int SNA_CMD_RM = 0xf6; // Read Modified
// ========== 3270 Orders ==========
public static final int ORDER_PT = 0x05; // Program Tab
public static final int ORDER_GE = 0x08; // Graphic Escape
public static final int ORDER_SBA = 0x11; // Set Buffer Address
public static final int ORDER_EUA = 0x12; // Erase Unprotected to Address
public static final int ORDER_IC = 0x13; // Insert Cursor
public static final int ORDER_SF = 0x1d; // Start Field
public static final int ORDER_SA = 0x28; // Set Attribute
public static final int ORDER_SFE = 0x29; // Start Field Extended
public static final int ORDER_YALE = 0x2b; // Yale sub command
public static final int ORDER_MF = 0x2c; // Modify Field
public static final int ORDER_RA = 0x3c; // Repeat to Address
// Format control orders
public static final int FCORDER_NULL = 0x00;
public static final int FCORDER_FF = 0x0c; // Form feed
public static final int FCORDER_CR = 0x0d; // Carriage return
public static final int FCORDER_SO = 0x0e; // Shift out (DBCS start)
public static final int FCORDER_SI = 0x0f; // Shift in (DBCS end)
public static final int FCORDER_NL = 0x15; // New line
public static final int FCORDER_EM = 0x19; // End of medium
public static final int FCORDER_LF = 0x25; // Line feed
public static final int FCORDER_DUP = 0x1c; // Duplicate
public static final int FCORDER_FM = 0x1e; // Field mark
public static final int FCORDER_SUB = 0x3f; // Substitute
public static final int FCORDER_EO = 0xff; // Eight ones
// ========== Field Attributes ==========
public static final int FA_PRINTABLE = 0xc0;
public static final int FA_PROTECT = 0x20; // Protected (1) / Unprotected (0)
public static final int FA_NUMERIC = 0x10; // Numeric (1) / Alphanumeric (0)
public static final int FA_INTENSITY = 0x0c; // Display/selector pen mask
public static final int FA_INT_NORM_NSEL = 0x00; // Normal, non-detect
public static final int FA_INT_NORM_SEL = 0x04; // Normal, detectable
public static final int FA_INT_HIGH_SEL = 0x08; // Intensified, detectable
public static final int FA_INT_ZERO_NSEL = 0x0c; // Non-display, non-detect
public static final int FA_RESERVED = 0x02;
public static final int FA_MODIFY = 0x01; // Modified
public static final int FA_MASK = FA_PRINTABLE | FA_PROTECT | FA_NUMERIC | FA_INTENSITY | FA_MODIFY;
public static boolean faIsModified(int fa) { return (fa & FA_MODIFY) != 0; }
public static boolean faIsNumeric(int fa) { return (fa & FA_NUMERIC) != 0; }
public static boolean faIsProtected(int fa) { return (fa & FA_PROTECT) != 0; }
public static boolean faIsSkip(int fa) { return (fa & FA_PROTECT) != 0 && (fa & FA_NUMERIC) != 0; }
public static boolean faIsZero(int fa) { return (fa & FA_INTENSITY) == FA_INT_ZERO_NSEL; }
public static boolean faIsHigh(int fa) { return (fa & FA_INTENSITY) == FA_INT_HIGH_SEL; }
public static boolean faIsNormal(int fa) {
return (fa & FA_INTENSITY) == FA_INT_NORM_NSEL || (fa & FA_INTENSITY) == FA_INT_NORM_SEL;
}
public static boolean faIsSelectable(int fa) {
return (fa & FA_INTENSITY) == FA_INT_NORM_SEL || (fa & FA_INTENSITY) == FA_INT_HIGH_SEL;
}
// ========== Extended Attributes ==========
public static final int XA_ALL = 0x00;
public static final int XA_3270 = 0xc0;
public static final int XA_VALIDATION = 0xc1;
public static final int XA_OUTLINING = 0xc2;
public static final int XA_HIGHLIGHTING = 0x41;
public static final int XA_FOREGROUND = 0x42;
public static final int XA_CHARSET = 0x43;
public static final int XA_BACKGROUND = 0x45;
public static final int XA_TRANSPARENCY = 0x46;
public static final int XA_INPUT_CONTROL = 0xfe;
// Highlighting values
public static final int XAH_DEFAULT = 0x00;
public static final int XAH_NORMAL = 0xf0;
public static final int XAH_BLINK = 0xf1;
public static final int XAH_REVERSE = 0xf2;
public static final int XAH_UNDERSCORE = 0xf4;
public static final int XAH_INTENSIFY = 0xf8;
// Default color
public static final int XAC_DEFAULT = 0x00;
// Outlining values
public static final int XAO_UNDERLINE = 0x01;
public static final int XAO_RIGHT = 0x02;
public static final int XAO_OVERLINE = 0x04;
public static final int XAO_LEFT = 0x08;
// Validation values
public static final int XAV_FILL = 0x04;
public static final int XAV_ENTRY = 0x02;
public static final int XAV_TRIGGER = 0x01;
// Transparency values
public static final int XAT_DEFAULT = 0x00;
public static final int XAT_OR = 0xf0;
public static final int XAT_XOR = 0xf1;
public static final int XAT_OPAQUE = 0xff;
// Input control
public static final int XAI_DISABLED = 0x00;
public static final int XAI_ENABLED = 0x01;
// ========== WCC (Write Control Character) ==========
public static final int WCC_RESET_BIT = 0x40;
public static final int WCC_START_PRINTER_BIT = 0x08;
public static final int WCC_SOUND_ALARM_BIT = 0x04;
public static final int WCC_KEYBOARD_RESTORE_BIT = 0x02;
public static final int WCC_RESET_MDT_BIT = 0x01;
public static boolean wccReset(int wcc) { return (wcc & WCC_RESET_BIT) != 0; }
public static boolean wccStartPrinter(int wcc) { return (wcc & WCC_START_PRINTER_BIT) != 0; }
public static boolean wccSoundAlarm(int wcc) { return (wcc & WCC_SOUND_ALARM_BIT) != 0; }
public static boolean wccKeyboardRestore(int wcc) { return (wcc & WCC_KEYBOARD_RESTORE_BIT) != 0; }
public static boolean wccResetMDT(int wcc) { return (wcc & WCC_RESET_MDT_BIT) != 0; }
// ========== AID (Attention Identifier) ==========
public static final int AID_NO = 0x60;
public static final int AID_QREPLY = 0x61;
public static final int AID_ENTER = 0x7d;
public static final int AID_PF1 = 0xf1;
public static final int AID_PF2 = 0xf2;
public static final int AID_PF3 = 0xf3;
public static final int AID_PF4 = 0xf4;
public static final int AID_PF5 = 0xf5;
public static final int AID_PF6 = 0xf6;
public static final int AID_PF7 = 0xf7;
public static final int AID_PF8 = 0xf8;
public static final int AID_PF9 = 0xf9;
public static final int AID_PF10 = 0x7a;
public static final int AID_PF11 = 0x7b;
public static final int AID_PF12 = 0x7c;
public static final int AID_PF13 = 0xc1;
public static final int AID_PF14 = 0xc2;
public static final int AID_PF15 = 0xc3;
public static final int AID_PF16 = 0xc4;
public static final int AID_PF17 = 0xc5;
public static final int AID_PF18 = 0xc6;
public static final int AID_PF19 = 0xc7;
public static final int AID_PF20 = 0xc8;
public static final int AID_PF21 = 0xc9;
public static final int AID_PF22 = 0x4a;
public static final int AID_PF23 = 0x4b;
public static final int AID_PF24 = 0x4c;
public static final int AID_OICR = 0xe6;
public static final int AID_MSR_MHS = 0xe7;
public static final int AID_SELECT = 0x7e;
public static final int AID_PA1 = 0x6c;
public static final int AID_PA2 = 0x6e;
public static final int AID_PA3 = 0x6b;
public static final int AID_CLEAR = 0x6d;
public static final int AID_SYSREQ = 0xf0;
public static final int AID_SF = 0x88;
public static final int SFID_QREPLY = 0x81;
// ========== Structured Field IDs ==========
public static final int SF_READ_PART = 0x01;
public static final int SF_RP_QUERY = 0x02;
public static final int SF_RP_QLIST = 0x03;
public static final int SF_RPQ_LIST = 0x00;
public static final int SF_RPQ_EQUIV = 0x40;
public static final int SF_RPQ_ALL = 0x80;
public static final int SF_ERASE_RESET = 0x03;
public static final int SF_ER_DEFAULT = 0x00;
public static final int SF_ER_ALT = 0x80;
public static final int SF_SET_REPLY_MODE = 0x09;
public static final int SF_SRM_FIELD = 0x00;
public static final int SF_SRM_XFIELD = 0x01;
public static final int SF_SRM_CHAR = 0x02;
public static final int SF_CREATE_PART = 0x0c;
public static final int SF_OUTBOUND_DS = 0x40;
public static final int SF_TRANSFER_DATA = 0xd0;
// ========== Query Reply codes ==========
public static final int QR_SUMMARY = 0x80;
public static final int QR_USABLE_AREA = 0x81;
public static final int QR_IMAGE = 0x82;
public static final int QR_TEXT_PART = 0x83;
public static final int QR_ALPHA_PART = 0x84;
public static final int QR_CHARSETS = 0x85;
public static final int QR_COLOR = 0x86;
public static final int QR_HIGHLIGHTING = 0x87;
public static final int QR_REPLY_MODES = 0x88;
public static final int QR_DBCS_ASIA = 0x91;
public static final int QR_DDM = 0x95;
public static final int QR_RPQNAMES = 0xa1;
public static final int QR_IMP_PART = 0xa6;
public static final int QR_NULL = 0xff;
// ========== Screen model sizes ==========
public static final int MODEL_2_ROWS = 24;
public static final int MODEL_2_COLS = 80;
public static final int MODEL_3_ROWS = 32;
public static final int MODEL_3_COLS = 80;
public static final int MODEL_4_ROWS = 43;
public static final int MODEL_4_COLS = 80;
public static final int MODEL_5_ROWS = 27;
public static final int MODEL_5_COLS = 132;
public static final int MAX_ROWS_COLS = 0x3fff;
// ========== Host colors ==========
public static final int HOST_COLOR_NEUTRAL_BLACK = 0;
public static final int HOST_COLOR_BLUE = 1;
public static final int HOST_COLOR_RED = 2;
public static final int HOST_COLOR_PINK = 3;
public static final int HOST_COLOR_GREEN = 4;
public static final int HOST_COLOR_TURQUOISE = 5;
public static final int HOST_COLOR_YELLOW = 6;
public static final int HOST_COLOR_NEUTRAL_WHITE = 7;
public static final int HOST_COLOR_BLACK = 8;
public static final int HOST_COLOR_DEEP_BLUE = 9;
public static final int HOST_COLOR_ORANGE = 10;
public static final int HOST_COLOR_PURPLE = 11;
public static final int HOST_COLOR_PALE_GREEN = 12;
public static final int HOST_COLOR_PALE_TURQUOISE = 13;
public static final int HOST_COLOR_GREY = 14;
public static final int HOST_COLOR_WHITE = 15;
// ========== Graphics rendition bits ==========
public static final int GR_BLINK = 0x01;
public static final int GR_REVERSE = 0x02;
public static final int GR_UNDERLINE = 0x04;
public static final int GR_INTENSIFY = 0x08;
// ========== Character set codes ==========
public static final int CS_MASK = 0x03;
public static final int CS_BASE = 0x00;
public static final int CS_APL = 0x01;
public static final int CS_LINEDRAW = 0x02;
public static final int CS_DBCS = 0x03;
public static final int CS_GE = 0x04;
// ========== BIND definitions ==========
public static final int BIND_RU = 0x31;
public static final int BIND_OFF_MAXRU_SEC = 10;
public static final int BIND_OFF_MAXRU_PRI = 11;
public static final int BIND_OFF_RD = 20;
public static final int BIND_OFF_CD = 21;
public static final int BIND_OFF_RA = 22;
public static final int BIND_OFF_CA = 23;
public static final int BIND_OFF_SSIZE = 24;
public static final int BIND_OFF_PLU_NAME_LEN = 27;
public static final int BIND_PLU_NAME_MAX = 8;
public static final int BIND_OFF_PLU_NAME = 28;
// BIND dimension flags
public static final int BIND_DIMS_PRESENT = 0x1;
public static final int BIND_DIMS_ALT = 0x2;
public static final int BIND_DIMS_VALID = 0x4;
// ========== EBCDIC common characters ==========
public static final int EBC_NULL = 0x00;
public static final int EBC_SPACE = 0x40;
public static final int EBC_AMPERSAND = 0x50;
public static final int EBC_MINUS = 0x60;
public static final int EBC_PERIOD = 0x4b;
public static final int EBC_COMMA = 0x6b;
public static final int EBC_DUP = 0x1c;
public static final int EBC_FM = 0x1e;
public static final int EBC_FF = 0x0c;
public static final int EBC_CR = 0x0d;
public static final int EBC_NL = 0x15;
public static final int EBC_EM = 0x19;
public static final int EBC_SUB = 0x3f;
public static final int EBC_EO = 0xff;
/**
* 6-bit code table for 12-bit buffer address encoding.
*/
public static final int[] CODE_TABLE = {
0x40, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
0xC8, 0xC9, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
0xD8, 0xD9, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
0x60, 0x61, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
0xE8, 0xE9, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7,
0xF8, 0xF9, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
};
/**
* Decode a 2-byte buffer address.
* Handles both 12-bit and 14-bit addressing.
*/
public static int decodeAddress(int b1, int b2) {
if ((b1 & 0xC0) == 0x00) {
// 14-bit format
return ((b1 & 0x3F) << 8) | b2;
} else {
// 12-bit format
return ((b1 & 0x3F) << 6) | (b2 & 0x3F);
}
}
/**
* Encode a buffer address into a 2-byte array.
* Uses 14-bit format if screen > 4096 positions, otherwise 12-bit.
*/
public static byte[] encodeAddress(int addr, int rows, int cols) {
byte[] result = new byte[2];
if (rows * cols > 0x1000) {
// 14-bit format
result[0] = (byte) ((addr >> 8) & 0x3F);
result[1] = (byte) (addr & 0xFF);
} else {
// 12-bit format
result[0] = (byte) CODE_TABLE[(addr >> 6) & 0x3F];
result[1] = (byte) CODE_TABLE[addr & 0x3F];
}
return result;
}
/** Check if a byte is a 3270 order. */
public static boolean isOrder(int b) {
switch (b) {
case ORDER_PT:
case ORDER_GE:
case ORDER_SBA:
case ORDER_EUA:
case ORDER_IC:
case ORDER_SF:
case ORDER_SA:
case ORDER_SFE:
case ORDER_YALE:
case ORDER_MF:
case ORDER_RA:
return true;
default:
return false;
}
}
/** Get human-readable command name. */
public static String commandName(int cmd) {
switch (cmd) {
case CMD_W: case SNA_CMD_W: return "Write";
case CMD_EW: case SNA_CMD_EW: return "EraseWrite";
case CMD_EWA: case SNA_CMD_EWA: return "EraseWriteAlternate";
case CMD_RB: case SNA_CMD_RB: return "ReadBuffer";
case CMD_RM: case SNA_CMD_RM: return "ReadModified";
case CMD_RMA: case SNA_CMD_RMA: return "ReadModifiedAll";
case CMD_EAU: case SNA_CMD_EAU: return "EraseAllUnprotected";
case CMD_WSF: case SNA_CMD_WSF: return "WriteStructuredField";
case CMD_NOP: return "NoOp";
default: return String.format("Unknown(0x%02x)", cmd);
}
}
}
@@ -0,0 +1,142 @@
package org.lib3270j.protocol;
/**
* TN3270E protocol constants per RFC 2355.
* Derived from tn3270e.h in x3270.
*/
public final class TN3270EConstants {
private TN3270EConstants() {}
// Negotiation operations
public static final int OP_ASSOCIATE = 0;
public static final int OP_CONNECT = 1;
public static final int OP_DEVICE_TYPE = 2;
public static final int OP_FUNCTIONS = 3;
public static final int OP_IS = 4;
public static final int OP_REASON = 5;
public static final int OP_REJECT = 6;
public static final int OP_REQUEST = 7;
public static final int OP_SEND = 8;
// Reason codes
public static final int REASON_CONN_PARTNER = 0;
public static final int REASON_DEVICE_IN_USE = 1;
public static final int REASON_INV_ASSOCIATE = 2;
public static final int REASON_INV_DEVICE_NAME = 3;
public static final int REASON_INV_DEVICE_TYPE = 4;
public static final int REASON_TYPE_NAME_ERROR = 5;
public static final int REASON_UNKNOWN_ERROR = 6;
public static final int REASON_UNSUPPORTED_REQ = 7;
// Function names
public static final int FUNC_BIND_IMAGE = 0;
public static final int FUNC_DATA_STREAM_CTL = 1;
public static final int FUNC_RESPONSES = 2;
public static final int FUNC_SCS_CTL_CODES = 3;
public static final int FUNC_SYSREQ = 4;
public static final int FUNC_CONTENTION_RESOLUTION = 5;
public static final int FUNC_SNA_SENSE = 6;
// Data type names
public static final int DT_3270_DATA = 0x00;
public static final int DT_SCS_DATA = 0x01;
public static final int DT_RESPONSE = 0x02;
public static final int DT_BIND_IMAGE = 0x03;
public static final int DT_UNBIND = 0x04;
public static final int DT_NVT_DATA = 0x05;
public static final int DT_REQUEST = 0x06;
public static final int DT_SSCP_LU_DATA = 0x07;
public static final int DT_PRINT_EOJ = 0x08;
public static final int DT_BID = 0x09;
// Request flags
public static final int RQF_ERR_COND_CLEARED = 0x00;
public static final int RQF_SEND_DATA = 0x01;
public static final int RQF_KEYBOARD_RESTORE = 0x02;
public static final int RQF_SIGNAL = 0x04;
// Response flags (header)
public static final int RSF_NO_RESPONSE = 0x00;
public static final int RSF_ERROR_RESPONSE = 0x01;
public static final int RSF_ALWAYS_RESPONSE = 0x02;
// Response flags (trailer)
public static final int RSF_POSITIVE_RESPONSE = 0x00;
public static final int RSF_NEGATIVE_RESPONSE = 0x01;
public static final int RSF_SNA_SENSE = 0x02;
// Positive response data
public static final int POS_DEVICE_END = 0x00;
// Negative response data
public static final int NEG_COMMAND_REJECT = 0x00;
public static final int NEG_INTERVENTION_REQUIRED = 0x01;
public static final int NEG_OPERATION_CHECK = 0x02;
public static final int NEG_COMPONENT_DISCONNECTED = 0x03;
// TN3270E header size
public static final int EH_SIZE = 5;
// UNBIND types
public static final int UNBIND_NORMAL = 0x01;
public static final int UNBIND_BIND_FORTHCOMING = 0x02;
public static final int UNBIND_VR_INOPERATIVE = 0x07;
public static final int UNBIND_RX_INOPERATIVE = 0x08;
public static final int UNBIND_HRESET = 0x09;
public static final int UNBIND_SSCP_GONE = 0x0a;
public static final int UNBIND_VR_DEACTIVATED = 0x0b;
public static final int UNBIND_LU_FAILURE_PERM = 0x0c;
public static final int UNBIND_LU_FAILURE_TEMP = 0x0e;
public static final int UNBIND_CLEANUP = 0x0f;
public static final int UNBIND_BAD_SENSE = 0xfe;
// Name lookups for tracing
private static final String[] REASON_NAMES = {
"CONN-PARTNER", "DEVICE-IN-USE", "INV-ASSOCIATE", "INV-NAME",
"INV-DEVICE-TYPE", "TYPE-NAME-ERROR", "UNKNOWN-ERROR", "UNSUPPORTED-REQ"
};
private static final String[] FUNCTION_NAMES = {
"BIND-IMAGE", "DATA-STREAM-CTL", "RESPONSES", "SCS-CTL-CODES",
"SYSREQ", "CONTENTION-RESOLUTION", "SNA-SENSE"
};
private static final String[] DATA_TYPE_NAMES = {
"3270-DATA", "SCS-DATA", "RESPONSE", "BIND-IMAGE", "UNBIND",
"NVT-DATA", "REQUEST", "SSCP-LU-DATA", "PRINT-EOJ", "BID"
};
private static final String[] HRSP_FLAG_NAMES = {
"NO-RESPONSE", "ERROR-RESPONSE", "ALWAYS-RESPONSE"
};
public static String reasonName(int code) {
return code >= 0 && code < REASON_NAMES.length ? REASON_NAMES[code] : "??";
}
public static String functionName(int code) {
return code >= 0 && code < FUNCTION_NAMES.length ? FUNCTION_NAMES[code] : "??";
}
public static String dataTypeName(int code) {
return code >= 0 && code < DATA_TYPE_NAMES.length ? DATA_TYPE_NAMES[code] : "??";
}
public static String responseHeaderFlagName(int code) {
return code >= 0 && code < HRSP_FLAG_NAMES.length ? HRSP_FLAG_NAMES[code] : "??";
}
/** Format a function set as a human-readable string. */
public static String functionNames(boolean[] funcs) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < funcs.length && i <= FUNC_SNA_SENSE; i++) {
if (funcs[i]) {
if (sb.length() > 0) sb.append(", ");
sb.append(functionName(i));
}
}
return sb.toString();
}
}
@@ -0,0 +1,117 @@
package org.lib3270j.protocol;
/**
* Telnet protocol constants from RFC 854 and extensions.
* Derived from arpa_telnet.h in x3270.
*/
public final class TelnetConstants {
private TelnetConstants() {}
// Telnet commands
public static final int IAC = 255; // Interpret As Command
public static final int DONT = 254; // You are not to use option
public static final int DO = 253; // Please, you use option
public static final int WONT = 252; // I won't use option
public static final int WILL = 251; // I will use option
public static final int SB = 250; // Interpret as subnegotiation
public static final int GA = 249; // You may reverse the line
public static final int EL = 248; // Erase the current line
public static final int EC = 247; // Erase the current character
public static final int AYT = 246; // Are you there
public static final int AO = 245; // Abort output
public static final int IP = 244; // Interrupt process
public static final int BREAK = 243; // Break
public static final int DM = 242; // Data mark
public static final int NOP = 241; // No operation
public static final int SE = 240; // End sub negotiation
public static final int EOR = 239; // End of record
public static final int SUSP = 237; // Suspend process
public static final int xEOF = 236; // End of file
// Telnet options
public static final int TELOPT_BINARY = 0; // 8-bit data path
public static final int TELOPT_ECHO = 1; // Echo
public static final int TELOPT_RCP = 2; // Prepare to reconnect
public static final int TELOPT_SGA = 3; // Suppress go ahead
public static final int TELOPT_NAMS = 4; // Approximate message size
public static final int TELOPT_STATUS = 5; // Give status
public static final int TELOPT_TM = 6; // Timing mark
public static final int TELOPT_TTYPE = 24; // Terminal type
public static final int TELOPT_EOR = 25; // End of record
public static final int TELOPT_NAWS = 31; // Window size
public static final int TELOPT_TSPEED = 32; // Terminal speed
public static final int TELOPT_LFLOW = 33; // Remote flow control
public static final int TELOPT_LINEMODE = 34; // Linemode option
public static final int TELOPT_XDISPLOC = 35; // X Display Location
public static final int TELOPT_OLD_ENVIRON = 36; // Old environment variables
public static final int TELOPT_AUTHENTICATION = 37; // Authenticate
public static final int TELOPT_ENCRYPT = 38; // Encryption option
public static final int TELOPT_NEW_ENVIRON = 39; // New environment variables
public static final int TELOPT_TN3270E = 40; // Extended 3270 regime
public static final int TELOPT_STARTTLS = 46; // Start TLS
public static final int TELOPT_EXOPL = 255; // Extended options list
// Sub-option qualifiers
public static final int TELQUAL_IS = 0; // Option is...
public static final int TELQUAL_SEND = 1; // Send option
public static final int TELQUAL_INFO = 2; // Info
// New-environ sub-option objects
public static final int TELOBJ_VAR = 0;
public static final int TELOBJ_VALUE = 1;
public static final int TELOBJ_ESC = 2;
public static final int TELOBJ_USERVAR = 3;
// STARTTLS sub-option
public static final int TLS_FOLLOWS = 1;
// Standard ports
public static final int TELNET_PORT = 23;
public static final int TELNETS_PORT = 992;
/** Telnet option name lookup for tracing. */
public static String optionName(int opt) {
switch (opt) {
case TELOPT_BINARY: return "BINARY";
case TELOPT_ECHO: return "ECHO";
case TELOPT_SGA: return "SGA";
case TELOPT_TTYPE: return "TTYPE";
case TELOPT_EOR: return "EOR";
case TELOPT_NAWS: return "NAWS";
case TELOPT_NEW_ENVIRON: return "NEW-ENVIRON";
case TELOPT_TN3270E: return "TN3270E";
case TELOPT_STARTTLS: return "STARTTLS";
case TELOPT_TSPEED: return "TSPEED";
case TELOPT_LFLOW: return "LFLOW";
case TELOPT_LINEMODE: return "LINEMODE";
case TELOPT_XDISPLOC: return "XDISPLOC";
case TELOPT_OLD_ENVIRON: return "OLD-ENVIRON";
default: return "OPT-" + opt;
}
}
/** Telnet command name lookup for tracing. */
public static String commandName(int cmd) {
switch (cmd) {
case IAC: return "IAC";
case DONT: return "DONT";
case DO: return "DO";
case WONT: return "WONT";
case WILL: return "WILL";
case SB: return "SB";
case GA: return "GA";
case EL: return "EL";
case EC: return "EC";
case AYT: return "AYT";
case AO: return "AO";
case IP: return "IP";
case BREAK: return "BRK";
case DM: return "DMARK";
case NOP: return "NOP";
case SE: return "SE";
case EOR: return "EOR";
default: return "CMD-" + cmd;
}
}
}
@@ -0,0 +1,77 @@
package org.lib3270j.screen;
/**
* Extended attribute structure for a single screen buffer position.
* Mirrors struct ea from globals.h in x3270.
*/
public class ExtendedAttribute {
/** EBCDIC character code at this position. */
public byte ec;
/** Field attribute byte (non-zero if this position IS a field attribute). */
public byte fa;
/** Foreground color (0x00 for default, or 0xf0-0xff for explicit). */
public byte fg;
/** Background color (0x00 for default, or 0xf0-0xff for explicit). */
public byte bg;
/**
* Graphics rendition bits.
* GR_BLINK=0x01, GR_REVERSE=0x02, GR_UNDERLINE=0x04, GR_INTENSIFY=0x08
*/
public byte gr;
/** Character set (CS_BASE=0, CS_APL=1, CS_LINEDRAW=2, CS_DBCS=3; CS_GE=0x04 flag). */
public byte cs;
/** Input control (DBCS). */
public byte ic;
/** DBCS state. */
public byte db;
/**
* Unicode character for display (set by translation from ec, or directly in NVT mode).
*/
public char ucs4;
/** Clear all attributes. */
public void clear() {
ec = 0;
fa = 0;
fg = 0;
bg = 0;
gr = 0;
cs = 0;
ic = 0;
db = 0;
ucs4 = 0;
}
/** Copy all values from another ExtendedAttribute. */
public void copyFrom(ExtendedAttribute other) {
this.ec = other.ec;
this.fa = other.fa;
this.fg = other.fg;
this.bg = other.bg;
this.gr = other.gr;
this.cs = other.cs;
this.ic = other.ic;
this.db = other.db;
this.ucs4 = other.ucs4;
}
/** Check if this position is a field attribute. */
public boolean isFieldAttribute() {
return fa != 0;
}
@Override
public String toString() {
return String.format("EA[ec=%02x fa=%02x fg=%02x bg=%02x gr=%02x cs=%02x u=%04x]",
ec & 0xFF, fa & 0xFF, fg & 0xFF, bg & 0xFF, gr & 0xFF, cs & 0xFF, (int) ucs4);
}
}
@@ -0,0 +1,329 @@
package org.lib3270j.screen;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.protocol.DS3270Constants.*;
/**
* The 3270 screen buffer.
* Manages the display buffer, cursor, field attributes, and screen dimensions.
* Equivalent to ea_buf[] / aea_buf[] and related state in ctlr.c.
*/
public class ScreenBuffer {
private ExtendedAttribute[] buffer; // Main screen buffer
private ExtendedAttribute[] altBuffer; // Alternate screen buffer
private final ExtendedAttribute defaultFA; // Default field attribute (ea_buf[-1])
private int maxRows, maxCols; // Maximum (alternate) dimensions
private int defRows, defCols; // Default dimensions (24x80)
private int altRows, altCols; // Alternate dimensions
private int rows, cols; // Current dimensions
private int cursorAddress;
private int bufferAddress;
private boolean screenAlt; // Using alternate screen?
private boolean formatted; // Screen has at least one field attribute?
private byte replyMode = SF_SRM_FIELD;
// Change tracking
private boolean screenChanged;
private int firstChanged = -1;
private int lastChanged = -1;
// Default attribute values
private byte defaultFg = 0x00;
private byte defaultBg = 0x00;
private byte defaultGr = 0x00;
private byte defaultCs = 0x00;
private byte defaultIc = 0x00;
private final EbcdicTranslator translator;
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
this.translator = translator;
this.defRows = MODEL_2_ROWS;
this.defCols = MODEL_2_COLS;
this.altRows = model.getAlternateRows();
this.altCols = model.getAlternateCols();
this.maxRows = altRows;
this.maxCols = altCols;
this.rows = defRows;
this.cols = defCols;
// Default field attribute (like ea_buf[-1])
defaultFA = new ExtendedAttribute();
defaultFA.fa = (byte) (FA_PRINTABLE | FA_MODIFY);
defaultFA.ic = 1;
allocateBuffers();
}
private void allocateBuffers() {
int size = maxRows * maxCols;
buffer = new ExtendedAttribute[size];
altBuffer = new ExtendedAttribute[size];
for (int i = 0; i < size; i++) {
buffer[i] = new ExtendedAttribute();
altBuffer[i] = new ExtendedAttribute();
}
cursorAddress = 0;
bufferAddress = 0;
}
/** Get the current screen buffer. */
public ExtendedAttribute[] getBuffer() { return buffer; }
/** Get a cell at the given buffer address. */
public ExtendedAttribute getCell(int addr) {
if (addr < 0 || addr >= rows * cols) return defaultFA;
return buffer[addr];
}
// ========== Dimension accessors ==========
public int getRows() { return rows; }
public int getCols() { return cols; }
public int getMaxRows() { return maxRows; }
public int getMaxCols() { return maxCols; }
public int getDefRows() { return defRows; }
public int getDefCols() { return defCols; }
public int getAltRows() { return altRows; }
public int getAltCols() { return altCols; }
public boolean isScreenAlt() { return screenAlt; }
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
public void setAlternateDimensions(int newAltRows, int newAltCols) {
if (newAltRows == altRows && newAltCols == altCols) return;
this.altRows = newAltRows;
this.altCols = newAltCols;
// maxRows/maxCols should be the larger of alt vs current max
if (newAltRows > maxRows || newAltCols > maxCols) {
this.maxRows = Math.max(maxRows, newAltRows);
this.maxCols = Math.max(maxCols, newAltCols);
allocateBuffers();
}
}
// ========== Cursor ==========
public int getCursorAddress() { return cursorAddress; }
public void setCursorAddress(int addr) { this.cursorAddress = addr; }
public int getCursorRow() { return cursorAddress / cols; }
public int getCursorCol() { return cursorAddress % cols; }
public int getBufferAddress() { return bufferAddress; }
public void setBufferAddress(int addr) { this.bufferAddress = addr; }
public byte getReplyMode() { return replyMode; }
public void setReplyMode(byte mode) { this.replyMode = mode; }
// ========== Screen erase ==========
/**
* Perform an erase, optionally using the alternate screen size.
*/
public void erase(boolean alt) {
clear();
int newRows = alt ? altRows : defRows;
int newCols = alt ? altCols : defCols;
if (alt == screenAlt && rows == newRows && cols == newCols) {
return;
}
rows = newRows;
cols = newCols;
screenAlt = alt;
}
/** Clear the entire buffer. */
public void clear() {
for (ExtendedAttribute ea : buffer) {
ea.clear();
}
cursorAddress = 0;
bufferAddress = 0;
formatted = false;
screenChanged = true;
defaultFg = 0x00;
defaultBg = 0x00;
defaultGr = 0x00;
defaultCs = 0x00;
defaultIc = 0x00;
replyMode = SF_SRM_FIELD;
}
/**
* Erase all unprotected fields.
*/
public void eraseAllUnprotected() {
int size = rows * cols;
boolean inUnprotected = false;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (ea.isFieldAttribute()) {
if (!faIsProtected(ea.fa & 0xFF)) {
inUnprotected = true;
// Clear modified bit
ea.fa = (byte) (ea.fa & ~FA_MODIFY);
} else {
inUnprotected = false;
}
} else if (inUnprotected) {
ea.ec = 0;
ea.ucs4 = 0;
ea.cs = 0;
ea.fg = 0;
ea.bg = 0;
ea.gr = 0;
ea.ic = 0;
}
}
// Move cursor to first unprotected field
cursorAddress = findNextUnprotected(0);
screenChanged = true;
}
// ========== Field attribute navigation ==========
/**
* Find the field attribute for a given buffer address.
* Returns -1 if screen is not formatted.
*/
public int findFieldAttribute(int baddr) {
if (!formatted) return -1;
int size = rows * cols;
int start = baddr;
do {
if (buffer[baddr].isFieldAttribute()) {
return baddr;
}
baddr = (baddr > 0) ? baddr - 1 : size - 1;
} while (baddr != start);
return -1;
}
/**
* Get the field attribute byte for a given position.
*/
public byte getFieldAttributeAt(int baddr) {
int fa_addr = findFieldAttribute(baddr);
if (fa_addr < 0) return defaultFA.fa;
return buffer[fa_addr].fa;
}
/**
* Find the next unprotected field after the given address.
* Returns 0 if none found.
*/
public int findNextUnprotected(int baddr) {
int size = rows * cols;
int start = baddr;
do {
int next = (baddr + 1) % size;
if (buffer[baddr].isFieldAttribute()
&& !faIsProtected(buffer[baddr].fa & 0xFF)
&& !buffer[next].isFieldAttribute()) {
return next;
}
baddr = next;
} while (baddr != start);
return 0;
}
/** Refresh the formatted flag by scanning for any field attributes. */
public void updateFormatted() {
formatted = false;
int size = rows * cols;
for (int i = 0; i < size; i++) {
if (buffer[i].isFieldAttribute()) {
formatted = true;
return;
}
}
}
public boolean isFormatted() { return formatted; }
public void setFormatted(boolean f) { this.formatted = f; }
// ========== Buffer address arithmetic ==========
/** Increment buffer address (wrapping). */
public int incrementAddress(int addr) {
return (addr + 1) % (rows * cols);
}
/** Decrement buffer address (wrapping). */
public int decrementAddress(int addr) {
return (addr > 0) ? addr - 1 : (rows * cols) - 1;
}
/** Convert buffer address to row. */
public int addressToRow(int addr) { return addr / cols; }
/** Convert buffer address to column. */
public int addressToCol(int addr) { return addr % cols; }
/** Convert row/col to buffer address. */
public int rowColToAddress(int row, int col) { return row * cols + col; }
// ========== Change tracking ==========
public boolean isScreenChanged() { return screenChanged; }
public void clearChanged() { screenChanged = false; firstChanged = -1; lastChanged = -1; }
public void markAllChanged() { screenChanged = true; }
// ========== Setters for model reconfiguration ==========
public void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
int altRows, int altCols) {
this.maxRows = maxRows;
this.maxCols = maxCols;
this.defRows = defRows;
this.defCols = defCols;
this.altRows = altRows;
this.altCols = altCols;
this.rows = defRows;
this.cols = defCols;
allocateBuffers();
}
public void translateToUnicode() {
int size = rows * cols;
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = buffer[i];
if (!ea.isFieldAttribute()) {
if (ea.ec == 0) {
ea.ucs4 = 0;
} else if (ea.cs == 1 || ea.cs == 0x04) { // CS_APL or CS_GE
ea.ucs4 = getAplGraphic(ea.ec & 0xFF);
} else {
ea.ucs4 = translator.ebcdicToUnicode(ea.ec & 0xFF);
}
}
}
}
private char getAplGraphic(int ec) {
switch (ec) {
case 0xA2: return '\u2500'; // Horizontal Line 's' -> '─'
case 0x85: return '\u2502'; // Vertical Line 'e' -> '│'
case 0xC5: return '\u250C'; // Top Left 'E' -> '┌'
case 0xD5: return '\u2510'; // Top Right 'N' -> '┐'
case 0xC4: return '\u2514'; // Bottom Left 'D' -> '└'
case 0xD4: return '\u2518'; // Bottom Right 'M' -> '┘'
case 0xC6: return '\u251C'; // T-Junction Left 'F' -> '├'
case 0xD6: return '\u2524'; // T-Junction Right 'O' -> '┤'
case 0xC7: return '\u252C'; // T-Junction Top 'G' -> '┬'
case 0xD7: return '\u2534'; // T-Junction Bottom 'P' -> '┴'
case 0xCB: return '\u253C'; // Cross -> '┼'
default: return translator.ebcdicToUnicode(ec);
}
}
/**
* Get the default field attribute.
*/
public ExtendedAttribute getDefaultFieldAttribute() {
return defaultFA;
}
}
@@ -0,0 +1,151 @@
package org.lib3270j.telnet;
import org.lib3270j.ConnectionState;
import org.lib3270j.ConnectionConfig;
import org.lib3270j.Telnet3270Client;
import java.io.*;
import java.net.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import static org.lib3270j.protocol.TelnetConstants.*;
/**
* Manages the raw TCP socket connection to a TN3270 host.
* Handles connect/disconnect, raw byte I/O, and spawns a reader thread.
*/
public class TelnetConnection {
private static final Logger log = Logger.getLogger(TelnetConnection.class.getName());
private static final int READ_BUFFER_SIZE = 32768;
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
private Thread readerThread;
private volatile boolean running;
private final TelnetFSM fsm;
private final ConnectionConfig config;
public TelnetConnection(ConnectionConfig config, TelnetFSM fsm) {
this.config = config;
this.fsm = fsm;
}
/**
* Connect to the host. Blocks until connection is established or fails.
*/
public void connect() throws IOException {
log.info("Connecting to " + config.getHost() + ":" + config.getPort());
socket = new Socket();
socket.setKeepAlive(true);
socket.setOOBInline(true);
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(config.getHost(), config.getPort()),
config.getConnectTimeoutMs());
inputStream = new BufferedInputStream(socket.getInputStream(), READ_BUFFER_SIZE);
outputStream = new BufferedOutputStream(socket.getOutputStream());
log.info("Connected to " + socket.getRemoteSocketAddress());
running = true;
readerThread = new Thread(this::readLoop, "TN3270-Reader");
readerThread.setDaemon(true);
readerThread.start();
}
/**
* Send raw bytes to the host.
*/
public synchronized void sendRaw(byte[] data) throws IOException {
sendRaw(data, 0, data.length);
}
/**
* Send raw bytes to the host with offset and length.
*/
public synchronized void sendRaw(byte[] data, int offset, int length) throws IOException {
if (outputStream == null) return;
outputStream.write(data, offset, length);
outputStream.flush();
if (log.isLoggable(Level.FINE)) {
log.fine("SENT " + length + " bytes: " + formatHex(data, offset, length));
}
}
/**
* Disconnect from the host.
*/
public void disconnect() {
running = false;
try {
if (socket != null && !socket.isClosed()) {
socket.shutdownInput();
socket.shutdownOutput();
socket.close();
}
} catch (IOException e) {
log.log(Level.FINE, "Error during disconnect", e);
}
socket = null;
inputStream = null;
outputStream = null;
log.info("Disconnected");
}
/**
* Check if the socket is connected.
*/
public boolean isConnected() {
return socket != null && socket.isConnected() && !socket.isClosed();
}
/**
* Main reader loop. Reads from socket and feeds bytes to the telnet FSM.
*/
private void readLoop() {
byte[] buf = new byte[READ_BUFFER_SIZE];
try {
while (running && isConnected()) {
int n = inputStream.read(buf);
if (n < 0) {
log.info("Host disconnected (EOF)");
fsm.onDisconnect();
break;
}
if (n > 0) {
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);
}
fsm.endOfNetworkData();
}
}
} catch (SocketException e) {
if (running) {
log.info("Socket closed: " + e.getMessage());
fsm.onDisconnect();
}
} catch (IOException e) {
if (running) {
log.log(Level.WARNING, "Read error", e);
fsm.onError("Read error: " + e.getMessage());
}
}
}
/** Format bytes as hex string for logging. */
static String formatHex(byte[] data, int offset, int length) {
StringBuilder sb = new StringBuilder(length * 3);
for (int i = 0; i < length && i < 128; i++) {
if (i > 0) sb.append(' ');
sb.append(String.format("%02x", data[offset + i] & 0xFF));
}
if (length > 128) sb.append("...");
return sb.toString();
}
}
@@ -0,0 +1,954 @@
package org.lib3270j.telnet;
import org.lib3270j.*;
import org.lib3270j.datastream.DataStreamProcessor;
import org.lib3270j.protocol.*;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.listener.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import java.util.logging.Level;
import static org.lib3270j.protocol.TelnetConstants.*;
import static org.lib3270j.protocol.TN3270EConstants.*;
/**
* Telnet finite state machine.
* Processes incoming bytes one at a time, handling telnet commands,
* option negotiation, sub-negotiation, and 3270 data stream framing.
*
* Eight states matching telnet.c:
* TNS_DATA, TNS_IAC, TNS_WILL, TNS_WONT, TNS_DO, TNS_DONT, TNS_SB, TNS_SB_IAC
*/
public class TelnetFSM {
private static final Logger log = Logger.getLogger(TelnetFSM.class.getName());
// Telnet FSM states
private static final int TNS_DATA = 0;
private static final int TNS_IAC = 1;
private static final int TNS_WILL = 2;
private static final int TNS_WONT = 3;
private static final int TNS_DO = 4;
private static final int TNS_DONT = 5;
private static final int TNS_SB = 6;
private static final int TNS_SB_IAC = 7;
private int state = TNS_DATA;
// Option state tracking
private final boolean[] myOpts = new boolean[256]; // options we have enabled
private final boolean[] hisOpts = new boolean[256]; // options the host has enabled
// 3270 input buffer (accumulated between telnet framing)
private final ByteArrayOutputStream ibuf = new ByteArrayOutputStream(32768);
// Sub-negotiation buffer
private final ByteArrayOutputStream sbbuf = new ByteArrayOutputStream(4096);
// TN3270E state
private boolean tn3270eNegotiated;
private TN3270ESubmode tn3270eSubmode = TN3270ESubmode.UNBOUND;
private boolean tn3270eBound;
private final boolean[] eFuncs = new boolean[8]; // Negotiated TN3270E functions
private int eXmitSeq;
private int responseRequired = RSF_NO_RESPONSE;
private boolean deferredWillTtype;
// Connection references
private TelnetConnection connection;
private final ConnectionConfig config;
private final ScreenBuffer screenBuffer;
private final DataStreamProcessor dsProcessor;
private volatile ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
// Listeners
private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<>();
private final List<ScreenUpdateListener> screenListeners = new CopyOnWriteArrayList<>();
// Connected LU info
private String connectedLu;
private String connectedType;
enum TN3270ESubmode { UNBOUND, E_3270, E_NVT, E_SSCP }
public TelnetFSM(ConnectionConfig config, ScreenBuffer screenBuffer, DataStreamProcessor dsProcessor) {
this.config = config;
this.screenBuffer = screenBuffer;
this.dsProcessor = dsProcessor;
}
public void setConnection(TelnetConnection connection) {
this.connection = connection;
}
public void addConnectionListener(ConnectionListener l) { connectionListeners.add(l); }
public void addScreenUpdateListener(ScreenUpdateListener l) { screenListeners.add(l); }
public ConnectionState getConnectionState() { return connectionState; }
public boolean[] getMyOpts() { return myOpts; }
public boolean[] getHisOpts() { return hisOpts; }
/**
* Called when the TCP connection is established.
* Initialize telnet state and prepare for negotiation.
*/
public void onConnected() {
state = TNS_DATA;
java.util.Arrays.fill(myOpts, false);
java.util.Arrays.fill(hisOpts, false);
java.util.Arrays.fill(eFuncs, false);
tn3270eNegotiated = false;
tn3270eSubmode = TN3270ESubmode.UNBOUND;
tn3270eBound = false;
eXmitSeq = 0;
deferredWillTtype = false;
ibuf.reset();
sbbuf.reset();
// Initial TN3270E function requests
eFuncs[FUNC_BIND_IMAGE] = true;
eFuncs[FUNC_RESPONSES] = true;
eFuncs[FUNC_SYSREQ] = true;
changeState(ConnectionState.TELNET_PENDING);
}
/**
* Feed a single byte from the network into the FSM.
*/
public void feedByte(int c) {
switch (state) {
case TNS_DATA:
processData(c);
break;
case TNS_IAC:
processIAC(c);
break;
case TNS_WILL:
processWill(c);
state = TNS_DATA;
break;
case TNS_WONT:
processWont(c);
state = TNS_DATA;
break;
case TNS_DO:
processDo(c);
state = TNS_DATA;
break;
case TNS_DONT:
processDont(c);
state = TNS_DATA;
break;
case TNS_SB:
processSB(c);
break;
case TNS_SB_IAC:
processSBIAC(c);
break;
}
}
/** Called at the end of a network read batch. */
public void endOfNetworkData() {
// Opportunity to flush any pending NVT data
}
// ========== TNS_DATA ==========
private void processData(int c) {
if (c == IAC) {
state = TNS_IAC;
return;
}
if (connectionState == ConnectionState.TELNET_PENDING) {
// Got data before any telnet commands — assume NVT mode
changeState(ConnectionState.CONNECTED_NVT);
}
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states)
if (connectionState.is3270() || connectionState.isTn3270e()) {
ibuf.write(c);
}
// NVT data would go to NVT processor (not implemented in initial version)
}
// ========== TNS_IAC ==========
private void processIAC(int c) {
switch (c) {
case IAC: // Escaped IAC — literal 0xFF
ibuf.write(c);
state = TNS_DATA;
break;
case EOR: // End of record — process accumulated 3270 data
log.fine("RCVD EOR");
if (connectionState.is3270() || connectionState.isTn3270e()) {
processEndOfRecord();
}
ibuf.reset();
state = TNS_DATA;
break;
case WILL: state = TNS_WILL; break;
case WONT: state = TNS_WONT; break;
case DO: state = TNS_DO; break;
case DONT: state = TNS_DONT; break;
case SB:
sbbuf.reset();
state = TNS_SB;
break;
case GA:
log.fine("RCVD GA");
state = TNS_DATA;
break;
case NOP:
log.fine("RCVD NOP");
state = TNS_DATA;
break;
default:
log.fine("RCVD IAC " + TelnetConstants.commandName(c));
state = TNS_DATA;
break;
}
}
// ========== TNS_WILL (host sends WILL option) ==========
private void processWill(int opt) {
log.info("RCVD WILL " + TelnetConstants.optionName(opt));
switch (opt) {
case TELOPT_SGA:
case TELOPT_BINARY:
case TELOPT_EOR:
case TELOPT_ECHO:
if (!hisOpts[opt]) {
hisOpts[opt] = true;
sendCommand(DO, opt);
}
break;
case TELOPT_TN3270E:
if (!hisOpts[opt]) {
hisOpts[opt] = true;
sendCommand(DO, opt);
}
break;
default:
sendCommand(DONT, opt);
break;
}
checkIn3270();
}
// ========== TNS_WONT ==========
private void processWont(int opt) {
log.info("RCVD WONT " + TelnetConstants.optionName(opt));
if (hisOpts[opt]) {
hisOpts[opt] = false;
sendCommand(DONT, opt);
}
checkIn3270();
}
// ========== TNS_DO (host requests we enable option) ==========
private void processDo(int opt) {
log.info("RCVD DO " + TelnetConstants.optionName(opt));
switch (opt) {
case TELOPT_BINARY:
case TELOPT_EOR:
case TELOPT_SGA:
if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
break;
case TELOPT_TTYPE:
if (!myOpts[opt]) {
myOpts[opt] = true;
if (hisOpts[TELOPT_TN3270E]) {
// Defer TTYPE response until TN3270E negotiation completes
deferredWillTtype = true;
} else {
sendCommand(WILL, opt);
}
}
break;
case TELOPT_TN3270E:
if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
// Start TN3270E sub-negotiation: send device type request
sendTN3270EDeviceTypeRequest();
}
break;
case TELOPT_NAWS:
if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
sendNaws();
break;
case TELOPT_NEW_ENVIRON:
if (!myOpts[opt]) {
myOpts[opt] = true;
sendCommand(WILL, opt);
}
break;
default:
sendCommand(WONT, opt);
break;
}
checkIn3270();
}
// ========== TNS_DONT ==========
private void processDont(int opt) {
log.info("RCVD DONT " + TelnetConstants.optionName(opt));
if (myOpts[opt]) {
myOpts[opt] = false;
sendCommand(WONT, opt);
}
checkIn3270();
}
// ========== TNS_SB (accumulating sub-negotiation data) ==========
private void processSB(int c) {
if (c == IAC) {
state = TNS_SB_IAC;
} else {
sbbuf.write(c);
}
}
// ========== TNS_SB_IAC (IAC seen during sub-negotiation) ==========
private void processSBIAC(int c) {
if (c == SE) {
// Sub-negotiation complete
processSubNegotiation(sbbuf.toByteArray());
state = TNS_DATA;
} else if (c == IAC) {
// Escaped IAC within sub-negotiation
sbbuf.write(IAC);
state = TNS_SB;
} else {
// Shouldn't happen, but recover
log.warning("Unexpected byte " + c + " after IAC in SB");
state = TNS_DATA;
}
}
// ========== Sub-negotiation processing ==========
private void processSubNegotiation(byte[] data) {
if (data.length < 1) return;
int opt = data[0] & 0xFF;
log.info("RCVD SB " + TelnetConstants.optionName(opt) + " (" + data.length + " bytes)");
switch (opt) {
case TELOPT_TTYPE:
handleTTypeSB(data);
break;
case TELOPT_TN3270E:
handleTN3270ESB(data);
break;
case TELOPT_NEW_ENVIRON:
handleNewEnvironSB(data);
break;
default:
log.info("Ignoring SB for option " + opt);
break;
}
}
// ========== TTYPE sub-negotiation ==========
private void handleTTypeSB(byte[] data) {
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
// Host asks for terminal type
String termType = config.getEffectiveTerminalType();
log.info("RCVD SB TTYPE SEND - Responding with: " + termType);
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TTYPE);
out.write(TELQUAL_IS);
for (char ch : termType.toCharArray()) {
out.write((byte) ch);
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB TTYPE IS " + termType + " SE");
}
}
// ========== NEW_ENVIRON sub-negotiation ==========
private void handleNewEnvironSB(byte[] data) {
if (data.length >= 2 && data[1] == TELQUAL_SEND) {
log.info("RCVD SB NEW-ENVIRON SEND - Responding with empty IS");
byte[] response = { (byte) IAC, (byte) SB, (byte) TELOPT_NEW_ENVIRON,
(byte) TELQUAL_IS, (byte) IAC, (byte) SE };
sendBytes(response);
log.info("SENT SB NEW-ENVIRON IS SE");
}
}
// ========== TN3270E sub-negotiation ==========
private void handleTN3270ESB(byte[] data) {
if (data.length < 2) return;
int op = data[1] & 0xFF;
log.info("TN3270E SB op=" + op + " (" + tn3270eOpName(op) + ")");
switch (op) {
case OP_SEND:
// Host asks us to send device-type request
sendTN3270EDeviceTypeRequest();
break;
case OP_DEVICE_TYPE:
handleTN3270EDeviceType(data);
break;
case OP_FUNCTIONS:
handleTN3270EFunctions(data);
break;
case OP_IS:
// Could be device-type IS or functions IS, check context
if (data.length >= 3 && data[2] == OP_DEVICE_TYPE) {
handleTN3270EDeviceType(data);
} else if (data.length >= 3 && data[2] == OP_FUNCTIONS) {
handleTN3270EFunctions(data);
}
break;
default:
log.info("Unhandled TN3270E op: " + op);
break;
}
}
private void sendTN3270EDeviceTypeRequest() {
String termType = config.getEffectiveTerminalType();
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TN3270E);
out.write(OP_DEVICE_TYPE);
out.write(OP_REQUEST);
for (char ch : termType.toCharArray()) {
out.write((byte) ch);
}
// Add LU name if specified
if (config.getLuName() != null && !config.getLuName().isEmpty()) {
out.write(OP_CONNECT);
for (char ch : config.getLuName().toCharArray()) {
out.write((byte) ch);
}
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB TN3270E DEVICE-TYPE REQUEST " + termType +
(config.getLuName() != null ? " CONNECT " + config.getLuName() : "") + " SE");
}
private void handleTN3270EDeviceType(byte[] data) {
// Parse: TN3270E DEVICE-TYPE IS <type> CONNECT <name>
// or: TN3270E DEVICE-TYPE REJECT REASON <code>
int pos = 2; // Skip TN3270E and DEVICE-TYPE
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
pos++;
}
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
// Rejection
pos++;
int reason = -1;
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
pos++;
if (pos < data.length) {
reason = data[pos] & 0xFF;
}
}
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
// Fall back to plain TN3270
myOpts[TELOPT_TN3270E] = false;
hisOpts[TELOPT_TN3270E] = false;
// Send deferred WILL TTYPE if needed
if (deferredWillTtype) {
sendCommand(WILL, TELOPT_TTYPE);
deferredWillTtype = false;
}
return;
}
// Parse device type name
StringBuilder deviceType = new StringBuilder();
while (pos < data.length && (data[pos] & 0xFF) != OP_CONNECT) {
deviceType.append((char) (data[pos] & 0xFF));
pos++;
}
// Parse device name (after CONNECT)
String deviceName = null;
if (pos < data.length && (data[pos] & 0xFF) == OP_CONNECT) {
pos++;
StringBuilder name = new StringBuilder();
while (pos < data.length) {
name.append((char) (data[pos] & 0xFF));
pos++;
}
deviceName = name.toString();
}
connectedType = deviceType.toString();
connectedLu = deviceName;
log.info("TN3270E device-type IS " + connectedType +
(connectedLu != null ? " CONNECT " + connectedLu : ""));
// Now send functions request
sendTN3270EFunctionsRequest();
}
private void sendTN3270EFunctionsRequest() {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_TN3270E);
out.write(OP_FUNCTIONS);
out.write(OP_REQUEST);
StringBuilder funcNames = new StringBuilder();
for (int i = 0; i < eFuncs.length; i++) {
if (eFuncs[i]) {
out.write(i);
if (funcNames.length() > 0) funcNames.append(" ");
funcNames.append(TN3270EConstants.functionName(i));
}
}
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
}
private void handleTN3270EFunctions(byte[] data) {
// Parse: TN3270E FUNCTIONS IS [func...]
int pos = 2; // Skip TN3270E, FUNCTIONS
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
pos++; // Skip IS
}
// The remaining bytes are the agreed-upon functions
java.util.Arrays.fill(eFuncs, false);
StringBuilder funcNames = new StringBuilder();
while (pos < data.length) {
int func = data[pos] & 0xFF;
if (func <= FUNC_SNA_SENSE) {
eFuncs[func] = true;
if (funcNames.length() > 0) funcNames.append(" ");
funcNames.append(TN3270EConstants.functionName(func));
}
pos++;
}
tn3270eNegotiated = true;
log.info("TN3270E functions IS: " + funcNames);
log.info("TN3270E negotiation complete");
// Move to CONNECTED_UNBOUND or CONNECTED_SSCP
changeState(ConnectionState.CONNECTED_UNBOUND);
// Notify listeners
for (ConnectionListener l : connectionListeners) {
l.onTN3270ENegotiated(connectedType, connectedLu);
}
}
// ========== End of Record processing ==========
private void processEndOfRecord() {
byte[] data = ibuf.toByteArray();
if (data.length == 0) return;
if (tn3270eNegotiated) {
// TN3270E mode: data starts with 5-byte header
processTN3270ERecord(data);
} else {
// Plain TN3270 mode: data is raw 3270 data stream
dsProcessor.processRecord(data, 0, data.length, true);
notifyScreenUpdate();
}
}
private void processTN3270ERecord(byte[] data) {
if (data.length < EH_SIZE) {
log.warning("TN3270E record too short: " + data.length);
return;
}
int dataType = data[0] & 0xFF;
int requestFlag = data[1] & 0xFF;
int responseFlag = data[2] & 0xFF;
int seqNumber = ((data[3] & 0xFF) << 8) | (data[4] & 0xFF);
log.fine("TN3270E header: type=" + TN3270EConstants.dataTypeName(dataType) +
" rqf=" + requestFlag + " rsf=" + responseFlag + " seq=" + seqNumber);
responseRequired = responseFlag;
switch (dataType) {
case DT_3270_DATA:
if (data.length > EH_SIZE) {
// Transition to 3270 mode
if (connectionState == ConnectionState.CONNECTED_UNBOUND ||
connectionState == ConnectionState.CONNECTED_SSCP) {
// Clear screen on transition to 3270 mode from unbound/SSCP
// This ensures old SSCP-LU data or stale content doesn't persist
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
}
dsProcessor.processRecord(data, EH_SIZE, data.length - EH_SIZE, true);
notifyScreenUpdate();
}
// Send positive response if required
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
break;
case DT_SSCP_LU_DATA:
if (connectionState == ConnectionState.CONNECTED_UNBOUND) {
// Clear screen on first SSCP-LU transition to remove stale data
screenBuffer.clear();
changeState(ConnectionState.CONNECTED_SSCP);
tn3270eSubmode = TN3270ESubmode.E_SSCP;
}
if (data.length > EH_SIZE) {
dsProcessor.processSscpLuData(data, EH_SIZE, data.length - EH_SIZE);
notifyScreenUpdate();
}
break;
case DT_BIND_IMAGE:
tn3270eBound = true;
// Parse BIND image for screen dimensions (SNA BIND format)
{
int bindLen = data.length - EH_SIZE;
StringBuilder bindHex = new StringBuilder();
for (int bi = EH_SIZE; bi < data.length && bi < EH_SIZE + 40; bi++) {
bindHex.append(String.format("%02x ", data[bi] & 0xFF));
}
log.info("Received BIND image (" + bindLen + " bytes)" +
" responseFlag=" + responseFlag + " raw: " + bindHex.toString().trim());
// SNA BIND RU offsets (from 3270ds.h):
// Byte 20 = RD (default rows), Byte 21 = CD (default cols)
// Byte 22 = RA (alternate rows), Byte 23 = CA (alternate cols)
// Byte 24 = SSIZE (screen size indicator)
final int BIND_OFF_RD = 20, BIND_OFF_CD = 21;
final int BIND_OFF_RA = 22, BIND_OFF_CA = 23;
final int BIND_OFF_SSIZE = 24;
if (bindLen > BIND_OFF_SSIZE) {
int ssize = data[EH_SIZE + BIND_OFF_SSIZE] & 0xFF;
int bindRd = data[EH_SIZE + BIND_OFF_RD] & 0xFF;
int bindCd = data[EH_SIZE + BIND_OFF_CD] & 0xFF;
int bindRa, bindCa;
switch (ssize) {
case 0x00: case 0x02:
// Default model 2 dimensions for both default and alt
bindRd = 24; bindCd = 80;
bindRa = 24; bindCa = 80;
break;
case 0x03:
// Default = 24x80, alternate = configured model max
bindRd = 24; bindCd = 80;
bindRa = screenBuffer.getMaxRows();
bindCa = screenBuffer.getMaxCols();
break;
case 0x7e:
// Both default and alternate = specified values
bindRa = bindRd; bindCa = bindCd;
break;
case 0x7f:
// Default and alternate are both specified separately
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
break;
default:
// Unknown SSIZE - use model defaults
bindRa = screenBuffer.getMaxRows();
bindCa = screenBuffer.getMaxCols();
break;
}
log.info("BIND SSIZE=0x" + String.format("%02x", ssize) +
" default=" + bindRd + "x" + bindCd +
" alt=" + bindRa + "x" + bindCa);
// Apply dimensions — constrain to model max
int maxR = screenBuffer.getMaxRows();
int maxC = screenBuffer.getMaxCols();
if (bindRa > 0 && bindCa > 0 && bindRa <= maxR && bindCa <= maxC) {
screenBuffer.setAlternateDimensions(bindRa, bindCa);
}
}
}
// Clear and reset screen for new session
screenBuffer.erase(false);
changeState(ConnectionState.CONNECTED_TN3270E);
tn3270eSubmode = TN3270ESubmode.E_3270;
notifyScreenUpdate();
// Send positive response if required (critical for ISPF NEWAPPL)
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
break;
case DT_UNBIND:
log.info("Received UNBIND responseFlag=" + responseFlag);
tn3270eBound = false;
// Restore alternate dimensions to configured model max (per x3270)
screenBuffer.setAlternateDimensions(
screenBuffer.getMaxRows(), screenBuffer.getMaxCols());
// Clear screen on UNBIND — essential for ISPF NEWAPPL transitions
screenBuffer.clear();
// Send positive response BEFORE changing state (host expects it)
if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) {
sendTN3270EPositiveResponse(seqNumber);
}
changeState(ConnectionState.CONNECTED_UNBOUND);
tn3270eSubmode = TN3270ESubmode.UNBOUND;
notifyScreenUpdate();
break;
case DT_NVT_DATA:
// NVT data in TN3270E mode
changeState(ConnectionState.CONNECTED_E_NVT);
tn3270eSubmode = TN3270ESubmode.E_NVT;
break;
case DT_RESPONSE:
log.fine("Received response, seq=" + seqNumber);
break;
default:
log.info("Unhandled TN3270E data type: " + dataType);
break;
}
}
private void sendTN3270EPositiveResponse(int seqNumber) {
byte[] resp = new byte[EH_SIZE + 1];
resp[0] = (byte) DT_RESPONSE;
resp[1] = 0;
resp[2] = (byte) RSF_POSITIVE_RESPONSE;
resp[3] = (byte) ((seqNumber >> 8) & 0xFF);
resp[4] = (byte) (seqNumber & 0xFF);
resp[5] = (byte) POS_DEVICE_END;
sendRecord(resp);
}
// ========== Check if we should transition to 3270 mode ==========
private void checkIn3270() {
if (connectionState != ConnectionState.TELNET_PENDING) return;
// For TN3270E, we wait for TN3270E negotiation to complete
if (myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
return; // TN3270E in progress
}
// For plain TN3270: need BINARY and EOR in both directions
if (myOpts[TELOPT_BINARY] && hisOpts[TELOPT_BINARY] &&
myOpts[TELOPT_EOR] && hisOpts[TELOPT_EOR]) {
log.info("Transitioning to plain TN3270 mode");
changeState(ConnectionState.CONNECTED_3270);
}
}
// ========== Sending helpers ==========
private void sendCommand(int cmd, int opt) {
byte[] msg = { (byte) IAC, (byte) cmd, (byte) opt };
sendBytes(msg);
log.info("SENT " + TelnetConstants.commandName(cmd) + " " + TelnetConstants.optionName(opt));
}
private void sendNaws() {
int cols = screenBuffer.getMaxCols();
int rows = screenBuffer.getMaxRows();
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(IAC);
out.write(SB);
out.write(TELOPT_NAWS);
writeNawsValue(out, cols);
writeNawsValue(out, rows);
out.write(IAC);
out.write(SE);
sendBytes(out.toByteArray());
log.info("SENT SB NAWS " + cols + " " + rows + " SE");
}
private void writeNawsValue(ByteArrayOutputStream out, int value) {
int hi = (value >> 8) & 0xFF;
int lo = value & 0xFF;
out.write(hi);
if (hi == IAC) out.write(IAC);
out.write(lo);
if (lo == IAC) out.write(IAC);
}
/**
* Send a 3270 record (with EOR framing, and TN3270E header if applicable).
*/
public void sendRecord(byte[] data) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + 10);
// Escape any IAC bytes in the payload
for (byte b : data) {
out.write(b & 0xFF);
if ((b & 0xFF) == IAC) {
out.write(IAC);
}
}
out.write(IAC);
out.write(EOR);
sendBytes(out.toByteArray());
}
/**
* Send a 3270 data record, with TN3270E header if in TN3270E mode.
*/
public void send3270Data(byte[] data) {
if (tn3270eNegotiated) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
// TN3270E header
out.write(DT_3270_DATA); // data type
out.write(0); // request flag
out.write(0); // response flag
out.write((eXmitSeq >> 8) & 0xFF); // seq high
out.write(eXmitSeq & 0xFF); // seq low
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
// 3270 data
for (byte b : data) {
out.write(b & 0xFF);
}
sendRecord(out.toByteArray());
} else {
sendRecord(data);
}
}
/**
* Send SSCP-LU data (in TN3270E SSCP-LU mode).
*/
public void sendSscpLuData(byte[] data) {
if (tn3270eNegotiated) {
ByteArrayOutputStream out = new ByteArrayOutputStream(data.length + EH_SIZE);
out.write(DT_SSCP_LU_DATA);
out.write(0);
out.write(0);
out.write((eXmitSeq >> 8) & 0xFF);
out.write(eXmitSeq & 0xFF);
eXmitSeq = (eXmitSeq + 1) & 0xFFFF;
for (byte b : data) {
out.write(b & 0xFF);
}
sendRecord(out.toByteArray());
}
}
private void sendBytes(byte[] data) {
try {
connection.sendRaw(data);
} catch (IOException e) {
log.log(Level.WARNING, "Send error", e);
onError("Send error: " + e.getMessage());
}
}
// ========== State management ==========
private void changeState(ConnectionState newState) {
ConnectionState old = this.connectionState;
this.connectionState = newState;
log.info("State: " + old + " -> " + newState);
for (ConnectionListener l : connectionListeners) {
l.onConnectionStateChanged(old, newState);
}
}
public void onDisconnect() {
changeState(ConnectionState.NOT_CONNECTED);
}
public void onError(String message) {
log.warning("Error: " + message);
for (ConnectionListener l : connectionListeners) {
l.onConnectionError(message);
}
}
private void notifyScreenUpdate() {
for (ScreenUpdateListener l : screenListeners) {
l.onScreenUpdated();
}
}
public boolean isTn3270eNegotiated() { return tn3270eNegotiated; }
public String getConnectedLu() { return connectedLu; }
public String getConnectedType() { return connectedType; }
private static String tn3270eOpName(int op) {
switch (op) {
case OP_ASSOCIATE: return "ASSOCIATE";
case OP_CONNECT: return "CONNECT";
case OP_DEVICE_TYPE: return "DEVICE-TYPE";
case OP_FUNCTIONS: return "FUNCTIONS";
case OP_IS: return "IS";
case OP_REASON: return "REASON";
case OP_REJECT: return "REJECT";
case OP_REQUEST: return "REQUEST";
case OP_SEND: return "SEND";
default: return "OP-" + op;
}
}
}
Executable
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
# Launch j3270 - Java TN3270 Terminal Emulator
# Usage: ./run.sh [host] [port] [model]
# Example: ./run.sh mainframe.example.com 23 4
JAVA_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$JAVA_DIR/build"
# Always rebuild to pick up changes
echo "Building lib3270j..."
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR/lib3270j" "$BUILD_DIR/j3270"
# Compile libraries
find "$JAVA_DIR/lib3270j/src" -name "*.java" -print0 | xargs -0 javac -d "$BUILD_DIR/lib3270j"
echo "Building j3270..."
find "$JAVA_DIR/j3270/src" -name "*.java" -print0 | xargs -0 javac -cp "$BUILD_DIR/lib3270j" -d "$BUILD_DIR/j3270"
echo "Build complete."
# ---------------------------------------------------------
# NEW: Package the shareable JAR
# ---------------------------------------------------------
echo "Packaging j3270.jar..."
# 1. Create the Manifest file indicating the entry point
# Note: The manifest file must end with a new line or carriage return
echo "Main-Class: org.pubvm.j3270.J3270App" > "$BUILD_DIR/MANIFEST.MF"
echo "" >> "$BUILD_DIR/MANIFEST.MF"
# 2. Build the JAR using both compiled directories
jar cvfm "$BUILD_DIR/j3270.jar" "$BUILD_DIR/MANIFEST.MF" \
-C "$BUILD_DIR/lib3270j" . \
-C "$BUILD_DIR/j3270" .
echo "Executable JAR created at: $BUILD_DIR/j3270.jar"
# ---------------------------------------------------------
# Run the newly packaged JAR
# ---------------------------------------------------------
exec java -Dapple.laf.useScreenMenuBar=true \
-Dapple.awt.application.name=j3270 \
-jar "$BUILD_DIR/j3270.jar" "$@"
+4
View File
@@ -0,0 +1,4 @@
rootProject.name = 'j3270-project'
include 'lib3270j'
include 'j3270'