Skip to content

Commit 1b4efb9

Browse files
committed
[Terminal] Improve handling terminal output
- Use `sh -i` instead of `sh` - Set `HOME=/` environment variable - Support basic foreground and background colors - Support bold, underline, italic, strikethrough, reverse video, and conceal. On some devices, opening a shell using `sh -i` may display a few warnings which can safely be ignored. The warnings are displayed because the terminal is not a real terminal (which is not possible in ADB mode). Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent a1713d0 commit 1b4efb9

1 file changed

Lines changed: 253 additions & 8 deletions

File tree

app/src/main/java/io/github/muntashirakon/AppManager/runner/TermActivity.java

Lines changed: 253 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,36 @@
22

33
package io.github.muntashirakon.AppManager.runner;
44

5+
import android.graphics.Color;
56
import android.graphics.Typeface;
67
import android.os.Bundle;
78
import android.os.PowerManager;
89
import android.text.Editable;
910
import android.text.Spanned;
11+
import android.text.style.BackgroundColorSpan;
12+
import android.text.style.ForegroundColorSpan;
13+
import android.text.style.StrikethroughSpan;
1014
import android.text.style.StyleSpan;
15+
import android.text.style.UnderlineSpan;
1116
import android.view.MenuItem;
1217
import android.view.inputmethod.EditorInfo;
1318
import android.widget.TextView;
1419

20+
import androidx.annotation.ColorInt;
1521
import androidx.annotation.MainThread;
1622
import androidx.annotation.NonNull;
1723
import androidx.annotation.Nullable;
24+
import androidx.annotation.UiThread;
1825
import androidx.appcompat.widget.AppCompatEditText;
1926
import androidx.appcompat.widget.AppCompatTextView;
2027

28+
import com.google.android.material.color.MaterialColors;
29+
2130
import java.io.BufferedOutputStream;
2231
import java.io.InputStream;
2332
import java.io.OutputStream;
2433
import java.nio.charset.StandardCharsets;
34+
import java.util.Arrays;
2535
import java.util.Objects;
2636
import java.util.concurrent.ExecutorService;
2737
import java.util.concurrent.Executors;
@@ -39,8 +49,9 @@ public class TermActivity extends BaseActivity {
3949
private Process mProc;
4050
private OutputStream mProcessOutputStream;
4151
private PowerManager.WakeLock mWakeLock;
42-
private boolean mCommandInProgress;
4352
private final ExecutorService mExecutor = Executors.newFixedThreadPool(3);
53+
private int mDefaultForegroundColor;
54+
private int mDefaultBackgroundColor;
4455

4556
@Override
4657
protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
@@ -49,11 +60,12 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
4960
mCommandInput = findViewById(R.id.command_input);
5061
mCommandOutput = findViewById(R.id.command_output);
5162
mCommandOutput.setText("", TextView.BufferType.EDITABLE);
63+
mDefaultForegroundColor = mCommandInput.getCurrentTextColor();
64+
mDefaultBackgroundColor = MaterialColors.getColor(mCommandInput, com.google.android.material.R.attr.colorSurface);
5265
mCommandInput.setOnEditorActionListener((v, actionId, event) -> {
5366
if (actionId == EditorInfo.IME_ACTION_DONE) {
5467
String command = Objects.requireNonNull(mCommandInput.getText()).toString();
55-
appendBoldOutput((mCommandInProgress ? " " : "$ ") + command);
56-
mCommandInProgress = command.endsWith("\\");
68+
appendBoldOutput(command);
5769
appendOutput("\n");
5870
if (mProcessOutputStream != null) {
5971
if (!ProcessCompat.isAlive(mProc)) {
@@ -96,16 +108,17 @@ private void initShell() {
96108
mWakeLock.acquire();
97109
mExecutor.submit(() -> {
98110
try {
99-
mProc = ProcessCompat.exec(new String[]{"sh"}/*, new String[]{"TERM=xterm-256color"}*/);
111+
mProc = ProcessCompat.exec(new String[]{"sh", "-i"}, new String[]{"TERM=xterm-256color", "HOME=/"});
100112
mProcessOutputStream = new BufferedOutputStream(mProc.getOutputStream());
101113
mExecutor.submit(() -> {
102114
try (InputStream in = mProc.getInputStream()) {
115+
AnsiState state = new AnsiState();
103116
byte[] buffer = new byte[1024];
104117
int len;
105118
while ((len = in.read(buffer)) != -1) {
106119
synchronized (mLock) {
107120
String chunk = new String(buffer, 0, len, StandardCharsets.UTF_8);
108-
runOnUiThread(() -> appendOutput(chunk));
121+
runOnUiThread(() -> processChunk(chunk, state));
109122
}
110123
}
111124
} catch (Throwable e) {
@@ -135,13 +148,245 @@ private void initShell() {
135148
});
136149
}
137150

151+
static class AnsiState {
152+
@ColorInt
153+
int foreground = -1;
154+
@ColorInt
155+
int background = -1;
156+
boolean bold = false;
157+
boolean underline = false;
158+
boolean italic = false;
159+
boolean strike = false;
160+
boolean reverse = false;
161+
boolean hide = false;
162+
163+
void reset() {
164+
foreground = -1;
165+
background = -1;
166+
bold = false;
167+
underline = false;
168+
italic = false;
169+
strike = false;
170+
reverse = false;
171+
hide = false;
172+
}
173+
}
174+
175+
@UiThread
176+
private void processChunk(@NonNull String chunk, @NonNull AnsiState state) {
177+
StringBuilder plainTextBuffer = new StringBuilder();
178+
for (int i = 0; i < chunk.length(); ++i) {
179+
char ch = chunk.charAt(i);
180+
if (ch == '\u001b') {
181+
// Start escape sequence
182+
// Flush plain text
183+
if (plainTextBuffer.length() > 0) {
184+
appendOutput(plainTextBuffer.toString(), state);
185+
plainTextBuffer.setLength(0);
186+
}
187+
++i;
188+
if (i >= chunk.length()) {
189+
// Invalid sequence
190+
break;
191+
}
192+
ch = chunk.charAt(i);
193+
if (ch == '[') {
194+
// Start of special sequences
195+
StringBuilder numberBuilder = new StringBuilder();
196+
numberBuilder.append(ch);
197+
for (++i; i < chunk.length(); ++i) {
198+
char c = chunk.charAt(i);
199+
if (c >= '0' && c <= '9') {
200+
// A number
201+
numberBuilder.append(c);
202+
} else if (c == ';') {
203+
// Separator
204+
numberBuilder.append(c);
205+
} else {
206+
// Any other ANSI character
207+
numberBuilder.append(c);
208+
processAnsiSeq(numberBuilder.toString(), state);
209+
// We're done
210+
break;
211+
}
212+
}
213+
} else if (ch == 'M') {
214+
// ENTER
215+
processAnsiSeq("M", state);
216+
} // else Invalid sequence
217+
} else {
218+
plainTextBuffer.append(ch);
219+
}
220+
}
221+
if (plainTextBuffer.length() > 0) {
222+
appendOutput(plainTextBuffer.toString(), state);
223+
}
224+
}
225+
138226
@MainThread
139-
private void appendOutput(String text) {
140-
mCommandOutput.append(text);
227+
void processAnsiSeq(@NonNull String seq, @NonNull AnsiState state) {
228+
if (seq.equals("[2J")) {
229+
resetOutput();
230+
state.reset();
231+
} else if (seq.matches("\\[[0-9;]*m")) {
232+
// Parse the numbers
233+
String params = seq.substring(1, seq.length() - 1);
234+
String[] codes = params.split(";");
235+
for (String code : codes) {
236+
int value;
237+
try {
238+
value = Integer.parseInt(code);
239+
} catch (Exception e) {
240+
if (code.isEmpty()) {
241+
value = 0; // Same as RESET
242+
} else continue;
243+
}
244+
switch (value) {
245+
case 0: // RESET
246+
state.reset();
247+
break;
248+
case 1: // BOLD
249+
state.bold = true;
250+
break;
251+
case 22: // RESET BOLD
252+
state.bold = false;
253+
break;
254+
case 3: // ITALIC
255+
state.italic = true;
256+
break;
257+
case 23: // RESET ITALIC
258+
state.italic = false;
259+
break;
260+
case 4: // UNDERLINE
261+
state.underline = true;
262+
break;
263+
case 24: // RESET UNDERLINE
264+
state.underline = false;
265+
break;
266+
case 7: // REVERSE VIDEO
267+
state.reverse = true;
268+
break;
269+
case 27: // RESET REVERSE VIDEO
270+
state.reverse = false;
271+
break;
272+
case 8: // HIDE
273+
state.hide = true;
274+
break;
275+
case 28: // RESET HIDE
276+
state.hide = false;
277+
break;
278+
case 9: // STRIKETHROUGH
279+
state.strike = true;
280+
break;
281+
case 29: // RESET STRIKETHROUGH
282+
state.strike = false;
283+
break;
284+
case 39: // RESET FOREGROUND
285+
state.foreground = -1;
286+
break;
287+
case 30:
288+
state.foreground = Color.BLACK;
289+
break;
290+
case 31:
291+
state.foreground = Color.RED;
292+
break;
293+
case 32:
294+
state.foreground = Color.GREEN;
295+
break;
296+
case 33:
297+
state.foreground = Color.YELLOW;
298+
break;
299+
case 34:
300+
state.foreground = Color.BLUE;
301+
break;
302+
case 35:
303+
state.foreground = Color.MAGENTA;
304+
break;
305+
case 36:
306+
state.foreground = Color.CYAN;
307+
break;
308+
case 37:
309+
state.foreground = Color.WHITE;
310+
break;
311+
case 49: // RESET BACKGROUND
312+
state.background = -1;
313+
break;
314+
case 40:
315+
state.background = Color.BLACK;
316+
break;
317+
case 41:
318+
state.background = Color.RED;
319+
break;
320+
case 42:
321+
state.background = Color.GREEN;
322+
break;
323+
case 43:
324+
state.background = Color.YELLOW;
325+
break;
326+
case 44:
327+
state.background = Color.BLUE;
328+
break;
329+
case 45:
330+
state.background = Color.MAGENTA;
331+
break;
332+
case 46:
333+
state.background = Color.CYAN;
334+
break;
335+
case 47:
336+
state.background = Color.WHITE;
337+
break;
338+
}
339+
}
340+
}
341+
}
342+
343+
@MainThread
344+
private void resetOutput() {
345+
mCommandOutput.setText("", TextView.BufferType.EDITABLE);
346+
}
347+
348+
@MainThread
349+
private void appendOutput(@NonNull String text) {
350+
mCommandOutput.getEditableText().append(text);
351+
}
352+
353+
@MainThread
354+
private void appendOutput(@NonNull String text, @NonNull AnsiState state) {
355+
if (state.hide) {
356+
// Replace with spaces
357+
char[] blank = new char[text.length()];
358+
Arrays.fill(blank, ' ');
359+
text = new String(blank);
360+
}
361+
Editable editable = mCommandOutput.getEditableText();
362+
int start = editable.length();
363+
editable.append(text);
364+
int end = editable.length();
365+
if (start == end) {
366+
return;
367+
}
368+
if (state.bold) {
369+
editable.setSpan(new StyleSpan(Typeface.BOLD), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
370+
}
371+
if (state.italic) {
372+
editable.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
373+
}
374+
if (state.underline) {
375+
editable.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
376+
}
377+
if (state.strike) {
378+
editable.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
379+
}
380+
int foregroundColor = state.foreground != -1 ? state.foreground : mDefaultForegroundColor;
381+
int backgroundColor = state.background != -1 ? state.background : mDefaultBackgroundColor;
382+
editable.setSpan(new ForegroundColorSpan(state.reverse ? backgroundColor : foregroundColor),
383+
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
384+
editable.setSpan(new BackgroundColorSpan(state.reverse ? foregroundColor : backgroundColor),
385+
start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
141386
}
142387

143388
@MainThread
144-
private void appendBoldOutput(String boldText) {
389+
private void appendBoldOutput(@NonNull String boldText) {
145390
Editable editable = mCommandOutput.getEditableText();
146391
int start = editable.length();
147392
editable.append(boldText);

0 commit comments

Comments
 (0)