client: add support dark colors via settings file (dark_mode variable)

This commit adds the ability to choose dark colors instead of regular colors. It may
was  useful  for solarized-like themes (green seems weird). By default
the client is using dark_mode = false.

Closes #171
This commit is contained in:
Maxim Zhukov 2021-02-06 14:19:55 +03:00
parent 755978a721
commit ea12318991
No known key found for this signature in database
GPG key ID: 7146F5C513417782
11 changed files with 173 additions and 76 deletions

View file

@ -15,6 +15,7 @@ Managing two crates in a single repository in combination with `cargo release` t
### Added
- `--all-failed` flag for `restart`. This will restart all tasks that didn't finish with a `Success` status.
- New config option `client.dark_mode` by [Mephistophiles](https://github.com/Mephistophiles). Default: `false`. Adds the ability to switch to dark colors instead of regular colors.
### Changed

4
Cargo.lock generated
View file

@ -1181,9 +1181,9 @@ dependencies = [
[[package]]
name = "pueue-lib"
version = "0.12.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fbde4949922bc8e6e783f66fbd322ab9f68fa4aa6e2c97ef9d19b9b20f6cbef"
checksum = "efaf3d7c6ca2958d334db59e981dcac98d2c893f681876b05c5e95b671288933"
dependencies = [
"anyhow",
"async-std",

View file

@ -23,7 +23,7 @@ name = "pueued"
path = "daemon/main.rs"
[dependencies]
pueue-lib = "0.12"
pueue-lib = "0.12.1"
#pueue-lib = { path = "../pueue-lib" }
anyhow = "1"

View file

@ -3,6 +3,7 @@ use std::io::{self, Write};
use std::{borrow::Cow, collections::HashMap};
use anyhow::{bail, Context, Result};
use colors::Colors;
use log::error;
use pueue_lib::network::message::*;
@ -28,6 +29,7 @@ use crate::display::*;
pub struct Client {
opt: CliArguments,
settings: Settings,
colors: Colors,
stream: GenericStream,
}
@ -50,9 +52,12 @@ impl Client {
bail!("Daemon went away after initial connection. Did you use the correct secret?")
}
let colors = Colors::new(&settings);
Ok(Client {
opt,
settings,
colors,
stream,
})
}
@ -115,7 +120,15 @@ impl Client {
quiet,
} => {
let group = group_or_default(group);
wait(&mut self.stream, task_ids, &group, *all, *quiet).await?;
wait(
&mut self.stream,
task_ids,
&group,
*all,
*quiet,
&self.colors,
)
.await?;
Ok(true)
}
SubCommand::Restart {
@ -193,14 +206,18 @@ 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(&text),
Message::Success(text) => print_success(&self.colors, &text),
Message::Failure(text) => {
print_error(&text);
print_error(&self.colors, &text);
std::process::exit(1);
}
Message::StatusResponse(state) => print_state(*state, &self.opt.cmd, &self.settings),
Message::LogResponse(task_logs) => print_logs(task_logs, &self.opt.cmd, &self.settings),
Message::GroupResponse(groups) => print_groups(groups),
Message::StatusResponse(state) => {
print_state(*state, &self.opt.cmd, &self.colors, &self.settings)
}
Message::LogResponse(task_logs) => {
print_logs(task_logs, &self.opt.cmd, &self.colors, &self.settings)
}
Message::GroupResponse(groups) => print_groups(groups, &self.colors),
Message::Stream(text) => {
print!("{}", text);
io::stdout().flush().unwrap();

View file

@ -9,8 +9,8 @@ use crossterm::style::{Attribute, Color};
use pueue_lib::network::protocol::GenericStream;
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use crate::commands::get_state;
use crate::display::helper::style_text;
use crate::{commands::get_state, display::colors::Colors};
/// Wait until tasks are done.
/// Tasks can be specified by:
@ -27,6 +27,7 @@ pub async fn wait(
group: &str,
all: bool,
quiet: bool,
colors: &Colors,
) -> Result<()> {
let mut first_run = true;
// Create a list of tracked tasks.
@ -68,7 +69,7 @@ pub async fn wait(
// Add any unknown tasks to our watchlist
// Don't log anything if this is the first run
if !quiet && !first_run {
let color = get_color_for_status(&task.status);
let color = get_color_for_status(&task.status, colors);
println!(
"{} - New task {} with status {}",
current_time,
@ -91,7 +92,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);
log_status_change(&current_time, previous_status, &task, colors);
}
}
@ -112,7 +113,12 @@ pub async fn wait(
Ok(())
}
fn log_status_change(current_time: &str, previous_status: TaskStatus, task: &Task) {
fn log_status_change(
current_time: &str,
previous_status: TaskStatus,
task: &Task,
colors: &Colors,
) {
// Finishing tasks get some special handling
if task.status == TaskStatus::Done {
let text = match task.result {
@ -120,14 +126,14 @@ fn log_status_change(current_time: &str, previous_status: TaskStatus, task: &Tas
format!(
"Task {} succeeded with {}",
style_text(task.id, None, Some(Attribute::Bold)),
style_text("0", Some(Color::Green), None)
style_text("0", Some(colors.green()), None)
)
}
Some(TaskResult::DependencyFailed) => {
format!(
"Task {} failed due to {}",
style_text(task.id, None, Some(Attribute::Bold)),
style_text("failed dependencies", Some(Color::Red), None)
style_text("failed dependencies", Some(colors.red()), None)
)
}
@ -135,28 +141,28 @@ fn log_status_change(current_time: &str, previous_status: TaskStatus, task: &Tas
format!(
"Task {} {}",
style_text(task.id, None, Some(Attribute::Bold)),
style_text("failed to spawn", Some(Color::Red), None)
style_text("failed to spawn", Some(colors.red()), None)
)
}
Some(TaskResult::Failed(exit_code)) => {
format!(
"Task {} failed with {}",
style_text(task.id, None, Some(Attribute::Bold)),
style_text(exit_code, Some(Color::Red), Some(Attribute::Bold))
style_text(exit_code, Some(colors.red()), Some(Attribute::Bold))
)
}
Some(TaskResult::Errored) => {
format!(
"Task {} experienced an {}.",
style_text(task.id, None, Some(Attribute::Bold)),
style_text("IO error", Some(Color::Red), Some(Attribute::Bold))
style_text("IO error", Some(colors.red()), Some(Attribute::Bold))
)
}
Some(TaskResult::Killed) => {
format!(
"Task {} has been {}",
style_text(task.id, None, Some(Attribute::Bold)),
style_text("killed", Some(Color::Red), None)
style_text("killed", Some(colors.red()), None)
)
}
None => panic!("Got a 'Done' task without a task result. Please report this bug."),
@ -165,8 +171,8 @@ fn log_status_change(current_time: &str, previous_status: TaskStatus, task: &Tas
return;
}
let new_status_color = get_color_for_status(&task.status);
let previous_status_color = get_color_for_status(&previous_status);
let new_status_color = get_color_for_status(&task.status, colors);
let previous_status_color = get_color_for_status(&previous_status, colors);
println!(
"{} - Task {} changed from {} to {}",
@ -177,10 +183,10 @@ fn log_status_change(current_time: &str, previous_status: TaskStatus, task: &Tas
);
}
fn get_color_for_status(task_status: &TaskStatus) -> Color {
fn get_color_for_status(task_status: &TaskStatus, colors: &Colors) -> Color {
match task_status {
TaskStatus::Running | TaskStatus::Done => Color::Green,
TaskStatus::Paused | TaskStatus::Locked => Color::White,
_ => Color::Yellow,
TaskStatus::Running | TaskStatus::Done => colors.green(),
TaskStatus::Paused | TaskStatus::Locked => colors.white(),
_ => colors.white(),
}
}

53
client/display/colors.rs Normal file
View file

@ -0,0 +1,53 @@
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,13 +1,13 @@
use pueue_lib::network::message::GroupResponseMessage;
use super::helper::*;
use super::{colors::Colors, helper::*};
pub fn print_groups(message: GroupResponseMessage) {
pub fn print_groups(message: GroupResponseMessage, colors: &Colors) {
let mut text = String::new();
let mut group_iter = message.groups.iter().peekable();
while let Some((name, status)) = group_iter.next() {
let parallel = *message.settings.get(name).unwrap();
let styled = get_group_headline(name, &status, parallel);
let styled = get_group_headline(name, &status, parallel, colors);
text.push_str(&styled);
if group_iter.peek().is_some() {

View file

@ -7,6 +7,8 @@ use crossterm::tty::IsTty;
use pueue_lib::state::GroupStatus;
use pueue_lib::task::Task;
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.
@ -50,14 +52,19 @@ pub fn has_special_columns(tasks: &BTreeMap<usize, Task>) -> (bool, bool, bool)
}
/// Return a nicely formatted headline that's displayed above group tables
pub fn get_group_headline(name: &str, status: &GroupStatus, parallel: usize) -> String {
pub fn get_group_headline(
name: &str,
status: &GroupStatus,
parallel: usize,
colors: &Colors,
) -> String {
// Style group name
let name = style(format!("Group \"{}\"", name)).attribute(Attribute::Bold);
// Print the current state of the group.
let status = match status {
GroupStatus::Running => style_text("running", Some(Color::Green), None),
GroupStatus::Paused => style_text("paused", Some(Color::Yellow), None),
GroupStatus::Running => style_text("running", Some(colors.green()), None),
GroupStatus::Paused => style_text("paused", Some(colors.yellow()), None),
};
format!("{} ({} parallel): {}", name, parallel, status)

View file

@ -11,7 +11,7 @@ use pueue_lib::network::message::TaskLogMessage;
use pueue_lib::settings::Settings;
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use super::helper::*;
use super::{colors::Colors, helper::*};
use crate::cli::SubCommand;
/// Print the log ouput of finished tasks.
@ -20,6 +20,7 @@ use crate::cli::SubCommand;
pub fn print_logs(
mut task_logs: BTreeMap<usize, TaskLogMessage>,
cli_command: &SubCommand,
colors: &Colors,
settings: &Settings,
) {
// Get actual commandline options.
@ -75,7 +76,7 @@ pub fn print_logs(
// Do the actual log printing
let mut task_iter = task_logs.iter_mut().peekable();
while let Some((_, mut task_log)) = task_iter.next() {
print_log(&mut task_log, settings, lines);
print_log(&mut task_log, colors, settings, lines);
// Add a newline if there is another task that's going to be printed.
if let Some((_, task_log)) = task_iter.peek() {
@ -95,42 +96,47 @@ pub fn print_logs(
/// lines: Whether we should reduce the log output of each task to a specific number of lines.
/// `None` implicates that everything should be printed.
/// This is only important, if we read local lines.
pub fn print_log(message: &mut TaskLogMessage, settings: &Settings, lines: Option<usize>) {
pub fn print_log(
message: &mut TaskLogMessage,
colors: &Colors,
settings: &Settings,
lines: Option<usize>,
) {
let task = &message.task;
// We only show logs of finished or running tasks.
if !vec![TaskStatus::Done, TaskStatus::Running, TaskStatus::Paused].contains(&task.status) {
return;
}
print_task_info(task);
print_task_info(task, colors);
if settings.client.read_local_logs {
print_local_log(message.task.id, settings, lines);
print_local_log(message.task.id, colors, settings, lines);
} else if message.stdout.is_some() && message.stderr.is_some() {
print_remote_log(message);
print_remote_log(message, colors);
} 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.
pub fn print_task_info(task: &Task) {
fn print_task_info(task: &Task, colors: &Colors) {
// Print task id and exit code.
let task_cell = Cell::new(format!("Task {}: ", task.id)).add_attribute(Attribute::Bold);
let (exit_status, color) = match &task.result {
Some(TaskResult::Success) => ("completed successfully".into(), Color::Green),
Some(TaskResult::Success) => ("completed successfully".into(), colors.green()),
Some(TaskResult::Failed(exit_code)) => {
(format!("failed with exit code {}", exit_code), Color::Red)
(format!("failed with exit code {}", exit_code), colors.red())
}
Some(TaskResult::FailedToSpawn(err)) => (format!("failed to spawn: {}", err), Color::Red),
Some(TaskResult::Killed) => ("killed by system or user".into(), Color::Red),
Some(TaskResult::Errored) => ("some IO error.\n Check daemon log.".into(), Color::Red),
Some(TaskResult::DependencyFailed) => ("dependency failed".into(), Color::Red),
Some(TaskResult::FailedToSpawn(err)) => (format!("failed to spawn: {}", err), colors.red()),
Some(TaskResult::Killed) => ("killed by system or user".into(), colors.red()),
Some(TaskResult::Errored) => ("some IO error.\n Check daemon log.".into(), colors.red()),
Some(TaskResult::DependencyFailed) => ("dependency failed".into(), colors.red()),
None => match &task.status {
TaskStatus::Paused => ("paused".into(), Color::White),
TaskStatus::Running => ("running".into(), Color::Yellow),
_ => (task.status.to_string(), Color::White),
TaskStatus::Paused => ("paused".into(), colors.white()),
TaskStatus::Running => ("running".into(), colors.yellow()),
_ => (task.status.to_string(), colors.white()),
},
};
let status_cell = Cell::new(exit_status).fg(color);
@ -181,7 +187,7 @@ pub fn print_task_info(task: &Task) {
/// The daemon didn't send any log output, thereby we didn't request any.
/// If that's the case, read the log files from the local pueue directory
pub fn print_local_log(task_id: usize, settings: &Settings, lines: Option<usize>) {
fn print_local_log(task_id: usize, colors: &Colors, settings: &Settings, lines: Option<usize>) {
let (mut stdout_file, mut stderr_file) =
match get_log_file_handles(task_id, &settings.shared.pueue_directory) {
Ok((stdout, stderr)) => (stdout, stderr),
@ -198,14 +204,14 @@ pub fn print_local_log(task_id: usize, settings: &Settings, lines: Option<usize>
&mut stdout,
&mut stdout_file,
&lines,
style_text("stdout:", Some(Color::Green), Some(Attribute::Bold)),
style_text("stdout:", Some(colors.green()), Some(Attribute::Bold)),
);
print_local_file(
&mut stdout,
&mut stderr_file,
&lines,
style_text("stderr:", Some(Color::Red), Some(Attribute::Bold)),
style_text("stderr:", Some(colors.red()), Some(Attribute::Bold)),
);
}
@ -234,27 +240,31 @@ pub fn print_local_file(stdout: &mut Stdout, file: &mut File, lines: &Option<usi
/// Prints log output received from the daemon.
/// We can safely call .unwrap() on stdout and stderr in here, since this
/// branch is always called after ensuring that both are `Some`.
pub fn print_remote_log(task_log: &TaskLogMessage) {
pub fn print_remote_log(task_log: &TaskLogMessage, colors: &Colors) {
// Save whether stdout was printed, so we can add a newline between outputs.
if !task_log.stdout.as_ref().unwrap().is_empty() {
if let Err(err) = print_remote_task_log(&task_log, true) {
if let Err(err) = print_remote_task_log(&task_log, colors, true) {
println!("Error while parsing stdout: {}", err);
}
}
if !task_log.stderr.as_ref().unwrap().is_empty() {
if let Err(err) = print_remote_task_log(&task_log, false) {
if let Err(err) = print_remote_task_log(&task_log, colors, false) {
println!("Error while parsing stderr: {}", err);
};
}
}
/// Print log output of a finished process.
pub fn print_remote_task_log(task_log: &TaskLogMessage, stdout: bool) -> Result<()> {
fn print_remote_task_log(task_log: &TaskLogMessage, colors: &Colors, stdout: bool) -> Result<()> {
let (pre_text, color, bytes) = if stdout {
("stdout: ", Color::Green, task_log.stdout.as_ref().unwrap())
(
"stdout: ",
colors.green(),
task_log.stdout.as_ref().unwrap(),
)
} else {
("stderr: ", Color::Red, task_log.stderr.as_ref().unwrap())
("stderr: ", colors.red(), task_log.stderr.as_ref().unwrap())
};
println!(

View file

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

View file

@ -8,11 +8,11 @@ use pueue_lib::settings::Settings;
use pueue_lib::state::State;
use pueue_lib::task::{Task, TaskResult, TaskStatus};
use super::helper::*;
use super::{colors::Colors, helper::*};
use crate::cli::SubCommand;
/// Print the current state of the daemon in a nicely formatted table.
pub fn print_state(state: State, cli_command: &SubCommand, settings: &Settings) {
pub fn print_state(state: State, cli_command: &SubCommand, colors: &Colors, settings: &Settings) {
let (json, group_only) = match cli_command {
SubCommand::Status { json, group } => (*json, group.clone()),
_ => panic!(
@ -43,9 +43,10 @@ pub fn print_state(state: State, cli_command: &SubCommand, settings: &Settings)
&"default",
&state.groups.get("default").unwrap(),
*state.settings.daemon.groups.get("default").unwrap(),
colors,
);
println!("{}", headline);
print_table(&tasks, settings);
print_table(&tasks, colors, settings);
// Add a newline if there are further groups to be printed
if sorted_tasks.len() > 1 {
@ -72,9 +73,10 @@ pub fn print_state(state: State, cli_command: &SubCommand, settings: &Settings)
&group,
&state.groups.get(group).unwrap(),
*state.settings.daemon.groups.get(group).unwrap(),
colors,
);
println!("{}", headline);
print_table(&tasks, settings);
print_table(&tasks, colors, settings);
// Add a newline between groups
if sorted_iter.peek().is_some() {
@ -84,7 +86,7 @@ pub fn print_state(state: State, cli_command: &SubCommand, settings: &Settings)
}
/// Print some tasks into a nicely formatted table
fn print_table(tasks: &BTreeMap<usize, Task>, settings: &Settings) {
fn print_table(tasks: &BTreeMap<usize, Task>, colors: &Colors, settings: &Settings) {
let (has_delayed_tasks, has_dependencies, has_labels) = has_special_columns(tasks);
// Create table header row
@ -127,16 +129,18 @@ fn print_table(tasks: &BTreeMap<usize, Task>, settings: &Settings) {
// 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::Running => (status_string, colors.green()),
TaskStatus::Paused | TaskStatus::Locked => (status_string, colors.white()),
TaskStatus::Done => match &task.result {
Some(TaskResult::Success) => (TaskResult::Success.to_string(), Color::Green),
Some(TaskResult::DependencyFailed) => ("Dependency failed".to_string(), Color::Red),
Some(TaskResult::FailedToSpawn(_)) => ("Failed to spawn".to_string(), Color::Red),
Some(result) => (result.to_string(), Color::Red),
Some(TaskResult::Success) => (TaskResult::Success.to_string(), colors.green()),
Some(TaskResult::DependencyFailed) => {
("Dependency failed".to_string(), colors.red())
}
Some(TaskResult::FailedToSpawn(_)) => ("Failed to spawn".to_string(), colors.red()),
Some(result) => (result.to_string(), colors.red()),
None => panic!("Got a 'Done' task without a task result. Please report this bug."),
},
_ => (status_string, Color::Yellow),
_ => (status_string, colors.yellow()),
};
row.add_cell(Cell::new(status_text).fg(color));
@ -161,8 +165,8 @@ fn print_table(tasks: &BTreeMap<usize, Task>, settings: &Settings) {
// Match the color of the exit code.
// If the exit_code is none, it has been killed by the task handler.
let exit_code_cell = match task.result {
Some(TaskResult::Success) => Cell::new("0").fg(Color::Green),
Some(TaskResult::Failed(code)) => Cell::new(&code.to_string()).fg(Color::Red),
Some(TaskResult::Success) => Cell::new("0").fg(colors.green()),
Some(TaskResult::Failed(code)) => Cell::new(&code.to_string()).fg(colors.red()),
_ => Cell::new(""),
};
row.add_cell(exit_code_cell);