Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecae8c0669
|
||
|
|
207fa48f15
|
||
|
|
ffae82bab2
|
@@ -144,6 +144,14 @@ build/outputs/apk/debug/a3270-debug.apk
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| z/VM Logon | GDDM / GOCA Graphics |
|
||||||
|
| :---: | :---: |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📜 Acknowledgements
|
## 📜 Acknowledgements
|
||||||
|
|
||||||
- [j3270](https://git.hugfreevikings.wtf/rudi/j3270) — Desktop emulator and `lib3270j` protocol engine
|
- [j3270](https://git.hugfreevikings.wtf/rudi/j3270) — Desktop emulator and `lib3270j` protocol engine
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "haus.nightmare.a3270"
|
applicationId "haus.nightmare.a3270"
|
||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 9
|
versionCode 10
|
||||||
versionName "1.2.4"
|
versionName "1.2.5"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:versionCode="9"
|
android:versionCode="10"
|
||||||
android:versionName="1.2.4">
|
android:versionName="1.2.5">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val inputExecutor = Executors.newSingleThreadExecutor()
|
private val inputExecutor = Executors.newSingleThreadExecutor()
|
||||||
private val inputDispatcher = inputExecutor.asCoroutineDispatcher()
|
private val inputDispatcher = inputExecutor.asCoroutineDispatcher()
|
||||||
|
|
||||||
// Non-blocking FIFO input channel buffer
|
// Bounded FIFO input channel buffer (prevents unbounded queue growth if worker blocks)
|
||||||
private val inputChannel = Channel<TerminalInputAction>(Channel.UNLIMITED)
|
private val inputChannel = Channel<TerminalInputAction>(256)
|
||||||
|
|
||||||
private val _connectionState = MutableStateFlow(ConnectionState.NOT_CONNECTED)
|
private val _connectionState = MutableStateFlow(ConnectionState.NOT_CONNECTED)
|
||||||
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
|
||||||
|
|||||||
@@ -508,9 +508,14 @@ fun FileTransferDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TSO Send Options
|
// TSO / CMS Allocation & Record Format Options
|
||||||
if (!isReceive && hostType == FTConfig.HostType.TSO) {
|
if (!isReceive && (hostType == FTConfig.HostType.TSO || hostType == FTConfig.HostType.CMS)) {
|
||||||
Text("TSO Dataset Allocation:", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = Color.Gray)
|
Text(
|
||||||
|
if (hostType == FTConfig.HostType.TSO) "TSO Dataset Allocation:" else "VM/CMS File Format:",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Gray
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -542,64 +547,70 @@ fun FileTransferDialog(
|
|||||||
onValueChange = { lreclStr = it.filter { c -> c.isDigit() } },
|
onValueChange = { lreclStr = it.filter { c -> c.isDigit() } },
|
||||||
label = { Text("LRECL", fontSize = 11.sp) },
|
label = { Text("LRECL", fontSize = 11.sp) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
keyboardOptions = TerminalKeyboardOptions.copy(
|
||||||
modifier = Modifier.weight(1f)
|
imeAction = if (hostType == FTConfig.HostType.TSO) ImeAction.Next else ImeAction.Done
|
||||||
|
),
|
||||||
|
modifier = if (hostType == FTConfig.HostType.TSO) Modifier.weight(1f) else Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
if (hostType == FTConfig.HostType.TSO) {
|
||||||
value = blksizeStr,
|
OutlinedTextField(
|
||||||
onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } },
|
value = blksizeStr,
|
||||||
label = { Text("BLKSIZE", fontSize = 11.sp) },
|
onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } },
|
||||||
singleLine = true,
|
label = { Text("BLKSIZE", fontSize = 11.sp) },
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
singleLine = true,
|
||||||
modifier = Modifier.weight(1f)
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
)
|
modifier = Modifier.weight(1f)
|
||||||
}
|
)
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text("Units:", fontSize = 11.sp)
|
|
||||||
Row {
|
|
||||||
listOf(
|
|
||||||
FTConfig.AllocationUnit.TRACKS to "Tracks",
|
|
||||||
FTConfig.AllocationUnit.CYLINDERS to "Cylinders",
|
|
||||||
FTConfig.AllocationUnit.AVBLOCK to "Avblock",
|
|
||||||
FTConfig.AllocationUnit.DEFAULT to "Default"
|
|
||||||
).forEach { (u, label) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = (units == u),
|
|
||||||
onClick = { units = u },
|
|
||||||
label = { Text(label, fontSize = 11.sp) }
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
if (hostType == FTConfig.HostType.TSO) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
OutlinedTextField(
|
verticalAlignment = Alignment.CenterVertically
|
||||||
value = spaceStr,
|
) {
|
||||||
onValueChange = { spaceStr = it },
|
Text("Units:", fontSize = 11.sp)
|
||||||
label = { Text("SPACE (Pri, Sec)", fontSize = 11.sp) },
|
Row {
|
||||||
placeholder = { Text("e.g. 5,2 or 1,1") },
|
listOf(
|
||||||
singleLine = true,
|
FTConfig.AllocationUnit.TRACKS to "Tracks",
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
FTConfig.AllocationUnit.CYLINDERS to "Cylinders",
|
||||||
modifier = Modifier.weight(0.62f)
|
FTConfig.AllocationUnit.AVBLOCK to "Avblock",
|
||||||
)
|
FTConfig.AllocationUnit.DEFAULT to "Default"
|
||||||
OutlinedTextField(
|
).forEach { (u, label) ->
|
||||||
value = dirBlocks,
|
FilterChip(
|
||||||
onValueChange = { dirBlocks = it.filter { c -> c.isDigit() } },
|
selected = (units == u),
|
||||||
label = { Text("Dir (PDS)", fontSize = 11.sp) },
|
onClick = { units = u },
|
||||||
placeholder = { Text("e.g. 10") },
|
label = { Text(label, fontSize = 11.sp) }
|
||||||
singleLine = true,
|
)
|
||||||
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
modifier = Modifier.weight(0.38f)
|
}
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = spaceStr,
|
||||||
|
onValueChange = { spaceStr = it },
|
||||||
|
label = { Text("SPACE (Pri, Sec)", fontSize = 11.sp) },
|
||||||
|
placeholder = { Text("e.g. 5,2 or 1,1") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next),
|
||||||
|
modifier = Modifier.weight(0.62f)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = dirBlocks,
|
||||||
|
onValueChange = { dirBlocks = it.filter { c -> c.isDigit() } },
|
||||||
|
label = { Text("Dir (PDS)", fontSize = 11.sp) },
|
||||||
|
placeholder = { Text("e.g. 10") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done),
|
||||||
|
modifier = Modifier.weight(0.38f)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,13 +648,17 @@ fun FileTransferDialog(
|
|||||||
setTransferMode(if (isAscii) FTConfig.TransferMode.ASCII else FTConfig.TransferMode.BINARY)
|
setTransferMode(if (isAscii) FTConfig.TransferMode.ASCII else FTConfig.TransferMode.BINARY)
|
||||||
setHostType(hostType)
|
setHostType(hostType)
|
||||||
setExistAction(if (overwrite) FTConfig.ExistAction.REPLACE else FTConfig.ExistAction.KEEP)
|
setExistAction(if (overwrite) FTConfig.ExistAction.REPLACE else FTConfig.ExistAction.KEEP)
|
||||||
if (!isReceive && hostType == FTConfig.HostType.TSO) {
|
if (!isReceive) {
|
||||||
setRecfm(recfm)
|
if (hostType == FTConfig.HostType.TSO || hostType == FTConfig.HostType.CMS) {
|
||||||
lreclStr.trim().toIntOrNull()?.let { setLrecl(it) }
|
setRecfm(recfm)
|
||||||
blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) }
|
lreclStr.trim().toIntOrNull()?.let { setLrecl(it) }
|
||||||
setUnits(units)
|
}
|
||||||
if (spaceStr.isNotBlank()) setSpace(spaceStr.trim())
|
if (hostType == FTConfig.HostType.TSO) {
|
||||||
if (dirBlocks.isNotBlank()) setDirectoryBlocks(dirBlocks.trim())
|
blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) }
|
||||||
|
setUnits(units)
|
||||||
|
if (spaceStr.isNotBlank()) setSpace(spaceStr.trim())
|
||||||
|
if (dirBlocks.isNotBlank()) setDirectoryBlocks(dirBlocks.trim())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (hostType == FTConfig.HostType.CMS && cmsOptions.isNotBlank()) {
|
if (hostType == FTConfig.HostType.CMS && cmsOptions.isNotBlank()) {
|
||||||
setOptions(cmsOptions.trim())
|
setOptions(cmsOptions.trim())
|
||||||
|
|||||||
@@ -77,7 +77,12 @@ fun MacroDialog(
|
|||||||
|
|
||||||
fun appendLog(msg: String) {
|
fun appendLog(msg: String) {
|
||||||
val line = "[${java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.US).format(java.util.Date())}] $msg\n"
|
val line = "[${java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.US).format(java.util.Date())}] $msg\n"
|
||||||
executionLog += line
|
val combined = executionLog + line
|
||||||
|
executionLog = if (combined.length > 131072) {
|
||||||
|
combined.substring(combined.length - 131072)
|
||||||
|
} else {
|
||||||
|
combined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(client) {
|
DisposableEffect(client) {
|
||||||
|
|||||||
@@ -126,6 +126,15 @@ fun TerminalView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val paint = remember {
|
||||||
|
Paint().apply {
|
||||||
|
isAntiAlias = true
|
||||||
|
typeface = Typeface.MONOSPACE
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var cachedGraphicsBitmap by remember { mutableStateOf<android.graphics.Bitmap?>(null) }
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -289,12 +298,6 @@ fun TerminalView(
|
|||||||
val offsetX = metrics.offsetX
|
val offsetX = metrics.offsetX
|
||||||
val offsetY = metrics.offsetY
|
val offsetY = metrics.offsetY
|
||||||
|
|
||||||
val paint = Paint().apply {
|
|
||||||
isAntiAlias = true
|
|
||||||
typeface = Typeface.MONOSPACE
|
|
||||||
textAlign = Paint.Align.CENTER
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate optimal font size ensuring characters fit cleanly in cell without crowding
|
// Calculate optimal font size ensuring characters fit cleanly in cell without crowding
|
||||||
val baseTextSize = cellHeight * 0.78f
|
val baseTextSize = cellHeight * 0.78f
|
||||||
paint.textSize = baseTextSize
|
paint.textSize = baseTextSize
|
||||||
@@ -335,8 +338,13 @@ fun TerminalView(
|
|||||||
val gWidth = graphicsPlane.canvasWidth
|
val gWidth = graphicsPlane.canvasWidth
|
||||||
val gHeight = graphicsPlane.canvasHeight
|
val gHeight = graphicsPlane.canvasHeight
|
||||||
val rgb = graphicsPlane.rgbBuffer
|
val rgb = graphicsPlane.rgbBuffer
|
||||||
if (gridW > 0 && gridH > 0 && rgb != null && gWidth > 0 && gHeight > 0) {
|
if (gridW > 0 && gridH > 0 && rgb != null && gWidth > 0 && gHeight > 0 && rgb.size >= gWidth * gHeight) {
|
||||||
val bmp = android.graphics.Bitmap.createBitmap(rgb, gWidth, gHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
var bmp = cachedGraphicsBitmap
|
||||||
|
if (bmp == null || bmp.width != gWidth || bmp.height != gHeight || bmp.isRecycled) {
|
||||||
|
bmp = android.graphics.Bitmap.createBitmap(gWidth, gHeight, android.graphics.Bitmap.Config.ARGB_8888)
|
||||||
|
cachedGraphicsBitmap = bmp
|
||||||
|
}
|
||||||
|
bmp.setPixels(rgb, 0, gWidth, 0, 0, gWidth, gHeight)
|
||||||
drawContext.canvas.nativeCanvas.drawBitmap(
|
drawContext.canvas.nativeCanvas.drawBitmap(
|
||||||
bmp,
|
bmp,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -229,6 +229,25 @@ class ConnectionAndHostDirectoryTest {
|
|||||||
assertEquals(FTConfig.HostType.CMS, state.hostType)
|
assertEquals(FTConfig.HostType.CMS, state.hostType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCmsFileTransferCommandGeneration() {
|
||||||
|
val cfg = FTConfig().apply {
|
||||||
|
setHostFilename("PROFILE EXEC A")
|
||||||
|
setLocalFilename("/tmp/profile.exec")
|
||||||
|
setDirection(FTConfig.Direction.SEND)
|
||||||
|
setTransferMode(FTConfig.TransferMode.ASCII)
|
||||||
|
setHostType(FTConfig.HostType.CMS)
|
||||||
|
setExistAction(FTConfig.ExistAction.REPLACE)
|
||||||
|
setRecfm(FTConfig.RecordFormat.FIXED)
|
||||||
|
setLrecl(80)
|
||||||
|
}
|
||||||
|
val cmd = cfg.buildCommand()
|
||||||
|
assertTrue("CMS command must contain RECFM F", cmd.contains("RECFM F"))
|
||||||
|
assertTrue("CMS command must contain LRECL 80", cmd.contains("LRECL 80"))
|
||||||
|
assertFalse("CMS command must not contain REPLACE keyword", cmd.contains("REPLACE"))
|
||||||
|
assertFalse("CMS command must not contain BLOCK keyword", cmd.contains("BLOCK"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testConnectChoiceEnum() {
|
fun testConnectChoiceEnum() {
|
||||||
val choices = ConnectChoice.values()
|
val choices = ConnectChoice.values()
|
||||||
|
|||||||
Reference in New Issue
Block a user