Nariman Jelveh hai 11 meses
pai
achega
0c4728fbc1
Modificáronse 6 ficheiros con 726 adicións e 749 borrados
  1. 2 1
      puter-gui.json
  2. 7 0
      run-selfhosted.js
  3. 0 691
      src/initgui.js
  4. 710 0
      src/keyboard.js
  5. 0 56
      src/static-assets.js
  6. 7 1
      utils.js

+ 2 - 1
puter-gui.json

@@ -29,7 +29,8 @@
             "/src/helpers.js",
             "/src/IPC.js",
             "/src/globals.js",
-            "/src/i18n/i18n.js"
+            "/src/i18n/i18n.js",
+            "/src/keyboard.js"
         ]
     },
     "bundle": {

+ 7 - 0
run-selfhosted.js

@@ -52,6 +52,13 @@ const surrounding_box = (col, lines) => {
     }
 }
 
+// Annoying polyfill for inconsistency in different node versions
+if ( ! import.meta.filename ) {
+    Object.defineProperty(import.meta, 'filename', {
+        get: import.meta.url.slice('file://'.length),
+    })
+}
+
 const main = async () => {
     const {
         Kernel,

+ 0 - 691
src/initgui.js

@@ -1148,697 +1148,6 @@ window.initgui = async function(){
         window.active_element = e.target;
     });
 
-    $(document).bind('keydown', async function(e){
-        const focused_el = document.activeElement;
-
-        //-----------------------------------------------------------------------
-        // ← ↑ → ↓: an arrow key is pressed
-        //-----------------------------------------------------------------------
-        if((e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
-            // ----------------------------------------------
-            // Launch menu is open
-            // ----------------------------------------------
-            if($('.launch-popover').length > 0){
-                // If no item is selected and down arrow is pressed, select the first item
-                if($('.launch-popover .start-app-card.launch-app-selected').length === 0 && (e.which === 40)){
-                    $('.launch-popover .start-app-card:visible').first().addClass('launch-app-selected');
-                    // blur search input
-                    $('.launch-popover .launch-search').blur();
-                    return false;
-                }
-                // if search input is focused and left or right arrow is pressed, return false
-                else if($('.launch-popover .launch-search').is(':focus') && (e.which === 37 || e.which === 39)){
-                    return false;
-                }
-                else{
-                    // If an item is already selected, move the selection up, down, left or right
-                    let selected_item = $('.launch-popover .start-app-card.launch-app-selected').get(0);
-                    let selected_item_index = $('.launch-popover .start-app-card:visible').index(selected_item);
-                    let selected_item_row = Math.floor(selected_item_index / 5);
-                    let selected_item_col = selected_item_index % 5;
-                    let selected_item_row_count = Math.ceil($('.launch-popover .start-app-card:visible').length / 5);
-                    let selected_item_col_count = 5;
-                    let new_selected_item_index = selected_item_index;
-                    let new_selected_item_row = selected_item_row;
-                    let new_selected_item_col = selected_item_col;
-                    let new_selected_item;
-
-                    // if up arrow is pressed
-                    if(e.which === 38){
-                        // if this item is in the first row, up arrow should bring the focus back to the search input
-                        if(selected_item_row === 0){
-                            $('.launch-popover .launch-search').focus();
-                            // unselect all items
-                            $('.launch-popover .start-app-card.launch-app-selected').removeClass('launch-app-selected');
-                            // bring cursor to the end of the search input
-                            $('.launch-popover .launch-search').val($('.launch-popover .launch-search').val());
-
-                            return false;
-                        }
-                        // if this item is not in the first row, move the selection up
-                        else{
-                            new_selected_item_row = selected_item_row - 1;
-                            if(new_selected_item_row < 0)
-                                new_selected_item_row = selected_item_row_count - 1;
-                        }
-                    }
-                    // if down arrow is pressed
-                    else if(e.which === 40){
-                        new_selected_item_row = selected_item_row + 1;
-                        if(new_selected_item_row >= selected_item_row_count)
-                            new_selected_item_row = 0;
-                    }
-                    // if left arrow is pressed
-                    else if(e.which === 37){
-                        new_selected_item_col = selected_item_col - 1;
-                        if(new_selected_item_col < 0)
-                            new_selected_item_col = selected_item_col_count - 1;
-                    }
-                    // if right arrow is pressed
-                    else if(e.which === 39){
-                        new_selected_item_col = selected_item_col + 1;
-                        if(new_selected_item_col >= selected_item_col_count)
-                            new_selected_item_col = 0;
-                    }
-                    new_selected_item_index = (new_selected_item_row * selected_item_col_count) + new_selected_item_col;
-                    new_selected_item = $('.launch-popover .start-app-card:visible').get(new_selected_item_index);
-                    $(selected_item).removeClass('launch-app-selected');
-                    $(new_selected_item).addClass('launch-app-selected');
-
-                    // make sure the selected item is visible in the popover by scrolling the popover
-                    let popover = $('.launch-popover').get(0);
-                    let popover_height = $('.launch-popover').height();
-                    let popover_scroll_top = popover.getBoundingClientRect().top;
-                    let popover_scroll_bottom = popover_scroll_top + popover_height;
-                    let selected_item_top = new_selected_item.getBoundingClientRect().top;
-                    let selected_item_bottom = new_selected_item.getBoundingClientRect().bottom;
-                    let isVisible = (selected_item_top >= popover_scroll_top) && (selected_item_bottom <= popover_scroll_top + popover_height);
-
-                    if ( ! isVisible ) {
-                        const scrollTop = selected_item_top - popover_scroll_top;
-                        const scrollBot = selected_item_bottom - popover_scroll_bottom;
-                        if (Math.abs(scrollTop) < Math.abs(scrollBot)) {
-                            popover.scrollTop += scrollTop;
-                        } else {
-                            popover.scrollTop += scrollBot;
-                        }
-                    }
-                    return false;
-                }
-            }
-            // ----------------------------------------------
-            // A context menu is open
-            // ----------------------------------------------
-            else if($('.context-menu').length > 0){
-                // if no item is selected and down arrow is pressed, select the first item
-                if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 40)){
-                    let selected_item = $('.context-menu-active .context-menu-item').get(0);
-                    window.select_ctxmenu_item(selected_item);
-                    return false;
-                }
-                // if no item is selected and up arrow is pressed, select the last item
-                else if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 38)){
-                    let selected_item = $('.context-menu .context-menu-item').get($('.context-menu .context-menu-item').length - 1);
-                    window.select_ctxmenu_item(selected_item);
-                    return false;
-                }
-                // if an item is selected and down arrow is pressed, select the next enabled item
-                else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 40)){
-                    let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
-                    let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
-                    let new_selected_item_index = selected_item_index + 1;
-                    let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
-                    while($(new_selected_item).hasClass('context-menu-item-disabled')){
-                        new_selected_item_index = new_selected_item_index + 1;
-                        new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
-                    }
-                    window.select_ctxmenu_item(new_selected_item);
-                    return false;
-                }
-                // if an item is selected and up arrow is pressed, select the previous enabled item
-                else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 38)){
-                    let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
-                    let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
-                    let new_selected_item_index = selected_item_index - 1;
-                    let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
-                    while($(new_selected_item).hasClass('context-menu-item-disabled')){
-                        new_selected_item_index = new_selected_item_index - 1;
-                        new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
-                    }
-                    window.select_ctxmenu_item(new_selected_item);
-                    return false;
-                }
-                // if right arrow is pressed, open the submenu by triggering a mouseover event
-                else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 39)){
-                    const selected_item = $('.context-menu-active .context-menu-item-active').get(0);
-                    $(selected_item).trigger('mouseover');
-                    // if the submenu is open, select the first item in the submenu
-                    if($(selected_item).hasClass('context-menu-item-submenu') === true){
-                        $(selected_item).removeClass('context-menu-item-active');
-                        $(selected_item).addClass('context-menu-item-active-blurred');
-                        window.select_ctxmenu_item($('.context-menu[data-is-submenu="true"] .context-menu-item').get(0));
-                    }
-                    return false;
-                }
-                // if left arrow is pressed on a submenu, close the submenu
-                else if($('.context-menu-active[data-is-submenu="true"]').length > 0 && (e.which === 37)){
-                    // get parent menu
-                    let parent_menu_id = $('.context-menu-active[data-is-submenu="true"]').data('parent-id');
-                    let parent_menu = $('.context-menu[data-element-id="' + parent_menu_id + '"]');
-                    // remove the submenu
-                    $('.context-menu-active[data-is-submenu="true"]').remove();
-                    // activate the parent menu
-                    $(parent_menu).addClass('context-menu-active');
-                    // select the item that opened the submenu
-                    let selected_item = $('.context-menu-active .context-menu-item-active-blurred').get(0);
-                    $(selected_item).removeClass('context-menu-item-active-blurred');
-                    $(selected_item).addClass('context-menu-item-active');
-
-                    return false;
-                }
-                // if enter is pressed, trigger a click event on the selected item
-                else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
-                    let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
-                    $(selected_item).trigger('click');
-                    return false;
-                }
-            }
-            // ----------------------------------------------
-            // Navigate items in the active item container
-            // ----------------------------------------------
-            else if(!$(focused_el).is('input') && !$(focused_el).is('textarea') && (e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
-                let item_width = 110, item_height = 110, selected_item;
-                // select first item in container if none is selected
-                if($(window.active_item_container).find('.item-selected').length === 0){
-                    selected_item = $(window.active_item_container).find('.item').get(0);
-                    window.active_element = selected_item;
-                    $(window.active_item_container).find('.item-selected').removeClass('item-selected');
-                    $(selected_item).addClass('item-selected');
-                    return false;
-                }
-                // if Shift key is pressed and ONE item is already selected, pick that item
-                else if($(window.active_item_container).find('.item-selected').length === 1 && e.shiftKey){
-                    selected_item = $(window.active_item_container).find('.item-selected').get(0);
-                }
-                // if Shift key is pressed and MORE THAN ONE item is selected, pick the latest active item
-                else if($(window.active_item_container).find('.item-selected').length > 1 && e.shiftKey){
-                    selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
-                }
-                // otherwise if an item is selected, pick that item
-                else if($(window.active_item_container).find('.item-selected').length === 1){
-                    selected_item = $(window.active_item_container).find('.item-selected').get(0);
-                }
-                else{
-                    selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
-                }
-
-                // override the default behavior of ctrl/meta key
-                // in some browsers ctrl/meta key + arrow keys will scroll the page or go back/forward in history
-                if(e.ctrlKey || e.metaKey){
-                    e.preventDefault();
-                    e.stopPropagation();
-                }
-
-                // get the position of the selected item
-                let active_el_pos = $(selected_item).hasClass('item') ? selected_item.getBoundingClientRect() : $(selected_item).closest('.item').get(0).getBoundingClientRect();
-                let xpos = active_el_pos.left + item_width/2;
-                let ypos = active_el_pos.top + item_height/2;
-                // these hold next item's position on the grid
-                let x_nxtpos, y_nxtpos;
-                // these hold the amount of pixels to scroll the container
-                let x_scroll = 0, y_scroll = 0;
-                // determine next item's position on the grid
-                // left
-                if(e.which === 37){
-                    x_nxtpos = (xpos - item_width) > 0 ? (xpos - item_width) : 0;
-                    y_nxtpos = (ypos);
-                    x_scroll = (item_width / 2);
-                }
-                // up
-                else if(e.which === 38){
-                    x_nxtpos = (xpos);
-                    y_nxtpos = (ypos - item_height) > 0 ? (ypos - item_height) : 0;
-                    y_scroll = -1 * (item_height / 2);
-                }
-                // right
-                else if(e.which === 39){
-                    x_nxtpos = (xpos + item_width);
-                    y_nxtpos = (ypos);
-                    x_scroll = -1 * (item_width / 2);
-                }
-                // down
-                else if(e.which === 40){
-                    x_nxtpos = (xpos);
-                    y_nxtpos = (ypos + item_height);
-                    y_scroll = (item_height / 2);
-                }
-
-                let elements_at_next_pos = document.elementsFromPoint(x_nxtpos, y_nxtpos);
-                let next_item;
-                for (let index = 0; index < elements_at_next_pos.length; index++) {
-                    const elem_at_next_pos = elements_at_next_pos[index];
-                    if($(elem_at_next_pos).hasClass('item') && $(elem_at_next_pos).closest('.item-container').is(window.active_item_container)){
-                        next_item = elem_at_next_pos;
-                        break;
-                    }
-                }
-
-                if(next_item){
-                    selected_item = next_item;
-                    window.active_element = next_item;
-                    // if ctrl or meta key is not pressed, unselect all items
-                    if(!e.shiftKey){
-                        $(window.active_item_container).find('.item').removeClass('item-selected');
-                    }
-                    $(next_item).addClass('item-selected');
-                    window.latest_selected_item = next_item;
-                    // scroll to the selected item only if this was a down or up move
-                    if(e.which === 38 || e.which === 40)
-                        next_item.scrollIntoView(false);
-                }
-            }
-        }
-        //-----------------------------------------------------------------------
-        // if the Esc key is pressed on a FileDialog/Alert, close that FileDialog/Alert
-        //-----------------------------------------------------------------------
-        else if(
-            // escape key code
-            e.which === 27 &&
-            // active window must be a FileDialog or Alert
-            ($('.window-active').hasClass('window-filedialog') || $('.window-active').hasClass('window-alert')) &&
-            // either don't close if an input is focused or if the input is the filename input
-            ((!$(focused_el).is('input') && !$(focused_el).is('textarea')) || $(focused_el).hasClass('savefiledialog-filename'))
-            ){
-            // close the FileDialog
-            $('.window-active').close();
-        }
-        //-----------------------------------------------------------------------
-        // if the Esc key is pressed on a Window Navbar Editor, deactivate the editor
-        //-----------------------------------------------------------------------
-        else if( e.which === 27 && $(focused_el).hasClass('window-navbar-path-input')){
-            $(focused_el).blur();
-            $(focused_el).val($(focused_el).closest('.window').attr('data-path'));
-            $(focused_el).attr('data-path', $(focused_el).closest('.window').attr('data-path'));
-        }
-
-        //-----------------------------------------------------------------------
-        // Esc key should:
-        //      - always close open context menus
-        //      - close the Launch Popover if it's open
-        //-----------------------------------------------------------------------
-        if( e.which === 27){
-            // close open context menus
-            $('.context-menu').remove();
-
-            // close the Launch Popover if it's open
-            $(".launch-popover").closest('.popover').fadeOut(200, function(){
-                $(".launch-popover").closest('.popover').remove();
-            });
-        }
-    })
-
-    $(document).bind('keydown', async function(e){
-        const focused_el = document.activeElement;
-        //-----------------------------------------------------------------------
-        // Shift+Delete (win)/ option+command+delete (Mac) key pressed
-        // Permanent delete bypassing trash after alert
-        //-----------------------------------------------------------------------
-        if((e.keyCode === 46 && e.shiftKey) || (e.altKey && e.metaKey && e.keyCode === 8)) {
-            let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected`);
-            if($selected_items.length > 0){
-                const alert_resp = await UIAlert({
-                    message: i18n('confirm_delete_multiple_items'),
-                    buttons:[
-                        {
-                            label: i18n('delete'),
-                            type: 'primary',
-                        },
-                        {
-                            label: i18n('cancel')
-                        },
-                    ]
-                })
-                if((alert_resp) === 'Delete'){
-                    for (let index = 0; index < $selected_items.length; index++) {
-                        const element = $selected_items[index];
-                        await window.delete_item(element);
-                    }
-                }
-            }
-            return false;
-        }
-        //-----------------------------------------------------------------------
-        // Delete (win)/ ctrl+delete (Mac) / cmd+delete (Mac) key pressed
-        // Permanent delete from trash after alert or move to trash
-        //-----------------------------------------------------------------------
-        if(e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey))) {
-            // permanent delete?
-            let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected[data-path^="${window.trash_path + '/'}"]`);
-            if($selected_items.length > 0){
-                const alert_resp = await UIAlert({
-                    message: i18n('confirm_delete_multiple_items'),
-                    buttons:[
-                        {
-                            label: i18n('delete'),
-                            type: 'primary',
-                        },
-                        {
-                            label: i18n('cancel')
-                        },
-                    ]
-                })
-                if((alert_resp) === 'Delete'){
-                    for (let index = 0; index < $selected_items.length; index++) {
-                        const element = $selected_items[index];
-                        await window.delete_item(element);
-                    }
-                    const trash = await puter.fs.stat(window.trash_path);
-                    if(window.socket){
-                        window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
-                    }
-
-                    if(trash.is_empty){
-                        $(`[data-app="trash"]`).find('.taskbar-icon > img').attr('src', window.icons['trash.svg']);
-                        $(`.item[data-path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
-                        $(`.window[data-path="${html_encode(window.trash_path)}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
-                    }
-                }
-            }
-            // regular delete?
-            else{
-                $selected_items = $(window.active_element).closest('.item-container').find('.item-selected');
-                if($selected_items.length > 0){
-                    // Only delete the items if we're not renaming one.
-                    if ($selected_items.children('.item-name-editor-active').length === 0) {
-                        window.move_items($selected_items, window.trash_path);
-                    }
-                }
-            }
-            return false;
-        }
-
-        //-----------------------------------------------------------------------
-        // A letter or number is pressed and there is no context menu open: search items by name
-        //-----------------------------------------------------------------------
-        if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length === 0){
-            if(window.keypress_item_seach_term !== '')
-                clearTimeout(window.keypress_item_seach_buffer_timeout);
-
-            window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
-                window.keypress_item_seach_term = '';
-            }, 700);
-
-            window.keypress_item_seach_term += e.key.toLocaleLowerCase();
-
-            let matches= [];
-            const selected_items = $(window.active_item_container).find(`.item-selected`).not('.item-disabled').first();
-
-            // if one item is selected and the selected item matches the search term, don't continue search and select this item again
-            if(selected_items.length === 1 && $(selected_items).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
-                return false;
-            }
-
-            // search for matches
-            let haystack = $(window.active_item_container).find(`.item`).not('.item-disabled');
-            for(let j=0; j < haystack.length; j++){
-                if($(haystack[j]).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
-                    matches.push(haystack[j])
-                }
-            }
-
-            if(matches.length > 0){
-                // if there are multiple matches and an item is already selected, remove all matches before the selected item
-                if(selected_items.length > 0 && matches.length > 1){
-                    let match_index;
-                    for(let i=0; i < matches.length - 1; i++){
-                        if($(matches[i]).is(selected_items)){
-                            match_index = i;
-                            break;
-                        }
-                    }
-                    matches.splice(0, match_index+1);
-                }
-                // deselect all selected sibling items
-                $(window.active_item_container).find(`.item-selected`).removeClass('item-selected');
-                // select matching item
-                $(matches[0]).not('.item-disabled').addClass('item-selected');
-                matches[0].scrollIntoView(false);
-                window.update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
-            }
-
-            return false;
-        }
-        //-----------------------------------------------------------------------
-        // A letter or number is pressed and there is a context menu open: search items by name
-        //-----------------------------------------------------------------------
-        else if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length > 0){
-            if(window.keypress_item_seach_term !== '')
-                clearTimeout(window.keypress_item_seach_buffer_timeout);
-
-            window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
-                window.keypress_item_seach_term = '';
-            }, 700);
-
-            window.keypress_item_seach_term += e.key.toLocaleLowerCase();
-
-            let matches= [];
-            const selected_items = $('.context-menu').find(`.context-menu-item-active`).first();
-
-            // if one item is selected and the selected item matches the search term, don't continue search and select this item again
-            if(selected_items.length === 1 && $(selected_items).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
-                return false;
-            }
-
-            // search for matches
-            let haystack = $('.context-menu-active').find(`.context-menu-item`);
-            for(let j=0; j < haystack.length; j++){
-                if($(haystack[j]).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
-                    matches.push(haystack[j])
-                }
-            }
-
-            if(matches.length > 0){
-                // if there are multiple matches and an item is already selected, remove all matches before the selected item
-                if(selected_items.length > 0 && matches.length > 1){
-                    let match_index;
-                    for(let i=0; i < matches.length - 1; i++){
-                        if($(matches[i]).is(selected_items)){
-                            match_index = i;
-                            break;
-                        }
-                    }
-                    matches.splice(0, match_index+1);
-                }
-                // deselect all selected sibling items
-                $('.context-menu').find(`.context-menu-item-active`).removeClass('context-menu-item-active');
-                // select matching item
-                $(matches[0]).addClass('context-menu-item-active');
-                // matches[0].scrollIntoView(false);
-                // update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
-            }
-
-            return false;
-        }
-    })
-
-    $(document).bind("keyup keydown", async function(e){
-        const focused_el = document.activeElement;
-        //-----------------------------------------------------------------------------
-        // Override ctrl/cmd + s/o
-        //-----------------------------------------------------------------------------
-        if((e.ctrlKey || e.metaKey) && (e.which === 83 || e.which === 79)){
-            e.preventDefault()
-            return false;
-        }
-        //-----------------------------------------------------------------------------
-        // Select All
-        // ctrl/command + a, will select all items on desktop and windows
-        //-----------------------------------------------------------------------------
-        if((e.ctrlKey || e.metaKey) && e.which === 65 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
-            let $parent_container = $(window.active_element).closest('.item-container');
-            if($parent_container.length === 0)
-                $parent_container = $(window.active_element).find('.item-container');
-
-            if($parent_container.attr('data-multiselectable') === 'false')
-                return false;
-
-            if($parent_container){
-                $($parent_container).find('.item').not('.item-disabled').addClass('item-selected');
-                window.update_explorer_footer_selected_items_count($parent_container.closest('.window'));
-            }
-
-            return false;
-        }
-        //-----------------------------------------------------------------------------
-        // Close Window
-        // ctrl + w, will close the active window
-        //-----------------------------------------------------------------------------
-        if(e.ctrlKey && e.which === 87){
-            let $parent_window = $(window.active_element).closest('.window');
-            if($parent_window.length === 0)
-                $parent_window = $(window.active_element).find('.window');
-
-
-            if($parent_window !== null){
-                $($parent_window).close();
-            }
-        }
-
-        //-----------------------------------------------------------------------------
-        // Copy
-        // ctrl/command + c, will copy selected items on the active element to the clipboard
-        //-----------------------------------------------------------------------------
-        if((e.ctrlKey || e.metaKey) && e.which === 67 &&
-            $(window.mouseover_window).attr('data-is_dir') !== 'false' &&
-            $(window.mouseover_window).attr('data-path') !== window.trash_path &&
-            !$(focused_el).is('input') &&
-            !$(focused_el).is('textarea')){
-            let $selected_items;
-
-            let parent_container = $(window.active_element).closest('.item-container');
-            if(parent_container.length === 0)
-                parent_container = $(window.active_element).find('.item-container');
-
-            if(parent_container !== null){
-                $selected_items = $(parent_container).find('.item-selected');
-                if($selected_items.length > 0){
-                    window.clipboard = [];
-                    window.clipboard_op = 'copy';
-                    $selected_items.each(function() {
-                        // error if trash is being copied
-                        if($(this).attr('data-path') === window.trash_path){
-                            return;
-                        }
-                        // add to clipboard
-                        window.clipboard.push({path: $(this).attr('data-path'), uid: $(this).attr('data-uid'), metadata: $(this).attr('data-metadata')});
-                    })
-                }
-            }
-            return false;
-        }
-        //-----------------------------------------------------------------------------
-        // Cut
-        // ctrl/command + x, will copy selected items on the active element to the clipboard
-        //-----------------------------------------------------------------------------
-        if((e.ctrlKey || e.metaKey) && e.which === 88 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
-            let $selected_items;
-            let parent_container = $(window.active_element).closest('.item-container');
-            if(parent_container.length === 0)
-                parent_container = $(window.active_element).find('.item-container');
-
-            if(parent_container !== null){
-                $selected_items = $(parent_container).find('.item-selected');
-                if($selected_items.length > 0){
-                    window.clipboard = [];
-                    window.clipboard_op = 'move';
-                    $selected_items.each(function() {
-                        window.clipboard.push($(this).attr('data-path'));
-                    })
-                }
-            }
-            return false;
-        }
-        //-----------------------------------------------------------------------
-        // Open
-        // Enter key on a selected item will open it
-        //-----------------------------------------------------------------------
-        if(e.which === 13 && !$(focused_el).is('input') && !$(focused_el).is('textarea') && (Date.now() - window.last_enter_pressed_to_rename_ts) >200
-            // prevent firing twice, because this will be fired on both keyup and keydown
-            && e.type === 'keydown'){
-            let $selected_items;
-
-            e.preventDefault();
-            e.stopPropagation();
-
-            // ---------------------------------------------
-            // if this is a selected Launch menu item, open it
-            // ---------------------------------------------
-            if($('.launch-app-selected').length > 0){
-                // close launch menu
-                $(".launch-popover").fadeOut(200, function(){
-                    window.launch_app({
-                        name: $('.launch-app-selected').attr('data-name'),
-                    })
-                    $(".launch-popover").remove();
-                });
-
-                return false;
-            }
-            // ---------------------------------------------
-            // if this is a selected context menu item, open it
-            // ---------------------------------------------
-            else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
-                // let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
-                // $(selected_item).trigger('mouseover');
-                // $(selected_item).trigger('click');
-
-                let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
-                $(selected_item).removeClass('context-menu-item-active');
-                $(selected_item).addClass('context-menu-item-active-blurred');
-                $(selected_item).trigger('mouseover');
-                $(selected_item).trigger('click');
-                if($('.context-menu[data-is-submenu="true"]').length > 0){
-                    let selected_item = $('.context-menu[data-is-submenu="true"] .context-menu-item').get(0);
-                    window.select_ctxmenu_item(selected_item);
-                }
-
-                return false;
-            }
-            // ---------------------------------------------
-            // if this is a selected item, open it
-            // ---------------------------------------------
-            else if(window.active_item_container){
-                $selected_items = $(window.active_item_container).find('.item-selected');
-                if($selected_items.length > 0){
-                    $selected_items.each(function() {
-                        window.open_item({
-                            item: this,
-                            new_window: e.metaKey || e.ctrlKey,
-                        });
-                    })
-                }
-                return false;
-            }
-
-            return false;
-        }
-        //----------------------------------------------
-        // Paste
-        // ctrl/command + v, will paste items from the clipboard to the active element
-        //----------------------------------------------
-        if((e.ctrlKey || e.metaKey) && e.which === 86 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
-            let target_path, target_el;
-
-            // continue only if there is something in the clipboard
-            if(window.clipboard.length === 0)
-                return;
-
-            let parent_container = determine_active_container_parent();
-
-            if(parent_container){
-                target_el = parent_container;
-                target_path = $(parent_container).attr('data-path');
-                // don't allow pasting in Trash
-                if((target_path === window.trash_path || target_path.startsWith(window.trash_path + '/')) && window.clipboard_op !== 'move')
-                    return;
-                // execute clipboard operation
-                if(window.clipboard_op === 'copy')
-                    window.copy_clipboard_items(target_path);
-                else if(window.clipboard_op === 'move')
-                    window.move_clipboard_items(target_el, target_path);
-            }
-            return false;
-        }
-        //-----------------------------------------------------------------------------
-        // Undo
-        // ctrl/command + z, will undo last action
-        //-----------------------------------------------------------------------------
-        if((e.ctrlKey || e.metaKey) && e.which === 90){
-            window.undo_last_action();
-            return false;
-        }
-    });
-
     // update mouse position coordinates
     $(document).mousemove(function(event){
         update_mouse_position(event.clientX, event.clientY);

+ 710 - 0
src/keyboard.js

@@ -0,0 +1,710 @@
+/**
+ * Copyright (C) 2024 Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import UIAlert from './UI/UIAlert.js';
+
+$(document).bind('keydown', async function(e){
+    const focused_el = document.activeElement;
+    //-----------------------------------------------------------------------
+    // ← ↑ → ↓: an arrow key is pressed
+    //-----------------------------------------------------------------------
+    if((e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
+        // ----------------------------------------------
+        // Launch menu is open
+        // ----------------------------------------------
+        if($('.launch-popover').length > 0){
+            // If no item is selected and down arrow is pressed, select the first item
+            if($('.launch-popover .start-app-card.launch-app-selected').length === 0 && (e.which === 40)){
+                $('.launch-popover .start-app-card:visible').first().addClass('launch-app-selected');
+                // blur search input
+                $('.launch-popover .launch-search').blur();
+                return false;
+            }
+            // if search input is focused and left or right arrow is pressed, return false
+            else if($('.launch-popover .launch-search').is(':focus') && (e.which === 37 || e.which === 39)){
+                return false;
+            }
+            else{
+                // If an item is already selected, move the selection up, down, left or right
+                let selected_item = $('.launch-popover .start-app-card.launch-app-selected').get(0);
+                let selected_item_index = $('.launch-popover .start-app-card:visible').index(selected_item);
+                let selected_item_row = Math.floor(selected_item_index / 5);
+                let selected_item_col = selected_item_index % 5;
+                let selected_item_row_count = Math.ceil($('.launch-popover .start-app-card:visible').length / 5);
+                let selected_item_col_count = 5;
+                let new_selected_item_index = selected_item_index;
+                let new_selected_item_row = selected_item_row;
+                let new_selected_item_col = selected_item_col;
+                let new_selected_item;
+
+                // if up arrow is pressed
+                if(e.which === 38){
+                    // if this item is in the first row, up arrow should bring the focus back to the search input
+                    if(selected_item_row === 0){
+                        $('.launch-popover .launch-search').focus();
+                        // unselect all items
+                        $('.launch-popover .start-app-card.launch-app-selected').removeClass('launch-app-selected');
+                        // bring cursor to the end of the search input
+                        $('.launch-popover .launch-search').val($('.launch-popover .launch-search').val());
+
+                        return false;
+                    }
+                    // if this item is not in the first row, move the selection up
+                    else{
+                        new_selected_item_row = selected_item_row - 1;
+                        if(new_selected_item_row < 0)
+                            new_selected_item_row = selected_item_row_count - 1;
+                    }
+                }
+                // if down arrow is pressed
+                else if(e.which === 40){
+                    new_selected_item_row = selected_item_row + 1;
+                    if(new_selected_item_row >= selected_item_row_count)
+                        new_selected_item_row = 0;
+                }
+                // if left arrow is pressed
+                else if(e.which === 37){
+                    new_selected_item_col = selected_item_col - 1;
+                    if(new_selected_item_col < 0)
+                        new_selected_item_col = selected_item_col_count - 1;
+                }
+                // if right arrow is pressed
+                else if(e.which === 39){
+                    new_selected_item_col = selected_item_col + 1;
+                    if(new_selected_item_col >= selected_item_col_count)
+                        new_selected_item_col = 0;
+                }
+                new_selected_item_index = (new_selected_item_row * selected_item_col_count) + new_selected_item_col;
+                new_selected_item = $('.launch-popover .start-app-card:visible').get(new_selected_item_index);
+                $(selected_item).removeClass('launch-app-selected');
+                $(new_selected_item).addClass('launch-app-selected');
+
+                // make sure the selected item is visible in the popover by scrolling the popover
+                let popover = $('.launch-popover').get(0);
+                let popover_height = $('.launch-popover').height();
+                let popover_scroll_top = popover.getBoundingClientRect().top;
+                let popover_scroll_bottom = popover_scroll_top + popover_height;
+                let selected_item_top = new_selected_item.getBoundingClientRect().top;
+                let selected_item_bottom = new_selected_item.getBoundingClientRect().bottom;
+                let isVisible = (selected_item_top >= popover_scroll_top) && (selected_item_bottom <= popover_scroll_top + popover_height);
+
+                if ( ! isVisible ) {
+                    const scrollTop = selected_item_top - popover_scroll_top;
+                    const scrollBot = selected_item_bottom - popover_scroll_bottom;
+                    if (Math.abs(scrollTop) < Math.abs(scrollBot)) {
+                        popover.scrollTop += scrollTop;
+                    } else {
+                        popover.scrollTop += scrollBot;
+                    }
+                }
+                return false;
+            }
+        }
+        // ----------------------------------------------
+        // A context menu is open
+        // ----------------------------------------------
+        else if($('.context-menu').length > 0){
+            // if no item is selected and down arrow is pressed, select the first item
+            if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 40)){
+                let selected_item = $('.context-menu-active .context-menu-item').get(0);
+                window.select_ctxmenu_item(selected_item);
+                return false;
+            }
+            // if no item is selected and up arrow is pressed, select the last item
+            else if($('.context-menu-active .context-menu-item-active').length === 0 && (e.which === 38)){
+                let selected_item = $('.context-menu .context-menu-item').get($('.context-menu .context-menu-item').length - 1);
+                window.select_ctxmenu_item(selected_item);
+                return false;
+            }
+            // if an item is selected and down arrow is pressed, select the next enabled item
+            else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 40)){
+                let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
+                let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
+                let new_selected_item_index = selected_item_index + 1;
+                let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
+                while($(new_selected_item).hasClass('context-menu-item-disabled')){
+                    new_selected_item_index = new_selected_item_index + 1;
+                    new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
+                }
+                window.select_ctxmenu_item(new_selected_item);
+                return false;
+            }
+            // if an item is selected and up arrow is pressed, select the previous enabled item
+            else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 38)){
+                let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
+                let selected_item_index = $('.context-menu-active .context-menu-item').index(selected_item);
+                let new_selected_item_index = selected_item_index - 1;
+                let new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
+                while($(new_selected_item).hasClass('context-menu-item-disabled')){
+                    new_selected_item_index = new_selected_item_index - 1;
+                    new_selected_item = $('.context-menu-active .context-menu-item').get(new_selected_item_index);
+                }
+                window.select_ctxmenu_item(new_selected_item);
+                return false;
+            }
+            // if right arrow is pressed, open the submenu by triggering a mouseover event
+            else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 39)){
+                const selected_item = $('.context-menu-active .context-menu-item-active').get(0);
+                $(selected_item).trigger('mouseover');
+                // if the submenu is open, select the first item in the submenu
+                if($(selected_item).hasClass('context-menu-item-submenu') === true){
+                    $(selected_item).removeClass('context-menu-item-active');
+                    $(selected_item).addClass('context-menu-item-active-blurred');
+                    window.select_ctxmenu_item($('.context-menu[data-is-submenu="true"] .context-menu-item').get(0));
+                }
+                return false;
+            }
+            // if left arrow is pressed on a submenu, close the submenu
+            else if($('.context-menu-active[data-is-submenu="true"]').length > 0 && (e.which === 37)){
+                // get parent menu
+                let parent_menu_id = $('.context-menu-active[data-is-submenu="true"]').data('parent-id');
+                let parent_menu = $('.context-menu[data-element-id="' + parent_menu_id + '"]');
+                // remove the submenu
+                $('.context-menu-active[data-is-submenu="true"]').remove();
+                // activate the parent menu
+                $(parent_menu).addClass('context-menu-active');
+                // select the item that opened the submenu
+                let selected_item = $('.context-menu-active .context-menu-item-active-blurred').get(0);
+                $(selected_item).removeClass('context-menu-item-active-blurred');
+                $(selected_item).addClass('context-menu-item-active');
+
+                return false;
+            }
+            // if enter is pressed, trigger a click event on the selected item
+            else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
+                let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
+                $(selected_item).trigger('click');
+                return false;
+            }
+        }
+        // ----------------------------------------------
+        // Navigate items in the active item container
+        // ----------------------------------------------
+        else if(!$(focused_el).is('input') && !$(focused_el).is('textarea') && (e.which === 37 || e.which === 38 || e.which === 39 || e.which === 40)){
+            let item_width = 110, item_height = 110, selected_item;
+            // select first item in container if none is selected
+            if($(window.active_item_container).find('.item-selected').length === 0){
+                selected_item = $(window.active_item_container).find('.item').get(0);
+                window.active_element = selected_item;
+                $(window.active_item_container).find('.item-selected').removeClass('item-selected');
+                $(selected_item).addClass('item-selected');
+                return false;
+            }
+            // if Shift key is pressed and ONE item is already selected, pick that item
+            else if($(window.active_item_container).find('.item-selected').length === 1 && e.shiftKey){
+                selected_item = $(window.active_item_container).find('.item-selected').get(0);
+            }
+            // if Shift key is pressed and MORE THAN ONE item is selected, pick the latest active item
+            else if($(window.active_item_container).find('.item-selected').length > 1 && e.shiftKey){
+                selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
+            }
+            // otherwise if an item is selected, pick that item
+            else if($(window.active_item_container).find('.item-selected').length === 1){
+                selected_item = $(window.active_item_container).find('.item-selected').get(0);
+            }
+            else{
+                selected_item = $(window.active_element).hasClass('item') ? window.active_element : $(window.active_element).closest('.item').get(0);
+            }
+
+            // override the default behavior of ctrl/meta key
+            // in some browsers ctrl/meta key + arrow keys will scroll the page or go back/forward in history
+            if(e.ctrlKey || e.metaKey){
+                e.preventDefault();
+                e.stopPropagation();
+            }
+
+            // get the position of the selected item
+            let active_el_pos = $(selected_item).hasClass('item') ? selected_item.getBoundingClientRect() : $(selected_item).closest('.item').get(0).getBoundingClientRect();
+            let xpos = active_el_pos.left + item_width/2;
+            let ypos = active_el_pos.top + item_height/2;
+            // these hold next item's position on the grid
+            let x_nxtpos, y_nxtpos;
+            // these hold the amount of pixels to scroll the container
+            let x_scroll = 0, y_scroll = 0;
+            // determine next item's position on the grid
+            // left
+            if(e.which === 37){
+                x_nxtpos = (xpos - item_width) > 0 ? (xpos - item_width) : 0;
+                y_nxtpos = (ypos);
+                x_scroll = (item_width / 2);
+            }
+            // up
+            else if(e.which === 38){
+                x_nxtpos = (xpos);
+                y_nxtpos = (ypos - item_height) > 0 ? (ypos - item_height) : 0;
+                y_scroll = -1 * (item_height / 2);
+            }
+            // right
+            else if(e.which === 39){
+                x_nxtpos = (xpos + item_width);
+                y_nxtpos = (ypos);
+                x_scroll = -1 * (item_width / 2);
+            }
+            // down
+            else if(e.which === 40){
+                x_nxtpos = (xpos);
+                y_nxtpos = (ypos + item_height);
+                y_scroll = (item_height / 2);
+            }
+
+            let elements_at_next_pos = document.elementsFromPoint(x_nxtpos, y_nxtpos);
+            let next_item;
+            for (let index = 0; index < elements_at_next_pos.length; index++) {
+                const elem_at_next_pos = elements_at_next_pos[index];
+                if($(elem_at_next_pos).hasClass('item') && $(elem_at_next_pos).closest('.item-container').is(window.active_item_container)){
+                    next_item = elem_at_next_pos;
+                    break;
+                }
+            }
+
+            if(next_item){
+                selected_item = next_item;
+                window.active_element = next_item;
+                // if ctrl or meta key is not pressed, unselect all items
+                if(!e.shiftKey){
+                    $(window.active_item_container).find('.item').removeClass('item-selected');
+                }
+                $(next_item).addClass('item-selected');
+                window.latest_selected_item = next_item;
+                // scroll to the selected item only if this was a down or up move
+                if(e.which === 38 || e.which === 40)
+                    next_item.scrollIntoView(false);
+            }
+        }
+    }
+    //-----------------------------------------------------------------------
+    // if the Esc key is pressed on a FileDialog/Alert, close that FileDialog/Alert
+    //-----------------------------------------------------------------------
+    else if(
+        // escape key code
+        e.which === 27 &&
+        // active window must be a FileDialog or Alert
+        ($('.window-active').hasClass('window-filedialog') || $('.window-active').hasClass('window-alert')) &&
+        // either don't close if an input is focused or if the input is the filename input
+        ((!$(focused_el).is('input') && !$(focused_el).is('textarea')) || $(focused_el).hasClass('savefiledialog-filename'))
+        ){
+        // close the FileDialog
+        $('.window-active').close();
+    }
+    //-----------------------------------------------------------------------
+    // if the Esc key is pressed on a Window Navbar Editor, deactivate the editor
+    //-----------------------------------------------------------------------
+    else if( e.which === 27 && $(focused_el).hasClass('window-navbar-path-input')){
+        $(focused_el).blur();
+        $(focused_el).val($(focused_el).closest('.window').attr('data-path'));
+        $(focused_el).attr('data-path', $(focused_el).closest('.window').attr('data-path'));
+    }
+
+    //-----------------------------------------------------------------------
+    // Esc key should:
+    //      - always close open context menus
+    //      - close the Launch Popover if it's open
+    //-----------------------------------------------------------------------
+    if( e.which === 27){
+        // close open context menus
+        $('.context-menu').remove();
+
+        // close the Launch Popover if it's open
+        $(".launch-popover").closest('.popover').fadeOut(200, function(){
+            $(".launch-popover").closest('.popover').remove();
+        });
+    }
+})
+
+$(document).bind('keydown', async function(e){
+    const focused_el = document.activeElement;
+    //-----------------------------------------------------------------------
+    // Shift+Delete (win)/ option+command+delete (Mac) key pressed
+    // Permanent delete bypassing trash after alert
+    //-----------------------------------------------------------------------
+    if((e.keyCode === 46 && e.shiftKey) || (e.altKey && e.metaKey && e.keyCode === 8)) {
+        let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected`);
+        if($selected_items.length > 0){
+            const alert_resp = await UIAlert({
+                message: i18n('confirm_delete_multiple_items'),
+                buttons:[
+                    {
+                        label: i18n('delete'),
+                        type: 'primary',
+                    },
+                    {
+                        label: i18n('cancel')
+                    },
+                ]
+            })
+            if((alert_resp) === 'Delete'){
+                for (let index = 0; index < $selected_items.length; index++) {
+                    const element = $selected_items[index];
+                    await window.delete_item(element);
+                }
+            }
+        }
+        return false;
+    }
+    //-----------------------------------------------------------------------
+    // Delete (win)/ ctrl+delete (Mac) / cmd+delete (Mac) key pressed
+    // Permanent delete from trash after alert or move to trash
+    //-----------------------------------------------------------------------
+    if(e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey))) {
+        // permanent delete?
+        let $selected_items = $(window.active_element).closest(`.item-container`).find(`.item-selected[data-path^="${window.trash_path + '/'}"]`);
+        if($selected_items.length > 0){
+            const alert_resp = await UIAlert({
+                message: i18n('confirm_delete_multiple_items'),
+                buttons:[
+                    {
+                        label: i18n('delete'),
+                        type: 'primary',
+                    },
+                    {
+                        label: i18n('cancel')
+                    },
+                ]
+            })
+            if((alert_resp) === 'Delete'){
+                for (let index = 0; index < $selected_items.length; index++) {
+                    const element = $selected_items[index];
+                    await window.delete_item(element);
+                }
+                const trash = await puter.fs.stat(window.trash_path);
+                if(window.socket){
+                    window.socket.emit('trash.is_empty', {is_empty: trash.is_empty});
+                }
+
+                if(trash.is_empty){
+                    $(`[data-app="trash"]`).find('.taskbar-icon > img').attr('src', window.icons['trash.svg']);
+                    $(`.item[data-path="${html_encode(window.trash_path)}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
+                    $(`.window[data-path="${html_encode(window.trash_path)}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
+                }
+            }
+        }
+        // regular delete?
+        else{
+            $selected_items = $(window.active_element).closest('.item-container').find('.item-selected');
+            if($selected_items.length > 0){
+                // Only delete the items if we're not renaming one.
+                if ($selected_items.children('.item-name-editor-active').length === 0) {
+                    window.move_items($selected_items, window.trash_path);
+                }
+            }
+        }
+        return false;
+    }
+
+    //-----------------------------------------------------------------------
+    // A letter or number is pressed and there is no context menu open: search items by name
+    //-----------------------------------------------------------------------
+    if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length === 0){
+        if(window.keypress_item_seach_term !== '')
+            clearTimeout(window.keypress_item_seach_buffer_timeout);
+
+        window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
+            window.keypress_item_seach_term = '';
+        }, 700);
+
+        window.keypress_item_seach_term += e.key.toLocaleLowerCase();
+
+        let matches= [];
+        const selected_items = $(window.active_item_container).find(`.item-selected`).not('.item-disabled').first();
+
+        // if one item is selected and the selected item matches the search term, don't continue search and select this item again
+        if(selected_items.length === 1 && $(selected_items).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
+            return false;
+        }
+
+        // search for matches
+        let haystack = $(window.active_item_container).find(`.item`).not('.item-disabled');
+        for(let j=0; j < haystack.length; j++){
+            if($(haystack[j]).attr('data-name').toLowerCase().startsWith(window.keypress_item_seach_term)){
+                matches.push(haystack[j])
+            }
+        }
+
+        if(matches.length > 0){
+            // if there are multiple matches and an item is already selected, remove all matches before the selected item
+            if(selected_items.length > 0 && matches.length > 1){
+                let match_index;
+                for(let i=0; i < matches.length - 1; i++){
+                    if($(matches[i]).is(selected_items)){
+                        match_index = i;
+                        break;
+                    }
+                }
+                matches.splice(0, match_index+1);
+            }
+            // deselect all selected sibling items
+            $(window.active_item_container).find(`.item-selected`).removeClass('item-selected');
+            // select matching item
+            $(matches[0]).not('.item-disabled').addClass('item-selected');
+            matches[0].scrollIntoView(false);
+            window.update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
+        }
+
+        return false;
+    }
+    //-----------------------------------------------------------------------
+    // A letter or number is pressed and there is a context menu open: search items by name
+    //-----------------------------------------------------------------------
+    else if(!e.ctrlKey && !e.metaKey && !$(focused_el).is('input') && !$(focused_el).is('textarea') && $('.context-menu').length > 0){
+        if(window.keypress_item_seach_term !== '')
+            clearTimeout(window.keypress_item_seach_buffer_timeout);
+
+        window.keypress_item_seach_buffer_timeout = setTimeout(()=>{
+            window.keypress_item_seach_term = '';
+        }, 700);
+
+        window.keypress_item_seach_term += e.key.toLocaleLowerCase();
+
+        let matches= [];
+        const selected_items = $('.context-menu').find(`.context-menu-item-active`).first();
+
+        // if one item is selected and the selected item matches the search term, don't continue search and select this item again
+        if(selected_items.length === 1 && $(selected_items).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
+            return false;
+        }
+
+        // search for matches
+        let haystack = $('.context-menu-active').find(`.context-menu-item`);
+        for(let j=0; j < haystack.length; j++){
+            if($(haystack[j]).text().toLowerCase().startsWith(window.keypress_item_seach_term)){
+                matches.push(haystack[j])
+            }
+        }
+
+        if(matches.length > 0){
+            // if there are multiple matches and an item is already selected, remove all matches before the selected item
+            if(selected_items.length > 0 && matches.length > 1){
+                let match_index;
+                for(let i=0; i < matches.length - 1; i++){
+                    if($(matches[i]).is(selected_items)){
+                        match_index = i;
+                        break;
+                    }
+                }
+                matches.splice(0, match_index+1);
+            }
+            // deselect all selected sibling items
+            $('.context-menu').find(`.context-menu-item-active`).removeClass('context-menu-item-active');
+            // select matching item
+            $(matches[0]).addClass('context-menu-item-active');
+            // matches[0].scrollIntoView(false);
+            // update_explorer_footer_selected_items_count($(window.active_element).closest('.window'));
+        }
+
+        return false;
+    }
+})
+
+$(document).bind("keyup keydown", async function(e){
+    const focused_el = document.activeElement;
+    //-----------------------------------------------------------------------------
+    // Override ctrl/cmd + s/o
+    //-----------------------------------------------------------------------------
+    if((e.ctrlKey || e.metaKey) && (e.which === 83 || e.which === 79)){
+        e.preventDefault()
+        return false;
+    }
+    //-----------------------------------------------------------------------------
+    // Select All
+    // ctrl/command + a, will select all items on desktop and windows
+    //-----------------------------------------------------------------------------
+    if((e.ctrlKey || e.metaKey) && e.which === 65 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
+        let $parent_container = $(window.active_element).closest('.item-container');
+        if($parent_container.length === 0)
+            $parent_container = $(window.active_element).find('.item-container');
+
+        if($parent_container.attr('data-multiselectable') === 'false')
+            return false;
+
+        if($parent_container){
+            $($parent_container).find('.item').not('.item-disabled').addClass('item-selected');
+            window.update_explorer_footer_selected_items_count($parent_container.closest('.window'));
+        }
+
+        return false;
+    }
+    //-----------------------------------------------------------------------------
+    // Close Window
+    // ctrl + w, will close the active window
+    //-----------------------------------------------------------------------------
+    if(e.ctrlKey && e.which === 87){
+        let $parent_window = $(window.active_element).closest('.window');
+        if($parent_window.length === 0)
+            $parent_window = $(window.active_element).find('.window');
+
+
+        if($parent_window !== null){
+            $($parent_window).close();
+        }
+    }
+
+    //-----------------------------------------------------------------------------
+    // Copy
+    // ctrl/command + c, will copy selected items on the active element to the clipboard
+    //-----------------------------------------------------------------------------
+    if((e.ctrlKey || e.metaKey) && e.which === 67 &&
+        $(window.mouseover_window).attr('data-is_dir') !== 'false' &&
+        $(window.mouseover_window).attr('data-path') !== window.trash_path &&
+        !$(focused_el).is('input') &&
+        !$(focused_el).is('textarea')){
+        let $selected_items;
+
+        let parent_container = $(window.active_element).closest('.item-container');
+        if(parent_container.length === 0)
+            parent_container = $(window.active_element).find('.item-container');
+
+        if(parent_container !== null){
+            $selected_items = $(parent_container).find('.item-selected');
+            if($selected_items.length > 0){
+                window.clipboard = [];
+                window.clipboard_op = 'copy';
+                $selected_items.each(function() {
+                    // error if trash is being copied
+                    if($(this).attr('data-path') === window.trash_path){
+                        return;
+                    }
+                    // add to clipboard
+                    window.clipboard.push({path: $(this).attr('data-path'), uid: $(this).attr('data-uid'), metadata: $(this).attr('data-metadata')});
+                })
+            }
+        }
+        return false;
+    }
+    //-----------------------------------------------------------------------------
+    // Cut
+    // ctrl/command + x, will copy selected items on the active element to the clipboard
+    //-----------------------------------------------------------------------------
+    if((e.ctrlKey || e.metaKey) && e.which === 88 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
+        let $selected_items;
+        let parent_container = $(window.active_element).closest('.item-container');
+        if(parent_container.length === 0)
+            parent_container = $(window.active_element).find('.item-container');
+
+        if(parent_container !== null){
+            $selected_items = $(parent_container).find('.item-selected');
+            if($selected_items.length > 0){
+                window.clipboard = [];
+                window.clipboard_op = 'move';
+                $selected_items.each(function() {
+                    window.clipboard.push($(this).attr('data-path'));
+                })
+            }
+        }
+        return false;
+    }
+    //-----------------------------------------------------------------------
+    // Open
+    // Enter key on a selected item will open it
+    //-----------------------------------------------------------------------
+    if(e.which === 13 && !$(focused_el).is('input') && !$(focused_el).is('textarea') && (Date.now() - window.last_enter_pressed_to_rename_ts) >200
+        // prevent firing twice, because this will be fired on both keyup and keydown
+        && e.type === 'keydown'){
+        let $selected_items;
+
+        e.preventDefault();
+        e.stopPropagation();
+
+        // ---------------------------------------------
+        // if this is a selected Launch menu item, open it
+        // ---------------------------------------------
+        if($('.launch-app-selected').length > 0){
+            // close launch menu
+            $(".launch-popover").fadeOut(200, function(){
+                window.launch_app({
+                    name: $('.launch-app-selected').attr('data-name'),
+                })
+                $(".launch-popover").remove();
+            });
+
+            return false;
+        }
+        // ---------------------------------------------
+        // if this is a selected context menu item, open it
+        // ---------------------------------------------
+        else if($('.context-menu-active .context-menu-item-active').length > 0 && (e.which === 13)){
+            // let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
+            // $(selected_item).trigger('mouseover');
+            // $(selected_item).trigger('click');
+
+            let selected_item = $('.context-menu-active .context-menu-item-active').get(0);
+            $(selected_item).removeClass('context-menu-item-active');
+            $(selected_item).addClass('context-menu-item-active-blurred');
+            $(selected_item).trigger('mouseover');
+            $(selected_item).trigger('click');
+            if($('.context-menu[data-is-submenu="true"]').length > 0){
+                let selected_item = $('.context-menu[data-is-submenu="true"] .context-menu-item').get(0);
+                window.select_ctxmenu_item(selected_item);
+            }
+
+            return false;
+        }
+        // ---------------------------------------------
+        // if this is a selected item, open it
+        // ---------------------------------------------
+        else if(window.active_item_container){
+            $selected_items = $(window.active_item_container).find('.item-selected');
+            if($selected_items.length > 0){
+                $selected_items.each(function() {
+                    window.open_item({
+                        item: this,
+                        new_window: e.metaKey || e.ctrlKey,
+                    });
+                })
+            }
+            return false;
+        }
+
+        return false;
+    }
+    //----------------------------------------------
+    // Paste
+    // ctrl/command + v, will paste items from the clipboard to the active element
+    //----------------------------------------------
+    if((e.ctrlKey || e.metaKey) && e.which === 86 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
+        let target_path, target_el;
+
+        // continue only if there is something in the clipboard
+        if(window.clipboard.length === 0)
+            return;
+
+        let parent_container = determine_active_container_parent();
+
+        if(parent_container){
+            target_el = parent_container;
+            target_path = $(parent_container).attr('data-path');
+            // don't allow pasting in Trash
+            if((target_path === window.trash_path || target_path.startsWith(window.trash_path + '/')) && window.clipboard_op !== 'move')
+                return;
+            // execute clipboard operation
+            if(window.clipboard_op === 'copy')
+                window.copy_clipboard_items(target_path);
+            else if(window.clipboard_op === 'move')
+                window.move_clipboard_items(target_el, target_path);
+        }
+        return false;
+    }
+    //-----------------------------------------------------------------------------
+    // Undo
+    // ctrl/command + z, will undo last action
+    //-----------------------------------------------------------------------------
+    if((e.ctrlKey || e.metaKey) && e.which === 90){
+        window.undo_last_action();
+        return false;
+    }
+});

+ 0 - 56
src/static-assets.js

@@ -1,56 +0,0 @@
-/**
- * Copyright (C) 2024 Puter Technologies Inc.
- *
- * This file is part of Puter.
- *
- * Puter is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as published
- * by the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- * 
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- * 
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-// Ordered list of statically-linked external JS libraries and scripts
-const lib_paths =[
-    `/lib/jquery-3.6.1/jquery-3.6.1.min.js`,
-    `/lib/viselect.min.js`,
-    `/lib/FileSaver.min.js`,
-    `/lib/socket.io/socket.io.min.js`,
-    `/lib/qrcode.min.js`,
-    `/lib/jquery-ui-1.13.2/jquery-ui.min.js`,
-    `/lib/lodash@4.17.21.min.js`,
-    `/lib/jquery.dragster.js`,
-    '/lib/jquery.menu-aim.js',
-    `/lib/html-entities.js`,
-    `/lib/timeago.min.js`,
-    `/lib/iro.min.js`,
-    `/lib/isMobile.min.js`,
-    `/lib/jszip-3.10.1.min.js`,
-]
-
-// Ordered list of CSS stylesheets
-const css_paths = [
-    '/css/normalize.css',
-    '/lib/jquery-ui-1.13.2/jquery-ui.min.css',
-    '/css/style.css',
-]
-
-// Ordered list of JS scripts
-const js_paths = [
-    '/init_sync.js',
-    '/init_async.js',
-    '/initgui.js',
-    '/helpers.js',
-    '/IPC.js',
-    '/globals.js',
-    `/i18n/i18n.js`,
-]
-
-export { lib_paths, css_paths, js_paths };

+ 7 - 1
utils.js

@@ -22,9 +22,15 @@ import path from 'path';
 import webpack from 'webpack';
 import CleanCSS from 'clean-css';
 import uglifyjs from 'uglify-js';
-import { lib_paths, css_paths, js_paths } from './src/static-assets.js';
 import { fileURLToPath } from 'url';
 
+// load puter-gui.json
+const puter_gui = JSON.parse(fs.readFileSync(path.join(__dirname, 'puter-gui.json')));
+// map puter_gui to lib_paths, css_paths, js_paths
+const lib_paths = puter_gui.lib_paths;
+const css_paths = puter_gui.css_paths;
+const js_paths = puter_gui.js_paths;
+
 // Polyfill __dirname, which doesn't exist in modules mode
 const __dirname = path.dirname(fileURLToPath(import.meta.url));