1
0
mirror of https://github.com/chylex/Bark-Browser.git synced 2025-09-15 23:32:11 +02:00

9 Commits
v1.0.0 ... wip

17 changed files with 191 additions and 30 deletions

BIN
.github/readme/screenshot.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/.idea/ /.idea/
/out/
/target/ /target/

View File

@@ -2,10 +2,8 @@ FROM rust:1.71.0 as builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN cargo build --release RUN ./scripts/build.sh
FROM scratch as exporter FROM scratch as exporter
COPY --from=builder /app/target/release/bark . COPY --from=builder /app/out/ .
# docker build --output out .

44
README.md Normal file
View File

@@ -0,0 +1,44 @@
# Bark
`bark` is a tree-based terminal filesystem browser and file manager with `vim`-style key bindings.
![Bark Browser Screenshot](.github/readme/screenshot.png)
# Features
- `ls`-style file listing
- `vim`-style navigation adapted for tree hierarchies
- Basic file management (create, rename, edit, delete)
- Support for Linux and Windows
See [action/mod.rs](https://github.com/chylex/Bark-Browser/blob/main/src/component/filesystem/action/mod.rs) for an up-to-date list of all key bindings.
# Roadmap
- Settings
- File search
- Visual mode for selecting multiple files
- Ex commands for more complex operations
- Directory statistics (total size, number of files, etc.)
- Tree filtering (views that only include certain files)
- Rebindable keys and macros
# Building
1. Install [Rust](https://www.rust-lang.org/tools/install).
2. Run `cargo run` to launch the application.
3. Run `scripts/build.sh` or `scripts/build.bat` to build a release binary into the `out/` folder.
## Windows Subsystem for Linux
Run `scripts/wsl.sh` from a Debian-based WSL environment to quickly install Rust and CMake into WSL.
## Docker
Run `docker build --output out .` to build a release binary into the `out/` folder on the host. BuildKit is required.
# Contributing
This project exists 1) because I couldn't find any tree-based file manager I liked and 2) because I wanted to have fun writing Rust, and I don't really want to spend time reading and reviewing pull requests.
For now, issues are closed, and I'm not accepting any major contributions — especially ones related to the roadmap. If you have a small idea, issue, or pull request, feel free to start a [discussion](https://github.com/chylex/Bark-Browser/discussions).

22
scripts/build.ps1 Normal file
View File

@@ -0,0 +1,22 @@
Set-Location (Split-Path $PSScriptRoot -Parent)
if (!(Test-Path "out")) {
mkdir "out"
}
cargo build --release
$version = & "target\release\bark.exe" --version
$arch = switch ((Get-WMIObject -Class Win32_Processor).Architecture) {
0 { "x86" }
1 { "mips" }
2 { "alpha" }
3 { "ppc" }
5 { "arm" }
6 { "ia64" }
9 { "x64" }
12 { "arm64" }
DEFAULT { "unknown" }
}
Compress-Archive -Force -Path "target\release\bark.exe" -DestinationPath "out\bark-${version}-windows-${arch}.zip"

20
scripts/build.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
cd ..
mkdir -p out
cargo build --release
cd target/release
VERSION=$(./bark --version)
ARCH=$(uname -m)
if [[ "$ARCH" == "x86_64" ]]; then
ARCH="x64"
elif [[ "$ARCH" == "aarch64" ]]; then
ARCH="arm64"
fi
tar czf "../../out/bark-${VERSION}-linux-${ARCH}.tar.gz" --owner=0 --group=0 -- bark

7
wsl.sh → scripts/wsl.sh Normal file → Executable file
View File

@@ -6,6 +6,7 @@ sudo apt-get install -y \
apt-transport-https \ apt-transport-https \
build-essential \ build-essential \
ca-certificates \ ca-certificates \
cmake \
gdb \ gdb \
gnupg \ gnupg \
software-properties-common \ software-properties-common \
@@ -15,9 +16,3 @@ sudo apt-get install -y \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env" source "$HOME/.cargo/env"
rustup component add rust-src rustup component add rust-src
# CMake
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | sudo apt-key add -
sudo apt-add-repository 'deb https://apt.kitware.com/ubuntu/ bionic main'
sudo apt-get update
sudo apt-get install -y cmake

View File

@@ -53,11 +53,7 @@ impl<'a> Layer for InputFieldDialogLayer<'a> {
} }
_ => { _ => {
if self.field.handle_input(key_binding) { ActionResult::draw_if(self.field.handle_input(key_binding))
ActionResult::Draw
} else {
ActionResult::Nothing
}
} }
} }
} }

View File

@@ -1,4 +1,5 @@
use crate::component::filesystem::FsLayer; use crate::component::filesystem::FsLayer;
use crate::component::input::InputFieldOverlayLayer;
use crate::state::action::{Action, ActionResult}; use crate::state::action::{Action, ActionResult};
use crate::state::Environment; use crate::state::Environment;
@@ -17,3 +18,13 @@ impl Action<FsLayer> for RedrawScreen {
ActionResult::Redraw ActionResult::Redraw
} }
} }
pub struct EnterCommandMode;
impl Action<FsLayer> for EnterCommandMode {
fn perform(&self, _layer: &mut FsLayer, _environment: &Environment) -> ActionResult {
ActionResult::PushLayer(Box::new(InputFieldOverlayLayer::new(":", Box::new(|command| {
ActionResult::PopLayer
}))))
}
}

View File

@@ -20,7 +20,7 @@ pub struct DeleteSelectedEntry;
impl Action<FsLayer> for DeleteSelectedEntry { impl Action<FsLayer> for DeleteSelectedEntry {
fn perform(&self, layer: &mut FsLayer, _environment: &Environment) -> ActionResult { fn perform(&self, layer: &mut FsLayer, _environment: &Environment) -> ActionResult {
if let Some(FileNode { node, entry, path }) = get_selected_file(layer) { if let Some(FileNode { node, entry, path }) = get_selected_file(layer) {
ActionResult::PushLayer(Box::new(create_delete_confirmation_dialog(layer, node.node_id(), entry, path.to_owned()))) ActionResult::push_layer(create_delete_confirmation_dialog(layer, node.node_id(), entry, path.to_owned()))
} else { } else {
ActionResult::Nothing ActionResult::Nothing
} }

View File

@@ -1,6 +1,6 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use crate::component::filesystem::action::application::{Quit, RedrawScreen}; use crate::component::filesystem::action::application::{EnterCommandMode, Quit, RedrawScreen};
use crate::component::filesystem::action::count::PushCountDigit; use crate::component::filesystem::action::count::PushCountDigit;
use crate::component::filesystem::action::file::{CreateDirectoryInParentOfSelectedEntry, CreateDirectoryInSelectedDirectory, CreateFileInParentOfSelectedEntry, CreateFileInSelectedDirectory, DeleteSelectedEntry, EditSelectedEntry, RenameSelectedEntry}; use crate::component::filesystem::action::file::{CreateDirectoryInParentOfSelectedEntry, CreateDirectoryInSelectedDirectory, CreateFileInParentOfSelectedEntry, CreateFileInSelectedDirectory, DeleteSelectedEntry, EditSelectedEntry, RenameSelectedEntry};
use crate::component::filesystem::action::movement::{CollapseSelectedOr, ExpandSelectedOr, MoveBetweenFirstAndLastSibling, MoveDown, MovementWithCountFactory, MovementWithFallbackFactory, MoveOrTraverseUpParent, MoveToFirst, MoveToLast, MoveToLineOr, MoveToNextSibling, MoveToParent, MoveToPreviousSibling, MoveUp, ScreenHeightRatio}; use crate::component::filesystem::action::movement::{CollapseSelectedOr, ExpandSelectedOr, MoveBetweenFirstAndLastSibling, MoveDown, MovementWithCountFactory, MovementWithFallbackFactory, MoveOrTraverseUpParent, MoveToFirst, MoveToLast, MoveToLineOr, MoveToNextSibling, MoveToParent, MoveToPreviousSibling, MoveUp, ScreenHeightRatio};
@@ -57,6 +57,7 @@ fn create_action_map() -> ActionKeyMap {
map(&mut me, "R", RenameSelectedEntry { prefill: false }); map(&mut me, "R", RenameSelectedEntry { prefill: false });
map(&mut me, "%", MoveBetweenFirstAndLastSibling); map(&mut me, "%", MoveBetweenFirstAndLastSibling);
map(&mut me, ":", EnterCommandMode);
map(&mut me, "<Ctrl-B>", MoveUp.with_custom_count(ScreenHeightRatio(1))); map(&mut me, "<Ctrl-B>", MoveUp.with_custom_count(ScreenHeightRatio(1)));
map(&mut me, "<Ctrl-C>", Quit); map(&mut me, "<Ctrl-C>", Quit);

View File

@@ -53,7 +53,7 @@ fn handle_delete_view_node(layer: &mut FsLayer, view_node_id: NodeId) {
let view = &mut layer.tree.view; let view = &mut layer.tree.view;
if layer.selected_view_node_id == view_node_id { if layer.selected_view_node_id == view_node_id {
layer.selected_view_node_id = view.get_node_above_id(view_node_id).unwrap_or_else(|| view.root_id()); layer.selected_view_node_id = view.get_node_below_id(view_node_id).or_else(|| view.get_node_above_id(view_node_id)).unwrap_or_else(|| view.root_id());
} }
if let Some(view_node) = view.remove(view_node_id) { if let Some(view_node) = view.remove(view_node_id) {

View File

@@ -209,7 +209,7 @@ impl<'a> StatefulWidget for InputFieldWidget<'a> {
} }
let style = Style::default() let style = Style::default()
.fg(Color::White) .fg(Color::Black)
.bg(self.default_background); .bg(self.default_background);
Clear.render(area, buf); Clear.render(area, buf);
@@ -222,7 +222,7 @@ impl<'a> StatefulWidget for InputFieldWidget<'a> {
if has_truncated_end { if has_truncated_end {
buf.get_mut(area.right().saturating_sub(1), area.y) buf.get_mut(area.right().saturating_sub(1), area.y)
.set_char('~') .set_char('~')
.set_fg(Color::White) .set_fg(Color::Black)
.set_bg(self.trimmed_background); .set_bg(self.trimmed_background);
} }

View File

@@ -1,5 +1,7 @@
use crossterm::event::{KeyCode, KeyModifiers}; use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::style::Color; use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Paragraph;
use crate::component::input::InputField; use crate::component::input::InputField;
use crate::input::keymap::KeyBinding; use crate::input::keymap::KeyBinding;
@@ -8,21 +10,23 @@ use crate::state::Environment;
use crate::state::layer::Layer; use crate::state::layer::Layer;
use crate::state::view::Frame; use crate::state::view::Frame;
pub struct InputFieldOverlayLayer { pub struct InputFieldOverlayLayer<'a> {
field: InputField, field: InputField,
read_only_prefix: &'a str,
confirm_action: Box<dyn Fn(String) -> ActionResult>, confirm_action: Box<dyn Fn(String) -> ActionResult>,
} }
impl InputFieldOverlayLayer { impl<'a> InputFieldOverlayLayer<'a> {
pub fn new(confirm_action: Box<dyn Fn(String) -> ActionResult>) -> Self { pub fn new(read_only_prefix: &'a str, confirm_action: Box<dyn Fn(String) -> ActionResult>) -> Self {
Self { Self {
field: InputField::new(), field: InputField::new(),
read_only_prefix,
confirm_action, confirm_action,
} }
} }
} }
impl Layer for InputFieldOverlayLayer { impl<'a> Layer for InputFieldOverlayLayer<'a> {
#[allow(clippy::wildcard_enum_match_arm)] #[allow(clippy::wildcard_enum_match_arm)]
fn handle_input(&mut self, _environment: &Environment, key_binding: KeyBinding) -> ActionResult { fn handle_input(&mut self, _environment: &Environment, key_binding: KeyBinding) -> ActionResult {
match (key_binding.code(), key_binding.modifiers()) { match (key_binding.code(), key_binding.modifiers()) {
@@ -35,18 +39,40 @@ impl Layer for InputFieldOverlayLayer {
(self.confirm_action)(self.field.text().to_owned()) (self.confirm_action)(self.field.text().to_owned())
} }
_ => { (KeyCode::Backspace, KeyModifiers::NONE) => {
if self.field.handle_input(key_binding) { if self.field.text().is_empty() {
ActionResult::Draw ActionResult::PopLayer
} else { } else {
ActionResult::Nothing ActionResult::draw_if(self.field.handle_input(key_binding))
} }
} }
_ => {
ActionResult::draw_if(self.field.handle_input(key_binding))
}
} }
} }
fn render(&mut self, frame: &mut Frame) { fn render(&mut self, frame: &mut Frame) {
let size = frame.size(); let size = frame.size();
self.field.render(frame, size.x, size.bottom().saturating_sub(1), size.width, Color::LightYellow, Color::Yellow); if size.width < 1 || size.height < 1 {
return;
}
let x = size.x;
let y = size.bottom().saturating_sub(1);
let prefix_style = Style::new()
.fg(Color::Black)
.bg(Color::LightYellow);
let prefix_paragraph = Paragraph::new(self.read_only_prefix)
.style(prefix_style);
frame.render_widget(prefix_paragraph, Rect { x, y, width: 1, height: 1 });
if size.width > 1 {
self.field.render(frame, x.saturating_add(1), y, size.width.saturating_sub(1), Color::LightYellow, Color::Yellow);
}
} }
} }

View File

@@ -13,3 +13,17 @@ pub enum ActionResult {
ReplaceLayer(Box<dyn Layer>), ReplaceLayer(Box<dyn Layer>),
PopLayer, PopLayer,
} }
impl ActionResult {
pub const fn draw_if(condition: bool) -> Self {
if condition {
Self::Draw
} else {
Self::Nothing
}
}
pub fn push_layer<T>(layer: T) -> Self where T: Layer + 'static {
Self::PushLayer(Box::new(layer))
}
}

32
src/state/event.rs Normal file
View File

@@ -0,0 +1,32 @@
use crate::state::Environment;
pub trait Event<L> {
fn dispatch(&self, layer: &mut L, environment: &Environment) -> EventResult;
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum EventResult {
Nothing,
Draw,
Redraw,
}
impl EventResult {
pub const fn draw_if(condition: bool) -> Self {
if condition {
Self::Draw
} else {
Self::Nothing
}
}
pub fn merge(self, other: Self) -> Self {
if self == Self::Redraw || other == Self::Redraw {
Self::Redraw
} else if self == Self::Draw || other == Self::Draw {
Self::Draw
} else {
Self::Nothing
}
}
}

View File

@@ -10,6 +10,7 @@ pub use self::environment::Environment;
mod environment; mod environment;
pub mod action; pub mod action;
pub mod event;
pub mod layer; pub mod layer;
pub mod view; pub mod view;