Add app-native interactive Git dialogs
- Open an interactive staging dialog for `git add -i`, `git stage -i`, and `git add --interactive`, allowing paths to be selected before staging. - Open an interactive rebase dialog for `git rebase -i` / `--interactive`, with pick, squash, drop, commit-message editing, and reorder controls. - Keep the existing synthetic runtime behavior available for direct engine/test calls, while making app-entered interactive commands show real UI instead of auto-staging. - Center manpage search navigation using actual text-layout highlight bounds instead of approximate line and column math. - Make editor dialogs use a full-screen IME-padded outer scroll container so controls remain reachable when the keyboard pans or resizes the dialog.
This commit is contained in:
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 131
|
versionCode = 132
|
||||||
versionName = "0.1.130"
|
versionName = "0.1.131"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ fun GitHugApp() {
|
|||||||
var hasRestoredProgress by remember { mutableStateOf(false) }
|
var hasRestoredProgress by remember { mutableStateOf(false) }
|
||||||
var editorState by remember { mutableStateOf<TextEditorState?>(null) }
|
var editorState by remember { mutableStateOf<TextEditorState?>(null) }
|
||||||
var gitMessageEditorState by remember { mutableStateOf<GitMessageEditorState?>(null) }
|
var gitMessageEditorState by remember { mutableStateOf<GitMessageEditorState?>(null) }
|
||||||
|
var interactiveAddState by remember { mutableStateOf<InteractiveAddState?>(null) }
|
||||||
|
var interactiveRebaseState by remember { mutableStateOf<InteractiveRebaseState?>(null) }
|
||||||
var manPageState by remember { mutableStateOf<ManPageState?>(null) }
|
var manPageState by remember { mutableStateOf<ManPageState?>(null) }
|
||||||
var solvedCelebrationTitle by remember { mutableStateOf<String?>(null) }
|
var solvedCelebrationTitle by remember { mutableStateOf<String?>(null) }
|
||||||
var showTerminalInputHint by remember { mutableStateOf(true) }
|
var showTerminalInputHint by remember { mutableStateOf(true) }
|
||||||
@@ -209,6 +211,8 @@ fun GitHugApp() {
|
|||||||
historyDraft = ""
|
historyDraft = ""
|
||||||
editorState = null
|
editorState = null
|
||||||
gitMessageEditorState = null
|
gitMessageEditorState = null
|
||||||
|
interactiveAddState = null
|
||||||
|
interactiveRebaseState = null
|
||||||
manPageState = null
|
manPageState = null
|
||||||
applyRecommendedPaneWeights(persist = false)
|
applyRecommendedPaneWeights(persist = false)
|
||||||
}
|
}
|
||||||
@@ -227,6 +231,8 @@ fun GitHugApp() {
|
|||||||
historyDraft = ""
|
historyDraft = ""
|
||||||
editorState = null
|
editorState = null
|
||||||
gitMessageEditorState = null
|
gitMessageEditorState = null
|
||||||
|
interactiveAddState = null
|
||||||
|
interactiveRebaseState = null
|
||||||
manPageState = null
|
manPageState = null
|
||||||
paneLayout = paneLayout.copy(
|
paneLayout = paneLayout.copy(
|
||||||
weights = paneLayout.weights + recommendedPaneWeights(
|
weights = paneLayout.weights + recommendedPaneWeights(
|
||||||
@@ -418,6 +424,59 @@ fun GitHugApp() {
|
|||||||
applyCommandResult(state.invocation.command, newRepo, lines, echoCommand = false)
|
applyCommandResult(state.invocation.command, newRepo, lines, echoCommand = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openInteractiveAdd(invocation: InteractiveAddInvocation) {
|
||||||
|
output = buildList {
|
||||||
|
addAll(output)
|
||||||
|
add("$ ${invocation.command}")
|
||||||
|
add("Opened interactive add")
|
||||||
|
}
|
||||||
|
interactiveAddState = InteractiveAddState(
|
||||||
|
invocation = invocation,
|
||||||
|
files = repo.files,
|
||||||
|
)
|
||||||
|
applyRecommendedPaneWeights(persist = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stageInteractiveSelection(selectedFiles: Set<String>) {
|
||||||
|
val state = interactiveAddState ?: return
|
||||||
|
val updatedRepo = repo.copy(
|
||||||
|
files = repo.files.map { file ->
|
||||||
|
if (file.name in selectedFiles) file.copy(staged = true) else file
|
||||||
|
},
|
||||||
|
)
|
||||||
|
interactiveAddState = null
|
||||||
|
val lines = if (selectedFiles.isEmpty()) {
|
||||||
|
listOf("No changes staged")
|
||||||
|
} else {
|
||||||
|
listOf("staged ${selectedFiles.size} path(s)") + selectedFiles.sorted().map { " $it" }
|
||||||
|
}
|
||||||
|
applyCommandResult(state.invocation.command, updatedRepo, lines, echoCommand = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openInteractiveRebase(invocation: InteractiveRebaseInvocation) {
|
||||||
|
val count = invocation.commitCount ?: repo.commits.size
|
||||||
|
val commits = repo.commits.takeLast(count.coerceAtLeast(0))
|
||||||
|
output = buildList {
|
||||||
|
addAll(output)
|
||||||
|
add("$ ${invocation.command}")
|
||||||
|
add("Opened interactive rebase")
|
||||||
|
}
|
||||||
|
interactiveRebaseState = InteractiveRebaseState(invocation = invocation, commits = commits)
|
||||||
|
applyRecommendedPaneWeights(persist = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyInteractiveRebaseSelection(drafts: List<RebaseCommitDraft>) {
|
||||||
|
val state = interactiveRebaseState ?: return
|
||||||
|
interactiveRebaseState = null
|
||||||
|
val newRepo = applyInteractiveRebase(repo, drafts)
|
||||||
|
applyCommandResult(
|
||||||
|
raw = state.invocation.command,
|
||||||
|
newRepo = newRepo,
|
||||||
|
lines = listOf("Successfully rebased and updated ${repo.headBranch}."),
|
||||||
|
echoCommand = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun runCommand() {
|
fun runCommand() {
|
||||||
val submittedText = commandInput.text
|
val submittedText = commandInput.text
|
||||||
val raw = submittedText.trim()
|
val raw = submittedText.trim()
|
||||||
@@ -453,6 +512,18 @@ fun GitHugApp() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val interactiveAddInvocation = parseInteractiveAddInvocation(raw)
|
||||||
|
if (interactiveAddInvocation != null) {
|
||||||
|
openInteractiveAdd(interactiveAddInvocation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val interactiveRebaseInvocation = parseInteractiveRebaseInvocation(raw)
|
||||||
|
if (interactiveRebaseInvocation != null) {
|
||||||
|
openInteractiveRebase(interactiveRebaseInvocation)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
|
val (newRepo, lines) = runtime.execute(currentLevel, repo, raw)
|
||||||
applyCommandResult(raw, newRepo, lines, echoCommand = true)
|
applyCommandResult(raw, newRepo, lines, echoCommand = true)
|
||||||
}
|
}
|
||||||
@@ -615,6 +686,28 @@ fun GitHugApp() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interactiveAddState?.let { state ->
|
||||||
|
InteractiveAddDialog(
|
||||||
|
state = state,
|
||||||
|
onClose = {
|
||||||
|
output = output + "Interactive add closed without staging"
|
||||||
|
interactiveAddState = null
|
||||||
|
},
|
||||||
|
onStage = { selectedFiles -> stageInteractiveSelection(selectedFiles) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interactiveRebaseState?.let { state ->
|
||||||
|
InteractiveRebaseDialog(
|
||||||
|
state = state,
|
||||||
|
onClose = {
|
||||||
|
output = output + "Interactive rebase closed without applying"
|
||||||
|
interactiveRebaseState = null
|
||||||
|
},
|
||||||
|
onApply = { drafts -> applyInteractiveRebaseSelection(drafts) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
manPageState?.let { state ->
|
manPageState?.let { state ->
|
||||||
ManPageDialog(
|
ManPageDialog(
|
||||||
state = state,
|
state = state,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
@@ -31,6 +32,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
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.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
||||||
data class GitMessageEditorState(
|
data class GitMessageEditorState(
|
||||||
val invocation: GitEditorInvocation,
|
val invocation: GitEditorInvocation,
|
||||||
@@ -46,64 +48,75 @@ fun GitMessageEditorDialog(
|
|||||||
) {
|
) {
|
||||||
val horizontalScroll = rememberScrollState()
|
val horizontalScroll = rememberScrollState()
|
||||||
val verticalScroll = rememberScrollState()
|
val verticalScroll = rememberScrollState()
|
||||||
|
val dialogScroll = rememberScrollState()
|
||||||
|
|
||||||
Dialog(onDismissRequest = onClose) {
|
Dialog(
|
||||||
Surface(
|
onDismissRequest = onClose,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.fillMaxHeight(0.72f)
|
.imePadding()
|
||||||
.imePadding(),
|
.verticalScroll(dialogScroll)
|
||||||
color = PanelPrimary,
|
.padding(12.dp),
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Surface(
|
||||||
modifier = Modifier.padding(12.dp),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 320.dp, max = 620.dp),
|
||||||
|
color = PanelPrimary,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
text = state.invocation.title,
|
modifier = Modifier.padding(12.dp),
|
||||||
color = TextPrimary,
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
) {
|
||||||
EditorButton(label = "Close", onClick = onClose)
|
Text(
|
||||||
EditorButton(label = "Save", enabled = state.content.isNotBlank(), onClick = onSave)
|
text = state.invocation.title,
|
||||||
}
|
color = TextPrimary,
|
||||||
Text(
|
fontWeight = FontWeight.Bold,
|
||||||
text = state.invocation.command,
|
fontSize = 20.sp,
|
||||||
color = TextSecondary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 13.sp,
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
.heightIn(min = 220.dp)
|
|
||||||
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
|
||||||
.padding(10.dp)
|
|
||||||
.horizontalScroll(horizontalScroll)
|
|
||||||
.verticalScroll(verticalScroll),
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = state.content,
|
|
||||||
onValueChange = onContentChange,
|
|
||||||
modifier = Modifier.sizeIn(minWidth = 1000.dp, minHeight = 700.dp),
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = TextPrimary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
),
|
|
||||||
cursorBrush = SolidColor(Accent),
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
autoCorrect = false,
|
|
||||||
keyboardType = KeyboardType.Ascii,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
EditorButton(label = "Close", onClick = onClose)
|
||||||
|
EditorButton(label = "Save", enabled = state.content.isNotBlank(), onClick = onSave)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = state.invocation.command,
|
||||||
|
color = TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(min = 220.dp)
|
||||||
|
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
||||||
|
.padding(10.dp)
|
||||||
|
.horizontalScroll(horizontalScroll)
|
||||||
|
.verticalScroll(verticalScroll),
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = state.content,
|
||||||
|
onValueChange = onContentChange,
|
||||||
|
modifier = Modifier.sizeIn(minWidth = 1000.dp, minHeight = 700.dp),
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = TextPrimary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(Accent),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
|
data class InteractiveAddInvocation(
|
||||||
|
val command: String,
|
||||||
|
val pathspec: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parseInteractiveAddInvocation(command: String): InteractiveAddInvocation? {
|
||||||
|
val tokens = GitSandboxEngine.tokenizeCommand(command)
|
||||||
|
if (tokens.size < 3 || tokens.firstOrNull() != "git") return null
|
||||||
|
|
||||||
|
val subcommand = tokens.getOrNull(1)
|
||||||
|
if (subcommand != "add" && subcommand != "stage") return null
|
||||||
|
if (tokens.drop(2).none { it == "-i" || it == "--interactive" }) return null
|
||||||
|
|
||||||
|
val pathspec = tokens
|
||||||
|
.drop(2)
|
||||||
|
.firstOrNull { token -> token != "-i" && token != "--interactive" && !token.startsWith("-") }
|
||||||
|
|
||||||
|
return InteractiveAddInvocation(command = command, pathspec = pathspec)
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
||||||
|
data class InteractiveAddState(
|
||||||
|
val invocation: InteractiveAddInvocation,
|
||||||
|
val files: List<GitFile>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InteractiveAddDialog(
|
||||||
|
state: InteractiveAddState,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onStage: (Set<String>) -> Unit,
|
||||||
|
) {
|
||||||
|
val candidates = remember(state.files, state.invocation.pathspec) {
|
||||||
|
state.files
|
||||||
|
.filter { file -> !file.staged }
|
||||||
|
.filter { file ->
|
||||||
|
val pathspec = state.invocation.pathspec
|
||||||
|
pathspec == null || pathspec == "." || file.name == pathspec || file.name.startsWith(pathspec.trimEnd('/') + "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var selectedFiles by remember(candidates) {
|
||||||
|
mutableStateOf(candidates.map { it.name }.toSet())
|
||||||
|
}
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onClose) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.76f)
|
||||||
|
.imePadding(),
|
||||||
|
color = PanelPrimary,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Interactive Add",
|
||||||
|
color = TextPrimary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = state.invocation.command,
|
||||||
|
color = TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
DialogButton(label = "Close", onClick = onClose)
|
||||||
|
DialogButton(
|
||||||
|
label = "Stage",
|
||||||
|
enabled = selectedFiles.isNotEmpty(),
|
||||||
|
onClick = { onStage(selectedFiles) },
|
||||||
|
)
|
||||||
|
DialogButton(
|
||||||
|
label = "All",
|
||||||
|
enabled = candidates.isNotEmpty(),
|
||||||
|
onClick = { selectedFiles = candidates.map { it.name }.toSet() },
|
||||||
|
)
|
||||||
|
DialogButton(
|
||||||
|
label = "None",
|
||||||
|
enabled = selectedFiles.isNotEmpty(),
|
||||||
|
onClick = { selectedFiles = emptySet() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
if (candidates.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "No changes available to stage.",
|
||||||
|
color = TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
candidates.forEach { file ->
|
||||||
|
val checked = file.name in selectedFiles
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
selectedFiles = if (isChecked) {
|
||||||
|
selectedFiles + file.name
|
||||||
|
} else {
|
||||||
|
selectedFiles - file.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = Accent,
|
||||||
|
uncheckedColor = TextMuted,
|
||||||
|
checkmarkColor = AppBackground,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "${file.statusLabel()} ${file.name}",
|
||||||
|
color = TextPrimary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GitFile.statusLabel(): String {
|
||||||
|
return when {
|
||||||
|
deleted -> "deleted: "
|
||||||
|
tracked -> "modified: "
|
||||||
|
else -> "new file: "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DialogButton(
|
||||||
|
label: String,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Accent,
|
||||||
|
contentColor = AppBackground,
|
||||||
|
disabledContainerColor = PanelTertiary,
|
||||||
|
disabledContentColor = TextMuted,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
|
data class InteractiveRebaseInvocation(
|
||||||
|
val command: String,
|
||||||
|
val commitCount: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parseInteractiveRebaseInvocation(command: String): InteractiveRebaseInvocation? {
|
||||||
|
val tokens = GitSandboxEngine.tokenizeCommand(command)
|
||||||
|
if (tokens.size < 3 || tokens.firstOrNull() != "git") return null
|
||||||
|
if (tokens.getOrNull(1) != "rebase") return null
|
||||||
|
if (tokens.drop(2).none { it == "-i" || it == "--interactive" }) return null
|
||||||
|
|
||||||
|
val commitCount = tokens
|
||||||
|
.drop(2)
|
||||||
|
.firstNotNullOfOrNull { token ->
|
||||||
|
Regex("""HEAD[~^](\d+)""")
|
||||||
|
.matchEntire(token)
|
||||||
|
?.groupValues
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.toIntOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
return InteractiveRebaseInvocation(command = command, commitCount = commitCount)
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
||||||
|
data class InteractiveRebaseState(
|
||||||
|
val invocation: InteractiveRebaseInvocation,
|
||||||
|
val commits: List<CommitNode>,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class RebaseAction(val label: String) {
|
||||||
|
PICK("pick"),
|
||||||
|
SQUASH("squash"),
|
||||||
|
DROP("drop"),
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RebaseCommitDraft(
|
||||||
|
val commit: CommitNode,
|
||||||
|
val message: String,
|
||||||
|
val action: RebaseAction,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InteractiveRebaseDialog(
|
||||||
|
state: InteractiveRebaseState,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onApply: (List<RebaseCommitDraft>) -> Unit,
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
var drafts by remember(state.commits) {
|
||||||
|
mutableStateOf(state.commits.map { commit ->
|
||||||
|
RebaseCommitDraft(
|
||||||
|
commit = commit,
|
||||||
|
message = if (commit.message == "First coommit") "First commit" else commit.message,
|
||||||
|
action = if (commit.message.contains("squash this commit", ignoreCase = true)) {
|
||||||
|
RebaseAction.SQUASH
|
||||||
|
} else {
|
||||||
|
RebaseAction.PICK
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}.withSuggestedOrder())
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onClose) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.84f)
|
||||||
|
.imePadding(),
|
||||||
|
color = PanelPrimary,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Interactive Rebase",
|
||||||
|
color = TextPrimary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = state.invocation.command,
|
||||||
|
color = TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
RebaseButton(label = "Close", onClick = onClose)
|
||||||
|
RebaseButton(label = "Apply", onClick = { onApply(drafts) })
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
if (drafts.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "No commits available for interactive rebase.",
|
||||||
|
color = TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
drafts.forEachIndexed { index, draft ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(PanelSecondary, RoundedCornerShape(6.dp))
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${draft.action.label} ${draft.commit.id}",
|
||||||
|
color = Accent,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = draft.message,
|
||||||
|
onValueChange = { message ->
|
||||||
|
drafts = drafts.replaceAt(index, draft.copy(message = message))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
RebaseButton(
|
||||||
|
label = "Pick",
|
||||||
|
enabled = draft.action != RebaseAction.PICK,
|
||||||
|
onClick = { drafts = drafts.replaceAt(index, draft.copy(action = RebaseAction.PICK)) },
|
||||||
|
)
|
||||||
|
RebaseButton(
|
||||||
|
label = "Squash",
|
||||||
|
enabled = draft.action != RebaseAction.SQUASH && index > 0,
|
||||||
|
onClick = { drafts = drafts.replaceAt(index, draft.copy(action = RebaseAction.SQUASH)) },
|
||||||
|
)
|
||||||
|
RebaseButton(
|
||||||
|
label = "Drop",
|
||||||
|
enabled = draft.action != RebaseAction.DROP,
|
||||||
|
onClick = { drafts = drafts.replaceAt(index, draft.copy(action = RebaseAction.DROP)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
RebaseButton(
|
||||||
|
label = "Up",
|
||||||
|
enabled = index > 0,
|
||||||
|
onClick = { drafts = drafts.move(index, index - 1) },
|
||||||
|
)
|
||||||
|
RebaseButton(
|
||||||
|
label = "Down",
|
||||||
|
enabled = index < drafts.lastIndex,
|
||||||
|
onClick = { drafts = drafts.move(index, index + 1) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyInteractiveRebase(repo: RepoState, drafts: List<RebaseCommitDraft>): RepoState {
|
||||||
|
val rebasedIds = drafts.map { it.commit.id }.toSet()
|
||||||
|
val untouched = repo.commits.filterNot { it.id in rebasedIds }
|
||||||
|
val rebased = drafts.fold(emptyList<CommitNode>()) { commits, draft ->
|
||||||
|
when (draft.action) {
|
||||||
|
RebaseAction.PICK -> commits + draft.commit.copy(message = draft.message)
|
||||||
|
RebaseAction.DROP -> commits
|
||||||
|
RebaseAction.SQUASH -> commits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val nextCommits = untouched + rebased
|
||||||
|
return repo.copy(
|
||||||
|
commits = nextCommits,
|
||||||
|
branches = repo.branches + (repo.headBranch to nextCommits.size),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<RebaseCommitDraft>.replaceAt(index: Int, draft: RebaseCommitDraft): List<RebaseCommitDraft> {
|
||||||
|
return mapIndexed { currentIndex, currentDraft -> if (currentIndex == index) draft else currentDraft }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<RebaseCommitDraft>.move(from: Int, to: Int): List<RebaseCommitDraft> {
|
||||||
|
return toMutableList().also { drafts ->
|
||||||
|
val item = drafts.removeAt(from)
|
||||||
|
drafts.add(to, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<RebaseCommitDraft>.withSuggestedOrder(): List<RebaseCommitDraft> {
|
||||||
|
val messages = map { it.message }
|
||||||
|
if (!messages.containsAll(listOf("First commit", "Second commit", "Third commit"))) return this
|
||||||
|
return sortedBy { draft ->
|
||||||
|
when (draft.message) {
|
||||||
|
"First commit" -> 1
|
||||||
|
"Second commit" -> 2
|
||||||
|
"Third commit" -> 3
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RebaseButton(
|
||||||
|
label: String,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Accent,
|
||||||
|
contentColor = AppBackground,
|
||||||
|
disabledContainerColor = PanelTertiary,
|
||||||
|
disabledContentColor = TextMuted,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,9 +30,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
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.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
@@ -59,8 +59,8 @@ fun ManPageDialog(
|
|||||||
) {
|
) {
|
||||||
val horizontalScrollState = rememberScrollState()
|
val horizontalScrollState = rememberScrollState()
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
val density = LocalDensity.current
|
|
||||||
var contentViewportSize by remember { mutableStateOf(IntSize.Zero) }
|
var contentViewportSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
var searchQuery by remember(state.content) { mutableStateOf("") }
|
var searchQuery by remember(state.content) { mutableStateOf("") }
|
||||||
var selectedMatch by remember(state.content) { mutableStateOf(0) }
|
var selectedMatch by remember(state.content) { mutableStateOf(0) }
|
||||||
val contentLines = remember(state.content) { state.content.lines() }
|
val contentLines = remember(state.content) { state.content.lines() }
|
||||||
@@ -110,18 +110,21 @@ fun ManPageDialog(
|
|||||||
selectedMatch = selectedMatch.coerceIn(0, (matchCount - 1).coerceAtLeast(0))
|
selectedMatch = selectedMatch.coerceIn(0, (matchCount - 1).coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(selectedMatch, matches) {
|
LaunchedEffect(selectedMatch, matches, textLayoutResult, contentViewportSize) {
|
||||||
val match = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
|
val match = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
|
||||||
val lineHeightPx = with(density) { 18.sp.toPx() }
|
val layoutResult = textLayoutResult ?: return@LaunchedEffect
|
||||||
val charWidthPx = with(density) { 8.sp.toPx() }
|
val startBounds = layoutResult.getBoundingBox(match.start)
|
||||||
|
val endBounds = layoutResult.getBoundingBox((match.end - 1).coerceAtLeast(match.start))
|
||||||
val viewportHeight = contentViewportSize.height.takeIf { it > 0 } ?: 0
|
val viewportHeight = contentViewportSize.height.takeIf { it > 0 } ?: 0
|
||||||
val viewportWidth = contentViewportSize.width.takeIf { it > 0 } ?: 0
|
val viewportWidth = contentViewportSize.width.takeIf { it > 0 } ?: 0
|
||||||
|
val highlightCenterY = (startBounds.top + endBounds.bottom) / 2f
|
||||||
|
val highlightCenterX = (startBounds.left + endBounds.right) / 2f
|
||||||
val verticalTarget = (
|
val verticalTarget = (
|
||||||
match.lineIndex * lineHeightPx - viewportHeight / 2f + lineHeightPx / 2f
|
highlightCenterY - viewportHeight / 2f
|
||||||
).toInt()
|
).toInt()
|
||||||
.coerceIn(0, verticalScrollState.maxValue)
|
.coerceIn(0, verticalScrollState.maxValue)
|
||||||
val horizontalTarget = (
|
val horizontalTarget = (
|
||||||
match.columnIndex * charWidthPx - viewportWidth / 2f + (match.end - match.start) * charWidthPx / 2f
|
highlightCenterX - viewportWidth / 2f
|
||||||
).toInt()
|
).toInt()
|
||||||
.coerceIn(0, horizontalScrollState.maxValue)
|
.coerceIn(0, horizontalScrollState.maxValue)
|
||||||
verticalScrollState.animateScrollTo(verticalTarget)
|
verticalScrollState.animateScrollTo(verticalTarget)
|
||||||
@@ -233,6 +236,7 @@ fun ManPageDialog(
|
|||||||
Text(
|
Text(
|
||||||
text = highlightedContent,
|
text = highlightedContent,
|
||||||
modifier = Modifier.widthIn(min = 1200.dp),
|
modifier = Modifier.widthIn(min = 1200.dp),
|
||||||
|
onTextLayout = { textLayoutResult = it },
|
||||||
color = TextPrimary,
|
color = TextPrimary,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
@@ -33,6 +34,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
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.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
|
||||||
data class TextEditorState(
|
data class TextEditorState(
|
||||||
@@ -53,75 +55,86 @@ fun TextEditorDialog(
|
|||||||
) {
|
) {
|
||||||
val editorHorizontalScroll = rememberScrollState()
|
val editorHorizontalScroll = rememberScrollState()
|
||||||
val editorVerticalScroll = rememberScrollState()
|
val editorVerticalScroll = rememberScrollState()
|
||||||
|
val dialogScroll = rememberScrollState()
|
||||||
|
|
||||||
Dialog(onDismissRequest = onClose) {
|
Dialog(
|
||||||
Surface(
|
onDismissRequest = onClose,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxSize()
|
||||||
.fillMaxHeight(0.86f)
|
.imePadding()
|
||||||
.imePadding(),
|
.verticalScroll(dialogScroll)
|
||||||
color = PanelPrimary,
|
.padding(12.dp),
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Surface(
|
||||||
modifier = Modifier.padding(12.dp),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 360.dp, max = 720.dp),
|
||||||
|
color = PanelPrimary,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
text = "Edit File",
|
modifier = Modifier.padding(12.dp),
|
||||||
color = TextPrimary,
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
) {
|
||||||
EditorButton(label = "Close", onClick = onClose)
|
Text(
|
||||||
EditorButton(label = "Save", enabled = state.saveAsPath.isNotBlank(), onClick = onSave)
|
text = "Edit File",
|
||||||
}
|
color = TextPrimary,
|
||||||
Text(
|
fontWeight = FontWeight.Bold,
|
||||||
text = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
|
fontSize = 20.sp,
|
||||||
color = TextPrimary,
|
)
|
||||||
fontWeight = FontWeight.Bold,
|
Row(
|
||||||
)
|
modifier = Modifier.fillMaxWidth(),
|
||||||
OutlinedTextField(
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
value = state.saveAsPath,
|
) {
|
||||||
onValueChange = onSaveAsPathChange,
|
EditorButton(label = "Close", onClick = onClose)
|
||||||
modifier = Modifier.fillMaxWidth(),
|
EditorButton(label = "Save", enabled = state.saveAsPath.isNotBlank(), onClick = onSave)
|
||||||
singleLine = true,
|
}
|
||||||
keyboardOptions = KeyboardOptions(
|
Text(
|
||||||
autoCorrect = false,
|
text = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
|
||||||
keyboardType = KeyboardType.Ascii,
|
color = TextPrimary,
|
||||||
imeAction = ImeAction.Done,
|
fontWeight = FontWeight.Bold,
|
||||||
),
|
)
|
||||||
label = { Text("File name") },
|
OutlinedTextField(
|
||||||
)
|
value = state.saveAsPath,
|
||||||
Box(
|
onValueChange = onSaveAsPathChange,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
singleLine = true,
|
||||||
.weight(1f)
|
|
||||||
.heightIn(min = 260.dp)
|
|
||||||
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
|
||||||
.padding(10.dp)
|
|
||||||
.horizontalScroll(editorHorizontalScroll)
|
|
||||||
.verticalScroll(editorVerticalScroll),
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = state.content,
|
|
||||||
onValueChange = onContentChange,
|
|
||||||
modifier = Modifier.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp),
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = TextPrimary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
),
|
|
||||||
cursorBrush = SolidColor(Accent),
|
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
autoCorrect = false,
|
autoCorrect = false,
|
||||||
keyboardType = KeyboardType.Ascii,
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
|
label = { Text("File name") },
|
||||||
)
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(min = 260.dp)
|
||||||
|
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
||||||
|
.padding(10.dp)
|
||||||
|
.horizontalScroll(editorHorizontalScroll)
|
||||||
|
.verticalScroll(editorVerticalScroll),
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = state.content,
|
||||||
|
onValueChange = onContentChange,
|
||||||
|
modifier = Modifier.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp),
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = TextPrimary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(Accent),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user