mirror of
https://github.com/chylex/Bark-Browser.git
synced 2025-09-15 14:32:12 +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/
|
||||
/out/
|
||||
/target/
|
||||
|
@@ -2,10 +2,8 @@ FROM rust:1.71.0 as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --release
|
||||
RUN ./scripts/build.sh
|
||||
|
||||
|
||||
FROM scratch as exporter
|
||||
COPY --from=builder /app/target/release/bark .
|
||||
|
||||
# docker build --output out .
|
||||
COPY --from=builder /app/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 \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
cmake \
|
||||
gdb \
|
||||
gnupg \
|
||||
software-properties-common \
|
||||
@@ -15,9 +16,3 @@ sudo apt-get install -y \
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source "$HOME/.cargo/env"
|
||||
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
|
||||
} else {
|
||||
ActionResult::Nothing
|
||||
}
|
||||
ActionResult::draw_if(self.field.handle_input(key_binding))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::component::filesystem::FsLayer;
|
||||
use crate::component::input::InputFieldOverlayLayer;
|
||||
use crate::state::action::{Action, ActionResult};
|
||||
use crate::state::Environment;
|
||||
|
||||
@@ -17,3 +18,13 @@ impl Action<FsLayer> for RedrawScreen {
|
||||
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 {
|
||||
fn perform(&self, layer: &mut FsLayer, _environment: &Environment) -> ActionResult {
|
||||
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 {
|
||||
ActionResult::Nothing
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
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::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};
|
||||
@@ -57,6 +57,7 @@ fn create_action_map() -> ActionKeyMap {
|
||||
map(&mut me, "R", RenameSelectedEntry { prefill: false });
|
||||
|
||||
map(&mut me, "%", MoveBetweenFirstAndLastSibling);
|
||||
map(&mut me, ":", EnterCommandMode);
|
||||
|
||||
map(&mut me, "<Ctrl-B>", MoveUp.with_custom_count(ScreenHeightRatio(1)));
|
||||
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;
|
||||
|
||||
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) {
|
||||
|
@@ -209,7 +209,7 @@ impl<'a> StatefulWidget for InputFieldWidget<'a> {
|
||||
}
|
||||
|
||||
let style = Style::default()
|
||||
.fg(Color::White)
|
||||
.fg(Color::Black)
|
||||
.bg(self.default_background);
|
||||
|
||||
Clear.render(area, buf);
|
||||
@@ -222,7 +222,7 @@ impl<'a> StatefulWidget for InputFieldWidget<'a> {
|
||||
if has_truncated_end {
|
||||
buf.get_mut(area.right().saturating_sub(1), area.y)
|
||||
.set_char('~')
|
||||
.set_fg(Color::White)
|
||||
.set_fg(Color::Black)
|
||||
.set_bg(self.trimmed_background);
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,7 @@
|
||||
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::input::keymap::KeyBinding;
|
||||
@@ -8,21 +10,23 @@ use crate::state::Environment;
|
||||
use crate::state::layer::Layer;
|
||||
use crate::state::view::Frame;
|
||||
|
||||
pub struct InputFieldOverlayLayer {
|
||||
pub struct InputFieldOverlayLayer<'a> {
|
||||
field: InputField,
|
||||
read_only_prefix: &'a str,
|
||||
confirm_action: Box<dyn Fn(String) -> ActionResult>,
|
||||
}
|
||||
|
||||
impl InputFieldOverlayLayer {
|
||||
pub fn new(confirm_action: Box<dyn Fn(String) -> ActionResult>) -> Self {
|
||||
impl<'a> InputFieldOverlayLayer<'a> {
|
||||
pub fn new(read_only_prefix: &'a str, confirm_action: Box<dyn Fn(String) -> ActionResult>) -> Self {
|
||||
Self {
|
||||
field: InputField::new(),
|
||||
read_only_prefix,
|
||||
confirm_action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layer for InputFieldOverlayLayer {
|
||||
impl<'a> Layer for InputFieldOverlayLayer<'a> {
|
||||
#[allow(clippy::wildcard_enum_match_arm)]
|
||||
fn handle_input(&mut self, _environment: &Environment, key_binding: KeyBinding) -> ActionResult {
|
||||
match (key_binding.code(), key_binding.modifiers()) {
|
||||
@@ -35,18 +39,40 @@ impl Layer for InputFieldOverlayLayer {
|
||||
(self.confirm_action)(self.field.text().to_owned())
|
||||
}
|
||||
|
||||
_ => {
|
||||
if self.field.handle_input(key_binding) {
|
||||
ActionResult::Draw
|
||||
(KeyCode::Backspace, KeyModifiers::NONE) => {
|
||||
if self.field.text().is_empty() {
|
||||
ActionResult::PopLayer
|
||||
} 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) {
|
||||
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>),
|
||||
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;
|
||||
pub mod action;
|
||||
pub mod event;
|
||||
pub mod layer;
|
||||
pub mod view;
|
||||
|
||||
|
Reference in New Issue
Block a user