Add low-output tooling mode

This commit is contained in:
Joe Tretter
2026-06-26 16:24:45 -05:00
parent 91913a9de8
commit 4b94173ae5
6 changed files with 224 additions and 207 deletions

View File

@@ -20,8 +20,8 @@ android {
applicationId = "solutions.tretter.githugandroid"
minSdk = 26
targetSdk = 35
versionCode = 180
versionName = "0.1.179"
versionCode = 181
versionName = "0.1.180"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true

View File

@@ -117,6 +117,15 @@ class GitRepositoryRuntimeInstrumentedTest {
}
}
@Test
fun visualFileEditorStartsWithActionsVisible() {
launchFreshApp().use {
submitTerminalCommand("edit notes.txt")
waitForTextEditorActions()
}
}
@Test
fun selectingLevelFromLevelsPaneUpdatesExerciseImmediately() {
launchFreshApp().use {
@@ -200,6 +209,7 @@ class GitRepositoryRuntimeInstrumentedTest {
composeRule.onAllNodes(hasText(path, substring = true)).fetchSemanticsNodes().isNotEmpty()
}
composeRule.onNodeWithTag("git-message-editor-path").assertIsDisplayed()
waitForGitEditorActions()
}
private fun waitForGitMessageEditor() {
@@ -207,6 +217,20 @@ class GitRepositoryRuntimeInstrumentedTest {
composeRule.onAllNodesWithTag("git-message-editor-content").fetchSemanticsNodes().isNotEmpty()
}
composeRule.onNodeWithTag("git-message-editor-content").assertIsDisplayed()
waitForGitEditorActions()
}
private fun waitForGitEditorActions() {
composeRule.onNodeWithTag("git-message-editor-save").assertIsDisplayed()
composeRule.onNodeWithTag("git-message-editor-dismiss").assertIsDisplayed()
}
private fun waitForTextEditorActions() {
composeRule.waitUntil(timeoutMillis = 30_000) {
composeRule.onAllNodesWithTag("text-editor-content").fetchSemanticsNodes().isNotEmpty()
}
composeRule.onNodeWithTag("text-editor-save").assertIsDisplayed()
composeRule.onNodeWithTag("text-editor-dismiss").assertIsDisplayed()
}
private fun currentGitEditorContent(): String {

View File

@@ -1,173 +0,0 @@
package solutions.tretter.githugandroid
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
data class GitMessageEditorState(
val invocation: GitEditorInvocation,
val content: String,
)
@Composable
fun GitMessageEditorDialog(
state: GitMessageEditorState,
onContentChange: (String) -> Unit,
onClose: () -> Unit,
onSave: () -> Unit,
) {
val horizontalScroll = rememberScrollState()
val verticalScroll = rememberScrollState()
val dialogScroll = rememberScrollState()
val contentFocusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(state.invocation.command) {
runCatching { contentFocusRequester.requestFocus() }
keyboardController?.show()
}
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Box(
modifier = Modifier
.fillMaxSize()
.imePadding()
.verticalScroll(dialogScroll)
.padding(12.dp),
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 360.dp, max = 720.dp),
color = PanelPrimary,
shape = RoundedCornerShape(8.dp),
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = state.invocation.title,
modifier = Modifier.testTag("git-message-editor-title"),
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
EditorButton(
label = "Save",
enabled = state.content.isNotBlank(),
modifier = Modifier.testTag("git-message-editor-save"),
onClick = onSave,
)
EditorButton(
label = "Dismiss",
modifier = Modifier.testTag("git-message-editor-dismiss"),
onClick = onClose,
)
}
Text(
text = state.invocation.displayPath,
modifier = Modifier.testTag("git-message-editor-path"),
color = TextSecondary,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.heightIn(min = 260.dp)
.background(TerminalBackground, RoundedCornerShape(6.dp))
.padding(10.dp)
.horizontalScroll(horizontalScroll)
.verticalScroll(verticalScroll),
) {
BasicTextField(
value = state.content,
onValueChange = onContentChange,
modifier = Modifier
.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp)
.focusRequester(contentFocusRequester)
.testTag("git-message-editor-content"),
textStyle = TextStyle(
color = TextPrimary,
fontFamily = FontFamily.Monospace,
fontSize = 14.sp,
),
cursorBrush = SolidColor(Accent),
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Ascii,
),
)
}
}
}
}
}
}
@Composable
private fun EditorButton(
label: String,
enabled: Boolean = true,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
enabled = enabled,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = Accent,
contentColor = AppBackground,
disabledContainerColor = PanelTertiary,
disabledContentColor = TextMuted,
),
) {
Text(label)
}
}

View File

@@ -7,13 +7,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
@@ -26,17 +24,16 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
@@ -50,6 +47,11 @@ data class TextEditorState(
val saveAsPath: String,
)
data class GitMessageEditorState(
val invocation: GitEditorInvocation,
val content: String,
)
@Composable
fun TextEditorDialog(
state: TextEditorState,
@@ -57,16 +59,75 @@ fun TextEditorDialog(
onSaveAsPathChange: (String) -> Unit,
onClose: () -> Unit,
onSave: () -> Unit,
) {
ParameterizedTextEditorDialog(
title = "Edit File",
titleTag = null,
metadata = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
metadataTag = null,
metadataStyle = EditorMetadataStyle.Prominent,
content = state.content,
saveAsPath = state.saveAsPath,
saveEnabled = state.saveAsPath.isNotBlank(),
testTagPrefix = "text-editor",
resetKey = "${state.originalCommand}\n${state.path}",
onContentChange = onContentChange,
onSaveAsPathChange = onSaveAsPathChange,
onClose = onClose,
onSave = onSave,
)
}
@Composable
fun GitMessageEditorDialog(
state: GitMessageEditorState,
onContentChange: (String) -> Unit,
onClose: () -> Unit,
onSave: () -> Unit,
) {
ParameterizedTextEditorDialog(
title = state.invocation.title,
titleTag = "git-message-editor-title",
metadata = state.invocation.displayPath,
metadataTag = "git-message-editor-path",
metadataStyle = EditorMetadataStyle.SecondaryMonospace,
content = state.content,
saveAsPath = null,
saveEnabled = state.content.isNotBlank(),
testTagPrefix = "git-message-editor",
resetKey = "${state.invocation.command}\n${state.invocation.displayPath}",
onContentChange = onContentChange,
onSaveAsPathChange = {},
onClose = onClose,
onSave = onSave,
)
}
@Composable
private fun ParameterizedTextEditorDialog(
title: String,
titleTag: String?,
metadata: String,
metadataTag: String?,
metadataStyle: EditorMetadataStyle,
content: String,
saveAsPath: String?,
saveEnabled: Boolean,
testTagPrefix: String,
resetKey: String,
onContentChange: (String) -> Unit,
onSaveAsPathChange: (String) -> Unit,
onClose: () -> Unit,
onSave: () -> Unit,
) {
val editorHorizontalScroll = rememberScrollState()
val editorVerticalScroll = rememberScrollState()
val dialogScroll = rememberScrollState()
val contentFocusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(state.originalCommand, state.path) {
runCatching { contentFocusRequester.requestFocus() }
keyboardController?.show()
LaunchedEffect(resetKey) {
dialogScroll.scrollTo(0)
editorVerticalScroll.scrollTo(0)
editorHorizontalScroll.scrollTo(0)
}
Dialog(
@@ -92,7 +153,8 @@ fun TextEditorDialog(
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = "Edit File",
text = title,
modifier = titleTag?.let { Modifier.testTag(it) } ?: Modifier,
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
@@ -101,26 +163,40 @@ fun TextEditorDialog(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
EditorButton(label = "Save", enabled = state.saveAsPath.isNotBlank(), onClick = onSave)
EditorButton(label = "Dismiss", onClick = onClose)
EditorButton(
label = "Save",
enabled = saveEnabled,
modifier = Modifier.testTag("$testTagPrefix-save"),
onClick = onSave,
)
EditorButton(
label = "Dismiss",
modifier = Modifier.testTag("$testTagPrefix-dismiss"),
onClick = onClose,
)
}
Text(
text = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
color = TextPrimary,
fontWeight = FontWeight.Bold,
)
OutlinedTextField(
value = state.saveAsPath,
onValueChange = onSaveAsPathChange,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Ascii,
imeAction = ImeAction.Done,
),
label = { Text("File name") },
text = metadata,
modifier = metadataTag?.let { Modifier.testTag(it) } ?: Modifier,
color = metadataStyle.color,
fontFamily = metadataStyle.fontFamily,
fontWeight = metadataStyle.fontWeight,
fontSize = metadataStyle.fontSize,
)
saveAsPath?.let { path ->
OutlinedTextField(
value = path,
onValueChange = onSaveAsPathChange,
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Ascii,
imeAction = ImeAction.Done,
),
label = { Text("File name") },
)
}
Box(
modifier = Modifier
.fillMaxWidth()
@@ -132,11 +208,11 @@ fun TextEditorDialog(
.verticalScroll(editorVerticalScroll),
) {
BasicTextField(
value = state.content,
value = content,
onValueChange = onContentChange,
modifier = Modifier
.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp)
.focusRequester(contentFocusRequester),
.testTag("$testTagPrefix-content"),
textStyle = TextStyle(
color = TextPrimary,
fontFamily = FontFamily.Monospace,
@@ -155,15 +231,34 @@ fun TextEditorDialog(
}
}
private enum class EditorMetadataStyle(
val color: Color,
val fontFamily: FontFamily? = null,
val fontWeight: FontWeight? = null,
val fontSize: TextUnit = 14.sp,
) {
Prominent(
color = TextPrimary,
fontWeight = FontWeight.Bold,
),
SecondaryMonospace(
color = TextSecondary,
fontFamily = FontFamily.Monospace,
fontSize = 13.sp,
),
}
@Composable
private fun EditorButton(
label: String,
enabled: Boolean = true,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
enabled = enabled,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = Accent,
contentColor = AppBackground,