﻿// TabRescue
// (c) 2025 Cris

// This is a script for Directory Opus.
// See https://www.gpsoft.com.au/endpoints/redirect.php?page=scripts for development information.

// Called by Directory Opus to initialize the script
function OnInit(initData) {
	initData.name = script_name;
	initData.version = script_version;
	initData.copyright = '(c) 2025 Cris';
	initData.url = 'https://resource.dopus.com/t/tabrescue-quickly-restore-your-closed-tabs/55768';
	initData.desc = 'Quickly restore your recently closed tabs';
	initData.default_enable = true;
	initData.min_version = '13.15.2';
	initData.config = DOpus.Create().OrderedMap();
	initData.config_desc = DOpus.Create().OrderedMap();
	initData.config_groups = DOpus.Create().OrderedMap();
	initData.config_group_order = DOpus.NewVector('General', 'Go qualifiers', 'Advanced');
	var o = DOpus.NewVector(DOpus.strings.Get('config_label_none'), DOpus.strings.Get('config_label_new_tab'), DOpus.strings.Get('config_label_new_tab_find'), DOpus.strings.Get('config_label_open_dual'),
		DOpus.strings.Get('config_label_open_dual_tab'), DOpus.strings.Get('config_label_new_lister'), DOpus.strings.Get('config_label_new_lister_dual'));
	AddConfig('max tabs per Lister', 15, DOpus.strings.Get('config_max_tabs'), 'General');
	AddConfig('log level', DOpus.NewVector(2, 'debug', 'standard', 'warning', 'off'), DOpus.strings.Get('config_log_level'), 'General');
	AddConfig('show warning for non existent paths', true, DOpus.strings.Get('config_show_warning'), 'Advanced');
	AddConfig('delayed tab closing detection', true, DOpus.strings.Get('config_use_tabs_timer'), 'Advanced');
	AddConfig('none', DOpus.NewVector(1, o(0), o(1), o(2), o(3), o(4), o(5), o(6)), DOpus.strings.Get('config_qual_none'), 'Go qualifiers', 0);
	AddConfig('alt', DOpus.NewVector(1, o(0), o(1), o(2), o(3), o(4), o(5), o(6)), DOpus.strings.Get('config_qual_alt'), 'Go qualifiers', 1);
	AddConfig('shift', DOpus.NewVector(2, o(0), o(1), o(2), o(3), o(4), o(5), o(6)), DOpus.strings.Get('config_qual_shift'), 'Go qualifiers', 2);
	AddConfig('ctrl', DOpus.NewVector(3, o(0), o(1), o(2), o(3), o(4), o(5), o(6)), DOpus.strings.Get('config_qual_ctrl'), 'Go qualifiers', 3);
	AddConfig('alt+ctrl', DOpus.NewVector(4, o(0), o(1), o(2), o(3), o(4), o(5), o(6)), DOpus.strings.Get('config_qual_alt+ctrl'), 'Go qualifiers', 4);
	AddConfig('alt+shift', DOpus.NewVector(5, o(0), o(1), o(2), o(3), o(4), o(5), o(6)), DOpus.strings.Get('config_qual_alt+shift'), 'Go qualifiers', 5);
	AddConfig('ctrl+shift', DOpus.NewVector(6, o(0), o(1), o(2), o(3), o(4), o(5), o(6)), DOpus.strings.Get('config_qual_ctrl+shift'), 'Go qualifiers', 6);
	o = null;

	function AddConfig(name, value, desc, group) {
		initData.config[name] = value;
		initData.config_desc(name) = desc;
		initData.config_groups(name) = group;
	}

}
var script_name = 'TabRescue';
var script_version = '2.0.1';

// Called to add commands to Opus
function OnAddCommands(addCmdData) {
	var cmd = addCmdData.AddCommand();
	cmd.name = script_name;
	cmd.method = 'OnTabRescue';
	cmd.desc = 'Quickly restore your recently closed tabs';
	cmd.label = script_name;
	cmd.template = 'LIST/O[checkifexist],LAST/S,PAUSE/S,UNPAUSE/S,TOGGLEPAUSE/S,CLEAR/S';
	cmd.hide = false;
	cmd.flags = 'L';
	cmd.icon = 'undoclosetab';
}

// Implement the TabRescue command
function OnTabRescue(scriptCmdData) {
	DOpus.ClearOutput();
	Log(2, '===== ' + script_name + ' v' + script_version + ' =====');
	Log(1, scriptCmdData.cmdline);
	var tab = scriptCmdData.func.sourcetab;
	if (!tab || !tab.lister) {
		Log(4, 'You need a lister opened for using this argument!');
	}
	else {

		var l_vars = tab.lister.vars;
		var cmd = scriptCmdData.func.command;
		if (scriptCmdData.func.args.got_arg.PAUSE) SetPause(tab.lister, l_vars, true);
		else if (scriptCmdData.func.args.got_arg.UNPAUSE) SetPause(tab.lister, l_vars, false);
		else if (scriptCmdData.func.args.got_arg.TOGGLEPAUSE) SetPause(tab.lister, l_vars, !GetPauseStatus(l_vars));
		else if (scriptCmdData.func.args.got_arg.CLEAR) ClearVars(cmd, tab.lister, lvars);
		else {
			var closed_tabs_map = l_vars.Exists(script_name + '_tlot') ? l_vars.Get(script_name + '_tlot') : null;
			if (!closed_tabs_map || closed_tabs_map.empty)
				Log(4, 'No saved tabs for this lister yet!');

			else {
				var FSU = DOpus.FSUtil();
				if (scriptCmdData.func.args.got_arg.LIST) ShowList(FSU, tab.lister, l_vars, closed_tabs_map, cmd, scriptCmdData.func.args.LIST);
				else if (l_vars.Exists(script_name + '_closed_last_tab')) RedoTab(tab, FSU, cmd, l_vars.Get(script_name + '_closed_last_tab'), scriptCmdData.func.qualifiers);
				else Log(4, 'No last closed tab path was found!');
				FSU = null;
			}
			closed_tabs_map = null;
		}
		cmd = null;

	}
	tab = null;
	l_vars = null;
	Log(2, '===== COMMAND FINISHED =====');
	DOpus.Delay(1);
	return;
}

function ShowList(FSU, lister, l_vars, closed_tabs_map, cmd, args) {
	var dlg = {};
	dlg.main = DOpus.Dlg();
	dlg.main.template = 'main';
	dlg.main.title = script_name + ' v' + script_version;
	dlg.main.singleton = script_name + ' v' + script_version;
	if (!dlg.main.Create()) return;
	Script.vars.Set('dlg_opened', true);
	var tabs_Enum, mapData = DOpus.Create().OrderedMap();
	var g_data = DOpus.NewMap(
		lister, DOpus.NewMap(
			'tabs', closed_tabs_map,
			'vars', l_vars
		)
	);

	var strings = {
		'keep': DOpus.strings.Get('label_keep_open'),
		'delaf': DOpus.strings.Get('label_remove_after'),
		'broom': DOpus.strings.Get('label_clear_nonex'),
		'pause': DOpus.strings.Get('label_pause'),
		'on': DOpus.strings.Get('label_on'),
		'off': DOpus.strings.Get('label_off')
	};
	var shell = new ActiveXObject('WScript.Shell');
	var str_tools = DOpus.Create().StringTools();
	var icons_map = DOpus.NewMap();
	var drives = GetDrives();

	try {
		icons_map.Set(FSU.Resolve('/system').Split()(0), '/system\\imageres.dll,31');
	}
	catch (err) {};
	dlg.listview = dlg.main.Control('listview');
	dlg.filter = dlg.main.Control('filter');
	dlg.clear_btn = dlg.main.Control('clear_btn');
	dlg.keep_btn = dlg.main.Control('keep_btn');
	dlg.x_btn = dlg.main.Control('x_btn');
	dlg.delaf_btn = dlg.main.Control('delaf_btn');
	dlg.broom_btn = dlg.main.Control('broom_btn');
	dlg.pause_btn = dlg.main.Control('pause_btn');

	dlg.keep_var = Script.vars.Exists('keep_dlg') ? Script.vars.Get('keep_dlg') : false;
	dlg.delaf_var = Script.vars.Exists('delaf_dlg') ? Script.vars.Get('delaf_dlg') : false;
	dlg.broom_var = /^checkifexist$/i.test(args);
	dlg.pause_var = l_vars.Exists(script_name + '_paused') ? !l_vars.Get(script_name + '_paused') : true;
	dlg.main.top = true;

	dlg.x_btn.Autosize();
	dlg.x_btn.visible = false;
	dlg.clear_btn.Autosize();
	dlg.keep_btn.Autosize();
	dlg.delaf_btn.Autosize();
	dlg.broom_btn.Autosize();
	dlg.pause_btn.Autosize();
	dlg.keep_btn.x = dlg.listview.x + dlg.listview.cx - dlg.keep_btn.cx;
	dlg.broom_btn.x = dlg.keep_btn.x - dlg.broom_btn.cx - 5;
	dlg.delaf_btn.x = dlg.broom_btn.x - dlg.delaf_btn.cx - 5;
	dlg.pause_btn.x = dlg.delaf_btn.x - dlg.pause_btn.cx - 5;

	updateBtn('keep', '122');
	updateBtn('delaf', '141');
	updateBtn('broom', '51');
	updateBtn('pause', '28');

	dlg.x_btn.SetTooltip(DOpus.strings.Get('label_clear_filter'));
	dlg.clear_btn.SetTooltip(DOpus.strings.Get('label_clear_list'));
	dlg.filter.cuetext = DOpus.strings.Get('label_cue_filter');
	dlg.main.AddHotkey('up', 'up');
	dlg.main.AddHotkey('down', 'down');
	dlg.main.AddHotkey('filter', 'F3');
	dlg.main.AddHotkey('esc', 'Escape');
	dlg.main.AddHotkey('copy', 'F8');
	dlg.main.AddHotkey('t_pause', 'Ctrl+p');
	dlg.main.AddHotkey('t_remove', 'Ctrl+r');
	dlg.main.AddHotkey('t_clear', 'Ctrl+l');
	dlg.main.AddHotkey('t_keep', 'Ctrl+k');
	AddHotkeys('open', 'Enter');

	var msg, value, rect;

	Log(1, 'check invalid : ' + dlg.broom_var);
	Log(1, 'keep status   : ' + dlg.keep_var);
	Log(1, 'remove status : ' + dlg.delaf_var);

	dlg.filter.focus = true;
	LoadMap(lister);
	SetDlgPos();
	dlg.main.AddCustomMsg(script_name + '_updateLister', true);
	dlg.main.AddCustomMsg(script_name + '_killAll', true);
	dlg.main.Show();
	main: while (true) {
		msg = dlg.main.GetMsg();
		if (!msg.result) break;
		// Log(1, 'event=' + msg.event + ' ; name=' + msg.name + ' ; data=' + msg.data + ' ; value=' + msg.value + ' ; index=' + msg.index);
		switch (msg.event) {
			case 'custom':
				if (msg.name === script_name + '_killAll') break main;
				if (msg.name === dlg.active_lister + script_name + '_pause_change') {
					dlg.pause_var = msg.data === 1;
					updateBtn('pause', '28');
				}
				else if (msg.name === dlg.active_lister + script_name + '_clearvars') {
					g_data(dlg.active_lister)('tabs').Clear();
					if (!dlg.keep_var) dlg.main.EndDlg(1);
					else LoadMap(dlg.active_lister);
				}
				else if (msg.name === script_name + '_updateLister') {
					try {
						var new_lister = DOpus.listers.lastactive;
						var new_vars = new_lister.vars;
						g_data.Set(new_lister, DOpus.NewMap(
							'tabs', new_vars.Exists(script_name + '_tlot') ? new_vars.Get(script_name + '_tlot') : DOpus.NewMap(),
							'vars', new_vars));
						cmd.SetSourceTab(new_lister.activetab);
						dlg.pause_var = new_vars.Exists(script_name + '_paused') ? !new_vars.Get(script_name + '_paused') : true;
						updateBtn('pause', '28');
						LoadMap(new_lister);
						new_lister = null;
						new_vars = null;
					}
					catch (err) {
						Log(4, 'Error loading new lister info:' + err.message);
						dlg.main.EndDlg(0);
					}

				}
				else if (msg.data == dlg.active_lister) LoadMap(dlg.active_lister);
				break;
			case 'selchange':
				if (msg.name === 'listview') {
					value = mapData.Exists(msg.data) ? msg.data : null;
					dlg.listview.SetTooltip(value && dlg.listview.focus ? mapData(value)('path') : '');
				}
				break;
			case 'editchange':
				if (msg.name === 'filter') {
					dlg.main.SetTimer(150, 'filter_timer');
					dlg.x_btn.visible = msg.value !== '';
				}
				break;
			case 'dblclk':
			case 'click':
			case 'hotkey':
				if (msg.name === 'up' || msg.name === 'down') {
					handleArrow(msg.name);
				}
				else if (msg.name === 'filter') dlg.filter.focus = true;
				else if (msg.name === 'esc') {
					if (dlg.filter.value) dlg.filter.value = '';
					else dlg.main.EndDlg(1);
				}
				else if (msg.name === 'copy') {
					if (value === null) break;
					if (mapData(value)('path')) DOpus.SetClip(mapData(value)('path'));
				}
				else if (msg.name === 'clear_btn') {
					ClearVars(cmd, dlg.active_lister, g_data(dlg.active_lister)('vars'));

				}
				else if (msg.name === 'x_btn') {
					dlg.filter.value = '';
				}
				else if (msg.name === 'keep_btn' || msg.name === 't_keep') {
					dlg.keep_var = !dlg.keep_var;
					updateBtn('keep', '122');
				}
				else if (msg.name === 'delaf_btn' || msg.name === 't_remove') {
					dlg.delaf_var = !dlg.delaf_var;
					updateBtn('delaf', '141');
				}
				else if (msg.name === 'broom_btn' || msg.name === 't_clear') {
					dlg.broom_var = !dlg.broom_var;
					updateBtn('broom', '51');
					LoadMap(dlg.active_lister);
				}
				else if (msg.name === 'pause_btn' || msg.name === 't_pause') {
					SetPause(dlg.active_lister, g_data(dlg.active_lister)('vars'), dlg.pause_var);
				}
				else if (msg.name === 'listview' || msg.name.indexOf('open') === 0) {
					if (value === null) break;
					if (RedoTab(dlg.main, FSU, cmd, mapData(value)('path'), msg.qualifiers)) {
						if (!dlg.keep_var) dlg.main.EndDlg(1);
					}
					if (dlg.delaf_var) RemoveFromList(value);
				}

				else if (msg.name.indexOf('num') === 0) {
					handleNumKey(msg.name.slice(-1), msg.qualifiers);
				}
				break;
			case 'timer':
				if (msg.name === 'filter_timer') {
					dlg.main.KillTimer('filter_timer');
					updateList(dlg.filter.value);
				}
				break;
			case 'focus':
				if (msg.name === 'listview' && !msg.data) {
					dlg.listview.SetTooltip('');
				}
				else if (msg.name === 'filter') {
					if (msg.data) DelNumHotkeys();
					else {
						for (var i = 0; i < 10; i++)
							AddHotkeys('num', i);
					}
				}
				break;
		}
	}
	Script.vars.Delete('dlg_opened');

	strings = null;
	mapData = null;
	icons_map = null;
	shell = null;
	str_tools = null;
	drives = null;
	tabs_Enum = null;
	Script.vars.Set('keep_dlg', dlg.keep_var);
	Script.vars.Set('delaf_dlg', dlg.delaf_var);
	Script.vars('keep_dlg').persist = true;
	Script.vars('delaf_dlg').persist = true;
	dlg = null;
	g_data = null;
	return;
	// ——— Helper functions ———
	function SetDlgPos() {
		var sys = DOpus.Create().SysInfo();
		var rect = sys.WorkAreas(sys.MouseMonitor());
		if (!rect.empty) {
			var mousex = sys.MousePosX();
			var mousey = sys.MousePosY();
			dlg.main.position = 'monitor';
			dlg.main.cx = dlg.listview.columns.GetColumnAt(0).width + dlg.listview.columns.GetColumnAt(1).width + dlg.listview.x * 2;
			var x = mousex + dlg.main.cx + 5 > rect.width ? (mousex - dlg.main.cx) : mousex;
			var y = mousey + dlg.main.cy + 5 > rect.height ? (mousey - dlg.main.cy) : mousey;
			dlg.main.x = x;
			dlg.main.y = y;
		}
		rect = null;
		sys = null;
		return;
	}

	function updateList(filter_value, sel_pos) {
		var pattern = filter_value ? new RegExp(filter_value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') : null;
		var id, row, path, entry;
		tabs_Enum.moveFirst();
		dlg.listview.redraw = false;
		dlg.listview.RemoveItem(-1);
		for (; !tabs_Enum.atEnd(); tabs_Enum.moveNext()) {
			id = tabs_Enum.item();
			path = mapData(id)('path');
			entry = mapData(id)('entry');
			if (!pattern || pattern.test(entry) || pattern.test(path)) {
				row = dlg.listview.GetItemAt(dlg.listview.InsertItemAt(0, entry, id));
				if (row) {
					if (!icons_map.Exists(path)) icons_map(path) = GetIcon(path);
					// Log(1, icons_map(path));
					row.icon = icons_map(path);
				}
			}
		}
		for (var i = 0; i < 10; i++) {
			row = dlg.listview.GetItemAt(i);
			if (row) row.subitems(0).text = i;
		}
		dlg.listview.columns.GetColumnAt(0).width = dlg.listview.cx - 40;
		dlg.listview.columns.GetColumnAt(1).width = -1;
		dlg.listview.redraw = true;
		dlg.listview.SelectItem(!sel_pos || sel_pos >= dlg.listview.count ? 0 : sel_pos);
		pattern = null;
		row = null;
	}

	function LoadMap(lister) {
		if (!g_data.Exists(lister)) return;
		Log(1, 'Loading closed tabs list for ' + lister);
		dlg.main.AddCustomMsg(lister + script_name + '_update', true);
		dlg.main.AddCustomMsg(lister + script_name + '_pause_change', true);
		dlg.main.AddCustomMsg(lister + script_name + '_clearvars', true);

		mapData.assign(GetList(FSU, g_data(lister)('tabs'), dlg.broom_var));
		tabs_Enum = new Enumerator(mapData);
		var sel_pos = dlg.listview.value;
		sel_pos = (DOpus.TypeOf(sel_pos) === 'object.DialogItem') ? sel_pos.index : 0;
		dlg.main.FlushMsg();
		updateList(dlg.filter.value, sel_pos);
		dlg.active_lister = String(lister);

	}

	function AddHotkeys(prefix, key) {
		dlg.main.AddHotkey(prefix + key, key);
		dlg.main.AddHotkey(prefix + 'Ctrl+' + key, 'Ctrl+' + key);
		dlg.main.AddHotkey(prefix + 'Alt+' + key, 'Alt+' + key);
		dlg.main.AddHotkey(prefix + 'Shift+' + key, 'Shift+' + key);
		dlg.main.AddHotkey(prefix + 'Alt+Shift+' + key, 'Alt+Shift+' + key);
		dlg.main.AddHotkey(prefix + 'Ctrl+Shift+' + key, 'Ctrl+Shift+' + key);
		dlg.main.AddHotkey(prefix + 'Ctrl+Alt+' + key, 'Ctrl+Alt+' + key);
	}

	function DelNumHotkeys() {
		for (var i = 0; i < 10; i++) {
			dlg.main.DelHotkey('num' + i);
			dlg.main.DelHotkey('numCtrl+' + i);
			dlg.main.DelHotkey('numAlt+' + i);
			dlg.main.DelHotkey('numShift+' + i);
			dlg.main.DelHotkey('numAlt+Shift+' + i);
			dlg.main.DelHotkey('numCtrl+Shift+' + i);
			dlg.main.DelHotkey('numCtrl+Alt+' + i);
		}

	}

	function handleArrow(dir) {
		if (!dlg.listview.focus) {
			dlg.listview.focus = true;
			return;
		}
		var idx = dlg.listview.value.index,
			max = dlg.listview.count - 1;
		idx = (dir === 'up') ?
			(idx > 0 ? idx - 1 : max) :
			(idx < max ? idx + 1 : 0);
		dlg.listview.SelectItem(idx);
	}

	function handleNumKey(num, qualifiers) {
		var item = dlg.listview.GetItemAt(num);
		if (!item || !mapData.Exists(item.data)) return;
		var path = mapData(item.data)('path');
		if (RedoTab(dlg.main, FSU, cmd, path, qualifiers)) {
			if (!dlg.keep_var) dlg.main.EndDlg(1);
		}
		if (dlg.delaf_var) RemoveFromList(item.data);
	}

	function RemoveFromList(item_id) {
		if (mapData.Exists(item_id)) {
			Log(1, 'Removing "' + mapData(item_id)('path') + '" from the list ...');
			g_data(dlg.active_lister)('tabs').erase(mapData(item_id)('path'));
			var sel_pos = dlg.listview.getItemByName(mapData(item_id)('entry'));
			sel_pos = sel_pos ? (sel_pos.index - 1) : 0;
			mapData.erase(item_id);
			tabs_Enum = new Enumerator(mapData);
			g_data(dlg.active_lister)('vars').Set(script_name + '_tlot', g_data(dlg.active_lister)('tabs'));
			g_data(dlg.active_lister)('vars').Set(script_name + '_count', g_data(dlg.active_lister)('tabs').size);
			updateList(dlg.filter.value, sel_pos);
		}
	}

	function updateBtn(name, ico) {
		dlg[name + '_btn'].label = '<a id="link"><%ddb' + (dlg[name + '_var'] ? 'i' : 'g') + ':' + ico + '></a>';
		dlg[name + '_btn'].SetTooltip(strings[name] + ' (' + (dlg[name + '_var'] ? strings.on : strings.off) + ')');
	}

	function GetIcon(path) {
		var icoPath;
		try {
			switch (FSU.PathType(path)) {
				case 'filesys':
					var p = FSU.NewPath(path);
					icoPath = FSU.GetType(path, 'a') === 'file' ? p.ext : drives.Exists(p.drive) ? GetIcoFromIni(path, FSU, str_tools) : 'dir';
					p = null;
					break;
				case 'shell':
					icoPath = GetIconFromRegistry(path.slice(2), shell);
					break;
				case 'coll':
				case 'lib':
				case 'zip':
					icoPath = path;
					break;
				case 'mtp':
					icoPath = '/system\\imageres.dll,42';
					break;
				case 'ftp':
					icoPath = '/home\\dopusrt.exe,8';
					break;
			}
		}
		catch (err) {}
		if (!icoPath) icoPath = 'dir';
		return icoPath;
	}

	function GetDrives() {
		Log(1, 'Getting info about drives...');
		var drives = FSU.drives();
		var f = DOpus.Create().StringSetI();
		var ico;
		for (var i = drives.length - 1; i >= 0; i--) {
			try {
				switch (drives(i).type) {
					case 'fixed':
					case 'removable':
						var m = drives(i).def_value.match(/^([A-Za-z]):\\?$/);
						if (!m) continue;
						// 'A'.charCodeAt(0) = 65, por lo que A->1 = 65 - 64
						f.insert(m[1].toUpperCase().charCodeAt(0) - 64);
						ico = ',27';
						break;

					case 'remote':
						ico = ',28';
						break;
					case 'ramdisk':
						ico = ',29';
						break;
					case 'cdrom':
						ico = ',25';
						break;
					default:
						continue;
				};
				icons_map(drives(i)) = '/system\\imageres.dll' + ico;
			}
			catch (err) {
				Log(3, '=> Error : ' + err.message);
			}
		}
		return f;
	}
}

function ClearVars(cmd, lister, l_vars) {
	if (!lister) return;
	Log(1, 'Removing all closed tabs from ' + lister + '...');
	try {
		l_vars.Set(script_name + '_count', 0);
		l_vars.Set(script_name + '_tlot', DOpus.Create().OrderedMap());
		l_vars.Delete(script_name + '_closed_last_tab');
		l_vars(script_name + '_count').persist = true;
		DOpus.SendCustomMsg(String(lister) + script_name + '_clearvars');

	}
	catch (err) {
		Log(4, '=> Error removing vars:' + err.message);
	}
	cmd.UpdateToggle();
}

function RedoTab(parent, FSU, cmd, tabpath, qualifiers) {
	if (!tabpath || (FSU.PathType(tabpath) === 'filesys' && !FSU.Exists(tabpath))) {
		if (Script.config['show warning for non existent paths']) Log(3, alert(parent, DOpus.strings.Get('msg_nopath').replace('%1', tabpath)));
		return false;
	}
	Log(2, 'Reopening "' + tabpath + '" ; qualifiers=' + qualifiers);
	cmd.ClearFiles();
	var options = ['', ' NEWTAB=tofront', ' NEWTAB=tofront,findexisting', ' NEWTAB=tofront OPENINDUAL', ' NEWTAB=tofront,findexisting OPENINDUAL', ' NEW', ' NEW=dual'];
	var qual_opts = {
		'none': Script.config['none'],
		'alt': Script.config['alt'],
		'shift': Script.config['shift'],
		'ctrl': Script.config['ctrl'],
		'alt,ctrl': Script.config['alt+ctrl'],
		'ctrl,alt': Script.config['alt+ctrl'],
		'alt,shift': Script.config['alt+shift'],
		'shift,alt': Script.config['alt+shift'],
		'ctrl,shift': Script.config['ctrl+shift'],
		'shift,ctrl': Script.config['ctrl+shift']
	};
	var o = qual_opts[qualifiers] || 0;
	var cmdline = 'Go "' + tabpath + '"' + (options[o]);
	Log(1, '=> cmdline : ' + cmdline);
	qual_opts = null;
	options = null;
	return cmd.RunCommand(cmdline);
}

function SetPause(lister, l_vars, state) {
	if (!l_vars) return;
	Log(1, 'Setting pause to "' + state + '" for Lister ' + lister);
	try {
		l_vars.Set(script_name + '_paused', state || false);
		DOpus.SendCustomMsg(String(lister) + script_name + '_pause_change', state ? 0 : 1);
	}
	catch (err) {
		Log(4, '=> Error setting pause :' + err.message);
	}
}

function GetPauseStatus(l_vars) {
	if (!l_vars) return false;
	try {
		return l_vars.Exists(script_name + '_paused') ? l_vars.Get(script_name + '_paused') : false;
	}
	catch (err) {
		Log(4, 'Error getting pause state for Lister:' + err.message);
		return false;
	}
}

function GetList(FSU, map, check_exist) {
	var m = DOpus.Create().OrderedMap();
	var max_tabs = Script.config['max tabs per Lister'];
	if (max_tabs < 0) max_tabs = 0;
	else if (max_tabs > 50) max_tabs = 50;
	Log(1, 'Max tabs      : ' + max_tabs);
	Log(1, 'Saved tabs    : ' + map.size);
	var tabs_Enum = new Enumerator(map);
	var tab, counter = 0;
	for (; !tabs_Enum.atEnd(); tabs_Enum.moveNext()) {
		if (max_tabs === counter) break;
		tab = tabs_Enum.item();
		if (!tab) continue;
		if (!check_exist || FSU.PathType(tab) !== 'filesys' || FSU.Exists(tab)) {
			m.Set(counter, DOpus.NewMap('entry', map(tab), 'path', tab));
			counter++;
		}

	}
	tabs_Enum = null;
	return m;
}

function GetIcoFromIni(path, FSU, str_tools) {
	Log(1, 'Getting ico from ini : ' + path);

	var icoPath;
	var iniPath = FSU.NewPath(path);
	iniPath.Add('desktop.ini');
	var file = FSU.OpenFile(iniPath);
	if (file.error === 0) {
		// Log(1, iniPath + ' existe');
		var blob = file.Read();
		file.Close();
		var opt = ['auto', 'utf-8'];
		for (var i = 0; i < opt.length; i++) {
			try {
				var content = str_tools.Decode(blob, opt[i]);

				if (content) {
					// Log(1, content);
					// Buscar IconResource=archivo,index
					var m = content.match(/IconResource\s*=\s*(?:"([^"]+)"|([^,\r\n]+))\s*,\s*([+-]?\d+)/i);
					if (m) {
						icoPath = FSU.Resolve(m[1] || m[2]) + "," + (parseInt(m[3], 10) || 0);
					}
					else { // Buscar IconFile y IconIndex
						var mFile = content.match(/IconFile\s*=\s*(?:"([^"]+)"|([^\r\n]+))/i);
						var mIndex = content.match(/IconIndex\s*=\s*([+-]?\d+)/i);
						if (mFile) {
							icoPath = FSU.Resolve(mFile[1] || mFile[2]) + "," + (mIndex ? parseInt(mIndex[3], 10) : 0);
						}
					}
					if (icoPath) break;
				}
			}
			catch (e) {
				// Log(3, '=> error :' + e.message);
			}
		}

	}
	return icoPath;
}

function GetIconFromRegistry(clsid, shell) {
	Log(1, 'Getting ico from reg : ' + clsid);

	var sub = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\CLSID\\" + clsid + "\\DefaultIcon\\",
		reg;
	try {
		reg = shell.RegRead("HKCU\\" + sub);
	}
	catch (e) {}
	if (!reg) try {
		reg = shell.RegRead("HKLM\\" + sub);
	}
	catch (e) {}
	if (reg) {
		var parts = reg.replace(/^"|"$/g, "").split(",");
		return parts[0] + "," + (parseInt(parts[1], 10) || 0);
	}
	return null;
}
// Called when a tab is closed
function OnCloseTab(closeTabData) {
	var tab = closeTabData.tab;
	if (!tab || !tab.lister) {
		Log(4, 'You need a lister opened in order to save a tab!');
	}
	else {
		var lister = tab.lister;
		var l_vars = lister.vars;
		if (GetPauseStatus(l_vars) === true) Log(2, 'Script is paused for Lister ' + lister + ' so no tab saving...');
		else {
			Script.vars.Set('target_vars', l_vars);
			Script.vars.Set('target_tab', tab);
			DOpus.SetTimer(String(lister), Script.config['delayed tab closing detection'] ? 100 : 1);
		}
		lister = null;
		l_vars = null;
	}
	tab = null;
}
// Called when a Lister is activated
function OnActivateLister(activateListerData) {
	if (!Script.vars.Exists('dlg_opened')) return;
	if (activateListerData.active) DOpus.SetTimer(0, 20);
}
// Called when a Lister is closed
function OnCloseLister(closeListerData) {
	if (!Script.vars.Exists('dlg_opened')) return;
	if (closeListerData.shutdown) {
		DOpus.SendCustomMsg(script_name + '_killAll');
		return;
	}
	DOpus.SetTimer(0, 20);
}

// Allows a script to have an event called at regular intervals
function OnPeriodicTimer(periodicTimerData) {
	DOpus.KillTimer(periodicTimerData.id);
	if (periodicTimerData.id === 0) {
		DOpus.SendCustomMsg(script_name + '_updateLister');
		return;
	}
	var l_vars = Script.vars.Exists('target_vars') ? Script.vars.Get('target_vars') : null;
	var tab = Script.vars.Exists('target_tab') ? Script.vars.Get('target_tab') : null;

	if (DOpus.TypeOf(l_vars) === 'object.Vars' && DOpus.TypeOf(tab) === 'object.Tab') {

		var closed_tabs_map = l_vars.Exists(script_name + '_tlot') ? l_vars.Get(script_name + '_tlot') : DOpus.Create().OrderedMap();
		var tab_path = tab.path || '';
		Log(1, 'Saving closed tab (' + tab_path + ') from lister ' + periodicTimerData.id + '...');
		if (closed_tabs_map.Exists(tab_path)) closed_tabs_map.erase(tab_path);
		Log(1, ' => Adding "' + tab_path + '" to saved tabs...');
		closed_tabs_map(tab_path) = tab.displayed_label;
		var max_tabs = Script.config['max tabs per Lister'];
		if (max_tabs < 0) max_tabs = 0;
		else if (max_tabs > 50) max_tabs = 50;
		var map_size = closed_tabs_map.size;
		Log(1, 'Max tabs      : ' + max_tabs);
		Log(1, 'Saved tabs    : ' + map_size);
		for (var tabs_Enum = new Enumerator(closed_tabs_map); !tabs_Enum.atEnd(); tabs_Enum.moveNext()) {
			if (map_size <= max_tabs) break;
			Log(1, ' => Erasing "' + tabs_Enum.item() + '"" from saved tabs...');
			closed_tabs_map.erase(tabs_Enum.item());
			map_size--;
		}

		l_vars.Set(script_name + '_closed_last_tab', tab_path);
		l_vars(script_name + '_closed_last_tab').persist = true;
		l_vars.Set(script_name + '_tlot', closed_tabs_map);
		l_vars(script_name + '_tlot').persist = true;
		l_vars.Set(script_name + '_count', closed_tabs_map.size);
		l_vars(script_name + '_count').persist = true;
		var cmd = DOpus.NewCommand().UpdateToggle();
		cmd = null;
		DOpus.SendCustomMsg(periodicTimerData.id + script_name + '_update', periodicTimerData.id);
	}
	l_vars = null;
	tab = null;
}

// Notifies a script it is being deleted through the Scripts management dialog
function OnDeleteScript(deleteScriptData) {
	Log(2, script_name + ' is going to be deleted. Cleaning old vars...');
	Script.vars.Delete('*');
	var listers = DOpus.Listers;
	for (var e = new Enumerator(listers); !e.atEnd(); e.moveNext())
		e.item().vars.Delete(script_name + '*');
	listers = null;
	e = null;
}

function alert(parent, message, level, choose) {
	var dlg = DOpus.Dlg();
	if (parent) {
		dlg.window = parent;
		dlg.disable_window = parent;
	}
	dlg.message = message;
	dlg.buttons = choose ? choose : '&OK';
	dlg.title = script_name + ' v' + script_version;
	if (level === 0) dlg.icon = 'info';
	else if (level === 1) dlg.icon = 'question';
	else if (level === 2) dlg.icon = 'warning';
	else dlg.icon = 'error';
	dlg.Show();
	return choose ? dlg.result : message;
}

function Log(level, text) {
	if (level === 4 || Script.config['log level'] < level) {
		if (level == 1) DOpus.Output('<#%vs_dragdrop_normal_action>DEBUG   => ' + text + '</#>');
		else if (level == 2) DOpus.Output('INFO    => ' + text);
		else if (level === 3) DOpus.Output('<#%vs_dragdrop_warning_action>WARNING => ' + text + '</#>');
		else DOpus.Output('ERROR   => ' + text, true);
	}
}
==SCRIPT RESOURCES
<resources>
	<resource type="strings">
		<strings lang="English">
			<string id="config_label_new_lister">New Lister</string>
			<string id="config_label_new_lister_dual">New Lister (dual)</string>
			<string id="config_label_new_tab">New Tab</string>
			<string id="config_label_new_tab_find">New Tab (findexisting)</string>
			<string id="config_label_none">None</string>
			<string id="config_label_open_dual">Open in dual</string>
			<string id="config_label_open_dual_tab">New Tab in dual</string>
			<string id="config_log_level">Log level to display. Use OFF to show only errors. 
Use DEBUG to show all messages. 
Use STANDARD to show only the most relevant information. 
Use WARNING to show messages that need your attention.</string>
			<string id="config_max_tabs">Maximum number of closed tabs to remember per Lister.</string>
			<string id="config_qual_alt">Specifies how a closed tab is reopened when the &apos;Alt&apos; key is held down.</string>
			<string id="config_qual_alt+ctrl">Specifies how a closed tab is reopened when the &apos;Alt+Ctrl&apos; keys are held down.</string>
			<string id="config_qual_alt+shift">Specifies how a closed tab is reopened when the &apos;Alt+Shift&apos; keys are held down.</string>
			<string id="config_qual_ctrl">Specifies how a closed tab is reopened when the &apos;Ctrl&apos; key is held down.</string>
			<string id="config_qual_ctrl+shift">Specifies how a closed tab is reopened when the &apos;Ctrl+Shift&apos; keys are held down.</string>
			<string id="config_qual_none">Specifies how a closed tab is reopened when no key qualifier is held down.</string>
			<string id="config_qual_shift">Specifies how a closed tab is reopened when the &apos;Shift&apos; key is held down.</string>
			<string id="config_show_warning">Show a warning when trying to open non existent paths.</string>
			<string id="config_use_tabs_timer">Use a workaround to avoid capturing tabs closed non-manually. 
Disable it if you usually close tabs very quickly and notice the script doesn’t detect that action.</string>
			<string id="label_clear_filter">Clear Filter</string>
			<string id="label_clear_list">Clear List</string>
			<string id="label_clear_nonex">(Ctrl + L) Clear non existent paths</string>
			<string id="label_cue_filter">Type here to filter...</string>
			<string id="label_keep_open">(Ctrl + K) Keep after reopen</string>
			<string id="label_off">Off</string>
			<string id="label_on">On</string>
			<string id="label_pause">(Ctrl + P) Toggle pause state for this Lister</string>
			<string id="label_remove_after">(Ctrl + R) Remove paths after reopen them</string>
			<string id="msg_nopath">&quot;%1&quot; does not seem to exist!</string>
		</strings>
		<strings lang="esm">
			<string id="config_label_new_lister">Nuevo Listado</string>
			<string id="config_label_new_lister_dual">Nuevo Listado (dual)</string>
			<string id="config_label_new_tab">Nueva Pestaña</string>
			<string id="config_label_new_tab_find">Nueva Pestaña (buscar existentes)</string>
			<string id="config_label_none">Ninguno</string>
			<string id="config_label_open_dual">Abrir en vista dual</string>
			<string id="config_label_open_dual_tab">Nueva Pestaña en vista dual</string>
			<string id="config_log_level">Nivel de registro a mostrar. Usa OFF para mostrar solo errores.  
DEBUG para mostrar todos los mensajes. 
STANDARD para mostrar solo la información más relevante.
WARNING para mostrar mensajes que requieren tu atención.</string>
			<string id="config_max_tabs">Máximo de pestañas cerradas para recordar por Listado.</string>
			<string id="config_qual_alt">Define cómo se vuelve a abrir la pestaña cerrada cuando se mantiene presionada la tecla &apos;Alt&apos;.</string>
			<string id="config_qual_alt+ctrl">Define cómo se vuelve a abrir la pestaña cerrada cuando se mantiene presionada la tecla &apos;Alt+Ctrl&apos;.</string>
			<string id="config_qual_alt+shift">Define cómo se vuelve a abrir la pestaña cerrada cuando se mantiene presionada la tecla &apos;Alt+Shift&apos;.</string>
			<string id="config_qual_ctrl">Define cómo se vuelve a abrir la pestaña cerrada cuando se mantiene presionada la tecla &apos;Ctrl&apos;.</string>
			<string id="config_qual_ctrl+shift">Define cómo se vuelve a abrir la pestaña cerrada cuando se mantiene presionadas las teclas &apos;Ctrl+Shift&apos;.</string>
			<string id="config_qual_none">Define cómo se vuelve a abrir la pestaña cerrada cuando no se mantiene presionada ninguna tecla calificadora.</string>
			<string id="config_qual_shift">Define cómo se vuelve a abrir la pestaña cerrada cuando se mantiene presionada la tecla &apos;Shift&apos;.</string>
			<string id="config_show_warning">Mostrar una advertencia al intentar abrir rutas no existentes.</string>
			<string id="config_use_tabs_timer">Emplear una forma de evitar capturar pestañas cerradas de forma no manual.
Deshabilitar si usualmente cierra las pestañas muy rápido y nota que el script no detecta está acción.</string>
			<string id="label_clear_filter">Limpiar filtro</string>
			<string id="label_clear_list">Borrar lista</string>
			<string id="label_clear_nonex">(Ctrl + L) Limpiar rutas no válidas</string>
			<string id="label_cue_filter">Escribir aquí para filtrar</string>
			<string id="label_keep_open">(Ctrl + K) Mantener tras reabrir pestaña</string>
			<string id="label_off">Apagado</string>
			<string id="label_on">Encendido</string>
			<string id="label_pause">(Ctrl + P) Alterna el estado de pausa para este Listado</string>
			<string id="label_remove_after">(Ctrl + R) Remover rutas tras reabrirlas</string>
			<string id="msg_nopath">&quot;%1&quot; no existe!</string>
		</strings>
	</resource>
	<resource name="main" type="dialog">
		<dialog height="178" lang="english" resize="yes" width="130">
			<control halign="left" height="8" name="x_btn" resize="x" tips="yes" title="&lt;a id=&quot;link&quot;&gt;&lt;%ddbi:1&gt;&lt;/a&gt;" type="markuptext" width="8" x="104" y="6" />
			<control halign="left" height="8" name="keep_btn" resize="xy" tips="yes" title="&lt;a id=&quot;link&quot;&gt;&lt;%ddbg:72&gt;&lt;/a&gt;" type="markuptext" width="8" x="112" y="168" />
			<control halign="left" height="8" name="broom_btn" resize="xy" tips="yes" title="&lt;a id=&quot;link&quot;&gt;&lt;%ddbg:51&gt;&lt;/a&gt;" type="markuptext" width="8" x="100" y="168" />
			<control halign="left" height="8" name="delaf_btn" resize="xy" tips="yes" title="&lt;a id=&quot;link&quot;&gt;&lt;%ddbg:141&gt;&lt;/a&gt;" type="markuptext" width="8" x="88" y="168" />
			<control halign="left" height="12" name="filter" resize="w" type="edit" width="112" x="4" y="4" />
			<control halign="left" height="8" name="clear_btn" resize="x" tips="yes" title="&lt;a id=&quot;link&quot;&gt;&lt;%ddbi:74&gt;&lt;/a&gt;" type="markuptext" width="8" x="118" y="6" />
			<control fullrow="yes" height="150" name="listview" noheader="yes" nosortheader="yes" resize="wh" smallicons="yes" type="listview" viewmode="details" width="126" x="2" y="18">
				<columns>
					<item text="Path" />
					<item text="#" />
				</columns>
			</control>
			<control halign="left" height="8" name="pause_btn" resize="xy" tips="yes" title="&lt;a id=&quot;link&quot;&gt;&lt;%ddbi:28&gt;&lt;/a&gt;" type="markuptext" width="8" x="76" y="168" />
		</dialog>
	</resource>
</resources>
