22
33package io .github .muntashirakon .AppManager .runner ;
44
5+ import android .graphics .Color ;
56import android .graphics .Typeface ;
67import android .os .Bundle ;
78import android .os .PowerManager ;
89import android .text .Editable ;
910import android .text .Spanned ;
11+ import android .text .style .BackgroundColorSpan ;
12+ import android .text .style .ForegroundColorSpan ;
13+ import android .text .style .StrikethroughSpan ;
1014import android .text .style .StyleSpan ;
15+ import android .text .style .UnderlineSpan ;
1116import android .view .MenuItem ;
1217import android .view .inputmethod .EditorInfo ;
1318import android .widget .TextView ;
1419
20+ import androidx .annotation .ColorInt ;
1521import androidx .annotation .MainThread ;
1622import androidx .annotation .NonNull ;
1723import androidx .annotation .Nullable ;
24+ import androidx .annotation .UiThread ;
1825import androidx .appcompat .widget .AppCompatEditText ;
1926import androidx .appcompat .widget .AppCompatTextView ;
2027
28+ import com .google .android .material .color .MaterialColors ;
29+
2130import java .io .BufferedOutputStream ;
2231import java .io .InputStream ;
2332import java .io .OutputStream ;
2433import java .nio .charset .StandardCharsets ;
34+ import java .util .Arrays ;
2535import java .util .Objects ;
2636import java .util .concurrent .ExecutorService ;
2737import 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