Skip to content

Commit 7cf80f0

Browse files
committed
[Terminal] Improve cursor support
- Add support for saving and resetting to position (\e[s and \e[u) - Add support for moving cursor to home (\e[H) and clearing the display (\e[J) - Add support for clearing the last line (\e[2K). Due to limitations, arbitrary cursor jump is not supported. Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent 1b4efb9 commit 7cf80f0

1 file changed

Lines changed: 72 additions & 9 deletions

File tree

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

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@
3939
import io.github.muntashirakon.AppManager.BaseActivity;
4040
import io.github.muntashirakon.AppManager.R;
4141
import io.github.muntashirakon.AppManager.compat.ProcessCompat;
42+
import io.github.muntashirakon.AppManager.logs.Log;
4243
import io.github.muntashirakon.AppManager.utils.CpuUtils;
4344

4445
// TODO: 11/9/23 Replace it with an actual terminal
4546
public class TermActivity extends BaseActivity {
47+
public static final String TAG = TermActivity.class.getSimpleName();
4648
private final Object mLock = new Object();
4749
private AppCompatEditText mCommandInput;
4850
private AppCompatTextView mCommandOutput;
@@ -52,6 +54,7 @@ public class TermActivity extends BaseActivity {
5254
private final ExecutorService mExecutor = Executors.newFixedThreadPool(3);
5355
private int mDefaultForegroundColor;
5456
private int mDefaultBackgroundColor;
57+
private final AnsiState mAnsiState = new AnsiState();
5558

5659
@Override
5760
protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
@@ -67,6 +70,7 @@ protected void onAuthenticated(@Nullable Bundle savedInstanceState) {
6770
String command = Objects.requireNonNull(mCommandInput.getText()).toString();
6871
appendBoldOutput(command);
6972
appendOutput("\n");
73+
mAnsiState.homePosition = mCommandOutput.getEditableText().length();
7074
if (mProcessOutputStream != null) {
7175
if (!ProcessCompat.isAlive(mProc)) {
7276
// Process is dead
@@ -112,13 +116,12 @@ private void initShell() {
112116
mProcessOutputStream = new BufferedOutputStream(mProc.getOutputStream());
113117
mExecutor.submit(() -> {
114118
try (InputStream in = mProc.getInputStream()) {
115-
AnsiState state = new AnsiState();
116119
byte[] buffer = new byte[1024];
117120
int len;
118121
while ((len = in.read(buffer)) != -1) {
119122
synchronized (mLock) {
120123
String chunk = new String(buffer, 0, len, StandardCharsets.UTF_8);
121-
runOnUiThread(() -> processChunk(chunk, state));
124+
runOnUiThread(() -> processChunk(chunk, mAnsiState));
122125
}
123126
}
124127
} catch (Throwable e) {
@@ -132,7 +135,7 @@ private void initShell() {
132135
while ((len = in.read(buffer)) != -1) {
133136
synchronized (mLock) {
134137
String chunk = new String(buffer, 0, len, StandardCharsets.UTF_8);
135-
runOnUiThread(() -> appendOutput(chunk));
138+
runOnUiThread(() -> processChunk(chunk, mAnsiState));
136139
}
137140
}
138141
} catch (Throwable e) {
@@ -159,6 +162,9 @@ static class AnsiState {
159162
boolean strike = false;
160163
boolean reverse = false;
161164
boolean hide = false;
165+
int savedPosition = -1;
166+
boolean navigateToHome = false;
167+
int homePosition = 0;
162168

163169
void reset() {
164170
foreground = -1;
@@ -169,6 +175,8 @@ void reset() {
169175
strike = false;
170176
reverse = false;
171177
hide = false;
178+
savedPosition = -1;
179+
navigateToHome = false;
172180
}
173181
}
174182

@@ -199,7 +207,7 @@ private void processChunk(@NonNull String chunk, @NonNull AnsiState state) {
199207
if (c >= '0' && c <= '9') {
200208
// A number
201209
numberBuilder.append(c);
202-
} else if (c == ';') {
210+
} else if (c == ';' || c == '?') {
203211
// Separator
204212
numberBuilder.append(c);
205213
} else {
@@ -225,9 +233,35 @@ private void processChunk(@NonNull String chunk, @NonNull AnsiState state) {
225233

226234
@MainThread
227235
void processAnsiSeq(@NonNull String seq, @NonNull AnsiState state) {
228-
if (seq.equals("[2J")) {
229-
resetOutput();
230-
state.reset();
236+
if (seq.matches("\\[[0-2]?J")) {
237+
String code = seq.substring(1, seq.length() - 1);
238+
switch (code) {
239+
case "2": // Clear entire screen
240+
resetOutput(state);
241+
state.reset();
242+
break;
243+
case "1": // Clear screen from cursor up
244+
Log.i(TAG, "Unhandled escape sequence: " + seq);
245+
break;
246+
case "0": // Clear screen from cursor down
247+
default:
248+
if (state.navigateToHome) {
249+
resetOutputToPosition(state.homePosition);
250+
state.reset();
251+
} // else Not handled
252+
}
253+
} else if (seq.equals("[H") || seq.equals("[;H") || seq.equals("[f")) {
254+
state.navigateToHome = true;
255+
} else if (seq.equals("[2K")) {
256+
clearLastLine();
257+
} else if (seq.equals("[s")) {
258+
Editable editable = mCommandOutput.getEditableText();
259+
state.savedPosition = editable.length();
260+
} else if (seq.equals("[u")) {
261+
Editable editable = mCommandOutput.getEditableText();
262+
if (state.savedPosition >= 0 && state.savedPosition <= editable.length()) {
263+
editable.delete(state.savedPosition, editable.length());
264+
}
231265
} else if (seq.matches("\\[[0-9;]*m")) {
232266
// Parse the numbers
233267
String params = seq.substring(1, seq.length() - 1);
@@ -335,14 +369,43 @@ void processAnsiSeq(@NonNull String seq, @NonNull AnsiState state) {
335369
case 47:
336370
state.background = Color.WHITE;
337371
break;
372+
default:
373+
Log.i(TAG, "Unhandled escape sequence: " + seq);
338374
}
339375
}
376+
} else {
377+
Log.i(TAG, "Unhandled escape sequence: " + seq);
340378
}
341379
}
342380

343381
@MainThread
344-
private void resetOutput() {
345-
mCommandOutput.setText("", TextView.BufferType.EDITABLE);
382+
private void resetOutput(@NonNull AnsiState state) {
383+
mCommandOutput.getEditableText().clear();
384+
state.homePosition = 0;
385+
}
386+
387+
@MainThread
388+
private void resetOutputToPosition(int position) {
389+
Editable editable = mCommandOutput.getEditableText();
390+
if (position <= 0) {
391+
editable.clear();
392+
} else {
393+
int length = editable.length();
394+
if (position < length) {
395+
editable.delete(position, length);
396+
}
397+
}
398+
}
399+
400+
private void clearLastLine() {
401+
Editable editable = mCommandOutput.getEditableText();
402+
int length = editable.length();
403+
if (length > 0) {
404+
// Find the index of the last newline character
405+
int lastNewline = editable.toString().lastIndexOf('\n', length - 2);
406+
// If no newline found, delete everything
407+
resetOutputToPosition(lastNewline + 1);
408+
}
346409
}
347410

348411
@MainThread

0 commit comments

Comments
 (0)