use color_eyre::{Result, eyre::eyre};
use std::process::Command;
use crate::job::Job;
use crate::update_content::ContentUpdater;
use crate::user_options::UserOptions;
#[derive(PartialEq, Clone, Debug)]
pub enum SortCategory {
Id,
Name,
Status,
Time,
Partition,
Nodes,
}
impl SortCategory {
pub fn next(&self) -> SortCategory {
match self {
SortCategory::Id => SortCategory::Name,
SortCategory::Name => SortCategory::Status,
SortCategory::Status => SortCategory::Time,
SortCategory::Time => SortCategory::Partition,
SortCategory::Partition => SortCategory::Nodes,
SortCategory::Nodes => SortCategory::Id,
}
}
}
#[derive(Debug, Clone)]
pub enum JobListAction {
Next,
Previous,
Select(usize),
SelectSortCategory(SortCategory),
NextSortCategory,
ReverseSortDirection,
UpdateSqueueCommand(String),
}
pub struct JobList {
pub jobs: Vec<Job>,
selected: usize,
job_details: String,
log_tail: String,
sort_category: SortCategory,
reverse: bool,
content_updater: ContentUpdater,
pub squeue_command: String,
}
impl JobList {
pub fn new() -> JobList {
JobList {
jobs: Vec::new(),
selected: 0,
job_details: String::new(),
log_tail: String::new(),
sort_category: SortCategory::Id,
reverse: false,
content_updater: ContentUpdater::new(),
squeue_command: format!("squeue -u {}", whoami())
}
}
}
fn whoami() -> String {
let command = Command::new("whoami")
.output();
match command {
Ok(output) => {
let output = String::from_utf8_lossy(&output.stdout);
output.to_string()
},
Err(_) => {
"Error executing whoami".to_string()
},
}
}
impl JobList {
pub fn get_job(&self) -> Option<&Job> {
self.jobs.get(self.selected)
}
pub fn get_job_details(&self) -> &str {
&self.job_details
}
pub fn get_log_tail(&self) -> &str {
&self.log_tail
}
pub fn get_index(&self) -> usize {
self.selected
}
pub fn get_sort_category(&self) -> &SortCategory {
&self.sort_category
}
pub fn is_reverse(&self) -> bool {
self.reverse
}
pub fn len(&self) -> usize {
self.jobs.len()
}
}
impl JobList {
pub fn set_index(&mut self, index: usize) -> Result<()> {
if self.jobs.is_empty() {
self.selected = 0;
self.job_details = "No job selected".to_string();
self.log_tail = "No job selected".to_string();
return Ok(());
}
if index >= self.jobs.len() {
return Err(eyre!("Index out of bounds"));
}
self.selected = index;
Ok(())
}
fn set_loading_text(&mut self) {
self.job_details = "loading...".to_string();
self.log_tail = "loading...".to_string();
}
pub fn select_job_by_id(&mut self, id: String) -> Result<()> {
let index = self.jobs.iter().position(|job| job.id == id);
match index {
Some(index) => {
self.set_index(index)?;
Ok(())
}
None => Err(eyre!("Job with id {} not found", id)),
}
}
pub fn handle_joblist_action(&mut self, action: JobListAction) {
match action {
JobListAction::Next => self.next(),
JobListAction::Previous => self.previous(),
JobListAction::Select(index) => {
if index < self.len() {
self.set_index(index).unwrap();
self.set_loading_text();
}
}
JobListAction::NextSortCategory => {
self.set_sort_category(self.sort_category.next());
}
JobListAction::ReverseSortDirection => {
self.negate_reverse();
}
JobListAction::SelectSortCategory(category) => {
if category != self.sort_category {
self.set_sort_category(category);
} else {
self.negate_reverse();
}
}
JobListAction::UpdateSqueueCommand(command) => {
self.squeue_command = command;
}
}
}
pub fn set_sort_category(&mut self, category: SortCategory) {
self.sort_category = category;
self.sort_raw();
self.set_index(0).unwrap();
}
pub fn negate_reverse(&mut self) {
self.reverse = !self.reverse;
self.sort_raw();
self.set_index(0).unwrap();
}
}
impl JobList {
pub fn update_jobs(&mut self, user_options: &UserOptions) {
let job: Option<Job> = self.get_job().cloned();
let command = self.squeue_command.clone();
match self.content_updater.tick(
job.clone(), command, user_options.clone()) {
Some(content) => {
self.jobs = content.job_list;
self.job_details = content.details_text;
self.log_tail = content.log_text;
}
None => { }
}
self.sort_raw();
if let Some(job) = job {
self.select_job_by_id(job.id).unwrap_or_else(|_| {
if self.selected >= self.len() {
self.set_index(0).unwrap();
}
});
}
}
pub fn next(&mut self) {
if self.jobs.is_empty() { return; }
let new_index = (self.selected + 1) % self.len();
self.set_index(new_index).unwrap();
self.set_loading_text();
}
pub fn previous(&mut self) {
if self.jobs.is_empty() { return; }
let job_count = self.len();
let new_index = (self.selected + job_count - 1) % job_count;
self.set_index(new_index).unwrap();
self.set_loading_text();
}
fn sort_raw(&mut self) {
if self.jobs.is_empty() { return; }
match self.sort_category {
SortCategory::Id => {
self.jobs.sort_by(|a, b| b.id.cmp(&a.id));
},
SortCategory::Name => {
self.jobs.sort_by(|a, b| {
a.name.cmp(&b.name).then_with(|| a.id.cmp(&b.id))
});
},
SortCategory::Status => {
self.jobs.sort_by(|a, b| {
a.status.priority().cmp(&b.status.priority())
.then_with(|| a.id.cmp(&b.id))
});
},
SortCategory::Time => {
self.jobs.sort_by(|a, b| {
a.time.cmp(&b.time).then_with(|| a.id.cmp(&b.id))
});
},
SortCategory::Partition => {
self.jobs.sort_by(|a, b| {
a.partition.cmp(&b.partition)
.then_with(|| a.id.cmp(&b.id))
});
},
SortCategory::Nodes => {
self.jobs.sort_by(|a, b| {
b.nodes.cmp(&a.nodes).then_with(|| a.id.cmp(&b.id))
});
},
}
if self.reverse {
self.jobs.reverse();
}
}
pub fn sort(&mut self) {
if self.jobs.is_empty() { return; }
if let Some(job) = self.get_job() {
let id = job.id.clone();
self.sort_raw();
self.select_job_by_id(id).unwrap();
} else {
self.sort_raw();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::job::JobStatus;
fn create_job_list() -> JobList {
let mut job_list = JobList::new();
job_list.jobs.push(Job::new(
"1", "job1", JobStatus::Running,
"00:00:00", "partition1", 1,
"workdir1", "command1", None));
job_list.jobs.push(Job::new(
"2", "job2", JobStatus::Pending,
"00:00:00", "partition1", 2,
"workdir2", "command2", None));
job_list.jobs.push(Job::new(
"3", "job3", JobStatus::Completing,
"00:00:00", "partition1", 3,
"workdir3", "command3", None));
job_list
}
#[test]
fn test_set_index() {
let mut job_list = create_job_list();
assert!(job_list.set_index(1).is_ok());
assert_eq!(job_list.selected, 1);
assert!(job_list.set_index(3).is_err());
let mut job_list = JobList::new();
assert!(job_list.set_index(0).is_ok());
assert_eq!(job_list.selected, 0);
assert_eq!(job_list.job_details, "No job selected");
assert_eq!(job_list.log_tail, "No job selected");
}
#[test]
fn test_select_job_by_id() {
let mut job_list = create_job_list();
assert!(job_list.select_job_by_id("2".to_string()).is_ok());
assert_eq!(job_list.selected, 1);
assert!(job_list.select_job_by_id("4".to_string()).is_err());
}
#[test]
fn test_next() {
let mut job_list = create_job_list();
assert_eq!(job_list.len(), 3);
job_list.next();
assert_eq!(job_list.selected, 1);
assert_eq!(job_list.job_details, "loading...");
assert_eq!(job_list.log_tail, "loading...");
job_list.selected = 2;
job_list.next();
assert_eq!(job_list.selected, 0);
}
#[test]
fn test_previous() {
let mut job_list = create_job_list();
assert_eq!(job_list.len(), 3);
job_list.previous();
assert_eq!(job_list.selected, 2);
assert_eq!(job_list.job_details, "loading...");
assert_eq!(job_list.log_tail, "loading...");
job_list.previous();
assert_eq!(job_list.selected, 1);
}
#[test]
fn test_reverse() {
let mut job_list = create_job_list();
job_list.selected = 1;
assert_eq!(job_list.reverse, false);
job_list.negate_reverse();
assert_eq!(job_list.reverse, true);
assert_eq!(job_list.selected, 0);
job_list.negate_reverse();
assert_eq!(job_list.reverse, false);
}
#[test]
fn test_sort() {
let mut job_list = create_job_list();
let job = job_list.get_job().unwrap().clone();
job_list.sort();
assert_eq!(job_list.jobs[0].id, "3");
assert_eq!(job_list.jobs[1].id, "2");
assert_eq!(job_list.jobs[2].id, "1");
assert_eq!(job_list.get_job().unwrap().id, job.id);
job_list.reverse = true;
job_list.sort();
assert_eq!(job_list.jobs[0].id, "1");
assert_eq!(job_list.jobs[1].id, "2");
assert_eq!(job_list.jobs[2].id, "3");
assert_eq!(job_list.get_job().unwrap().id, job.id);
}
}