Repository: Canop/bacon (language: rust)

Issue:
allow scrolling half-pages
the `less` pager has the following keybindings:
```
  d  ^D             *  Forward  one half-window (and set half-window to N).
  u  ^U             *  Backward one half-window (and set half-window to N).
```

i use these somewhat often and miss them in bacon. bacon has two features that are kind of close:
- `scroll-line(N)`. this works but does not adapt to the window height; if i set it to a large value it skips full pages when my terminal isn't full screen; and if i set it to a small value it does not scroll a full half-page when the terminal is maximixed.
- `scroll-pages`. this only accepts integers, not fractional values

how do you feel about changing `scroll-pages` to accept a float instead of an integer? right now it gives a syntax error so it should be backwards compatible:
```
Error: Failed to parse configuration file at "/home/jyn/.config/bacon/prefs.toml"

Caused by:
    TOML parse error at line 34, column 1
       |
    34 | [keybindings]
       | ^^^^^^^^^^^^^
    Action not understood: "scroll-pages(-.5)" (did you mean "job:scroll-pages(-.5)"?)
```


Discussion / hints:
I see no problem with that. 

If you want to try making a PR, do it. Or I'll do it.
amazing! i'm not sure when i'll have time to make a PR, i'm happy for you to work on it.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 970453f..15a2255 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 I'm following [this roadmap](https://dystroy.org/blog/bacon-everything-roadmap/). If you're interested in having bacon usable for other languages than Rust, come have a chat.
 
+### Next
+- allow specify scroll-pages action with a floating point number - Fix #264
+
 <a name="v3.3.0"></a>
 ### v3.3.0 - 2024/11/16
 - bacon can now be launched without Cargo.toml file
diff --git a/Cargo.lock b/Cargo.lock
index c4ff1c0..ebef423 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1740,9 +1740,9 @@ dependencies = [
 
 [[package]]
 name = "termimad"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cda3a7471f9978706978454c45ef8dda67e9f8f3cdb9319eb2e9323deb6ae62"
+checksum = "ea6a5d4cf55d9f1cb04fcda48f725772d0733ae34e030dfc4dd36e738a5965f4"
 dependencies = [
  "coolor",
  "crokey",
diff --git a/Cargo.toml b/Cargo.toml
index 91ecd97..5876ee9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,7 +26,7 @@ notify = "7.0"
 rustc-hash = "2"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-termimad = "0.31"
+termimad = "0.31.1"
 toml = "0.8"
 unicode-width = "0.2"
 vte = "0.8"
@@ -39,7 +39,7 @@ codegen-units = 1
 
 [patch.crates-io]
 # clap-help = { path = "../clap-help" }
-# termimad = { path = "../termimad" }
+# termimad = { path = "../termimad" }
 # crokey = { path = "../crokey" }
 # coolor = { path = "../coolor" }
 # lazy-regex = { path = "../lazy-regex" }
diff --git a/src/conf/action.rs b/src/conf/action.rs
index 60a0f48..1139900 100644
--- a/src/conf/action.rs
+++ b/src/conf/action.rs
@@ -170,7 +170,9 @@ fn test_action_string_round_trip() {
             },
         })),
         Action::Internal(Internal::Help),
-        Action::Internal(Internal::Scroll(ScrollCommand::Pages(-2))),
+        Action::Internal(Internal::Scroll(ScrollCommand::MilliPages(1500))),
+        Action::Internal(Internal::Scroll(ScrollCommand::MilliPages(-500))),
+        Action::Internal(Internal::Scroll(ScrollCommand::MilliPages(-2000))),
         Action::Export("my export".to_string()),
     ];
     for action in actions {
diff --git a/src/conf/keybindings.rs b/src/conf/keybindings.rs
index 518fee1..1bb12b5 100644
--- a/src/conf/keybindings.rs
+++ b/src/conf/keybindings.rs
@@ -38,9 +38,12 @@ impl Default for KeyBindings {
         bindings.set(key!(End), Internal::Scroll(ScrollCommand::Bottom));
         bindings.set(key!(Up), Internal::Scroll(ScrollCommand::Lines(-1)));
         bindings.set(key!(Down), Internal::Scroll(ScrollCommand::Lines(1)));
-        bindings.set(key!(PageUp), Internal::Scroll(ScrollCommand::Pages(-1)));
-        bindings.set(key!(PageDown), Internal::Scroll(ScrollCommand::Pages(1)));
-        bindings.set(key!(Space), Internal::Scroll(ScrollCommand::Pages(1)));
+        bindings.set(key!(PageUp), Internal::Scroll(ScrollCommand::pages(-1)));
+        bindings.set(key!(PageDown), Internal::Scroll(ScrollCommand::pages(1)));
+        bindings.set(
+            key!(Space),
+            Internal::Scroll(ScrollCommand::MilliPages(800)),
+        );
         bindings.set(key!(f), Internal::ScopeToFailures);
         bindings.set(key!(esc), Internal::Back);
         bindings.set(key!(ctrl - d), JobRef::Default);
@@ -155,7 +158,7 @@ fn test_deserialize_keybindings() {
     }
     let toml = r#"
     [keybindings]
-    Ctrl-U = "internal:scroll-pages(-2)"
+    Ctrl-U = "internal:scroll-pages(-.5)"
     Ctrl-d = "internal:scroll-page(1)"
     alt-q = "internal:quit"
     alt-p = "job:previous"
@@ -163,13 +166,15 @@ fn test_deserialize_keybindings() {
     let conf = toml::from_str::<Config>(toml).unwrap();
     assert_eq!(
         conf.keybindings.get(key!(ctrl - u)),
-        Some(&Action::Internal(Internal::Scroll(ScrollCommand::Pages(
-            -2
-        )))),
+        Some(&Action::Internal(Internal::Scroll(
+            ScrollCommand::MilliPages(-500)
+        ))),
     );
     assert_eq!(
         conf.keybindings.get(key!(ctrl - d)),
-        Some(&Action::Internal(Internal::Scroll(ScrollCommand::Pages(1)))),
+        Some(&Action::Internal(Internal::Scroll(
+            ScrollCommand::MilliPages(1000)
+        ))),
     );
     assert_eq!(conf.keybindings.get(key!(z)), None,);
     assert_eq!(
diff --git a/src/conf/settings.rs b/src/conf/settings.rs
index 866b564..b853f26 100644
--- a/src/conf/settings.rs
+++ b/src/conf/settings.rs
@@ -1,7 +1,14 @@
 use {
     crate::*,
-    anyhow::{bail, Result},
-    std::{collections::HashMap, path::PathBuf, time::Duration},
+    anyhow::{
+        Result,
+        bail,
+    },
+    std::{
+        collections::HashMap,
+        path::PathBuf,
+        time::Duration,
+    },
 };
 
 /// The settings used in the application.
diff --git a/src/help/help_page.rs b/src/help/help_page.rs
index 7613148..9fcedb3 100644
--- a/src/help/help_page.rs
+++ b/src/help/help_page.rs
@@ -115,8 +115,8 @@ impl HelpPage {
             ScrollCommand::Lines(lines) => {
                 text_view.try_scroll_lines(lines);
             }
-            ScrollCommand::Pages(pages) => {
-                text_view.try_scroll_pages(pages);
+            ScrollCommand::MilliPages(milli_pages) => {
+                text_view.try_scroll_pages(milli_pages as f64 / 1000f64);
             }
         }
         self.scroll = text_view.scroll;
diff --git a/src/internal.rs b/src/internal.rs
index 0d93cf1..c64eee7 100644
--- a/src/internal.rs
+++ b/src/internal.rs
@@ -140,7 +140,9 @@ fn test_internal_string_round_trip() {
         Internal::ReRun,
         Internal::ReloadConfig,
         Internal::ScopeToFailures,
-        Internal::Scroll(ScrollCommand::Pages(-3)),
+        Internal::Scroll(ScrollCommand::MilliPages(-3000)),
+        Internal::Scroll(ScrollCommand::MilliPages(-350)),
+        Internal::Scroll(ScrollCommand::MilliPages(1561)),
         Internal::Scroll(ScrollCommand::Top),
         Internal::ToggleBacktrace("1"),
         Internal::ToggleBacktrace("full"),
diff --git a/src/mission.rs b/src/mission.rs
index 09bad18..9e51a77 100644
--- a/src/mission.rs
+++ b/src/mission.rs
@@ -2,8 +2,10 @@ use {
     crate::*,
     lazy_regex::regex_replace_all,
     rustc_hash::FxHashSet,
-    std::collections::HashMap,
-    std::path::PathBuf,
+    std::{
+        collections::HashMap,
+        path::PathBuf,
+    },
 };
 
 /// the description of the mission of bacon
diff --git a/src/scroll.rs b/src/scroll.rs
index 2299d76..a241a5f 100644
--- a/src/scroll.rs
+++ b/src/scroll.rs
@@ -19,10 +19,13 @@ pub enum ScrollCommand {
     Top,
     Bottom,
     Lines(i32),
-    Pages(i32),
+    MilliPages(i32),
 }
 
 impl ScrollCommand {
+    pub fn pages(n: i32) -> Self {
+        Self::MilliPages(n * 1000)
+    }
     fn to_lines(
         self,
         content_height: usize,
@@ -32,7 +35,15 @@ impl ScrollCommand {
             Self::Top => -(content_height as i32),
             Self::Bottom => content_height as i32,
             Self::Lines(n) => n,
-            Self::Pages(n) => n * page_height as i32,
+            Self::MilliPages(n) => {
+                let lines = n as f64 * page_height as f64 / 1000.0;
+                let lines = if lines < 0.0 {
+                    lines.floor()
+                } else {
+                    lines.ceil()
+                };
+                lines as i32
+            }
         }
     }
     /// Return the action description to show in doc/help
@@ -55,11 +66,19 @@ impl ScrollCommand {
                     txt(-lines, "line", "up")
                 }
             }
-            Self::Pages(pages) => {
-                if *pages > 0 {
-                    txt(*pages, "page", "down")
+            Self::MilliPages(n) => {
+                if n % 1000 == 0 {
+                    let pages = n / 1000;
+                    if pages > 0 {
+                        txt(pages, "page", "down")
+                    } else {
+                        txt(-pages, "page", "up")
+                    }
                 } else {
-                    txt(-pages, "page", "up")
+                    let pages: f64 = *n as f64 / 1000.0;
+                    let s = format!("{pages:.3}");
+                    let s = s.trim_matches('0');
+                    format!("scroll {s} pages")
                 }
             }
         }
@@ -90,7 +109,17 @@ impl fmt::Display for ScrollCommand {
             Self::Top => write!(f, "scroll-to-top"),
             Self::Bottom => write!(f, "scroll-to-bottom"),
             Self::Lines(n) => write!(f, "scroll-lines({n})"),
-            Self::Pages(n) => write!(f, "scroll-pages({n})"),
+            Self::MilliPages(n) => {
+                if n % 1000 == 0 {
+                    let n = n / 1000;
+                    write!(f, "scroll-pages({n})")
+                } else {
+                    let n: f64 = *n as f64 / 1000.0;
+                    let s = format!("{n:.3}");
+                    let s = s.trim_matches('0');
+                    write!(f, "scroll-pages({s})")
+                }
+            }
         }
     }
 }
@@ -103,9 +132,14 @@ impl std::str::FromStr for ScrollCommand {
             r#"^scroll[-_]?lines?\((?<n>[+-]?\d{1,4})\)$"#i => Self::Lines(
                 n.parse().unwrap() // can't fail because [+-]?\d{1,4}
             ),
-            r#"^scroll[-_]?pages?\((?<n>[+-]?\d{1,4})\)$"#i => Self::Pages(
-                n.parse().unwrap() // can't fail because [+-]?\d{1,4}
+            r#"^scroll[-_]?pages?\((?<n>[+-]?\d{1,4})\)$"#i => Self::MilliPages(
+                n.parse::<i32>().unwrap() * 1000 // can't fail because [+-]?\d{1,4}
             ),
+            r#"^scroll[-_]?pages?\((?<n>[+-]?\d*\.\d{1,3})\)$"#i => {
+                let n: f64 = n.parse().unwrap(); // can't fail
+                let n: i32 = (n * 1000.0).round() as i32;
+                Self::MilliPages(n)
+            }
         )
         .ok_or("not a valid scroll command")
     }
@@ -159,8 +193,8 @@ fn test_scroll_command_string_round_trip() {
     let commands = [
         ScrollCommand::Lines(3),
         ScrollCommand::Lines(-12),
-        ScrollCommand::Pages(1),
-        ScrollCommand::Pages(-2),
+        ScrollCommand::MilliPages(1000),
+        ScrollCommand::MilliPages(-2000),
         ScrollCommand::Top,
         ScrollCommand::Bottom,
     ];
@@ -173,5 +207,12 @@ fn test_scroll_command_string_alternative_writings() {
     assert_eq!("SCROLL-TO-TOP".parse(), Ok(ScrollCommand::Top));
     assert_eq!("ScrollLines(5)".parse(), Ok(ScrollCommand::Lines(5)));
     assert_eq!("scroll-lines(+12)".parse(), Ok(ScrollCommand::Lines(12)));
-    assert_eq!("scroll_pages(-2)".parse(), Ok(ScrollCommand::Pages(-2)));
+    assert_eq!(
+        "scroll_pages(-2)".parse(),
+        Ok(ScrollCommand::MilliPages(-2000))
+    );
+    assert_eq!(
+        "scroll_pages(-.2)".parse(),
+        Ok(ScrollCommand::MilliPages(-200))
+    );
 }


Test patch: