Merge pull request #320 from mjpieters/cli-color-option

Add --color cli switch
This commit is contained in:
Arne Beer 2022-07-17 14:39:27 +02:00 committed by GitHub
commit 910a3332e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 239 additions and 203 deletions

View file

@ -9,6 +9,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Only style the 'group' header in status output when on a TTY.
## Unreleased
### Added
- Use the new `--color` command-line switch to control when pueue will use colors in its output. The default is `auto`, which means it'll enable colors when connected to a TTY. The other options are `never` and `always`.
## [2.0.4] - 2022-06-05
### Fixed

View file

@ -421,6 +421,13 @@ pub enum GroupCommand {
Remove { name: String },
}
#[derive(Parser, ArgEnum, Debug, Clone, PartialEq)]
pub enum ColorChoice {
Auto,
Never,
Always,
}
#[derive(Parser, ArgEnum, Debug, Clone, PartialEq, Eq)]
pub enum Shell {
Bash,
@ -442,6 +449,10 @@ pub struct CliArguments {
#[clap(short, long, parse(from_occurrences))]
pub verbose: u8,
/// Colorize the output; auto enables color output when connected to a tty.
#[clap(long, arg_enum, default_value = "auto")]
pub color: ColorChoice,
/// Path to a specific pueue config file to use.
/// This ignores all other config files.
#[clap(short, long, value_hint = ValueHint::FilePath)]

View file

@ -1,10 +1,10 @@
use std::env::{current_dir, vars};
use std::io::{self, Write};
use std::io::{self, stdout, Write};
use std::{borrow::Cow, collections::HashMap};
use anyhow::{bail, Context, Result};
use clap::crate_version;
use colors::Colors;
use crossterm::tty::IsTty;
use log::error;
use pueue_lib::network::message::*;
@ -13,7 +13,7 @@ use pueue_lib::network::secret::read_shared_secret;
use pueue_lib::settings::Settings;
use pueue_lib::state::PUEUE_DEFAULT_GROUP;
use crate::cli::{CliArguments, GroupCommand, SubCommand};
use crate::cli::{CliArguments, ColorChoice, GroupCommand, SubCommand};
use crate::commands::*;
use crate::display::*;
@ -27,7 +27,7 @@ use crate::display::*;
pub struct Client {
subcommand: SubCommand,
settings: Settings,
colors: Colors,
style: OutputStyle,
stream: GenericStream,
}
@ -96,7 +96,12 @@ impl Client {
);
}
let colors = Colors::new(&settings);
let style_enabled = match opt.color {
ColorChoice::Auto => stdout().is_tty(),
ColorChoice::Always => true,
ColorChoice::Never => false,
};
let style = OutputStyle::new(&settings, style_enabled);
// If no subcommand is given, we default to the `status` subcommand without any arguments.
let subcommand = if let Some(subcommand) = opt.cmd {
@ -110,7 +115,7 @@ impl Client {
Ok(Client {
settings,
colors,
style,
stream,
subcommand,
})
@ -180,7 +185,7 @@ impl Client {
&group,
*all,
*quiet,
&self.colors,
&self.style,
)
.await?;
Ok(true)
@ -232,7 +237,7 @@ impl Client {
format_state(
&mut self.stream,
&self.subcommand,
&self.colors,
&self.style,
&self.settings,
)
.await?;
@ -274,25 +279,19 @@ impl Client {
/// and handle messages from the daemon. Otherwise the client will simply exit.
fn handle_response(&self, message: Message) -> bool {
match message {
Message::Success(text) => print_success(&self.colors, &text),
Message::Success(text) => print_success(&self.style, &text),
Message::Failure(text) => {
print_error(&self.colors, &text);
print_error(&self.style, &text);
std::process::exit(1);
}
Message::StatusResponse(state) => {
let tasks = state.tasks.iter().map(|(_, task)| task.clone()).collect();
print_state(
*state,
tasks,
&self.subcommand,
&self.colors,
&self.settings,
)
print_state(*state, tasks, &self.subcommand, &self.style, &self.settings)
}
Message::LogResponse(task_logs) => {
print_logs(task_logs, &self.subcommand, &self.colors, &self.settings)
print_logs(task_logs, &self.subcommand, &self.style, &self.settings)
}
Message::GroupResponse(groups) => print_groups(groups, &self.colors),
Message::GroupResponse(groups) => print_groups(groups, &self.style),
Message::Stream(text) => {
print!("{}", text);
io::stdout().flush().unwrap();

View file

@ -9,7 +9,7 @@ use pueue_lib::{network::protocol::GenericStream, settings::Settings, task::Task
use crate::{
cli::SubCommand,
display::{colors::Colors, print_state},
display::{print_state, OutputStyle},
};
/// This function tries to read a map or list of JSON serialized [Task]s from `stdin`.
@ -18,7 +18,7 @@ use crate::{
pub async fn format_state(
stream: &mut GenericStream,
command: &SubCommand,
colors: &Colors,
style: &OutputStyle,
settings: &Settings,
) -> Result<()> {
// Read the raw input to a buffer
@ -45,7 +45,7 @@ pub async fn format_state(
.await
.context("Failed to get the current state from daemon")?;
print_state(state, tasks, command, colors, settings);
print_state(state, tasks, command, style, settings);
Ok(())
}

View file

@ -9,8 +9,7 @@ use crossterm::style::{Attribute, Color};
use pueue_lib::network::protocol::GenericStream;
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use crate::display::helper::style_text;
use crate::{commands::get_state, display::colors::Colors};
use crate::{commands::get_state, display::OutputStyle};
/// Wait until tasks are done.
/// Tasks can be specified by:
@ -27,7 +26,7 @@ pub async fn wait(
group: &str,
all: bool,
quiet: bool,
colors: &Colors,
style: &OutputStyle,
) -> Result<()> {
let mut first_run = true;
// Create a list of tracked tasks.
@ -75,9 +74,9 @@ pub async fn wait(
None => {
// Add any unknown tasks to our watchlist
if !quiet {
let color = get_color_for_status(&task.status, colors);
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let status = style_text(&task.status, Some(color), None);
let color = get_color_for_status(&task.status);
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let status = style.style_text(&task.status, Some(color), None);
if !first_run {
// Don't log non-active tasks in the initial loop.
@ -105,7 +104,7 @@ pub async fn wait(
// Update the (previous) task status and log any changes
watched_tasks.insert(task.id, task.status.clone());
if !quiet {
log_status_change(&current_time, previous_status, task, colors);
log_status_change(&current_time, previous_status, task, style);
}
}
@ -132,40 +131,40 @@ fn log_status_change(
current_time: &str,
previous_status: TaskStatus,
task: &Task,
colors: &Colors,
style: &OutputStyle,
) {
// Finishing tasks get some special handling
if let TaskStatus::Done(result) = &task.status {
let text = match result {
TaskResult::Success => {
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let status = style_text("0", Some(colors.green()), None);
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let status = style.style_text("0", Some(Color::Green), None);
format!("Task {task_id} succeeded with {status}")
}
TaskResult::DependencyFailed => {
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let status = style_text("failed dependencies", Some(colors.red()), None);
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let status = style.style_text("failed dependencies", Some(Color::Red), None);
format!("Task {task_id} failed due to {status}")
}
TaskResult::FailedToSpawn(_) => {
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let status = style_text("failed to spawn", Some(colors.red()), None);
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let status = style.style_text("failed to spawn", Some(Color::Red), None);
format!("Task {task_id} {status}")
}
TaskResult::Failed(exit_code) => {
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let status = style_text(exit_code, Some(colors.red()), Some(Attribute::Bold));
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let status = style.style_text(exit_code, Some(Color::Red), Some(Attribute::Bold));
format!("Task {task_id} failed with {status}")
}
TaskResult::Errored => {
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let status = style_text("IO error", Some(colors.red()), Some(Attribute::Bold));
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let status = style.style_text("IO error", Some(Color::Red), Some(Attribute::Bold));
format!("Task {task_id} experienced an {status}.")
}
TaskResult::Killed => {
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let status = style_text("killed", Some(colors.red()), None);
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let status = style.style_text("killed", Some(Color::Red), None);
format!("Task {task_id} has been {status}")
}
};
@ -173,19 +172,19 @@ fn log_status_change(
return;
}
let new_status_color = get_color_for_status(&task.status, colors);
let previous_status_color = get_color_for_status(&previous_status, colors);
let new_status_color = get_color_for_status(&task.status);
let previous_status_color = get_color_for_status(&previous_status);
let task_id = style_text(task.id, None, Some(Attribute::Bold));
let previous_status = style_text(previous_status, Some(previous_status_color), None);
let new_status = style_text(&task.status, Some(new_status_color), None);
let task_id = style.style_text(task.id, None, Some(Attribute::Bold));
let previous_status = style.style_text(previous_status, Some(previous_status_color), None);
let new_status = style.style_text(&task.status, Some(new_status_color), None);
println!("{current_time} - Task {task_id} changed from {previous_status} to {new_status}",);
}
fn get_color_for_status(task_status: &TaskStatus, colors: &Colors) -> Color {
fn get_color_for_status(task_status: &TaskStatus) -> Color {
match task_status {
TaskStatus::Running | TaskStatus::Done(_) => colors.green(),
TaskStatus::Paused | TaskStatus::Locked => colors.white(),
_ => colors.white(),
TaskStatus::Running | TaskStatus::Done(_) => Color::Green,
TaskStatus::Paused | TaskStatus::Locked => Color::White,
_ => Color::White,
}
}

View file

@ -1,53 +0,0 @@
use crossterm::style::Color;
use pueue_lib::settings::Settings;
/// Color wrapper for actual colors depending on settings
/// Using dark colors if dark_mode is enabled
pub struct Colors {
/// red color
red: Color,
/// green color
green: Color,
/// white color
white: Color,
/// yellow color
yellow: Color,
}
impl Colors {
/// init color-scheme depending on settings
pub const fn new(settings: &Settings) -> Self {
if settings.client.dark_mode {
Self {
green: Color::DarkGreen,
red: Color::DarkRed,
yellow: Color::DarkYellow,
white: Color::White,
}
} else {
Self {
green: Color::Green,
red: Color::Red,
yellow: Color::Yellow,
white: Color::White,
}
}
}
/// return green color
pub const fn green(&self) -> Color {
self.green
}
/// return red color
pub const fn red(&self) -> Color {
self.red
}
/// return yellow color
pub const fn yellow(&self) -> Color {
self.yellow
}
/// return white color
pub const fn white(&self) -> Color {
self.white
}
}

View file

@ -1,12 +1,12 @@
use pueue_lib::network::message::GroupResponseMessage;
use super::{colors::Colors, helper::*};
use super::{helper::*, OutputStyle};
pub fn print_groups(message: GroupResponseMessage, colors: &Colors) {
pub fn print_groups(message: GroupResponseMessage, style: &OutputStyle) {
let mut text = String::new();
let mut group_iter = message.groups.iter().peekable();
while let Some((name, group)) = group_iter.next() {
let styled = get_group_headline(name, group, colors);
let styled = get_group_headline(name, group, style);
text.push_str(&styled);
if group_iter.peek().is_some() {

View file

@ -1,38 +1,11 @@
use std::collections::BTreeMap;
use std::io::stdout;
use crossterm::style::{style, Attribute, Color, Stylize};
use crossterm::tty::IsTty;
use crossterm::style::{Attribute, Color};
use pueue_lib::state::{Group, GroupStatus};
use pueue_lib::task::{Task, TaskStatus};
use super::colors::Colors;
/// This is a simple small helper function with the purpose of easily styling text,
/// while also prevent styling if we're printing to a non-tty output.
/// If there's any kind of styling in the code, it should be done with the help of this function.
pub fn style_text<T: ToString>(
text: T,
color: Option<Color>,
attribute: Option<Attribute>,
) -> String {
let text = text.to_string();
// No tty, we aren't allowed to do any styling
if !stdout().is_tty() {
return text;
}
let mut styled = style(text);
if let Some(color) = color {
styled = styled.with(color);
}
if let Some(attribute) = attribute {
styled = styled.attribute(attribute);
}
styled.to_string()
}
use super::OutputStyle;
/// By default, several columns aren't shown until there's actually some data to display.
/// This function determines, which of those columns actually need to be shown.
@ -57,14 +30,14 @@ pub fn has_special_columns(tasks: &[Task]) -> (bool, bool, bool) {
}
/// Return a nicely formatted headline that's displayed above group tables
pub fn get_group_headline(name: &str, group: &Group, colors: &Colors) -> String {
pub fn get_group_headline(name: &str, group: &Group, style: &OutputStyle) -> String {
// Style group name
let name = style_text(format!("Group \"{}\"", name), None, Some(Attribute::Bold));
let name = style.style_text(format!("Group \"{}\"", name), None, Some(Attribute::Bold));
// Print the current state of the group.
let status = match group.status {
GroupStatus::Running => style_text("running", Some(colors.green()), None),
GroupStatus::Paused => style_text("paused", Some(colors.yellow()), None),
GroupStatus::Running => style.style_text("running", Some(Color::Green), None),
GroupStatus::Paused => style.style_text("paused", Some(Color::Yellow), None),
};
format!("{} ({} parallel): {}", name, group.parallel_tasks, status)

View file

@ -6,11 +6,16 @@ use comfy_table::*;
use pueue_lib::log::{get_log_file_handle, seek_to_last_lines};
use pueue_lib::settings::Settings;
use crate::display::{colors::Colors, helper::*};
use crate::display::OutputStyle;
/// The daemon didn't send any log output, thereby we didn't request any.
/// If that's the case, read the log file from the local pueue directory.
pub fn print_local_log(task_id: usize, colors: &Colors, settings: &Settings, lines: Option<usize>) {
pub fn print_local_log(
task_id: usize,
style: &OutputStyle,
settings: &Settings,
lines: Option<usize>,
) {
let mut file = match get_log_file_handle(task_id, &settings.shared.pueue_directory()) {
Ok(file) => file,
Err(err) => {
@ -26,7 +31,7 @@ pub fn print_local_log(task_id: usize, colors: &Colors, settings: &Settings, lin
&mut stdout,
&mut file,
&lines,
style_text("output:", Some(colors.green()), Some(Attribute::Bold)),
style.style_text("output:", Some(Color::Green), Some(Attribute::Bold)),
);
}

View file

@ -6,7 +6,7 @@ use pueue_lib::network::message::TaskLogMessage;
use pueue_lib::settings::Settings;
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use super::colors::Colors;
use super::OutputStyle;
use crate::cli::SubCommand;
mod json;
@ -43,7 +43,7 @@ pub fn determine_log_line_amount(full: bool, lines: &Option<usize>) -> Option<us
pub fn print_logs(
mut task_logs: BTreeMap<usize, TaskLogMessage>,
cli_command: &SubCommand,
colors: &Colors,
style: &OutputStyle,
settings: &Settings,
) {
// Get actual commandline options.
@ -80,7 +80,7 @@ pub fn print_logs(
// Iterate over each task and print the respective log.
let mut task_iter = task_logs.iter_mut().peekable();
while let Some((_, task_log)) = task_iter.next() {
print_log(task_log, colors, settings, lines);
print_log(task_log, style, settings, lines);
// Add a newline if there is another task that's going to be printed.
if let Some((_, task_log)) = task_iter.peek() {
@ -103,7 +103,7 @@ pub fn print_logs(
/// This is only important, if we read local lines.
fn print_log(
message: &mut TaskLogMessage,
colors: &Colors,
style: &OutputStyle,
settings: &Settings,
lines: Option<usize>,
) {
@ -116,38 +116,38 @@ fn print_log(
return;
}
print_task_info(task, colors);
print_task_info(task, style);
if settings.client.read_local_logs {
print_local_log(message.task.id, colors, settings, lines);
print_local_log(message.task.id, style, settings, lines);
} else if message.output.is_some() {
print_remote_log(message, colors);
print_remote_log(message, style);
} else {
println!("Logs requested from pueue daemon, but none received. Please report this bug.");
}
}
/// Print some information about a task, which is displayed on top of the task's log output.
fn print_task_info(task: &Task, colors: &Colors) {
fn print_task_info(task: &Task, style: &OutputStyle) {
// Print task id and exit code.
let task_cell = Cell::new(format!("Task {}: ", task.id)).add_attribute(Attribute::Bold);
let task_cell = style.styled_cell(format!("Task {}: ", task.id), None, Some(Attribute::Bold));
let (exit_status, color) = match &task.status {
TaskStatus::Paused => ("paused".into(), colors.white()),
TaskStatus::Running => ("running".into(), colors.yellow()),
TaskStatus::Paused => ("paused".into(), Color::White),
TaskStatus::Running => ("running".into(), Color::Yellow),
TaskStatus::Done(result) => match result {
TaskResult::Success => ("completed successfully".into(), colors.green()),
TaskResult::Success => ("completed successfully".into(), Color::Green),
TaskResult::Failed(exit_code) => {
(format!("failed with exit code {}", exit_code), colors.red())
(format!("failed with exit code {}", exit_code), Color::Red)
}
TaskResult::FailedToSpawn(err) => (format!("failed to spawn: {}", err), colors.red()),
TaskResult::Killed => ("killed by system or user".into(), colors.red()),
TaskResult::Errored => ("some IO error.\n Check daemon log.".into(), colors.red()),
TaskResult::DependencyFailed => ("dependency failed".into(), colors.red()),
TaskResult::FailedToSpawn(err) => (format!("failed to spawn: {}", err), Color::Red),
TaskResult::Killed => ("killed by system or user".into(), Color::Red),
TaskResult::Errored => ("some IO error.\n Check daemon log.".into(), Color::Red),
TaskResult::DependencyFailed => ("dependency failed".into(), Color::Red),
},
_ => (task.status.to_string(), colors.white()),
_ => (task.status.to_string(), Color::White),
};
let status_cell = Cell::new(exit_status).fg(color);
let status_cell = style.styled_cell(exit_status, Some(color), None);
// The styling of the task number and status is done by a single-row table.
let mut table = Table::new();
@ -163,24 +163,24 @@ fn print_task_info(task: &Task, colors: &Colors) {
// Command and path
table.add_row(vec![
Cell::new("Command:").add_attribute(Attribute::Bold),
style.styled_cell("Command:", None, Some(Attribute::Bold)),
Cell::new(&task.command),
]);
table.add_row(vec![
Cell::new("Path:").add_attribute(Attribute::Bold),
style.styled_cell("Path:", None, Some(Attribute::Bold)),
Cell::new(&task.path.to_string_lossy()),
]);
// Start and end time
if let Some(start) = task.start {
table.add_row(vec![
Cell::new("Start:").add_attribute(Attribute::Bold),
style.styled_cell("Start:", None, Some(Attribute::Bold)),
Cell::new(start.to_rfc2822()),
]);
}
if let Some(end) = task.end {
table.add_row(vec![
Cell::new("End:").add_attribute(Attribute::Bold),
style.styled_cell("End:", None, Some(Attribute::Bold)),
Cell::new(end.to_rfc2822()),
]);
}

View file

@ -6,15 +6,15 @@ use snap::read::FrameDecoder;
use pueue_lib::network::message::TaskLogMessage;
use crate::display::{colors::Colors, helper::*};
use super::OutputStyle;
/// Prints log output received from the daemon.
/// We can safely call .unwrap() on output in here, since this
/// branch is always called after ensuring that it is `Some`.
pub fn print_remote_log(task_log: &TaskLogMessage, colors: &Colors) {
pub fn print_remote_log(task_log: &TaskLogMessage, style: &OutputStyle) {
if let Some(bytes) = task_log.output.as_ref() {
if !bytes.is_empty() {
let header = style_text("output: ", Some(colors.green()), Some(Attribute::Bold));
let header = style.style_text("output: ", Some(Color::Green), Some(Attribute::Bold));
println!("\n{header}",);
if let Err(err) = decompress_and_print_remote_log(bytes) {

View file

@ -1,25 +1,26 @@
pub mod colors;
mod follow;
mod group;
pub mod helper;
mod log;
mod state;
pub mod style;
use self::{colors::Colors, helper::style_text};
use crossterm::style::Color;
// Re-exports
pub use self::follow::follow_local_task_logs;
pub use self::group::print_groups;
pub use self::log::{determine_log_line_amount, print_logs};
pub use self::state::print_state;
pub use self::style::OutputStyle;
/// Used to style any generic success message from the daemon.
pub fn print_success(_colors: &Colors, message: &str) {
pub fn print_success(_style: &OutputStyle, message: &str) {
println!("{}", message);
}
/// Used to style any generic failure message from the daemon.
pub fn print_error(colors: &Colors, message: &str) {
let styled = style_text(message, Some(colors.red()), None);
pub fn print_error(style: &OutputStyle, message: &str) {
let styled = style.style_text(message, Some(Color::Red), None);
println!("{}", styled);
}

View file

@ -8,7 +8,7 @@ use pueue_lib::settings::Settings;
use pueue_lib::state::{State, PUEUE_DEFAULT_GROUP};
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use super::{colors::Colors, helper::*};
use super::{helper::*, OutputStyle};
use crate::cli::SubCommand;
/// Print the current state of the daemon in a nicely formatted table.
@ -18,7 +18,7 @@ pub fn print_state(
state: State,
tasks: Vec<Task>,
cli_command: &SubCommand,
colors: &Colors,
style: &OutputStyle,
settings: &Settings,
) {
let (json, group_only) = match cli_command {
@ -34,18 +34,18 @@ pub fn print_state(
}
if let Some(group) = group_only {
print_single_group(state, tasks, settings, colors, group);
print_single_group(state, tasks, settings, style, group);
return;
}
print_all_groups(state, tasks, settings, colors);
print_all_groups(state, tasks, settings, style);
}
fn print_single_group(
state: State,
tasks: Vec<Task>,
settings: &Settings,
colors: &Colors,
style: &OutputStyle,
group_name: String,
) {
// Sort all tasks by their respective group;
@ -60,7 +60,7 @@ fn print_single_group(
// Only a single group is requested. Print that group and return.
let tasks = sorted_tasks.entry(group_name.clone()).or_default();
let headline = get_group_headline(&group_name, group, colors);
let headline = get_group_headline(&group_name, group, style);
println!("{headline}");
// Show a message if the requested group doesn't have any tasks.
@ -68,10 +68,10 @@ fn print_single_group(
println!("Task list is empty. Add tasks with `pueue add -g {group_name} -- [cmd]`");
return;
}
print_table(tasks, colors, settings);
print_table(tasks, style, settings);
}
fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, colors: &Colors) {
fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, style: &OutputStyle) {
// 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.
@ -79,7 +79,7 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, colors:
let headline = get_group_headline(
PUEUE_DEFAULT_GROUP,
state.groups.get(PUEUE_DEFAULT_GROUP).unwrap(),
colors,
style,
);
println!("{headline}\n");
println!("Task list is empty. Add tasks with `pueue add -- [cmd]`");
@ -95,10 +95,10 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, colors:
let headline = get_group_headline(
PUEUE_DEFAULT_GROUP,
state.groups.get(PUEUE_DEFAULT_GROUP).unwrap(),
colors,
style,
);
println!("{headline}");
print_table(tasks, colors, settings);
print_table(tasks, style, settings);
// Add a newline if there are further groups to be printed
if sorted_tasks.len() > 1 {
@ -115,9 +115,9 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, colors:
continue;
}
let headline = get_group_headline(group, state.groups.get(group).unwrap(), colors);
let headline = get_group_headline(group, state.groups.get(group).unwrap(), style);
println!("{headline}");
print_table(tasks, colors, settings);
print_table(tasks, style, settings);
// Add a newline between groups
if sorted_iter.peek().is_some() {
@ -127,7 +127,7 @@ fn print_all_groups(state: State, tasks: Vec<Task>, settings: &Settings, colors:
}
/// Print some tasks into a nicely formatted table
fn print_table(tasks: &[Task], colors: &Colors, settings: &Settings) {
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
@ -163,23 +163,23 @@ fn print_table(tasks: &[Task], colors: &Colors, settings: &Settings) {
if let Some(height) = settings.client.max_status_lines {
row.max_height(height);
}
row.add_cell(Cell::new(&task.id.to_string()));
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, colors.green()),
TaskStatus::Paused | TaskStatus::Locked => (status_string, colors.white()),
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(), colors.green()),
TaskResult::DependencyFailed => ("Dependency failed".to_string(), colors.red()),
TaskResult::FailedToSpawn(_) => ("Failed to spawn".to_string(), colors.red()),
TaskResult::Failed(code) => (format!("Failed ({code})"), colors.red()),
_ => (result.to_string(), colors.red()),
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, colors.yellow()),
_ => (status_string, Color::Yellow),
};
row.add_cell(Cell::new(status_text).fg(color));
row.add_cell(style.styled_cell(status_text, Some(color), None));
if has_delayed_tasks {
if let TaskStatus::Stashed {
@ -211,11 +211,7 @@ fn print_table(tasks: &[Task], colors: &Colors, settings: &Settings) {
}
if has_labels {
if let Some(label) = &task.label {
row.add_cell(label.into());
} else {
row.add_cell(Cell::new(""));
}
row.add_cell(Cell::new(&task.label.as_deref().unwrap_or_default()));
}
// Add command and path.

99
client/display/style.rs Normal file
View file

@ -0,0 +1,99 @@
use pueue_lib::settings::Settings;
use comfy_table::Cell;
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
pub struct OutputStyle {
/// whether or not ANSI styling is enabled
enabled: bool,
/// red color
red: Color,
/// green color
green: Color,
/// yellow color
yellow: Color,
}
impl OutputStyle {
/// init color-scheme depending on settings
pub const fn new(settings: &Settings, enabled: bool) -> Self {
if settings.client.dark_mode {
Self {
enabled,
green: Color::DarkGreen,
red: Color::DarkRed,
yellow: Color::DarkYellow,
}
} else {
Self {
enabled,
green: Color::Green,
red: Color::Red,
yellow: Color::Yellow,
}
}
}
fn map_color(&self, color: Color) -> Color {
match color {
Color::Green => self.green,
Color::Red => self.red,
Color::Yellow => self.yellow,
_ => color,
}
}
/// This is a helper method with the purpose of easily styling text,
/// while also prevent styling if we're printing to a non-tty output.
/// If there's any kind of styling in the code, it should be done with the help of this method.
pub fn style_text<T: ToString>(
&self,
text: T,
color: Option<Color>,
attribute: Option<Attribute>,
) -> String {
let text = text.to_string();
// Styling disabled
if !self.enabled {
return text;
}
let mut styled = style(text);
if let Some(color) = color {
styled = styled.with(self.map_color(color));
}
if let Some(attribute) = attribute {
styled = styled.attribute(attribute);
}
styled.to_string()
}
/// A helper method to produce styled Comfy-table cells.
/// Use this anywhere you need to create Comfy-table cells, so that the correct
/// colors are used depending on the current color mode and dark-mode preset.
pub fn styled_cell<T: ToString>(
&self,
text: T,
color: Option<Color>,
attribute: Option<Attribute>,
) -> Cell {
let mut cell = Cell::new(text.to_string());
// Styling disabled
if !self.enabled {
return cell;
}
if let Some(color) = color {
cell = cell.fg(self.map_color(color));
}
if let Some(attribute) = attribute {
cell = cell.add_attribute(attribute);
}
cell
}
}