shell_compose/
display.rs

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        // let level = record.level();
19
20        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
36// See https://jvns.ca/blog/2024/10/01/terminal-colours/ for infos about color support
37
38const 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}