@@ -268,19 +268,101 @@ delete_terms() {
268268set_posts_statuses () {
269269 wp post update " $( wp post list --post_type=" ${1} " --post_status=" ${2} " --format=ids) " --post_status=" ${3} "
270270}
271+ _project_open_root () {
272+ printf ' %s\n' " ${PROJECT_OPEN_ROOT:- ${HOME} / Code} "
273+ }
274+
275+ _project_open_scan () {
276+ local root
277+ root=" $( _project_open_root) "
278+ # Projects live at <root>/<category>/<project> and always hold a .git, so one
279+ # find at that fixed depth lists them; submodules sit deeper and fall out.
280+ find " ${root} " -mindepth 3 -maxdepth 3 -name ' .git' 2> /dev/null |
281+ sed ' s#/\.git$##' |
282+ LC_ALL=C sort
283+ }
284+
285+ _project_open_cache_file () {
286+ # Key the cache by a hash of the root so a different PROJECT_OPEN_ROOT never
287+ # reads a stale cache. Hashing (not char substitution) avoids path collisions;
288+ # trailing slashes are stripped so "/x" and "/x/" share one cache.
289+ local root key
290+ root=" $( _project_open_root) "
291+ root=" ${root%/ } "
292+ key=" $( printf ' %s' " ${root} " | cksum | cut -d' ' -f1) "
293+ printf ' %s\n' " ${XDG_CACHE_HOME:- ${HOME} / .cache} /project_open/projects-${key} "
294+ }
295+
296+ _project_open_build_cache () {
297+ local cache dir tmp root
298+ root=" $( _project_open_root) "
299+ [[ -d " ${root} " ]] || return 1
300+ cache=" $( _project_open_cache_file) "
301+ [[ -f " ${cache} " ]] && return 0
302+
303+ dir=" ${cache%/* } "
304+ mkdir -p " ${dir} " 2> /dev/null || return 1
305+ tmp=" $( mktemp " ${dir} /projects.XXXXXX" ) " || return 1
306+ if _project_open_scan > " ${tmp} " && mv -f " ${tmp} " " ${cache} " ; then
307+ return 0
308+ fi
309+ rm -f " ${tmp} "
310+ return 1
311+ }
312+
313+ _project_open_read_cache () {
314+ local cache line
315+ cache=" $( _project_open_cache_file) "
316+ [[ -f " ${cache} " ]] || return 0
317+ while IFS= read -r line || [[ -n " ${line} " ]]; do
318+ printf ' %s\n' " ${line} "
319+ done < " ${cache} "
320+ }
321+
322+ po_refresh () {
323+ local cache
324+ cache=" $( _project_open_cache_file) "
325+ rm -f " ${cache} "
326+ _project_open_build_cache
327+ }
328+
329+ _project_open_resolve () {
330+ local name=" ${1} " cache_only=" ${2} " cache line tmp
331+ [[ -n " ${name} " ]] || return 1
332+ cache=" $( _project_open_cache_file) "
333+ if [[ -f " ${cache} " ]]; then
334+ while IFS= read -r line || [[ -n " ${line} " ]]; do
335+ [[ " ${line##*/ } " == " ${name} " ]] && { printf ' %s\n' " ${line} " ; return 0; }
336+ done < " ${cache} "
337+ fi
338+ [[ -n " ${cache_only} " ]] && return 1
339+ tmp=" $( mktemp " ${TMPDIR:-/ tmp} /project_open_scan.XXXXXX" ) " || return 1
340+ _project_open_scan > " ${tmp} "
341+ # shellcheck disable=SC2094
342+ while IFS= read -r line || [[ -n " ${line} " ]]; do
343+ [[ " ${line##*/ } " == " ${name} " ]] && { printf ' %s\n' " ${line} " ; rm -f " ${tmp} " ; return 0; }
344+ done < " ${tmp} "
345+ rm -f " ${tmp} "
346+ return 1
347+ }
348+
271349project_open () {
272- WP_PATH =" ${HOME} /Code/wordpress "
350+ CODE_PATH =" $( _project_open_root ) "
273351
274352 if [ -z " ${1} " ]; then
275- NEW_PATH=" ${WP_PATH } "
353+ NEW_PATH=" ${CODE_PATH } "
276354
277355 tmux setw automatic-rename
278356 else
279- NEW_PATH=" ${WP_PATH} /${1} "
357+ NEW_PATH=" $( _project_open_resolve " ${1} " ) "
358+ if [ -z " ${NEW_PATH} " ]; then
359+ printf ' No project named %s under %s\n' " ${1} " " ${CODE_PATH} " >&2
360+ return 1
361+ fi
280362 tmux rename-window " ${1} "
281363 fi
282364
283- if [ -n " ${2} " ] && [[ " ${2} " != " theme " ]] ; then
365+ if [ -n " ${2} " ]; then
284366 # shellcheck disable=SC2164
285367 cd " ${NEW_PATH} /${2} "
286368 return
@@ -292,23 +374,70 @@ project_open() {
292374 NEW_PATH=" ${NEW_PATH} /site"
293375 fi
294376
295- if [ -z " ${2} " ]; then
296- # shellcheck disable=SC2164
297- cd " ${NEW_PATH} "
298- return
377+ # shellcheck disable=SC2164
378+ cd " ${NEW_PATH} "
379+ }
380+ alias po=' project_open'
381+
382+ _project_open_projects () {
383+ local proj
384+ _project_open_read_cache | while IFS= read -r proj; do
385+ printf ' %s\n' " ${proj##*/ } "
386+ done
387+ }
388+
389+ _project_open_targets () {
390+ local name=" ${1} " project_path target_dir
391+ [[ -n " ${name} " ]] || return 0
392+ project_path=" $( _project_open_resolve " ${name} " cache_only) "
393+ [[ -n " ${project_path} " ]] || return 0
394+ find " ${project_path} " -mindepth 1 -maxdepth 1 -type d -not -name ' .*' 2> /dev/null |
395+ while IFS= read -r target_dir; do
396+ printf ' %s\n' " ${target_dir##*/ } "
397+ done |
398+ LC_ALL=C sort
399+ }
400+
401+ _project_open_complete_bash () {
402+ local current previous candidate
403+ current=" ${COMP_WORDS[COMP_CWORD]} "
404+ COMPREPLY=()
405+
406+ if (( COMP_CWORD == 1 )) ; then
407+ while IFS= read -r candidate; do
408+ [[ " ${candidate} " == " ${current} " * ]] && COMPREPLY+=(" ${candidate} " )
409+ done < <( _project_open_projects)
410+ return 0
299411 fi
300412
301- # Find only themes using Composer.
302- THEMES=( " ${NEW_PATH} /web/app/themes/ " * /composer.json)
303- THEME_PATH= " ${THEMES[1] // composer.json / } "
304- if [ -z " ${THEME_PATH} " ] ; then
305- echo " No theme available " >&2
306- return 1
413+ if (( COMP_CWORD == 2 )) ; then
414+ previous= " ${COMP_WORDS[COMP_CWORD-1]} "
415+ while IFS= read -r candidate ; do
416+ [[ " ${candidate} " == " ${current} " * ]] && COMPREPLY+=( " ${candidate} " )
417+ done < <( _project_open_targets " ${previous} " )
418+ return 0
307419 fi
308420
309- cd " ${THEME_PATH} " || return
421+ return 0
310422}
311- alias po=' project_open'
423+
424+ if [[ -n " ${BASH_VERSION:- } " ]]; then
425+ complete -F _project_open_complete_bash project_open
426+ complete -F _project_open_complete_bash po
427+ if [[ $- == * i* ]]; then
428+ ( _project_open_build_cache & ) > /dev/null 2>&1
429+ fi
430+ fi
431+
432+ if [[ -n " ${ZSH_VERSION:- } " ]]; then
433+ # Completion is registered the canonical way via an autoloaded #compdef
434+ # function on $fpath (shell/completions/_project_open). Here we only kick
435+ # off the async cache build; eval defers the zsh-only `&!` past bash parsing.
436+ if [[ -o interactive ]]; then
437+ eval ' _project_open_build_cache >/dev/null 2>&1 &!'
438+ fi
439+ fi
440+
312441project_edit () {
313442 WP_PATH=" ${HOME} /Code/wordpress"
314443
0 commit comments