├── .gitignore ├── Cargo.toml ├── README.md ├── images └── example.gif ├── rustfmt.toml └── src ├── app.rs ├── main.rs ├── parser ├── buffers.rs ├── compiler.rs ├── fields.rs ├── logdata.rs ├── mod.rs └── value.rs ├── ui ├── index.rs ├── mod.rs ├── model.rs └── widgets │ ├── info.rs │ ├── lineedit.rs │ ├── mod.rs │ └── table.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "journal1c" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tui = "0.19.0" 8 | crossterm = "0.24.0" 9 | regex = "1.6.0" 10 | lazy_static = "1.4.0" 11 | chrono = "0.4.20" 12 | walkdir = "2.3.2" 13 | indexmap = "1.9.1" 14 | clap = { version = "3.2.16", features = ["derive"] } 15 | thiserror = "1.0.32" 16 | cli-clipboard = "0.2.1" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1c_log_viewer 2 | 3 | **1c_log_viewer** консольная программа позволяющая просматривать, фильтровать, анализировать 4 | технологический журнал 1С. 5 | 6 | ![](images/example.gif) 7 | 8 | ## Установка 9 | 10 | Используйте [Rust](https://www.rust-lang.org/tools/install) для установки 1c_log_viewer. 11 | 12 | ```bash 13 | cargo install --git https://github.com/tuplecats/1c-log-viewer 14 | ``` 15 | 16 | ## Использование 17 | 18 | ### Параметры 19 | ```` 20 | -d, --directory=PATH Путь к директории с файлами логов 21 | (Также ищет файлы в поддиректориях) 22 | 23 | --from=TIME Временая точка начала чтения логов. 24 | Формат: now-{digit}{s/m/h/d/w} 25 | Пример: now-1d или now-30s 26 | ```` 27 | 28 | ````bash 29 | journal1c -d path\to\log\dir 30 | ```` 31 | 32 | ### Фильтрация (Язык запросов) 33 | 34 | Фильтры задаются в строке поиска `Ctrl+F` 35 | 36 | ```sql 37 | WHERE time > 'now-1d' AND (event = "PROC" OR Txt=/ping/) 38 | ``` 39 | | Тип значение | Описание | Пример | 40 | |----------------------|------------------------------------|-------------------------------------------------| 41 | | Дата | Задается в одинарных кавычках `''` | `'now-1d'`; `'now-5m'`; `'2022-08-02 14:00:00'` | 42 | | Строка | Задается в двойных кавычках `""` | `"example"` | 43 | | Число | | `0`; `1`; `2` | 44 | | Регулярное выражение | Задается между `//` | `/[0-9]+/` | 45 | 46 | ### Фильтрация (Регулярные выражения) 47 | 48 | Фильтры задаются в строке поиска `Ctrl+F` 49 | 50 | ``` 51 | /regex/ 52 | ``` -------------------------------------------------------------------------------- /images/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuplecats/1c-log-viewer/d0f13053145445fbaccdea6bbec756eb138d088f/images/example.gif -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity="Crate" -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parser::{Compiler, FieldMap, Value}, 3 | ui::widgets::{KeyValueView, LineEdit, TableView, WidgetExt}, 4 | LogCollection, LogParser, 5 | }; 6 | use chrono::NaiveDateTime; 7 | use crossterm::{ 8 | event, 9 | event::{Event, KeyCode, KeyModifiers}, 10 | }; 11 | use std::{cell::RefCell, error::Error, rc::Rc, time::Duration}; 12 | use tui::{ 13 | backend::Backend, 14 | layout::{Constraint, Direction, Layout}, 15 | style::{Color, Style}, 16 | text::{Span, Spans, Text}, 17 | widgets::Paragraph, 18 | Frame, Terminal, 19 | }; 20 | 21 | #[derive(Default)] 22 | enum ActiveWidget { 23 | SearchBox, 24 | 25 | #[default] 26 | LogTable, 27 | 28 | InfoView, 29 | } 30 | 31 | pub struct App { 32 | pub table: Rc>, 33 | pub search: Rc>, 34 | pub text: Rc>, 35 | pub log_data: Rc>, 36 | 37 | pub prev_size: (u16, u16), 38 | 39 | state: ActiveWidget, 40 | } 41 | 42 | impl App { 43 | pub fn new>(dir: T, date: Option) -> Self { 44 | let dir = dir.into(); 45 | let widths = vec![ 46 | Constraint::Percentage(20), 47 | Constraint::Percentage(20), 48 | Constraint::Percentage(20), 49 | Constraint::Percentage(20), 50 | Constraint::Percentage(20), 51 | ]; 52 | 53 | let log_data = Rc::new(RefCell::new(LogCollection::new(LogParser::parse( 54 | dir, date, 55 | )))); 56 | 57 | let mut table_view = TableView::new(widths); 58 | table_view.set_model(log_data.clone()); 59 | 60 | let app = Self { 61 | table: Rc::new(RefCell::new(table_view)), 62 | search: Rc::new(RefCell::new(LineEdit::new("Filter".into()))), 63 | text: Rc::new(RefCell::new(KeyValueView::new())), 64 | log_data: log_data.clone(), 65 | prev_size: (0, 0), 66 | state: ActiveWidget::default(), 67 | }; 68 | 69 | app.table.borrow_mut().set_focus(true); 70 | 71 | let log_data = Rc::downgrade(&app.log_data); 72 | let table = Rc::downgrade(&app.table); 73 | app.search 74 | .borrow_mut() 75 | .on_changed(move |sender| match log_data.upgrade() { 76 | Some(model) => match model.borrow_mut().set_filter(sender.text().to_string()) { 77 | Err(e) => { 78 | sender.set_border_text(e.to_string()); 79 | sender.set_style(Style::default().fg(Color::Red)); 80 | } 81 | _ => { 82 | sender.set_border_text(String::new()); 83 | sender.set_style(Style::default()); 84 | if let Some(table) = table.upgrade() { 85 | table.borrow_mut().reset_state(); 86 | } 87 | } 88 | }, 89 | None => {} 90 | }); 91 | 92 | let text = Rc::downgrade(&app.text); 93 | let log_data = Rc::downgrade(&app.log_data); 94 | app.table 95 | .borrow_mut() 96 | .on_selection_changed(move |_sender, index| { 97 | if let (Some(log_data), Some(text)) = (log_data.upgrade(), text.upgrade()) { 98 | if let Some(index) = index { 99 | if let Some(line) = log_data.borrow().line(index) { 100 | text.borrow_mut().set_data(line.fields().into()); 101 | return; 102 | } 103 | } 104 | 105 | // Panic if we can't borrow. Because dont need reset state when filter from info widget. 106 | if let Ok(mut borrowed) = text.try_borrow_mut() { 107 | borrowed.set_data(FieldMap::new()); 108 | } 109 | } 110 | }); 111 | 112 | let search = Rc::downgrade(&app.search); 113 | app.text.borrow_mut().on_add_to_filter(move |(key, value)| { 114 | if let Some(search) = search.upgrade() { 115 | let value = match value { 116 | Value::String(s) => format!("\"{}\"", s), 117 | Value::Number(n) => n.to_string(), 118 | Value::DateTime(n) => format!("'{}'", n.format("%Y-%m-%d %H:%M:%S%.9f")), 119 | _ => unreachable!(), 120 | }; 121 | 122 | let mut search_borrowed = search.borrow_mut(); 123 | search_borrowed.show(); 124 | let text = search_borrowed.text().to_string(); 125 | if text.trim().is_empty() { 126 | search_borrowed.set_text(format!(r#"WHERE {} = {}"#, key, value)); 127 | } else if let Ok(query) = Compiler::new().compile(text.trim()) { 128 | if !query.is_regex() { 129 | search_borrowed.set_text(format!(r#"{} AND {} = {}"#, text, key, value)); 130 | } 131 | } 132 | } 133 | }); 134 | 135 | app 136 | } 137 | 138 | pub fn run(&mut self, terminal: &mut Terminal) -> Result<(), Box> { 139 | loop { 140 | terminal.draw(|f| ui(f, self))?; 141 | 142 | if event::poll(Duration::from_millis(100))? { 143 | let event = event::read()?; 144 | match event { 145 | Event::Key(key) => match key.code { 146 | KeyCode::Char('q') if key.modifiers == KeyModifiers::CONTROL => { 147 | return Ok(()) 148 | } 149 | KeyCode::Char('f') if key.modifiers == KeyModifiers::CONTROL => { 150 | match self.state { 151 | ActiveWidget::LogTable | ActiveWidget::InfoView => { 152 | self.search.borrow_mut().set_visible(true); 153 | self.set_active_widget(ActiveWidget::SearchBox); 154 | } 155 | ActiveWidget::SearchBox => { 156 | self.search.borrow_mut().set_visible(false); 157 | self.set_active_widget(ActiveWidget::LogTable); 158 | } 159 | } 160 | } 161 | KeyCode::Tab => { 162 | // Next active widget 163 | match self.state { 164 | ActiveWidget::LogTable => { 165 | self.set_active_widget(ActiveWidget::InfoView); 166 | } 167 | ActiveWidget::SearchBox => { 168 | self.set_active_widget(ActiveWidget::LogTable); 169 | } 170 | ActiveWidget::InfoView => { 171 | if self.search.borrow().visible() { 172 | self.set_active_widget(ActiveWidget::SearchBox); 173 | } else { 174 | self.set_active_widget(ActiveWidget::LogTable); 175 | } 176 | } 177 | } 178 | } 179 | _ => match self.state { 180 | ActiveWidget::LogTable => self.table.borrow_mut().key_press_event(key), 181 | ActiveWidget::SearchBox => { 182 | self.search.borrow_mut().key_press_event(key) 183 | } 184 | ActiveWidget::InfoView => self.text.borrow_mut().key_press_event(key), 185 | }, 186 | }, 187 | _ => {} 188 | } 189 | } 190 | } 191 | } 192 | 193 | fn set_active_widget(&mut self, widget: ActiveWidget) { 194 | match widget { 195 | ActiveWidget::LogTable => { 196 | self.table.borrow_mut().set_focus(true); 197 | self.search.borrow_mut().set_focus(false); 198 | self.text.borrow_mut().set_focus(false) 199 | } 200 | ActiveWidget::SearchBox => { 201 | self.table.borrow_mut().set_focus(false); 202 | self.search.borrow_mut().set_focus(true); 203 | self.text.borrow_mut().set_focus(false) 204 | } 205 | ActiveWidget::InfoView => { 206 | self.table.borrow_mut().set_focus(false); 207 | self.search.borrow_mut().set_focus(false); 208 | self.text.borrow_mut().set_focus(true) 209 | } 210 | } 211 | 212 | self.state = widget; 213 | } 214 | } 215 | 216 | fn ui(f: &mut Frame, app: &mut App) { 217 | let rects = Layout::default() 218 | .direction(Direction::Vertical) 219 | .constraints(vec![Constraint::Min(1), Constraint::Length(1)]) 220 | .split(f.size()); 221 | 222 | let keys_rect = rects[1]; 223 | let rects = Layout::default() 224 | .direction(Direction::Vertical) 225 | .constraints(vec![ 226 | Constraint::Length(if app.search.borrow().visible() { 3 } else { 0 }), 227 | Constraint::Percentage(60), 228 | Constraint::Percentage(40), 229 | ]) 230 | .split(rects[0]); 231 | 232 | if rects[0].width != app.search.borrow().width() 233 | || rects[0].height != app.search.borrow().height() 234 | { 235 | app.search 236 | .borrow_mut() 237 | .resize(rects[0].width, rects[0].height); 238 | } 239 | if rects[1].width != app.table.borrow().width() 240 | || rects[1].height != app.table.borrow().height() 241 | { 242 | app.table 243 | .borrow_mut() 244 | .resize(rects[1].width, rects[1].height); 245 | } 246 | if rects[2].width != app.text.borrow().width() || rects[2].height != app.text.borrow().height() 247 | { 248 | app.text 249 | .borrow_mut() 250 | .resize(rects[2].width, rects[2].height); 251 | } 252 | 253 | app.prev_size = (f.size().width, f.size().height); 254 | if app.search.borrow().visible() { 255 | f.render_widget(app.search.borrow_mut().widget(), rects[0]); 256 | } 257 | 258 | f.render_widget(app.table.borrow_mut().widget(), rects[1]); 259 | f.render_widget(app.text.borrow_mut().widget(), rects[2]); 260 | 261 | let mut common_keys = vec![ 262 | Span::styled("Ctrl+Q", Style::default().fg(Color::White)), 263 | Span::raw(" "), 264 | Span::styled("Quit", Style::default().fg(Color::LightCyan)), 265 | Span::raw(" | "), 266 | Span::styled("Ctrl+F", Style::default().fg(Color::White)), 267 | Span::raw(" "), 268 | Span::styled("Search", Style::default().fg(Color::LightCyan)), 269 | Span::raw(" | "), 270 | Span::styled("Tab", Style::default().fg(Color::White)), 271 | Span::raw(" "), 272 | Span::styled("Next widget", Style::default().fg(Color::LightCyan)), 273 | ]; 274 | 275 | match app.state { 276 | ActiveWidget::LogTable => { 277 | common_keys.extend_from_slice(&[ 278 | Span::raw(" | "), 279 | Span::styled("PageUp", Style::default().fg(Color::White)), 280 | Span::raw(" "), 281 | Span::styled("Go to begin", Style::default().fg(Color::LightCyan)), 282 | Span::raw(" | "), 283 | Span::styled("PageDown", Style::default().fg(Color::White)), 284 | Span::raw(" "), 285 | Span::styled("Go to end", Style::default().fg(Color::LightCyan)), 286 | ]); 287 | } 288 | ActiveWidget::SearchBox => common_keys.extend_from_slice(&[ 289 | Span::raw(" | "), 290 | Span::styled("Ctrl-Bckspc", Style::default().fg(Color::White)), 291 | Span::raw(" "), 292 | Span::styled("Clear", Style::default().fg(Color::LightCyan)), 293 | ]), 294 | ActiveWidget::InfoView => { 295 | common_keys.extend_from_slice(&[ 296 | Span::raw(" | "), 297 | Span::styled("C", Style::default().fg(Color::White)), 298 | Span::raw(" "), 299 | Span::styled("Copy", Style::default().fg(Color::LightCyan)), 300 | Span::raw(" | "), 301 | Span::styled("F", Style::default().fg(Color::White)), 302 | Span::raw(" "), 303 | Span::styled("Add to filter", Style::default().fg(Color::LightCyan)), 304 | Span::raw(" | "), 305 | Span::styled("PageUp", Style::default().fg(Color::White)), 306 | Span::raw(" "), 307 | Span::styled("Go to begin", Style::default().fg(Color::LightCyan)), 308 | Span::raw(" | "), 309 | Span::styled("PageDown", Style::default().fg(Color::White)), 310 | Span::raw(" "), 311 | Span::styled("Go to end", Style::default().fg(Color::LightCyan)), 312 | ]); 313 | } 314 | }; 315 | 316 | f.render_widget( 317 | Paragraph::new(Text::from(Spans::from(common_keys))), 318 | keys_rect, 319 | ) 320 | } 321 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod parser; 3 | mod ui; 4 | mod util; 5 | 6 | /// TODO: 7 | /// 1. Добить запрос с разными типами 8 | /// 2. Индексация по полям 9 | /// 3. Читать файлы и запоминать только байты конкретных данных 10 | use crate::parser::LogParser; 11 | use app::App; 12 | use clap::Parser; 13 | use crossterm::{ 14 | event::{DisableMouseCapture, EnableMouseCapture}, 15 | execute, 16 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 17 | }; 18 | use std::error::Error; 19 | use tui::{backend::CrosstermBackend, Terminal}; 20 | 21 | use crate::util::parse_date; 22 | use parser::logdata::LogCollection; 23 | 24 | #[derive(Parser, Debug)] 25 | #[clap(author, version, about, long_about = None, verbatim_doc_comment)] 26 | struct Args { 27 | /// Путь к директории с файлами логов 28 | /// (Также ищет файлы в поддиректориях) 29 | #[clap(short, long, value_parser, verbatim_doc_comment)] 30 | directory: String, 31 | 32 | /// Временая точка начала чтения логов. 33 | /// Формат: now-{digit}{s/m/h/d/w} 34 | /// Пример: now-1d или now-30s 35 | #[clap(long, value_parser, verbatim_doc_comment)] 36 | from: Option, 37 | } 38 | 39 | fn main() -> Result<(), Box> { 40 | let args = Args::parse(); 41 | let date = match &args.from { 42 | Some(value) => Some(parse_date(value.as_str())?), 43 | None => None, 44 | }; 45 | 46 | enable_raw_mode()?; 47 | let mut stdout = std::io::stdout(); 48 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 49 | let backend = CrosstermBackend::new(stdout); 50 | let mut terminal = Terminal::new(backend)?; 51 | 52 | App::new(args.directory.as_str(), date).run(&mut terminal)?; 53 | 54 | // restore terminal 55 | disable_raw_mode()?; 56 | execute!( 57 | terminal.backend_mut(), 58 | LeaveAlternateScreen, 59 | DisableMouseCapture 60 | )?; 61 | terminal.show_cursor()?; 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /src/parser/buffers.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::BufReader, 4 | sync::{Arc, Mutex, RwLock}, 5 | }; 6 | 7 | lazy_static::lazy_static! { 8 | static ref BUFFERS: RwLock>>>> = RwLock::new(Vec::new()); 9 | } 10 | 11 | #[inline] 12 | pub(super) fn add_buffer(buffer: BufReader) -> usize { 13 | let mut lock = BUFFERS.write().unwrap(); 14 | lock.push(Arc::new(Mutex::new(buffer))); 15 | lock.len() - 1 16 | } 17 | 18 | #[inline] 19 | pub(super) fn get_buffer(index: usize) -> Arc>> { 20 | let lock = BUFFERS.read().unwrap(); 21 | lock.get(index).cloned().unwrap() 22 | } 23 | -------------------------------------------------------------------------------- /src/parser/compiler.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{FieldMap, Value}; 2 | use chrono::{Duration, NaiveDateTime}; 3 | use regex::Regex; 4 | use std::{ 5 | fmt::{Display, Formatter}, 6 | iter::Peekable, 7 | ops::Deref, 8 | slice::Iter, 9 | str::Chars, 10 | }; 11 | use thiserror::Error; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct RegexCmp { 15 | inner: Regex, 16 | value: String, 17 | } 18 | 19 | impl RegexCmp { 20 | pub fn new>(value: T) -> Result { 21 | let value = value.into(); 22 | 23 | Ok(RegexCmp { 24 | inner: regex::Regex::new(value.as_str())?, 25 | value, 26 | }) 27 | } 28 | } 29 | 30 | impl Deref for RegexCmp { 31 | type Target = Regex; 32 | 33 | fn deref(&self) -> &Self::Target { 34 | &self.inner 35 | } 36 | } 37 | 38 | impl PartialEq for RegexCmp { 39 | fn eq(&self, other: &Self) -> bool { 40 | self.value == other.value 41 | } 42 | } 43 | 44 | #[derive(Debug, Clone)] 45 | pub enum Token { 46 | WHERE, 47 | AND, 48 | OR, 49 | OpenBrace, 50 | CloseBrace, 51 | Identifier(String), 52 | String(String), 53 | Number(f64), 54 | Regex(RegexCmp), 55 | Date(NaiveDateTime), 56 | DESC, 57 | ASC, 58 | 59 | Less, 60 | Greater, 61 | Equal, 62 | LE, 63 | GE, 64 | NE, 65 | } 66 | 67 | impl Display for Token { 68 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 69 | match self { 70 | Token::WHERE => write!(f, "WHERE"), 71 | Token::AND => write!(f, "AND"), 72 | Token::OR => write!(f, "OR"), 73 | Token::OpenBrace => write!(f, "{{"), 74 | Token::CloseBrace => write!(f, "}}"), 75 | Token::Identifier(s) => write!(f, "{}", s), 76 | Token::String(s) => write!(f, "{}", s), 77 | Token::Number(s) => write!(f, "{}", s), 78 | Token::Regex(s) => write!(f, "{}", s.value), 79 | Token::Date(s) => write!(f, "{}", s), 80 | Token::DESC => write!(f, "DESC"), 81 | Token::ASC => write!(f, "ASC"), 82 | Token::Less => write!(f, "<"), 83 | Token::Greater => write!(f, ">"), 84 | Token::Equal => write!(f, "="), 85 | Token::LE => write!(f, "<="), 86 | Token::GE => write!(f, ">="), 87 | Token::NE => write!(f, "!="), 88 | } 89 | } 90 | } 91 | 92 | impl PartialEq for Token { 93 | fn eq(&self, other: &Self) -> bool { 94 | match (self, other) { 95 | (Token::WHERE, Token::WHERE) => true, 96 | (Token::AND, Token::AND) => true, 97 | (Token::OR, Token::OR) => true, 98 | (Token::OpenBrace, Token::OpenBrace) => true, 99 | (Token::CloseBrace, Token::CloseBrace) => true, 100 | (Token::Identifier(s1), Token::Identifier(s2)) => s1 == s2, 101 | (Token::String(s1), Token::String(s2)) => s1 == s2, 102 | (Token::Number(s1), Token::Number(s2)) => s1 == s2, 103 | //(Token::Regex(s1), Token::Regex(s2)) => s1 == s2, 104 | (Token::Date(s1), Token::Date(s2)) => s1 == s2, 105 | (Token::DESC, Token::DESC) => true, 106 | (Token::ASC, Token::ASC) => true, 107 | (Token::Less, Token::Less) => true, 108 | (Token::Greater, Token::Greater) => true, 109 | (Token::Equal, Token::Equal) => true, 110 | (Token::LE, Token::LE) => true, 111 | (Token::GE, Token::GE) => true, 112 | (Token::NE, Token::NE) => true, 113 | _ => false, 114 | } 115 | } 116 | } 117 | 118 | #[derive(Error, Debug)] 119 | pub enum ParseError { 120 | UnexpectedToken(Token), 121 | UnexpectedChar(char), 122 | RegexParseError(#[from] regex::Error), 123 | TimeParseError(#[from] chrono::ParseError), 124 | FloatParseError(#[from] std::num::ParseFloatError), 125 | InvalidDate, 126 | UnexpectedEndOfInput, 127 | } 128 | 129 | impl Display for ParseError { 130 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 131 | match self { 132 | ParseError::UnexpectedToken(token) => write!(f, "Unexpected token: {}", token), 133 | ParseError::UnexpectedChar(c) => write!(f, "Unexpected char: {}", c), 134 | ParseError::RegexParseError(e) => write!(f, "Regex parse error: {}", e), 135 | ParseError::TimeParseError(e) => write!(f, "time parse error: {}", e), 136 | ParseError::FloatParseError(e) => write!(f, "float parse error: {}", e), 137 | ParseError::InvalidDate => write!(f, "Invalid date"), 138 | ParseError::UnexpectedEndOfInput => write!(f, "Unexpected end of input"), 139 | } 140 | } 141 | } 142 | 143 | #[derive(Debug, PartialEq, Clone)] 144 | pub enum Query { 145 | Expr(Option>, Option>), 146 | Regex(RegexCmp), 147 | And(Box, Box), 148 | Or(Box, Box), 149 | 150 | Equal(Token, Token), 151 | GE(Token, Token), 152 | LE(Token, Token), 153 | Greater(Token, Token), 154 | Less(Token, Token), 155 | NE(Token, Token), 156 | } 157 | 158 | impl Query { 159 | pub fn accept<'a>(&self, log_data: &FieldMap<'a>) -> bool { 160 | match self { 161 | Query::Expr(where_expr, _) => { 162 | if let Some(where_expr) = where_expr { 163 | if !where_expr.accept(log_data) { 164 | return false; 165 | } 166 | } 167 | true 168 | } 169 | Query::Regex(regex) => { 170 | // if let Value::String(s) = fields.get("event").unwrap() { 171 | // if regex.is_match(&s) { 172 | // return true 173 | // } 174 | // } 175 | // 176 | // if let Value::String(s) = fields.get("process").unwrap() { 177 | // if regex.is_match(&s) { 178 | // return true 179 | // } 180 | // } 181 | 182 | for (_, field) in log_data.iter() { 183 | if let Value::String(s) = field { 184 | if regex.is_match(s.as_ref()) { 185 | return true; 186 | } 187 | } 188 | } 189 | 190 | false 191 | } 192 | Query::And(left, right) => left.accept(log_data) && right.accept(log_data), 193 | Query::Or(left, right) => left.accept(log_data) || right.accept(log_data), 194 | Query::Equal(left, right) => match (left, right) { 195 | (Token::Identifier(left), Token::String(right)) => log_data 196 | .get(left) 197 | .map(|x| x.iter().any(|x| x == right)) 198 | .unwrap_or(false), 199 | (Token::Identifier(left), Token::Number(right)) => log_data 200 | .get(left) 201 | .map(|x| x.iter().any(|x| x == right)) 202 | .unwrap_or(false), 203 | (Token::Identifier(left), Token::Regex(right)) => log_data 204 | .get(left) 205 | .map(|x| x.iter().any(|x| right.is_match(x.to_string().as_str()))) 206 | .unwrap_or(false), 207 | (Token::Identifier(left), Token::Date(right)) => log_data 208 | .get(left) 209 | .map(|x| x.iter().any(|x| x == right)) 210 | .unwrap_or(false), 211 | _ => false, 212 | }, 213 | Query::GE(left, right) => match (left, right) { 214 | (Token::Identifier(left), Token::String(right)) => log_data 215 | .get(left) 216 | .map(|x| x.iter().any(|x| x >= right)) 217 | .unwrap_or(false), 218 | (Token::Identifier(left), Token::Number(right)) => log_data 219 | .get(left) 220 | .map(|x| x.iter().any(|x| x >= right)) 221 | .unwrap_or(false), 222 | (Token::Identifier(left), Token::Date(right)) => log_data 223 | .get(left) 224 | .map(|x| x.iter().any(|x| x >= right)) 225 | .unwrap_or(false), 226 | _ => false, 227 | }, 228 | Query::LE(left, right) => match (left, right) { 229 | (Token::Identifier(left), Token::String(right)) => log_data 230 | .get(left) 231 | .map(|x| x.iter().any(|x| x <= right)) 232 | .unwrap_or(false), 233 | (Token::Identifier(left), Token::Number(right)) => log_data 234 | .get(left) 235 | .map(|x| x.iter().any(|x| x <= right)) 236 | .unwrap_or(false), 237 | (Token::Identifier(left), Token::Date(right)) => log_data 238 | .get(left) 239 | .map(|x| x.iter().any(|x| x <= right)) 240 | .unwrap_or(false), 241 | _ => false, 242 | }, 243 | Query::Greater(left, right) => match (left, right) { 244 | (Token::Identifier(left), Token::String(right)) => log_data 245 | .get(left) 246 | .map(|x| x.iter().any(|x| x > right)) 247 | .unwrap_or(false), 248 | (Token::Identifier(left), Token::Number(right)) => log_data 249 | .get(left) 250 | .map(|x| x.iter().any(|x| x > right)) 251 | .unwrap_or(false), 252 | (Token::Identifier(left), Token::Date(right)) => log_data 253 | .get(left) 254 | .map(|x| x.iter().any(|x| x > right)) 255 | .unwrap_or(false), 256 | _ => false, 257 | }, 258 | Query::Less(left, right) => match (left, right) { 259 | (Token::Identifier(left), Token::String(right)) => log_data 260 | .get(left) 261 | .map(|x| x.iter().any(|x| x < right)) 262 | .unwrap_or(false), 263 | (Token::Identifier(left), Token::Number(right)) => log_data 264 | .get(left) 265 | .map(|x| x.iter().any(|x| x < right)) 266 | .unwrap_or(false), 267 | (Token::Identifier(left), Token::Date(right)) => log_data 268 | .get(left) 269 | .map(|x| x.iter().any(|x| x < right)) 270 | .unwrap_or(false), 271 | _ => false, 272 | }, 273 | Query::NE(left, right) => match (left, right) { 274 | (Token::Identifier(left), Token::String(right)) => log_data 275 | .get(left) 276 | .map(|x| x.iter().any(|x| x != right)) 277 | .unwrap_or(false), 278 | (Token::Identifier(left), Token::Number(right)) => log_data 279 | .get(left) 280 | .map(|x| x.iter().any(|x| x != right)) 281 | .unwrap_or(false), 282 | (Token::Identifier(left), Token::Date(right)) => log_data 283 | .get(left) 284 | .map(|x| x.iter().any(|x| x != right)) 285 | .unwrap_or(false), 286 | _ => false, 287 | }, 288 | } 289 | } 290 | 291 | pub fn is_regex(&self) -> bool { 292 | matches!(self, Query::Regex(_)) 293 | } 294 | } 295 | 296 | pub struct Compiler { 297 | now: NaiveDateTime, 298 | } 299 | 300 | impl Compiler { 301 | pub fn new() -> Self { 302 | Self { 303 | now: chrono::Local::now().naive_local(), 304 | } 305 | } 306 | 307 | fn parse_numeric>( 308 | &self, 309 | iter: &mut Peekable, 310 | ) -> Result { 311 | let mut tmp = String::new(); 312 | while iter.peek().is_some() && iter.peek().unwrap().is_numeric() { 313 | tmp.push(iter.next().unwrap()); 314 | } 315 | Ok(tmp.parse::()?) 316 | } 317 | 318 | fn parse_date(&self, iter: &mut Peekable) -> Result { 319 | let mut tmp = String::new(); 320 | iter.next(); 321 | while iter.peek().is_some() && iter.peek().unwrap().ne(&'\'') { 322 | tmp.push(iter.next().unwrap()); 323 | } 324 | iter.next(); 325 | if tmp.starts_with("now") { 326 | match tmp.chars().nth(3) { 327 | Some('-') => { 328 | let mut str_iter = tmp.chars().skip(4).peekable(); 329 | let offset = self.parse_numeric(&mut str_iter)?; 330 | match str_iter.next() { 331 | Some('s') => Ok(Token::Date(self.now - Duration::seconds(offset as i64))), 332 | Some('m') => Ok(Token::Date(self.now - Duration::minutes(offset as i64))), 333 | Some('h') => Ok(Token::Date(self.now - Duration::hours(offset as i64))), 334 | Some('d') => Ok(Token::Date(self.now - Duration::days(offset as i64))), 335 | Some('w') => Ok(Token::Date(self.now - Duration::weeks(offset as i64))), 336 | Some(c) => return Err(ParseError::UnexpectedChar(c)), 337 | _ => return Err(ParseError::UnexpectedEndOfInput), 338 | } 339 | } 340 | Some(_) => return Err(ParseError::InvalidDate), 341 | None => Ok(Token::Date(self.now)), 342 | } 343 | } else { 344 | Ok(Token::Date(NaiveDateTime::parse_from_str( 345 | &tmp, 346 | "%Y-%m-%d %H:%M:%S%.9f", 347 | )?)) 348 | } 349 | } 350 | 351 | fn tokenize(&self, program: &str) -> Result, ParseError> { 352 | let mut tokens = vec![]; 353 | let mut iter = program.chars().peekable(); 354 | loop { 355 | match iter.peek() { 356 | Some(&c) => match c { 357 | 'a'..='z' | 'A'..='Z' => { 358 | let mut tmp = String::new(); 359 | while let Some(&peek) = iter.peek() { 360 | match peek { 361 | 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | ':' 362 | if !tmp.is_empty() => 363 | { 364 | tmp.push(peek); 365 | iter.next(); 366 | } 367 | 'a'..='z' | 'A'..='Z' | '_' => { 368 | tmp.push(peek); 369 | iter.next(); 370 | } 371 | _ => break, 372 | } 373 | } 374 | 375 | match tmp.as_str() { 376 | "WHERE" => tokens.push(Token::WHERE), 377 | "AND" => tokens.push(Token::AND), 378 | "OR" => tokens.push(Token::OR), 379 | "DESC" => tokens.push(Token::DESC), 380 | "ASC" => tokens.push(Token::ASC), 381 | _ => tokens.push(Token::Identifier(tmp)), 382 | } 383 | } 384 | '0'..='9' => { 385 | tokens.push(Token::Number(self.parse_numeric(&mut iter)?)); 386 | iter.next(); 387 | } 388 | '"' => { 389 | let mut tmp = String::new(); 390 | iter.next(); 391 | while iter.peek().is_some() && iter.peek().unwrap().ne(&'"') { 392 | tmp.push(iter.next().unwrap()); 393 | } 394 | iter.next(); 395 | tokens.push(Token::String(tmp)); 396 | } 397 | '\'' => { 398 | tokens.push(self.parse_date(&mut iter)?); 399 | } 400 | '/' => { 401 | //regex 402 | let mut tmp = String::new(); 403 | iter.next(); 404 | while iter.peek().is_some() && iter.peek().unwrap().ne(&'/') { 405 | tmp.push(iter.next().unwrap()); 406 | } 407 | iter.next(); 408 | tokens.push(Token::Regex(RegexCmp::new(&tmp)?)); 409 | } 410 | '(' => { 411 | tokens.push(Token::OpenBrace); 412 | iter.next(); 413 | } 414 | ')' => { 415 | tokens.push(Token::CloseBrace); 416 | iter.next(); 417 | } 418 | '=' => { 419 | tokens.push(Token::Equal); 420 | iter.next(); 421 | } 422 | '>' => { 423 | iter.next(); 424 | match iter.peek() { 425 | Some(&'=') => { 426 | iter.next(); 427 | tokens.push(Token::GE) 428 | } 429 | _ => tokens.push(Token::Greater), 430 | } 431 | } 432 | '<' => { 433 | iter.next(); 434 | match iter.peek() { 435 | Some(&'=') => { 436 | iter.next(); 437 | tokens.push(Token::LE) 438 | } 439 | _ => tokens.push(Token::Less), 440 | } 441 | } 442 | '!' => { 443 | iter.next(); 444 | match iter.peek() { 445 | Some(&'=') => { 446 | iter.next(); 447 | tokens.push(Token::NE) 448 | } 449 | Some(&c) => return Err(ParseError::UnexpectedChar(c)), 450 | _ => return Err(ParseError::UnexpectedEndOfInput), 451 | } 452 | } 453 | ' ' => { 454 | iter.next(); 455 | } 456 | c => return Err(ParseError::UnexpectedChar(c)), 457 | }, 458 | None => break, 459 | } 460 | } 461 | 462 | Ok(tokens) 463 | } 464 | 465 | fn compile_value( 466 | &self, 467 | iter: &mut Peekable>, 468 | allow_reg: bool, 469 | ) -> Result { 470 | match iter.peek() { 471 | Some(Token::String(value)) => { 472 | iter.next(); 473 | Ok(Token::String(value.clone())) 474 | } 475 | Some(Token::Number(value)) => { 476 | iter.next(); 477 | Ok(Token::Number(value.clone())) 478 | } 479 | Some(Token::Regex(value)) if allow_reg => { 480 | iter.next(); 481 | Ok(Token::Regex(value.clone())) 482 | } 483 | Some(Token::Date(value)) => { 484 | iter.next(); 485 | Ok(Token::Date(value.clone())) 486 | } 487 | Some(&t) => Err(ParseError::UnexpectedToken(t.clone())), 488 | None => Err(ParseError::UnexpectedEndOfInput), 489 | } 490 | } 491 | 492 | fn compile_condition(&self, iter: &mut Peekable>) -> Result { 493 | match iter.peek() { 494 | Some(Token::OpenBrace) => { 495 | iter.next(); 496 | let expr = self.compile_expression(iter); 497 | iter.next(); 498 | expr 499 | } 500 | Some(Token::Identifier(ident)) => { 501 | let left = Token::Identifier(ident.clone()); 502 | iter.next(); 503 | match iter.peek() { 504 | Some(Token::Equal) => { 505 | iter.next(); 506 | Ok(Query::Equal(left, self.compile_value(iter, true)?)) 507 | } 508 | Some(Token::Greater) => { 509 | iter.next(); 510 | Ok(Query::Greater(left, self.compile_value(iter, false)?)) 511 | } 512 | Some(Token::Less) => { 513 | iter.next(); 514 | Ok(Query::Less(left, self.compile_value(iter, false)?)) 515 | } 516 | Some(Token::GE) => { 517 | iter.next(); 518 | Ok(Query::GE(left, self.compile_value(iter, false)?)) 519 | } 520 | Some(Token::LE) => { 521 | iter.next(); 522 | Ok(Query::LE(left, self.compile_value(iter, false)?)) 523 | } 524 | Some(Token::NE) => { 525 | iter.next(); 526 | Ok(Query::NE(left, self.compile_value(iter, false)?)) 527 | } 528 | Some(&t) => Err(ParseError::UnexpectedToken(t.clone())), 529 | _ => Err(ParseError::UnexpectedEndOfInput), 530 | } 531 | } 532 | Some(&t) => Err(ParseError::UnexpectedToken(t.clone())), 533 | None => Err(ParseError::UnexpectedEndOfInput), 534 | } 535 | } 536 | 537 | fn compile_term(&self, iter: &mut Peekable>) -> Result { 538 | let mut ast = self.compile_condition(iter)?; 539 | while let Some(Token::OR) = iter.peek() { 540 | iter.next(); 541 | ast = Query::Or(Box::new(ast), Box::new(self.compile_condition(iter)?)); 542 | } 543 | Ok(ast) 544 | } 545 | 546 | fn compile_expression(&self, iter: &mut Peekable>) -> Result { 547 | let mut ast = self.compile_term(iter)?; 548 | while let Some(Token::AND) = iter.peek() { 549 | iter.next(); 550 | ast = Query::And(Box::new(ast), Box::new(self.compile_term(iter)?)); 551 | } 552 | Ok(ast) 553 | } 554 | 555 | pub(crate) fn compile(&self, program: &str) -> Result { 556 | let tokens = self.tokenize(program)?; 557 | let mut iter = tokens.iter().peekable(); 558 | let mut ast = Query::Expr(None, None); 559 | while iter.peek().is_some() { 560 | match iter.next() { 561 | Some(Token::WHERE) => { 562 | if let Query::Expr(left, _) = &mut ast { 563 | *left = Some(Box::new(self.compile_expression(&mut iter)?)); 564 | } 565 | } 566 | Some(Token::Regex(regex)) => { 567 | ast = Query::Regex(regex.clone()); 568 | if let Some(token) = iter.next() { 569 | return Err(ParseError::UnexpectedToken(token.clone())); 570 | } 571 | } 572 | Some(other) => return Err(ParseError::UnexpectedToken(other.clone())), 573 | None => return Err(ParseError::UnexpectedEndOfInput), 574 | } 575 | } 576 | 577 | Ok(ast) 578 | } 579 | } 580 | 581 | #[test] 582 | fn test_tokenizer() { 583 | let compiler = Compiler::new(); 584 | let tokens = compiler 585 | .tokenize("WHERE date > 'now' AND date < 'now-1d'") 586 | .unwrap(); 587 | dbg!(tokens); 588 | } 589 | 590 | #[test] 591 | fn compile_regex() { 592 | let compiler = Compiler::new(); 593 | let query = compiler.compile("/John/").unwrap(); 594 | dbg!(query); 595 | } 596 | 597 | #[test] 598 | fn test_regex_tokenize() { 599 | let compiler = Compiler::new(); 600 | let tokens = compiler 601 | .tokenize("WHERE name = /John/ AND age > 20") 602 | .unwrap(); 603 | assert!(matches!(tokens[3], Token::Regex(_))); 604 | } 605 | -------------------------------------------------------------------------------- /src/parser/fields.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{FieldMap, Value}; 2 | use std::{borrow::Cow, cell::Cell}; 3 | 4 | #[derive(Clone, Copy)] 5 | enum ParseState { 6 | StartLogLine, 7 | Duration, 8 | EventField, 9 | Undefined, 10 | Key, 11 | Value, 12 | Finish, 13 | } 14 | 15 | #[derive(PartialEq)] 16 | enum ParseValueState { 17 | BeginParse, 18 | ReadValueUntil(u8), 19 | ReadValueToNext, 20 | Finish(u8), 21 | } 22 | 23 | pub struct Fields { 24 | reader: String, 25 | state: Cell, 26 | index: Cell, 27 | } 28 | 29 | impl Fields { 30 | pub fn new(reader: String) -> Self { 31 | Fields { 32 | reader, 33 | state: Cell::new(ParseState::StartLogLine), 34 | index: Cell::new(0), 35 | } 36 | } 37 | 38 | pub fn current(&self) -> usize { 39 | self.index.get() 40 | } 41 | 42 | fn read_until(&self, find: u8) -> Option<&str> { 43 | let begin = self.index.get(); 44 | let mut size = 0 as usize; 45 | while let Some(byte) = self.read_byte() { 46 | size += 1; 47 | 48 | if byte == find { 49 | break; 50 | } 51 | } 52 | 53 | let size = size.saturating_sub(1); 54 | match size { 55 | 0 => None, 56 | _ => Some(&self.reader[begin..(begin + size)]), 57 | } 58 | } 59 | 60 | fn read_byte(&self) -> Option { 61 | if self.index.get() == self.reader.len() { 62 | return None; 63 | } 64 | 65 | self.index 66 | .set(self.index.get().saturating_add(1).min(self.reader.len())); 67 | Some(self.reader.as_bytes()[self.index.get() - 1]) 68 | } 69 | 70 | fn read_value(&self) -> Option<&str> { 71 | let mut value = ""; 72 | let mut value_state = ParseValueState::BeginParse; 73 | 74 | loop { 75 | match value_state { 76 | ParseValueState::BeginParse => match self.read_byte() { 77 | Some(char) if char == b'\r' || char == b'\n' || char == b',' => { 78 | value = ""; 79 | value_state = ParseValueState::Finish(char); 80 | } 81 | Some(char) if char == b'\'' || char == b'"' => { 82 | value_state = ParseValueState::ReadValueUntil(char); 83 | } 84 | Some(_) => { 85 | value_state = ParseValueState::ReadValueToNext; 86 | } 87 | None => unreachable!(), 88 | }, 89 | ParseValueState::ReadValueUntil(quote) => { 90 | let begin = self.current(); 91 | while let Some(char) = self.read_byte() { 92 | match char { 93 | b'\'' | b'"' if char == quote => { 94 | let end = self.current().saturating_sub(1); 95 | let read = self.read_byte(); 96 | match read { 97 | Some(byte) if char == byte => continue, 98 | _ => {} 99 | }; 100 | 101 | value = &self.reader[begin..end]; 102 | value_state = ParseValueState::Finish(read.unwrap()); 103 | break; 104 | } 105 | _ => {} 106 | } 107 | } 108 | } 109 | ParseValueState::ReadValueToNext => { 110 | let begin = self.current().saturating_sub(1); 111 | while let Some(char) = self.read_byte() { 112 | match char { 113 | b'\r' | b'\n' | b',' => { 114 | value = &self.reader[begin..self.current().saturating_sub(1)]; 115 | value_state = ParseValueState::Finish(char); 116 | break; 117 | } 118 | _ => {} 119 | } 120 | } 121 | } 122 | ParseValueState::Finish(char) => { 123 | match char { 124 | b'\r' => { 125 | self.read_byte()?; //read n 126 | self.state.set(ParseState::Finish); 127 | } 128 | b'\n' => { 129 | self.state.set(ParseState::Finish); 130 | } 131 | b',' => { 132 | self.state.set(ParseState::Key); 133 | } 134 | _ => unreachable!(), 135 | } 136 | break; 137 | } 138 | } 139 | } 140 | 141 | Some(value) 142 | } 143 | 144 | pub fn parse_field<'a>(&'a self) -> Option<(Cow<'a, str>, &str)> { 145 | let mut key = ""; 146 | let value; 147 | 148 | loop { 149 | match self.state.get() { 150 | ParseState::StartLogLine => { 151 | let value = self.read_until(b'-')?; 152 | self.state.set(ParseState::Duration); 153 | return Some((Cow::Borrowed("time"), value)); 154 | } 155 | ParseState::Duration => { 156 | let value = self.read_until(b',')?; 157 | self.state.set(ParseState::EventField); 158 | return Some((Cow::Borrowed("duration"), value)); 159 | } 160 | ParseState::EventField => { 161 | let value = self.read_until(b',')?; 162 | self.state.set(ParseState::Undefined); 163 | return Some((Cow::Borrowed("event"), value)); 164 | } 165 | ParseState::Undefined => { 166 | let _ = self.read_until(b',')?; 167 | self.state.set(ParseState::Key); 168 | } 169 | ParseState::Key => { 170 | key = self.read_until(b'=')?; 171 | self.state.set(ParseState::Value); 172 | } 173 | ParseState::Value => { 174 | value = self.read_value()?; 175 | return Some((Cow::Borrowed(key), value)); 176 | } 177 | ParseState::Finish => { 178 | self.state.set(ParseState::StartLogLine); 179 | break; 180 | } 181 | } 182 | } 183 | 184 | None 185 | } 186 | 187 | pub fn iter(&self) -> Iter<'_> { 188 | Iter { inner: self } 189 | } 190 | } 191 | 192 | pub struct Iter<'a> { 193 | inner: &'a Fields, 194 | } 195 | 196 | impl<'a> Iterator for Iter<'a> { 197 | type Item = (Cow<'a, str>, &'a str); 198 | 199 | fn next(&mut self) -> Option { 200 | self.inner.parse_field() 201 | } 202 | } 203 | 204 | impl From for FieldMap<'static> { 205 | fn from(iter: Fields) -> Self { 206 | let mut map = FieldMap::new(); 207 | while let Some((k, v)) = iter.parse_field() { 208 | if k == "time" { 209 | continue; 210 | } 211 | map.insert(k.to_string(), Value::from(v.to_string())) 212 | } 213 | map 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/parser/logdata.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parser::LogString, 3 | ui::{index::ModelIndex, model::DataModel}, 4 | }; 5 | use std::{ 6 | borrow::Cow, 7 | sync::{mpsc::Receiver, Arc, RwLock}, 8 | }; 9 | 10 | use crate::parser::{compiler::ParseError, value::Value, Compiler, FieldMap, Fields, Query}; 11 | use std::{ 12 | sync::{ 13 | mpsc::{Sender, TryRecvError}, 14 | Mutex, RwLockReadGuard, RwLockWriteGuard, 15 | }, 16 | time::Duration, 17 | }; 18 | 19 | struct Inner { 20 | lines: Vec, 21 | filter: Option, 22 | mapping: Vec, 23 | notifier: Mutex>>, 24 | } 25 | 26 | impl Inner { 27 | fn accept_row(&self, row: usize) -> bool { 28 | let line = match self.lines.get(row) { 29 | Some(line) => line, 30 | _ => unreachable!(), 31 | }; 32 | 33 | if let Some(filter) = &self.filter { 34 | let mut map = FieldMap::new(); 35 | let iter = Fields::new(line.to_string()); 36 | while let Some((k, v)) = iter.parse_field() { 37 | map.insert(k, Value::from(v)) 38 | } 39 | return filter.accept(&map); 40 | } 41 | 42 | // Когда фильтр не указан, то строку принимаем всегда 43 | true 44 | } 45 | } 46 | 47 | pub struct LogCollection(Arc>); 48 | 49 | impl Clone for LogCollection { 50 | fn clone(&self) -> Self { 51 | LogCollection(self.0.clone()) 52 | } 53 | } 54 | 55 | impl LogCollection { 56 | pub fn new(receiver: Receiver) -> LogCollection { 57 | let (notifier, rx) = std::sync::mpsc::channel(); 58 | let this = LogCollection(Arc::new(RwLock::new(Inner { 59 | lines: vec![], 60 | filter: None, 61 | mapping: vec![], 62 | notifier: Mutex::new(notifier), 63 | }))); 64 | 65 | let this_cloned = this.clone(); 66 | std::thread::spawn(move || { 67 | while let Ok(data) = receiver.recv() { 68 | this_cloned.inner_mut().lines.push(data); 69 | } 70 | }); 71 | 72 | let this_cloned = this.clone(); 73 | std::thread::spawn(move || { 74 | let mut row = 0; 75 | loop { 76 | match rx.try_recv() { 77 | Ok(filter) => { 78 | let mut write = this_cloned.inner_mut(); 79 | write.filter = filter; 80 | write.mapping.clear(); 81 | row = 0; 82 | } 83 | Err(TryRecvError::Disconnected) => { 84 | break; 85 | } 86 | _ => {} 87 | } 88 | 89 | let rows = this_cloned.inner().lines.len(); 90 | if row >= rows { 91 | std::thread::sleep(Duration::from_millis(100)); 92 | continue; 93 | } 94 | 95 | let accept = this_cloned.inner().accept_row(row); 96 | if accept { 97 | this_cloned.inner_mut().mapping.push(row) 98 | } 99 | 100 | row += 1; 101 | } 102 | }); 103 | 104 | this 105 | } 106 | 107 | pub fn set_filter(&self, filter: String) -> Result<(), ParseError> { 108 | if filter.trim().is_empty() { 109 | self.inner_mut() 110 | .notifier 111 | .lock() 112 | .unwrap() 113 | .send(None) 114 | .unwrap(); 115 | return Ok(()); 116 | } 117 | 118 | let current = self.inner().filter.clone(); 119 | match Compiler::new().compile(filter.as_str()) { 120 | Ok(filter) => { 121 | if current.is_none() || current.unwrap() != filter { 122 | self.inner_mut() 123 | .notifier 124 | .lock() 125 | .unwrap() 126 | .send(Some(filter)) 127 | .unwrap(); 128 | } 129 | 130 | Ok(()) 131 | } 132 | Err(e) => Err(e), 133 | } 134 | } 135 | 136 | pub fn line(&self, row: usize) -> Option { 137 | let this = self.inner(); 138 | this.mapping 139 | .get(row) 140 | .and_then(|i| this.lines.get(*i)) 141 | .cloned() 142 | } 143 | 144 | fn inner(&self) -> RwLockReadGuard<'_, Inner> { 145 | self.0.read().unwrap() 146 | } 147 | 148 | fn inner_mut(&self) -> RwLockWriteGuard<'_, Inner> { 149 | self.0.write().unwrap() 150 | } 151 | } 152 | 153 | impl DataModel for LogCollection { 154 | fn rows(&self) -> usize { 155 | self.inner().mapping.len() 156 | } 157 | 158 | fn cols(&self) -> usize { 159 | 5 160 | } 161 | 162 | fn header_index(&self, name: &str) -> Option { 163 | match name { 164 | "time" => Some(0), 165 | "event" => Some(1), 166 | "duration" => Some(2), 167 | "process" => Some(3), 168 | "OSThread" => Some(4), 169 | _ => None, 170 | } 171 | } 172 | 173 | fn header_data(&self, column: usize) -> Option> { 174 | match column { 175 | 0 => Some(Cow::Borrowed("time")), 176 | 1 => Some(Cow::Borrowed("event")), 177 | 2 => Some(Cow::Borrowed("duration")), 178 | 3 => Some(Cow::Borrowed("process")), 179 | 4 => Some(Cow::Borrowed("OSThread")), 180 | _ => None, 181 | } 182 | } 183 | 184 | fn data(&self, index: ModelIndex) -> Option> { 185 | let this = self.inner(); 186 | let line = this.mapping.get(index.row()); 187 | 188 | match (line, index.column()) { 189 | (Some(&line), 0) => Some( 190 | this.lines 191 | .get(line) 192 | .unwrap() 193 | .get("time") 194 | .unwrap_or_default(), 195 | ), 196 | (Some(&line), 1) => Some( 197 | this.lines 198 | .get(line) 199 | .unwrap() 200 | .get("event") 201 | .unwrap_or_default(), 202 | ), 203 | (Some(&line), 2) => Some( 204 | this.lines 205 | .get(line) 206 | .unwrap() 207 | .get("duration") 208 | .unwrap_or_default(), 209 | ), 210 | (Some(&line), 3) => Some( 211 | this.lines 212 | .get(line) 213 | .unwrap() 214 | .get("process") 215 | .unwrap_or_default(), 216 | ), 217 | (Some(&line), 4) => Some( 218 | this.lines 219 | .get(line) 220 | .unwrap() 221 | .get("OSThread") 222 | .unwrap_or_default(), 223 | ), 224 | _ => None, 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parser::buffers::{add_buffer, get_buffer}, 3 | util::parse_time, 4 | }; 5 | use chrono::{NaiveDate, NaiveDateTime, Timelike}; 6 | pub use compiler::{Compiler, Query}; 7 | pub use fields::*; 8 | use indexmap::IndexMap; 9 | use std::{ 10 | borrow::Cow, 11 | fs::OpenOptions, 12 | io, 13 | io::{BufReader, Read, Seek, SeekFrom}, 14 | sync::mpsc::{channel, Receiver, Sender}, 15 | }; 16 | pub use value::*; 17 | use walkdir::{DirEntry, WalkDir}; 18 | 19 | mod buffers; 20 | mod compiler; 21 | mod fields; 22 | pub mod logdata; 23 | mod value; 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct FieldMap<'a> { 27 | values: IndexMap, Value<'a>>, 28 | } 29 | 30 | impl<'a> FieldMap<'a> { 31 | pub fn new() -> FieldMap<'a> { 32 | FieldMap { 33 | values: IndexMap::with_capacity(16), 34 | } 35 | } 36 | 37 | pub fn insert>>(&mut self, key: T, value: Value<'a>) { 38 | let key = key.into(); 39 | 40 | if let Some(inner) = self.values.get_mut(&key) { 41 | match inner { 42 | Value::MultiValue(arr) => arr.push(value), 43 | _ => *inner = Value::MultiValue(vec![inner.clone(), value]), 44 | } 45 | } else { 46 | self.values.insert(key, value); 47 | } 48 | } 49 | 50 | pub fn iter(&self) -> impl Iterator { 51 | self.values 52 | .iter() 53 | .flat_map(|(a, b)| b.iter().map(|b| (a.as_ref(), b))) 54 | } 55 | 56 | pub fn get(&self, name: impl AsRef) -> Option<&Value> { 57 | self.values.get(name.as_ref()) 58 | } 59 | 60 | pub fn get_index(&self, index: usize) -> Option<(String, &Value)> { 61 | let mut inner_index = 0; 62 | for value in self.values.iter() { 63 | if inner_index + value.1.len() > index { 64 | inner_index = index - inner_index; 65 | return Some((value.0.to_string(), &value.1[inner_index])); 66 | } 67 | 68 | inner_index += value.1.len(); 69 | } 70 | None 71 | } 72 | 73 | pub fn len(&self) -> usize { 74 | self.values.iter().map(|(_, v)| v).map(Value::len).sum() 75 | } 76 | } 77 | 78 | #[derive(Debug, Clone)] 79 | pub struct LogString { 80 | buffer: usize, 81 | time: NaiveDateTime, 82 | begin: u64, 83 | size: u64, 84 | } 85 | 86 | impl LogString { 87 | pub fn new(buffer: usize, time: NaiveDateTime, begin: u64, size: u64) -> Self { 88 | Self { 89 | buffer, 90 | time, 91 | begin, 92 | size, 93 | } 94 | } 95 | 96 | #[inline] 97 | pub fn begin(&self) -> u64 { 98 | self.begin 99 | } 100 | 101 | #[inline] 102 | pub fn len(&self) -> usize { 103 | self.size as usize 104 | } 105 | 106 | pub fn fields(&self) -> Fields { 107 | Fields::new(self.to_string()) 108 | } 109 | 110 | pub fn get(&self, name: &str) -> Option> { 111 | match name { 112 | "time" => Some(Value::DateTime(self.time)), 113 | _ => { 114 | let f = self.fields(); 115 | f.iter() 116 | .find(|(k, _)| k == name) 117 | .map(|(_, v)| Value::from(v.to_string())) 118 | } 119 | } 120 | } 121 | } 122 | 123 | impl ToString for LogString { 124 | fn to_string(&self) -> String { 125 | let buffer = get_buffer(self.buffer); 126 | let mut lock = buffer.lock().unwrap(); 127 | lock.seek(SeekFrom::Start(self.begin() + 3)).unwrap(); 128 | 129 | let mut data = vec![0; self.len()]; 130 | lock.read_exact(&mut data).unwrap(); 131 | unsafe { String::from_utf8_unchecked(data) } 132 | } 133 | } 134 | 135 | pub struct LogParser; 136 | 137 | impl LogParser { 138 | pub fn parse(dir: String, date: Option) -> Receiver { 139 | let (sender, receiver) = channel(); 140 | std::thread::spawn(move || LogParser::parse_dir(dir, date, sender)); 141 | receiver 142 | } 143 | 144 | // А может сделать итератор, который парсит 145 | fn parse_dir( 146 | path: String, 147 | date: Option, 148 | sender: Sender, 149 | ) -> io::Result<()> { 150 | let walk = WalkDir::new(path) 151 | .follow_links(true) 152 | .into_iter() 153 | .filter_map(Result::ok) 154 | .filter(|e| { 155 | !e.file_type().is_dir() && e.file_name().to_string_lossy().ends_with(".log") 156 | }); 157 | 158 | let hour_date = date.map(|date| NaiveDate::from(date.date()).and_hms(date.hour(), 0, 0)); 159 | let regex = regex::Regex::new(r#"^\d{8}[.]log$"#).unwrap(); 160 | let mut files = walk 161 | .filter_map(|e| { 162 | let name = e.file_name().to_string_lossy().to_string(); 163 | if regex.is_match(&name) { 164 | let year = 2000 + name[0..2].parse::().unwrap(); 165 | let month = name[2..4].parse::().unwrap(); 166 | let day = name[4..6].parse::().unwrap(); 167 | let hour = name[6..8].parse::().unwrap(); 168 | 169 | let date_time = NaiveDate::from_ymd(year, month, day).and_hms(hour, 0, 0); 170 | match hour_date { 171 | Some(hour_date) if date_time < hour_date => None, 172 | _ => Some((e, date_time)), 173 | } 174 | } else { 175 | None 176 | } 177 | }) 178 | .collect::>(); 179 | 180 | files.sort_by(|(_, name), (_, name2)| name.cmp(name2)); 181 | 182 | let parts = files.into_iter().fold( 183 | Vec::>::new(), 184 | |mut acc, (entry, time)| { 185 | if acc.is_empty() { 186 | acc.push(vec![]); 187 | } else if acc.last().unwrap().is_empty() 188 | || acc.last().unwrap().last().unwrap().1 != time 189 | { 190 | acc.push(vec![]); 191 | } 192 | 193 | acc.last_mut().unwrap().push((entry, time)); 194 | acc 195 | }, 196 | ); 197 | 198 | for part in parts { 199 | let rows = part 200 | .into_iter() 201 | .map(|(entry, time)| { 202 | let mut file = OpenOptions::new().read(true).open(entry.path()).unwrap(); 203 | file.seek(SeekFrom::Start(3)).unwrap(); 204 | let mut data = String::with_capacity(1024 * 30); 205 | file.read_to_string(&mut data).unwrap(); 206 | 207 | (add_buffer(BufReader::new(file)), data, time) 208 | }) 209 | .filter(|(_, data, _)| !data.is_empty()) 210 | .collect::>(); 211 | 212 | let mut part = rows 213 | .into_iter() 214 | .map(|(buf, data, hour)| (buf, Fields::new(data), hour)) 215 | .collect::>(); 216 | 217 | let mut lines = vec![None; part.len()]; 218 | loop { 219 | for (index, (buffer, data, hour)) in part.iter_mut().enumerate() { 220 | if lines[index].is_some() { 221 | continue; 222 | } 223 | 224 | loop { 225 | let begin = data.current() as u64; 226 | match data.parse_field() { 227 | Some((key, value)) if key == "time" => { 228 | let time = parse_time(*hour, &value); 229 | match date { 230 | Some(date) if time < date => {} 231 | _ => { 232 | while let Some(_) = data.parse_field() {} 233 | let end = data.current() as u64; 234 | 235 | let line = 236 | LogString::new(*buffer, time, begin, end - begin); 237 | lines[index] = Some(line); 238 | break; 239 | } 240 | } 241 | } 242 | Some(_) => unreachable!(), 243 | None => break, 244 | } 245 | } 246 | } 247 | 248 | let min = lines 249 | .iter() 250 | .enumerate() 251 | .filter_map(|(index, value)| { 252 | if let Some(value) = value.as_ref() { 253 | Some((index, value)) 254 | } else { 255 | None 256 | } 257 | }) 258 | .min_by(|(_, value1), (_, value2)| { 259 | value1 260 | .get("time") 261 | .unwrap() 262 | .partial_cmp(&value2.get("time").unwrap()) 263 | .unwrap() 264 | }) 265 | .map(|(index, _)| index); 266 | 267 | if lines.iter().all(Option::is_none) { 268 | break; 269 | } 270 | 271 | if let Some(min) = min { 272 | let mut tmp = None; 273 | std::mem::swap(&mut lines[min], &mut tmp); 274 | sender.send(tmp.unwrap()).unwrap() 275 | } 276 | } 277 | } 278 | 279 | Ok(()) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/parser/value.rs: -------------------------------------------------------------------------------- 1 | use chrono::NaiveDateTime; 2 | use std::{borrow::Cow, fmt::Display, ops::Index}; 3 | 4 | #[derive(Debug, Clone)] 5 | pub enum Value<'a> { 6 | String(Cow<'a, str>), 7 | Number(f64), 8 | DateTime(NaiveDateTime), 9 | MultiValue(Vec>), 10 | } 11 | 12 | impl<'a> Default for Value<'a> { 13 | fn default() -> Self { 14 | Value::String(Cow::Owned(String::new())) 15 | } 16 | } 17 | 18 | impl<'a> Value<'a> { 19 | pub fn len(&self) -> usize { 20 | match self { 21 | Value::MultiValue(arr) => arr.len(), 22 | _ => 1, 23 | } 24 | } 25 | 26 | pub fn iter(&self) -> Box + '_> { 27 | match self { 28 | Value::MultiValue(arr) => Box::new(arr.iter()), 29 | _ => Box::new(std::iter::repeat(self).take(1)), 30 | } 31 | } 32 | } 33 | 34 | impl<'a> Index for Value<'a> { 35 | type Output = Value<'a>; 36 | 37 | fn index(&self, index: usize) -> &Self::Output { 38 | match self { 39 | Value::MultiValue(arr) => &arr[index], 40 | _ => self, 41 | } 42 | } 43 | } 44 | 45 | impl<'a> From<&'a str> for Value<'a> { 46 | fn from(string: &'a str) -> Self { 47 | if let Ok(value) = string.parse::() { 48 | Self::Number(value) 49 | } else { 50 | Self::String(Cow::from(string)) 51 | } 52 | } 53 | } 54 | 55 | impl<'a> From for Value<'a> { 56 | fn from(string: String) -> Self { 57 | if let Ok(value) = string.as_str().parse::() { 58 | Self::Number(value) 59 | } else { 60 | Self::String(Cow::from(string)) 61 | } 62 | } 63 | } 64 | 65 | impl<'a> Display for Value<'a> { 66 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 67 | match self { 68 | Value::String(s) => write!(f, "{}", s), 69 | Value::Number(n) => write!(f, "{}", n), 70 | Value::DateTime(dt) => write!(f, "{}", dt), 71 | Value::MultiValue(arr) => write!(f, "{:?}", arr), 72 | } 73 | } 74 | } 75 | 76 | impl<'a> PartialEq for Value<'a> { 77 | fn eq(&self, other: &Self) -> bool { 78 | match (self, other) { 79 | (Value::String(s1), Value::String(s2)) => s1 == s2, 80 | (Value::Number(n1), Value::Number(n2)) => n1 == n2, 81 | (Value::DateTime(dt1), Value::DateTime(dt2)) => dt1 == dt2, 82 | _ => false, 83 | } 84 | } 85 | } 86 | 87 | impl<'a> PartialOrd for Value<'a> { 88 | fn partial_cmp(&self, other: &Self) -> Option { 89 | match (self, other) { 90 | (Value::String(s1), Value::String(s2)) => s1.partial_cmp(s2), 91 | (Value::Number(n1), Value::Number(n2)) => n1.partial_cmp(n2), 92 | (Value::DateTime(dt1), Value::DateTime(dt2)) => dt1.partial_cmp(dt2), 93 | _ => None, 94 | } 95 | } 96 | } 97 | 98 | impl<'a> PartialEq for Value<'a> { 99 | fn eq(&self, other: &String) -> bool { 100 | match self { 101 | Value::String(s) => s.as_ref() == other, 102 | _ => false, 103 | } 104 | } 105 | } 106 | 107 | impl<'a> PartialOrd for Value<'a> { 108 | fn partial_cmp(&self, other: &String) -> Option { 109 | match self { 110 | Value::String(s) => s.as_ref().partial_cmp(other), 111 | _ => None, 112 | } 113 | } 114 | } 115 | 116 | impl<'a> PartialEq for Value<'a> { 117 | fn eq(&self, other: &f64) -> bool { 118 | match self { 119 | Value::Number(n) => n == other, 120 | _ => false, 121 | } 122 | } 123 | } 124 | 125 | impl<'a> PartialOrd for Value<'a> { 126 | fn partial_cmp(&self, other: &f64) -> Option { 127 | match self { 128 | Value::Number(n) => n.partial_cmp(other), 129 | _ => None, 130 | } 131 | } 132 | } 133 | 134 | impl<'a> PartialEq for Value<'a> { 135 | fn eq(&self, other: &NaiveDateTime) -> bool { 136 | match self { 137 | Value::DateTime(dt) => dt == other, 138 | _ => false, 139 | } 140 | } 141 | } 142 | 143 | impl<'a> PartialOrd for Value<'a> { 144 | fn partial_cmp(&self, other: &NaiveDateTime) -> Option { 145 | match self { 146 | Value::DateTime(dt) => dt.partial_cmp(other), 147 | _ => None, 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/ui/index.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default)] 2 | pub struct ModelIndex(usize, usize); 3 | 4 | impl ModelIndex { 5 | pub fn new(row: usize, col: usize) -> Self { 6 | ModelIndex(row, col) 7 | } 8 | 9 | pub fn row(&self) -> usize { 10 | self.0 11 | } 12 | 13 | pub fn column(&self) -> usize { 14 | self.1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod index; 2 | pub mod model; 3 | pub mod widgets; 4 | -------------------------------------------------------------------------------- /src/ui/model.rs: -------------------------------------------------------------------------------- 1 | use crate::{parser::Value, ui::index::ModelIndex}; 2 | use std::{any::Any, borrow::Cow, fmt::Display}; 3 | use tui::text::Text; 4 | 5 | #[derive(Default)] 6 | pub struct Column<'a> { 7 | pub text: Text<'a>, 8 | } 9 | 10 | pub trait DataModel { 11 | fn rows(&self) -> usize; 12 | 13 | fn cols(&self) -> usize; 14 | 15 | fn header_index(&self, name: &str) -> Option; 16 | 17 | fn header_data(&self, column: usize) -> Option>; 18 | 19 | fn data(&self, index: ModelIndex) -> Option; 20 | 21 | fn as_any(&self) -> &dyn Any { 22 | &() 23 | } 24 | } 25 | 26 | impl DataModel for Vec { 27 | fn rows(&self) -> usize { 28 | self.len() 29 | } 30 | 31 | fn cols(&self) -> usize { 32 | 1 33 | } 34 | 35 | fn header_index(&self, _name: &str) -> Option { 36 | None 37 | } 38 | 39 | fn header_data(&self, _column: usize) -> Option> { 40 | None 41 | } 42 | 43 | fn data(&self, index: ModelIndex) -> Option> { 44 | self.get(index.row()).map(|s| Value::from(s.to_string())) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ui/widgets/info.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | parser::{FieldMap, Value}, 3 | ui::widgets::WidgetExt, 4 | util::sub_strings, 5 | }; 6 | use cli_clipboard::{ClipboardContext, ClipboardProvider}; 7 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 8 | use std::{fmt::Debug, mem}; 9 | use tui::{ 10 | buffer::Buffer, 11 | layout::{Constraint, Direction, Layout, Rect}, 12 | style::{Color, Style}, 13 | widgets::{Block, Borders, Widget}, 14 | }; 15 | 16 | struct State { 17 | pub offset: usize, 18 | pub index: usize, 19 | pub rows_size: Vec, 20 | } 21 | 22 | impl Debug for State { 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | write!( 25 | f, 26 | "offset: {}, index: {}, row_size: {:?}", 27 | self.offset, self.index, self.rows_size 28 | ) 29 | } 30 | } 31 | 32 | impl Default for State { 33 | fn default() -> Self { 34 | State { 35 | offset: 0, 36 | index: 0, 37 | rows_size: Vec::new(), 38 | } 39 | } 40 | } 41 | 42 | pub struct KeyValueView { 43 | state: State, 44 | data: FieldMap<'static>, 45 | 46 | focused: bool, 47 | visible: bool, 48 | 49 | width: u16, 50 | height: u16, 51 | 52 | on_add_to_filter: Box, 53 | } 54 | 55 | impl KeyValueView { 56 | pub fn new() -> Self { 57 | Self { 58 | state: State::default(), 59 | data: FieldMap::new(), 60 | focused: false, 61 | visible: false, 62 | width: 0, 63 | height: 0, 64 | 65 | on_add_to_filter: Box::new(|_| {}), 66 | } 67 | } 68 | 69 | fn calculate_row_bounds(&mut self) { 70 | let offset = self.state.offset.min(self.data.len().saturating_sub(1)); 71 | let inner_height = self.height.saturating_sub(3) as usize; 72 | let mut start = offset; 73 | let mut height = 0; 74 | 75 | for (index, &item) in self.state.rows_size.iter().enumerate().skip(offset) { 76 | height += item; 77 | if index == self.state.index { 78 | break; 79 | } 80 | } 81 | 82 | while height > inner_height { 83 | height = height.saturating_sub(self.state.rows_size[start]); 84 | start += 1; 85 | } 86 | 87 | self.state.offset = start.min(self.state.index); 88 | } 89 | 90 | fn next(&mut self) { 91 | self.state.index = self 92 | .state 93 | .index 94 | .saturating_add(1) 95 | .min(self.data.len().saturating_sub(1)); 96 | self.calculate_row_bounds(); 97 | } 98 | 99 | fn prev(&mut self) { 100 | self.state.index = self.state.index.saturating_sub(1); 101 | self.calculate_row_bounds(); 102 | } 103 | 104 | pub fn update_state(&mut self) { 105 | let rects = Layout::default() 106 | .constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref()) 107 | .direction(Direction::Horizontal) 108 | .split(Rect { 109 | x: 1, 110 | y: 1, 111 | width: self.width.saturating_sub(1), 112 | height: self.height.saturating_sub(1), 113 | }); 114 | 115 | for (_, v) in self.data.iter() { 116 | let v = v.to_string(); 117 | let splits = sub_strings(v.as_str(), rects[1].width as usize); 118 | self.state.rows_size.push(splits.len().max(1)); 119 | } 120 | } 121 | 122 | pub fn set_data(&mut self, data: FieldMap<'static>) { 123 | self.data = data; 124 | 125 | self.state.rows_size.clear(); 126 | self.state.offset = 0; 127 | self.state.index = 0; 128 | 129 | self.update_state(); 130 | } 131 | 132 | pub fn widget(&self) -> impl Widget + '_ { 133 | Renderer(&self) 134 | } 135 | 136 | pub fn on_add_to_filter(&mut self, callback: impl FnMut((String, &Value)) + 'static) { 137 | self.on_add_to_filter = Box::new(callback); 138 | } 139 | 140 | fn emit_add_to_filter(&mut self) { 141 | let mut on_add_to_filter = mem::replace(&mut self.on_add_to_filter, Box::new(|_| {})); 142 | on_add_to_filter(self.data.get_index(self.state.index).unwrap()); 143 | self.on_add_to_filter = on_add_to_filter; 144 | } 145 | } 146 | 147 | impl WidgetExt for KeyValueView { 148 | fn set_focus(&mut self, focus: bool) { 149 | self.focused = focus 150 | } 151 | 152 | fn focused(&self) -> bool { 153 | self.focused 154 | } 155 | 156 | fn visible(&self) -> bool { 157 | self.visible 158 | } 159 | 160 | fn set_visible(&mut self, visible: bool) { 161 | self.visible = visible 162 | } 163 | 164 | fn key_press_event(&mut self, event: KeyEvent) { 165 | match event { 166 | KeyEvent { 167 | code: KeyCode::Down, 168 | modifiers: KeyModifiers::NONE, 169 | } => { 170 | self.next(); 171 | } 172 | KeyEvent { 173 | code: KeyCode::Up, 174 | modifiers: KeyModifiers::NONE, 175 | } => { 176 | self.prev(); 177 | } 178 | KeyEvent { 179 | code: KeyCode::Char('c'), 180 | modifiers: KeyModifiers::NONE, 181 | } => { 182 | if let Ok(mut ctx) = ClipboardContext::new() { 183 | if let Some((_, value)) = self.data.get_index(self.state.index) { 184 | if let Ok(_) = ctx.set_contents(value.to_string()) {} 185 | } 186 | } 187 | } 188 | KeyEvent { 189 | code: KeyCode::Char('f'), 190 | modifiers: KeyModifiers::NONE, 191 | } => { 192 | if self.data.len() > 0 { 193 | self.emit_add_to_filter(); 194 | } 195 | } 196 | KeyEvent { 197 | code: KeyCode::PageUp, 198 | modifiers: KeyModifiers::NONE, 199 | } => { 200 | self.state.index = 0; 201 | self.state.offset = 0; 202 | self.calculate_row_bounds(); 203 | } 204 | KeyEvent { 205 | code: KeyCode::PageDown, 206 | modifiers: KeyModifiers::NONE, 207 | } => { 208 | self.state.index = self.data.len().saturating_sub(1); 209 | self.calculate_row_bounds(); 210 | } 211 | _ => {} 212 | } 213 | } 214 | 215 | fn resize(&mut self, width: u16, height: u16) { 216 | self.width = width; 217 | self.height = height; 218 | self.state.rows_size.clear(); 219 | self.update_state(); 220 | self.calculate_row_bounds(); 221 | } 222 | 223 | fn width(&self) -> u16 { 224 | self.width 225 | } 226 | 227 | fn height(&self) -> u16 { 228 | self.height 229 | } 230 | } 231 | 232 | struct Renderer<'a>(&'a KeyValueView); 233 | 234 | impl<'a> Widget for Renderer<'a> { 235 | fn render(self, area: Rect, buf: &mut Buffer) { 236 | if area.area() == 0 { 237 | return; 238 | } 239 | 240 | let block_style = match self.0.focused() { 241 | true => Style::default().fg(Color::LightYellow), 242 | false => Style::default(), 243 | }; 244 | let block = Block::default() 245 | .borders(Borders::ALL) 246 | .border_style(block_style) 247 | .title("Info"); 248 | 249 | let area = { 250 | let inner_area = block.inner(area); 251 | block.render(area, buf); 252 | inner_area 253 | }; 254 | 255 | let rects = Layout::default() 256 | .constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref()) 257 | .direction(Direction::Horizontal) 258 | .split(area); 259 | 260 | // Draw header 261 | if area.area() == 0 { 262 | return; 263 | } 264 | 265 | buf.set_string(rects[0].left(), rects[0].top(), "Name", Style::default()); 266 | buf.set_string(rects[1].left(), rects[1].top(), "Value", Style::default()); 267 | 268 | // Draw key - value pairs 269 | let width = rects[1].width; 270 | let available_height = rects[1].height; 271 | let mut rendered_lines = 1 as u16; 272 | for (i, (k, v)) in self.0.data.iter().enumerate().skip(self.0.state.offset) { 273 | if rendered_lines >= available_height { 274 | break; 275 | } 276 | 277 | let style = if i == self.0.state.index { 278 | Style::default().fg(Color::LightMagenta) 279 | } else { 280 | Style::default() 281 | }; 282 | 283 | buf.set_string( 284 | rects[0].left(), 285 | rects[1].top() + rendered_lines as u16, 286 | k, 287 | style, 288 | ); 289 | 290 | let v = v.to_string(); 291 | let splits = sub_strings(v.as_str(), width as usize); 292 | splits 293 | .iter() 294 | .take(available_height.saturating_sub(rendered_lines) as usize) 295 | .enumerate() 296 | .for_each(|(index, s)| { 297 | buf.set_string( 298 | rects[1].left(), 299 | rects[1].top() + rendered_lines + index as u16, 300 | s, 301 | style, 302 | ); 303 | }); 304 | 305 | rendered_lines += splits.len().max(1) as u16; 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/ui/widgets/lineedit.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::widgets::WidgetExt; 2 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 3 | use std::{cell::RefCell, mem}; 4 | use tui::{ 5 | buffer::Buffer, 6 | layout::Rect, 7 | style::{Color, Modifier, Style}, 8 | text::{Span, Spans}, 9 | widgets::{Block, Borders, Widget}, 10 | }; 11 | 12 | pub struct LineEdit { 13 | name: String, 14 | text: String, 15 | cwp: RefCell<(u16, u16, usize)>, 16 | style: Style, 17 | border_text: String, 18 | 19 | visible: bool, 20 | focus: bool, 21 | 22 | width: u16, 23 | height: u16, 24 | 25 | on_changed: Box, 26 | } 27 | 28 | impl LineEdit { 29 | pub fn new(name: String) -> Self { 30 | LineEdit { 31 | name, 32 | text: String::new(), 33 | cwp: RefCell::new((0, 0, 0)), 34 | style: Style::default(), 35 | border_text: String::new(), 36 | 37 | visible: false, 38 | focus: false, 39 | 40 | width: 0, 41 | height: 0, 42 | 43 | on_changed: Box::new(|_| {}), 44 | } 45 | } 46 | 47 | pub fn text(&self) -> &str { 48 | self.text.as_str() 49 | } 50 | 51 | pub fn set_text(&mut self, text: String) { 52 | self.text = text; 53 | self.scroll_to_end(); 54 | self.emit_on_changed(); 55 | } 56 | 57 | pub fn scroll_to_start(&self) { 58 | let (_, width, _) = *self.cwp.borrow(); 59 | *self.cwp.borrow_mut() = (0, width, 0); 60 | } 61 | 62 | pub fn scroll_to_end(&self) { 63 | let width = self.width().saturating_sub(2); 64 | let cursor = if self.text.len() as u16 > width { 65 | width 66 | } else { 67 | self.text.len() as u16 68 | }; 69 | *self.cwp.borrow_mut() = ( 70 | cursor, 71 | width, 72 | self.text.len().saturating_sub(width as usize), 73 | ); 74 | } 75 | 76 | pub fn scroll(&self, right: bool) { 77 | let (mut cursor, width, mut position) = *self.cwp.borrow(); 78 | if right { 79 | // go forward 80 | if (cursor as usize + position) < self.text.len() { 81 | if cursor.saturating_add(1) >= width { 82 | position = position.saturating_add(1); 83 | } else { 84 | cursor = cursor.saturating_add(1); 85 | } 86 | } 87 | } else { 88 | if position.saturating_sub(1) == position { 89 | cursor = cursor.saturating_sub(1); 90 | } else { 91 | position = position.saturating_sub(1); 92 | } 93 | } 94 | *self.cwp.borrow_mut() = (cursor, width, position); 95 | } 96 | 97 | pub fn widget(&self) -> impl Widget + '_ { 98 | Renderer(self) 99 | } 100 | 101 | pub fn set_style(&mut self, style: Style) { 102 | self.style = style; 103 | } 104 | 105 | #[allow(dead_code)] 106 | pub fn style(&self) -> Style { 107 | self.style 108 | } 109 | 110 | pub fn set_border_text(&mut self, text: String) { 111 | self.border_text = text; 112 | } 113 | 114 | // Events 115 | pub fn on_changed(&mut self, f: F) { 116 | self.on_changed = Box::new(f); 117 | } 118 | 119 | pub fn emit_on_changed(&mut self) { 120 | let mut on_changed = mem::replace(&mut self.on_changed, Box::new(|_| {})); 121 | on_changed(self); 122 | self.on_changed = on_changed; 123 | } 124 | } 125 | 126 | impl WidgetExt for LineEdit { 127 | fn set_focus(&mut self, focus: bool) { 128 | self.focus = focus; 129 | } 130 | 131 | fn focused(&self) -> bool { 132 | self.focus 133 | } 134 | 135 | fn visible(&self) -> bool { 136 | self.visible 137 | } 138 | 139 | fn set_visible(&mut self, visible: bool) { 140 | self.visible = visible; 141 | } 142 | 143 | fn show(&mut self) { 144 | self.set_visible(true); 145 | } 146 | 147 | fn hide(&mut self) { 148 | self.set_visible(false); 149 | } 150 | 151 | fn key_press_event(&mut self, event: KeyEvent) { 152 | match event { 153 | KeyEvent { 154 | code: KeyCode::Char(char), 155 | .. 156 | } => { 157 | let (cursor, _, position) = *self.cwp.borrow(); 158 | self.text.insert(cursor as usize + position, char); 159 | self.scroll(true); 160 | self.emit_on_changed(); 161 | } 162 | KeyEvent { 163 | code: KeyCode::Backspace, 164 | modifiers: KeyModifiers::NONE, 165 | } => { 166 | let (cursor, _, position) = *self.cwp.borrow(); 167 | let index = cursor as usize + position; 168 | if index.saturating_sub(1) != index { 169 | self.text.remove(index - 1); 170 | self.scroll(false); 171 | self.emit_on_changed(); 172 | } 173 | } 174 | KeyEvent { 175 | code: KeyCode::Delete, 176 | modifiers: KeyModifiers::NONE, 177 | } => { 178 | let (cursor, _, position) = *self.cwp.borrow(); 179 | let index = cursor as usize + position; 180 | if index < self.text.len() { 181 | self.text.remove(index); 182 | self.emit_on_changed(); 183 | } 184 | } 185 | KeyEvent { 186 | code: KeyCode::Right, 187 | modifiers: KeyModifiers::NONE, 188 | } => self.scroll(true), 189 | KeyEvent { 190 | code: KeyCode::Left, 191 | modifiers: KeyModifiers::NONE, 192 | } => self.scroll(false), 193 | KeyEvent { 194 | code: KeyCode::Backspace, 195 | modifiers: KeyModifiers::CONTROL, 196 | } => { 197 | self.text.clear(); 198 | self.scroll_to_start(); 199 | self.emit_on_changed(); 200 | } 201 | _ => {} 202 | } 203 | } 204 | 205 | fn resize(&mut self, width: u16, height: u16) { 206 | self.width = width; 207 | self.height = height; 208 | } 209 | 210 | fn width(&self) -> u16 { 211 | self.width 212 | } 213 | 214 | fn height(&self) -> u16 { 215 | self.height 216 | } 217 | } 218 | 219 | struct Renderer<'a>(&'a LineEdit); 220 | 221 | impl<'a> Widget for Renderer<'a> { 222 | fn render(self, area: Rect, buf: &mut Buffer) { 223 | if area.area() == 0 || !self.0.visible() { 224 | return; 225 | } 226 | 227 | let border_text = match !self.0.border_text.is_empty() { 228 | true if self.0.name.is_empty() => self.0.border_text.clone(), 229 | true => { 230 | format!("{} | {}", self.0.name, self.0.border_text) 231 | } 232 | false => self.0.name.clone(), 233 | }; 234 | 235 | let block_style = match self.0.focused() { 236 | true => Style::default().fg(Color::LightYellow), 237 | false => Style::default(), 238 | }; 239 | let block = Block::default() 240 | .borders(Borders::ALL) 241 | .border_style(block_style) 242 | .title(border_text); 243 | 244 | let input_area = { 245 | let inner_area = block.inner(area); 246 | block.render(area, buf); 247 | inner_area 248 | }; 249 | 250 | let (cursor, mut width, position) = *self.0.cwp.borrow(); 251 | if width != input_area.width { 252 | width = input_area.width; 253 | } 254 | 255 | let cursor_pos = position + cursor as usize; 256 | let end_length = width.saturating_sub(cursor_pos as u16) as usize; 257 | 258 | let text = Spans::from(vec![ 259 | Span::raw( 260 | self.0 261 | .text 262 | .chars() 263 | .skip(position) 264 | .take(cursor as usize) 265 | .collect::(), 266 | ), 267 | Span::styled( 268 | self.0 269 | .text 270 | .chars() 271 | .nth(cursor_pos) 272 | .map(String::from) 273 | .unwrap_or(String::from(" ")), 274 | Style::default().add_modifier(Modifier::REVERSED), 275 | ), 276 | Span::raw( 277 | self.0 278 | .text 279 | .chars() 280 | .skip(cursor_pos + 1) 281 | .take(end_length) 282 | .collect::(), 283 | ), 284 | ]); 285 | 286 | buf.set_spans(input_area.x, input_area.y, &text, width); 287 | 288 | *self.0.cwp.borrow_mut() = (cursor, width, position); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/ui/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::KeyEvent; 2 | 3 | mod info; 4 | mod lineedit; 5 | mod table; 6 | 7 | pub use info::*; 8 | pub use lineedit::*; 9 | pub use table::*; 10 | 11 | pub trait WidgetExt { 12 | fn set_focus(&mut self, _focus: bool) {} 13 | 14 | fn focused(&self) -> bool { 15 | false 16 | } 17 | 18 | fn visible(&self) -> bool { 19 | true 20 | } 21 | 22 | fn set_visible(&mut self, _visible: bool) {} 23 | 24 | fn show(&mut self) { 25 | self.set_visible(true) 26 | } 27 | 28 | fn hide(&mut self) { 29 | self.set_visible(false) 30 | } 31 | 32 | fn key_press_event(&mut self, _event: KeyEvent) {} 33 | 34 | fn resize(&mut self, _width: u16, _height: u16) {} 35 | 36 | fn width(&self) -> u16; 37 | 38 | fn height(&self) -> u16; 39 | } 40 | -------------------------------------------------------------------------------- /src/ui/widgets/table.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::{index::ModelIndex, model::DataModel, widgets::WidgetExt}; 2 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 3 | use std::{cell::RefCell, mem, rc::Rc}; 4 | use tui::{ 5 | buffer::Buffer, 6 | layout::{Constraint, Direction, Layout, Rect}, 7 | style::{Color, Style}, 8 | widgets::{Block, Borders, Widget}, 9 | }; 10 | 11 | #[derive(Default)] 12 | struct State { 13 | begin: usize, 14 | index: Option, 15 | } 16 | 17 | impl State { 18 | fn selected(&self) -> Option { 19 | self.index 20 | } 21 | 22 | fn select(&mut self, index: Option) { 23 | self.index = index; 24 | if index.is_none() { 25 | self.begin = 0; 26 | } 27 | } 28 | } 29 | 30 | #[derive(Debug, Copy, Clone)] 31 | pub struct TableViewStyle { 32 | common: Style, 33 | selected_row_style: Style, 34 | header_style: Style, 35 | column_spacing: u16, 36 | } 37 | 38 | impl TableViewStyle { 39 | #[allow(dead_code)] 40 | pub fn common(mut self, style: Style) -> Self { 41 | self.common = style; 42 | self 43 | } 44 | 45 | #[allow(dead_code)] 46 | pub fn selected_row_style(mut self, style: Style) -> Self { 47 | self.selected_row_style = style; 48 | self 49 | } 50 | 51 | #[allow(dead_code)] 52 | pub fn header_style(mut self, style: Style) -> Self { 53 | self.header_style = style; 54 | self 55 | } 56 | } 57 | 58 | impl Default for TableViewStyle { 59 | fn default() -> Self { 60 | TableViewStyle { 61 | common: Style::default(), 62 | selected_row_style: Style::default().bg(Color::White).fg(Color::Black), 63 | header_style: Style::default().bg(Color::Green).fg(Color::Black), 64 | column_spacing: 1, 65 | } 66 | } 67 | } 68 | 69 | pub struct TableView { 70 | state: State, 71 | model: Option>>, 72 | widths: Vec, 73 | style: TableViewStyle, 74 | 75 | visible: bool, 76 | focus: bool, 77 | width: u16, 78 | height: u16, 79 | 80 | on_selection_changed: Box) + 'static>, 81 | } 82 | 83 | impl TableView { 84 | pub fn new(widths: Vec) -> Self { 85 | Self { 86 | state: State::default(), 87 | model: None, 88 | widths, 89 | style: TableViewStyle::default(), 90 | visible: true, 91 | focus: false, 92 | width: 0, 93 | height: 0, 94 | 95 | on_selection_changed: Box::new(|_, _| {}), 96 | } 97 | } 98 | 99 | pub fn set_model(&mut self, model: Rc>) { 100 | self.state = State::default(); 101 | self.model = Some(model); 102 | } 103 | 104 | #[allow(dead_code)] 105 | pub fn style(&self) -> TableViewStyle { 106 | self.style 107 | } 108 | 109 | #[allow(dead_code)] 110 | pub fn set_style(&mut self, style: TableViewStyle) { 111 | self.style = style; 112 | } 113 | 114 | pub fn reset_state(&mut self) { 115 | self.state.select(None); 116 | self.state.begin = 0; 117 | self.update_state(); 118 | self.emit_selection_changed(); 119 | } 120 | 121 | fn update_state(&mut self) { 122 | let index = self.state.index.unwrap_or(0); 123 | let row_count = self.height.saturating_sub(4) as usize; 124 | 125 | if row_count == 0 { 126 | return; 127 | } 128 | 129 | if index > (self.state.begin + row_count) { 130 | self.state.begin = index - row_count; 131 | } else if index < self.state.begin { 132 | self.state.begin = index; 133 | } 134 | } 135 | 136 | pub fn next(&mut self) { 137 | if let Some(model) = self.model.clone() { 138 | let i = self.next_inner(self.state.selected(), model.borrow().rows()); 139 | self.state.select(i); 140 | self.update_state(); 141 | self.emit_selection_changed(); 142 | } 143 | } 144 | 145 | fn next_inner(&mut self, current: Option, length: usize) -> Option { 146 | if length == 0 { 147 | return None; 148 | } 149 | 150 | Some(match current { 151 | Some(i) => { 152 | if i >= length - 1 { 153 | 0 154 | } else { 155 | i + 1 156 | } 157 | } 158 | None => 0, 159 | }) 160 | } 161 | 162 | pub fn prev(&mut self) { 163 | if let Some(model) = self.model.clone() { 164 | let i = self.prev_inner(self.state.selected(), model.borrow().rows()); 165 | self.state.select(i); 166 | self.update_state(); 167 | self.emit_selection_changed(); 168 | } 169 | } 170 | 171 | fn prev_inner(&mut self, current: Option, length: usize) -> Option { 172 | if length == 0 { 173 | return None; 174 | } 175 | 176 | Some(match current { 177 | Some(i) => { 178 | if i == 0 { 179 | length - 1 180 | } else { 181 | i - 1 182 | } 183 | } 184 | None => 0, 185 | }) 186 | } 187 | 188 | pub fn widget(&self) -> impl Widget + '_ { 189 | Renderer(self) 190 | } 191 | 192 | fn get_column_widths(&self, max_width: u16) -> Vec { 193 | let mut constraints = Vec::with_capacity(self.widths.len() * 2); 194 | for constraint in self.widths.iter() { 195 | constraints.push(*constraint); 196 | constraints.push(Constraint::Length(self.style.column_spacing)); 197 | } 198 | 199 | if !self.widths.is_empty() { 200 | constraints.pop(); 201 | } 202 | 203 | let chunks = Layout::default() 204 | .direction(Direction::Horizontal) 205 | .constraints(constraints) 206 | .split(Rect { 207 | x: 0, 208 | y: 0, 209 | width: max_width, 210 | height: 1, 211 | }); 212 | 213 | chunks.iter().step_by(2).map(|c| c.width).collect() 214 | } 215 | 216 | pub fn on_selection_changed( 217 | &mut self, 218 | callback: impl FnMut(&mut Self, Option) + 'static, 219 | ) { 220 | self.on_selection_changed = Box::new(callback); 221 | } 222 | 223 | pub fn emit_selection_changed(&mut self) { 224 | let mut on_selection_changed = 225 | mem::replace(&mut self.on_selection_changed, Box::new(|_, _| {})); 226 | on_selection_changed(self, self.state.index); 227 | self.on_selection_changed = on_selection_changed; 228 | } 229 | 230 | fn rows(&self) -> usize { 231 | if let Some(model) = self.model.clone() { 232 | model.borrow().rows() 233 | } else { 234 | 0 235 | } 236 | } 237 | } 238 | 239 | impl WidgetExt for TableView { 240 | fn set_focus(&mut self, focus: bool) { 241 | self.focus = focus; 242 | } 243 | 244 | fn focused(&self) -> bool { 245 | self.focus 246 | } 247 | 248 | fn visible(&self) -> bool { 249 | self.visible 250 | } 251 | 252 | fn set_visible(&mut self, visible: bool) { 253 | self.visible = visible; 254 | } 255 | 256 | fn show(&mut self) { 257 | self.set_visible(true) 258 | } 259 | 260 | fn hide(&mut self) { 261 | self.set_visible(false) 262 | } 263 | 264 | fn key_press_event(&mut self, event: KeyEvent) { 265 | match event { 266 | KeyEvent { 267 | code: KeyCode::Up, 268 | modifiers: KeyModifiers::NONE, 269 | } => self.prev(), 270 | KeyEvent { 271 | code: KeyCode::Down, 272 | modifiers: KeyModifiers::NONE, 273 | } => self.next(), 274 | KeyEvent { 275 | code: KeyCode::PageUp, 276 | modifiers: KeyModifiers::NONE, 277 | } => { 278 | self.state.begin = 0; 279 | self.state.index = if self.rows() > 0 { Some(0) } else { None }; 280 | self.emit_selection_changed(); 281 | } 282 | KeyEvent { 283 | code: KeyCode::PageDown, 284 | modifiers: KeyModifiers::NONE, 285 | } => { 286 | self.state.select(if self.rows() > 0 { 287 | Some(self.rows() - 1) 288 | } else { 289 | None 290 | }); 291 | self.update_state(); 292 | self.emit_selection_changed(); 293 | } 294 | _ => {} 295 | } 296 | } 297 | 298 | fn resize(&mut self, width: u16, height: u16) { 299 | self.width = width; 300 | self.height = height; 301 | self.update_state(); 302 | } 303 | 304 | fn width(&self) -> u16 { 305 | self.width 306 | } 307 | 308 | fn height(&self) -> u16 { 309 | self.height 310 | } 311 | } 312 | 313 | struct Renderer<'a>(&'a TableView); 314 | 315 | impl<'a> Widget for Renderer<'a> { 316 | fn render(self, area: Rect, buf: &mut Buffer) { 317 | if area.area() == 0 || !self.0.visible() { 318 | return; 319 | } 320 | 321 | let block_style = match self.0.focused() { 322 | true => Style::default().fg(Color::LightYellow), 323 | false => Style::default(), 324 | }; 325 | 326 | let block = Block::default() 327 | .borders(Borders::ALL) 328 | .border_style(block_style) 329 | .title(format!( 330 | "{}/{}", 331 | self.0.state.selected().map_or(0, |i| i + 1), 332 | self.0 333 | .model 334 | .as_ref() 335 | .map_or(0, |model| model.borrow().rows()) 336 | )); 337 | 338 | let model = match self.0.model { 339 | Some(ref model) => model.borrow(), 340 | None => return, 341 | }; 342 | 343 | let rows = model.rows(); 344 | let cols = model.cols(); 345 | 346 | let table_area = { 347 | let inner_area = block.inner(area); 348 | block.render(area, buf); 349 | inner_area 350 | }; 351 | 352 | let has_selection = self.0.state.selected().is_some(); 353 | let rows_height = table_area.height.saturating_sub(1); 354 | let column_widths = self.0.get_column_widths(table_area.width); 355 | let mut current_height = 1; 356 | let (data_rows, data_columns) = (rows, cols); 357 | 358 | buf.set_style( 359 | Rect { 360 | x: table_area.left(), 361 | y: table_area.top(), 362 | width: table_area.width, 363 | height: table_area.height.min(1), 364 | }, 365 | self.0.style.header_style, 366 | ); 367 | 368 | let mut col = table_area.left(); 369 | for (&width, cell) in column_widths.iter().zip(0..data_columns) { 370 | let header_data = model.header_data(cell).unwrap_or_default(); 371 | buf.set_stringn( 372 | col, 373 | table_area.top(), 374 | header_data, 375 | width as usize, 376 | Style::default(), 377 | ); 378 | col += width + 1; 379 | } 380 | 381 | // Render rows 382 | if data_rows == 0 { 383 | return; 384 | } 385 | 386 | let (start, end) = ( 387 | self.0.state.begin, 388 | self.0.state.begin + rows_height as usize, 389 | ); 390 | //self.0.state.offset = start; 391 | 392 | for index in (0..data_rows).skip(self.0.state.begin).take(end - start) { 393 | let (row, mut col) = (table_area.top() + current_height, table_area.left()); 394 | current_height += 1; 395 | let table_row_area = Rect { 396 | x: col, 397 | y: row, 398 | width: table_area.width, 399 | height: 1, 400 | }; 401 | 402 | if has_selection && self.0.state.selected().unwrap() == index { 403 | buf.set_style(table_row_area, self.0.style.selected_row_style) 404 | } 405 | 406 | for (&width, cell) in column_widths.iter().zip(0..data_columns) { 407 | let data = model 408 | .data(ModelIndex::new(index, cell)) 409 | .map(|d| d.to_string()) 410 | .unwrap_or_default(); 411 | 412 | buf.set_stringn(col, row, data, width as usize, Style::default()); 413 | col += width + 1; 414 | } 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Duration, Local, NaiveDateTime, NaiveTime, Timelike}; 2 | use regex::Regex; 3 | use std::str::FromStr; 4 | 5 | pub fn parse_date(value: &str) -> Result { 6 | let now = Local::now().naive_local(); 7 | let regex = Regex::new(r#"^now-(\d+)([smhdw])$"#)?; 8 | 9 | match regex.captures(value) { 10 | Some(captures) if captures.len() == 3 => match (captures.get(1), captures.get(2)) { 11 | (Some(offset), Some(char)) => { 12 | let offset = offset 13 | .as_str() 14 | .parse::() 15 | .map_err(|_| regex::Error::Syntax(String::from("Cannot parse number")))?; 16 | 17 | match char.as_str() { 18 | "s" => Ok(now - Duration::seconds(offset as i64)), 19 | "m" => Ok(now - Duration::minutes(offset as i64)), 20 | "h" => Ok(now - Duration::hours(offset as i64)), 21 | "d" => Ok(now - Duration::days(offset as i64)), 22 | "w" => Ok(now - Duration::weeks(offset as i64)), 23 | _ => unreachable!(), 24 | } 25 | } 26 | _ => Err(regex::Error::Syntax("Invalid captures".to_string())), 27 | }, 28 | _ => Err(regex::Error::Syntax("Invalid value".to_string())), 29 | } 30 | } 31 | 32 | pub fn parse_time(hour: NaiveDateTime, time: &str) -> NaiveDateTime { 33 | let minutes_pos = time 34 | .as_bytes() 35 | .iter() 36 | .position(|char| *char == b':') 37 | .unwrap(); 38 | let seconds_pos = time 39 | .as_bytes() 40 | .iter() 41 | .position(|char| *char == b'.') 42 | .unwrap(); 43 | 44 | let minutes = match u32::from_str(&time[0..minutes_pos]) { 45 | Ok(v) => v, 46 | Err(_) => unreachable!(), 47 | }; 48 | let seconds = u32::from_str(&time[(minutes_pos + 1)..seconds_pos]).unwrap(); 49 | let nanos = &time[(seconds_pos + 1)..]; 50 | let nanos_count = nanos.chars().count(); 51 | let nanos = u32::from_str(nanos).unwrap(); 52 | 53 | match nanos_count { 54 | 0..=3 => NaiveDateTime::new( 55 | hour.date(), 56 | NaiveTime::from_hms_milli(hour.time().hour(), minutes, seconds, nanos), 57 | ), 58 | 4..=6 => NaiveDateTime::new( 59 | hour.date(), 60 | NaiveTime::from_hms_micro(hour.time().hour(), minutes, seconds, nanos), 61 | ), 62 | _ => NaiveDateTime::new( 63 | hour.date(), 64 | NaiveTime::from_hms_nano(hour.time().hour(), minutes, seconds, nanos), 65 | ), 66 | } 67 | } 68 | 69 | pub fn sub_strings(string: &str, sub_len: usize) -> Vec<&str> { 70 | let mut subs = Vec::with_capacity(string.len() * 2 / sub_len); 71 | let mut iter = string.chars(); 72 | let mut pos = 0; 73 | 74 | while pos < string.len() { 75 | let mut len = 0; 76 | for ch in iter.by_ref().take(sub_len) { 77 | len += ch.len_utf8(); 78 | if ch == '\n' { 79 | break; 80 | } 81 | } 82 | subs.push(&string[pos..pos + len]); 83 | pos += len; 84 | } 85 | subs 86 | } 87 | --------------------------------------------------------------------------------