diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 99bce7db0c..fca7581f62 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -120,6 +120,7 @@ async function UIWindow(options) { options.window_class = (options.window_class !== undefined ? ' ' + options.window_class : ''); options.is_visible = options.is_visible ?? true; + options.background = options.background ?? false; // if only one instance is allowed, bring focus to the window that is already open if(options.single_instance && options.app !== ''){ @@ -579,7 +580,8 @@ async function UIWindow(options) { $(el_window_head_icon).attr('src', window.icons['shared.svg']); } // focus on this window and deactivate other windows - if ( options.is_visible ) { + // BUT: Don't focus if this is a background app - background apps should not steal focus + if ( options.is_visible && !options.background ) { $(el_window).focusWindow(); } @@ -1410,7 +1412,10 @@ async function UIWindow(options) { el_window_app_iframe.contentWindow.postMessage({msg: "drop", x: (window.mouseX - rect.left), y: (window.mouseY - rect.top), items: items}, '*'); // bring focus to this window + // BUT: Don't focus if this is a background app - background apps should not steal focus + if ( !options.background ) { $(el_window).focusWindow(); + } } // if this window is not a directory, cancel drop. @@ -1552,10 +1557,13 @@ async function UIWindow(options) { // make sure to cancel any previous timeouts otherwise the window will be brought to front multiple times clearTimeout(drag_enter_timeout); // If items are dragged over this window long enough, bring it to front + // BUT: Don't focus if this is a background app - background apps should not steal focus + if ( !options.background ) { drag_enter_timeout = setTimeout(function(){ // focus window $(el_window).focusWindow(); }, 1400); + } }, leave: function (dragsterEvent, event) { // cancel the timeout for 'bringing window to front' diff --git a/src/gui/src/helpers/launch_app.js b/src/gui/src/helpers/launch_app.js index c878cf703d..88b4bb9d8d 100644 --- a/src/gui/src/helpers/launch_app.js +++ b/src/gui/src/helpers/launch_app.js @@ -51,6 +51,11 @@ const launch_app = async (options)=>{ app_info.uuid = app_info.uuid ?? app_info.uid; app_info.uid = app_info.uid ?? app_info.uuid; + // Allow callers to override background setting (e.g., when launched from terminal with & operator) + if (options.background !== undefined) { + app_info.background = options.background; + } + // If no `options.name` is provided, use the app name from the app_info options.name = options.name ?? app_info.name; @@ -346,6 +351,7 @@ const launch_app = async (options)=>{ is_visible: ! app_info.background, is_maximized: options.maximized, is_fullpage: options.is_fullpage, + background: app_info.background, // Pass background flag to UIWindow ...(options.pseudonym ? {pseudonym: options.pseudonym} : {}), ...window_options, is_resizable: window_resizable, @@ -400,7 +406,8 @@ const launch_app = async (options)=>{ // If `window-active` is set (meanign the window is focused), focus the window one more time // this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown) - if($(process.references.el_win).hasClass('window-active')){ + // BUT: Don't focus if this is a background app - background apps should not steal focus + if($(process.references.el_win).hasClass('window-active') && !app_info.background){ $(process.references.el_win).focusWindow(); } }); diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js index 22982cc782..8ee230f2c2 100644 --- a/src/gui/src/services/ExecService.js +++ b/src/gui/src/services/ExecService.js @@ -29,7 +29,7 @@ export class ExecService extends Service { } // This method is exposed to apps via IPCService. - async launchApp ({ app_name, args, pseudonym }, { ipc_context, msg_id } = {}) { + async launchApp ({ app_name, args, pseudonym, background }, { ipc_context, msg_id } = {}) { const app = ipc_context?.caller?.app; const process = ipc_context?.caller?.process; @@ -44,6 +44,14 @@ export class ExecService extends Service { this.log.info('launchApp connection', connection); + // Fetch app info to check if it's a background app + let app_info; + if (app_name !== 'explorer') { + app_info = await puter.apps.get(app_name); + } else { + app_info = []; + } + const params = {}; for ( const provider of this.param_providers ) { Object.assign(params, provider()); @@ -55,6 +63,7 @@ export class ExecService extends Service { name: app_name, pseudonym, args: args ?? {}, + background: background, parent_instance_id: app?.appInstanceID, uuid: child_instance_id, params, @@ -87,7 +96,8 @@ export class ExecService extends Service { // If `window-active` is set (meanign the window is focused), focus the window one more time // this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown) - if($(child_process.references.el_win).hasClass('window-active')){ + // BUT: Don't focus if this is a background app - background apps should not steal focus + if($(child_process.references.el_win).hasClass('window-active') && !app_info.background){ $(child_process.references.el_win).focusWindow(); } }); diff --git a/src/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js b/src/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js index 12b64a0665..340ef0f3eb 100644 --- a/src/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js +++ b/src/phoenix/src/ansi-shell/parsing/buildParserFirstHalf.js @@ -146,6 +146,7 @@ export const buildParserFirstHalf = (sp, profile) => { parserBuilder.def(a => a.literal('|').assign({ $: 'op.pipe' })), parserBuilder.def(a => a.literal('>').assign({ $: 'op.redirect', direction: 'out' })), parserBuilder.def(a => a.literal('<').assign({ $: 'op.redirect', direction: 'in' })), + parserBuilder.def(a => a.literal('&').assign({ $: 'op.background' })), { parser: parserBuilder.def(a => a.literal(')').assign({ $: 'op.close' })), transition: { diff --git a/src/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js b/src/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js index a89ce3166f..cc533f862d 100644 --- a/src/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js +++ b/src/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js @@ -119,6 +119,7 @@ class ShellConstructsPStratumImpl { node.tokens = []; node.inputRedirects = []; node.outputRedirects = []; + node.background = false; }, next ({ value, lexer }) { if ( value.$ === 'op.line-terminator' ) { @@ -137,6 +138,12 @@ class ShellConstructsPStratumImpl { this.pop(); return; } + if ( value.$ === 'op.background' ) { + // Mark this command as background and consume the operator + this.stack_top.node.background = true; + lexer.next(); + return; + } if ( value.$ === 'op.redirect' ) { this.push('redirect', { direction: value.direction }); lexer.next(); diff --git a/src/phoenix/src/ansi-shell/pipeline/Pipeline.js b/src/phoenix/src/ansi-shell/pipeline/Pipeline.js index 3bb58426ae..5ff54de9d1 100644 --- a/src/phoenix/src/ansi-shell/pipeline/Pipeline.js +++ b/src/phoenix/src/ansi-shell/pipeline/Pipeline.js @@ -135,14 +135,16 @@ export class PreparedCommand { // args: ast.args.map(node => node.text), inputRedirect, outputRedirects, + background: ast.background || false, }); } - constructor ({ command, args, inputRedirect, outputRedirects }) { + constructor ({ command, args, inputRedirect, outputRedirects, background }) { this.command = command; this.args = args; this.inputRedirect = inputRedirect; this.outputRedirects = outputRedirects; + this.background = background || false; } setContext (ctx) { @@ -234,6 +236,12 @@ export class PreparedCommand { command, args, outputIsRedirected: this.outputRedirects.length > 0, + background: this.background, + }, + env: { + ...this.ctx.env, + // Set BACKGROUND environment variable if command is run in background + ...(this.background ? { BACKGROUND: '1' } : {}), } }); diff --git a/src/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js b/src/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js index 7d78dcc7e9..fd1b13d641 100644 --- a/src/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js +++ b/src/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js @@ -58,7 +58,10 @@ export class PuterAppCommandProvider { }, env: {...ctx.env}, }; - const child = await puter.ui.launchApp(id, args); + // Check if app should be launched in background + // The & operator sets BACKGROUND=1 in the environment, or we can check ctx.locals.background + const shouldRunInBackground = ctx.env.BACKGROUND === '1' || ctx.env.BACKGROUND === 'true' || ctx.locals.background === true; + const child = await puter.ui.launchApp(id, args, undefined, { background: shouldRunInBackground }); const resize_listener = evt => { child.postMessage({ diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index f42111efeb..19635d39d2 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -1009,7 +1009,7 @@ class UI extends EventListener { } // Returns a Promise - launchApp = async function launchApp(app_name, args, callback) { + launchApp = async function launchApp(app_name, args, callback, options) { let pseudonym = undefined; if ( app_name.includes('#(as)') ) { [app_name, pseudonym] = app_name.split('#(as)'); @@ -1021,6 +1021,7 @@ class UI extends EventListener { app_name, pseudonym, args, + background: options?.background, }, });