diff --git a/README.md b/README.md index b85385c..cce1638 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ lwsm restore my-session lwsm restore --closeAllOpenWindows ``` + ### List saved sessions: ``` lwsm list @@ -64,6 +65,60 @@ lwsm rename oldName newName ### Adjusting the configuration: You can manually edit the config file present at `~/.config/lwsm/config.json` and the session files in `~/.config/lwsm/sessionData/[sessionName].json`. +### Adding input arguments to programs when restoring a session: +Making use of the "--allowInputArgs" flag, it is possible to restore sessions passing input parameters to the programs. +The parameters have to be added manually to the saved .json files. + +For google chrome you can add: +``` +"executableArgs": "--new-window gmail.com google.com" +``` + +For firefox: +``` +"executableArgs": "--new-window gmail.com" +``` +or (if you want to open more than one tab): +``` +"executableArgs": " gmail.com google.com" +``` + +For nautilus: +``` +"executableArgs": "/home/user/Desktop" +``` + +For Vscode: +``` +"executableArgs": "/home/user/Desktop" +``` + +The json would have a format similar to this: +``` + { + "windowId": "0x2c0000b", + ... + "simpleName": "Google-chrome", + "executableFile": "google-chrome.desktop", + "desktopFilePath": "/usr/share/applications/google-chrome.desktop", + "executableArgs": "--new-window gmail.com" + }, + { + "windowId": "0x2e00001", + ... + "simpleName": "Code", + "executableFile": "code.desktop", + "desktopFilePath": "/usr/share/applications/code.desktop", + "executableArgs": "/path/to/you/code" + }, +``` + +After edditing the json, you can call lwsm with the following command: +``` +# restore the session from ~/.config/lwsm/sessionData/my-session.json +lwsm restore my-session --allowInputArgs +``` + ### Command-line completion: ``` # Automatically install command-line completion diff --git a/cmd.js b/cmd.js index e944dda..b012f35 100755 --- a/cmd.js +++ b/cmd.js @@ -65,6 +65,7 @@ const savePrompts = { }; const isCloseAllWinBefore = process.argv.indexOf('--closeAllOpenWindows') > -1; +const isAllowInputArgs = process.argv.indexOf('--allowInputArgs') > -1; const action = process.argv[2]; let sessionName; if (process.argv[3] && !process.argv[3].match(/^--/)) { @@ -91,7 +92,7 @@ console.log(`lwsm ${version} --> ${action} ${new Date()}`); if (action === 'save') { base.saveSession(sessionName, savePrompts).then(killX11); } else if (action === 'restore') { - base.restoreSession(sessionName, isCloseAllWinBefore).then(killX11); + base.restoreSession(sessionName, isCloseAllWinBefore, isAllowInputArgs).then(killX11); } else if (action === 'remove') { base.removeSession(sessionName).then(killX11); } else if (action === 'resetCfg') { diff --git a/src/defaultConfig.ts b/src/defaultConfig.ts index 5a42482..a3617f1 100644 --- a/src/defaultConfig.ts +++ b/src/defaultConfig.ts @@ -2,6 +2,7 @@ export const DEFAULT_CFG = { GIVE_X11_TIME_TIMEOUT: 80, POLL_ALL_APPS_STARTED_INTERVAL: 2000, POLL_ALL_MAX_TIMEOUT: 120000, + POLL_ALL_MAX_TIMEOUT_ALLOW_ARGS: 1200000, SAVE_SESSION_IN_PRETTY_FORMAT: true, WM_CLASS_AND_EXECUTABLE_FILE_MAP: { "gnome-terminal-server.Gnome-terminal": "gnome-terminal", diff --git a/src/index.ts b/src/index.ts index f99ffe2..14fb22f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -173,7 +173,8 @@ function saveSessionForDisplayToDb( function restoreSession( sessionName: string, - isCloseAllOpenWindows: boolean + isCloseAllOpenWindows: boolean, + isAllowInputArgs: boolean ): Promise { const sessionToHandle = sessionName || "DEFAULT"; @@ -213,7 +214,21 @@ function restoreSession( return getActiveWindowListFlow(); }) .then(currentWindowList => { - return _startSessionPrograms(savedWindowList, currentWindowList); + if (isAllowInputArgs) { + return _startAndWaitPrograms(savedWindowList); + } else { + return _startSessionPrograms(savedWindowList, currentWindowList) + .then(() => { + // gets current window list by itself and returns the updated variant + return _waitForAllAppsToStart(savedWindowList); + }) + .then((updatedCurrentWindowList: WinObj[]) => { + return _updateWindowIds( + savedWindowList, + updatedCurrentWindowList + ); + }); + } }) .then(() => { // gets current window list by itself and returns the updated variant @@ -417,6 +432,7 @@ function _guessFilePath(win: WinObj, inputHandler): Promise { `\n Alternative guessing approach for "${win.simpleName}" SUCCESS -> ${ent[0]}` ); win.executableFile = ent[0]; + win.executableArgs = ""; fulfill(win.executableFile); } }); @@ -425,9 +441,11 @@ function _guessFilePath(win: WinObj, inputHandler): Promise { .then(input => { if (_isDesktopFile(win.executableFile)) { win.desktopFilePath = input; + win.executableArgs = ""; fulfill(win.desktopFilePath); } else { win.executableFile = input; + win.executableArgs = ""; fulfill(win.executableFile); } }) @@ -469,12 +487,137 @@ async function _startSessionPrograms( }) .map(win => { win.instancesStarted += 1; - return startProgram(win.executableFile, win.desktopFilePath); + return startProgram(win.executableFile, win.desktopFilePath, ""); }); await Promise.all(promises); } +// This function is necessary to make possible sending custom arguments to applications to start, +// making sure it will keep track of which windows correspond to which instance of the applications +async function _startAndWaitPrograms(windowList: WinObj[]) { + // Clear the windowIds from the window objects + windowList.forEach(win => { + win.windowId = null; + win.windowIdDec = null; + }); + + // Get the windowIds of all Ids that were previously opened in order to + // avoid these windows from being used isntead of the newly created windows + // (necessary for windows with custom input arguments) + let activeWindows = await getActiveWindowListFlow(); + let blackWinIdList = activeWindows.map(win => win.windowId); + + // Match all the previously opened windows with the windows that do not have + // custom arguments (in this case, no blacklist needed) + await _matchWindows(windowList, false, []); + + let windowsNotStarted = windowList.filter(win => win.windowId === null); + let windowStarted = windowList.filter(win => win.windowId !== null); + let windowsToStart = _getWindowsToStart(windowsNotStarted, windowStarted); + + let totalTimeWaited = 0; + // Runs the loop until all windows have been opened or time limit is over + while (windowList.find(win => win.windowId === null)) { + totalTimeWaited += CFG.POLL_ALL_APPS_STARTED_INTERVAL; + if (totalTimeWaited > CFG.POLL_ALL_MAX_TIMEOUT_ALLOW_ARGS) { + console.error("POLL_ALL_MAX_TIMEOUT_ALLOW_ARGS reached"); + windowList + .filter(win => win.windowId === null) + .forEach(e => { + console.log("Unable to start: ", e.wmClassName); + }); + break; + } + + let promises = windowsToStart.map(win => { + startProgram(win.executableFile, win.desktopFilePath, win.executableArgs); + }); + await Promise.all(promises); + + // Try to match newly started windows, the black list is needed to avoid matching + // windows that have custom arguments with windows oppened before running lwsm + await _matchWindows(windowList, true, blackWinIdList); + // Update the lists with the windows not to be started yet, the windows to be started next + // and the ones that already have the command to start sent + windowsNotStarted = windowsNotStarted.filter( + winNotStarted => + !windowsToStart.find(winToStart => winToStart === winNotStarted) + ); + windowStarted = windowStarted.concat(windowsToStart); + windowsToStart = _getWindowsToStart(windowsNotStarted, windowStarted); + } + + windowList.forEach(win => { + win.windowIdDec = parseInt(win.windowId, 16); + }); +} + +// Get, from windowList, all the windows that can be started without the risk of +// mixing same application windows with different arguments. The others will have to wait for the +// previous windows to start, before the command can be issued +function _getWindowsToStart(windowList: WinObj[], windowsStarted: WinObj[]) { + let windowsToStart = []; + for (let win of windowList) { + let shouldAdd = true; + // If another instance of the same application with different input arguments + // is in the list of windows to start, then the current one should not be added + windowsToStart.forEach(winToRun => { + if ( + win.executableFile === winToRun.executableFile && + win.executableArgs !== winToRun.executableArgs + ) { + shouldAdd = false; + } + }); + // If another instance of the same application is in the list of windows that + // have been sent command to start, but still don't have a windowId, the current one should not be added + if ( + windowsStarted.find( + winStarted => + winStarted.windowId === null && + winStarted.wmClassName === win.wmClassName + ) + ) { + shouldAdd = false; + } + if (!win.windowId && shouldAdd) { + windowsToStart.push(win); + } + } + return windowsToStart; +} + +// Match the windows that have been opened already to the windows on windowList +async function _matchWindows( + windowList: WinObj[], + includeExecsWithArgs: boolean, + blackWinIdList: String[] +) { + let activeWindows = await getActiveWindowListFlow(); + // Remove the active windows that already have a window assigned on windowList + // and the ones that are on the black list + activeWindows = activeWindows.filter( + actWin => !windowList.find(win => win.windowId === actWin.windowId) + ); + activeWindows = activeWindows.filter( + actWin => !blackWinIdList.find(windowId => windowId === actWin.windowId) + ); + + for (let win of windowList.filter(win => win.windowId === null)) { + let activeWindowMatch = activeWindows.find( + actWin => actWin.wmClassName == win.wmClassName + ); + if ( + activeWindowMatch && + (!win.executableArgs || win.executableArgs === "" || includeExecsWithArgs) + ) { + win.windowId = activeWindowMatch.windowId; + activeWindows = activeWindows.filter(e => e !== activeWindowMatch); + } + } +} + function _getNumberOfInstancesToRun( windowToMatch: WinObj, windowList: WinObj[] @@ -576,7 +719,6 @@ async function _restoreWindowPositions( for (const win of savedWindowList) { promises.push(restoreWindowPosition(win)); promises.push(moveToWorkspace(win.windowId, win.wmCurrentDesktopNr)); - // The promises are not executed until the last item is reached or // the desktop_nr is different from the previous entry and which case // the app waits for those to finish before continuing the process diff --git a/src/model.ts b/src/model.ts index 2f77e51..f69448e 100644 --- a/src/model.ts +++ b/src/model.ts @@ -16,6 +16,7 @@ export interface WinObj extends WinObjIdOnly { height: number; simpleName: string; executableFile: string; + executableArgs: string; desktopFilePath?: string; instancesStarted?: number; } diff --git a/src/otherCmd.ts b/src/otherCmd.ts index f22170e..071f026 100644 --- a/src/otherCmd.ts +++ b/src/otherCmd.ts @@ -79,7 +79,8 @@ export async function getAdditionalMetaDataForWin( // TODO prettify args structure export function startProgram( executableFile: string, - desktopFilePath: string + desktopFilePath: string, + executableArgs: string ): Promise { IS_DEBUG && console.log("DEBUG: startProgram():", executableFile, desktopFilePath); @@ -88,14 +89,26 @@ export function startProgram( let args = []; if (desktopFilePath) { cmd = `awk`; - args.push( - '/^Exec=/ {sub("^Exec=", ""); gsub(" ?%[cDdFfikmNnUuv]", ""); exit system($0)}' - ); + + if (executableArgs) { + args.push( + `/^Exec=/ {sub("^Exec=", ""); gsub(" ?%[cDdFfikmNnUuv]", " ${executableArgs}"); exit system($0)}` + ); + } else { + args.push( + '/^Exec=/ {sub("^Exec=", ""); gsub(" ?%[cDdFfikmNnUuv]", ""); exit system($0)}' + ); + } + args.push(desktopFilePath); } else { const parsedCmd = parseCmdArgs(executableFile); cmd = parsedCmd[0]; args = parsedCmd[1]; + if (executableArgs) { + var executableArgsArray = executableArgs.split(" "); + args = args.concat(executableArgsArray); + } } return new Promise(fulfill => {