1
0
mirror of https://github.com/nukesor/pueue synced 2024-07-08 20:06:21 +00:00

refactor: Add TableBuilder

This commit is contained in:
Arne Beer 2022-08-28 17:33:02 +02:00
parent b0e6c35ebc
commit 196d5bec09
No known key found for this signature in database
GPG Key ID: CC9408F679023B65
6 changed files with 311 additions and 216 deletions

24
Cargo.lock generated
View File

@ -885,9 +885,9 @@ dependencies = [
[[package]]
name = "pest"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4"
checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048"
dependencies = [
"thiserror",
"ucd-trie",
@ -895,9 +895,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91"
checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1"
dependencies = [
"pest",
"pest_generator",
@ -905,9 +905,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad"
checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c"
dependencies = [
"pest",
"pest_meta",
@ -918,13 +918,13 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04"
checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6"
dependencies = [
"once_cell",
"pest",
"sha-1",
"sha1",
]
[[package]]
@ -1425,10 +1425,10 @@ dependencies = [
]
[[package]]
name = "sha-1"
version = "0.10.0"
name = "sha1"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549"
dependencies = [
"cfg-if",
"cpufeatures",

View File

@ -1,30 +1,8 @@
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`.
///
/// 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)
}
use pueue_lib::{settings::Settings, task::Task};
/// Sort given tasks by their groups.
/// 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
}
/// 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)
}

View File

@ -8,6 +8,7 @@ pub mod helper;
mod log;
mod state;
pub mod style;
mod table_builder;
use crossterm::style::Color;

View File

@ -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::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::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.
/// This allows us to print the tasks in any user-defined order.
pub fn print_state(
pub fn print_state<'a>(
state: State,
tasks: Vec<Task>,
cli_command: &SubCommand,
style: &OutputStyle,
settings: &Settings,
style: &'a OutputStyle,
settings: &'a Settings,
) {
let (json, group_only) = match cli_command {
SubCommand::Status { json, group } => (*json, group.clone()),
@ -36,12 +30,14 @@ pub fn print_state(
return;
}
let table_builder = TableBuilder::new(settings, style);
if let Some(group) = group_only {
print_single_group(state, tasks, settings, style, group);
print_single_group(state, tasks, style, group, table_builder);
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.
@ -50,9 +46,9 @@ pub fn print_state(
fn print_single_group(
state: State,
tasks: Vec<Task>,
settings: &Settings,
style: &OutputStyle,
group_name: String,
table_builder: TableBuilder,
) {
// Sort all tasks by their respective group;
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]`");
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.
///
/// 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
// Print the state of the default group anyway, since this is information one wants to
// see most of the time anyway.
@ -107,7 +110,8 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, style:
style,
);
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
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);
println!("{headline}");
print_table(tasks, style, settings);
let table = table_builder.clone().build(tasks);
println!("{table}");
// Add a newline between groups
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)
}

View File

@ -6,6 +6,7 @@ use crossterm::style::{style, Attribute, Color, Stylize};
/// 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.
/// - Using dark colors if dark_mode is enabled
#[derive(Debug, Clone)]
pub struct OutputStyle {
/// whether or not ANSI styling is enabled
pub enabled: bool,

View 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
}
}