From e82d01003b3e5f1721d393674906ae381132d4b7 Mon Sep 17 00:00:00 2001 From: Rudi Date: Thu, 3 Sep 2026 16:49:49 -0400 Subject: [PATCH] IBM-DYNAMIC --- .../java/haus/nightmare/a3270/MainActivity.kt | 217 ++++++-- .../haus/nightmare/a3270/TerminalViewModel.kt | 83 ++- .../nightmare/a3270/storage/AppSettings.kt | 179 +++++++ .../nightmare/a3270/storage/HostStorage.kt | 6 + .../haus/nightmare/a3270/ui/ConnectDialog.kt | 62 ++- .../haus/nightmare/a3270/ui/OiaStatusBar.kt | 61 ++- .../a3270/ui/PrinterSessionDialog.kt | 15 +- .../a3270/ui/ScreenExporterDialog.kt | 120 +++-- .../haus/nightmare/a3270/ui/SettingsDialog.kt | 480 ++++++++++++------ .../haus/nightmare/a3270/ui/TerminalView.kt | 267 ++++++++-- .../a3270/DynamicAndParityFeaturesTest.kt | 104 ++++ .../a3270/NvtAndExtendedFeaturesTest.kt | 179 +++++++ 12 files changed, 1467 insertions(+), 306 deletions(-) create mode 100644 src/test/java/haus/nightmare/a3270/DynamicAndParityFeaturesTest.kt create mode 100644 src/test/java/haus/nightmare/a3270/NvtAndExtendedFeaturesTest.kt diff --git a/src/main/java/haus/nightmare/a3270/MainActivity.kt b/src/main/java/haus/nightmare/a3270/MainActivity.kt index ebfa691..ce67bfa 100644 --- a/src/main/java/haus/nightmare/a3270/MainActivity.kt +++ b/src/main/java/haus/nightmare/a3270/MainActivity.kt @@ -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(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() } ) diff --git a/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt b/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt index b7972fc..28a97f3 100644 --- a/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt +++ b/src/main/java/haus/nightmare/a3270/TerminalViewModel.kt @@ -146,6 +146,21 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application private val _isLightPenMode = MutableStateFlow(false) val isLightPenMode: StateFlow = _isLightPenMode.asStateFlow() + private val _isCrosshairRuler = MutableStateFlow(AppSettings.isCrosshairRulerEnabled(application)) + val isCrosshairRuler: StateFlow = _isCrosshairRuler.asStateFlow() + + private val _cursorStyle = MutableStateFlow(AppSettings.getCursorStyle(application)) + val cursorStyle: StateFlow = _cursorStyle.asStateFlow() + + private val _blockSelectMode = MutableStateFlow(AppSettings.isBlockSelectModeEnabled(application)) + val blockSelectMode: StateFlow = _blockSelectMode.asStateFlow() + + private val _uiTheme = MutableStateFlow(AppSettings.getUiTheme(application)) + val uiTheme: StateFlow = _uiTheme.asStateFlow() + + private val _isInsertMode = MutableStateFlow(false) + val isInsertMode: StateFlow = _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() + _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) } diff --git a/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt b/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt index d78b9a1..e87ddb4 100644 --- a/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt +++ b/src/main/java/haus/nightmare/a3270/storage/AppSettings.kt @@ -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)) + } + } + } + } } diff --git a/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt b/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt index e92ae47..b9d93fd 100644 --- a/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt +++ b/src/main/java/haus/nightmare/a3270/storage/HostStorage.kt @@ -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"), diff --git a/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt b/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt index 978c526..7e1f6de 100644 --- a/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/ConnectDialog.kt @@ -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(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, diff --git a/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt b/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt index cacbdeb..f15538f 100644 --- a/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt +++ b/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt @@ -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 ) diff --git a/src/main/java/haus/nightmare/a3270/ui/PrinterSessionDialog.kt b/src/main/java/haus/nightmare/a3270/ui/PrinterSessionDialog.kt index df90f75..cb3ab28 100644 --- a/src/main/java/haus/nightmare/a3270/ui/PrinterSessionDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/PrinterSessionDialog.kt @@ -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) diff --git a/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt b/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt index 0fd193d..3b4671f 100644 --- a/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/ScreenExporterDialog.kt @@ -316,20 +316,24 @@ private fun generateHtml(screenBuffer: ScreenBuffer?, rows: Int, cols: Int, mask sb.append("\n\n\n") sb.append("\n") sb.append("\n\n\n
\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) {
                         '<' -> "<"
@@ -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
 }
diff --git a/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt b/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt
index 8f39838..cfc2b3c 100644
--- a/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt
+++ b/src/main/java/haus/nightmare/a3270/ui/SettingsDialog.kt
@@ -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")
+                }
+            }
+        )
+    }
 }
diff --git a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt
index 133d0fa..0b9f452 100644
--- a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt
+++ b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt
@@ -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(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]
     }
diff --git a/src/test/java/haus/nightmare/a3270/DynamicAndParityFeaturesTest.kt b/src/test/java/haus/nightmare/a3270/DynamicAndParityFeaturesTest.kt
new file mode 100644
index 0000000..405efdb
--- /dev/null
+++ b/src/test/java/haus/nightmare/a3270/DynamicAndParityFeaturesTest.kt
@@ -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)
+    }
+}
diff --git a/src/test/java/haus/nightmare/a3270/NvtAndExtendedFeaturesTest.kt b/src/test/java/haus/nightmare/a3270/NvtAndExtendedFeaturesTest.kt
new file mode 100644
index 0000000..f75ef38
--- /dev/null
+++ b/src/test/java/haus/nightmare/a3270/NvtAndExtendedFeaturesTest.kt
@@ -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)
+    }
+}