11use eframe:: egui;
22use serde:: { Deserialize , Serialize } ;
3+ use std:: collections:: HashSet ;
34use std:: sync:: mpsc;
45use 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 ) ]
810struct 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+
1321impl Default for Settings {
1422 fn default ( ) -> Self {
1523 Self {
@@ -27,27 +35,43 @@ struct LogEntry {
2735struct 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
3949impl 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