11-- @description ReaDashboard
2- -- @version 1.1.0
2+ -- @version 1.1.1
33-- @author Anshul
44-- @credits solger (for ReaLauncher concept)
55-- @about
1111-- - SWS Extensions
1212-- @changelog
1313--
14+ -- v1.1.1
15+ -- + Added recursive subfolder scanning for artwork with configurable depth (Settings -> Data Sources)
16+ -- + Added Tab key shortcut to jump focus from project area back to the search bar
17+ -- + Added Ctrl+Down/Up shortcuts to instantly jump focus from the search bar into the project area
18+ --
1419-- v1.1.0
1520-- + Guitar Mode toggle — hide all guitar-specific fields (strings, tuning, transpose, guitar, amp) via Settings; data untouched
1621-- + Scan staleness indicator — status bar shows when the last hard refresh was run (right-aligned, faint); Refresh button tooltip updated
@@ -142,7 +147,7 @@ local ImGui = require 'imgui' '0.10'
142147local CFG = {
143148 -- Script identity
144149 SCRIPT_NAME = ' ReaDashboard' ,
145- SCRIPT_VERSION = ' 1.1.0 ' ,
150+ SCRIPT_VERSION = ' 1.1.1 ' ,
146151 EXT_SECTION = ' ReaDashboard' ,
147152
148153 -- Window defaults
@@ -549,6 +554,7 @@ local S = {
549554 active_tab = ' recent' , -- 'recent', 'all', or 'settings'
550555 universal_search = true ,
551556 auto_focus_search = false , -- auto-focus search bar on script open
557+ search_bar_active = false , -- runtime: search bar has focus or was just deactivated this frame
552558 recent_count_in_filtered = 0 ,
553559 keep_open = true ,
554560 persistent_mode = false , -- keep script alive in background when window closed
@@ -654,6 +660,7 @@ local S = {
654660 ALL_PROJECTS_PATH = ' ' , -- primary path (legacy, for backward compat)
655661 additional_project_paths = {}, -- additional scan paths (list of strings)
656662 default_artwork_path = ' ' , -- fallback art when project has no art
663+ art_scan_depth = 1 , -- depth to recursively scan for artwork (0 = root only)
657664 placeholder_full_name = false , -- show full project name on placeholder instead of initials
658665 debug_logging = false ,
659666 ALL_SCAN_MAX_DEPTH = 10 ,
@@ -718,14 +725,15 @@ local S = {
718725
719726 -- Search history
720727 search_history = {}, -- circular buffer of recent searches (newest first)
721- search_history_max = 8 , -- max entries to keep
728+ search_history_max = 10 , -- max entries to keep
722729 search_history_enabled = true , -- toggle in Settings
723730 search_history_idx = 0 , -- current Up/Down arrow index (0 = live input, 1+ = history)
724731 search_buf_live = ' ' , -- live input before arrow navigation started
725732 search_history_callback = nil , -- EEL callback function for InputTextFlags_CallbackHistory
726733
727734 -- Programmatic tab switching (set by keyboard shortcut, consumed by tab drawing code)
728735 pending_tab = nil , -- nil or 'recent'/'all'/'settings'/'actions'
736+ focus_search_pending = false , -- runtime: Ctrl+F triggered, focus search bar next frame
729737}
730738
731739--- Recompute all derived theme colors from current S state. Call after any theme setting change.
@@ -2636,6 +2644,37 @@ local function LoadRecentProjects()
26362644 return result
26372645end
26382646
2647+ --- Helper: Collect image full paths recursively up to max_depth.
2648+ local function CollectImages (dir , max_depth , current_depth , results )
2649+ current_depth = current_depth or 0
2650+ results = results or {}
2651+
2652+ local fidx = 0
2653+ while true do
2654+ local fname = reaper .EnumerateFiles (dir , fidx )
2655+ if not fname then break end
2656+ local ext = fname :lower ():match (' %.([^%.]+)$' )
2657+ if ext == ' jpg' or ext == ' jpeg' or ext == ' png' then
2658+ results [# results + 1 ] = dir .. ' /' .. fname
2659+ end
2660+ fidx = fidx + 1
2661+ end
2662+
2663+ if current_depth < max_depth then
2664+ local didx = 0
2665+ while true do
2666+ local dname = reaper .EnumerateSubdirectories (dir , didx )
2667+ if not dname then break end
2668+ -- ignore dot folders (.git, .reaper, etc) to be safe
2669+ if dname :sub (1 , 1 ) ~= ' .' then
2670+ CollectImages (dir .. ' /' .. dname , max_depth , current_depth + 1 , results )
2671+ end
2672+ didx = didx + 1
2673+ end
2674+ end
2675+ return results
2676+ end
2677+
26392678--- Enrich projects with metadata from spicetify DB, RPP parsing, album art, and tags.
26402679local function EnrichProjects (projectList )
26412680 ScanSpicetifyDB ()
@@ -2688,30 +2727,21 @@ local function EnrichProjects(projectList)
26882727 end
26892728 end
26902729
2691- -- 1b-1d. Relaxed matching: enumerate all images in folder
2730+ -- 1b-1d. Relaxed matching: enumerate all images in folder and allowed subfolders
26922731 if not artFound then
2693- local images = {}
2694- local fidx = 0
2695- while true do
2696- local fname = reaper .EnumerateFiles (proj .dir , fidx )
2697- if not fname then break end
2698- local ext = fname :lower ():match (' %.([^%.]+)$' )
2699- if ext == ' jpg' or ext == ' jpeg' or ext == ' png' then
2700- images [# images + 1 ] = fname
2701- end
2702- fidx = fidx + 1
2703- end
2732+ local images = CollectImages (proj .dir , S .art_scan_depth )
27042733
27052734 if # images == 1 then
2706- -- 1b. Single image rule: only one image in folder → use it
2707- proj .albumArtPath = proj . dir .. ' / ' .. images [1 ]
2735+ -- 1b. Single image rule: only one image found → use it
2736+ proj .albumArtPath = images [1 ]
27082737 artFound = true
27092738 elseif # images > 1 then
27102739 -- 1c. Project-name match: image filename contains the project name
27112740 local projNameLower = proj .name :lower ()
2712- for _ , img in ipairs (images ) do
2713- if img :lower ():find (projNameLower , 1 , true ) then
2714- proj .albumArtPath = proj .dir .. ' /' .. img
2741+ for _ , imgPath in ipairs (images ) do
2742+ local filename = imgPath :match (' [^/\\ ]+$' ) or imgPath
2743+ if filename :lower ():find (projNameLower , 1 , true ) then
2744+ proj .albumArtPath = imgPath
27152745 artFound = true
27162746 break
27172747 end
@@ -2720,8 +2750,7 @@ local function EnrichProjects(projectList)
27202750 -- 1d. Most-recently-modified fallback
27212751 if not artFound then
27222752 local bestImg , bestDate = nil , ' '
2723- for _ , img in ipairs (images ) do
2724- local imgPath = proj .dir .. ' /' .. img
2753+ for _ , imgPath in ipairs (images ) do
27252754 local imgExists , imgDate = GetFileInfo (imgPath )
27262755 if imgExists and imgDate > bestDate then
27272756 bestDate = imgDate
@@ -3911,6 +3940,7 @@ local function SaveState()
39113940 Set (' symlink_dest' , S .symlink_dest )
39123941
39133942 Set (' default_artwork_path' , S .default_artwork_path )
3943+ Set (' art_scan_depth' , tostring (S .art_scan_depth ))
39143944 Set (' placeholder_full_name' , S .placeholder_full_name and ' 1' or ' 0' )
39153945 Set (' all_projects_path' , S .ALL_PROJECTS_PATH )
39163946 Set (' additional_project_paths' , table.concat (S .additional_project_paths , ' |' ))
@@ -4136,6 +4166,8 @@ local function LoadState()
41364166
41374167 local dap = G (' default_artwork_path' )
41384168 if dap ~= ' ' then S .default_artwork_path = dap end
4169+ local asd = tonumber (G (' art_scan_depth' ))
4170+ if asd then S .art_scan_depth = asd end
41394171 local pfn = G (' placeholder_full_name' )
41404172 if pfn ~= ' ' then S .placeholder_full_name = (pfn == ' 1' ) end
41414173
@@ -4439,6 +4471,12 @@ local function DrawTopBar()
44394471 ImGui .SetKeyboardFocusHere (ctx )
44404472 end
44414473
4474+ -- Ctrl+F: one-shot focus request
4475+ if S .focus_search_pending then
4476+ ImGui .SetKeyboardFocusHere (ctx )
4477+ S .focus_search_pending = false
4478+ end
4479+
44424480 -- Before rendering InputText: reset callback signals and pre-compute both
44434481 -- possible history buffers so the EEL callback can pick the right one.
44444482 -- Direction: Down = older history, Up = newer/live (reversed from typical shell).
@@ -4477,8 +4515,24 @@ local function DrawTopBar()
44774515 local use_history_cb = S .search_history_enabled and S .search_history_callback and # S .search_history > 0
44784516 local search_flags = use_history_cb and ImGui .InputTextFlags_CallbackHistory or 0
44794517 local search_cb = use_history_cb and S .search_history_callback or nil
4518+ if use_history_cb then
4519+ local ctrl_down = ImGui .IsKeyDown (ctx , ImGui .Key_LeftCtrl ) or ImGui .IsKeyDown (ctx , ImGui .Key_RightCtrl )
4520+ ImGui .Function_SetValue (S .search_history_callback , ' ctrl_is_down' , ctrl_down and 1 or 0 )
4521+ end
44804522 changed , S .search_buf = ImGui .InputTextWithHint (ctx , ' ##search' , ' Search projects, artists, tags...' , S .search_buf , search_flags , search_cb )
44814523 local search_deactivated = ImGui .IsItemDeactivated (ctx )
4524+ -- Capture while we're still on the right item: true if currently editing OR just deactivated this frame.
4525+ -- Used by HandleKeys to avoid closing the window when Escape is pressed to exit the search bar.
4526+ S .search_bar_active = ImGui .IsItemActive (ctx ) or search_deactivated
4527+
4528+ -- Tab while search bar is focused: jump directly to project list
4529+ if S .search_bar_active and ImGui .IsKeyPressed (ctx , ImGui .Key_Tab ) then
4530+ local shift_held = ImGui .IsKeyDown (ctx , ImGui .Key_LeftShift ) or ImGui .IsKeyDown (ctx , ImGui .Key_RightShift )
4531+ if not shift_held then
4532+ local new_idx = S .selected_idx < 1 and 1 or S .selected_idx
4533+ SelectOnly (new_idx )
4534+ end
4535+ end
44824536
44834537 -- Auto-save to history when search bar loses focus with non-empty text
44844538 if search_deactivated and S .search_buf ~= ' ' then
@@ -6981,6 +7035,11 @@ local function HandleKeys()
69817035 return
69827036 end
69837037
7038+ -- Ctrl+F = focus search bar (works even when other items have focus)
7039+ if ctrl_down and ImGui .IsKeyPressed (ctx , ImGui .Key_F ) then
7040+ S .focus_search_pending = true
7041+ end
7042+
69847043 -- Ctrl+1/2/3/4 = switch to tab (Recent/All/Settings/Actions)
69857044 if ctrl_down then
69867045 local tab_targets = { ' recent' , ' all' , ' settings' , ' actions' }
@@ -7005,6 +7064,29 @@ local function HandleKeys()
70057064 end
70067065 end
70077066
7067+ -- Ctrl+Down/Up: Jump focus from search bar to project area
7068+ -- If already in project area, this falls through to normal arrow navigation
7069+ if ctrl_down and (ImGui .IsKeyPressed (ctx , ImGui .Key_DownArrow ) or ImGui .IsKeyPressed (ctx , ImGui .Key_UpArrow )) then
7070+ if S .search_bar_active then
7071+ -- Search -> Project area
7072+ local new_idx = S .selected_idx < 1 and 1 or S .selected_idx
7073+ SelectOnly (new_idx )
7074+ return -- Prevent Arrow Key navigation block from processing this as a double-movement
7075+ end
7076+ end
7077+
7078+ -- Tab: Return focus to search bar from project area
7079+ if ImGui .IsKeyPressed (ctx , ImGui .Key_Tab ) and not S .search_bar_active then
7080+ local shift_held = ImGui .IsKeyDown (ctx , ImGui .Key_LeftShift ) or ImGui .IsKeyDown (ctx , ImGui .Key_RightShift )
7081+ if not shift_held and not ctrl_down then
7082+ -- Only jump if we are in the project tabs, so we don't break Settings tab navigation
7083+ if S .active_tab == ' recent' or S .active_tab == ' all' or S .active_tab == ' favorites' then
7084+ S .focus_search_pending = true
7085+ return
7086+ end
7087+ end
7088+ end
7089+
70087090 -- F5 = normal refresh (cached), Shift+F5 = hard refresh (re-scan all)
70097091 if ImGui .IsKeyPressed (ctx , ImGui .Key_F5 ) then
70107092 if ImGui .IsKeyDown (ctx , ImGui .Key_LeftShift ) or ImGui .IsKeyDown (ctx , ImGui .Key_RightShift ) then
@@ -7023,13 +7105,15 @@ local function HandleKeys()
70237105 S .window_open = false
70247106 end
70257107
7026- -- Escape: clear selection first, then close/hide window if setting allows
7108+ -- Escape: clear selection first, then close/hide window if setting allows.
7109+ -- Guard: if the search bar was active this frame, Escape only unfocused it (Dear ImGui
7110+ -- handles that internally). Do NOT also close the window — let the user start navigating.
70277111 if ImGui .IsKeyPressed (ctx , ImGui .Key_Escape ) and not shift_held then
70287112 if SelectionCount () > 1 then
70297113 -- Reduce to single selection on primary idx
70307114 S .selected = {}
70317115 if S .selected_idx > 0 then S .selected [S .selected_idx ] = true end
7032- elseif S .close_on_escape then
7116+ elseif S .close_on_escape and not S . search_bar_active then
70337117 S .window_open = false
70347118 end
70357119 end
@@ -7122,6 +7206,11 @@ local function HandleKeys()
71227206 end
71237207 end
71247208 end
7209+
7210+ -- / = focus search bar (only when no input is active, to avoid stealing the character)
7211+ if ImGui .IsKeyPressed (ctx , ImGui .Key_Slash ) and not ctrl_down then
7212+ S .focus_search_pending = true
7213+ end
71257214 end
71267215end
71277216
@@ -7268,6 +7357,15 @@ local function DrawSettingsTab()
72687357 end
72697358 end
72707359
7360+ -- Artwork Recursive Scan Depth
7361+ ImGui .AlignTextToFramePadding (ctx )
7362+ ImGui .Text (ctx , ' Art Scan Depth:' )
7363+ if ImGui .IsItemHovered (ctx ) then ImGui .SetTooltip (ctx , ' Subfolder depth to scan for custom artwork (1 = immediate subfolders).\n 0 = only check root project folder.' ) end
7364+ ImGui .SameLine (ctx , lbl_w )
7365+ ImGui .SetNextItemWidth (ctx , 100 )
7366+ local sd_chg , sd_new = ImGui .InputInt (ctx , ' ##art_scan_depth' , S .art_scan_depth )
7367+ if sd_chg then S .art_scan_depth = math.max (0 , math.min (9 , sd_new )); changed = true end
7368+
72717369 ImGui .Spacing (ctx )
72727370
72737371 -- Guitar mode toggle
@@ -8459,6 +8557,11 @@ local function DrawActionsTab()
84598557 { ' Up / Down' , ' Navigate project list (in grid: move by row) or cycle search history' },
84608558 { ' Left / Right' , ' Navigate grid columns' },
84618559 { ' Home / End' , ' Jump to first or last project' },
8560+ { ' Tab' , ' Toggle focus between search bar and project list' },
8561+ { ' Ctrl+Down/Up' , ' Jump focus from search bar to project list' },
8562+ { ' Ctrl+F or /' , ' Focus search bar' },
8563+ { ' Escape (in search)' , ' Unfocus search bar (does not close window)' },
8564+ { ' Enter (in search)' , ' Open selected / first result directly' },
84628565 { ' Ctrl+A' , ' Select all visible projects' },
84638566 { ' Ctrl+C' , ' Copy selected project path(s)' },
84648567 { ' Ctrl+Click' , ' Toggle project in/out of selection' },
@@ -9061,7 +9164,7 @@ local function Init()
90619164 -- Arrow key mapping: Down = older history (direction 1), Up = newer/live (direction -1).
90629165 -- This feels natural: "scroll down through past searches, up to return to current".
90639166 S .search_history_callback = ImGui .CreateFunctionFromEEL ([[
9064- EventFlag == InputTextFlags_CallbackHistory ? (
9167+ EventFlag == InputTextFlags_CallbackHistory && !ctrl_is_down ? (
90659168 EventKey == Key_DownArrow ? (
90669169 can_go_older ? (
90679170 hist_direction = 1;
0 commit comments