file_manager.py (13218B)
1 from talon import app, Module, Context, actions, ui, imgui, settings, app, registry 2 from os.path import expanduser 3 from subprocess import Popen 4 from pathlib import Path 5 from typing import List, Union 6 import os 7 import math 8 import re 9 from itertools import islice 10 11 mod = Module() 12 ctx = Context() 13 14 mod.tag("file_manager", desc="Tag for enabling generic file management commands") 15 mod.list("file_manager_directories", desc="List of subdirectories for the current path") 16 mod.list("file_manager_files", desc="List of files at the root of the current path") 17 18 19 setting_auto_show_pickers = mod.setting( 20 "file_manager_auto_show_pickers", 21 type=int, 22 default=0, 23 desc="Enable to show the file/directories pickers automatically", 24 ) 25 setting_folder_limit = mod.setting( 26 "file_manager_folder_limit", 27 type=int, 28 default=1000, 29 desc="Maximum number of files/folders to iterate", 30 ) 31 setting_file_limit = mod.setting( 32 "file_manager_file_limit", 33 type=int, 34 default=1000, 35 desc="Maximum number of files to iterate", 36 ) 37 setting_imgui_limit = mod.setting( 38 "file_manager_imgui_limit", 39 type=int, 40 default=20, 41 desc="Maximum number of files/folders to display in the imgui", 42 ) 43 setting_imgui_string_limit = mod.setting( 44 "file_manager_string_limit", 45 type=int, 46 default=20, 47 desc="Maximum like of string to display in the imgui", 48 ) 49 cached_path = None 50 file_selections = folder_selections = [] 51 current_file_page = current_folder_page = 1 52 53 ctx.lists["self.file_manager_directories"] = [] 54 ctx.lists["self.file_manager_files"] = [] 55 56 directories_to_remap = {} 57 user_path = os.path.expanduser("~") 58 if app.platform == "windows": 59 is_windows = True 60 import ctypes 61 62 GetUserNameEx = ctypes.windll.secur32.GetUserNameExW 63 NameDisplay = 3 64 65 size = ctypes.pointer(ctypes.c_ulong(0)) 66 GetUserNameEx(NameDisplay, None, size) 67 68 nameBuffer = ctypes.create_unicode_buffer(size.contents.value) 69 GetUserNameEx(NameDisplay, nameBuffer, size) 70 one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) 71 72 # this is probably not the correct way to check for onedrive, quick and dirty 73 if os.path.isdir(os.path.expanduser(os.path.join("~", r"OneDrive\Desktop"))): 74 default_folder = os.path.join("~", "Desktop") 75 76 directories_to_remap = { 77 "Desktop": os.path.join(one_drive_path, "Desktop"), 78 "Documents": os.path.join(one_drive_path, "Documents"), 79 "Downloads": os.path.join(user_path, "Downloads"), 80 "Music": os.path.join(user_path, "Music"), 81 "OneDrive": one_drive_path, 82 "Pictures": os.path.join(one_drive_path, "Pictures"), 83 "Videos": os.path.join(user_path, "Videos"), 84 } 85 else: 86 # todo use expanduser for cross platform support 87 directories_to_remap = { 88 "Desktop": os.path.join(user_path, "Desktop"), 89 "Documents": os.path.join(user_path, "Documents"), 90 "Downloads": os.path.join(user_path, "Downloads"), 91 "Music": os.path.join(user_path, "Music"), 92 "OneDrive": one_drive_path, 93 "Pictures": os.path.join(user_path, "Pictures"), 94 "Videos": os.path.join(user_path, "Videos"), 95 } 96 97 98 @mod.action_class 99 class Actions: 100 def file_manager_current_path() -> str: 101 """Returns the current path for the active file manager.""" 102 return "" 103 104 def file_manager_open_parent(): 105 """file_manager_open_parent""" 106 return 107 108 def file_manager_go_forward(): 109 """file_manager_go_forward_directory""" 110 111 def file_manager_go_back(): 112 """file_manager_go_forward_directory""" 113 114 def file_manager_open_volume(volume: str): 115 """file_manager_open_volume""" 116 117 def file_manager_open_directory(path: str): 118 """opens the directory that's already visible in the view""" 119 120 def file_manager_select_directory(path: str): 121 """selects the directory""" 122 123 def file_manager_new_folder(name: str): 124 """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" 125 126 def file_manager_show_properties(): 127 """Shows the properties for the file""" 128 129 def file_manager_terminal_here(): 130 """Opens terminal at current location""" 131 132 def file_manager_open_file(path: str): 133 """opens the file""" 134 135 def file_manager_select_file(path: str): 136 """selects the file""" 137 138 def file_manager_refresh_title(): 139 """Refreshes the title to match current directory. this is for e.g. windows command prompt that will need to do some magic. """ 140 return 141 142 def file_manager_update_lists(): 143 """Forces an update of the lists (e.g., when file or folder created)""" 144 update_lists() 145 146 def file_manager_toggle_pickers(): 147 """Shows the pickers""" 148 if gui_files.showing: 149 gui_files.hide() 150 gui_folders.hide() 151 else: 152 gui_files.show() 153 gui_folders.show() 154 155 def file_manager_hide_pickers(): 156 """Hides the pickers""" 157 if gui_files.showing: 158 gui_files.hide() 159 gui_folders.hide() 160 161 def file_manager_open_user_directory(path: str): 162 """expands and opens the user directory""" 163 # this functionality exists mostly for windows. 164 # since OneDrive does strange stuff... 165 if path in directories_to_remap: 166 path = directories_to_remap[path] 167 168 path = os.path.expanduser(os.path.join("~", path)) 169 actions.user.file_manager_open_directory(path) 170 171 def file_manager_get_directory_by_index(index: int) -> str: 172 """Returns the requested directory for the imgui display by index""" 173 index = (current_folder_page - 1) * setting_imgui_limit.get() + index 174 assert index < len(folder_selections) 175 return folder_selections[index] 176 177 def file_manager_get_file_by_index(index: int) -> str: 178 """Returns the requested directory for the imgui display by index""" 179 index = (current_file_page - 1) * setting_imgui_limit.get() + index 180 assert index < len(file_selections) 181 return file_selections[index] 182 183 def file_manager_next_file_page(): 184 """next_file_page""" 185 global current_file_page 186 if gui_files.showing: 187 if current_file_page != total_file_pages: 188 current_file_page += 1 189 else: 190 current_file_page = 1 191 gui_files.show() 192 193 def file_manager_previous_file_page(): 194 """previous_file_page""" 195 global current_file_page 196 if gui_files.showing: 197 if current_file_page != 1: 198 current_file_page -= 1 199 else: 200 current_file_page = total_file_pages 201 202 gui_files.show() 203 204 def file_manager_next_folder_page(): 205 """next_folder_page""" 206 global current_folder_page 207 if gui_folders.showing: 208 if current_folder_page != total_folder_pages: 209 current_folder_page += 1 210 else: 211 current_folder_page = 1 212 213 gui_folders.show() 214 215 def file_manager_previous_folder_page(): 216 """previous_folder_page""" 217 global current_folder_page 218 if gui_folders.showing: 219 if current_folder_page != 1: 220 current_folder_page -= 1 221 else: 222 current_folder_page = total_folder_pages 223 224 gui_folders.show() 225 226 227 pattern = re.compile(r"[A-Z][a-z]*|[a-z]+|\d") 228 229 230 def create_spoken_forms(symbols, max_len=30): 231 return [" ".join(list(islice(pattern.findall(s), max_len))) for s in symbols] 232 233 234 def is_dir(f): 235 try: 236 return f.is_dir() 237 except: 238 return False 239 240 241 def is_file(f): 242 try: 243 return f.is_file() 244 except: 245 return False 246 247 248 def get_directory_map(current_path): 249 directories = [ 250 f.name 251 for f in islice( 252 current_path.iterdir(), settings.get("user.file_manager_folder_limit", 1000) 253 ) 254 if is_dir(f) 255 ] 256 # print(len(directories)) 257 spoken_forms = create_spoken_forms(directories) 258 return dict(zip(spoken_forms, directories)) 259 260 261 def get_file_map(current_path): 262 files = [ 263 f.name 264 for f in islice( 265 current_path.iterdir(), settings.get("user.file_manager_file_limit", 1000) 266 ) 267 if is_file(f) 268 ] 269 # print(str(files)) 270 spoken_forms = create_spoken_forms([p for p in files]) 271 return dict(zip(spoken_forms, [f for f in files])) 272 273 274 @imgui.open(y=10, x=900) 275 def gui_folders(gui: imgui.GUI): 276 global current_folder_page, total_folder_pages 277 total_folder_pages = math.ceil( 278 len(ctx.lists["self.file_manager_directories"]) / setting_imgui_limit.get() 279 ) 280 gui.text( 281 "Select a directory ({}/{})".format(current_folder_page, total_folder_pages) 282 ) 283 gui.line() 284 285 index = 1 286 current_index = (current_folder_page - 1) * setting_imgui_limit.get() 287 288 while index <= setting_imgui_limit.get() and current_index < len(folder_selections): 289 name = ( 290 ( 291 folder_selections[current_index][: setting_imgui_string_limit.get()] 292 + ".." 293 ) 294 if len(folder_selections[current_index]) > setting_imgui_string_limit.get() 295 else folder_selections[current_index] 296 ) 297 gui.text("{}: {} ".format(index, name)) 298 current_index += 1 299 index = index + 1 300 301 # if total_folder_pages > 1: 302 # gui.spacer() 303 304 # if gui.button('Next...'): 305 # actions.user.file_manager_next_folder_page() 306 307 # if gui.button("Previous..."): 308 # actions.user.file_manager_previous_folder_page() 309 310 311 @imgui.open(y=10, x=1300) 312 def gui_files(gui: imgui.GUI): 313 global file_selections, current_file_page, total_file_pages 314 total_file_pages = math.ceil(len(file_selections) / setting_imgui_limit.get()) 315 316 gui.text("Select a file ({}/{})".format(current_file_page, total_file_pages)) 317 gui.line() 318 index = 1 319 current_index = (current_file_page - 1) * setting_imgui_limit.get() 320 321 while index <= setting_imgui_limit.get() and current_index < len(file_selections): 322 name = ( 323 (file_selections[current_index][: setting_imgui_string_limit.get()] + "..") 324 if len(file_selections[current_index]) > setting_imgui_string_limit.get() 325 else file_selections[current_index] 326 ) 327 328 gui.text("{}: {} ".format(index, name)) 329 current_index = current_index + 1 330 index = index + 1 331 332 # if total_file_pages > 1: 333 # gui.spacer() 334 335 # if gui.button('Next...'): 336 # actions.user.file_manager_next_file_page() 337 338 # if gui.button("Previous..."): 339 # actions.user.file_manager_previous_file_page() 340 341 342 def clear_lists(): 343 global folder_selections, file_selections 344 if ( 345 len(ctx.lists["self.file_manager_directories"]) > 0 346 or len(ctx.lists["self.file_manager_files"]) > 0 347 ): 348 current_folder_page = current_file_page = 1 349 ctx.lists["self.file_manager_directories"] = [] 350 ctx.lists["self.file_manager_files"] = [] 351 folder_selections = [] 352 file_selections = [] 353 354 355 def update_gui(): 356 if gui_folders.showing or setting_auto_show_pickers.get() >= 1: 357 gui_folders.show() 358 gui_files.show() 359 360 361 def update_lists(): 362 global folder_selections, file_selections, current_folder_page, current_file_page 363 is_valid_path = False 364 path = actions.user.file_manager_current_path() 365 directories = {} 366 files = {} 367 folder_selections = [] 368 file_selections = [] 369 # print(path) 370 try: 371 current_path = Path(path) 372 is_valid_path = current_path.is_dir() 373 except: 374 is_valid_path = False 375 376 if is_valid_path: 377 # print("valid..." + str(current_path)) 378 try: 379 directories = get_directory_map(current_path) 380 files = get_file_map(current_path) 381 except: 382 # print("invalid path...") 383 384 directories = {} 385 files = {} 386 387 current_folder_page = current_file_page = 1 388 ctx.lists["self.file_manager_directories"] = directories 389 ctx.lists["self.file_manager_files"] = files 390 folder_selections = sorted(directories.values(), key=str.casefold) 391 file_selections = sorted(files.values(), key=str.casefold) 392 393 update_gui() 394 395 396 def win_event_handler(window): 397 global cached_path 398 399 # on windows, we get events from the clock 400 # and such, so this check is important 401 if not window.app.exe or window != ui.active_window(): 402 return 403 404 path = actions.user.file_manager_current_path() 405 406 if not "user.file_manager" in registry.tags: 407 actions.user.file_manager_hide_pickers() 408 clear_lists() 409 elif path: 410 if cached_path != path: 411 update_lists() 412 elif cached_path: 413 clear_lists() 414 actions.user.file_manager_hide_pickers() 415 416 cached_path = path 417 418 419 def register_events(): 420 ui.register("win_title", win_event_handler) 421 ui.register("win_focus", win_event_handler) 422 423 424 # prevent scary errors in the log by waiting for talon to be fully loaded 425 # before registering the events 426 app.register("ready", register_events) 427