mirror of
https://github.com/nukesor/pueue
synced 2024-07-23 19:25:16 +00:00
refactor: Add TableBuilder
This commit is contained in:
parent
b0e6c35ebc
commit
196d5bec09
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -885,9 +885,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest"
|
name = "pest"
|
||||||
version = "2.3.0"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4"
|
checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ucd-trie",
|
"ucd-trie",
|
||||||
|
@ -895,9 +895,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_derive"
|
name = "pest_derive"
|
||||||
version = "2.3.0"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91"
|
checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_generator",
|
"pest_generator",
|
||||||
|
@ -905,9 +905,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_generator"
|
name = "pest_generator"
|
||||||
version = "2.3.0"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad"
|
checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pest",
|
"pest",
|
||||||
"pest_meta",
|
"pest_meta",
|
||||||
|
@ -918,13 +918,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pest_meta"
|
name = "pest_meta"
|
||||||
version = "2.3.0"
|
version = "2.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04"
|
checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pest",
|
"pest",
|
||||||
"sha-1",
|
"sha1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1425,10 +1425,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha1"
|
||||||
version = "0.10.0"
|
version = "0.10.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
|
checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures",
|
||||||
|
|
|
@ -1,30 +1,8 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use pueue_lib::task::{Task, TaskStatus};
|
use chrono::Local;
|
||||||
|
|
||||||
/// This is a helper function for working with tables when calling `pueue status`.
|
use pueue_lib::{settings::Settings, task::Task};
|
||||||
///
|
|
||||||
/// By default, several columns aren't shown until there's at least one task with relevant data.
|
|
||||||
/// This function determines whether any of those columns should be shown.
|
|
||||||
pub fn has_special_columns(tasks: &[Task]) -> (bool, bool, bool) {
|
|
||||||
// Check whether there are any delayed tasks.
|
|
||||||
let has_delayed_tasks = tasks.iter().any(|task| {
|
|
||||||
matches!(
|
|
||||||
task.status,
|
|
||||||
TaskStatus::Stashed {
|
|
||||||
enqueue_at: Some(_)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check whether there are any tasks with dependencies.
|
|
||||||
let has_dependencies = tasks.iter().any(|task| !task.dependencies.is_empty());
|
|
||||||
|
|
||||||
// Check whether there are any tasks a label.
|
|
||||||
let has_labels = tasks.iter().any(|task| task.label.is_some());
|
|
||||||
|
|
||||||
(has_delayed_tasks, has_dependencies, has_labels)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sort given tasks by their groups.
|
/// Sort given tasks by their groups.
|
||||||
/// This is needed to print a table for each group.
|
/// This is needed to print a table for each group.
|
||||||
|
@ -40,3 +18,50 @@ pub fn sort_tasks_by_group(tasks: Vec<Task>) -> BTreeMap<String, Vec<Task>> {
|
||||||
|
|
||||||
sorted_task_groups
|
sorted_task_groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the formatted `start` and `end` text for a given task.
|
||||||
|
///
|
||||||
|
/// 1. If the start || end is today, skip the date.
|
||||||
|
/// 2. Otherwise show the date in both.
|
||||||
|
///
|
||||||
|
/// If the task doesn't have a start and/or end yet, an empty string will be returned
|
||||||
|
/// for the respective field.
|
||||||
|
pub fn formatted_start_end(task: &Task, settings: &Settings) -> (String, String) {
|
||||||
|
// Get the start time.
|
||||||
|
// If the task didn't start yet, just return two empty strings.
|
||||||
|
let start = match task.start {
|
||||||
|
Some(start) => start,
|
||||||
|
None => return ("".into(), "".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the task started today, just show the time.
|
||||||
|
// Otherwise show the full date and time.
|
||||||
|
let started_today = start >= Local::today().and_hms(0, 0, 0);
|
||||||
|
let formatted_start = if started_today {
|
||||||
|
start
|
||||||
|
.format(&settings.client.status_time_format)
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
start
|
||||||
|
.format(&settings.client.status_datetime_format)
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get finish time, if already set. Otherwise only return the formatted start.
|
||||||
|
let end = match task.end {
|
||||||
|
Some(end) => end,
|
||||||
|
None => return (formatted_start, "".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the task ended today we only show the time.
|
||||||
|
// In all other circumstances, we show the full date.
|
||||||
|
let finished_today = end >= Local::today().and_hms(0, 0, 0);
|
||||||
|
let formatted_end = if finished_today {
|
||||||
|
end.format(&settings.client.status_time_format).to_string()
|
||||||
|
} else {
|
||||||
|
end.format(&settings.client.status_datetime_format)
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
(formatted_start, formatted_end)
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ pub mod helper;
|
||||||
mod log;
|
mod log;
|
||||||
mod state;
|
mod state;
|
||||||
pub mod style;
|
pub mod style;
|
||||||
|
mod table_builder;
|
||||||
|
|
||||||
use crossterm::style::Color;
|
use crossterm::style::Color;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
use std::string::ToString;
|
|
||||||
|
|
||||||
use chrono::{Duration, Local};
|
|
||||||
use comfy_table::presets::UTF8_HORIZONTAL_ONLY;
|
|
||||||
use comfy_table::*;
|
|
||||||
|
|
||||||
use pueue_lib::settings::Settings;
|
use pueue_lib::settings::Settings;
|
||||||
use pueue_lib::state::{State, PUEUE_DEFAULT_GROUP};
|
use pueue_lib::state::{State, PUEUE_DEFAULT_GROUP};
|
||||||
use pueue_lib::task::{Task, TaskResult, TaskStatus};
|
use pueue_lib::task::Task;
|
||||||
|
|
||||||
use super::{helper::*, OutputStyle};
|
use super::{helper::*, table_builder::TableBuilder, OutputStyle};
|
||||||
use crate::cli::SubCommand;
|
use crate::cli::SubCommand;
|
||||||
use crate::display::group::get_group_headline;
|
use crate::display::group::get_group_headline;
|
||||||
|
|
||||||
|
@ -17,12 +11,12 @@ use crate::display::group::get_group_headline;
|
||||||
///
|
///
|
||||||
/// We pass the tasks as a separate parameter and as a list.
|
/// We pass the tasks as a separate parameter and as a list.
|
||||||
/// This allows us to print the tasks in any user-defined order.
|
/// This allows us to print the tasks in any user-defined order.
|
||||||
pub fn print_state(
|
pub fn print_state<'a>(
|
||||||
state: State,
|
state: State,
|
||||||
tasks: Vec<Task>,
|
tasks: Vec<Task>,
|
||||||
cli_command: &SubCommand,
|
cli_command: &SubCommand,
|
||||||
style: &OutputStyle,
|
style: &'a OutputStyle,
|
||||||
settings: &Settings,
|
settings: &'a Settings,
|
||||||
) {
|
) {
|
||||||
let (json, group_only) = match cli_command {
|
let (json, group_only) = match cli_command {
|
||||||
SubCommand::Status { json, group } => (*json, group.clone()),
|
SubCommand::Status { json, group } => (*json, group.clone()),
|
||||||
|
@ -36,12 +30,14 @@ pub fn print_state(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let table_builder = TableBuilder::new(settings, style);
|
||||||
|
|
||||||
if let Some(group) = group_only {
|
if let Some(group) = group_only {
|
||||||
print_single_group(state, tasks, settings, style, group);
|
print_single_group(state, tasks, style, group, table_builder);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_all_groups(state, tasks, settings, style);
|
print_all_groups(state, tasks, style, table_builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The user requested only a single group to be displayed.
|
/// The user requested only a single group to be displayed.
|
||||||
|
@ -50,9 +46,9 @@ pub fn print_state(
|
||||||
fn print_single_group(
|
fn print_single_group(
|
||||||
state: State,
|
state: State,
|
||||||
tasks: Vec<Task>,
|
tasks: Vec<Task>,
|
||||||
settings: &Settings,
|
|
||||||
style: &OutputStyle,
|
style: &OutputStyle,
|
||||||
group_name: String,
|
group_name: String,
|
||||||
|
table_builder: TableBuilder,
|
||||||
) {
|
) {
|
||||||
// Sort all tasks by their respective group;
|
// Sort all tasks by their respective group;
|
||||||
let mut sorted_tasks = sort_tasks_by_group(tasks);
|
let mut sorted_tasks = sort_tasks_by_group(tasks);
|
||||||
|
@ -74,13 +70,20 @@ fn print_single_group(
|
||||||
println!("Task list is empty. Add tasks with `pueue add -g {group_name} -- [cmd]`");
|
println!("Task list is empty. Add tasks with `pueue add -g {group_name} -- [cmd]`");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
print_table(tasks, style, settings);
|
|
||||||
|
let table = table_builder.build(tasks);
|
||||||
|
println!("{table}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print all groups. All tasks will be shown in the table of their assigned group.
|
/// Print all groups. All tasks will be shown in the table of their assigned group.
|
||||||
///
|
///
|
||||||
/// This will create multiple tables, one table for each group.
|
/// This will create multiple tables, one table for each group.
|
||||||
fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, style: &OutputStyle) {
|
fn print_all_groups(
|
||||||
|
state: State,
|
||||||
|
tasks: Vec<Task>,
|
||||||
|
style: &OutputStyle,
|
||||||
|
table_builder: TableBuilder,
|
||||||
|
) {
|
||||||
// Early exit and hint if there are no tasks in the queue
|
// Early exit and hint if there are no tasks in the queue
|
||||||
// Print the state of the default group anyway, since this is information one wants to
|
// Print the state of the default group anyway, since this is information one wants to
|
||||||
// see most of the time anyway.
|
// see most of the time anyway.
|
||||||
|
@ -107,7 +110,8 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, style:
|
||||||
style,
|
style,
|
||||||
);
|
);
|
||||||
println!("{headline}");
|
println!("{headline}");
|
||||||
print_table(tasks, style, settings);
|
let table = table_builder.clone().build(tasks);
|
||||||
|
println!("{table}");
|
||||||
|
|
||||||
// Add a newline if there are further groups to be printed
|
// Add a newline if there are further groups to be printed
|
||||||
if sorted_tasks.len() > 1 {
|
if sorted_tasks.len() > 1 {
|
||||||
|
@ -126,7 +130,8 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, style:
|
||||||
|
|
||||||
let headline = get_group_headline(group, state.groups.get(group).unwrap(), style);
|
let headline = get_group_headline(group, state.groups.get(group).unwrap(), style);
|
||||||
println!("{headline}");
|
println!("{headline}");
|
||||||
print_table(tasks, style, settings);
|
let table = table_builder.clone().build(tasks);
|
||||||
|
println!("{table}");
|
||||||
|
|
||||||
// Add a newline between groups
|
// Add a newline between groups
|
||||||
if sorted_iter.peek().is_some() {
|
if sorted_iter.peek().is_some() {
|
||||||
|
@ -134,165 +139,3 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, style:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display tasks in a nicely formatted table.
|
|
||||||
///
|
|
||||||
/// For info on how this works, take a look at [comfy_table].
|
|
||||||
fn print_table(tasks: &[Task], style: &OutputStyle, settings: &Settings) {
|
|
||||||
let (has_delayed_tasks, has_dependencies, has_labels) = has_special_columns(tasks);
|
|
||||||
|
|
||||||
// Create table header row
|
|
||||||
let mut headers = vec![Cell::new("Id"), Cell::new("Status")];
|
|
||||||
|
|
||||||
if has_delayed_tasks {
|
|
||||||
headers.push(Cell::new("Enqueue At"));
|
|
||||||
}
|
|
||||||
if has_dependencies {
|
|
||||||
headers.push(Cell::new("Deps"));
|
|
||||||
}
|
|
||||||
if has_labels {
|
|
||||||
headers.push(Cell::new("Label"));
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.append(&mut vec![
|
|
||||||
Cell::new("Command"),
|
|
||||||
Cell::new("Path"),
|
|
||||||
Cell::new("Start"),
|
|
||||||
Cell::new("End"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Initialize comfy table.
|
|
||||||
let mut table = Table::new();
|
|
||||||
table
|
|
||||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
|
||||||
.load_preset(UTF8_HORIZONTAL_ONLY)
|
|
||||||
.set_header(headers);
|
|
||||||
|
|
||||||
// Explicitly force styling, in case we aren't on a tty, but `--color=always` is set.
|
|
||||||
if style.enabled {
|
|
||||||
table.enforce_styling();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add rows one by one.
|
|
||||||
for task in tasks.iter() {
|
|
||||||
let mut row = Row::new();
|
|
||||||
if let Some(height) = settings.client.max_status_lines {
|
|
||||||
row.max_height(height);
|
|
||||||
}
|
|
||||||
row.add_cell(Cell::new(&task.id));
|
|
||||||
|
|
||||||
// Determine the human readable task status representation and the respective color.
|
|
||||||
let status_string = task.status.to_string();
|
|
||||||
let (status_text, color) = match &task.status {
|
|
||||||
TaskStatus::Running => (status_string, Color::Green),
|
|
||||||
TaskStatus::Paused | TaskStatus::Locked => (status_string, Color::White),
|
|
||||||
TaskStatus::Done(result) => match result {
|
|
||||||
TaskResult::Success => (TaskResult::Success.to_string(), Color::Green),
|
|
||||||
TaskResult::DependencyFailed => ("Dependency failed".to_string(), Color::Red),
|
|
||||||
TaskResult::FailedToSpawn(_) => ("Failed to spawn".to_string(), Color::Red),
|
|
||||||
TaskResult::Failed(code) => (format!("Failed ({code})"), Color::Red),
|
|
||||||
_ => (result.to_string(), Color::Red),
|
|
||||||
},
|
|
||||||
_ => (status_string, Color::Yellow),
|
|
||||||
};
|
|
||||||
row.add_cell(style.styled_cell(status_text, Some(color), None));
|
|
||||||
|
|
||||||
if has_delayed_tasks {
|
|
||||||
if let TaskStatus::Stashed {
|
|
||||||
enqueue_at: Some(enqueue_at),
|
|
||||||
} = task.status
|
|
||||||
{
|
|
||||||
// Only show the date if the task is not supposed to be enqueued today.
|
|
||||||
let enqueue_today =
|
|
||||||
enqueue_at <= Local::today().and_hms(0, 0, 0) + Duration::days(1);
|
|
||||||
let formatted_enqueue_at = if enqueue_today {
|
|
||||||
enqueue_at.format(&settings.client.status_time_format)
|
|
||||||
} else {
|
|
||||||
enqueue_at.format(&settings.client.status_datetime_format)
|
|
||||||
};
|
|
||||||
row.add_cell(Cell::new(formatted_enqueue_at));
|
|
||||||
} else {
|
|
||||||
row.add_cell(Cell::new(""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_dependencies {
|
|
||||||
let text = task
|
|
||||||
.dependencies
|
|
||||||
.iter()
|
|
||||||
.map(|id| id.to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", ");
|
|
||||||
row.add_cell(Cell::new(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_labels {
|
|
||||||
row.add_cell(Cell::new(&task.label.as_deref().unwrap_or_default()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add command and path.
|
|
||||||
if settings.client.show_expanded_aliases {
|
|
||||||
row.add_cell(Cell::new(&task.command));
|
|
||||||
} else {
|
|
||||||
row.add_cell(Cell::new(&task.original_command));
|
|
||||||
}
|
|
||||||
row.add_cell(Cell::new(&task.path.to_string_lossy()));
|
|
||||||
|
|
||||||
// Add start and end info
|
|
||||||
let (start, end) = formatted_start_end(task, settings);
|
|
||||||
row.add_cell(Cell::new(start));
|
|
||||||
row.add_cell(Cell::new(end));
|
|
||||||
|
|
||||||
table.add_row(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the table.
|
|
||||||
println!("{table}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the formatted `start` and `end` text for a given task.
|
|
||||||
///
|
|
||||||
/// 1. If the start || end is today, skip the date.
|
|
||||||
/// 2. Otherwise show the date in both.
|
|
||||||
///
|
|
||||||
/// If the task doesn't have a start and/or end yet, an empty string will be returned
|
|
||||||
/// for the respective field.
|
|
||||||
fn formatted_start_end(task: &Task, settings: &Settings) -> (String, String) {
|
|
||||||
// Get the start time.
|
|
||||||
// If the task didn't start yet, just return two empty strings.
|
|
||||||
let start = match task.start {
|
|
||||||
Some(start) => start,
|
|
||||||
None => return ("".into(), "".into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the task started today, just show the time.
|
|
||||||
// Otherwise show the full date and time.
|
|
||||||
let started_today = start >= Local::today().and_hms(0, 0, 0);
|
|
||||||
let formatted_start = if started_today {
|
|
||||||
start
|
|
||||||
.format(&settings.client.status_time_format)
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
start
|
|
||||||
.format(&settings.client.status_datetime_format)
|
|
||||||
.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get finish time, if already set. Otherwise only return the formatted start.
|
|
||||||
let end = match task.end {
|
|
||||||
Some(end) => end,
|
|
||||||
None => return (formatted_start, "".into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the task ended today we only show the time.
|
|
||||||
// In all other circumstances, we show the full date.
|
|
||||||
let finished_today = end >= Local::today().and_hms(0, 0, 0);
|
|
||||||
let formatted_end = if finished_today {
|
|
||||||
end.format(&settings.client.status_time_format).to_string()
|
|
||||||
} else {
|
|
||||||
end.format(&settings.client.status_datetime_format)
|
|
||||||
.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
(formatted_start, formatted_end)
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ use crossterm::style::{style, Attribute, Color, Stylize};
|
||||||
/// OutputStyle wrapper for actual colors depending on settings
|
/// OutputStyle wrapper for actual colors depending on settings
|
||||||
/// - Enables styles if color mode is 'always', or if color mode is 'auto' and output is a tty.
|
/// - Enables styles if color mode is 'always', or if color mode is 'auto' and output is a tty.
|
||||||
/// - Using dark colors if dark_mode is enabled
|
/// - Using dark colors if dark_mode is enabled
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct OutputStyle {
|
pub struct OutputStyle {
|
||||||
/// whether or not ANSI styling is enabled
|
/// whether or not ANSI styling is enabled
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
|
|
225
client/display/table_builder.rs
Normal file
225
client/display/table_builder.rs
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
use chrono::{Duration, Local};
|
||||||
|
use comfy_table::presets::UTF8_HORIZONTAL_ONLY;
|
||||||
|
use comfy_table::*;
|
||||||
|
|
||||||
|
use pueue_lib::settings::Settings;
|
||||||
|
use pueue_lib::task::{Task, TaskResult, TaskStatus};
|
||||||
|
|
||||||
|
use super::helper::formatted_start_end;
|
||||||
|
use super::OutputStyle;
|
||||||
|
|
||||||
|
/// This builder is responsible for determining which table columns should be displayed and
|
||||||
|
/// building a full [comfy_table] from a list of given [Task]s.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TableBuilder<'a> {
|
||||||
|
settings: &'a Settings,
|
||||||
|
style: &'a OutputStyle,
|
||||||
|
|
||||||
|
/// This following fields represent which columns should be displayed when executing
|
||||||
|
/// `pueue status`. `true` for any column means that it'll be shown in the table.
|
||||||
|
id: bool,
|
||||||
|
status: bool,
|
||||||
|
enqueue_at: bool,
|
||||||
|
dependencies: bool,
|
||||||
|
label: bool,
|
||||||
|
command: bool,
|
||||||
|
path: bool,
|
||||||
|
start: bool,
|
||||||
|
end: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TableBuilder<'a> {
|
||||||
|
pub fn new(settings: &'a Settings, style: &'a OutputStyle) -> Self {
|
||||||
|
Self {
|
||||||
|
settings,
|
||||||
|
style,
|
||||||
|
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
enqueue_at: false,
|
||||||
|
dependencies: false,
|
||||||
|
label: false,
|
||||||
|
command: true,
|
||||||
|
path: true,
|
||||||
|
start: true,
|
||||||
|
end: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(mut self, tasks: &[Task]) -> Table {
|
||||||
|
self.determine_special_columns(tasks);
|
||||||
|
|
||||||
|
let mut table = Table::new();
|
||||||
|
table
|
||||||
|
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||||
|
.load_preset(UTF8_HORIZONTAL_ONLY)
|
||||||
|
.set_header(self.build_header())
|
||||||
|
.add_rows(self.build_task_rows(tasks));
|
||||||
|
|
||||||
|
// Explicitly force styling, in case we aren't on a tty, but `--color=always` is set.
|
||||||
|
if self.style.enabled {
|
||||||
|
table.enforce_styling();
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
/// By default, several columns aren't shown until there's at least one task with relevant data.
|
||||||
|
/// This function determines whether any of those columns should be shown.
|
||||||
|
fn determine_special_columns(&mut self, tasks: &[Task]) {
|
||||||
|
// Check whether there are any delayed tasks.
|
||||||
|
let has_delayed_tasks = tasks.iter().any(|task| {
|
||||||
|
matches!(
|
||||||
|
task.status,
|
||||||
|
TaskStatus::Stashed {
|
||||||
|
enqueue_at: Some(_)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if has_delayed_tasks {
|
||||||
|
self.enqueue_at = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether there are any tasks with dependencies.
|
||||||
|
if tasks.iter().any(|task| !task.dependencies.is_empty()) {
|
||||||
|
self.dependencies = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether there are any tasks a label.
|
||||||
|
if tasks.iter().any(|task| task.label.is_some()) {
|
||||||
|
self.label = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a header row based on the current selection of columns.
|
||||||
|
fn build_header(&self) -> Row {
|
||||||
|
let mut header = Vec::new();
|
||||||
|
|
||||||
|
// Create table header row
|
||||||
|
if self.id {
|
||||||
|
header.push(Cell::new("Id"));
|
||||||
|
}
|
||||||
|
if self.status {
|
||||||
|
header.push(Cell::new("Status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enqueue_at {
|
||||||
|
header.push(Cell::new("Enqueue At"));
|
||||||
|
}
|
||||||
|
if self.dependencies {
|
||||||
|
header.push(Cell::new("Deps"));
|
||||||
|
}
|
||||||
|
if self.label {
|
||||||
|
header.push(Cell::new("Label"));
|
||||||
|
}
|
||||||
|
if self.command {
|
||||||
|
header.push(Cell::new("Command"));
|
||||||
|
}
|
||||||
|
if self.path {
|
||||||
|
header.push(Cell::new("Path"));
|
||||||
|
}
|
||||||
|
if self.start {
|
||||||
|
header.push(Cell::new("Start"));
|
||||||
|
}
|
||||||
|
if self.end {
|
||||||
|
header.push(Cell::new("End"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Row::from(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_task_rows(&self, tasks: &[Task]) -> Vec<Row> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
// Add rows one by one.
|
||||||
|
for task in tasks.iter() {
|
||||||
|
let mut row = Row::new();
|
||||||
|
// Users can set a max height per row.
|
||||||
|
if let Some(height) = self.settings.client.max_status_lines {
|
||||||
|
row.max_height(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.id {
|
||||||
|
row.add_cell(Cell::new(&task.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.status {
|
||||||
|
// Determine the human readable task status representation and the respective color.
|
||||||
|
let status_string = task.status.to_string();
|
||||||
|
let (status_text, color) = match &task.status {
|
||||||
|
TaskStatus::Running => (status_string, Color::Green),
|
||||||
|
TaskStatus::Paused | TaskStatus::Locked => (status_string, Color::White),
|
||||||
|
TaskStatus::Done(result) => match result {
|
||||||
|
TaskResult::Success => (TaskResult::Success.to_string(), Color::Green),
|
||||||
|
TaskResult::DependencyFailed => {
|
||||||
|
("Dependency failed".to_string(), Color::Red)
|
||||||
|
}
|
||||||
|
TaskResult::FailedToSpawn(_) => ("Failed to spawn".to_string(), Color::Red),
|
||||||
|
TaskResult::Failed(code) => (format!("Failed ({code})"), Color::Red),
|
||||||
|
_ => (result.to_string(), Color::Red),
|
||||||
|
},
|
||||||
|
_ => (status_string, Color::Yellow),
|
||||||
|
};
|
||||||
|
row.add_cell(self.style.styled_cell(status_text, Some(color), None));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enqueue_at {
|
||||||
|
if let TaskStatus::Stashed {
|
||||||
|
enqueue_at: Some(enqueue_at),
|
||||||
|
} = task.status
|
||||||
|
{
|
||||||
|
// Only show the date if the task is not supposed to be enqueued today.
|
||||||
|
let enqueue_today =
|
||||||
|
enqueue_at <= Local::today().and_hms(0, 0, 0) + Duration::days(1);
|
||||||
|
let formatted_enqueue_at = if enqueue_today {
|
||||||
|
enqueue_at.format(&self.settings.client.status_time_format)
|
||||||
|
} else {
|
||||||
|
enqueue_at.format(&self.settings.client.status_datetime_format)
|
||||||
|
};
|
||||||
|
row.add_cell(Cell::new(formatted_enqueue_at));
|
||||||
|
} else {
|
||||||
|
row.add_cell(Cell::new(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.dependencies {
|
||||||
|
let text = task
|
||||||
|
.dependencies
|
||||||
|
.iter()
|
||||||
|
.map(|id| id.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ");
|
||||||
|
row.add_cell(Cell::new(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.label {
|
||||||
|
row.add_cell(Cell::new(&task.label.as_deref().unwrap_or_default()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add command and path.
|
||||||
|
if self.command {
|
||||||
|
if self.settings.client.show_expanded_aliases {
|
||||||
|
row.add_cell(Cell::new(&task.command));
|
||||||
|
} else {
|
||||||
|
row.add_cell(Cell::new(&task.original_command));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.path {
|
||||||
|
row.add_cell(Cell::new(&task.path.to_string_lossy()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add start and end info
|
||||||
|
let (start, end) = formatted_start_end(task, self.settings);
|
||||||
|
if self.start {
|
||||||
|
row.add_cell(Cell::new(start));
|
||||||
|
}
|
||||||
|
if self.end {
|
||||||
|
row.add_cell(Cell::new(end));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue