Skip to content

Commit d18851f

Browse files
committed
feat(broadcast): textarea for multi-line commands and scripts
1 parent ad78dc2 commit d18851f

3 files changed

Lines changed: 59 additions & 27 deletions

File tree

src/locales/en.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,7 @@
880880
"deselectAll": "Deselect All",
881881
"execute": "Execute",
882882
"cancel": "Cancel",
883-
"commandPlaceholder": "Enter command to run on all selected nodes...",
884-
"hintEnter": "Enter to execute",
883+
"commandPlaceholder": "Enter command or paste a script...",
885884
"running": "Running...",
886885
"done": "Done",
887886
"noSSHNodes": "No nodes with SSH configured",

src/locales/ru.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,7 @@
880880
"deselectAll": "Снять выбор",
881881
"execute": "Выполнить",
882882
"cancel": "Отмена",
883-
"commandPlaceholder": "Введите команду для выполнения на выбранных нодах...",
884-
"hintEnter": "Enter для выполнения",
883+
"commandPlaceholder": "Введите команду или вставьте скрипт...",
885884
"running": "Выполняется...",
886885
"done": "Готово",
887886
"noSSHNodes": "Нет нод с настроенным SSH",

views/broadcast-terminal.ejs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@
157157
.command-row {
158158
display: flex;
159159
gap: 8px;
160-
align-items: center;
160+
align-items: flex-start;
161161
}
162162
163163
.command-input {
@@ -171,11 +171,24 @@
171171
font-size: 13px;
172172
outline: none;
173173
transition: border-color 0.15s;
174+
resize: none;
175+
overflow: hidden;
176+
min-height: 36px;
177+
max-height: 200px;
178+
line-height: 1.5;
174179
}
175180
.command-input:focus { border-color: #58a6ff; }
176181
.command-input::placeholder { color: #6e7681; }
177182
.command-input:disabled { opacity: 0.5; }
178183
184+
.command-actions {
185+
display: flex;
186+
flex-direction: column;
187+
gap: 4px;
188+
align-items: flex-end;
189+
flex-shrink: 0;
190+
}
191+
179192
.hint {
180193
font-size: 11px;
181194
color: #484f58;
@@ -409,23 +422,25 @@
409422
</div>
410423
<!-- Command input -->
411424
<div class="command-row">
412-
<input
413-
type="text"
425+
<textarea
414426
class="command-input"
415427
id="cmdInput"
416428
placeholder="<%- i18nKeys.cmdPlaceholder %>"
429+
rows="1"
417430
autocomplete="off"
418431
autocorrect="off"
419432
autocapitalize="off"
420433
spellcheck="false"
421-
>
422-
<button class="btn btn-primary" id="btnExecute" onclick="doExecute()" disabled>
423-
<i class="ti ti-player-play"></i> <%- i18nKeys.execute %>
424-
</button>
425-
<button class="btn btn-danger" id="btnCancel" onclick="doCancel()" style="display:none">
426-
<i class="ti ti-player-stop"></i> <%- i18nKeys.cancel %>
427-
</button>
428-
<span class="hint"><%- typeof t !== 'undefined' ? t('broadcast.hintEnter') : 'Enter to execute' %></span>
434+
></textarea>
435+
<div class="command-actions">
436+
<button class="btn btn-primary" id="btnExecute" onclick="doExecute()" disabled>
437+
<i class="ti ti-player-play"></i> <%- i18nKeys.execute %>
438+
</button>
439+
<button class="btn btn-danger" id="btnCancel" onclick="doCancel()" style="display:none">
440+
<i class="ti ti-player-stop"></i> <%- i18nKeys.cancel %>
441+
</button>
442+
<span class="hint">Ctrl+Enter</span>
443+
</div>
429444
</div>
430445
</div>
431446

@@ -765,28 +780,47 @@
765780
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
766781
}
767782
783+
// ── Textarea auto-resize ──────────────────────────────────────────
784+
function autoResize(el) {
785+
el.style.height = 'auto';
786+
el.style.overflow = 'hidden';
787+
const next = Math.min(el.scrollHeight, 200);
788+
el.style.height = next + 'px';
789+
if (el.scrollHeight > 200) el.style.overflow = 'auto';
790+
}
791+
768792
// ── Keyboard shortcuts ────────────────────────────────────────────
769793
document.addEventListener('DOMContentLoaded', () => {
770794
const input = document.getElementById('cmdInput');
771795
796+
input.addEventListener('input', () => autoResize(input));
797+
772798
input.addEventListener('keydown', (e) => {
773-
if (e.key === 'Enter') {
799+
const multiline = input.value.includes('\n');
800+
801+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
774802
e.preventDefault();
775803
doExecute();
776-
} else if (e.key === 'ArrowUp') {
777-
e.preventDefault();
778-
if (cmdHistoryIdx < cmdHistory.length - 1) {
804+
} else if (e.key === 'ArrowUp' && !multiline) {
805+
const cursorAtStart = input.selectionStart === 0;
806+
if (cursorAtStart && cmdHistoryIdx < cmdHistory.length - 1) {
807+
e.preventDefault();
779808
cmdHistoryIdx++;
780809
input.value = cmdHistory[cmdHistoryIdx];
810+
autoResize(input);
781811
}
782-
} else if (e.key === 'ArrowDown') {
783-
e.preventDefault();
784-
if (cmdHistoryIdx > 0) {
785-
cmdHistoryIdx--;
786-
input.value = cmdHistory[cmdHistoryIdx];
787-
} else {
788-
cmdHistoryIdx = -1;
789-
input.value = '';
812+
} else if (e.key === 'ArrowDown' && !multiline) {
813+
const cursorAtEnd = input.selectionStart === input.value.length;
814+
if (cursorAtEnd) {
815+
e.preventDefault();
816+
if (cmdHistoryIdx > 0) {
817+
cmdHistoryIdx--;
818+
input.value = cmdHistory[cmdHistoryIdx];
819+
} else {
820+
cmdHistoryIdx = -1;
821+
input.value = '';
822+
}
823+
autoResize(input);
790824
}
791825
}
792826
});

0 commit comments

Comments
 (0)