Rustでlessクローンを作りたい その4


https://qiita.com/hhatto/items/03e99666cc8baa8514e6

Rustでlessクローンの続き その4

こまかいブラッシュアップとか検索ジャンプ後の表示バグを修正する予定。


## やじるしキーでもカーソル移動したい

以下でイベント取れるので、hjklキーの割り当てと合わせる感じで処理させます。

* `crossterm::event::KeyCode::Up`
* `crossterm::event::KeyCode::Down`
* `crossterm::event::KeyCode::Right`
* `crossterm::event::KeyCode::Left`

```diff
diff --git a/1207_less_clone/src/main.rs b/1207_less_clone/src/main.rs
index b07fe39..7925c87 100644
--- a/1207_less_clone/src/main.rs
+++ b/1207_less_clone/src/main.rs
@@ -120,11 +120,11 @@ fn less_loop(filename: &str) -> Result<()> {

             match event {
                 Event::Key(KeyEvent {
-                    code: KeyCode::Char('h'),
+                    code: KeyCode::Char('h') | KeyCode::Left,
                     modifiers: _,
                 }) => execute!(stdout(), MoveLeft(1))?,
                 Event::Key(KeyEvent {
-                    code: KeyCode::Char('j'),
+                    code: KeyCode::Char('j') | KeyCode::Down,
                     modifiers: _,
                 }) => {
                     if window_rows-3 == row {
@@ -137,7 +137,7 @@ fn less_loop(filename: &str) -> Result<()> {
                     }
                 },
                 Event::Key(KeyEvent {
-                    code: KeyCode::Char('k'),
+                    code: KeyCode::Char('k') | KeyCode::Up,
                     modifiers: _,
                 }) => {
                     if 0 == row {
@@ -150,7 +150,7 @@ fn less_loop(filename: &str) -> Result<()> {
                     }
                 },
                 Event::Key(KeyEvent {
-                    code: KeyCode::Char('l'),
+                    code: KeyCode::Char('l') | KeyCode::Right,
                     modifiers: _,
                 }) => execute!(stdout(), MoveRight(1))?,
                 Event::Key(KeyEvent {
```

## カーソル移動でターミナルの上端下端、ファイル先端終端を考慮する

バグってただけという。

```diff
diff --git a/1207_less_clone/src/main.rs b/1207_less_clone/src/main.rs
index 7925c87..067faba 100644
--- a/1207_less_clone/src/main.rs
+++ b/1207_less_clone/src/main.rs
@@ -35,6 +35,9 @@ impl DisplayLines {
     }
 }
 
+const STATUS_LINE_OFFSET: usize = 2;
+const DISPLAY_BOTTOM_LINE_OFFSET: usize = STATUS_LINE_OFFSET + 1;
+
 fn search(filename: &str, search_word: &str) -> Result<Vec<(u64, String)>> {
     let matcher = RegexMatcher::new(search_word).unwrap();
     let mut matches: Vec<(u64, String)> = vec![];
@@ -49,20 +52,21 @@ fn search(filename: &str, search_word: &str) -> Result<Vec<(u64, String)>> {
 fn less_loop(filename: &str) -> Result<()> {
     let f = File::open(filename)?;
     let lines = ropey::Rope::from_reader(f)?;
-    let line_count = lines.len_lines();
+    let line_count = lines.len_lines() - 1;
     let mut is_search_mode = false;
 
     let mut search_word_vec: Vec<char> = [].to_vec();
     let (_, window_rows) = terminal::size()?;
     let mut display_lines = DisplayLines { start: 0, end: 0 };
 
-    for idx in 0..window_rows {
-        println!("{}", lines.line(idx as usize));
-        execute!(stdout(), MoveTo(0, idx as u16))?;
-        if idx as usize >= line_count - 1 {
+    for idx in 0..(window_rows - STATUS_LINE_OFFSET as u16) {
+        *display_lines.end_mut() = idx as u64;
+        // NOTE: use format, because debug print
+        let disp = format!("{}", lines.line(idx as usize));
+        execute!(stdout(), MoveTo(0, idx as u16), Print(disp))?;
+        if idx >= line_count as u16 {
             break
         }
-        *display_lines.end_mut() = idx as u64;
     }
     execute!(stdout(), MoveTo(0, 0))?;
 
@@ -127,12 +131,13 @@ fn less_loop(filename: &str) -> Result<()> {
                     code: KeyCode::Char('j') | KeyCode::Down,
                     modifiers: _,
                 }) => {
-                    if window_rows-3 == row {
+                    if (window_rows - DISPLAY_BOTTOM_LINE_OFFSET as u16) == row && line_count != (display_lines.end + 1) as usize {
                         *display_lines.start_mut() = display_lines.start + 1;
                         *display_lines.end_mut() = display_lines.end + 1;
                         let l = lines.line(display_lines.end as usize);
-                        execute!(stdout(), ScrollUp(1), Print(l), RestorePosition)?;
-                    } else {
+                        // NOTE: use format because debug print
+                        execute!(stdout(), ScrollUp(1), Print(format!("{}", l)), RestorePosition)?;
+                    } else if (window_rows - DISPLAY_BOTTOM_LINE_OFFSET as u16) != row && line_count != (row + 1) as usize {
                         execute!(stdout(), MoveDown(1))?;
                     }
                 },
@@ -140,12 +145,13 @@ fn less_loop(filename: &str) -> Result<()> {
                     code: KeyCode::Char('k') | KeyCode::Up,
                     modifiers: _,
                 }) => {
-                    if 0 == row {
+                    if 0 == row && display_lines.start > 0 {
                         *display_lines.start_mut() = display_lines.start - 1;
                         *display_lines.end_mut() = display_lines.end - 1;
-                        let l = lines.line(display_lines.end as usize);
-                        execute!(stdout(), ScrollDown(1), Print(l), RestorePosition)?;
-                    } else {
+                        let l = lines.line(display_lines.start as usize);
+                        // NOTE: use format because debug print
+                        execute!(stdout(), ScrollDown(1), Print(format!("{}", l)), RestorePosition)?;
+                    } else if 0 != row {
                         execute!(stdout(), MoveUp(1))?;
                     }
                 },
```

とりあえず困った時は `execute!(crossterm::style::Print("hogehoge"))`で出力してデバッグする感じ。
結局プリントデバッグという:sweat:


## ステータス用の行を追加
Vimのステータスラインが分かりやすいので、組み込んでみた。
現在の `カーソル行/全行数(xx%)` みたいな感じで表示する。

```diff
diff --git a/1207_less_clone/src/main.rs b/1207_less_clone/src/main.rs
index 23c8a63..f5bbadf 100644
--- a/1207_less_clone/src/main.rs
+++ b/1207_less_clone/src/main.rs
@@ -6,7 +6,7 @@ use crossterm::{
     cursor::{position, DisableBlinking, MoveTo, MoveUp, MoveDown, MoveLeft, MoveRight, RestorePosition, SavePosition},
     event::{read, Event, KeyCode, KeyEvent, KeyModifiers},
     execute,
-    style::Print,
+    style::{Color, Print, ResetColor, SetBackgroundColor},
     terminal,
     terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType, ScrollDown, ScrollUp},
     Result,
@@ -49,6 +49,50 @@ fn search(filename: &str, search_word: &str) -> Result<Vec<(u64, String)>> {
     Ok(matches)
 }
 
+fn clear_status_line() -> Result<()> {
+    let (window_columns, window_rows) = terminal::size()?;
+
+    let mut status_line = Vec::new();
+    for _ in 0..window_columns {
+        status_line.push(" ");
+    }
+
+    execute!(
+        stdout(),
+        SavePosition,
+        MoveTo(0, window_rows - STATUS_LINE_OFFSET as u16),
+        Print(String::from_iter(status_line)),
+        RestorePosition,
+    )?;
+
+    Ok(())
+}
+
+fn render_status_line(line_count: u64, max_line_count: usize) -> Result<()> {
+    let (window_columns, window_rows) = terminal::size()?;
+
+    let mut status_line = Vec::new();
+    for _ in 0..window_columns {
+        status_line.push(" ");
+    }
+
+    let percentage = line_count as f64 / max_line_count as f64 * 100.;
+
+    execute!(
+        stdout(),
+        SavePosition,
+        MoveTo(0, window_rows - STATUS_LINE_OFFSET as u16),
+        SetBackgroundColor(Color::Blue),
+        Print(String::from_iter(status_line)),
+        MoveTo(0, window_rows - STATUS_LINE_OFFSET as u16),
+        Print(format!("{}/{}({:3.0}%)", line_count, max_line_count, percentage as usize)),
+        ResetColor,
+        RestorePosition,
+    )?;
+
+    Ok(())
+}
+
 fn less_loop(filename: &str) -> Result<()> {
     let f = File::open(filename)?;
     let lines = ropey::Rope::from_reader(f)?;
@@ -71,8 +115,12 @@ fn less_loop(filename: &str) -> Result<()> {
     execute!(stdout(), MoveTo(0, 0))?;
 
     loop {
-        let event = read()?;
         let (_, row) = position()?;
+        let _ = render_status_line(display_lines.start + 1 + row as u64, line_count);
+
+        let event = read()?;
+
+        let _ = clear_status_line();
 
         if is_search_mode {
             match event {
```

ちょっと効率悪そうだけど、以下のような感じで実装。

1. イベント取得直前にステータス行をレンダリング
2. イベント取得(`read()`)
3. イベント取得(`read()`)が戻ってきたら、その直後にステータス行を一旦クリア
4. イベント毎の処理
5. 1に戻る（これを毎ループ繰り返し）

色は `crossterm::style::SetBackgroundColor(crossterm::style::Color::Blue))` 等で背景色を設定。 `Print()`する。`crossterm::style::ResetColor` で塗る感じ。

以下参照。
https://docs.rs/crossterm/0.22.1/crossterm/style/index.html

## おわりに

今の動きはこんな感じ。

<a href="https://www.loom.com/share/fee6520571df4fafa61ce2e8fcf589a2">
    <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/fee6520571df4fafa61ce2e8fcf589a2-with-play.gif">
  </a>

ステータス行はこんな感じになった。
![statusline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1775/d833e93a-c107-d337-38af-7ab4aba51a82.png)

というわけで今日はここまで :wave_tone3:


今日の成果
https://github.com/hhatto/advent-2021/compare/bdfa30aa516e6ec97ef2971486bc761923f4ded6..c4a5b5d9b608f02bdcfc2546d1e55a9bf8337c78