11use anyhow:: { Context , Error , Result , bail} ;
22use crossterm:: { QueueableCommand , cursor, terminal} ;
3+ use serde:: Deserialize ;
34use std:: {
45 collections:: HashSet ,
56 env,
@@ -11,7 +12,7 @@ use std::{
1112 atomic:: { AtomicUsize , Ordering :: Relaxed } ,
1213 mpsc,
1314 } ,
14- thread,
15+ thread:: { self , JoinHandle } ,
1516} ;
1617
1718use crate :: {
@@ -49,6 +50,44 @@ pub enum CheckProgress {
4950 Pending ,
5051}
5152
53+ #[ derive( Deserialize ) ]
54+ struct Pane {
55+ id : u32 ,
56+ }
57+
58+ #[ must_use]
59+ pub struct EditCmdJoinHandle ( Option < JoinHandle < Result < ( String , u32 ) > > > ) ;
60+
61+ fn parse_pane_id ( b : & [ u8 ] ) -> Option < ( String , u32 ) > {
62+ // Remove newline
63+ let b = b. get ( "terminal_" . len ( ) ..b. len ( ) . saturating_sub ( 1 ) ) ?;
64+ let id_str = str:: from_utf8 ( b) . ok ( ) ?;
65+
66+ let ( first, rest) = b. split_first ( ) ?;
67+ let mut id = u32:: from ( first - b'0' ) ;
68+
69+ for c in rest {
70+ id = 10 * id + u32:: from ( c - b'0' ) ;
71+ }
72+
73+ Some ( ( id_str. to_owned ( ) , id) )
74+ }
75+
76+ fn close_pane ( pane_id : & str ) -> Result < ( ) > {
77+ Command :: new ( "zellij" )
78+ . arg ( "action" )
79+ . arg ( "close-pane" )
80+ . arg ( "-p" )
81+ . arg ( pane_id)
82+ . stdin ( Stdio :: null ( ) )
83+ . stdout ( Stdio :: null ( ) )
84+ . stderr ( Stdio :: null ( ) )
85+ . status ( )
86+ . context ( "Failed to run `zellij action close-pane -p ID`" ) ?;
87+
88+ Ok ( ( ) )
89+ }
90+
5291pub struct AppState {
5392 current_exercise_ind : usize ,
5493 exercises : Vec < Exercise > ,
@@ -61,12 +100,15 @@ pub struct AppState {
61100 official_exercises : bool ,
62101 cmd_runner : CmdRunner ,
63102 emit_file_links : bool ,
103+ zellij : bool ,
104+ open_pane : Option < ( String , u32 , usize ) > ,
64105}
65106
66107impl AppState {
67108 pub fn new (
68109 exercise_infos : Vec < ExerciseInfo > ,
69110 final_message : & ' static str ,
111+ zellij : bool ,
70112 ) -> Result < ( Self , StateFileStatus ) > {
71113 let cmd_runner = CmdRunner :: build ( ) ?;
72114 let mut state_file = OpenOptions :: new ( )
@@ -175,6 +217,8 @@ impl AppState {
175217 cmd_runner,
176218 // VS Code has its own file link handling
177219 emit_file_links : env:: var_os ( "TERM_PROGRAM" ) . is_none_or ( |v| v != "vscode" ) ,
220+ zellij,
221+ open_pane : None ,
178222 } ;
179223
180224 Ok ( ( slf, state_file_status) )
@@ -553,6 +597,86 @@ impl AppState {
553597
554598 Ok ( ( ) )
555599 }
600+
601+ pub fn close_pane ( & mut self ) -> Result < ( ) > {
602+ if let Some ( ( pane_id_str, _, _) ) = self . open_pane . take ( ) {
603+ close_pane ( & pane_id_str) ?;
604+ }
605+
606+ Ok ( ( ) )
607+ }
608+
609+ pub fn edit_cmd ( & mut self ) -> Result < EditCmdJoinHandle > {
610+ if !self . zellij {
611+ return Ok ( EditCmdJoinHandle ( None ) ) ;
612+ }
613+
614+ let open_pane = self . open_pane . take ( ) ;
615+ let current_exercise_ind = self . current_exercise_ind ;
616+ let mut edit_cmd = Command :: new ( "zellij" ) ;
617+ edit_cmd
618+ . arg ( "action" )
619+ . arg ( "edit" )
620+ . arg ( & self . current_exercise ( ) . path )
621+ . stdin ( Stdio :: null ( ) )
622+ . stderr ( Stdio :: null ( ) ) ;
623+
624+ let handle = thread:: Builder :: new ( )
625+ . spawn ( move || {
626+ if let Some ( ( pane_id_str, pane_id, exercise_ind) ) = open_pane {
627+ if exercise_ind == current_exercise_ind {
628+ // Check if the pane is still open
629+ let mut output = Command :: new ( "zellij" )
630+ . arg ( "action" )
631+ . arg ( "list-panes" )
632+ . arg ( "-j" )
633+ . stdin ( Stdio :: null ( ) )
634+ . stderr ( Stdio :: null ( ) )
635+ . output ( )
636+ . context ( "Failed to run `zellij action list-panes -j`" ) ?;
637+
638+ if !output. status . success ( ) {
639+ bail ! ( "`zellij action list-panes -j` didn't exit successfully" ) ;
640+ }
641+
642+ // Remove newline
643+ output. stdout . pop ( ) ;
644+
645+ let panes = serde_json:: de:: from_slice :: < Vec < Pane > > ( & output. stdout )
646+ . context (
647+ "Failed to parse the output of `zellij action list-panes -j`" ,
648+ ) ?;
649+
650+ if panes. iter ( ) . any ( |pane| pane. id == pane_id) {
651+ return Ok ( ( pane_id_str, pane_id) ) ;
652+ }
653+ } else {
654+ close_pane ( & pane_id_str) ?;
655+ }
656+ }
657+
658+ let output = edit_cmd. output ( ) ?;
659+
660+ if !output. status . success ( ) {
661+ bail ! ( "Failed to open a new Zellij editor pane" ) ;
662+ }
663+
664+ parse_pane_id ( & output. stdout )
665+ . context ( "Failed to parse the ID of the new Zellij pane" )
666+ } )
667+ . context ( "Failed to spawn a thread to open and close Zellij panes" ) ?;
668+
669+ Ok ( EditCmdJoinHandle ( Some ( handle) ) )
670+ }
671+
672+ pub fn join_edit_cmd ( & mut self , handle : EditCmdJoinHandle ) -> Result < ( ) > {
673+ if let Some ( handle) = handle. 0 {
674+ let ( pane_id_str, pane_id) = handle. join ( ) . unwrap ( ) ?;
675+ self . open_pane = Some ( ( pane_id_str, pane_id, self . current_exercise_ind ) ) ;
676+ }
677+
678+ Ok ( ( ) )
679+ }
556680}
557681
558682const BAD_INDEX_ERR : & str = "The current exercise index is higher than the number of exercises" ;
@@ -608,6 +732,8 @@ mod tests {
608732 official_exercises : true ,
609733 cmd_runner : CmdRunner :: build ( ) . unwrap ( ) ,
610734 emit_file_links : true ,
735+ zellij : false ,
736+ open_pane : None ,
611737 } ;
612738
613739 let mut assert = |done : [ bool ; 3 ] , expected : [ Option < usize > ; 3 ] | {
0 commit comments