├── README.md ├── UNLICENSE ├── filetree.kak └── filetree.perl /README.md: -------------------------------------------------------------------------------- 1 | # kakoune-filetree 2 | 3 | [Kakoune](http://kakoune.org) plugin to view and navigate files, using `tree`. 4 | 5 | [![Demo](https://asciinema.org/a/568907.png)](https://asciinema.org/a/568907) 6 | 7 | ## Setup 8 | 9 | Add `filetree.kak` and `filetree.perl` to your `autoload` directory: `~/.config/kak/autoload/`, or source the kakoune file manually. 10 | The two files must be in the same directory for the plugin to function. 11 | 12 | The plugin has a dependency on `tree` as well as `perl`. 13 | 14 | ## Basic usage 15 | 16 | Simply call `:filetree`. A new buffer will be open with all files found below the specified directory (Kakoune's current directory by default), presented in a tree-like structure. 17 | 18 | From the `*filetree*` buffer, you can open files by pressing Return (calling the command `filetree-open-selected-files`). 19 | 20 | The files that are open in buffers are highlighted with a special face. 21 | 22 | ## Navigation 23 | 24 | The `filetree-edit` command can be used for opening files, using the fuzzy matching engine of Kakoune. The completions are generated using the content of the `*filetree*` buffer: any file in the tree is proposed as a possible completion. This command can be used from any context, as long as the `*filetree*` buffer exists. 25 | 26 | The `filetree-goto` command can be used for selecting entries in the filetree, using the same fuzzy completion as with `filetree-edit`. 27 | 28 | The following commands can be used to navigate the files and directories depending on their relationship: 29 | * `filetree-select-prev-sibling`: select next entry in the same directory as the selected entry (``) 30 | * `filetree-select-next-sibling`: select previous entry in the same directory as the selected entry (``) 31 | * `filetree-select-parent-directory`: select parent directory of the selected entry (``) 32 | * `filetree-select-first-child`: select first entry in the selected directory (``) 33 | * `filetree-select-direct-children`: select all entries directly in the selected directory 34 | * `filetree-select-all-children`: select all entries in all subdirectories of the selected directory 35 | 36 | The `filetree-select-open-files` command can be used to select all files currently opened in buffers. 37 | 38 | ## Manipulation 39 | 40 | The `*filetree*` buffer can be modified by hand as long as the general tree-structure is preserved (for example, certain irrelevant files or directories can be filtered out by deleting the corresponding lines). 41 | 42 | The `*filetree*` buffer supports limited manipulation of the filesystem with the commands `filetree-create-child` and `filetree-create-sibling`. 43 | Note that the two commands do not create the files themselves, they only adjust the buffer to keep the tree structure valid. The desired filename can then be typed out, and then opened with `filetree-open-selected-files -create`. 44 | 45 | It is currently not possible (nor planned) to rename, move or delete files using this plugin. 46 | 47 | ## Customization 48 | 49 | The `filetree` command offers various switches to control which files are shown in the tree and in which order. 50 | * `-files-first`: for each level, show files before directories 51 | * `-dirs-first`: for each level, show directories before files 52 | * `-consider-gitignore`: do not show any entries matched by gitignore rules 53 | * `-no-empty-dirs`: do not show empty directories 54 | * `-show-hidden`: show hidden files and directories 55 | * `-depth `: only traverse the root directory up to directories deep (unlimited by default) 56 | * `-only-dirs`: only show directories, not files 57 | 58 | In addition, the following options and faces can be changed to affect the style of the tree: 59 | * option `filetree_indentation_level`: number of padding characters for each depth level (>=0, default 3) 60 | * face `FileTreePipes`: used for the indentation lines of the tree (`rgb:606060,default`) 61 | * face `FileTreeDirName`: used for directories (`blue,default+b`) 62 | * face `FileTreeFileName`: used for files (`default,default`) 63 | * face `FileTreeOpenFiles`: used for files that have an open buffer (`black,yellow`) 64 | * face `FileTreeEmptyName`: used for highlighting empty (and therefore invalid) entries, such as when creating them (`default,red`) 65 | 66 | ## License 67 | 68 | [Unlicense](http://unlicense.org) 69 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /filetree.kak: -------------------------------------------------------------------------------- 1 | declare-option -hidden str filetree_script_path %val{source} 2 | 3 | provide-module filetree %{ 4 | 5 | declare-option -docstring "Name of the client in which all source code jumps will be executed" str jumpclient 6 | declare-option -docstring "name of the client in which utilities display information" str toolsclient 7 | 8 | declare-option -hidden bool filetree_highlight_dirty 9 | declare-option -hidden str filetree_root_directory 10 | declare-option -hidden range-specs filetree_open_files 11 | 12 | declare-option int filetree_indentation_level 3 13 | 14 | face global FileTreeOpenFiles black,yellow 15 | face global FileTreePipesColor rgb:606060,default 16 | face global FileTreeDirColor blue,default+b 17 | face global FileTreeFileName default,default 18 | face global FileTreeEmptyName black,red 19 | 20 | define-command filetree-switch-or-start -params .. -docstring ' 21 | filetree-switch-or-start: switch to the *filetree* buffer. 22 | If the buffer does not exist, or the current kakoune directory has changed, it is generated from 23 | scratch. In this case, all arguments are forwarded to the ''filetree'' command. 24 | ' -shell-script-candidates %{ 25 | printf '%s\n' -files-first -dirs-first -consider-gitignore -no-empty-dirs -show-hidden -depth './' */ 26 | } %{ 27 | try %{ 28 | eval -try-client %opt{toolsclient} %{ 29 | buffer *filetree* 30 | eval %sh{ [ "$kak_opt_filetree_root_directory" != "$PWD" ] && printf 'fail' } 31 | } 32 | } catch %{ 33 | filetree %arg{@} 34 | } 35 | } 36 | 37 | define-command filetree -params .. -docstring ' 38 | filetree [] []: open an interactive buffer representing the current directory 39 | Switches: 40 | -files-first: for each level, show files before directories 41 | -dirs-first: for each level, show directories before files 42 | -consider-gitignore: do not show any entries matched by gitignore rules 43 | -no-empty-dirs: do not show empty directories 44 | -show-hidden: show hidden files and directories 45 | -depth : only traverse the root directory up to directories deep (unlimited by default) 46 | -only-dirs: only show directories, not files 47 | -no-report: do not show footer after the tree 48 | ' -shell-script-candidates %{ 49 | printf '%s\n' -files-first -dirs-first -consider-gitignore -no-empty-dirs \ 50 | -show-hidden -depth -only-dirs -no-report './' */ 51 | } %{ 52 | eval -save-regs 't' %{ 53 | eval %sh{ 54 | sorting='' 55 | prune='' 56 | gitignore='' 57 | depth='' 58 | directory='' 59 | hidden='' 60 | only_dirs='' 61 | no_report='' 62 | 63 | arg_num=0 64 | accept_switch='y' 65 | while [ $# -ne 0 ]; do 66 | arg_num=$((arg_num + 1)) 67 | arg=$1 68 | shift 69 | if [ $accept_switch = 'y' ]; then 70 | got_switch='y' 71 | if [ "$arg" = '-files-first' ]; then 72 | sorting='--filesfirst' 73 | elif [ "$arg" = '-dirs-first' ]; then 74 | sorting='--dirsfirst' 75 | elif [ "$arg" = '-no-empty-dirs' ]; then 76 | prune='--prune' 77 | elif [ "$arg" = '-show-hidden' ]; then 78 | hidden='-a' 79 | elif [ "$arg" = '-consider-gitignore' ]; then 80 | gitignore='--gitignore' 81 | elif [ "$arg" = '-only-dirs' ]; then 82 | only_dirs="-P '*/'" 83 | elif [ "$arg" = '-no-report' ]; then 84 | no_report="--noreport" 85 | elif [ "$arg" = '-depth' ]; then 86 | if [ $# -eq 0 ]; then 87 | echo 'fail "Missing argument to -depth"' 88 | exit 1 89 | fi 90 | arg_num=$((arg_num + 1)) 91 | depth="-L $1" 92 | shift 93 | elif [ "$arg" = '--' ]; then 94 | accept_switch='n' 95 | else 96 | got_switch='n' 97 | fi 98 | [ $got_switch = 'y' ] && continue 99 | fi 100 | if [ "$directory" != '' ]; then 101 | printf "fail \"Unknown argument '%%arg{%s}'\"" "$arg_num" 102 | exit 1 103 | elif [ "$arg" != '' ]; then 104 | directory="$arg" 105 | else 106 | printf "fail \"Invalid directory '%%arg{%s}'\"" "$arg_num" 107 | exit 1 108 | fi 109 | done 110 | [ "$directory" = '' ] && directory='.' 111 | # strip trailing '/' 112 | while [ "$directory" != "${directory%/}" ]; do 113 | directory=${directory%/} 114 | done 115 | if [ ! -d "$directory" ]; then 116 | printf "fail \"Directory '%%arg{%s}' does not exist\"" "$arg_num" 117 | exit 1 118 | fi 119 | fifo=$(mktemp -u) 120 | mkfifo "$fifo" 121 | # $kak_opt_filetree_indentation_level <- need to let the script access this var 122 | perl_script="${kak_opt_filetree_script_path%/*}/filetree.perl" 123 | (tree -p -v $only_dirs $no_report $hidden $sorting $prune $gitignore $depth "$directory" | perl "$perl_script" 'process' > "$fifo") < /dev/null > /dev/null 2>&1 & 124 | printf "set-register t '%s'" "$fifo" 125 | } 126 | try %{ delete-buffer *filetree* } 127 | set-option global filetree_highlight_dirty false 128 | 129 | edit -fifo %reg{t} *filetree* 130 | set-option buffer filetree_root_directory %sh{ printf '%s' "$PWD" } 131 | # put hook in double quotes to interpolate %reg{t} 132 | hook -always -once buffer BufCloseFifo .* " 133 | nop %%sh{ rm '%reg{t}' } 134 | exec ged 135 | set-option global filetree_highlight_dirty true 136 | filetree-refresh-files-highlight 137 | " 138 | 139 | # highlight tree part 140 | add-highlighter buffer/ regex '^[│ ]*[├└]─* ' 0:FileTreePipesColor 141 | # highlight directories (using the /) 142 | add-highlighter buffer/ regex '^(?:[│ ]*[├└]─* )?([^\n]*?)/$' 1:FileTreeDirColor 143 | add-highlighter buffer/ regex '^(?:[│ ]*[├└]─* )([^\n/]*?)$' 1:FileTreeFileName 144 | add-highlighter buffer/ regex '^(?:[│ ]*[├└]─* )(\n)' 1:FileTreeEmptyName 145 | add-highlighter buffer/ ranges filetree_open_files 146 | 147 | map buffer normal ': filetree-open-selected-files' 148 | map buffer normal ': filetree-open-selected-files -create' 149 | map buffer normal ': filetree-select-prev-sibling' 150 | map buffer normal ': filetree-select-next-sibling' 151 | map buffer normal ': filetree-select-parent-directory' 152 | map buffer normal ': filetree-select-first-child' 153 | } 154 | } 155 | 156 | define-command filetree-select-open-files %{ 157 | eval select -timestamp %sh{ 158 | # TODO might not work with filenames containing ' 159 | eval set -- "$kak_quoted_opt_filetree_open_files" 160 | printf '''%s''' "$1" 161 | shift 162 | for val do 163 | printf ' ''%s''' "${val%|*}" 164 | done 165 | } 166 | } 167 | 168 | define-command filetree-create-sibling %{ 169 | # TODO handle root dir: forbid entirely? 170 | filetree-select-path-component 171 | try %{ 172 | exec -draft '/\z' 173 | exec 'xyP' 174 | exec -draft 'ghh/[├└]r├' 175 | } catch %{ 176 | exec 'xyp' 177 | exec -draft 'ghh/[├└]r├' 178 | } 179 | exec 'ghh/[├└]─* l' 180 | try %{ exec '\nGLd' } 181 | } 182 | 183 | define-command filetree-create-child %{ 184 | # TODO handle root dir 185 | filetree-select-path-component 186 | # fail if we're not on a directory 187 | try %{ 188 | exec -draft '/\z' 189 | } catch %{ 190 | fail 'Cannot create child of a regular file' 191 | } 192 | exec 'xyp' # copy line below 193 | exec 'ghh/[├└]─* ' # select end of pipe from the new line 194 | try %{ exec -draft 'l\nGLd' } # remove filename 195 | exec 'yP' # copy-prepend it 196 | exec 'r;k' # replace it with space, and select the first character 197 | try %{ 198 | exec 'jr│' # select the one just above: if it's ├, turn into │ 199 | } catch %{ 200 | exec 'r└j' # otherwise into └ 201 | } 202 | exec '/[├└]' # select the real pipe connector 203 | try %{ 204 | exec -draft 'j[├└]' # and depending on whether it's the last child 205 | exec 'r├' # replace with ├ 206 | } catch %{ 207 | exec 'r└' # or └ 208 | } 209 | exec 'gll' 210 | } 211 | 212 | define-command filetree-select-parent-directory -docstring ' 213 | filetree-select-next-sibling: in the *filetree* buffer, select the parent directory of the current element 214 | ' %{ 215 | eval -itersel %{ 216 | try %{ 217 | exec -draft ';ghH\n.' 218 | } catch %{ 219 | fail 'Already at top parent' 220 | } 221 | try %{ 222 | exec ';x1s(^[│ ]+)[└├]─* ' 223 | exec "^(?!%val{selection})([^\n]*?)/$" 224 | filetree-select-path-component 225 | } catch %{ 226 | exec ggxH 227 | } 228 | } 229 | } 230 | 231 | define-command filetree-select-next-sibling -docstring ' 232 | filetree-select-next-sibling: in the *filetree* buffer, select the next element in the same directory 233 | ' %{ 234 | eval -itersel -save-regs '/' %{ 235 | exec ';x' 236 | exec '1s^([ │]*)[└├]' 237 | try %{ 238 | exec '\A[└├]\z' 239 | reg slash "\n(([^\n]*\n)*?(^[└├]))?" 240 | } catch %{ 241 | reg slash "\n((^%val{selection}[^\n]*\n)*?(^%val{selection}[└├]))?" 242 | } 243 | exec 'gh/' 244 | exec '\A\n\z' # if we didn't match anything, fail 245 | exec ';' 246 | filetree-select-path-component 247 | } 248 | } 249 | define-command filetree-select-prev-sibling -docstring ' 250 | filetree-select-prev-sibling: in the *filetree* buffer, select the previous element in the same directory 251 | ' %{ 252 | eval -itersel -save-regs '/' %{ 253 | exec ';x' 254 | exec '1s^([ │]*)[└├]' 255 | try %{ 256 | exec '\A[└├]\z' 257 | reg slash "((^├[^\n]*\n)(^[^\n]*\n)*?)?^." 258 | } catch %{ 259 | reg slash "((^%val{selection}├[^\n]*\n)(^%val{selection}[^\n]*\n)*?)?^." 260 | } 261 | exec 'gl' 262 | exec '\A^.\z' # if we didn't match anything, fail 263 | exec ';' 264 | filetree-select-path-component 265 | } 266 | } 267 | 268 | define-command filetree-select-first-child -docstring ' 269 | filetree-select-first-child: in the *filetree* buffer, select the first element in the selected directory 270 | ' %{ 271 | eval -itersel %{ 272 | exec ';x' 273 | exec -draft '/$' 274 | try %{ 275 | exec -draft 'ghH\A.\z' 276 | exec 'j' 277 | } catch %{ 278 | exec 's^[ │]*[└├]─* ' 279 | exec "jx\A^[│ ]{%val{selection_length}}" 280 | } 281 | filetree-select-path-component 282 | } 283 | } 284 | 285 | define-command filetree-select-direct-children -docstring ' 286 | filetree-select-direct-children: in the *filetree* buffer, select all elements in the selected directory 287 | ' %{ 288 | eval -itersel -save-regs 'l' %{ 289 | exec ';x' 290 | exec -draft '/$' 291 | try %{ 292 | exec -draft 'ghH\A.\z' 293 | reg l '0' 294 | exec gh 295 | } catch %{ 296 | exec 's^[ │]*[└├]─* ' 297 | reg l %val{selection_length} 298 | } 299 | exec "gll/(^[│ ]{%reg{l}}[^\n]+\n)*|." 300 | exec '\A.\z' 301 | exec "^[│ ]{%reg{l}}[└├]" 302 | filetree-select-path-component 303 | } 304 | } 305 | 306 | define-command filetree-select-all-children -docstring ' 307 | filetree-select-all-children: in the *filetree* buffer, select all elements which are descendents of the selected directory 308 | ' %{ 309 | eval -itersel -save-regs 'l' %{ 310 | exec ';x' 311 | exec -draft '/$' 312 | try %{ 313 | exec -draft 'ghH\A.\z' 314 | reg l '0' 315 | exec gh 316 | } catch %{ 317 | exec 's^[ │]*[└├]─* ' 318 | reg l %val{selection_length} 319 | } 320 | exec "gll/(^[│ ]{%reg{l}}[^\n]*\n)*|." 321 | exec '\A.\z' 322 | exec '' 323 | filetree-select-path-component 324 | } 325 | } 326 | 327 | define-command -hidden filetree-eval-on-fullpath -params 1 %{ 328 | eval -save-regs 'p' %{ 329 | eval -draft %{ 330 | exec ',' 331 | reg p %val{selection} 332 | try %{ 333 | # TODO not exactly elegant 334 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 335 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 336 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 337 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 338 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 339 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 340 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 341 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 342 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 343 | filetree-select-parent-directory; reg p "%val{selection}%reg{p}" 344 | } 345 | } 346 | eval %arg{1} 347 | } 348 | } 349 | 350 | define-command -hidden filetree-open-selected-file -params ..1 %{ 351 | filetree-eval-on-fullpath %sh{ 352 | printf '%s' "try %{ 353 | buffer %reg{p} 354 | } catch %{ 355 | edit -existing %reg{p} 356 | reg e \"x%reg{e}\" 357 | } catch %{" 358 | if [ "$1" = '-create' ]; then 359 | printf '%s' " 360 | edit %reg{p} 361 | reg c \"x%reg{c}\" 362 | } catch %{" 363 | fi 364 | printf '%s' "reg f \"x%reg{f}\" 365 | }" 366 | } 367 | } 368 | 369 | define-command filetree-open-selected-files -params ..1 -docstring ' 370 | filetree-open-selected-files [-create]: open the files currently selected 371 | ' -shell-script-candidates %{ 372 | printf "%s\n" '-create' 373 | } %{ 374 | eval -save-regs 'cef' %{ 375 | reg e '' 376 | reg c '' 377 | reg f '' 378 | filetree-select-path-component 379 | exec '/\z' 380 | try %{ 381 | # open all non-main selections in a draft context 382 | eval -draft %{ 383 | exec '' 384 | eval -itersel %{ eval -draft -verbatim filetree-open-selected-file %arg{@} } 385 | } 386 | } 387 | filetree-open-selected-file %arg{@} 388 | eval %sh{ 389 | # echo some "helpful" info 390 | num_opened="${#kak_reg_e}" 391 | num_created="${#kak_reg_c}" 392 | num_failed="${#kak_reg_f}" 393 | total=$(( num_opened + num_created + num_failed )) 394 | [ "$total" -eq 0 ] && exit 395 | 396 | str_opened="${num_opened} existing file" 397 | [ "$num_opened" -ne 1 ] && str_opened="${str_opened}s" 398 | str_created="${num_created} new file" 399 | [ "$num_created" -ne 1 ] && str_created="${str_created}s" 400 | str_failed="${num_failed} file" 401 | [ "$num_failed" -ne 1 ] && str_failed="${str_failed}s" 402 | 403 | printf "echo '" 404 | if [ "$num_opened" -eq "$total" ]; then 405 | printf "Opened %s" "$str_opened" 406 | elif [ "$num_created" -eq "$total" ]; then 407 | printf "Opened %s" "$str_created" 408 | elif [ "$num_failed" -eq "$total" ]; then 409 | printf "Failed to open %s" "$str_failed" 410 | elif [ "$num_failed" -eq 0 ]; then # opened + created 411 | printf "Opened %s, and %s" "$str_opened" "$str_created" 412 | elif [ "$num_created" -eq 0 ]; then # opened + failed 413 | printf "Opened %s, and failed to open %s" "$str_opened" "$str_failed" 414 | elif [ "$num_opened" -eq 0 ]; then # created + failed 415 | printf "Opened %s, and failed to open %s" "$str_created" "$str_failed" 416 | else 417 | printf "Opened %s, %s, and failed to open %s" "$str_opened" "$str_created" "$str_failed" 418 | fi 419 | printf "'" 420 | } 421 | } 422 | } 423 | 424 | define-command filetree-select-path-component %{ 425 | exec ';x1s^[│ ]*[├└]─* (.*)\n' 426 | } 427 | 428 | hook global WinDisplay '^\*filetree\*$' %{ 429 | try %{ 430 | eval %sh{ [ "$kak_opt_filetree_highlight_dirty" = 'false' ] && printf 'fail' } 431 | filetree-refresh-files-highlight 432 | set global filetree_highlight_dirty false 433 | } 434 | } 435 | 436 | hook global BufCreate .* %{ set global filetree_highlight_dirty true } 437 | hook global BufClose .* %{ set global filetree_highlight_dirty true } 438 | 439 | define-command -hidden filetree-refresh-files-highlight %{ 440 | try %{ 441 | eval -draft -buffer *filetree* %{ 442 | eval select %sh{ 443 | script="${kak_opt_filetree_script_path%/*}/filetree.perl" 444 | echo "write '$kak_response_fifo'" > "$kak_command_fifo" 445 | eval set -- "$kak_quoted_buflist" 446 | perl "$script" 'match-buffers' "$@" < "$kak_response_fifo" 447 | } 448 | filetree-select-path-component 449 | 450 | set-option buffer filetree_open_files %val{timestamp} 451 | eval -no-hooks -draft -itersel %{ 452 | set -add buffer filetree_open_files "%val{selection_desc}|FileTreeOpenFiles" 453 | } 454 | } 455 | } 456 | } 457 | 458 | define-command filetree-edit -params 1.. -docstring ' 459 | filetree-edit: edit the specified files. 460 | The completions are provided by the *filetree* buffer. 461 | ' %{ 462 | edit %arg{@} 463 | } 464 | 465 | complete-command -menu filetree-edit shell-script-candidates %{ 466 | fifo=$(mktemp -u) 467 | mkfifo "$fifo" 468 | echo "try %{ eval -buffer *filetree* %{ write '$fifo' } } catch %{ echo -to-file '$fifo' '' }" | kak -p "$kak_session" 469 | perl "${kak_opt_filetree_script_path%/*}/filetree.perl" 'flatten-nodirs' < "$fifo" 470 | rm "$fifo" 471 | } 472 | 473 | define-command filetree-goto -params 1.. -docstring ' 474 | filetree-goto: select the specified path elements in the *filetree* buffer 475 | ' %{ 476 | buffer *filetree* 477 | eval select %sh{ 478 | script="${kak_opt_filetree_script_path%/*}/filetree.perl" 479 | echo "write '$kak_response_fifo'" > "$kak_command_fifo" 480 | perl "$script" 'match-buffers' "$@" < "$kak_response_fifo" 481 | } 482 | filetree-select-path-component 483 | } 484 | 485 | complete-command -menu filetree-goto shell-script-candidates %{ 486 | fifo=$(mktemp -u) 487 | mkfifo "$fifo" 488 | echo "try %{ eval -buffer *filetree* %{ write '$fifo' } } catch %{ echo -to-file '$fifo' '' }" | kak -p "$kak_session" 489 | perl "${kak_opt_filetree_script_path%/*}/filetree.perl" 'flatten-all' < "$fifo" 490 | rm "$fifo" 491 | } 492 | 493 | } 494 | 495 | require-module filetree 496 | -------------------------------------------------------------------------------- /filetree.perl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | my $operation = $ARGV[0]; 5 | if (not defined($operation)) { 6 | exit(3); 7 | } 8 | shift; 9 | 10 | sub read_line_by_line { 11 | my $callback = $_[0]; 12 | 13 | my $padding_size = 0; 14 | my @dir_stack; 15 | my $line_count = 0; 16 | 17 | my $prev_depth = 0; 18 | while (my $input = ) { 19 | chomp($input); 20 | if ($input eq "") { 21 | last; 22 | } elsif ($line_count == 0) { 23 | if ($input ne './') { 24 | push(@dir_stack, substr($input, 0, -1)); 25 | } 26 | $line_count = 1; 27 | next; 28 | } 29 | 30 | my $depth = 1; 31 | if ($line_count == 1) { 32 | # need to infer the width of the pipes, based on the first line 33 | if ($input !~ m/\G(└|├)((─)* )/gc) { 34 | exit(1); 35 | } 36 | # the pipe character is actually length '3' 37 | $padding_size = (length($2) - 1) / 3; 38 | } else { 39 | while ($input =~ m/\G(│| ) {$padding_size} /gco) { 40 | $depth += 1; 41 | } 42 | if ($input !~ m/\G(└|├)(─)* /gc) { 43 | exit(1); 44 | } 45 | } 46 | $line_count += 1; 47 | 48 | if ($depth <= $prev_depth) { 49 | my $remove = $prev_depth - $depth + 1; 50 | splice(@dir_stack, -$remove); 51 | } elsif ($depth > $prev_depth + 1) { 52 | # does not make sense to grow by >1 level 53 | exit(1); 54 | } 55 | $input =~ m|\G(.*?)(/?)$|gc; 56 | my $component = $1; 57 | my $is_dir = ($2 eq '/'); 58 | $prev_depth = $depth; 59 | push(@dir_stack, $component); 60 | $callback->(join('/', @dir_stack), $is_dir, $line_count); 61 | } 62 | } 63 | 64 | if ($operation eq "flatten-all") { 65 | sub callback1 { 66 | print("$_[0]\n"); 67 | } 68 | read_line_by_line(\&callback1); 69 | } elsif ($operation eq "flatten-nodirs") { 70 | sub callback2 { 71 | if (not $_[1]) { 72 | print("$_[0]\n"); 73 | } 74 | } 75 | read_line_by_line(\&callback2); 76 | } elsif ($operation eq "match-buffers") { 77 | my %map; 78 | for my $buf (@ARGV) { 79 | $map{$buf} = 1; 80 | } 81 | sub callback3 { 82 | my $path = $_[0]; 83 | if (exists($map{$path})) { 84 | my $line = $_[2]; 85 | print("'$line.1,$line.1' "); 86 | delete $map{$path}; 87 | } 88 | } 89 | read_line_by_line(\&callback3); 90 | } elsif ($operation eq "process") { 91 | 92 | my $repetition = int($ENV{"kak_opt_filetree_indentation_level"} or 3); 93 | my $first = 1; 94 | 95 | while (my $input = ) { 96 | chomp($input); 97 | if ($input eq "") { 98 | print("\n"); 99 | last; 100 | } 101 | my $out = ""; 102 | if ($first == 1) { 103 | $first = 0; 104 | } else { 105 | while ($input =~ m/\G(?:(│)\xc2\xa0\xc2\xa0|( ) ) /gc) { 106 | $out .= ($1 or $2) . ' ' x ($repetition + 1); 107 | } 108 | if ($input !~ m/\G(└|├)(─)─ /gc) { 109 | exit(1); 110 | } 111 | $out .= $1 . $2 x $repetition . ' '; 112 | } 113 | my $type = ''; 114 | if ($input =~ m/\G\[([-sdl]).{9}\] /gc) { 115 | $type = $1; 116 | } 117 | if ($type eq 'l') { 118 | $input =~ m/\G(.*) -> .*?$/gc; 119 | $out .= $1; 120 | } else { 121 | $input =~ m/\G(.*)$/gc; 122 | $out .= $1; 123 | } 124 | if ($type eq 'd') { 125 | $out .= '/'; 126 | } 127 | print("$out\n"); 128 | } 129 | 130 | my $last = ; 131 | print("$last"); 132 | 133 | } else { 134 | exit(2); 135 | } 136 | --------------------------------------------------------------------------------