This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -257,11 +257,11 @@ 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 gWidth = graphicsPlane.canvasWidth
|
||||
val gHeight = graphicsPlane.canvasHeight
|
||||
val rgb = graphicsPlane.rgbBuffer
|
||||
if (rgb != null) {
|
||||
val bmp = android.graphics.Bitmap.createBitmap(rgb, gridW, gridH, android.graphics.Bitmap.Config.ARGB_8888)
|
||||
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,
|
||||
@@ -270,7 +270,6 @@ fun TerminalView(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw Crosshair Ruler if enabled
|
||||
if (crosshairRuler && cols > 0 && rows > 0) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user