diff --git a/Cargo.lock b/Cargo.lock index 8c4eabe..55feabb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,7 +1064,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hwatch" -version = "0.3.16" +version = "0.3.17" dependencies = [ "ansi-parser", "ansi_term", diff --git a/Cargo.toml b/Cargo.toml index 07aa85a..240e2d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ keywords = ["watch", "command", "monitoring"] license-file = "LICENSE" name = "hwatch" repository = "https://github.com/blacknon/hwatch" -version = "0.3.16" +version = "0.3.17" [dependencies] ansi-parser = "0.9.0" diff --git a/src/app.rs b/src/app.rs index 3b54e9b..6dd1629 100644 --- a/src/app.rs +++ b/src/app.rs @@ -28,6 +28,7 @@ use tui::{ Frame, Terminal, }; use similar::{ChangeTag, TextDiff}; +use unicode_width::UnicodeWidthStr; // local module use crate::ansi::get_ansi_strip_str; @@ -411,8 +412,7 @@ impl<'a> App<'a> { // match input_mode match self.input_mode { InputMode::Filter | InputMode::RegexFilter => { - // - let input_text_x = self.header_area.input_text.len() as u16 + 1; + let input_text_x = self.header_area.input_text.width() as u16 + 1; let input_text_y = self.header_area.area.y + 1; // set cursor diff --git a/src/header.rs b/src/header.rs index 424ec06..03f1b31 100644 --- a/src/header.rs +++ b/src/header.rs @@ -2,7 +2,7 @@ // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. -// TODO: commandの表示を単色ではなく、Syntax highlightしたカラーリングに書き換える(v0.3.9) +// TODO: commandの表示を単色ではなく、Syntax highlightしたカラーリングに書き換える??(v0.3.9) // TODO: input内容の表示 // TODO: 幅調整系の数字をconstにする(生数字で雑計算だとわけわからん) @@ -14,6 +14,7 @@ use tui::{ Frame, prelude::Line, }; +use unicode_width::UnicodeWidthStr; // local module use crate::app::{ActiveArea, InputMode}; @@ -187,7 +188,7 @@ impl<'a> HeaderArea<'a> { } // filter keyword. - let filter_keyword_width = if width > ((self.banner.len() + 20) + 2 + 14) { + let filter_keyword_width = if width > ((self.banner.len() + 20) + 2 + 14) && width > 59 { // width - POSITION_X_HELP_TEXT - 2 - 14 // length("[Number] [Color] [Output] [history] [Line(Only)]") = 48 // length("[Number] [Color] [Reverse] [Output] [history] [Line(Only)]") = 58 @@ -195,7 +196,8 @@ impl<'a> HeaderArea<'a> { } else { 0 }; - let filter_keyword = format!("{:wid$}", self.input_text, wid = filter_keyword_width); + // format!("{:wid$}", self.input_text, wid = filter_keyword_width); + let filter_keyword = format_with_multibyte_width(&self.input_text, filter_keyword_width); let filter_keyword_style: Style; if self.input_text.is_empty() { @@ -358,3 +360,14 @@ impl<'a> HeaderArea<'a> { frame.render_widget(block, self.area); } } + +fn format_with_multibyte_width(input: &str, target_width: usize) -> String { + let current_width = UnicodeWidthStr::width(input); + if current_width >= target_width { + input.to_string() + } else { + // 残りの幅を計算し、スペースでパディングを追加 + let padding = " ".repeat(target_width - current_width); + format!("{}{}", input, padding) + } +} diff --git a/src/main.rs b/src/main.rs index bd4b460..a8d77f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,12 @@ // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. +// v0.3.18 +// TODO(blacknon): watchウィンドウの表示を折り返しだけではなく、横方向にスクロールして出力するモードも追加する +// TODO(blacknon): コマンドが終了していなくても、インターバル間隔でコマンドを実行する +// (パラレルで実行してもよいコマンドじゃないといけないよ、という機能か。投げっぱなしにしてintervalで待つようにするオプションを付ける) +// TODO(blacknon): DiffModeをInterfaceで取り扱うようにし、historyへの追加や検索時のhitなどについてもInterface側で取り扱えるようにする。(DiffModeのPlugin化の布石) + // v0.3.xx // TODO(blacknon): [FR: add "completion" subcommand](https://github.com/blacknon/hwatch/issues/107) // TODO(blacknon): [[FR] add precise interval option](https://github.com/blacknon/hwatch/issues/111) @@ -10,9 +16,6 @@ // TODO(blacknon): filter modeの検索ヒット数を表示する(どうやってやろう…?というより、どこに表示させよう…?) // TODO(blacknon): Windowsのバイナリをパッケージマネジメントシステムでインストール可能になるよう、Releaseでうまいこと処理をする // TODO(blacknon): UTF-8以外のエンコードでも動作するよう対応する(エンコード対応) -// TODO(blacknon): watchウィンドウの表示を折り返しだけではなく、横方向にスクロールして出力するモードも追加する -// TODO(blacknon): コマンドが終了していなくても、インターバル間隔でコマンドを実行する -// (パラレルで実行してもよいコマンドじゃないといけないよ、という機能か。投げっぱなしにしてintervalで待つようにするオプションを付ける) // TODO(blacknon): 空白の数だけ違う場合、diffとして扱わないようにするオプションの追加(shortcut keyではなく、`:set hogehoge...`で指定する機能として実装) // TODO(blacknon): watchをモダンよりのものに変更する // TODO(blacknon): diff modeをさらに複数用意し、選択・切り替えできるdiffをオプションから指定できるようにする(watchをold-watchにして、モダンなwatchをデフォルトにしたり) diff --git a/src/view.rs b/src/view.rs index ea02c72..9ebac29 100644 --- a/src/view.rs +++ b/src/view.rs @@ -314,11 +314,6 @@ fn send_input(tx: Sender) -> io::Result<()> { if let Ok(event) = result { let _ = tx.send(AppEvent::TerminalEvent(event)); } - - // clearing buffer - while crossterm::event::poll(std::time::Duration::from_millis(0)).unwrap() { - let _ = crossterm::event::read()?; - } } Ok(()) } diff --git a/src/watch.rs b/src/watch.rs index 555dcd1..d136d41 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -12,10 +12,13 @@ use tui::{ widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, Frame }; - use regex::Regex; use unicode_segmentation::UnicodeSegmentation; +// set highlight style +static KEYWORD_HIGHLIGHT_STYLE: Style = Style::new().fg(Color::Black).bg(Color::Yellow); +static SELECTED_KEYWORD_HIGHLIGHT_STYLE: Style = Style::new().fg(Color::Black).bg(Color::Cyan); + #[derive(Clone)] pub struct WatchArea<'a> { /// ratatui::layout::Rect. The area to draw the widget in. @@ -27,6 +30,9 @@ pub struct WatchArea<'a> { /// Wrapped data. wrap_data: Vec>, + /// highlighted data. + highlight_data: Vec>, + /// search keyword. keyword: String, @@ -73,6 +79,8 @@ impl<'a> WatchArea<'a> { wrap_data: vec![Line::from("")], + highlight_data: vec![Line::from("")], + keyword: String::from(""), keyword_is_regex: false, @@ -121,6 +129,15 @@ impl<'a> WatchArea<'a> { // update keyword position self.keyword_position = get_keyword_positions(&self.wrap_data, &self.keyword, self.keyword_is_regex, self.is_line_number, self.is_line_diff_head); } + + // set highlight style + self.highlight_data = highlight_text( + self.wrap_data.clone(), + self.keyword_position.clone(), + self.selected_keyword, + KEYWORD_HIGHLIGHT_STYLE, + SELECTED_KEYWORD_HIGHLIGHT_STYLE + ); } /// @@ -132,6 +149,15 @@ impl<'a> WatchArea<'a> { // update keyword position self.keyword_position = get_keyword_positions(&self.wrap_data, &self.keyword, self.keyword_is_regex, self.is_line_number, self.is_line_diff_head); } + + // set highlight style + self.highlight_data = highlight_text( + self.wrap_data.clone(), + self.keyword_position.clone(), + self.selected_keyword, + KEYWORD_HIGHLIGHT_STYLE, + SELECTED_KEYWORD_HIGHLIGHT_STYLE + ); } /// @@ -171,6 +197,15 @@ impl<'a> WatchArea<'a> { } else { self.keyword_position = vec![]; } + + // set highlight style + self.highlight_data = highlight_text( + self.wrap_data.clone(), + self.keyword_position.clone(), + self.selected_keyword, + KEYWORD_HIGHLIGHT_STYLE, + SELECTED_KEYWORD_HIGHLIGHT_STYLE + ); } /// @@ -179,7 +214,15 @@ impl<'a> WatchArea<'a> { self.keyword_is_regex = false; self.keyword_position = vec![]; self.selected_keyword = -1; - } + + // set highlight style + self.highlight_data = highlight_text( + self.wrap_data.clone(), + self.keyword_position.clone(), + self.selected_keyword, + KEYWORD_HIGHLIGHT_STYLE, + SELECTED_KEYWORD_HIGHLIGHT_STYLE + ); } /// pub fn previous_keyword(&mut self) { @@ -203,6 +246,15 @@ impl<'a> WatchArea<'a> { // scroll move self.scroll_move(position.0 as i16); } + + // set highlight style + self.highlight_data = highlight_text( + self.wrap_data.clone(), + self.keyword_position.clone(), + self.selected_keyword, + KEYWORD_HIGHLIGHT_STYLE, + SELECTED_KEYWORD_HIGHLIGHT_STYLE + ); } /// @@ -231,15 +283,20 @@ impl<'a> WatchArea<'a> { self.scroll_move(position.0 as i16); } } + + // set highlight style + self.highlight_data = highlight_text( + self.wrap_data.clone(), + self.keyword_position.clone(), + self.selected_keyword, + KEYWORD_HIGHLIGHT_STYLE, + SELECTED_KEYWORD_HIGHLIGHT_STYLE + ); } /// pub fn draw(&mut self, frame: &mut Frame) { - // set highlight style - let highlight_style = Style::new().fg(Color::Black).bg(Color::Yellow); - let selected_highlight_style = Style::new().fg(Color::Black).bg(Color::Cyan); - - let block_data = highlight_text(&self.wrap_data, self.keyword_position.clone(), self.selected_keyword, selected_highlight_style, highlight_style); + let block_data = self.highlight_data.clone(); // declare variables let pane_block: Block<'_>; @@ -371,14 +428,18 @@ impl<'a> WatchArea<'a> { /// fn get_keyword_positions(lines: &Vec, keyword: &str, is_regex: bool, is_line_number: bool, is_diff_head: bool) -> Vec<(usize, usize, usize)> { + // Ignore the number of characters at the beginning of the line specified by `ignore_head_count` when searching. let mut ignore_head_count = 0; + + // if is_line_number { let num_count = lines.len().to_string().len(); ignore_head_count = num_count + 3; // ^` | ` } + // ` ` | ` + ` | ` - ` if is_diff_head { - ignore_head_count += 4; // ` ` | ` + ` | ` - ` + ignore_head_count += 4; } let re = if is_regex { @@ -390,25 +451,25 @@ fn get_keyword_positions(lines: &Vec, keyword: &str, is_regex: bool, is_li let mut hits = Vec::new(); for (line_index, line) in lines.iter().enumerate() { - let combined_text: String = line.spans.iter().map(|span| span.content.as_ref()).collect(); - let combined_text = if is_line_number { - combined_text[ignore_head_count..].to_string() - } else { - combined_text - }; + let base_combined_text: String = line.spans.iter().map(|span| span.content.as_ref()).collect(); + let combined_text: String = base_combined_text.chars().skip(ignore_head_count).collect(); if let Some(re) = &re { + for mat in re.find_iter(&combined_text) { hits.push((line_index, mat.start() + ignore_head_count, mat.end() + ignore_head_count)); } } else { let mut start_position = 0; + let keyword_len = keyword.chars().count(); + let combined_text_chars: Vec = combined_text.chars().collect(); - while let Some(pos) = combined_text[start_position..].find(keyword) { - let match_start = start_position + pos; - let match_end = match_start + keyword.len(); - hits.push((line_index, match_start + ignore_head_count, match_end + ignore_head_count)); - start_position = match_end; + while start_position + keyword_len <= combined_text_chars.len() { + let current_slice: String = combined_text_chars[start_position .. (start_position + keyword_len)].iter().collect(); + if current_slice == keyword { + hits.push((line_index, start_position + ignore_head_count, start_position + keyword_len + ignore_head_count)); + } + start_position += 1; } } } @@ -468,7 +529,7 @@ fn wrap_utf8_lines<'a>(lines: &Vec, width: usize) -> Vec> { } /// -fn highlight_text<'a>(lines: &'a Vec, positions: Vec<(usize, usize, usize)>, selected_keyword: i16, selected_highlight_style: Style, highlight_style: Style) -> Vec> { +fn highlight_text(lines: Vec, positions: Vec<(usize, usize, usize)>, selected_keyword: i16, selected_highlight_style: Style, highlight_style: Style) -> Vec { let mut new_lines = Vec::new(); let mut current_count:i16 = 0; @@ -477,10 +538,11 @@ fn highlight_text<'a>(lines: &'a Vec, positions: Vec<(usize, usize, usize) let mut current_pos = 0; // Get the highlighted position of the corresponding keyword for this line - let line_hits: Vec<(usize, usize, usize)> = positions - .iter() + let line_hits: Vec<(usize, usize)> = positions + .clone() + .into_iter() .filter(|(line_index, _, _)| *line_index == i) - .cloned() + .map(|(_, start_position, end_position)| (start_position, end_position)) .collect(); // Process each Span to generate a new Span @@ -492,26 +554,28 @@ fn highlight_text<'a>(lines: &'a Vec, positions: Vec<(usize, usize, usize) // Processing when the highlight range spans Span if !line_hits.is_empty() { let mut last_pos = 0; - for (_, start_position, end_position) in line_hits.iter() { + + for (start_position, end_position) in line_hits.iter() { // Ignore if the hit is after the current span if *start_position >= span_end { continue; } // Calculating highlight_start and highlight_end - let highlight_start = (*start_position).saturating_sub(span_start); // 値が負にならないように調整 + let highlight_start = (*start_position).saturating_sub(span_start); let highlight_end = (*end_position).min(span_end).saturating_sub(span_start); if highlight_start > last_pos { + let before_highlight_text: String = span_text.chars().skip(last_pos).take(highlight_start - last_pos).collect(); new_spans.push(Span::styled( - span_text[last_pos..highlight_start].to_string(), + before_highlight_text, span.style, )); } - let text_str: String = span_text[highlight_start..highlight_end].to_string(); + let text_str: String = span_text.chars().skip(highlight_start).take(highlight_end-highlight_start).collect(); - if text_str.len() > 0 { + if text_str.chars().count() > 0 { if current_count == selected_keyword { new_spans.push(Span::styled( text_str, @@ -529,9 +593,10 @@ fn highlight_text<'a>(lines: &'a Vec, positions: Vec<(usize, usize, usize) last_pos = highlight_end; } - if last_pos < span_text.len() { + if last_pos < span_text.chars().count() { + let after_highlight_text:String = span_text.chars().skip(last_pos).collect(); new_spans.push(Span::styled( - span_text[last_pos..].to_string(), + after_highlight_text, span.style, )); } @@ -540,7 +605,7 @@ fn highlight_text<'a>(lines: &'a Vec, positions: Vec<(usize, usize, usize) new_spans.push(Span::styled(span_text.clone(), span.style)); } - current_pos += span_text.len(); + current_pos += span_text.chars().count(); } new_lines.push(Line::from(new_spans));