diff --git a/src/core.py b/src/core.py index 2fc6e3a..a2624c7 100755 --- a/src/core.py +++ b/src/core.py @@ -33,15 +33,12 @@ from fnmatch import fnmatch from re import search -from time import sleep,strftime,localtime,time -from hashlib import sha1 +from time import sleep,strftime,localtime,time,perf_counter from os import stat,scandir,sep,symlink,link,cpu_count,name as os_name,rename as os_rename,remove as os_remove from os.path import dirname,relpath,normpath,join as path_join,abspath as abspath,exists as path_exists,isdir as path_isdir -from subprocess import run as subprocess_run - if os_name=='nt': from subprocess import CREATE_NO_WINDOW @@ -49,19 +46,24 @@ from pickle import dumps,loads from zstandard import ZstdCompressor,ZstdDecompressor -from send2trash import send2trash +#lazyfied +#from hashlib import sha1 +#from subprocess import run as subprocess_run +#from send2trash import send2trash -from numpy import array as numpy_array -from PIL.Image import open as image_open,new as image_new, alpha_composite as image_alpha_composite -from imagehash import average_hash,phash,dhash +#from numpy import array as numpy_array +#from PIL.Image import open as image_open,new as image_new, alpha_composite as image_alpha_composite +#from imagehash import average_hash,phash,dhash -from sklearn.cluster import DBSCAN +#from sklearn.cluster import DBSCAN DELETE=0 SOFTLINK=1 HARDLINK=2 WIN_LNK=3 +IMAGES_EXTENSIONS = ('.jpeg','.jpg','.jp2','.jpx','.j2k','.png','.bmp','.dds','.dib','.gif','.tga','.tiff','.tif','.webp','.xbm') + def localtime_catched(t): try: #mtime sometimes happens to be negative (Virtual box ?) @@ -115,6 +117,8 @@ def abort(self): self.abort_action=True def calc(self): + from hashlib import sha1 + CRC_BUFFER_SIZE=4*1024*1024 buf = bytearray(CRC_BUFFER_SIZE) view = memoryview(buf) @@ -304,7 +308,7 @@ def scan(self): self_scan_results_images = self.scan_results_images = set() ############################################################################################# if self_similarity_mode: - supported_extensions = ('.jpeg','.jpg','.jp2','.jpx','.j2k','.png','.bmp','.dds','.dib','.eps','.gif','.tga','.tiff','.tif','.webp','.xbm') + supported_extensions = IMAGES_EXTENSIONS self_log_skipped = self.log_skipped @@ -616,6 +620,9 @@ def images_hashes_cache_write(self): self.info='' def imagehsh_calc_in_thread(self,i,hash_size,all_rotations,source_dict,result_dict): + from PIL.Image import open as image_open,new as image_new, alpha_composite as image_alpha_composite + from imagehash import average_hash,phash,dhash + def my_hash_combo(file,hash_size): seq_hash = [] seq_hash_extend = seq_hash.extend @@ -637,8 +644,11 @@ def my_hash_combo(file,hash_size): try: file = image_open(fullpath) - if file.mode == 'RGBA': - file = image_alpha_composite(image_new('RGBA', file.size, (255,255,255)), file) + + if file.mode != 'RGBA': + file = file.convert("RGBA") + + file = image_alpha_composite(image_new('RGBA', file.size, (255,255,255)), file) except Exception as e: self.log.error(f'opening file: {fullpath} error: {e}.') @@ -664,6 +674,9 @@ def my_hash_combo(file,hash_size): info='' def image_hashing(self,hash_size,all_rotations): + #musi tu byc - inaczej pyinstaller & nuitka nie dzialaja + from numpy import array as numpy_array + self.images_hashes_cache_read() self.scanned_paths=self.paths_to_scan.copy() @@ -742,6 +755,7 @@ def image_hashing(self,hash_size,all_rotations): for i in range(max_threads): if imagehash_threads[i].is_alive(): all_dead=False + break if all_dead: break @@ -751,7 +765,9 @@ def image_hashing(self,hash_size,all_rotations): self.info_size_done_perc = sto_by_self_sum_size*self.info_size_done self.info_files_done_perc = sto_by_self_info_total*self.info_files_done - sleep(0.02) + sleep(0.05) + + self.info = self.info_line = 'Joining threads ...' for i in range(max_threads): imagehash_threads[i].join() @@ -773,6 +789,8 @@ def image_hashing(self,hash_size,all_rotations): sys_exit() #thread def similarity_clustering(self,hash_size,distance,all_rotations): + from sklearn.cluster import DBSCAN + pool = [] keys = [] @@ -787,7 +805,11 @@ def similarity_clustering(self,hash_size,distance,all_rotations): self.info_line = self.info = 'Clustering ...' + t0=perf_counter() + self.log.info(f'start DBSCAN') labels = DBSCAN(eps=de_norm_distance, min_samples=2,n_jobs=-1,metric='manhattan',algorithm='kd_tree').fit(pool).labels_ + t1=perf_counter() + self.log.info(f'DBSCAN end. Time:{t1-t0}') #with rotation variants groups_dict = defaultdict(set) @@ -810,7 +832,6 @@ def similarity_clustering(self,hash_size,distance,all_rotations): files_already_in_group=set() files_already_in_group_add = files_already_in_group.add - pruned_groups_dict = defaultdict(set) for label in groups_sorted_by_quantity: #print(f'{label=}',type(label)) @@ -829,11 +850,14 @@ def similarity_clustering(self,hash_size,distance,all_rotations): ############################################## self_files_of_images_groups = self.files_of_images_groups = {} - #print(f'{pruned_groups_dict=}') - for label,keys in pruned_groups_dict.items(): + groups_digits=len(str(len(pruned_groups_dict))) + + relabel_nr=0 + for label,keys in sorted(pruned_groups_dict.items(), key = lambda x : max([y[6] for y in x[1]]),reverse=True ): if len(keys)>1: - self_files_of_images_groups[str(label)] = keys + self_files_of_images_groups[f'G{str(relabel_nr).zfill(groups_digits)}'] = keys + relabel_nr+=1 sys_exit() #thread @@ -1225,6 +1249,8 @@ def delete_file(self,file_name,l_info): return f'Deletion error:{e}' def delete_file_to_trash(self,file_name,l_info): + from send2trash import send2trash + l_info(f'deleting file to trash:{file_name}') try: send2trash(file_name) @@ -1248,6 +1274,8 @@ def do_soft_link(self,src,dest,relative,l_info): return 'Error on soft linking:%s' % e def do_win_lnk_link(self,src,dest,l_info): + from subprocess import run as subprocess_run + l_info('win-lnk-linking %s<-%s',src,dest) try: powershell_cmd = f'$ol=(New-Object -ComObject WScript.Shell).CreateShortcut("{dest}")\n\r$ol.TargetPath="{src}"\n\r$ol.Save()' diff --git a/src/dude.py b/src/dude.py index 77a5e77..2a1a93c 100755 --- a/src/dude.py +++ b/src/dude.py @@ -31,23 +31,15 @@ from signal import signal,SIGINT -from configparser import ConfigParser -from subprocess import Popen - -from tkinter import Tk,Toplevel,PhotoImage,Menu,PanedWindow,Label,LabelFrame,Frame,StringVar,BooleanVar,IntVar,Canvas - +from tkinter import Tk,Toplevel,PhotoImage,Menu,PanedWindow,Label,LabelFrame,Frame,StringVar,BooleanVar,IntVar from tkinter.ttk import Checkbutton,Radiobutton,Treeview,Scrollbar,Button,Entry,Combobox,Scale,Style - from tkinter.filedialog import askdirectory,asksaveasfilename - from tkinterdnd2 import DND_FILES, TkinterDnD from collections import defaultdict -from threading import Thread from traceback import format_stack from fnmatch import fnmatch -from shutil import rmtree from time import sleep,strftime,time,perf_counter @@ -61,9 +53,13 @@ from os.path import abspath,normpath,dirname,join as path_join,isfile as path_isfile,split as path_split,exists as path_exists,isdir, splitext as path_splitext -from PIL import Image, ImageTk -from PIL.ImageTk import PhotoImage as ImageTk_PhotoImage -from PIL.Image import NEAREST,BILINEAR,open as image_open +#lazyfied +#from configparser import ConfigParser +#from subprocess import Popen +#from shutil import rmtree +#from PIL import Image, ImageTk +#from PIL.ImageTk import PhotoImage as ImageTk_PhotoImage +#from PIL.Image import NEAREST,BILINEAR,open as image_open windows = bool(os_name=='nt') @@ -162,10 +158,14 @@ HOMEPAGE='https://github.com/PJDude/dude' +TEXT_EXTENSIONS = ('.txt','.bat','.sh','.md','.html','.py','.cpp','.h','.ini','.tcl','.xml','.url','.lnk','.diz','.lng','.log','.rc','.csv','.ps1','.js','.v','.sv','.do') + #DE_NANO = 1_000_000_000 class Config: def __init__(self,config_dir): + from configparser import ConfigParser + #l_debug('Initializing config: %s', config_dir) self.config = ConfigParser() self.config.add_section('main') @@ -1107,14 +1107,14 @@ def file_cascade_post(): self_file_cascade_add_separator() self_file_cascade_add_command(label = 'Settings ...',command=lambda : self.get_settings_dialog().show(), accelerator="F2",image = self_ico['settings'],compound='left') self_file_cascade_add_separator() - self_file_cascade_add_command(label = 'Show/Update Preview', command = lambda : self.show_preview(),accelerator='F9',image = self.ico_empty,compound='left') + self_file_cascade_add_command(label = 'Show/Update Preview', command = lambda : self.show_preview(),accelerator='F9',image = self.ico_empty,compound='left',state=('disabled','normal')[bool(not self.cfg_get_bool(CFG_KEY_PREVIEW_AUTO_UPDATE) or not self.preview_shown )]) self_file_cascade_add_command(label = 'Hide Preview window', command = lambda : self.hide_preview(),accelerator='F11',image = self.ico_empty,compound='left',state=('disabled','normal')[self.preview_shown]) self_file_cascade_add_separator() self_file_cascade_add_command(label = 'Remove empty folders in specified directory ...',command=self.empty_folder_remove_ask,image = self.ico_empty,compound='left') self_file_cascade_add_separator() self_file_cascade_add_command(label = 'Save CSV',command = self.csv_save,state=item_actions_state,image = self.ico_empty,compound='left') self_file_cascade_add_separator() - self_file_cascade_add_command(label = 'Erase CRC Cache',command = self.cache_clean,image = self.ico_empty,compound='left') + self_file_cascade_add_command(label = 'Erase Cache',command = self.cache_clean,image = self.ico_empty,compound='left') self_file_cascade_add_separator() self_file_cascade_add_command(label = 'Exit',command = self.exit,image = self_ico['exit'],compound='left') @@ -2620,17 +2620,15 @@ def tree_on_mouse_button_press(self,event,toggle=False): def preview_conf(self,event): if self.preview_shown: - #print('preview_conf',event) + #z eventu czasami idzie lewizna (start bez obrazka) + geometry = self.preview.geometry() + self.cfg.set('preview',str(geometry),section='geometry') - self.cfg.set('preview',str(self.preview.geometry()),section='geometry') - - new_preview_size = (event.width,event.height) - new_preview_size = (event.width,event.height) + new_preview_size = tuple([int(x) for x in geometry.split('+')[0].split('x')]) if self.preview_size!=new_preview_size: self.txt_label_heigh = self.preview_label_txt.winfo_height() - #print('preview_conf - real') self.preview_size=new_preview_size self.preview_photo_image_cache={} @@ -2665,67 +2663,80 @@ def show_preview(self): self.main.focus_set() self.sel_tree.focus_set() - text_extensions = ('.txt','.bat','.sh','.md','.html','.py','.cpp','.h','.ini','.tcl','.xml','.url','.lnk','.diz','.lng','.log','.rc','.csv','.ps1','.js','.v','.sv','.do') - pic_extensions = ('.jpeg','.jpg','.jp2','.jpx','.j2k','.png','.bmp','.dds','.dib','.eps','.gif','.tga','.tiff','.tif','.webp','.xbm') + last_image_read = '' + last_image_read_path = '' + def update_preview(self): if self.preview_shown: + from PIL import Image, ImageTk + from PIL.ImageTk import PhotoImage as ImageTk_PhotoImage + from PIL.Image import NEAREST,BILINEAR,open as image_open + path = self.sel_full_path_to_file if path: head,ext = path_splitext(path) + ext_lower = ext.lower() - if ext.lower() in self.text_extensions: + if ext_lower in TEXT_EXTENSIONS: self.preview_label_img.pack_forget() try: with open(path,'rt', encoding='utf-8', errors='ignore') as file: self.preview_text.delete(1.0, 'end') - self.preview_text.insert('end', file.read()) - self.preview_label_txt.configure(text=path) + cont_lines=file.readlines() + self.preview_label_txt.configure(text=f'lines:{fnumber(len(cont_lines))}') + self.preview_text.insert('end', ''.join(cont_lines)) except Exception as e: self.preview_label_txt.configure(text=str(e)) - self.preview.title('Dude - Preview') + self.preview.title(f'Dude - Preview {e}') else: self.preview_frame_txt.pack(fill='both',expand=1) self.preview.title(path) - elif ext.lower() in self.pic_extensions: + elif ext_lower in IMAGES_EXTENSIONS: self.preview_frame_txt.pack_forget() try: self_preview_photo_image_cache = self.preview_photo_image_cache + self_preview_photo_image_list = self.preview_photo_image_list if path not in self_preview_photo_image_cache: - im1 = image_open(path) - if im1.mode != 'RGBA': - im1 = im1.convert("RGBA") + if path != self.last_image_read_path: + self.last_image_read = image_open(path) + if self.last_image_read.mode != 'RGBA': + self.last_image_read = self.last_image_read.convert("RGBA") + self.last_image_read_path = path preview_size_width,preview_size_height = self.preview_size - height = im1.height + height = self.last_image_read.height ratio_y = height/(preview_size_height-self.txt_label_heigh) - width = im1.width + width = self.last_image_read.width ratio_x = width/preview_size_width biggest_ratio = max(ratio_x,ratio_y,1) size = ( int (width/biggest_ratio), int(height/biggest_ratio)) - self_preview_photo_image_cache[path]=(ImageTk_PhotoImage(im1.resize(size,BILINEAR)),f'{width} x {height} pixels' + (f' ({round(100.0/biggest_ratio)}%)' if biggest_ratio>1 else '') ) - self_preview_photo_image_list = self.preview_photo_image_list - self_preview_photo_image_list.append(path) + self_preview_photo_image_cache[path]=(ImageTk_PhotoImage(self.last_image_read.resize(size,BILINEAR)),f'{width} x {height} pixels' + (f' ({round(100.0/biggest_ratio)}%)' if biggest_ratio>1 else '') ) + if len(self_preview_photo_image_list)>self.preview_photo_image_limit: del self_preview_photo_image_cache[self_preview_photo_image_list.pop(0)] + else: + self_preview_photo_image_list.remove(path) - self.preview_label_img.configure(image=self_preview_photo_image_cache[path][0]) + self_preview_photo_image_list.append(path) + self.preview_label_img.configure(image=self_preview_photo_image_cache[path][0]) self.preview_label_txt.configure(text=self_preview_photo_image_cache[path][1]) + except Exception as e: self.preview_label_txt.configure(text=str(e)) - self.preview.title('Dude - Preview') + self.preview.title(f'Dude - Preview {e}') else: self.preview_label_img.pack(fill='both',expand=1) self.preview.title(path) @@ -2734,13 +2745,13 @@ def update_preview(self): self.preview_frame_txt.pack_forget() self.preview_label_img.pack_forget() self.preview_label_txt.configure(text='') - self.preview.title('Dude - Preview') + self.preview.title('Dude - Preview (format)') else: self.preview_frame_txt.pack_forget() self.preview_label_img.pack_forget() self.preview_label_txt.configure(text='') - self.preview.title('Dude - Preview') + self.preview.title('Dude - Preview (no path)') def hide_preview(self): self_preview = self.preview @@ -3136,7 +3147,7 @@ def context_menu_show(self,event): pop_add_command(label = 'Scan ...', command = self.scan_dialog_show,accelerator='S',image = self.ico['scan'],compound='left') pop_add_command(label = 'Settings ...', command = lambda : self.get_settings_dialog().show(),accelerator='F2',image = self.ico['settings'],compound='left') pop_add_separator() - pop_add_command(label = 'Show/Update Preview', command = lambda : self.show_preview(),accelerator='F9',image = self.ico_empty,compound='left') + pop_add_command(label = 'Show/Update Preview', command = lambda : self.show_preview(),accelerator='F9',image = self.ico_empty,compound='left',state=('disabled','normal')[bool(not self.cfg_get_bool(CFG_KEY_PREVIEW_AUTO_UPDATE) or not self.preview_shown )]) pop_add_command(label = 'Hide Preview window', command = lambda : self.hide_preview(),accelerator='F11',image = self.ico_empty,compound='left',state=('disabled','normal')[self.preview_shown]) pop_add_separator() pop_add_command(label = 'Copy full path',command = self.clip_copy_full_path_with_file,accelerator='Ctrl+C',state = 'normal' if (self.sel_kind and self.sel_kind!=self.CRC) else 'disabled', image = self.ico_empty,compound='left') @@ -3237,7 +3248,7 @@ def column_sort(self, tree): self_tree_sort_item = self.tree_sort_item if tree==self.groups_tree: - if colname in ('path','file','ctime_h'): + if colname in ('path','file','ctime_h') or (self.similarity_mode and colname=='size_h'): for crc in self.tree_children[tree]: self_tree_sort_item(tree,crc,False) else: @@ -3293,6 +3304,8 @@ def status_progress(self,text='',do_log=False): @restore_status_line @logwrapper def scan(self): + from threading import Thread + self.status('Scanning...') self.cfg.write() @@ -3401,8 +3414,8 @@ def scan(self): wait_var_get = wait_var.get while scan_thread_is_alive(): - new_data[3]='%s (%s)' % (local_bytes_to_str(dude_core.info_size_sum),local_bytes_to_str(dude_core.info_size_sum_images)) if similarity_mode else local_bytes_to_str(dude_core.info_size_sum) - new_data[4]='%s files (%s)' % (fnumber(dude_core.info_counter),fnumber(dude_core.info_counter_images)) if similarity_mode else '%s files' % fnumber(dude_core.info_counter) + new_data[3]=local_bytes_to_str(dude_core.info_size_sum) + new_data[4]=fnumber(dude_core.info_counter) anything_changed=False for i in (3,4): @@ -3420,6 +3433,10 @@ def scan(self): if anything_changed: time_without_busy_sign=now + if similarity_mode: + self_progress_dialog_on_scan_lab_r1_config(text=local_bytes_to_str(dude_core.info_size_sum_images)) + self_progress_dialog_on_scan_lab_r2_config(text=fnumber(dude_core.info_counter_images)) + if update_once: update_once=False self_tooltip_message[str_self_progress_dialog_on_scan_abort_button]='If you abort at this stage,\nyou will not get any results.' @@ -3469,7 +3486,7 @@ def scan(self): self_get_hg_ico = self.get_hg_ico if similarity_mode: - self_progress_dialog_on_scan_lab[0].configure(image='',text='') + self_progress_dialog_on_scan_lab[0].configure(image=self.ico_empty,text='') self_progress_dialog_on_scan_lab[1].configure(image='',text='') self_progress_dialog_on_scan_lab[2].configure(image='',text='') self_progress_dialog_on_scan_lab[3].configure(image='',text='') @@ -3487,16 +3504,14 @@ def scan(self): ih_thread_is_alive = ih_thread.is_alive - bytes_to_str_dude_core_sum_size = local_bytes_to_str(dude_core.sum_size) - - #self_progress_dialog_on_scan_lab[2].configure(text=dude_core.info_line) + bytes_to_str_dude_core_sum_size = local_bytes_to_str(dude_core.info_size_sum_images) + fnumber_dude_core_info_counter_images = fnumber(dude_core.info_counter_images) + aborted=False while ih_thread_is_alive(): anything_changed=False - size_progress_info=dude_core.info_size_done_perc - #print(f'{size_progress_info=}') if size_progress_info!=prev_progress_size: prev_progress_size=size_progress_info @@ -3505,26 +3520,19 @@ def scan(self): anything_changed=True quant_progress_info=dude_core.info_files_done_perc - if quant_progress_info!=prev_progress_quant: prev_progress_quant=quant_progress_info self_progress_dialog_on_scan_progr2var_set(quant_progress_info) - self_progress_dialog_on_scan_lab_r2_config(text='%s / %s' % (fnumber(dude_core.info_files_done),fnumber(dude_core.info_total))) + self_progress_dialog_on_scan_lab_r2_config(text='%s / %s' % (fnumber(dude_core.info_files_done),fnumber_dude_core_info_counter_images)) anything_changed=True self_progress_dialog_on_scan_area_main_update() - if dude_core.can_abort: - if self.action_abort: - self_progress_dialog_on_scan_lab[0].configure(text='',image='') - self_progress_dialog_on_scan_lab[1].configure(image='',text='Images hashing aborted') - self_progress_dialog_on_scan_lab[2].configure(text='') - self_progress_dialog_on_scan_lab[3].configure(text='') - self_progress_dialog_on_scan_lab[4].configure(text='') - self_progress_dialog_on_scan_area_main_update() - dude_core.abort() - break + if dude_core.can_abort and self.action_abort and not dude_core.abort_action: + dude_core.abort_action = True + self_progress_dialog_on_scan_lab[3].configure(image='',text='Aborted') + self_progress_dialog_on_scan.abort_button.configure(state='disabled',text='',image='') self_progress_dialog_on_scan_lab[0].configure(image=self_get_hg_ico(),text='') @@ -3535,10 +3543,17 @@ def scan(self): ih_thread.join() + self_progress_dialog_on_scan_progr1var_set(100) + self_progress_dialog_on_scan_progr2var_set(100) + + self_progress_dialog_on_scan_lab_r1_config(text='-') + self_progress_dialog_on_scan_lab_r2_config(text='-') + self_progress_dialog_on_scan.widget.title('Data clustering') + self_progress_dialog_on_scan.abort_button.configure(state='disabled',text='',image='') #################################################### - self_progress_dialog_on_scan_lab[0].configure(image='',text='') + self_progress_dialog_on_scan_lab[0].configure(image=self.ico_empty,text='') self_progress_dialog_on_scan_lab[1].configure(text='... Clustering data ...') self_progress_dialog_on_scan_lab[2].configure(text='') self_progress_dialog_on_scan_lab[3].configure(text='') @@ -3555,11 +3570,11 @@ def scan(self): sc_thread_is_alive = sc_thread.is_alive - self_progress_dialog_on_scan.abort_button.configure(state='disabled',text='',image='') - while sc_thread_is_alive(): self_progress_dialog_on_scan_lab[0].configure(image=self_get_hg_ico(),text='') + self_progress_dialog_on_scan_lab[1].configure(image='',text=dude_core.info_line) + self_main_after(50,lambda : wait_var_set(not wait_var_get())) self_main_wait_variable(wait_var) @@ -3568,14 +3583,13 @@ def scan(self): self_progress_dialog_on_scan.widget.config(cursor="watch") if not self.action_abort: - self_progress_dialog_on_scan_lab[0].configure(image='',text='Finished.') + self_progress_dialog_on_scan_lab[0].configure(image=self.ico_empty,text='Finished.') self_progress_dialog_on_scan_lab[1].configure(image='',text='... Rendering data ...') self_progress_dialog_on_scan_lab[2].configure(image='',text='') self_progress_dialog_on_scan_lab[3].configure(image='',text='') self_progress_dialog_on_scan_lab[4].configure(image='',text='') self_progress_dialog_on_scan_area_main_update() - #self_status('Finishing CRC Thread...') ############################# #self_progress_dialog_on_scan.label.configure(text='\n\nrendering data ...\n') @@ -3594,7 +3608,7 @@ def scan(self): crc_thread.start() update_once=True - self_progress_dialog_on_scan_lab[0].configure(image='',text='') + self_progress_dialog_on_scan_lab[0].configure(image=self.ico_empty,text='') self_progress_dialog_on_scan_lab[1].configure(image='',text='') self_progress_dialog_on_scan_lab[2].configure(image='',text='') self_progress_dialog_on_scan_lab[3].configure(image='',text='') @@ -5488,13 +5502,11 @@ def process_files_core(self,action,processed_items,remaining_items): self.process_files_core_perc_1 = self.process_files_size_sum*100/self.process_files_total_size self.process_files_core_perc_2 = self.process_files_counter*100/self.process_files_total - #for item in items_dict.values(): # kind,size,group, index_tuple = self_groups_tree_item_to_data[item] # (pathnr,path,file_name,ctime,dev,inode)=index_tuple # index_tuple_extended = pathnr,path,file_name,ctime,dev,inode,size - self.process_files_counter+=1 to_keep_item=remaining_items[group][0] @@ -5902,6 +5914,7 @@ def csv_save(self): @logwrapper def cache_clean(self): + from shutil import rmtree try: rmtree(CACHE_DIR) except Exception as e: @@ -5982,6 +5995,8 @@ def tree_action(self,tree,item,alt_pressed=False): @logwrapper def open_folder(self): + from subprocess import Popen + tree=self.sel_tree params=[] @@ -5998,6 +6013,7 @@ def open_folder(self): return if wrapper:=self.cfg_get(CFG_KEY_WRAPPER_FOLDERS): + params_num = self.cfg_get(CFG_KEY_WRAPPER_FOLDERS_PARAMS) num = 1024 if params_num=='all' else int(params_num) @@ -6011,6 +6027,8 @@ def open_folder(self): @logwrapper def open_file(self): + from subprocess import Popen + if self.sel_path_full and self.sel_file: file_to_open = sep.join([self.sel_path_full,self.sel_file])