Skip to content

Commit 67a4af2

Browse files
committed
feat(cli[ls]): Add --full flag for complete config output
Add --full flag to tmuxp ls command for detailed workspace inspection. Changes: - Add --full argument to subparser - Update _get_workspace_info() with include_config parameter - Add _render_config_tree() helper for human-readable window/pane hierarchy - Update _output_flat() and _output_tree() to display config tree when full=True - JSON/NDJSON output with --full includes full parsed config object - Human output with --full shows tree: windows with layout, panes with commands Example human output with --full: dev ├── editor [main-horizontal] │ ├── pane 0: vim │ └── pane 1: git status └── shell └── pane 0 Example JSON with --full: {"name": "dev", ..., "config": {"session_name": "dev", "windows": [...]}} Tests added: - test_ls_full_flag_subparser - test_get_workspace_info_include_config - test_get_workspace_info_no_config_by_default - test_ls_json_full_includes_config - test_ls_full_tree_shows_windows - test_ls_full_flat_shows_windows - test_ls_full_without_json_no_config_in_output
1 parent 57faf91 commit 67a4af2

File tree

2 files changed

+330
-32
lines changed

2 files changed

+330
-32
lines changed

src/tmuxp/cli/ls.py

Lines changed: 158 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@
4949
[
5050
"tmuxp ls",
5151
"tmuxp ls --tree",
52+
"tmuxp ls --full",
5253
],
5354
),
5455
(
5556
"Machine-readable output:",
5657
[
5758
"tmuxp ls --json",
59+
"tmuxp ls --json --full",
5860
"tmuxp ls --ndjson",
5961
"tmuxp ls --json | jq '.[] | .name'",
6062
],
@@ -113,6 +115,7 @@ class CLILsNamespace(argparse.Namespace):
113115
tree: bool
114116
output_json: bool
115117
output_ndjson: bool
118+
full: bool
116119

117120

118121
def create_ls_subparser(
@@ -155,14 +158,20 @@ def create_ls_subparser(
155158
dest="output_ndjson",
156159
help="output as NDJSON (one JSON per line)",
157160
)
161+
parser.add_argument(
162+
"--full",
163+
action="store_true",
164+
help="include full config content in output",
165+
)
158166
return parser
159167

160168

161169
def _get_workspace_info(
162170
filepath: pathlib.Path,
163171
*,
164172
source: str = "global",
165-
) -> WorkspaceInfo:
173+
include_config: bool = False,
174+
) -> dict[str, t.Any]:
166175
"""Extract metadata from a workspace file.
167176
168177
Parameters
@@ -171,11 +180,13 @@ def _get_workspace_info(
171180
Path to the workspace file.
172181
source : str
173182
Source location: "local" or "global". Default "global".
183+
include_config : bool
184+
If True, include full parsed config content. Default False.
174185
175186
Returns
176187
-------
177-
WorkspaceInfo
178-
Workspace metadata dictionary.
188+
dict[str, Any]
189+
Workspace metadata dictionary. Includes 'config' key when include_config=True.
179190
180191
Examples
181192
--------
@@ -197,95 +208,204 @@ def _get_workspace_info(
197208
>>> info_local = _get_workspace_info(temp_path, source="local")
198209
>>> info_local['source']
199210
'local'
211+
>>> info_full = _get_workspace_info(temp_path, include_config=True)
212+
>>> 'config' in info_full
213+
True
214+
>>> info_full['config']['session_name']
215+
'test-session'
200216
>>> temp_path.unlink()
201217
"""
202218
stat = filepath.stat()
203219
ext = filepath.suffix.lower()
204220
file_format = "json" if ext == ".json" else "yaml"
205221

206-
# Try to extract session_name from config
222+
# Try to extract session_name and optionally full config
207223
session_name: str | None = None
224+
config_content: dict[str, t.Any] | None = None
208225
try:
209226
config = ConfigReader.from_file(filepath)
210227
if isinstance(config.content, dict):
211228
session_name = config.content.get("session_name")
229+
if include_config:
230+
config_content = config.content
212231
except Exception:
213232
# If we can't parse it, just skip session_name
214233
pass
215234

216-
return WorkspaceInfo(
217-
name=filepath.stem,
218-
path=str(PrivatePath(filepath)),
219-
format=file_format,
220-
size=stat.st_size,
221-
mtime=datetime.datetime.fromtimestamp(
235+
result: dict[str, t.Any] = {
236+
"name": filepath.stem,
237+
"path": str(PrivatePath(filepath)),
238+
"format": file_format,
239+
"size": stat.st_size,
240+
"mtime": datetime.datetime.fromtimestamp(
222241
stat.st_mtime,
223242
tz=datetime.timezone.utc,
224243
).isoformat(),
225-
session_name=session_name,
226-
source=source,
227-
)
244+
"session_name": session_name,
245+
"source": source,
246+
}
247+
248+
if include_config:
249+
result["config"] = config_content
250+
251+
return result
252+
253+
254+
def _render_config_tree(config: dict[str, t.Any], colors: Colors) -> list[str]:
255+
"""Render config windows/panes as tree lines for human output.
256+
257+
Parameters
258+
----------
259+
config : dict[str, Any]
260+
Parsed config content.
261+
colors : Colors
262+
Color manager.
263+
264+
Returns
265+
-------
266+
list[str]
267+
Lines of formatted tree output.
268+
269+
Examples
270+
--------
271+
>>> from tmuxp.cli._colors import ColorMode, Colors
272+
>>> colors = Colors(ColorMode.NEVER)
273+
>>> config = {
274+
... "session_name": "dev",
275+
... "windows": [
276+
... {"window_name": "editor", "layout": "main-horizontal"},
277+
... {"window_name": "shell"},
278+
... ],
279+
... }
280+
>>> lines = _render_config_tree(config, colors)
281+
>>> "editor" in lines[0]
282+
True
283+
>>> "shell" in lines[1]
284+
True
285+
"""
286+
lines: list[str] = []
287+
windows = config.get("windows", [])
288+
289+
for i, window in enumerate(windows):
290+
if not isinstance(window, dict):
291+
continue
292+
293+
is_last_window = i == len(windows) - 1
294+
prefix = "└── " if is_last_window else "├── "
295+
child_prefix = " " if is_last_window else "│ "
296+
297+
# Window line
298+
window_name = window.get("window_name", f"window {i}")
299+
layout = window.get("layout", "")
300+
layout_info = f" [{layout}]" if layout else ""
301+
lines.append(f"{prefix}{colors.info(window_name)}{colors.muted(layout_info)}")
302+
303+
# Panes
304+
panes = window.get("panes", [])
305+
for j, pane in enumerate(panes):
306+
is_last_pane = j == len(panes) - 1
307+
pane_prefix = "└── " if is_last_pane else "├── "
308+
309+
# Get pane command summary
310+
if isinstance(pane, dict):
311+
cmds = pane.get("shell_command", [])
312+
if isinstance(cmds, str):
313+
cmd_str = cmds
314+
elif isinstance(cmds, list) and cmds:
315+
cmd_str = str(cmds[0])
316+
else:
317+
cmd_str = ""
318+
elif isinstance(pane, str):
319+
cmd_str = pane
320+
else:
321+
cmd_str = ""
322+
323+
# Truncate long commands
324+
if len(cmd_str) > 40:
325+
cmd_str = cmd_str[:37] + "..."
326+
327+
pane_info = f": {cmd_str}" if cmd_str else ""
328+
lines.append(
329+
f"{child_prefix}{pane_prefix}{colors.muted(f'pane {j}')}{pane_info}"
330+
)
331+
332+
return lines
228333

229334

230335
def _output_flat(
231-
workspaces: list[WorkspaceInfo],
336+
workspaces: list[dict[str, t.Any]],
232337
formatter: OutputFormatter,
233338
colors: Colors,
339+
*,
340+
full: bool = False,
234341
) -> None:
235342
"""Output workspaces in flat list format.
236343
237344
Groups workspaces by source (local vs global) for human output.
238345
239346
Parameters
240347
----------
241-
workspaces : list[WorkspaceInfo]
348+
workspaces : list[dict[str, Any]]
242349
Workspaces to display.
243350
formatter : OutputFormatter
244351
Output formatter.
245352
colors : Colors
246353
Color manager.
354+
full : bool
355+
If True, show full config details in tree format. Default False.
247356
"""
248357
# Separate by source for human output grouping
249358
local_workspaces = [ws for ws in workspaces if ws["source"] == "local"]
250359
global_workspaces = [ws for ws in workspaces if ws["source"] == "global"]
251360

361+
def output_workspace(ws: dict[str, t.Any], show_path: bool) -> None:
362+
"""Output a single workspace."""
363+
formatter.emit(ws)
364+
path_info = f" {colors.muted(ws['path'])}" if show_path else ""
365+
formatter.emit_text(f" {colors.info(ws['name'])}{path_info}")
366+
367+
# With --full, show config tree
368+
if full and ws.get("config"):
369+
for line in _render_config_tree(ws["config"], colors):
370+
formatter.emit_text(f" {line}")
371+
252372
# Output local workspaces first (closest to user's context)
253373
if local_workspaces:
254374
formatter.emit_text(colors.muted("Local workspaces:"))
255375
for ws in local_workspaces:
256-
formatter.emit(dict(ws))
257-
formatter.emit_text(
258-
f" {colors.info(ws['name'])} {colors.muted(ws['path'])}"
259-
)
376+
output_workspace(ws, show_path=True)
260377

261378
# Output global workspaces
262379
if global_workspaces:
263380
if local_workspaces:
264381
formatter.emit_text("") # Blank line separator
265382
formatter.emit_text(colors.muted("Global workspaces:"))
266383
for ws in global_workspaces:
267-
formatter.emit(dict(ws))
268-
formatter.emit_text(f" {colors.info(ws['name'])}")
384+
output_workspace(ws, show_path=False)
269385

270386

271387
def _output_tree(
272-
workspaces: list[WorkspaceInfo],
388+
workspaces: list[dict[str, t.Any]],
273389
formatter: OutputFormatter,
274390
colors: Colors,
391+
*,
392+
full: bool = False,
275393
) -> None:
276394
"""Output workspaces grouped by directory (tree view).
277395
278396
Parameters
279397
----------
280-
workspaces : list[WorkspaceInfo]
398+
workspaces : list[dict[str, Any]]
281399
Workspaces to display.
282400
formatter : OutputFormatter
283401
Output formatter.
284402
colors : Colors
285403
Color manager.
404+
full : bool
405+
If True, show full config details in tree format. Default False.
286406
"""
287407
# Group by parent directory
288-
by_directory: dict[str, list[WorkspaceInfo]] = {}
408+
by_directory: dict[str, list[dict[str, t.Any]]] = {}
289409
for ws in workspaces:
290410
# Extract parent directory from path
291411
parent = str(pathlib.Path(ws["path"]).parent)
@@ -300,16 +420,21 @@ def _output_tree(
300420

301421
for ws in dir_workspaces:
302422
# JSON/NDJSON output
303-
formatter.emit(dict(ws))
423+
formatter.emit(ws)
304424

305425
# Human output: indented workspace name
306426
ws_name = ws["name"]
307-
ws_session = ws["session_name"]
427+
ws_session = ws.get("session_name")
308428
session_info = ""
309429
if ws_session and ws_session != ws_name:
310430
session_info = f" {colors.muted(f'→ {ws_session}')}"
311431
formatter.emit_text(f" {colors.info(ws_name)}{session_info}")
312432

433+
# With --full, show config tree
434+
if full and ws.get("config"):
435+
for line in _render_config_tree(ws["config"], colors):
436+
formatter.emit_text(f" {line}")
437+
313438

314439
def command_ls(
315440
args: CLILsNamespace | None = None,
@@ -335,24 +460,25 @@ def command_ls(
335460
color_mode = get_color_mode(args.color if args else None)
336461
colors = Colors(color_mode)
337462

338-
# Determine output mode
463+
# Determine output mode and options
339464
output_json = args.output_json if args else False
340465
output_ndjson = args.output_ndjson if args else False
341466
tree = args.tree if args else False
467+
full = args.full if args else False
342468
output_mode = get_output_mode(output_json, output_ndjson)
343469
formatter = OutputFormatter(output_mode)
344470

345471
# 1. Collect local workspace files (cwd and parents)
346472
local_files = find_local_workspace_files()
347-
workspaces: list[WorkspaceInfo] = [
348-
_get_workspace_info(f, source="local") for f in local_files
473+
workspaces: list[dict[str, t.Any]] = [
474+
_get_workspace_info(f, source="local", include_config=full) for f in local_files
349475
]
350476

351477
# 2. Collect global workspace files (~/.tmuxp/)
352478
tmuxp_dir = pathlib.Path(get_workspace_dir())
353479
if tmuxp_dir.exists() and tmuxp_dir.is_dir():
354480
workspaces.extend(
355-
_get_workspace_info(f, source="global")
481+
_get_workspace_info(f, source="global", include_config=full)
356482
for f in sorted(tmuxp_dir.iterdir())
357483
if not f.is_dir()
358484
and f.suffix.lower() in VALID_WORKSPACE_DIR_FILE_EXTENSIONS
@@ -365,8 +491,8 @@ def command_ls(
365491

366492
# Output based on mode
367493
if tree:
368-
_output_tree(workspaces, formatter, colors)
494+
_output_tree(workspaces, formatter, colors, full=full)
369495
else:
370-
_output_flat(workspaces, formatter, colors)
496+
_output_flat(workspaces, formatter, colors, full=full)
371497

372498
formatter.finalize()

0 commit comments

Comments
 (0)