Initial Commit
This commit is contained in:
@@ -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.
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.
@@ -0,0 +1,2 @@
|
||||
#Wed Mar 18 14:12:38 EDT 2026
|
||||
gradle.version=8.5
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
@@ -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.
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
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" "$@"
|
||||
@@ -0,0 +1,4 @@
|
||||
rootProject.name = 'j3270-project'
|
||||
|
||||
include 'lib3270j'
|
||||
include 'j3270'
|
||||
Reference in New Issue
Block a user