feat(move): add move functionality

This commit is contained in:
Kristofers Solo 2025-07-09 18:05:08 +03:00
parent 095b102ecf
commit 8870478cc6
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
14 changed files with 211 additions and 136 deletions

View File

@ -13,6 +13,7 @@ delete = "d"
delete_force = "D" delete_force = "D"
select = " " select = " "
toggle_help = "?" toggle_help = "?"
move = "m"
[colors] [colors]
highlight_background = "magenta" highlight_background = "magenta"

View File

@ -6,7 +6,7 @@ pub enum Action {
NextTorrent, NextTorrent,
PrevTorrent, PrevTorrent,
SwitchTab(u8), SwitchTab(u8),
ToggleHelp, // Add this line ToggleHelp,
ToggleTorrent, ToggleTorrent,
ToggleAll, ToggleAll,
PauseAll, PauseAll,
@ -15,4 +15,6 @@ pub enum Action {
Delete(bool), Delete(bool),
Rename, Rename,
Select, Select,
Submit,
Cancel,
} }

View File

@ -1,9 +1,10 @@
use super::{types::Selected, Torrents}; use super::{types::Selected, Torrents};
use color_eyre::{eyre::eyre, Result};
use std::{collections::HashSet, path::Path}; use std::{collections::HashSet, path::Path};
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus}; use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
impl Torrents { impl Torrents {
pub async fn toggle(&mut self, ids: Selected) -> color_eyre::eyre::Result<()> { pub async fn toggle(&mut self, ids: Selected) -> Result<()> {
let ids: HashSet<_> = ids.into(); let ids: HashSet<_> = ids.into();
let torrents_to_toggle: Vec<_> = self let torrents_to_toggle: Vec<_> = self
.torrents .torrents
@ -20,15 +21,13 @@ impl Torrents {
self.client self.client
.torrent_action(action, vec![id]) .torrent_action(action, vec![id])
.await .await
.map_err(|e| { .map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
})?;
} }
} }
Ok(()) Ok(())
} }
pub async fn toggle_all(&mut self) -> color_eyre::eyre::Result<()> { pub async fn toggle_all(&mut self) -> Result<()> {
let torrents_to_toggle: Vec<_> = self let torrents_to_toggle: Vec<_> = self
.torrents .torrents
.iter() .iter()
@ -49,18 +48,16 @@ impl Torrents {
self.client self.client
.torrent_action(action, vec![id]) .torrent_action(action, vec![id])
.await .await
.map_err(|e| { .map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
})?;
} }
Ok(()) Ok(())
} }
pub async fn start_all(&mut self) -> color_eyre::eyre::Result<()> { pub async fn start_all(&mut self) -> Result<()> {
self.action_all(TorrentAction::StartNow).await self.action_all(TorrentAction::StartNow).await
} }
pub async fn stop_all(&mut self) -> color_eyre::eyre::Result<()> { pub async fn stop_all(&mut self) -> Result<()> {
self.action_all(TorrentAction::Stop).await self.action_all(TorrentAction::Stop).await
} }
@ -69,43 +66,35 @@ impl Torrents {
torrent: &Torrent, torrent: &Torrent,
location: &Path, location: &Path,
move_from: Option<bool>, move_from: Option<bool>,
) -> color_eyre::eyre::Result<()> { ) -> Result<()> {
if let Some(id) = torrent.id() { if let Some(id) = torrent.id() {
self.client self.client
.torrent_set_location(vec![id], location.to_string_lossy().into(), move_from) .torrent_set_location(vec![id], location.to_string_lossy().into(), move_from)
.await .await
.map_err(|e| { .map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
})?;
} }
Ok(()) Ok(())
} }
pub async fn delete( pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> Result<()> {
&mut self,
ids: Selected,
delete_local_data: bool,
) -> color_eyre::eyre::Result<()> {
self.client self.client
.torrent_remove(ids.into(), delete_local_data) .torrent_remove(ids.into(), delete_local_data)
.await .await
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?; .map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
Ok(()) Ok(())
} }
pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> color_eyre::eyre::Result<()> { pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> Result<()> {
if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) { if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) {
self.client self.client
.torrent_rename_path(vec![id], old_name, name.to_string_lossy().into()) .torrent_rename_path(vec![id], old_name, name.to_string_lossy().into())
.await .await
.map_err(|e| { .map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
})?;
} }
Ok(()) Ok(())
} }
async fn action_all(&mut self, action: TorrentAction) -> color_eyre::eyre::Result<()> { async fn action_all(&mut self, action: TorrentAction) -> Result<()> {
let ids = self let ids = self
.torrents .torrents
.iter() .iter()
@ -115,7 +104,7 @@ impl Torrents {
self.client self.client
.torrent_action(action, ids) .torrent_action(action, ids)
.await .await
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?; .map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
Ok(()) Ok(())
} }
} }

View File

@ -6,6 +6,7 @@ pub mod types;
pub mod utils; pub mod utils;
use crate::config::Config; use crate::config::Config;
use color_eyre::Result;
use ratatui::widgets::TableState; use ratatui::widgets::TableState;
use types::Selected; use types::Selected;
pub use {tab::Tab, torrent::Torrents}; pub use {tab::Tab, torrent::Torrents};
@ -20,12 +21,15 @@ pub struct App<'a> {
pub torrents: Torrents, pub torrents: Torrents,
pub show_help: bool, pub show_help: bool,
pub config: Config, pub config: Config,
pub input: String,
pub cursor_position: usize,
pub input_mode: bool,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
/// Constructs a new instance of [`App`]. /// Constructs a new instance of [`App`].
/// Returns instance of `Self`. /// Returns instance of `Self`.
pub fn new(config: Config) -> color_eyre::eyre::Result<Self> { pub fn new(config: Config) -> Result<Self> {
Ok(Self { Ok(Self {
running: true, running: true,
tabs: &[Tab::All, Tab::Active, Tab::Downloading], tabs: &[Tab::All, Tab::Active, Tab::Downloading],
@ -34,11 +38,14 @@ impl<'a> App<'a> {
torrents: Torrents::new()?, // Handle the Result here torrents: Torrents::new()?, // Handle the Result here
show_help: false, show_help: false,
config, config,
input: String::new(),
cursor_position: 0,
input_mode: false,
}) })
} }
/// Handles the tick event of the terminal. /// Handles the tick event of the terminal.
pub async fn tick(&mut self) -> color_eyre::eyre::Result<()> { pub async fn tick(&mut self) -> Result<()> {
self.torrents.update().await?; self.torrents.update().await?;
Ok(()) Ok(())
} }
@ -122,20 +129,28 @@ impl<'a> App<'a> {
self.show_help = true; self.show_help = true;
} }
pub async fn toggle_torrents(&mut self) -> color_eyre::eyre::Result<()> { pub async fn toggle_torrents(&mut self) -> Result<()> {
let ids = self.selected(false); let ids = self.selected(false);
self.torrents.toggle(ids).await?; self.torrents.toggle(ids).await?;
self.close_help(); self.close_help();
Ok(()) Ok(())
} }
pub async fn delete(&mut self, delete_local_data: bool) -> color_eyre::eyre::Result<()> { pub async fn delete(&mut self, delete_local_data: bool) -> Result<()> {
let ids = self.selected(false); let ids = self.selected(false);
self.torrents.delete(ids, delete_local_data).await?; self.torrents.delete(ids, delete_local_data).await?;
self.close_help(); self.close_help();
Ok(()) Ok(())
} }
pub async fn move_torrent(&mut self) -> Result<()> {
self.torrents.move_selection(&self.input).await?;
self.input.clear();
self.cursor_position = 0;
self.input_mode = false;
Ok(())
}
pub fn select(&mut self) { pub fn select(&mut self) {
if let Selected::Current(current_id) = self.selected(true) { if let Selected::Current(current_id) = self.selected(true) {
if self.torrents.selected.contains(&current_id) { if self.torrents.selected.contains(&current_id) {

View File

@ -1,7 +1,7 @@
use color_eyre::eyre::Result; use color_eyre::{eyre::eyre, Result};
use std::{collections::HashSet, fmt::Debug}; use std::{collections::HashSet, fmt::Debug};
use transmission_rpc::{ use transmission_rpc::{
types::{Torrent, TorrentGetField}, types::{Id, Torrent, TorrentGetField},
TransClient, TransClient,
}; };
use url::Url; use url::Url;
@ -50,11 +50,20 @@ impl Torrents {
.client .client
.torrent_get(self.fields.clone(), None) .torrent_get(self.fields.clone(), None)
.await .await
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))? .map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?
.arguments .arguments
.torrents; .torrents;
Ok(self) Ok(self)
} }
pub async fn move_selection(&mut self, location: &str) -> Result<()> {
let ids: Vec<Id> = self.selected.iter().map(|id| Id::Id(*id)).collect();
self.client
.torrent_set_location(ids, location.to_string(), Some(true))
.await
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
Ok(())
}
} }
impl Debug for Torrents { impl Debug for Torrents {
@ -66,7 +75,9 @@ impl Debug for Torrents {
write!( write!(
f, f,
"fields: "fields:
{:?};\n\ntorrents: {:?}", {:?};
torrents: {:?}",
fields, self.torrents fields, self.torrents
) )
} }

View File

@ -11,11 +11,11 @@ use transmission_rpc::types::{
pub trait Wrapper { pub trait Wrapper {
fn title(&self) -> String { fn title(&self) -> String {
"".to_string() String::new()
} }
fn value(&self, torrent: &Torrent) -> String { fn value(&self, torrent: &Torrent) -> String {
format!("{}", torrent.name.as_ref().unwrap_or(&String::from(""))) torrent.name.clone().unwrap_or_default()
} }
fn width(&self) -> u16 { fn width(&self) -> u16 {

View File

@ -18,6 +18,7 @@ pub struct KeybindsConfig {
pub delete_force: Option<String>, pub delete_force: Option<String>,
pub select: Option<String>, pub select: Option<String>,
pub toggle_help: Option<String>, pub toggle_help: Option<String>,
pub move_torrent: Option<String>,
} }
impl Default for KeybindsConfig { impl Default for KeybindsConfig {
@ -37,6 +38,7 @@ impl Default for KeybindsConfig {
delete_force: Some("D".to_string()), delete_force: Some("D".to_string()),
select: Some(" ".to_string()), select: Some(" ".to_string()),
toggle_help: Some("?".to_string()), toggle_help: Some("?".to_string()),
move_torrent: Some("m".to_string()),
} }
} }
} }

View File

@ -1,4 +1,4 @@
use color_eyre::eyre::Result; use color_eyre::Result;
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;

View File

@ -1,109 +1,68 @@
use crate::app::{action::Action, App}; use crate::app::{action::Action, App};
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tracing::{event, info_span, Level}; use tracing::{event, info_span, Level};
/// Handles the key events of [`App`]. fn handle_input(key_event: KeyEvent, app: &mut App) -> Option<Action> {
#[tracing::instrument]
pub fn get_action(key_event: KeyEvent, app: &App) -> Option<Action> {
let span = info_span!("get_action");
let _enter = span.enter();
event!(Level::INFO, "handling key event: {:?}", key_event);
let config_keybinds = &app.config.keybinds;
// Helper to check if a KeyEvent matches a configured keybind string
let matches_keybind = |event: &KeyEvent, config_key: &Option<String>| {
if let Some(key_str) = config_key {
let parts: Vec<&str> = key_str.split('+').collect();
let mut parsed_modifiers = KeyModifiers::NONE;
let mut parsed_key_code = None;
for part in &parts {
match part.to_lowercase().as_str() {
"ctrl" => parsed_modifiers.insert(KeyModifiers::CONTROL),
"alt" => parsed_modifiers.insert(KeyModifiers::ALT),
"shift" => parsed_modifiers.insert(KeyModifiers::SHIFT),
"esc" => parsed_key_code = Some(KeyCode::Esc),
"enter" => parsed_key_code = Some(KeyCode::Enter),
"left" => parsed_key_code = Some(KeyCode::Left),
"right" => parsed_key_code = Some(KeyCode::Right),
"up" => parsed_key_code = Some(KeyCode::Up),
"down" => parsed_key_code = Some(KeyCode::Down),
"tab" => parsed_key_code = Some(KeyCode::Tab),
"backspace" => parsed_key_code = Some(KeyCode::Backspace),
"delete" => parsed_key_code = Some(KeyCode::Delete),
"home" => parsed_key_code = Some(KeyCode::Home),
"end" => parsed_key_code = Some(KeyCode::End),
"pageup" => parsed_key_code = Some(KeyCode::PageUp),
"pagedown" => parsed_key_code = Some(KeyCode::PageDown),
"null" => parsed_key_code = Some(KeyCode::Null),
"insert" => parsed_key_code = Some(KeyCode::Insert),
_ => {
if part.len() == 1 {
if let Some(c) = part.chars().next() {
parsed_key_code = Some(KeyCode::Char(c));
} else {
return false;
}
} else if part.starts_with("f") && part.len() > 1 {
if let Ok(f_num) = part[1..].parse::<u8>() {
parsed_key_code = Some(KeyCode::F(f_num));
} else {
return false;
}
} else {
return false;
}
}
}
}
if parsed_key_code.is_none() {
return false;
}
event.code == parsed_key_code.unwrap() && event.modifiers == parsed_modifiers
} else {
false
}
};
match key_event.code { match key_event.code {
_ if matches_keybind(&key_event, &config_keybinds.quit) => Some(Action::Quit), KeyCode::Enter => Some(Action::Submit),
_ if matches_keybind(&key_event, &config_keybinds.next_tab) => Some(Action::NextTab), KeyCode::Char(c) => {
_ if matches_keybind(&key_event, &config_keybinds.prev_tab) => Some(Action::PrevTab), app.input.push(c);
_ if matches_keybind(&key_event, &config_keybinds.next_torrent) => { app.cursor_position = app.input.len();
Some(Action::NextTorrent) None
} }
_ if matches_keybind(&key_event, &config_keybinds.prev_torrent) => { KeyCode::Backspace => {
Some(Action::PrevTorrent) app.input.pop();
app.cursor_position = app.input.len();
None
} }
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_1) => { KeyCode::Esc => Some(Action::Cancel),
Some(Action::SwitchTab(0))
}
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_2) => {
Some(Action::SwitchTab(1))
}
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_3) => {
Some(Action::SwitchTab(2))
}
_ if matches_keybind(&key_event, &config_keybinds.toggle_torrent) => {
Some(Action::ToggleTorrent)
}
_ if matches_keybind(&key_event, &config_keybinds.toggle_all) => Some(Action::ToggleAll),
_ if matches_keybind(&key_event, &config_keybinds.delete) => Some(Action::Delete(false)),
_ if matches_keybind(&key_event, &config_keybinds.delete_force) => {
Some(Action::Delete(true))
}
_ if matches_keybind(&key_event, &config_keybinds.select) => Some(Action::Select),
_ if matches_keybind(&key_event, &config_keybinds.toggle_help) => Some(Action::ToggleHelp),
_ => None, _ => None,
} }
} }
/// Handles the key events of [`App`].
#[tracing::instrument]
pub fn get_action(key_event: KeyEvent, app: &mut App) -> Option<Action> {
if app.input_mode {
return handle_input(key_event, app);
}
let span = info_span!("get_action");
let _enter = span.enter();
event!(Level::INFO, "handling key event: {:?}", key_event);
let keybinds = &app.config.keybinds;
let actions = [
(Action::Quit, &keybinds.quit),
(Action::NextTab, &keybinds.next_tab),
(Action::PrevTab, &keybinds.prev_tab),
(Action::NextTorrent, &keybinds.next_torrent),
(Action::PrevTorrent, &keybinds.prev_torrent),
(Action::SwitchTab(0), &keybinds.switch_tab_1),
(Action::SwitchTab(1), &keybinds.switch_tab_2),
(Action::SwitchTab(2), &keybinds.switch_tab_3),
(Action::ToggleTorrent, &keybinds.toggle_torrent),
(Action::ToggleAll, &keybinds.toggle_all),
(Action::Delete(false), &keybinds.delete),
(Action::Delete(true), &keybinds.delete_force),
(Action::Select, &keybinds.select),
(Action::ToggleHelp, &keybinds.toggle_help),
(Action::Move, &keybinds.move_torrent),
];
for (action, keybind) in actions {
if matches_keybind(&key_event, keybind) {
return Some(action);
}
}
None
}
/// Handles the updates of [`App`]. /// Handles the updates of [`App`].
#[tracing::instrument] #[tracing::instrument]
pub async fn update(app: &mut App<'_>, action: Action) -> color_eyre::eyre::Result<()> { pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
let span = info_span!("update"); let span = info_span!("update");
let _enter = span.enter(); let _enter = span.enter();
event!(Level::INFO, "updating app with action: {:?}", action); event!(Level::INFO, "updating app with action: {:?}", action);
@ -119,10 +78,76 @@ pub async fn update(app: &mut App<'_>, action: Action) -> color_eyre::eyre::Resu
Action::ToggleAll => app.torrents.toggle_all().await?, Action::ToggleAll => app.torrents.toggle_all().await?,
Action::PauseAll => app.torrents.stop_all().await?, Action::PauseAll => app.torrents.stop_all().await?,
Action::StartAll => app.torrents.start_all().await?, Action::StartAll => app.torrents.start_all().await?,
Action::Move => unimplemented!(), Action::Move => app.input_mode = true,
Action::Delete(x) => app.delete(x).await?, Action::Delete(x) => app.delete(x).await?,
Action::Rename => unimplemented!(), Action::Rename => unimplemented!(),
Action::Select => app.select(), Action::Select => app.select(),
Action::Submit => app.move_torrent().await?,
Action::Cancel => {
app.input.clear();
app.input_mode = false;
}
} }
Ok(()) Ok(())
} }
/// Check if a KeyEvent matches a configured keybind string
fn matches_keybind(event: &KeyEvent, config_key: &Option<String>) -> bool {
let Some(key_str) = config_key else {
return false;
};
let (modifiers, key_code) = parse_keybind(key_str);
let Some(key_code) = key_code else {
return false;
};
event.code == key_code && event.modifiers == modifiers
}
fn parse_keybind(key_str: &str) -> (KeyModifiers, Option<KeyCode>) {
let mut modifiers = KeyModifiers::NONE;
let mut key_code = None;
for part in key_str.split('+') {
match part.trim().to_lowercase().as_str() {
"ctrl" => modifiers.insert(KeyModifiers::CONTROL),
"alt" => modifiers.insert(KeyModifiers::ALT),
"shift" => modifiers.insert(KeyModifiers::SHIFT),
key @ ("esc" | "enter" | "left" | "right" | "up" | "down" | "tab" | "backspace"
| "delete" | "home" | "end" | "pageup" | "pagedown" | "null" | "insert") => {
key_code = Some(match key {
"esc" => KeyCode::Esc,
"enter" => KeyCode::Enter,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"tab" => KeyCode::Tab,
"backspace" => KeyCode::Backspace,
"delete" => KeyCode::Delete,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"null" => KeyCode::Null,
"insert" => KeyCode::Insert,
_ => unreachable!(),
});
}
f_key if f_key.starts_with('f') => {
if let Ok(num) = f_key[1..].parse::<u8>() {
key_code = Some(KeyCode::F(num));
}
}
single_char if single_char.len() == 1 => {
if let Some(c) = single_char.chars().next() {
key_code = Some(KeyCode::Char(c));
}
}
_ => return (modifiers, None),
}
}
(modifiers, key_code)
}

View File

@ -1,4 +1,4 @@
use color_eyre::eyre::Result; use color_eyre::Result;
use tracing_appender::rolling; use tracing_appender::rolling;
use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

View File

@ -1,6 +1,6 @@
mod log; mod log;
use color_eyre::eyre::Result; use color_eyre::Result;
use log::setup_logger; use log::setup_logger;
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use std::io; use std::io;
@ -40,7 +40,7 @@ async fn main() -> Result<()> {
match tui.events.next()? { match tui.events.next()? {
Event::Tick => app.tick().await?, Event::Tick => app.tick().await?,
Event::Key(key_event) => { Event::Key(key_event) => {
if let Some(action) = get_action(key_event, &app) { if let Some(action) = get_action(key_event, &mut app) {
update(&mut app, action).await?; update(&mut app, action).await?;
} }
} }

View File

@ -1,7 +1,7 @@
use crate::app::App; use crate::app::App;
use crate::event::EventHandler; use crate::event::EventHandler;
use crate::ui; use crate::ui;
use color_eyre::eyre::Result; use color_eyre::Result;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::Backend; use ratatui::backend::Backend;

25
src/ui/input.rs Normal file
View File

@ -0,0 +1,25 @@
use crate::app::App;
use ratatui::{prelude::*, widgets::*};
pub fn render(f: &mut Frame, app: &mut App) {
let size = f.area();
let input_area = Rect::new(size.width / 4, size.height / 2 - 1, size.width / 2, 3);
let block = Block::default().title("Move to").borders(Borders::ALL);
f.render_widget(Clear, input_area);
f.render_widget(block, input_area);
let input = Paragraph::new(app.input.as_str()).block(Block::default());
f.render_widget(
input,
input_area.inner(Margin {
vertical: 1,
horizontal: 1,
}),
);
f.set_cursor_position(ratatui::layout::Position::new(
input_area.x + app.cursor_position as u16 + 1,
input_area.y + 1,
));
}

View File

@ -1,4 +1,5 @@
mod help; mod help;
mod input;
mod table; mod table;
use crate::app::{App, Tab}; use crate::app::{App, Tab};
@ -63,4 +64,8 @@ pub fn render(app: &mut App, frame: &mut Frame) {
if app.show_help { if app.show_help {
render_help(frame, app); render_help(frame, app);
} }
if app.input_mode {
input::render(frame, app);
}
} }