diff --git a/puter-gui.json b/puter-gui.json
index 5450e577..ef1927c5 100644
--- a/puter-gui.json
+++ b/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": {
diff --git a/run-selfhosted.js b/run-selfhosted.js
index 9cddb3cb..a4540b71 100644
--- a/run-selfhosted.js
+++ b/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,
diff --git a/src/initgui.js b/src/initgui.js
index 5a57fb31..e003c951 100644
--- a/src/initgui.js
+++ b/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);
diff --git a/src/keyboard.js b/src/keyboard.js
new file mode 100644
index 00000000..89860bae
--- /dev/null
+++ b/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 .
+ */
+
+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;
+ }
+});
\ No newline at end of file
diff --git a/src/static-assets.js b/src/static-assets.js
deleted file mode 100644
index 39303fc6..00000000
--- a/src/static-assets.js
+++ /dev/null
@@ -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 .
- */
-
-// 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 };
\ No newline at end of file
diff --git a/utils.js b/utils.js
index 04cf957f..9cab1a0f 100644
--- a/utils.js
+++ b/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));