diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 8f40f8d..dbabe51 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -41,6 +41,12 @@ jobs: run: | chmod +x ./gradlew ./build.sh + - name: Run Unit Tests + run: | + ./gradlew testDebugUnitTest --no-daemon + env: + J3270_DIR: ${{ github.workspace }}/j3270 + - name: Build Android APK run: | ./gradlew assembleDebug --no-daemon diff --git a/src/main/java/haus/nightmare/a3270/MainActivity.kt b/src/main/java/haus/nightmare/a3270/MainActivity.kt index 055073b..0393f9e 100644 --- a/src/main/java/haus/nightmare/a3270/MainActivity.kt +++ b/src/main/java/haus/nightmare/a3270/MainActivity.kt @@ -814,6 +814,7 @@ fun MainScreen( inhibitReason = viewModel.getClient()?.oia?.inputInhibited ?: 0, luName = viewModel.getClient()?.telnetFSM?.connectedLu ?: "", isNumericField = isNumericField, + uiTheme = uiTheme, onToggleDocMode = { viewModel.toggleDocMode() }, onToggleWordWrap = { viewModel.toggleWordWrap() }, onToggleAplMode = { viewModel.toggleAplMode() }, diff --git a/src/main/java/haus/nightmare/a3270/ui/FileTransferDialog.kt b/src/main/java/haus/nightmare/a3270/ui/FileTransferDialog.kt index c06f14c..e59cc21 100644 --- a/src/main/java/haus/nightmare/a3270/ui/FileTransferDialog.kt +++ b/src/main/java/haus/nightmare/a3270/ui/FileTransferDialog.kt @@ -51,6 +51,42 @@ data class LastTransferState( ) private var lastTransferState: LastTransferState? = null +fun formatHostFilename(localName: String, hostType: FTConfig.HostType): String { + if (hostType == FTConfig.HostType.CMS) { + val lastDot = localName.lastIndexOf('.') + return if (lastDot > 0) { + val fn = localName.substring(0, lastDot).replace(Regex("[^a-zA-Z0-9]"), "").uppercase() + val ft = localName.substring(lastDot + 1).replace(Regex("[^a-zA-Z0-9]"), "").uppercase() + "${fn.ifBlank { "FILE" }} ${ft.ifBlank { "TEXT" }} A" + } else { + val clean = localName.replace(Regex("[^a-zA-Z0-9]"), "").uppercase() + "${clean.ifBlank { "FILE" }} FILE A" + } + } else if (hostType == FTConfig.HostType.TSO) { + val cleanBase = localName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase() + return "'${cleanBase.ifBlank { "DATA" }}'" + } + return localName +} + +fun formatLocalFilenameFromHost(hostFile: String, hostType: FTConfig.HostType): String { + val cleanName = hostFile.trim('\'', '"') + if (hostType == FTConfig.HostType.CMS) { + val tokens = cleanName.trim().split(Regex("\\s+")) + if (tokens.size >= 2) { + return "${tokens[0].lowercase()}.${tokens[1].lowercase()}" + } else if (tokens.size == 1 && tokens[0].isNotBlank()) { + return "${tokens[0].lowercase()}.txt" + } + } else { + val lastDot = cleanName.lastIndexOf('.') + if (lastDot > 0) { + return "${cleanName.substring(lastDot + 1).lowercase()}.txt" + } + } + return if (cleanName.isNotBlank()) "${cleanName.lowercase()}.txt" else "download.txt" +} + @Composable fun FileTransferDialog( initialHostType: String = "TSO", @@ -102,8 +138,7 @@ fun FileTransferDialog( localFile = tempFile.absolutePath localDisplayName = displayName if (hostFile.isBlank()) { - val cleanBase = displayName.substringBeforeLast('.').replace(Regex("[^a-zA-Z0-9]"), "").uppercase() - hostFile = if (hostType == FTConfig.HostType.TSO) "'$cleanBase'" else "$cleanBase FILE A" + hostFile = formatHostFilename(displayName, hostType) } Toast.makeText(context, "Selected: $displayName", Toast.LENGTH_SHORT).show() } catch (e: Exception) { @@ -159,8 +194,7 @@ fun FileTransferDialog( onSelectDataset = { selected -> hostFile = selected if (localFile.isBlank()) { - val base = selected.trim('\'').substringAfterLast('.').substringAfterLast(' ') - val safeName = if (base.isNotBlank()) "$base.txt" else "download.txt" + val safeName = formatLocalFilenameFromHost(selected, hostType) val tempDir = File(context.cacheDir, "ft_download").apply { if (!exists()) mkdirs() } val tempFile = File(tempDir, safeName) localFile = tempFile.absolutePath @@ -377,8 +411,7 @@ fun FileTransferDialog( onClick = { if (isReceive) { val defName = if (hostFile.isNotBlank()) { - val base = hostFile.trim('\'').substringAfterLast('.').substringAfterLast(' ') - if (base.isNotBlank()) "$base.txt" else "download.txt" + formatLocalFilenameFromHost(hostFile, hostType) } else "download.txt" createDocumentLauncher.launch(defName) } else { diff --git a/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt b/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt index 4701cf8..f87d400 100644 --- a/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt +++ b/src/main/java/haus/nightmare/a3270/ui/OiaStatusBar.kt @@ -40,8 +40,17 @@ fun OiaStatusBar( inhibitReason: Int = 0, luName: String = "", isNumericField: Boolean = false, + uiTheme: String = "DARK", modifier: Modifier = Modifier ) { + val isLight = uiTheme.equals("LIGHT", ignoreCase = true) + val barBg = if (isLight) Color(0xFFE9ECEF) else Color(0xFF161719) + val defaultTextClr = if (isLight) Color(0xFF212529) else Color.White + val sysAvailClr = if (isLight) Color(0xFF005AC8) else Color(0xFF7890F0) + val inputInhibitedClr = if (isLight) Color(0xFF141414) else Color(0xFFFFFFFF) + val attentionClr = if (isLight) Color(0xFFB46400) else Color(0xFFFFFF00) + val commCheckClr = if (isLight) Color(0xFFBE1414) else Color(0xFFFF0000) + 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("%03d/%03d [%04d]", row, col, cursorAddr) @@ -72,22 +81,24 @@ fun OiaStatusBar( else -> "X LOCKED" } 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) + haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_COMM_CHECK -> commCheckClr + haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OPERATOR_DUE -> attentionClr + haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_NUMERIC_ONLY -> attentionClr + haus.nightmare.lib3270j.ecl.ECLConstants.INHIBIT_OVERFLOW -> attentionClr + else -> inputInhibitedClr } txt to clr } else if (isKeyboardLocked) { - "X SYSTEM" to Color(0xFFFFFFFF) + "X SYSTEM" to inputInhibitedClr } else { - "READY" to Color(0xFF7890F0) + "READY" to sysAvailClr } Row( modifier = modifier .fillMaxWidth() .height(26.dp) - .background(Color(0xFF161719)) + .background(barBg) .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween @@ -105,7 +116,7 @@ fun OiaStatusBar( Spacer(modifier = Modifier.width(6.dp)) Text( text = hostText, - color = Color.White, + color = defaultTextClr, fontSize = 11.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold, @@ -316,7 +327,7 @@ fun OiaStatusBar( // Cursor Position & Buffer Address Text( text = posStr, - color = Color(0xFF22B8CF), + color = if (isLight) Color(0xFF0C8599) else Color(0xFF22B8CF), fontSize = 10.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold diff --git a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt index c1e2e38..2d81b1f 100644 --- a/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt +++ b/src/main/java/haus/nightmare/a3270/ui/TerminalView.kt @@ -257,18 +257,17 @@ fun TerminalView( 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 - ) - } + val gWidth = graphicsPlane.canvasWidth + val gHeight = graphicsPlane.canvasHeight + val rgb = graphicsPlane.rgbBuffer + if (gridW > 0 && gridH > 0 && rgb != null && gWidth > 0 && gHeight > 0) { + val bmp = android.graphics.Bitmap.createBitmap(rgb, gWidth, gHeight, android.graphics.Bitmap.Config.ARGB_8888) + drawContext.canvas.nativeCanvas.drawBitmap( + bmp, + null, + android.graphics.RectF(offsetX, offsetY, offsetX + gridW.toFloat(), offsetY + gridH.toFloat()), + null + ) } } diff --git a/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt b/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt index d129785..ab171eb 100644 --- a/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt +++ b/src/test/java/haus/nightmare/a3270/EntryAssistAndModesTest.kt @@ -236,4 +236,20 @@ class EntryAssistAndModesTest { assertEquals("false", parsed["modes.numericlock"]) assertEquals("true", parsed["modes.autoskip"]) } + + @Test + fun testCmsFilenameFormatting() { + // Upload (local -> host) + assertEquals("PROFILE EXEC A", haus.nightmare.a3270.ui.formatHostFilename("profile.exec", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS)) + assertEquals("TEST TXT A", haus.nightmare.a3270.ui.formatHostFilename("test.txt", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS)) + assertEquals("README FILE A", haus.nightmare.a3270.ui.formatHostFilename("README", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS)) + + // Download (host -> local) + assertEquals("profile.exec", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("PROFILE EXEC A", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS)) + assertEquals("test.data", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("TEST DATA A1", haus.nightmare.lib3270j.ft.FTConfig.HostType.CMS)) + + // TSO + assertEquals("'MYDATA'", haus.nightmare.a3270.ui.formatHostFilename("mydata.txt", haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO)) + assertEquals("dataset.txt", haus.nightmare.a3270.ui.formatLocalFilenameFromHost("'USER.DATASET'", haus.nightmare.lib3270j.ft.FTConfig.HostType.TSO)) + } }