├── .envrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── enhancement-suggestion.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── renovate.json └── workflows │ ├── superfile-build-test.yml │ ├── testsuite-run.yml │ └── update-gomod2nix.yml ├── .gitignore ├── .golangci.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── asset ├── contributors.svg ├── demo.gif ├── demo.png ├── prompt_shell_mode.png ├── prompt_spf_mode.png ├── spf.desktop ├── superfileicon.png ├── superfilelogoblack.png ├── superfilelogounused.png ├── superfilelogowhite.png ├── theme │ ├── 0x96f.png │ ├── ayu-dark.png │ ├── blood.png │ ├── catppuccin-frappe.png │ ├── catppuccin-latte.png │ ├── catppuccin-macchiato.png │ ├── catppuccin.png │ ├── dracula.png │ ├── everforest-dark-medium.png │ ├── gruvbox-dark-hard.png │ ├── gruvbox.png │ ├── hacks.png │ ├── kaolin.png │ ├── monokai.png │ ├── nord.png │ ├── onedark.png │ ├── poimandres.png │ ├── rose-pine.png │ ├── sugarplum.png │ └── tokyonight.png └── warp.png ├── build.sh ├── cd_on_quit ├── cd_on_quit.fish ├── cd_on_quit.ps1 └── cd_on_quit.sh ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── gomod2nix.toml ├── main.go ├── release ├── release.sh ├── release_check.md └── remove_all_spf_config.sh ├── src ├── cmd │ └── main.go ├── config │ ├── fixed_variable.go │ └── icon │ │ ├── function.go │ │ └── icon.go ├── internal │ ├── backend │ │ └── ReadMe.md │ ├── common │ │ ├── ReadMe.md │ │ ├── config_type.go │ │ ├── default_config.go │ │ ├── icon_utils.go │ │ ├── icon_utils_test.go │ │ ├── load_config.go │ │ ├── predefined_variable.go │ │ ├── string_function.go │ │ ├── string_function_test.go │ │ ├── style.go │ │ ├── style_function.go │ │ └── type.go │ ├── config_function.go │ ├── default_config.go │ ├── file_operations.go │ ├── file_operations_compress.go │ ├── file_operations_extract.go │ ├── function.go │ ├── function_test.go │ ├── handle_file_operations.go │ ├── handle_modal.go │ ├── handle_panel_movement.go │ ├── handle_panel_navigation.go │ ├── handle_panel_up_down.go │ ├── handle_panel_up_down_test.go │ ├── key_function.go │ ├── model.go │ ├── model_file_operations_test.go │ ├── model_prompt_test.go │ ├── model_render.go │ ├── model_render_test.go │ ├── model_test.go │ ├── test_utils.go │ ├── type.go │ ├── type_utils.go │ ├── ui │ │ ├── ReadMe.md │ │ ├── prompt │ │ │ ├── ReadMe.md │ │ │ ├── consts.go │ │ │ ├── error.go │ │ │ ├── model.go │ │ │ ├── model_test.go │ │ │ ├── tokenize.go │ │ │ ├── tokenize_test.go │ │ │ ├── type.go │ │ │ ├── utils.go │ │ │ └── utils_test.go │ │ ├── rendering │ │ │ ├── ReadMe.md │ │ │ ├── border.go │ │ │ ├── content_renderer.go │ │ │ ├── content_renderer_test.go │ │ │ ├── renderer.go │ │ │ ├── renderer_core.go │ │ │ ├── renderer_test.go │ │ │ ├── truncate.go │ │ │ └── truncate_test.go │ │ ├── sidebar │ │ │ ├── ReadMe.md │ │ │ ├── consts.go │ │ │ ├── directory_utils.go │ │ │ ├── disk_utils.go │ │ │ ├── navigation.go │ │ │ ├── navigation_test.go │ │ │ ├── render.go │ │ │ ├── sidebar.go │ │ │ ├── type.go │ │ │ ├── utils.go │ │ │ └── utils_test.go │ │ └── spf_renderers.go │ ├── utils │ │ ├── ReadMe.md │ │ ├── bool_file_store.go │ │ ├── bool_file_store_test.go │ │ ├── consts.go │ │ ├── file_utils.go │ │ ├── file_utils_test.go │ │ ├── fzf_utils.go │ │ ├── log_utils.go │ │ ├── shell_utils.go │ │ ├── tea_utils.go │ │ └── ui_utils.go │ └── wheel_function.go ├── pkg │ ├── file_preview │ │ ├── auto.go │ │ ├── image_preview.go │ │ ├── pdf_preview.go │ │ └── utils.go │ └── string_function │ │ └── overplace.go └── superfile_config │ ├── config.toml │ ├── hotkeys.toml │ ├── theme │ ├── 0x96f.toml │ ├── ayu-dark.toml │ ├── blood.toml │ ├── catppuccin-frappe.toml │ ├── catppuccin-latte.toml │ ├── catppuccin-macchiato.toml │ ├── catppuccin.toml │ ├── dracula.toml │ ├── everforest-dark-medium.toml │ ├── gruvbox-dark-hard.toml │ ├── gruvbox.toml │ ├── hacks.toml │ ├── kaolin.toml │ ├── monokai.toml │ ├── nord.toml │ ├── onedark.toml │ ├── poimandres.toml │ ├── rose-pine.toml │ ├── sugarplum.toml │ └── tokyonight.toml │ └── vimHotkeys.toml ├── testsuite ├── .gitignore ├── Notes.md ├── ReadMe.md ├── core │ ├── __init__.py │ ├── base_test.py │ ├── environment.py │ ├── fs_manager.py │ ├── keys.py │ ├── pyautogui_manager.py │ ├── runner.py │ ├── spf_manager.py │ ├── test_constants.py │ ├── tmux_manager.py │ └── utils.py ├── docs │ └── tmux.md ├── main.py ├── requirements.txt └── tests │ ├── __init__.py │ ├── chooser_file_test.py │ ├── command_test.py │ ├── compress_extract_test.py │ ├── copy_dir_test.py │ ├── copy_test.py │ ├── copyw_test.py │ ├── cut_test.py │ ├── delete_dir_test.py │ ├── delete_test.py │ ├── empty_panel_test.py │ ├── nav_and_copy_path_test.py │ └── rename_test.py ├── vhs ├── demo.tape ├── open_spf_and_quit.tape ├── spf_file_panel_movement.tape ├── spf_file_panel_navigation.tape ├── spf_file_panel_selection_mode.tape └── spf_panel_navigation.tape └── website ├── README.md ├── astro.config.mjs ├── ec.config.mjs ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── _redirects ├── favicon.svg ├── google0fdf22175b8dde4d.html ├── install.ps1 ├── install.sh ├── og.jpg └── uninstall.ps1 ├── src ├── assets │ ├── logo-titled.svg │ ├── logo.png │ ├── superfile-day.svg │ ├── superfile-night.svg │ └── superfileicon.png ├── components │ ├── GithubStar.astro │ ├── LastUpdated.astro │ ├── about.astro │ └── code.astro ├── content │ ├── config.ts │ └── docs │ │ ├── changelog.md │ │ ├── configure │ │ ├── config-file-path.md │ │ ├── custom-hotkeys.mdx │ │ ├── custom-theme.mdx │ │ ├── enable-plugin.md │ │ └── superfile-config.mdx │ │ ├── contribute │ │ ├── file-struct.md │ │ ├── how-to-contribute.md │ │ └── implementation-info.md │ │ ├── getting-started │ │ ├── installation.md │ │ └── tutorial.md │ │ ├── index.mdx │ │ ├── list │ │ ├── hotkey-list.md │ │ ├── plugin-list.md │ │ └── theme-list.md │ │ ├── overview.md │ │ └── troubleshooting.md ├── env.d.ts └── styles │ └── custom.css └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: yorukot # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **System information (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | - superfile Version [e.g. 1.1.1] 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement-suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Enhancement suggestion 3 | about: Enhance existing designs 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **The part you want to Enhancement** 11 | Please briefly describe the part you want to strengthen 12 | 13 | **Why it is necessary to enhancement** 14 | Please explain why it needs to be enhancement, what are the flaws in the existing design, etc. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: idea 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please give a summary of the changes made to this repo through this change. Also include context and motivation behind this along with dependencies that are required for this change (if any). 4 | 5 | # Fixes 6 | 7 | If there are any issues that this fixes, please mention it/them. 8 | 9 | ## Type of change 10 | 11 | Please delete options that are not relevant. 12 | 13 | - [ ] Bug fix 14 | - [ ] New feature 15 | - [ ] New Theme 16 | - [ ] Theme update 17 | - [ ] Enhancement 18 | 19 | # Tests 20 | 21 | If this has been tested, then please describe how it has been tested and if it passed those tests. If it didn't pass, then why so? 22 | 23 | Also mention the configurations for the tests below: 24 | 25 | **Test Configurations** 26 | * OS: 27 | * Version: 28 | 29 | # Screenshots (if appropriate) 30 | 31 | Include screenshots here if appropriate. 32 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended", ":dependencyDashboard", "default:automergeDigest"], 4 | "packageRules": [ 5 | { 6 | "matchDatasources": ["go", "github-releases", "github-tags"], 7 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 8 | "automerge": true, 9 | "paths": ["/"], 10 | "ignorePaths": [ 11 | "website/" 12 | ] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/superfile-build-test.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main, develop ] 6 | pull_request: 7 | branches: [ main, develop ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.24.1 20 | 21 | - name: Build 22 | run: | 23 | go build 24 | 25 | - name: Test 26 | run: go test ./... 27 | 28 | - name: Test `go fmt` creates no diffs 29 | run: go fmt ./... && git diff --exit-code 30 | 31 | - name: golangci-lint 32 | uses: golangci/golangci-lint-action@v6 33 | with: 34 | version: v1.64 35 | -------------------------------------------------------------------------------- /.github/workflows/testsuite-run.yml: -------------------------------------------------------------------------------- 1 | name: Python Testsuite Run 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Install tmux 17 | run: sudo apt-get update && sudo apt-get install -y tmux 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.24.1 23 | - name: Build superfile 24 | run: ./build.sh 25 | 26 | # timeout command just launches and kills spf, to create the config directories 27 | - name: Check installation 28 | run: tmux -V; ls; pwd; ls bin/; bin/spf path-list; timeout 1s bin/spf 29 | continue-on-error: true 30 | - name: set debug 31 | run: sed -i 's/debug = false/debug = true/g' /home/runner/.config/superfile/config.toml 32 | 33 | - name: Set up Python 34 | uses: actions/setup-python@v5 35 | with: 36 | python-version: '3.13' 37 | 38 | - name: Install Dependencies 39 | run: pip install -r testsuite/requirements.txt 40 | 41 | - name: Run Tests 42 | run: python testsuite/main.py -d 43 | 44 | - name: Print logs 45 | if: always() 46 | run: cat ~/.local/state/superfile/superfile.log 47 | -------------------------------------------------------------------------------- /.github/workflows/update-gomod2nix.yml: -------------------------------------------------------------------------------- 1 | name: Update gomod2nix.toml 2 | on: 3 | push: 4 | paths: 5 | - 'go.mod' 6 | - 'go.sum' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | dependabot: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Install Nix 17 | uses: cachix/install-nix-action@v31 18 | with: 19 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 20 | nix_path: nixpkgs=channel:nixos-unstable 21 | 22 | - name: Update checksum 23 | run: | 24 | nix develop --extra-experimental-features "nix-command flakes" '.#' -c "gomod2nix" 25 | # git push if we have a diff 26 | if [[ -n $(git diff) ]]; then 27 | git config --global user.email "107802416+yorukot@users.noreply.github.com" 28 | git config --global user.name "yorukot" 29 | git commit -am "chore: update gomod2nix" 30 | BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's/refs\/heads\///g') 31 | git push origin HEAD:$BRANCH_NAME 32 | fi 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | bin/ 3 | dist/ 4 | .idea/ 5 | 6 | # generated types 7 | .astro/ 8 | 9 | # dependencies 10 | node_modules/ 11 | 12 | # logs 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # Go test coverage reports 19 | coverage.out 20 | coverage.html 21 | 22 | # environment variables 23 | .env 24 | .env.production 25 | .direnv 26 | 27 | # macOS-specific files 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to superfile 2 | 3 | Welcome to superfile! This document shall serve as a guide for you to follow in your journey to contributing to this project. 4 | There are many ways to contribute to superfile: 5 | - Reporting Bugs 6 | - Resolving issues 7 | - Adding a theme 8 | - Sharing an idea and working on it 9 | - Working on a feature with other contributors. 10 | - And More… 11 | 12 | To get started, take a look at the following sections. 13 | 14 | ## Issues 15 | 16 | ### Did you spot a problem in superfile? 17 | 18 | Firstly you should check if such an issue was previously opened/closed for your problem on the repository. If it doesn't then you should create a new issue. 19 | 20 | ### Do you want to solve an issue? 21 | 22 | If there is an issue you think you can solve, and want to solve, then you should create a new fork of this repository. 23 | In that repository you should create a new branch for the issue you are working on and commit changes there. 24 | When the issue is solved, and you want it to be integrated into the official repository, you may create a pull request for the same. 25 | The description of the pull request should clearly describe both the issue and the solution along with other necessary information. 26 | The developers will merge after making the necessary changes (if arises a need to do so). 27 | 28 | ### Do you want to add a new theme? 29 | 30 | Firstly check if the theme you want to add is not already added. If it is, then you work may go waste and be left redundant. 31 | If no such theme exists, then you may create your own theme. Following steps will guide you for it: 32 | - As a template, copy an existing theme's TOML file to your theme and then do the customizations. This will reduce errors from your side and make your work easy. 33 | - To tests your theme, go to `~/.config/superfile/config/config.toml` and change description. 34 | - Make the changes you want and finish the theme. 35 | - Then you can open a pull request for the same and follow the steps described in the previous section. 36 | 37 | ### Do you want to share an idea? 38 | 39 | superfile welcomes new ideas. If you have an idea you should first check if a similar or identical idea was presented previously or not, or check thoroughly if the idea is already present in superfile. 40 | To share your idea you can open a discussion in https://github.com/MHNightCat/superfile/discussions 41 | There you can share your idea and if you want to work on it, you can follow the same steps as mentioned in previously. 42 | 43 | ### Do you want to contribute but don't know how? 44 | 45 | Your first resource in this should be https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project 46 | This file serves as your guide specifically for this project to help you get your contributions into the project. 47 | If you still have some questions or need help, feel free to open a discussion on the same. 48 | 49 | # Thank You 🙏 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-2024 Yorukot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /asset/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/demo.gif -------------------------------------------------------------------------------- /asset/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/demo.png -------------------------------------------------------------------------------- /asset/prompt_shell_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/prompt_shell_mode.png -------------------------------------------------------------------------------- /asset/prompt_spf_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/prompt_spf_mode.png -------------------------------------------------------------------------------- /asset/spf.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=spf 3 | GenericName=superfile 4 | Comment=fancy and modern terminal file manager 5 | Type=Application 6 | MimeType=inode/directory 7 | Icon=utilities-terminal 8 | Terminal=true 9 | TryExec=spf 10 | Exec=spf %u 11 | Categories=Utility;System;FileTools;FileManager;Filesystem;ConsoleOnly 12 | Keywords=File;Manager;Explorer 13 | -------------------------------------------------------------------------------- /asset/superfileicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/superfileicon.png -------------------------------------------------------------------------------- /asset/superfilelogoblack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/superfilelogoblack.png -------------------------------------------------------------------------------- /asset/superfilelogounused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/superfilelogounused.png -------------------------------------------------------------------------------- /asset/superfilelogowhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/superfilelogowhite.png -------------------------------------------------------------------------------- /asset/theme/0x96f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/0x96f.png -------------------------------------------------------------------------------- /asset/theme/ayu-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/ayu-dark.png -------------------------------------------------------------------------------- /asset/theme/blood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/blood.png -------------------------------------------------------------------------------- /asset/theme/catppuccin-frappe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/catppuccin-frappe.png -------------------------------------------------------------------------------- /asset/theme/catppuccin-latte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/catppuccin-latte.png -------------------------------------------------------------------------------- /asset/theme/catppuccin-macchiato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/catppuccin-macchiato.png -------------------------------------------------------------------------------- /asset/theme/catppuccin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/catppuccin.png -------------------------------------------------------------------------------- /asset/theme/dracula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/dracula.png -------------------------------------------------------------------------------- /asset/theme/everforest-dark-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/everforest-dark-medium.png -------------------------------------------------------------------------------- /asset/theme/gruvbox-dark-hard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/gruvbox-dark-hard.png -------------------------------------------------------------------------------- /asset/theme/gruvbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/gruvbox.png -------------------------------------------------------------------------------- /asset/theme/hacks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/hacks.png -------------------------------------------------------------------------------- /asset/theme/kaolin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/kaolin.png -------------------------------------------------------------------------------- /asset/theme/monokai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/monokai.png -------------------------------------------------------------------------------- /asset/theme/nord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/nord.png -------------------------------------------------------------------------------- /asset/theme/onedark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/onedark.png -------------------------------------------------------------------------------- /asset/theme/poimandres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/poimandres.png -------------------------------------------------------------------------------- /asset/theme/rose-pine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/rose-pine.png -------------------------------------------------------------------------------- /asset/theme/sugarplum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/sugarplum.png -------------------------------------------------------------------------------- /asset/theme/tokyonight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/theme/tokyonight.png -------------------------------------------------------------------------------- /asset/warp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/asset/warp.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # build the app 4 | CGO_ENABLED=0 go build -o ./bin/spf 5 | -------------------------------------------------------------------------------- /cd_on_quit/cd_on_quit.fish: -------------------------------------------------------------------------------- 1 | function spf 2 | set os $(uname -s) 3 | 4 | if test "$os" = "Linux" 5 | set spf_last_dir "$HOME/.local/state/superfile/lastdir" 6 | end 7 | 8 | if test "$os" = "Darwin" 9 | set spf_last_dir "$HOME/Library/Application Support/superfile/lastdir" 10 | end 11 | 12 | command spf $argv 13 | 14 | if test -f "$spf_last_dir" 15 | source "$spf_last_dir" 16 | rm -f -- "$spf_last_dir" >> /dev/null 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /cd_on_quit/cd_on_quit.ps1: -------------------------------------------------------------------------------- 1 | function spf() { 2 | param ( 3 | [string[]]$Params 4 | ) 5 | $spf_location = [Environment]::GetFolderPath("LocalApplicationData") + "\Programs\superfile\spf.exe" 6 | $SPF_LAST_DIR_PATH = [Environment]::GetFolderPath("LocalApplicationData") + "\superfile\lastdir" 7 | 8 | & $spf_location @Params 9 | 10 | if (Test-Path $SPF_LAST_DIR_PATH) { 11 | $SPF_LAST_DIR = Get-Content -Path $SPF_LAST_DIR_PATH 12 | Invoke-Expression $SPF_LAST_DIR 13 | Remove-Item -Force $SPF_LAST_DIR_PATH 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cd_on_quit/cd_on_quit.sh: -------------------------------------------------------------------------------- 1 | spf() { 2 | os=$(uname -s) 3 | 4 | # Linux 5 | if [[ "$os" == "Linux" ]]; then 6 | export SPF_LAST_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/superfile/lastdir" 7 | fi 8 | 9 | # macOS 10 | if [[ "$os" == "Darwin" ]]; then 11 | export SPF_LAST_DIR="$HOME/Library/Application Support/superfile/lastdir" 12 | fi 13 | 14 | command spf "$@" 15 | 16 | [ ! -f "$SPF_LAST_DIR" ] || { 17 | . "$SPF_LAST_DIR" 18 | rm -f -- "$SPF_LAST_DIR" > /dev/null 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1747046372, 7 | "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "inputs": { 21 | "systems": "systems" 22 | }, 23 | "locked": { 24 | "lastModified": 1731533236, 25 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "gomod2nix": { 38 | "inputs": { 39 | "flake-utils": [ 40 | "flake-utils" 41 | ], 42 | "nixpkgs": [ 43 | "nixpkgs" 44 | ] 45 | }, 46 | "locked": { 47 | "lastModified": 1745875161, 48 | "narHash": "sha256-0YkWCS13jpoo3+sX/3kcgdxBNt1VZTmvF+FhZb4rFKI=", 49 | "owner": "nix-community", 50 | "repo": "gomod2nix", 51 | "rev": "2cbd7fdd6eeab65c494cc426e18f4e4d2a5e35c0", 52 | "type": "github" 53 | }, 54 | "original": { 55 | "owner": "nix-community", 56 | "repo": "gomod2nix", 57 | "type": "github" 58 | } 59 | }, 60 | "nixpkgs": { 61 | "locked": { 62 | "lastModified": 1747744144, 63 | "narHash": "sha256-W7lqHp0qZiENCDwUZ5EX/lNhxjMdNapFnbErcbnP11Q=", 64 | "owner": "nixos", 65 | "repo": "nixpkgs", 66 | "rev": "2795c506fe8fb7b03c36ccb51f75b6df0ab2553f", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "nixos", 71 | "ref": "nixos-unstable", 72 | "repo": "nixpkgs", 73 | "type": "github" 74 | } 75 | }, 76 | "root": { 77 | "inputs": { 78 | "flake-compat": "flake-compat", 79 | "flake-utils": "flake-utils", 80 | "gomod2nix": "gomod2nix", 81 | "nixpkgs": "nixpkgs" 82 | } 83 | }, 84 | "systems": { 85 | "locked": { 86 | "lastModified": 1681028828, 87 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 91 | "type": "github" 92 | }, 93 | "original": { 94 | "owner": "nix-systems", 95 | "repo": "default", 96 | "type": "github" 97 | } 98 | } 99 | }, 100 | "root": "root", 101 | "version": 7 102 | } 103 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A fancy, pretty terminal file manager"; 3 | inputs = { 4 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 | 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | 8 | flake-compat.url = "github:edolstra/flake-compat"; 9 | flake-compat.flake = false; 10 | 11 | gomod2nix.url = "github:nix-community/gomod2nix"; 12 | gomod2nix.inputs.nixpkgs.follows = "nixpkgs"; 13 | gomod2nix.inputs.flake-utils.follows = "flake-utils"; 14 | }; 15 | 16 | outputs = inputs @ {...}: 17 | inputs.flake-utils.lib.eachDefaultSystem 18 | ( 19 | system: let 20 | overlays = [ 21 | inputs.gomod2nix.overlays.default 22 | ]; 23 | pkgs = import inputs.nixpkgs { 24 | inherit system overlays; 25 | }; 26 | in rec { 27 | packages = rec { 28 | superfile = pkgs.buildGoApplication { 29 | pname = "superfile"; 30 | version = "1.3.1"; 31 | src = ./.; 32 | modules = ./gomod2nix.toml; 33 | nativeCheckInputs = [ pkgs.writableTmpDirAsHomeHook ]; 34 | }; 35 | default = superfile; 36 | }; 37 | 38 | apps = rec { 39 | superfile = { 40 | type = "app"; 41 | program = "${packages.superfile}/bin/superfile"; 42 | }; 43 | default = superfile; 44 | }; 45 | 46 | devShells = { 47 | default = pkgs.mkShell { 48 | packages = with pkgs; [ 49 | ## golang 50 | delve 51 | go-outline 52 | go 53 | golangci-lint 54 | gopkgs 55 | gopls 56 | gotools 57 | nix 58 | gomod2nix 59 | nixpkgs-fmt 60 | ]; 61 | }; 62 | }; 63 | } 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yorukot/superfile 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/adrg/xdg v0.5.3 7 | github.com/alecthomas/chroma/v2 v2.16.0 8 | github.com/atotto/clipboard v0.1.4 9 | github.com/barasher/go-exiftool v1.10.0 10 | github.com/charmbracelet/bubbles v0.20.0 11 | github.com/charmbracelet/bubbletea v1.3.4 12 | github.com/charmbracelet/lipgloss v1.0.0 13 | github.com/charmbracelet/x/ansi v0.8.0 14 | github.com/hymkor/trash-go v0.2.0 15 | github.com/lithammer/shortuuid v3.0.0+incompatible 16 | github.com/muesli/termenv v0.16.0 17 | github.com/reinhrst/fzf-lib v0.9.0 18 | github.com/rkoesters/xdg v0.0.1 19 | github.com/shirou/gopsutil/v4 v4.25.3 20 | github.com/stretchr/testify v1.10.0 21 | github.com/urfave/cli/v2 v2.27.6 22 | golang.org/x/mod v0.24.0 23 | golift.io/xtractr v0.2.2 24 | ) 25 | 26 | require ( 27 | github.com/charmbracelet/x/term v0.2.1 // indirect 28 | github.com/ebitengine/purego v0.8.3 // indirect 29 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 30 | golang.org/x/image v0.18.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | 34 | require ( 35 | github.com/andybalholm/brotli v1.0.5 // indirect 36 | github.com/bodgit/plumbing v1.3.0 // indirect 37 | github.com/bodgit/sevenzip v1.4.0 // indirect 38 | github.com/bodgit/windows v1.0.1 // indirect 39 | github.com/connesc/cipherio v0.2.1 // indirect 40 | github.com/dlclark/regexp2 v1.11.5 // indirect 41 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 42 | github.com/google/uuid v1.6.0 // indirect 43 | github.com/hashicorp/errwrap v1.1.0 // indirect 44 | github.com/hashicorp/go-multierror v1.1.1 // indirect 45 | github.com/kdomanski/iso9660 v0.3.3 // indirect 46 | github.com/klauspost/compress v1.16.3 // indirect 47 | github.com/nwaples/rardecode v1.1.3 // indirect 48 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 49 | github.com/ulikunitz/xz v0.5.11 // indirect 50 | github.com/yorukot/ansichroma v0.1.0 51 | go4.org v0.0.0-20230225012048-214862532bf5 // indirect 52 | ) 53 | 54 | require ( 55 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 56 | github.com/charmbracelet/harmonica v0.2.0 // indirect 57 | github.com/charmbracelet/x/exp/term v0.0.0-20240814160751-e2dc8b53b604 58 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 59 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 60 | github.com/disintegration/imaging v1.6.2 61 | github.com/go-ole/go-ole v1.2.6 // indirect 62 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 63 | github.com/mattn/go-isatty v0.0.20 // indirect 64 | github.com/mattn/go-localereader v0.0.1 // indirect 65 | github.com/mattn/go-runewidth v0.0.16 66 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 67 | github.com/muesli/cancelreader v0.2.2 // indirect 68 | github.com/muesli/reflow v0.3.0 69 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 70 | github.com/pelletier/go-toml/v2 v2.2.4 71 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 72 | github.com/rivo/uniseg v0.4.7 // indirect 73 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 74 | github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd 75 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 76 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 77 | golang.org/x/sync v0.11.0 // indirect 78 | golang.org/x/sys v0.30.0 // indirect 79 | golang.org/x/text v0.16.0 // indirect 80 | ) 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/yorukot/superfile/src/cmd" 7 | ) 8 | 9 | var ( 10 | //go:embed src/superfile_config/* 11 | content embed.FS 12 | ) 13 | 14 | func main() { 15 | cmd.Run(content) 16 | } 17 | -------------------------------------------------------------------------------- /release/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S bash -euo pipefail 2 | 3 | projectName="superfile" 4 | version="v1.3.1" 5 | osList=("darwin" "linux" "windows") 6 | archList=("amd64" "arm64") 7 | mkdir dist 8 | 9 | for os in "${osList[@]}"; do 10 | if [ "$os" = "windows" ]; then 11 | for arch in "${archList[@]}"; do 12 | echo "$projectName-$os-$version-$arch" 13 | mkdir "./dist/$projectName-$os-$version-$arch" 14 | cd ../ || exit 15 | env GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -o "./release/dist/$projectName-$os-$version-$arch/spf.exe" main.go 16 | cd ./release || exit 17 | zip -r "./dist/$projectName-$os-$version-$arch.zip" "./dist/$projectName-$os-$version-$arch" 18 | done 19 | else 20 | for arch in "${archList[@]}"; do 21 | echo "$projectName-$os-$version-$arch" 22 | mkdir "./dist/$projectName-$os-$version-$arch" 23 | cd ../ || exit 24 | env GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -o "./release/dist/$projectName-$os-$version-$arch/spf" main.go 25 | cd ./release || exit 26 | tar czf "./dist/$projectName-$os-$version-$arch.tar.gz" "./dist/$projectName-$os-$version-$arch" 27 | done 28 | fi 29 | done 30 | w -------------------------------------------------------------------------------- /release/release_check.md: -------------------------------------------------------------------------------- 1 | - [ ] check all plugins is disable 2 | - [ ] check update version and zip file -------------------------------------------------------------------------------- /release/remove_all_spf_config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -r "$HOME/.config/superfile" 4 | rm -r "$HOME/.local/state/superfile" 5 | rm -r "$HOME/.local/share/superfile" -------------------------------------------------------------------------------- /src/config/icon/function.go: -------------------------------------------------------------------------------- 1 | package icon 2 | 3 | func InitIcon(nerdfont bool, directoryIconColor string) { 4 | // Make sure that these alternatives are ASCII characters only. 5 | // Dont place any special unicode characters here. 6 | if !nerdfont { 7 | // Do we need this to be empty ? Maybe it should just be normal space ? 8 | Space = "" 9 | SuperfileIcon = "" 10 | 11 | Home = "" 12 | Download = "" 13 | Documents = "" 14 | Pictures = "" 15 | Videos = "" 16 | Music = "" 17 | Templates = "" 18 | PublicShare = "" 19 | 20 | // file operations 21 | CompressFile = "" 22 | ExtractFile = "" 23 | Copy = "" 24 | Cut = "" 25 | Delete = "" 26 | 27 | // other 28 | Cursor = ">" 29 | Browser = "B" 30 | Select = "S" 31 | Error = "" 32 | Warn = "" 33 | Done = "" 34 | InOperation = "" 35 | Directory = "" 36 | Search = "" 37 | SortAsc = "^" 38 | SortDesc = "v" 39 | Pinned = "" 40 | Disk = "" 41 | } 42 | 43 | if directoryIconColor == "" { 44 | directoryIconColor = "NONE" // Dark yellowish 45 | } 46 | Folders["folder"] = Style{ 47 | Icon: "\uf07b", // Printable Rune : "" 48 | Color: directoryIconColor, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/internal/backend/ReadMe.md: -------------------------------------------------------------------------------- 1 | # Backend Package 2 | Handles operations on the User's OS. 3 | For example, executing shell commands, performing file operations on user's files... 4 | Reading OS-specific configurations like disk partitions. 5 | 6 | The name 'backend' isn't the most appropriate, open to suggestions. 7 | This would modularize the code, and would enable us to write unit tests 8 | where we would 'mock' the backend functionality with dummy interface 9 | implementations 10 | 11 | # Dependencies 12 | Should not import any "ui" package 13 | Can import common and its subpackages 14 | 15 | # Implementation specifications 16 | Try to implement everything via interfaces, so that we can easily write unit tests 17 | -------------------------------------------------------------------------------- /src/internal/common/ReadMe.md: -------------------------------------------------------------------------------- 1 | # common package 2 | Defines common utilities for ui and file operations package 3 | everyone can use common package, but common package should not have any dependency on 4 | any other package. Currently, common package is a big monolith, but we plan to separate it into config, 5 | 6 | 7 | # Dependencies 8 | - src/config package -------------------------------------------------------------------------------- /src/internal/common/default_config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // Variables for holding default configurations of each settings 4 | var ( 5 | HotkeysTomlString string 6 | ConfigTomlString string 7 | DefaultThemeString string 8 | ) 9 | 10 | var Theme ThemeType 11 | var Config ConfigType 12 | var Hotkeys HotkeysType 13 | -------------------------------------------------------------------------------- /src/internal/common/icon_utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/yorukot/superfile/src/config/icon" 8 | ) 9 | 10 | func GetElementIcon(file string, isDir bool, nerdFont bool) icon.Style { 11 | ext := strings.TrimPrefix(filepath.Ext(file), ".") 12 | name := file 13 | 14 | if !nerdFont { 15 | return icon.Style{ 16 | Icon: "", 17 | Color: Theme.FilePanelFG, 18 | } 19 | } 20 | 21 | if isDir { 22 | resultIcon := icon.Folders["folder"] 23 | betterIcon, hasBetterIcon := icon.Folders[name] 24 | if hasBetterIcon { 25 | resultIcon = betterIcon 26 | } 27 | return resultIcon 28 | } 29 | // default icon for all files. try to find a better one though... 30 | resultIcon := icon.Icons["file"] 31 | // resolve aliased extensions 32 | extKey := strings.ToLower(ext) 33 | alias, hasAlias := icon.Aliases[extKey] 34 | if hasAlias { 35 | extKey = alias 36 | } 37 | 38 | // see if we can find a better icon based on extension alone 39 | betterIcon, hasBetterIcon := icon.Icons[extKey] 40 | if hasBetterIcon { 41 | resultIcon = betterIcon 42 | } 43 | 44 | // now look for icons based on full names 45 | fullName := name 46 | 47 | fullName = strings.ToLower(fullName) 48 | fullAlias, hasFullAlias := icon.Aliases[fullName] 49 | if hasFullAlias { 50 | fullName = fullAlias 51 | } 52 | bestIcon, hasBestIcon := icon.Icons[fullName] 53 | if hasBestIcon { 54 | resultIcon = bestIcon 55 | } 56 | if resultIcon.Color == "NONE" { 57 | return icon.Style{ 58 | Icon: resultIcon.Icon, 59 | Color: Theme.FilePanelFG, 60 | } 61 | } 62 | return resultIcon 63 | } 64 | -------------------------------------------------------------------------------- /src/internal/common/icon_utils_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yorukot/superfile/src/config/icon" 7 | ) 8 | 9 | func TestGetElementIcon(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | file string 13 | isDir bool 14 | nerdFont bool 15 | expected icon.Style 16 | }{ 17 | { 18 | name: "Non-nerdfont returns empty icon", 19 | file: "test.txt", 20 | isDir: false, 21 | nerdFont: false, 22 | expected: icon.Style{ 23 | Icon: "", 24 | Color: Theme.FilePanelFG, 25 | }, 26 | }, 27 | { 28 | name: "Directory with nerd font", 29 | file: "folder", 30 | isDir: true, 31 | nerdFont: true, 32 | expected: icon.Folders["folder"], 33 | }, 34 | { 35 | name: "File with known extension", 36 | file: "test.js", 37 | isDir: false, 38 | nerdFont: true, 39 | expected: icon.Icons["js"], 40 | }, 41 | { 42 | name: "Full name takes priority over extension", 43 | file: "gulpfile.js", 44 | isDir: false, 45 | nerdFont: true, 46 | expected: icon.Icons["gulpfile.js"], 47 | }, 48 | { 49 | name: ".git directory", 50 | file: ".git", 51 | isDir: true, 52 | nerdFont: true, 53 | expected: icon.Folders[".git"], 54 | }, 55 | { 56 | name: "superfile directory", 57 | file: "superfile", 58 | isDir: true, 59 | nerdFont: true, 60 | expected: icon.Folders["superfile"], 61 | }, 62 | { 63 | name: "package.json file", 64 | file: "package.json", 65 | isDir: false, 66 | nerdFont: true, 67 | expected: icon.Icons["package"], 68 | }, 69 | { 70 | name: "File with unknown extension", 71 | file: "test.xyz", 72 | isDir: false, 73 | nerdFont: true, 74 | expected: icon.Style{ 75 | Icon: icon.Icons["file"].Icon, 76 | // Theme is not defined here, so this will be blank 77 | Color: Theme.FilePanelFG, 78 | }, 79 | }, 80 | { 81 | name: "File with aliased name", 82 | file: "dockerfile", 83 | isDir: false, 84 | nerdFont: true, 85 | expected: icon.Icons["dockerfile"], 86 | }, 87 | } 88 | 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | result := GetElementIcon(tt.file, tt.isDir, tt.nerdFont) 92 | if result.Icon != tt.expected.Icon || result.Color != tt.expected.Color { 93 | t.Errorf("GetElementIcon() = %v, want %v", result, tt.expected) 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/internal/common/predefined_variable.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | "github.com/yorukot/superfile/src/config/icon" 8 | ) 9 | 10 | const WheelRunTime = 5 11 | const DefaultCommandTimeout = 5000 * time.Millisecond 12 | 13 | var ( 14 | MinimumHeight = 24 15 | MinimumWidth = 60 16 | 17 | // Todo : These are model object properties, not global properties 18 | // We are modifying them in the code many time. They need to be part of model struct. 19 | MinFooterHeight = 6 20 | ModalWidth = 60 21 | ModalHeight = 7 22 | ) 23 | 24 | var ( 25 | SideBarSuperfileTitle string 26 | SideBarPinnedDivider string 27 | SideBarDisksDivider string 28 | SideBarNoneText string 29 | 30 | ProcessBarNoneText string 31 | 32 | FilePanelTopDirectoryIcon string 33 | FilePanelNoneText string 34 | 35 | FilePreviewNoContentText string 36 | FilePreviewNoFileInfoText string 37 | FilePreviewUnsupportedFormatText string 38 | FilePreviewDirectoryUnreadableText string 39 | FilePreviewEmptyText string 40 | 41 | LipglossError string 42 | ) 43 | 44 | var ( 45 | UnsupportedPreviewFormats = []string{".pdf", ".torrent"} 46 | ) 47 | 48 | // No dependencies 49 | func LoadInitialPrerenderedVariables() { 50 | LipglossError = lipgloss.NewStyle().Foreground(lipgloss.Color("#F93939")).Render("Error") + 51 | lipgloss.NewStyle().Foreground(lipgloss.Color("#00FFEE")).Render(" ┃ ") 52 | } 53 | 54 | // Dependecies - Todo We should programmatically guarantee these dependencies. And log error 55 | // if its not satisfied. 56 | // LoadThemeConfig() in style.go should be finished 57 | // loadConfigFile() in config_types.go should be finished 58 | // InitIcon() in config package in function.go should be finished 59 | func LoadPrerenderedVariables() { 60 | SideBarSuperfileTitle = SidebarTitleStyle.Render(" " + icon.SuperfileIcon + " superfile") 61 | 62 | SideBarPinnedDivider = SidebarTitleStyle.Render(icon.Pinned+" Pinned") + SidebarDividerStyle.Render(" ───────────") 63 | 64 | SideBarDisksDivider = SidebarTitleStyle.Render(icon.Disk+" Disks") + SidebarDividerStyle.Render(" ────────────") 65 | 66 | SideBarNoneText = SidebarStyle.Render(" " + icon.Error + " None") 67 | 68 | ProcessBarNoneText = icon.Error + " No processes running" 69 | 70 | FilePanelTopDirectoryIcon = FilePanelTopDirectoryIconStyle.Render(" " + icon.Directory + icon.Space) 71 | FilePanelNoneText = FilePanelStyle.Render(" " + icon.Error + " No such file or directory") 72 | 73 | FilePreviewNoContentText = "--- " + icon.Error + " No content to preview ---" 74 | FilePreviewNoFileInfoText = "--- " + icon.Error + " Could not get file info ---" 75 | FilePreviewUnsupportedFormatText = "--- " + icon.Error + " Unsupported formats ---" 76 | FilePreviewDirectoryUnreadableText = "--- " + icon.Error + " Cannot read directory ---" 77 | FilePreviewEmptyText = "--- Empty ---" 78 | } 79 | -------------------------------------------------------------------------------- /src/internal/common/type.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // Placeholder inteface for now, might later move 'model' type to commons and have 4 | // and add an execute(model) function to this 5 | type ModelAction interface { 6 | String() string 7 | } 8 | 9 | type NoAction struct { 10 | } 11 | 12 | func (n NoAction) String() string { 13 | return "NoAction" 14 | } 15 | 16 | type ShellCommandAction struct { 17 | Command string 18 | } 19 | 20 | func (s ShellCommandAction) String() string { 21 | return "ShellCommandAction for command " + s.Command 22 | } 23 | 24 | // We could later move 'model' type to commons and have 25 | // these actions implement an execute(model) interface 26 | type SplitPanelAction struct{} 27 | 28 | func (s SplitPanelAction) String() string { 29 | return "SplitPanelAction" 30 | } 31 | 32 | type CDCurrentPanelAction struct { 33 | Location string 34 | } 35 | 36 | func (c CDCurrentPanelAction) String() string { 37 | return "CDCurrentPanelAction to " + c.Location 38 | } 39 | 40 | type OpenPanelAction struct { 41 | Location string 42 | } 43 | 44 | func (o OpenPanelAction) String() string { 45 | return "OpenPanelAction at " + o.Location 46 | } 47 | -------------------------------------------------------------------------------- /src/internal/file_operations_compress.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "log/slog" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/charmbracelet/bubbles/progress" 11 | "github.com/lithammer/shortuuid" 12 | "github.com/yorukot/superfile/src/config/icon" 13 | "github.com/yorukot/superfile/src/internal/common" 14 | ) 15 | 16 | func zipSource(source, target string) error { 17 | id := shortuuid.New() 18 | prog := progress.New() 19 | prog.PercentageStyle = common.FooterStyle 20 | 21 | totalFiles, err := countFiles(source) 22 | 23 | if err != nil { 24 | slog.Error("Error while zip file count files ", "error", err) 25 | } 26 | 27 | p := process{ 28 | name: "zip files", 29 | progress: prog, 30 | state: inOperation, 31 | total: totalFiles, 32 | done: 0, 33 | } 34 | 35 | message := channelMessage{ 36 | messageID: id, 37 | messageType: sendProcess, 38 | processNewState: p, 39 | } 40 | 41 | _, err = os.Stat(target) 42 | if os.IsExist(err) { 43 | p.name = icon.CompressFile + icon.Space + "File already exist" 44 | message.processNewState = p 45 | channel <- message 46 | return nil 47 | } 48 | 49 | f, err := os.Create(target) 50 | if err != nil { 51 | return err 52 | } 53 | defer f.Close() 54 | 55 | writer := zip.NewWriter(f) 56 | defer writer.Close() 57 | 58 | err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error { 59 | p.name = icon.CompressFile + icon.Space + filepath.Base(path) 60 | if len(channel) < 5 { 61 | message.processNewState = p 62 | channel <- message 63 | } 64 | 65 | if err != nil { 66 | return err 67 | } 68 | 69 | header, err := zip.FileInfoHeader(info) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | header.Method = zip.Deflate 75 | 76 | header.Name, err = filepath.Rel(filepath.Dir(source), path) 77 | if err != nil { 78 | return err 79 | } 80 | if info.IsDir() { 81 | header.Name += "/" 82 | } 83 | 84 | headerWriter, err := writer.CreateHeader(header) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | if info.IsDir() { 90 | return nil 91 | } 92 | 93 | f, err := os.Open(path) 94 | if err != nil { 95 | return err 96 | } 97 | defer f.Close() 98 | 99 | _, err = io.Copy(headerWriter, f) 100 | if err != nil { 101 | return err 102 | } 103 | p.done++ 104 | if len(channel) < 5 { 105 | message.processNewState = p 106 | channel <- message 107 | } 108 | return nil 109 | }) 110 | 111 | if err != nil { 112 | slog.Error("Error while zip file", "error", err) 113 | p.state = failure 114 | message.processNewState = p 115 | channel <- message 116 | } 117 | p.state = successful 118 | p.done = totalFiles 119 | 120 | message.processNewState = p 121 | channel <- message 122 | 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /src/internal/file_operations_extract.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "log/slog" 5 | "time" 6 | 7 | "github.com/charmbracelet/bubbles/progress" 8 | "github.com/lithammer/shortuuid" 9 | "github.com/yorukot/superfile/src/config/icon" 10 | "github.com/yorukot/superfile/src/internal/common" 11 | "golift.io/xtractr" 12 | ) 13 | 14 | func extractCompressFile(src, dest string) error { 15 | id := shortuuid.New() 16 | 17 | prog := progress.New(common.GenerateGradientColor()) 18 | prog.PercentageStyle = common.FooterStyle 19 | 20 | p := process{ 21 | name: icon.ExtractFile + icon.Space + "unzip file", 22 | progress: prog, 23 | state: inOperation, 24 | total: 1, 25 | done: 0, 26 | doneTime: time.Time{}, 27 | } 28 | message := channelMessage{ 29 | messageID: id, 30 | messageType: sendProcess, 31 | processNewState: p, 32 | } 33 | 34 | if len(channel) < 5 { 35 | channel <- message 36 | } 37 | 38 | x := &xtractr.XFile{ 39 | FilePath: src, 40 | OutputDir: dest, 41 | FileMode: 0644, 42 | DirMode: 0755, 43 | } 44 | 45 | _, _, _, err := xtractr.ExtractFile(x) 46 | 47 | if err != nil { 48 | p.state = failure 49 | p.doneTime = time.Now() 50 | message.processNewState = p 51 | if len(channel) < 5 { 52 | channel <- message 53 | } 54 | slog.Error("Error extracting", "path", src, "error", err) 55 | return err 56 | } 57 | 58 | p.state = successful 59 | p.done = 1 60 | p.doneTime = time.Now() 61 | message.processNewState = p 62 | if len(channel) < 5 { 63 | channel <- message 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /src/internal/model_file_operations_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/yorukot/superfile/src/internal/common" 11 | "github.com/yorukot/superfile/src/internal/utils" 12 | ) 13 | 14 | // Todo : Add test for model initialized with multiple directories 15 | // Todo : Add test for clipboard different variations, cut paste 16 | // Todo : Add test for tea resizing 17 | // Todo : Add test for quitting 18 | 19 | func TestCopy(t *testing.T) { 20 | curTestDir := filepath.Join(testDir, "TestCopy") 21 | dir1 := filepath.Join(curTestDir, "dir1") 22 | dir2 := filepath.Join(curTestDir, "dir2") 23 | file1 := filepath.Join(dir1, "file1.txt") 24 | t.Run("Basic Copy", func(t *testing.T) { 25 | setupDirectories(t, curTestDir, dir1, dir2) 26 | setupFiles(t, file1) 27 | t.Cleanup(func() { 28 | os.RemoveAll(curTestDir) 29 | }) 30 | 31 | m := defaultTestModel(dir1) 32 | 33 | // Todo validate current panel is "dir1" 34 | // Todo : Move all basic validation to a separate test 35 | // Everything that doesn't have anything to do with copy paste 36 | 37 | // validate file1 38 | // Todo : improve the interface we use to interact with filepanel 39 | 40 | // Todo : file1.txt should not be duplicated 41 | 42 | // Todo : Having to send a random keypress to initiate model init. 43 | // Should not have to do that 44 | TeaUpdateWithErrCheck(t, &m, nil) 45 | 46 | assert.Equal(t, "file1.txt", 47 | m.fileModel.filePanels[m.filePanelFocusIndex].element[0].name) 48 | 49 | TeaUpdateWithErrCheck(t, &m, utils.TeaRuneKeyMsg(common.Hotkeys.CopyItems[0])) 50 | 51 | // Todo : validate clipboard 52 | assert.False(t, m.copyItems.cut) 53 | assert.Equal(t, file1, m.copyItems.items[0]) 54 | 55 | // move to dir2 56 | m.updateCurrentFilePanelDir("../dir2") 57 | TeaUpdateWithErrCheck(t, &m, utils.TeaRuneKeyMsg(common.Hotkeys.PasteItems[0])) 58 | 59 | // Actual paste may take time, since its an os operations 60 | assert.Eventually(t, func() bool { 61 | _, err := os.Lstat(filepath.Join(dir2, "file1.txt")) 62 | return err == nil 63 | }, time.Second, 10*time.Millisecond) 64 | 65 | // Todo : still on clipboard 66 | assert.False(t, m.copyItems.cut) 67 | assert.Equal(t, file1, m.copyItems.items[0]) 68 | 69 | TeaUpdateWithErrCheck(t, &m, utils.TeaRuneKeyMsg(common.Hotkeys.PasteItems[0])) 70 | 71 | // Actual paste may take time, since its an os operations 72 | assert.Eventually(t, func() bool { 73 | _, err := os.Lstat(filepath.Join(dir2, "file1(1).txt")) 74 | return err == nil 75 | }, time.Second, 10*time.Millisecond) 76 | assert.FileExists(t, filepath.Join(dir2, "file1(1).txt")) 77 | // Todo : Also validate process bar having two processes. 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /src/internal/test_utils.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/stretchr/testify/require" 10 | "github.com/yorukot/superfile/src/internal/common" 11 | ) 12 | 13 | var SampleDataBytes = []byte("This is sample") //nolint: gochecknoglobals // Effectively const 14 | 15 | func defaultTestModel(dirs ...string) model { 16 | m := defaultModelConfig(false, false, false, dirs) 17 | _, _ = TeaUpdate(&m, tea.WindowSizeMsg{Width: 2 * common.MinimumWidth, Height: 2 * common.MinimumHeight}) 18 | return m 19 | } 20 | 21 | func setupDirectories(t *testing.T, dirs ...string) { 22 | t.Helper() 23 | for _, dir := range dirs { 24 | err := os.MkdirAll(dir, 0755) 25 | require.NoError(t, err) 26 | } 27 | } 28 | 29 | func setupFilesWithData(t *testing.T, data []byte, files ...string) { 30 | t.Helper() 31 | for _, file := range files { 32 | err := os.WriteFile(file, data, 0644) 33 | require.NoError(t, err) 34 | } 35 | } 36 | 37 | func setupFiles(t *testing.T, files ...string) { 38 | setupFilesWithData(t, SampleDataBytes, files...) 39 | } 40 | 41 | // TeaUpdate : Utility to send update to model , majorly used in tests 42 | // Not using pointer receiver as this is more like a utility, than 43 | // a member function of model 44 | // Todo : Consider wrapping TeaUpdate with a helper that both forwards the return 45 | // values and does a require.NoError(t, err) 46 | func TeaUpdate(m *model, msg tea.Msg) (tea.Cmd, error) { 47 | resModel, cmd := m.Update(msg) 48 | 49 | mObj, ok := resModel.(model) 50 | if !ok { 51 | return cmd, fmt.Errorf("unexpected model type: %T", resModel) 52 | } 53 | *m = mObj 54 | return cmd, nil 55 | } 56 | 57 | func TeaUpdateWithErrCheck(t *testing.T, m *model, msg tea.Msg) tea.Cmd { 58 | cmd, err := TeaUpdate(m, msg) 59 | require.NoError(t, err) 60 | return cmd 61 | } 62 | 63 | // Is the command tea.quit, or a batch that contains tea.quit 64 | func IsTeaQuit(cmd tea.Cmd) bool { 65 | if cmd == nil { 66 | return false 67 | } 68 | msg := cmd() 69 | switch msg := msg.(type) { 70 | case tea.QuitMsg: 71 | return true 72 | case tea.BatchMsg: 73 | for _, curCmd := range msg { 74 | if IsTeaQuit(curCmd) { 75 | return true 76 | } 77 | } 78 | return false 79 | default: 80 | return false 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/internal/type_utils.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yorukot/superfile/src/internal/common" 7 | "github.com/yorukot/superfile/src/internal/utils" 8 | ) 9 | 10 | const invalidTypeString = "InvalidType" 11 | 12 | // reset the items slice and set the cut value 13 | func (c *copyItems) reset(cut bool) { 14 | c.cut = cut 15 | c.items = c.items[:0] 16 | } 17 | 18 | // ================ Model related utils ======================= 19 | 20 | // Non fatal Validations. This indicates bug / programming errors, not user configuration mistake 21 | func (m *model) validateLayout() error { 22 | if 0 < m.footerHeight && m.footerHeight < common.MinFooterHeight { 23 | return fmt.Errorf("footerHeight %v is too small", m.footerHeight) 24 | } 25 | if !m.toggleFooter && m.footerHeight != 0 { 26 | return fmt.Errorf("footer closed and footerHeight %v is non zero", m.footerHeight) 27 | } 28 | // PanelHeight + 2 lines (main border) + actual footer height 29 | if m.fullHeight != (m.mainPanelHeight+2)+utils.FullFooterHeight(m.footerHeight, m.toggleFooter) { 30 | return fmt.Errorf("invalid model layout, fullHeight : %v, mainPanelHeight : %v, footerHeight : %v", 31 | m.fullHeight, m.mainPanelHeight, m.footerHeight) 32 | } 33 | // Todo : Add check for width as well 34 | return nil 35 | } 36 | 37 | // ================ filepanel 38 | 39 | func filePanelSlice(dir []string) []filePanel { 40 | res := make([]filePanel, len(dir)) 41 | for i := range dir { 42 | res[i] = defaultFilePanel(dir[i]) 43 | } 44 | return res 45 | } 46 | 47 | func defaultFilePanel(dir string) filePanel { 48 | return filePanel{ 49 | render: 0, 50 | cursor: 0, 51 | location: dir, 52 | sortOptions: sortOptionsModel{ 53 | width: 20, 54 | height: 4, 55 | open: false, 56 | cursor: common.Config.DefaultSortType, 57 | data: sortOptionsModelData{ 58 | options: []string{"Name", "Size", "Date Modified", "Type"}, 59 | selected: common.Config.DefaultSortType, 60 | reversed: common.Config.SortOrderReversed, 61 | }, 62 | }, 63 | panelMode: browserMode, 64 | focusType: focus, 65 | directoryRecords: make(map[string]directoryRecord), 66 | searchBar: common.GenerateSearchBar(), 67 | } 68 | } 69 | 70 | // ================ String method for easy logging ===================== 71 | 72 | func (f focusPanelType) String() string { 73 | switch f { 74 | case nonePanelFocus: 75 | return "nonePanelFocus" 76 | case processBarFocus: 77 | return "processBarFocus" 78 | case sidebarFocus: 79 | return "sidebarFocus" 80 | case metadataFocus: 81 | return "metadataFocus" 82 | default: 83 | return invalidTypeString 84 | } 85 | } 86 | 87 | func (f filePanelFocusType) String() string { 88 | switch f { 89 | case noneFocus: 90 | return "noneFocus" 91 | case secondFocus: 92 | return "secondFocus" 93 | case focus: 94 | return "focus" 95 | default: 96 | return invalidTypeString 97 | } 98 | } 99 | 100 | func (p panelMode) String() string { 101 | switch p { 102 | case selectMode: 103 | return "selectMode" 104 | case browserMode: 105 | return "browserMode" 106 | default: 107 | return invalidTypeString 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/internal/ui/ReadMe.md: -------------------------------------------------------------------------------- 1 | # ui package 2 | 3 | # To-dos 4 | - Put model, filePanel, sidebarModel, etc. in separate packages like this -------------------------------------------------------------------------------- /src/internal/ui/prompt/ReadMe.md: -------------------------------------------------------------------------------- 1 | # prompt package 2 | This is for the Prompt modal of superfile 3 | 4 | Handles user input updates, spf model updates, and returns a PromptAction to model. 5 | 6 | 7 | # Coverage 8 | 9 | ```bash 10 | cd /path/to/ui/prompt 11 | # Basic coverage 12 | go test -cover 13 | 14 | # HTML report 15 | go test -coverprofile=coverage.out && go tool cover -html=coverage.out -o coverage.html 16 | ``` 17 | Current coverage is 91.3%. 18 | -------------------------------------------------------------------------------- /src/internal/ui/prompt/consts.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import "time" 4 | 5 | // These could as well be property of prompt Model vs being global consts 6 | // But its fine 7 | const ( 8 | promptHeadlineText = "Superfile Prompt" 9 | 10 | OpenCommand = "open" 11 | SplitCommand = "split" 12 | CdCommand = "cd" 13 | 14 | // We could later make this configurable. But, not needed now. 15 | spfPromptChar = ">" 16 | shellPromptChar = ":" 17 | 18 | successMessagePrefix = "Success" 19 | failureMessagePrefix = "Error" 20 | 21 | shellModeString = "(Shell Mode)" 22 | spfModeString = "(SPF Mode)" 23 | 24 | // Error message string 25 | tokenizationError = "Failed during tokenization" 26 | splitCommandArgError = "split command should not be given arguments" 27 | 28 | // Timeout for command executed for shell substitution 29 | shellSubTimeout = 1000 * time.Millisecond 30 | shellSubTimeoutInTests = 100 * time.Millisecond 31 | 32 | defaultTestCwd = "/" 33 | 34 | PromptMinWidth = 10 35 | PromptMinHeight = 3 36 | 37 | defaultTestWidth = 100 38 | defaultTestMaxHeight = 100 39 | ) 40 | 41 | func modeString(shellMode bool) string { 42 | if shellMode { 43 | return shellModeString 44 | } 45 | return spfModeString 46 | } 47 | 48 | func shellPrompt(shellMode bool) string { 49 | if shellMode { 50 | return shellPromptChar 51 | } 52 | return spfPromptChar 53 | } 54 | 55 | func defaultCommandSlice() []promptCommand { 56 | return []promptCommand{ 57 | { 58 | command: OpenCommand, 59 | usage: OpenCommand + " ", 60 | description: "Open a new panel at a specified path", 61 | }, 62 | { 63 | command: SplitCommand, 64 | usage: SplitCommand, 65 | description: "Open a new panel at a current file panel's path", 66 | }, 67 | { 68 | command: CdCommand, 69 | usage: CdCommand + " ", 70 | description: "Change directory of current panel", 71 | }, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/internal/ui/prompt/error.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import "fmt" 4 | 5 | // This is to generate error objects that can be nicely printed to UI 6 | type invalidCmdError struct { 7 | uiMsg string 8 | wrappedError error 9 | } 10 | 11 | func (e invalidCmdError) Error() string { 12 | if e.wrappedError == nil { 13 | return e.uiMsg 14 | } 15 | return e.wrappedError.Error() 16 | } 17 | 18 | func (e invalidCmdError) Unwrap() error { 19 | return e.wrappedError 20 | } 21 | 22 | func (e invalidCmdError) uiMessage() string { 23 | return e.uiMsg 24 | } 25 | 26 | type envVarNotFoundError struct { 27 | varName string 28 | } 29 | 30 | func (e envVarNotFoundError) Error() string { 31 | return fmt.Sprintf("env var %s not found", e.varName) 32 | } 33 | 34 | type bracketMatchError struct { 35 | openChar rune 36 | closeChar rune 37 | } 38 | 39 | func (p bracketMatchError) Error() string { 40 | return fmt.Sprintf("could not find matching %c for %c", p.closeChar, p.openChar) 41 | } 42 | 43 | func roundBracketMatchError() bracketMatchError { 44 | return bracketMatchError{openChar: '(', closeChar: ')'} 45 | } 46 | 47 | func curlyBracketMatchError() bracketMatchError { 48 | return bracketMatchError{openChar: '{', closeChar: '}'} 49 | } 50 | -------------------------------------------------------------------------------- /src/internal/ui/prompt/tokenize.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "github.com/yorukot/superfile/src/internal/utils" 12 | ) 13 | 14 | // split into tokens 15 | func tokenizePromptCommand(command string, cwdLocation string) ([]string, error) { 16 | command, err := resolveShellSubstitution(shellSubTimeout, command, cwdLocation) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return strings.Fields(command), nil 21 | } 22 | 23 | // Replace ${} and $() with values 24 | func resolveShellSubstitution(subCmdTimeout time.Duration, command string, cwdLocation string) (string, error) { 25 | resCommand := strings.Builder{} 26 | cmdRunes := []rune(command) 27 | i := 0 28 | for i < len(cmdRunes) { 29 | if i+1 < len(cmdRunes) && cmdRunes[i] == '$' { 30 | switch cmdRunes[i+1] { 31 | // ${ spotted 32 | case '{': 33 | // Look for Ending '}' 34 | end := findEndingBracket(cmdRunes, i+1, '{', '}') 35 | if end == -1 { 36 | return "", errors.New("unexpected error in tokenization") 37 | } 38 | if end == len(cmdRunes) { 39 | return "", curlyBracketMatchError() 40 | } 41 | 42 | envVarName := string(cmdRunes[i+2 : end]) 43 | 44 | // We can add a layer of abstraction for better unit testing 45 | value, ok := os.LookupEnv(envVarName) 46 | if !ok { 47 | return "", envVarNotFoundError{varName: envVarName} 48 | } 49 | // Might Handle values being too big, or having multiple lines 50 | // But this is based on user input, so it is probably okay for now 51 | // Same comment for command substitution 52 | resCommand.WriteString(value) 53 | 54 | i = end + 1 55 | case '(': 56 | // Look for ending ')' 57 | end := findEndingBracket(cmdRunes, i+1, '(', ')') 58 | if end == -1 { 59 | return "", errors.New("unexpected error in tokenization") 60 | } 61 | 62 | if end == len(cmdRunes) { 63 | return "", roundBracketMatchError() 64 | } 65 | 66 | subCmd := string(cmdRunes[i+2 : end]) 67 | retCode, output, err := utils.ExecuteCommandInShell(subCmdTimeout, cwdLocation, subCmd) 68 | 69 | if retCode == -1 { 70 | return "", fmt.Errorf("could not execute shell substitution command : %s : %w", subCmd, err) 71 | } 72 | // We are allowing commands that exit with non zero status code 73 | // We still use its output 74 | if retCode != 0 { 75 | slog.Debug("substitution command exited with non zero status", "retCode", retCode, 76 | "command", subCmd) 77 | } 78 | resCommand.WriteString(output) 79 | 80 | i = end + 1 81 | default: 82 | resCommand.WriteRune(cmdRunes[i]) 83 | i++ 84 | } 85 | } else { 86 | resCommand.WriteRune(cmdRunes[i]) 87 | i++ 88 | } 89 | } 90 | 91 | return resCommand.String(), nil 92 | } 93 | 94 | func findEndingBracket(r []rune, openIdx int, openParan rune, closeParan rune) int { 95 | if openIdx < 0 || openIdx >= len(r) || r[openIdx] != openParan { 96 | return -1 97 | } 98 | 99 | openCount := 1 100 | i := openIdx + 1 101 | for i < len(r) && openCount != 0 { 102 | if r[i] == openParan { 103 | openCount++ 104 | } else if r[i] == closeParan { 105 | openCount-- 106 | } 107 | if openCount != 0 { 108 | i++ 109 | } 110 | } 111 | return i 112 | } 113 | -------------------------------------------------------------------------------- /src/internal/ui/prompt/type.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import "github.com/charmbracelet/bubbles/textinput" 4 | 5 | // No need to name it as PromptModel. It will me imported as prompt.Model 6 | type Model struct { 7 | 8 | // Configuration 9 | headline string 10 | commands []promptCommand 11 | spfPromptHotkey string 12 | shellPromptHotkey string 13 | closeOnSuccess bool 14 | 15 | // State 16 | open bool 17 | // whether its shellMode or spfMode 18 | // Always use setShellMode to adjust 19 | shellMode bool 20 | textInput textinput.Model 21 | resultMsg string 22 | 23 | // Whether the user intended action was successful 24 | actionSuccess bool 25 | 26 | // Dimensions - Exported, since model will be dynamically adjusting them 27 | width int 28 | // Height is dynamically adjusted based on content 29 | maxHeight int 30 | } 31 | 32 | // This is only used to render suggestions 33 | // Should not be exported 34 | type promptCommand struct { 35 | command string 36 | usage string 37 | description string 38 | } 39 | -------------------------------------------------------------------------------- /src/internal/ui/prompt/utils.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/yorukot/superfile/src/internal/common" 8 | ) 9 | 10 | func getPromptAction(shellMode bool, value string, cwdLocation string) (common.ModelAction, error) { 11 | noAction := common.NoAction{} 12 | if value == "" { 13 | return noAction, nil 14 | } 15 | if shellMode { 16 | return common.ShellCommandAction{ 17 | Command: value, 18 | }, nil 19 | } 20 | 21 | promptArgs, err := tokenizePromptCommand(value, cwdLocation) 22 | if err != nil { 23 | return noAction, invalidCmdError{ 24 | uiMsg: tokenizationError + " : " + err.Error(), 25 | wrappedError: fmt.Errorf("error during tokenization : %w", err), 26 | } 27 | } 28 | 29 | switch promptArgs[0] { 30 | case "split": 31 | if len(promptArgs) != 1 { 32 | return noAction, invalidCmdError{ 33 | uiMsg: splitCommandArgError, 34 | } 35 | } 36 | return common.SplitPanelAction{}, nil 37 | case "cd": 38 | if len(promptArgs) != 2 { 39 | return noAction, invalidCmdError{ 40 | uiMsg: fmt.Sprintf("cd command needs exactly one argument, received %d", 41 | len(promptArgs)-1), 42 | } 43 | } 44 | return common.CDCurrentPanelAction{ 45 | Location: promptArgs[1], 46 | }, nil 47 | case "open": 48 | if len(promptArgs) != 2 { 49 | return noAction, invalidCmdError{ 50 | uiMsg: fmt.Sprintf("open command needs exactly one argument, received %d", 51 | len(promptArgs)-1), 52 | } 53 | } 54 | return common.OpenPanelAction{ 55 | Location: promptArgs[1], 56 | }, nil 57 | 58 | default: 59 | return noAction, invalidCmdError{ 60 | uiMsg: "Invalid spf command : " + promptArgs[0], 61 | } 62 | } 63 | } 64 | 65 | // Only allocates memory proportional to first token's size 66 | // Only works for space right now. Does not splits command based on 67 | // \n or \t , etc 68 | func getFirstToken(command string) string { 69 | command = strings.TrimSpace(command) 70 | spaceIndex := strings.IndexByte(command, ' ') 71 | if spaceIndex == -1 { 72 | return command 73 | } 74 | return command[:spaceIndex] 75 | } 76 | -------------------------------------------------------------------------------- /src/internal/ui/rendering/ReadMe.md: -------------------------------------------------------------------------------- 1 | # renderer package 2 | Responsible for rendering 3 | 4 | # Dependencies 5 | This package should not not import any other UI package, and should have minimal, ideally zero, dependency on common, utils or any other spf package. Its meant as a utilites to be used by ui components and main model. 6 | It also should not be even in-directly coupled with any UI components. Assume anything like color, style, border config of any other UI component change. This package should not have any changes. 7 | 8 | # To-dos 9 | - [ ] Use rendering package for other models like sort Menu, Help menu, etc. 10 | 11 | # Notes 12 | - Can we move this whole thing into a good useful TUI library outside of this repo ?. At least code it in a way that it can be moved 13 | -------------------------------------------------------------------------------- /src/internal/ui/rendering/content_renderer.go: -------------------------------------------------------------------------------- 1 | package rendering 2 | 3 | import ( 4 | "log/slog" 5 | "strings" 6 | 7 | "github.com/yorukot/superfile/src/internal/common" 8 | ) 9 | 10 | type ContentRenderer struct { 11 | lines []string 12 | 13 | // Allow at max this many lines. If there are lesser lines 14 | maxLines int 15 | // Every line should have at most this many characters 16 | maxLineWidth int 17 | sanitizeContent bool 18 | 19 | // We can add alignStyle if needed 20 | truncateStyle TruncateStyle 21 | } 22 | 23 | func (r *ContentRenderer) CntLines() int { 24 | return len(r.lines) 25 | } 26 | 27 | func (r *ContentRenderer) AddLines(lines ...string) { 28 | for _, line := range lines { 29 | r.AddLineWithCustomTruncate(line, r.truncateStyle) 30 | } 31 | } 32 | 33 | func (r *ContentRenderer) ClearLines() { 34 | r.lines = r.lines[:0] 35 | } 36 | 37 | // Maybe better return an error ? 38 | // AddLineWithCustomTruncate adds lines to the renderer, truncating each line according to the specified style. 39 | // It does not trims whitespace, and its possible to add multiple empty lines using this. 40 | func (r *ContentRenderer) AddLineWithCustomTruncate(lineStr string, truncateStyle TruncateStyle) { 41 | // If string is multiline, add individual lines separately 42 | // We dont use strings.Lines() we need to allow adding empty strings "" as line. 43 | for line := range strings.SplitSeq(lineStr, "\n") { 44 | if len(r.lines) >= r.maxLines { 45 | slog.Error("Max lines reached", "maxLines", r.maxLines) 46 | return 47 | } 48 | // Sanitazation should be done before truncate. Sanitization can increase width 49 | // For ex: Converting problematic unicode nbsp to spaces. 50 | if r.sanitizeContent { 51 | line = common.MakePrintableWithEscCheck(line, true) 52 | } 53 | // Some characters like "\t" are considered 1 width 54 | line = TruncateBasedOnStyle(line, r.maxLineWidth, truncateStyle) 55 | 56 | r.lines = append(r.lines, line) 57 | } 58 | } 59 | 60 | func (r *ContentRenderer) Render() string { 61 | return strings.Join(r.lines, "\n") 62 | } 63 | 64 | func NewContentRenderer(maxLines int, maxLineWidth int, truncateStyle TruncateStyle) ContentRenderer { 65 | return ContentRenderer{ 66 | lines: make([]string, 0), 67 | maxLines: maxLines, 68 | maxLineWidth: maxLineWidth, 69 | truncateStyle: truncateStyle, 70 | sanitizeContent: true, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/internal/ui/rendering/content_renderer_test.go: -------------------------------------------------------------------------------- 1 | package rendering 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestContentRendererBasic(t *testing.T) { 10 | t.Run("Basic test", func(t *testing.T) { 11 | r := NewContentRenderer(6, 5, PlainTruncateRight) 12 | r.AddLines("123456") 13 | r.AddLines("12345\n12345", "123") 14 | assert.Equal(t, 4, r.CntLines()) 15 | r.AddLineWithCustomTruncate("123456", TailsTruncateRight) 16 | r.AddLines("\t1234") 17 | // Should be ignored 18 | r.AddLines("1234") 19 | 20 | res := r.Render() 21 | expected := "12345\n" + 22 | "12345\n" + 23 | "12345\n" + 24 | "123\n" + 25 | "12...\n" + 26 | " 1" 27 | assert.Equal(t, expected, res, "Basic truncation, and adding lines") 28 | 29 | r.ClearLines() 30 | assert.Zero(t, r.CntLines(), "ClearLines should remove all content") 31 | 32 | r.AddLines("\x00\x11\x1babc") 33 | assert.Equal(t, "\x1babc", r.Render()) 34 | 35 | r.sanitizeContent = false 36 | r.ClearLines() 37 | 38 | r.AddLines("\x00\x11\x1babc") 39 | 40 | assert.Equal(t, "\x00\x11\x1babc", r.Render()) 41 | 42 | r = NewContentRenderer(0, 0, PlainTruncateRight) 43 | r.AddLines("L1") 44 | r.AddLines("L2") 45 | assert.Equal(t, "", r.Render()) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/internal/ui/rendering/truncate.go: -------------------------------------------------------------------------------- 1 | package rendering 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/charmbracelet/x/exp/term/ansi" 7 | ) 8 | 9 | type TruncateStyle int 10 | 11 | // These truncate styles must preserve ansi escape codes. If something doesn't preserves 12 | // it shouldn't be here 13 | const ( 14 | PlainTruncateRight = iota 15 | TailsTruncateRight 16 | ) 17 | 18 | func TruncateBasedOnStyle(line string, maxWidth int, truncateStyle TruncateStyle) string { 19 | switch truncateStyle { 20 | case PlainTruncateRight: 21 | return ansi.Truncate(line, maxWidth, "") 22 | case TailsTruncateRight: 23 | return ansi.Truncate(line, maxWidth, "...") 24 | default: 25 | slog.Error("Invalid truncate style", "style", truncateStyle) 26 | return "" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/internal/ui/rendering/truncate_test.go: -------------------------------------------------------------------------------- 1 | package rendering 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTruncate(t *testing.T) { 12 | testStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff")) 13 | testdata := []struct { 14 | name string 15 | line string 16 | maxWidth int 17 | style TruncateStyle 18 | expected string 19 | }{ 20 | { 21 | name: "No truncate", 22 | line: "abc", 23 | maxWidth: 10, 24 | style: PlainTruncateRight, 25 | expected: "abc", 26 | }, 27 | { 28 | name: "Plain truncate", 29 | line: "abcdefgh", 30 | maxWidth: 5, 31 | style: PlainTruncateRight, 32 | expected: "abcde", 33 | }, 34 | { 35 | name: "Tails truncate", 36 | line: "abcdefgh", 37 | maxWidth: 5, 38 | style: TailsTruncateRight, 39 | expected: "ab...", 40 | }, 41 | { 42 | name: "Invalid style", 43 | line: "abcdefgh", 44 | maxWidth: 5, 45 | style: 10, 46 | expected: "", 47 | }, 48 | { 49 | name: "Tails truncate with too less width", 50 | line: "abcdefgh", 51 | maxWidth: 2, 52 | style: TailsTruncateRight, 53 | expected: "", 54 | }, 55 | { 56 | name: "Wide characters", 57 | line: "✅1✅2✅3", 58 | maxWidth: 3, 59 | style: PlainTruncateRight, 60 | expected: "✅1", 61 | }, 62 | { 63 | name: "Wide characters 2", 64 | line: "✅1✅2✅3", 65 | maxWidth: 4, 66 | style: PlainTruncateRight, 67 | expected: "✅1", 68 | }, 69 | { 70 | name: "Wide characters 3", 71 | line: "✅1✅2✅3", 72 | maxWidth: 4, 73 | style: TailsTruncateRight, 74 | expected: "...", 75 | }, 76 | { 77 | name: "Ansi color sequence", 78 | line: testStyle.Render("1234"), 79 | maxWidth: 4, 80 | style: TailsTruncateRight, 81 | expected: testStyle.Render("1..."), 82 | }, 83 | } 84 | for _, tt := range testdata { 85 | t.Run(tt.name, func(t *testing.T) { 86 | assert.Equal(t, tt.expected, TruncateBasedOnStyle(tt.line, tt.maxWidth, tt.style)) 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/internal/ui/sidebar/ReadMe.md: -------------------------------------------------------------------------------- 1 | # sidebar package 2 | This is for the sidebar UI, and for fetching and updating sidebar directories 3 | 4 | # To-dos 5 | - Add missing unit tests 6 | - Separate out implementation of file I/O operations. (Disk listing, Reading and Updating pinned.json) 7 | This package should only be concerned with UI/UX. 8 | - Implementing a proper state transitioning for the sidebar's different modes (normal, search, rename) 9 | - Some methods could be made more pure by reducing side effects 10 | 11 | # Coverage 12 | 13 | ```bash 14 | cd /path/to/ui/sidebar 15 | go test -cover 16 | ``` 17 | Current coverage is 29.3%. -------------------------------------------------------------------------------- /src/internal/ui/sidebar/consts.go: -------------------------------------------------------------------------------- 1 | package sidebar 2 | 3 | // These are effectively consts 4 | // Had to use `var` as go doesn't allows const structs 5 | var pinnedDividerDir = directory{ //nolint: gochecknoglobals // This is more like a const. 6 | Name: "", 7 | Location: "Pinned+-*/=?", 8 | } 9 | 10 | var diskDividerDir = directory{ //nolint: gochecknoglobals // This is more like a const. 11 | Name: "", 12 | Location: "Disks+-*/=?", 13 | } 14 | 15 | // superfile logo + blank line + search bar 16 | const sideBarInitialHeight = 3 17 | -------------------------------------------------------------------------------- /src/internal/ui/sidebar/disk_utils.go: -------------------------------------------------------------------------------- 1 | package sidebar 2 | 3 | import ( 4 | "log/slog" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/shirou/gopsutil/v4/disk" 10 | "github.com/yorukot/superfile/src/internal/utils" 11 | ) 12 | 13 | // Get external media directories 14 | func getExternalMediaFolders() []directory { 15 | // only get physical drives 16 | parts, err := disk.Partitions(false) 17 | 18 | if err != nil { 19 | slog.Error("Error while getting external media: ", "error", err) 20 | return nil 21 | } 22 | var disks []directory 23 | for _, disk := range parts { 24 | // ShouldListDisk, DiskName, and DiskLocation, each has runtime.GOOS checks 25 | // We can ideally reduce it to one check only. 26 | if shouldListDisk(disk.Mountpoint) { 27 | disks = append(disks, directory{ 28 | Name: diskName(disk.Mountpoint), 29 | Location: diskLocation(disk.Mountpoint), 30 | }) 31 | } 32 | } 33 | return disks 34 | } 35 | 36 | func shouldListDisk(mountPoint string) bool { 37 | if runtime.GOOS == utils.OsWindows { 38 | // We need to get C:, D: drive etc in the list 39 | return true 40 | } 41 | 42 | // Should always list the main disk 43 | if mountPoint == "/" { 44 | return true 45 | } 46 | 47 | // Todo : make a configurable field in config.yaml 48 | // excluded_disk_mounts = ["/Volumes/.timemachine"] 49 | // Mountpoints that are in subdirectory of disk_mounts 50 | // but still are to be excluded in disk section of sidebar 51 | if strings.HasPrefix(mountPoint, "/Volumes/.timemachine") { 52 | return false 53 | } 54 | 55 | // We avoid listing all mounted partitions (Otherwise listed disk could get huge) 56 | // but only a few partitions that usually corresponds to external physical devices 57 | // For example : mounts like /boot, /var/ will get skipped 58 | // This can be inaccurate based on your system setup if you mount any external devices 59 | // on other directories, or if you have some extra mounts on these directories 60 | // Todo : make a configurable field in config.yaml 61 | // disk_mounts = ["/mnt", "/media", "/run/media", "/Volumes"] 62 | // Only block devicies that are mounted on these or any subdirectory of these Mountpoints 63 | // Will be shown in disk sidebar 64 | return strings.HasPrefix(mountPoint, "/mnt") || 65 | strings.HasPrefix(mountPoint, "/media") || 66 | strings.HasPrefix(mountPoint, "/run/media") || 67 | strings.HasPrefix(mountPoint, "/Volumes") 68 | } 69 | 70 | func diskName(mountPoint string) string { 71 | // In windows we dont want to use filepath.Base as it returns "\" for when 72 | // mountPoint is any drive root "C:", "D:", etc. Hence causing same name 73 | // for each drive 74 | if runtime.GOOS == utils.OsWindows { 75 | return mountPoint 76 | } 77 | 78 | // This might cause duplicate names in case you mount two devices in 79 | // /mnt/usb and /mnt/dir2/usb . Full mountpoint is a more accurate way 80 | // but that results in messy UI, hence we do this. 81 | return filepath.Base(mountPoint) 82 | } 83 | 84 | func diskLocation(mountPoint string) string { 85 | // In windows if you are in "C:\some\path", "cd C:" will not cd to root of C: drive 86 | // but "cd C:\" will 87 | if runtime.GOOS == utils.OsWindows { 88 | return filepath.Join(mountPoint, "\\") 89 | } 90 | return mountPoint 91 | } 92 | -------------------------------------------------------------------------------- /src/internal/ui/sidebar/render.go: -------------------------------------------------------------------------------- 1 | package sidebar 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/yorukot/superfile/src/internal/ui" 7 | 8 | "github.com/yorukot/superfile/src/config/icon" 9 | "github.com/yorukot/superfile/src/internal/common" 10 | "github.com/yorukot/superfile/src/internal/ui/rendering" 11 | ) 12 | 13 | // Render returns the rendered sidebar string 14 | func (s *Model) Render(mainPanelHeight int, sidebarFocussed bool, currentFilePanelLocation string) string { 15 | if common.Config.SidebarWidth == 0 { 16 | return "" 17 | } 18 | slog.Debug("Rendering sidebar.", "cursor", s.cursor, 19 | "renderIndex", s.renderIndex, "dirs count", len(s.directories), 20 | "sidebar focused", sidebarFocussed) 21 | 22 | r := ui.SidebarRenderer(mainPanelHeight+2, common.Config.SidebarWidth+2, sidebarFocussed) 23 | 24 | r.AddLines(common.SideBarSuperfileTitle, "") 25 | 26 | if s.searchBar.Focused() || s.searchBar.Value() != "" || sidebarFocussed { 27 | r.AddLines(s.searchBar.View()) 28 | } 29 | 30 | if s.NoActualDir() { 31 | r.AddLines(common.SideBarNoneText) 32 | } else { 33 | s.directoriesRender(mainPanelHeight, currentFilePanelLocation, sidebarFocussed, r) 34 | } 35 | return r.Render() 36 | } 37 | 38 | func (s *Model) directoriesRender(mainPanelHeight int, curFilePanelFileLocation string, sideBarFocussed bool, r *rendering.Renderer) { 39 | // Cursor should always point to a valid directory at this point 40 | if s.isCursorInvalid() { 41 | slog.Error("Unexpected situation in sideBar Model. "+ 42 | "Cursor is at invalid position, while there are valid directories", "cursor", s.cursor, 43 | "directory count", len(s.directories)) 44 | } 45 | 46 | // Todo : This is not true when searchbar is not rendered(totalHeight is 2, not 3), 47 | // so we end up underutilizing one line for our render. But it wont break anything. 48 | totalHeight := sideBarInitialHeight 49 | for i := s.renderIndex; i < len(s.directories); i++ { 50 | if totalHeight+s.directories[i].RequiredHeight() > mainPanelHeight { 51 | break 52 | } 53 | 54 | totalHeight += s.directories[i].RequiredHeight() 55 | 56 | switch s.directories[i] { 57 | case pinnedDividerDir: 58 | r.AddLines("", common.SideBarPinnedDivider, "") 59 | case diskDividerDir: 60 | r.AddLines("", common.SideBarDisksDivider, "") 61 | default: 62 | cursor := " " 63 | if s.cursor == i && sideBarFocussed && !s.searchBar.Focused() { 64 | cursor = icon.Cursor 65 | } 66 | if s.renaming && s.cursor == i { 67 | r.AddLines(s.rename.View()) 68 | } else { 69 | renderStyle := common.SidebarStyle 70 | if s.directories[i].Location == curFilePanelFileLocation { 71 | renderStyle = common.SidebarSelectedStyle 72 | } 73 | line := common.FilePanelCursorStyle.Render(cursor+" ") + renderStyle.Render(s.directories[i].Name) 74 | r.AddLineWithCustomTruncate(line, rendering.TailsTruncateRight) 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/internal/ui/sidebar/type.go: -------------------------------------------------------------------------------- 1 | package sidebar 2 | 3 | import "github.com/charmbracelet/bubbles/textinput" 4 | 5 | type directory struct { 6 | Location string `json:"location"` 7 | Name string `json:"name"` 8 | } 9 | 10 | type Model struct { 11 | directories []directory 12 | renderIndex int 13 | cursor int 14 | rename textinput.Model 15 | renaming bool 16 | searchBar textinput.Model 17 | } 18 | -------------------------------------------------------------------------------- /src/internal/ui/sidebar/utils.go: -------------------------------------------------------------------------------- 1 | package sidebar 2 | 3 | func (d directory) IsDivider() bool { 4 | return d == pinnedDividerDir || d == diskDividerDir 5 | } 6 | func (d directory) RequiredHeight() int { 7 | if d.IsDivider() { 8 | return 3 9 | } 10 | return 1 11 | } 12 | 13 | // True if only dividers are in directories slice, 14 | // but no actual directories 15 | // This will be pretty quick. But we can replace it with 16 | // len(s.directories) <= 2 - More hacky and hardcoded-like, but faster 17 | func (s *Model) NoActualDir() bool { 18 | for _, d := range s.directories { 19 | if !d.IsDivider() { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | func (s *Model) isCursorInvalid() bool { 27 | return s.cursor < 0 || s.cursor >= len(s.directories) || s.directories[s.cursor].IsDivider() 28 | } 29 | 30 | func (s *Model) resetCursor() { 31 | s.cursor = 0 32 | // Move to first non Divider dir 33 | for i, d := range s.directories { 34 | if !d.IsDivider() { 35 | s.cursor = i 36 | return 37 | } 38 | } 39 | // If all directories are divider, code will reach here. and s.cursor will stay 0 40 | // Or s.directories is empty 41 | } 42 | 43 | // SearchBarFocused returns whether the search bar is focused 44 | func (s *Model) SearchBarFocused() bool { 45 | return s.searchBar.Focused() 46 | } 47 | 48 | // SearchBarBlur removes focus from the search bar 49 | func (s *Model) SearchBarBlur() { 50 | s.searchBar.Blur() 51 | } 52 | 53 | // SearchBarFocus sets focus on the search bar 54 | func (s *Model) SearchBarFocus() { 55 | s.searchBar.Focus() 56 | } 57 | 58 | // IsRenaming returns whether the sidebar is currently in renaming mode 59 | func (s *Model) IsRenaming() bool { 60 | return s.renaming 61 | } 62 | 63 | // GetCurrentDirectoryLocation returns the location of the currently selected directory 64 | func (s *Model) GetCurrentDirectoryLocation() string { 65 | if s.isCursorInvalid() || s.NoActualDir() { 66 | return "" 67 | } 68 | return s.directories[s.cursor].Location 69 | } 70 | 71 | func (s *Model) pinnedIndexRange() (int, int) { 72 | // pinned directories start after well-known directories and the divider 73 | // Can't use getPinnedDirectories() here, as if we are in search mode, we would be showing 74 | // and having less directories in sideBar.directories slice 75 | 76 | // Todo : This is inefficient to iterate each time for this. 77 | // This information can be kept precomputed 78 | pinnedDividerIdx := -1 79 | diskDividerIdx := -1 80 | for i, d := range s.directories { 81 | if d == pinnedDividerDir { 82 | pinnedDividerIdx = i 83 | } 84 | if d == diskDividerDir { 85 | diskDividerIdx = i 86 | break 87 | } 88 | } 89 | return pinnedDividerIdx + 1, diskDividerIdx - 1 90 | } 91 | -------------------------------------------------------------------------------- /src/internal/utils/ReadMe.md: -------------------------------------------------------------------------------- 1 | # utils package 2 | Independent utilities with zero dependencies with other packages 3 | -------------------------------------------------------------------------------- /src/internal/utils/bool_file_store.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | // This file provides utilities for storing boolean values in a file 10 | 11 | // Read file with "true" / "false" as content. In case of issues, return defaultValue 12 | func ReadBoolFile(path string, defaultValue bool) bool { 13 | data, err := os.ReadFile(path) 14 | if err != nil { 15 | slog.Error("Error in readBoolFile", "path", path, "error", err) 16 | return defaultValue 17 | } 18 | 19 | // Not using strconv.ParseBool() as it allows other values like : "TRUE" 20 | // Using exact string comparison with predefined constants ensures 21 | // consistent behavior and prevents issues with case-insensitivity or 22 | // unexpected values like "yes", "on", etc. that ParseBool would accept 23 | switch string(data) { 24 | case TrueString: 25 | return true 26 | case FalseString: 27 | return false 28 | default: 29 | return defaultValue 30 | } 31 | } 32 | 33 | func WriteBoolFile(path string, value bool) error { 34 | return os.WriteFile(path, []byte(strconv.FormatBool(value)), 0644) 35 | } 36 | -------------------------------------------------------------------------------- /src/internal/utils/consts.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | TrueString = "true" 5 | FalseString = "false" 6 | // These are used while comparing with runtime.GOOS 7 | // OsWindows represents the Windows operating system identifier 8 | OsWindows = "windows" 9 | // OsDarwin represents the macOS (Darwin) operating system identifier 10 | OsDarwin = "darwin" 11 | ) 12 | -------------------------------------------------------------------------------- /src/internal/utils/file_utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/adrg/xdg" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestResolveAbsPath(t *testing.T) { 14 | sep := string(filepath.Separator) 15 | dir1 := "abc" 16 | dir2 := "def" 17 | 18 | absPrefix := "" 19 | if runtime.GOOS == "windows" { 20 | absPrefix = "C:" // Windows absolute path prefix 21 | } 22 | root := absPrefix + sep 23 | 24 | testdata := []struct { 25 | name string 26 | cwd string 27 | path string 28 | expectedRes string 29 | }{ 30 | { 31 | name: "Path cleaup Test 1", 32 | cwd: absPrefix + sep, 33 | path: absPrefix + strings.Repeat(sep, 10), 34 | expectedRes: absPrefix + sep, 35 | }, 36 | { 37 | name: "Basic test", 38 | cwd: filepath.Join(root, dir1), 39 | path: dir2, 40 | expectedRes: filepath.Join(root, dir1, dir2), 41 | }, 42 | { 43 | name: "Ignore cwd for abs path", 44 | cwd: filepath.Join(root, dir1), 45 | path: filepath.Join(root, dir2), 46 | expectedRes: filepath.Join(root, dir2), 47 | }, 48 | { 49 | name: "Path cleanup Test 2", 50 | cwd: absPrefix + strings.Repeat(sep, 4) + dir1, 51 | path: "." + sep + "." + sep + dir2, 52 | expectedRes: filepath.Join(root, dir1, dir2), 53 | }, 54 | { 55 | name: "Basic test with ~", 56 | cwd: root, 57 | path: "~", 58 | expectedRes: xdg.Home, 59 | }, 60 | { 61 | name: "~ should not be resolved if not first", 62 | cwd: dir1, 63 | path: filepath.Join(dir2, "~"), 64 | expectedRes: filepath.Join(dir1, dir2, "~"), 65 | }, 66 | } 67 | 68 | for _, tt := range testdata { 69 | t.Run(tt.name, func(t *testing.T) { 70 | assert.Equal(t, tt.expectedRes, ResolveAbsPath(tt.cwd, tt.path)) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/internal/utils/fzf_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/reinhrst/fzf-lib" 4 | 5 | // Returning a string slice causes inefficiency in current usage 6 | func FzfSearch(query string, source []string) []fzf.MatchResult { 7 | fzfSearcher := fzf.New(source, fzf.DefaultOptions()) 8 | fzfSearcher.Search(query) 9 | // Todo : This is a blocking call, which will cause the UI to freeze if the query is slow. 10 | // Need to put a timeout on this 11 | fzfResults := <-fzfSearcher.GetResultChannel() 12 | fzfSearcher.End() 13 | return fzfResults.Matches 14 | } 15 | -------------------------------------------------------------------------------- /src/internal/utils/log_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | ) 8 | 9 | // Print line to stderr and exit with status 1 10 | // Cannot use log.Fataln() as slog.SetDefault() causes those lines to 11 | // go into log file 12 | func PrintlnAndExit(args ...any) { 13 | fmt.Fprintln(os.Stderr, args...) 14 | os.Exit(1) 15 | } 16 | 17 | // Print formatted output line to stderr and exit with status 1 18 | // Cannot use log.Fataln() as slog.SetDefault() causes those lines to 19 | // go into log file 20 | func PrintfAndExit(format string, args ...any) { 21 | fmt.Fprintf(os.Stderr, format, args...) 22 | os.Exit(1) 23 | } 24 | 25 | // Used in unit test 26 | func SetRootLoggerToStdout(debug bool) { 27 | level := slog.LevelInfo 28 | if debug { 29 | level = slog.LevelDebug 30 | } 31 | slog.SetDefault(slog.New(slog.NewTextHandler( 32 | os.Stdout, &slog.HandlerOptions{Level: level}))) 33 | } 34 | 35 | // Used in unit test 36 | func SetRootLoggerToDiscarded() { 37 | slog.SetDefault(slog.New(slog.DiscardHandler)) 38 | } 39 | -------------------------------------------------------------------------------- /src/internal/utils/shell_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "os/exec" 9 | "runtime" 10 | "time" 11 | ) 12 | 13 | // Choose correct shell as per OS 14 | func ExecuteCommandInShell(timeLimit time.Duration, cmdDir string, shellCommand string) (int, string, error) { 15 | // Linux and Darwin 16 | baseCmd := "/bin/sh" 17 | args := []string{"-c", shellCommand} 18 | 19 | if runtime.GOOS == OsWindows { 20 | baseCmd = "powershell.exe" 21 | args[0] = "-Command" 22 | } 23 | 24 | return ExecuteCommand(timeLimit, cmdDir, baseCmd, args...) 25 | } 26 | 27 | func ExecuteCommand(timeLimit time.Duration, cmdDir string, baseCmd string, args ...string) (int, string, error) { 28 | ctx, cancel := context.WithTimeout(context.Background(), timeLimit) 29 | defer cancel() 30 | 31 | cmd := exec.CommandContext(ctx, baseCmd, args...) 32 | cmd.Dir = cmdDir 33 | outputBytes, err := cmd.CombinedOutput() 34 | retCode := -1 35 | 36 | if errors.Is(ctx.Err(), context.DeadlineExceeded) { 37 | slog.Error("User's command timed out", "outputBytes", outputBytes, 38 | "cmd error", err, "ctx error", ctx.Err()) 39 | return retCode, string(outputBytes), ctx.Err() 40 | } 41 | 42 | if err == nil { 43 | retCode = 0 44 | } else if exitErr, ok := err.(*exec.ExitError); ok { //nolint: errorlint // We dont expect error to be Wrapped here, so we are using type assertion not errors.As 45 | retCode = exitErr.ExitCode() 46 | } else { 47 | err = fmt.Errorf("unexpected Error in command execution : %w", err) 48 | } 49 | 50 | return retCode, string(outputBytes), err 51 | } 52 | -------------------------------------------------------------------------------- /src/internal/utils/tea_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import tea "github.com/charmbracelet/bubbletea" 4 | 5 | func TeaRuneKeyMsg(msg string) tea.KeyMsg { 6 | return tea.KeyMsg{ 7 | Type: tea.KeyRunes, 8 | Runes: []rune(msg), 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/internal/utils/ui_utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // We have three panels, so 6 characters for border 4 | // <---><---><---> 5 | // Hence we have (fullWidth - 6) / 3 = fullWidth/3 - 2 6 | func FooterWidth(fullWidth int) int { 7 | return fullWidth/3 - 2 8 | } 9 | 10 | // Including borders 11 | func FullFooterHeight(footerHeight int, toggleFooter bool) int { 12 | if toggleFooter { 13 | return footerHeight + 2 14 | } 15 | return 0 16 | } 17 | -------------------------------------------------------------------------------- /src/internal/wheel_function.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/yorukot/superfile/src/internal/common" 7 | ) 8 | 9 | func wheelMainAction(msg string, m *model) { 10 | slog.Debug("wheelMainAction called", "msg", msg, "focusPanel", m.focusPanel) 11 | var action func() 12 | switch msg { 13 | case "wheel up": 14 | switch m.focusPanel { 15 | case sidebarFocus: 16 | action = func() { m.sidebarModel.ListUp(m.mainPanelHeight) } 17 | case processBarFocus: 18 | action = func() { m.processBarModel.listUp(m.footerHeight) } 19 | case metadataFocus: 20 | action = func() { m.fileMetaData.listUp() } 21 | case nonePanelFocus: 22 | action = func() { m.fileModel.filePanels[m.filePanelFocusIndex].listUp(m.mainPanelHeight) } 23 | } 24 | 25 | case "wheel down": 26 | switch m.focusPanel { 27 | case sidebarFocus: 28 | action = func() { m.sidebarModel.ListDown(m.mainPanelHeight) } 29 | case processBarFocus: 30 | action = func() { m.processBarModel.listDown(m.footerHeight) } 31 | case metadataFocus: 32 | action = func() { m.fileMetaData.listDown() } 33 | case nonePanelFocus: 34 | action = func() { m.fileModel.filePanels[m.filePanelFocusIndex].listDown(m.mainPanelHeight) } 35 | } 36 | default: 37 | slog.Error("Unexpected type of mouse action in wheelMainAction", "msg", msg) 38 | return 39 | } 40 | 41 | for range common.WheelRunTime { 42 | action() 43 | } 44 | 45 | if m.focusPanel == nonePanelFocus { 46 | m.fileMetaData.renderIndex = 0 47 | go func() { 48 | m.returnMetaData() 49 | }() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pkg/file_preview/auto.go: -------------------------------------------------------------------------------- 1 | package filepreview 2 | -------------------------------------------------------------------------------- /src/pkg/file_preview/pdf_preview.go: -------------------------------------------------------------------------------- 1 | package filepreview 2 | -------------------------------------------------------------------------------- /src/pkg/file_preview/utils.go: -------------------------------------------------------------------------------- 1 | package filepreview 2 | -------------------------------------------------------------------------------- /src/superfile_config/config.toml: -------------------------------------------------------------------------------- 1 | # More details are at https://superfile.netlify.app/configure/superfile-config/ 2 | # 3 | # change your theme 4 | theme = 'catppuccin' 5 | # 6 | # The editor files will be opened with. (Leave blank to use the EDITOR environment variable). 7 | editor = "" 8 | # 9 | # The editor directories will be opened with. (Leave blank to use the default editors). 10 | dir_editor = "" 11 | # 12 | # Auto check for update 13 | auto_check_update = true 14 | # 15 | # Cd on quit (For more details, please check out https://superfile.netlify.app/configure/superfile-config/#cd_on_quit) 16 | cd_on_quit = false 17 | # 18 | # Whether to open file preview automatically every time superfile is opened. 19 | default_open_file_preview = true 20 | # 21 | # Whether to show image preview 22 | show_image_preview = true 23 | # 24 | # 25 | # Whether to hide additional footer info for file panel. 26 | show_panel_footer_info = true 27 | # 28 | # The path of the first file panel when superfile is opened. 29 | default_directory = "." 30 | # 31 | # Display file sizes using powers of 1000 (kB, MB, GB) instead of powers of 1024 (KiB, MiB, GiB). 32 | file_size_use_si = false 33 | # 34 | # Default sort type (0: Name, 1: Size, 2: Date Modified, 3: Type). 35 | default_sort_type = 0 36 | # 37 | # Default sort order (false: Ascending, true: Descending). 38 | sort_order_reversed = false 39 | # 40 | # Case sensitive sort by name (upper "B" comes before lower "a" if true). 41 | case_sensitive_sort = false 42 | # 43 | # Whether to exit the shell on successful command execution. 44 | shell_close_on_success = false 45 | # 46 | # Whether to enable debug mode. 47 | debug = false 48 | # 49 | # ================ Style ================= 50 | # 51 | # Whether to use the builtin syntax highlighting with chroma or use bat. Values: "" for builtin chroma, "bat" for bat 52 | code_previewer = '' 53 | # 54 | # If you don't have or don't want Nerdfont installed you can turn this off 55 | nerdfont = true 56 | # 57 | # Set transparent background or not (this only work when your terminal background is transparent) 58 | transparent_background = false 59 | # 60 | # File preview width allow '0' (this mean same as file panel),'x' x must be from 2 to 10 (This means that the width of the file preview will be one xth of the total width.) 61 | file_preview_width = 0 62 | # 63 | # The length of the sidebar. If you don't want to display the sidebar, you can input 0 directly. If you want to display the value, please place it in the range of 3-20. 64 | sidebar_width = 20 65 | # 66 | # Border style 67 | # Make sure to add strings exactly one character wide. Use ' ' for borderless 68 | border_top = '─' 69 | border_bottom = '─' 70 | border_left = '│' 71 | border_right = '│' 72 | border_top_left = '╭' 73 | border_top_right = '╮' 74 | border_bottom_left = '╰' 75 | border_bottom_right = '╯' 76 | border_middle_left = '├' 77 | border_middle_right = '┤' 78 | # 79 | # ==========PLUGINS========== # 80 | # 81 | # Show more detailed metadata, please install exiftool before enabling this plugin! 82 | metadata = false 83 | # 84 | # Enable MD5 checksum generation for files 85 | enable_md5_checksum = false 86 | -------------------------------------------------------------------------------- /src/superfile_config/hotkeys.toml: -------------------------------------------------------------------------------- 1 | # ================================================================================================= 2 | # Global hotkeys (cannot conflict with other hotkeys) 3 | confirm = ['enter', 'right', 'l'] 4 | quit = ['q', 'esc'] 5 | 6 | # movement 7 | list_up = ['up', 'k'] 8 | list_down = ['down', 'j'] 9 | page_up = ['pgup',''] 10 | page_down = ['pgdown',''] 11 | # file panel control 12 | create_new_file_panel = ['n', ''] 13 | close_file_panel = ['w', ''] 14 | next_file_panel = ['tab', 'L'] 15 | previous_file_panel = ['shift+left', 'H'] 16 | toggle_file_preview_panel = ['f', ''] 17 | open_sort_options_menu = ['o', ''] 18 | toggle_reverse_sort = ['R', ''] 19 | # change focus 20 | focus_on_process_bar = ['p', ''] 21 | focus_on_sidebar = ['s', ''] 22 | focus_on_metadata = ['m', ''] 23 | # create file/directory and rename 24 | file_panel_item_create = ['ctrl+n', ''] 25 | file_panel_item_rename = ['ctrl+r', ''] 26 | # file operations 27 | copy_items = ['ctrl+c', ''] 28 | cut_items = ['ctrl+x', ''] 29 | paste_items = ['ctrl+v', 'ctrl+w', ''] 30 | delete_items = ['ctrl+d', 'delete', ''] 31 | # compress and extract 32 | extract_file = ['ctrl+e', ''] 33 | compress_file = ['ctrl+a', ''] 34 | # editor 35 | open_file_with_editor = ['e', ''] 36 | open_current_directory_with_editor = ['E', ''] 37 | # other 38 | pinned_directory = ['P', ''] 39 | toggle_dot_file = ['.', ''] 40 | change_panel_mode = ['v', ''] 41 | open_help_menu = ['?', ''] 42 | open_command_line = [':', ''] 43 | open_spf_prompt = ['>', ''] 44 | copy_path = ['ctrl+p', ''] 45 | copy_present_working_directory = ['c', ''] 46 | toggle_footer = ['F', ''] 47 | # ================================================================================================= 48 | # Typing hotkeys (can conflict with all hotkeys) 49 | confirm_typing = ['enter', ''] 50 | cancel_typing = ['ctrl+c', 'esc'] 51 | # ================================================================================================= 52 | # Normal mode hotkeys (can conflict with other modes, cannot conflict with global hotkeys) 53 | parent_directory = ['h', 'left', 'backspace'] 54 | search_bar = ['/', ''] 55 | # ================================================================================================= 56 | # Select mode hotkeys (can conflict with other modes, cannot conflict with global hotkeys) 57 | file_panel_select_mode_items_select_down = ['shift+down', 'J'] 58 | file_panel_select_mode_items_select_up = ['shift+up', 'K'] 59 | file_panel_select_all_items = ['A', ''] 60 | -------------------------------------------------------------------------------- /src/superfile_config/theme/0x96f.toml: -------------------------------------------------------------------------------- 1 | # 0x96f 2 | # Theme create by: https://github.com/filipjanevski 3 | 4 | code_syntax_highlight = "monokai" 5 | 6 | # ========= Border ========= 7 | file_panel_border = "#757075" 8 | sidebar_border = "#757075" 9 | footer_border = "#757075" 10 | 11 | # ========= Border Active ========= 12 | file_panel_border_active = "#ffca58" 13 | sidebar_border_active = "#baebf6" 14 | footer_border_active = "#ffca58" 15 | modal_border_active = "#ffca58" 16 | 17 | # ========= Background (bg) ========= 18 | full_screen_bg = "#262427" 19 | file_panel_bg = "#262427" 20 | sidebar_bg = "#262427" 21 | footer_bg = "#262427" 22 | modal_bg = "#262427" 23 | 24 | # ========= Foreground (fg) ========= 25 | full_screen_fg = "#fcfcfc" 26 | file_panel_fg = "#fcfcfc" 27 | sidebar_fg = "#fcfcfc" 28 | footer_fg = "#fcfcfc" 29 | modal_fg = "#fcfcfc" 30 | 31 | # ========= Special Color ========= 32 | cursor = "#ffca58" 33 | correct = "#c6e472" 34 | error = "#ff8787" 35 | hint = "#baebf6" 36 | cancel = "#fcfcfc" 37 | # Gradient color can only have two colors! 38 | gradient_color = ["#ff7272", "#bcdf59"] 39 | 40 | # ========= File Panel Special Items ========= 41 | file_panel_top_directory_icon = "#64d2e8" 42 | file_panel_top_path = "#fcfcfc" 43 | file_panel_item_selected_fg = "#c6e472" 44 | file_panel_item_selected_bg = "#262427" 45 | 46 | # ========= Sidebar Special Items ========= 47 | sidebar_title = "#64d2e8" 48 | sidebar_item_selected_fg = "#c6e472" 49 | sidebar_item_selected_bg = "#262427" 50 | sidebar_divider = "#757075" 51 | 52 | # ========= Modal Special Items ========= 53 | modal_cancel_fg = "#262427" 54 | modal_cancel_bg = "#ff8787" 55 | 56 | modal_confirm_fg = "#262427" 57 | modal_confirm_bg = "#bcdf59" 58 | 59 | # ========= Help Menu ========= 60 | help_menu_hotkey = "#ff8787" 61 | help_menu_title = "#64d2e8" 62 | -------------------------------------------------------------------------------- /src/superfile_config/theme/ayu-dark.toml: -------------------------------------------------------------------------------- 1 | # Ayu Dark 2 | # Theme create by: https://github.com/rustnomicon 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "ayu-dark" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#242936" 14 | sidebar_border = "#1f2430" 15 | footer_border = "#242936" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#ffcc66" 19 | sidebar_border_active = "#ff7733" 20 | footer_border_active = "#aad94c" 21 | modal_border_active = "#5c6773" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#0F1419" 25 | file_panel_bg = "#0F1419" 26 | sidebar_bg = "#0F1419" 27 | footer_bg = "#0F1419" 28 | modal_bg = "#0F1419" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#B3B1AD" 32 | file_panel_fg = "#B3B1AD" 33 | sidebar_fg = "#B3B1AD" 34 | footer_fg = "#B3B1AD" 35 | modal_fg = "#B3B1AD" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#FFCC66" 39 | correct = "#AAD94C" 40 | error = "#FF3333" 41 | hint = "#36A3D9" 42 | cancel = "#F07178" 43 | # Gradient color can only have two colors! 44 | gradient_color = ["#FFB454", "#36A3D9"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#AAD94C" 48 | file_panel_top_path = "#FFCC66" 49 | file_panel_item_selected_fg = "#36A3D9" 50 | file_panel_item_selected_bg = "#1F2430" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#36A3D9" 54 | sidebar_item_selected_fg = "#36A3D9" 55 | sidebar_item_selected_bg = "#1F2430" 56 | sidebar_divider = "#242936" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#0F1419" 60 | modal_cancel_bg = "#F07178" 61 | 62 | modal_confirm_fg = "#0F1419" 63 | modal_confirm_bg = "#36A3D9" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#36A3D9" 67 | help_menu_title = "#F07178" 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/blood.toml: -------------------------------------------------------------------------------- 1 | # Blood 2 | # Theme create by: https://github.com/charlesrocket 3 | # Update by(sort by time): 4 | 5 | # Thank you! 6 | 7 | code_syntax_highlight = "onedark" 8 | 9 | # border 10 | file_panel_border = "#9a0000" 11 | sidebar_border = "#790000" 12 | footer_border = "#790000" 13 | 14 | # border active 15 | file_panel_border_active = "#ff0000" 16 | sidebar_border_active = "#ff0000" 17 | footer_border_active = "#ff0000" 18 | modal_border_active = "#ff0000" 19 | 20 | # background (bg) 21 | full_screen_bg = "#000000" 22 | file_panel_bg = "#000000" 23 | sidebar_bg = "#000000" 24 | footer_bg = "#000000" 25 | modal_bg = "#000000" 26 | 27 | # foreground (fg) 28 | full_screen_fg = "#f8f8f2" 29 | file_panel_fg = "#f8f8f2" 30 | sidebar_fg = "#f8f8f2" 31 | footer_fg = "#f8f8f2" 32 | modal_fg = "#f8f8f2" 33 | 34 | # special color 35 | cursor = "#ff0000" 36 | correct = "#47ef7d" 37 | error = "#d70000" 38 | hint = "#5bd9f3" 39 | cancel = "#6575ab" 40 | gradient_color = ["#720000", "#ff0000"] 41 | 42 | # file panel special items 43 | file_panel_top_directory_icon = "#ff522e" 44 | file_panel_top_path = "#ff9999" 45 | file_panel_item_selected_fg = "#ff8d34" 46 | file_panel_item_selected_bg = "#524549" 47 | 48 | # sidebar special items 49 | sidebar_title = "#dd0000" 50 | sidebar_item_selected_fg = "#000000" 51 | sidebar_item_selected_bg = "#ff8d34" 52 | sidebar_divider = "#615250" 53 | 54 | # modal special items 55 | modal_cancel_fg = "#f9f9fe" 56 | modal_cancel_bg = "#000042" 57 | 58 | modal_confirm_fg = "#f9f9fe" 59 | modal_confirm_bg = "#ffb86c" 60 | 61 | # help menu 62 | help_menu_hotkey = "#ff8d34" 63 | help_menu_title = "#ff6666" 64 | -------------------------------------------------------------------------------- /src/superfile_config/theme/catppuccin-frappe.toml: -------------------------------------------------------------------------------- 1 | # Catppuccin Frappe Flavor 2 | # Theme create by: https://github.com/GV14982 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-frappe" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#737994" # Overlay0 14 | sidebar_border = "#303446" # Base 15 | footer_border = "#737994" # Overlay0 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#babbf1" # Lavendar 19 | sidebar_border_active = "#e78284" # Red 20 | footer_border_active = "#a6d189" # Green 21 | modal_border_active = "#949cbb" # Overlay2 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#303446" # Base 25 | file_panel_bg = "#303446" # Base 26 | sidebar_bg = "#303446" # Base 27 | footer_bg = "#303446" # Base 28 | modal_bg = "#303446" # Base 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#a5adce" # Subtext0 32 | file_panel_fg = "#a5adce" # Subtext0 33 | sidebar_fg = "#a5adce" # Subtext0 34 | footer_fg = "#a5adce" # Subtext0 35 | modal_fg = "#a5adce" # Subtext0 36 | 37 | # ========= Special Color ========= 38 | cursor = "#f2d5cf" # Rosewater 39 | correct = "#a6d189" # Green 40 | error = "#e78284" # Red 41 | hint = "#85c1dc" # Sapphire 42 | cancel = "#ea999c" # Maroon 43 | # Gradient color can only have two color! 44 | gradient_color = ["#8caaee", "#ca9ee6"] # [Blue, Mauve] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#a6d189" # Green 48 | file_panel_top_path = "#89b5fa" # Blue 49 | file_panel_item_selected_fg = "#99d1db" # Sky 50 | file_panel_item_selected_bg = "#303446" # Base 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#85c1dc" # Sapphire 54 | sidebar_item_selected_fg = "#99d1db" # Sky 55 | sidebar_item_selected_bg = "#303446" # Base 56 | sidebar_divider = "#949cbb" # Overlay2 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#414559" # Surface0 60 | modal_cancel_bg = "#ea999c" # Maroon 61 | 62 | modal_confirm_fg = "#414559" # Surface0 63 | modal_confirm_bg = "#99d1db" # Sky 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#99d1db" # Sky 67 | help_menu_title = "#ea999c" # Maroon 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/catppuccin-latte.toml: -------------------------------------------------------------------------------- 1 | # Catppuccin Latte Flavor 2 | # Theme create by: https://github.com/GV14982 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-latte" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#9ca0b0" # Overlay0 14 | sidebar_border = "#eff1f5" # Base 15 | footer_border = "#9ca0b0" # Overlay0 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#7287fd" # Lavender 19 | sidebar_border_active = "#40a02b" # Green 20 | footer_border_active = "#40a02b" # Green 21 | modal_border_active = "#7c7f93" # Overlay2 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#eff1f5" # Base 25 | file_panel_bg = "#eff1f5" # Base 26 | sidebar_bg = "#eff1f5" # Base 27 | footer_bg = "#eff1f5" # Base 28 | modal_bg = "#eff1f5" # Base 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#4c4f69" # Text 32 | file_panel_fg = "#4c4f69" # Text 33 | sidebar_fg = "#4c4f69" # Text 34 | footer_fg = "#4c4f69" # Text 35 | modal_fg = "#4c4f69" # Text 36 | 37 | # ========= Special Color ========= 38 | cursor = "#dc8a78" # Rosewater 39 | correct = "#40a02b" # Green 40 | error = "#d20f39" # Red 41 | hint = "#209fb5" # Sapphire 42 | cancel = "#e64553" # Maroon 43 | # Gradient color can only have two color! 44 | gradient_color = ["#1e66f5", "#ca9ee6"] # [Blue, Mauve] 45 | directory_icon_color = "#444444" # Darker shade of grey 46 | 47 | # ========= File Panel Special Items ========= 48 | file_panel_top_directory_icon = "#40a02b" # Green 49 | file_panel_top_path = "#89b5fa" # Blue 50 | file_panel_item_selected_fg = "#04a5e5" # Sky 51 | file_panel_item_selected_bg = "#eff1f5" # Base 52 | 53 | # ========= Sidebar Special Items ========= 54 | sidebar_title = "#209fb5" # Sapphire 55 | sidebar_item_selected_fg = "#04a5e5" # Sky 56 | sidebar_item_selected_bg = "#eff1f5" # Base 57 | sidebar_divider = "#7c7f93" # Overlay2 58 | 59 | # ========= Modal Special Items ========= 60 | modal_cancel_fg = "#eff1f5" # Base 61 | modal_cancel_bg = "#e64553" # Maroon 62 | 63 | modal_confirm_fg = "#eff1f5" # Base 64 | modal_confirm_bg = "#04a5e5" # Sky 65 | 66 | # ========= Help Menu ========= 67 | help_menu_hotkey = "#04a5e5" # Sky 68 | help_menu_title = "#fe640b" # Peach 69 | -------------------------------------------------------------------------------- /src/superfile_config/theme/catppuccin-macchiato.toml: -------------------------------------------------------------------------------- 1 | # Catppuccin Macchiato Flavor 2 | # Theme create by: https://github.com/GV14982 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-macchiato" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#6e738d" # Overlay0 14 | sidebar_border = "#24273a" # Base 15 | footer_border = "#6e738d" # Overlay0 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#b7bdf8" # Lavendar 19 | sidebar_border_active = "#ed8796" # Red 20 | footer_border_active = "#a6da95" # Green 21 | modal_border_active = "#939ab7" # Overlay2 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#24273a" # Base 25 | file_panel_bg = "#24273a" # Base 26 | sidebar_bg = "#24273a" # Base 27 | footer_bg = "#24273a" # Base 28 | modal_bg = "#24273a" # Base 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#a5adcb" # Subtext0 32 | file_panel_fg = "#a5adcb" # Subtext0 33 | sidebar_fg = "#a5adcb" # Subtext0 34 | footer_fg = "#a5adcb" # Subtext0 35 | modal_fg = "#a5adcb" # Subtext0 36 | 37 | # ========= Special Color ========= 38 | cursor = "#f4dbd6" # Rosewater 39 | correct = "#a6da95" # Green 40 | error = "#ed8796" # Red 41 | hint = "#7dc4e4" # Sapphire 42 | cancel = "#ee99a0" # Maroon 43 | # Gradient color can only have two color! 44 | gradient_color = ["#8aadf4", "#c6a0f6"] # [Blue, Mauve] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#a6da95" # Green 48 | file_panel_top_path = "#8aadf4" # Blue 49 | file_panel_item_selected_fg = "#91d7e3" # Sky 50 | file_panel_item_selected_bg = "#24273a" # Base 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#7dc4e4" # Sapphire 54 | sidebar_item_selected_fg = "#91d7e3" # Sky 55 | sidebar_item_selected_bg = "#24273a" # Base 56 | sidebar_divider = "#939ab7" # Overlay2 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#363a4f" # Surface0 60 | modal_cancel_bg = "#ee99a0" # Maroon 61 | 62 | modal_confirm_fg = "#363a4f" # Surface0 63 | modal_confirm_bg = "#91d7e3" # Sky 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#91d7e3" # Sky 67 | help_menu_title = "#ee99a0" # Maroon 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/catppuccin.toml: -------------------------------------------------------------------------------- 1 | # Catppuccin 2 | # Theme create by: https://github.com/AnshumanNeon 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-mocha" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#6c7086" 14 | sidebar_border = "#1e1e2e" 15 | footer_border = "#6c7086" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#b4befe" 19 | sidebar_border_active = "#f38ba8" 20 | footer_border_active = "#a6e3a1" 21 | modal_border_active = "#868686" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#1e1e2e" 25 | file_panel_bg = "#1e1e2e" 26 | sidebar_bg = "#1e1e2e" 27 | footer_bg = "#1e1e2e" 28 | modal_bg = "#1e1e2e" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#a6adc8" 32 | file_panel_fg = "#a6adc8" 33 | sidebar_fg = "#a6adc8" 34 | footer_fg = "#a6adc8" 35 | modal_fg = "#a6adc8" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#f5e0dc" 39 | correct = "#a6e3a1" 40 | error = "#f38ba8" 41 | hint = "#73c7ec" 42 | cancel = "#eba0ac" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#89b4fa", "#cba6f7"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#a6e3a1" 48 | file_panel_top_path = "#89b5fa" 49 | file_panel_item_selected_fg = "#98D0FD" 50 | file_panel_item_selected_bg = "#1e1e2e" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#74c7ec" 54 | sidebar_item_selected_fg = "#A6DBF7" 55 | sidebar_item_selected_bg = "#1e1e2e" 56 | sidebar_divider = "#868686" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#383838" 60 | modal_cancel_bg = "#eba0ac" 61 | 62 | modal_confirm_fg = "#383838" 63 | modal_confirm_bg = "#89dceb" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#89dceb" 67 | help_menu_title = "#eba0ac" 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/dracula.toml: -------------------------------------------------------------------------------- 1 | # Dracula 2 | # Theme create by: https://github.com/BeanieBarrow 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "dracula" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#6272a4" 14 | sidebar_border = "#282a36" 15 | footer_border = "#6272a4" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#44475a" 19 | sidebar_border_active = "#44475a" 20 | footer_border_active = "#44475a" 21 | modal_border_active = "#44475a" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#282a36" 25 | file_panel_bg = "#282a36" 26 | sidebar_bg = "#282a36" 27 | footer_bg = "#282a36" 28 | modal_bg = "#282a36" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#f8f8f2" 32 | file_panel_fg = "#f8f8f2" 33 | sidebar_fg = "#f8f8f2" 34 | footer_fg = "#f8f8f2" 35 | modal_fg = "#f8f8f2" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#ff79c6" 39 | correct = "#50fa7b" 40 | error = "#ff5555" 41 | hint = "#8be9fd" 42 | cancel = "#6272a4" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#50fa7b", "#ff5555"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#50fa7b" 48 | file_panel_top_path = "#8be9fd" 49 | file_panel_item_selected_fg = "#ffb86c" 50 | file_panel_item_selected_bg = "#282a36" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#bd93f9" 54 | sidebar_item_selected_fg = "#ffb86c" 55 | sidebar_item_selected_bg = "#282a36" 56 | sidebar_divider = "#868686" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#f8f8f2" 60 | modal_cancel_bg = "#6272a4" 61 | 62 | modal_confirm_fg = "#f8f8f2" 63 | modal_confirm_bg = "#ffb86c" 64 | 65 | 66 | # ========= Help Menu ========= 67 | help_menu_hotkey = "#ffb86c" 68 | help_menu_title = "#bd93f9" -------------------------------------------------------------------------------- /src/superfile_config/theme/everforest-dark-medium.toml: -------------------------------------------------------------------------------- 1 | # Everforest Dark Medium 2 | # Theme create by: https://github.com/dotintegral 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-macchiato" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#859289" 14 | sidebar_border = "#2D353B" 15 | footer_border = "#859289" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#FFF1C5" 19 | sidebar_border_active = "#DBBC7F" 20 | footer_border_active = "#DBBC7F" 21 | modal_border_active = "#859289" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#2D353B" 25 | file_panel_bg = "#2D353B" 26 | sidebar_bg = "#2D353B" 27 | footer_bg = "#2D353B" 28 | modal_bg = "#2D353B" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#D3C6AA" 32 | file_panel_fg = "#D3C6AA" 33 | sidebar_fg = "#D3C6AA" 34 | footer_fg = "#D3C6AA" 35 | modal_fg = "#D3C6AA" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#A7C080" 39 | correct = "#A7C080" 40 | error = "#E67E80" 41 | hint = "#7FBBB3" 42 | cancel = "#859289" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#A7C080", "#E67E80"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#A7C080" 48 | file_panel_top_path = "#7FBBB3" 49 | file_panel_item_selected_fg = "#D699B6" 50 | file_panel_item_selected_bg = "#232A2E" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#D699B6" 54 | sidebar_item_selected_fg = "#E69875" 55 | sidebar_item_selected_bg = "#2D353B" 56 | sidebar_divider = "#859289" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#D3C6AA" 60 | modal_cancel_bg = "#232A2E" 61 | 62 | modal_confirm_fg = "#D3C6AA" 63 | modal_confirm_bg = "#E69875" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#A7C080" 67 | help_menu_title = "#E69875" 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/gruvbox-dark-hard.toml: -------------------------------------------------------------------------------- 1 | # Gruvbox Dark Hard 2 | # Theme create by: https://github.com/frost-phoenix 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "gruvbox" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#FBF1C7" 14 | sidebar_border = "#928374" 15 | footer_border = "#928374" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#98971A" 19 | sidebar_border_active = "#B16286" 20 | footer_border_active = "#D79921" 21 | modal_border_active = "#689D6A" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#1D2021" 25 | file_panel_bg = "#1D2021" 26 | sidebar_bg = "#1D2021" 27 | footer_bg = "#1D2021" 28 | modal_bg = "#1D2021" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#FBF1C7" 32 | file_panel_fg = "#FBF1C7" 33 | sidebar_fg = "#FBF1C7" 34 | footer_fg = "#FBF1C7" 35 | modal_fg = "#FBF1C7" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#689D6A" 39 | correct = "#98971A" 40 | error = "#FF6969" 41 | hint = "#468588" 42 | cancel = "#838383" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#FB4934", "#B8BB26"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#689D6A" 48 | file_panel_top_path = "#458588" 49 | file_panel_item_selected_fg = "#D65D0E" 50 | file_panel_item_selected_bg = "" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#B16286" 54 | sidebar_item_selected_fg = "#D65D0E" 55 | sidebar_item_selected_bg = "" 56 | sidebar_divider = "#928374" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#FB4934" 60 | modal_cancel_bg = "" 61 | 62 | modal_confirm_fg = "#B8BB26" 63 | modal_confirm_bg = "" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#689D6A" 67 | help_menu_title = "#B16286" 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/gruvbox.toml: -------------------------------------------------------------------------------- 1 | # Gruvbox 2 | # Theme create by: https://github.com/yorukot 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "gruvbox" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#868686" 14 | sidebar_border = "#282828" 15 | footer_border = "#868686" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#FFF1C5" 19 | sidebar_border_active = "#D79921" 20 | footer_border_active = "#D79921" 21 | modal_border_active = "#868686" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#282828" 25 | file_panel_bg = "#282828" 26 | sidebar_bg = "#282828" 27 | footer_bg = "#282828" 28 | modal_bg = "#282828" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#EBDBB2" 32 | file_panel_fg = "#EBDBB2" 33 | sidebar_fg = "#EBDBB2" 34 | footer_fg = "#EBDBB2" 35 | modal_fg = "#EBDBB2" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#8EC07C" 39 | correct = "#8ec07c" 40 | error = "#FF6969" 41 | hint = "#468588" 42 | cancel = "#838383" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#689d6a", "#fb4934"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#8EC07C" 48 | file_panel_top_path = "#458588" 49 | file_panel_item_selected_fg = "#D3869B" 50 | file_panel_item_selected_bg = "#282828" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#CC241D" 54 | sidebar_item_selected_fg = "#E8751A" 55 | sidebar_item_selected_bg = "#282828" 56 | sidebar_divider = "#868686" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#EBDBB2" 60 | modal_cancel_bg = "#6D6D6D" 61 | 62 | modal_confirm_fg = "#EBDBB2" 63 | modal_confirm_bg = "#FF4D00" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#8EC07C" 67 | help_menu_title = "#FF4D00" -------------------------------------------------------------------------------- /src/superfile_config/theme/hacks.toml: -------------------------------------------------------------------------------- 1 | # Hacks 2 | # Theme create by: https://github.com/charlesrocket 3 | # Update by(sort by time): 4 | 5 | # Thank you! 6 | 7 | code_syntax_highlight = "onedark" 8 | 9 | # border 10 | file_panel_border = "#afff00" 11 | sidebar_border = "#afff00" 12 | footer_border = "#afff00" 13 | 14 | # border active 15 | file_panel_border_active = "#6532ff" 16 | sidebar_border_active = "#6532ff" 17 | footer_border_active = "#6532ff" 18 | modal_border_active = "#6532ff" 19 | 20 | # background (bg) 21 | full_screen_bg = "#000000" 22 | file_panel_bg = "#000000" 23 | sidebar_bg = "#000000" 24 | footer_bg = "#000000" 25 | modal_bg = "#000000" 26 | 27 | # foreground (fg) 28 | full_screen_fg = "#f8f8f2" 29 | file_panel_fg = "#f8f8f2" 30 | sidebar_fg = "#f8f8f2" 31 | footer_fg = "#f8f8f2" 32 | modal_fg = "#f8f8f2" 33 | 34 | # special color 35 | cursor = "#ff0000" 36 | correct = "#47ef7d" 37 | error = "#d70000" 38 | hint = "#5bd9f3" 39 | cancel = "#6575ab" 40 | gradient_color = ["#00ff00", "#afff00"] 41 | 42 | # file panel special items 43 | file_panel_top_directory_icon = "#afff00" 44 | file_panel_top_path = "#afff00" 45 | file_panel_item_selected_fg = "#ff8d34" 46 | file_panel_item_selected_bg = "#524549" 47 | 48 | # sidebar special items 49 | sidebar_title = "#afff00" 50 | sidebar_item_selected_fg = "#000000" 51 | sidebar_item_selected_bg = "#ff8d34" 52 | sidebar_divider = "#615250" 53 | 54 | # modal special items 55 | modal_cancel_fg = "#f9f9fe" 56 | modal_cancel_bg = "#000042" 57 | 58 | modal_confirm_fg = "#f9f9fe" 59 | modal_confirm_bg = "#ffb86c" 60 | 61 | # help menu 62 | help_menu_hotkey = "#ff8d34" 63 | help_menu_title = "#afff00" 64 | -------------------------------------------------------------------------------- /src/superfile_config/theme/kaolin.toml: -------------------------------------------------------------------------------- 1 | # Kaolin 2 | # Theme create by: https://github.com/AnshumanNeon 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-macchiato" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#74b09a" 14 | sidebar_border = "#17171a" 15 | footer_border = "#74b09a" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#74b09a" 19 | sidebar_border_active = "#57b2c2" 20 | footer_border_active = "#57b2c2" 21 | modal_border_active = "#868686" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#17171a" 25 | file_panel_bg = "#17171a" 26 | sidebar_bg = "#17171a" 27 | footer_bg = "#17171a" 28 | modal_bg = "#17171a" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#efefef" 32 | file_panel_fg = "#efefef" 33 | sidebar_fg = "#efefef" 34 | footer_fg = "#efefef" 35 | modal_fg = "#efefef" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#f5c791" 39 | correct = "#74b09a" 40 | error = "#c74a4d" 41 | hint = "#4fa8a3" 42 | cancel = "#d7936d" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#74b09a", "#c74a4d"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#f5c791" 48 | file_panel_top_path = "#d7936d" 49 | file_panel_item_selected_fg = "#4fa8a3" 50 | file_panel_item_selected_bg = "#17171a" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#f5c791" 54 | sidebar_item_selected_fg = "#ba667d" 55 | sidebar_item_selected_bg = "#17171a" 56 | sidebar_divider = "#868686" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#eedcc1" 60 | modal_cancel_bg = "#c74a4d" 61 | 62 | modal_confirm_fg = "#eedcc1" 63 | modal_confirm_bg = "#3e594a" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#4fa8a3" 67 | help_menu_title = "#f5c791" -------------------------------------------------------------------------------- /src/superfile_config/theme/monokai.toml: -------------------------------------------------------------------------------- 1 | # OneDark 2 | # Theme create by: https://github.com/CommandJoo 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "monokai" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#75715E" # Overlay0 14 | footer_border = "#75715E" # Overlay0 15 | sidebar_border = "#75715E" # Base 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#66D9EF" # Lavendar 19 | footer_border_active = "#A9DC76" # Green 20 | modal_border_active = "#66D9EF" # Overlay2 21 | sidebar_border_active = "#F92672" # Red 22 | 23 | # ========= Background (bg) ========= 24 | file_panel_bg = "#272822" # Base 25 | footer_bg = "#272822" # Base 26 | full_screen_bg = "#272822" # Base 27 | modal_bg = "#272822" # Base 28 | sidebar_bg = "#272822" # Base 29 | 30 | # ========= Foreground (fg) ========= 31 | file_panel_fg = "#F8F8F2" # Subtext0 32 | footer_fg = "#F8F8F2" # Subtext0 33 | full_screen_fg = "#F8F8F2" # Subtext0 34 | modal_fg = "#F8F8F2" # Subtext0 35 | sidebar_fg = "#F8F8F2" # Subtext0 36 | 37 | # ========= Special Color ========= 38 | cancel = "#E6DB74" # Maroon 39 | correct = "#A6E22E" # Green 40 | cursor = "#66D9EF" # Rosewater 41 | error = "#F92672" # Red 42 | hint = "#66D9EF" # Sapphire 43 | # Gradient color can only have two color! 44 | gradient_color = ["#66D9EF", "#AE81FF"] # [Blue, Mauve] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_item_selected_bg = "#2E2E2E" # Base 48 | file_panel_item_selected_fg = "#66D9EF" # Sky 49 | file_panel_top_directory_icon = "#E6DB74" # Green 50 | file_panel_top_path = "#E6DB74" # Blue 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_divider = "#75715E" # Overlay2 54 | sidebar_item_selected_bg = "#272822" # Base 55 | sidebar_item_selected_fg = "#66D9EF" # Sky 56 | sidebar_title = "#66D9EF" # Sapphire 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_bg = "#F92672" # Maroon 60 | modal_cancel_fg = "#75715E" # Surface0 61 | 62 | modal_confirm_bg = "#66D9EF" # Sky 63 | modal_confirm_fg = "#75715E" # Surface0 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#66D9EF" # Sky 67 | help_menu_title = "#AE81FF" # Maroon 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/nord.toml: -------------------------------------------------------------------------------- 1 | # Nord 2 | # Theme create by: https://github.com/rames-eltany 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "nord" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#4c566a" 14 | sidebar_border = "#2e3440" 15 | footer_border = "#4c566a" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#d8dee9" 19 | sidebar_border_active = "#b48ead" 20 | footer_border_active = "#b48ead" 21 | modal_border_active = "#868686" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#2e3440" 25 | file_panel_bg = "#2e3440" 26 | sidebar_bg = "#2e3440" 27 | footer_bg = "#2e3440" 28 | modal_bg = "#2e3440" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#e5e9f0" 32 | file_panel_fg = "#e5e9f0" 33 | sidebar_fg = "#e5e9f0" 34 | footer_fg = "#e5e9f0" 35 | modal_fg = "#e5e9f0" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#88c0d0" 39 | correct = "#88c0d0" 40 | error = "#bf616a" 41 | hint = "#8fbcbb" 42 | cancel = "#d8dee9" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#81a1c1", "#bf616a"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#88c0d0" 48 | file_panel_top_path = "#88c0d0" 49 | file_panel_item_selected_fg = "#bf616a" 50 | file_panel_item_selected_bg = "#2e3440" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#81a1c1" 54 | sidebar_item_selected_fg = "#88c0d0" 55 | sidebar_item_selected_bg = "#2e3440" 56 | sidebar_divider = "#868686" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#e5e9f0" 60 | modal_cancel_bg = "#4c566a" 61 | 62 | modal_confirm_fg = "#e5e9f0" 63 | modal_confirm_bg = "#bf616a" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#8fbcbb" 67 | help_menu_title = "#81a1c1" -------------------------------------------------------------------------------- /src/superfile_config/theme/onedark.toml: -------------------------------------------------------------------------------- 1 | # OneDark 2 | # Theme create by: https://github.com/CommandJoo 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make sidebar border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "onedark" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#737994" # Overlay0 14 | sidebar_border = "#737994" # Base 15 | footer_border = "#737994" # Overlay0 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#57A5E5" # Lavendar 19 | sidebar_border_active = "#DE5D68" # Red 20 | footer_border_active = "#8FB573" # Green 21 | modal_border_active = "#51A8B3" # Overlay2 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#2C2D31" # Base 25 | file_panel_bg = "#232326" # Base 26 | sidebar_bg = "#232326" # Base 27 | footer_bg = "#232326" # Base 28 | modal_bg = "#35363B" # Base 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#A7AAB0" # Subtext0 32 | file_panel_fg = "#A7AAB0" # Subtext0 33 | sidebar_fg = "#A7AAB0" # Subtext0 34 | footer_fg = "#A7AAB0" # Subtext0 35 | modal_fg = "#A7AAB0" # Subtext0 36 | 37 | # ========= Special Color ========= 38 | cursor = "#68AEE8" # Rosewater 39 | correct = "#a6d189" # Green 40 | error = "#DE5D68" # Red 41 | hint = "#68AEE8" # Sapphire 42 | cancel = "#C49060" # Maroon 43 | # Gradient color can only have two color! 44 | gradient_color = ["#68AEE8", "#BB70D2"] # [Blue, Mauve] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#DBB671" # Green 48 | file_panel_top_path = "#DBB671" # Blue 49 | file_panel_item_selected_fg = "#51A8B3" # Sky 50 | file_panel_item_selected_bg = "#2C2D31" # Base 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#57A5E5" # Sapphire 54 | sidebar_item_selected_fg = "#51A8B3" # Sky 55 | sidebar_item_selected_bg = "#2C2D31" # Base 56 | sidebar_divider = "#818387" # Overlay2 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#414559" # Surface0 60 | modal_cancel_bg = "#DE5D68" # Maroon 61 | 62 | modal_confirm_fg = "#414559" # Surface0 63 | modal_confirm_bg = "#51A8B3" # Sky 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#51A8B3" # Sky 67 | help_menu_title = "#BB70D2" # Maroon 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/poimandres.toml: -------------------------------------------------------------------------------- 1 | # Poimandres 2 | # Theme create by: https://github.com/Myles-J 3 | # Update by(sort by time): 4 | # - Update code_syntax_highlight(I couldn't find a matching theme so I'm using this one for now.) - github.com/yorukot 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-mocha" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#6c7086" 14 | sidebar_border = "#2a303c" 15 | footer_border = "#6c7086" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#7fbbb3" 19 | sidebar_border_active = "#7fbbb3" 20 | footer_border_active = "#7fbbb3" 21 | modal_border_active = "#7fbbb3" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#1b1d24" 25 | file_panel_bg = "#1b1d24" 26 | sidebar_bg = "#1b1d24" 27 | footer_bg = "#1b1d24" 28 | modal_bg = "#1b1d24" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#cdd6f4" 32 | file_panel_fg = "#cdd6f4" 33 | sidebar_fg = "#cdd6f4" 34 | footer_fg = "#cdd6f4" 35 | modal_fg = "#cdd6f4" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#74c7ec" 39 | correct = "#a6e3a1" 40 | error = "#f38ba8" 41 | hint = "#89b4fa" 42 | cancel = "#6c7086" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#a6e3a1", "#f38ba8"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#a6e3a1" 48 | file_panel_top_path = "#89b4fa" 49 | file_panel_item_selected_fg = "#a6e3a1" 50 | file_panel_item_selected_bg = "#2a303c" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#cba6f7" 54 | sidebar_item_selected_fg = "#a6e3a1" 55 | sidebar_item_selected_bg = "#2a303c" 56 | sidebar_divider = "#6c7086" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#cdd6f4" 60 | modal_cancel_bg = "#6c7086" 61 | 62 | modal_confirm_fg = "#cdd6f4" 63 | modal_confirm_bg = "#4a4e69" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#a6e3a1" 67 | help_menu_title = "#cba6f7" 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/rose-pine.toml: -------------------------------------------------------------------------------- 1 | # Rose Pine 2 | # Theme create by: https://github.com/pearcidar 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "rose-pine" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#403d52" 14 | sidebar_border = "#191724" 15 | footer_border = "#403d52" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#6e6e86" 19 | sidebar_border_active = "#c4a7e7" 20 | footer_border_active = "#f6c177" 21 | modal_border_active = "#868686" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#191724" 25 | file_panel_bg = "#191724" 26 | sidebar_bg = "#191724" 27 | footer_bg = "#191724" 28 | modal_bg = "#191724" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#e0def4" 32 | file_panel_fg = "#e0def4" 33 | sidebar_fg = "#e0def4" 34 | footer_fg = "#e0def4" 35 | modal_fg = "#e0def4" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#9ccfd8" 39 | correct = "#8ec07c" 40 | error = "#ff6969" 41 | hint = "#31784f" 42 | cancel = "#838383" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#31784f", "#eb6f92"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#9ccfd8" 48 | file_panel_top_path = "#ebbcba" 49 | file_panel_item_selected_fg = "#c4a7e7" 50 | file_panel_item_selected_bg = "#191724" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#6e6e86" 54 | sidebar_item_selected_fg = "#f6c177" 55 | sidebar_item_selected_bg = "#191724" 56 | sidebar_divider = "#868686" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#e0def4" 60 | modal_cancel_bg = "#524f67" 61 | 62 | modal_confirm_fg = "#e0def4" 63 | modal_confirm_bg = "#eb6f92" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#f6c177" 67 | help_menu_title = "#9ccfd8" -------------------------------------------------------------------------------- /src/superfile_config/theme/sugarplum.toml: -------------------------------------------------------------------------------- 1 | # Sugarplum 2 | # Theme create by: https://github.com/lemonlime0x3C33 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-macchiato" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#a175d4" 14 | sidebar_border = "a175d4" 15 | footer_border = "#a175d4" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#53aaa1" 19 | sidebar_border_active = "#53aaa1" 20 | footer_border_active = "#53aaa1" 21 | modal_border_active = "#53aaa1" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#111147" 25 | file_panel_bg = "#111147" 26 | sidebar_bg = "#111147" 27 | footer_bg = "#111147" 28 | modal_bg = "#111147" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#db7ddd" 32 | file_panel_fg = "#db7ddd" 33 | sidebar_fg = "#d0beee" 34 | footer_fg = "#5ca8dc" 35 | modal_fg = "#98c7a3" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#53b397" 39 | correct = "#524094" 40 | error = "#2082a6" 41 | hint = "#91d4c2" 42 | cancel = "#b53dff" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#249a84", "#5ca8dc"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#249a84" 48 | file_panel_top_path = "#249a84" 49 | file_panel_item_selected_fg = "#53b397" 50 | file_panel_item_selected_bg = "#53b397" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#db7ddd" 54 | sidebar_item_selected_fg = "#249a84" 55 | sidebar_item_selected_bg = "#111147" 56 | sidebar_divider = "#565f89" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#7c4094" 60 | modal_cancel_bg = "#7c4094" 61 | 62 | modal_confirm_fg = "#7c4094" 63 | modal_confirm_bg = "#7c4094" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#7dcfff" 67 | help_menu_title = "#73daca" 68 | -------------------------------------------------------------------------------- /src/superfile_config/theme/tokyonight.toml: -------------------------------------------------------------------------------- 1 | # Tokyonight 2 | # Theme create by: https://github.com/pearcidar 3 | # Update by(sort by time): 4 | # 5 | # Thanks for all contributor!! 6 | 7 | # If you want to make border display just set it same as sidebar background color 8 | 9 | # Code syntax highlight theme (you can go to https://github.com/alecthomas/chroma/blob/master/styles to find one you like) 10 | code_syntax_highlight = "catppuccin-macchiato" 11 | 12 | # ========= Border ========= 13 | file_panel_border = "#414868" 14 | sidebar_border = "#24283b" 15 | footer_border = "#414868" 16 | 17 | # ========= Border Active ========= 18 | file_panel_border_active = "#b4befe" 19 | sidebar_border_active = "#f7768e" 20 | footer_border_active = "#73daca" 21 | modal_border_active = "#73daca" 22 | 23 | # ========= Background (bg) ========= 24 | full_screen_bg = "#1a1b26" 25 | file_panel_bg = "#1a1b26" 26 | sidebar_bg = "#1a1b26" 27 | footer_bg = "#1a1b26" 28 | modal_bg = "#1a1b26" 29 | 30 | # ========= Foreground (fg) ========= 31 | full_screen_fg = "#a9b1d6" 32 | file_panel_fg = "#a9b1d6" 33 | sidebar_fg = "#a9b1d6" 34 | footer_fg = "#a9b1d6" 35 | modal_fg = "#a9b1d6" 36 | 37 | # ========= Special Color ========= 38 | cursor = "#ff9e64" 39 | correct = "#9ece6a" 40 | error = "#f7768e" 41 | hint = "#7dcfff" 42 | cancel = "#ff9e64" 43 | # Gradient color can only have two color! 44 | gradient_color = ["#7aa2f7", "#bb9af7"] 45 | 46 | # ========= File Panel Special Items ========= 47 | file_panel_top_directory_icon = "#73daca" 48 | file_panel_top_path = "#7aa2f7" 49 | file_panel_item_selected_fg = "#2ac3de" 50 | file_panel_item_selected_bg = "#1a1b26" 51 | 52 | # ========= Sidebar Special Items ========= 53 | sidebar_title = "#73daca" 54 | sidebar_item_selected_fg = "#7dcfff" 55 | sidebar_item_selected_bg = "#1a1b26" 56 | sidebar_divider = "#565f89" 57 | 58 | # ========= Modal Special Items ========= 59 | modal_cancel_fg = "#24383b" 60 | modal_cancel_bg = "#e0af68" 61 | 62 | modal_confirm_fg = "#24283b" 63 | modal_confirm_bg = "#9ece6a" 64 | 65 | # ========= Help Menu ========= 66 | help_menu_hotkey = "#7dcfff" 67 | help_menu_title = "#73daca" 68 | -------------------------------------------------------------------------------- /src/superfile_config/vimHotkeys.toml: -------------------------------------------------------------------------------- 1 | # This is maintain by github.com/nonepork 2 | # I know this is not really that "vim", but the control flow is different. 3 | # ================================================================================================= 4 | # Global hotkeys (cannot conflict with other hotkeys) 5 | confirm = ['enter', ''] 6 | quit = ['ctrl+c', ''] # also know as, theprimeagen troller 7 | # movement 8 | list_up = ['k', ''] 9 | list_down = ['j', ''] 10 | page_up = ['pgup',''] 11 | page_down = ['pgdown',''] 12 | # file panel control 13 | create_new_file_panel = ['n', ''] 14 | close_file_panel = ['q', ''] 15 | next_file_panel = ['tab', ''] 16 | previous_file_panel = ['shift+tab', ''] 17 | toggle_file_preview_panel = ['f', ''] 18 | open_sort_options_menu = ['o', ''] 19 | toggle_reverse_sort = ['R', ''] 20 | # change focus 21 | focus_on_process_bar = ['ctrl+p', ''] 22 | focus_on_sidebar = ['ctrl+s', ''] 23 | focus_on_metadata = ['ctrl+d', ''] 24 | # create file/directory and rename 25 | file_panel_item_create = ['a', ''] 26 | file_panel_item_rename = ['r', ''] 27 | # file operations 28 | copy_items = ['y', ''] 29 | cut_items = ['x', ''] 30 | paste_items = ['p', ''] 31 | delete_items = ['d', ''] 32 | # compress and extract 33 | extract_file = ['ctrl+e', ''] 34 | compress_file = ['ctrl+a', ''] 35 | # editor 36 | open_file_with_editor = ['e', ''] 37 | open_current_directory_with_editor = ['E', ''] 38 | # other 39 | pinned_directory = ['P', ''] 40 | toggle_dot_file = ['.', ''] 41 | change_panel_mode = ['m', ''] 42 | open_help_menu = ['?', ''] 43 | open_command_line = [':', ''] 44 | copy_path = ['Y', ''] 45 | copy_present_working_directory = ['c', ''] 46 | toggle_footer = ['ctrl+f', ''] 47 | # ================================================================================================= 48 | # Typing hotkeys (can conflict with all hotkeys) 49 | confirm_typing = ['enter', ''] 50 | cancel_typing = ['esc', ''] 51 | # ================================================================================================= 52 | # Normal mode hotkeys (can conflict with other modes, cannot conflict with global hotkeys) 53 | parent_directory = ['-', ''] 54 | search_bar = ['/', ''] 55 | # ================================================================================================= 56 | # Select mode hotkeys (can conflict with other modes, cannot conflict with global hotkeys) 57 | file_panel_select_mode_items_select_down = ['J', ''] 58 | file_panel_select_mode_items_select_up = ['K', ''] 59 | file_panel_select_all_items = ['A', ''] 60 | -------------------------------------------------------------------------------- /testsuite/.gitignore: -------------------------------------------------------------------------------- 1 | # python venv site packages 2 | site-packages/ 3 | 4 | #python venvs 5 | .venv/ 6 | 7 | # python pycache 8 | __pycache__/ 9 | *.pyc -------------------------------------------------------------------------------- /testsuite/ReadMe.md: -------------------------------------------------------------------------------- 1 | ## Coding style rules 2 | - Prefer using strong typing 3 | - Prefer using type hinting for the first time the variable is declared, and for functions paremeters and return types 4 | - Use `-> None` to explicitly indicate no return value 5 | 6 | ### Ideas 7 | - Recommended to integrate your IDE with PEP8 to highlight PEP8 violations in real-time 8 | - Enforcing PEP8 via `pylint flake8 pycodestyle` and via pre commit hooks 9 | 10 | ## Writing New testcases 11 | - Just create a file ending with `_test.py` in `tests` directory 12 | - Any subclass of BaseTest with name ending with `Test` will be executed 13 | - see `run_tests` and `get_testcases` in `core/runner.py` for more info 14 | 15 | ## Setup 16 | Requires python 3.9 or later. 17 | 18 | ## Setup for MacOS / Linux 19 | 20 | ### Install tmux 21 | - You need to have tmux installed. See https://github.com/tmux/tmux/wiki 22 | 23 | ### Python virtual env setup 24 | ``` 25 | # cd to this directory 26 | cd 27 | python3 -m venv .venv 28 | .venv/bin/pip install --upgrade pip 29 | .venv/bin/pip install -r requirements.txt 30 | ``` 31 | 32 | ### Make sure you build spf 33 | ``` 34 | # cd to the superfile repo root (parent of this) 35 | cd 36 | ./build.sh 37 | ``` 38 | 39 | ### Running testsuite 40 | ``` 41 | .venv/bin/python3 main.py 42 | ``` 43 | ## Setup for Windows 44 | Coming soon. 45 | 46 | 47 | 48 | ### Python virtual env setup 49 | ``` 50 | # cd to this directory 51 | cd 52 | python3 -m venv .venv 53 | .venv\Scripts\python -m pip install --upgrade pip 54 | .venv\Scripts\pip -r requirements.txt 55 | ``` 56 | 57 | ### Make sure you build spf 58 | ``` 59 | # cd to the superfile repo root (parent of this) 60 | cd 61 | go build -o bin/spf.exe 62 | ``` 63 | 64 | ### Running testsuite 65 | Notes 66 | - You must keep your focus on the terminal for the entire duration of test run. `pyautogui` sends keypress to process on focus. 67 | 68 | ``` 69 | .venv\Scripts\python main.py 70 | ``` 71 | 72 | ## Tips while running tests 73 | - Use `-d` or `--debug` to enable debug logs during test run. 74 | - If you see flakiness in test runs due to superfile being still open, consider using `--close-wait-time` options to increase wait time for superfile to close. Note : For now we have enforing superfile to close within a specific time window in tests to reduce test flakiness 75 | - Make sure that your hotkeys are set to default hotkeys. Tests use default hotkeys for now. 76 | - Use `-t` or `--tests` to only run specific tests 77 | - Example `python main.py -d -t RenameTest CopyTest` 78 | - If you see `libtmux` errors like `libtmux.exc.LibTmuxException: ['no server running on /private/tmp/tmux-501/superfile']` Make sure your python version is up to date -------------------------------------------------------------------------------- /testsuite/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/testsuite/core/__init__.py -------------------------------------------------------------------------------- /testsuite/core/environment.py: -------------------------------------------------------------------------------- 1 | from core.spf_manager import BaseSPFManager 2 | from core.fs_manager import TestFSManager 3 | 4 | class Environment: 5 | """Manage test environment 6 | Manage cleanup of environment and other stuff at a single place 7 | """ 8 | def __init__(self, spf_manager : BaseSPFManager, fs_manager : TestFSManager ): 9 | self.spf_mgr = spf_manager 10 | self.fs_mgr = fs_manager 11 | 12 | def cleanup(self) -> None: 13 | self.spf_mgr.close_spf() 14 | self.fs_mgr.cleanup() -------------------------------------------------------------------------------- /testsuite/core/fs_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from tempfile import TemporaryDirectory 3 | from pathlib import Path 4 | import os 5 | from io import StringIO 6 | 7 | class TestFSManager: 8 | """Manage the temporary files for test and the cleanup 9 | """ 10 | def __init__(self): 11 | self.logger = logging.getLogger() 12 | self.logger.debug("Initialized %s", self.__class__.__name__) 13 | self.temp_dir_obj = TemporaryDirectory() 14 | self.temp_dir = Path(self.temp_dir_obj.name) 15 | 16 | def abspath(self, relative_path : Path) -> Path: 17 | return self.temp_dir / relative_path 18 | 19 | def check_exists(self, relative_path : Path) -> bool: 20 | return self.abspath(relative_path).exists() 21 | 22 | def read_file(self, relative_path: Path) -> str: 23 | content = "" 24 | try: 25 | with open(self.abspath(relative_path), 'r', encoding="utf-8") as f: 26 | content = f.read() 27 | except FileNotFoundError: 28 | self.logger.error("File not found: %s", relative_path) 29 | except PermissionError: 30 | self.logger.error("Permission denied when reading file: %s", relative_path) 31 | return content 32 | 33 | def makedirs(self, relative_path : Path) -> None: 34 | # Overloaded '/' operator 35 | os.makedirs(self.temp_dir / relative_path, exist_ok=True) 36 | 37 | def create_file(self, relative_path : Path, data : str = "") -> None: 38 | """Create files 39 | Make sure directories exist 40 | Args: 41 | relative_path (Path): Relative path from test root 42 | """ 43 | with open(self.temp_dir / relative_path, 'w', encoding="utf-8") as f: 44 | f.write(data) 45 | 46 | def tree(self, relative_root : Path = None) -> str: 47 | if relative_root is None: 48 | root = self.temp_dir 49 | else: 50 | root = self.temp_dir / relative_root 51 | res = StringIO() 52 | for item in root.rglob('*'): 53 | path_str = str(item.relative_to(root)) 54 | if item.is_dir(): 55 | res.write(f"D-{path_str}\n") 56 | else: 57 | res.write(f"F-{path_str}\n") 58 | return res.getvalue() 59 | 60 | def cleanup(self) -> None: 61 | """Cleaup the temporary directory 62 | Its okay to forget it though, it will be cleaned on program exit then. 63 | """ 64 | self.temp_dir_obj.cleanup() 65 | 66 | def __repr__(self) -> str: 67 | return f"{self.__class__.__name__}(temp_dir = {self.temp_dir})" 68 | -------------------------------------------------------------------------------- /testsuite/core/keys.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | import platform 3 | 4 | class Keys(ABC): 5 | def __init__(self, ascii_code : int): 6 | self.ascii_code = ascii_code 7 | 8 | def __repr__(self) -> str: 9 | return f"Key(code={self.ascii_code})" 10 | 11 | # Will isinstance of Keys work for object of CtrlKeys ? 12 | class CtrlKeys(Keys): 13 | def __init__(self, char : str): 14 | # Only allowing single alphabetic character 15 | # assert is good here as all objects are defined statically 16 | assert len(char) == 1 17 | assert char.isalpha() and char.islower() 18 | self.char = char 19 | # Ctrl + A starts at 1 20 | super().__init__(ord(char) - ord('a') + 1) 21 | 22 | # Maybe have keycode 23 | class SpecialKeys(Keys): 24 | def __init__(self, ascii_code : int, key_name : str): 25 | super().__init__(ascii_code) 26 | self.key_name = key_name 27 | 28 | 29 | 30 | KEY_CTRL_A : Keys = CtrlKeys('a') 31 | KEY_CTRL_C : Keys = CtrlKeys('c') 32 | KEY_CTRL_E : Keys = CtrlKeys('e') 33 | KEY_CTRL_D : Keys = CtrlKeys('d') 34 | KEY_CTRL_M : Keys = CtrlKeys('m') 35 | KEY_CTRL_P : Keys = CtrlKeys('p') 36 | KEY_CTRL_R : Keys = CtrlKeys('r') 37 | KEY_CTRL_V : Keys = CtrlKeys('v') 38 | KEY_CTRL_W : Keys = CtrlKeys('w') 39 | KEY_CTRL_X : Keys = CtrlKeys('x') 40 | 41 | # Platform specific keys 42 | KEY_PASTE : Keys = KEY_CTRL_V 43 | if platform.system() == "Windows" : 44 | KEY_PASTE = KEY_CTRL_W 45 | 46 | # See https://vimdoc.sourceforge.net/htmldoc/digraph.html#digraph-table for key codes 47 | # If keyname is not the same string as key code in pyautogui, need to handle separately 48 | KEY_BACKSPACE : Keys = SpecialKeys(8 , "Backspace") 49 | KEY_ENTER : Keys = SpecialKeys(13, "Enter") 50 | KEY_ESC : Keys = SpecialKeys(27, "Esc") 51 | KEY_DELETE : Keys = SpecialKeys(127 , "Delete") 52 | 53 | 54 | NO_ASCII = -1 55 | 56 | # Some keys dont have ascii codes, they have to be handled separately 57 | # Make sure key name is the same string as key code for Tmux 58 | KEY_DOWN : Keys = SpecialKeys(NO_ASCII, "Down") 59 | KEY_UP : Keys = SpecialKeys(NO_ASCII, "Up") 60 | KEY_LEFT : Keys = SpecialKeys(NO_ASCII, "Left") 61 | KEY_RIGHT : Keys = SpecialKeys(NO_ASCII, "Right") 62 | -------------------------------------------------------------------------------- /testsuite/core/pyautogui_manager.py: -------------------------------------------------------------------------------- 1 | import time 2 | import subprocess 3 | import pyautogui 4 | import core.keys as keys 5 | from core.spf_manager import BaseSPFManager 6 | 7 | class PyAutoGuiSPFManager(BaseSPFManager): 8 | """Manage SPF via subprocesses and pyautogui 9 | Cross platform, but it globally takes over the input, so you need the terminal 10 | constantly on focus during test run 11 | """ 12 | SPF_START_DELAY : float = 0.5 13 | def __init__(self, spf_path : str): 14 | super().__init__(spf_path) 15 | self.spf_process = None 16 | 17 | 18 | def start_spf(self, start_dir : str = None, args : list[str] = None) -> None: 19 | spf_args = [self.spf_path] 20 | if args : 21 | spf_args += args 22 | spf_args.append(start_dir) 23 | 24 | self.spf_process = subprocess.Popen(spf_args, 25 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 26 | time.sleep(PyAutoGuiSPFManager.SPF_START_DELAY) 27 | 28 | # Need to send a sample keypress otherwise it ignores first keypress 29 | self.send_text_input('x') 30 | 31 | 32 | def send_text_input(self, text : str, all_at_once : bool = False) -> None: 33 | if all_at_once : 34 | pyautogui.write(text) 35 | else: 36 | for c in text: 37 | pyautogui.write(c) 38 | 39 | def send_special_input(self, key : keys.Keys) -> None: 40 | if isinstance(key, keys.CtrlKeys): 41 | pyautogui.hotkey('ctrl', key.char) 42 | elif isinstance(key, keys.SpecialKeys): 43 | pyautogui.press(key.key_name.lower()) 44 | else: 45 | raise Exception(f"Unknown key : {key}") 46 | 47 | def get_rendered_output(self) -> str: 48 | return "[Not supported yet]" 49 | 50 | 51 | def is_spf_running(self) -> bool: 52 | self._is_spf_running = (self.spf_process is not None) and (self.spf_process.poll() is None) 53 | return self._is_spf_running 54 | 55 | def close_spf(self) -> None: 56 | if self.spf_process is not None: 57 | self.spf_process.terminate() 58 | 59 | # Override 60 | def runtime_info(self) -> str: 61 | if self.spf_process is None: 62 | return "[No process]" 63 | else: 64 | return f"[PID : {self.spf_process.pid}, poll : {self.spf_process.poll()}]" 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /testsuite/core/spf_manager.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | import core.keys as keys 3 | 4 | class BaseSPFManager(ABC): 5 | 6 | def __init__(self, spf_path : str): 7 | self.spf_path = spf_path 8 | # _ denotes the internal variables, anyone should not directly read/modify 9 | self._is_spf_running : bool = False 10 | 11 | @abstractmethod 12 | def start_spf(self, start_dir : str = None, args : list[str] = None) -> None: 13 | pass 14 | 15 | @abstractmethod 16 | def send_text_input(self, text : str, all_at_once : bool = False) -> None: 17 | pass 18 | 19 | @abstractmethod 20 | def send_special_input(self, key : keys.Keys) -> None: 21 | pass 22 | 23 | @abstractmethod 24 | def get_rendered_output(self) -> str: 25 | pass 26 | 27 | 28 | @abstractmethod 29 | def is_spf_running(self) -> bool: 30 | """ 31 | We allow using _is_spf_running variable for efficiency 32 | But this method should give the true state, although this might have some calculations 33 | """ 34 | return self._is_spf_running 35 | 36 | @abstractmethod 37 | def close_spf(self) -> None: 38 | """ 39 | Close spf if its running and cleanup any other resources 40 | """ 41 | 42 | def runtime_info(self) -> str: 43 | return "[No runtime info]" 44 | 45 | -------------------------------------------------------------------------------- /testsuite/core/test_constants.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | FILE_TEXT1 : str = "This is a sample Text\n" 4 | 5 | KEY_DELAY : float = 0.05 # seconds 6 | OPERATION_DELAY : float = 0.3 # seconds 7 | 8 | # 0.3 second was too less for windows 9 | # 0.5 second Github workflow failed for with superfile is still running errors 10 | CLOSE_WAIT_TIME : float = 0.5 # seconds 11 | 12 | # Platform specific consts 13 | FILE_CREATE_COMMAND : str = "touch" 14 | if platform.system() == "Windows" : 15 | FILE_CREATE_COMMAND = "ni" 16 | -------------------------------------------------------------------------------- /testsuite/core/tmux_manager.py: -------------------------------------------------------------------------------- 1 | import libtmux 2 | import time 3 | import logging 4 | import core.keys as keys 5 | from core.spf_manager import BaseSPFManager 6 | 7 | class TmuxSPFManager(BaseSPFManager): 8 | """ 9 | Tmux based Manager 10 | After running spf, you can connect to the session via 11 | tmux -L superfile attach -t spf_session 12 | Wont work in windows 13 | """ 14 | # Class variables 15 | SPF_START_DELAY : float = 0.1 # seconds 16 | SPF_SOCKET_NAME : str = "superfile" 17 | 18 | # Init should not allocate any resources 19 | def __init__(self, spf_path : str): 20 | super().__init__(spf_path) 21 | self.logger = logging.getLogger() 22 | self.server = libtmux.Server(socket_name=TmuxSPFManager.SPF_SOCKET_NAME) 23 | self.logger.debug("server object : %s", self.server) 24 | self.spf_session : libtmux.Session = None 25 | self.spf_pane : libtmux.Pane = None 26 | 27 | def start_spf(self, start_dir : str = None, args : list[str] = None) -> None: 28 | spf_command = self.spf_path 29 | if args: 30 | spf_command += " " + " ".join(args) 31 | 32 | self.logger.debug("windows_command : %s", spf_command) 33 | 34 | 35 | self.spf_session= self.server.new_session('spf_session', 36 | window_command=spf_command, 37 | start_directory=start_dir) 38 | time.sleep(TmuxSPFManager.SPF_START_DELAY) 39 | self.logger.debug("spf_session initialised : %s", self.spf_session) 40 | 41 | self.spf_pane = self.spf_session.active_pane 42 | self._is_spf_running = True 43 | 44 | def _send_key(self, key : str) -> None: 45 | self.spf_pane.send_keys(key, enter=False) 46 | 47 | def send_text_input(self, text : str, all_at_once : bool = True) -> None: 48 | if all_at_once: 49 | self._send_key(text) 50 | else: 51 | for c in text: 52 | self._send_key(c) 53 | 54 | def send_special_input(self, key : keys.Keys) -> str: 55 | if key.ascii_code != keys.NO_ASCII: 56 | self._send_key(chr(key.ascii_code)) 57 | elif isinstance(key, keys.SpecialKeys): 58 | self._send_key(key.key_name) 59 | else: 60 | raise Exception(f"Unknown key : {key}") 61 | 62 | def get_rendered_output(self) -> str: 63 | return "[Not supported yet]" 64 | 65 | def is_spf_running(self) -> bool: 66 | self._is_spf_running = (self.spf_session is not None) \ 67 | and (self.spf_session in self.server.sessions) 68 | 69 | return self._is_spf_running 70 | 71 | def close_spf(self) -> None: 72 | if self.is_spf_running(): 73 | self.server.kill_session(self.spf_session.name) 74 | 75 | # Override 76 | def runtime_info(self) -> str: 77 | return str(self.server.sessions) 78 | 79 | def __repr__(self) -> str: 80 | return f"{self.__class__.__name__}(server : {self.server}, " + \ 81 | f"session : {self.spf_session}, running : {self._is_spf_running})" 82 | -------------------------------------------------------------------------------- /testsuite/core/utils.py: -------------------------------------------------------------------------------- 1 | import pyperclip 2 | # ------ Clipboard utils 3 | 4 | # This creates a layer of abstraction. 5 | # Now the user of the fuction doesn't need to import pyperclip 6 | # or need to even know what pyperclip was used. 7 | def get_sys_clipboard_text() -> str : 8 | return pyperclip.paste() -------------------------------------------------------------------------------- /testsuite/docs/tmux.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | This is to document the behviour of tmux, and how could we use it in testsuite 3 | 4 | # Tmux concepts and working info 5 | - Tmux creates a main server process, and one new process for each session. 6 | image 7 | - `-s` and `-n` for window naming. 8 | - We have prefix keys to send commands to tmux. 9 | 10 | # Sample usage with spf 11 | 12 | ## Sending keys to termux and controlling from outside. 13 | image 14 | image 15 | 16 | # Knowledge sharing 17 | - `tmux new 'spf'` - Run spf in tmux 18 | - `tmux attach -t ` attach to an existing session. You can have two windows duplicating same behaviour. 19 | - `tmux kill-session -t ` kill session 20 | - `Ctrl+B`+`:` - Enter commands 21 | - `Ctrl+B`+`D` - Detach from session 22 | - `:source ~/.tmux.conf` - Change the config of running server 23 | - We have already a wrapper library for termux in python !!!!! 24 | - How to send key press/tmux commands to the process ? 25 | 26 | 27 | # References 28 | - https://github.com/tmux/tmux/wiki/Getting-Started 29 | - https://tao-of-tmux.readthedocs.io/en/latest/manuscript/10-scripting.html#controlling-tmux-send-keys 30 | - https://github.com/tmux-python/libtmux 31 | -------------------------------------------------------------------------------- /testsuite/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | from pathlib import Path 5 | 6 | from core.runner import run_tests 7 | import core.test_constants as tconst 8 | 9 | 10 | def configure_logging(debug : bool = False) -> None: 11 | # Prefer stdout instead of default stderr 12 | handler = logging.StreamHandler(sys.stdout) 13 | 14 | # 7s to align all log levelnames - WARNING is the largest level, with size 7 15 | handler.setFormatter(logging.Formatter( 16 | '[%(asctime)s - %(levelname)7s] %(message)s', 17 | datefmt='%Y-%m-%d %H:%M:%S' 18 | )) 19 | 20 | 21 | logger = logging.getLogger() 22 | logger.addHandler(handler) 23 | 24 | if debug: 25 | logger.setLevel(logging.DEBUG) 26 | else: 27 | logger.setLevel(logging.INFO) 28 | 29 | logging.getLogger("libtmux").setLevel(logging.WARNING) 30 | 31 | def main(): 32 | # Setup argument parser 33 | parser = argparse.ArgumentParser(description='superfile testsuite') 34 | parser.add_argument('-d', '--debug',action='store_true', 35 | help='Enable debug logging') 36 | parser.add_argument('--close-wait-time', type=float, 37 | help='Override default wait time after closing spf') 38 | parser.add_argument('--spf-path', type=str, 39 | help='Override the default spf executable path(../bin/spf) under test') 40 | parser.add_argument('-t', '--tests', nargs='+', 41 | help='Specify one or more than one space separated testcases to be run') 42 | # Parse arguments 43 | args = parser.parse_args() 44 | if args.close_wait_time is not None: 45 | tconst.CLOSE_WAIT_TIME = args.close_wait_time 46 | 47 | configure_logging(args.debug) 48 | 49 | # Default path 50 | # We maybe should run this only in main.py file. 51 | spf_path = Path(__file__).parent.parent / "bin" / "spf" 52 | 53 | if args.spf_path is not None: 54 | spf_path = Path(args.spf_path) 55 | # Resolve any symlinks, and make it absolute 56 | spf_path = spf_path.resolve() 57 | 58 | success = run_tests(spf_path, only_run_tests=args.tests) 59 | if success: 60 | sys.exit(0) 61 | else: 62 | sys.exit(1) 63 | 64 | main() 65 | -------------------------------------------------------------------------------- /testsuite/requirements.txt: -------------------------------------------------------------------------------- 1 | pyautogui; sys_platform == "win32" 2 | libtmux; sys_platform == "linux" or sys_platform == "darwin" 3 | pyperclip 4 | assertpy -------------------------------------------------------------------------------- /testsuite/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/testsuite/tests/__init__.py -------------------------------------------------------------------------------- /testsuite/tests/chooser_file_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import time 3 | 4 | from core.base_test import GenericTestImpl 5 | from core.environment import Environment 6 | import core.test_constants as tconst 7 | 8 | TESTROOT = Path("chooser_file_ops") 9 | DIR1 = TESTROOT / "dir1" 10 | DIR2 = TESTROOT / "dir2" 11 | FILE1 = DIR1 / "file1.txt" 12 | CHOOSER_FILE = DIR2 / "chooser_file.txt" 13 | 14 | 15 | 16 | class ChooserFileTest(GenericTestImpl): 17 | 18 | def __init__(self, test_env : Environment): 19 | super().__init__( 20 | test_env=test_env, 21 | test_root=TESTROOT, 22 | start_dir=DIR1, 23 | test_dirs=[DIR1, DIR2], 24 | test_files=[(FILE1, tconst.FILE_TEXT1)], 25 | key_inputs=['e'], 26 | validate_spf_closed=True, 27 | close_wait_time=3 28 | ) 29 | 30 | # Override 31 | def start_spf(self) -> None: 32 | self.env.spf_mgr.start_spf(self.env.fs_mgr.abspath(self.start_dir), 33 | ["--chooser-file", str(self.env.fs_mgr.abspath(CHOOSER_FILE))]) 34 | assert self.env.spf_mgr.is_spf_running(), "Superfile is not running" 35 | 36 | # Override 37 | def end_execution(self) -> None: 38 | self.logger.debug("Skipping esc key press for Chooser file test") 39 | time.sleep(self.close_wait_time) 40 | self.logger.debug("Finished Execution") 41 | # Override 42 | def validate(self) -> bool: 43 | if not super().validate(): 44 | return False 45 | 46 | try: 47 | assert self.env.fs_mgr.check_exists(CHOOSER_FILE), f"File {CHOOSER_FILE} does not exists" 48 | chooser_file_content = self.env.fs_mgr.read_file(CHOOSER_FILE) 49 | assert chooser_file_content == str(self.env.fs_mgr.abspath(FILE1)), \ 50 | f"Expected '{self.env.fs_mgr.abspath(FILE1)}', got '{chooser_file_content}'" 51 | 52 | except AssertionError as ae: 53 | self.logger.debug("Test assertion failed : %s", ae, exc_info=True) 54 | return False 55 | 56 | return True -------------------------------------------------------------------------------- /testsuite/tests/command_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.keys as keys 6 | import core.test_constants as tconst 7 | 8 | TESTROOT = Path("cmd_ops") 9 | DIR1 = TESTROOT / "dir1" 10 | FILE1 = TESTROOT / "file1" 11 | 12 | class CommandTest(GenericTestImpl): 13 | """Test compression and extraction 14 | """ 15 | def __init__(self, test_env : Environment): 16 | super().__init__( 17 | test_env=test_env, 18 | test_root=TESTROOT, 19 | start_dir=TESTROOT, 20 | test_dirs=[TESTROOT], 21 | key_inputs=[':', 'mkdir dir1', keys.KEY_ENTER, ':', tconst.FILE_CREATE_COMMAND + ' file1', keys.KEY_ENTER], 22 | validate_exists=[DIR1, FILE1] 23 | ) 24 | -------------------------------------------------------------------------------- /testsuite/tests/compress_extract_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("ce_ops") 9 | DIR1 = TESTROOT / "dir1" 10 | FILE1 = DIR1 / "file1" 11 | FILE2 = DIR1 / "file2" 12 | 13 | DIR1_ZIPPED = TESTROOT / "dir1.zip" 14 | 15 | DIR1_EXTRACTED = TESTROOT / "dir1(1)" / "dir1" 16 | FILE1_EXTRACTED = DIR1_EXTRACTED / "file1" 17 | FILE2_EXTRACTED = DIR1_EXTRACTED / "file2" 18 | 19 | 20 | class CompressExtractTest(GenericTestImpl): 21 | """Test compression and extraction 22 | 23 | Args: 24 | GenericTestImpl (_type_): _description_ 25 | """ 26 | def __init__(self, test_env : Environment): 27 | super().__init__( 28 | test_env=test_env, 29 | test_root=TESTROOT, 30 | start_dir=TESTROOT, 31 | test_dirs=[DIR1], 32 | test_files=[(FILE1, tconst.FILE_TEXT1), (FILE2, tconst.FILE_TEXT1)], 33 | key_inputs=[keys.KEY_CTRL_A, keys.KEY_DOWN, keys.KEY_CTRL_E], 34 | validate_exists=[DIR1, DIR1_ZIPPED, DIR1_EXTRACTED, FILE1_EXTRACTED, FILE2_EXTRACTED] 35 | ) 36 | -------------------------------------------------------------------------------- /testsuite/tests/copy_dir_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("copy_dir") 9 | DIR1 = TESTROOT / "dir1" 10 | NESTED_DIR1 = DIR1 / "nested1" 11 | NESTED_DIR2 = DIR1 / "nested2" 12 | FILE1 = NESTED_DIR1 / "file1.txt" 13 | 14 | DIR2 = TESTROOT / "dir2" 15 | 16 | DIR1_COPIED = DIR2 / "dir1" 17 | FILE1_COPIED = DIR1_COPIED / "nested1" / "file1.txt" 18 | 19 | 20 | 21 | class CopyDirTest(GenericTestImpl): 22 | 23 | def __init__(self, test_env : Environment): 24 | super().__init__( 25 | test_env=test_env, 26 | test_root=TESTROOT, 27 | start_dir=TESTROOT, 28 | test_dirs=[DIR1, DIR2, NESTED_DIR1, NESTED_DIR2], 29 | test_files=[(FILE1, tconst.FILE_TEXT1)], 30 | key_inputs=[keys.KEY_CTRL_C, keys.KEY_DOWN, keys.KEY_ENTER, keys.KEY_PASTE], 31 | validate_exists=[DIR1_COPIED, FILE1_COPIED, DIR1, FILE1] 32 | ) -------------------------------------------------------------------------------- /testsuite/tests/copy_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("copy_ops") 9 | DIR1 = TESTROOT / "dir1" 10 | DIR2 = TESTROOT / "dir2" 11 | FILE1 = DIR1 / "file1.txt" 12 | FILE1_COPY1 = DIR1 / "file1(1).txt" 13 | FILE1_COPY2 = DIR2 / "file1.txt" 14 | 15 | 16 | 17 | class CopyTest(GenericTestImpl): 18 | 19 | def __init__(self, test_env : Environment): 20 | super().__init__( 21 | test_env=test_env, 22 | test_root=TESTROOT, 23 | start_dir=DIR1, 24 | test_dirs=[DIR1, DIR2], 25 | test_files=[(FILE1, tconst.FILE_TEXT1)], 26 | key_inputs=[keys.KEY_CTRL_C, keys.KEY_PASTE], 27 | validate_exists=[FILE1, FILE1_COPY1], 28 | # If you want to validate spf being close, wait time needs to be high 29 | # Otherwise tests are flaky 30 | validate_spf_closed=True, 31 | close_wait_time=3 32 | ) -------------------------------------------------------------------------------- /testsuite/tests/copyw_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("copyw_ops") 9 | FILE1 = TESTROOT / "file1.txt" 10 | FILE1_COPY1 = TESTROOT / "file1(1).txt" 11 | 12 | class CopyWTest(GenericTestImpl): 13 | """Testcase to validate copying with Ctrl+W shortcut 14 | """ 15 | def __init__(self, test_env : Environment): 16 | super().__init__( 17 | test_env=test_env, 18 | test_root=TESTROOT, 19 | start_dir=TESTROOT, 20 | test_dirs=[TESTROOT], 21 | test_files=[(FILE1, tconst.FILE_TEXT1)], 22 | key_inputs=[keys.KEY_CTRL_C, keys.KEY_CTRL_W], 23 | validate_exists=[FILE1, FILE1_COPY1] 24 | ) -------------------------------------------------------------------------------- /testsuite/tests/cut_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("cut_ops") 9 | DIR1 = TESTROOT / "dir1" 10 | DIR2 = TESTROOT / "dir2" 11 | FILE1 = DIR1 / "file1.txt" 12 | FILE1_CUT1 = DIR2 / "file1.txt" 13 | 14 | 15 | 16 | class CutTest(GenericTestImpl): 17 | 18 | def __init__(self, test_env : Environment): 19 | super().__init__( 20 | test_env=test_env, 21 | test_root=TESTROOT, 22 | start_dir=DIR1, 23 | test_dirs=[DIR1, DIR2], 24 | test_files=[(FILE1, tconst.FILE_TEXT1)], 25 | key_inputs=[keys.KEY_CTRL_X, keys.KEY_LEFT, keys.KEY_DOWN, 26 | keys.KEY_ENTER, keys.KEY_PASTE], 27 | validate_exists=[FILE1_CUT1], 28 | validate_not_exists=[FILE1] 29 | ) 30 | -------------------------------------------------------------------------------- /testsuite/tests/delete_dir_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("delete_dir") 9 | DIR1 = TESTROOT / "dir1" 10 | NESTED_DIR1 = DIR1 / "nested1" 11 | NESTED_DIR2 = DIR1 / "nested2" 12 | FILE1 = NESTED_DIR1 / "file1.txt" 13 | 14 | 15 | class DeleteDirTest(GenericTestImpl): 16 | 17 | def __init__(self, test_env : Environment): 18 | super().__init__( 19 | test_env=test_env, 20 | test_root=TESTROOT, 21 | start_dir=TESTROOT, 22 | test_dirs=[TESTROOT, DIR1, NESTED_DIR1, NESTED_DIR2], 23 | test_files=[(FILE1, tconst.FILE_TEXT1)], 24 | key_inputs=[keys.KEY_CTRL_D, keys.KEY_ENTER], 25 | validate_not_exists=[DIR1, NESTED_DIR1, NESTED_DIR2, FILE1] 26 | ) 27 | -------------------------------------------------------------------------------- /testsuite/tests/delete_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("delete_ops") 9 | FILE1 = TESTROOT / "file_to_delete.txt" 10 | 11 | 12 | 13 | class DeleteTest(GenericTestImpl): 14 | 15 | def __init__(self, test_env : Environment): 16 | super().__init__( 17 | test_env=test_env, 18 | test_root=TESTROOT, 19 | start_dir=TESTROOT, 20 | test_dirs=[TESTROOT], 21 | test_files=[(FILE1, tconst.FILE_TEXT1)], 22 | key_inputs=[keys.KEY_CTRL_D, keys.KEY_ENTER], 23 | validate_not_exists=[FILE1] 24 | ) 25 | -------------------------------------------------------------------------------- /testsuite/tests/empty_panel_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | import time 8 | 9 | TESTROOT = Path("empty_panel_ops") 10 | DIR1 = TESTROOT / "dir1" 11 | 12 | 13 | class EmptyPanelTest(GenericTestImpl): 14 | """ 15 | Validate that spf doesn't crashes when we try to 16 | perform operations on empty file panel 17 | """ 18 | def __init__(self, test_env : Environment): 19 | super().__init__( 20 | test_env=test_env, 21 | test_root=TESTROOT, 22 | start_dir=DIR1, 23 | test_dirs=[DIR1], 24 | key_inputs=[ 25 | keys.KEY_CTRL_C, # Try copy 26 | keys.KEY_CTRL_X, # Try cut 27 | keys.KEY_CTRL_D, # Try delete 28 | keys.KEY_PASTE, # Try paste 29 | keys.KEY_CTRL_R, # Try rename 30 | keys.KEY_CTRL_P, # Try copy location 31 | 'e', # Try open with editor 32 | keys.KEY_ENTER, 33 | keys.KEY_RIGHT, 34 | keys.KEY_CTRL_A, # Try archiving 35 | keys.KEY_CTRL_E, # Try extract 36 | 'v', # Try going to Select mode 37 | 'J', # Try select down 38 | 'K', # Try select up 39 | 'A', # select all 40 | 'v', 41 | '.', # Try toggle dotfiles 42 | ], 43 | # Makes sure spf doesn't crashes 44 | validate_spf_running=True 45 | ) 46 | 47 | # Override 48 | def test_execute(self) -> None: 49 | self.start_spf() 50 | self.send_input() 51 | time.sleep(tconst.OPERATION_DELAY) 52 | # Intentionally not closing spf to ensure it remains running, 53 | # which is verified by the validate_spf_running flag which is set 54 | # to true for this testcase 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /testsuite/tests/nav_and_copy_path_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | from core.utils import get_sys_clipboard_text 6 | import core.test_constants as tconst 7 | import core.keys as keys 8 | from assertpy import assert_that 9 | import time 10 | 11 | TESTROOT = Path("nav_ops") 12 | DIR1 = TESTROOT / "dir1" 13 | FILE1 = TESTROOT / "file1" 14 | FILE2 = TESTROOT / "file2" 15 | 16 | # Temporarily disabled, till we fix xclip does not works in github actions 17 | class NavCopyPathTest_Disabled(GenericTestImpl): 18 | """Test navigation, and Copying of path 19 | """ 20 | def __init__(self, test_env : Environment): 21 | super().__init__( 22 | test_env=test_env, 23 | test_root=TESTROOT, 24 | start_dir=TESTROOT, 25 | test_dirs=[TESTROOT, DIR1], 26 | test_files=[(FILE1, tconst.FILE_TEXT1), (FILE2, tconst.FILE_TEXT1)] 27 | ) 28 | 29 | # Override 30 | def test_execute(self) -> None: 31 | self.start_spf() 32 | time.sleep(tconst.OPERATION_DELAY) 33 | # > dir1 34 | # file1 35 | # file2 36 | 37 | self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P) 38 | time.sleep(tconst.KEY_DELAY) 39 | assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(DIR1))) 40 | 41 | self.env.spf_mgr.send_special_input(keys.KEY_DOWN) 42 | time.sleep(tconst.KEY_DELAY) 43 | # dir1 44 | # > file1 45 | # file2 46 | self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P) 47 | time.sleep(tconst.KEY_DELAY) 48 | assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(FILE1))) 49 | 50 | self.env.spf_mgr.send_special_input(keys.KEY_DOWN) 51 | time.sleep(tconst.KEY_DELAY) 52 | # dir1 53 | # file1 54 | # > file2 55 | self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P) 56 | time.sleep(tconst.KEY_DELAY) 57 | assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(FILE2))) 58 | 59 | self.env.spf_mgr.send_special_input(keys.KEY_UP) 60 | time.sleep(tconst.KEY_DELAY) 61 | # dir1 62 | # > file1 63 | # file2 64 | self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P) 65 | time.sleep(tconst.KEY_DELAY) 66 | assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(FILE1))) 67 | 68 | self.env.spf_mgr.send_special_input(keys.KEY_DOWN) 69 | time.sleep(tconst.KEY_DELAY) 70 | self.env.spf_mgr.send_special_input(keys.KEY_DOWN) 71 | time.sleep(tconst.KEY_DELAY) 72 | # > dir1 73 | # file1 74 | # file2 75 | self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P) 76 | time.sleep(tconst.KEY_DELAY) 77 | assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(DIR1))) 78 | 79 | self.end_execution() 80 | -------------------------------------------------------------------------------- /testsuite/tests/rename_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from core.base_test import GenericTestImpl 4 | from core.environment import Environment 5 | import core.test_constants as tconst 6 | import core.keys as keys 7 | 8 | TESTROOT = Path("rename_ops") 9 | DIR1 = TESTROOT / "dir1" 10 | 11 | # No extension, as in case of extension, the edit cursor appears before the dot, 12 | # not at the end of filename 13 | FILE1 = DIR1 / "file1" 14 | FILE1_RENAMED = DIR1 / "file2" 15 | 16 | 17 | 18 | class RenameTest(GenericTestImpl): 19 | 20 | def __init__(self, test_env : Environment): 21 | super().__init__( 22 | test_env=test_env, 23 | test_root=TESTROOT, 24 | start_dir=DIR1, 25 | test_dirs=[DIR1], 26 | test_files=[(FILE1, tconst.FILE_TEXT1)], 27 | key_inputs=[keys.KEY_CTRL_R, keys.KEY_BACKSPACE, '2', keys.KEY_ENTER], 28 | validate_exists=[FILE1_RENAMED], 29 | validate_not_exists=[FILE1] 30 | ) 31 | -------------------------------------------------------------------------------- /vhs/demo.tape: -------------------------------------------------------------------------------- 1 | Output asset/demo.gif 2 | 3 | Set Shell "base" 4 | Set FontSize 20 5 | Set Width 1920 6 | Set Height 1080 7 | Set FontFamily "Comic Mono, RobotoMono Nerd Font" 8 | Set Framerate 15 9 | 10 | Type "spf" 11 | Sleep 1500ms 12 | Enter 13 | Sleep 700ms 14 | 15 | Type "b" 16 | Sleep 500ms 17 | Down@600ms 1 18 | Type "l" 19 | 20 | Ctrl+p 21 | Sleep 600ms 22 | 23 | Ctrl+n 24 | Sleep 300ms 25 | Ctrl+n 26 | Sleep 300ms 27 | Ctrl+n 28 | Sleep 300ms 29 | 30 | Ctrl+w 31 | Sleep 300ms 32 | Ctrl+w 33 | Sleep 700ms 34 | 35 | Type "b" 36 | 37 | Down@600ms 2 38 | 39 | Sleep 600ms 40 | Type "l" 41 | 42 | Sleep 600ms 43 | Tab 44 | 45 | Sleep 600ms 46 | Type "c" 47 | Sleep 500ms 48 | Type "test.txt" 49 | Sleep 500ms 50 | Enter 51 | Sleep 600ms 52 | 53 | Down@700ms 1 54 | 55 | Sleep 600ms 56 | Type "d" 57 | 58 | Sleep 700ms 59 | Type "f" 60 | Sleep 600ms 61 | Type "test folder" 62 | Sleep 700ms 63 | Enter 64 | Sleep 700ms 65 | Type "r" 66 | Sleep 600ms 67 | Backspace 12 68 | Type "rename this folder" 69 | Sleep 500ms 70 | Enter 71 | Sleep 700ms 72 | Type "d" 73 | Sleep 600ms 74 | Tab 75 | Sleep 600ms 76 | Ctrl+c 77 | Sleep 600ms 78 | Tab 79 | Sleep 600ms 80 | Ctrl+v 81 | Sleep 600ms 82 | Type "l" 83 | Sleep 600ms 84 | Type "d" 85 | Sleep 300ms 86 | Type "d" 87 | Sleep 300ms 88 | Type "d" 89 | Sleep 300ms 90 | Type "v" 91 | Sleep 600ms 92 | Type "J" 93 | Sleep 500ms 94 | Type "J" 95 | Sleep 500ms 96 | Type "J" 97 | Sleep 600ms 98 | 99 | Ctrl+d 100 | Sleep 600ms 101 | 102 | Type "J" 103 | Sleep 400ms 104 | Type "j" 105 | Sleep 400ms 106 | Type "J" 107 | Sleep 400ms 108 | Type "j" 109 | Sleep 400ms 110 | Type "J" 111 | Sleep 400ms 112 | Type "j" 113 | Sleep 700ms 114 | 115 | Ctrl+x 116 | Sleep 700ms 117 | Tab 118 | Sleep 600ms 119 | Ctrl+v 120 | Sleep 700ms 121 | 122 | Ctrl+w 123 | Sleep 500ms 124 | 125 | Escape 126 | 127 | Type "Thanks for watching" 128 | Sleep 5s 129 | -------------------------------------------------------------------------------- /vhs/open_spf_and_quit.tape: -------------------------------------------------------------------------------- 1 | Output asset/demo.gif 2 | 3 | Set Shell "base" 4 | Set FontSize 30 5 | Set Width 2560 6 | Set Height 1500 7 | Set FontFamily "Comic Mono, RobotoMono Nerd Font" 8 | Set PlaybackSpeed 0.6 9 | Set Padding 50 10 | Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#1e1e2e", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" } 11 | 12 | Type "spf" 13 | Sleep 1500ms 14 | Enter 15 | Sleep 1500ms 16 | Type "q" 17 | Sleep 1500ms -------------------------------------------------------------------------------- /vhs/spf_file_panel_movement.tape: -------------------------------------------------------------------------------- 1 | Output asset/demo.gif 2 | 3 | Set Shell "base" 4 | Set FontSize 30 5 | Set Framerate 60 6 | Set Width 2560 7 | Set Height 1500 8 | Set FontFamily "Comic Mono, RobotoMono Nerd Font" 9 | Set PlaybackSpeed 0.6 10 | Set Padding 50 11 | Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#1e1e2e", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" } 12 | 13 | Type "spf" 14 | Sleep 1500ms 15 | Enter 16 | Sleep 1500ms 17 | Down@150ms 5 18 | Sleep 150ms 19 | Up@150ms 3 20 | Sleep 700ms 21 | Enter 22 | Sleep 1500ms 23 | -------------------------------------------------------------------------------- /vhs/spf_file_panel_navigation.tape: -------------------------------------------------------------------------------- 1 | Output asset/demo.gif 2 | 3 | Set Shell "base" 4 | Set FontSize 30 5 | Set Framerate 60 6 | Set Width 2560 7 | Set Height 1500 8 | Set FontFamily "Comic Mono, RobotoMono Nerd Font" 9 | Set PlaybackSpeed 0.6 10 | Set Padding 50 11 | Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#1e1e2e", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" } 12 | 13 | Type "spf" 14 | Sleep 1500ms 15 | Enter 16 | Sleep 1500ms 17 | Ctrl+N 18 | Sleep 500ms 19 | Ctrl+N 20 | Sleep 500ms 21 | Ctrl+N 22 | Sleep 500ms 23 | Ctrl+N 24 | Sleep 500ms 25 | 26 | Type "L" 27 | Sleep 200ms 28 | Type "L" 29 | Sleep 200ms 30 | Type "L" 31 | Sleep 200ms 32 | Type "L" 33 | Sleep 200ms 34 | 35 | Type "H" 36 | Sleep 200ms 37 | Type "H" 38 | Sleep 200ms 39 | Type "H" 40 | Sleep 200ms 41 | Type "H" 42 | Sleep 200ms 43 | 44 | Ctrl+W 45 | Sleep 500ms 46 | Ctrl+W 47 | Sleep 500ms 48 | Ctrl+W 49 | Sleep 500ms 50 | Ctrl+W 51 | Sleep 500ms -------------------------------------------------------------------------------- /vhs/spf_file_panel_selection_mode.tape: -------------------------------------------------------------------------------- 1 | Output asset/demo.gif 2 | 3 | Set Shell "base" 4 | Set FontSize 30 5 | Set Framerate 60 6 | Set Width 2560 7 | Set Height 1500 8 | Set FontFamily "Comic Mono, RobotoMono Nerd Font" 9 | Set PlaybackSpeed 0.6 10 | Set Padding 50 11 | Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#1e1e2e", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" } 12 | 13 | Type "spf" 14 | Sleep 1500ms 15 | Enter 16 | Sleep 1500ms 17 | Type "v" 18 | 19 | Sleep 600ms 20 | 21 | Type "l" 22 | Sleep 300ms 23 | Type "l" 24 | Sleep 300ms 25 | 26 | Type "J" 27 | Sleep 300ms 28 | Type "J" 29 | Sleep 300ms 30 | Type "J" 31 | Sleep 300ms 32 | Type "J" 33 | Sleep 300ms 34 | Type "J" 35 | Sleep 300ms 36 | 37 | Type "l" 38 | Sleep 300ms 39 | 40 | Type "K" 41 | Sleep 300ms 42 | Type "K" 43 | Sleep 300ms 44 | Type "K" 45 | Sleep 300ms 46 | Type "K" 47 | Sleep 300ms 48 | 49 | Ctrl+A 50 | Sleep 1500ms 51 | Type "v" 52 | Sleep 1500ms 53 | -------------------------------------------------------------------------------- /vhs/spf_panel_navigation.tape: -------------------------------------------------------------------------------- 1 | Output asset/demo.gif 2 | 3 | Set Shell "base" 4 | Set FontSize 30 5 | Set Width 2560 6 | Set Height 1500 7 | Set FontFamily "Comic Mono, RobotoMono Nerd Font" 8 | Set PlaybackSpeed 0.6 9 | Set Padding 50 10 | Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#1e1e2e", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" } 11 | 12 | Type "spf" 13 | Sleep 1500ms 14 | Enter 15 | Sleep 1500ms 16 | Type "b" 17 | Sleep 1500ms 18 | Type "p" 19 | Sleep 1500ms 20 | Type "m" 21 | Sleep 1500ms 22 | Type "m" 23 | Sleep 1500ms -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | ``` 4 | npm create astro@latest -- --template starlight 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 9 | 10 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 11 | 12 | ## 🚀 Project Structure 13 | 14 | Inside of your Astro + Starlight project, you'll see the following folders and files: 15 | 16 | ``` 17 | . 18 | ├── public/ 19 | ├── src/ 20 | │ ├── assets/ 21 | │ ├── content/ 22 | │ │ ├── docs/ 23 | │ │ └── config.ts 24 | │ └── env.d.ts 25 | ├── astro.config.mjs 26 | ├── package.json 27 | └── tsconfig.json 28 | ``` 29 | 30 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 31 | 32 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 33 | 34 | Static assets, like favicons, can be placed in the `public/` directory. 35 | 36 | ## 🧞 Commands 37 | 38 | All commands are run from the root of the project, from a terminal: 39 | 40 | | Command | Action | 41 | | :------------------------ | :----------------------------------------------- | 42 | | `npm install` | Installs dependencies | 43 | | `npm run dev` | Starts local dev server at `localhost:3000` | 44 | | `npm run build` | Build your production site to `./dist/` | 45 | | `npm run preview` | Preview your build locally, before deploying | 46 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 47 | | `npm run astro -- --help` | Get help using the Astro CLI | 48 | 49 | ## 👀 Want to learn more? 50 | 51 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 52 | -------------------------------------------------------------------------------- /website/ec.config.mjs: -------------------------------------------------------------------------------- 1 | import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'; 2 | import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'; 3 | 4 | /** @type {import('@astrojs/starlight/expressive-code').StarlightExpressiveCodeOptions} */ 5 | export default { 6 | // Example: Using a custom plugin (which makes this `ec.config.mjs` file necessary) 7 | // plugins: [pluginCollapsibleSections(), pluginLineNumbers()], 8 | // ... any other options you want to configure 9 | }; 10 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ossified-orbit", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/starlight": "^0.34.1", 14 | "@expressive-code/plugin-collapsible-sections": "^0.41.2", 15 | "@expressive-code/plugin-line-numbers": "^0.41.2", 16 | "@fontsource/ibm-plex-mono": "^5.2.5", 17 | "@fontsource/ibm-plex-serif": "^5.2.5", 18 | "astro": "^5.7.7", 19 | "hast-util-to-html": "^9.0.5", 20 | "sharp": "^0.34.1", 21 | "starlight-giscus": "^0.6.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /website/public/_redirects: -------------------------------------------------------------------------------- 1 | # redirect all /docs requests to the root domain 2 | 3 | /docs/\* /:splat 301 4 | -------------------------------------------------------------------------------- /website/public/google0fdf22175b8dde4d.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google0fdf22175b8dde4d.html -------------------------------------------------------------------------------- /website/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/website/public/og.jpg -------------------------------------------------------------------------------- /website/public/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [switch] 3 | $AllUsers 4 | ) 5 | 6 | function FolderIsInPATH($Path_to_directory) { 7 | return ([Environment]::GetEnvironmentVariable("PATH", "User") -split ';').TrimEnd('\') -contains $Path_to_directory.TrimEnd('\') 8 | } 9 | 10 | Write-Host -ForegroundColor DarkRed " ______ __ __ " 11 | Write-Host -ForegroundColor Red " / \ / |/ | " 12 | Write-Host -ForegroundColor DarkYellow " _______ __ __ ______ ______ ______ /`$`$`$`$`$`$ |`$`$/ `$`$ | ______ " 13 | Write-Host -ForegroundColor Yellow " / |/ | / | / \ / \ / \ `$`$ |_ `$`$/ / |`$`$ | / \ " 14 | Write-Host -ForegroundColor DarkGreen "/`$`$`$`$`$`$`$/ `$`$ | `$`$ |/`$`$`$`$`$`$ |/`$`$`$`$`$`$ |/`$`$`$`$`$`$ |`$`$ | `$`$ |`$`$ |/`$`$`$`$`$`$ |" 15 | Write-Host -ForegroundColor Green "`$`$ \ `$`$ | `$`$ |`$`$ | `$`$ |`$`$ `$`$ |`$`$ | `$`$/ `$`$`$`$/ `$`$ |`$`$ |`$`$ `$`$ |" 16 | Write-Host -ForegroundColor DarkBlue " `$`$`$`$`$`$ |`$`$ \__`$`$ |`$`$ |__`$`$ |`$`$`$`$`$`$`$`$/ `$`$ | `$`$ | `$`$ |`$`$ |`$`$`$`$`$`$`$`$/ " 17 | Write-Host -ForegroundColor Blue " `$`$/ `$`$ `$`$/ `$`$ `$`$/ `$`$ |`$`$ | `$`$ | `$`$ |`$`$ |`$`$ |" 18 | Write-Host -ForegroundColor DarkMagenta "`$`$`$`$`$`$`$/ `$`$`$`$`$`$/ `$`$`$`$`$`$`$/ `$`$`$`$`$`$`$/ `$`$/ `$`$/ `$`$/ `$`$/ `$`$`$`$`$`$`$/ " 19 | Write-Host -ForegroundColor Magenta " `$`$ | " 20 | Write-Host -ForegroundColor DarkRed " `$`$ | " 21 | Write-Host -ForegroundColor Red " `$`$/ " 22 | Write-Host "" 23 | 24 | $package = "superfile" 25 | 26 | $installInstructions = @' 27 | This uninstaller is only available for Windows. 28 | '@ 29 | if ($IsMacOS) { 30 | Write-Host "$installInstructions" 31 | exit 32 | } 33 | if ($IsLinux) { 34 | Write-Host "$installInstructions" 35 | exit 36 | } 37 | 38 | Write-Host "Removing folder..." 39 | 40 | $superfileProgramPath = [Environment]::GetFolderPath("LocalApplicationData") + "\Programs\superfile" 41 | try { 42 | if (Test-Path $superfileProgramPath) { 43 | Remove-Item -Path $superfileProgramPath -Recurse -Force 44 | } 45 | } 46 | catch { 47 | Write-Host "An error occurred: $_" 48 | exit 49 | } 50 | 51 | Write-Host "Removing environment path..." 52 | 53 | try { 54 | if (FolderIsInPATH "$superfileProgramPath\") { 55 | $envPath = [Environment]::GetEnvironmentVariable("PATH", "User") 56 | $updatedPath =($envPath.Split(';') | Where-Object { $_ -ne "$superfileProgramPath" }) -join ';' 57 | [Environment]::SetEnvironmentVariable("PATH", $updatedPath, "User") 58 | } 59 | } 60 | catch { 61 | Write-Host "An error occurred: $_" 62 | exit 63 | } 64 | 65 | Write-Host @' 66 | Uninstall Done! 67 | '@ 68 | 69 | -------------------------------------------------------------------------------- /website/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/website/src/assets/logo.png -------------------------------------------------------------------------------- /website/src/assets/superfileicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yorukot/superfile/238d8dcd090f24815b6717a65d6f1daddace9a2d/website/src/assets/superfileicon.png -------------------------------------------------------------------------------- /website/src/components/LastUpdated.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Props } from '@astrojs/starlight/props'; 3 | import Default from '@astrojs/starlight/components/LastUpdated.astro'; 4 | 5 | const { lastUpdated } = Astro.props; 6 | 7 | --- 8 | 9 | {lastUpdated && ( 10 |
11 | 12 |
13 | )} 14 | 15 | -------------------------------------------------------------------------------- /website/src/components/about.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /website/src/components/code.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Code as SCode } from '@astrojs/starlight/components' 3 | 4 | import fs from 'node:fs/promises'; 5 | 6 | interface Props { 7 | file: string; 8 | language?: string; 9 | meta?: string; 10 | } 11 | 12 | const { file, language, meta } = Astro.props; 13 | const fileNamePath = '../' + file; 14 | const fileEtension = file.split('.').pop() ?? 'js'; 15 | const code = await fs.readFile(fileNamePath, 'utf-8'); 16 | const lang = language ?? fileEtension; 17 | const metaa = `title="${file}"` + (meta ? ` ${meta}` : '') 18 | 19 | 20 | --- 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /website/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | i18n: defineCollection({ type: 'data', schema: i18nSchema() }), 7 | }; 8 | -------------------------------------------------------------------------------- /website/src/content/docs/configure/custom-hotkeys.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom hotkeys 3 | description: Customize your own hotkeys 4 | head: 5 | - tag: title 6 | content: Custom hotkeys | superfile 7 | --- 8 | 9 | import CodeBlock from '../../../components/code.astro'; 10 | 11 | You can enter the following command to set it up; 12 | 13 | [Click me to know where is HOTKEYS_PATH](/configure/config-file-path#hotkeys) 14 | 15 | ```bash 16 | $EDITOR HOTKEYS_PATH 17 | ``` 18 | 19 | :::caution 20 | Please do not use hotkeys with ascii codes conflicting with keys used to control superfile : 21 | - `Ctrl+M` - conflicts with `Enter` Key 22 | - `Ctrl+I` - conflicts with `Tab` Key 23 | - `Ctrl+?`, `Ctrl+[` - conflicts with `Delete` and `Backspace` Key 24 | ::: 25 | 26 | ### Default superfile hotkeys 27 | 28 | :::caution 29 | If you are a vim user, the default hotkeys may make you hate superfile. 30 | ::: 31 | 32 | superfile default hotkeys design concept: 33 | - All hotkeys that will change to files use `ctrl+key` (As long as you don't press ctrl your files will always be safe). 34 | - Non-control file classes use the first letters of words as hotkeys. 35 | 36 | 37 | 38 | ### Vim like superfile hotkeys 39 | 40 | -------------------------------------------------------------------------------- /website/src/content/docs/configure/custom-theme.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom theme 3 | description: Custom your own superfile theme 4 | head: 5 | - tag: title 6 | content: Custom theme | superfile 7 | --- 8 | 9 | import CodeBlock from '../../../components/code.astro'; 10 | 11 | ### Use an existing theme 12 | 13 | You can enter the following command to set it up; 14 | 15 | [Click me to know where is CONFIG_PATH](/configure/config-file-path#config) 16 | 17 | ```bash 18 | $EDITOR CONFIG_PATH 19 | ``` 20 | 21 | You can first go to the [theme list](/list/theme-list) to find a theme you like (or if you don't have one you like, you can make one yourself!) 22 | 23 | Once you find one you like, copy it and paste it into the theme in the config_path file. 24 | 25 | ```diff 26 | - theme = 'catppuccin' 27 | + theme = 'theme_name_you_like' 28 | ``` 29 | 30 | ### Create your own theme 31 | 32 | [Click me to know where is THEME_DIRECTORY](/configure/config-file-path#config) 33 | 34 | If you want to customize your own theme, you can go to `THEME_DIRECTORY/YOUR_THEME_NAME.toml` and copy the existing theme's json to your own theme file 35 | 36 | Don't forget to change the `theme` variable in `config.toml` to your theme name. 37 | 38 | [If you are satisfied with your theme, you might as well put it into the default theme list!](/how-to-contribute) 39 | 40 | ### Default theme 41 | 42 | -------------------------------------------------------------------------------- /website/src/content/docs/configure/enable-plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Enable plugin 3 | description: Enable superfile plguins 4 | head: 5 | - tag: title 6 | content: Enable plugins | superfile 7 | --- 8 | 9 | You can enter the following command to set it up; 10 | 11 | [Click me to know where is CONFIG_PATH](/configure/config-file-path#config) 12 | 13 | ```bash 14 | $EDITOR CONFIG_PATH 15 | ``` 16 | 17 | example: 18 | I want to enable metadata plugin 19 | 20 | Please make sure you have installed the Requirements of this plugin. 21 | 22 | After that edit `config.toml` using your preferred editor: 23 | 24 | ``` 25 | $EDITOR CONFIG_PATH 26 | ``` 27 | 28 | and change: 29 | 30 | ```diff 31 | - metadata = false 32 | + metadata = true 33 | ``` 34 | 35 | ## Plugin list 36 | 37 | [click me to check plugin list](/list/plugin-list) -------------------------------------------------------------------------------- /website/src/content/docs/contribute/how-to-contribute.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to Contribute 3 | description: How to contribute to the project, including ways to show your support, report bugs, and more. 4 | head: 5 | - tag: title 6 | content: How to Contribute | superfile 7 | --- 8 | 9 | # Contributing to superfile 10 | 11 | Welcome to superfile! This document shall serve as a guide for you to follow in your journey to contributing to this project. 12 | There are many ways to contribute to superfile: 13 | - Reporting Bugs 14 | - Resolving issues 15 | - Adding a theme 16 | - Sharing an idea and working on it 17 | - Working on a feature with other contributors. 18 | - And More… 19 | 20 | To get started, take a look at the following sections. 21 | 22 | ## Issues 23 | 24 | ### Did you spot a problem in superfile? 25 | 26 | Firstly you should check if such an issue was previously opened/closed for your problem on the repository. If it doesn't then you should create a new issue. 27 | 28 | ### Do you want to solve an issue? 29 | 30 | If there is an issue you think you can solve, and want to solve, then you should create a new fork of this repository. 31 | In that repository you should create a new branch for the issue you are working on and commit changes there. 32 | When the issue is solved, and you want it to be integrated into the official repository, you may create a pull request for the same. 33 | The description of the pull request should clearly describe both the issue and the solution along with other necessary information. 34 | The developers will merge after making the necessary changes (if arises a need to do so). 35 | 36 | ### Do you want to add a new theme? 37 | 38 | Firstly check if the theme you want to add is not already added. If it is, then you work may go waste and be left redundant. 39 | If no such theme exists, then you may create your own theme. Following steps will guide you for it: 40 | - As a template, copy an existing theme's TOML file to your theme and then do the customizations. This will reduce errors from your side and make your work easy. 41 | - To tests your theme, go to [`CONFIG_PATH`](/configure/config-file-path#config) and change description. 42 | - Make the changes you want and finish the theme. 43 | - Then you can open a pull request for the same and follow the steps described in the previous section. 44 | 45 | ### Do you want to share an idea? 46 | 47 | superfile welcomes new ideas. If you have an idea you should first check if a similar or identical idea was presented previously or not, or check thoroughly if the idea is already present in superfile. 48 | To share your idea you can open a discussion in https://github.com/yorukot/superfile/discussions 49 | There you can share your idea and if you want to work on it, you can follow the same steps as mentioned in previously. 50 | 51 | ### Do you want to contribute but don't know how? 52 | 53 | Your first resource in this should be https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project 54 | This file serves as your guide specifically for this project to help you get your contributions into the project. 55 | If you still have some questions or need help, feel free to open a discussion on the same. 56 | 57 | # Thank You 🙏 58 | -------------------------------------------------------------------------------- /website/src/content/docs/contribute/implementation-info.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Implementation info 3 | description: A collection of general information regarding how various things work 4 | head: 5 | - tag: title 6 | content: Implementation info | superfile 7 | --- 8 | 9 | # Implmentation info 10 | The purpose of this document is to provide some implementation details to the reader that are not so obvious from the code and not very straightforward to figure out. 11 | 12 | ## How default configuration files are packaged with app 13 | We use golangs `embed.FS` and embed all files in `src/superfile_config/` into our spf binary. In `src/internal/config_function.go`, the function `LoadAllDefaultConfig()` reads these embedded files, and write them to disk / in memory configuratin variables. 14 | -------------------------------------------------------------------------------- /website/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: superfile | terminal-based file manager 3 | description: "superfile is a very fancy and modern terminal file manager that can complete the file operations you need!!" 4 | template: splash 5 | lastUpdated: false 6 | editUrl: false 7 | hero: 8 | title: Perfect Terminal-based file manager 🚀! 9 | tagline: "superfile is a very fancy and modern terminal file manager that can complete the file operations you need!!" 10 | image: 11 | file: ../../assets/logo.png 12 | actions: 13 | - text: Get Started 14 | link: /overview 15 | icon: right-arrow 16 | variant: primary 17 | - text: View on GitHub 18 | link: https://github.com/yorukot/superfile 19 | icon: external 20 | --- 21 | 22 | import { Card, CardGrid } from '@astrojs/starlight/components'; 23 | import GithubStar from '../../components/GithubStar.astro'; 24 | import About from '../../components/about.astro'; 25 | 26 | 27 | 28 | ## Features 29 | 30 | 31 | 32 | It can be said that good-looking is the original intention of superfile, so the entire superfile should be as beautiful as possible. 33 | 34 | 35 | This file manager allows you to do almost everything you want to do on a file manager. 36 | 37 | 38 | From basic Hotkey, the entire theme color and even the border Style can be customized. 39 | 40 | 41 | Multiple panel allows you to view multiple directories at the same time and copy and paste in just a few simple steps without having to return to the main directory. 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /website/src/content/docs/list/plugin-list.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugin list 3 | description: superfile plugin list 4 | head: 5 | - tag: title 6 | content: Plugin list | superfile 7 | --- 8 | 9 | ## Metadata 10 | description: Show more detailed metadata 11 | 12 | Requirements: `exiftool` 13 | 14 | name in config.toml: `metadata` -------------------------------------------------------------------------------- /website/src/content/docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | description: An overview of why we built this starter, including its features, the libraries used, and more. 4 | head: 5 | - tag: title 6 | content: Overview | superfile 7 | --- 8 | 9 | ![](https://github.com/yorukot/superfile/blob/main/asset/demo.png?raw=true) 10 | 11 | # What is superfile? 12 | superfile is a modern terminal file manager crafted with a strong focus on user interface, functionality, and ease of use. Built with [Go](https://go.dev/) and [Bubble Tea](https://github.com/charmbracelet/bubbletea), it combines a visually appealing design with the simplicity of terminal tools, providing a fresh, accessible approach to file management. 13 | 14 | # Why was superfile built? 15 | Before creating superfile, I tried a lot of terminal file managers, but I was often disappointed by their UI design. So, I built superfile with a primary focus on delivering a refined, user-friendly interface. 16 | 17 | # Why should I use superfile? 18 | superfile is sleek and visually appealing, making it a great choice for lightweight file or directory tasks. While it may not be as feature-packed as some other terminal file managers, it excels in usability and design. If you’re looking for a full-featured file manager, I’d recommend tools like [Yazi](https://github.com/sxyazi/yazi) or others. However, for straightforward tasks with a clean interface, superfile is an excellent option. -------------------------------------------------------------------------------- /website/src/content/docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Troubleshooting 3 | description: Have you encountered any problems? Come here and take a look. 4 | head: 5 | - tag: title 6 | content: Troubleshooting | superfile 7 | --- 8 | 9 | ## My superfile icon doesn't display correctly 10 | 11 | Try these things below: 12 | 13 | - Make sure you already install [nerdfont](https://www.nerdfonts.com/font-downloads) (You can choose whatever font you like!) 14 | - Apply this font to your terminal,This may require different settings depending on the terminal.You can check how to set it up! 15 | 16 | ## Help! My superfile's rendering is all messed up! 17 | 18 | Try these things below: 19 | 20 | - Set your locale to utf-8 21 | - chcp 65001 ( If that's an option for your shell ) 22 | - Set environment variable RUNEWIDTH_EASTASIAN to 0 (`RUNEWIDTH_EASTASIAN=0`) -------------------------------------------------------------------------------- /website/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /website/src/styles/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --sl-font: 'IBM Plex Mono', sans-serif; 3 | } 4 | 5 | /* Dark mode colors. */ 6 | :root { 7 | --sl-color-accent-low: #2c230a; 8 | --sl-color-accent: #846500; 9 | --sl-color-accent-high: #d4c8ab; 10 | --sl-color-white: #ffffff; 11 | --sl-color-gray-1: #eceef2; 12 | --sl-color-gray-2: #c0c2c7; 13 | --sl-color-gray-3: #888b96; 14 | --sl-color-gray-4: #545861; 15 | --sl-color-gray-5: #353841; 16 | --sl-color-gray-6: #24272f; 17 | --sl-color-black: #17181c; 18 | } 19 | /* Light mode colors. */ 20 | :root[data-theme='light'] { 21 | --sl-color-accent-low: #dfd6c0; 22 | --sl-color-accent: #866700; 23 | --sl-color-accent-high: #3f3003; 24 | --sl-color-white: #17181c; 25 | --sl-color-gray-1: #24272f; 26 | --sl-color-gray-2: #353841; 27 | --sl-color-gray-3: #545861; 28 | --sl-color-gray-4: #888b96; 29 | --sl-color-gray-5: #c0c2c7; 30 | --sl-color-gray-6: #eceef2; 31 | --sl-color-gray-7: #f5f6f8; 32 | --sl-color-black: #ffffff; 33 | } 34 | 35 | :root { 36 | --purple-hsl: 205, 60%, 60%; 37 | --overlay-blurple: hsla(var(--purple-hsl), 0.4); 38 | } 39 | 40 | :root[data-theme='light'] { 41 | --purple-hsl: 255, 85%, 65%; 42 | } 43 | 44 | [data-has-hero] .page { 45 | background: linear-gradient(215deg, var(--overlay-blurple), transparent 40%), 46 | radial-gradient(var(--overlay-blurple), transparent 40%) no-repeat -60vw -40vh / 47 | 105vw 200vh, 48 | radial-gradient(var(--overlay-blurple), transparent 65%) no-repeat 50% 49 | calc(100% + 20rem) / 60rem 30rem; 50 | } 51 | 52 | [data-has-hero] header { 53 | border-bottom: 1px solid transparent; 54 | background-color: transparent; 55 | -webkit-backdrop-filter: blur(16px); 56 | backdrop-filter: blur(16px); 57 | } 58 | 59 | [data-has-hero] .hero > img { 60 | filter: drop-shadow(0 0 3rem var(--overlay-blurple)); 61 | } 62 | 63 | [data-page-title] { 64 | font-size: 3rem; 65 | } 66 | 67 | /* date page title onl 2.5rem on mobile devices */ 68 | @media (max-width: 768px) { 69 | [data-page-title] { 70 | font-size: 2.5rem; 71 | } 72 | } 73 | 74 | .card-grid > .card { 75 | border-radius: 10px; 76 | } 77 | 78 | .card > .title { 79 | font-size: 1.3rem; 80 | font-weight: 600; 81 | line-height: 1.2; 82 | } 83 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | --------------------------------------------------------------------------------