Skip to content

Commit bc05723

Browse files
committed
feat: cancel jobs
fix: correctly display job ids in arrays
1 parent 518afdb commit bc05723

File tree

2 files changed

+155
-51
lines changed

2 files changed

+155
-51
lines changed

src/app.rs

Lines changed: 146 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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

88
use crate::file_watcher::{FileWatcherError, FileWatcherHandle};
99
use crate::job_watcher::JobWatcherHandle;
@@ -12,19 +12,24 @@ use crossterm::event::{Event, KeyCode, KeyEvent};
1212
use std::io;
1313
use 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

2222
pub enum Focus {
2323
Jobs,
2424
}
2525

26+
pub enum Dialog {
27+
ConfirmCancelJob(String),
28+
}
29+
2630
pub 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

3944
pub 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+
5571
pub 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

src/job_watcher.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ impl JobWatcher {
4949
loop {
5050
let jobs: Vec<Job> = Command::new("squeue")
5151
.args(&cli_args)
52-
.arg("-h")
53-
.arg("-O")
52+
.arg("--array")
53+
.arg("--noheader")
54+
.arg("--Format")
5455
.arg(&output_format)
5556
.output()
5657
.expect("failed to execute process")
@@ -83,7 +84,12 @@ impl JobWatcher {
8384
let working_dir = parts[15];
8485

8586
Some(Job {
86-
id: id.to_owned(),
87+
job_id: id.to_owned(),
88+
array_id: array_job_id.to_owned(),
89+
array_step: match array_task_id {
90+
"N/A" => None,
91+
_ => Some(array_task_id.to_owned()),
92+
},
8793
name: name.to_owned(),
8894
state: state.to_owned(),
8995
state_compact: state_compact.to_owned(),

0 commit comments

Comments
 (0)