From 6a43dab6dd7bd9bfd0ea2e68f53fb320b7ac933f Mon Sep 17 00:00:00 2001
From: Uncle Stretch <uncle.stretch@ghostchain.io>
Date: Tue, 10 Jun 2025 21:01:46 +0300
Subject: [PATCH] start adding generic traits and default implementations for
 repeating functionality of components

Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
---
 Cargo.toml                            |  2 +-
 src/components/generic/activatable.rs |  6 ++
 src/components/generic/helpable.rs    | 11 +++
 src/components/generic/mod.rs         |  7 ++
 src/components/generic/scrollable.rs  | 58 ++++++++++++++++
 src/components/help.rs                | 73 +++++++++++---------
 src/components/menu.rs                | 96 ++++++++++++---------------
 src/components/mod.rs                 |  1 +
 8 files changed, 168 insertions(+), 86 deletions(-)
 create mode 100644 src/components/generic/activatable.rs
 create mode 100644 src/components/generic/helpable.rs
 create mode 100644 src/components/generic/mod.rs
 create mode 100644 src/components/generic/scrollable.rs

diff --git a/Cargo.toml b/Cargo.toml
index 77210a0..b211cbf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
 name = "ghost-eye"
 authors = ["str3tch <stretch@ghostchain.io>"]
 description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost"
-version = "0.3.59"
+version = "0.3.60"
 edition = "2021"
 homepage = "https://git.ghostchain.io/ghostchain"
 repository = "https://git.ghostchain.io/ghostchain/ghost-eye"
diff --git a/src/components/generic/activatable.rs b/src/components/generic/activatable.rs
new file mode 100644
index 0000000..4653b96
--- /dev/null
+++ b/src/components/generic/activatable.rs
@@ -0,0 +1,6 @@
+pub trait Activatable {
+    fn is_active(&self) -> bool;
+    fn is_inactive(&self) -> bool;
+    fn set_active(&mut self);
+    fn set_inactive(&mut self);
+}
diff --git a/src/components/generic/helpable.rs b/src/components/generic/helpable.rs
new file mode 100644
index 0000000..ae1fdc6
--- /dev/null
+++ b/src/components/generic/helpable.rs
@@ -0,0 +1,11 @@
+use color_eyre::Result;
+use crate::action::Action;
+
+use super::Activatable;
+
+pub trait Helpable: Activatable {
+    fn open_help_popup(&mut self) -> Result<Option<Action>> {
+        self.set_inactive();
+        Ok(Some(Action::Help))
+    }
+}
diff --git a/src/components/generic/mod.rs b/src/components/generic/mod.rs
new file mode 100644
index 0000000..ac13d13
--- /dev/null
+++ b/src/components/generic/mod.rs
@@ -0,0 +1,7 @@
+mod activatable;
+mod helpable;
+mod scrollable;
+
+pub use activatable::Activatable;
+pub use helpable::Helpable;
+pub use scrollable::Scrollable;
diff --git a/src/components/generic/scrollable.rs b/src/components/generic/scrollable.rs
new file mode 100644
index 0000000..d550f91
--- /dev/null
+++ b/src/components/generic/scrollable.rs
@@ -0,0 +1,58 @@
+use std::{
+    cmp::{PartialEq, PartialOrd}, ops::{Add, Sub}
+};
+
+use color_eyre::Result;
+use crate::action::Action;
+
+pub trait Scrollable {
+    type IndexType: Add<Output = Self::IndexType>
+        + Sub<Output = Self::IndexType>
+        + PartialOrd
+        + PartialEq
+        + From<u8>
+        + Copy;
+
+    fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>>;
+    fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>>;
+
+    fn selected_index(&self) -> Option<Self::IndexType>;
+    fn items_length(&self) -> Self::IndexType; 
+
+    fn next_row(&mut self) -> Result<Option<Action>> {
+        let length = self.items_length();
+        if length == 0.into() {
+            return Ok(None);
+        }
+
+        let new_index = match self.selected_index() {
+            Some(index) => {
+                if index < length - 1.into() {
+                    index + 1.into()
+                } else {
+                    index
+                }
+            },
+            None => 0.into(),
+        };
+        self.apply_next_row(new_index)
+    }
+
+    fn prev_row(&mut self) -> Result<Option<Action>> {
+        if self.items_length() == 0.into() {
+            return Ok(None);
+        }
+
+        let new_index = match self.selected_index() {
+            Some(index) => {
+                if index == 0.into() {
+                    0.into()
+                } else {
+                    index - 1.into()
+                }
+            }
+            None => 0.into()
+        };
+        self.apply_prev_row(new_index)
+    }
+}
diff --git a/src/components/help.rs b/src/components/help.rs
index a621d5a..21ccc2a 100644
--- a/src/components/help.rs
+++ b/src/components/help.rs
@@ -12,9 +12,13 @@ use ratatui::{
 };
 
 use super::palette::StylePalette;
+use super::generic::{Activatable, Helpable, Scrollable};
 use super::Component;
+
 use crate::{action::Action, app::Mode, config::Config};
 
+const ITEM_HEIGHT: usize = 3;
+
 #[derive(Debug, Clone)]
 pub struct Help {
     is_active: bool,
@@ -24,32 +28,10 @@ pub struct Help {
     table_state: TableState,
 }
 
-const ITEM_HEIGHT: usize = 3;
-
 impl Help {
-    fn move_down(&mut self) -> Result<Option<Action>> {
-        let i = match self.table_state.selected() {
-            Some(i) => {
-                if i >= self.current_mode.get_help_data().len() {
-                    0
-                } else {
-                    i + 1
-                }
-            },
-            None => 0,
-        };
-        self.table_state.select(Some(i));
-        self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
-        Ok(None)
-    }
-
-    fn move_up(&mut self) -> Result<Option<Action>> {
-        let i = match self.table_state.selected() {
-            Some(i) => i.saturating_sub(1),
-            None => 0,
-        };
-        self.table_state.select(Some(i));
-        self.scroll_state = self.scroll_state.position(i * ITEM_HEIGHT);
+    fn move_index(&mut self, new_index: usize) -> Result<Option<Action>> {
+        self.table_state.select(Some(new_index));
+        self.scroll_state = self.scroll_state.position(new_index * ITEM_HEIGHT);
         Ok(None)
     }
 }
@@ -66,6 +48,35 @@ impl Default for Help {
     }
 }
 
+impl Activatable for Help {
+    fn is_active(&self) -> bool { self.is_active }
+    fn is_inactive(&self) -> bool { !self.is_active }
+    fn set_inactive(&mut self) { self.is_active = false; }
+    fn set_active(&mut self) { self.is_active = true; }
+}
+
+impl Helpable for Help { }
+
+impl Scrollable for Help {
+    type IndexType = usize;
+
+    fn selected_index(&self) -> Option<Self::IndexType> {
+        self.table_state.selected()
+    }
+
+    fn items_length(&self) -> Self::IndexType {
+        self.current_mode.get_help_data().len()
+    }
+
+    fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
+        self.move_index(new_index)
+    }
+
+    fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
+        self.move_index(new_index)
+    }
+}
+
 impl Component for Help {
     fn register_config_handler(&mut self, config: Config) -> Result<()> {
         if let Some(style) = config.styles.get(&crate::app::Mode::Help) {
@@ -80,7 +91,7 @@ impl Component for Help {
 
     fn update(&mut self, action: Action) -> Result<Option<Action>> {
         match action {
-            Action::Help if !self.is_active => self.is_active = true,
+            Action::Help if self.is_inactive() => self.set_active(),
             Action::SetActiveScreen(mode) => {
                 if self.current_mode != mode {
                     self.current_mode = mode;
@@ -95,10 +106,10 @@ impl Component for Help {
 
     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
         match key.code {
-            KeyCode::Up | KeyCode::Char('k') => self.move_up(),
-            KeyCode::Down | KeyCode::Char('j') => self.move_down(),
-            KeyCode::Esc if self.is_active => {
-                self.is_active = false;
+            KeyCode::Up | KeyCode::Char('k') if self.is_active() => self.next_row(),
+            KeyCode::Down | KeyCode::Char('j') if self.is_active() => self.prev_row(),
+            KeyCode::Esc if self.is_active() => {
+                self.set_inactive();
                 Ok(Some(Action::SetActiveScreen(self.current_mode)))
             },
             _ => Ok(None),
@@ -106,7 +117,7 @@ impl Component for Help {
     }
 
     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
-        if self.is_active {
+        if self.is_active() {
             let highlight_symbol = Text::from(vec![
                 "".into(),
                 " █ ".into(),
diff --git a/src/components/menu.rs b/src/components/menu.rs
index fdb345b..8f27a35 100644
--- a/src/components/menu.rs
+++ b/src/components/menu.rs
@@ -1,14 +1,14 @@
 use color_eyre::Result;
 use ratatui::{prelude::*, widgets::*};
-use tokio::sync::mpsc::UnboundedSender;
 use crossterm::event::{KeyEvent, KeyCode};
 
 use super::Component;
 use super::palette::StylePalette;
 use crate::{config::Config, action::Action, app::Mode};
 
+use super::generic::{Activatable, Helpable, Scrollable};
+
 pub struct Menu {
-    command_tx: Option<UnboundedSender<Action>>,
     list_state: ListState,
     items: Vec<String>,
     is_active: bool,
@@ -16,15 +16,41 @@ pub struct Menu {
 }
 
 impl Default for Menu {
-    fn default() -> Self {
-        Menu::new()
+    fn default() -> Self { Menu::new() }
+}
+
+impl Activatable for Menu {
+    fn is_active(&self) -> bool { self.is_active }
+    fn is_inactive(&self) -> bool { !self.is_active }
+    fn set_inactive(&mut self) { self.is_active = false; }
+    fn set_active(&mut self) { self.is_active = true; }
+}
+
+impl Helpable for Menu { }
+
+impl Scrollable for Menu {
+    type IndexType = usize;
+
+    fn selected_index(&self) -> Option<Self::IndexType> {
+        self.list_state.selected()
+    }
+
+    fn items_length(&self) -> Self::IndexType {
+        self.items.len()
+    }
+
+    fn apply_next_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
+        self.move_index(new_index)
+    }
+
+    fn apply_prev_row(&mut self, new_index: Self::IndexType) -> Result<Option<Action>> {
+        self.move_index(new_index)
     }
 }
 
 impl Menu {
     pub fn new() -> Self {
         let mut new_list = Self {
-            command_tx: None,
             list_state: ListState::default(),
             items: vec![
                 String::from("Explorer"),
@@ -42,39 +68,9 @@ impl Menu {
         new_list
     }
 
-    fn next_row(&mut self) -> Result<Option<Action>> {
-        let i = match self.list_state.selected() {
-            Some(i) => {
-                if i >= self.items.len() - 1 {
-                    i
-                } else {
-                    i + 1
-                }
-            },
-            None => 0,
-        };
-        self.list_state.select(Some(i));
-        match i {
-            0 => Ok(Some(Action::SetMode(Mode::Explorer))),
-            1 => Ok(Some(Action::SetMode(Mode::Wallet))),
-            2 => Ok(Some(Action::SetMode(Mode::Validator))),
-            _ => Ok(Some(Action::SetMode(Mode::Empty))),
-        }
-    }
-
-    fn previous_row(&mut self) -> Result<Option<Action>> {
-        let i = match self.list_state.selected() {
-            Some(i) => {
-                if i == 0 {
-                    0
-                } else {
-                    i - 1
-                }
-            },
-            None => 0
-        };
-        self.list_state.select(Some(i));
-        match i {
+    fn move_index(&mut self, new_index: usize) -> Result<Option<Action>> {
+        self.list_state.select(Some(new_index));
+        match new_index {
             0 => Ok(Some(Action::SetMode(Mode::Explorer))),
             1 => Ok(Some(Action::SetMode(Mode::Wallet))),
             2 => Ok(Some(Action::SetMode(Mode::Validator))),
@@ -86,17 +82,12 @@ impl Menu {
 impl Component for Menu {
     fn update(&mut self, action: Action) -> Result<Option<Action>> {
         match action {
-            Action::SetActiveScreen(Mode::Menu) => self.is_active = true,
+            Action::SetActiveScreen(Mode::Menu) => self.set_active(),
             _ => {}
-        };
+        }
         Ok(None)
     }
 
-    fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
-        self.command_tx = Some(tx);
-        Ok(())
-    }
-
     fn register_config_handler(&mut self, config: Config) -> Result<()> {
         if let Some(style) = config.styles.get(&crate::app::Mode::Menu) {
             self.palette.with_normal_style(style.get("normal_style").copied());
@@ -110,10 +101,10 @@ impl Component for Menu {
 
     fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
         match key.code {
-            KeyCode::Up | KeyCode::Char('k') if self.is_active => self.previous_row(), 
-            KeyCode::Down | KeyCode::Char('j') if self.is_active => self.next_row(), 
-            KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active => {
-                self.is_active = false;
+            KeyCode::Up | KeyCode::Char('k') if self.is_active() => self.prev_row(), 
+            KeyCode::Down | KeyCode::Char('j') if self.is_active() => self.next_row(), 
+            KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right if self.is_active() => {
+                self.set_inactive();
                 match self.list_state.selected() {
                     Some(0) => Ok(Some(Action::SetActiveScreen(Mode::Explorer))),
                     Some(1) => Ok(Some(Action::SetActiveScreen(Mode::Wallet))),
@@ -121,10 +112,7 @@ impl Component for Menu {
                     _ => Ok(Some(Action::SetActiveScreen(Mode::Empty))),
                 }
             },
-            KeyCode::Char('?') if self.is_active => {
-                self.is_active = false;
-                Ok(Some(Action::Help))
-            },
+            KeyCode::Char('?') if self.is_active() => self.open_help_popup(),
             _ => Ok(None),
         }
     }
@@ -132,7 +120,7 @@ impl Component for Menu {
     fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
         let [menu, _] = super::menu_layout(area);
 
-        let (color, border_type) = self.palette.create_border_style(self.is_active);
+        let (color, border_type) = self.palette.create_border_style(self.is_active());
         let block = Block::bordered()
             .border_style(color)
             .border_type(border_type);
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 583e81d..f99d43e 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -18,6 +18,7 @@ pub mod wallet;
 pub mod validator;
 pub mod empty;
 pub mod help;
+pub mod generic;
 
 pub trait Component {
     fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {