Skip to content
This repository was archived by the owner on Apr 6, 2026. It is now read-only.

Commit ceed898

Browse files
committed
feat: better log filtering
1 parent 4596e0b commit ceed898

3 files changed

Lines changed: 100 additions & 58 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ edition = "2024"
77
eframe = "0.28"
88
egui = "0.28"
99
egui_logger = "0.6"
10-
xshell = "0.2"
1110
serde = { version = "1.0", features = ["derive"] }
1211
serde_json = "1.0"
1312
chrono = { version = "0.4", features = ["serde"] }

src/main.rs

Lines changed: 100 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
use eframe::egui;
22
use serde::{Deserialize, Serialize};
3+
use std::collections::HashSet;
34
use std::sync::mpsc;
45
use std::thread;
5-
use xshell::{Shell, cmd};
6+
use std::process::{Command, Stdio};
7+
use std::io::{BufRead, BufReader};
68

79
#[derive(Debug, Clone, Serialize, Deserialize)]
810
struct Settings {
911
log_command: String,
1012
refresh_interval: u64,
1113
}
1214

15+
#[derive(Debug, Clone, PartialEq)]
16+
enum FilterMode {
17+
IncludeSelected,
18+
ExcludeSelected,
19+
}
20+
1321
impl Default for Settings {
1422
fn default() -> Self {
1523
Self {
@@ -27,27 +35,43 @@ struct LogEntry {
2735
struct LogsApp {
2836
settings: Settings,
2937
logs: Vec<LogEntry>,
30-
filter_text: String,
38+
selected_log_levels: HashSet<String>,
39+
filter_mode: FilterMode,
3140
search_text: String,
3241
auto_scroll: bool,
3342
show_settings: bool,
3443
log_receiver: Option<mpsc::Receiver<String>>,
3544
log_thread_handle: Option<thread::JoinHandle<()>>,
3645
settings_changed: bool,
46+
current_level_filter: String,
3747
}
3848

3949
impl Default for LogsApp {
4050
fn default() -> Self {
51+
let mut selected_log_levels = HashSet::new();
52+
selected_log_levels.insert("trace".to_string());
53+
selected_log_levels.insert("debug".to_string());
54+
selected_log_levels.insert("info".to_string());
55+
selected_log_levels.insert("warn".to_string());
56+
selected_log_levels.insert("warning".to_string());
57+
selected_log_levels.insert("error".to_string());
58+
selected_log_levels.insert("err".to_string());
59+
selected_log_levels.insert("fatal".to_string());
60+
selected_log_levels.insert("critical".to_string());
61+
selected_log_levels.insert("crit".to_string());
62+
4163
Self {
4264
settings: Settings::default(),
4365
logs: Vec::new(),
44-
filter_text: String::new(),
66+
selected_log_levels,
67+
filter_mode: FilterMode::IncludeSelected,
4568
search_text: String::new(),
4669
auto_scroll: true,
4770
show_settings: false,
4871
log_receiver: None,
4972
log_thread_handle: None,
5073
settings_changed: false,
74+
current_level_filter: "All Levels".to_string(),
5175
}
5276
}
5377
}
@@ -64,8 +88,6 @@ impl LogsApp {
6488
let command = self.settings.log_command.clone();
6589

6690
let handle = thread::spawn(move || {
67-
let sh = Shell::new().unwrap();
68-
6991
let parts: Vec<&str> = command.split_whitespace().collect();
7092
if parts.is_empty() {
7193
return;
@@ -74,22 +96,28 @@ impl LogsApp {
7496
let program = parts[0];
7597
let args = &parts[1..];
7698

77-
let result = if args.is_empty() {
78-
cmd!(sh, "{program}").read()
79-
} else {
80-
let mut cmd_builder = cmd!(sh, "{program}");
81-
for arg in args {
82-
cmd_builder = cmd_builder.arg(arg);
83-
}
84-
cmd_builder.read()
85-
};
86-
87-
if let Ok(output) = result {
88-
for line in output.lines() {
89-
if tx.send(line.to_string()).is_err() {
90-
break;
99+
let mut cmd = Command::new(program);
100+
cmd.args(args)
101+
.stdout(Stdio::piped())
102+
.stderr(Stdio::piped());
103+
104+
if let Ok(mut child) = cmd.spawn() {
105+
if let Some(stdout) = child.stdout.take() {
106+
let reader = BufReader::new(stdout);
107+
for line in reader.lines() {
108+
match line {
109+
Ok(line_content) => {
110+
if tx.send(line_content).is_err() {
111+
break;
112+
}
113+
}
114+
Err(_) => break,
115+
}
91116
}
92117
}
118+
119+
// Clean up the child process
120+
let _ = child.wait();
93121
}
94122
});
95123

@@ -123,13 +151,19 @@ impl LogsApp {
123151
self.logs
124152
.iter()
125153
.filter(|entry| {
126-
let matches_filter = if self.filter_text.is_empty() {
154+
let matches_filter = if self.selected_log_levels.is_empty() {
127155
true
128156
} else {
129-
entry
130-
.content
131-
.to_lowercase()
132-
.contains(&self.filter_text.to_lowercase())
157+
let content_lower = entry.content.to_lowercase();
158+
159+
let contains_selected_level = self.selected_log_levels.iter().any(|level| {
160+
content_lower.contains(&level.to_lowercase())
161+
});
162+
163+
match self.filter_mode {
164+
FilterMode::IncludeSelected => contains_selected_level,
165+
FilterMode::ExcludeSelected => !contains_selected_level,
166+
}
133167
};
134168

135169
let matches_search = if self.search_text.is_empty() {
@@ -184,8 +218,48 @@ impl eframe::App for LogsApp {
184218

185219
ui.separator();
186220

187-
ui.label("Filter:");
188-
ui.text_edit_singleline(&mut self.filter_text);
221+
ui.label("Command:");
222+
ui.add(egui::TextEdit::singleline(&mut self.settings.log_command).desired_width(200.0));
223+
if ui.button("Apply").clicked() {
224+
self.restart_log_collection();
225+
}
226+
227+
ui.separator();
228+
229+
ui.label("Log Level Filter:");
230+
ui.horizontal(|ui| {
231+
egui::ComboBox::from_label("Level")
232+
.selected_text(&self.current_level_filter)
233+
.show_ui(ui, |ui| {
234+
let levels = [
235+
("All Levels", "All Levels"),
236+
("TRACE", "trace"),
237+
("DEBUG", "debug"),
238+
("INFO", "info"),
239+
("WARN", "warn"),
240+
("WARNING", "warning"),
241+
("ERROR", "error"),
242+
("ERR", "err"),
243+
("FATAL", "fatal"),
244+
("CRITICAL", "critical"),
245+
("CRIT", "crit"),
246+
];
247+
248+
for (display_name, level_key) in levels {
249+
if ui.selectable_value(&mut self.current_level_filter, display_name.to_string(), display_name).clicked() {
250+
self.selected_log_levels.clear();
251+
if level_key != "All Levels" {
252+
self.selected_log_levels.insert(level_key.to_string());
253+
}
254+
}
255+
}
256+
});
257+
258+
ui.separator();
259+
ui.label("Mode:");
260+
ui.radio_value(&mut self.filter_mode, FilterMode::IncludeSelected, "Include");
261+
ui.radio_value(&mut self.filter_mode, FilterMode::ExcludeSelected, "Exclude");
262+
});
189263

190264
ui.separator();
191265

@@ -210,14 +284,6 @@ impl eframe::App for LogsApp {
210284
egui::Window::new("Settings")
211285
.open(&mut show_settings)
212286
.show(ctx, |ui| {
213-
ui.label("Log Command:");
214-
if ui
215-
.text_edit_singleline(&mut self.settings.log_command)
216-
.changed()
217-
{
218-
self.settings_changed = true;
219-
}
220-
221287
ui.label("Refresh Interval (ms):");
222288
if ui
223289
.add(egui::Slider::new(
@@ -238,13 +304,6 @@ impl eframe::App for LogsApp {
238304
reset_settings = true;
239305
}
240306
});
241-
242-
ui.separator();
243-
ui.label("Examples:");
244-
ui.label("• journalctl -f");
245-
ui.label("• tail -f /var/log/syslog");
246-
ui.label("• docker logs -f container_name");
247-
ui.label("• kubectl logs -f pod_name");
248307
});
249308
}
250309

0 commit comments

Comments
 (0)