Was inspired to make this script after seeing another thread.
It generates and copies to the clipboard a tree view of selected files and folders with lots of options and features, such as automatic expansion of sub folders (to customizable depth) and customizable appearance.
Example Output:
Variable Options:
- Show Files First (
filesFirst
): When set to true, files will be listed before folders in the tree view. - Expansion Depth (
expandDepth
): Controls how deep the folder structure should be expanded.- Set to 0 for no expansion, names of files/folders in source directory only
- Set to -1 for unlimited depth
- Set to any positive integer for that specific depth
- Context Line (
contextLine
): Determines what to show as the top line of the tree view. (Also affected by "showSourceOneUpContext" variable)- "none" - No context line
- "folder" - Name of the source folder
- "path" - Full path of the source folder
- Up One Context (
showSourceOneUpContext
): When set to true, the top context line will show relative to one layer up. (It won't show any more files/folders, it basically just adds an extra indentation level for the source folder)
Optional Command Arguments (For Above Variables):
FILES_FIRST
EXPAND_DEPTH
CONTEXT_LINE
UP_ONE_CONTEXT
Appearance Customizability Variables:
You can customize the appearance of the tree view by modifying these variables:
middleFileBranch
: Character(s) for files at a middle branchendFileBranch
: Character(s) for files at an end branchmiddleFolderBranch
: Character(s) for folders at a middle branchendFolderBranch
: Character(s) for folders at an end branchverticalBranch
: Character(s) for spacers and directory layersfolderPrefix
andfolderSuffix
: Strings to add before and after folder names, in case you want to make folders show with brackets or something around them [Like This]discontinuousBranchForLastFile
: Whether or not to use the "endFileBranch" for the last file in files first mode. You can see this in action in the screenshot above between lines 6-7 and 17-18. If false, it would use the "middleFileBranch" value.
Arguments Template (Must put this in the 'Template' box in the command editor to use arguments):
UP_ONE_CONTEXT/O/S,FILES_FIRST/O/S,EXPAND_DEPTH/O/N,CONTEXT_LINE/O
Usage Example - Basic argument usage when calling script as a user command:
-
Copy_Tree_View UP_ONE_CONTEXT FILES_FIRST EXPAND_DEPTH=-1 CONTEXT_LINE="path"
How to Install (Easy Way):
- Download the User Command .dcf file here:
Copy_Tree_View.dcf (18.7 KB) - Open the Customize Menu > "User Commands" Tab
- Drag the file into the list of user commands. Or use the 'Import' button to import the file.
How to Install Manually:
Copy the script below to a new User Command via the Command Editor window. Be sure to set it as Script Function using JScript, and to set the Argument Template.
// Copy a tree view of selected files/folders
// By ThioJoe
// Updated: 7/3/24 (1.0.2)
// Argument Template:
// UP_ONE_CONTEXT/O/S,FILES_FIRST/O/S,EXPAND_DEPTH/O/N,CONTEXT_LINE/O
function OnClick(clickData)
{
// ------- Options (These values will be used if no corresponding argument is specified when calling script --------
// True/False - Whether to list files before folders
// > Optional Argument Name: FILES_FIRST (Switch, no value needed)
var filesFirst = false;
// Integer - Depth to expand folders. 0 for no expansion, -1 for unlimited depth
// > Optional Argument Name: EXPAND_DEPTH (Integer value)
var expandDepth = -1;
// String - What to show as the top line. Nothing, name of the source folder, or path of the source folder
// Possible Values: "none", "folder", "path"
// > Optional Argument Name: CONTEXT_LINE (String value)
var contextLine = "folder";
// True/False - If true (or argument included), the context line will be moved up by one folder.
// > Optional Argument Name: UP_ONE_CONTEXT (Switch, no value needed)
var showSourceOneUpContext = false;
// -----------------------------------------------------------------------------------------------------------------
// Example usage of arguments:
// Copy_Tree_View UP_ONE_CONTEXT FILES_FIRST EXPAND_DEPTH=1 CONTEXT_LINE="path"
// -----------------------------------------------------------------------------------------------------------------
// ------------------------ APPEARANCE SETTINGS ------------------------
// The character(s) to use for files at a middle branch of the tree
var middleFileBranch = "├───";
// The character(s) to use for files at an end branch of the tree
var endFileBranch = "└───";
// The character(s) to use for folders at a middle branch of the tree
var middleFolderBranch = "├───";
// The character(s) to use for folders at an end branch of the tree
var endFolderBranch = "└───";
// The character(s) to use as a spacer and directory layers not connected to a file/folder
var verticalBranch = "│";
// Folder name settings, such as to add brackets surrounding folder names like this or something: [Folder Name]
// The string to prefix folder names with (default is empty string)
var folderPrefix = "";
// The string to suffix folder names with (default is empty string)
var folderSuffix = "";
// In files first mode, this sets whether to show the endFileBranch string above for the last file in the folder, even if there are folders below it
var discontinuousBranchForLastFile = true;
// ---------------------------------------------------------------------
// Parse optional arguments if they're there
if (clickData.func.args.got_arg.UP_ONE_CONTEXT) {
showSourceOneUpContext = true;
//DOpus.Output("Received UP_ONE_CONTEXT argument");
}
if (clickData.func.args.got_arg.FILES_FIRST) {
filesFirst = true;
//DOpus.Output("Received FILES_FIRST argument");
}
if (clickData.func.args.got_arg.EXPAND_DEPTH) {
// Validate argument value
var argExpandDepth = parseInt(clickData.func.args.EXPAND_DEPTH, 10);
if (!isNaN(argExpandDepth) && (argExpandDepth >= -1)) {
expandDepth = argExpandDepth;
} else {
expandDepth = -1;
DOpus.Output("ERROR: Invalid EXPAND_DEPTH argument. Must be an integer >= -1. Got: " + clickData.func.args.EXPAND_DEPTH);
}
//DOpus.Output("Received EXPAND_DEPTH argument: " + expandDepth);
}
if (clickData.func.args.got_arg.CONTEXT_LINE) {
// Validate argument value
var argContextLine = clickData.func.args.CONTEXT_LINE.toLowerCase();
if (argContextLine === "none" || argContextLine === "folder" || argContextLine === "path") {
contextLine = argContextLine;
} else {
contextLine = "folder";
DOpus.Output("ERROR: Invalid CONTEXT_LINE argument. Must be either 'none', 'folder', or 'path'. Got: " + clickData.func.args.CONTEXT_LINE);
}
//DOpus.Output("Received CONTEXT_LINE argument: " + contextLine);
}
// Further variable setup
contextLine = contextLine.toLowerCase()
if (discontinuousBranchForLastFile === false) {
var lastFileBranch = middleFileBranch;
} else {
var lastFileBranch = endFileBranch;
}
var tab = clickData.func.sourcetab;
var selectedItems = tab.selected;
var sourcePathDepth = tab.path.Split.count;
// Adjust initial depth based on the context
var initialDepth = showSourceOneUpContext ? sourcePathDepth - 1 : sourcePathDepth;
var expandedItems = expandSelectedItems(selectedItems, expandDepth);
// Decide top level line to print. Whether current folder name, current full path name, or up-one context
var topLine = "";
if (contextLine !== "none") {
if (showSourceOneUpContext === true) {
var parentPath = DOpus.FSUtil.NewPath(tab.path);
parentPath.Parent();
if (contextLine === "folder") {
topLine = parentPath.filepart + "\n";
} else if (contextLine === "path") {
topLine = parentPath + "\n";
}
} else if (showSourceOneUpContext === false) {
if (contextLine === "folder") {
topLine = tab.path.filepart + "\n";
} else if (contextLine === "path") {
topLine = tab.path + "\n";
}
}
}
// Create the tree output
var treeOutput = topLine;
treeOutput += generateTree(expandedItems, initialDepth, filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch);
DOpus.SetClip(treeOutput);
}
function expandSelectedItems(items, expandDepth) {
var expandedItems = DOpus.Create().Vector();
function expand(item, depth) {
expandedItems.push_back(item);
if (item.is_dir && (expandDepth === -1 || depth < expandDepth)) {
var folderEnum = DOpus.FSUtil.ReadDir(item, false);
while (!folderEnum.complete) {
var subItem = folderEnum.Next();
if (subItem) {
expand(subItem, depth + 1);
}
}
}
}
for (var i = 0; i < items.count; i++) {
expand(items(i), 0);
}
return expandedItems;
}
function generateTree(items, baseDepth, filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch) {
var treeText = "";
var pathTree = {};
// Build the tree structure
for (var i = 0; i < items.count; i++) {
var item = items(i);
var relativePathParts = item.realpath.Split(baseDepth);
// Navigate through the tree structure
var currentLevel = pathTree;
for (var j = 0; j < relativePathParts.count; j++) {
var part = relativePathParts(j);
if (!currentLevel[part]) {
currentLevel[part] = { "_isDir": (j < relativePathParts.count - 1 || item.is_dir) };
}
currentLevel = currentLevel[part];
}
}
// Convert the tree structure to text
treeText = convertTreeToText(pathTree, "", "", filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, true);
return treeText;
}
function convertTreeToText(tree, folderTerminator, indent, filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, isRoot) {
var treeText = "";
var entries = [];
var dirs = [];
var files = [];
for (var key in tree) {
if (tree.hasOwnProperty(key) && key !== "_isDir") {
if (tree[key]["_isDir"]) {
dirs.push(key);
} else {
files.push(key);
}
}
}
// Sort files and directories as needed
if (filesFirst) {
entries = files.concat(dirs);
} else {
entries = dirs.concat(files);
}
for (var i = 0; i < entries.length; i++) {
var key = entries[i];
var isDir = tree[key]["_isDir"];
var isLastEntry = (i === entries.length - 1);
var isLastFile = (i === files.length - 1 && filesFirst && i < files.length);
var line = indent + (isDir ? (isLastEntry ? endFolderBranch : middleFolderBranch) : (isLastEntry ? endFileBranch : (isLastFile ? lastFileBranch : middleFileBranch))) + (isDir ? folderPrefix + key + folderSuffix : key) + "\n";
treeText += line;
if (isDir) {
var subTreeText = convertTreeToText(tree[key], folderTerminator, indent + (isLastEntry ? " " : verticalBranch + " "), filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, false);
treeText += subTreeText;
// Check if current directory is empty
var isCurrentDirEmpty = subTreeText.replace(/^\s+|\s+$/g, '').length === 0;
// Only add spacing if it's not the last entry
if (!isLastEntry) {
// Get the next entry
var nextEntry = entries[i + 1];
// Check if the next entry is a directory
if (tree[nextEntry] && tree[nextEntry]["_isDir"]) {
var nextSubTreeText = convertTreeToText(tree[nextEntry], folderTerminator, "", filesFirst, middleFileBranch, endFileBranch, middleFolderBranch, endFolderBranch, verticalBranch, folderPrefix, folderSuffix, lastFileBranch, false);
var isNextDirEmpty = nextSubTreeText.replace(/^\s+|\s+$/g, '').length === 0;
// Add spacer line if:
// 1. Current directory is not empty, OR
// 2. Current directory is empty but the next one is not
if (!isCurrentDirEmpty || (isCurrentDirEmpty && !isNextDirEmpty)) {
treeText += indent + verticalBranch + "\n";
}
} else {
// If next entry is not a directory, always add a spacer
treeText += indent + verticalBranch + "\n";
}
}
}
// Add a vertical line after the last file and before the first folder
if (isLastFile && filesFirst && dirs.length > 0) {
treeText += indent + verticalBranch + "\n";
}
}
return treeText;
}
Changes:
- 1.0.2: Added
discontinuousBranchForLastFile
for further advanced appearance customization. Also fixed line spacing for a special case for completely empty folders.