Skip to content

Commit f024ecd

Browse files
committed
feat(terminal): add touch selection "More" menu API and wire select dialog
1 parent 9318510 commit f024ecd

File tree

3 files changed

+290
-37
lines changed

3 files changed

+290
-37
lines changed

src/components/terminal/terminalManager.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import EditorFile from "lib/editorFile";
77
import TerminalComponent from "./terminal";
8+
import TerminalTouchSelection from "./terminalTouchSelection";
89
import "@xterm/xterm/css/xterm.css";
910
import quickTools from "components/quickTools";
1011
import toast from "components/toast";
@@ -729,6 +730,32 @@ class TerminalManager {
729730
return this.terminals;
730731
}
731732

733+
/**
734+
* Register a touch-selection "More" menu option.
735+
* @param {object} option
736+
* @returns {string|null}
737+
*/
738+
addTouchSelectionMoreOption(option) {
739+
return TerminalTouchSelection.addMoreOption(option);
740+
}
741+
742+
/**
743+
* Remove a touch-selection "More" menu option.
744+
* @param {string} id
745+
* @returns {boolean}
746+
*/
747+
removeTouchSelectionMoreOption(id) {
748+
return TerminalTouchSelection.removeMoreOption(id);
749+
}
750+
751+
/**
752+
* List touch-selection "More" menu options.
753+
* @returns {Array<object>}
754+
*/
755+
getTouchSelectionMoreOptions() {
756+
return TerminalTouchSelection.getMoreOptions();
757+
}
758+
732759
/**
733760
* Write to a specific terminal
734761
* @param {string} terminalId - Terminal ID

src/components/terminal/terminalTouchSelection.js

Lines changed: 253 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,139 @@
11
/**
22
* Touch Selection for Terminal
33
*/
4+
import select from "dialogs/select";
45
import "./terminalTouchSelection.css";
56

7+
const DEFAULT_MORE_OPTION_ID = "__acode_terminal_select_all__";
8+
const terminalMoreOptions = new Map();
9+
let terminalMoreOptionCounter = 0;
10+
11+
function ensureDefaultMoreOption() {
12+
if (terminalMoreOptions.has(DEFAULT_MORE_OPTION_ID)) return;
13+
14+
terminalMoreOptions.set(DEFAULT_MORE_OPTION_ID, {
15+
id: DEFAULT_MORE_OPTION_ID,
16+
label: () => strings["select all"] || "Select all",
17+
icon: "text_format",
18+
action: ({ touchSelection }) => touchSelection.selectAllText(),
19+
});
20+
}
21+
22+
function normalizeMoreOption(option) {
23+
if (!option || typeof option !== "object" || Array.isArray(option)) {
24+
console.warn(
25+
"[TerminalTouchSelection] addMoreOption expects an option object.",
26+
);
27+
return null;
28+
}
29+
30+
const id =
31+
option.id != null && option.id !== ""
32+
? String(option.id)
33+
: `terminal_more_option_${++terminalMoreOptionCounter}`;
34+
const label = option.label ?? option.text ?? option.title;
35+
const action = option.action || option.onselect || option.onclick;
36+
37+
if (!label) {
38+
console.warn(
39+
`[TerminalTouchSelection] More option '${id}' must provide a label/text/title.`,
40+
);
41+
return null;
42+
}
43+
44+
if (typeof action !== "function") {
45+
console.warn(
46+
`[TerminalTouchSelection] More option '${id}' must provide an action function.`,
47+
);
48+
return null;
49+
}
50+
51+
return {
52+
id,
53+
label,
54+
icon: option.icon || null,
55+
enabled: option.enabled,
56+
action,
57+
};
58+
}
59+
60+
function resolveMoreOptionLabel(option, context) {
61+
try {
62+
const value =
63+
typeof option.label === "function" ? option.label(context) : option.label;
64+
return value == null ? "" : String(value);
65+
} catch (error) {
66+
console.warn(
67+
`[TerminalTouchSelection] Failed to resolve label for option '${option.id}'.`,
68+
error,
69+
);
70+
return "";
71+
}
72+
}
73+
74+
function isMoreOptionEnabled(option, context) {
75+
try {
76+
if (typeof option.enabled === "function") {
77+
return option.enabled(context) !== false;
78+
}
79+
if (option.enabled === undefined) return true;
80+
return option.enabled !== false;
81+
} catch (error) {
82+
console.warn(
83+
`[TerminalTouchSelection] Failed to resolve enabled state for option '${option.id}'.`,
84+
error,
85+
);
86+
return true;
87+
}
88+
}
89+
690
export default class TerminalTouchSelection {
91+
/**
92+
* Register an option for the "More" menu in touch selection.
93+
* @param {{
94+
* id?: string,
95+
* label?: string|function(object):string,
96+
* text?: string,
97+
* title?: string,
98+
* icon?: string,
99+
* enabled?: boolean|function(object):boolean,
100+
* action?: function(object):void|Promise<void>,
101+
* onselect?: function(object):void|Promise<void>,
102+
* onclick?: function(object):void|Promise<void>
103+
* }} option
104+
* @returns {string|null}
105+
*/
106+
static addMoreOption(option) {
107+
ensureDefaultMoreOption();
108+
const normalized = normalizeMoreOption(option);
109+
if (!normalized) return null;
110+
terminalMoreOptions.set(normalized.id, normalized);
111+
return normalized.id;
112+
}
113+
114+
/**
115+
* Remove a registered "More" menu option by id.
116+
* @param {string} id
117+
* @returns {boolean}
118+
*/
119+
static removeMoreOption(id) {
120+
ensureDefaultMoreOption();
121+
if (id == null || id === "") return false;
122+
return terminalMoreOptions.delete(String(id));
123+
}
124+
125+
/**
126+
* List all registered "More" menu options.
127+
* @returns {Array<object>}
128+
*/
129+
static getMoreOptions() {
130+
ensureDefaultMoreOption();
131+
return [...terminalMoreOptions.values()].map((option) => ({ ...option }));
132+
}
133+
7134
constructor(terminal, container, options = {}) {
135+
ensureDefaultMoreOption();
136+
8137
this.terminal = terminal;
9138
this.container = container;
10139
this.options = {
@@ -783,17 +912,27 @@ export default class TerminalTouchSelection {
783912
// Mark that context menu should stay visible
784913
this.contextMenuShouldStayVisible = true;
785914

786-
// Position context menu - center it on selection with viewport bounds checking
787-
const startPos = this.terminalCoordsToPixels(this.selectionStart);
788-
const endPos = this.terminalCoordsToPixels(this.selectionEnd);
915+
// Position context menu - center it on selection (or fallback to center).
916+
const startPos = this.selectionStart
917+
? this.terminalCoordsToPixels(this.selectionStart)
918+
: null;
919+
const endPos = this.selectionEnd
920+
? this.terminalCoordsToPixels(this.selectionEnd)
921+
: null;
922+
923+
const menuWidth = this.contextMenu.offsetWidth || 200;
924+
const menuHeight = this.contextMenu.offsetHeight || 50;
925+
const containerRect = this.container.getBoundingClientRect();
926+
927+
let menuX;
928+
let menuY;
789929

790930
if (startPos || endPos) {
791-
// Use whichever position is available, or center between them
792-
let centerX, baseY;
931+
let centerX;
932+
let baseY;
793933

794934
if (startPos && endPos) {
795935
centerX = (startPos.x + endPos.x) / 2;
796-
// Position below the lower of the two positions
797936
baseY = Math.max(startPos.y, endPos.y);
798937
} else if (startPos) {
799938
centerX = startPos.x;
@@ -803,36 +942,24 @@ export default class TerminalTouchSelection {
803942
baseY = endPos.y;
804943
}
805944

806-
const menuWidth = this.contextMenu.offsetWidth || 200;
807-
const menuHeight = this.contextMenu.offsetHeight || 50;
808-
809-
const containerRect = this.container.getBoundingClientRect();
810-
811-
// Calculate initial position
812-
let menuX = centerX - menuWidth / 2;
813-
let menuY = baseY + this.cellDimensions.height + 40;
814-
815-
// Ensure menu stays within terminal bounds horizontally
816-
const minX = 10; // padding from left edge
817-
const maxX = containerRect.width - menuWidth - 10; // padding from right edge
818-
menuX = Math.max(minX, Math.min(menuX, maxX));
945+
menuX = centerX - menuWidth / 2;
946+
menuY = baseY + this.cellDimensions.height + 40;
947+
} else {
948+
menuX = (containerRect.width - menuWidth) / 2;
949+
menuY = containerRect.height - menuHeight - 20;
950+
}
819951

820-
// Ensure menu stays within terminal bounds vertically
821-
const maxY = containerRect.height - menuHeight - 10; // padding from bottom
822-
if (menuY > maxY) {
823-
// If menu would go below terminal, position it above the selection
824-
const topY =
825-
startPos && endPos ? Math.min(startPos.y, endPos.y) : baseY;
826-
menuY = topY - menuHeight - 10;
827-
}
952+
const minX = 10;
953+
const maxX = containerRect.width - menuWidth - 10;
954+
menuX = Math.max(minX, Math.min(menuX, maxX));
828955

829-
// Final bounds check
830-
menuY = Math.max(10, Math.min(menuY, maxY));
956+
const minY = 10;
957+
const maxY = containerRect.height - menuHeight - 10;
958+
menuY = Math.max(minY, Math.min(menuY, maxY));
831959

832-
this.contextMenu.style.left = `${menuX}px`;
833-
this.contextMenu.style.top = `${menuY}px`;
834-
this.contextMenu.style.display = "flex";
835-
}
960+
this.contextMenu.style.left = `${menuX}px`;
961+
this.contextMenu.style.top = `${menuY}px`;
962+
this.contextMenu.style.display = "flex";
836963
}
837964

838965
createContextMenu() {
@@ -843,7 +970,10 @@ export default class TerminalTouchSelection {
843970
const menuItems = [
844971
{ label: strings["copy"], action: this.copySelection.bind(this) },
845972
{ label: strings["paste"], action: this.pasteFromClipboard.bind(this) },
846-
{ label: "More...", action: this.showMoreOptions.bind(this) },
973+
{
974+
label: `${strings["more"] || "More"}...`,
975+
action: this.showMoreOptions.bind(this),
976+
},
847977
];
848978

849979
menuItems.forEach((item) => {
@@ -932,10 +1062,96 @@ export default class TerminalTouchSelection {
9321062
}
9331063
}
9341064

1065+
selectAllText() {
1066+
if (!this.terminal?.selectAll) return;
1067+
this.terminal.selectAll();
1068+
this.currentSelection = this.terminal.getSelection();
1069+
this.isSelecting = !!this.currentSelection;
1070+
this.selectionStart = null;
1071+
this.selectionEnd = null;
1072+
this.hideHandles();
1073+
1074+
if (this.options.showContextMenu && this.currentSelection) {
1075+
this.showContextMenu();
1076+
}
1077+
}
1078+
1079+
getMoreOptionsContext() {
1080+
return {
1081+
terminal: this.terminal,
1082+
touchSelection: this,
1083+
selection: this.currentSelection || this.terminal.getSelection(),
1084+
clearSelection: () => this.forceClearSelection(),
1085+
copySelection: () => this.copySelection(),
1086+
pasteFromClipboard: () => this.pasteFromClipboard(),
1087+
selectAll: () => this.selectAllText(),
1088+
};
1089+
}
1090+
1091+
getResolvedMoreOptions() {
1092+
ensureDefaultMoreOption();
1093+
const context = this.getMoreOptionsContext();
1094+
1095+
return [...terminalMoreOptions.values()]
1096+
.map((option) => {
1097+
const label = resolveMoreOptionLabel(option, context);
1098+
if (!label) return null;
1099+
1100+
return {
1101+
...option,
1102+
label,
1103+
disabled: !isMoreOptionEnabled(option, context),
1104+
};
1105+
})
1106+
.filter(Boolean);
1107+
}
1108+
1109+
async executeMoreOption(option) {
1110+
if (!option || typeof option.action !== "function" || option.disabled) {
1111+
if (this.isSelecting && this.options.showContextMenu) {
1112+
this.showContextMenu();
1113+
}
1114+
return;
1115+
}
1116+
1117+
try {
1118+
await option.action(this.getMoreOptionsContext());
1119+
} catch (error) {
1120+
console.error(
1121+
`[TerminalTouchSelection] Failed to execute more option '${option.id}'.`,
1122+
error,
1123+
);
1124+
window.toast?.("Failed to execute action.");
1125+
} finally {
1126+
if (this.isSelecting && this.options.showContextMenu) {
1127+
this.showContextMenu();
1128+
}
1129+
}
1130+
}
1131+
9351132
showMoreOptions() {
936-
// Implement additional options if needed
937-
window.toast("More options are not implemented yet.");
938-
this.forceClearSelection();
1133+
const moreOptions = this.getResolvedMoreOptions();
1134+
if (!moreOptions.length) return;
1135+
1136+
const items = moreOptions.map((option) => ({
1137+
value: option.id,
1138+
text: option.label,
1139+
icon: option.icon,
1140+
disabled: option.disabled,
1141+
}));
1142+
1143+
this.hideContextMenu(true);
1144+
1145+
select(strings["more"] || "More", items, true)
1146+
.then((selectedId) => {
1147+
const option = moreOptions.find((entry) => entry.id === selectedId);
1148+
return this.executeMoreOption(option);
1149+
})
1150+
.catch(() => {
1151+
if (this.isSelecting && this.options.showContextMenu) {
1152+
this.showContextMenu();
1153+
}
1154+
});
9391155
}
9401156

9411157
clearSelection() {

0 commit comments

Comments
 (0)