diff --git a/README.md b/README.md index 2fcf7e6..a3780e8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build & Test](https://git.hugfreevikings.wtf/rudi/a3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/a3270/actions) [![Android](https://img.shields.io/badge/Android-API%2024%2B%20%7C%20Compose-green.svg)](https://developer.android.com) -[![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE) +[![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE) An **x3270-aligned IBM 3270 mainframe terminal emulator for Android**, written in Kotlin using **Jetpack Compose** and powered by the `lib3270j` protocol engine from [j3270](https://git.hugfreevikings.wtf/rudi/j3270). Designed for mobile access to IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E. @@ -93,14 +93,6 @@ build/outputs/apk/debug/a3270-debug.apk --- -## 🤝 Contributing - -Contributions are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker. - -I have contributed no code to this project, it was entirely written by LLMs with my guidance only. - ---- - ## 📜 Acknowledgements - [j3270](https://git.hugfreevikings.wtf/rudi/j3270) — Desktop emulator and `lib3270j` protocol engine diff --git a/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt b/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt index a2b2671..f11ae59 100644 --- a/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt +++ b/src/main/java/org/pubvm/a3270/ui/ConnectDialog.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* @@ -17,6 +18,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -25,6 +29,12 @@ import androidx.compose.ui.window.DialogProperties import org.pubvm.a3270.storage.HostStorage import org.pubvm.a3270.storage.SavedHost +private val TerminalKeyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false, + keyboardType = KeyboardType.Ascii +) + @Composable fun ConnectDialog( onDismiss: () -> Unit, @@ -87,13 +97,14 @@ fun ConnectDialog( } fun saveCurrentProfile(): SavedHost? { - val port = portStr.toIntOrNull() ?: if (useTls) 992 else 23 - if (host.isBlank()) return null - val nameToSave = profileName.ifBlank { "${host.trim()}:$port" } + val port = portStr.trim().toIntOrNull() ?: if (useTls) 992 else 23 + val trimmedHost = host.trim() + if (trimmedHost.isBlank()) return null + val nameToSave = profileName.trim().ifBlank { "$trimmedHost:$port" } val hostToSave = SavedHost( id = selectedHostId ?: java.util.UUID.randomUUID().toString(), name = nameToSave, - host = host.trim(), + host = trimmedHost, port = port, model = modelNum, luName = luName.trim(), @@ -273,6 +284,7 @@ fun ConnectDialog( onValueChange = { profileName = it }, label = { Text("Profile Name (Optional)") }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next), modifier = Modifier.fillMaxWidth() ) @@ -282,18 +294,20 @@ fun ConnectDialog( ) { OutlinedTextField( value = host, - onValueChange = { host = it }, + onValueChange = { host = it.filterNot { c -> c.isWhitespace() } }, label = { Text("Host / IP Address") }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next), modifier = Modifier .weight(0.68f) .focusRequester(focusRequester) ) OutlinedTextField( value = portStr, - onValueChange = { portStr = it }, + onValueChange = { portStr = it.filter { c -> c.isDigit() } }, label = { Text("Port") }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next), modifier = Modifier.weight(0.32f) ) } @@ -417,6 +431,7 @@ fun ConnectDialog( onValueChange = { luName = it }, label = { Text("LU Name / Device Pool (Optional)") }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done), modifier = Modifier.fillMaxWidth() ) diff --git a/src/main/java/org/pubvm/a3270/ui/FileTransferDialog.kt b/src/main/java/org/pubvm/a3270/ui/FileTransferDialog.kt index 0d7bd78..5096a87 100644 --- a/src/main/java/org/pubvm/a3270/ui/FileTransferDialog.kt +++ b/src/main/java/org/pubvm/a3270/ui/FileTransferDialog.kt @@ -2,6 +2,7 @@ package org.pubvm.a3270.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* @@ -9,11 +10,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.lib3270j.ft.FTConfig import org.pubvm.a3270.FTProgressState +private val TerminalKeyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false, + keyboardType = KeyboardType.Ascii +) + @Composable fun FileTransferDialog( initialHostType: String = "TSO", @@ -101,6 +111,7 @@ fun FileTransferDialog( label = { Text("Host Dataset / File Name") }, placeholder = { Text(if (hostType == FTConfig.HostType.TSO) "'USER.DATA'" else "PROFILE EXEC A") }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next), modifier = Modifier.fillMaxWidth() ) @@ -111,6 +122,9 @@ fun FileTransferDialog( placeholder = { Text("sample.txt") }, supportingText = { Text("Relative names will be stored in app Downloads/Files storage", fontSize = 10.sp) }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy( + imeAction = if (!isReceive && hostType == FTConfig.HostType.TSO) ImeAction.Next else ImeAction.Done + ), modifier = Modifier.fillMaxWidth() ) @@ -208,16 +222,18 @@ fun FileTransferDialog( ) { OutlinedTextField( value = lreclStr, - onValueChange = { lreclStr = it }, + onValueChange = { lreclStr = it.filter { c -> c.isDigit() } }, label = { Text("LRECL", fontSize = 11.sp) }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Next), modifier = Modifier.weight(1f) ) OutlinedTextField( value = blksizeStr, - onValueChange = { blksizeStr = it }, + onValueChange = { blksizeStr = it.filter { c -> c.isDigit() } }, label = { Text("BLKSIZE", fontSize = 11.sp) }, singleLine = true, + keyboardOptions = TerminalKeyboardOptions.copy(imeAction = ImeAction.Done), modifier = Modifier.weight(1f) ) } @@ -237,8 +253,8 @@ fun FileTransferDialog( setExistAction(if (overwrite) FTConfig.ExistAction.REPLACE else FTConfig.ExistAction.KEEP) if (!isReceive && hostType == FTConfig.HostType.TSO) { setRecfm(recfm) - lreclStr.toIntOrNull()?.let { setLrecl(it) } - blksizeStr.toIntOrNull()?.let { setBlksize(it) } + lreclStr.trim().toIntOrNull()?.let { setLrecl(it) } + blksizeStr.trim().toIntOrNull()?.let { setBlksize(it) } } } onStartTransfer(config)