Agent Ransack Search to Collection

BETA
Uses Agent Ransack to search file content in the source directory. Creates a collection containing every file that surfaced a match. Places an HTML summary of the search results in the collection. Opens the preview pane and focuses the HTML summary to view matches.

This is slower than using the agent ransack GUI, but is handy for saving your searches. Also avoids opening a new window.

Button

<?xml version="1.0"?>
<button backcol="none" display="icon" textcol="none">
	<label>New Button</label>
	<icon1>#notepadpp</icon1>
	<function type="normal">
		<instruction>@runmode:hide</instruction>
		<instruction>ARContentToCollection QUERY=&quot;{dlgstring|Containing text?}&quot; FILEPATTERN=&quot;{dlgstring|File pattern?|*.*}&quot;</instruction>
	</function>
</button>

Script-Addin

// AgentRansackToCollection.js
// Directory Opus Script Add-in
// J.Fourie
// Adds command:
//   ARContentToCollection
//
// Main button:
//   ARContentToCollection QUERY="{dlgstring|Containing text?}" FILEPATTERN="{dlgstring|File pattern?|*.*}"
//
// Current-folder-only button:
//   ARContentToCollection QUERY="{dlgstring|Containing text?}" FILEPATTERN="{dlgstring|File pattern?|*.*}" NORECURSE

var HTML_CONFIG = {
    reportTitle: "Agent Ransack Results",
    theme: "dark",                 // "dark" or "light"
    includeRawOutput: true,
    includeFullPath: true,
    includeFolder: false,
    includeModified: true,
    includeHitCount: true,
    includeQueryMetadata: true,
    includeOpenLinks: true,
    includeCopyablePath: true,
    includeSummaryCards: true,
    maxSnippetsPerFile: 5,
    highlightQuery: true,
    sortBy: "name",                // "name", "path", "hits", "modified"
    showRowNumbers: true,
    compactMode: false,
    includeReportInCollection: true
};

function OnInit(initData) {
    initData.name = "Agent Ransack To Collection";
    initData.version = "1.3";
    initData.copyright = "J.Fourie";
    initData.desc = "Search current folder with Agent Ransack and import content matches into a Directory Opus collection with an HTML report.";
    initData.default_enable = true;

    var cmd = initData.AddCommand();
    cmd.name = "ARContentToCollection";
    cmd.method = "OnARContentToCollection";
    cmd.desc = "Run Agent Ransack content search in the current source folder and import matches into a collection.";
    cmd.label = "Agent Ransack Content Search";
    cmd.template = "QUERY/K,FILEPATTERN/K,COLL/K,NORECURSE/S,NOOPEN/S,KEEPDEBUG/S,THEME/K,MAXSNIPS/N,NORAW/S";
}

function OnARContentToCollection(data) {
    var args = data.func.args;
    var tab = data.func.sourcetab;
    var cmdObj = data.func.command;

    var queryText = getArg(args, "QUERY", "");
    var filePattern = getArg(args, "FILEPATTERN", "*.*");
    var collectionName = getArg(args, "COLL", "");

    var noRecurse = hasSwitch(args, "NORECURSE");
    var noOpen = hasSwitch(args, "NOOPEN");
    var keepDebug = hasSwitch(args, "KEEPDEBUG");

    var themeArg = getArg(args, "THEME", "");
    if (themeArg) {
        HTML_CONFIG.theme = themeArg;
    }

    var maxSnipsArg = getArg(args, "MAXSNIPS", "");
    if (maxSnipsArg) {
        var ms = parseInt(maxSnipsArg, 10);
        if (!isNaN(ms) && ms >= 0) {
            HTML_CONFIG.maxSnippetsPerFile = ms;
        }
    }

    if (hasSwitch(args, "NORAW")) {
        HTML_CONFIG.includeRawOutput = false;
    }

    queryText = clean(queryText);
    filePattern = clean(filePattern);

    if (!queryText) {
        DOpus.Output("ARContentToCollection: missing QUERY.");
        return;
    }

    if (!filePattern) {
        filePattern = "*.*";
    }

    var searchRoot = clean(String(tab.path));
    searchRoot = normaliseSlash(searchRoot);

    collectionName = clean(collectionName);

    if (!collectionName) {
        collectionName = makeCollectionName(queryText, searchRoot);
    }

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var shell = new ActiveXObject("WScript.Shell");

    var agentExe = findAgentRansackExe(fso);
    var dopusrt = findDOpusRT(fso);

    if (!agentExe) {
        DOpus.Output("ARContentToCollection: could not find flpsearch.exe.");
        return;
    }

    if (!dopusrt) {
        DOpus.Output("ARContentToCollection: could not find dopusrt.exe.");
        return;
    }

    var temp = fso.GetSpecialFolder(2).Path;
    var stamp = makeStamp();

    var rawOutput = fso.BuildPath(temp, "ar-content-raw-" + stamp + ".txt");
    var pathList = fso.BuildPath(temp, "ar-content-paths-" + stamp + ".txt");
    var logPath = fso.BuildPath(temp, "ar-content-debug-" + stamp + ".log");
    var htmlReport = fso.BuildPath(temp, "AgentRansackResults_Report-" + stamp + ".html");

    logToFile(fso, logPath, "SearchRoot=" + searchRoot);
    logToFile(fso, logPath, "Query=" + queryText);
    logToFile(fso, logPath, "FilePattern=" + filePattern);
    logToFile(fso, logPath, "CollectionName=" + collectionName);
    logToFile(fso, logPath, "RawOutput=" + rawOutput);
    logToFile(fso, logPath, "PathList=" + pathList);
    logToFile(fso, logPath, "HtmlReport=" + htmlReport);

    var arCmd = quote(agentExe)
        + " -d " + quote(searchRoot)
        + " -f " + quote(filePattern)
        + " -c " + quote(queryText)
        + " -o " + quote(rawOutput)
        + " -oft"
        + " -ofr:contents"
        + " -oc"
        + " -ol 20"
        + " -oe8";

    if (!noRecurse) {
        arCmd += " -s";
    }

    DOpus.Output("Running Agent Ransack...");
    DOpus.Output(arCmd);
    logToFile(fso, logPath, "Command=" + arCmd);

    var rc = shell.Run(arCmd, 0, true);

    if (rc !== 0) {
        DOpus.Output("Agent Ransack returned error code: " + rc);
        logToFile(fso, logPath, "Agent Ransack error code=" + rc);
        return;
    }

    if (!fso.FileExists(rawOutput)) {
        DOpus.Output("Agent Ransack did not create raw output.");
        logToFile(fso, logPath, "Raw output missing.");
        return;
    }

    var raw = readTextUtf8(rawOutput);

    logToFile(fso, logPath, "----- RAW OUTPUT START -----");
    logToFile(fso, logPath, raw);
    logToFile(fso, logPath, "----- RAW OUTPUT END -----");

    var records = parseAgentRansackOutput(raw, searchRoot, fso, logPath);
    sortRecords(records);

    var html = makeHtmlReport(records, raw, searchRoot, queryText);
    writeTextUtf8(htmlReport, html);

    var paths = [];

    if (HTML_CONFIG.includeReportInCollection) {
        paths.push(htmlReport);
    }

    for (var i = 0; i < records.length; i++) {
        paths.push(records[i].path);
    }

    writePathList(pathList, paths);

    logToFile(fso, logPath, "Matched file count=" + records.length);
    logToFile(fso, logPath, "Collection path count=" + paths.length);

    if (paths.length === 0) {
        DOpus.Output("No results. Log: " + logPath);
        return;
    }

    var importCmd = quote(dopusrt)
        + " /col import /utf8 /clear /create /nocheck "
        + quote(collectionName)
        + " "
        + quote(pathList);

    DOpus.Output("Importing collection: " + collectionName);
    logToFile(fso, logPath, "ImportCommand=" + importCmd);

    var importRc = shell.Run(importCmd, 0, true);

    if (importRc !== 0) {
        DOpus.Output("dopusrt import failed: " + importRc);
        logToFile(fso, logPath, "dopusrt import failed=" + importRc);
        return;
    }

	if (!noOpen) {
	    cmdObj.RunCommand('Go PATH="coll://' + collectionName.replace(/"/g, '""') + '"');
	
	    shell.Run('cmd.exe /d /c ping -n 2 127.0.0.1 >nul', 0, true);
	
	    cmdObj.RunCommand("Set VIEWPANE=on");
	    cmdObj.RunCommand("Set VIEWPANELOCK=off");
	
	    cmdObj.ClearFiles();
	    cmdObj.AddFile(htmlReport);
	    cmdObj.RunCommand("Select FROMSCRIPT DESELECTNOMATCH SETFOCUS");
	
	    shell.Run('cmd.exe /d /c ping -n 1 127.0.0.1 >nul', 0, true);
	    cmdObj.RunCommand("Select THIS");
	}

    DOpus.Output("Agent Ransack collection created: " + collectionName);
    DOpus.Output("Matched files: " + records.length);
    DOpus.Output("HTML report: " + htmlReport);
    DOpus.Output("Debug log: " + logPath);

    if (!keepDebug) {
        try {
            if (fso.FileExists(rawOutput)) {
                fso.DeleteFile(rawOutput, true);
            }

            if (fso.FileExists(pathList)) {
                fso.DeleteFile(pathList, true);
            }
        } catch (e) {
        }
    }
}

function getArg(args, name, def) {
    try {
        if (args.got_arg[name]) {
            return String(args[name]);
        }
    } catch (e1) {
    }

    try {
        var lower = name.toLowerCase();
        if (args.got_arg[lower]) {
            return String(args[lower]);
        }
    } catch (e2) {
    }

    try {
        if (typeof args[name] !== "undefined" && String(args[name]) !== "") {
            return String(args[name]);
        }
    } catch (e3) {
    }

    return def;
}

function hasSwitch(args, name) {
    try {
        if (args.got_arg[name]) {
            return true;
        }
    } catch (e1) {
    }

    try {
        var lower = name.toLowerCase();
        if (args.got_arg[lower]) {
            return true;
        }
    } catch (e2) {
    }

    return false;
}

function makeCollectionName(queryText, searchRoot) {
    var q = sanitizeCollectionPart(queryText);
    var l = sanitizeLocationPart(searchRoot);

    if (!q) {
        q = "query";
    }

    if (!l) {
        l = "location";
    }

    var prefix = "RS-Q_";
    var middle = "--L_";

    var name = prefix + q + middle + l;

    // Keep collection names readable.
    if (name.length > 140) {
        var maxQueryLen = 50;
        var maxLocationLen = 140 - prefix.length - middle.length - maxQueryLen;

        if (maxLocationLen < 30) {
            maxLocationLen = 30;
        }

        q = q.substring(0, maxQueryLen);
        l = tailString(l, maxLocationLen);

        name = prefix + q + middle + l;
    }

    return name;
}

function sanitizeCollectionPart(s) {
    s = String(s);

    // Remove characters that can break Opus collection paths or command quoting.
    s = s.replace(/[\\\/:*?"<>|^&%;=+,`~!@#$()[\]{}]/g, "_");

    // Collapse whitespace and underscores.
    s = s.replace(/\s+/g, "_");
    s = s.replace(/_+/g, "_");

    // Trim edge junk.
    s = s.replace(/^_+|_+$/g, "");

    return s;
}

function sanitizeLocationPart(s) {
    s = String(s);

    // Preserve drive letter in a safe way:
    // C:\Utilities\Airline_Trader -> C_Utilities_Airline_Trader
    s = s.replace(/^([A-Za-z]):\\?/, "$1_");

    // Replace path separators and unsafe characters.
    s = s.replace(/[\\\/:*?"<>|^&%;=+,`~!@#$()[\]{}]/g, "_");

    // Collapse whitespace and underscores.
    s = s.replace(/\s+/g, "_");
    s = s.replace(/_+/g, "_");

    // Trim edge junk.
    s = s.replace(/^_+|_+$/g, "");

    return s;
}

function tailString(s, maxLen) {
    s = String(s);

    if (s.length <= maxLen) {
        return s;
    }

    return s.substring(s.length - maxLen);
}

function findAgentRansackExe(fso) {
    var candidates = [
        "C:\\Program Files\\Mythicsoft\\Agent Ransack\\flpsearch.exe",
        "C:\\Program Files\\Mythicsoft\\FileLocator Pro\\flpsearch.exe",
        "C:\\Program Files (x86)\\Mythicsoft\\Agent Ransack\\flpsearch.exe",
        "C:\\Program Files (x86)\\Mythicsoft\\FileLocator Pro\\flpsearch.exe"
    ];

    for (var i = 0; i < candidates.length; i++) {
        if (fso.FileExists(candidates[i])) {
            return candidates[i];
        }
    }

    return "";
}

function findDOpusRT(fso) {
    var candidates = [
        "C:\\Program Files\\GPSoftware\\Directory Opus\\dopusrt.exe",
        "C:\\Program Files (x86)\\GPSoftware\\Directory Opus\\dopusrt.exe"
    ];

    for (var i = 0; i < candidates.length; i++) {
        if (fso.FileExists(candidates[i])) {
            return candidates[i];
        }
    }

    return "";
}

function makeStamp() {
    var d = new Date();

    return ""
        + d.getFullYear()
        + pad2(d.getMonth() + 1)
        + pad2(d.getDate())
        + "-"
        + pad2(d.getHours())
        + pad2(d.getMinutes())
        + pad2(d.getSeconds())
        + "-"
        + Math.floor(Math.random() * 100000);
}

function pad2(n) {
    n = String(n);
    return n.length < 2 ? "0" + n : n;
}

function quote(s) {
    return '"' + String(s).replace(/"/g, '""') + '"';
}

function logToFile(fso, path, msg) {
    try {
        var f = fso.OpenTextFile(path, 8, true);
        f.WriteLine(msg);
        f.Close();
    } catch (e) {
    }
}

function readTextUtf8(path) {
    var stream = new ActiveXObject("ADODB.Stream");
    stream.Type = 2;
    stream.Charset = "utf-8";
    stream.Open();
    stream.LoadFromFile(path);
    var text = stream.ReadText();
    stream.Close();
    return text;
}

function writeTextUtf8(path, text) {
    var stream = new ActiveXObject("ADODB.Stream");
    stream.Type = 2;
    stream.Charset = "utf-8";
    stream.Open();
    stream.WriteText(text);
    stream.SaveToFile(path, 2);
    stream.Close();
}

function writePathList(path, list) {
    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var f = fso.CreateTextFile(path, true, false);

    for (var i = 0; i < list.length; i++) {
        f.WriteLine(list[i]);
    }

    f.Close();
}

function trim(s) {
    return String(s).replace(/^\s+|\s+$/g, "");
}

function clean(s) {
    s = trim(s);
    s = s.replace(/^\uFEFF/, "");
    s = s.replace(/^"+|"+$/g, "");
    s = s.replace(/[;,]+$/g, "");
    return trim(s);
}

function normaliseSlash(s) {
    return String(s).replace(/\//g, "\\");
}

function startsWithIgnoreCase(s, prefix) {
    s = String(s).toLowerCase();
    prefix = String(prefix).toLowerCase();
    return s.indexOf(prefix) === 0;
}

function isRootedPath(s) {
    return /^[A-Za-z]:\\/.test(s);
}

function isRealFile(path, fso) {
    try {
        return fso.FileExists(path) && !fso.FolderExists(path);
    } catch (e) {
        return false;
    }
}

function htmlEscape(s) {
    return String(s)
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;");
}

function regexEscape(s) {
    return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function highlightText(text, query) {
    if (!HTML_CONFIG.highlightQuery) {
        return htmlEscape(text);
    }

    query = clean(query);

    if (!query) {
        return htmlEscape(text);
    }

    var escaped = htmlEscape(text);
    var q = regexEscape(htmlEscape(query));

    try {
        var re = new RegExp("(" + q + ")", "ig");
        return escaped.replace(re, "<mark>$1</mark>");
    } catch (e) {
        return escaped;
    }
}

function fileUrl(path) {
    var p = normaliseSlash(path).replace(/\\/g, "/");
    return "file:///" + encodeURI(p);
}

function addRecord(path, modified, hitCount, dict, records, searchRoot, fso, logPath, reason) {
    path = clean(normaliseSlash(path));

    if (!path) {
        return null;
    }

    if (!isRootedPath(path)) {
        logToFile(fso, logPath, "SKIP not rooted [" + reason + "]: " + path);
        return null;
    }

    if (!startsWithIgnoreCase(path, searchRoot)) {
        logToFile(fso, logPath, "SKIP outside root [" + reason + "]: " + path);
        return null;
    }

    if (!isRealFile(path, fso)) {
        logToFile(fso, logPath, "SKIP not real file [" + reason + "]: " + path);
        return null;
    }

    var key = path.toLowerCase();

    if (!dict[key]) {
        dict[key] = {
            path: path,
            name: fso.GetFileName(path),
            folder: fso.GetParentFolderName(path),
            modified: modified || "",
            hitCount: hitCount || "",
            snippets: []
        };

        records.push(dict[key]);
        logToFile(fso, logPath, "ADD [" + reason + "]: " + path);
    } else {
        if (modified && !dict[key].modified) {
            dict[key].modified = modified;
        }

        if (hitCount && !dict[key].hitCount) {
            dict[key].hitCount = hitCount;
        }
    }

    return dict[key];
}

function parseFullPathDateRow(line) {
    var re = /^([A-Za-z]:\\.*)\s+(\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d{2}(?::\d{2})?\s*(?:AM|PM)?)\s+(\d+)\s*$/i;
    var m = re.exec(line);

    if (!m) {
        return null;
    }

    return {
        path: clean(m[1]),
        modified: clean(m[2]),
        hitCount: clean(m[3])
    };
}

function parseFilenameFolderDateRow(line) {
    var re = /^(.*?)\s+([A-Za-z]:\\.*\\)\s+(\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d{2}(?::\d{2})?\s*(?:AM|PM)?)(?:\s+(\d+))?\s*$/i;
    var m = re.exec(line);

    if (!m) {
        return null;
    }

    var filename = clean(m[1]);
    var folder = clean(m[2]);
    var path = folder + filename;

    return {
        path: clean(path),
        modified: clean(m[3]),
        hitCount: clean(m[4] || "")
    };
}

function parseSnippetLine(line) {
    var re = /^(\d+)\s+(.*)$/;
    var m = re.exec(line);

    if (!m) {
        return null;
    }

    return {
        lineNo: clean(m[1]),
        text: clean(m[2])
    };
}

function parseAgentRansackOutput(raw, searchRoot, fso, logPath) {
    searchRoot = clean(normaliseSlash(searchRoot));

    if (searchRoot.length > 0 && !/[\\]$/.test(searchRoot)) {
        searchRoot = searchRoot + "\\";
    }

    var lines = raw.split(/\r?\n/);
    var dict = {};
    var records = [];
    var currentRecord = null;

    for (var i = 0; i < lines.length; i++) {
        var line = clean(lines[i]);

        if (!line) {
            continue;
        }

        var row = parseFullPathDateRow(line);

        if (!row) {
            row = parseFilenameFolderDateRow(line);
        }

        if (row) {
            currentRecord = addRecord(row.path, row.modified, row.hitCount, dict, records, searchRoot, fso, logPath, "ResultRow");
            continue;
        }

        var snippet = parseSnippetLine(line);

        if (snippet && currentRecord) {
            currentRecord.snippets.push(snippet);
            logToFile(fso, logPath, "SNIPPET " + currentRecord.path + " L" + snippet.lineNo);
        }
    }

    return records;
}

function numericHitCount(record) {
    var n = parseInt(record.hitCount, 10);
    if (isNaN(n)) {
        return 0;
    }
    return n;
}

function sortRecords(records) {
    var mode = String(HTML_CONFIG.sortBy || "name").toLowerCase();

    records.sort(function(a, b) {
        if (mode === "hits") {
            return numericHitCount(b) - numericHitCount(a);
        }

        if (mode === "path") {
            var ap = a.path.toLowerCase();
            var bp = b.path.toLowerCase();
            if (ap < bp) return -1;
            if (ap > bp) return 1;
            return 0;
        }

        if (mode === "modified") {
            var am = a.modified.toLowerCase();
            var bm = b.modified.toLowerCase();
            if (am < bm) return 1;
            if (am > bm) return -1;
            return 0;
        }

        var an = a.name.toLowerCase();
        var bn = b.name.toLowerCase();
        if (an < bn) return -1;
        if (an > bn) return 1;
        return 0;
    });
}

function totalHits(records) {
    var total = 0;

    for (var i = 0; i < records.length; i++) {
        total += numericHitCount(records[i]);
    }

    return total;
}

function themeCss() {
    if (String(HTML_CONFIG.theme).toLowerCase() === "light") {
        return ""
            + "body{font-family:Segoe UI,Arial,sans-serif;margin:20px;background:#f7f7f7;color:#111;}\r\n"
            + "h1{font-size:20px;margin:0 0 10px 0;}\r\n"
            + ".meta{color:#333;margin-bottom:18px;font-size:13px;line-height:1.5;}\r\n"
            + ".cards{display:flex;gap:10px;margin:12px 0 18px 0;flex-wrap:wrap;}\r\n"
            + ".card{background:#fff;border:1px solid #ccc;border-radius:8px;padding:10px 12px;min-width:130px;}\r\n"
            + ".card .k{font-size:11px;color:#666;text-transform:uppercase;}\r\n"
            + ".card .v{font-size:18px;font-weight:600;}\r\n"
            + "table{border-collapse:collapse;width:100%;font-size:13px;background:#fff;}\r\n"
            + "th,td{border:1px solid #ddd;padding:7px 8px;vertical-align:top;}\r\n"
            + "th{background:#eee;text-align:left;position:sticky;top:0;}\r\n"
            + "tr:nth-child(even){background:#fafafa;}\r\n"
            + "a{color:#0645ad;text-decoration:none;}\r\n"
            + "a:hover{text-decoration:underline;}\r\n"
            + ".path{color:#555;font-size:12px;word-break:break-all;}\r\n"
            + ".snippet{white-space:pre-wrap;font-family:Consolas,monospace;color:#222;margin-bottom:4px;}\r\n"
            + ".line{color:#8a5a00;font-weight:bold;}\r\n"
            + "mark{background:#fff176;color:#000;padding:0 2px;}\r\n"
            + "details{margin-top:22px;}\r\n"
            + "pre{white-space:pre-wrap;background:#fff;border:1px solid #ccc;padding:10px;overflow:auto;}\r\n";
    }

    return ""
        + "body{font-family:Segoe UI,Arial,sans-serif;margin:20px;background:#111;color:#eee;}\r\n"
        + "h1{font-size:20px;margin:0 0 10px 0;}\r\n"
        + ".meta{color:#bbb;margin-bottom:18px;font-size:13px;line-height:1.5;}\r\n"
        + ".cards{display:flex;gap:10px;margin:12px 0 18px 0;flex-wrap:wrap;}\r\n"
        + ".card{background:#1b1b1b;border:1px solid #333;border-radius:8px;padding:10px 12px;min-width:130px;}\r\n"
        + ".card .k{font-size:11px;color:#aaa;text-transform:uppercase;}\r\n"
        + ".card .v{font-size:18px;font-weight:600;}\r\n"
        + "table{border-collapse:collapse;width:100%;font-size:13px;}\r\n"
        + "th,td{border:1px solid #333;padding:7px 8px;vertical-align:top;}\r\n"
        + "th{background:#222;text-align:left;position:sticky;top:0;}\r\n"
        + "tr:nth-child(even){background:#181818;}\r\n"
        + "a{color:#8ab4f8;text-decoration:none;}\r\n"
        + "a:hover{text-decoration:underline;}\r\n"
        + ".path{color:#aaa;font-size:12px;word-break:break-all;}\r\n"
        + ".snippet{white-space:pre-wrap;font-family:Consolas,monospace;color:#ddd;margin-bottom:4px;}\r\n"
        + ".line{color:#f0c674;font-weight:bold;}\r\n"
        + "mark{background:#f5fc0c;color:#000;padding:0 2px;}\r\n"
        + "details{margin-top:22px;}\r\n"
        + "pre{white-space:pre-wrap;background:#080808;border:1px solid #333;padding:10px;overflow:auto;}\r\n";
}

function makeHtmlReport(records, rawText, searchRoot, queryText) {
    var now = new Date();
    var maxSnips = parseInt(HTML_CONFIG.maxSnippetsPerFile, 10);

    if (isNaN(maxSnips) || maxSnips < 0) {
        maxSnips = 5;
    }

    var html = "";
    html += "<!doctype html>\r\n";
    html += "<html><head><meta charset=\"utf-8\">\r\n";
    html += "<title>" + htmlEscape(HTML_CONFIG.reportTitle) + "</title>\r\n";
    html += "<style>\r\n";
    html += themeCss();

    if (HTML_CONFIG.compactMode) {
        html += "body{margin:10px;} th,td{padding:4px 5px;font-size:12px;} .snippet{font-size:12px;}\r\n";
    }

    html += "</style>\r\n";
    html += "</head><body>\r\n";

    html += "<h1>" + htmlEscape(HTML_CONFIG.reportTitle) + "</h1>\r\n";

    if (HTML_CONFIG.includeQueryMetadata) {
        html += "<div class=\"meta\">";
        html += "<div><b>Query:</b> " + htmlEscape(queryText) + "</div>";
        html += "<div><b>Search root:</b> " + htmlEscape(searchRoot) + "</div>";
        html += "<div><b>Generated:</b> " + htmlEscape(now.toLocaleString()) + "</div>";
        html += "</div>\r\n";
    }

    if (HTML_CONFIG.includeSummaryCards) {
        html += "<div class=\"cards\">";
        html += "<div class=\"card\"><div class=\"k\">Files</div><div class=\"v\">" + records.length + "</div></div>";
        html += "<div class=\"card\"><div class=\"k\">Hits</div><div class=\"v\">" + totalHits(records) + "</div></div>";
        html += "<div class=\"card\"><div class=\"k\">Mode</div><div class=\"v\">content</div></div>";
        html += "</div>\r\n";
    }

    html += "<table>\r\n";
    html += "<thead><tr>";

    if (HTML_CONFIG.showRowNumbers) {
        html += "<th>#</th>";
    }

    html += "<th>File</th>";

    if (HTML_CONFIG.includeFolder) {
        html += "<th>Folder</th>";
    }

    if (HTML_CONFIG.includeHitCount) {
        html += "<th>Hits</th>";
    }

    if (HTML_CONFIG.includeModified) {
        html += "<th>Modified</th>";
    }

    html += "<th>Preview</th>";
    html += "</tr></thead>\r\n";
    html += "<tbody>\r\n";

    for (var i = 0; i < records.length; i++) {
        var r = records[i];

        html += "<tr>";

        if (HTML_CONFIG.showRowNumbers) {
            html += "<td>" + (i + 1) + "</td>";
        }

        html += "<td>";

        if (HTML_CONFIG.includeOpenLinks) {
            html += "<a href=\"" + htmlEscape(fileUrl(r.path)) + "\">" + htmlEscape(r.name) + "</a>";
        } else {
            html += htmlEscape(r.name);
        }

        if (HTML_CONFIG.includeFullPath) {
            html += "<div class=\"path\">" + htmlEscape(r.path) + "</div>";
        }

        if (HTML_CONFIG.includeCopyablePath) {
            html += "<div class=\"path\"><code>" + htmlEscape(r.path) + "</code></div>";
        }

        html += "</td>";

        if (HTML_CONFIG.includeFolder) {
            html += "<td class=\"path\">" + htmlEscape(r.folder) + "</td>";
        }

        if (HTML_CONFIG.includeHitCount) {
            html += "<td>" + htmlEscape(r.hitCount) + "</td>";
        }

        if (HTML_CONFIG.includeModified) {
            html += "<td>" + htmlEscape(r.modified) + "</td>";
        }

        html += "<td>";

        if (r.snippets.length === 0) {
            html += "<span class=\"path\">No snippet captured.</span>";
        } else {
            var limit = r.snippets.length;

            if (maxSnips > 0 && limit > maxSnips) {
                limit = maxSnips;
            }

            for (var s = 0; s < limit; s++) {
                html += "<div class=\"snippet\">";
                html += "<span class=\"line\">L" + htmlEscape(r.snippets[s].lineNo) + "</span> ";
                html += highlightText(r.snippets[s].text, queryText);
                html += "</div>";
            }

            if (r.snippets.length > limit) {
                html += "<div class=\"path\">+" + (r.snippets.length - limit) + " more snippet(s) hidden by config.</div>";
            }
        }

        html += "</td>";
        html += "</tr>\r\n";
    }

    html += "</tbody></table>\r\n";

    if (HTML_CONFIG.includeRawOutput) {
        html += "<details><summary>Raw Agent Ransack output</summary>\r\n";
        html += "<pre>" + htmlEscape(rawText) + "</pre>\r\n";
        html += "</details>\r\n";
    }

    html += "</body></html>\r\n";

    return html;
}

Normal GUI search button:

  • Searches in the selected file/folder (if no file/folder is selected, searches in source directory)
<?xml version="1.0"?>
<button backcol="none" display="both" hotkey_label="yes" icon_size="large" textcol="none">
	<label>Ransack Search in File/Source</label>
	<hotkeys>
		<chord>
			<key>186,0</key>
			<key>R</key>
			<key>S</key>
		</chord>
	</hotkeys>
	<icon1>#Ransack_(32px_&amp;_22px):AgentRansack_104</icon1>
	<function type="batch">
		<instruction>@icon:/DOpusConfig/Icon_toggle/ransack.png</instruction>
		<instruction>@runmode:hide</instruction>
		<instruction>@ifsel:files</instruction>
		<instruction>	&quot;C:\Program Files\Mythicsoft\Agent Ransack\AgentRansack.exe&quot; -pa -r -d &quot;{allfilepath|..\|noterm|sep=;}&quot; -c &quot;{dlgstring|Search Within Text:}&quot;</instruction>
		<instruction>@ifsel:else</instruction>
		<instruction>	&quot;C:\Program Files\Mythicsoft\Agent Ransack\AgentRansack.exe&quot; -pa -r -d &quot;{sourcepath|noterm}&quot; -c &quot;{dlgstring|Search Within Text:}&quot;</instruction>
	</function>
</button>
4 Likes

Very cool.
I've had my eye on agent ransack pro for a while now. The price is pretty steep though.
I like having a content search ability, but not having the entire index always mounted onto my ram, as void tools everything does. Ideally a "mount the index only while using the app".

Windows can index content too but I don't want that either. And the search tools are nowhere near agent ransack, everything or dopus.

I can think of a few benefits of content search:

  • PDF manuals content: You can have the content of all your user manuals indexed and quickly reference them with a content search. Like a google search engine for your computer.
  • Reference library content: I had mentioned the Logos app with my Christian books, that does this. You could index the content of all your technical textbooks, but they would have to be in the correct file format.
  • Content transcripts: I was thinking that I could bulk download chosen youtube transcripts, with timestamps, and then I can quickly find where certain things were said in which video. There are websites that already can do this though, I think. Haven't really tried this yet.
  • Notes: Obsidian has a search tool for its .md notes already, but maybe if someone uses another format for notes then they could use agent ransack instead. Or might be helpful for searching straight from the desktop (I'm assuming agent ransack pro supports the .md file type etc.)
  • Search from anywhere?: Listary has the hotkey to pop up its omnibox search from anywhere. But I have too many friggin indexes going on. I wonder if agent ransack pro has the same "run on taskbar" feature and a hotkey. But then it's back to the problem of ram use. People say the ram is energized always anyway but I still don't like it.
1 Like

@jfour18 I use Agent Ransack too - in my view the best app in its genre - but I missed out on your post, discovered it by accident. I used AG more often in the past, and I did an indexing of many hundreds of ebooks, also a couple of downloaded websites. It's a great program and allows to instantly read the relevant found parts of text. It has improved over time. The indexing however is heavy-duty, and slow on my laptop. (Fortunately you can configure it to keep extracted text in cache). Do you use indexes?

Anyway, I never used AG via the command line interface but your button works great for on-the-fly searches. Appreciated!

1 Like

@ASUNDER YouTube transcripts? You can easily downloaded those with yt-dlp, e.g. with a DO button that takes the url from clipboard and calls yt-dlp with --sub-langs en,auto or something like that (and the url). Although lately, I prefer using the excellent NoteGTP browser extension (in Chrome or Edge, for instance) which has the intelligence to make for a more readable text, and also generates a Summary via Claude AI, and more - if you pay a little (you get 1000 quota's). But you can basically do YouTube transcripts for free. You still need to save them to disk manually, but it's worth the little extra manip.

2 Likes

That sounds interesting, but hacky. Not sure how that works. I would maybe prefer dedicated software for that, but haven't looked yet, been busy. Do you have an example of this?

Nah, in my use case I would want exact transcripts, for quickly finding things people have said, for making my own videos + music out of the quotes.

It's not exactly a 'hack' because downloading video's isn't really illegal as far as I know - it would be illegal to distribute material to the public if it isn't yours, but no one can punish you for having downloaded videos (as long as its content isn't the subject of criminal investigation).

yt-dlp is properly supported, it is the new version of an older one (it was formerly yt-download, I think).

An example? You mean for a windows system? I do it via with a powershell script, but that requires windows 10 or 11 and preferrably powershell version 7 (although 5 would also work, with some small adaptations). Not sure if that's fine for you. I use powershell because in powershell I can stack multiple simultaneous download windows together, rather than random windows all over the place.

1 Like

I meant Dopus is a file manager, not an internet interface tool. (anything script based starts off as potentially hacky in my mind. I don't mean criminal, I mean not very good). I used to have DVD Video Soft suite for doing this stuff. Not sure if they have a transcript tool though.

Searched for the yt-dlp. First result says, "A feature rich command line" ...
Nope I'll pass.

There's some browser extensions that are ok for this. Some social media management software too, but those carry a subscription fee. Might be worth it some day, if I ever get my buns in gear. :roll_eyes:

Thanks for the helps anyway.

You’re recommending ExifTool and a related script in another thread. You know it’s also a command line tool, right? :slight_smile:

yt-dlp is a good solution if you need to download things from YouTube. People have been using it for a long time.

Command line tools are the easiest things to automate from other software (like Opus).

1 Like

I gave a heads up for the script cause he already uses exiftool. I don't use either right now. Not trying to argue against it though, just saying I don't do command line tools myself. A response to his recommendation, just so he isn't left wondering.

OBS tho.

The issue with downloaders is someone else gets to determine if you can have a local copy of the media. And they know you have it. Disk recording is anonymous and irresistable.

Keeping transcripts as a seperate issue to actually having the video in hand, because most transcripts I wouldn't need, and most videos I don't need but I would need to know where something was said.

To reference youtube I use bookmarks though, not downloads. If I just need ten seconds out of an hour long video, then a download would be a big waste of resources.

That's an appeal to consensus fallacy.
It's possible they use something because they are unaware of an existing better solution. Like how I've used windows explorer all my life while dopus existed the whole time. "Most people use windows explorer." Ya, they use it in error.

Said the computer programmer. :roll_eyes:

But a lot depends on what you consider resources. You can "waste" some of your internet quota, fair enough. But I rather waste bandwidth than time. As I saw myself watching more video podcasts over time, and looking up podcasts (from knowledgeable folks) about a certain subject I wanted to explore in depth, I found it more producive to download, say, the 15 most promising video's in one folder and then start listening, quickly switching back to a video to listen again to a particular part - in order to finally get a good idea about the big picture. Doing this via bookmarks was - for me at least - less confortable. Not to mention that when I download a video, the url is automatically stored with the video, data that could be used by a simple button taking that url and download the transcript. I may also mark a few real good ones with some Opus label and then finally delete the others. For me, that meant saving resources - especially time and frustration.

I should also add, however, that a transcripts downloaded via yt-dlp is not very readable (unless you also implement auto-cleanup tools in the process - which I didn't do, it's just not worth it). That's why I ultimately switched to that other tool I mentioned, to get a readable transcript (and more). So I made a bit of a mistake when I said you can get a clean transcript via yt-dlp - I think it requires a more complex setup to get a reall nice readable transcript that way. Even just reading the plain YouTube transcript (disabling the timestamps) reads better.

Yes, I should thank you. I actually started using it because you mentioned it in the off-topic 'software stack' post. It has proven very helpful to me as well. I do use indices in the normal GUI version. I have been meaning to check whether the command line API allows me to call an existing index to speed up the process. I have not gotten around to it yet I am in the process of setting up a Linux OS, as an escape hatch in case Microsoft continues to disappoint. I will doubtless investigate at some later stage.

1 Like

Ah, yes, your productivity stack post. I picked up some ideas there too. And your idea for a direct search via a button is, in turn, a nice trick I now use as well with Agent Ransack.

Ultimately, one of the most surprising things about Directory Opus - looking at it from an "innocent- naive" perspective at least - has always been this "next new button that does some great magic".

1 Like

That's true. Let the computer do the work.

I'm also not acknowledging how a stream ends up on your screen anyway. It gets downloaded somehow to watch it anyway.

Ya. This is one of the few tasks I don't mind utilizing AI for. Finding things.
I like being able to search content, not just relying on file names and metadata. AI is good with that; especially if it's already scraped the media catalogue already. Transcripts are a good quick low cost way of handling video speech, cause it's just plain text.

1 Like