1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
/*
This file is part of jellything (https://codeberg.org/metamuffin/jellything)
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
use jellycommon::{
api::{LogLevel, LogLine},
routes::u_admin_log,
};
use markup::raw;
use std::fmt::Write;
markup::define! {
ServerLogPage<'a>(warnonly: bool, messages: &'a [String]) {
h1 { "Server Log" }
a[href=u_admin_log(!warnonly)] { @if *warnonly { "Show everything" } else { "Show only warnings" }}
code.log[id="log"] {
table { @for e in *messages {
@raw(e)
}}
}
}
ServerLogLine<'a>(e: &'a LogLine) {
tr[class=format!("level-{}", e.level).to_ascii_lowercase()] {
td.time { @e.time.to_rfc3339() }
td.loglevel { @format_level(e.level) }
td.module { @e.module }
td { @markup::raw(vt100_to_html(&e.message)) }
}
}
}
pub fn render_log_line(line: &LogLine) -> String {
ServerLogLine { e: line }.to_string()
}
fn vt100_to_html(s: &str) -> String {
let mut out = HtmlOut::default();
let mut st = vte::Parser::new();
st.advance(&mut out, s.as_bytes());
out.s
}
fn format_level(level: LogLevel) -> impl markup::Render {
let (s, c) = match level {
LogLevel::Debug => ("DEBUG", "blue"),
LogLevel::Error => ("ERROR", "red"),
LogLevel::Warn => ("WARN", "yellow"),
LogLevel::Info => ("INFO", "green"),
LogLevel::Trace => ("TRACE", "lightblue"),
};
markup::new! { span[style=format!("color:{c}")] {@s} }
}
#[derive(Default)]
pub struct HtmlOut {
s: String,
color: bool,
}
impl HtmlOut {
pub fn set_color(&mut self, [r, g, b]: [u8; 3]) {
self.reset_color();
self.color = true;
write!(self.s, "<span style=color:#{:02x}{:02x}{:02x}>", r, g, b).unwrap()
}
pub fn reset_color(&mut self) {
if self.color {
write!(self.s, "</span>").unwrap();
self.color = false;
}
}
}
impl vte::Perform for HtmlOut {
fn print(&mut self, c: char) {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | ' ' => self.s.push(c),
x => write!(self.s, "&#{};", x as u32).unwrap(),
}
}
fn execute(&mut self, _byte: u8) {}
fn hook(&mut self, _params: &vte::Params, _i: &[u8], _ignore: bool, _a: char) {}
fn put(&mut self, _byte: u8) {}
fn unhook(&mut self) {}
fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {}
fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {}
fn csi_dispatch(
&mut self,
params: &vte::Params,
_intermediates: &[u8],
_ignore: bool,
action: char,
) {
let mut k = params.iter();
#[allow(clippy::single_match)]
match action {
'm' => match k.next().unwrap_or(&[0]).first().unwrap_or(&0) {
c @ (30..=37 | 40..=47) => {
let c = if *c >= 40 { *c - 10 } else { *c };
self.set_color(match c {
30 => [0, 0, 0],
31 => [255, 0, 0],
32 => [0, 255, 0],
33 => [255, 255, 0],
34 => [0, 0, 255],
35 => [255, 0, 255],
36 => [0, 255, 255],
37 => [255, 255, 255],
_ => unreachable!(),
});
}
_ => (),
},
_ => (),
}
}
}
|