Module: Pikuri::Workspace::Listing

Defined in:
lib/pikuri/workspace/listing.rb

Overview

Renders a directory as rows a model can orient from — name-sorted, directories first, sizes ls -h style:

Listing.render(project_root, filesystem: fs, depth: 2)   # =>
lib/
pikuri/  [3 dirs, 1 file]
version.rb  48B
spec/  [empty]
README.md  4.2K
broken.md  [Errno::ENOENT]

A directory shows [3 dirs, 1 file] exactly where its contents aren't on screen — the deepest rendered level — so lib/ above, expanded, never restates children that are already rows. Counts are immediate children, not recursive.

A row that can't be measured prints its errno class rather than a prose word or a fake zero: ENOENT is a dangling symlink or an entry that vanished mid-listing, EACCES a directory you can't open, ELOOP a symlink cycle. Each implies a different follow-up, and a model reads EACCES unaided. Only the root failing raises.

A credential path is [denied] and is never descended into or +stat+ed (+.ssh/ [denied]+). Shown rather than omitted because this listing answers "what is here", where a silent omission is a lie the model would burn turns re-probing — and ~/.ssh existing is not the secret; its filenames and bytes are. That is why Filesystem#denied? is a required constructor-level input here: a default of "nothing is denied" would let a new caller leak by forgetting.

Implementation details

Walks with Dir.children, not the rg --files prefix derivation Search::Glob uses, so symlinked / gitignored-only / empty directories can't silently vanish from the answer. The price is a caller-visible one: nothing evaluates .gitignore here, so everything shows except .git/ls behaviour, and the one place the file tools diverge on gitignore. Full argument in DECISIONS.md D_directory_listing.

Constant Summary collapse

MAX_BYTES =

Returns hard byte cap on rendered output. Same value as Search::Glob::MAX_BYTES; re-declared rather than cross-referenced because Zeitwerk's eager-load order between siblings isn't guaranteed.

Returns:

  • (Integer)

    hard byte cap on rendered output. Same value as Search::Glob::MAX_BYTES; re-declared rather than cross-referenced because Zeitwerk's eager-load order between siblings isn't guaranteed.

50 * 1024
MAX_BYTES_LABEL =

Returns human-readable MAX_BYTES for truncation messages.

Returns:

  • (String)

    human-readable MAX_BYTES for truncation messages.

"#{MAX_BYTES / 1024} KB"
DEFAULT_DEPTH =

Returns default depth — immediate children only.

Returns:

  • (Integer)

    default depth — immediate children only.

1
MAX_DEPTH =

Returns deepest depth honoured; larger values clamp down. Past three levels a listing stops being orientation and becomes the whole-tree dump MAX_BYTES would eat anyway.

Returns:

  • (Integer)

    deepest depth honoured; larger values clamp down. Past three levels a listing stops being orientation and becomes the whole-tree dump MAX_BYTES would eat anyway.

3
INDENT =

Returns per-level indent for nested rows.

Returns:

  • (String)

    per-level indent for nested rows.

'  '
GAP =

Returns separator between an entry's name and its size / summary column.

Returns:

  • (String)

    separator between an entry's name and its size / summary column.

'  '
GIT_DIR =

Returns the one entry never listed, matching Search::Glob's --glob '!.git/*'.

Returns:

  • (String)

    the one entry never listed, matching Search::Glob's --glob '!.git/*'.

'.git'
SIZE_UNITS =

Returns size suffixes, ls -h style.

Returns:

  • (Array<String>)

    size suffixes, ls -h style.

%w[B K M G T].freeze
DENIED =

Returns stands in for the size / summary column on a path under Filesystem#denied?.

Returns:

  • (String)

    stands in for the size / summary column on a path under Filesystem#denied?.

'[denied]'

Class Method Summary collapse

Class Method Details

.render(dir, filesystem:, depth: DEFAULT_DEPTH) ⇒ String

Render +dir+'s entries.

Parameters:

  • dir (String, Pathname)

    absolute directory path

  • filesystem (Filesystem)

    consulted per row via Filesystem#denied?; required, never defaulted (see the class doc)

  • depth (Integer) (defaults to: DEFAULT_DEPTH)

    levels to descend, clamped to 1..{MAX_DEPTH}

Returns:

  • (String)

    newline-joined rows, head-truncated at MAX_BYTES; "" when dir has no entries

Raises:

  • (SystemCallError)

    if dir itself can't be opened



87
88
89
90
91
92
93
94
95
96
# File 'lib/pikuri/workspace/listing.rb', line 87

def self.render(dir, filesystem:, depth: DEFAULT_DEPTH)
  rows = child_rows(dir.to_s, filesystem: filesystem,
                    depth: depth.clamp(1, MAX_DEPTH), indent: '')
  return '' if rows.empty?

  content, marker = Search::Utils.head_truncate(
    rows.join("\n"), max_bytes: MAX_BYTES, hint: 'list a subdirectory instead'
  )
  content + marker
end