Skip to content

Commit 70356ef

Browse files
committed
Update
1 parent af3479a commit 70356ef

2 files changed

Lines changed: 217 additions & 16 deletions

File tree

app/src/main/java/com/omarea/krscript/model/ShellHandlerBase.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ public abstract class ShellHandlerBase extends Handler {
4747
// (hiếm gặp trong thực tế vì choose thường chiếm trọn 1 dòng echo riêng), phần đó sẽ bị
4848
// gộp nhầm vào bên trong. Chấp nhận đánh đổi này để ưu tiên đúng cho trường hợp phổ biến.
4949
private static final Pattern CHOOSE_PATTERN = Pattern.compile("choose:\\[(.*)\\]");
50+
// "pick:[...]" / "pickv:[...]" / "pickh:[...]" - giống choose:[...] nhưng KHÔNG hiện nút
51+
// bấm riêng, chỉ hiện đáp án dạng link ngay trong log. Nhóm 1 là "v"/"h"/null (hướng xếp),
52+
// nhóm 2 là nội dung phương án (định dạng giống hệt choose:[...]).
53+
private static final Pattern PICK_PATTERN = Pattern.compile("pick(v|h)?:\\[(.*)\\]");
5054
private static final Pattern EXIT_PATTERN = Pattern.compile("exit:\\[(.*?)\\]");
5155

5256
protected abstract void onProgress(int current, int total);
@@ -137,6 +141,19 @@ public ChoiceOption(String value, String label) {
137141
protected void onChooseRequest(java.util.List<ChoiceOption> options) {
138142
}
139143

144+
/**
145+
* Được gọi khi script yêu cầu hiển thị các đáp án dưới dạng LINK ngay trong log, KHÔNG có
146+
* nút bấm riêng (khác với onChooseRequest ở trên), thông qua cú pháp:
147+
* echo "pick:[1|Yes,2|No]" -> mặc định xếp DỌC
148+
* echo "pickv:[1|Yes,2|No]" -> xếp DỌC, mỗi đáp án 1 dòng dạng "1. Nhãn"
149+
* echo "pickh:[1|Yes,2|No]" -> xếp NGANG, các đáp án dạng "[ Nhãn ]" nối cạnh nhau
150+
* Nội dung phương án cùng định dạng "giá_trị|nhãn" như choose:[...]. Khi người dùng ấn vào
151+
* 1 đáp án, giá trị tương ứng được ghi vào stdin (kèm xuống dòng) y hệt onChooseRequest.
152+
* Mặc định không làm gì; lớp con override để hiển thị.
153+
*/
154+
protected void onPickRequest(java.util.List<ChoiceOption> options, boolean vertical) {
155+
}
156+
140157
/**
141158
* Parse nội dung bên trong "choose:[...]" thành danh sách phương án.
142159
* Định dạng mỗi phương án: "giá_trị|nhãn", các phương án cách nhau bởi dấu phẩy.
@@ -220,6 +237,18 @@ protected void onReaderMsg(Object msg) {
220237
return;
221238
}
222239
}
240+
241+
// === PHÁT HIỆN YÊU CẦU CHỌN ĐÁP ÁN DẠNG LINK TRONG LOG (KHÔNG CÓ NÚT RIÊNG):
242+
// pick:[...] / pickv:[...] (dọc, mặc định) / pickh:[...] (ngang) ===
243+
Matcher pickMatcher = PICK_PATTERN.matcher(cleanLog);
244+
if (pickMatcher.find()) {
245+
java.util.List<ChoiceOption> options = parseChooseOptions(pickMatcher.group(2).trim());
246+
if (!options.isEmpty()) {
247+
boolean vertical = !"h".equals(pickMatcher.group(1)); // mặc định dọc, trừ khi "pickh:"
248+
onPickRequest(options, vertical);
249+
return;
250+
}
251+
}
223252

224253
// Parser cũ giữ nguyên
225254
Matcher amMatcher = AM_PATTERN.matcher(cleanLog);

app/src/main/java/com/omarea/krscript/ui/DialogLogFragment.kt

Lines changed: 188 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import android.content.ClipData
55
import android.content.ClipboardManager
66
import android.content.Context
77
import android.content.DialogInterface
8+
import android.graphics.Color
89
import android.os.Build
910
import android.os.Bundle
1011
import android.os.Message
1112
import android.text.Editable
1213
import android.text.SpannableString
1314
import android.text.SpannableStringBuilder
1415
import android.text.Spanned
16+
import android.text.TextPaint
1517
import android.text.method.LinkMovementMethod
18+
import android.text.style.ClickableSpan
1619
import android.text.style.ForegroundColorSpan
20+
import android.text.style.UnderlineSpan
1721
import android.view.KeyEvent
1822
import android.view.LayoutInflater
1923
import android.view.View
@@ -313,6 +317,25 @@ class DialogLogFragment : DialogFragment() {
313317
fun onCompleted()
314318
}
315319

320+
/**
321+
* Span dùng để hiển thị 1 đáp án của "choose:[...]" dưới dạng link có thể ấn NGAY TRONG
322+
* log (tương tự URLSpan cho hyperlink), bên cạnh cách chọn bằng nút bấm đã có sẵn.
323+
* Không giữ tham chiếu tới View/Editable - chỉ gọi callback [onTap] với giá trị tương ứng,
324+
* để nơi gọi (onChooseRequest) tự quyết định phải làm gì tiếp theo (ghi stdin, cập nhật
325+
* giao diện...). Nhờ vậy có thể tái sử dụng đúng 1 luồng xử lý dùng chung cho cả nút bấm
326+
* lẫn link trong log.
327+
*/
328+
private class ChoiceSpan(val value: String, private val onTap: (String) -> Unit) : ClickableSpan() {
329+
override fun onClick(widget: View) {
330+
onTap(value)
331+
}
332+
333+
override fun updateDrawState(ds: TextPaint) {
334+
super.updateDrawState(ds)
335+
ds.isUnderlineText = true
336+
}
337+
}
338+
316339
class MyShellHandler(
317340
context: Context,
318341
private var actionEventHandler: IActionEventHandler?,
@@ -335,6 +358,14 @@ class DialogLogFragment : DialogFragment() {
335358
private val basicColor = getColor(R.color.kr_shell_log_basic)
336359
private val scriptColor = getColor(R.color.kr_shell_log_script)
337360
private val endColor = getColor(R.color.kr_shell_log_end)
361+
362+
// Màu cho các đáp án "choose:[...]" hiển thị dạng link ngay trong log (giống URL):
363+
// - choiceLinkColor: màu đáp án còn có thể ấn (chưa trả lời)
364+
// - choiceAnsweredColor: màu đáp án ĐÃ chọn, dùng để đánh dấu kết quả
365+
// - choiceDisabledColor: màu các đáp án còn lại (không được chọn) sau khi đã trả lời
366+
private val choiceLinkColor = Color.parseColor("#4FC3F7")
367+
private val choiceAnsweredColor = Color.parseColor("#7CFC00")
368+
private val choiceDisabledColor = Color.parseColor("#808080")
338369
private var hasError = false
339370
private var lineCount = 0
340371

@@ -449,6 +480,34 @@ class DialogLogFragment : DialogFragment() {
449480
val minComfortableWidthPx = dpToPx(40f)
450481
val buttonHeightPx = dpToPx(40f)
451482

483+
// Người dùng có 2 cách trả lời: ấn nút bên dưới, HOẶC ấn thẳng vào đáp án hiển thị
484+
// dạng link ngay trong log. Cờ này đảm bảo dù chọn cách nào trước, đáp án cũng chỉ
485+
// được gửi (writeInput) đúng 1 lần, và cách còn lại sẽ được vô hiệu hoá/cập nhật
486+
// giao diện ngay sau đó.
487+
val answered = AtomicBoolean(false)
488+
val choiceSpans = mutableListOf<ChoiceSpan>()
489+
490+
fun onAnswered(value: String) {
491+
if (!answered.compareAndSet(false, true)) return
492+
writeInput(value)
493+
row.visibility = View.GONE
494+
container.removeAllViews()
495+
496+
// Vô hiệu hoá + tô lại màu các link trong log: đáp án vừa chọn được tô sáng,
497+
// các đáp án còn lại chuyển màu xám (không còn ấn được vì đã gỡ ClickableSpan).
498+
val editable = logViewRef.get()?.editableText ?: return
499+
for (span in choiceSpans) {
500+
val start = editable.getSpanStart(span)
501+
val end = editable.getSpanEnd(span)
502+
if (start < 0 || end < 0) continue
503+
editable.removeSpan(span)
504+
editable.setSpan(
505+
ForegroundColorSpan(if (span.value == value) choiceAnsweredColor else choiceDisabledColor),
506+
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
507+
)
508+
}
509+
}
510+
452511
fun buildButtons(availableWidth: Int) {
453512
inputRowRef.get()?.visibility = View.GONE
454513
container.removeAllViews()
@@ -488,9 +547,7 @@ class DialogLogFragment : DialogFragment() {
488547
if (index < count - 1) it.marginEnd = gapPx
489548
}
490549
setOnClickListener {
491-
writeInput(option.value)
492-
row.visibility = View.GONE
493-
container.removeAllViews()
550+
onAnswered(option.value)
494551
}
495552
}
496553
container.addView(button)
@@ -525,6 +582,81 @@ class DialogLogFragment : DialogFragment() {
525582
// có lượt layout nào được lên lịch (ví dụ view vừa addView() xong).
526583
measureView.requestLayout()
527584
}
585+
586+
// Đồng thời chèn các đáp án dạng link ngay trong log (giống hyperlink URL), đi qua
587+
// ĐÚNG cơ chế dispatchLogUpdate() như mọi dòng log khác, để logBuffer/uiAppliedLength
588+
// luôn nhất quán - không thao tác trực tiếp lên editableText ở đây.
589+
val builder = SpannableStringBuilder()
590+
builder.append('\n')
591+
for (option in options) {
592+
val start = builder.length
593+
builder.append(option.label)
594+
val end = builder.length
595+
val span = ChoiceSpan(option.value) { value -> onAnswered(value) }
596+
choiceSpans.add(span)
597+
builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
598+
builder.setSpan(ForegroundColorSpan(choiceLinkColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
599+
builder.setSpan(UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
600+
builder.append('\n')
601+
}
602+
dispatchLogUpdate(builder)
603+
}
604+
605+
/**
606+
* Hiện các đáp án dưới dạng LINK ngay trong log (không có nút bấm riêng), cho cú pháp
607+
* "pick:[...]" / "pickv:[...]" (dọc) / "pickh:[...]" (ngang). Tách biệt hoàn toàn với
608+
* onChooseRequest() ở trên - không tạo nút, không dùng chooseRow/chooseOptionsContainer.
609+
*/
610+
override fun onPickRequest(options: MutableList<ChoiceOption>, vertical: Boolean) {
611+
val answered = AtomicBoolean(false)
612+
val choiceSpans = mutableListOf<ChoiceSpan>()
613+
614+
fun onAnswered(value: String) {
615+
if (!answered.compareAndSet(false, true)) return
616+
writeInput(value)
617+
618+
val editable = logViewRef.get()?.editableText ?: return
619+
for (span in choiceSpans) {
620+
val start = editable.getSpanStart(span)
621+
val end = editable.getSpanEnd(span)
622+
if (start < 0 || end < 0) continue
623+
editable.removeSpan(span)
624+
editable.setSpan(
625+
ForegroundColorSpan(if (span.value == value) choiceAnsweredColor else choiceDisabledColor),
626+
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
627+
)
628+
}
629+
}
630+
631+
fun appendChoice(builder: SpannableStringBuilder, text: String, value: String) {
632+
val start = builder.length
633+
builder.append(text)
634+
val end = builder.length
635+
val span = ChoiceSpan(value) { v -> onAnswered(v) }
636+
choiceSpans.add(span)
637+
builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
638+
builder.setSpan(ForegroundColorSpan(choiceLinkColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
639+
builder.setSpan(UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
640+
}
641+
642+
val builder = SpannableStringBuilder()
643+
builder.append('\n')
644+
if (vertical) {
645+
// Xếp DỌC: mỗi đáp án 1 dòng, dạng "1. Nhãn", cách nhau 1 dòng trống để tạo
646+
// vùng chạm đủ lớn trên di động. Toàn bộ "1. Nhãn" (kể cả số thứ tự) đều bấm được.
647+
options.forEachIndexed { index, option ->
648+
appendChoice(builder, "${index + 1}. ${option.label}", option.value)
649+
builder.append("\n\n")
650+
}
651+
} else {
652+
// Xếp NGANG: dạng "[ Nhãn ]" nối cạnh nhau trên cùng 1 dòng.
653+
options.forEachIndexed { index, option ->
654+
appendChoice(builder, "[ ${option.label} ]", option.value)
655+
if (index != options.lastIndex) builder.append(" ")
656+
}
657+
builder.append('\n')
658+
}
659+
dispatchLogUpdate(builder)
528660
}
529661

530662
override fun handleMessage(msg: Message) {
@@ -621,20 +753,60 @@ class DialogLogFragment : DialogFragment() {
621753
private fun updateLogWithColor(text: String, forcedColor: Int?) {
622754
var parsedLog: CharSequence = AnsiColorParser.parse(text)
623755

624-
// Lệnh `clear` trong script sẽ phát ra mã CSI xoá màn hình (ESC[2J/ESC[3J), được
625-
// AnsiColorParser nhận diện ở trên. Xoá sạch logBuffer ngay tại đây (background
626-
// thread) và đánh dấu invalid từ vị trí 0 để lần flushToUi() kế tiếp thay thế TOÀN
627-
// BỘ nội dung cũ đang hiển thị bằng nội dung mới (thường là rỗng), giống hệt cách
628-
// xử lý ghi đè do '\r' - không cần đụng trực tiếp vào UI thread, tránh race với
629-
// pendingUiUpdate/flushToUi.
630-
if (AnsiColorParser.consumePendingClear()) {
631-
synchronized(logBuffer) {
632-
logBuffer.clear()
633-
lineCount = 0
634-
lineStart = 0
635-
pendingOverwrite = false
636-
markInvalidFrom(0)
756+
// Lệnh `clear` (hoặc chương trình vẽ lại 1 phần màn hình như progress bar) sẽ phát ra
757+
// mã CSI "Erase in Display" (ESC[nJ), được AnsiColorParser nhận diện ở trên. Vì log
758+
// buffer ở đây chỉ append tuần tự (không theo dõi toạ độ cursor thật như terminal),
759+
// ta xấp xỉ "vị trí cursor" như sau:
760+
// - Nếu vừa gặp '\r' (pendingOverwrite = true, đang chờ ghi đè dòng hiện tại) -> cursor
761+
// coi như đang ở ĐẦU dòng hiện tại (lineStart).
762+
// - Ngược lại (append bình thường, chưa ghi đè gì) -> cursor coi như đang ở CUỐI buffer.
763+
// Xấp xỉ này khớp chính xác với các chương trình dùng '\r' để vẽ lại dòng (progress
764+
// bar, spinner...), nhưng sẽ không chính xác nếu chương trình dùng CUP (ESC[r;cH) để
765+
// di chuyển cursor tự do - các mã di chuyển cursor này hiện chưa được theo dõi.
766+
when (AnsiColorParser.consumePendingErase()) {
767+
2, 3 -> {
768+
// Xoá toàn bộ màn hình (lệnh `clear`)
769+
synchronized(logBuffer) {
770+
logBuffer.clear()
771+
lineCount = 0
772+
lineStart = 0
773+
pendingOverwrite = false
774+
markInvalidFrom(0)
775+
}
776+
}
777+
0 -> {
778+
// Xoá từ cursor (xấp xỉ) tới hết buffer
779+
synchronized(logBuffer) {
780+
val cursorPos = (if (pendingOverwrite) lineStart else logBuffer.length)
781+
.coerceIn(0, logBuffer.length)
782+
if (cursorPos < logBuffer.length) {
783+
logBuffer.delete(cursorPos, logBuffer.length)
784+
markInvalidFrom(cursorPos)
785+
}
786+
pendingOverwrite = false
787+
}
788+
}
789+
1 -> {
790+
// Xoá từ đầu buffer tới cursor (xấp xỉ). Vì phần đầu bị xoá, mọi toạ độ phía
791+
// sau đều dịch chuyển - xử lý tương tự logic "cắt tỉa 5000 dòng" bên dưới.
792+
synchronized(logBuffer) {
793+
val cursorPos = (if (pendingOverwrite) lineStart else logBuffer.length)
794+
.coerceIn(0, logBuffer.length)
795+
if (cursorPos > 0) {
796+
var removedLines = 0
797+
for (k in 0 until cursorPos) {
798+
if (logBuffer[k] == '\n') removedLines++
799+
}
800+
logBuffer.delete(0, cursorPos)
801+
lineCount = (lineCount - removedLines).coerceAtLeast(0)
802+
lineStart = (lineStart - cursorPos).coerceAtLeast(0)
803+
uiAppliedLength = (uiAppliedLength - cursorPos).coerceAtLeast(0)
804+
uiInvalidFrom = 0
805+
}
806+
pendingOverwrite = false
807+
}
637808
}
809+
else -> {}
638810
}
639811

640812
if (forcedColor != null && !text.contains("\u001B[")) {

0 commit comments

Comments
 (0)