Compare commits

..
1 Commits
Author SHA1 Message Date
Codex de0f7654d0 Integrate group management into sessions
Gitea Smoke / smoke (push) Successful in 0s
Gitea Android APK / build (push) Successful in 12m42s
2026-07-15 16:10:18 +00:00
@@ -27,6 +27,7 @@ import android.text.TextUtils;
import android.text.InputType; import android.text.InputType;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.view.Gravity; import android.view.Gravity;
import android.view.DragEvent;
import android.view.HapticFeedbackConstants; import android.view.HapticFeedbackConstants;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -595,25 +596,11 @@ 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));
row.addView(primaryButton(" Session", view -> promptCreateSession()), new LinearLayout.LayoutParams( addHeaderTool(row, "", "Session display settings", view -> promptSessionSettings(""));
ViewGroup.LayoutParams.WRAP_CONTENT, addHeaderTool(row, "", "Refresh sessions", view -> refreshSessions());
dp(38) addHeaderTool(row, "", "Create session", view -> promptCreateSession());
)); addHeaderTool(row, "", "Manage groups", view -> showGroupManagement());
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 display = terminalToolButton("", view -> promptSessionSettings(""));
display.setContentDescription("Session display settings");
utilities.addView(display);
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;
} }
@@ -788,12 +775,12 @@ public final class MainActivity extends Activity {
} }
private void showMainNavigation() { private void showMainNavigation() {
String[] items = {"Projects", "Tools", "Update", "About"}; String[] items = {"Sessions / groups", "Tools", "Update", "About"};
new AppDialogBuilder() new AppDialogBuilder()
.setTitle("tmuxctl") .setTitle("tmuxctl")
.setItems(items, (dialog, which) -> { .setItems(items, (dialog, which) -> {
if (which == 0) { if (which == 0) {
renderProjectsScreen(); openSessionPage();
} else if (which == 1) { } else if (which == 1) {
renderToolsScreen(); renderToolsScreen();
} else if (which == 2) { } else if (which == 2) {
@@ -839,7 +826,7 @@ public final class MainActivity extends Activity {
)); ));
content.addView(sectionTitle("Kanban / Messages")); content.addView(sectionTitle("Kanban / Messages"));
content.addView(actionPanel( content.addView(actionPanel(
actionButton("Projects", view -> showRaw("Kanban projects", () -> api.kanbanProjects())), actionButton("Groups", view -> showRaw("Groups", () -> api.kanbanProjects())),
actionButton("New project", view -> promptCreateKanbanProject()), actionButton("New project", view -> promptCreateKanbanProject()),
actionButton("Delete project", view -> promptDeleteKanbanProject()), actionButton("Delete project", view -> promptDeleteKanbanProject()),
actionButton("Remove session", view -> promptRemoveKanbanSession()) actionButton("Remove session", view -> promptRemoveKanbanSession())
@@ -1245,7 +1232,6 @@ public final class MainActivity extends Activity {
row.setPadding(dp(8), dp(5), dp(8), dp(5)); row.setPadding(dp(8), dp(5), dp(8), dp(5));
row.addView(navButton(PAGE_SERVERS, selected, view -> renderServerScreen())); row.addView(navButton(PAGE_SERVERS, selected, view -> renderServerScreen()));
row.addView(navButton(PAGE_SESSIONS, selected, view -> openSessionPage())); row.addView(navButton(PAGE_SESSIONS, selected, view -> openSessionPage()));
row.addView(navButton(PAGE_PROJECTS, selected, view -> renderProjectsScreen()));
row.addView(navButton(PAGE_TOOLS, selected, view -> renderToolsScreen())); row.addView(navButton(PAGE_TOOLS, selected, view -> renderToolsScreen()));
row.addView(navButton(PAGE_UPDATE, selected, view -> renderUpdateScreen())); row.addView(navButton(PAGE_UPDATE, selected, view -> renderUpdateScreen()));
row.addView(navButton(PAGE_ABOUT, selected, view -> renderAboutScreen())); row.addView(navButton(PAGE_ABOUT, selected, view -> renderAboutScreen()));
@@ -1498,6 +1484,7 @@ public final class MainActivity extends Activity {
private View sessionGroupSection(String name, List<SessionSummary> sessions, boolean project) { private View sessionGroupSection(String name, List<SessionSummary> sessions, boolean project) {
LinearLayout section = new LinearLayout(this); LinearLayout section = new LinearLayout(this);
section.setOrientation(LinearLayout.VERTICAL); section.setOrientation(LinearLayout.VERTICAL);
section.setOnDragListener((view, event) -> handleSessionDrop(section, name, project, event));
LinearLayout heading = new LinearLayout(this); LinearLayout heading = new LinearLayout(this);
heading.setOrientation(LinearLayout.HORIZONTAL); heading.setOrientation(LinearLayout.HORIZONTAL);
@@ -1520,6 +1507,10 @@ public final class MainActivity extends Activity {
count.setTextSize(11); count.setTextSize(11);
count.setTypeface(Typeface.MONOSPACE); count.setTypeface(Typeface.MONOSPACE);
heading.addView(count); heading.addView(count);
Button actions = terminalToolButton("", view -> showGroupActions(name, project));
actions.setContentDescription("Actions for " + name);
heading.addView(new View(this), new LinearLayout.LayoutParams(0, 1, 1));
heading.addView(actions);
section.addView(heading); section.addView(heading);
if (sessions.isEmpty()) { if (sessions.isEmpty()) {
@@ -1531,13 +1522,79 @@ 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), spacedMatchWrap(0, 10)); section.addView(sessionRow(session, project ? name : null), spacedMatchWrap(0, 10));
} }
} }
return section; return section;
} }
private boolean handleSessionDrop(
LinearLayout section,
String targetGroup,
boolean targetIsGroup,
DragEvent event
) {
if (!(event.getLocalState() instanceof SessionDrag)) {
return false;
}
if (event.getAction() == DragEvent.ACTION_DRAG_ENTERED) {
section.setBackground(rounded(COLOR_CARD_ALT, 8, COLOR_ACCENT, 1));
return true;
}
if (event.getAction() == DragEvent.ACTION_DRAG_EXITED
|| event.getAction() == DragEvent.ACTION_DRAG_ENDED) {
section.setBackgroundColor(Color.TRANSPARENT);
return true;
}
if (event.getAction() == DragEvent.ACTION_DROP) {
section.setBackgroundColor(Color.TRANSPARENT);
SessionDrag drag = (SessionDrag) event.getLocalState();
moveSessionBetweenGroups(drag.sessionName, drag.sourceGroup, targetIsGroup ? targetGroup : null);
return true;
}
return true;
}
private void showGroupActions(String groupName, boolean project) {
String[] items = project
? new String[]{"Add session", "Messages", "Delete group"}
: new String[]{"Create group", "Refresh sessions"};
new AppDialogBuilder()
.setTitle(groupName)
.setItems(items, (dialog, which) -> {
if (!project) {
if (which == 0) {
promptCreateKanbanProject();
} else {
refreshSessions();
}
} else if (which == 0) {
promptAddKanbanSessionToProject(groupName);
} else if (which == 1) {
showRaw("Group messages: " + groupName, () -> api.groupMessages(groupName));
} else {
runApiAction("Delete group", () -> api.deleteKanbanProject(groupName));
}
})
.show();
}
private void showGroupManagement() {
new AppDialogBuilder()
.setTitle("Groups")
.setItems(new String[]{"Create group", "Delete group", "Group messages"}, (dialog, which) -> {
if (which == 0) {
promptCreateKanbanProject();
} else if (which == 1) {
promptDeleteKanbanProject();
} else {
promptGroupMessages();
}
})
.show();
}
private void refreshProjects() { private void refreshProjects() {
if (projectList != null) { if (projectList != null) {
projectList.removeAllViews(); projectList.removeAllViews();
@@ -1685,7 +1742,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), spacedMatchWrap(0, 10)); list.addView(sessionRow(session, null), spacedMatchWrap(0, 10));
} }
if (sessionSummaryText != null) { if (sessionSummaryText != null) {
sessionSummaryText.setText("Server: " + getServerUrl() + "\nSessions: " + sessions.size()); sessionSummaryText.setText("Server: " + getServerUrl() + "\nSessions: " + sessions.size());
@@ -1693,7 +1750,7 @@ public final class MainActivity extends Activity {
setStatus("Loaded " + sessions.size() + " sessions"); setStatus("Loaded " + sessions.size() + " sessions");
} }
private View sessionRow(SessionSummary session) { private View sessionRow(SessionSummary session, String sourceGroup) {
LinearLayout row = new LinearLayout(this); LinearLayout row = new LinearLayout(this);
row.setOrientation(LinearLayout.HORIZONTAL); row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL); row.setGravity(Gravity.CENTER_VERTICAL);
@@ -1703,7 +1760,13 @@ public final class MainActivity extends Activity {
row.setOnClickListener(view -> openTerminal(session.name)); row.setOnClickListener(view -> openTerminal(session.name));
row.setOnLongClickListener(view -> { row.setOnLongClickListener(view -> {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
showSessionActions(session.name); ClipData data = ClipData.newPlainText("tmux-session", session.name);
view.startDragAndDrop(
data,
new View.DragShadowBuilder(view),
new SessionDrag(session.name, sourceGroup),
0
);
return true; return true;
}); });
@@ -1757,7 +1820,7 @@ public final class MainActivity extends Activity {
row.addView(textBlock, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); row.addView(textBlock, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1));
Button move = terminalToolButton("", view -> promptAddKanbanSession(session.name)); Button move = terminalToolButton("", view -> promptAddKanbanSession(session.name));
move.setContentDescription("Move " + session.name + " to project"); move.setContentDescription("Move " + session.name + " to group");
row.addView(move); row.addView(move);
Button kill = terminalToolButton("×", view -> confirmKill(session.name)); Button kill = terminalToolButton("×", view -> confirmKill(session.name));
kill.setContentDescription("Kill session"); kill.setContentDescription("Kill session");
@@ -3055,7 +3118,103 @@ public final class MainActivity extends Activity {
} }
private void promptAddKanbanSession(String sessionName) { private void promptAddKanbanSession(String sessionName) {
promptText("Add to kanban project", "project", "", projectName -> api.addKanbanSession(projectName, sessionName)); progressBar.setVisibility(View.VISIBLE);
executor.execute(() -> {
try {
List<String> groups = kanbanGroupNames(api.kanbanProjects());
runOnUiThread(() -> {
progressBar.setVisibility(View.GONE);
List<String> choices = new ArrayList<>();
choices.add("Ungrouped");
choices.addAll(groups);
choices.add(" Create new group");
new AppDialogBuilder()
.setTitle("Move " + sessionName)
.setItems(choices.toArray(new String[0]), (dialog, which) -> {
if (which == 0) {
moveSessionToGroup(sessionName, null);
} else if (which == choices.size() - 1) {
promptCreateGroupForSession(sessionName);
} else {
moveSessionToGroup(sessionName, choices.get(which));
}
})
.show();
});
} catch (Exception error) {
runOnUiThread(() -> {
progressBar.setVisibility(View.GONE);
showMessage("Group load failed: " + error.getMessage());
});
}
});
}
private List<String> kanbanGroupNames(String projectsText) throws Exception {
List<String> groups = new ArrayList<>();
JSONObject object = new JSONObject(projectsText == null || projectsText.isEmpty() ? "{}" : projectsText);
JSONArray projects = object.optJSONArray("projects");
for (int index = 0; projects != null && index < projects.length(); index++) {
JSONObject project = projects.optJSONObject(index);
if (project != null && !project.optString("name").isEmpty()) {
groups.add(project.optString("name"));
}
}
return groups;
}
private void promptCreateGroupForSession(String sessionName) {
promptText("Create group for " + sessionName, "group name", "", groupName -> {
if (groupName.isEmpty()) {
throw new IllegalArgumentException("Group name is required");
}
api.createKanbanProject(groupName, "~", "");
api.addKanbanSession(groupName, sessionName);
});
}
private void moveSessionToGroup(String sessionName, String targetGroup) {
runApiAction("Move session", () -> {
JSONObject object = new JSONObject(api.kanbanProjects());
JSONArray projects = object.optJSONArray("projects");
boolean alreadyInTarget = false;
for (int projectIndex = 0; projects != null && projectIndex < projects.length(); projectIndex++) {
JSONObject project = projects.optJSONObject(projectIndex);
if (project == null) {
continue;
}
String projectName = project.optString("name");
JSONArray agents = project.optJSONArray("agents");
for (int agentIndex = 0; agents != null && agentIndex < agents.length(); agentIndex++) {
if (!sessionName.equals(agentSessionName(agents.optJSONObject(agentIndex)))) {
continue;
}
if (projectName.equals(targetGroup)) {
alreadyInTarget = true;
} else {
api.removeKanbanSession(projectName, sessionName, false);
}
}
}
if (targetGroup != null && !alreadyInTarget) {
api.addKanbanSession(targetGroup, sessionName);
}
});
}
private void moveSessionBetweenGroups(String sessionName, String sourceGroup, String targetGroup) {
if ((sourceGroup == null && targetGroup == null)
|| (sourceGroup != null && sourceGroup.equals(targetGroup))) {
return;
}
runApiAction("Move session", () -> {
if (sourceGroup != null) {
api.removeKanbanSession(sourceGroup, sessionName, false);
}
if (targetGroup != null) {
api.addKanbanSession(targetGroup, sessionName);
}
});
} }
private void promptAddKanbanSessionToProject(String projectName) { private void promptAddKanbanSessionToProject(String projectName) {
@@ -5309,6 +5468,16 @@ public final class MainActivity extends Activity {
void onValue(int value); void onValue(int value);
} }
private static final class SessionDrag {
final String sessionName;
final String sourceGroup;
SessionDrag(String sessionName, String sourceGroup) {
this.sessionName = sessionName;
this.sourceGroup = sourceGroup;
}
}
private static final class SheetAction { private static final class SheetAction {
final String icon; final String icon;
final String title; final String title;