What does it do?
scape is a lightweight, fast, and cross-platform terminal file manager written in Rust, with asynchronous tasks being multiplexed across all physical CPU cores using tokio and it's rt-threaded feature. This allows non-blocking reads of directories, files, and keyboard input.
Instead of a normal GUI, file icons, and mouse input, scape lives entirely in the terminal. It displays text, some color, and allows you to use only a keyboard to navigate.
Being a vim user, I decided to design scape with existing vim bindings in mind. You use h j k l to move around, and vim-style key chords are also available. I find that using terminal and command line applications greatly boosts my workflow, as I am able to navigate around my filesystem quicker with a keyboard than with a mouse.
Why?
Being frustrated with existing terminal file managers (i.e. broot in Rust, lf in Go), I decided to make my own. There's nothing wrong with them, just small things that I felt I could do better.
In its current state, scape is not yet rivalling the functionality of these existing solutions, but serves as a starting point for future development.
It doesn't look like much, but for me, developing this was less about the program itself, and more about diving deeper into Rust, and asynchronous Rust programming: I learned a lot from making this.
How does it work?
scape only has two dependencies:
- crossterm - A cross platform library for terminal manipulation
- tokio - A fast, asynchronous runtime library
Essentially, crossterm allows us to write text to the terminal, push some colors, and move stuff around how we'd like. tokio allows for concurrent/parallel tasks, meaning operations that could take a while, like reading a directory or listening for keystrokes, can be ran at the same time.
On the topic of keystrokes, scape is designed for easy implementation of configurations in the future. Take a look at how easy it is to add a new chord (a chord is a sequence of keys pressed to yield an action from the program):
let f_chords: HashMap<Chord, Cmd> = HashMap::new();
f_chords.insert(Chord('q', 'q'), Cmd::Quit);
I wrote it as f_chord, meaning functional chord, since it has a "function", or Cmd attached to it. This particular chord would require me to press 'qq' to quit scape. The Cmd is listened for asynchronously, and when it's detected, a corresponding action is performed. If you're wondering how Chord and Cmd are defined, they're quite simple:
#[derive(Copy, Clone, Debug)]
pub enum Cmd {
Quit, // Leave the program
DirUp, // Scroll up
DirDown, // Scroll down
DirOut, // Go to the parent directory
DirIn // Go into the directory you're highlighting
}
#[derive(Eq, Hash)]
pub struct Chord(pub char, pub char);
The derivations on Cmd are there because of how I designed scape around Rust's type system. The derivations on Chord are useful because it allows us to use HashMaps and HashSets. These collections are great because they offer fast lookup, and all keys are guaranteed to be unique. This means we don't allow any duplicate Chord sequences, or any duplicate directories in our cache.
Challenges
While I've tinkered with Rust in the past, scape is my first real Rust project. Rust's safety is one of its main features, and that can somewhat slow down a developer used to more unsafe languages. On top of that, thinking in/writing in an asynchronous mindset for the first time was definitely a challenge. That being said, tokio was fantastic to work with, and I'll definitely be using it to continue scape's development.
Considerations for the future
Here's a quick list of things I plan to consider for continuing scape:
- Leverage Rust's ownership model and type system more - I didn't really make full use of it in this project, given the short period of time and how new I am to Rust, but I am definitely interested in learning Rust more depth and applying it to scape.
- Segregate tokio tasks - At the moment, input handling and rendering are sort of intertwined. I'd like to seperate those, along with other functions of the program, to make it easier to work on.
- Directory caching - I almost implemented it in this version, but decided against it. My original implementation was a
HashMapkeyed with thePathBufof the directory in question. This theoretically would have been pretty fast, but I'm open to explore more (and potentially better) solutions. - Nailing down the filesystem/file tree structure - I'm not entirely happy with how I structured the filesystem. I think I could have done a better job. With more considerations and time, it should see big improvement.
- Configuration system - I'd like users to be able to configure scape to their liking, through a file.
- Scrolling - A basic quality of life feature that I did not have time to add. My original idea involved mutating a slice of a
Vec, which is an illegal operation in Rust. I'll have to rethink my approach. - Editor and Opener support - I'd like to be able to hit a key and open a file in vim, or NotePad, or whatever I choose!
- File and Image previews - In an attempt to completely replace my GUI file manager, I'd like to have file and image previews.
- nvim plugin - Once scape is in a stable place, I plan on making an
nvimplugin for it, so that I can use it to navigate around my codebase. - Typed commands - On top of chords and single-key actions, I'd like to able to type some commands. Some examples are
mkdirandmkfile - Symlink representations - Implement accurate representation of symlinks (safely)
- Test on MacOS and common Linux distributions - These systems remain untested at the moment
- "cd" functionality - I'd like to be able to change directories of the parent shell from within scape
- File operations (move/rename, delete, etc.)
- Upload to crates.io - This is so anyone can do
cargo install scapeand have thescapecommand ready to them
Bugs
Currently, there are two known bugs:
- Slight flickering and general glitchiness when traversing large directories: I don't know the cause of this yet. I think it might have something to do with
cursor::Hidefrom crossterm not working properly. - When more than one chord is defined, only the last defined one works (FIXED): This was because I forgot to drain the chord
HashSet. It has been fixed.
Continuing the project
Right now, you can find scape at https://github.com/marc1/scape-vh, vh being for VikesHack. I'll be continuing the project at https://github.com/marc1/scape in the future.
Built With
- crossterm
- rust
- tokio
Log in or sign up for Devpost to join the conversation.