diff --git a/amdhelper-tui/Cargo.toml b/amdhelper-tui/Cargo.toml new file mode 100644 index 0000000..60b7943 --- /dev/null +++ b/amdhelper-tui/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "amdhelper-tui" +version = "0.1.0" +edition = "2021" + +[dependencies] +amdhelper = { path = "../amdhelper" } +crossterm = "0.28.1" +ratatui = "0.28.1" diff --git a/amdhelper-tui/src/main.rs b/amdhelper-tui/src/main.rs new file mode 100644 index 0000000..af66f62 --- /dev/null +++ b/amdhelper-tui/src/main.rs @@ -0,0 +1,168 @@ +#![warn(clippy::nursery, unused_extern_crates)] + +use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; +use ethylene_amd::{apply_chromium_patch, get_apps, get_patches, patch::{chromium::ChromiumPatch, EthylenePatch}, EthyleneApp}; +use ratatui::{ + buffer::Buffer, + layout::{Alignment, Rect}, + style::{palette::tailwind::SLATE, Modifier, Style, Stylize}, + text::Line, + widgets::{ + block::{Position, Title}, + Block, List, ListItem, ListState, StatefulWidget, Widget, + }, + DefaultTerminal, +}; +fn main() -> std::io::Result<()> { + let mut terminal = ratatui::init(); + terminal.clear()?; + + let app_result = App::default().and_then(|v| v.run(&mut terminal)); + ratatui::restore(); + app_result +} + +const SELECTED_STYLE: Style = Style::new().bg(SLATE.c300).add_modifier(Modifier::BOLD); + +struct App { + exit: bool, + app_list: AppList, +} + +struct AppList { + items: Vec, + selected_items: Vec, + state: ListState, +} + +impl App { + fn default() -> std::io::Result { + let patches = get_patches(); + Ok(Self { + exit: false, + app_list: AppList { + items: get_apps(&patches)?, + selected_items: vec![], + state: ListState::default(), + }, + }) + } + + pub fn run(mut self, terminal: &mut DefaultTerminal) -> std::io::Result<()> { + while !self.exit { + terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; + self.handle_events()?; + } + Ok(()) + } + + fn handle_events(&mut self) -> std::io::Result<()> { + match crossterm::event::read()? { + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + self.handle_key_event(key_event) + } + _ => {} + }; + Ok(()) + } + + fn handle_key_event(&mut self, key_event: KeyEvent) { + match key_event.code { + KeyCode::Char('q') | KeyCode::Esc => self.exit(), + KeyCode::Char('j') | KeyCode::Down => self.select_next(), + KeyCode::Char('k') | KeyCode::Up => self.select_previous(), + KeyCode::Char('a') => self.apply_patches(), + KeyCode::Enter => self.select_item(), + _ => {} + } + } + + fn apply_patches(&mut self) { + if self.app_list.selected_items.len() < 1 { return } + apply_chromium_patch(ChromiumPatch, self.get_selected_apps()); + + self.app_list.selected_items = vec![]; + } + + fn get_selected_apps(&mut self) -> Vec<&EthyleneApp> { + self.app_list.selected_items.iter().map(|i| self.app_list.items.get(*i).unwrap()).collect::>() + } + + fn exit(&mut self) { + self.exit = true; + } + + fn select_item(&mut self) { + let selected_index = self.app_list.state.selected().unwrap(); + let selected_items = self.app_list.selected_items.clone(); + if selected_items.contains(&selected_index) { + self.app_list.selected_items.remove( + selected_items + .iter() + .position(|item| *item == selected_index) + .unwrap(), + ); + } else { + self.app_list.selected_items.push(selected_index); + } + } + + fn select_next(&mut self) { + self.app_list.state.select_next(); + } + + fn select_previous(&mut self) { + self.app_list.state.select_previous(); + } +} + +impl Widget for &mut App { + fn render(self, area: Rect, buf: &mut Buffer) { + let title = Title::from(" EthyleneAMD ".bold()).alignment(Alignment::Center); + let instructions = Title::from(Line::from(vec![ + " ".red(), + "Apply".into(), + " ".red(), + "Quit ".into(), + ])) + .alignment(Alignment::Center) + .position(Position::Bottom); + let block = Block::bordered() + .title(title) + .title(instructions) + .border_type(ratatui::widgets::BorderType::Rounded); + + self.render_list(block, area, buf); + } +} + +impl App { + fn render_list(&mut self, block: Block<'_>, area: Rect, buf: &mut Buffer) { + let items: Vec = self + .app_list + .items + .iter() + .enumerate() + .map(|(i, app_item)| { + ListItem::from(format!( + "{}. {}", + i + 1, + app_item.name.clone() + + (if self.app_list.selected_items.contains(&i) { + " <" + } else { + "" + }) + )) + }) + .collect(); + + let list = List::new(items) + .block(block) + .highlight_style(SELECTED_STYLE) + .highlight_symbol(">") + .highlight_spacing(ratatui::widgets::HighlightSpacing::Always); + + StatefulWidget::render(list, area, buf, &mut self.app_list.state); + } +}