Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a25a2d16b6 | ||
|
|
a88b6cf98d |
@@ -3,6 +3,7 @@ package com.neatstudio.tmuxandroid;
|
|||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -14,6 +15,7 @@ import android.graphics.BitmapFactory;
|
|||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.graphics.drawable.GradientDrawable;
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.StateListDrawable;
|
import android.graphics.drawable.StateListDrawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -23,12 +25,14 @@ import android.os.Looper;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.HapticFeedbackConstants;
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
@@ -127,8 +131,6 @@ public final class MainActivity extends Activity {
|
|||||||
private TextView terminalText;
|
private TextView terminalText;
|
||||||
private TextView terminalMetaText;
|
private TextView terminalMetaText;
|
||||||
private TextView terminalConnectionText;
|
private TextView terminalConnectionText;
|
||||||
private Button terminalReadingButton;
|
|
||||||
private Button terminalFullButton;
|
|
||||||
private ScrollView terminalScroll;
|
private ScrollView terminalScroll;
|
||||||
private LinearLayout terminalChatList;
|
private LinearLayout terminalChatList;
|
||||||
private TextView terminalLiveStatusText;
|
private TextView terminalLiveStatusText;
|
||||||
@@ -153,6 +155,7 @@ public final class MainActivity extends Activity {
|
|||||||
private int lastActiveSessionCount = -1;
|
private int lastActiveSessionCount = -1;
|
||||||
private TerminalScreenBuffer terminalScreen = new TerminalScreenBuffer(DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS);
|
private TerminalScreenBuffer terminalScreen = new TerminalScreenBuffer(DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS);
|
||||||
private final StringBuilder queuedTerminalInput = new StringBuilder();
|
private final StringBuilder queuedTerminalInput = new StringBuilder();
|
||||||
|
private final Set<String> terminalExpandedBlocks = new HashSet<>();
|
||||||
private final Set<String> terminalExpandedMessages = new HashSet<>();
|
private final Set<String> terminalExpandedMessages = new HashSet<>();
|
||||||
private final List<ConversationMessage> terminalConversationMessages = new ArrayList<>();
|
private final List<ConversationMessage> terminalConversationMessages = new ArrayList<>();
|
||||||
private boolean terminalConnected;
|
private boolean terminalConnected;
|
||||||
@@ -277,7 +280,7 @@ public final class MainActivity extends Activity {
|
|||||||
view -> promptCustomServer()
|
view -> promptCustomServer()
|
||||||
));
|
));
|
||||||
for (String url : serverUrls) {
|
for (String url : serverUrls) {
|
||||||
content.addView(serverProfileCard(url, !isPresetServer(url)), matchWrap());
|
content.addView(serverProfileCard(url), spacedHeight(72, 0, 12));
|
||||||
}
|
}
|
||||||
|
|
||||||
content.addView(createServerUtilityRow());
|
content.addView(createServerUtilityRow());
|
||||||
@@ -290,7 +293,7 @@ public final class MainActivity extends Activity {
|
|||||||
setStatus("Servers");
|
setStatus("Servers");
|
||||||
}
|
}
|
||||||
|
|
||||||
private View serverProfileCard(String url, boolean removable) {
|
private View serverProfileCard(String url) {
|
||||||
String label = url.replace("http://", "").replace(":3000", "");
|
String label = url.replace("http://", "").replace(":3000", "");
|
||||||
boolean selected = url.equals(getServerUrl());
|
boolean selected = url.equals(getServerUrl());
|
||||||
LinearLayout card = new LinearLayout(this);
|
LinearLayout card = new LinearLayout(this);
|
||||||
@@ -356,13 +359,6 @@ public final class MainActivity extends Activity {
|
|||||||
stateBlock.addView(active);
|
stateBlock.addView(active);
|
||||||
stateBlock.addView(total);
|
stateBlock.addView(total);
|
||||||
card.addView(stateBlock);
|
card.addView(stateBlock);
|
||||||
if (removable) {
|
|
||||||
Button remove = terminalToolButton("×", view -> confirmRemoveSavedServer(url));
|
|
||||||
remove.setContentDescription("Remove " + label);
|
|
||||||
remove.setTextColor(COLOR_DANGER);
|
|
||||||
card.addView(remove);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextView chevron = new TextView(this);
|
TextView chevron = new TextView(this);
|
||||||
chevron.setText("›");
|
chevron.setText("›");
|
||||||
chevron.setTextColor(COLOR_TEXT_DIM);
|
chevron.setTextColor(COLOR_TEXT_DIM);
|
||||||
@@ -370,12 +366,11 @@ public final class MainActivity extends Activity {
|
|||||||
chevron.setGravity(Gravity.CENTER);
|
chevron.setGravity(Gravity.CENTER);
|
||||||
card.addView(chevron, new LinearLayout.LayoutParams(dp(24), dp(36)));
|
card.addView(chevron, new LinearLayout.LayoutParams(dp(24), dp(36)));
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
Button remove = terminalToolButton("×", view -> confirmRemoveSavedServer(url));
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
remove.setContentDescription("Remove " + label);
|
||||||
dp(68)
|
remove.setTextColor(COLOR_DANGER);
|
||||||
);
|
card.addView(remove);
|
||||||
params.bottomMargin = dp(10);
|
|
||||||
card.setLayoutParams(params);
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +392,6 @@ public final class MainActivity extends Activity {
|
|||||||
sessionGroupList.setOrientation(LinearLayout.VERTICAL);
|
sessionGroupList.setOrientation(LinearLayout.VERTICAL);
|
||||||
sessionGroupList.addView(projectStateText("Loading sessions and groups..."), matchWrap());
|
sessionGroupList.addView(projectStateText("Loading sessions and groups..."), matchWrap());
|
||||||
content.addView(sessionGroupList, matchWrap());
|
content.addView(sessionGroupList, matchWrap());
|
||||||
content.addView(createNewGroupCard(), matchWrap());
|
|
||||||
scroll.addView(content);
|
scroll.addView(content);
|
||||||
root.addView(scroll, new LinearLayout.LayoutParams(
|
root.addView(scroll, new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
@@ -503,65 +497,25 @@ public final class MainActivity extends Activity {
|
|||||||
titleBlock.addView(title);
|
titleBlock.addView(title);
|
||||||
row.addView(titleBlock, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
row.addView(titleBlock, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
||||||
|
|
||||||
Button refresh = terminalToolButton("↻", view -> refreshSessions());
|
|
||||||
refresh.setContentDescription("Refresh sessions");
|
|
||||||
row.addView(refresh);
|
|
||||||
row.addView(primaryButton("+ Session", view -> promptCreateSession()), new LinearLayout.LayoutParams(
|
row.addView(primaryButton("+ Session", view -> promptCreateSession()), new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
dp(38)
|
dp(38)
|
||||||
));
|
));
|
||||||
header.addView(row, matchWrap());
|
header.addView(row, matchWrap());
|
||||||
|
|
||||||
|
LinearLayout utilities = new LinearLayout(this);
|
||||||
|
utilities.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
utilities.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
|
||||||
|
Button refresh = compactButton("↻ Refresh", view -> refreshSessions());
|
||||||
|
refresh.setContentDescription("Refresh sessions");
|
||||||
|
utilities.addView(refresh);
|
||||||
|
Button addGroup = compactButton("+ New group", view -> promptCreateKanbanProject());
|
||||||
|
addGroup.setContentDescription("Add group");
|
||||||
|
utilities.addView(addGroup);
|
||||||
|
header.addView(utilities, spacedMatchWrap(8, 0));
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
private View createNewGroupCard() {
|
|
||||||
LinearLayout card = new LinearLayout(this);
|
|
||||||
card.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
card.setPadding(dp(12), dp(11), dp(12), dp(12));
|
|
||||||
card.setBackground(cardBackground());
|
|
||||||
|
|
||||||
TextView label = new TextView(this);
|
|
||||||
label.setText("NEW GROUP");
|
|
||||||
label.setTextColor(COLOR_TEXT_DIM);
|
|
||||||
label.setTextSize(9);
|
|
||||||
label.setTypeface(Typeface.MONOSPACE);
|
|
||||||
card.addView(label);
|
|
||||||
|
|
||||||
LinearLayout row = new LinearLayout(this);
|
|
||||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
row.setPadding(0, dp(7), 0, 0);
|
|
||||||
EditText input = new EditText(this);
|
|
||||||
input.setHint("group name");
|
|
||||||
input.setSingleLine(true);
|
|
||||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
|
||||||
styleInput(input);
|
|
||||||
row.addView(input, new LinearLayout.LayoutParams(0, dp(40), 1));
|
|
||||||
Button create = compactButton("Create", view -> {
|
|
||||||
String name = input.getText().toString().trim();
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
showMessage("Group name is empty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runApiAction("Create kanban project", () -> api.createKanbanProject(name, "~", ""));
|
|
||||||
});
|
|
||||||
LinearLayout.LayoutParams createParams = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
||||||
dp(40)
|
|
||||||
);
|
|
||||||
createParams.leftMargin = dp(8);
|
|
||||||
row.addView(create, createParams);
|
|
||||||
card.addView(row, matchWrap());
|
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
);
|
|
||||||
params.topMargin = dp(8);
|
|
||||||
params.bottomMargin = dp(10);
|
|
||||||
card.setLayoutParams(params);
|
|
||||||
return card;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Button primaryButton(String label, View.OnClickListener listener) {
|
private Button primaryButton(String label, View.OnClickListener listener) {
|
||||||
Button button = toolbarButton(label, listener);
|
Button button = toolbarButton(label, listener);
|
||||||
button.setTextColor(Color.rgb(14, 38, 24));
|
button.setTextColor(Color.rgb(14, 38, 24));
|
||||||
@@ -572,6 +526,10 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String serverDisplayName(String url) {
|
private String serverDisplayName(String url) {
|
||||||
|
String savedName = prefs.getString("server_name:" + url, "").trim();
|
||||||
|
if (!savedName.isEmpty()) {
|
||||||
|
return savedName;
|
||||||
|
}
|
||||||
Uri parsed = Uri.parse(url);
|
Uri parsed = Uri.parse(url);
|
||||||
String host = parsed.getHost();
|
String host = parsed.getHost();
|
||||||
if (host == null || host.isEmpty()) {
|
if (host == null || host.isEmpty()) {
|
||||||
@@ -585,28 +543,71 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void promptCustomServer() {
|
private void promptCustomServer() {
|
||||||
EditText input = new EditText(this);
|
Dialog dialog = new Dialog(this);
|
||||||
input.setSingleLine(true);
|
LinearLayout sheet = bottomSheetContent(dialog, "Add server", "Save a Tailscale host and connect now");
|
||||||
input.setHint("100.64.0.x:3000");
|
EditText name = sheetField(sheet, "NAME", "e.g. build-box", "");
|
||||||
input.setText(getServerUrl());
|
|
||||||
input.setSelectAllOnFocus(true);
|
LinearLayout endpointLabels = new LinearLayout(this);
|
||||||
styleInput(input);
|
endpointLabels.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
new AlertDialog.Builder(this)
|
endpointLabels.setPadding(0, dp(14), 0, dp(6));
|
||||||
.setTitle("Add server")
|
TextView hostLabel = formLabel("HOST (TAILSCALE IP)");
|
||||||
.setView(input)
|
endpointLabels.addView(hostLabel, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
||||||
.setNegativeButton("Cancel", null)
|
TextView portLabel = formLabel("PORT");
|
||||||
.setPositiveButton("Connect", (dialog, which) -> {
|
LinearLayout.LayoutParams portLabelParams = new LinearLayout.LayoutParams(dp(82), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
String url = normalizeServerUrl(input.getText().toString());
|
portLabelParams.leftMargin = dp(8);
|
||||||
saveCustomServer(url);
|
endpointLabels.addView(portLabel, portLabelParams);
|
||||||
|
sheet.addView(endpointLabels, matchWrap());
|
||||||
|
LinearLayout endpoint = new LinearLayout(this);
|
||||||
|
endpoint.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
EditText host = new EditText(this);
|
||||||
|
host.setHint("100.64.0.x");
|
||||||
|
host.setSingleLine(true);
|
||||||
|
host.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||||
|
styleInput(host);
|
||||||
|
endpoint.addView(host, new LinearLayout.LayoutParams(0, dp(44), 1));
|
||||||
|
EditText port = new EditText(this);
|
||||||
|
port.setText("3000");
|
||||||
|
port.setSelectAllOnFocus(true);
|
||||||
|
port.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
|
styleInput(port);
|
||||||
|
LinearLayout.LayoutParams portParams = new LinearLayout.LayoutParams(dp(82), dp(44));
|
||||||
|
portParams.leftMargin = dp(8);
|
||||||
|
endpoint.addView(port, portParams);
|
||||||
|
sheet.addView(endpoint, matchWrap());
|
||||||
|
|
||||||
|
Button connect = primaryButton("Connect", null);
|
||||||
|
connect.setOnClickListener(view -> {
|
||||||
|
String rawHost = host.getText().toString().trim();
|
||||||
|
String rawPort = port.getText().toString().trim();
|
||||||
|
if (rawHost.isEmpty()) {
|
||||||
|
host.setError("Host is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!rawPort.matches("[0-9]{1,5}")) {
|
||||||
|
port.setError("Invalid port");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String url = normalizeServerUrl(rawHost + ":" + rawPort);
|
||||||
|
saveCustomServer(url, name.getText().toString().trim());
|
||||||
|
dialog.dismiss();
|
||||||
selectServer(url);
|
selectServer(url);
|
||||||
openSessionPage();
|
openSessionPage();
|
||||||
})
|
});
|
||||||
.show();
|
LinearLayout.LayoutParams connectParams = matchWrap();
|
||||||
|
connectParams.height = dp(46);
|
||||||
|
connectParams.topMargin = dp(18);
|
||||||
|
sheet.addView(connect, connectParams);
|
||||||
|
showBottomSheet(dialog, sheet, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> savedServerUrls() {
|
private List<String> savedServerUrls() {
|
||||||
List<String> urls = new ArrayList<>();
|
List<String> urls = new ArrayList<>();
|
||||||
Collections.addAll(urls, SERVER_PROFILES);
|
Set<String> hidden = prefs.getStringSet("hidden_server_urls", Collections.emptySet());
|
||||||
|
for (String preset : SERVER_PROFILES) {
|
||||||
|
if (!hidden.contains(preset)) {
|
||||||
|
urls.add(preset);
|
||||||
|
}
|
||||||
|
}
|
||||||
Set<String> custom = prefs.getStringSet("custom_server_urls", Collections.emptySet());
|
Set<String> custom = prefs.getStringSet("custom_server_urls", Collections.emptySet());
|
||||||
List<String> sortedCustom = new ArrayList<>(custom);
|
List<String> sortedCustom = new ArrayList<>(custom);
|
||||||
Collections.sort(sortedCustom);
|
Collections.sort(sortedCustom);
|
||||||
@@ -616,7 +617,7 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
String active = getServerUrl();
|
String active = getServerUrl();
|
||||||
if (!urls.contains(active)) {
|
if (!urls.contains(active) && !hidden.contains(active)) {
|
||||||
urls.add(0, active);
|
urls.add(0, active);
|
||||||
}
|
}
|
||||||
return urls;
|
return urls;
|
||||||
@@ -631,15 +632,24 @@ public final class MainActivity extends Activity {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveCustomServer(String url) {
|
private void saveCustomServer(String url, String name) {
|
||||||
if (isPresetServer(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Set<String> custom = new HashSet<>(
|
Set<String> custom = new HashSet<>(
|
||||||
prefs.getStringSet("custom_server_urls", Collections.emptySet())
|
prefs.getStringSet("custom_server_urls", Collections.emptySet())
|
||||||
);
|
);
|
||||||
|
Set<String> hidden = new HashSet<>(
|
||||||
|
prefs.getStringSet("hidden_server_urls", Collections.emptySet())
|
||||||
|
);
|
||||||
|
hidden.remove(url);
|
||||||
|
if (!isPresetServer(url)) {
|
||||||
custom.add(url);
|
custom.add(url);
|
||||||
prefs.edit().putStringSet("custom_server_urls", custom).apply();
|
}
|
||||||
|
SharedPreferences.Editor editor = prefs.edit()
|
||||||
|
.putStringSet("custom_server_urls", custom)
|
||||||
|
.putStringSet("hidden_server_urls", hidden);
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
editor.putString("server_name:" + url, name);
|
||||||
|
}
|
||||||
|
editor.apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void confirmRemoveSavedServer(String url) {
|
private void confirmRemoveSavedServer(String url) {
|
||||||
@@ -651,12 +661,23 @@ public final class MainActivity extends Activity {
|
|||||||
Set<String> custom = new HashSet<>(
|
Set<String> custom = new HashSet<>(
|
||||||
prefs.getStringSet("custom_server_urls", Collections.emptySet())
|
prefs.getStringSet("custom_server_urls", Collections.emptySet())
|
||||||
);
|
);
|
||||||
|
Set<String> hidden = new HashSet<>(
|
||||||
|
prefs.getStringSet("hidden_server_urls", Collections.emptySet())
|
||||||
|
);
|
||||||
custom.remove(url);
|
custom.remove(url);
|
||||||
SharedPreferences.Editor editor = prefs.edit()
|
SharedPreferences.Editor editor = prefs.edit()
|
||||||
.putStringSet("custom_server_urls", custom);
|
.putStringSet("custom_server_urls", custom)
|
||||||
|
.remove("server_name:" + url);
|
||||||
|
if (isPresetServer(url)) {
|
||||||
|
hidden.add(url);
|
||||||
|
editor.putStringSet("hidden_server_urls", hidden);
|
||||||
|
}
|
||||||
if (url.equals(getServerUrl())) {
|
if (url.equals(getServerUrl())) {
|
||||||
editor.putString("server_url", DEFAULT_TAILSCALE_URL);
|
List<String> remaining = savedServerUrls();
|
||||||
api = new SessionApiClient(DEFAULT_TAILSCALE_URL);
|
remaining.remove(url);
|
||||||
|
String fallback = remaining.isEmpty() ? BuildConfig.DEFAULT_SERVER_URL : remaining.get(0);
|
||||||
|
editor.putString("server_url", fallback);
|
||||||
|
api = new SessionApiClient(fallback);
|
||||||
connectAppEvents();
|
connectAppEvents();
|
||||||
}
|
}
|
||||||
editor.apply();
|
editor.apply();
|
||||||
@@ -1243,7 +1264,7 @@ public final class MainActivity extends Activity {
|
|||||||
project.optString("name", "Project"),
|
project.optString("name", "Project"),
|
||||||
groupSessions,
|
groupSessions,
|
||||||
true
|
true
|
||||||
), matchWrap());
|
), spacedMatchWrap(4, 16));
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
@@ -1254,7 +1275,10 @@ public final class MainActivity extends Activity {
|
|||||||
ungrouped.add(session);
|
ungrouped.add(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sessionGroupList.addView(sessionGroupSection("Ungrouped", ungrouped, false), matchWrap());
|
sessionGroupList.addView(
|
||||||
|
sessionGroupSection("Ungrouped", ungrouped, false),
|
||||||
|
spacedMatchWrap(4, 16)
|
||||||
|
);
|
||||||
setStatus("Loaded " + sessions.size() + " sessions");
|
setStatus("Loaded " + sessions.size() + " sessions");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,16 +1327,10 @@ public final class MainActivity extends Activity {
|
|||||||
section.addView(empty, matchWrap());
|
section.addView(empty, matchWrap());
|
||||||
} else {
|
} else {
|
||||||
for (SessionSummary session : sessions) {
|
for (SessionSummary session : sessions) {
|
||||||
section.addView(sessionRow(session), matchWrap());
|
section.addView(sessionRow(session), spacedMatchWrap(0, 10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
);
|
|
||||||
params.bottomMargin = dp(7);
|
|
||||||
section.setLayoutParams(params);
|
|
||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1357,7 +1375,7 @@ public final class MainActivity extends Activity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int index = 0; index < projects.length(); index++) {
|
for (int index = 0; index < projects.length(); index++) {
|
||||||
projectList.addView(projectCard(projects.getJSONObject(index)), matchWrap());
|
projectList.addView(projectCard(projects.getJSONObject(index)));
|
||||||
}
|
}
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
projectList.addView(projectStateText(text == null || text.isEmpty() ? "(empty)" : text), matchWrap());
|
projectList.addView(projectStateText(text == null || text.isEmpty() ? "(empty)" : text), matchWrap());
|
||||||
@@ -1463,7 +1481,7 @@ public final class MainActivity extends Activity {
|
|||||||
list.addView(empty);
|
list.addView(empty);
|
||||||
}
|
}
|
||||||
for (SessionSummary session : sessions) {
|
for (SessionSummary session : sessions) {
|
||||||
list.addView(sessionRow(session), matchWrap());
|
list.addView(sessionRow(session), spacedMatchWrap(0, 10));
|
||||||
}
|
}
|
||||||
if (sessionSummaryText != null) {
|
if (sessionSummaryText != null) {
|
||||||
sessionSummaryText.setText("Server: " + getServerUrl() + "\nSessions: " + sessions.size());
|
sessionSummaryText.setText("Server: " + getServerUrl() + "\nSessions: " + sessions.size());
|
||||||
@@ -1542,43 +1560,110 @@ public final class MainActivity extends Activity {
|
|||||||
kill.setTextColor(COLOR_DANGER);
|
kill.setTextColor(COLOR_DANGER);
|
||||||
row.addView(kill);
|
row.addView(kill);
|
||||||
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
);
|
|
||||||
params.bottomMargin = dp(7);
|
|
||||||
row.setLayoutParams(params);
|
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void promptCreateSession() {
|
private void promptCreateSession() {
|
||||||
EditText input = new EditText(this);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
input.setSingleLine(true);
|
executor.execute(() -> {
|
||||||
input.setHint("session-name");
|
List<String> groups = new ArrayList<>();
|
||||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
try {
|
||||||
new AlertDialog.Builder(this)
|
JSONObject object = new JSONObject(api.kanbanProjects());
|
||||||
.setTitle("Create tmux session")
|
JSONArray projects = object.optJSONArray("projects");
|
||||||
.setView(input)
|
for (int index = 0; projects != null && index < projects.length(); index++) {
|
||||||
.setNegativeButton("Cancel", null)
|
JSONObject project = projects.optJSONObject(index);
|
||||||
.setPositiveButton("Create", (dialog, which) -> {
|
if (project != null && !project.optString("name").isEmpty()) {
|
||||||
|
groups.add(project.optString("name"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
showCreateSessionDialog(groups);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCreateSessionDialog(List<String> groups) {
|
||||||
|
Dialog dialog = new Dialog(this);
|
||||||
|
LinearLayout sheet = bottomSheetContent(
|
||||||
|
dialog,
|
||||||
|
"New session",
|
||||||
|
"Create a tmux session and optionally place it in a group"
|
||||||
|
);
|
||||||
|
EditText input = sheetField(sheet, "NAME", "session-name", "");
|
||||||
|
|
||||||
|
TextView groupLabel = formLabel("GROUP");
|
||||||
|
groupLabel.setPadding(0, dp(14), 0, dp(6));
|
||||||
|
sheet.addView(groupLabel, matchWrap());
|
||||||
|
HorizontalScrollView groupScroller = new HorizontalScrollView(this);
|
||||||
|
groupScroller.setHorizontalScrollBarEnabled(false);
|
||||||
|
LinearLayout groupRow = new LinearLayout(this);
|
||||||
|
groupRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
List<Button> groupButtons = new ArrayList<>();
|
||||||
|
List<String> choices = new ArrayList<>();
|
||||||
|
choices.add("");
|
||||||
|
choices.addAll(groups);
|
||||||
|
String[] selectedGroup = {""};
|
||||||
|
for (String group : choices) {
|
||||||
|
String label = group.isEmpty() ? "Ungrouped" : group;
|
||||||
|
Button choice = compactButton(label, null);
|
||||||
|
choice.setOnClickListener(view -> {
|
||||||
|
selectedGroup[0] = group;
|
||||||
|
for (int index = 0; index < groupButtons.size(); index++) {
|
||||||
|
boolean selected = choices.get(index).equals(group);
|
||||||
|
groupButtons.get(index).setTextColor(selected ? COLOR_ACCENT : COLOR_TEXT_MUTED);
|
||||||
|
groupButtons.get(index).setBackground(selected
|
||||||
|
? rounded(COLOR_ACCENT_DARK, 8, COLOR_ACCENT, 1)
|
||||||
|
: buttonBackground());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
groupButtons.add(choice);
|
||||||
|
groupRow.addView(choice);
|
||||||
|
}
|
||||||
|
groupButtons.get(0).performClick();
|
||||||
|
groupScroller.addView(groupRow, new HorizontalScrollView.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
dp(40)
|
||||||
|
));
|
||||||
|
sheet.addView(groupScroller, matchWrap());
|
||||||
|
|
||||||
|
Button create = primaryButton("Create session", null);
|
||||||
|
create.setOnClickListener(view -> {
|
||||||
String name = input.getText().toString().trim();
|
String name = input.getText().toString().trim();
|
||||||
if (!name.matches("[A-Za-z0-9._-]+")) {
|
if (!name.matches("[A-Za-z0-9._-]+")) {
|
||||||
showMessage("Invalid session name");
|
input.setError("Use letters, numbers, dot, dash, or underscore");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
create.setEnabled(false);
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
try {
|
try {
|
||||||
api.createSession(name);
|
api.createSession(name);
|
||||||
|
if (!selectedGroup[0].isEmpty()) {
|
||||||
|
api.addKanbanSession(selectedGroup[0], name);
|
||||||
|
}
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
|
dialog.dismiss();
|
||||||
showMessage("Created " + name);
|
showMessage("Created " + name);
|
||||||
refreshSessions();
|
refreshSessions();
|
||||||
});
|
});
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
runOnUiThread(() -> showMessage("Create failed: " + error.getMessage()));
|
runOnUiThread(() -> {
|
||||||
|
create.setEnabled(true);
|
||||||
|
showMessage("Create failed: " + error.getMessage());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
.show();
|
LinearLayout.LayoutParams createParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dp(46)
|
||||||
|
);
|
||||||
|
createParams.topMargin = dp(16);
|
||||||
|
sheet.addView(create, createParams);
|
||||||
|
|
||||||
|
showBottomSheet(dialog, sheet, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void confirmKill(String sessionName) {
|
private void confirmKill(String sessionName) {
|
||||||
@@ -1603,6 +1688,7 @@ public final class MainActivity extends Activity {
|
|||||||
private void openTerminal(String sessionName) {
|
private void openTerminal(String sessionName) {
|
||||||
closeTerminalSocket();
|
closeTerminalSocket();
|
||||||
activeSessionName = sessionName;
|
activeSessionName = sessionName;
|
||||||
|
terminalViewMode = TERMINAL_VIEW_CHAT;
|
||||||
projectList = null;
|
projectList = null;
|
||||||
sessionGroupList = null;
|
sessionGroupList = null;
|
||||||
terminalScreen = new TerminalScreenBuffer(DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS);
|
terminalScreen = new TerminalScreenBuffer(DEFAULT_TERMINAL_COLS, DEFAULT_TERMINAL_ROWS);
|
||||||
@@ -1612,6 +1698,7 @@ public final class MainActivity extends Activity {
|
|||||||
terminalSelectionEnabled = false;
|
terminalSelectionEnabled = false;
|
||||||
terminalFollowOutput = true;
|
terminalFollowOutput = true;
|
||||||
terminalKeyPage = 0;
|
terminalKeyPage = 0;
|
||||||
|
terminalExpandedBlocks.clear();
|
||||||
terminalExpandedMessages.clear();
|
terminalExpandedMessages.clear();
|
||||||
terminalConversationMessages.clear();
|
terminalConversationMessages.clear();
|
||||||
terminalLiveStatusText = null;
|
terminalLiveStatusText = null;
|
||||||
@@ -1662,7 +1749,6 @@ public final class MainActivity extends Activity {
|
|||||||
1
|
1
|
||||||
));
|
));
|
||||||
terminalAccessoryBar = createAccessoryBar();
|
terminalAccessoryBar = createAccessoryBar();
|
||||||
terminalAccessoryBar.setVisibility(terminalViewMode == TERMINAL_VIEW_CHAT ? View.GONE : View.VISIBLE);
|
|
||||||
root.addView(terminalAccessoryBar, new LinearLayout.LayoutParams(
|
root.addView(terminalAccessoryBar, new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
dp(TERMINAL_KEYS_HEIGHT_DP)
|
dp(TERMINAL_KEYS_HEIGHT_DP)
|
||||||
@@ -1765,13 +1851,6 @@ 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 -> showTerminalView(TERMINAL_VIEW_CHAT, true));
|
|
||||||
terminalReadingButton.setContentDescription("Chat view");
|
|
||||||
terminalFullButton = terminalToolButton("终", view -> showTerminalView(TERMINAL_VIEW_FULL, true));
|
|
||||||
terminalFullButton.setContentDescription("Full terminal view");
|
|
||||||
styleTerminalReadingButton();
|
|
||||||
bar.addView(terminalReadingButton);
|
|
||||||
bar.addView(terminalFullButton);
|
|
||||||
bar.addView(terminalToolButton("☰", view -> showTerminalActions(sessionName)));
|
bar.addView(terminalToolButton("☰", view -> showTerminalActions(sessionName)));
|
||||||
return bar;
|
return bar;
|
||||||
}
|
}
|
||||||
@@ -1779,55 +1858,37 @@ public final class MainActivity extends Activity {
|
|||||||
private void showTerminalView(int mode, boolean announce) {
|
private void showTerminalView(int mode, boolean announce) {
|
||||||
terminalViewMode = mode;
|
terminalViewMode = mode;
|
||||||
if (terminalScroll == null || terminalText == null || terminalChatList == null) {
|
if (terminalScroll == null || terminalText == null || terminalChatList == null) {
|
||||||
styleTerminalReadingButton();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalScroll.removeAllViews();
|
terminalScroll.removeAllViews();
|
||||||
if (mode == TERMINAL_VIEW_CHAT) {
|
if (mode == TERMINAL_VIEW_CHAT) {
|
||||||
terminalScroll.addView(terminalChatList, new ScrollView.LayoutParams(
|
terminalText.setHorizontallyScrolling(false);
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
terminalText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
terminalText.setHighlightColor(Color.TRANSPARENT);
|
||||||
));
|
|
||||||
renderTerminalConversation();
|
|
||||||
} else {
|
} else {
|
||||||
terminalText.setHorizontallyScrolling(true);
|
terminalText.setHorizontallyScrolling(true);
|
||||||
terminalText.setGravity(Gravity.BOTTOM | Gravity.START);
|
|
||||||
terminalText.setMovementMethod(null);
|
terminalText.setMovementMethod(null);
|
||||||
|
}
|
||||||
|
terminalText.setGravity(Gravity.BOTTOM | Gravity.START);
|
||||||
terminalScroll.addView(terminalText, new ScrollView.LayoutParams(
|
terminalScroll.addView(terminalText, new ScrollView.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
));
|
));
|
||||||
renderTerminalNow();
|
renderTerminalNow();
|
||||||
}
|
|
||||||
styleTerminalReadingButton();
|
|
||||||
boolean chat = mode == TERMINAL_VIEW_CHAT;
|
|
||||||
if (terminalAccessoryBar != null) {
|
if (terminalAccessoryBar != null) {
|
||||||
terminalAccessoryBar.setVisibility(chat ? View.GONE : View.VISIBLE);
|
terminalAccessoryBar.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
if (terminalComposerTabs != null) {
|
if (terminalComposerTabs != null) {
|
||||||
terminalComposerTabs.setVisibility(chat ? View.GONE : View.VISIBLE);
|
terminalComposerTabs.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
if (inputField != null) {
|
if (inputField != null) {
|
||||||
inputField.setHint(chat ? "Message" : "type command or text");
|
inputField.setHint(mode == TERMINAL_VIEW_CHAT ? "message or command" : "type command or text");
|
||||||
}
|
}
|
||||||
if (announce) {
|
if (announce) {
|
||||||
setStatus(mode == TERMINAL_VIEW_CHAT ? "Chat view" : "Full terminal mode");
|
setStatus(mode == TERMINAL_VIEW_CHAT ? "Content view" : "Raw terminal mode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void styleTerminalReadingButton() {
|
|
||||||
if (terminalReadingButton == null || terminalFullButton == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
styleTerminalViewButton(terminalReadingButton, terminalViewMode == TERMINAL_VIEW_CHAT);
|
|
||||||
styleTerminalViewButton(terminalFullButton, terminalViewMode == TERMINAL_VIEW_FULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void styleTerminalViewButton(Button button, boolean selected) {
|
|
||||||
button.setTextColor(selected ? Color.rgb(14, 38, 24) : COLOR_TEXT_MUTED);
|
|
||||||
button.setBackground(selected ? 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);
|
||||||
@@ -1964,8 +2025,19 @@ public final class MainActivity extends Activity {
|
|||||||
empty.setPadding(dp(12), dp(24), dp(12), dp(24));
|
empty.setPadding(dp(12), dp(24), dp(12), dp(24));
|
||||||
terminalChatList.addView(empty, matchWrap());
|
terminalChatList.addView(empty, matchWrap());
|
||||||
} else {
|
} else {
|
||||||
for (ConversationMessage message : terminalConversationMessages) {
|
for (int index = 0; index < terminalConversationMessages.size();) {
|
||||||
|
ConversationMessage message = terminalConversationMessages.get(index);
|
||||||
|
if (!message.isTool()) {
|
||||||
terminalChatList.addView(conversationMessageRow(message));
|
terminalChatList.addView(conversationMessageRow(message));
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<ConversationMessage> tools = new ArrayList<>();
|
||||||
|
while (index < terminalConversationMessages.size()
|
||||||
|
&& terminalConversationMessages.get(index).isTool()) {
|
||||||
|
tools.add(terminalConversationMessages.get(index++));
|
||||||
|
}
|
||||||
|
terminalChatList.addView(conversationToolGroup(tools));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminalChatList.addView(conversationLiveTerminalPanel(), matchWrap());
|
terminalChatList.addView(conversationLiveTerminalPanel(), matchWrap());
|
||||||
@@ -2020,27 +2092,24 @@ public final class MainActivity extends Activity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!tool && !user) {
|
||||||
|
row.addView(bubble, matchWrap());
|
||||||
|
} else if (!tool) {
|
||||||
LinearLayout.LayoutParams bubbleParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams bubbleParams = new LinearLayout.LayoutParams(
|
||||||
0,
|
0,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
tool ? 1f : user ? 0.82f : 0.94f
|
0.82f
|
||||||
);
|
);
|
||||||
if (!tool) {
|
|
||||||
View spacer = new View(this);
|
View spacer = new View(this);
|
||||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
user ? 0.18f : 0.06f
|
0.18f
|
||||||
);
|
);
|
||||||
if (user) {
|
|
||||||
row.addView(spacer, spacerParams);
|
row.addView(spacer, spacerParams);
|
||||||
row.addView(bubble, bubbleParams);
|
row.addView(bubble, bubbleParams);
|
||||||
} else {
|
} else {
|
||||||
row.addView(bubble, bubbleParams);
|
row.addView(bubble, matchWrap());
|
||||||
row.addView(spacer, spacerParams);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
row.addView(bubble, bubbleParams);
|
|
||||||
}
|
}
|
||||||
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
@@ -2051,6 +2120,64 @@ public final class MainActivity extends Activity {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private View conversationToolGroup(List<ConversationMessage> tools) {
|
||||||
|
LinearLayout group = new LinearLayout(this);
|
||||||
|
group.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
HorizontalScrollView scroller = new HorizontalScrollView(this);
|
||||||
|
scroller.setHorizontalScrollBarEnabled(false);
|
||||||
|
LinearLayout chips = new LinearLayout(this);
|
||||||
|
chips.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
chips.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
for (ConversationMessage message : tools) {
|
||||||
|
String label = compactLabel(conversationToolTitle(message), 18);
|
||||||
|
Button chip = compactButton(label, view -> {
|
||||||
|
if (!terminalExpandedMessages.add(message.messageId)) {
|
||||||
|
terminalExpandedMessages.remove(message.messageId);
|
||||||
|
}
|
||||||
|
renderTerminalConversation();
|
||||||
|
});
|
||||||
|
chip.setTextSize(9);
|
||||||
|
chip.setContentDescription("Toggle " + label);
|
||||||
|
chips.addView(chip);
|
||||||
|
}
|
||||||
|
scroller.addView(chips, new HorizontalScrollView.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
dp(34)
|
||||||
|
));
|
||||||
|
group.addView(scroller, matchWrap());
|
||||||
|
|
||||||
|
for (ConversationMessage message : tools) {
|
||||||
|
if (!terminalExpandedMessages.contains(message.messageId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LinearLayout detail = new LinearLayout(this);
|
||||||
|
detail.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
detail.setPadding(dp(10), dp(7), dp(10), dp(9));
|
||||||
|
detail.setBackground(rounded(COLOR_FIELD, 7, COLOR_BORDER_SOFT, 1));
|
||||||
|
TextView title = bodyText(conversationToolTitle(message));
|
||||||
|
title.setTextColor(COLOR_ACCENT_WARM);
|
||||||
|
title.setTextSize(9);
|
||||||
|
title.setTypeface(Typeface.MONOSPACE, Typeface.BOLD);
|
||||||
|
detail.addView(title, matchWrap());
|
||||||
|
detail.addView(conversationContent(message, true), matchWrap());
|
||||||
|
LinearLayout.LayoutParams detailParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
detailParams.topMargin = dp(5);
|
||||||
|
group.addView(detail, detailParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
params.bottomMargin = dp(8);
|
||||||
|
group.setLayoutParams(params);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
private TextView conversationContent(ConversationMessage message, boolean tool) {
|
private TextView conversationContent(ConversationMessage message, boolean tool) {
|
||||||
TextView content = new TextView(this);
|
TextView content = new TextView(this);
|
||||||
content.setText(message.content.isEmpty() ? "(empty)" : message.content);
|
content.setText(message.content.isEmpty() ? "(empty)" : message.content);
|
||||||
@@ -2360,6 +2487,7 @@ public final class MainActivity extends Activity {
|
|||||||
|
|
||||||
private void showTerminalActions(String sessionName) {
|
private void showTerminalActions(String sessionName) {
|
||||||
String[] items = {
|
String[] items = {
|
||||||
|
terminalViewMode == TERMINAL_VIEW_CHAT ? "Open raw terminal" : "Open content view",
|
||||||
"Reconnect",
|
"Reconnect",
|
||||||
"Clear local view and tmux history",
|
"Clear local view and tmux history",
|
||||||
"Split horizontal",
|
"Split horizontal",
|
||||||
@@ -2380,53 +2508,57 @@ public final class MainActivity extends Activity {
|
|||||||
.setItems(items, (dialog, which) -> {
|
.setItems(items, (dialog, which) -> {
|
||||||
switch (which) {
|
switch (which) {
|
||||||
case 0:
|
case 0:
|
||||||
connectTerminal(sessionName);
|
showTerminalView(terminalViewMode == TERMINAL_VIEW_CHAT
|
||||||
|
? TERMINAL_VIEW_FULL : TERMINAL_VIEW_CHAT, true);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
|
connectTerminal(sessionName);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
terminalScreen.clear();
|
terminalScreen.clear();
|
||||||
terminalText.setText("");
|
terminalText.setText("");
|
||||||
if (terminalSocket != null) {
|
if (terminalSocket != null) {
|
||||||
terminalSocket.clearHistory();
|
terminalSocket.clearHistory();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 3:
|
||||||
runApiAction("Split horizontal", () -> api.splitPane(sessionName, "horizontal"));
|
runApiAction("Split horizontal", () -> api.splitPane(sessionName, "horizontal"));
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 4:
|
||||||
runApiAction("Split vertical", () -> api.splitPane(sessionName, "vertical"));
|
runApiAction("Split vertical", () -> api.splitPane(sessionName, "vertical"));
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 5:
|
||||||
sendTerminalInput("\u0002z");
|
sendTerminalInput("\u0002z");
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 6:
|
||||||
if (terminalSocket != null) {
|
if (terminalSocket != null) {
|
||||||
terminalSocket.scroll(-terminalRows);
|
terminalSocket.scroll(-terminalRows);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 7:
|
||||||
if (terminalSocket != null) {
|
if (terminalSocket != null) {
|
||||||
terminalSocket.scroll(terminalRows);
|
terminalSocket.scroll(terminalRows);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 7:
|
case 8:
|
||||||
showRaw("Session status", () -> api.sessionStatus(sessionName));
|
showRaw("Session status", () -> api.sessionStatus(sessionName));
|
||||||
break;
|
break;
|
||||||
case 8:
|
case 9:
|
||||||
promptSendCommand(sessionName);
|
promptSendCommand(sessionName);
|
||||||
break;
|
break;
|
||||||
case 9:
|
case 10:
|
||||||
sendTerminalInput("\u0002");
|
sendTerminalInput("\u0002");
|
||||||
break;
|
break;
|
||||||
case 10:
|
case 11:
|
||||||
sendTerminalInput("\u0002d");
|
sendTerminalInput("\u0002d");
|
||||||
break;
|
break;
|
||||||
case 11:
|
case 12:
|
||||||
sendTerminalInput("\u0002c");
|
sendTerminalInput("\u0002c");
|
||||||
break;
|
break;
|
||||||
case 12:
|
case 13:
|
||||||
sendTerminalInput("\u0002n");
|
sendTerminalInput("\u0002n");
|
||||||
break;
|
break;
|
||||||
case 13:
|
case 14:
|
||||||
sendTerminalInput("\u0002p");
|
sendTerminalInput("\u0002p");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -2455,7 +2587,6 @@ public final class MainActivity extends Activity {
|
|||||||
LinearLayout tabGrid = new LinearLayout(this);
|
LinearLayout tabGrid = new LinearLayout(this);
|
||||||
tabGrid.setOrientation(LinearLayout.VERTICAL);
|
tabGrid.setOrientation(LinearLayout.VERTICAL);
|
||||||
terminalComposerTabs = tabGrid;
|
terminalComposerTabs = tabGrid;
|
||||||
tabGrid.setVisibility(terminalViewMode == TERMINAL_VIEW_CHAT ? View.GONE : View.VISIBLE);
|
|
||||||
LinearLayout firstTabRow = terminalKeyRow();
|
LinearLayout firstTabRow = terminalKeyRow();
|
||||||
LinearLayout secondTabRow = terminalKeyRow();
|
LinearLayout secondTabRow = terminalKeyRow();
|
||||||
addAccessoryTab(firstTabRow, "Edit", 0);
|
addAccessoryTab(firstTabRow, "Edit", 0);
|
||||||
@@ -2644,22 +2775,34 @@ public final class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void promptCreateKanbanProject() {
|
private void promptCreateKanbanProject() {
|
||||||
LinearLayout form = formRoot();
|
Dialog dialog = new Dialog(this);
|
||||||
EditText name = formField(form, "Project name", "project");
|
LinearLayout sheet = bottomSheetContent(
|
||||||
EditText path = formField(form, "Path", "~");
|
dialog,
|
||||||
EditText server = formField(form, "SSH server optional", "");
|
"New group",
|
||||||
new AlertDialog.Builder(this)
|
"Organize related tmux sessions under one project"
|
||||||
.setTitle("Create kanban project")
|
);
|
||||||
.setView(form)
|
EditText name = sheetField(sheet, "NAME", "group name", "");
|
||||||
.setNegativeButton("Cancel", null)
|
EditText path = sheetField(sheet, "WORKING PATH", "~", "~");
|
||||||
.setPositiveButton("Create", (dialog, which) -> runApiAction("Create kanban project", () ->
|
EditText server = sheetField(sheet, "SSH SERVER (OPTIONAL)", "user@host", "");
|
||||||
api.createKanbanProject(
|
Button create = primaryButton("Create group", null);
|
||||||
name.getText().toString().trim(),
|
create.setOnClickListener(view -> {
|
||||||
|
String groupName = name.getText().toString().trim();
|
||||||
|
if (groupName.isEmpty()) {
|
||||||
|
name.setError("Group name is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
runApiAction("Create group", () -> api.createKanbanProject(
|
||||||
|
groupName,
|
||||||
defaultValue(path.getText().toString().trim(), "~"),
|
defaultValue(path.getText().toString().trim(), "~"),
|
||||||
server.getText().toString().trim()
|
server.getText().toString().trim()
|
||||||
)
|
));
|
||||||
))
|
});
|
||||||
.show();
|
LinearLayout.LayoutParams createParams = matchWrap();
|
||||||
|
createParams.height = dp(46);
|
||||||
|
createParams.topMargin = dp(18);
|
||||||
|
sheet.addView(create, createParams);
|
||||||
|
showBottomSheet(dialog, sheet, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void promptDeleteKanbanProject() {
|
private void promptDeleteKanbanProject() {
|
||||||
@@ -2969,7 +3112,7 @@ public final class MainActivity extends Activity {
|
|||||||
setStatus("Probing Tailscale APIs...");
|
setStatus("Probing Tailscale APIs...");
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
StringBuilder text = new StringBuilder();
|
StringBuilder text = new StringBuilder();
|
||||||
for (String url : SERVER_PROFILES) {
|
for (String url : savedServerUrls()) {
|
||||||
text.append(url).append('\n');
|
text.append(url).append('\n');
|
||||||
try {
|
try {
|
||||||
SessionApiClient client = new SessionApiClient(url);
|
SessionApiClient client = new SessionApiClient(url);
|
||||||
@@ -3183,6 +3326,81 @@ public final class MainActivity extends Activity {
|
|||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TextView formLabel(String text) {
|
||||||
|
TextView label = new TextView(this);
|
||||||
|
label.setText(text);
|
||||||
|
label.setTextColor(COLOR_TEXT_DIM);
|
||||||
|
label.setTextSize(9);
|
||||||
|
label.setTypeface(Typeface.MONOSPACE);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout bottomSheetContent(Dialog dialog, String titleText, String subtitleText) {
|
||||||
|
LinearLayout sheet = new LinearLayout(this);
|
||||||
|
sheet.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
sheet.setPadding(dp(18), dp(14), dp(18), dp(24));
|
||||||
|
sheet.setBackground(rounded(COLOR_CARD, 8, COLOR_BORDER, 1));
|
||||||
|
|
||||||
|
LinearLayout header = new LinearLayout(this);
|
||||||
|
header.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
header.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout titleBlock = new LinearLayout(this);
|
||||||
|
titleBlock.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
TextView title = new TextView(this);
|
||||||
|
title.setText(titleText);
|
||||||
|
title.setTextColor(COLOR_TEXT);
|
||||||
|
title.setTextSize(16);
|
||||||
|
title.setTypeface(Typeface.DEFAULT_BOLD);
|
||||||
|
TextView subtitle = bodyText(subtitleText);
|
||||||
|
subtitle.setTextSize(10);
|
||||||
|
subtitle.setPadding(0, dp(3), dp(8), 0);
|
||||||
|
titleBlock.addView(title, matchWrap());
|
||||||
|
titleBlock.addView(subtitle, matchWrap());
|
||||||
|
header.addView(titleBlock, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
|
||||||
|
Button close = terminalToolButton("×", view -> dialog.dismiss());
|
||||||
|
close.setContentDescription("Close " + titleText);
|
||||||
|
header.addView(close);
|
||||||
|
sheet.addView(header, spacedMatchWrap(0, 10));
|
||||||
|
return sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EditText sheetField(LinearLayout sheet, String labelText, String hint, String value) {
|
||||||
|
TextView label = formLabel(labelText);
|
||||||
|
label.setPadding(0, dp(10), 0, dp(6));
|
||||||
|
sheet.addView(label, matchWrap());
|
||||||
|
EditText input = new EditText(this);
|
||||||
|
input.setSingleLine(true);
|
||||||
|
input.setHint(hint);
|
||||||
|
input.setText(value);
|
||||||
|
input.setSelectAllOnFocus(!value.isEmpty());
|
||||||
|
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||||
|
styleInput(input);
|
||||||
|
sheet.addView(input, new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dp(44)
|
||||||
|
));
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showBottomSheet(Dialog dialog, LinearLayout sheet, EditText focus) {
|
||||||
|
dialog.setContentView(sheet);
|
||||||
|
dialog.setCanceledOnTouchOutside(true);
|
||||||
|
dialog.show();
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
||||||
|
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
WindowManager.LayoutParams attributes = window.getAttributes();
|
||||||
|
attributes.gravity = Gravity.BOTTOM;
|
||||||
|
attributes.dimAmount = 0.65f;
|
||||||
|
window.setAttributes(attributes);
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||||
|
}
|
||||||
|
if (focus != null) {
|
||||||
|
focus.requestFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private EditText formField(LinearLayout form, String label, String value) {
|
private EditText formField(LinearLayout form, String label, String value) {
|
||||||
TextView title = new TextView(this);
|
TextView title = new TextView(this);
|
||||||
title.setText(label);
|
title.setText(label);
|
||||||
@@ -3607,10 +3825,21 @@ public final class MainActivity extends Activity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastTerminalRenderMs = System.currentTimeMillis();
|
lastTerminalRenderMs = System.currentTimeMillis();
|
||||||
|
if (terminalViewMode == TERMINAL_VIEW_CHAT) {
|
||||||
|
terminalText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
terminalText.setHighlightColor(Color.TRANSPARENT);
|
||||||
|
terminalText.setText(terminalScreen.renderFocused(terminalExpandedBlocks, key -> {
|
||||||
|
if (!terminalExpandedBlocks.add(key)) {
|
||||||
|
terminalExpandedBlocks.remove(key);
|
||||||
|
}
|
||||||
|
renderTerminalNow();
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
terminalText.setMovementMethod(null);
|
terminalText.setMovementMethod(null);
|
||||||
terminalText.setText(terminalScreen.render());
|
terminalText.setText(terminalScreen.render());
|
||||||
|
}
|
||||||
updateTerminalLivePanel();
|
updateTerminalLivePanel();
|
||||||
if (terminalViewMode != TERMINAL_VIEW_CHAT && terminalFollowOutput) {
|
if (terminalFollowOutput) {
|
||||||
terminalScroll.post(() -> terminalScroll.fullScroll(View.FOCUS_DOWN));
|
terminalScroll.post(() -> terminalScroll.fullScroll(View.FOCUS_DOWN));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3913,6 +4142,23 @@ public final class MainActivity extends Activity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LinearLayout.LayoutParams spacedMatchWrap(int topDp, int bottomDp) {
|
||||||
|
LinearLayout.LayoutParams params = matchWrap();
|
||||||
|
params.topMargin = dp(topDp);
|
||||||
|
params.bottomMargin = dp(bottomDp);
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout.LayoutParams spacedHeight(int heightDp, int topDp, int bottomDp) {
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dp(heightDp)
|
||||||
|
);
|
||||||
|
params.topMargin = dp(topDp);
|
||||||
|
params.bottomMargin = dp(bottomDp);
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
private void showMessage(String message) {
|
private void showMessage(String message) {
|
||||||
if (message == null || message.trim().isEmpty()) {
|
if (message == null || message.trim().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -146,33 +146,82 @@ final class TerminalScreenBuffer {
|
|||||||
|
|
||||||
CharSequence renderFocused(Set<String> expandedBlocks, FocusToggle toggle) {
|
CharSequence renderFocused(Set<String> expandedBlocks, FocusToggle toggle) {
|
||||||
SpannableStringBuilder output = new SpannableStringBuilder();
|
SpannableStringBuilder output = new SpannableStringBuilder();
|
||||||
List<String> hiddenRows = new ArrayList<>();
|
int row = 0;
|
||||||
int hiddenStart = -1;
|
while (row < rows) {
|
||||||
for (int row = 0; row < rows; row++) {
|
|
||||||
String text = rowText(row).trim();
|
String text = rowText(row).trim();
|
||||||
if (containsHan(text)) {
|
if (!isCollapsibleHeader(text)) {
|
||||||
if (!hiddenRows.isEmpty()) {
|
if (text.isEmpty()) {
|
||||||
appendFocusBlock(output, hiddenRows, hiddenStart, row - 1, expandedBlocks, toggle);
|
if (output.length() > 0) {
|
||||||
hiddenRows.clear();
|
output.append('\n');
|
||||||
hiddenStart = -1;
|
|
||||||
}
|
}
|
||||||
appendLine(output, text);
|
} else {
|
||||||
} else if (!text.isEmpty()) {
|
appendStyledRow(output, row);
|
||||||
if (hiddenStart < 0) {
|
|
||||||
hiddenStart = row;
|
|
||||||
}
|
}
|
||||||
|
row++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int startRow = row;
|
||||||
|
List<String> hiddenRows = new ArrayList<>();
|
||||||
hiddenRows.add(text);
|
hiddenRows.add(text);
|
||||||
|
row++;
|
||||||
|
while (row < rows) {
|
||||||
|
String next = rowText(row).trim();
|
||||||
|
if (next.isEmpty() || isTopLevelLine(next)) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
hiddenRows.add(next);
|
||||||
|
row++;
|
||||||
}
|
}
|
||||||
if (!hiddenRows.isEmpty()) {
|
appendFocusBlock(output, hiddenRows, startRow, row - 1, expandedBlocks, toggle);
|
||||||
appendFocusBlock(output, hiddenRows, hiddenStart, rows - 1, expandedBlocks, toggle);
|
|
||||||
}
|
}
|
||||||
if (output.length() == 0) {
|
if (output.length() == 0) {
|
||||||
output.append("暂无可读内容");
|
output.append("Waiting for terminal output");
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCollapsibleHeader(String text) {
|
||||||
|
boolean activity = text.startsWith("• ") || text.startsWith("● ");
|
||||||
|
String header = stripActivityMarker(text);
|
||||||
|
return activity && (header.startsWith("Ran ")
|
||||||
|
|| header.equals("Explored")
|
||||||
|
|| header.startsWith("Explored ")
|
||||||
|
|| header.startsWith("Searched ")
|
||||||
|
|| header.startsWith("Read ")
|
||||||
|
|| header.startsWith("List ")
|
||||||
|
|| header.startsWith("Viewed ")
|
||||||
|
|| header.startsWith("Opened ")
|
||||||
|
|| header.startsWith("Fetched ")
|
||||||
|
|| header.startsWith("Downloaded ")
|
||||||
|
|| header.startsWith("Wrote ")
|
||||||
|
|| header.startsWith("Edited ")
|
||||||
|
|| header.startsWith("Applied ")
|
||||||
|
|| header.startsWith("Updated ")
|
||||||
|
|| header.startsWith("Checked ")
|
||||||
|
|| header.startsWith("Inspected ")
|
||||||
|
|| header.startsWith("Waited ")
|
||||||
|
|| header.startsWith("Working")
|
||||||
|
|| header.startsWith("Stop hook"))
|
||||||
|
|| text.startsWith("─ Worked for ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTopLevelLine(String text) {
|
||||||
|
return text.startsWith("• ")
|
||||||
|
|| text.startsWith("● ")
|
||||||
|
|| text.startsWith("› ")
|
||||||
|
|| text.startsWith("> ")
|
||||||
|
|| text.startsWith("─ ")
|
||||||
|
|| isCollapsibleHeader(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripActivityMarker(String text) {
|
||||||
|
if (text.startsWith("• ") || text.startsWith("● ")) {
|
||||||
|
return text.substring(2).trim();
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
private void appendFocusBlock(
|
private void appendFocusBlock(
|
||||||
SpannableStringBuilder output,
|
SpannableStringBuilder output,
|
||||||
List<String> lines,
|
List<String> lines,
|
||||||
@@ -208,25 +257,22 @@ final class TerminalScreenBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String focusSummary(List<String> lines) {
|
private String focusSummary(List<String> lines) {
|
||||||
|
String first = stripActivityMarker(lines.get(0));
|
||||||
String joined = String.join(" ", lines).toLowerCase(Locale.ROOT);
|
String joined = String.join(" ", lines).toLowerCase(Locale.ROOT);
|
||||||
String type;
|
String type;
|
||||||
if (joined.contains("error") || joined.contains("failed") || joined.contains("exception")) {
|
if (first.startsWith("Explored") || first.startsWith("Searched")
|
||||||
type = "错误输出";
|
|| first.startsWith("Read ") || first.startsWith("List ")
|
||||||
} else if (joined.contains("working") || joined.contains("running")
|
|| first.startsWith("Viewed ") || first.startsWith("Inspected ")) {
|
||||||
|| joined.contains("waiting") || joined.contains("interrupt")) {
|
type = "Explored";
|
||||||
type = "运行状态";
|
} else if (first.startsWith("Working") || first.startsWith("Stop hook")
|
||||||
} else if (joined.contains("test") || joined.contains("build") || joined.contains("compile")
|
|| first.startsWith("─ Worked for ")) {
|
||||||
|| joined.contains("gradle") || joined.contains("webpack") || joined.contains("npm")) {
|
type = "Status";
|
||||||
type = "构建/测试";
|
} else if (joined.contains("error") || joined.contains("failed") || joined.contains("exception")) {
|
||||||
} else if (joined.contains("git ") || joined.contains("commit") || joined.contains("push")) {
|
type = "Command failed";
|
||||||
type = "Git 操作";
|
|
||||||
} else if (lines.get(0).startsWith(">") || lines.get(0).startsWith("$")
|
|
||||||
|| lines.get(0).startsWith("!")) {
|
|
||||||
type = "命令与输出";
|
|
||||||
} else {
|
} else {
|
||||||
type = "终端细节";
|
type = "Command output";
|
||||||
}
|
}
|
||||||
return type + " · " + lines.size() + " 行 · " + compactSummary(lines.get(0), 42);
|
return type + " · " + lines.size() + " lines · " + compactSummary(first, 48);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String compactSummary(String text, int maxChars) {
|
private String compactSummary(String text, int maxChars) {
|
||||||
@@ -244,6 +290,18 @@ final class TerminalScreenBuffer {
|
|||||||
output.append(text);
|
output.append(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendStyledRow(SpannableStringBuilder output, int row) {
|
||||||
|
if (output.length() > 0) {
|
||||||
|
output.append('\n');
|
||||||
|
}
|
||||||
|
int limit = cols;
|
||||||
|
while (limit > 0 && cells[row][limit - 1].value == ' '
|
||||||
|
&& cells[row][limit - 1].bg == DEFAULT_BG) {
|
||||||
|
limit--;
|
||||||
|
}
|
||||||
|
appendRow(output, row, limit);
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -583,15 +641,19 @@ final class TerminalScreenBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void appendRow(SpannableStringBuilder output, int row) {
|
private void appendRow(SpannableStringBuilder output, int row) {
|
||||||
|
appendRow(output, row, cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRow(SpannableStringBuilder output, int row, int limit) {
|
||||||
int col = 0;
|
int col = 0;
|
||||||
while (col < cols) {
|
while (col < limit) {
|
||||||
Cell first = cells[row][col];
|
Cell first = cells[row][col];
|
||||||
int start = output.length();
|
int start = output.length();
|
||||||
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;
|
boolean isDim = first.dim;
|
||||||
while (col < cols) {
|
while (col < limit) {
|
||||||
Cell cell = cells[row][col];
|
Cell cell = cells[row][col];
|
||||||
if (cell.fg != fgColor || cell.bg != bgColor || cell.bold != isBold || cell.dim != isDim) {
|
if (cell.fg != fgColor || cell.bg != bgColor || cell.bold != isBold || cell.dim != isDim) {
|
||||||
break;
|
break;
|
||||||
@@ -626,18 +688,6 @@ final class TerminalScreenBuffer {
|
|||||||
return text.toString();
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user