mirror of
https://github.com/chylex/Bark-Browser.git
synced 2025-09-15 23:32:11 +02:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
4c5a663f51
|
|||
95c12d4f61
|
|||
e59ecf2f08
|
|||
c27d9d7401
|
|||
c9fe9f8f0e
|
|||
bdd25b89e3
|
|||
19623d745a
|
|||
c44c0691c9
|
|||
7519472a43
|
BIN
.github/readme/screenshot.png
vendored
Normal file
BIN
.github/readme/screenshot.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
|
/out/
|
||||||
/target/
|
/target/
|
||||||
|
@@ -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
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Bark
|
||||||
|
|
||||||
|
`bark` is a tree-based terminal filesystem browser and file manager with `vim`-style key bindings.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# 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
22
scripts/build.ps1
Normal 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
20
scripts/build.sh
Executable 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
7
wsl.sh → scripts/wsl.sh
Normal file → Executable 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
|
|
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
}))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
32
src/state/event.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user