mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
291 lines
7.7 KiB
Rust
291 lines
7.7 KiB
Rust
use super::{
|
|
utils::{
|
|
filetree::{FileTreeItem, FileTreeItemKind},
|
|
statustree::{MoveSelection, StatusTree},
|
|
},
|
|
CommandBlocking, DrawableComponent,
|
|
};
|
|
use crate::{
|
|
components::{CommandInfo, Component},
|
|
keys,
|
|
queue::{InternalEvent, NeedsUpdate, Queue},
|
|
strings, ui,
|
|
ui::style::Theme,
|
|
};
|
|
|
|
use anyhow::Result;
|
|
use asyncgit::{hash, StatusItem, StatusItemType};
|
|
use crossterm::event::Event;
|
|
use std::{borrow::Cow, convert::From, path::Path};
|
|
use strings::{commands, order};
|
|
use tui::{backend::Backend, layout::Rect, widgets::Text, Frame};
|
|
|
|
///
|
|
pub struct FileTreeComponent {
|
|
title: String,
|
|
tree: StatusTree,
|
|
current_hash: u64,
|
|
focused: bool,
|
|
show_selection: bool,
|
|
queue: Queue,
|
|
theme: Theme,
|
|
}
|
|
|
|
impl FileTreeComponent {
|
|
///
|
|
pub fn new(
|
|
title: &str,
|
|
focus: bool,
|
|
queue: Queue,
|
|
theme: &Theme,
|
|
) -> Self {
|
|
Self {
|
|
title: title.to_string(),
|
|
tree: StatusTree::default(),
|
|
current_hash: 0,
|
|
focused: focus,
|
|
show_selection: focus,
|
|
queue,
|
|
theme: *theme,
|
|
}
|
|
}
|
|
|
|
///
|
|
pub fn update(&mut self, list: &[StatusItem]) -> Result<()> {
|
|
let new_hash = hash(list);
|
|
if self.current_hash != new_hash {
|
|
self.tree.update(list)?;
|
|
self.current_hash = new_hash;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
///
|
|
pub fn selection(&self) -> Option<FileTreeItem> {
|
|
self.tree.selected_item()
|
|
}
|
|
|
|
///
|
|
pub fn focus_select(&mut self, focus: bool) {
|
|
self.focus(focus);
|
|
self.show_selection = focus;
|
|
}
|
|
|
|
/// returns true if list is empty
|
|
pub fn is_empty(&self) -> bool {
|
|
self.tree.is_empty()
|
|
}
|
|
|
|
///
|
|
pub fn is_file_seleted(&self) -> bool {
|
|
if let Some(item) = self.tree.selected_item() {
|
|
match item.kind {
|
|
FileTreeItemKind::File(_) => true,
|
|
_ => false,
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn move_selection(&mut self, dir: MoveSelection) -> bool {
|
|
let changed = self.tree.move_selection(dir);
|
|
|
|
if changed {
|
|
self.queue
|
|
.borrow_mut()
|
|
.push_back(InternalEvent::Update(NeedsUpdate::DIFF));
|
|
}
|
|
|
|
changed
|
|
}
|
|
|
|
fn item_to_text(
|
|
item: &FileTreeItem,
|
|
width: u16,
|
|
selected: bool,
|
|
theme: Theme,
|
|
) -> Option<Text> {
|
|
let indent_str = if item.info.indent == 0 {
|
|
String::from("")
|
|
} else {
|
|
format!("{:w$}", " ", w = (item.info.indent as usize) * 2)
|
|
};
|
|
|
|
if !item.info.visible {
|
|
return None;
|
|
}
|
|
|
|
match &item.kind {
|
|
FileTreeItemKind::File(status_item) => {
|
|
let status_char =
|
|
Self::item_status_char(status_item.status);
|
|
let file = Path::new(&status_item.path)
|
|
.file_name()
|
|
.and_then(std::ffi::OsStr::to_str)
|
|
.expect("invalid path.");
|
|
|
|
let txt = if selected {
|
|
format!(
|
|
"{} {}{:w$}",
|
|
status_char,
|
|
indent_str,
|
|
file,
|
|
w = width as usize
|
|
)
|
|
} else {
|
|
format!("{} {}{}", status_char, indent_str, file)
|
|
};
|
|
|
|
Some(Text::Styled(
|
|
Cow::from(txt),
|
|
theme.item(status_item.status, selected),
|
|
))
|
|
}
|
|
|
|
FileTreeItemKind::Path(path_collapsed) => {
|
|
let collapse_char =
|
|
if path_collapsed.0 { '▸' } else { '▾' };
|
|
|
|
let txt = if selected {
|
|
format!(
|
|
" {}{}{:w$}",
|
|
indent_str,
|
|
collapse_char,
|
|
item.info.path,
|
|
w = width as usize
|
|
)
|
|
} else {
|
|
format!(
|
|
" {}{}{}",
|
|
indent_str, collapse_char, item.info.path,
|
|
)
|
|
};
|
|
|
|
Some(Text::Styled(
|
|
Cow::from(txt),
|
|
theme.text(true, selected),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn item_status_char(item_type: StatusItemType) -> char {
|
|
match item_type {
|
|
StatusItemType::Modified => 'M',
|
|
StatusItemType::New => '+',
|
|
StatusItemType::Deleted => '-',
|
|
StatusItemType::Renamed => 'R',
|
|
_ => ' ',
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DrawableComponent for FileTreeComponent {
|
|
fn draw<B: Backend>(
|
|
&mut self,
|
|
f: &mut Frame<B>,
|
|
r: Rect,
|
|
) -> Result<()> {
|
|
let selection_offset =
|
|
self.tree.tree.items().iter().enumerate().fold(
|
|
0,
|
|
|acc, (idx, e)| {
|
|
let visible = e.info.visible;
|
|
let index_above_select =
|
|
idx < self.tree.selection.unwrap_or(0);
|
|
|
|
if !visible && index_above_select {
|
|
acc + 1
|
|
} else {
|
|
acc
|
|
}
|
|
},
|
|
);
|
|
|
|
let items =
|
|
self.tree.tree.items().iter().enumerate().filter_map(
|
|
|(idx, e)| {
|
|
Self::item_to_text(
|
|
e,
|
|
r.width,
|
|
self.show_selection
|
|
&& self
|
|
.tree
|
|
.selection
|
|
.map_or(false, |e| e == idx),
|
|
self.theme,
|
|
)
|
|
},
|
|
);
|
|
|
|
ui::draw_list(
|
|
f,
|
|
r,
|
|
&self.title.to_string(),
|
|
items,
|
|
self.tree.selection.map(|idx| idx - selection_offset),
|
|
self.focused,
|
|
self.theme,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Component for FileTreeComponent {
|
|
fn commands(
|
|
&self,
|
|
out: &mut Vec<CommandInfo>,
|
|
force_all: bool,
|
|
) -> CommandBlocking {
|
|
out.push(
|
|
CommandInfo::new(
|
|
commands::NAVIGATE_TREE,
|
|
!self.is_empty(),
|
|
self.focused || force_all,
|
|
)
|
|
.order(order::NAV),
|
|
);
|
|
|
|
CommandBlocking::PassingOn
|
|
}
|
|
|
|
fn event(&mut self, ev: Event) -> Result<bool> {
|
|
if self.focused {
|
|
if let Event::Key(e) = ev {
|
|
return match e {
|
|
keys::MOVE_DOWN => {
|
|
Ok(self.move_selection(MoveSelection::Down))
|
|
}
|
|
keys::MOVE_UP => {
|
|
Ok(self.move_selection(MoveSelection::Up))
|
|
}
|
|
keys::HOME | keys::SHIFT_UP => {
|
|
Ok(self.move_selection(MoveSelection::Home))
|
|
}
|
|
keys::END | keys::SHIFT_DOWN => {
|
|
Ok(self.move_selection(MoveSelection::End))
|
|
}
|
|
keys::MOVE_LEFT => {
|
|
Ok(self.move_selection(MoveSelection::Left))
|
|
}
|
|
keys::MOVE_RIGHT => {
|
|
Ok(self.move_selection(MoveSelection::Right))
|
|
}
|
|
_ => Ok(false),
|
|
};
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
fn focused(&self) -> bool {
|
|
self.focused
|
|
}
|
|
fn focus(&mut self, focus: bool) {
|
|
self.focused = focus
|
|
}
|
|
}
|