Skip to content

Commit

Permalink
feat: preview config dir rather open it directly
Browse files Browse the repository at this point in the history
  • Loading branch information
Jackhr-arch committed Feb 10, 2025
1 parent 8e20a4d commit 3b8fb1d
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 19 deletions.
35 changes: 20 additions & 15 deletions src/backend/impl_tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,27 +98,32 @@ impl BackEnd {
Err(e) => CallBack::Error(e.to_string()),
}
}
Call::Service(tabs::service::BackendOp::OpenThis(path)) => {
if let Err(e) = crate::utils::ipc::spawn(
"sh",
vec![
"-c",
if path.is_dir() {
&self.open_dir_cmd
} else {
&self.edit_cmd
}
.replace("%s", path.to_str().unwrap())
.as_str(),
],
) {
CallBack::TuiExtend(vec!["Failed".to_owned(), e.to_string()])
} else {
CallBack::TuiExtend(vec!["Success".to_owned()])
}
}
Call::Service(tabs::service::BackendOp::TuiExtend(extend_op)) => {
match extend_op {
tabs::service::ExtendOp::FullLog => match self.logcat(0, 1024) {
Ok(v) => CallBack::TuiExtend(v),
Err(e) => CallBack::Error(e.to_string()),
},
tabs::service::ExtendOp::OpenClashtuiConfigDir => {
if let Err(e) = crate::utils::ipc::spawn(
"sh",
vec![
"-c",
self.open_dir_cmd
.replace("%s", crate::HOME_DIR.to_str().unwrap())
.as_str(),
],
) {
CallBack::TuiExtend(vec!["Failed".to_owned(), e.to_string()])
} else {
CallBack::TuiExtend(vec!["Success".to_owned()])
}
}
tabs::service::ExtendOp::ViewClashtuiConfigDir => unreachable!(),
tabs::service::ExtendOp::GenerateInfoList => {
let mut infos = vec![
"# CLASHTUI".to_owned(),
Expand Down
33 changes: 29 additions & 4 deletions src/tui/frontend/tabs/service.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::backend::ServiceOp;
use crate::define_enum;
use crate::tui::widget::Browser;
use crate::{clash::webapi::Mode, tui::widget::PopRes};
use crossterm::event::KeyEvent;

Expand All @@ -14,12 +15,13 @@ pub enum BackendOp {
SwitchMode(Mode),
ServiceCTL(ServiceOp),
TuiExtend(ExtendOp),
OpenThis(std::path::PathBuf),
}

define_enum!(
#[derive(Clone, Copy, Debug)]
pub enum ExtendOp {
OpenClashtuiConfigDir,
ViewClashtuiConfigDir,
// Generate a list of information
// about the application and clash core
GenerateInfoList,
Expand All @@ -31,6 +33,7 @@ pub(in crate::tui::frontend) struct ServiceTab {
inner: List,
popup_content: Option<PopMsg>,
backend_content: Option<Call>,
file_browser: Option<Browser>,
}

const MODE: [Mode; 3] = [Mode::Rule, Mode::Direct, Mode::Global];
Expand All @@ -49,6 +52,7 @@ impl Default for ServiceTab {
inner,
popup_content: None,
backend_content: None,
file_browser: None,
}
}
}
Expand Down Expand Up @@ -100,11 +104,26 @@ impl TabCont for ServiceTab {

impl Drawable for ServiceTab {
fn render(&mut self, f: &mut Frame, area: Rect, _: bool) {
self.inner.render(f, area, true);
self.inner.render(f, area, self.file_browser.is_none());
if let Some(instance) = self.file_browser.as_mut() {
instance.render(f, area, true);
}
}
/// - Catched event -> [EventState::WorkDone]
/// - unrecognized event -> [EventState::NotConsumed]
fn handle_key_event(&mut self, ev: &KeyEvent) -> EventState {
if let Some(instance) = self.file_browser.as_mut() {
match instance.handle_key_event(ev) {
EventState::Yes => {
self.backend_content = Some(Call::Service(BackendOp::OpenThis(
self.file_browser.take().unwrap().path(),
)))
}
EventState::Cancel => self.file_browser = None,
EventState::NotConsumed | EventState::WorkDone => (),
}
return EventState::WorkDone;
};
let event_state = self.inner.handle_key_event(ev);
match event_state {
EventState::Yes => {
Expand All @@ -124,8 +143,14 @@ impl Drawable for ServiceTab {
idx if idx == ServiceOp::const_len() + 1 => (),
idx if idx < ServiceOp::const_len() + 2 + ExtendOp::const_len() => {
let op = ExtendOp::ALL[index - 2 - ServiceOp::const_len()];
self.backend_content = Some(Call::Service(BackendOp::TuiExtend(op)));
self.popup_content = Some(PopMsg::Prompt(vec!["working".to_owned()]));
if let ExtendOp::ViewClashtuiConfigDir = op {
self.file_browser = Some(Browser::new(&crate::HOME_DIR))
} else {
self.backend_content =
Some(Call::Service(BackendOp::TuiExtend(op)));
self.popup_content =
Some(PopMsg::Prompt(vec!["working".to_owned()]));
}
}
_ => unreachable!(),
}
Expand Down
187 changes: 187 additions & 0 deletions src/tui/widget/browser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use std::path::PathBuf;

use crossterm::event::{KeyCode, KeyEventKind};
use ratatui::{prelude as Ra, widgets as Raw};

use super::tools;
use crate::tui::misc::EventState;
use crate::tui::{Drawable, Theme};

pub struct Browser {
cwd: PathBuf,
items: Vec<Box<dyn Fp>>,
selected: usize,
}

impl Drawable for Browser {
fn render(&mut self, f: &mut ratatui::Frame, _: ratatui::layout::Rect, _: bool) {
let area = tools::centered_rect(Ra::Constraint::Fill(2), Ra::Constraint::Fill(2), f.area());

let mut state = Raw::ListState::default().with_selected(Some(self.selected));

let list = Raw::List::new(self.items.iter().map(|file| {
Ra::Text::raw(file.name()).style(Ra::Style::default().fg(if file.is_dir() {
Ra::Color::LightCyan
} else {
Ra::Color::default()
}))
}))
.scroll_padding((area.height - 3).div_ceil(2) as usize)
.highlight_style(
Ra::Style::default()
.bg(Theme::get().list_hl_bg_fouced)
.add_modifier(Ra::Modifier::BOLD),
)
.block(
Raw::Block::bordered()
.border_style(Ra::Style::default().fg(Theme::get().list_block_fouced_fg))
.title_top(format!("Browser: {}", self.cwd.display()))
.title_bottom(Ra::Line::raw("Press 'O' to open selected dir/file").right_aligned()),
);

f.render_widget(Raw::Clear, area);
f.render_stateful_widget(list, area, &mut state);
}

fn handle_key_event(&mut self, ev: &crossterm::event::KeyEvent) -> EventState {
if ev.kind != KeyEventKind::Press {
return EventState::NotConsumed;
}
match ev.code {
KeyCode::Right | KeyCode::Enter => {
if self.items[self.selected].is_dir() {
self.cwd = self.items.swap_remove(self.selected).path();
self.update();
} else {
return EventState::Yes;
}
}
KeyCode::Left | KeyCode::Backspace => {
if self.items[0].name() == UPPER_DIR {
self.cwd = self.items.swap_remove(0).path();
self.update();
}
}

KeyCode::Up => self.selected = self.selected.saturating_sub(1),
KeyCode::Down => self.selected = (self.selected + 1).min(self.items.len() - 1),

KeyCode::Home => self.selected = 0,
KeyCode::End => self.selected = self.items.len() - 1,

KeyCode::Char('O') | KeyCode::Char('o') => return EventState::Yes,
KeyCode::Esc => return EventState::Cancel,
_ => (),
};
EventState::WorkDone
}
}

impl Browser {
pub fn new(path: &std::path::Path) -> Self {
let mut instance = Self {
cwd: path.to_path_buf(),
items: vec![],
selected: 0,
};
instance.update();
instance
}
pub fn path(mut self) -> std::path::PathBuf {
self.items.swap_remove(self.selected).path()
}
fn update(&mut self) {
if let Err(e) = self.get_and_set_files() {
let err = format!("Failed to open {}: {e}", self.cwd.display());
log::error!("{err}");
self.items = vec![Box::new(FsErr(err))];
};
self.selected = 0
}
fn get_and_set_files(&mut self) -> std::io::Result<()> {
let (mut dirs, mut none_dirs): (Vec<_>, Vec<_>) = std::fs::read_dir(&self.cwd)?
.filter_map(|entry| entry.ok())
.map(|e| {
let path = e.path();
Box::new(FileOrDir(path)).to_dyn()
})
.partition(|file| file.is_dir());

dirs.sort_unstable_by(|f1, f2| f1.name().cmp(&f2.name()));
none_dirs.sort_unstable_by(|f1, f2| f1.name().cmp(&f2.name()));

if let Some(parent) = self.cwd.parent() {
let mut files: Vec<Box<dyn Fp>> = Vec::with_capacity(1 + dirs.len() + none_dirs.len());

files.push(Box::new(UpperDir(parent.to_path_buf())));

files.extend(dirs);
files.extend(none_dirs);

self.items = files
} else {
let mut files = Vec::with_capacity(dirs.len() + none_dirs.len());

files.extend(dirs);
files.extend(none_dirs);

self.items = files;
};

Ok(())
}
}

const UPPER_DIR: &str = "../";
trait Fp: Send {
fn name(&self) -> std::borrow::Cow<'_, str>;
fn is_dir(&self) -> bool;
fn path(self: Box<Self>) -> std::path::PathBuf;
#[inline]
fn to_dyn(self: Box<Self>) -> Box<dyn Fp>
where
Self: Sized + 'static,
{
self
}
}
#[derive(Debug)]
struct FileOrDir(PathBuf);
impl Fp for FileOrDir {
/// the path is built from [std::fs::DirEntry]
/// thus it will never end in '.','..'
fn name(&self) -> std::borrow::Cow<'_, str> {
self.0.file_name().unwrap().to_string_lossy()
}
fn is_dir(&self) -> bool {
self.0.is_dir()
}
fn path(self: Box<FileOrDir>) -> std::path::PathBuf {
self.0
}
}
struct UpperDir(PathBuf);
impl Fp for UpperDir {
fn name(&self) -> std::borrow::Cow<'_, str> {
std::borrow::Cow::Borrowed(UPPER_DIR)
}
fn is_dir(&self) -> bool {
self.0.is_dir()
}
fn path(self: Box<UpperDir>) -> std::path::PathBuf {
self.0
}
}
struct FsErr(String);
impl Fp for FsErr {
fn name(&self) -> std::borrow::Cow<'_, str> {
std::borrow::Cow::Borrowed(&self.0)
}
fn is_dir(&self) -> bool {
true
}
/// current, just redirect to temp dir
fn path(self: Box<Self>) -> std::path::PathBuf {
std::env::temp_dir()
}
}
2 changes: 2 additions & 0 deletions src/tui/widget/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod browser;
mod input;
mod list;
mod popup;
pub mod tools;

pub use browser::Browser;
pub use list::List;
pub use popup::Popup;

Expand Down

0 comments on commit 3b8fb1d

Please sign in to comment.