@@ -169,6 +169,101 @@ class WorkspaceBuilder:
169169 >>> sorted([window.name for window in session.windows])
170170 ['editor', 'logging', 'test']
171171
172+ **Progress callback:**
173+
174+ >>> calls: list[str] = []
175+ >>> progress_cfg = {
176+ ... "session_name": "progress-demo",
177+ ... "windows": [{"window_name": "main", "panes": [{"shell_command": []}]}],
178+ ... }
179+ >>> builder = WorkspaceBuilder(
180+ ... session_config=progress_cfg,
181+ ... server=server,
182+ ... on_progress=calls.append,
183+ ... )
184+ >>> builder.build()
185+ >>> "Workspace built" in calls
186+ True
187+
188+ **Before-script hook:**
189+
190+ >>> hook_calls: list[bool] = []
191+ >>> no_script_cfg = {
192+ ... "session_name": "hook-demo",
193+ ... "windows": [{"window_name": "main", "panes": [{"shell_command": []}]}],
194+ ... }
195+ >>> builder = WorkspaceBuilder(
196+ ... session_config=no_script_cfg,
197+ ... server=server,
198+ ... on_before_script=lambda: hook_calls.append(True),
199+ ... )
200+ >>> builder.build()
201+ >>> hook_calls # no before_script in config, callback not fired
202+ []
203+
204+ **Script output hook:**
205+
206+ >>> script_lines: list[str] = []
207+ >>> no_script_cfg2 = {
208+ ... "session_name": "script-output-demo",
209+ ... "windows": [{"window_name": "main", "panes": [{"shell_command": []}]}],
210+ ... }
211+ >>> builder = WorkspaceBuilder(
212+ ... session_config=no_script_cfg2,
213+ ... server=server,
214+ ... on_script_output=script_lines.append,
215+ ... )
216+ >>> builder.build()
217+ >>> script_lines # no before_script in config, callback not fired
218+ []
219+
220+ **Build events hook:**
221+
222+ >>> events: list[dict] = []
223+ >>> event_cfg = {
224+ ... "session_name": "events-demo",
225+ ... "windows": [{"window_name": "main", "panes": [{"shell_command": []}]}],
226+ ... }
227+ >>> builder = WorkspaceBuilder(
228+ ... session_config=event_cfg,
229+ ... server=server,
230+ ... on_build_event=events.append,
231+ ... )
232+ >>> builder.build()
233+ >>> [e["event"] for e in events]
234+ ['session_created', 'window_started', 'pane_creating',
235+ 'window_done', 'workspace_built']
236+ >>> next(e for e in events if e["event"] == "session_created")["session_pane_total"]
237+ 1
238+
239+ **Build events with before_script:**
240+
241+ ``before_script_started`` fires before the script runs;
242+ ``before_script_done`` fires in ``finally`` (success or failure).
243+
244+ >>> script_events: list[dict] = []
245+ >>> script_event_cfg = {
246+ ... "session_name": "script-events-demo",
247+ ... "before_script": "echo hello",
248+ ... "windows": [{"window_name": "main", "panes": [{"shell_command": []}]}],
249+ ... }
250+ >>> builder = WorkspaceBuilder(
251+ ... session_config=script_event_cfg,
252+ ... server=server,
253+ ... on_build_event=script_events.append,
254+ ... )
255+ >>> builder.build()
256+ >>> event_names = [e["event"] for e in script_events]
257+ >>> "before_script_started" in event_names
258+ True
259+ >>> "before_script_done" in event_names
260+ True
261+ >>> bs_start = event_names.index("before_script_started")
262+ >>> bs_done = event_names.index("before_script_done")
263+ >>> win_start = event_names.index("window_started")
264+ >>> bs_start < bs_done < win_start
265+ True
266+
172267 The normal phase of loading is:
173268
174269 1. Load JSON / YAML file via :class:`pathlib.Path`::
@@ -211,12 +306,20 @@ class WorkspaceBuilder:
211306 server : Server
212307 _session : Session | None
213308 session_name : str
309+ on_progress : t .Callable [[str ], None ] | None
310+ on_before_script : t .Callable [[], None ] | None
311+ on_script_output : t .Callable [[str ], None ] | None
312+ on_build_event : t .Callable [[dict [str , t .Any ]], None ] | None
214313
215314 def __init__ (
216315 self ,
217316 session_config : dict [str , t .Any ],
218317 server : Server ,
219318 plugins : list [t .Any ] | None = None ,
319+ on_progress : t .Callable [[str ], None ] | None = None ,
320+ on_before_script : t .Callable [[], None ] | None = None ,
321+ on_script_output : t .Callable [[str ], None ] | None = None ,
322+ on_build_event : t .Callable [[dict [str , t .Any ]], None ] | None = None ,
220323 ) -> None :
221324 """Initialize workspace loading.
222325
@@ -231,6 +334,23 @@ def __init__(
231334 server : :class:`libtmux.Server`
232335 tmux server to build session in
233336
337+ on_progress : callable, optional
338+ callback for progress updates during building
339+
340+ on_before_script : callable, optional
341+ called just before ``before_script`` runs; use to clear the terminal
342+ (e.g. stop a spinner) so script output is not interleaved
343+
344+ on_script_output : callable, optional
345+ called with each output line from ``before_script`` subprocess; when
346+ set, raw TTY tee is suppressed so the caller can route lines to a
347+ live panel instead
348+
349+ on_build_event : callable, optional
350+ called with a dict event at each structural build milestone (session
351+ created, window started/done, pane creating, workspace built); used
352+ by the CLI to render a live session tree
353+
234354 Notes
235355 -----
236356 TODO: Initialize :class:`libtmux.Session` from here, in
@@ -248,6 +368,10 @@ def __init__(
248368
249369 self .session_config = session_config
250370 self .plugins = plugins
371+ self .on_progress = on_progress
372+ self .on_before_script = on_before_script
373+ self .on_script_output = on_script_output
374+ self .on_build_event = on_build_event
251375
252376 if self .server is not None and self .session_exists (
253377 session_name = self .session_config ["session_name" ],
@@ -332,7 +456,21 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
332456 assert session is not None
333457 assert session .name is not None
334458
459+ if self .on_progress :
460+ self .on_progress (f"Session created: { session .name } " )
461+
335462 self ._session = session
463+ if self .on_build_event :
464+ self .on_build_event (
465+ {
466+ "event" : "session_created" ,
467+ "name" : session .name ,
468+ "window_total" : len (self .session_config ["windows" ]),
469+ "session_pane_total" : sum (
470+ len (w .get ("panes" , [])) for w in self .session_config ["windows" ]
471+ ),
472+ }
473+ )
336474 _log = TmuxpLoggerAdapter (
337475 logger ,
338476 {"tmux_session" : self .session_config ["session_name" ]},
@@ -354,6 +492,10 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
354492 focus = None
355493
356494 if "before_script" in self .session_config :
495+ if self .on_before_script :
496+ self .on_before_script ()
497+ if self .on_build_event :
498+ self .on_build_event ({"event" : "before_script_started" })
357499 try :
358500 cwd = None
359501
@@ -364,7 +506,11 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
364506 _log .debug (
365507 "running before script" ,
366508 )
367- run_before_script (self .session_config ["before_script" ], cwd = cwd )
509+ run_before_script (
510+ self .session_config ["before_script" ],
511+ cwd = cwd ,
512+ on_line = self .on_script_output ,
513+ )
368514 except Exception :
369515 _log .error (
370516 "before script failed" ,
@@ -376,6 +522,9 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
376522 )
377523 self .session .kill ()
378524 raise
525+ finally :
526+ if self .on_build_event :
527+ self .on_build_event ({"event" : "before_script_done" })
379528
380529 if "options" in self .session_config :
381530 for option , value in self .session_config ["options" ].items ():
@@ -414,10 +563,17 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
414563 if focus_pane :
415564 focus_pane .select ()
416565
566+ if self .on_build_event :
567+ self .on_build_event ({"event" : "window_done" })
568+
417569 if focus :
418570 focus .select ()
419571
572+ if self .on_progress :
573+ self .on_progress ("Workspace built" )
420574 _log .info ("workspace built" )
575+ if self .on_build_event :
576+ self .on_build_event ({"event" : "workspace_built" })
421577
422578 def iter_create_windows (
423579 self ,
@@ -450,6 +606,17 @@ def iter_create_windows(
450606 ):
451607 window_name = window_config .get ("window_name" , None )
452608
609+ if self .on_progress :
610+ self .on_progress (f"Creating window: { window_name or window_iterator } " )
611+ if self .on_build_event :
612+ self .on_build_event (
613+ {
614+ "event" : "window_started" ,
615+ "name" : window_name or str (window_iterator ),
616+ "pane_total" : len (window_config ["panes" ]),
617+ }
618+ )
619+
453620 is_first_window_pass = self .first_window_pass (
454621 window_iterator ,
455622 session ,
@@ -545,6 +712,17 @@ def iter_create_panes(
545712 window_config ["panes" ],
546713 start = pane_base_index ,
547714 ):
715+ if self .on_progress :
716+ self .on_progress (f"Creating pane: { pane_index } " )
717+ if self .on_build_event :
718+ self .on_build_event (
719+ {
720+ "event" : "pane_creating" ,
721+ "pane_num" : pane_index - int (pane_base_index ) + 1 ,
722+ "pane_total" : len (window_config ["panes" ]),
723+ }
724+ )
725+
548726 if pane_index == int (pane_base_index ):
549727 pane = window .active_pane
550728 else :
0 commit comments