Skip to content

Commit 73a30d6

Browse files
committed
feat: add full-featured interactive mode (closes #17)
Yeah.. I wanted to relase it to v2.3.0 but released now... :)
1 parent f6db41f commit 73a30d6

File tree

8 files changed

+562
-914
lines changed

8 files changed

+562
-914
lines changed

src/commands.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::process::ExitCode;
66
use std::{env, fs};
77

88
use crate::config;
9-
use crate::interactive::InteractiveMode;
9+
use crate::interactive::InteractiveApp;
1010
use crate::models::*;
1111
use crate::utils::*;
1212
use crate::variables;
@@ -85,7 +85,7 @@ pub fn run_command<W: Write>(
8585
}
8686
Commands::Interactive => {
8787
let mut terminal = ratatui::init();
88-
let result = InteractiveMode::init().run(&mut terminal);
88+
let result = InteractiveApp::new().run(&mut terminal);
8989
ratatui::restore();
9090
if let Err(error) = result {
9191
error!("{}", error);

src/interactive.rs

+20-152
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,33 @@
1-
mod draw;
2-
mod list;
3-
mod tests;
4-
1+
pub mod state;
2+
pub mod controller;
3+
pub mod view;
4+
#[cfg(test)]
5+
pub mod tests;
6+
7+
use crate::variables; // Function to get environment variables.
8+
use ratatui::{backend::Backend, Terminal};
59
use std::io;
610

7-
use super::variables::get_variables;
8-
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
9-
use ratatui::{Frame, Terminal};
10-
11-
#[derive(Clone)]
12-
pub enum Mode {
13-
List,
14-
}
15-
16-
impl Default for Mode {
17-
fn default() -> Self {
18-
Self::List
19-
}
20-
}
21-
22-
#[derive(Clone)]
23-
pub struct InteractiveMode {
24-
mode: Mode,
25-
exit: bool,
26-
entries: Vec<(String, String)>,
27-
current_index: usize,
28-
scroll_offset: usize,
29-
visible_options: usize,
30-
truncation_len: usize,
31-
value_scroll_offset: usize, // For horizontal scrolling in value panel
11+
pub struct InteractiveApp {
12+
state: state::AppState,
3213
}
3314

34-
impl Default for InteractiveMode {
35-
fn default() -> Self {
36-
InteractiveMode {
37-
mode: Mode::List,
38-
exit: false,
39-
entries: get_variables(),
40-
current_index: 0,
41-
scroll_offset: 0,
42-
visible_options: 30,
43-
truncation_len: 30,
44-
value_scroll_offset: 0,
15+
impl InteractiveApp {
16+
pub fn new() -> Self {
17+
Self {
18+
state: state::AppState::new(variables::get_variables()),
4519
}
4620
}
47-
}
48-
49-
impl InteractiveMode {
50-
/// Initialize InteractiveMode
51-
pub fn init() -> Self {
52-
Self::default()
53-
}
5421

55-
/// Run TUI interface for interactive mode
56-
pub fn run<B>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()>
22+
pub fn run<B>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()>
5723
where
58-
B: ratatui::backend::Backend,
24+
B: Backend,
5925
{
60-
while !self.exit {
61-
terminal.draw(|frame| self.draw(frame))?;
62-
self.handle_events()?;
26+
while !self.state.should_quit {
27+
terminal.draw(|f| view::render(&self.state, f))?;
28+
// Handle input (this may update scrolling, reload, etc.)
29+
controller::handle_input(&mut self.state)?;
6330
}
6431
Ok(())
6532
}
66-
67-
/// Draw TUI
68-
fn draw(&mut self, frame: &mut Frame) {
69-
frame.render_widget(self, frame.area());
70-
}
71-
72-
/// Handle events
73-
fn handle_events(&mut self) -> io::Result<()> {
74-
match event::read()? {
75-
// it's important to check that the event is a key press event as
76-
// crossterm also emits key release and repeat events on Windows.
77-
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
78-
self.handle_key_event(key_event)
79-
}
80-
_ => {}
81-
};
82-
Ok(())
83-
}
84-
85-
/// Handle keypresses
86-
fn handle_key_event(&mut self, key_event: KeyEvent) {
87-
match key_event.code {
88-
KeyCode::Char('q') | KeyCode::Char('Q')
89-
if key_event.modifiers == KeyModifiers::CONTROL =>
90-
{
91-
self.exit()
92-
}
93-
KeyCode::Down => self.down(),
94-
KeyCode::Up => self.up(),
95-
KeyCode::Left => self.scroll_value_left(),
96-
KeyCode::Right => self.scroll_value_right(),
97-
KeyCode::Char('r') | KeyCode::Char('R')
98-
if key_event.modifiers == KeyModifiers::CONTROL =>
99-
{
100-
self.reload()
101-
}
102-
_ => {}
103-
}
104-
}
105-
106-
/// Scroll value left
107-
fn scroll_value_left(&mut self) {
108-
if self.value_scroll_offset > 0 {
109-
self.value_scroll_offset -= 1;
110-
}
111-
}
112-
113-
/// Scroll value right
114-
fn scroll_value_right(&mut self) {
115-
if let Some((_, value)) = self.entries.get(self.current_index) {
116-
if self.value_scroll_offset < value.len() {
117-
self.value_scroll_offset += 1;
118-
}
119-
}
120-
}
121-
122-
/// Exit
123-
fn exit(&mut self) {
124-
self.exit = true;
125-
}
126-
127-
/// Scroll list down
128-
fn down(&mut self) {
129-
let max_index = self.entries.len().saturating_sub(1);
130-
if self.current_index < max_index {
131-
self.current_index += 1;
132-
self.value_scroll_offset = 0;
133-
134-
// Keep a fixed number of items visible before scrolling
135-
let visible_area = self.visible_options.saturating_sub(8);
136-
let scroll_trigger = self.scroll_offset + (visible_area.saturating_sub(4));
137-
138-
// Only scroll when we're past the visible area
139-
if self.current_index > scroll_trigger {
140-
self.scroll_offset += 1;
141-
}
142-
}
143-
}
144-
145-
/// Scroll list up
146-
fn up(&mut self) {
147-
if self.current_index > 0 {
148-
self.current_index -= 1;
149-
self.value_scroll_offset = 0;
150-
151-
// Scroll up when cursor moves above current scroll position
152-
if self.current_index < self.scroll_offset {
153-
self.scroll_offset = self.current_index;
154-
}
155-
}
156-
}
157-
158-
/// Reload variables list
159-
fn reload(&mut self) {
160-
self.entries = super::variables::get_variables();
161-
self.current_index = 0;
162-
self.scroll_offset = 0;
163-
self.value_scroll_offset = 0;
164-
}
16533
}

0 commit comments

Comments
 (0)