Explorar el Código

Merge pull request #282 from HeyPuter/eric/task-manager

Add a task manager
Eric Dubé hace 1 año
padre
commit
b897598fac

+ 11 - 1
src/UI/UIDesktop.js

@@ -35,6 +35,7 @@ import new_context_menu_item from "../helpers/new_context_menu_item.js"
 import refresh_item_container from "../helpers/refresh_item_container.js"
 import refresh_item_container from "../helpers/refresh_item_container.js"
 import changeLanguage from "../i18n/i18nChangeLanguage.js"
 import changeLanguage from "../i18n/i18nChangeLanguage.js"
 import UIWindowSettings from "./Settings/UIWindowSettings.js"
 import UIWindowSettings from "./Settings/UIWindowSettings.js"
+import UIWindowTaskManager from "./UIWindowTaskManager.js"
 
 
 async function UIDesktop(options){
 async function UIDesktop(options){
     let h = '';
     let h = '';
@@ -1190,7 +1191,7 @@ $(document).on('click', '.user-options-menu-btn', async function(e){
                 }
                 }
             },
             },
             //--------------------------------------------------
             //--------------------------------------------------
-            // Change Password
+            // Settings
             //--------------------------------------------------
             //--------------------------------------------------
             {
             {
                 html: i18n('settings'),
                 html: i18n('settings'),
@@ -1199,6 +1200,15 @@ $(document).on('click', '.user-options-menu-btn', async function(e){
                 }
                 }
             },
             },
             //--------------------------------------------------
             //--------------------------------------------------
+            // Task Manager
+            //--------------------------------------------------
+            {
+                html: i18n('task_manager'),
+                onClick: async function(){
+                    UIWindowTaskManager();
+                }
+            },
+            //--------------------------------------------------
             // Contact Us
             // Contact Us
             //--------------------------------------------------
             //--------------------------------------------------
             {
             {

+ 1 - 0
src/UI/UIWindow.js

@@ -2773,6 +2773,7 @@ window.sidebar_item_droppable = (el_window)=>{
 // closes a window
 // closes a window
 $.fn.close = async function(options) {
 $.fn.close = async function(options) {
     options = options || {};
     options = options || {};
+    console.log(options);
     $(this).each(async function() {
     $(this).each(async function() {
         const el_iframe = $(this).find('.window-app-iframe');
         const el_iframe = $(this).find('.window-app-iframe');
         const app_uses_sdk = el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true';
         const app_uses_sdk = el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true';

+ 263 - 0
src/UI/UIWindowTaskManager.js

@@ -0,0 +1,263 @@
+import { END_HARD, END_SOFT } from "../definitions.js";
+import UIAlert from "./UIAlert.js";
+import UIContextMenu from "./UIContextMenu.js";
+import UIWindow from "./UIWindow.js";
+
+const UIWindowTaskManager = async function UIWindowTaskManager () {
+    const svc_process = globalThis.services.get('process');
+
+    const w = await UIWindow({
+        title: i18n('task_manager'),
+        icon: globalThis.icons['cog.svg'],
+        uid: null,
+        is_dir: false,
+        message: 'message',
+        app: 'taskmgr',
+        // body_icon: options.body_icon,
+        // backdrop: options.backdrop ?? false,
+        is_resizable: true,
+        is_droppable: false,
+        has_head: true,
+        selectable_body: true,
+        draggable_body: false,
+        allow_context_menu: true,
+        // allow_native_ctxmenu: true,
+        show_in_taskbar: true,
+        dominant: true,
+        body_content: '',
+        width: 350,
+        // parent_uuid: options.parent_uuid,
+        // ...options.window_options,
+        window_css:{
+            height: 'initial',
+        },
+        body_css: {
+            width: 'initial',
+            padding: '20px',
+            // 'background-color': `hsla(
+            //     var(--primary-hue),
+            //     calc(max(var(--primary-saturation) - 15%, 0%)),
+            //     calc(min(100%,var(--primary-lightness) + 20%)), .91)`,
+            'background-color': `hsla(
+                var(--primary-hue),
+                var(--primary-saturation),
+                var(--primary-lightness),
+                var(--primary-alpha))`,
+            'backdrop-filter': 'blur(3px)',
+            
+        }
+    });
+    const w_body = w.querySelector('.window-body');
+    w_body.classList.add('taskmgr');
+
+    const Indent = ({ has_trunk, has_branch }) => {
+        const el = document.createElement('div');
+        el.classList.add('taskmgr-indentcell');
+        if ( has_trunk ) {
+            // Add new child element
+            const el_indentcell_child = document.createElement('div');
+            el_indentcell_child.classList.add('taskmgr-indentcell-trunk');
+            el.appendChild(el_indentcell_child);
+        }
+        if ( has_branch ) {
+            const el_indentcell_child = document.createElement('div');
+            el_indentcell_child.classList.add('taskmgr-indentcell-branch');
+            el.appendChild(el_indentcell_child);
+        }
+
+        return {
+            appendTo (parent) {
+                parent.appendChild(el);
+                return this;
+            }
+        };
+    };
+
+    const Task = ({ placement, name }) => {
+        const {
+            indent_level, last_item,
+            parent_last_item,
+        } = placement;
+
+        const el = document.createElement('div');
+        el.classList.add('taskmgr-task');
+
+        for ( let i=0; i < indent_level; i++ ) {
+            const last_cell = i === indent_level - 1;
+            Indent({
+                has_trunk: (last_cell && ( ! last_item )) ||
+                    (!last_cell && !parent_last_item[i+1]),
+                has_branch: last_cell
+            }).appendTo(el);
+        }
+
+        const el_title = document.createElement('div');
+        el_title.classList.add('taskmgr-task-title');
+        el_title.innerText = name;
+        el.appendChild(el_title);
+
+        return {
+            el () { return el; },
+            appendTo (parent) {
+                parent.appendChild(el);
+                return this;
+            }
+        };
+    }
+
+    // https://codepen.io/fomkin/pen/gOgoBVy
+    const Table = ({ headings }) => {
+        const el_table = $(`
+            <table>
+                <thead>
+                    <tr>
+                        ${headings.map(heading =>
+                            `<th><span>${heading}<span></th>`).join('')}
+                    </tr>
+                </thead>
+                <tbody></tbody>
+            </table>
+        `)[0];
+
+        const el_tbody = el_table.querySelector('tbody');
+
+        return {
+            el () { return el_table; },
+            add (el) {
+                if ( typeof el.el === 'function' ) el = el.el();
+                el_tbody.appendChild(el);
+                return this;
+            },
+            clear () {
+                el_tbody.innerHTML = '';
+            }
+        };
+    };
+
+    const Row = () => {
+        const el_tr = document.createElement('tr');
+        return {
+            attach (parent) {
+                parent.appendChild(el_tr);
+                return this;
+            },
+            el () { return el_tr; },
+            add (el) {
+                if ( typeof el.el === 'function' ) el = el.el();
+                const el_td = document.createElement('td');
+                el_td.appendChild(el);
+                el_tr.appendChild(el_td);
+                return this;
+            }
+        };
+    };
+
+    const el_taskarea = document.createElement('div');
+    el_taskarea.classList.add('taskmgr-taskarea');
+
+    const tasktable = Table({
+        headings: [
+            i18n('taskmgr_header_name'),
+            i18n('taskmgr_header_type'),
+            i18n('taskmgr_header_status'),
+        ]
+    });
+
+    el_taskarea.appendChild(tasktable.el());
+
+    const end_process_ = async (process, force) => {
+        let confirmation;
+
+        if ( process.is_init() ) {
+            if ( ! force ) {
+                confirmation = i18n('close_all_windows_confirm');
+            } else {
+                confirmation = i18n('restart_puter_confirm');
+            }
+        } else if ( force ) {
+            confirmation = i18n('end_process_force_confirm');
+        }
+
+        if ( confirmation ) {
+            const alert_resp = await UIAlert({
+                message: confirmation,
+                buttons:[
+                    {
+                        label: i18n('yes'),
+                        value: true,
+                        type: 'primary',
+                    },
+                    {
+                        label: i18n('no'),
+                        value: false,
+                    },
+                ]
+            })
+            if ( ! alert_resp ) return;
+        }
+
+        process.signal(force ? END_HARD : END_SOFT);
+    }
+
+    const iter_tasks = (items, { indent_level, parent_last_item }) => {
+        for ( let i=0 ; i < items.length; i++ ) {
+            const row = Row();
+            const item = items[i];
+            const last_item = i === items.length - 1;
+            row.add(Task({
+                placement: {
+                    parent_last_item,
+                    indent_level,
+                    last_item,
+                },
+                name: item.name
+            }));
+            row.add($(`<span>${i18n('process_type_' + item.type)}</span>`)[0])
+            row.add($(`<span>${i18n('process_status_' + item.status.i18n_key)}</span>`)[0])
+            tasktable.add(row);
+
+            $(row.el()).on('contextmenu', () => {
+                UIContextMenu({
+                    parent_element: $(el_taskarea),
+                    items: [
+                        {
+                            html: i18n('close'),
+                            onClick: () => {
+                                end_process_(item);
+                            }
+                        },
+                        {
+                            html: i18n('force_quit'),
+                            onClick: () => {
+                                end_process_(item, true);
+                            }
+                        }
+                    ]
+                });
+            })
+
+            const children = svc_process.get_children_of(item.uuid);
+            if ( children ) {
+                iter_tasks(children, {
+                    indent_level: indent_level + 1,
+                    parent_last_item:
+                        [...parent_last_item, last_item],
+                });
+            }
+        }
+    };
+
+    const interval = setInterval(() => {
+        tasktable.clear();
+        const processes = [svc_process.get_init()];
+        iter_tasks(processes, { indent_level: 0, parent_last_item: [] });
+    }, 500)
+
+    w.on_close = () => {
+        clearInterval(interval);
+    }
+
+    w_body.appendChild(el_taskarea);
+}
+
+export default UIWindowTaskManager;

+ 7 - 0
src/UI/UIWindowThemeDialog.js

@@ -1,4 +1,5 @@
 import UIWindow from "./UIWindow.js";
 import UIWindow from "./UIWindow.js";
+import UIWindowColorPicker from "./UIWindowColorPicker.js";
 
 
 const UIWindowThemeDialog = async function UIWindowThemeDialog (options) {
 const UIWindowThemeDialog = async function UIWindowThemeDialog (options) {
     options = options ?? {};
     options = options ?? {};
@@ -114,6 +115,12 @@ const UIWindowThemeDialog = async function UIWindowThemeDialog (options) {
             svc_theme.reset();
             svc_theme.reset();
         })
         })
         ;
         ;
+    Button({ label: i18n('reset_colors') })
+        .appendTo(w_body)
+        .onPress(() => {
+            UIWindowColorPicker();
+        })
+        ;
 
 
     Slider({
     Slider({
         label: i18n('hue'),
         label: i18n('hue'),

+ 110 - 0
src/css/style.css

@@ -3781,4 +3781,114 @@ label {
     flex-direction: row;
     flex-direction: row;
     gap: 10px;
     gap: 10px;
     justify-content: flex-end;
     justify-content: flex-end;
+}
+
+.taskmgr {
+    box-sizing: border-box;
+    /* could have been avoided with box-sizing: border-box */
+    height: calc(100% - 30px);
+    display: flex;
+    flex-direction: column;
+
+    --scale: 2pt;
+    --line-color: #6e6e6ebd;
+}
+
+.taskmgr * {
+    box-sizing: border-box;
+}
+
+.taskmgr table {
+    border-collapse: collapse;
+}
+
+.taskmgr-taskarea {
+    flex-grow: 1;
+    display: flex;
+    flex-direction: column;
+    background-color: rgba(255,255,255,0.8);
+    border: 2px inset rgba(127, 127, 127, 0.3);
+    overflow: auto;
+}
+
+.taskmgr-taskarea table thead {
+}
+
+.taskmgr-taskarea table th {
+    -webkit-box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2);
+    box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2);
+    backdrop-filter: blur(2px);
+    position: sticky;
+    z-index: 100;
+    padding: 0;
+    top: 0;
+    background-color: hsla(0, 0%, 100%, 0.8);
+    text-align: left;
+}
+
+.taskmgr-taskarea table th > span {
+    display: inline-block;
+    width: 100%;
+    /* we set borders on this span because */
+    /* borders fly away from sticky headers */
+    border-bottom: 1px solid #e0e0e0;
+
+    /* padding order: top right bottom left */
+    padding:
+        calc(10 * var(--scale))
+        calc(2.5 * var(--scale))
+        calc(5 * var(--scale))
+        calc(2.5 * var(--scale));
+}
+
+.taskmgr-taskarea table th:not(:last-of-type) > span {
+    /* we set borders on this span because */
+    /* borders fly away from sticky headers */
+    border-right: 1px solid #e0e0e0;
+}
+
+.taskmgr-taskarea table td {
+    border-bottom: 1px solid #e0e0e0;
+}
+
+.taskmgr-taskarea table td > span {
+    padding: 0 calc(2.5 * var(--scale));
+}
+
+.taskmgr-indentcell {
+    position: relative;
+    align-items: right;
+    width: calc(10 * var(--scale));
+    height: calc(10 * var(--scale));
+}
+
+.taskmgr-indentcell-trunk {
+    position: absolute;
+    top: 0;
+    left: calc(5 * var(--scale));
+    width: calc(5 * var(--scale));
+    height: calc(10 * var(--scale));
+    border-left: 2px solid var(--line-color);
+}
+
+.taskmgr-indentcell-branch {
+    position: absolute;
+    top: 0;
+    left: calc(5 * var(--scale));
+    width: calc(5 * var(--scale));
+    height: calc(5 * var(--scale));
+    border-left: 2px solid var(--line-color);
+    border-bottom: 2px solid var(--line-color);
+    border-radius: 0 0 0 calc(2.5 * var(--scale));
+}
+
+.taskmgr-task {
+    display: flex;
+    height: calc(10 * var(--scale));
+    line-height: calc(10 * var(--scale));
+}
+
+.taskmgr-task-title {
+    flex-grow: 1;
+    padding-left: calc(2.5 * var(--scale));
 }
 }

+ 91 - 0
src/definitions.js

@@ -19,3 +19,94 @@
 export class Service {
 export class Service {
     //
     //
 };
 };
+
+export const PROCESS_INITIALIZING = { i18n_key: 'initializing' };
+export const PROCESS_RUNNING = { i18n_key: 'running' };
+
+// Something is cloning these objects, so '===' checks don't work.
+// To work around this, the `i` property is used to compare them.
+export const END_SOFT = { i: 0, end: true, i18n_key: 'end_soft' };
+export const END_HARD = { i: 1, end: true, i18n_key: 'end_hard' };
+
+export class Process {
+    constructor ({ uuid, parent, name, meta }) {
+        this.uuid = uuid;
+        this.parent = parent;
+        this.name = name;
+        this.meta = meta;
+        this.references = {};
+
+        this.status = PROCESS_INITIALIZING;
+
+        this._construct();
+    }
+    _construct () {}
+
+    chstatus (status) {
+        this.status = status;
+    }
+
+    is_init () {}
+
+    signal (sig) {
+        this._signal(sig);
+    }
+
+    get type () {
+        const _to_type_name = (name) => {
+            return name.replace(/Process$/, '').toLowerCase();
+        };
+        return this.type_ || _to_type_name(this.constructor.name) ||
+            'invalid'
+    }
+};
+
+export class InitProcess extends Process {
+    static created_ = false;
+
+    is_init () { return true; }
+
+    _construct () {
+        this.name = 'Puter';
+
+        if (InitProcess.created_) {
+            throw new Error('InitProccess already created');
+        }
+
+        InitProcess.created_ = true;
+    }
+
+    _signal (sig) {
+        const svc_process = globalThis.services.get('process');
+        for ( const process of svc_process.processes ) {
+            if ( process === this ) continue;
+            process.signal(sig);
+        }
+
+        if ( sig.i !== END_HARD.i ) return;
+
+        // Currently this is the only way to terminate `init`.
+        window.location.reload();
+    }
+}
+
+export class PortalProcess extends Process {
+    _construct () { this.type_ = 'app' }
+    _signal (sig) {
+        if ( sig.end ) {
+            $(this.references.el_win).close({
+                bypass_iframe_messaging: sig.i === END_HARD.i
+            });
+        }
+    }
+};
+export class PseudoProcess extends Process {
+    _construct () { this.type_ = 'ui' }
+    _signal (sig) {
+        if ( sig.end ) {
+            $(this.references.el_win).close({
+                bypass_iframe_messaging: sig.i === END_HARD.i
+            });
+        }
+    }
+};

+ 48 - 3
src/helpers.js

@@ -36,6 +36,7 @@ import update_username_in_gui from './helpers/update_username_in_gui.js';
 import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js';
 import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js';
 import content_type_to_icon from './helpers/content_type_to_icon.js';
 import content_type_to_icon from './helpers/content_type_to_icon.js';
 import UIWindowDownloadDirProg from './UI/UIWindowDownloadDirProg.js';
 import UIWindowDownloadDirProg from './UI/UIWindowDownloadDirProg.js';
+import { PROCESS_RUNNING, PortalProcess, PseudoProcess } from "./definitions.js";
 
 
 window.is_auth = ()=>{
 window.is_auth = ()=>{
     if(localStorage.getItem("auth_token") === null || auth_token === null)
     if(localStorage.getItem("auth_token") === null || auth_token === null)
@@ -1680,10 +1681,30 @@ window.launch_app = async (options)=>{
         // add file_signature to options
         // add file_signature to options
         file_signature = file_signature.items;
         file_signature = file_signature.items;
     }
     }
+
+    // -----------------------------------
+    // Create entry to track the "portal"
+    // (portals are processese in Puter's GUI)
+    // -----------------------------------
+
+    let el_win;
+    let process;
+
     //------------------------------------
     //------------------------------------
     // Explorer
     // Explorer
     //------------------------------------
     //------------------------------------
     if(options.name === 'explorer'){
     if(options.name === 'explorer'){
+        process = new PseudoProcess({
+            uuid,
+            name: 'explorer',
+            parent: options.parent_instance_id,
+            meta: {
+                launch_options: options,
+                app_info: app_info,
+            }
+        });
+        const svc_process = globalThis.services.get('process');
+        svc_process.register(process);
         if(options.path === window.home_path){
         if(options.path === window.home_path){
             title = 'Home';
             title = 'Home';
             icon = window.icons['folder-home.svg'];
             icon = window.icons['folder-home.svg'];
@@ -1697,7 +1718,7 @@ window.launch_app = async (options)=>{
             title = path.dirname(options.path);
             title = path.dirname(options.path);
 
 
         // open window
         // open window
-        UIWindow({
+        el_win = UIWindow({
             element_uuid: uuid,
             element_uuid: uuid,
             icon: icon,
             icon: icon,
             path: options.path ?? window.home_path,
             path: options.path ?? window.home_path,
@@ -1713,6 +1734,18 @@ window.launch_app = async (options)=>{
     // All other apps
     // All other apps
     //------------------------------------
     //------------------------------------
     else{
     else{
+        process = new PortalProcess({
+            uuid,
+            name: app_info.name,
+            parent: options.parent_instance_id,
+            meta: {
+                launch_options: options,
+                app_info: app_info,
+            }
+        });
+        const svc_process = globalThis.services.get('process');
+        svc_process.register(process);
+
         //-----------------------------------
         //-----------------------------------
         // iframe_url
         // iframe_url
         //-----------------------------------
         //-----------------------------------
@@ -1808,7 +1841,7 @@ window.launch_app = async (options)=>{
 
 
         console.log('backgrounded??', app_info.background);
         console.log('backgrounded??', app_info.background);
 
 
-        const el_win = UIWindow({
+        el_win = UIWindow({
             element_uuid: uuid,
             element_uuid: uuid,
             title: title,
             title: title,
             iframe_url: iframe_url.href,
             iframe_url: iframe_url.href,
@@ -1826,7 +1859,7 @@ window.launch_app = async (options)=>{
             is_fullpage: options.is_fullpage,
             is_fullpage: options.is_fullpage,
             ...window_options,
             ...window_options,
             show_in_taskbar: app_info.background ? false : window_options?.show_in_taskbar,
             show_in_taskbar: app_info.background ? false : window_options?.show_in_taskbar,
-        }); 
+        });
 
 
         if ( ! app_info.background ) {
         if ( ! app_info.background ) {
             $(el_win).show();
             $(el_win).show();
@@ -1859,6 +1892,18 @@ window.launch_app = async (options)=>{
             })
             })
         }
         }
     }
     }
+
+    (async () => {
+        const el = await el_win;
+        console.log('RESOV', el);
+        $(el).on('remove', () => {
+            const svc_process = globalThis.services.get('process');
+            svc_process.unregister(process.uuid);
+        });
+
+        process.references.el_win = el;
+        process.chstatus(PROCESS_RUNNING);
+    })();
 }
 }
 
 
 window.open_item = async function(options){
 window.open_item = async function(options){

+ 16 - 0
src/i18n/translations/en.js

@@ -39,7 +39,9 @@ const en = {
         change_password: "Change Password",
         change_password: "Change Password",
         change_ui_colors: "Change UI Colors",
         change_ui_colors: "Change UI Colors",
         change_username: "Change Username",
         change_username: "Change Username",
+        close: 'Close',
         close_all_windows: "Close All Windows",
         close_all_windows: "Close All Windows",
+        close_all_windows_confirm: "Are you sure you want to close all windows?",
         close_all_windows_and_log_out: 'Close Windows and Log Out',
         close_all_windows_and_log_out: 'Close Windows and Log Out',
         change_always_open_with: "Do you want to always open this type of file with",
         change_always_open_with: "Do you want to always open this type of file with",
         color: 'Color',
         color: 'Color',
@@ -88,11 +90,15 @@ const en = {
         empty_trash: 'Empty Trash',
         empty_trash: 'Empty Trash',
         empty_trash_confirmation: `Are you sure you want to permanently delete the items in Trash?`,
         empty_trash_confirmation: `Are you sure you want to permanently delete the items in Trash?`,
         emptying_trash: 'Emptying Trash…',
         emptying_trash: 'Emptying Trash…',
+        end_hard: "End Hard",
+        end_process_force_confirm: "Are you sure you want to force-quit this process?",
+        end_soft: "End Soft",
         enter_password_to_confirm_delete_user: "Enter your password to confirm account deletion",
         enter_password_to_confirm_delete_user: "Enter your password to confirm account deletion",
         feedback: "Feedback",
         feedback: "Feedback",
         feedback_c2a: "Please use the form below to send us your feedback, comments, and bug reports.",
         feedback_c2a: "Please use the form below to send us your feedback, comments, and bug reports.",
         feedback_sent_confirmation: "Thank you for contacting us. If you have an email associated with your account, you will hear back from us as soon as possible.",
         feedback_sent_confirmation: "Thank you for contacting us. If you have an email associated with your account, you will hear back from us as soon as possible.",
         fit: "Fit",
         fit: "Fit",
+        force_quit: 'Force Quit',
         forgot_pass_c2a: "Forgot password?",
         forgot_pass_c2a: "Forgot password?",
         from: "From",
         from: "From",
         general: "General",
         general: "General",
@@ -154,6 +160,11 @@ const en = {
         privacy: "Privacy",
         privacy: "Privacy",
         proceed_to_login: 'Proceed to login',
         proceed_to_login: 'Proceed to login',
         proceed_with_account_deletion: "Proceed with Account Deletion",
         proceed_with_account_deletion: "Proceed with Account Deletion",
+        process_status_initializing: "Initializing",
+        process_status_running: "Running",
+        process_type_app: 'App',
+        process_type_init: 'Init',
+        process_type_ui: 'UI',
         properties: "Properties",
         properties: "Properties",
         publish: "Publish",
         publish: "Publish",
         publish_as_website: 'Publish as website',
         publish_as_website: 'Publish as website',
@@ -172,6 +183,7 @@ const en = {
         replace_all: 'Replace All',
         replace_all: 'Replace All',
         resend_confirmation_code: "Re-send Confirmation Code",
         resend_confirmation_code: "Re-send Confirmation Code",
         reset_colors: "Reset Colors",
         reset_colors: "Reset Colors",
+        restart_puter_confirm: "Are you sure you want to restart Puter?",
         restore: "Restore",
         restore: "Restore",
         saturation: 'Saturation',
         saturation: 'Saturation',
         save_account: 'Save account',
         save_account: 'Save account',
@@ -203,6 +215,10 @@ const en = {
         storage_usage: "Storage Usage",
         storage_usage: "Storage Usage",
         storage_puter_used: 'used by Puter',
         storage_puter_used: 'used by Puter',
         taking_longer_than_usual: 'Taking a little longer than usual. Please wait...',
         taking_longer_than_usual: 'Taking a little longer than usual. Please wait...',
+        task_manager: "Task Manager",
+        taskmgr_header_name: "Name",
+        taskmgr_header_status: "Status",
+        taskmgr_header_type: "Type",
         terms: "Terms",
         terms: "Terms",
         text_document: 'Text document',
         text_document: 'Text document',
         tos_fineprint: `By clicking 'Create Free Account' you agree to Puter's {{link=terms}}Terms of Service{{/link}} and {{link=privacy}}Privacy Policy{{/link}}.`,
         tos_fineprint: `By clicking 'Create Free Account' you agree to Puter's {{link=terms}}Terms of Service{{/link}} and {{link=privacy}}Privacy Policy{{/link}}.`,

+ 10 - 0
src/initgui.js

@@ -36,6 +36,9 @@ import PuterDialog from './UI/PuterDialog.js';
 import determine_active_container_parent from './helpers/determine_active_container_parent.js';
 import determine_active_container_parent from './helpers/determine_active_container_parent.js';
 import { ThemeService } from './services/ThemeService.js';
 import { ThemeService } from './services/ThemeService.js';
 import { BroadcastService } from './services/BroadcastService.js';
 import { BroadcastService } from './services/BroadcastService.js';
+import UIWindowTaskManager from './UI/UIWindowTaskManager.js';
+import { ProcessService } from './services/ProcessService.js';
+import { PROCESS_RUNNING } from './definitions.js';
 
 
 const launch_services = async function () {
 const launch_services = async function () {
     const services_l_ = [];
     const services_l_ = [];
@@ -51,10 +54,17 @@ const launch_services = async function () {
 
 
     register('broadcast', new BroadcastService());
     register('broadcast', new BroadcastService());
     register('theme', new ThemeService());
     register('theme', new ThemeService());
+    register('process', new ProcessService())
 
 
     for (const [_, instance] of services_l_) {
     for (const [_, instance] of services_l_) {
         await instance._init();
         await instance._init();
     }
     }
+
+    // Set init process status
+    {
+        const svc_process = globalThis.services.get('process');
+        svc_process.get_init().chstatus(PROCESS_RUNNING);
+    }
 };
 };
 
 
 window.initgui = async function(){
 window.initgui = async function(){

+ 70 - 0
src/services/ProcessService.js

@@ -0,0 +1,70 @@
+import { InitProcess, Service } from "../definitions.js";
+
+// The NULL UUID is also the UUID for the init process.
+const NULL_UUID = '00000000-0000-0000-0000-000000000000';
+
+export class ProcessService extends Service {
+    async _init () {
+        this.processes = [];
+        this.processes_map = new Map();
+        this.uuid_to_treelist = new Map();
+
+        const root = new InitProcess({
+            uuid: NULL_UUID,
+        });
+        this.register_(root);
+    }
+
+    get_init () {
+        return this.processes_map.get(NULL_UUID);
+    }
+
+    get_children_of (uuid) {
+        if ( ! uuid ) {
+            uuid = NULL_UUID;
+        }
+
+        return this.uuid_to_treelist.get(uuid);
+    }
+
+    register (process) {
+        this.register_(process);
+        this.attach_to_parent_(process);
+    }
+
+    register_ (process) {
+        this.processes.push(process);
+        this.processes_map.set(process.uuid, process);
+        this.uuid_to_treelist.set(process.uuid, []);
+    }
+
+    attach_to_parent_ (process) {
+        process.parent = process.parent ?? NULL_UUID;
+        const parent_list = this.uuid_to_treelist.get(process.parent);
+        parent_list.push(process);
+    }
+
+    unregister (uuid) {
+        const process = this.processes_map.get(uuid);
+        if ( ! process ) {
+            throw new Error(`Process with uuid ${uuid} not found`);
+        }
+
+        this.processes_map.delete(uuid);
+        this.processes.splice(this.processes.indexOf(process), 1);
+
+        const parent_list = this.uuid_to_treelist.get(process.parent);
+        parent_list.splice(parent_list.indexOf(process), 1);
+
+        const children = this.uuid_to_treelist.get(process.uuid);
+
+        delete this.uuid_to_treelist[process.uuid];
+        this.processes.splice(this.processes.indexOf(process), 1);
+
+        // Transfer children to init process
+        for ( const child of children ) {
+            child.parent = NULL_UUID;
+            this.attach_to_parent_(child);
+        }
+    }
+}