diff --git a/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java b/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java index 0b70356..f8b4558 100644 --- a/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java +++ b/app/src/main/java/com/neatstudio/tmuxandroid/MainActivity.java @@ -25,6 +25,7 @@ import android.os.Looper; import android.provider.Settings; import android.text.TextUtils; import android.text.InputType; +import android.text.method.LinkMovementMethod; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -154,6 +155,7 @@ public final class MainActivity extends Activity { private int lastActiveSessionCount = -1; private TerminalScreenBuffer terminalScreen = new TerminalScreenBuffer(DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS); private final StringBuilder queuedTerminalInput = new StringBuilder(); + private final Set terminalExpandedBlocks = new HashSet<>(); private final Set terminalExpandedMessages = new HashSet<>(); private final List terminalConversationMessages = new ArrayList<>(); private boolean terminalConnected; @@ -1696,6 +1698,7 @@ public final class MainActivity extends Activity { terminalSelectionEnabled = false; terminalFollowOutput = true; terminalKeyPage = 0; + terminalExpandedBlocks.clear(); terminalExpandedMessages.clear(); terminalConversationMessages.clear(); terminalLiveStatusText = null; @@ -1746,7 +1749,6 @@ public final class MainActivity extends Activity { 1 )); terminalAccessoryBar = createAccessoryBar(); - terminalAccessoryBar.setVisibility(terminalViewMode == TERMINAL_VIEW_CHAT ? View.GONE : View.VISIBLE); root.addView(terminalAccessoryBar, new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, dp(TERMINAL_KEYS_HEIGHT_DP) @@ -1860,33 +1862,30 @@ public final class MainActivity extends Activity { } terminalScroll.removeAllViews(); if (mode == TERMINAL_VIEW_CHAT) { - terminalScroll.addView(terminalChatList, new ScrollView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - )); - renderTerminalConversation(); + terminalText.setHorizontallyScrolling(false); + terminalText.setMovementMethod(LinkMovementMethod.getInstance()); + terminalText.setHighlightColor(Color.TRANSPARENT); } else { terminalText.setHorizontallyScrolling(true); - terminalText.setGravity(Gravity.BOTTOM | Gravity.START); terminalText.setMovementMethod(null); - terminalScroll.addView(terminalText, new ScrollView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - )); - renderTerminalNow(); } - boolean chat = mode == TERMINAL_VIEW_CHAT; + terminalText.setGravity(Gravity.BOTTOM | Gravity.START); + terminalScroll.addView(terminalText, new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + )); + renderTerminalNow(); if (terminalAccessoryBar != null) { - terminalAccessoryBar.setVisibility(chat ? View.GONE : View.VISIBLE); + terminalAccessoryBar.setVisibility(View.VISIBLE); } if (terminalComposerTabs != null) { - terminalComposerTabs.setVisibility(chat ? View.GONE : View.VISIBLE); + terminalComposerTabs.setVisibility(View.VISIBLE); } if (inputField != null) { - inputField.setHint(chat ? "Message" : "type command or text"); + inputField.setHint(mode == TERMINAL_VIEW_CHAT ? "message or command" : "type command or text"); } if (announce) { - setStatus(mode == TERMINAL_VIEW_CHAT ? "Chat view" : "Full terminal mode"); + setStatus(mode == TERMINAL_VIEW_CHAT ? "Content view" : "Raw terminal mode"); } } @@ -2588,7 +2587,6 @@ public final class MainActivity extends Activity { LinearLayout tabGrid = new LinearLayout(this); tabGrid.setOrientation(LinearLayout.VERTICAL); terminalComposerTabs = tabGrid; - tabGrid.setVisibility(terminalViewMode == TERMINAL_VIEW_CHAT ? View.GONE : View.VISIBLE); LinearLayout firstTabRow = terminalKeyRow(); LinearLayout secondTabRow = terminalKeyRow(); addAccessoryTab(firstTabRow, "Edit", 0); @@ -3827,10 +3825,21 @@ public final class MainActivity extends Activity { return; } lastTerminalRenderMs = System.currentTimeMillis(); - terminalText.setMovementMethod(null); - terminalText.setText(terminalScreen.render()); + if (terminalViewMode == TERMINAL_VIEW_CHAT) { + terminalText.setMovementMethod(LinkMovementMethod.getInstance()); + terminalText.setHighlightColor(Color.TRANSPARENT); + terminalText.setText(terminalScreen.renderFocused(terminalExpandedBlocks, key -> { + if (!terminalExpandedBlocks.add(key)) { + terminalExpandedBlocks.remove(key); + } + renderTerminalNow(); + })); + } else { + terminalText.setMovementMethod(null); + terminalText.setText(terminalScreen.render()); + } updateTerminalLivePanel(); - if (terminalViewMode != TERMINAL_VIEW_CHAT && terminalFollowOutput) { + if (terminalFollowOutput) { terminalScroll.post(() -> terminalScroll.fullScroll(View.FOCUS_DOWN)); } } diff --git a/app/src/main/java/com/neatstudio/tmuxandroid/TerminalScreenBuffer.java b/app/src/main/java/com/neatstudio/tmuxandroid/TerminalScreenBuffer.java index ddcfb38..ff6dcb7 100644 --- a/app/src/main/java/com/neatstudio/tmuxandroid/TerminalScreenBuffer.java +++ b/app/src/main/java/com/neatstudio/tmuxandroid/TerminalScreenBuffer.java @@ -146,33 +146,82 @@ final class TerminalScreenBuffer { CharSequence renderFocused(Set expandedBlocks, FocusToggle toggle) { SpannableStringBuilder output = new SpannableStringBuilder(); - List hiddenRows = new ArrayList<>(); - int hiddenStart = -1; - for (int row = 0; row < rows; row++) { + int row = 0; + while (row < rows) { String text = rowText(row).trim(); - if (containsHan(text)) { - if (!hiddenRows.isEmpty()) { - appendFocusBlock(output, hiddenRows, hiddenStart, row - 1, expandedBlocks, toggle); - hiddenRows.clear(); - hiddenStart = -1; + if (!isCollapsibleHeader(text)) { + if (text.isEmpty()) { + if (output.length() > 0) { + output.append('\n'); + } + } else { + appendStyledRow(output, row); } - appendLine(output, text); - } else if (!text.isEmpty()) { - if (hiddenStart < 0) { - hiddenStart = row; - } - hiddenRows.add(text); + row++; + continue; } - } - if (!hiddenRows.isEmpty()) { - appendFocusBlock(output, hiddenRows, hiddenStart, rows - 1, expandedBlocks, toggle); + + int startRow = row; + List hiddenRows = new ArrayList<>(); + hiddenRows.add(text); + row++; + while (row < rows) { + String next = rowText(row).trim(); + if (next.isEmpty() || isTopLevelLine(next)) { + break; + } + hiddenRows.add(next); + row++; + } + appendFocusBlock(output, hiddenRows, startRow, row - 1, expandedBlocks, toggle); } if (output.length() == 0) { - output.append("暂无可读内容"); + output.append("Waiting for terminal output"); } return output; } + private boolean isCollapsibleHeader(String text) { + boolean activity = text.startsWith("• ") || text.startsWith("● "); + String header = stripActivityMarker(text); + return activity && (header.startsWith("Ran ") + || header.equals("Explored") + || header.startsWith("Explored ") + || header.startsWith("Searched ") + || header.startsWith("Read ") + || header.startsWith("List ") + || header.startsWith("Viewed ") + || header.startsWith("Opened ") + || header.startsWith("Fetched ") + || header.startsWith("Downloaded ") + || header.startsWith("Wrote ") + || header.startsWith("Edited ") + || header.startsWith("Applied ") + || header.startsWith("Updated ") + || header.startsWith("Checked ") + || header.startsWith("Inspected ") + || header.startsWith("Waited ") + || header.startsWith("Working") + || header.startsWith("Stop hook")) + || text.startsWith("─ Worked for "); + } + + private boolean isTopLevelLine(String text) { + return text.startsWith("• ") + || text.startsWith("● ") + || text.startsWith("› ") + || text.startsWith("> ") + || text.startsWith("─ ") + || isCollapsibleHeader(text); + } + + private String stripActivityMarker(String text) { + if (text.startsWith("• ") || text.startsWith("● ")) { + return text.substring(2).trim(); + } + return text; + } + private void appendFocusBlock( SpannableStringBuilder output, List lines, @@ -208,25 +257,22 @@ final class TerminalScreenBuffer { } private String focusSummary(List lines) { + String first = stripActivityMarker(lines.get(0)); String joined = String.join(" ", lines).toLowerCase(Locale.ROOT); String type; - if (joined.contains("error") || joined.contains("failed") || joined.contains("exception")) { - type = "错误输出"; - } else if (joined.contains("working") || joined.contains("running") - || joined.contains("waiting") || joined.contains("interrupt")) { - type = "运行状态"; - } else if (joined.contains("test") || joined.contains("build") || joined.contains("compile") - || joined.contains("gradle") || joined.contains("webpack") || joined.contains("npm")) { - type = "构建/测试"; - } else if (joined.contains("git ") || joined.contains("commit") || joined.contains("push")) { - type = "Git 操作"; - } else if (lines.get(0).startsWith(">") || lines.get(0).startsWith("$") - || lines.get(0).startsWith("!")) { - type = "命令与输出"; + if (first.startsWith("Explored") || first.startsWith("Searched") + || first.startsWith("Read ") || first.startsWith("List ") + || first.startsWith("Viewed ") || first.startsWith("Inspected ")) { + type = "Explored"; + } else if (first.startsWith("Working") || first.startsWith("Stop hook") + || first.startsWith("─ Worked for ")) { + type = "Status"; + } else if (joined.contains("error") || joined.contains("failed") || joined.contains("exception")) { + type = "Command failed"; } else { - type = "终端细节"; + type = "Command output"; } - return type + " · " + lines.size() + " 行 · " + compactSummary(lines.get(0), 42); + return type + " · " + lines.size() + " lines · " + compactSummary(first, 48); } private String compactSummary(String text, int maxChars) { @@ -244,6 +290,18 @@ final class TerminalScreenBuffer { output.append(text); } + private void appendStyledRow(SpannableStringBuilder output, int row) { + if (output.length() > 0) { + output.append('\n'); + } + int limit = cols; + while (limit > 0 && cells[row][limit - 1].value == ' ' + && cells[row][limit - 1].bg == DEFAULT_BG) { + limit--; + } + appendRow(output, row, limit); + } + private int handleEscape(String text, int index) { if (index + 1 >= text.length()) { return -1; @@ -583,15 +641,19 @@ final class TerminalScreenBuffer { } private void appendRow(SpannableStringBuilder output, int row) { + appendRow(output, row, cols); + } + + private void appendRow(SpannableStringBuilder output, int row, int limit) { int col = 0; - while (col < cols) { + while (col < limit) { Cell first = cells[row][col]; int start = output.length(); int fgColor = first.fg; int bgColor = first.bg; boolean isBold = first.bold; boolean isDim = first.dim; - while (col < cols) { + while (col < limit) { Cell cell = cells[row][col]; if (cell.fg != fgColor || cell.bg != bgColor || cell.bold != isBold || cell.dim != isDim) { break; @@ -626,18 +688,6 @@ final class TerminalScreenBuffer { return text.toString(); } - private boolean containsHan(String text) { - for (int index = 0; index < text.length(); index++) { - Character.UnicodeBlock block = Character.UnicodeBlock.of(text.charAt(index)); - if (block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS - || block == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A - || block == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS) { - return true; - } - } - return false; - } - private void saveCursor() { savedRow = cursorRow; savedCol = cursorCol;