IBM-DYNAMIC

This commit is contained in:
2026-09-03 16:49:49 -04:00
parent 31b85f491c
commit 022a88b155
12 changed files with 1467 additions and 306 deletions
@@ -25,6 +25,7 @@ import android.content.Context
import android.view.KeyCharacterMap
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
import haus.nightmare.a3270.service.TerminalService
import haus.nightmare.a3270.storage.AppSettings
import haus.nightmare.a3270.storage.HostStorage
import haus.nightmare.a3270.ui.ConnectDialog
import haus.nightmare.a3270.ui.FileTransferDialog
@@ -51,19 +52,15 @@ class MainActivity : ComponentActivity() {
rootLogger.removeHandler(h)
}
rootLogger.addHandler(object : java.util.logging.Handler() {
override fun publish(record: java.util.logging.LogRecord?) {
if (record == null) return
override fun publish(record: java.util.logging.LogRecord) {
val tag = "lib3270j-" + (record.loggerName?.substringAfterLast('.') ?: "core")
val msg = record.message ?: ""
val tag = "a3270-" + (record.loggerName?.substringAfterLast('.') ?: "lib3270j")
when {
record.level.intValue() >= java.util.logging.Level.SEVERE.intValue() ->
android.util.Log.e(tag, msg, record.thrown)
record.level.intValue() >= java.util.logging.Level.WARNING.intValue() ->
android.util.Log.w(tag, msg, record.thrown)
record.level.intValue() >= java.util.logging.Level.INFO.intValue() ->
android.util.Log.i(tag, msg, record.thrown)
else ->
android.util.Log.d(tag, msg, record.thrown)
when (record.level.intValue()) {
java.util.logging.Level.SEVERE.intValue() -> android.util.Log.e(tag, msg, record.thrown)
java.util.logging.Level.WARNING.intValue() -> android.util.Log.w(tag, msg, record.thrown)
java.util.logging.Level.INFO.intValue() -> android.util.Log.i(tag, msg, record.thrown)
java.util.logging.Level.CONFIG.intValue() -> android.util.Log.d(tag, msg, record.thrown)
else -> android.util.Log.v(tag, msg, record.thrown)
}
}
override fun flush() {}
@@ -74,6 +71,8 @@ class MainActivity : ComponentActivity() {
private val viewModel: TerminalViewModel by viewModels()
private val isShiftPressedState = mutableStateOf(false)
private val isFindDialogOpenState = mutableStateOf(false)
private val isFtDialogOpenState = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -88,17 +87,29 @@ class MainActivity : ComponentActivity() {
}
setContent {
MaterialTheme(
colorScheme = darkColorScheme(
val uiTheme by viewModel.uiTheme.collectAsState()
val colorScheme = if (uiTheme.equals("LIGHT", ignoreCase = true)) {
lightColorScheme(
background = Color(0xFFF2F2F4),
surface = Color.White,
primary = Color(0xFF1971C2)
)
} else {
darkColorScheme(
background = Color.Black,
surface = Color(0xFF1E1E1E),
primary = Color(0xFF339AF0)
)
) {
}
MaterialTheme(colorScheme = colorScheme) {
MainScreen(
viewModel = viewModel,
isShiftPressed = isShiftPressedState.value,
onClearShift = { isShiftPressedState.value = false }
onClearShift = { isShiftPressedState.value = false },
isFindDialogOpen = isFindDialogOpenState.value,
onSetFindDialogOpen = { isFindDialogOpenState.value = it },
isFtDialogOpen = isFtDialogOpenState.value,
onSetFtDialogOpen = { isFtDialogOpenState.value = it }
)
}
}
@@ -199,6 +210,14 @@ class MainActivity : ComponentActivity() {
// 2. Ctrl Shortcuts
if (ctrl) {
when (keyCode) {
KeyEvent.KEYCODE_F -> {
isFindDialogOpenState.value = true
return true
}
KeyEvent.KEYCODE_D -> {
viewModel.disconnect()
return true
}
KeyEvent.KEYCODE_V -> {
pasteFromClipboard()
return true
@@ -246,9 +265,13 @@ class MainActivity : ComponentActivity() {
}
}
// 3. Alt Shortcuts (PA keys, Clear, Reset)
// 3. Alt Shortcuts (PA keys, Clear, Reset, Enter, Erase, Attn, SysReq, CursorSelect, Ruler, FT)
if (alt) {
when (keyCode) {
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER -> {
viewModel.sendAid(AID_ENTER)
return true
}
KeyEvent.KEYCODE_1 -> {
viewModel.sendAid(AID_PA1)
return true
@@ -261,7 +284,7 @@ class MainActivity : ComponentActivity() {
viewModel.sendAid(AID_PA3)
return true
}
KeyEvent.KEYCODE_C -> {
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_K -> {
viewModel.sendAid(AID_CLEAR)
return true
}
@@ -269,12 +292,34 @@ class MainActivity : ComponentActivity() {
viewModel.resetKeyboard()
return true
}
KeyEvent.KEYCODE_D -> {
viewModel.deleteWord()
KeyEvent.KEYCODE_E -> {
viewModel.eraseInput()
return true
}
KeyEvent.KEYCODE_E -> {
viewModel.fieldEnd()
KeyEvent.KEYCODE_A -> {
viewModel.attn()
return true
}
KeyEvent.KEYCODE_S -> {
viewModel.sysReq()
return true
}
KeyEvent.KEYCODE_Q -> {
viewModel.cursorSelect()
return true
}
KeyEvent.KEYCODE_X -> {
viewModel.toggleCrosshairRuler()
val isRuler = viewModel.isCrosshairRuler.value
Toast.makeText(this, "Crosshair Ruler: " + (if (isRuler) "ON" else "OFF"), Toast.LENGTH_SHORT).show()
return true
}
KeyEvent.KEYCODE_T -> {
isFtDialogOpenState.value = true
return true
}
KeyEvent.KEYCODE_D -> {
viewModel.deleteWord()
return true
}
KeyEvent.KEYCODE_L -> {
@@ -424,7 +469,11 @@ class MainActivity : ComponentActivity() {
fun MainScreen(
viewModel: TerminalViewModel,
isShiftPressed: Boolean = false,
onClearShift: () -> Unit = {}
onClearShift: () -> Unit = {},
isFindDialogOpen: Boolean = false,
onSetFindDialogOpen: (Boolean) -> Unit = {},
isFtDialogOpen: Boolean = false,
onSetFtDialogOpen: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
val connectionState by viewModel.connectionState.collectAsState()
@@ -449,6 +498,28 @@ fun MainScreen(
val ftState by viewModel.ftState.collectAsState()
val isLightPenMode by viewModel.isLightPenMode.collectAsState()
val isCrosshairRuler by viewModel.isCrosshairRuler.collectAsState()
val cursorStyle by viewModel.cursorStyle.collectAsState()
val blockSelectMode by viewModel.blockSelectMode.collectAsState()
val uiTheme by viewModel.uiTheme.collectAsState()
val isInsertMode by viewModel.isInsertMode.collectAsState()
val isNumericField = remember(screenBuffer, cursorAddr, screenVersion) {
if (screenBuffer != null && screenBuffer?.isFormatted == true) {
val fa = screenBuffer?.getFieldAttributeAt(cursorAddr) ?: 0.toByte()
(fa.toInt() and haus.nightmare.lib3270j.protocol.DS3270Constants.FA_NUMERIC) != 0
} else false
}
val modelName = remember(rows, cols) {
when {
rows == 24 && cols == 80 -> "3279-2"
rows == 32 && cols == 80 -> "3279-3"
rows == 43 && cols == 80 -> "3279-4"
rows == 27 && cols == 132 -> "3279-5"
else -> "DYNAMIC"
}
}
var showConnectDialog by remember { mutableStateOf(false) }
var showFtDialog by remember { mutableStateOf(false) }
var showSettingsDialog by remember { mutableStateOf(false) }
@@ -458,6 +529,20 @@ fun MainScreen(
var showScriptDialog by remember { mutableStateOf(false) }
var showPrinterSessionDialog by remember { mutableStateOf(false) }
LaunchedEffect(isFindDialogOpen) {
if (isFindDialogOpen) {
showFindDialog = true
onSetFindDialogOpen(false)
}
}
LaunchedEffect(isFtDialogOpen) {
if (isFtDialogOpen) {
showFtDialog = true
onSetFtDialogOpen(false)
}
}
var terminalInputViewRef by remember { mutableStateOf<TerminalInputView?>(null) }
val lifecycleOwner = LocalLifecycleOwner.current
@@ -476,25 +561,40 @@ fun MainScreen(
}
LaunchedEffect(Unit) {
val autoHost = HostStorage.getAutoConnectHost(context)
if (autoHost != null && !connectionState.isConnected()) {
viewModel.connect(
autoHost.host,
autoHost.port,
autoHost.model,
autoHost.luName,
autoHost.hostType,
autoHost.useTls,
autoHost.tlsVerifyCert,
autoHost.tn3270e,
autoHost.graphicsMode,
autoHost.codePage,
autoHost.proxyType,
autoHost.proxyHost,
autoHost.proxyPort,
autoHost.proxyUsername,
autoHost.proxyPassword
)
val startupBehavior = AppSettings.getStartupBehavior(context)
when (startupBehavior) {
AppSettings.StartupBehavior.AUTO_CONNECT -> {
val autoHost = HostStorage.getAutoConnectHost(context)
if (autoHost != null && !connectionState.isConnected()) {
viewModel.connect(
autoHost.host,
autoHost.port,
autoHost.model,
autoHost.dynamicRows,
autoHost.dynamicCols,
autoHost.luName,
autoHost.hostType,
autoHost.useTls,
autoHost.tlsVerifyCert,
autoHost.tn3270e,
autoHost.graphicsMode,
autoHost.codePage,
autoHost.proxyType,
autoHost.proxyHost,
autoHost.proxyPort,
autoHost.proxyUsername,
autoHost.proxyPassword
)
}
}
AppSettings.StartupBehavior.SHOW_CONNECT -> {
if (!connectionState.isConnected()) {
showConnectDialog = true
}
}
AppSettings.StartupBehavior.DO_NOTHING -> {
// Do nothing
}
}
}
@@ -532,6 +632,13 @@ fun MainScreen(
blinkCursor = cursorBlink,
searchHighlightAddr = searchHighlightAddr,
searchHighlightLen = searchHighlightLen,
crosshairRuler = isCrosshairRuler,
cursorStyle = cursorStyle,
isInsertMode = isInsertMode,
blockSelectMode = blockSelectMode,
onPasteLineWrap = { text, wrapCol, wordWrap ->
viewModel.pasteLineWrap(text, wrapCol, wordWrap)
},
onTapAddress = { addr ->
viewModel.setCursor(addr)
terminalInputViewRef?.showSoftKeyboard()
@@ -664,9 +771,11 @@ fun MainScreen(
graphicsMode = defaultGraphicsMode,
codePage = codePage,
isLightPenMode = isLightPenMode,
isInsertMode = viewModel.getClient()?.inputProcessor?.isInsertMode ?: false,
isInsertMode = isInsertMode,
inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0,
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: ""
luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "",
isNumericField = isNumericField,
modelName = modelName
)
}
}
@@ -674,9 +783,9 @@ fun MainScreen(
if (showConnectDialog) {
ConnectDialog(
onDismiss = { showConnectDialog = false },
onConnect = { host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
onConnect = { host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass ->
showConnectDialog = false
viewModel.connect(host, port, model, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
viewModel.connect(host, port, model, dynamicRows, dynamicCols, luName, hostType, useTls, tlsVerifyCert, tn3270e, graphicsMode, cp, pType, pHost, pPort, pUser, pPass)
terminalInputViewRef?.showSoftKeyboard()
}
)
@@ -709,10 +818,24 @@ fun MainScreen(
initialVerifyCerts = verifyCerts,
initialDefaultGraphicsMode = defaultGraphicsMode,
initialDefaultCodePage = codePage,
initialUiTheme = uiTheme,
initialCrosshairRuler = isCrosshairRuler,
initialCursorStyle = cursorStyle,
initialBlockSelectMode = blockSelectMode,
initialStartupBehavior = AppSettings.getStartupBehavior(context).name,
initialDynamicRows = AppSettings.getDynamicRows(context),
initialDynamicCols = AppSettings.getDynamicCols(context),
onDismiss = { showSettingsDialog = false },
onSave = { mask, blink, haptic, verify, gfx, defaultCp ->
onSave = { mask, blink, haptic, verify, gfx, defaultCp, theme, crosshair, style, blockSel, startup, dynRows, dynCols ->
showSettingsDialog = false
viewModel.updateSettings(mask, blink, haptic, verify, gfx, defaultCp)
viewModel.setUiTheme(theme)
viewModel.setCrosshairRuler(crosshair)
viewModel.setCursorStyle(style)
viewModel.setBlockSelectMode(blockSel)
AppSettings.setStartupBehavior(context, AppSettings.StartupBehavior.valueOf(startup))
AppSettings.setDynamicRows(context, dynRows)
AppSettings.setDynamicCols(context, dynCols)
terminalInputViewRef?.showSoftKeyboard()
}
)
@@ -146,6 +146,21 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
private val _isLightPenMode = MutableStateFlow(false)
val isLightPenMode: StateFlow<Boolean> = _isLightPenMode.asStateFlow()
private val _isCrosshairRuler = MutableStateFlow(AppSettings.isCrosshairRulerEnabled(application))
val isCrosshairRuler: StateFlow<Boolean> = _isCrosshairRuler.asStateFlow()
private val _cursorStyle = MutableStateFlow(AppSettings.getCursorStyle(application))
val cursorStyle: StateFlow<String> = _cursorStyle.asStateFlow()
private val _blockSelectMode = MutableStateFlow(AppSettings.isBlockSelectModeEnabled(application))
val blockSelectMode: StateFlow<Boolean> = _blockSelectMode.asStateFlow()
private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application))
val uiTheme: StateFlow<String> = _uiTheme.asStateFlow()
private val _isInsertMode = MutableStateFlow(false)
val isInsertMode: StateFlow<Boolean> = _isInsertMode.asStateFlow()
fun toggleLightPen() {
_isLightPenMode.value = !_isLightPenMode.value
}
@@ -154,6 +169,53 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
_isLightPenMode.value = enabled
}
fun setCrosshairRuler(enabled: Boolean) {
AppSettings.setCrosshairRulerEnabled(getApplication(), enabled)
_isCrosshairRuler.value = enabled
}
fun toggleCrosshairRuler() {
setCrosshairRuler(!_isCrosshairRuler.value)
}
fun setCursorStyle(style: String) {
AppSettings.setCursorStyle(getApplication(), style)
_cursorStyle.value = style
}
fun setBlockSelectMode(enabled: Boolean) {
AppSettings.setBlockSelectModeEnabled(getApplication(), enabled)
_blockSelectMode.value = enabled
}
fun setUiTheme(theme: String) {
AppSettings.setUiTheme(getApplication(), theme)
_uiTheme.value = theme
}
fun reloadSettings() {
val app = getApplication<Application>()
_maskHiddenInput.value = AppSettings.isMaskHiddenInputEnabled(app)
_cursorBlink.value = AppSettings.isCursorBlinkEnabled(app)
_hapticFeedback.value = AppSettings.isHapticFeedbackEnabled(app)
_verifyCerts.value = AppSettings.isVerifyCertsEnabled(app)
_defaultGraphicsMode.value = AppSettings.getDefaultGraphicsMode(app)
_codePage.value = AppSettings.getDefaultCodePage(app)
_isCrosshairRuler.value = AppSettings.isCrosshairRulerEnabled(app)
_cursorStyle.value = AppSettings.getCursorStyle(app)
_blockSelectMode.value = AppSettings.isBlockSelectModeEnabled(app)
_uiTheme.value = AppSettings.getUiTheme(app)
}
fun pasteLineWrap(text: String, endCol: Int, wordWrap: Boolean) {
val cur = client
if (cur != null && cur.connectionState.isFullSession) {
val curPos = cur.screenBuffer.cursorAddress
cur.ps?.pasteLineWrap(text, curPos, endCol, wordWrap)
_screenVersion.value = System.currentTimeMillis()
}
}
fun setSearchHighlight(addr: Int, len: Int) {
_searchHighlightAddr.value = addr
_searchHighlightLen.value = len
@@ -352,6 +414,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
c.sendNVTChar('\u001B')
} else {
ip.reset()
_isInsertMode.value = ip.isInsertMode
}
}
is TerminalInputAction.WordLeft -> {
@@ -374,8 +437,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
}
}
is TerminalInputAction.DeleteWord -> {
if (!isNvt) {
ip.isKeyboardLocked = false
if (isNvt) {
c.sendNVTString("\u0017")
} else {
ip.processDeleteWord()
}
}
@@ -385,13 +449,15 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
}
}
is TerminalInputAction.Attn -> {
if (!isNvt) {
ip.attn()
if (isNvt) {
c.sendNVTChar('\u0003')
} else {
c.attn()
}
}
is TerminalInputAction.SysReq -> {
if (!isNvt) {
ip.sysReq()
c.sysReq()
}
}
is TerminalInputAction.Dup -> {
@@ -407,6 +473,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
is TerminalInputAction.ToggleInsert -> {
if (!isNvt) {
ip.processToggleInsert()
_isInsertMode.value = ip.isInsertMode
}
}
is TerminalInputAction.SetCursor -> {
@@ -543,6 +610,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
host: String,
port: Int = 23,
modelNum: Int = 2,
dynamicRows: Int = 62,
dynamicCols: Int = 160,
luName: String = "",
hostType: String = "TSO",
useTls: Boolean = false,
@@ -577,6 +646,7 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
}
val model = when (modelNum) {
0 -> TerminalModel.IBM_DYNAMIC
3 -> TerminalModel.IBM_3279_3
4 -> TerminalModel.IBM_3279_4
5 -> TerminalModel.IBM_3279_5
@@ -597,6 +667,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
_oiaText.value = "Connecting to $effectiveHost:$effectivePort" + (if (effectiveTls) " [TLS]" else "") + "..."
val config = ConnectionConfig(effectiveHost, effectivePort, model).apply {
if (model == TerminalModel.IBM_DYNAMIC) {
setDynamicDimensions(dynamicRows, dynamicCols)
}
if (luName.isNotBlank()) {
setLuName(luName)
}
@@ -2,6 +2,13 @@ package haus.nightmare.a3270.storage
import android.content.Context
import android.content.SharedPreferences
import java.io.BufferedReader
import java.io.PrintWriter
import java.io.Reader
import java.io.Writer
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object AppSettings {
private const val PREFS_NAME = "a3270_settings"
@@ -13,6 +20,21 @@ object AppSettings {
private const val KEY_DEFAULT_GRAPHICS_MODE = "default_graphics_mode"
private const val KEY_DEFAULT_CODE_PAGE = "default_code_page"
// New settings aligned with j3270
private const val KEY_UI_THEME = "ui_theme"
private const val KEY_CROSSHAIR_RULER = "crosshair_ruler"
private const val KEY_CURSOR_STYLE = "cursor_style"
private const val KEY_BLOCK_SELECT_MODE = "block_select_mode"
private const val KEY_STARTUP_BEHAVIOR = "startup_behavior"
private const val KEY_DYNAMIC_ROWS = "dynamic_rows"
private const val KEY_DYNAMIC_COLS = "dynamic_cols"
enum class StartupBehavior {
SHOW_CONNECT,
DO_NOTHING,
AUTO_CONNECT
}
private fun getPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
@@ -64,4 +86,161 @@ object AppSettings {
fun setDefaultCodePage(context: Context, codePage: String) {
getPrefs(context).edit().putString(KEY_DEFAULT_CODE_PAGE, codePage).apply()
}
// ========== UI Theme ==========
fun getUiTheme(context: Context): String {
return getPrefs(context).getString(KEY_UI_THEME, "DARK") ?: "DARK"
}
fun setUiTheme(context: Context, theme: String) {
getPrefs(context).edit().putString(KEY_UI_THEME, if (theme.equals("LIGHT", ignoreCase = true)) "LIGHT" else "DARK").apply()
}
// ========== Crosshair Ruler ==========
fun isCrosshairRulerEnabled(context: Context): Boolean {
return getPrefs(context).getBoolean(KEY_CROSSHAIR_RULER, false)
}
fun setCrosshairRulerEnabled(context: Context, enabled: Boolean) {
getPrefs(context).edit().putBoolean(KEY_CROSSHAIR_RULER, enabled).apply()
}
// ========== Cursor Style ==========
fun getCursorStyle(context: Context): String {
return getPrefs(context).getString(KEY_CURSOR_STYLE, "BLOCK") ?: "BLOCK"
}
fun setCursorStyle(context: Context, style: String) {
val s = if (style.equals("UNDERLINE", ignoreCase = true)) "UNDERLINE" else "BLOCK"
getPrefs(context).edit().putString(KEY_CURSOR_STYLE, s).apply()
}
// ========== Block Select Mode ==========
fun isBlockSelectModeEnabled(context: Context): Boolean {
return getPrefs(context).getBoolean(KEY_BLOCK_SELECT_MODE, false)
}
fun setBlockSelectModeEnabled(context: Context, enabled: Boolean) {
getPrefs(context).edit().putBoolean(KEY_BLOCK_SELECT_MODE, enabled).apply()
}
// ========== Startup Behavior ==========
fun getStartupBehavior(context: Context): StartupBehavior {
val s = getPrefs(context).getString(KEY_STARTUP_BEHAVIOR, StartupBehavior.SHOW_CONNECT.name)
return try {
StartupBehavior.valueOf(s ?: StartupBehavior.SHOW_CONNECT.name)
} catch (_: IllegalArgumentException) {
StartupBehavior.SHOW_CONNECT
}
}
fun setStartupBehavior(context: Context, behavior: StartupBehavior) {
getPrefs(context).edit().putString(KEY_STARTUP_BEHAVIOR, behavior.name).apply()
}
// ========== Dynamic Screen Dimensions ==========
fun getDynamicRows(context: Context): Int {
return getPrefs(context).getInt(KEY_DYNAMIC_ROWS, 62).coerceIn(24, 255)
}
fun setDynamicRows(context: Context, rows: Int) {
getPrefs(context).edit().putInt(KEY_DYNAMIC_ROWS, rows.coerceIn(24, 255)).apply()
}
fun getDynamicCols(context: Context): Int {
return getPrefs(context).getInt(KEY_DYNAMIC_COLS, 160).coerceIn(80, 255)
}
fun setDynamicCols(context: Context, cols: Int) {
getPrefs(context).edit().putInt(KEY_DYNAMIC_COLS, cols.coerceIn(80, 255)).apply()
}
// ========== INI Config Export / Import ==========
fun exportToIni(context: Context, writer: Writer) {
val p = PrintWriter(writer)
val timeStamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
p.println("; a3270 / j3270 Configuration File")
p.println("; Exported on $timeStamp")
p.println()
p.println("[appearance]")
p.println("javaUiTheme = ${getUiTheme(context)}")
p.println("crosshairRuler = ${isCrosshairRulerEnabled(context)}")
p.println("cursorStyle = ${getCursorStyle(context)}")
p.println()
p.println("[behavior]")
p.println("startupBehavior = ${getStartupBehavior(context).name}")
val autoHost = HostStorage.getAutoConnectHost(context)
if (autoHost != null) {
p.println("autoConnectHost = ${autoHost.host}")
p.println("autoConnectPort = ${autoHost.port}")
p.println("autoConnectTls = ${autoHost.useTls}")
p.println("autoConnectVerifyCert = ${autoHost.tlsVerifyCert}")
p.println("autoConnectTn3270e = ${autoHost.tn3270e}")
}
p.println("codePage = ${getDefaultCodePage(context)}")
p.println("dynamicRows = ${getDynamicRows(context)}")
p.println("dynamicCols = ${getDynamicCols(context)}")
p.println("blockSelectMode = ${isBlockSelectModeEnabled(context)}")
p.println()
p.println("[graphics]")
p.println("graphicsMode = ${getDefaultGraphicsMode(context)}")
p.println()
p.flush()
}
fun loadFromIni(context: Context, reader: Reader) {
val bufferedReader = BufferedReader(reader)
var currentSection = ""
bufferedReader.forEachLine { rawLine ->
val line = rawLine.trim()
if (line.isNotEmpty() && !line.startsWith(";") && !line.startsWith("#")) {
if (line.startsWith("[") && line.endsWith("]")) {
currentSection = line.substring(1, line.length - 1).lowercase(Locale.US).trim()
} else {
val eq = line.indexOf('=')
if (eq >= 0) {
val key = line.substring(0, eq).trim()
val value = line.substring(eq + 1).trim()
applyConfigEntry(context, currentSection, key, value)
}
}
}
}
}
private fun applyConfigEntry(context: Context, section: String, key: String, value: String) {
when (section) {
"appearance" -> {
when (key.lowercase(Locale.US)) {
"javauitheme", "theme", "uitheme" -> setUiTheme(context, value)
"crosshairruler", "ruler" -> setCrosshairRulerEnabled(context, value.toBoolean())
"cursorstyle" -> setCursorStyle(context, value)
}
}
"behavior", "connection" -> {
when (key.lowercase(Locale.US)) {
"startupbehavior" -> {
try {
setStartupBehavior(context, StartupBehavior.valueOf(value.uppercase(Locale.US)))
} catch (_: Exception) {}
}
"codepage", "charset" -> setDefaultCodePage(context, value)
"dynamicrows", "dynamic_rows" -> value.toIntOrNull()?.let { setDynamicRows(context, it) }
"dynamiccols", "dynamic_cols" -> value.toIntOrNull()?.let { setDynamicCols(context, it) }
"blockselectmode" -> setBlockSelectModeEnabled(context, value.toBoolean())
}
}
"graphics" -> {
if (key.equals("graphicsmode", ignoreCase = true) || key.equals("mode", ignoreCase = true)) {
setDefaultGraphicsMode(context, value.uppercase(Locale.US))
}
}
}
}
}
@@ -12,6 +12,8 @@ data class SavedHost(
val host: String,
val port: Int = 23,
val model: Int = 2,
val dynamicRows: Int = 62,
val dynamicCols: Int = 160,
val luName: String = "",
val autoConnect: Boolean = false,
val hostType: String = "TSO",
@@ -33,6 +35,8 @@ data class SavedHost(
put("host", host)
put("port", port)
put("model", model)
put("dynamicRows", dynamicRows)
put("dynamicCols", dynamicCols)
put("luName", luName)
put("autoConnect", autoConnect)
put("hostType", hostType)
@@ -57,6 +61,8 @@ data class SavedHost(
host = json.optString("host", "127.0.0.1"),
port = json.optInt("port", 23),
model = json.optInt("model", 2),
dynamicRows = json.optInt("dynamicRows", 62),
dynamicCols = json.optInt("dynamicCols", 160),
luName = json.optString("luName", ""),
autoConnect = json.optBoolean("autoConnect", false),
hostType = json.optString("hostType", "TSO"),
@@ -42,6 +42,8 @@ fun ConnectDialog(
host: String,
port: Int,
model: Int,
dynamicRows: Int,
dynamicCols: Int,
luName: String,
hostType: String,
useTls: Boolean,
@@ -61,9 +63,11 @@ fun ConnectDialog(
var selectedHostId by remember { mutableStateOf<String?>(null) }
var profileName by remember { mutableStateOf("") }
var host by remember { mutableStateOf("127.0.0.1") }
var portStr by remember { mutableStateOf("23") }
var host by remember { mutableStateOf("") }
var portStr by remember { mutableStateOf("") }
var modelNum by remember { mutableIntStateOf(2) }
var dynamicRowsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()) }
var dynamicColsStr by remember { mutableStateOf(haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()) }
var luName by remember { mutableStateOf("") }
var autoConnect by remember { mutableStateOf(false) }
var hostType by remember { mutableStateOf("TSO") }
@@ -85,6 +89,8 @@ fun ConnectDialog(
host = saved.host
portStr = saved.port.toString()
modelNum = saved.model
dynamicRowsStr = saved.dynamicRows.toString()
dynamicColsStr = saved.dynamicCols.toString()
luName = saved.luName
autoConnect = saved.autoConnect
hostType = saved.hostType
@@ -104,9 +110,11 @@ fun ConnectDialog(
fun clearFields() {
selectedHostId = null
profileName = ""
host = "127.0.0.1"
portStr = "23"
host = ""
portStr = ""
modelNum = 2
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
luName = ""
autoConnect = false
hostType = "TSO"
@@ -126,14 +134,21 @@ fun ConnectDialog(
fun saveCurrentProfile(): SavedHost? {
val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23
val trimmedHost = host.trim()
if (trimmedHost.isBlank()) return null
if (trimmedHost.isBlank()) {
android.widget.Toast.makeText(context, "Please enter a Host / IP Address", android.widget.Toast.LENGTH_SHORT).show()
return null
}
val nameToSave = profileName.trim().ifBlank { "$trimmedHost:$port" }
val dynRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
val dynCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
val hostToSave = SavedHost(
id = selectedHostId ?: java.util.UUID.randomUUID().toString(),
name = nameToSave,
host = trimmedHost,
port = port,
model = modelNum,
dynamicRows = dynRows,
dynamicCols = dynCols,
luName = luName.trim(),
autoConnect = autoConnect,
hostType = hostType,
@@ -258,7 +273,7 @@ fun ConnectDialog(
color = Color.White
)
Text(
text = "${saved.host}:${saved.port} (M${saved.model} - ${saved.hostType})$tlsTag$eTag$gfxTag" +
text = "${saved.host}:${saved.port} (${if (saved.model == 0) "Dyn ${saved.dynamicRows}x${saved.dynamicCols}" else "M" + saved.model} - ${saved.hostType})$tlsTag$eTag$gfxTag" +
if (saved.autoConnect) " [Auto]" else "",
fontSize = 10.sp,
color = Color.LightGray
@@ -273,6 +288,8 @@ fun ConnectDialog(
hostToConn.host,
hostToConn.port,
hostToConn.model,
hostToConn.dynamicRows,
hostToConn.dynamicCols,
hostToConn.luName,
hostToConn.hostType,
hostToConn.useTls,
@@ -335,6 +352,7 @@ fun ConnectDialog(
value = host,
onValueChange = { host = it.filterNot { c -> c.isWhitespace() } },
label = { Text("Host / IP Address") },
placeholder = { Text("127.0.0.1", color = Color.Gray) },
singleLine = true,
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
modifier = Modifier
@@ -345,6 +363,7 @@ fun ConnectDialog(
value = portStr,
onValueChange = { portStr = it.filter { c -> c.isDigit() } },
label = { Text("Port") },
placeholder = { Text(if (useTls) "992" else "23", color = Color.Gray) },
singleLine = true,
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
modifier = Modifier.weight(0.32f)
@@ -363,7 +382,8 @@ fun ConnectDialog(
2 to "M2",
3 to "M3",
4 to "M4",
5 to "M5"
5 to "M5",
0 to "DYN"
).forEach { (m, label) ->
val isSelected = (modelNum == m)
Surface(
@@ -389,6 +409,32 @@ fun ConnectDialog(
}
}
}
// If Dynamic Model selected, allow specifying Rows and Columns
if (modelNum == 0) {
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = dynamicRowsStr,
onValueChange = { dynamicRowsStr = it },
label = { Text("Dyn Rows (24-255)") },
singleLine = true,
modifier = Modifier.weight(1f),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
OutlinedTextField(
value = dynamicColsStr,
onValueChange = { dynamicColsStr = it },
label = { Text("Dyn Cols (80-255)") },
singleLine = true,
modifier = Modifier.weight(1f),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
}
}
// Host System Type (TSO, CMS, CICS)
@@ -745,6 +791,8 @@ fun ConnectDialog(
saved.host,
saved.port,
saved.model,
saved.dynamicRows,
saved.dynamicCols,
saved.luName,
saved.hostType,
saved.useTls,
@@ -32,11 +32,13 @@ fun OiaStatusBar(
isInsertMode: Boolean = false,
inhibitReason: Int = 0,
luName: String = "",
isNumericField: Boolean = false,
modelName: String = "",
modifier: Modifier = Modifier
) {
val row = if (cols > 0 && rows > 0) ((cursorAddr / cols) % rows) + 1 else 1
val col = if (cols > 0) (cursorAddr % cols) + 1 else 1
val posStr = String.format("%02d/%02d", row, col)
val posStr = String.format("%03d/%03d [%04d]", row, col, cursorAddr)
val hostText = if (currentHost.isNotBlank()) currentHost else oiaText
@@ -63,13 +65,18 @@ fun OiaStatusBar(
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> "X OP"
else -> "X LOCKED"
}
txt to Color(0xFFFF6B6B)
val clr = when (inhibitReason) {
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> Color(0xFFFF0000)
haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> Color(0xFFFFFF00)
else -> Color(0xFFFFFFFF)
}
txt to clr
} else if (isKeyboardLocked) {
"X SYSTEM" to Color(0xFFFF6B6B)
"X SYSTEM" to Color(0xFFFFFFFF)
} else if (isInsertMode) {
"INSERT" to Color(0xFF339AF0)
"INSERT" to Color(0xFF7890F0)
} else {
"READY" to Color(0xFF51CF66)
"READY" to Color(0xFF7890F0)
}
Row(
@@ -89,7 +96,7 @@ fun OiaStatusBar(
Box(
modifier = Modifier
.size(8.dp)
.background(if (connectionState.isConnected()) Color(0xFF51CF66) else Color(0xFF868E96))
.background(if (connectionState.isConnected()) Color(0xFF7890F0) else Color(0xFF868E96))
)
Spacer(modifier = Modifier.width(6.dp))
Text(
@@ -159,6 +166,42 @@ fun OiaStatusBar(
}
}
// Field Status: NUM vs ALPHA
if (connectionState.isConnected() && rows > 0 && cols > 0) {
Spacer(modifier = Modifier.width(6.dp))
Surface(
color = Color(0xFF339AF0).copy(alpha = 0.2f),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = if (isNumericField) "NUM" else "ALPHA",
color = if (isNumericField) Color(0xFFFFD43B) else Color(0xFF74C0FC),
fontSize = 9.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
)
}
}
// Model / Dimensions Badge
if (connectionState.isConnected() && rows > 0 && cols > 0) {
Spacer(modifier = Modifier.width(6.dp))
Surface(
color = Color(0xFF495057).copy(alpha = 0.3f),
shape = RoundedCornerShape(4.dp)
) {
Text(
text = if (modelName.isNotBlank()) modelName else "${rows}x${cols}",
color = Color(0xFFCED4DA),
fontSize = 9.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp)
)
}
}
// Light Pen Badge
if (isLightPenMode) {
Spacer(modifier = Modifier.width(6.dp))
@@ -230,11 +273,11 @@ fun OiaStatusBar(
)
}
// Cursor Position
// Cursor Position & Buffer Address
Text(
text = "R$posStr",
text = posStr,
color = Color(0xFF22B8CF),
fontSize = 11.sp,
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
@@ -41,8 +41,8 @@ import java.util.Locale
@Composable
fun PrinterSessionDialog(
initialHost: String = "127.0.0.1",
initialPort: Int = 23,
initialHost: String = "",
initialPort: Int = 0,
initialCodePage: String = "037",
initialUseTls: Boolean = false,
onDismiss: () -> Unit
@@ -50,8 +50,8 @@ fun PrinterSessionDialog(
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
var host by remember { mutableStateOf(initialHost.ifBlank { "127.0.0.1" }) }
var portStr by remember { mutableStateOf(if (initialPort > 0) initialPort.toString() else "23") }
var host by remember { mutableStateOf(initialHost) }
var portStr by remember { mutableStateOf(if (initialPort > 0) initialPort.toString() else "") }
var printerLu by remember { mutableStateOf("") }
var assocDisplayLu by remember { mutableStateOf("") }
var codePage by remember { mutableStateOf(initialCodePage) }
@@ -69,8 +69,9 @@ fun PrinterSessionDialog(
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
fun startSession() {
val port = portStr.trim().toIntOrNull() ?: 23
val config = PrinterConfig(host.trim(), port).apply {
val targetHost = host.trim().ifBlank { "127.0.0.1" }
val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23
val config = PrinterConfig(targetHost, port).apply {
this.isUseTls = useTls
this.codePage = codePage
if (printerLu.isNotBlank()) this.printerLuName = printerLu.trim()
@@ -273,6 +274,7 @@ fun PrinterSessionDialog(
value = host,
onValueChange = { host = it },
label = { Text("Host") },
placeholder = { Text("127.0.0.1", color = Color.Gray) },
singleLine = true,
modifier = Modifier.weight(2.5f)
)
@@ -280,6 +282,7 @@ fun PrinterSessionDialog(
value = portStr,
onValueChange = { portStr = it },
label = { Text("Port") },
placeholder = { Text(if (useTls) "992" else "23", color = Color.Gray) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.weight(1f)
@@ -316,20 +316,24 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
sb.append("<!DOCTYPE html>\n<html>\n<head>\n")
sb.append("<meta charset=\"UTF-8\">\n")
sb.append("<style>\n")
sb.append("body { background-color: #0A0A0A; color: #00FF66; font-family: monospace; font-size: 14px; margin: 20px; }\n")
sb.append("body { background-color: #0A0A0A; color: #00FF00; font-family: monospace; font-size: 14px; margin: 20px; }\n")
sb.append("pre { line-height: 1.2; font-family: monospace; }\n")
sb.append(".c-green { color: #00FF66; }\n")
sb.append(".c-green { color: #00FF00; }\n")
sb.append(".c-white { color: #FFFFFF; }\n")
sb.append(".c-blue { color: #3399FF; }\n")
sb.append(".c-red { color: #FF3333; }\n")
sb.append(".c-yellow { color: #FFFF33; }\n")
sb.append(".c-turquoise { color: #00E5FF; }\n")
sb.append(".c-pink { color: #FF66CC; }\n")
sb.append(".c-blue { color: #7890F0; }\n")
sb.append(".c-red { color: #FF0000; }\n")
sb.append(".c-yellow { color: #FFFF00; }\n")
sb.append(".c-turquoise { color: #00FFFF; }\n")
sb.append(".c-pink { color: #FF00FF; }\n")
sb.append(".c-orange { color: #FFA200; }\n")
sb.append(".c-mustard { color: #A0A000; }\n")
sb.append(".c-grey { color: #C0C0C0; }\n")
sb.append("</style>\n</head>\n<body>\n<pre>\n")
var currentHidden = false
var currentProt = false
var currentHi = false
var currentFieldEa: haus.nightmare.lib3270j.screen.ExtendedAttribute? = null
for (r in 0 until rows) {
for (c in 0 until cols) {
@@ -340,16 +344,40 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask
currentProt = (faVal and FA_PROTECT) != 0
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
currentFieldEa = cell
sb.append(' ')
} else {
if (currentHidden && maskHidden) {
sb.append(' ')
} else {
val ch = cell.ucs4
val colorClass = when {
currentHi -> "c-white"
currentProt -> "c-blue"
else -> "c-green"
val fgByte = if (cell.fg != 0.toByte()) (cell.fg.toInt() and 0xFF)
else (currentFieldEa?.fg?.toInt()?.and(0xFF) ?: 0)
val colorClass = if (fgByte in 0xf0..0xff) {
when (fgByte - 0xf0) {
1 -> "c-blue"
2 -> "c-red"
3 -> "c-pink"
4 -> "c-green"
5 -> "c-turquoise"
6 -> "c-yellow"
7 -> "c-white"
9 -> "c-blue"
10 -> "c-orange"
11 -> "c-pink"
12 -> "c-green"
13 -> "c-turquoise"
14 -> "c-mustard"
15 -> "c-grey"
else -> "c-green"
}
} else {
when {
currentProt && currentHi -> "c-white"
currentProt && !currentHi -> "c-turquoise"
!currentProt && currentHi -> "c-red"
else -> "c-green"
}
}
val escChar = when (ch) {
'<' -> "&lt;"
@@ -394,6 +422,18 @@ private fun generateBitmap(
if (screenBuffer == null) return bitmap
// Layer 1: Vector graphics plane (under text)
if (graphicsPlane != null && graphicsPlane.hasContent()) {
val rgb = graphicsPlane.rgbBuffer
val gWidth = graphicsPlane.canvasWidth
val gHeight = graphicsPlane.canvasHeight
if (rgb != null && gWidth > 0 && gHeight > 0) {
val gfxBmp = Bitmap.createBitmap(rgb, gWidth, gHeight, Bitmap.Config.ARGB_8888)
val scaledGfx = Bitmap.createScaledBitmap(gfxBmp, width, height, true)
canvas.drawBitmap(scaledGfx, 0f, 0f, null)
}
}
val textPaint = Paint().apply {
isAntiAlias = true
typeface = Typeface.MONOSPACE
@@ -403,6 +443,7 @@ private fun generateBitmap(
var currentHidden = false
var currentProt = false
var currentHi = false
var currentFieldEa: haus.nightmare.lib3270j.screen.ExtendedAttribute? = null
for (r in 0 until rows) {
for (c in 0 until cols) {
@@ -416,22 +457,47 @@ private fun generateBitmap(
currentProt = (faVal and FA_PROTECT) != 0
currentHi = (faVal and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL
currentHidden = (faVal and FA_INT_HIGH_SEL) == 0
currentFieldEa = cell
} else {
val csVal = cell.cs.toInt() and 0xFF
val ecVal = cell.ec.toInt() and 0xFF
val fgByte = if (cell.fg != 0.toByte()) (cell.fg.toInt() and 0xFF)
else (currentFieldEa?.fg?.toInt()?.and(0xFF) ?: 0)
val cellColor = if (fgByte in 0xf0..0xff) {
when (fgByte - 0xf0) {
1 -> android.graphics.Color.parseColor("#7890F0")
2 -> android.graphics.Color.parseColor("#FF0000")
3 -> android.graphics.Color.parseColor("#FF00FF")
4 -> android.graphics.Color.parseColor("#00FF00")
5 -> android.graphics.Color.parseColor("#00FFFF")
6 -> android.graphics.Color.parseColor("#FFFF00")
7 -> android.graphics.Color.WHITE
9 -> android.graphics.Color.parseColor("#000080")
10 -> android.graphics.Color.parseColor("#FFA200")
11 -> android.graphics.Color.parseColor("#800080")
12 -> android.graphics.Color.parseColor("#008000")
13 -> android.graphics.Color.parseColor("#008080")
14 -> android.graphics.Color.parseColor("#A0A000")
15 -> android.graphics.Color.parseColor("#C0C0C0")
else -> android.graphics.Color.parseColor("#00FF00")
}
} else {
when {
currentProt && currentHi -> android.graphics.Color.WHITE
currentProt && !currentHi -> android.graphics.Color.parseColor("#00FFFF")
!currentProt && currentHi -> android.graphics.Color.parseColor("#FF0000")
else -> android.graphics.Color.parseColor("#00FF00")
}
}
var drawnAsPs = false
if (csVal >= 0x40 && programSymbolManager != null) {
val slot = programSymbolManager.getSymbol(csVal, ecVal)
if (slot != null) {
val symWidth = slot.width
val symHeight = slot.height
val fgArgb = when {
currentHi -> android.graphics.Color.WHITE
currentProt -> android.graphics.Color.parseColor("#3399FF")
else -> android.graphics.Color.parseColor("#00FF66")
}
val bgArgb = android.graphics.Color.parseColor("#0A0A0A")
val rgbArray = slot.getRgbPixels(fgArgb, bgArgb)
val bgArgb = android.graphics.Color.TRANSPARENT
val rgbArray = slot.getRgbPixels(cellColor, bgArgb)
if (rgbArray != null && symWidth > 0 && symHeight > 0) {
val symBmp = Bitmap.createBitmap(rgbArray, symWidth, symHeight, Bitmap.Config.ARGB_8888)
canvas.drawBitmap(symBmp, null, android.graphics.RectF(left, top, left + charWidth, top + charHeight), null)
@@ -443,11 +509,7 @@ private fun generateBitmap(
if (!drawnAsPs && (!currentHidden || !maskHidden)) {
val ch = cell.ucs4
if (ch in ' '..'~' || ch > '\u007F') {
textPaint.color = when {
currentHi -> android.graphics.Color.WHITE
currentProt -> android.graphics.Color.parseColor("#3399FF")
else -> android.graphics.Color.parseColor("#00FF66")
}
textPaint.color = cellColor
canvas.drawText(ch.toString(), left + 2f, top + 18f, textPaint)
}
}
@@ -455,17 +517,5 @@ private fun generateBitmap(
}
}
// Overlay vector graphics plane if present
if (graphicsPlane != null && graphicsPlane.hasContent()) {
val rgb = graphicsPlane.rgbBuffer
val gWidth = graphicsPlane.canvasWidth
val gHeight = graphicsPlane.canvasHeight
if (rgb != null && gWidth > 0 && gHeight > 0) {
val gfxBmp = Bitmap.createBitmap(rgb, gWidth, gHeight, Bitmap.Config.ARGB_8888)
val scaledGfx = Bitmap.createScaledBitmap(gfxBmp, width, height, true)
canvas.drawBitmap(scaledGfx, 0f, 0f, null)
}
}
return bitmap
}
@@ -26,9 +26,33 @@ fun SettingsDialog(
initialVerifyCerts: Boolean = true,
initialDefaultGraphicsMode: String = "BOTH",
initialDefaultCodePage: String = "037",
initialUiTheme: String = "DARK",
initialCrosshairRuler: Boolean = false,
initialCursorStyle: String = "BLOCK",
initialBlockSelectMode: Boolean = false,
initialStartupBehavior: String = "SHOW_CONNECT",
initialDynamicRows: Int = 62,
initialDynamicCols: Int = 160,
onDismiss: () -> Unit,
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean, verifyCerts: Boolean, defaultGraphicsMode: String, defaultCodePage: String) -> Unit
onSave: (
maskHiddenInput: Boolean,
cursorBlink: Boolean,
hapticFeedback: Boolean,
verifyCerts: Boolean,
defaultGraphicsMode: String,
defaultCodePage: String,
uiTheme: String,
crosshairRuler: Boolean,
cursorStyle: String,
blockSelectMode: Boolean,
startupBehavior: String,
dynamicRows: Int,
dynamicCols: Int
) -> Unit
) {
val context = androidx.compose.ui.platform.LocalContext.current
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
@@ -36,6 +60,16 @@ fun SettingsDialog(
var defaultGraphicsMode by remember { mutableStateOf(initialDefaultGraphicsMode) }
var defaultCodePage by remember { mutableStateOf(initialDefaultCodePage) }
var uiTheme by remember { mutableStateOf(initialUiTheme) }
var crosshairRuler by remember { mutableStateOf(initialCrosshairRuler) }
var cursorStyle by remember { mutableStateOf(initialCursorStyle) }
var blockSelectMode by remember { mutableStateOf(initialBlockSelectMode) }
var startupBehavior by remember { mutableStateOf(initialStartupBehavior) }
var dynamicRowsStr by remember { mutableStateOf(initialDynamicRows.toString()) }
var dynamicColsStr by remember { mutableStateOf(initialDynamicCols.toString()) }
var showImportDialog by remember { mutableStateOf(false) }
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -45,7 +79,7 @@ fun SettingsDialog(
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
modifier = Modifier
.fillMaxWidth(0.95f)
.heightIn(max = 680.dp)
.heightIn(max = 700.dp)
.padding(vertical = 16.dp)
) {
Column(
@@ -63,65 +97,88 @@ fun SettingsDialog(
Spacer(modifier = Modifier.height(16.dp))
// Setting 1: Mask Protected/Hidden Input with '*'
// SECTION 1: APPEARANCE
Text("Appearance", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
Spacer(modifier = Modifier.height(8.dp))
// UI Theme
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("UI Theme", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
listOf("DARK" to "Dark", "LIGHT" to "Light").forEach { (themeKey, themeLabel) ->
val isSelected = uiTheme.equals(themeKey, ignoreCase = true)
FilterChip(
selected = isSelected,
onClick = { uiTheme = themeKey },
label = { Text(themeLabel, fontSize = 11.sp) }
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
// Crosshair Ruler
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text(
text = "Show '*' for Hidden Fields",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
Text(
text = "Display asterisks for typed characters in non-display / password fields rather than blank spaces.",
fontSize = 12.sp,
color = Color.LightGray
)
Text("Crosshair Ruler", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Display intersecting row/column ruler tracking cursor position.", fontSize = 12.sp, color = Color.LightGray)
}
Switch(
checked = maskHiddenInput,
onCheckedChange = { maskHiddenInput = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = Color(0xFF2B8A3E)
)
checked = crosshairRuler,
onCheckedChange = { crosshairRuler = it },
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
)
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = Color(0xFF2C2D30))
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(10.dp))
// Setting 2: Blinking Cursor
// Cursor Style
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text(
text = "Blinking Cursor",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
Text(
text = "Blink the terminal cursor to clearly indicate current screen position.",
fontSize = 12.sp,
color = Color.LightGray
)
Text("Cursor Style", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Shape of 3270 screen cursor.", fontSize = 12.sp, color = Color.LightGray)
}
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
listOf("BLOCK" to "Block", "UNDERLINE" to "Underline").forEach { (styleKey, styleLabel) ->
val isSelected = cursorStyle.equals(styleKey, ignoreCase = true)
FilterChip(
selected = isSelected,
onClick = { cursorStyle = styleKey },
label = { Text(styleLabel, fontSize = 11.sp) }
)
}
}
}
Spacer(modifier = Modifier.height(10.dp))
// Blinking Cursor
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text("Blinking Cursor", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Blink the terminal cursor indicator.", fontSize = 12.sp, color = Color.LightGray)
}
Switch(
checked = cursorBlink,
onCheckedChange = { cursorBlink = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = Color(0xFF2B8A3E)
)
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
)
}
@@ -129,86 +186,138 @@ fun SettingsDialog(
HorizontalDivider(color = Color(0xFF2C2D30))
Spacer(modifier = Modifier.height(16.dp))
// Setting 3: Button Haptic Feedback
// SECTION 2: INTERACTION & BEHAVIOR
Text("Interaction & Behavior", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
Spacer(modifier = Modifier.height(8.dp))
// Mask Hidden Inputs
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text(
text = "Button Haptic Feedback",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
Text(
text = "Vibrate softly when tapping function and navigation buttons to match keyboard tactile feedback.",
fontSize = 12.sp,
color = Color.LightGray
)
Text("Show '*' for Hidden Fields", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Display asterisks for typed characters in password fields.", fontSize = 12.sp, color = Color.LightGray)
}
Switch(
checked = maskHiddenInput,
onCheckedChange = { maskHiddenInput = it },
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
)
}
Spacer(modifier = Modifier.height(10.dp))
// Block Selection Mode
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text("Block Selection Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Select rectangular blocks instead of linear character stream.", fontSize = 12.sp, color = Color.LightGray)
}
Switch(
checked = blockSelectMode,
onCheckedChange = { blockSelectMode = it },
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
)
}
Spacer(modifier = Modifier.height(10.dp))
// Haptic Feedback
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text("Button Haptic Feedback", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Vibrate softly when tapping keybar buttons.", fontSize = 12.sp, color = Color.LightGray)
}
Switch(
checked = hapticFeedback,
onCheckedChange = { hapticFeedback = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = Color(0xFF2B8A3E)
)
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
)
}
Spacer(modifier = Modifier.height(10.dp))
// Startup Behavior
Column(modifier = Modifier.fillMaxWidth()) {
Text("Startup Behavior", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Action to take when launching the application.", fontSize = 12.sp, color = Color.LightGray)
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
listOf(
"SHOW_CONNECT" to "Show Connect",
"DO_NOTHING" to "Do Nothing",
"AUTO_CONNECT" to "Auto-Connect"
).forEach { (sbKey, sbLabel) ->
val isSelected = startupBehavior.equals(sbKey, ignoreCase = true)
FilterChip(
selected = isSelected,
onClick = { startupBehavior = sbKey },
label = { Text(sbLabel, fontSize = 10.sp) },
modifier = Modifier.weight(1f)
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = Color(0xFF2C2D30))
Spacer(modifier = Modifier.height(16.dp))
// Setting 4: Overall TLS Certificate Verification
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text(
text = "Verify TLS Certificates",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
// SECTION 3: PROTOCOL & DYNAMIC DEFAULTS
Text("Protocol & Defaults", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
Spacer(modifier = Modifier.height(8.dp))
// Default Dynamic Dimensions
Column(modifier = Modifier.fillMaxWidth()) {
Text("Default Dynamic Screen Dimensions", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Rows (24-255) and columns (80-255) for IBM-DYNAMIC sessions.", fontSize = 12.sp, color = Color.LightGray)
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = dynamicRowsStr,
onValueChange = { dynamicRowsStr = it },
label = { Text("Rows (24-255)") },
singleLine = true,
modifier = Modifier.weight(1f),
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
)
)
Text(
text = "Enforce SSL/TLS certificate validation. Turn off to allow self-signed or unverified certificates without security prompts.",
fontSize = 12.sp,
color = Color.LightGray
OutlinedTextField(
value = dynamicColsStr,
onValueChange = { dynamicColsStr = it },
label = { Text("Cols (80-255)") },
singleLine = true,
modifier = Modifier.weight(1f),
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
)
)
}
Switch(
checked = verifyCerts,
onCheckedChange = { verifyCerts = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = Color(0xFF2B8A3E)
)
)
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = Color(0xFF2C2D30))
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
// Setting 5: Default Graphics Support Mode
// Graphics Mode
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Default Graphics Mode",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
Text(
text = "Configure default vector graphics (GOCA) and Programmed Symbols (APL) mode.",
fontSize = 12.sp,
color = Color.LightGray
)
Spacer(modifier = Modifier.height(8.dp))
Text("Default Graphics Mode", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
@@ -220,53 +329,26 @@ fun SettingsDialog(
"NONE" to "Off"
).forEach { (modeKey, modeLabel) ->
val isSelected = defaultGraphicsMode.equals(modeKey, ignoreCase = true)
Surface(
shape = RoundedCornerShape(6.dp),
color = if (isSelected) Color(0xFF2B8A3E) else Color(0xFF2C2D30),
modifier = Modifier
.weight(1f)
.height(34.dp)
.clickable { defaultGraphicsMode = modeKey }
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize().padding(horizontal = 2.dp)
) {
Text(
text = modeLabel,
color = Color.White,
fontSize = 11.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}
FilterChip(
selected = isSelected,
onClick = { defaultGraphicsMode = modeKey },
label = { Text(modeLabel, fontSize = 10.sp) },
modifier = Modifier.weight(1f)
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = Color(0xFF2C2D30))
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(12.dp))
// Setting 6: Default EBCDIC Code Page
// Default CodePage
var codePageMenuExpanded by remember { mutableStateOf(false) }
val codePagesList = haus.nightmare.a3270.storage.HostStorage.AVAILABLE_CODE_PAGES
val selectedCpLabel = codePagesList.firstOrNull { it.first == defaultCodePage }?.second ?: "Code Page $defaultCodePage"
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = "Default EBCDIC Code Page",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
Text(
text = "Character encoding translation table for new sessions.",
fontSize = 12.sp,
color = Color.LightGray
)
Spacer(modifier = Modifier.height(8.dp))
Text("Default EBCDIC Code Page", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Spacer(modifier = Modifier.height(6.dp))
Box(modifier = Modifier.fillMaxWidth()) {
Surface(
shape = RoundedCornerShape(6.dp),
@@ -278,27 +360,18 @@ fun SettingsDialog(
.clickable { codePageMenuExpanded = true }
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 10.dp),
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = selectedCpLabel,
color = Color.White,
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
Text(text = selectedCpLabel, color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.Medium)
Text("", color = Color.Gray, fontSize = 10.sp)
}
}
DropdownMenu(
expanded = codePageMenuExpanded,
onDismissRequest = { codePageMenuExpanded = false },
modifier = Modifier
.background(Color(0xFF2C2D30))
.heightIn(max = 280.dp)
modifier = Modifier.background(Color(0xFF2C2D30)).heightIn(max = 280.dp)
) {
codePagesList.forEach { (cpId, label) ->
DropdownMenuItem(
@@ -320,6 +393,57 @@ fun SettingsDialog(
}
}
Spacer(modifier = Modifier.height(12.dp))
// Verify TLS Certs
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text("Verify TLS Certificates", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Validate TLS certificates against system trust store.", fontSize = 12.sp, color = Color.LightGray)
}
Switch(
checked = verifyCerts,
onCheckedChange = { verifyCerts = it },
colors = SwitchDefaults.colors(checkedThumbColor = Color.White, checkedTrackColor = Color(0xFF2B8A3E))
)
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = Color(0xFF2C2D30))
Spacer(modifier = Modifier.height(16.dp))
// SECTION 4: INI CONFIGURATION BACKUP
Text("Configuration File (INI)", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color(0xFF339AF0))
Text("Compatible with j3270 desktop INI configuration files.", fontSize = 12.sp, color = Color.LightGray)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedButton(
onClick = {
val sw = java.io.StringWriter()
haus.nightmare.a3270.storage.AppSettings.exportToIni(context, sw)
val iniText = sw.toString()
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(iniText))
android.widget.Toast.makeText(context, "Exported INI copied to clipboard", android.widget.Toast.LENGTH_SHORT).show()
},
modifier = Modifier.weight(1f)
) {
Text("Export to INI", fontSize = 12.sp)
}
OutlinedButton(
onClick = { showImportDialog = true },
modifier = Modifier.weight(1f)
) {
Text("Import from INI", fontSize = 12.sp)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Action Buttons
@@ -333,7 +457,23 @@ fun SettingsDialog(
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
onSave(maskHiddenInput, cursorBlink, hapticFeedback, verifyCerts, defaultGraphicsMode, defaultCodePage)
val dRows = dynamicRowsStr.trim().toIntOrNull()?.coerceIn(24, 255) ?: 62
val dCols = dynamicColsStr.trim().toIntOrNull()?.coerceIn(80, 255) ?: 160
onSave(
maskHiddenInput,
cursorBlink,
hapticFeedback,
verifyCerts,
defaultGraphicsMode,
defaultCodePage,
uiTheme,
crosshairRuler,
cursorStyle,
blockSelectMode,
startupBehavior,
dRows,
dCols
)
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
) {
@@ -343,4 +483,54 @@ fun SettingsDialog(
}
}
}
if (showImportDialog) {
var importText by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = { showImportDialog = false },
title = { Text("Import INI Configuration") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Paste INI configuration content below:", fontSize = 12.sp)
OutlinedTextField(
value = importText,
onValueChange = { importText = it },
modifier = Modifier
.fillMaxWidth()
.height(180.dp),
maxLines = 10
)
}
},
confirmButton = {
TextButton(onClick = {
showImportDialog = false
if (importText.isNotBlank()) {
try {
haus.nightmare.a3270.storage.AppSettings.loadFromIni(context, java.io.StringReader(importText))
uiTheme = haus.nightmare.a3270.storage.AppSettings.getUiTheme(context)
crosshairRuler = haus.nightmare.a3270.storage.AppSettings.isCrosshairRulerEnabled(context)
cursorStyle = haus.nightmare.a3270.storage.AppSettings.getCursorStyle(context)
blockSelectMode = haus.nightmare.a3270.storage.AppSettings.isBlockSelectModeEnabled(context)
startupBehavior = haus.nightmare.a3270.storage.AppSettings.getStartupBehavior(context).name
dynamicRowsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicRows(context).toString()
dynamicColsStr = haus.nightmare.a3270.storage.AppSettings.getDynamicCols(context).toString()
defaultCodePage = haus.nightmare.a3270.storage.AppSettings.getDefaultCodePage(context)
defaultGraphicsMode = haus.nightmare.a3270.storage.AppSettings.getDefaultGraphicsMode(context)
android.widget.Toast.makeText(context, "Settings imported from INI", android.widget.Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
android.widget.Toast.makeText(context, "Error importing: ${e.message}", android.widget.Toast.LENGTH_LONG).show()
}
}
}) {
Text("Import")
}
},
dismissButton = {
TextButton(onClick = { showImportDialog = false }) {
Text("Cancel")
}
}
)
}
}
@@ -6,12 +6,10 @@ import android.widget.Toast
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
@@ -24,32 +22,36 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
import haus.nightmare.lib3270j.screen.ExtendedAttribute
import haus.nightmare.lib3270j.screen.ScreenBuffer
// Standard 3279 Host Colors (16-color palette)
// Standard 3279 Host Colors (16-color palette) aligned 1:1 with IBM Host On-Demand ColorRemapModel3270
private val HOST_COLORS = arrayOf(
Color(0xFF000000), // 0: Neutral Black
Color(0xFF5078FF), // 1: Blue
Color(0xFFFF3232), // 2: Red
Color(0xFFFF82B4), // 3: Pink
Color(0xFF32CD32), // 4: Green
Color(0xFF40E0D0), // 5: Turquoise
Color(0xFFFFFF50), // 6: Yellow
Color(0xFF7890F0), // 1: Blue (0x7890F0 CUSTOMBLUE)
Color(0xFFFF0000), // 2: Red
Color(0xFFFF00FF), // 3: Pink
Color(0xFF00FF00), // 4: Green
Color(0xFF00FFFF), // 5: Turquoise / Cyan
Color(0xFFFFFF00), // 6: Yellow
Color(0xFFFFFFFF), // 7: Neutral White
Color(0xFF000000), // 8: Black
Color(0xFF1E3CB4), // 9: Deep Blue
Color(0xFFFFA500), // 10: Orange
Color(0xFFB482FF), // 11: Purple
Color(0xFF90EE90), // 12: Pale Green
Color(0xFFAFEEEE), // 13: Pale Turquoise
Color(0xFFAAAAAA), // 14: Grey
Color(0xFFFFFFFF) // 15: White
Color(0xFF000080), // 9: Deep Blue
Color(0xFFFFA200), // 10: Orange (0xFFFFA200)
Color(0xFF800080), // 11: Purple
Color(0xFF008000), // 12: Pale Green
Color(0xFF008080), // 13: Pale Turquoise
Color(0xFFA0A000), // 14: Mustard (0xFFA0A000)
Color(0xFFC0C0C0) // 15: Grey (0xFFC0C0C0)
)
private val COLOR_BLACK = Color(0xFF0A0A0A)
private val SELECTION_COLOR = Color(75, 110, 175, 102) // 40% alpha blend per HoD
private val SEARCH_HIGHLIGHT_COLOR = Color(255, 215, 0, 120) // Gold per HoD
private val CROSSHAIR_RULER_COLOR = Color(0, 255, 0, 102) // 40% alpha green per HoD
@Composable
fun TerminalView(
@@ -64,12 +66,17 @@ fun TerminalView(
isLightPenMode: Boolean = false,
maskHiddenFields: Boolean = true,
blinkCursor: Boolean = true,
crosshairRuler: Boolean = false,
cursorStyle: String = "BLOCK",
isInsertMode: Boolean = false,
blockSelectMode: Boolean = false,
searchHighlightAddr: Int = -1,
searchHighlightLen: Int = 0,
onTapAddress: (Int) -> Unit,
onLightPenSelect: ((Int) -> Unit)? = null,
onGraphicTouch: ((Int, Int, Int) -> Unit)? = null,
onPasteText: (String) -> Unit = {},
onPasteLineWrap: ((String, Int, Boolean) -> Unit)? = null,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@@ -83,6 +90,7 @@ fun TerminalView(
var selectionEnd by remember { mutableStateOf<Offset?>(null) }
var showContextMenu by remember { mutableStateOf(false) }
var contextMenuOffset by remember { mutableStateOf(Offset.Zero) }
var showPasteWrapDialog by remember { mutableStateOf(false) }
// Blinking cursor state (~530ms interval matching j3270)
var cursorVisible by remember { mutableStateOf(true) }
@@ -98,6 +106,15 @@ fun TerminalView(
}
}
// Text blinking timer (500ms cycle matching j3270)
var textBlinkVisible by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
while (true) {
delay(500L)
textBlinkVisible = !textBlinkVisible
}
}
Box(modifier = modifier.fillMaxSize()) {
Canvas(
modifier = Modifier
@@ -235,6 +252,46 @@ fun TerminalView(
selMaxCol = maxOf(startCol, endCol)
}
// 1. Draw Vector Graphics Plane under text if present
if (graphicsPlane != null && graphicsPlane.hasContent()) {
val gridW = metrics.gridWidth.toInt()
val gridH = metrics.gridHeight.toInt()
if (gridW > 0 && gridH > 0) {
graphicsPlane.resize(gridW, gridH)
val rgb = graphicsPlane.rgbBuffer
if (rgb != null) {
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
drawContext.canvas.nativeCanvas.drawBitmap(
bmp,
null,
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
null
)
}
}
}
// 2. Draw Crosshair Ruler if enabled
if (crosshairRuler && cols > 0 && rows > 0) {
val curCol = (activeCursorAddr % cols).coerceIn(0, cols - 1)
val curRow = ((activeCursorAddr / cols) % rows).coerceIn(0, rows - 1)
val gridW = cols * cellWidth
val gridH = rows * cellHeight
val cx = offsetX + curCol * cellWidth
val cy = offsetY + curRow * cellHeight
drawRect(
color = CROSSHAIR_RULER_COLOR,
topLeft = Offset(offsetX, cy),
size = Size(gridW, cellHeight)
)
drawRect(
color = CROSSHAIR_RULER_COLOR,
topLeft = Offset(cx, offsetY),
size = Size(cellWidth, gridH)
)
}
for (r in 0 until rows) {
for (c in 0 until cols) {
val addr = r * cols + c
@@ -249,6 +306,7 @@ fun TerminalView(
var isBold = false
var isUnderline = false
var isReverse = false
var isBlink = false
var csVal = 0
var ecVal = 0
@@ -296,6 +354,7 @@ fun TerminalView(
if ((grVal and GR_INTENSIFY) != 0) isBold = true
if ((grVal and GR_UNDERLINE) != 0) isUnderline = true
if ((grVal and GR_REVERSE) != 0) isReverse = true
if ((grVal and GR_BLINK) != 0) isBlink = true
}
} else {
fgColor = HOST_COLORS[HOST_COLOR_GREEN]
@@ -307,7 +366,7 @@ fun TerminalView(
bgColor = tmp
}
if (bgColor != COLOR_BLACK) {
if (bgColor != COLOR_BLACK && bgColor != Color.Transparent) {
drawRect(
color = bgColor,
topLeft = Offset(left, top),
@@ -315,32 +374,51 @@ fun TerminalView(
)
}
// Draw selection box highlight if selected
val isSelected = r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
// Selection highlight (Block select vs Linear stream select)
val isSelected = if (blockSelectMode) {
r in selMinRow..selMaxRow && c in selMinCol..selMaxCol
} else {
if (selMinRow == -1 || selMaxRow == -1) false
else if (selMinRow == selMaxRow) r == selMinRow && c in selMinCol..selMaxCol
else if (r == selMinRow) c >= selMinCol
else if (r == selMaxRow) c <= selMaxCol
else r in (selMinRow + 1) until selMaxRow
}
val isSearchHighlighted = searchHighlightAddr >= 0 && searchHighlightLen > 0 &&
addr >= searchHighlightAddr && addr < (searchHighlightAddr + searchHighlightLen)
if (isSearchHighlighted) {
drawRect(
color = Color(0x99FFC107), // Amber / Gold highlight for search matches
color = SEARCH_HIGHLIGHT_COLOR,
topLeft = Offset(left, top),
size = Size(cellWidth, cellHeight)
)
} else if (isSelected) {
drawRect(
color = Color(0x773399FF),
color = SELECTION_COLOR,
topLeft = Offset(left, top),
size = Size(cellWidth, cellHeight)
)
}
// Cursor indicator (respects blinking toggle & timer)
// Cursor indicator (respects blinking toggle & timer, cursor style, insert mode)
if (addr == activeCursorAddr && !isSelected && cursorVisible) {
drawRect(
color = HOST_COLORS[HOST_COLOR_TURQUOISE].copy(alpha = 0.5f),
topLeft = Offset(left, top),
size = Size(cellWidth, cellHeight)
)
val isUnderlineCursor = cursorStyle.equals("UNDERLINE", ignoreCase = true) || isInsertMode
if (isUnderlineCursor) {
val ulH = maxOf(4f, cellHeight / 4f)
drawRect(
color = Color(255, 255, 255, 180),
topLeft = Offset(left, top + cellHeight - ulH),
size = Size(cellWidth, ulH)
)
} else {
drawRect(
color = Color(255, 255, 255, 180),
topLeft = Offset(left, top),
size = Size(cellWidth, cellHeight)
)
}
}
// Draw Programmed Symbol (PS / APL) if defined
@@ -364,7 +442,8 @@ fun TerminalView(
}
}
if (!drawnAsPs && charVal != ' ') {
val suppressText = isBlink && !textBlinkVisible
if (!drawnAsPs && charVal != ' ' && !suppressText) {
paint.color = fgColor.toArgb()
paint.isFakeBoldText = isBold
val fontMetrics = paint.fontMetrics
@@ -390,22 +469,44 @@ fun TerminalView(
}
}
// Draw Vector Graphics Plane overlay if present
if (graphicsPlane != null && graphicsPlane.hasContent()) {
val gridW = metrics.gridWidth.toInt()
val gridH = metrics.gridHeight.toInt()
if (gridW > 0 && gridH > 0) {
graphicsPlane.resize(gridW, gridH)
val rgb = graphicsPlane.rgbBuffer
if (rgb != null) {
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
drawContext.canvas.nativeCanvas.drawBitmap(
bmp,
null,
android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()),
null
)
}
// Draw Graphic Cursor if active in GOCA or GraphicsPlane
val isGraphicCursorActive = (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) ||
(graphicsPlane != null && graphicsPlane.isGraphicCursorAttached)
if (isGraphicCursorActive && graphicsPlane != null) {
val gocaX = if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) gocaDecoder.graphicCursorX else graphicsPlane.graphicCursorX
val gocaY = if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive) gocaDecoder.graphicCursorY else graphicsPlane.graphicCursorY
val canvasPx = graphicsPlane.mapX(gocaX)
val canvasPy = graphicsPlane.mapY(gocaY)
val gridW = metrics.gridWidth
val gridH = metrics.gridHeight
val gWidth = graphicsPlane.canvasWidth.toFloat()
val gHeight = graphicsPlane.canvasHeight.toFloat()
val px = offsetX + if (gWidth > 0) (canvasPx.toFloat() * gridW / gWidth) else canvasPx.toFloat()
val py = offsetY + if (gHeight > 0) (canvasPy.toFloat() * gridH / gHeight) else canvasPy.toFloat()
val shape = graphicsPlane.hodCursorShape
if (shape == 2) {
// Box cursor
drawRect(
color = Color.White,
topLeft = Offset(px - 6f, py - 6f),
size = Size(12f, 12f),
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 2f)
)
} else {
// Crosshair cursor
drawLine(
color = Color.White,
start = Offset(px - 10f, py),
end = Offset(px + 10f, py),
strokeWidth = 2f
)
drawLine(
color = Color.White,
start = Offset(px, py - 10f),
end = Offset(px, py + 10f),
strokeWidth = 2f
)
}
}
}
@@ -425,7 +526,7 @@ fun TerminalView(
text = { Text("Copy Selection") },
onClick = {
showContextMenu = false
copySelection(selectionStart, selectionEnd, screenBuffer, rows, cols, clipboardManager, context)
copySelection(selectionStart, selectionEnd, screenBuffer, rows, cols, clipboardManager, context, blockSelectMode)
}
)
DropdownMenuItem(
@@ -460,6 +561,65 @@ fun TerminalView(
}
}
)
DropdownMenuItem(
text = { Text("Paste with Line Wrap...") },
onClick = {
showContextMenu = false
showPasteWrapDialog = true
}
)
}
if (showPasteWrapDialog) {
var marginText by remember { mutableStateOf(cols.toString()) }
var wordWrap by remember { mutableStateOf(true) }
AlertDialog(
onDismissRequest = { showPasteWrapDialog = false },
title = { Text("Paste with Line Wrap") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Wrap text at column (1-$cols, 0 = no wrap):", fontSize = 13.sp)
OutlinedTextField(
value = marginText,
onValueChange = { marginText = it },
singleLine = true,
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(
keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = wordWrap, onCheckedChange = { wordWrap = it })
Spacer(modifier = Modifier.width(4.dp))
Text("Word wrap lines")
}
}
},
confirmButton = {
TextButton(onClick = {
showPasteWrapDialog = false
val clipText = clipboardManager.getText()?.text
if (!clipText.isNullOrEmpty()) {
val margin = marginText.trim().toIntOrNull() ?: cols
if (onPasteLineWrap != null) {
onPasteLineWrap(clipText, margin, wordWrap)
} else {
onPasteText(clipText)
}
} else {
Toast.makeText(context, "Clipboard is empty", Toast.LENGTH_SHORT).show()
}
}) {
Text("Paste")
}
},
dismissButton = {
TextButton(onClick = { showPasteWrapDialog = false }) {
Text("Cancel")
}
}
)
}
}
}
@@ -522,7 +682,8 @@ private fun copySelection(
rows: Int,
cols: Int,
clipboardManager: androidx.compose.ui.platform.ClipboardManager,
context: android.content.Context
context: android.content.Context,
blockSelectMode: Boolean = false
) {
if (start != null && end != null && screenBuffer != null && cols > 0 && rows > 0) {
val displayMetrics = context.resources.displayMetrics
@@ -544,7 +705,9 @@ private fun copySelection(
val sb = StringBuilder()
for (r in minRow..maxRow) {
val line = StringBuilder()
for (c in minCol..maxCol) {
val colStart = if (blockSelectMode) minCol else (if (r == minRow) minCol else 0)
val colEnd = if (blockSelectMode) maxCol else (if (r == maxRow) maxCol else cols - 1)
for (c in colStart..colEnd) {
val addr = r * cols + c
if (addr < (rows * cols)) {
val cell = screenBuffer.getCell(addr)
@@ -560,7 +723,7 @@ private fun copySelection(
val textToCopy = sb.toString().trimEnd()
if (textToCopy.isNotBlank()) {
clipboardManager.setText(AnnotatedString(textToCopy))
Toast.makeText(context, "Copied selected block to clipboard", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Copied selection to clipboard", Toast.LENGTH_SHORT).show()
}
}
}
@@ -575,7 +738,7 @@ private fun getFgColorForAttribute(ea: ExtendedAttribute, currentFieldEa: Extend
}
val fa = currentFA.toInt() and 0xFF
return if (faIsProtected(fa)) {
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_BLUE]
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_WHITE] else HOST_COLORS[HOST_COLOR_TURQUOISE]
} else {
if (faIsHigh(fa)) HOST_COLORS[HOST_COLOR_RED] else HOST_COLORS[HOST_COLOR_GREEN]
}
@@ -0,0 +1,104 @@
package haus.nightmare.a3270
import haus.nightmare.a3270.storage.SavedHost
import haus.nightmare.lib3270j.ConnectionConfig
import haus.nightmare.lib3270j.TerminalModel
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
import org.json.JSONObject
import org.junit.Assert.*
import org.junit.Test
import java.util.UUID
class DynamicAndParityFeaturesTest {
@Test
fun testSavedHostDynamicModelSerialization() {
val host = SavedHost(
id = UUID.randomUUID().toString(),
name = "Dynamic Mainframe Session",
host = "mainframe.example.org",
port = 23,
model = 0,
dynamicRows = 62,
dynamicCols = 160,
luName = "DYNLU01",
autoConnect = false,
hostType = "TSO",
useTls = true,
tlsVerifyCert = true,
tn3270e = true,
graphicsMode = "BOTH",
codePage = "037"
)
val json = host.toJson()
assertEquals(0, json.getInt("model"))
assertEquals(62, json.getInt("dynamicRows"))
assertEquals(160, json.getInt("dynamicCols"))
val deserialized = SavedHost.fromJson(json)
assertEquals(0, deserialized.model)
assertEquals(62, deserialized.dynamicRows)
assertEquals(160, deserialized.dynamicCols)
assertEquals("Dynamic Mainframe Session", deserialized.name)
assertEquals("mainframe.example.org", deserialized.host)
assertEquals(23, deserialized.port)
assertTrue(deserialized.useTls)
assertEquals("BOTH", deserialized.graphicsMode)
}
@Test
fun testSavedHostBackwardCompatibilityDefaults() {
// Legacy JSON without dynamicRows and dynamicCols
val json = JSONObject().apply {
put("id", "legacy-id")
put("name", "Legacy Host")
put("host", "legacy.host")
put("port", 23)
put("model", 2)
}
val deserialized = SavedHost.fromJson(json)
assertEquals(62, deserialized.dynamicRows)
assertEquals(160, deserialized.dynamicCols)
assertEquals(2, deserialized.model)
}
@Test
fun testConnectionConfigDynamicModel() {
val config = ConnectionConfig("10.0.0.1", 23)
config.setDynamicDimensions(72, 140)
assertEquals(TerminalModel.IBM_DYNAMIC, config.model)
assertTrue(config.isDynamicModel)
assertEquals(72, config.dynamicRows)
assertEquals(140, config.dynamicCols)
}
@Test
fun testHodColorRemappingConstants() {
// Base 4-Color Model parity verification:
// Normal Protected must be Turquoise/Cyan (0x00FFFF), NOT Blue
// Intensified Unprotected must be Red (0xFF0000)
// Normal Unprotected is Green (0x00FF00)
// Intensified Protected is White (0xFFFFFF)
val turquoiseRgb = 0x00FFFF
val redRgb = 0xFF0000
val greenRgb = 0x00FF00
val whiteRgb = 0xFFFFFF
assertEquals(0x00FFFF, turquoiseRgb)
assertEquals(0xFF0000, redRgb)
assertEquals(0x00FF00, greenRgb)
assertEquals(0xFFFFFF, whiteRgb)
// Validate bit patterns match IBM 3270 Field Attributes
val prot = FA_PROTECT
val num = FA_NUMERIC
val hi = FA_INT_HIGH_SEL
assertTrue((prot and FA_PROTECT) != 0)
assertTrue((num and FA_NUMERIC) != 0)
assertTrue((hi and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL)
}
}
@@ -0,0 +1,179 @@
package haus.nightmare.a3270
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import haus.nightmare.lib3270j.TerminalModel
import haus.nightmare.lib3270j.charset.CodePageRegistry
import haus.nightmare.lib3270j.charset.EbcdicTranslator
import haus.nightmare.lib3270j.ecl.ECLConstants
import haus.nightmare.lib3270j.input.InputProcessor
import haus.nightmare.lib3270j.nvt.NvtProcessor
import haus.nightmare.lib3270j.printer.PrinterDefinitionTable
import haus.nightmare.lib3270j.protocol.DS3270Constants.*
import haus.nightmare.lib3270j.screen.ScreenBuffer
class NvtAndExtendedFeaturesTest {
private lateinit var screenBuffer: ScreenBuffer
private lateinit var translator: EbcdicTranslator
private lateinit var inputProcessor: InputProcessor
@Before
fun setUp() {
val model = TerminalModel.IBM_3279_2
translator = EbcdicTranslator()
screenBuffer = ScreenBuffer(model, translator)
inputProcessor = InputProcessor(screenBuffer, translator, null)
}
@Test
fun testExtendedCodePagesAvailability() {
val testCps = listOf(
"420", "424", "803", "838", "1160",
"1025", "1123", "1154", "880", "905", "1026", "1155",
"1140", "1141", "1142", "1143", "1144", "1145", "1146", "1147", "1148", "1149",
"930", "939", "935", "937", "1388", "1371", "933"
)
for (cp in testCps) {
val trans = CodePageRegistry.getCodePage(cp)
assertNotNull("Code page $cp should be registered in CodePageRegistry", trans)
assertEquals(cp, trans.codePageId)
}
}
@Test
fun testExtendedCodePageTranslation() {
// Test Greek Cp875
val greekTrans = CodePageRegistry.getCodePage("875")
assertNotNull(greekTrans)
val alphaEbcdic = greekTrans.unicodeToEbcdic('α')
val alphaUnicode = greekTrans.ebcdicToUnicode(alphaEbcdic.toInt() and 0xFF)
assertEquals('α', alphaUnicode)
// Test Cyrillic Cp1025
val cyrTrans = CodePageRegistry.getCodePage("1025")
assertNotNull(cyrTrans)
val cyrA = 'А' // Cyrillic capital A
val cyrEbcdic = cyrTrans.unicodeToEbcdic(cyrA)
val cyrResult = cyrTrans.ebcdicToUnicode(cyrEbcdic.toInt() and 0xFF)
assertEquals(cyrA, cyrResult)
// Test Euro variant Cp1140 has € symbol at 0x9F
val euroTrans = CodePageRegistry.getCodePage("1140")
assertNotNull(euroTrans)
val euroUnicode = euroTrans.ebcdicToUnicode(0x9F)
assertEquals('€', euroUnicode)
}
@Test
fun testPDTTables() {
val pcl5 = PrinterDefinitionTable.createPcl5PDT()
assertNotNull(pcl5)
assertEquals("PCL_5", pcl5.name)
assertNotNull(pcl5.getControlCode(PrinterDefinitionTable.CMD_START_JOB))
val escp = PrinterDefinitionTable.createEpsonEscPPDT()
assertNotNull(escp)
assertEquals("EPSON_ESC_P", escp.name)
assertNotNull(escp.getControlCode(PrinterDefinitionTable.CMD_START_BOLD))
val ps = PrinterDefinitionTable.createPostScriptPDT()
assertNotNull(ps)
assertEquals("POSTSCRIPT", ps.name)
val psStart = String(ps.getControlCode(PrinterDefinitionTable.CMD_START_JOB) ?: ByteArray(0), java.nio.charset.StandardCharsets.ISO_8859_1)
assertTrue(psStart.contains("%!PS"))
val plain = PrinterDefinitionTable.createPlainTextPDT()
assertNotNull(plain)
assertEquals("PLAIN_TEXT", plain.name)
}
@Test
fun testWordNavigationAndEditing() {
val text = "IBM MAINFRAME SYSTEM"
for (i in text.indices) {
screenBuffer.getCell(i).ucs4 = text[i]
screenBuffer.getCell(i).ec = translator.unicodeToEbcdic(text[i]).toByte()
}
screenBuffer.cursorAddress = 0
inputProcessor.processWordRight()
assertEquals(5, screenBuffer.cursorAddress)
inputProcessor.processWordRight()
assertEquals(16, screenBuffer.cursorAddress)
inputProcessor.processWordLeft()
assertEquals(5, screenBuffer.cursorAddress)
inputProcessor.processWordLeft()
assertEquals(0, screenBuffer.cursorAddress)
}
@Test
fun testFieldEndAndToggleInsert() {
screenBuffer.setCellFA(0, FA_PRINTABLE.toByte())
screenBuffer.setCellFA(20, (FA_PRINTABLE or FA_PROTECT).toByte())
val text = "HELLO"
for (i in text.indices) {
screenBuffer.getCell(1 + i).ucs4 = text[i]
screenBuffer.getCell(1 + i).ec = translator.unicodeToEbcdic(text[i]).toByte()
}
screenBuffer.cursorAddress = 1
inputProcessor.processFieldEnd()
assertEquals(6, screenBuffer.cursorAddress)
assertFalse(inputProcessor.isInsertMode)
inputProcessor.processToggleInsert()
assertTrue(inputProcessor.isInsertMode)
inputProcessor.processToggleInsert()
assertFalse(inputProcessor.isInsertMode)
}
@Test
fun testNvtFunctionKeySequences() {
val nvt = NvtProcessor(screenBuffer, null)
val f1 = nvt.getFunctionKeySequence(1)
val f5 = nvt.getFunctionKeySequence(5)
val f10 = nvt.getFunctionKeySequence(10)
assertNotNull(f1)
assertNotNull(f5)
assertNotNull(f10)
assertTrue(f1.startsWith("\u001B"))
assertTrue(f5.startsWith("\u001B"))
assertTrue(f10.startsWith("\u001B"))
}
@Test
fun testECLFieldWrapping() {
val totalCells = screenBuffer.rows * screenBuffer.cols
val faPos = totalCells - 10
screenBuffer.setCellFA(faPos, FA_PRINTABLE.toByte())
screenBuffer.setCellFA(15, (FA_PRINTABLE or FA_PROTECT).toByte())
val fieldList = screenBuffer.buildFieldList()
assertNotNull(fieldList)
val wrappedField = fieldList.findField(totalCells - 5)
assertNotNull(wrappedField)
assertTrue("Field spanning buffer boundary should be marked wrapped", wrappedField.isWrapped)
assertTrue(wrappedField.contains(totalCells - 2))
assertTrue(wrappedField.contains(5))
}
@Test
fun testOiaInhibitConstants() {
assertEquals(1, ECLConstants.INHIBIT_SYSTEM_LOCK)
assertEquals(0, ECLConstants.INHIBIT_NOT_INHIBITED)
assertEquals(5, ECLConstants.INHIBIT_COMM_CHECK)
assertEquals(3, ECLConstants.INHIBIT_PROTECTED_FIELD)
assertEquals(2, ECLConstants.INHIBIT_NUMERIC_ONLY)
assertEquals(4, ECLConstants.INHIBIT_OVERFLOW)
assertEquals(6, ECLConstants.INHIBIT_OPERATOR_DUE)
}
}