diff --git a/Plugin/unraid/css/varaverk.css b/Plugin/unraid/css/varaverk.css
index 78b0f34..46e7144 100644
--- a/Plugin/unraid/css/varaverk.css
+++ b/Plugin/unraid/css/varaverk.css
@@ -324,24 +324,75 @@
border: 1px solid #333; border-radius: 4px; padding: 10px 12px; resize: none;
line-height: 1.5; scroll-behavior: auto; tab-size: 2; width: 100%; box-sizing: border-box; }
-/* Editor layout: gutter + inner area */
-#vv-editor-wrap { display: flex; }
-#vv-ln-gutter { width: 42px; min-width: 42px; flex-shrink: 0;
+/* Editor layout: gutter + inner area — unified bordered block */
+#vv-editor-wrap { display: flex; border: 1px solid #2e2e2e; border-radius: 4px 4px 0 0;
+ overflow: hidden; background: #0d0d0d; }
+#vv-ln-gutter { width: 46px; min-width: 46px; flex-shrink: 0;
background: #0a0a0a; border-right: 1px solid #1c1c1c;
font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;
color: #3a3a3a; text-align: right; padding: 10px 8px 10px 0;
overflow: hidden; user-select: none; white-space: pre; }
-#vv-editor-inner { position: relative; flex: 1; overflow: hidden; }
+.vv-gln-cur { color: #ccc; font-weight: bold; }
+#vv-editor-inner { position: relative; flex: 1; overflow: hidden; background: #0d0d0d; }
-/* Syntax-highlight overlay sits over the transparent textarea */
-#vv-hl-overlay { display: none; position: absolute; top: 1px; left: 1px; right: 1px; bottom: 1px;
+/* Current line highlight — sits behind overlay */
+#vv-cur-line { position: absolute; left: 0; right: 0; height: 18px;
+ background: rgba(255,255,255,0.04);
+ border-top: 1px solid rgba(255,255,255,0.03);
+ border-bottom: 1px solid rgba(255,255,255,0.03);
+ pointer-events: none; display: none; }
+
+/* Syntax-highlight overlay — transparent bg so cur-line shows through */
+#vv-hl-overlay { display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
margin: 0; padding: 10px 12px; box-sizing: border-box;
font-family: monospace; font-size: 12px; line-height: 1.5; tab-size: 2;
white-space: pre-wrap; word-break: break-all; overflow: hidden;
pointer-events: none; user-select: none;
- background: #0d0d0d; border: none; border-radius: 3px; }
+ background: transparent; border: none; border-radius: 0; }
.vv-editor-hl #vv-hl-overlay { display: block; }
-.vv-editor-hl #vv-editor-body { color: transparent; caret-color: #ddd; background: transparent; }
+.vv-editor-hl #vv-editor-body { color: transparent; caret-color: #ddd; background: transparent;
+ border: none !important; border-radius: 0 !important; }
+
+/* Visible selection — works even when text is transparent */
+.vv-editor-hl #vv-editor-body::selection { background: rgba(100,149,237,0.35); color: transparent; }
+.vv-editor-hl #vv-editor-body::-moz-selection{ background: rgba(100,149,237,0.35); color: transparent; }
+
+/* Word match and search marks in the overlay */
+.vv-word-mark { background: rgba(255,220,60,0.14); outline: 1px solid rgba(255,220,60,0.35); border-radius: 2px; }
+.vv-search-mark { background: rgba(255,140,20,0.22); outline: 1px solid rgba(255,140,20,0.45); border-radius: 2px; }
+.vv-search-mark-current{ background: rgba(255,90,0,0.40); outline: 2px solid rgba(255,100,0,0.7); border-radius: 2px; }
+
+/* Slim scrollbar for editor */
+#vv-editor-body::-webkit-scrollbar { width: 8px; height: 8px; }
+#vv-editor-body::-webkit-scrollbar-track { background: #0a0a0a; }
+#vv-editor-body::-webkit-scrollbar-thumb { background: #2a2a2a; border-radius: 4px; }
+#vv-editor-body::-webkit-scrollbar-thumb:hover { background: #444; }
+
+/* Editor find bar */
+#vv-editor-find { display: none; align-items: center; gap: 6px; padding: 5px 8px;
+ margin-bottom: 5px; background: #161616; border: 1px solid #333;
+ border-radius: 4px; font-size: 12px; }
+#vv-editor-find.vv-find-open { display: flex; }
+#vv-find-input { flex: 1; min-width: 0; background: #0d0d0d; border: 1px solid #333;
+ color: #ddd; padding: 4px 8px; border-radius: 3px;
+ font-family: monospace; font-size: 12px; }
+#vv-find-input:focus { border-color: #6495ed; outline: none; box-shadow: 0 0 0 1px rgba(100,149,237,0.25); }
+#vv-find-input.vv-find-no-match { border-color: #c62828; box-shadow: 0 0 0 1px rgba(198,40,40,0.3); }
+.vv-find-count { font-size: 11px; color: #555; white-space: nowrap; flex-shrink: 0; min-width: 64px; text-align: right; }
+.vv-find-nav-btn{ padding: 2px 8px !important; }
+#vv-find-close { color: #555; cursor: pointer; font-size: 16px; line-height: 1;
+ padding: 0 2px; flex-shrink: 0; }
+#vv-find-close:hover { color: #aaa; }
+
+/* Editor status bar */
+#vv-editor-status { display: none; align-items: center; gap: 14px; padding: 3px 10px;
+ background: #0e0e0e; border: 1px solid #2e2e2e; border-top: none;
+ border-radius: 0 0 4px 4px; font-family: monospace;
+ font-size: 10px; color: #444; user-select: none; flex-shrink: 0; }
+#vv-editor-status.vv-ed-active { display: flex; }
+.vv-es-pos { color: #666; }
+.vv-es-sel { color: #555; }
+.vv-es-lang { margin-left: auto; color: #4a6a7a; }
/* Suggestions panel — accordion */
#vv-suggestions { overflow-y: auto; }
diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php
index 2c43ab2..c67ff62 100644
--- a/Plugin/unraid/pages/scheduler.php
+++ b/Plugin/unraid/pages/scheduler.php
@@ -872,15 +872,31 @@ Still the same two servers, two households, the same media stack running itself.
Name:
+
+
+
+
+
+ ×
+
+
+ onkeydown="vvEditorKeydown(event)"
+ onkeyup="vvEditorCursorMoved()" onmouseup="vvEditorCursorMoved()"
+ onclick="vvEditorCursorMoved()">
+
+ Ln 1, Col 1
+
+
+
@@ -951,6 +967,11 @@ let vvConfId = null;
let vvSelectedRow = null;
let vvCurrentSiId = null;
let vvCurrentSiHdr = null;
+// Editor state
+let vvWordMatch = null; // word under cursor — highlighted everywhere in overlay
+let vvFindTerm = ''; // active find-bar search string
+let vvFindMatches = []; // [{start,end}] in raw text
+let vvFindIdx = 0; // current match index
// Setup mode — injected by PHP when navigating from the first-run wizard
let vvSetupConf = = json_encode($vv_setup_conf) ?>;
@@ -1092,6 +1113,8 @@ function vvRestoreInvert() {
}
function vvBackToSuggestions() {
+ vvEditorReset();
+ document.getElementById('vv-editor-status').classList.remove('vv-ed-active');
if (vvActiveId) {
const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
@@ -1520,6 +1543,7 @@ function vvAddScript() {
document.getElementById('vv-editor-body').value = '#!/bin/bash\n\n';
vvShowEditorMode('New Script');
vvSyncHlOverlay();
+ vvEditorCursorMoved();
nameEl.focus();
}
@@ -1543,6 +1567,7 @@ function vvEditScript(id) {
.then(d => {
document.getElementById('vv-editor-body').value = d.ok ? d.content : '# Error loading script';
vvSyncHlOverlay();
+ vvEditorCursorMoved();
})
.catch(() => {
document.getElementById('vv-editor-body').value = '# Error loading script';
@@ -1553,7 +1578,10 @@ function vvEditScript(id) {
}
function vvShowEditorMode(title) {
+ vvEditorReset();
document.getElementById('vv-editor').classList.add('vv-editor-hl');
+ document.getElementById('vv-editor-status').classList.add('vv-ed-active');
+ document.getElementById('vv-es-lang').textContent = 'Bash';
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-si-view').style.display = 'none';
@@ -2588,12 +2616,16 @@ function vvEditRawConf(file) {
document.getElementById('vv-editor-body').value = d.ok ? d.content : '# Error loading file';
document.getElementById('vv-editor').classList.add('vv-editor-hl');
vvSyncHlOverlay();
+ vvEditorCursorMoved();
requestAnimationFrame(vvFitRight);
})
.catch(() => { document.getElementById('vv-editor-body').value = '# Load failed'; });
}
function vvShowRawConfMode(title) {
+ vvEditorReset();
+ document.getElementById('vv-editor-status').classList.add('vv-ed-active');
+ document.getElementById('vv-es-lang').textContent = 'Bash / Config';
document.getElementById('vv-suggestions').style.display = 'none';
document.getElementById('vv-log-pre').style.display = 'none';
document.getElementById('vv-si-view').style.display = 'none';
@@ -2671,14 +2703,52 @@ function vvSaveRawConf() {
// ── Highlighted conf editor overlay ──────────────────────────────────────────
+// ── Overlay helpers ────────────────────────────────────────────────────────────
+
+// Annotate text occurrences in already-rendered HTML (text between > and < only).
+// Returns modified HTML with wrapping each whole-word match.
+function vvMarkInHtml(html, word, cls, caseInsensitive) {
+ if (!word || word.length < 1) return html;
+ const hw = word.replace(/&/g,'&').replace(//g,'>');
+ const esc = hw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const fl = caseInsensitive ? 'gi' : 'g';
+ const re = new RegExp('(?((?:[^<])*)
+ '>' + txt.replace(re, `$&`) + '<'
+ );
+}
+
+// Like vvMarkInHtml but marks the curIdx-th match differently.
+function vvMarkSearchInHtml(html, term, curIdx) {
+ if (!term) return html;
+ const hw = term.replace(/&/g,'&').replace(//g,'>');
+ const esc = hw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ const re = new RegExp(esc, 'gi');
+ let idx = 0;
+ return html.replace(/>((?:[^<])*) {
+ const repl = txt.replace(re, m => {
+ const cls = idx++ === curIdx ? 'vv-search-mark-current' : 'vv-search-mark';
+ return `${m}`;
+ });
+ return '>' + repl + '<';
+ });
+}
+
function vvSyncHlOverlay() {
const ov = document.getElementById('vv-hl-overlay');
if (!ov || !document.getElementById('vv-editor').classList.contains('vv-editor-hl')) return;
const ta = document.getElementById('vv-editor-body');
- ov.innerHTML = vvHl(ta.value, false) + '\n';
+ let html = vvHl(ta.value, false) + '\n';
+ if (vvFindTerm) {
+ html = vvMarkSearchInHtml(html, vvFindTerm, vvFindIdx);
+ } else if (vvWordMatch) {
+ html = vvMarkInHtml(html, vvWordMatch, 'vv-word-mark', false);
+ }
+ ov.innerHTML = html;
ov.scrollTop = ta.scrollTop;
ov.scrollLeft = ta.scrollLeft;
vvUpdateLineNums();
+ vvUpdateCurLine();
}
function vvSyncHlScroll() {
@@ -2689,17 +2759,165 @@ function vvSyncHlScroll() {
ov.scrollLeft = ta.scrollLeft;
const gutter = document.getElementById('vv-ln-gutter');
if (gutter) gutter.scrollTop = ta.scrollTop;
+ vvUpdateCurLine();
}
function vvUpdateLineNums() {
const gutter = document.getElementById('vv-ln-gutter');
const ta = document.getElementById('vv-editor-body');
if (!gutter || !ta) return;
- const count = ta.value.split('\n').length;
- let nums = '';
- for (let i = 1; i <= count; i++) nums += i + '\n';
- gutter.textContent = nums;
- gutter.scrollTop = ta.scrollTop;
+ const lines = ta.value.split('\n').length;
+ const curLn = ta.value.slice(0, ta.selectionStart).split('\n').length;
+ let html = '';
+ for (let i = 1; i <= lines; i++) {
+ html += i === curLn ? `${i}\n` : `${i}\n`;
+ }
+ gutter.innerHTML = html;
+ gutter.scrollTop = ta.scrollTop;
+}
+
+// Position the current-line highlight strip behind the overlay
+function vvUpdateCurLine() {
+ const ta = document.getElementById('vv-editor-body');
+ const cl = document.getElementById('vv-cur-line');
+ if (!cl || !ta) return;
+ const lineH = 18; // font-size(12) × line-height(1.5)
+ const padTop = 10; // overlay padding-top
+ const lineN = ta.value.slice(0, ta.selectionStart).split('\n').length - 1; // 0-indexed
+ cl.style.top = (padTop + lineN * lineH - ta.scrollTop) + 'px';
+ cl.style.height = lineH + 'px';
+ cl.style.display = '';
+}
+
+// Called on cursor move (keyup, mouseup, click) — update word match, status bar, cur-line
+function vvEditorCursorMoved() {
+ vvUpdateCurLine();
+ vvUpdateEditorStatus();
+
+ if (vvFindTerm) { vvSyncHlOverlay(); return; } // find mode owns the overlay
+
+ const ta = document.getElementById('vv-editor-body');
+ const start = ta.selectionStart;
+ const end = ta.selectionEnd;
+ const val = ta.value;
+
+ // If user selected a word (not whitespace, at least 2 chars) → highlight all occurrences
+ if (end > start) {
+ const sel = val.slice(start, end);
+ const word = /^[\w$]{2,}$/.test(sel) ? sel : null;
+ if (word !== vvWordMatch) { vvWordMatch = word; vvSyncHlOverlay(); }
+ return;
+ }
+
+ // Find the identifier word at cursor position
+ const before = val.slice(0, start);
+ const after = val.slice(start);
+ const wBefore = before.match(/[$A-Za-z_][\w]*$/) ?? [''];
+ const wAfter = after.match(/^[\w]+/) ?? [''];
+ const word = wBefore[0].replace(/^\$/, '') + wAfter[0]; // strip leading $ for matching
+ const newWord = word.length >= 2 ? word : null;
+ if (newWord !== vvWordMatch) { vvWordMatch = newWord; vvSyncHlOverlay(); }
+}
+
+function vvUpdateEditorStatus() {
+ const ta = document.getElementById('vv-editor-body');
+ const pos = document.getElementById('vv-es-pos');
+ const sel = document.getElementById('vv-es-sel');
+ if (!ta || !pos) return;
+ const start = ta.selectionStart;
+ const end = ta.selectionEnd;
+ const val = ta.value;
+ const before = val.slice(0, start);
+ const lines = before.split('\n');
+ pos.textContent = `Ln ${lines.length}, Col ${lines[lines.length - 1].length + 1}`;
+ if (end > start) {
+ const chars = end - start;
+ const lns = val.slice(start, end).split('\n').length;
+ sel.textContent = lns > 1 ? `(${chars} chars · ${lns} lines)` : `(${chars} chars)`;
+ } else {
+ sel.textContent = '';
+ }
+}
+
+// ── Find bar ───────────────────────────────────────────────────────────────────
+
+function vvFindOpen() {
+ const bar = document.getElementById('vv-editor-find');
+ bar.classList.add('vv-find-open');
+ const inp = document.getElementById('vv-find-input');
+ // Pre-fill with selected word if any
+ const ta = document.getElementById('vv-editor-body');
+ const sel = ta.value.slice(ta.selectionStart, ta.selectionEnd);
+ if (sel && /^\w+$/.test(sel)) { inp.value = sel; }
+ inp.focus(); inp.select();
+ vvFindSearch();
+}
+
+function vvFindClose() {
+ document.getElementById('vv-editor-find').classList.remove('vv-find-open');
+ document.getElementById('vv-find-input').value = '';
+ document.getElementById('vv-find-input').classList.remove('vv-find-no-match');
+ document.getElementById('vv-find-count').textContent = '';
+ vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0;
+ document.getElementById('vv-editor-body').focus();
+ vvSyncHlOverlay();
+}
+
+function vvFindSearch() {
+ const inp = document.getElementById('vv-find-input');
+ const cnt = document.getElementById('vv-find-count');
+ vvFindTerm = inp.value;
+ if (!vvFindTerm) {
+ vvFindMatches = []; vvFindIdx = 0;
+ cnt.textContent = '';
+ inp.classList.remove('vv-find-no-match');
+ vvSyncHlOverlay(); return;
+ }
+ const ta = document.getElementById('vv-editor-body');
+ const re = new RegExp(vvFindTerm.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi');
+ vvFindMatches = [];
+ let m;
+ while ((m = re.exec(ta.value)) !== null) vvFindMatches.push({start: m.index, end: m.index + m[0].length});
+ if (!vvFindMatches.length) {
+ vvFindIdx = 0;
+ cnt.textContent = 'no matches';
+ inp.classList.add('vv-find-no-match');
+ } else {
+ vvFindIdx = Math.min(vvFindIdx, vvFindMatches.length - 1);
+ cnt.textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`;
+ inp.classList.remove('vv-find-no-match');
+ const cur = vvFindMatches[vvFindIdx];
+ ta.selectionStart = cur.start; ta.selectionEnd = cur.end;
+ }
+ vvSyncHlOverlay();
+}
+
+function vvFindNav(dir) {
+ if (!vvFindMatches.length) return;
+ vvFindIdx = (vvFindIdx + dir + vvFindMatches.length) % vvFindMatches.length;
+ document.getElementById('vv-find-count').textContent = `${vvFindIdx + 1} / ${vvFindMatches.length}`;
+ const ta = document.getElementById('vv-editor-body');
+ const cur = vvFindMatches[vvFindIdx];
+ ta.selectionStart = cur.start; ta.selectionEnd = cur.end;
+ ta.focus();
+ const lineN = ta.value.slice(0, cur.start).split('\n').length - 1;
+ ta.scrollTop = Math.max(0, lineN * 18 - 80);
+ vvSyncHlOverlay();
+}
+
+function vvFindKeydown(e) {
+ if (e.key === 'Enter') { e.preventDefault(); vvFindNav(e.shiftKey ? -1 : 1); }
+ if (e.key === 'Escape') { e.preventDefault(); vvFindClose(); }
+}
+
+function vvEditorReset() {
+ vvWordMatch = null; vvFindTerm = ''; vvFindMatches = []; vvFindIdx = 0;
+ const bar = document.getElementById('vv-editor-find');
+ if (bar) { bar.classList.remove('vv-find-open'); }
+ const inp = document.getElementById('vv-find-input');
+ if (inp) { inp.value = ''; inp.classList.remove('vv-find-no-match'); }
+ const cnt = document.getElementById('vv-find-count');
+ if (cnt) cnt.textContent = '';
}
function vvEditorKeydown(e) {
@@ -2715,6 +2933,21 @@ function vvEditorKeydown(e) {
return;
}
+ // Ctrl/Cmd+F — open find bar
+ if (e.key === 'f' && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ vvFindOpen();
+ return;
+ }
+
+ // Escape — close find bar if open
+ if (e.key === 'Escape') {
+ const bar = document.getElementById('vv-editor-find');
+ if (bar && bar.classList.contains('vv-find-open')) {
+ e.preventDefault(); vvFindClose(); return;
+ }
+ }
+
// Ctrl/Cmd+/ — toggle line comment
if (e.key === '/' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();