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
118121def 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
161169def _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
230335def _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
271387def _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
314439def 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