1use crate::{Job, JobType, ProcInfo, ProcStatus};
2use anstyle_query::{term_supports_ansi_color, truecolor};
3use bytesize::ByteSize;
4use chrono::Local;
5use comfy_table::{presets::UTF8_FULL, ContentArrangement, Table};
6use env_logger::{
7 fmt::style::{AnsiColor, Color, RgbColor, Style},
8 Env,
9};
10use std::io::Write;
11
12pub fn init_cli_logger() {
13 let color = Formatter::default().log_color_app();
14 let mut builder = env_logger::Builder::from_env(Env::default().default_filter_or("info"));
15 builder.format(move |buf, record| {
16 let target = record.target();
17 let time = buf.timestamp();
18 writeln!(buf, "{color}{time} [{target}] {}{color:#}", record.args(),)
21 });
22
23 builder.init();
24}
25
26pub fn init_daemon_logger() {
27 let mut builder = env_logger::Builder::from_env(Env::default().default_filter_or("info"));
28 builder.format(|buf, record| {
29 let target = record.target();
30 writeln!(buf, "[{target}] {}", record.args(),)
31 });
32
33 builder.init();
34}
35
36const PALETTE: [Style; 20] = [
39 Style::new().fg_color(Some(Color::Rgb(RgbColor(0, 238, 110)))),
40 Style::new().fg_color(Some(Color::Rgb(RgbColor(11, 123, 224)))),
41 Style::new().fg_color(Some(Color::Rgb(RgbColor(2, 219, 129)))),
42 Style::new().fg_color(Some(Color::Rgb(RgbColor(3, 206, 142)))),
43 Style::new().fg_color(Some(Color::Rgb(RgbColor(9, 149, 198)))),
44 Style::new().fg_color(Some(Color::Rgb(RgbColor(7, 168, 179)))),
45 Style::new().fg_color(Some(Color::Rgb(RgbColor(4, 193, 154)))),
46 Style::new().fg_color(Some(Color::Rgb(RgbColor(8, 155, 192)))),
47 Style::new().fg_color(Some(Color::Rgb(RgbColor(5, 187, 161)))),
48 Style::new().fg_color(Some(Color::Rgb(RgbColor(6, 181, 167)))),
49 Style::new().fg_color(Some(Color::Rgb(RgbColor(12, 117, 230)))),
50 Style::new().fg_color(Some(Color::Rgb(RgbColor(6, 174, 173)))),
51 Style::new().fg_color(Some(Color::Rgb(RgbColor(1, 232, 116)))),
52 Style::new().fg_color(Some(Color::Rgb(RgbColor(8, 162, 186)))),
53 Style::new().fg_color(Some(Color::Rgb(RgbColor(4, 200, 148)))),
54 Style::new().fg_color(Some(Color::Rgb(RgbColor(9, 142, 205)))),
55 Style::new().fg_color(Some(Color::Rgb(RgbColor(10, 136, 211)))),
56 Style::new().fg_color(Some(Color::Rgb(RgbColor(3, 213, 135)))),
57 Style::new().fg_color(Some(Color::Rgb(RgbColor(1, 225, 123)))),
58 Style::new().fg_color(Some(Color::Rgb(RgbColor(11, 130, 217)))),
59];
60
61const ERR_PALETTE: [Style; 20] = [
62 Style::new().fg_color(Some(Color::Rgb(RgbColor(237, 227, 66)))),
63 Style::new().fg_color(Some(Color::Rgb(RgbColor(251, 112, 199)))),
64 Style::new().fg_color(Some(Color::Rgb(RgbColor(249, 127, 182)))),
65 Style::new().fg_color(Some(Color::Rgb(RgbColor(253, 96, 217)))),
66 Style::new().fg_color(Some(Color::Rgb(RgbColor(250, 119, 191)))),
67 Style::new().fg_color(Some(Color::Rgb(RgbColor(240, 204, 93)))),
68 Style::new().fg_color(Some(Color::Rgb(RgbColor(241, 196, 102)))),
69 Style::new().fg_color(Some(Color::Rgb(RgbColor(242, 189, 110)))),
70 Style::new().fg_color(Some(Color::Rgb(RgbColor(239, 212, 84)))),
71 Style::new().fg_color(Some(Color::Rgb(RgbColor(243, 181, 119)))),
72 Style::new().fg_color(Some(Color::Rgb(RgbColor(244, 173, 128)))),
73 Style::new().fg_color(Some(Color::Rgb(RgbColor(245, 166, 137)))),
74 Style::new().fg_color(Some(Color::Rgb(RgbColor(238, 219, 75)))),
75 Style::new().fg_color(Some(Color::Rgb(RgbColor(246, 150, 155)))),
76 Style::new().fg_color(Some(Color::Rgb(RgbColor(254, 89, 226)))),
77 Style::new().fg_color(Some(Color::Rgb(RgbColor(247, 142, 164)))),
78 Style::new().fg_color(Some(Color::Rgb(RgbColor(248, 135, 173)))),
79 Style::new().fg_color(Some(Color::Rgb(RgbColor(246, 158, 146)))),
80 Style::new().fg_color(Some(Color::Rgb(RgbColor(252, 104, 208)))),
81 Style::new().fg_color(Some(Color::Rgb(RgbColor(255, 81, 235)))),
82];
83
84const UNSTYLED: Style = Style::new();
85
86pub struct Formatter {
87 supports_truecolor: bool,
88 supports_ansi_color: bool,
89}
90
91impl Default for Formatter {
92 fn default() -> Self {
93 Formatter {
94 supports_truecolor: truecolor(),
95 supports_ansi_color: term_supports_ansi_color(),
96 }
97 }
98}
99
100impl Formatter {
101 pub fn log_color_proc(&self, idx: usize, err: bool) -> &'static Style {
102 if self.supports_truecolor {
103 if err {
104 &ERR_PALETTE[idx % 20]
105 } else {
106 &PALETTE[idx % 20]
107 }
108 } else {
109 &UNSTYLED
110 }
111 }
112
113 pub fn log_color_app(&self) -> &'static Style {
114 const COLOR: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Magenta)));
115 if self.supports_ansi_color {
116 &COLOR
117 } else {
118 &UNSTYLED
119 }
120 }
121
122 pub fn log_info(&self, text: &str) {
123 let color = self.log_color_app();
124 let time = Local::now().format("%F %T%.3f");
125 println!("{color}{time} [dispatcher] {text}{color:#}")
126 }
127}
128
129fn clip_str(text: &str, max_len: usize) -> String {
130 if text.len() > max_len {
131 format!("{}...", &text[..max_len.max(3) - 3])
132 } else {
133 text.to_string()
134 }
135}
136
137pub fn proc_info_table(proc_infos: &[ProcInfo]) {
138 const EMPTY: String = String::new();
139
140 let mut table = Table::new();
141 table
142 .load_preset(UTF8_FULL)
143 .set_header(vec![
144 "Job", "PID", "Status", "Command", "Start", "End", "Cpu", "Mem", "Virt", "Write",
145 "Total", "Read", "Total",
146 ])
147 .set_content_arrangement(ContentArrangement::DynamicFullWidth)
148 .add_rows(proc_infos.iter().map(|info| {
149 let status = match &info.state {
150 ProcStatus::ExitOk => "Success".to_string(),
151 ProcStatus::ExitErr(code) => format!("Error {code}"),
152 ProcStatus::Unknown(err) => clip_str(err, 20),
153 st => format!("{st:?}"),
154 };
155 let command = info.cmd_args.join(" ");
156 let end = if let Some(ts) = info.end {
157 format!("{}", ts.format("%F %T"))
158 } else {
159 EMPTY
160 };
161 vec![
162 format!("{}", info.job_id),
163 format!("{}", info.pid),
164 status,
165 clip_str(&command, 30),
166 format!("{}", info.start.format("%F %T")),
167 end,
168 format!("{:.1}%", info.cpu),
169 format!("{}", ByteSize(info.memory)),
170 format!("{}", ByteSize(info.virtual_memory)),
171 format!("{}/s", ByteSize(info.written_bytes)),
172 format!("{}", ByteSize(info.total_written_bytes)),
173 format!("{}/s", ByteSize(info.read_bytes)),
174 format!("{}", ByteSize(info.total_read_bytes)),
175 ]
176 }));
177
178 println!("{table}");
179}
180
181pub fn job_info_table(jobs: &[Job]) {
182 const EMPTY: String = String::new();
183
184 let mut table = Table::new();
185 table
186 .load_preset(UTF8_FULL)
187 .set_header(vec!["Job", "Command", "At"])
188 .set_content_arrangement(ContentArrangement::DynamicFullWidth)
189 .add_rows(jobs.iter().map(|job| {
190 let command = match &job.info.job_type {
191 JobType::Shell => &job.info.args.join(" "),
192 JobType::Service(s) => s,
193 JobType::Cron(_) => &job.info.args.join(" "),
194 };
195 let at = if let JobType::Cron(at) = &job.info.job_type {
196 at
197 } else {
198 &EMPTY
199 };
200 vec![format!("{}", job.id), clip_str(command, 30), at.to_string()]
201 }));
202
203 println!("{table}");
204}