Compare commits

...
14 Commits
Author SHA1 Message Date
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
Codex 8fda3fa2da Align primary UI with v0 design
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Failing after 8m54s
2026-07-10 14:23:25 +00:00
Codex 69f40e7d36 Apply v0 tmux management UI
Gitea Android APK / build (push) Failing after 0s
Gitea Smoke / smoke (push) Successful in 0s
2026-07-10 10:47:27 +00:00
Codex bc918e7664 Use full terminal text width
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Failing after 8m19s
2026-07-10 09:11:03 +00:00
Codex aba3930417 Fix TLS socket wrapper compilation
Gitea Android APK / build (push) Failing after 0s
Gitea Smoke / smoke (push) Successful in 0s
2026-07-10 03:57:57 +00:00
Codex 4476f41aa6 Reconnect terminal websockets automatically
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Has been cancelled
2026-07-10 03:51:42 +00:00
Codex 5122683761 Compact terminal composer controls
Gitea Android APK / build (push) Failing after 0s
Gitea Smoke / smoke (push) Successful in 0s
2026-07-10 02:43:11 +00:00
Codex 924835dd55 Fix terminal enter and ungrouped sessions
Gitea Smoke / smoke (push) Successful in 1s
Gitea Android APK / build (push) Successful in 11m55s
2026-07-09 16:36:52 +00:00
5 changed files with 1224 additions and 369 deletions
+22 -4
View File
@@ -16,13 +16,31 @@ jobs:
run: |
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
apt-get update
apt-get install -y git curl unzip
install_packages git curl unzip
fi
if ! command -v java >/dev/null 2>&1; then
apt-get update
apt-get install -y openjdk-17-jdk-headless
if command -v apk >/dev/null 2>&1; then
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
java -version
@@ -4,6 +4,7 @@ import android.util.Base64;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
@@ -14,16 +15,22 @@ import java.util.Arrays;
import javax.net.ssl.SSLSocketFactory;
final class AppEventSocketClient {
private static final long HEARTBEAT_INTERVAL_MS = 15000L;
private static final int SOCKET_CONNECT_TIMEOUT_MS = 10000;
private static final int SOCKET_READ_TIMEOUT_MS = 45000;
interface Listener {
void onMessage(String text);
void onClosed();
}
private final Listener listener;
private final Object writeLock = new Object();
private Socket socket;
private BufferedInputStream input;
private BufferedOutputStream output;
private volatile boolean closed;
private Thread heartbeatThread;
AppEventSocketClient(Listener listener) {
this.listener = listener;
@@ -40,12 +47,11 @@ final class AppEventSocketClient {
sendFrame(8, new byte[0]);
} catch (Exception ignored) {
}
try {
if (socket != null) {
socket.close();
}
} catch (Exception ignored) {
}
closeSocketQuietly();
}
boolean isClosed() {
return closed;
}
private void run(String baseUrl) {
@@ -55,6 +61,7 @@ final class AppEventSocketClient {
input = new BufferedInputStream(socket.getInputStream());
output = new BufferedOutputStream(socket.getOutputStream());
handshake(uri);
startHeartbeat();
while (!closed) {
Frame frame = readFrame();
if (frame.opcode == 1) {
@@ -69,12 +76,7 @@ final class AppEventSocketClient {
} finally {
closed = true;
listener.onClosed();
try {
if (socket != null) {
socket.close();
}
} catch (Exception ignored) {
}
closeSocketQuietly();
}
}
@@ -91,10 +93,39 @@ final class AppEventSocketClient {
if (port == -1) {
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
}
Socket raw = new Socket();
raw.connect(new InetSocketAddress(uri.getHost(), port), SOCKET_CONNECT_TIMEOUT_MS);
Socket connected;
if ("wss".equalsIgnoreCase(uri.getScheme())) {
return SSLSocketFactory.getDefault().createSocket(uri.getHost(), port);
connected = ((SSLSocketFactory) SSLSocketFactory.getDefault())
.createSocket(raw, uri.getHost(), port, true);
} else {
connected = raw;
}
return new Socket(uri.getHost(), port);
connected.setKeepAlive(true);
connected.setTcpNoDelay(true);
connected.setSoTimeout(SOCKET_READ_TIMEOUT_MS);
return connected;
}
private void startHeartbeat() {
heartbeatThread = new Thread(() -> {
while (!closed) {
try {
Thread.sleep(HEARTBEAT_INTERVAL_MS);
if (!closed) {
sendFrame(9, new byte[0]);
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return;
} catch (Exception error) {
closeSocketQuietly();
return;
}
}
}, "app-events-ws-heartbeat");
heartbeatThread.start();
}
private void handshake(URI uri) throws Exception {
@@ -181,32 +212,47 @@ final class AppEventSocketClient {
}
private void sendFrame(int opcode, byte[] payload) throws Exception {
if (output == null) {
return;
}
output.write(0x80 | opcode);
byte[] mask = new byte[4];
new SecureRandom().nextBytes(mask);
int length = payload.length;
if (length < 126) {
output.write(0x80 | length);
} else if (length <= 0xffff) {
output.write(0x80 | 126);
output.write((length >>> 8) & 0xff);
output.write(length & 0xff);
} else {
output.write(0x80 | 127);
for (int i = 7; i >= 0; i--) {
output.write((length >>> (8 * i)) & 0xff);
synchronized (writeLock) {
if (output == null) {
return;
}
output.write(0x80 | opcode);
byte[] mask = new byte[4];
new SecureRandom().nextBytes(mask);
int length = payload.length;
if (length < 126) {
output.write(0x80 | length);
} else if (length <= 0xffff) {
output.write(0x80 | 126);
output.write((length >>> 8) & 0xff);
output.write(length & 0xff);
} else {
output.write(0x80 | 127);
for (int i = 7; i >= 0; i--) {
output.write((length >>> (8 * i)) & 0xff);
}
}
output.write(mask);
byte[] masked = Arrays.copyOf(payload, payload.length);
for (int i = 0; i < masked.length; i++) {
masked[i] = (byte) (masked[i] ^ mask[i % 4]);
}
output.write(masked);
output.flush();
}
output.write(mask);
byte[] masked = Arrays.copyOf(payload, payload.length);
for (int i = 0; i < masked.length; i++) {
masked[i] = (byte) (masked[i] ^ mask[i % 4]);
}
private void closeSocketQuietly() {
if (heartbeatThread != null) {
heartbeatThread.interrupt();
heartbeatThread = null;
}
try {
if (socket != null) {
socket.close();
}
} catch (Exception ignored) {
}
output.write(masked);
output.flush();
}
private static final class Frame {
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,7 @@ import java.util.List;
final class TerminalScreenBuffer {
private static final int DEFAULT_FG = 0xffe6ebf2;
private static final int DEFAULT_BG = Color.TRANSPARENT;
private static final int TERMINAL_BG = 0xff0b0e13;
private int cols;
private int rows;
@@ -27,6 +28,7 @@ final class TerminalScreenBuffer {
private int fg = DEFAULT_FG;
private int bg = DEFAULT_BG;
private boolean bold;
private boolean dim;
TerminalScreenBuffer(int cols, int rows) {
resize(cols, rows);
@@ -74,6 +76,7 @@ final class TerminalScreenBuffer {
fg = DEFAULT_FG;
bg = DEFAULT_BG;
bold = false;
dim = false;
}
void write(String text) {
@@ -257,10 +260,14 @@ final class TerminalScreenBuffer {
fg = DEFAULT_FG;
bg = DEFAULT_BG;
bold = false;
dim = false;
} else if (value == 1) {
bold = true;
} else if (value == 2) {
dim = true;
} else if (value == 22) {
bold = false;
dim = false;
} else if (value == 39) {
fg = DEFAULT_FG;
} else if (value == 49) {
@@ -302,7 +309,7 @@ final class TerminalScreenBuffer {
wrapPending = false;
newLine();
}
cells[cursorRow][cursorCol].set(value, fg, bg, bold);
cells[cursorRow][cursorCol].set(value, fg, bg, bold, dim);
if (cursorCol == cols - 1) {
wrapPending = true;
} else {
@@ -453,15 +460,19 @@ final class TerminalScreenBuffer {
int fgColor = first.fg;
int bgColor = first.bg;
boolean isBold = first.bold;
boolean isDim = first.dim;
while (col < cols) {
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;
}
output.append(cell.value);
col++;
}
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);
if (bgColor != DEFAULT_BG) {
output.setSpan(new BackgroundColorSpan(bgColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -613,6 +624,15 @@ final class TerminalScreenBuffer {
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) {
return Math.max(min, Math.min(max, value));
}
@@ -622,19 +642,22 @@ final class TerminalScreenBuffer {
int fg = DEFAULT_FG;
int bg = DEFAULT_BG;
boolean bold;
boolean dim;
void clear() {
value = ' ';
fg = DEFAULT_FG;
bg = DEFAULT_BG;
bold = false;
dim = 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;
fg = nextFg;
bg = nextBg;
bold = nextBold;
dim = nextDim;
}
void copyFrom(Cell other) {
@@ -642,6 +665,7 @@ final class TerminalScreenBuffer {
fg = other.fg;
bg = other.bg;
bold = other.bold;
dim = other.dim;
}
}
}
@@ -6,6 +6,7 @@ import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.nio.charset.StandardCharsets;
@@ -19,6 +20,10 @@ import java.util.concurrent.RejectedExecutionException;
import javax.net.ssl.SSLSocketFactory;
final class TerminalSocketClient {
private static final long HEARTBEAT_INTERVAL_MS = 15000L;
private static final int SOCKET_CONNECT_TIMEOUT_MS = 10000;
private static final int SOCKET_READ_TIMEOUT_MS = 45000;
interface Listener {
void onConnected();
void onOutput(String data);
@@ -35,6 +40,7 @@ final class TerminalSocketClient {
private BufferedOutputStream output;
private volatile boolean closed;
private Thread thread;
private Thread heartbeatThread;
TerminalSocketClient(Listener listener) {
this.listener = listener;
@@ -97,6 +103,7 @@ final class TerminalSocketClient {
"rows", rows
);
listener.onConnected();
startHeartbeat();
readLoop();
} catch (Exception error) {
if (!closed) {
@@ -123,10 +130,39 @@ final class TerminalSocketClient {
if (port == -1) {
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
}
Socket raw = new Socket();
raw.connect(new InetSocketAddress(uri.getHost(), port), SOCKET_CONNECT_TIMEOUT_MS);
Socket connected;
if ("wss".equalsIgnoreCase(uri.getScheme())) {
return SSLSocketFactory.getDefault().createSocket(uri.getHost(), port);
connected = ((SSLSocketFactory) SSLSocketFactory.getDefault())
.createSocket(raw, uri.getHost(), port, true);
} else {
connected = raw;
}
return new Socket(uri.getHost(), port);
connected.setKeepAlive(true);
connected.setTcpNoDelay(true);
connected.setSoTimeout(SOCKET_READ_TIMEOUT_MS);
return connected;
}
private void startHeartbeat() {
heartbeatThread = new Thread(() -> {
while (!closed) {
try {
Thread.sleep(HEARTBEAT_INTERVAL_MS);
if (!closed) {
sendFrame(9, new byte[0]);
}
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return;
} catch (Exception error) {
closeSocketQuietly();
return;
}
}
}, "terminal-ws-heartbeat");
heartbeatThread.start();
}
private void handshake(URI uri) throws Exception {
@@ -279,6 +315,10 @@ final class TerminalSocketClient {
}
private void closeSocketQuietly() {
if (heartbeatThread != null) {
heartbeatThread.interrupt();
heartbeatThread = null;
}
try {
if (socket != null) {
socket.close();