Compare commits

...
9 Commits
Author SHA1 Message Date
Codex daf2990b15 Clear stale terminal frames on resize
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Successful in 12m24s
2026-07-12 02:00:55 +00:00
Codex 96fe5ecb8f Improve mobile session reading and pane layout
Gitea Smoke / smoke (push) Successful in 2s
Gitea Android APK / build (push) Has been cancelled
2026-07-12 01:59:55 +00:00
Codex f103e9e065 Render ANSI dim terminal text
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Successful in 11m47s
2026-07-11 08:41:33 +00:00
Codex 6a1052d437 Insert uploaded image paths
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Successful in 11m50s
2026-07-11 08:11:46 +00:00
Codex 7e028b09d4 Reduce terminal key overflow
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Failing after 8m17s
2026-07-11 08:01:21 +00:00
Codex f6e801defc Rearrange terminal composer controls
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Successful in 12m15s
2026-07-11 03:12:52 +00:00
Codex 90f2088265 Support mixed Gitea runners
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Failing after 8m12s
2026-07-10 16:18:55 +00:00
Codex 0d0f218d52 Compact terminal input controls
Gitea Android APK / build (push) Failing after 0s
Gitea Smoke / smoke (push) Successful in 0s
2026-07-10 15:54:51 +00:00
Codex 01f2c66f48 Refine terminal input controls
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Failing after 8m32s
2026-07-10 15:11:12 +00:00
3 changed files with 574 additions and 147 deletions
+22 -4
View File
@@ -16,13 +16,31 @@ jobs:
run: | run: |
set -eu set -eu
install_packages() {
if command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y "$@"
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache "$@"
elif command -v dnf >/dev/null 2>&1; then
dnf install -y "$@"
else
echo "No supported package manager found." >&2
exit 1
fi
}
if ! command -v git >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1 || ! command -v unzip >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1 || ! command -v unzip >/dev/null 2>&1; then
apt-get update install_packages git curl unzip
apt-get install -y git curl unzip
fi fi
if ! command -v java >/dev/null 2>&1; then if ! command -v java >/dev/null 2>&1; then
apt-get update if command -v apk >/dev/null 2>&1; then
apt-get install -y openjdk-17-jdk-headless install_packages openjdk17-jdk
elif command -v dnf >/dev/null 2>&1; then
install_packages java-17-openjdk-devel
else
install_packages openjdk-17-jdk-headless
fi
fi fi
java -version java -version
@@ -66,7 +66,7 @@ public final class MainActivity extends Activity {
private static final int MAX_TERMINAL_COLS = 140; private static final int MAX_TERMINAL_COLS = 140;
private static final int MIN_TERMINAL_ROWS = 8; private static final int MIN_TERMINAL_ROWS = 8;
private static final int MAX_TERMINAL_ROWS = 80; private static final int MAX_TERMINAL_ROWS = 80;
private static final int TERMINAL_KEYS_HEIGHT_DP = 44; private static final int TERMINAL_KEYS_HEIGHT_DP = 34;
private static final int STATUS_NORMAL = 0; private static final int STATUS_NORMAL = 0;
private static final int STATUS_BUSY = 1; private static final int STATUS_BUSY = 1;
private static final int STATUS_SUCCESS = 2; private static final int STATUS_SUCCESS = 2;
@@ -124,10 +124,17 @@ public final class MainActivity extends Activity {
private LinearLayout sessionGroupList; private LinearLayout sessionGroupList;
private TextView terminalText; private TextView terminalText;
private TextView terminalMetaText; private TextView terminalMetaText;
private TextView terminalConnectionText;
private Button terminalReadingButton;
private ScrollView terminalScroll; private ScrollView terminalScroll;
private EditText inputField; private EditText inputField;
private Button terminalKeyPageButton; private final Button[] terminalAccessoryTabButtons = new Button[4];
private View terminalAccessoryBar;
private View terminalComposerBar; private View terminalComposerBar;
private LinearLayout terminalImagePreviewBar;
private ImageView terminalImagePreview;
private TextView terminalImagePreviewPath;
private String terminalImagePath = "";
private LinearLayout terminalGroupRow; private LinearLayout terminalGroupRow;
private String activeSessionName; private String activeSessionName;
private String terminalPathStatus = ""; private String terminalPathStatus = "";
@@ -144,6 +151,7 @@ public final class MainActivity extends Activity {
private boolean terminalRenderPending; private boolean terminalRenderPending;
private boolean terminalSelectionEnabled; private boolean terminalSelectionEnabled;
private boolean terminalFollowOutput = true; private boolean terminalFollowOutput = true;
private boolean terminalReadingMode = true;
private int terminalKeyPage; private int terminalKeyPage;
private int terminalReconnectAttempt; private int terminalReconnectAttempt;
private int terminalConnectionGeneration; private int terminalConnectionGeneration;
@@ -1595,6 +1603,10 @@ public final class MainActivity extends Activity {
terminalSelectionEnabled = false; terminalSelectionEnabled = false;
terminalFollowOutput = true; terminalFollowOutput = true;
terminalKeyPage = 0; terminalKeyPage = 0;
terminalImagePath = "";
terminalImagePreviewBar = null;
terminalImagePreview = null;
terminalImagePreviewPath = null;
terminalPathStatus = "path loading"; terminalPathStatus = "path loading";
terminalSocketStatus = "terminal idle"; terminalSocketStatus = "terminal idle";
terminalEventStatus = "events listening"; terminalEventStatus = "events listening";
@@ -1613,9 +1625,10 @@ public final class MainActivity extends Activity {
terminalScroll.setBackgroundColor(COLOR_TERMINAL_BG); terminalScroll.setBackgroundColor(COLOR_TERMINAL_BG);
terminalText = new TextView(this); terminalText = new TextView(this);
terminalText.setTextColor(COLOR_TEXT); terminalText.setTextColor(COLOR_TEXT);
terminalText.setTextSize(13); terminalText.setTextSize(11);
terminalText.setTypeface(Typeface.MONOSPACE); terminalText.setTypeface(Typeface.MONOSPACE);
terminalText.setIncludeFontPadding(false); terminalText.setIncludeFontPadding(false);
terminalText.setHorizontallyScrolling(!terminalReadingMode);
terminalText.setLineSpacing(0, 1.05f); terminalText.setLineSpacing(0, 1.05f);
terminalText.setGravity(Gravity.BOTTOM | Gravity.START); terminalText.setGravity(Gravity.BOTTOM | Gravity.START);
terminalText.setTextIsSelectable(terminalSelectionEnabled); terminalText.setTextIsSelectable(terminalSelectionEnabled);
@@ -1634,12 +1647,13 @@ public final class MainActivity extends Activity {
0, 0,
1 1
)); ));
terminalComposerBar = createComposerBar(); terminalAccessoryBar = createAccessoryBar();
root.addView(terminalComposerBar, matchWrap()); root.addView(terminalAccessoryBar, new LinearLayout.LayoutParams(
root.addView(createAccessoryBar(), new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
dp(TERMINAL_KEYS_HEIGHT_DP) dp(TERMINAL_KEYS_HEIGHT_DP)
)); ));
terminalComposerBar = createComposerBar();
root.addView(terminalComposerBar, matchWrap());
terminalScroll.post(() -> { terminalScroll.post(() -> {
resizeTerminalToViewport(false); resizeTerminalToViewport(false);
connectTerminal(sessionName); connectTerminal(sessionName);
@@ -1665,6 +1679,10 @@ public final class MainActivity extends Activity {
titleBlock.setGravity(Gravity.CENTER_VERTICAL); titleBlock.setGravity(Gravity.CENTER_VERTICAL);
titleBlock.setPadding(dp(9), 0, dp(7), 0); titleBlock.setPadding(dp(9), 0, dp(7), 0);
LinearLayout titleRow = new LinearLayout(this);
titleRow.setOrientation(LinearLayout.HORIZONTAL);
titleRow.setGravity(Gravity.CENTER_VERTICAL);
TextView title = new TextView(this); TextView title = new TextView(this);
title.setText(sessionName); title.setText(sessionName);
title.setTextColor(COLOR_TEXT); title.setTextColor(COLOR_TEXT);
@@ -1673,6 +1691,22 @@ public final class MainActivity extends Activity {
title.setSingleLine(true); title.setSingleLine(true);
title.setEllipsize(TextUtils.TruncateAt.END); title.setEllipsize(TextUtils.TruncateAt.END);
title.setIncludeFontPadding(false); title.setIncludeFontPadding(false);
titleRow.addView(title, new LinearLayout.LayoutParams(
0,
dp(17),
1
));
terminalConnectionText = new TextView(this);
terminalConnectionText.setTextSize(9);
terminalConnectionText.setTypeface(Typeface.MONOSPACE);
terminalConnectionText.setSingleLine(true);
terminalConnectionText.setIncludeFontPadding(false);
terminalConnectionText.setPadding(dp(7), 0, 0, 0);
titleRow.addView(terminalConnectionText, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
dp(17)
));
terminalMetaText = new TextView(this); terminalMetaText = new TextView(this);
terminalMetaText.setTextColor(COLOR_TEXT_DIM); terminalMetaText.setTextColor(COLOR_TEXT_DIM);
@@ -1683,7 +1717,7 @@ public final class MainActivity extends Activity {
terminalMetaText.setIncludeFontPadding(false); terminalMetaText.setIncludeFontPadding(false);
updateTerminalMeta(); updateTerminalMeta();
titleBlock.addView(title, new LinearLayout.LayoutParams( titleBlock.addView(titleRow, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
dp(17) dp(17)
)); ));
@@ -1692,10 +1726,36 @@ public final class MainActivity extends Activity {
dp(13) dp(13)
)); ));
bar.addView(titleBlock, new LinearLayout.LayoutParams(0, dp(32), 1)); bar.addView(titleBlock, new LinearLayout.LayoutParams(0, dp(32), 1));
terminalReadingButton = terminalToolButton("", view ->
setTerminalReadingMode(!terminalReadingMode));
terminalReadingButton.setContentDescription("Toggle Chinese reading mode");
styleTerminalReadingButton();
bar.addView(terminalReadingButton);
bar.addView(terminalToolButton("", view -> showTerminalActions(sessionName))); bar.addView(terminalToolButton("", view -> showTerminalActions(sessionName)));
return bar; return bar;
} }
private void setTerminalReadingMode(boolean reading) {
terminalReadingMode = reading;
if (terminalText != null) {
terminalText.setHorizontallyScrolling(!reading);
}
styleTerminalReadingButton();
renderTerminalNow();
setStatus(reading ? "Chinese reading mode" : "Full terminal mode");
}
private void styleTerminalReadingButton() {
if (terminalReadingButton == null) {
return;
}
terminalReadingButton.setText(terminalReadingMode ? "" : "");
terminalReadingButton.setTextColor(terminalReadingMode ? Color.rgb(14, 38, 24) : COLOR_TEXT_MUTED);
terminalReadingButton.setBackground(terminalReadingMode
? rounded(COLOR_ACCENT, 7, COLOR_ACCENT, 1)
: buttonBackground());
}
private HorizontalScrollView createTerminalGroupBar() { private HorizontalScrollView createTerminalGroupBar() {
HorizontalScrollView scroller = new HorizontalScrollView(this); HorizontalScrollView scroller = new HorizontalScrollView(this);
scroller.setHorizontalScrollBarEnabled(false); scroller.setHorizontalScrollBarEnabled(false);
@@ -1735,18 +1795,13 @@ public final class MainActivity extends Activity {
try { try {
List<SessionSummary> sessions = api.getSessions(); List<SessionSummary> sessions = api.getSessions();
String path = ""; String path = "";
String command = "";
for (SessionSummary session : sessions) { for (SessionSummary session : sessions) {
if (sessionName.equals(session.name)) { if (sessionName.equals(session.name)) {
path = defaultValue(session.currentPath, ""); path = defaultValue(session.currentPath, "");
command = defaultValue(session.currentCommand, "");
break; break;
} }
} }
String meta = path.isEmpty() ? "path unavailable" : path; String meta = path.isEmpty() ? "path unavailable" : compactTerminalPath(path);
if (!command.isEmpty()) {
meta = meta + " · " + command;
}
String finalMeta = meta; String finalMeta = meta;
runOnUiThread(() -> { runOnUiThread(() -> {
if (sessionName.equals(activeSessionName)) { if (sessionName.equals(activeSessionName)) {
@@ -1766,18 +1821,28 @@ public final class MainActivity extends Activity {
} }
private void updateTerminalMeta() { private void updateTerminalMeta() {
if (terminalMetaText == null) { if (terminalMetaText == null || terminalConnectionText == null) {
return; return;
} }
StringBuilder meta = new StringBuilder(); terminalMetaText.setText(defaultValue(terminalPathStatus, "path loading"));
meta.append(defaultValue(terminalPathStatus, "path loading")); String socket = terminalSocketStatus.toLowerCase(java.util.Locale.ROOT);
if (!terminalSocketStatus.isEmpty()) { if (socket.contains("connected") && !socket.contains("disconnected")) {
meta.append(" · ").append(terminalSocketStatus); terminalConnectionText.setText("● connected");
terminalConnectionText.setTextColor(COLOR_SUCCESS);
} else if (socket.contains("error") || socket.contains("disconnected")) {
terminalConnectionText.setText("● disconnected");
terminalConnectionText.setTextColor(COLOR_DANGER);
} else {
terminalConnectionText.setText("● connecting");
terminalConnectionText.setTextColor(COLOR_ACCENT_WARM);
} }
if (!terminalEventStatus.isEmpty()) { }
meta.append(" · ").append(terminalEventStatus);
} private String compactTerminalPath(String path) {
terminalMetaText.setText(meta.toString()); String compact = path.trim();
compact = compact.replaceFirst("^/home/[^/]+(?=/|$)", "~");
compact = compact.replaceFirst("^/Users/[^/]+(?=/|$)", "~");
return compact;
} }
private void renderTerminalGroupSessions(String sessionName, String text) { private void renderTerminalGroupSessions(String sessionName, String text) {
@@ -1990,6 +2055,7 @@ public final class MainActivity extends Activity {
"Clear local view and tmux history", "Clear local view and tmux history",
"Split horizontal", "Split horizontal",
"Split vertical", "Split vertical",
"Zoom active pane",
"Page up", "Page up",
"Page down", "Page down",
"Session status", "Session status",
@@ -2021,34 +2087,37 @@ public final class MainActivity extends Activity {
runApiAction("Split vertical", () -> api.splitPane(sessionName, "vertical")); runApiAction("Split vertical", () -> api.splitPane(sessionName, "vertical"));
break; break;
case 4: case 4:
sendTerminalInput("\u0002z");
break;
case 5:
if (terminalSocket != null) { if (terminalSocket != null) {
terminalSocket.scroll(-terminalRows); terminalSocket.scroll(-terminalRows);
} }
break; break;
case 5: case 6:
if (terminalSocket != null) { if (terminalSocket != null) {
terminalSocket.scroll(terminalRows); terminalSocket.scroll(terminalRows);
} }
break; break;
case 6: case 7:
showRaw("Session status", () -> api.sessionStatus(sessionName)); showRaw("Session status", () -> api.sessionStatus(sessionName));
break; break;
case 7: case 8:
promptSendCommand(sessionName); promptSendCommand(sessionName);
break; break;
case 8: case 9:
sendTerminalInput("\u0002"); sendTerminalInput("\u0002");
break; break;
case 9: case 10:
sendTerminalInput("\u0002d"); sendTerminalInput("\u0002d");
break; break;
case 10: case 11:
sendTerminalInput("\u0002c"); sendTerminalInput("\u0002c");
break; break;
case 11: case 12:
sendTerminalInput("\u0002n"); sendTerminalInput("\u0002n");
break; break;
case 12: case 13:
sendTerminalInput("\u0002p"); sendTerminalInput("\u0002p");
break; break;
default: default:
@@ -2061,20 +2130,51 @@ public final class MainActivity extends Activity {
private LinearLayout createComposerBar() { private LinearLayout createComposerBar() {
LinearLayout bar = new LinearLayout(this); LinearLayout bar = new LinearLayout(this);
bar.setOrientation(LinearLayout.VERTICAL); bar.setOrientation(LinearLayout.VERTICAL);
bar.setPadding(dp(9), dp(7), dp(9), dp(8)); bar.setPadding(dp(8), dp(5), dp(8), dp(7));
bar.setBackgroundColor(COLOR_BAR); bar.setBackgroundColor(COLOR_BAR);
terminalImagePreviewBar = createComposerImagePreviewBar();
bar.addView(terminalImagePreviewBar, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
dp(64)
));
LinearLayout row = new LinearLayout(this); LinearLayout row = new LinearLayout(this);
row.setOrientation(LinearLayout.HORIZONTAL); row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.BOTTOM); row.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout tabGrid = new LinearLayout(this);
tabGrid.setOrientation(LinearLayout.VERTICAL);
LinearLayout firstTabRow = terminalKeyRow();
LinearLayout secondTabRow = terminalKeyRow();
addAccessoryTab(firstTabRow, "Edit", 0);
addAccessoryTab(firstTabRow, "Ctrl", 1);
addAccessoryTab(secondTabRow, "Nav", 2);
addAccessoryTab(secondTabRow, "Sym", 3);
tabGrid.addView(firstTabRow, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1
));
LinearLayout.LayoutParams secondTabParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1
);
secondTabParams.topMargin = dp(3);
tabGrid.addView(secondTabRow, secondTabParams);
row.addView(tabGrid, new LinearLayout.LayoutParams(
dp(72),
ViewGroup.LayoutParams.MATCH_PARENT
));
inputField = new EditText(this); inputField = new EditText(this);
inputField.setTextColor(COLOR_TEXT); inputField.setTextColor(COLOR_TEXT);
inputField.setHintTextColor(COLOR_TEXT_DIM); inputField.setHintTextColor(COLOR_TEXT_DIM);
inputField.setHint("type a command"); inputField.setHint("type command or text");
inputField.setSingleLine(false); inputField.setSingleLine(false);
inputField.setMinLines(1); inputField.setMinLines(3);
inputField.setMaxLines(2); inputField.setMaxLines(5);
inputField.setCursorVisible(true); inputField.setCursorVisible(true);
inputField.setFocusableInTouchMode(true); inputField.setFocusableInTouchMode(true);
inputField.setGravity(Gravity.TOP | Gravity.START); inputField.setGravity(Gravity.TOP | Gravity.START);
@@ -2099,25 +2199,122 @@ public final class MainActivity extends Activity {
} }
return false; return false;
}); });
Button image = terminalToolButton("", view -> pickImageForSession(activeSessionName));
image.setContentDescription("Upload image");
row.addView(image, new LinearLayout.LayoutParams(dp(40), dp(40)));
LinearLayout.LayoutParams inputParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams inputParams = new LinearLayout.LayoutParams(
0, 0,
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
1 1
); );
inputParams.leftMargin = dp(6); inputParams.leftMargin = dp(5);
inputParams.rightMargin = dp(6); inputParams.rightMargin = dp(5);
row.addView(inputField, inputParams); row.addView(inputField, inputParams);
Button send = primaryButton("", view -> sendLine());
send.setTextSize(18); LinearLayout actions = new LinearLayout(this);
actions.setOrientation(LinearLayout.VERTICAL);
Button send = composerPrimaryButton("Send", view -> sendLine());
send.setContentDescription("Send input"); send.setContentDescription("Send input");
row.addView(send, new LinearLayout.LayoutParams(dp(40), dp(40))); actions.addView(send, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1
));
Button image = composerButton("Img", view -> pickImageForSession(activeSessionName));
image.setContentDescription("Upload image");
LinearLayout.LayoutParams imageParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1
);
imageParams.topMargin = dp(3);
actions.addView(image, imageParams);
row.addView(actions, new LinearLayout.LayoutParams(
dp(48),
ViewGroup.LayoutParams.MATCH_PARENT
));
bar.addView(row, matchWrap()); bar.addView(row, matchWrap());
return bar; return bar;
} }
private LinearLayout createComposerImagePreviewBar() {
LinearLayout previewBar = new LinearLayout(this);
previewBar.setOrientation(LinearLayout.HORIZONTAL);
previewBar.setGravity(Gravity.CENTER_VERTICAL);
previewBar.setPadding(dp(4), dp(3), dp(2), dp(7));
previewBar.setVisibility(View.GONE);
terminalImagePreview = new ImageView(this);
terminalImagePreview.setScaleType(ImageView.ScaleType.CENTER_CROP);
terminalImagePreview.setAdjustViewBounds(false);
terminalImagePreview.setBackground(rounded(COLOR_FIELD, 6, COLOR_BORDER, 1));
terminalImagePreview.setContentDescription("Open uploaded image preview");
terminalImagePreview.setOnClickListener(view -> {
if (!terminalImagePath.isEmpty()) {
showImagePreview(terminalImagePath, "");
}
});
previewBar.addView(terminalImagePreview, new LinearLayout.LayoutParams(dp(52), dp(52)));
terminalImagePreviewPath = bodyText("Loading image preview...");
terminalImagePreviewPath.setSingleLine(true);
terminalImagePreviewPath.setEllipsize(TextUtils.TruncateAt.MIDDLE);
terminalImagePreviewPath.setTextSize(10);
LinearLayout.LayoutParams pathParams = new LinearLayout.LayoutParams(0, dp(52), 1);
pathParams.leftMargin = dp(9);
previewBar.addView(terminalImagePreviewPath, pathParams);
Button clear = terminalToolButton("×", view -> clearComposerImagePreview());
clear.setContentDescription("Clear image preview");
previewBar.addView(clear);
return previewBar;
}
private void loadComposerImagePreview(String path) {
if (terminalImagePreviewBar == null || path == null || path.isEmpty()) {
return;
}
terminalImagePath = path;
terminalImagePreview.setImageDrawable(null);
terminalImagePreviewPath.setText("Loading " + path);
terminalImagePreviewBar.setVisibility(View.VISIBLE);
int generation = terminalConnectionGeneration;
ImageView preview = terminalImagePreview;
TextView previewPath = terminalImagePreviewPath;
executor.execute(() -> {
try {
byte[] bytes = api.imagePreview(path, "");
Bitmap bitmap = decodePreviewBitmap(bytes, 1200);
if (bitmap == null) {
throw new IllegalStateException("Preview is not a supported bitmap");
}
runOnUiThread(() -> {
if (generation == terminalConnectionGeneration
&& path.equals(terminalImagePath)
&& preview == terminalImagePreview) {
preview.setImageBitmap(bitmap);
previewPath.setText(path);
}
});
} catch (Exception error) {
runOnUiThread(() -> {
if (generation == terminalConnectionGeneration
&& path.equals(terminalImagePath)
&& previewPath == terminalImagePreviewPath) {
previewPath.setText("Preview unavailable " + path);
}
});
}
});
}
private void clearComposerImagePreview() {
terminalImagePath = "";
if (terminalImagePreview != null) {
terminalImagePreview.setImageDrawable(null);
}
if (terminalImagePreviewBar != null) {
terminalImagePreviewBar.setVisibility(View.GONE);
}
}
private void promptRenameSession(String sessionName) { private void promptRenameSession(String sessionName) {
promptText("Rename session", sessionName, sessionName, nextName -> { promptText("Rename session", sessionName, sessionName, nextName -> {
if (!nextName.matches("[A-Za-z0-9._-]+")) { if (!nextName.matches("[A-Za-z0-9._-]+")) {
@@ -2546,7 +2743,7 @@ public final class MainActivity extends Activity {
executor.execute(() -> { executor.execute(() -> {
try { try {
byte[] bytes = api.imagePreview(path, basePath); byte[] bytes = api.imagePreview(path, basePath);
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); Bitmap bitmap = decodePreviewBitmap(bytes, 1800);
if (bitmap == null) { if (bitmap == null) {
throw new IllegalStateException("Preview is not a supported bitmap"); throw new IllegalStateException("Preview is not a supported bitmap");
} }
@@ -2571,6 +2768,23 @@ public final class MainActivity extends Activity {
}); });
} }
private Bitmap decodePreviewBitmap(byte[] bytes, int maxDimension) {
BitmapFactory.Options bounds = new BitmapFactory.Options();
bounds.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, bounds);
if (bounds.outWidth <= 0 || bounds.outHeight <= 0) {
return null;
}
int sampleSize = 1;
while (bounds.outWidth / sampleSize > maxDimension
|| bounds.outHeight / sampleSize > maxDimension) {
sampleSize *= 2;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
}
private void promptText(String title, String hint, String value, TextApiAction action) { private void promptText(String title, String hint, String value, TextApiAction action) {
EditText input = new EditText(this); EditText input = new EditText(this);
input.setSingleLine(true); input.setSingleLine(true);
@@ -2676,19 +2890,26 @@ public final class MainActivity extends Activity {
return input; return input;
} }
private HorizontalScrollView createAccessoryBar() { private LinearLayout createAccessoryBar() {
HorizontalScrollView scroller = new HorizontalScrollView(this); LinearLayout panel = new LinearLayout(this);
scroller.setHorizontalScrollBarEnabled(false); panel.setOrientation(LinearLayout.VERTICAL);
scroller.setBackgroundColor(COLOR_PANEL); panel.setPadding(dp(4), dp(3), dp(4), dp(3));
panel.setBackgroundColor(COLOR_PANEL);
LinearLayout row = terminalKeyRow(); HorizontalScrollView keyScroller = new HorizontalScrollView(this);
row.setPadding(dp(8), dp(5), dp(8), dp(5)); keyScroller.setHorizontalScrollBarEnabled(false);
addAccessoryPageKeys(row); keyScroller.setFillViewport(true);
scroller.addView(row, new HorizontalScrollView.LayoutParams( LinearLayout keyRow = terminalKeyRow();
addAccessoryPageKeys(keyRow);
keyScroller.addView(keyRow, new HorizontalScrollView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT ViewGroup.LayoutParams.MATCH_PARENT
)); ));
return scroller; panel.addView(keyScroller, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
return panel;
} }
private LinearLayout terminalKeyRow() { private LinearLayout terminalKeyRow() {
@@ -2699,35 +2920,68 @@ public final class MainActivity extends Activity {
} }
private void addAccessoryPageKeys(LinearLayout row) { private void addAccessoryPageKeys(LinearLayout row) {
addAccessoryButton(row, "ctrl", view -> sendTerminalInput("\u0002"));
addSoftKey(row, "esc", "\u001b");
addSoftKey(row, "tab", "\t");
addTextKey(row, "~", "~");
addTextKey(row, "/", "/");
addTextKey(row, "-", "-");
addTextKey(row, "|", "|");
addSoftKey(row, "C-c", "\u0003");
addSoftKey(row, "C-d", "\u0004");
addSoftKey(row, "C-z", "\u001a");
addSoftKey(row, "C-l", "\u000c");
addComposerButton(row, "", () -> moveComposerCursor(-1));
addComposerButton(row, "", () -> moveComposerCursor(1));
addSoftKey(row, "", "\u001b[A");
addSoftKey(row, "", "\u001b[B");
addSoftButton(row, "Paste", view -> pasteClipboard());
}
private String accessoryPageName() {
switch (terminalKeyPage) { switch (terminalKeyPage) {
case 1: case 1:
return "CTRL"; addSoftKey(row, "Esc", "\u001b");
addSoftKey(row, "Tab", "\t");
addSoftKey(row, "C-c", "\u0003");
addSoftKey(row, "C-d", "\u0004");
addSoftKey(row, "C-z", "\u001a");
addSoftKey(row, "C-l", "\u000c");
addSoftKey(row, "C-r", "\u0012");
addSoftKey(row, "C-a", "\u0001");
addSoftKey(row, "C-e", "\u0005");
addSoftKey(row, "C-u", "\u0015");
addSoftKey(row, "C-k", "\u000b");
addSoftKey(row, "C-b", "\u0002");
break;
case 2: case 2:
return "NAV"; addSoftKey(row, "", "\u001b[D");
addSoftKey(row, "", "\u001b[C");
addSoftKey(row, "", "\u001b[A");
addSoftKey(row, "", "\u001b[B");
addSoftKey(row, "Home", "\u001b[H");
addSoftKey(row, "End", "\u001b[F");
addSoftKey(row, "Pg↑", "\u001b[5~");
addSoftKey(row, "Pg↓", "\u001b[6~");
addSoftKey(row, "Clear", "\u000c");
addSoftKey(row, "Detach", "\u0002d");
break;
case 3: case 3:
return "SYM"; addTextKey(row, "~", "~");
addTextKey(row, "/", "/");
addTextKey(row, "-", "-");
addTextKey(row, "_", "_");
addTextKey(row, ".", ".");
addTextKey(row, "|", "|");
addTextKey(row, "&", "&");
addTextKey(row, ";", ";");
addTextKey(row, "$", "$");
addTextKey(row, "\"", "\"");
addTextKey(row, "'", "'");
addTextKey(row, "Space", " ");
break;
case 0: case 0:
default: default:
return "EDIT"; addComposerButton(row, "", () -> moveComposerCursor(-1));
addComposerButton(row, "", () -> moveComposerCursor(1));
addSoftKey(row, "", "\u001b[A");
addSoftKey(row, "", "\u001b[B");
addSoftKey(row, "Esc", "\u001b");
addSoftKey(row, "Tab", "\t");
addSoftKey(row, "", TERMINAL_ENTER);
addSoftButton(row, "Paste", view -> pasteClipboard());
addComposerButton(row, "", this::backspaceComposerText);
addTextKey(row, "NL", "\n");
addComposerButton(row, "Bot", this::scrollTerminalBottom);
addComposerButton(row, "Sel", () -> {
terminalSelectionEnabled = !terminalSelectionEnabled;
if (terminalText != null) {
terminalText.setTextIsSelectable(terminalSelectionEnabled);
}
setStatus(terminalSelectionEnabled ? "Terminal selection on" : "Terminal selection off");
});
break;
} }
} }
@@ -2823,7 +3077,7 @@ public final class MainActivity extends Activity {
} }
int horizontalPadding = terminalText.getPaddingLeft() + terminalText.getPaddingRight(); int horizontalPadding = terminalText.getPaddingLeft() + terminalText.getPaddingRight();
int verticalPadding = terminalText.getPaddingTop() + terminalText.getPaddingBottom(); int verticalPadding = terminalText.getPaddingTop() + terminalText.getPaddingBottom();
float charWidth = terminalText.getPaint().measureText("W"); float charWidth = terminalText.getPaint().measureText("0000000000") / 10f;
if (charWidth <= 0f) { if (charWidth <= 0f) {
charWidth = dp(8); charWidth = dp(8);
} }
@@ -2997,19 +3251,20 @@ public final class MainActivity extends Activity {
private void setTerminalKeyPage(int page) { private void setTerminalKeyPage(int page) {
terminalKeyPage = (page + 4) % 4; terminalKeyPage = (page + 4) % 4;
updateTerminalPageButton(); updateAccessoryTabs();
if (activeSessionName != null) { if (activeSessionName != null) {
renderTerminalControlsOnly(); renderTerminalControlsOnly();
} }
} }
private void renderTerminalControlsOnly() { private void renderTerminalControlsOnly() {
int composerIndex = terminalComposerBar == null ? -1 : root.indexOfChild(terminalComposerBar); int accessoryIndex = terminalAccessoryBar == null ? -1 : root.indexOfChild(terminalAccessoryBar);
if (composerIndex < 0 || composerIndex + 1 >= root.getChildCount()) { if (accessoryIndex < 0) {
return; return;
} }
root.removeViewAt(composerIndex + 1); root.removeViewAt(accessoryIndex);
root.addView(createAccessoryBar(), composerIndex + 1, new LinearLayout.LayoutParams( terminalAccessoryBar = createAccessoryBar();
root.addView(terminalAccessoryBar, accessoryIndex, new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
dp(TERMINAL_KEYS_HEIGHT_DP) dp(TERMINAL_KEYS_HEIGHT_DP)
)); ));
@@ -3038,7 +3293,9 @@ public final class MainActivity extends Activity {
return; return;
} }
lastTerminalRenderMs = System.currentTimeMillis(); lastTerminalRenderMs = System.currentTimeMillis();
terminalText.setText(terminalScreen.render()); terminalText.setText(terminalReadingMode
? terminalScreen.renderFocused()
: terminalScreen.render());
if (terminalFollowOutput) { if (terminalFollowOutput) {
terminalScroll.post(() -> terminalScroll.fullScroll(View.FOCUS_DOWN)); terminalScroll.post(() -> terminalScroll.fullScroll(View.FOCUS_DOWN));
} }
@@ -3162,29 +3419,23 @@ public final class MainActivity extends Activity {
return button; return button;
} }
private Button terminalPageButton() { private Button composerButton(String label, View.OnClickListener listener) {
Button button = toolbarButton("", view -> setTerminalKeyPage(terminalKeyPage + 1)); Button button = toolbarButton(label, listener);
button.setTextSize(9); button.setTextSize(10);
button.setPadding(dp(4), 0, dp(4), 0); button.setPadding(dp(5), 0, dp(5), 0);
button.setMinWidth(dp(42)); button.setMinWidth(dp(38));
button.setMinimumWidth(dp(42)); button.setMinimumWidth(dp(38));
button.setOnLongClickListener(view -> { button.setMinHeight(0);
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); button.setMinimumHeight(0);
setTerminalKeyPage(terminalKeyPage - 1);
return true;
});
updateTerminalPageButton(button);
return button; return button;
} }
private void updateTerminalPageButton() { private Button composerPrimaryButton(String label, View.OnClickListener listener) {
updateTerminalPageButton(terminalKeyPageButton); Button button = composerButton(label, listener);
} button.setTextColor(Color.rgb(14, 38, 24));
button.setTextSize(11);
private void updateTerminalPageButton(Button button) { button.setBackground(rounded(COLOR_ACCENT, 7, COLOR_ACCENT, 1));
if (button != null) { return button;
button.setText(accessoryPageName());
}
} }
private void styleInput(EditText input) { private void styleInput(EditText input) {
@@ -3199,8 +3450,8 @@ public final class MainActivity extends Activity {
private void styleComposerInput(EditText input) { private void styleComposerInput(EditText input) {
input.setTextColor(COLOR_TEXT); input.setTextColor(COLOR_TEXT);
input.setHintTextColor(COLOR_TEXT_DIM); input.setHintTextColor(COLOR_TEXT_DIM);
input.setTextSize(14); input.setTextSize(13);
input.setPadding(dp(9), dp(6), dp(9), dp(6)); input.setPadding(dp(8), dp(5), dp(8), dp(5));
input.setBackground(inputBackground()); input.setBackground(inputBackground());
} }
@@ -3252,32 +3503,64 @@ public final class MainActivity extends Activity {
addSoftButton(row, label, view -> insertComposerText(text)); addSoftButton(row, label, view -> insertComposerText(text));
} }
private void addAccessoryButton(LinearLayout row, String label, View.OnClickListener listener) {
addSoftButton(row, label, listener);
}
private void addComposerButton(LinearLayout row, String label, Runnable action) { private void addComposerButton(LinearLayout row, String label, Runnable action) {
addSoftButton(row, label, view -> action.run()); addSoftButton(row, label, view -> action.run());
} }
private void addSoftButton(LinearLayout row, String label, View.OnClickListener listener) { private void addSoftButton(LinearLayout row, String label, View.OnClickListener listener) {
Button button = toolbarButton(label, listener); Button button = toolbarButton(label, listener);
button.setTextSize(isArrowLabel(label) ? 15 : 9); button.setTextSize(isArrowLabel(label) ? 14 : 9);
button.setPadding(dp(4), 0, dp(4), 0); button.setPadding(dp(2), 0, dp(2), 0);
int width = "Space".equals(label) ? 56 : (isArrowLabel(label) ? 32 : 38); button.setMinWidth(dp(24));
button.setMinWidth(dp(width)); button.setMinimumWidth(dp(24));
button.setMinimumWidth(dp(width)); button.setMinHeight(0);
button.setMinimumHeight(0);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
dp(27) ViewGroup.LayoutParams.MATCH_PARENT,
0
); );
params.leftMargin = dp(1);
params.rightMargin = dp(1); params.rightMargin = dp(1);
params.topMargin = dp(2);
params.bottomMargin = dp(1);
row.addView(button, params); row.addView(button, params);
} }
private void addAccessoryTab(LinearLayout row, String label, int page) {
Button button = toolbarButton(label, view -> setTerminalKeyPage(page));
button.setTextSize(8);
button.setPadding(dp(3), 0, dp(3), 0);
button.setMinWidth(0);
button.setMinimumWidth(0);
button.setMinHeight(0);
button.setMinimumHeight(0);
terminalAccessoryTabButtons[page] = button;
styleAccessoryTab(button, terminalKeyPage == page);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
0,
ViewGroup.LayoutParams.MATCH_PARENT,
1
);
if (row.getChildCount() > 0) {
params.leftMargin = dp(3);
}
row.addView(button, params);
}
private void updateAccessoryTabs() {
for (int page = 0; page < terminalAccessoryTabButtons.length; page++) {
Button button = terminalAccessoryTabButtons[page];
if (button != null) {
styleAccessoryTab(button, terminalKeyPage == page);
}
}
}
private void styleAccessoryTab(Button button, boolean selected) {
button.setTextColor(selected ? Color.rgb(14, 38, 24) : COLOR_TEXT_MUTED);
button.setBackground(selected
? rounded(COLOR_ACCENT, 6, COLOR_ACCENT, 1)
: rounded(COLOR_CARD_ALT, 6, COLOR_BORDER_SOFT, 1));
}
private boolean isArrowLabel(String label) { private boolean isArrowLabel(String label) {
return "".equals(label) || "".equals(label) || "".equals(label) || "".equals(label); return "".equals(label) || "".equals(label) || "".equals(label) || "".equals(label);
} }
@@ -3585,9 +3868,15 @@ public final class MainActivity extends Activity {
throw new IllegalStateException("Cannot open selected image"); throw new IllegalStateException("Cannot open selected image");
} }
String response = api.uploadImage(sessionName, readAllBytes(input)); String response = api.uploadImage(sessionName, readAllBytes(input));
String imagePath = new JSONObject(response).optString("absolutePath", "").trim();
if (imagePath.isEmpty()) {
throw new IllegalStateException("Upload response did not include absolutePath");
}
runOnUiThread(() -> { runOnUiThread(() -> {
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
showTextDialog("Image upload", response); insertComposerPath(imagePath);
loadComposerImagePreview(imagePath);
setStatus("Image path inserted");
}); });
} catch (Exception error) { } catch (Exception error) {
runOnUiThread(() -> { runOnUiThread(() -> {
@@ -3609,6 +3898,21 @@ public final class MainActivity extends Activity {
return output.toByteArray(); return output.toByteArray();
} }
private void insertComposerPath(String path) {
if (inputField == null) {
return;
}
int start = Math.max(0, inputField.getSelectionStart());
int end = Math.max(0, inputField.getSelectionEnd());
int from = Math.min(start, end);
int to = Math.max(start, end);
CharSequence current = inputField.getText();
String prefix = from > 0 && !Character.isWhitespace(current.charAt(from - 1)) ? " " : "";
String suffix = to < current.length() && Character.isWhitespace(current.charAt(to)) ? "" : " ";
inputField.getText().replace(from, to, prefix + path + suffix);
inputField.requestFocus();
}
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (activeSessionName != null) { if (activeSessionName != null) {
@@ -14,6 +14,7 @@ import java.util.List;
final class TerminalScreenBuffer { final class TerminalScreenBuffer {
private static final int DEFAULT_FG = 0xffe6ebf2; private static final int DEFAULT_FG = 0xffe6ebf2;
private static final int DEFAULT_BG = Color.TRANSPARENT; private static final int DEFAULT_BG = Color.TRANSPARENT;
private static final int TERMINAL_BG = 0xff0b0e13;
private int cols; private int cols;
private int rows; private int rows;
@@ -27,6 +28,7 @@ final class TerminalScreenBuffer {
private int fg = DEFAULT_FG; private int fg = DEFAULT_FG;
private int bg = DEFAULT_BG; private int bg = DEFAULT_BG;
private boolean bold; private boolean bold;
private boolean dim;
TerminalScreenBuffer(int cols, int rows) { TerminalScreenBuffer(int cols, int rows) {
resize(cols, rows); resize(cols, rows);
@@ -35,9 +37,6 @@ final class TerminalScreenBuffer {
void resize(int nextCols, int nextRows) { void resize(int nextCols, int nextRows) {
nextCols = Math.max(1, nextCols); nextCols = Math.max(1, nextCols);
nextRows = Math.max(1, nextRows); nextRows = Math.max(1, nextRows);
Cell[][] previous = cells;
int previousRows = rows;
int previousCols = cols;
cols = nextCols; cols = nextCols;
rows = nextRows; rows = nextRows;
cells = new Cell[rows][cols]; cells = new Cell[rows][cols];
@@ -46,21 +45,11 @@ final class TerminalScreenBuffer {
cells[row][col] = new Cell(); cells[row][col] = new Cell();
} }
} }
if (previous != null) { cursorRow = 0;
int copyRows = Math.min(previousRows, rows); cursorCol = 0;
int copyCols = Math.min(previousCols, cols); savedRow = 0;
int previousStart = Math.max(0, previousRows - copyRows); savedCol = 0;
int nextStart = Math.max(0, rows - copyRows); wrapPending = false;
for (int row = 0; row < copyRows; row++) {
for (int col = 0; col < copyCols; col++) {
cells[nextStart + row][col].copyFrom(previous[previousStart + row][col]);
}
}
}
cursorRow = clamp(cursorRow, 0, rows - 1);
cursorCol = clamp(cursorCol, 0, cols - 1);
savedRow = clamp(savedRow, 0, rows - 1);
savedCol = clamp(savedCol, 0, cols - 1);
} }
void clear() { void clear() {
@@ -74,6 +63,7 @@ final class TerminalScreenBuffer {
fg = DEFAULT_FG; fg = DEFAULT_FG;
bg = DEFAULT_BG; bg = DEFAULT_BG;
bold = false; bold = false;
dim = false;
} }
void write(String text) { void write(String text) {
@@ -131,6 +121,42 @@ final class TerminalScreenBuffer {
return output; return output;
} }
CharSequence renderFocused() {
List<String> visible = new ArrayList<>();
int hiddenRows = 0;
for (int row = 0; row < rows; row++) {
String text = rowText(row).trim();
if (containsHan(text)) {
if (hiddenRows > 0) {
visible.add("... 已折叠 " + hiddenRows + " 行终端内容 ...");
hiddenRows = 0;
}
visible.add(text);
} else if (!text.isEmpty()) {
hiddenRows++;
}
}
if (hiddenRows > 0) {
visible.add("... 已折叠 " + hiddenRows + " 行终端内容 ...");
}
if (visible.isEmpty()) {
for (int row = Math.max(0, rows - 4); row < rows; row++) {
String text = rowText(row).trim();
if (!text.isEmpty()) {
visible.add(text);
}
}
}
StringBuilder output = new StringBuilder();
for (int index = 0; index < visible.size(); index++) {
if (index > 0) {
output.append('\n');
}
output.append(visible.get(index));
}
return output;
}
private int handleEscape(String text, int index) { private int handleEscape(String text, int index) {
if (index + 1 >= text.length()) { if (index + 1 >= text.length()) {
return -1; return -1;
@@ -257,10 +283,14 @@ final class TerminalScreenBuffer {
fg = DEFAULT_FG; fg = DEFAULT_FG;
bg = DEFAULT_BG; bg = DEFAULT_BG;
bold = false; bold = false;
dim = false;
} else if (value == 1) { } else if (value == 1) {
bold = true; bold = true;
} else if (value == 2) {
dim = true;
} else if (value == 22) { } else if (value == 22) {
bold = false; bold = false;
dim = false;
} else if (value == 39) { } else if (value == 39) {
fg = DEFAULT_FG; fg = DEFAULT_FG;
} else if (value == 49) { } else if (value == 49) {
@@ -302,14 +332,34 @@ final class TerminalScreenBuffer {
wrapPending = false; wrapPending = false;
newLine(); newLine();
} }
cells[cursorRow][cursorCol].set(value, fg, bg, bold); int width = isWideCharacter(value) ? 2 : 1;
if (cursorCol == cols - 1) { if (width == 2 && cursorCol == cols - 1) {
newLine();
}
cells[cursorRow][cursorCol].set(value, fg, bg, bold, dim);
if (width == 2) {
cells[cursorRow][cursorCol + 1].setContinuation(fg, bg, bold, dim);
}
if (cursorCol + width >= cols) {
cursorCol = cols - 1;
wrapPending = true; wrapPending = true;
} else { } else {
cursorCol++; cursorCol += width;
} }
} }
private boolean isWideCharacter(char value) {
return value >= '\u1100' && (value <= '\u115f'
|| value == '\u2329' || value == '\u232a'
|| (value >= '\u2e80' && value <= '\ua4cf' && value != '\u303f')
|| (value >= '\uac00' && value <= '\ud7a3')
|| (value >= '\uf900' && value <= '\ufaff')
|| (value >= '\ufe10' && value <= '\ufe19')
|| (value >= '\ufe30' && value <= '\ufe6f')
|| (value >= '\uff00' && value <= '\uff60')
|| (value >= '\uffe0' && value <= '\uffe6'));
}
private void newLine() { private void newLine() {
wrapPending = false; wrapPending = false;
cursorRow++; cursorRow++;
@@ -453,15 +503,21 @@ final class TerminalScreenBuffer {
int fgColor = first.fg; int fgColor = first.fg;
int bgColor = first.bg; int bgColor = first.bg;
boolean isBold = first.bold; boolean isBold = first.bold;
boolean isDim = first.dim;
while (col < cols) { while (col < cols) {
Cell cell = cells[row][col]; Cell cell = cells[row][col];
if (cell.fg != fgColor || cell.bg != bgColor || cell.bold != isBold) { if (cell.fg != fgColor || cell.bg != bgColor || cell.bold != isBold || cell.dim != isDim) {
break; break;
} }
output.append(cell.value); if (!cell.continuation) {
output.append(cell.value);
}
col++; col++;
} }
int end = output.length(); int end = output.length();
if (isDim) {
fgColor = blendColor(fgColor, bgColor == DEFAULT_BG ? TERMINAL_BG : bgColor, 0.55f);
}
output.setSpan(new ForegroundColorSpan(fgColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); output.setSpan(new ForegroundColorSpan(fgColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (bgColor != DEFAULT_BG) { if (bgColor != DEFAULT_BG) {
output.setSpan(new BackgroundColorSpan(bgColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); output.setSpan(new BackgroundColorSpan(bgColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -472,6 +528,29 @@ final class TerminalScreenBuffer {
} }
} }
private String rowText(int row) {
StringBuilder text = new StringBuilder(cols);
for (int col = 0; col < cols; col++) {
Cell cell = cells[row][col];
if (!cell.continuation) {
text.append(cell.value);
}
}
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() { private void saveCursor() {
savedRow = cursorRow; savedRow = cursorRow;
savedCol = cursorCol; savedCol = cursorCol;
@@ -613,6 +692,15 @@ final class TerminalScreenBuffer {
return value == 0 ? 0 : 55 + value * 40; return value == 0 ? 0 : 55 + value * 40;
} }
private static int blendColor(int foreground, int background, float foregroundRatio) {
float backgroundRatio = 1f - foregroundRatio;
return Color.rgb(
Math.round(Color.red(foreground) * foregroundRatio + Color.red(background) * backgroundRatio),
Math.round(Color.green(foreground) * foregroundRatio + Color.green(background) * backgroundRatio),
Math.round(Color.blue(foreground) * foregroundRatio + Color.blue(background) * backgroundRatio)
);
}
private static int clamp(int value, int min, int max) { private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value)); return Math.max(min, Math.min(max, value));
} }
@@ -622,19 +710,34 @@ final class TerminalScreenBuffer {
int fg = DEFAULT_FG; int fg = DEFAULT_FG;
int bg = DEFAULT_BG; int bg = DEFAULT_BG;
boolean bold; boolean bold;
boolean dim;
boolean continuation;
void clear() { void clear() {
value = ' '; value = ' ';
fg = DEFAULT_FG; fg = DEFAULT_FG;
bg = DEFAULT_BG; bg = DEFAULT_BG;
bold = false; bold = false;
dim = false;
continuation = false;
} }
void set(char nextValue, int nextFg, int nextBg, boolean nextBold) { void set(char nextValue, int nextFg, int nextBg, boolean nextBold, boolean nextDim) {
value = nextValue; value = nextValue;
fg = nextFg; fg = nextFg;
bg = nextBg; bg = nextBg;
bold = nextBold; bold = nextBold;
dim = nextDim;
continuation = false;
}
void setContinuation(int nextFg, int nextBg, boolean nextBold, boolean nextDim) {
value = ' ';
fg = nextFg;
bg = nextBg;
bold = nextBold;
dim = nextDim;
continuation = true;
} }
void copyFrom(Cell other) { void copyFrom(Cell other) {
@@ -642,6 +745,8 @@ final class TerminalScreenBuffer {
fg = other.fg; fg = other.fg;
bg = other.bg; bg = other.bg;
bold = other.bold; bold = other.bold;
dim = other.dim;
continuation = other.continuation;
} }
} }
} }