@@ -2,8 +2,8 @@ use crossbeam::{
22 channel:: { unbounded, Receiver } ,
33 select,
44} ;
5- use std:: path:: PathBuf ;
6- use std:: time:: Duration ;
5+ use std:: { cmp :: min , path:: PathBuf , process :: Command } ;
6+ use std:: { process :: Stdio , time:: Duration } ;
77
88use crate :: file_watcher:: { FileWatcherError , FileWatcherHandle } ;
99use crate :: job_watcher:: JobWatcherHandle ;
@@ -12,19 +12,24 @@ use crossterm::event::{Event, KeyCode, KeyEvent};
1212use std:: io;
1313use tui:: {
1414 backend:: Backend ,
15- layout:: { Constraint , Direction , Layout } ,
15+ layout:: { Constraint , Direction , Layout , Rect } ,
1616 style:: { Color , Modifier , Style } ,
1717 text:: { Span , Spans , Text } ,
18- widgets:: { Block , Borders , List , ListItem , ListState , Paragraph , Wrap } ,
18+ widgets:: { Block , Borders , Clear , List , ListItem , ListState , Paragraph , Wrap } ,
1919 Frame , Terminal ,
2020} ;
2121
2222pub enum Focus {
2323 Jobs ,
2424}
2525
26+ pub enum Dialog {
27+ ConfirmCancelJob ( String ) ,
28+ }
29+
2630pub struct App {
2731 focus : Focus ,
32+ dialog : Option < Dialog > ,
2833 jobs : Vec < Job > ,
2934 job_list_state : ListState ,
3035 job_stdout : Result < String , FileWatcherError > ,
@@ -37,7 +42,9 @@ pub struct App {
3742}
3843
3944pub struct Job {
40- pub id : String ,
45+ pub job_id : String ,
46+ pub array_id : String ,
47+ pub array_step : Option < String > ,
4148 pub name : String ,
4249 pub state : String ,
4350 pub state_compact : String ,
@@ -52,6 +59,15 @@ pub struct Job {
5259 // pub stderr: Option<PathBuf>,
5360}
5461
62+ impl Job {
63+ fn id ( & self ) -> String {
64+ match self . array_step . as_ref ( ) {
65+ Some ( array_step) => format ! ( "{}_{}" , self . array_id, array_step) ,
66+ None => self . job_id . clone ( ) ,
67+ }
68+ }
69+ }
70+
5571pub enum AppMessage {
5672 Jobs ( Vec < Job > ) ,
5773 JobStdout ( Result < String , FileWatcherError > ) ,
@@ -63,6 +79,7 @@ impl App {
6379 let ( sender, receiver) = unbounded ( ) ;
6480 Self {
6581 focus : Focus :: Jobs ,
82+ dialog : None ,
6683 jobs : Vec :: new ( ) ,
6784 _job_watcher : JobWatcherHandle :: new ( sender. clone ( ) , Duration :: from_secs ( 2 ) ) ,
6885 job_list_state : {
@@ -112,45 +129,74 @@ impl App {
112129 AppMessage :: Jobs ( jobs) => self . jobs = jobs,
113130 AppMessage :: JobStdout ( content) => self . job_stdout = content,
114131 AppMessage :: Key ( key) => {
115- match key. code {
116- KeyCode :: Char ( 'h' ) | KeyCode :: Left => self . focus_previous_panel ( ) ,
117- KeyCode :: Char ( 'l' ) | KeyCode :: Right => self . focus_next_panel ( ) ,
118- KeyCode :: Char ( 'k' ) | KeyCode :: Up => match self . focus {
119- Focus :: Jobs => self . select_previous_job ( ) ,
120- } ,
121- KeyCode :: Char ( 'j' ) | KeyCode :: Down => match self . focus {
122- Focus :: Jobs => self . select_next_job ( ) ,
123- } ,
124- KeyCode :: PageDown => {
125- self . job_stdout_offset = self . job_stdout_offset . saturating_sub (
126- if key
127- . modifiers
128- . contains ( crossterm:: event:: KeyModifiers :: CONTROL )
129- {
130- 10
131- } else {
132- 1
133- } ,
134- )
135- }
136- KeyCode :: PageUp => {
137- self . job_stdout_offset = self . job_stdout_offset . saturating_add (
138- if key
139- . modifiers
140- . contains ( crossterm:: event:: KeyModifiers :: CONTROL )
132+ if let Some ( dialog) = & self . dialog {
133+ match dialog {
134+ Dialog :: ConfirmCancelJob ( id) => match key. code {
135+ KeyCode :: Enter | KeyCode :: Char ( 'y' ) => {
136+ Command :: new ( "scancel" )
137+ . arg ( id)
138+ . stdout ( Stdio :: null ( ) )
139+ . stderr ( Stdio :: null ( ) )
140+ . spawn ( )
141+ . expect ( "failed to execute scancel" ) ;
142+ self . dialog = None ;
143+ }
144+ KeyCode :: Esc => {
145+ self . dialog = None ;
146+ }
147+ _ => { }
148+ } ,
149+ } ;
150+ } else {
151+ match key. code {
152+ KeyCode :: Char ( 'h' ) | KeyCode :: Left => self . focus_previous_panel ( ) ,
153+ KeyCode :: Char ( 'l' ) | KeyCode :: Right => self . focus_next_panel ( ) ,
154+ KeyCode :: Char ( 'k' ) | KeyCode :: Up => match self . focus {
155+ Focus :: Jobs => self . select_previous_job ( ) ,
156+ } ,
157+ KeyCode :: Char ( 'j' ) | KeyCode :: Down => match self . focus {
158+ Focus :: Jobs => self . select_next_job ( ) ,
159+ } ,
160+ KeyCode :: PageDown => {
161+ self . job_stdout_offset = self . job_stdout_offset . saturating_sub (
162+ if key
163+ . modifiers
164+ . contains ( crossterm:: event:: KeyModifiers :: CONTROL )
165+ {
166+ 10
167+ } else {
168+ 1
169+ } ,
170+ )
171+ }
172+ KeyCode :: PageUp => {
173+ self . job_stdout_offset = self . job_stdout_offset . saturating_add (
174+ if key
175+ . modifiers
176+ . contains ( crossterm:: event:: KeyModifiers :: CONTROL )
177+ {
178+ 10
179+ } else {
180+ 1
181+ } ,
182+ )
183+ }
184+ KeyCode :: End => self . job_stdout_offset = 0 ,
185+ // KeyCode::Home => {
186+ // // somehow scroll to top?
187+ // }
188+ KeyCode :: Char ( 'c' ) => {
189+ if let Some ( id) = self
190+ . job_list_state
191+ . selected ( )
192+ . and_then ( |i| self . jobs . get ( i) . map ( |j| j. id ( ) ) )
141193 {
142- 10
143- } else {
144- 1
145- } ,
146- )
147- }
148- KeyCode :: End => self . job_stdout_offset = 0 ,
149- // KeyCode::Home => {
150- // // somehow scroll to top?
151- // }
152- _ => { }
153- } ;
194+ self . dialog = Some ( Dialog :: ConfirmCancelJob ( id) ) ;
195+ }
196+ }
197+ _ => { }
198+ } ;
199+ }
154200 }
155201 }
156202
@@ -190,16 +236,20 @@ impl App {
190236 Span :: styled( "pgup/pgdown/end" , Style :: default ( ) . fg( Color :: Blue ) ) ,
191237 Span :: styled( ": scroll" , Style :: default ( ) . fg( Color :: LightBlue ) ) ,
192238 Span :: raw( " | " ) ,
193- Span :: styled( "ctrl " , Style :: default ( ) . fg( Color :: Blue ) ) ,
194- Span :: styled( ": fast scroll " , Style :: default ( ) . fg( Color :: LightBlue ) ) ,
239+ Span :: styled( "esc " , Style :: default ( ) . fg( Color :: Blue ) ) ,
240+ Span :: styled( ": cancel " , Style :: default ( ) . fg( Color :: LightBlue ) ) ,
195241 Span :: raw( " | " ) ,
196242 Span :: styled( "q" , Style :: default ( ) . fg( Color :: Blue ) ) ,
197243 Span :: styled( ": quit" , Style :: default ( ) . fg( Color :: LightBlue ) ) ,
244+ Span :: raw( " | " ) ,
245+ Span :: styled( "c" , Style :: default ( ) . fg( Color :: Blue ) ) ,
246+ Span :: styled( ": cancel job" , Style :: default ( ) . fg( Color :: LightBlue ) ) ,
198247 ] ) ;
199248 let help = Paragraph :: new ( help) ;
200249 f. render_widget ( help, content_help[ 1 ] ) ;
201250
202251 // Jobs
252+ let max_id_len = self . jobs . iter ( ) . map ( |j| j. id ( ) . len ( ) ) . max ( ) . unwrap_or ( 0 ) ;
203253 let max_user_len = self . jobs . iter ( ) . map ( |j| j. user . len ( ) ) . max ( ) . unwrap_or ( 0 ) ;
204254 let max_partition_len = self
205255 . jobs
@@ -228,7 +278,10 @@ impl App {
228278 Style :: default ( ) ,
229279 ) ,
230280 Span :: raw( " " ) ,
231- Span :: styled( & j. id, Style :: default ( ) . fg( Color :: Yellow ) ) ,
281+ Span :: styled(
282+ format!( "{:<max$.max$}" , j. id( ) , max = max_id_len) ,
283+ Style :: default ( ) . fg( Color :: Yellow ) ,
284+ ) ,
232285 Span :: raw( " " ) ,
233286 Span :: styled(
234287 format!( "{:<max$.max$}" , j. partition, max = max_partition_len) ,
@@ -254,8 +307,12 @@ impl App {
254307 Block :: default ( )
255308 . title ( "Job Queue" )
256309 . borders ( Borders :: ALL )
257- . border_style ( match self . focus {
258- Focus :: Jobs => Style :: default ( ) . fg ( Color :: Green ) ,
310+ . border_style ( if self . dialog . is_some ( ) {
311+ Style :: default ( )
312+ } else {
313+ match self . focus {
314+ Focus :: Jobs => Style :: default ( ) . fg ( Color :: Green ) ,
315+ }
259316 } ) ,
260317 )
261318 . highlight_style ( Style :: default ( ) . bg ( Color :: Green ) . fg ( Color :: Black ) ) ;
@@ -353,6 +410,47 @@ impl App {
353410 . block ( log_block) ;
354411
355412 f. render_widget ( log, log_area) ;
413+
414+ if let Some ( dialog) = & self . dialog {
415+ fn centered_lines ( percent_x : u16 , lines : u16 , r : Rect ) -> Rect {
416+ let dy = r. height . saturating_sub ( lines) / 2 ;
417+ let r = Rect :: new ( r. x , r. y + dy, r. width , min ( lines, r. height - dy) ) ;
418+
419+ Layout :: default ( )
420+ . direction ( Direction :: Horizontal )
421+ . constraints (
422+ [
423+ Constraint :: Percentage ( ( 100 - percent_x) / 2 ) ,
424+ Constraint :: Percentage ( percent_x) ,
425+ Constraint :: Percentage ( ( 100 - percent_x) / 2 ) ,
426+ ]
427+ . as_ref ( ) ,
428+ )
429+ . split ( r) [ 1 ]
430+ }
431+
432+ match dialog {
433+ Dialog :: ConfirmCancelJob ( id) => {
434+ let dialog = Paragraph :: new ( Spans :: from ( vec ! [
435+ Span :: raw( "Cancel job " ) ,
436+ Span :: styled( id, Style :: default ( ) . add_modifier( Modifier :: BOLD ) ) ,
437+ Span :: raw( "?" ) ,
438+ ] ) )
439+ . style ( Style :: default ( ) . fg ( Color :: White ) )
440+ . wrap ( Wrap { trim : true } )
441+ . block (
442+ Block :: default ( )
443+ . title ( "Confirm" )
444+ . borders ( Borders :: ALL )
445+ . style ( Style :: default ( ) . fg ( Color :: Green ) ) ,
446+ ) ;
447+
448+ let area = centered_lines ( 75 , 3 , f. size ( ) ) ;
449+ f. render_widget ( Clear , area) ;
450+ f. render_widget ( dialog, area) ;
451+ }
452+ }
453+ }
356454 }
357455}
358456
0 commit comments