FAYT script : Dynamic Tab Groups

Dynamic Tab Groups

Release history

V1.2 : Fixed typo in textcolor value
FAYT Dynamic TabGroup.opusscriptinstall (9.6 KB)

V1.1 : Initial release
FAYT Dynamic TabGroup.opusscriptinstall (9.6 KB)

Main concept

The main usage is when you need to use TabGroups for specific tasks, but only needs them temporarily.
This FAYT script will remember which tabs were opened when one group was loaded and will allow to close them all at a later stage.
The script introduces some commands that you can access through the FAYT interface (and is inspired by one of the examples given in this section of the forum).

Available commands

Once you enter the FAYT through its shortcut key ($ by default) you can either :

  • open / append : lists all the TabGroups from your settings to let you choose which one to open (just after the active tabs) or append (at the end of all opened tabs).

    • Each TabGroup is presented alongside information about it : wether it's a single or dual pane (single = ▬ / dual = ▬ ▬), and the number of tabs (plus their up/left and down/right location when dual pane tab group : ◤ 4 tabs | ◢ 2 tabs)
    • Single pane tab groups are opened on the active pane / Dual pane tab groups are opened according to your settings for this tab group
    • Tab groups are always opened without closing other tabs even if it configured to do so, since it goes against the concept of this FAYT script.
  • close : lists all opened groups and let you choose which group you want to close tabs. Tabs are closed based on the tab group they were loaded from, even if you changed its location afterwards.

    • Example shows that two groups have been opened (from tabs names and from the list of options presented in FAYT field) and you can choose which one to close (giving you information about how many tabs will be closed in each pane)
  • make permanent : Lets you choose one group that you don't want to close by mistake. It becomes "permanent" in the lister and won't be presented in the close list.

  • help : displays a window explaining each command

  • forget all : same as make permanent but for all opened tab groups

  • rebase group ids : see help.

Configuration

4 parameters can be modified in the script setup (access from Script Manager) :
image

  • ID ordering mode : see help
  • Log to console : will display some debug option in console if set to Yes
  • Suggestion retention delay : to improve performance, each command's suggestion menu is not rebuilt unless this delay has passed since last display of a suggestion.
  • Tabs naming scheme : lets you define the way the tabs opened with open or append command are renamed. You can use %id in the name to make the group id appear in the tab name. Other available identifiers from GO TABNAME (%N, %R, %P, %!) can also be used.

Quick keys configuration

image

  • You can there enable an option to use aliases instead of full commands (by default the alias is the first letter of the command).

Source code


// Adds a FAYT Quick Key which contains 4 commands :
// * open: 		lists all the TabGroups declared within Opus so you can decide to add one.
//				Note that the group will be opened without closing other tabs, even if it'the group settings.
//
// * close: 	lists for the current lister, all groups that have been opened prevously with the add command
//				that still have tabs opeened in at least one of the pane.
//				Executing the command will close these tabs.
//				/!\ Tabs are deleted regardless of the current folder they are 
//
// * rebase Group IDs: 	Each time a tab group is added by open command, it is assigned an ID (starting at 1 for frst group, incrementing by 1).
//						Depending on the configuration of the script, ID management will either :
//						  - Always increment [incremental setting], even when groups are closed
//						  - Use ID equals to highest ID in use + 1 [optimized setting]
//						  - Use the smallest possible ID, filling the gaps left after closing groups, if any [extreme setting].
//						This command can be usefull when you're using the <b>incremental</b> setting and want to reset the current ID count to 'highest in use + 1' and restart from there for future groups.
//
// * make permanent: 	lists all current inserted groups. Executing the command will remove markers for that group so it can no longer be removed
//						by this script. It's made permanent (unless you decide to close the tabs by yourself).
//
// * forget All : 	Similar to making everything permanent. It will also reinitialize the group IDs to 1.
//
// * help : Will open a popup with the help for the different commands.
// ---

// (?:^| |\(|\[|\/|\\)([a-zA-ZäöüßÄÖÜ])



function OnInit(initData) {
	initData.name = "FAYT Dynamic TabGroups";
	initData.version = "1.1";
	initData.copyright = "(c) 2024 Stéphane Alessio";
	initData.desc = "";
	initData.default_enable = true;
	initData.min_version = "13.10";
	initData.group = "FAYT Scripts";

	// settings & defaults
	initData.config_desc = DOpus.Create.Map();
	var option_name = "";

	option_name = "Tabs naming scheme";
	initData.Config[option_name] = "⬛%id▶ %N";
	initData.config_desc(option_name) = "Use '%id' where you want the group ID to be inserted. Other options (%N, %R, %P, %!) are described in Opus manual (GO TABNAME)";
	// use with Script.Config["Tabs naming scheme"]

	option_name = "Suggestions retention delay";
	initData.Config[option_name] = 4000;
	initData.config_desc(option_name) = "Represents the time in ms between two requests for suggestions under which the suggestion list is not rebuilt";

	option_name = "ID ordering mode";
	initData.Config[option_name] = "incremental";
	initData.config_desc(option_name) = "Defines the ordering mode of TabGroups identifiers.\n - incremental: new id is always latest + 1.\n - optimized: new id is highest still opened + 1.\n - extreme: new id is smallest available (filling gaps if any).";

	option_name = "Log to console";
	initData.Config[option_name] = false;
	initData.config_desc(option_name) = "true to have some logs to console. false to make it mute.";
}


///////////////////////////////////////////////////////////////////////////
// Define these values so our script is easier to read.
var FLAG_UseAliasInsteadOfCommand = (1<<0);
// var FLAG_OptB = (1<<1);
// var FLAG_OptC = (1<<2);
// ...


///////////////////////////////////////////////////////////////////////////
// Generic FAYTcommand class
function FAYTcommand(verb) {
	this.verb = verb;
	this.alias = verb.substring(0,1).toLowerCase();
	this.GetCommand = function(scriptFAYTData) { return ( (scriptFAYTData.flags & FLAG_UseAliasInsteadOfCommand)?this.alias:this.verb ); }
	this.lastSuggestionBuildTime = new Date(0);
	this.suggestionRetentionTime = 4000;
	this.latestSuggestions = DOpus.Create.Map();
	this.BuildSuggestions = function(scriptFAYTData) { this.latestSuggestions = null; return this.latestSuggestions; };
	this.ExecuteAction = function(scriptFAYTData) { return null; }
	this.MatchesInput = function(scriptFAYTData) { 
		return this.GetCommand(scriptFAYTData).match(new RegExp('^' + scriptFAYTData.cmdline.substring(0, this.GetCommand(scriptFAYTData).length), "i"));
	}
	this.CheckIfRebuildNeeded = function () {
		var curDate = new Date();

		var rebuildTimeMsg = new Array();
		rebuildTimeMsg.push("[" + this.verb.toUpperCase() + " menu] ");
		rebuildTimeMsg.push(((curDate.valueOf() - this.lastSuggestionBuildTime.valueOf()) < this.suggestionRetentionTime)?"use previous suggestions":"REBUILD");
		rebuildTimeMsg.push("diff=" + (curDate.valueOf() - this.lastSuggestionBuildTime.valueOf()) + " vs. ref=" + this.suggestionRetentionTime);
		rebuildTimeMsg.push("curDate=" + curDate.valueOf() + " / lastDate=" + this.lastSuggestionBuildTime.valueOf());
		dout(rebuildTimeMsg.join(" | "));

		return (!(curDate.valueOf() - this.lastSuggestionBuildTime.valueOf()) < this.suggestionRetentionTime);
	}
}
///////////////////////////////////////////////////////////////////////////


// DOpus.Output("************************ Building actions list");
FAYTcommandsList = DOpus.Create.Map();

// OPEN
// =====
openCommand = new FAYTcommand("open");
openCommand.BuildSuggestions = BuildGroupsSuggestionsFromSettings;
openCommand.ExecuteAction = ExecuteOpenAction;
openCommand.GrpPaths = null;
openCommand.TabGroups = null;
FAYTcommandsList(openCommand.verb) = openCommand;


// APPEND
// =======
openAtEndCommand = new FAYTcommand("append");
openAtEndCommand.BuildSuggestions = BuildGroupsSuggestionsFromSettings;
openAtEndCommand.ExecuteAction = ExecuteOpenAction;
openAtEndCommand.GrpPaths = null;
openAtEndCommand.TabGroups = null;
FAYTcommandsList(openAtEndCommand.verb) = openAtEndCommand;

// CLOSE
// ======
closeCommand = new FAYTcommand("close");
closeCommand.BuildSuggestions = function(sfd) {
	if (!this.CheckIfRebuildNeeded()) {
		this.lastSuggestionBuildTime = new Date();
		return this.latestSuggestions;
	}

	return BuildGroupsSuggestionsFromTabVars(this, sfd);
}
closeCommand.ExecuteAction = ExecuteCloseAction;
closeCommand.GrpTabs = null;
FAYTcommandsList(closeCommand.verb) = closeCommand;

// HELP
// ====
helpCommand = new FAYTcommand("help");
helpCommand.alias = "help";
helpCommand.latestSuggestions("help") = "Displays help about Dynamic TabGroups commands.";
helpCommand.BuildSuggestions = function(sfd) { return this.latestSuggestions; };
helpCommand.ExecuteAction = ExecuteHelpAction;
FAYTcommandsList(helpCommand.verb) = helpCommand;


// REBASE GROUP IDS
// ================
rebaseGpIdsCommand = new FAYTcommand("rebase group ids");
rebaseGpIdsCommand.alias = "rebase group ids";
rebaseGpIdsCommand.latestSuggestions("rebase group ids") = "Optimizes for future groups the highest group id to 'highest in use'+1";
rebaseGpIdsCommand.BuildSuggestions = function(sfd) { return this.latestSuggestions; };
rebaseGpIdsCommand.ExecuteAction = function(sfd) {
	var seqVar;
	sfd.tab.CloseFAYT();
	var lister = sfd.tab.lister;
	if (!lister.vars.exists("DynGrpMaxSeq")) return;
	seqVar = lister.vars("DynGrpMaxSeq");
	dout("Current SeqIdValue = " + seqVar.value);
	var highestId = 0;
	for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		if (tab.vars.exists("DynGrpId")) {
			highestId = Math.max(highestId, tab.vars("DynGrpId"));
		}
	}
	seqVar.value = highestId;
	dout("Newly optimized seq id = " + seqVar.value);
};
FAYTcommandsList(rebaseGpIdsCommand.verb) = rebaseGpIdsCommand;


// MAKE PERMANENT
// ==============
makePermanentCommand = new FAYTcommand("make permanent");
makePermanentCommand.BuildSuggestions = function(sfd) {
	if (!this.CheckIfRebuildNeeded()) {
		this.lastSuggestionBuildTime = new Date();
		return this.latestSuggestions;
	}

	return BuildGroupsSuggestionsFromTabVars(this, sfd);
}
makePermanentCommand.ExecuteAction = ExecuteMakePermanentAction;
makePermanentCommand.GrpTabs = null;
FAYTcommandsList(makePermanentCommand.verb) = makePermanentCommand;


// FORGET ALL
// ==========
forgetAllCommand = new FAYTcommand("forget all");
forgetAllCommand.latestSuggestions("forget all") = "Make all currently opened dynamic groups permanent";
forgetAllCommand.BuildSuggestions = function(sfd) { return this.latestSuggestions; }
forgetAllCommand.ExecuteAction = ExecuteForgetAllAction;
FAYTcommandsList(forgetAllCommand.verb) = forgetAllCommand;


// === END OF COMMANDS DEFINITIONS ========================================================================

function OnAddCommands(addCmdData) {
	var cmd = addCmdData.AddCommand();
	cmd.name = "FAYTDynGroups";
	cmd.method = "OnFAYTDynGroups";
	cmd.desc = "";
	cmd.label = "FAYTDynGroups";
	cmd.template = "";
	cmd.hide = true; // Hide from button editor menus
	cmd.icon = "script";

	var fayt = cmd.fayt;
	fayt.enable = true;
	fayt.key = "$";
	fayt.textcolor = "##ff8000";
	fayt.backcolor = "#000000";
	fayt.label = "Dynamic Tab Groups";
	fayt.flags = DOpus.Create.Map();
	fayt.realtime = true; // Call on each keypress, not just after return
	fayt.wantempty = false;

	fayt.flags[FLAG_UseAliasInsteadOfCommand] = "Use alias instead of command";
	// fayt.flags[FLAG_OptB] = "Option B";
	// fayt.flags[FLAG_OptC] = "Option C";
	// ...

	var cmd = addCmdData.AddCommand();
	cmd.name = "FAYTDynGroups_ClearAll";
	cmd.method = "OnFAYTDynGroups_ClearAll";
	cmd.desc = "";
	cmd.label = "FAYTDynGroups_ClearAll";
	cmd.template = "";
	cmd.hide = false; // Hide from button editor menus
	cmd.icon = "script";
}

function OnFAYTDynGroups(scriptFAYTData) {
	dout("***IN*** '" + scriptFAYTData.cmdline + "' Flags=" + scriptFAYTData.flags + " Key=" + scriptFAYTData.key  + "(" + scriptFAYTData.key.length + ")" + " suggest=" + scriptFAYTData.suggest);

	var localSuggestions = DOpus.Create.Map();

	// dout("fayt = "+ scriptFAYTData.fayt);
	if (scriptFAYTData.fayt != "FAYTDynGroups")
	{
		dout('Unexpected FAYT: "' + scriptFAYTData.fayt + '"');
		scriptFAYTData.tab.CloseFAYT();
		return;
	}

	// Set suggestions retention delay for all actions
	SetRetentionDelayOnActions();

	// Build suggestion list from possible actions (all matching actions feed the suggestions list)
	for (var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext()) {
		// dout("Command <" + FAYTcommandsList(e.item()).verb + "> ==> GetCommand = <" + FAYTcommandsList(e.item()).GetCommand(scriptFAYTData) + ">");
		if (FAYTcommandsList(e.item()).MatchesInput(scriptFAYTData))
			localSuggestions.merge(FAYTcommandsList(e.item()).BuildSuggestions(scriptFAYTData));
	}

	var tab = scriptFAYTData.tab;

	if (scriptFAYTData.key == "return")
	{
		// Execute the action upon each FAYTcommand that provided this suggestion (should be only one)
		for (var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext())
			if (FAYTcommandsList(e.item()).latestSuggestions.exists(scriptFAYTData.cmdline))
				FAYTcommandsList(e.item()).ExecuteAction(scriptFAYTData);
		return;
	}

	if (scriptFAYTData.suggest)
	{
		dout("Suggesting " + ((localSuggestions!=null)?localSuggestions.count:0) + " results");
		tab.UpdateFAYTSuggestions(localSuggestions);
	}
}

function SetRetentionDelayOnActions() {
	for (var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext())
		FAYTcommandsList(e.item()).suggestionRetentionTime = Script.Config["Suggestions retention delay"];
}

function ExecuteOpenAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		// dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.latestSuggestions(name) + "'");
		dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.GrpPaths(name) + "'");
	else
		dout("Trying to execute on unknown group");

	// Checking call is made for the right command - Really necessary ???
	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	// if (!name.match(/^open /i)) {
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	dout("Request to open group '" + this.GrpPaths(name) + "' from " + this.verb + " command");

	// If open is called in "append" mode, we need to make the last tabs of each pane active (by selecting them)
	var cmd = DOpus.Create.Command();
	tab.lister.Update();
	var srcLastTab 	= null;
	var destLastTab = null;
	var activePane = "right"; 	// will be changed to left in the loop below if active tab is found in the lister.tabsleft
	if (this.verb == "append") {
		dout("Need to do special things to open tabs 'at the end'");
		var lastLeftTab = null;
		for (var e = new Enumerator(tab.lister.tabsleft); !e.atEnd(); e.moveNext()) {
			dout("Parsing left tabs => " + e.item().displayed_label);
			lastLeftTab = e.item();
			if (lastLeftTab == tab.lister.activetab) activePane = "left";
		}
		if (activePane == "left") srcLastTab = lastLeftTab;
		else destLastTab = lastLeftTab;

		var lastRightTab = null;
		for (var e = new Enumerator(tab.lister.tabsright); !e.atEnd(); e.moveNext()) {
			dout("Parsing right tabs => " + e.item().displayed_label);
			lastRightTab = e.item();
		}
		if (activePane == "right") srcLastTab = lastRightTab;
		else destLastTab = lastRightTab;


		// We need to 'select' (e.g. make active) the last tab on src & dest 
		// ... but do it only if the tabgroup we're about to open has tabs to open there
		var tabGroup = this.TabGroups(name);
		var opensTabsInSrc 	= true;		// by default - will be true for all "non-dual" tabgroups.
		var opensTabsInDest = false;	// by default - will be false for all "non-dual" tabgroups.
		if (tabGroup.dual) {
			opensTabsInSrc = (activePane == "left")?(tabGroup.lefttabs.count > 0):(tabGroup.righttabs.count > 0);
			opensTabsInDest = (activePane == "left")?(tabGroup.righttabs.count > 0):(tabGroup.lefttabs.count > 0);
		}
		
		dout("opensTabsInSrc = " + opensTabsInSrc + " | opensTabsInDest = " + opensTabsInDest);

		if (tab.lister.dual != 0 && opensTabsInDest)
			cmd.RunCommand("Go TABSELECT=$" + destLastTab);
		if (opensTabsInSrc)
			cmd.RunCommand("Go TABSELECT=$" + srcLastTab);
	}

	// Loading the selected TabGroup
	cmd.AddLine('GO TABGROUPLOAD "' + this.GrpPaths(name) + '" TABCLOSEALL=no');
	var r = cmd.Run();
	// dout("Execution status = " + r);
	// dout("Cmd.results.newtabs");

	// Getting the newly opened tabs to rename them and set the tab vars to identofy them later (other commands such as close, etc ...)
	var openedTabs = DOpus.Create.Vector();
	var seqId = GetNextSequenceId(tab.lister);

	cmd.Clear();

	// Setting tab vars
	for (var e = new Enumerator(cmd.results.newtabs); !e.atEnd(); e.moveNext()) {
		var newTab = e.item();
		dout("  New Tab => " + newTab.displayed_label + " >> " + newTab.path);
		newTab.vars.Set("DynGrpId", seqId);
		newTab.vars("DynGrpId").persist = true;
		newTab.vars.Set("DynGrpName", this.GrpPaths(name));
		newTab.vars("DynGrpName").persist = true;
		openedTabs.push_back(newTab);
	}

	tab.lister.Update();
	var activeTab = tab.lister.activetab;
	dout("== Active tab is on the " + ((activeTab.right)?"Right/Bottom pane":"Left/Up pane"));
	var currentPane = (activeTab.right)?"right":"left";

	// Renaming (if a pattern has been set by the user in the script config)
	var newTabName = Script.Config["Tabs naming scheme"].replace(/%id/, seqId);
	dout("New tab name : " + newTabName);
	if (newTabName != "") {
		for (var e = new Enumerator(openedTabs); !e.atEnd(); e.moveNext()) {
			var newTab = e.item();
			dout("Tab " + newTab.displayed_label + " is on " + ((newTab.right)?"Right/Bottom pane":"Left/Up pane"));
			cmd.Clear();

			cmd.SetSourceTab(newTab);
			cmd.AddLine('GO TABNAME="' + newTabName + '"');
			var res = cmd.Run();
			// dout("Command status (>0 expected) = " + res + " | " + cmd.results.result);
		}
	}

	// dout("End of open execute action.");
	return;
}

function ExecuteCloseAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		// dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.latestSuggestions(name) + "'");
		dout("[" + this.verb + "] Execute on : '" + name + "' '>> # tabs = '" + this.GrpTabs(name).count + "'");
	else
		dout("Trying to execute on unknown group");

	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	dout("Request to close group with " + this.GrpTabs(name).count + " tabs.");

	var cmd = DOpus.Create.Command();
	for (var e = new Enumerator(this.GrpTabs(name)); !e.atEnd(); e.moveNext()) {
		dout("Trying to close tab '" + e.item().displayed_label + "'");
		cmd.AddLine("GO TABCLOSE=" + e.item());
	}
	cmd.Run();

	return;
}

function ExecuteMakePermanentAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		// dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.latestSuggestions(name) + "'");
		dout("[" + this.verb + "] Execute on : '" + name + "' '>> # tabs = '" + this.GrpTabs(name).count + "'");
	else
		dout("Trying to execute on unknown group");

	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	dout("Request to make permanent group with " + this.GrpTabs(name).count + " tabs.");

	var id = -1;
	for (var e = new Enumerator(this.GrpTabs(name)); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		dout("Trying to make permanent tab '" + tab.displayed_label + "'");
		if (tab.vars.Exists("DynGrpId")) tab.vars.Delete("DynGrpId");
		if (tab.vars.Exists("DynGrpName")) tab.vars.Delete("DynGrpName");	
	}

	return;
}

function ExecuteForgetAllAction(scriptFAYTData) {
	var name = scriptFAYTData.cmdline;
	var tab = scriptFAYTData.tab;

	if (this.latestSuggestions.exists(scriptFAYTData.cmdline))
		// dout("[" + this.verb + "] Execute on : '" + name + "' '>> Group Name : '" + this.latestSuggestions(name) + "'");
		dout("[" + this.verb + "] Executing.");
	else
		dout("Trying to execute on unknown group");

	var regex = new RegExp("^" + this.GetCommand(scriptFAYTData) + " ", "i");
	if (!name.match(regex)) {
		dout("Unknown action : " + name);
		return;
	}

	tab.CloseFAYT();
	dout("Request to forget all.");

	var id = -1;
	for (var e = new Enumerator(scriptFAYTData.tab.lister.tabs); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		dout("Trying to make permanent tab '" + tab.displayed_label + "'");
		if (tab.vars.Exists("DynGrpId")) tab.vars.Delete("DynGrpId");
		if (tab.vars.Exists("DynGrpName")) tab.vars.Delete("DynGrpName");	
	}

	return;
}

function ExecuteHelpAction(scriptFAYTData) {
	var tab = scriptFAYTData.tab;
	tab.CloseFAYT();

	var Dlg = DOpus.Dlg;
	Dlg.window = tab;
	Dlg.template = "helpDlg";
	Dlg.Show();
}

function BuildGroupsSuggestionsFromSettings(scriptFAYTData) {
	if (!this.CheckIfRebuildNeeded()) {
		this.lastSuggestionBuildTime = new Date();
		return this.latestSuggestions;
	}

	this.latestSuggestions = DOpus.Create.Map();
	this.GrpPaths = DOpus.Create.Map();
	this.TabGroups = DOpus.Create.Map();

	// Parse TabGroups
	// Analyzing tabgroups
	var allTabGroups = GetGroupsFromSettings(DOpus.TabGroups);
	// dout("Total # of tabGroups : " + allTabGroups.count);

	for (var e = new Enumerator(allTabGroups); !e.atEnd(); e.moveNext()) {
		var grpPath = e.item();
		// dout("grpPath = " + grpPath);
		var grp = allTabGroups(grpPath);
		var hint = grpPath;
		// dout("padding after with " + (maxGroupLength + 6 - grpPath.length) + " spaces");
		hint += "       ";
		if (grp.desc != "")
			hint += "  { " + grp.desc + " }       ";


		if (grp.dual) {
			// Manage extra desc for dual listers
			var leftCount = 0;
			if (grp.lefttabs.count != undefined && grp.lefttabs.count > 0)		leftCount = grp.lefttabs.count;
			var rightCount = 0;
			if (grp.righttabs.count != undefined && grp.righttabs.count > 0) 	rightCount = grp.righttabs.count;

			hint += "▬▬  ";
			hint += "◤ " + ((leftCount > 0) ? leftCount : "no") 	+ " tab" + ((leftCount > 1) ? "s" : "") + " | ";
			hint += "◢ " + ((rightCount > 0) ? rightCount : "no") 	+ " tab" + ((rightCount > 1) ? "s" : "");
		}
		else {
			hint += "▬  " + grp.tabs.count + " tab" + ((grp.tabs.count > 1) ? "s" : "");
		}
		// dout("hint => " + hint);

		// this.latestSuggestions("open " + GetExtendedInitials(initials, this.latestSuggestions)) = e.item();
		var initials = GetInitials(grpPath);
		var grpCommand = this.GetCommand(scriptFAYTData) + " " + GetExtendedInitials(this, initials, scriptFAYTData);
		this.latestSuggestions(grpCommand) = hint;

		this.GrpPaths(grpCommand) = grpPath;
		this.TabGroups(grpCommand) = grp;
	}

	this.lastSuggestionBuildTime = new Date();
	return this.latestSuggestions;
}

function BuildGroupsSuggestionsFromTabVars(faytCommand, scriptFAYTData) {
	if (!faytCommand.CheckIfRebuildNeeded()) {
		faytCommand.lastSuggestionBuildTime = new Date();
		return faytCommand.latestSuggestions;
	}

	faytCommand.latestSuggestions = DOpus.Create.Map();
	faytCommand.GrpTabs = DOpus.Create.Map();

	// Find all tabs marked as "in a dynamic group" (based on tab vars) both in leftabs and in righttabs
	var allTabs = GetAllMarkedTabs(scriptFAYTData.tab.lister);

	// Enumerate all keys to build suggestions & GrpTabs object.
	for (var e = new Enumerator(allTabs); !e.atEnd(); e.moveNext()) {
		var key = e.item();
		var tabs = allTabs(e.item());

		var grpCommand = "";

		if (tabs.exists("left")) {
			var id = tabs("left").id;
			var grpName = tabs("left").name;

			var hint = "[Grp:" + id + "] " + grpName + "    ";
			if (scriptFAYTData.tab.lister.dual == 0)		// Single Display
				hint += "[ ▬  " + tabs("left").list.count + " tab" + ((tabs("left").list.count>1)?"s ]":" ]");
			else if (scriptFAYTData.tab.lister.dual == 1)	// Dual Display - Vertical Layout
				hint += "[ ◀ " + tabs("left").list.count + " tab" + ((tabs("left").list.count>1)?"s ":" ");		//  ▶
			else if (scriptFAYTData.tab.lister.dual == 2)	// Dual Display - Horizontal Layout
				hint += "[ ▲ " + tabs("left").list.count + " tab" + ((tabs("left").list.count>1)?"s ":" ");		//  ▼

			var initials = GetInitials(tabs("left").id + " - " + tabs("left").name);
			grpCommand = faytCommand.GetCommand(scriptFAYTData) + " " + GetExtendedInitials(faytCommand, initials, scriptFAYTData);
			faytCommand.latestSuggestions(grpCommand) = hint;
			faytCommand.GrpTabs(grpCommand) = tabs("left").list;
		}

		if (tabs.exists("right")) {
			var id = tabs("right").id;
			var grpName = tabs("right").name;

			var hint = "";
			var newTabs = null;
			if (grpCommand == "") {	// There were no 'left/up' tabs for this group
				var initials = GetInitials(tabs("right").id + " - " + tabs("right").name);
				grpCommand = faytCommand.GetCommand(scriptFAYTData) + " " + GetExtendedInitials(faytCommand, initials, scriptFAYTData);
				hint = "[Grp:" + id + "] " + grpName + "    [ ";
				oldTabs = DOpus.Create.Vector();
			}
			else {
				hint = faytCommand.latestSuggestions(grpCommand) + " | ";
				oldTabs = faytCommand.GrpTabs(grpCommand);
			}

			if (scriptFAYTData.tab.lister.dual == 1)	// Dual Display - Vertical Layout
				hint += "▶ " + tabs("right").list.count + " tab" + ((tabs("right").list.count>1)?"s ":" ");		//  ▶
			else if (scriptFAYTData.tab.lister.dual == 2)	// Dual Display - Horizontal Layout
				hint += "▼ " + tabs("right").list.count + " tab" + ((tabs("right").list.count>1)?"s ":" ");		//  ▼

			oldTabs.append(tabs("right").list);
			faytCommand.GrpTabs(grpCommand) = oldTabs;
		}

		if (scriptFAYTData.tab.lister.dual >= 1)
			hint += "]";
		faytCommand.latestSuggestions(grpCommand) = hint;
	}

	faytCommand.lastSuggestionBuildTime = new Date();
	return faytCommand.latestSuggestions;
}

function GetAllMarkedTabs(lister) {
	var allTabs = DOpus.Create.Map();
	lister.Update();	// refresh with new tabs, deleted tabs, etc ...
	for (var e = new Enumerator(lister.tabsleft); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		// dout("working on tab '" + tab.displayed_label + "'");
		if (tab.vars.exists("DynGrpId")) {
			if (tab.vars.exists("DynGrpName")) {
				var key = tab.vars("DynGrpId") + "_" + tab.vars("DynGrpName");
				if (!allTabs.exists(key)) {
					allTabs(key) = DOpus.Create.Map();
					allTabs(key)("left") = {"id": tab.vars("DynGrpId"), "name": tab.vars("DynGrpName"), "list": DOpus.Create.Vector() };
				}
				allTabs(key)("left").list.push_back(tab);
			}
			else {
				dout("ERROR : tab has a group Id (" + tab.vars("DynGrpId") + ") but no group Name");
				var key = tab.vars("DynGrpId") + "_[Uknown]";
				if (!allTabs.exists(key)) {
					allTabs(key) = DOpus.Create.Map();
					allTabs(key)("left") = {"id": tab.vars("DynGrpId"), "name": "[Unknown]", "list": DOpus.Create.Vector() };
				}
				allTabs(key)("left").list.push_back(tab);
			}
		}
	}
	for (var e = new Enumerator(lister.tabsright); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		if (tab.vars.exists("DynGrpId")) {
			if (tab.vars.exists("DynGrpName")) {
				var key = tab.vars("DynGrpId") + "_" + tab.vars("DynGrpName");
				if (!allTabs.exists(key))
					allTabs(key) = DOpus.Create.Map();
				if (!allTabs(key).exists("right"))
					allTabs(key)("right") = { "id": tab.vars("DynGrpId"), "name": tab.vars("DynGrpName"), "list": DOpus.Create.Vector() };
				allTabs(key)("right").list.push_back(tab);
			}
			else {
				dout("ERROR : tab has a group Id (" + tab.vars("DynGrpId") + ") but no group Name");
				var key = tab.vars("DynGrpId") + "_[Uknown]";
				if (!allTabs.exists(key))
					allTabs(key) = DOpus.Create.Map();
				if (!allTabs(key).exists("right"))
					allTabs(key)("right") = { "id": tab.vars("DynGrpId"), "name": "[Unknown]", "list": DOpus.Create.Vector() };
				allTabs(key)("right").list.push_back(tab);
			}
		}
	}

	return allTabs;	
}

function GetExtendedInitials(faytCommand, initials, scriptFAYTData) {
	// dout("GetExtendedInitials for '" + initials + "'");
	if (!faytCommand.latestSuggestions.exists(faytCommand.GetCommand(scriptFAYTData) + " " + initials)) return initials;
	// dout("Already exists");
	var i=1;
	while (faytCommand.latestSuggestions.exists(faytCommand.GetCommand(scriptFAYTData) + " " + initials + "_" + i)) i++;
	// dout("Requires renaming to '" + initials + "_" + i + "'");
	return initials + "_" + i;
}

function GetGroupsFromSettings(tabGroupList, path) {
	var rootPath = path || "";
	// dout("Path = '" + rootPath + "' | Input tabGroupList #" + tabGroupList.count + " items");
	var m = DOpus.Create.Map();
	for (var e = new Enumerator(tabGroupList); !e.atEnd(); e.moveNext()) {
		var grp = e.item();
		if (grp.hidden) continue;
		// dout("GetGroupsFromSettings : Processing : " + grp.name);
		if (grp.folder)
			m.merge(GetGroupsFromSettings(grp, rootPath + grp.name + "/"));
		else
			m(rootPath + grp.name) = grp;
	}
	// dout("Ending GetGroupsFromSettings for path=" + rootPath);
	return m;
}

function GetInitials(s) {
	// dout("GetInitials: >" + s + "<");
	var reFolders = new RegExp("/([a-zA-Z])(?=.*?/)","g");
	var regexp = /(?:^| |\(|\[|\/|\\|-)([0-9a-zA-ZäöüßÄÖÜ])/g;

	var out = "";
	// First Letter of each folder in path
	var foldersFirstLetters = ("/"+s).match(reFolders);
	if (foldersFirstLetters) {
		// dout("folder match");
		for (var i = 0; i < foldersFirstLetters.length; i++) {
			// dout("adding:" + foldersFirstLetters[i]);
			out += foldersFirstLetters[i].replace('/', '');
		}
	}
	// dout("Folders 'id' = " + out);

	// Then get first letter of each word
	var r = GetLastPathPart(s).match(regexp);
	for (var i = 0; i < r.length; i++)
		out += r[i].replace(/ |\(|\[|\\/, '');

	// dout ("output=" + out.toUpperCase());
	return out.toUpperCase();
}

function GetNextSequenceId(lister) {
	// Get ID ordering mode to choose proper action
	var orderingMode = Script.Config["ID ordering mode"].toLowerCase();
	if (!arrayIncludes([ "incremental", "optimized", "extreme" ], orderingMode))
		orderingMode = "incremental";		// default mode when non valid mode in settings.

	// Read or create DynGrpMaxSeq variable on lister
	var seqVar;
	if (lister.vars.exists("DynGrpMaxSeq")) {
		// dout("Sequence exists");
		seqVar = lister.vars("DynGrpMaxSeq");
		dout("Seq Value found = " + seqVar.value);
	}
	else {
		lister.vars.Set("DynGrpMaxSeq", 0);
		dout("Sequence exists after adding ? = " + lister.vars.exists("DynGrpMaxSeq"));
		seqVar = lister.vars("DynGrpMaxSeq");
		seqVar.persist = true;
	}

	if (orderingMode == "incremental") {
		seqVar.value = seqVar.value + 1;
		dout("Lister seq id returned = " + seqVar.value);
		return seqVar.value;
	}
	else if (orderingMode == "optimized") {
		// Search highest used id
		var highestId = 0;
		for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
			var tab = e.item();
			if (tab.vars.exists("DynGrpId")) {
				highestId = Math.max(highestId, tab.vars("DynGrpId"));
			}
		}
		seqVar.value = highestId + 1;
		return seqVar.value;
	}
	else {	// orderingMode == "extreme"
		// var highestId = 0;
		var grpIds = DOpus.Create.Map();
		for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
			var tab = e.item();
			if (tab.vars.exists("DynGrpId")) {
				grpIds(tab.vars("DynGrpId")) = tab.vars("DynGrpId");
				// highestId = Math.max(highestId, tab.vars("DynGrpId"));
			}
		}

		// Setting  sequenceId in lister in case we then revert to another mode.
		seqVar.value = seqVar.value + 1;

		// Now, searching first available id
		var i = 1;
		while (grpIds.exists(""+i)) i++;
		dout("index " + i + " is available. Returning index.");
		return i;	
	}
}

function GetLastPathPart(s) {
	s = s + "";
	while (s.length > 0 && s.slice(-1) == "/")
	{
		s = s.slice(0,-1);
	}
	var slashPos = s.lastIndexOf("/");
	if (slashPos != -1)
	{
		s = s.slice(slashPos+1);
	}
	return s;
}


///////////////////////////////////////////////////////////////////////////
function arrayIncludes(arr, searchElement) {
	for (var i = 0; i < arr.length; i++)
		if ("" + arr[i] === "" + searchElement)
			return true;
	return false;
}



///////////////////////////////////////////////////////////////////////////
// Command to clear all variables in Lister and in tabs
// ... everything is forgottent ...
function OnFAYTDynGroups_ClearAll(scriptCmdData) {
	for(var e = new Enumerator(FAYTcommandsList); !e.atEnd(); e.moveNext()) {
		e.item().latestSuggestions = null;
		e.item().lastSuggestionBuildTime = 0;
	}

	var lister = scriptCmdData.func.sourcetab.lister;
	if (lister.vars.exists("DynGrpMaxSeq"))  lister.vars.Delete("DynGrpMaxSeq");
	for (var e = new Enumerator(lister.tabs); !e.atEnd(); e.moveNext()) {
		var tab = e.item();
		if (tab.vars.exists("DynGrpId")) tab.vars.Delete("DynGrpId");
		if (tab.vars.exists("DynGrpName")) tab.vars.Delete("DynGrpName");
	}
 }


///////////////////////////////////////////////////////////////////////////
if (!String.prototype.padStart) {
	String.prototype.padStart = function padStart(targetLength,padString) {
        targetLength = targetLength>>0; //truncate if number or convert non-number to 0;
        // dout("in padstart / targetLength=" + targetLength);
        padString = String((typeof padString !== 'undefined' ? padString : ' '));
        if (this.length > targetLength) {
            return String(this);
        }
        else {
            targetLength = targetLength-this.length;
            if (targetLength > padString.length) {
                padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
            }
            return padString.slice(0,targetLength) + String(this);
        }
    };
}


///////////////////////////////////////////////////////////////////////////
if (!String.prototype.repeat) {
	String.prototype.repeat = function(count) {
	    'use strict';
	    if (this == null)
			throw new TypeError('can\'t convert ' + this + ' to object');

	    var str = '' + this;
	    count = +count;
	    if (count != count)
			count = 0;

	    if (count < 0)
			throw new RangeError('repeat count must be non-negative');

	    if (count == Infinity)
			throw new RangeError('repeat count must be less than infinity');

	    count = Math.floor(count);
	    if (str.length == 0 || count == 0)
			return '';

	    // Ensuring count is a 31-bit integer allows us to heavily optimize the
	    // main part. But anyway, most current (August 2014) browsers can't handle
	    // strings 1 << 28 chars or longer, so:
	    if (str.length * count >= 1 << 28)
			throw new RangeError('repeat count must not overflow maximum string size');

	    var maxCount = str.length * count;
	    count = Math.floor(Math.log(count) / Math.log(2));
	    while (count) {
	       str += str;
	       count--;
	    }
	    str += str.substring(0, maxCount - str.length);
	    return str;
	};
}


///////////////////////////////////////////////////////////////////////////
// Helper dout function
function dout(msg, error, time) {
	if (error == undefined) error = false;
	if (time == undefined) time = true;
	// DOpus.Output("msg=" + msg);
	// var mode = Script.Config["ID ordering mode"];
	var logToConsole = Script.Config["Log to console"];
	
	if (logToConsole) DOpus.output(msg, error, time);
}


==SCRIPT RESOURCES
<resources>
	<resource name="helpDlg" type="dialog">
		<dialog height="316" lang="francais" resize="yes" title="Dynamic Groups FAYT help" width="580">
			<control close="0" default="yes" height="14" name="btClose" resize="xy" title="Close" type="button" width="62" x="510" y="296" />
			<control height="284" name="mupText1" resize="wh" scroll="yes" title="&lt;b&gt;&lt;#347deb&gt;open [o] / append [a]&lt;/#&gt;&lt;/b&gt;\nLists all the TabGroups declared within Opus so you can decide which one to add.\nExecuting the command will open the different tabs from the tab group (after active tabs with &lt;b&gt;&lt;#347deb&gt;open&lt;/#&gt;&lt;/b&gt;, at the end of all tabs with &lt;b&gt;&lt;#347deb&gt;append&lt;/#&gt;&lt;/b&gt;).\nIn a dual pane lister: if the selected tab group is a single pane group, tabs are opened in the active pane.\nIn a single pane lister: if the selected tab group is a dual pane group, lister is converted to dual pane, and tabs are opened as configured.\nNote that the group will be opened without closing other tabs, even if it&apos;s configured to do so.\n\n\n&lt;b&gt;&lt;#347deb&gt;close [c]&lt;/#&gt;&lt;/b&gt;\nLists all groups (for the current lister) that have been prevously opened with the &lt;b&gt;&lt;#347deb&gt;open&lt;/#&gt;&lt;/b&gt; command that still have tabs opened in at least one of the panes.\nExecuting the command will close these tabs.\n&lt;u&gt;/!\&lt;/u&gt;Tabs are closed regardless of the current folder they might now represent (e.g. even if it has changed since opening).\n\n\n&lt;b&gt;&lt;#347deb&gt;rebase group ids&lt;/#&gt;&lt;/b&gt;\nEach time a tab group is added by &lt;b&gt;&lt;#347deb&gt;open&lt;/#&gt;&lt;/b&gt; command, it is assigned an ID (starting at 1 for frst group, incrementing by 1).\nDepending on the configuration of the script, ID management will either :\n - Always increment [&lt;b&gt;incremental&lt;/b&gt; setting], even when groups are closed\n - Use ID equals to highest ID in use + 1 [&lt;b&gt;optimized&lt;/b&gt; setting]\n - Use the smallest possible ID, filling the gaps left after closing groups, if any [&lt;b&gt;extreme&lt;/b&gt; setting].\nThis command can be usefull when you&apos;re using the &lt;b&gt;incremental&lt;/b&gt; setting and want to reset the current ID count to &apos;highest in use + 1&apos; and restart from there for future groups.\n\n\n&lt;b&gt;&lt;#347deb&gt;make permanent [m]&lt;/#&gt;&lt;/b&gt;\nLists all current inserted groups.\nExecuting the command will remove markers for that group so it can no longer be removed by this script. It&apos;s like it&apos;s been made permanent (unless you decide to close the tabs by yourself).\n\n\n&lt;b&gt;&lt;#347deb&gt;forget all [f]&lt;/#&gt;&lt;/b&gt;\nSimilar to making everything permanent. It will also reinitialize the group IDs to 1.\n\n\n&lt;b&gt;&lt;#347deb&gt;help&lt;/#&gt;&lt;/b&gt;\nOpens this help window." type="markuptext" width="564" x="8" y="8" />
		</dialog>
	</resource>
</resources>
5 Likes

I am referring to code line numbers 212 and 213.

I tried to change the Find as you type background color to #FFFFFF but it didn't do anything.
The FAYT background color after entering $ is still #000000 black.
Am I looking in the wrong place ?

Is the double # syntax for the font color a typographical error ?

It works fine as it is though. I only need to type a couple blind characters of 'open' before I get the drop down menu. Then all is well.

Thankyou for posting this ! :slight_smile:
I know this code was quite an effort to do.

1 Like

Those define the default colors assigned to the script when first installed, but if you want to edit the colors in your config, it's done through Preferences / Filtering and Sorting / Quick Keys, like with the built-in FAYT modes.

1 Like

Thanks for the feedback.
I fixed the typo in the textcolor line.
V1.2 below :
FAYT Dynamic TabGroup.opusscriptinstall (9.6 KB)

Regarding the number of characters to enter before having the open suggestions, one is enough on my end.
If you want quicker access, you can toggle on the use of alias :

That way, open command will appear upon entering "o+Space" (add will go for "a+Space", and son on ...).

@Leo @PassThePeas
Thanks !