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>
3 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