Debug input 2
Build and Test a3270 / Build Android APK (push) Successful in 4m25s

This commit is contained in:
2026-08-21 01:03:14 -04:00
parent 96ee5ebce1
commit cf58b5c72c
7 changed files with 179 additions and 81 deletions
+3 -1
View File
@@ -1,7 +1,6 @@
# a3270 # a3270
[![Build & Test](https://git.hugfreevikings.wtf/rudi/a3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/a3270/actions) [![Build & Test](https://git.hugfreevikings.wtf/rudi/a3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/a3270/actions)
[![Latest Release](https://git.hugfreevikings.wtf/rudi/a3270/badges/release.svg)](https://git.hugfreevikings.wtf/rudi/a3270/releases)
[![Android](https://img.shields.io/badge/Android-API%2024%2B%20%7C%20Compose-green.svg)](https://developer.android.com) [![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](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE)
@@ -110,3 +109,6 @@ I have contributed no code to this project, it was entirely written by LLMs with
- Claude Opus 4.6 - Claude Opus 4.6
- Gemini 3.1 Pro - Gemini 3.1 Pro
- Gemini 3.7 Flash - Gemini 3.7 Flash
- Gemma4 12B and 26B
- GPT-OSS 120B
- Qwen3 4B and 32B
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "org.pubvm.a3270" applicationId "org.pubvm.a3270"
minSdk 24 minSdk 24
targetSdk 34 targetSdk 34
versionCode 3 versionCode 4
versionName "0.1.2" versionName "0.1.3"
} }
buildTypes { buildTypes {
@@ -147,6 +147,7 @@ fun MainScreen(
val screenVersion by viewModel.screenVersion.collectAsState() val screenVersion by viewModel.screenVersion.collectAsState()
val maskHiddenInput by viewModel.maskHiddenInput.collectAsState() val maskHiddenInput by viewModel.maskHiddenInput.collectAsState()
val cursorBlink by viewModel.cursorBlink.collectAsState() val cursorBlink by viewModel.cursorBlink.collectAsState()
val hapticFeedback by viewModel.hapticFeedback.collectAsState()
val ftState by viewModel.ftState.collectAsState() val ftState by viewModel.ftState.collectAsState()
var showConnectDialog by remember { mutableStateOf(false) } var showConnectDialog by remember { mutableStateOf(false) }
@@ -249,6 +250,7 @@ fun MainScreen(
TwoRowKeyBar( TwoRowKeyBar(
connectionState = connectionState, connectionState = connectionState,
isShiftPressed = isShiftPressed, isShiftPressed = isShiftPressed,
hapticFeedbackEnabled = hapticFeedback,
onClearShift = onClearShift, onClearShift = onClearShift,
onConnectClick = { showConnectDialog = true }, onConnectClick = { showConnectDialog = true },
onDisconnectClick = { viewModel.disconnect() }, onDisconnectClick = { viewModel.disconnect() },
@@ -318,10 +320,11 @@ fun MainScreen(
ftProgressState = ftState, ftProgressState = ftState,
onDismiss = { showFtDialog = false }, onDismiss = { showFtDialog = false },
onStartTransfer = { config -> onStartTransfer = { config ->
val error = viewModel.startFileTransfer(config) viewModel.startFileTransfer(config) { error ->
if (error != null) { if (error != null) {
Toast.makeText(context, error, Toast.LENGTH_LONG).show() Toast.makeText(context, error, Toast.LENGTH_LONG).show()
} }
}
}, },
onCancelTransfer = { onCancelTransfer = {
viewModel.cancelFileTransfer() viewModel.cancelFileTransfer()
@@ -333,10 +336,11 @@ fun MainScreen(
SettingsDialog( SettingsDialog(
initialMaskHiddenInput = maskHiddenInput, initialMaskHiddenInput = maskHiddenInput,
initialCursorBlink = cursorBlink, initialCursorBlink = cursorBlink,
initialHapticFeedback = hapticFeedback,
onDismiss = { showSettingsDialog = false }, onDismiss = { showSettingsDialog = false },
onSave = { mask, blink -> onSave = { mask, blink, haptic ->
showSettingsDialog = false showSettingsDialog = false
viewModel.updateSettings(mask, blink) viewModel.updateSettings(mask, blink, haptic)
terminalInputViewRef?.showSoftKeyboard() terminalInputViewRef?.showSoftKeyboard()
} }
) )
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.lib3270j.ConnectionConfig import org.lib3270j.ConnectionConfig
import org.lib3270j.ConnectionState import org.lib3270j.ConnectionState
import org.lib3270j.Telnet3270Client import org.lib3270j.Telnet3270Client
@@ -92,6 +93,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
private val _cursorBlink = MutableStateFlow(AppSettings.isCursorBlinkEnabled(application)) private val _cursorBlink = MutableStateFlow(AppSettings.isCursorBlinkEnabled(application))
val cursorBlink: StateFlow<Boolean> = _cursorBlink.asStateFlow() val cursorBlink: StateFlow<Boolean> = _cursorBlink.asStateFlow()
private val _hapticFeedback = MutableStateFlow(AppSettings.isHapticFeedbackEnabled(application))
val hapticFeedback: StateFlow<Boolean> = _hapticFeedback.asStateFlow()
// File Transfer State // File Transfer State
private var fileTransferCoordinator: FileTransfer? = null private var fileTransferCoordinator: FileTransfer? = null
private val _ftState = MutableStateFlow(FTProgressState()) private val _ftState = MutableStateFlow(FTProgressState())
@@ -191,11 +195,13 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
} }
} }
fun updateSettings(maskHidden: Boolean, blink: Boolean) { fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean) {
AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden) AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden)
AppSettings.setCursorBlinkEnabled(getApplication(), blink) AppSettings.setCursorBlinkEnabled(getApplication(), blink)
AppSettings.setHapticFeedbackEnabled(getApplication(), haptic)
_maskHiddenInput.value = maskHidden _maskHiddenInput.value = maskHidden
_cursorBlink.value = blink _cursorBlink.value = blink
_hapticFeedback.value = haptic
} }
fun connect(host: String, port: Int = 23, modelNum: Int = 2, luName: String = "", hostType: String = "TSO") { fun connect(host: String, port: Int = 23, modelNum: Int = 2, luName: String = "", hostType: String = "TSO") {
@@ -316,9 +322,17 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
} }
} }
fun startFileTransfer(config: FTConfig): String? { fun startFileTransfer(config: FTConfig, onResult: (String?) -> Unit = {}) {
val c = client ?: return "Terminal is not connected." val c = client
if (c == null || !_connectionState.value.isConnected()) {
val err = "Terminal is not connected."
_ftState.value = FTProgressState(isActive = false, statusMessage = err, isError = true)
onResult(err)
return
}
viewModelScope.launch(Dispatchers.IO) {
try {
// Resolve local path if relative (default to app files or download directory) // Resolve local path if relative (default to app files or download directory)
val rawPath = config.localFilename val rawPath = config.localFilename
if (!rawPath.startsWith("/")) { if (!rawPath.startsWith("/")) {
@@ -382,11 +396,31 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
isError = true isError = true
) )
} }
return err withContext(Dispatchers.Main) {
onResult(err)
}
} catch (e: Exception) {
log.warning("Error in startFileTransfer: ${e.message}")
_ftState.value = FTProgressState(
isActive = false,
statusMessage = "Transfer error: ${e.message}",
isError = true
)
withContext(Dispatchers.Main) {
onResult(e.message)
}
}
}
} }
fun cancelFileTransfer() { fun cancelFileTransfer() {
viewModelScope.launch(Dispatchers.IO) {
try {
fileTransferCoordinator?.cancel() fileTransferCoordinator?.cancel()
} catch (e: Exception) {
log.warning("Error cancelling transfer: ${e.message}")
}
}
} }
fun typeChar(ch: Char) { fun typeChar(ch: Char) {
@@ -7,6 +7,7 @@ object AppSettings {
private const val PREFS_NAME = "a3270_settings" private const val PREFS_NAME = "a3270_settings"
private const val KEY_MASK_HIDDEN_INPUT = "mask_hidden_input" private const val KEY_MASK_HIDDEN_INPUT = "mask_hidden_input"
private const val KEY_CURSOR_BLINK = "cursor_blink" private const val KEY_CURSOR_BLINK = "cursor_blink"
private const val KEY_HAPTIC_FEEDBACK = "haptic_feedback"
private fun getPrefs(context: Context): SharedPreferences { private fun getPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
@@ -27,4 +28,12 @@ object AppSettings {
fun setCursorBlinkEnabled(context: Context, enabled: Boolean) { fun setCursorBlinkEnabled(context: Context, enabled: Boolean) {
getPrefs(context).edit().putBoolean(KEY_CURSOR_BLINK, enabled).apply() getPrefs(context).edit().putBoolean(KEY_CURSOR_BLINK, enabled).apply()
} }
fun isHapticFeedbackEnabled(context: Context): Boolean {
return getPrefs(context).getBoolean(KEY_HAPTIC_FEEDBACK, true)
}
fun setHapticFeedbackEnabled(context: Context, enabled: Boolean) {
getPrefs(context).edit().putBoolean(KEY_HAPTIC_FEEDBACK, enabled).apply()
}
} }
@@ -1,20 +1,20 @@
package org.pubvm.a3270.ui package org.pubvm.a3270.ui
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.focus.focusProperties
import org.lib3270j.ConnectionState import org.lib3270j.ConnectionState
import org.lib3270j.protocol.DS3270Constants.* import org.lib3270j.protocol.DS3270Constants.*
@@ -22,6 +22,7 @@ import org.lib3270j.protocol.DS3270Constants.*
fun TwoRowKeyBar( fun TwoRowKeyBar(
connectionState: ConnectionState, connectionState: ConnectionState,
isShiftPressed: Boolean = false, isShiftPressed: Boolean = false,
hapticFeedbackEnabled: Boolean = true,
onClearShift: () -> Unit = {}, onClearShift: () -> Unit = {},
onConnectClick: () -> Unit, onConnectClick: () -> Unit,
onDisconnectClick: () -> Unit, onDisconnectClick: () -> Unit,
@@ -58,6 +59,7 @@ fun TwoRowKeyBar(
label = "", label = "",
color = Color(0xFF343A40), color = Color(0xFF343A40),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
hapticFeedbackEnabled = hapticFeedbackEnabled,
innerPaddingHorizontal = 0.dp, innerPaddingHorizontal = 0.dp,
onClick = { menuExpanded = true } onClick = { menuExpanded = true }
) )
@@ -106,6 +108,7 @@ fun TwoRowKeyBar(
label = tabLabel, label = tabLabel,
color = Color(0xFF1C7ED6), color = Color(0xFF1C7ED6),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
hapticFeedbackEnabled = hapticFeedbackEnabled,
innerPaddingHorizontal = 0.dp, innerPaddingHorizontal = 0.dp,
onClick = { onClick = {
if (isShiftPressed) { if (isShiftPressed) {
@@ -118,34 +121,34 @@ fun TwoRowKeyBar(
) )
// 3. RESET // 3. RESET
KeyButton("RESET", Color(0xFFE67700), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onReset) KeyButton("RESET", Color(0xFFE67700), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onReset)
// 4. ENTER // 4. ENTER
KeyButton("ENTER", Color(0xFF2B8A3E), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_ENTER) }) KeyButton("ENTER", Color(0xFF2B8A3E), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_ENTER) })
// 5. CLEAR // 5. CLEAR
KeyButton("CLEAR", Color(0xFFC92A2A), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_CLEAR) }) KeyButton("CLEAR", Color(0xFFC92A2A), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_CLEAR) })
// 6. PA1 // 6. PA1
KeyButton("PA1", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA1) }) KeyButton("PA1", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA1) })
// 7. PA2 // 7. PA2
KeyButton("PA2", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA2) }) KeyButton("PA2", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA2) })
// 8. PA3 // 8. PA3
KeyButton("PA3", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA3) }) KeyButton("PA3", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA3) })
// 9. Left Navigation // 9. Left Navigation
KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorLeft) KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorLeft)
// 10. Up Navigation // 10. Up Navigation
KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorUp) KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorUp)
// 11. Down Navigation // 11. Down Navigation
KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorDown) KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorDown)
// 12. Right Navigation // 12. Right Navigation
KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorRight) KeyButton("", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorRight)
} }
Spacer(modifier = Modifier.height(1.dp).fillMaxWidth().background(Color(0xFF2C2D30))) Spacer(modifier = Modifier.height(1.dp).fillMaxWidth().background(Color(0xFF2C2D30)))
@@ -174,6 +177,7 @@ fun TwoRowKeyBar(
label = "F$i", label = "F$i",
color = Color(0xFF364FC7), color = Color(0xFF364FC7),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
hapticFeedbackEnabled = hapticFeedbackEnabled,
innerPaddingHorizontal = 0.dp, innerPaddingHorizontal = 0.dp,
onClick = { onClick = {
onSendAid(aid) onSendAid(aid)
@@ -193,10 +197,17 @@ private fun KeyButton(
color: Color, color: Color,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
hapticFeedbackEnabled: Boolean = true,
innerPaddingHorizontal: androidx.compose.ui.unit.Dp = 0.dp innerPaddingHorizontal: androidx.compose.ui.unit.Dp = 0.dp
) { ) {
val view = LocalView.current
Surface( Surface(
onClick = onClick, onClick = {
if (hapticFeedbackEnabled) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
onClick()
},
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
color = color, color = color,
shadowElevation = 1.dp, shadowElevation = 1.dp,
@@ -1,7 +1,9 @@
package org.pubvm.a3270.ui package org.pubvm.a3270.ui
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -16,11 +18,13 @@ import androidx.compose.ui.window.Dialog
fun SettingsDialog( fun SettingsDialog(
initialMaskHiddenInput: Boolean, initialMaskHiddenInput: Boolean,
initialCursorBlink: Boolean, initialCursorBlink: Boolean,
initialHapticFeedback: Boolean = true,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean) -> Unit onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean) -> Unit
) { ) {
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) } var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
var cursorBlink by remember { mutableStateOf(initialCursorBlink) } var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = onDismiss) {
Card( Card(
@@ -33,6 +37,7 @@ fun SettingsDialog(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(20.dp) .padding(20.dp)
) { ) {
Text( Text(
@@ -106,6 +111,39 @@ fun SettingsDialog(
) )
} }
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = Color(0xFF2C2D30))
Spacer(modifier = Modifier.height(16.dp))
// Setting 3: Button Haptic Feedback
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
Text(
text = "Button Haptic Feedback",
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
Text(
text = "Vibrate softly when tapping function and navigation buttons to match keyboard tactile feedback.",
fontSize = 12.sp,
color = Color.LightGray
)
}
Switch(
checked = hapticFeedback,
onCheckedChange = { hapticFeedback = it },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = Color(0xFF2B8A3E)
)
)
}
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Action Buttons // Action Buttons
@@ -119,7 +157,7 @@ fun SettingsDialog(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Button( Button(
onClick = { onClick = {
onSave(maskHiddenInput, cursorBlink) onSave(maskHiddenInput, cursorBlink, hapticFeedback)
}, },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E)) colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
) { ) {