3 Commits

Author SHA1 Message Date
rudi b81a244bb9 Add bugfixes from testing
Build and Test a3270 / Build Android APK (push) Successful in 2m36s
Release a3270 / Build & Publish Release (push) Successful in 3m58s
2026-09-12 17:22:19 -04:00
rudi 351379b5b3 Merge branch 'main' of git.hugfreevikings.wtf:rudi/a3270
Build and Test a3270 / Build Android APK (push) Successful in 4m20s
# Conflicts:
#	build.sh
#	src/main/java/haus/nightmare/a3270/ui/TerminalView.kt
2026-09-12 15:54:59 +00:00
rudi eba1406cbb updtaes from j3270 2026-09-12 15:50:42 +00:00
7 changed files with 237 additions and 13 deletions
+1
View File
@@ -10,3 +10,4 @@ captures/
*.jar *.jar
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar
*.txt *.txt
*.log*
+5
View File
@@ -45,6 +45,10 @@ if [ ! -d "$J3270_PATH/lib3270j" ]; then
exit 1 exit 1
fi fi
if [ $# -gt 0 ]; then
echo "Running: ./gradlew $@"
./gradlew "$@"
else
echo "Building debug APK..." echo "Building debug APK..."
./gradlew assembleDebug --no-daemon ./gradlew assembleDebug --no-daemon
@@ -56,3 +60,4 @@ if [ -f "$APK_PATH" ]; then
else else
echo "Build completed, but APK not found at $APK_PATH" echo "Build completed, but APK not found at $APK_PATH"
fi fi
fi
Vendored
+20
View File
@@ -115,6 +115,26 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Prefer compatible JDK (JDK 17 or 21) if active Java is incompatible (Java 22+)
if [ -d "/Users/rudi/.sdkman/candidates/java/21.0.6-sem" ]; then
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem"
elif [ -d "$HOME/.sdkman/candidates/java/21.0.2-open" ]; then
CURRENT_JAVA_CMD="${JAVA_HOME:+$JAVA_HOME/bin/java}"
CURRENT_JAVA_CMD="${CURRENT_JAVA_CMD:-java}"
CURRENT_VER=$("$CURRENT_JAVA_CMD" -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1)
if [ -z "$JAVA_HOME" ] || [ "$CURRENT_VER" -gt 21 ] 2>/dev/null; then
export JAVA_HOME="$HOME/.sdkman/candidates/java/21.0.2-open"
fi
fi
if [ -z "$ANDROID_HOME" ]; then
if [ -d "$HOME/Library/Android/sdk" ]; then
export ANDROID_HOME="$HOME/Library/Android/sdk"
elif [ -d "$HOME/Android/Sdk" ]; then
export ANDROID_HOME="$HOME/Android/Sdk"
fi
fi
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -789,21 +789,26 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication()) val globalVerify = AppSettings.isVerifyCertsEnabled(getApplication())
val effectiveVerify = tlsVerifyCert && globalVerify val effectiveVerify = tlsVerifyCert && globalVerify
val gMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr) val gMode = haus.nightmare.lib3270j.graphics.GraphicsMode.fromString(graphicsModeStr)
val effectiveLu = if (luName.isNotBlank()) luName.trim() else (parsedConfig.luName ?: "").trim()
_isTlsActive.value = effectiveTls _isTlsActive.value = effectiveTls
_isTlsVerified.value = effectiveVerify _isTlsVerified.value = effectiveVerify
_oiaText.value = "Connecting to $effectiveHost:$effectivePort" + (if (effectiveTls) " [TLS]" else "") + "..." _oiaText.value = "Connecting to " +
(if (effectiveLu.isNotBlank()) "$effectiveLu@" else "") +
"$effectiveHost:$effectivePort" +
(if (effectiveTls) " [TLS]" else "") + "..."
val config = ConnectionConfig(effectiveHost, effectivePort, model).apply { val config = ConnectionConfig(effectiveHost, effectivePort, model).apply {
if (model == TerminalModel.IBM_DYNAMIC) { if (model == TerminalModel.IBM_DYNAMIC) {
setDynamicDimensions(dynamicRows, dynamicCols) setDynamicDimensions(dynamicRows, dynamicCols)
} }
if (luName.isNotBlank()) { if (effectiveLu.isNotBlank()) {
setLuName(luName) setLuName(effectiveLu)
} }
isUseTls = effectiveTls isUseTls = effectiveTls
isTlsVerifyCert = effectiveVerify isTlsVerifyCert = effectiveVerify
isTn3270eEnabled = effectiveTn3270e isTn3270eEnabled = effectiveTn3270e
isAutoSysUnlock = true
graphicsMode = gMode graphicsMode = gMode
codePage = codePageStr codePage = codePageStr
@@ -728,7 +728,7 @@ private fun copySelection(
} }
} }
private fun getFgColorForAttribute( internal fun getFgColorForAttribute(
ea: ExtendedAttribute, ea: ExtendedAttribute,
currentFieldEa: ExtendedAttribute?, currentFieldEa: ExtendedAttribute?,
currentFA: Byte, currentFA: Byte,
@@ -1,12 +1,18 @@
package haus.nightmare.a3270 package haus.nightmare.a3270
import haus.nightmare.a3270.storage.SavedHost import haus.nightmare.a3270.storage.SavedHost
import haus.nightmare.a3270.ui.getFgColorForAttribute
import haus.nightmare.lib3270j.ConnectionConfig import haus.nightmare.lib3270j.ConnectionConfig
import haus.nightmare.lib3270j.TerminalModel import haus.nightmare.lib3270j.TerminalModel
import haus.nightmare.lib3270j.charset.EbcdicTranslator
import haus.nightmare.lib3270j.datastream.DataStreamProcessor
import haus.nightmare.lib3270j.protocol.DS3270Constants.* import haus.nightmare.lib3270j.protocol.DS3270Constants.*
import haus.nightmare.lib3270j.screen.ExtendedAttribute
import haus.nightmare.lib3270j.screen.ScreenBuffer
import org.json.JSONObject import org.json.JSONObject
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import java.io.ByteArrayOutputStream
import java.util.UUID import java.util.UUID
class DynamicAndParityFeaturesTest { class DynamicAndParityFeaturesTest {
@@ -101,4 +107,73 @@ class DynamicAndParityFeaturesTest {
assertTrue((num and FA_NUMERIC) != 0) assertTrue((num and FA_NUMERIC) != 0)
assertTrue((hi and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL) assertTrue((hi and FA_INT_HIGH_SEL) == FA_INT_HIGH_SEL)
} }
@Test
fun testCharacterAttributesPersistAcrossFields() {
val model = TerminalModel.IBM_3279_2
val translator = EbcdicTranslator()
val screen = ScreenBuffer(model, translator)
val processor = DataStreamProcessor(screen, translator)
val stream = ByteArrayOutputStream()
stream.write(CMD_EW)
stream.write(0xC3) // WCC
// SF(prot, skip)
stream.write(ORDER_SF)
stream.write(FA_PRINTABLE or FA_PROTECT or FA_NUMERIC)
// SA(yellow): XA_FOREGROUND, COLOR_YELLOW (0xF6)
stream.write(ORDER_SA)
stream.write(XA_FOREGROUND)
stream.write(0xF6)
// Data: " 4 "
stream.write(translator.stringToEbcdic(" 4 "))
// Second SF(prot, skip)
stream.write(ORDER_SF)
stream.write(FA_PRINTABLE or FA_PROTECT or FA_NUMERIC)
// Data: "DISPLAY"
stream.write(translator.stringToEbcdic("DISPLAY"))
val record = stream.toByteArray()
processor.processRecord(record, 0, record.size, true)
// " 4 " cells (indices 1..5) should have yellow foreground (0xF6)
for (i in 1..5) {
assertEquals(0xF6.toByte(), screen.getCell(i).fg)
}
// Across second SF, "DISPLAY" cells (indices 7..13) must retain yellow foreground (0xF6) (commit c28c097e)
for (i in 7..13) {
assertEquals(0xF6.toByte(), screen.getCell(i).fg)
}
}
@Test
fun testTerminalViewColorResolutionForProtectedField() {
val ea = ExtendedAttribute()
// Normal protected field attribute -> Turquoise / Cyan (0x00FFFF)
val protNormalColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_PROTECT).toByte())
assertEquals(androidx.compose.ui.graphics.Color(0xFF00FFFF), protNormalColor)
// Intensified protected field attribute -> White (0xFFC0C0C0 per HOST_COLOR_WHITE / HoD palette)
val protHighColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_PROTECT or FA_INT_HIGH_SEL).toByte())
assertEquals(androidx.compose.ui.graphics.Color(0xFFC0C0C0), protHighColor)
// Normal unprotected -> Green (0x00FF00)
val unprotNormalColor = getFgColorForAttribute(ea, null, FA_PRINTABLE.toByte())
assertEquals(androidx.compose.ui.graphics.Color(0xFF00FF00), unprotNormalColor)
// Intensified unprotected -> Red (0xFF0000)
val unprotHighColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_INT_HIGH_SEL).toByte())
assertEquals(androidx.compose.ui.graphics.Color(0xFFFF0000), unprotHighColor)
// Character mode override (SA yellow = 0xF6)
ea.fg = 0xF6.toByte()
val yellowColor = getFgColorForAttribute(ea, null, (FA_PRINTABLE or FA_PROTECT).toByte())
assertEquals(androidx.compose.ui.graphics.Color(0xFFFFFF00), yellowColor)
}
} }
@@ -8,6 +8,8 @@ import haus.nightmare.lib3270j.datastream.DataStreamProcessor
import haus.nightmare.lib3270j.graphics.GocaDecoder import haus.nightmare.lib3270j.graphics.GocaDecoder
import haus.nightmare.lib3270j.graphics.GraphicsPlane import haus.nightmare.lib3270j.graphics.GraphicsPlane
import haus.nightmare.lib3270j.input.InputProcessor import haus.nightmare.lib3270j.input.InputProcessor
import haus.nightmare.lib3270j.graphics.GocaConstants
import haus.nightmare.a3270.ui.calculateGridMetrics
import haus.nightmare.lib3270j.protocol.DS3270Constants.* import haus.nightmare.lib3270j.protocol.DS3270Constants.*
import haus.nightmare.lib3270j.protocol.TelnetConstants.* import haus.nightmare.lib3270j.protocol.TelnetConstants.*
import haus.nightmare.lib3270j.protocol.TN3270EConstants.* import haus.nightmare.lib3270j.protocol.TN3270EConstants.*
@@ -175,4 +177,120 @@ class GraphicsAndSysReqTest {
inputProcessor.sendAid(AID_SYSREQ) inputProcessor.sendAid(AID_SYSREQ)
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.connectionState) assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.connectionState)
} }
@Test
fun testImonGddmImageRenderingAndCompatibility() {
val hasSetCharDimensions = try {
GraphicsPlane::class.java.getMethod("setCharDimensions", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
true
} catch (_: NoSuchMethodException) {
false
}
if (!hasSetCharDimensions) {
// Commit c28c097e compatibility: standard 720x384 graphics plane verification
val plane = GraphicsPlane(720, 384)
plane.setScreenDimensions(80, 24)
val decoder = GocaDecoder(plane)
assertEquals(720, plane.totalWidth)
assertEquals(384, plane.totalHeight)
val out = ByteArrayOutputStream()
out.write(GocaConstants.G_GSCP)
out.write(0x04)
out.write(0x00); out.write(0x10) // X = 16
out.write(0x00); out.write(0x10) // Y = 16
out.write(GocaConstants.G_GSCOL)
out.write(0x01)
decoder.decodeStream(out.toByteArray(), 0, out.size())
val metrics = calculateGridMetrics(1080f, 1920f, 80, 24)
assertTrue(metrics.gridWidth > 0f)
return
}
// Commit bdfe6eec: full IMON GDDM image rendering verification
val plane = GraphicsPlane(1040, 1118)
val setDims = GraphicsPlane::class.java.getMethod("setCharDimensions", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
setDims.invoke(plane, 13, 26)
plane.setScreenDimensions(80, 43)
val decoder = GocaDecoder(plane)
assertEquals(1040, plane.totalWidth)
assertEquals(1118, plane.totalHeight)
assertEquals(520, plane.xMax)
assertEquals(558, plane.yMax)
val out = ByteArrayOutputStream()
// GSCP: Set Current Position (377, 494)
out.write(GocaConstants.G_GSCP)
out.write(0x04)
out.write(0x01); out.write(0x79) // X = 377
out.write(0x01); out.write(0xEE) // Y = 494
// GSCOL: Color 1 = Blue
out.write(GocaConstants.G_GSCOL)
out.write(0x01)
// G_GBIMGC (0x91): Begin Image Current Position: len=6, flags=0, w=80, h=80
out.write(GocaConstants.G_GBIMGC)
out.write(0x06)
out.write(0x00); out.write(0x00)
out.write(0x00); out.write(0x50) // w = 80
out.write(0x00); out.write(0x50) // h = 80
// G_GIMD (0x92): 10 bytes scanline
out.write(GocaConstants.G_GIMD)
out.write(0x0A)
out.write(byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()))
// G_GEIMG (0x93): End Image with 2-byte operand
out.write(GocaConstants.G_GEIMG)
out.write(0x02)
out.write(0x00); out.write(0x00)
// Second image tile: Color 2 = Red
out.write(GocaConstants.G_GSCOL)
out.write(0x02)
out.write(GocaConstants.G_GBIMGC)
out.write(0x06)
out.write(0x00); out.write(0x00)
out.write(0x00); out.write(0x50)
out.write(0x00); out.write(0x50)
out.write(GocaConstants.G_GIMD)
out.write(0x0A)
out.write(byteArrayOf(0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte(), 0xAA.toByte()))
out.write(GocaConstants.G_GEIMG)
out.write(0x02)
out.write(0x00); out.write(0x00)
val stream = out.toByteArray()
decoder.decodeStream(stream, 0, stream.size)
assertTrue(plane.hasContent())
val mappedX = plane.mapX(377)
val mappedY = plane.mapY(494)
assertEquals(897, mappedX)
assertEquals(64, mappedY)
val rgb = plane.rgbBuffer
assertNotNull(rgb)
val pixelAtImg = rgb[mappedY * 1040 + mappedX]
assertNotEquals(0, pixelAtImg)
// Verify grid metrics and UI scaling calculation against IMON canvas
val metrics = calculateGridMetrics(1080f, 1920f, 80, 43)
assertTrue(metrics.gridWidth > 0f)
assertTrue(metrics.gridHeight > 0f)
// Touch scaling test: touch in the middle of grid maps accurately to plane coordinates
val touchScreenX = metrics.offsetX + metrics.gridWidth / 2f
val touchScreenY = metrics.offsetY + metrics.gridHeight / 2f
val planeX = (((touchScreenX - metrics.offsetX) * plane.canvasWidth) / metrics.gridWidth).toInt()
val planeY = (((touchScreenY - metrics.offsetY) * plane.canvasHeight) / metrics.gridHeight).toInt()
assertEquals(plane.canvasWidth / 2, planeX)
assertEquals(plane.canvasHeight / 2, planeY)
}
} }