[
  {
    "path": ".envrc",
    "content": "use flake\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: yorukot # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**System information (please complete the following information):**\n - OS: [e.g. iOS]\n - Version [e.g. 22]\n - superfile Version [e.g. 1.1.1]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/enhancement-suggestion.md",
    "content": "---\nname: Enhancement suggestion\nabout: Enhance existing designs\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**The part you want to Enhancement**\nPlease briefly describe the part you want to strengthen\n\n**Why it is necessary to enhancement**\nPlease explain why it needs to be enhancement, what are the flaws in the existing design, etc.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: idea\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/pull_request_template.md",
    "content": "# Description\n\nBriefly summarize what this PR changes.  \nInclude any context or motivation behind the change. If it depends on other changes or tools, please mention that too.\n\n# Related Issues\n\nIf this PR fixes or relates to existing issues, list them here.  \nExample: `Fixes #123`\n\n# Screenshots (Optional)\n\nAdd screenshots if this helps reviewers understand the change.\n\n---\n\n# ✅ Pre-Submission Checklist\n\nPlease go through the following steps **before** submitting this PR.  \nYou can delete this section after confirming everything is done.\n\n- [ ] I have run `go fmt ./...` to format the code\n- [ ] I have run `golangci-lint run` and fixed any reported issues\n- [ ] I have tested my changes and verified they work as expected\n- [ ] I have reviewed the diff to make sure I’m not committing any debug logs or TODOs\n- [ ] I have checked that the PR title follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format"
  },
  {
    "path": ".github/renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\"config:recommended\", \":dependencyDashboard\", \"default:automergeDigest\"],\n  \"packageRules\": [\n    {\n      \"matchDatasources\": [\"go\", \"github-releases\", \"github-tags\"],\n      \"matchUpdateTypes\": [\"minor\", \"patch\", \"pin\", \"digest\"],\n      \"automerge\": true,\n      \"paths\": [\"/\"],\n      \"ignorePaths\": [\n        \"website/\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/first-interaction.yml",
    "content": "name: \"Welcome First-Time Contributor\"\n\non:\n  issues:\n    types: [opened]\n  pull_request:\n    types: [opened]\n\npermissions:\n  contents: read\n  issues: write\n  pull-requests: write\n\njobs:\n  greet:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/first-interaction@v3\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          issue_message: |\n            👋 Hi there, and welcome to **superfile**!\n\n            Thanks for opening your first issue. We really appreciate your interest and involvement.\n\n            A maintainer might ask you for more details or clarification to better understand the issue.  \n            That’s totally normal and helps us solve the problem more effectively.\n\n            If you plan to submit a PR, make sure to check our [Contribution Guide](https://github.com/yorukot/superfile/blob/main/CONTRIBUTING.md)\n\n          pr_message: |\n            🎉 Thank you for your first contribution to **superfile**!\n\n            We’re really excited to have you here 🙌\n\n            A maintainer might ask you to make a few changes before we can merge this PR.  \n            That’s totally normal and part of the process. Don’t worry, we’ll help guide you through it.\n\n            👉 Please also take a moment to review our [Contribution Guide](https://github.com/yorukot/superfile/blob/main/CONTRIBUTING.md)\n\n            If you have any questions, feel free to open a [Discussion](https://github.com/yorukot/superfile/discussions) or just ask in the comments!\n"
  },
  {
    "path": ".github/workflows/lint-pr-title.yml",
    "content": "name: \"Lint PR Title\"\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - reopened\n      - synchronize\n\npermissions:\n  pull-requests: write\n\njobs:\n  lint:\n    name: Check PR title format\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@v6\n        id: lint_pr_title\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          types: |\n            feat\n            fix\n            refactor\n            chore\n            docs\n            test\n            style\n            perf\n            ci\n            build\n\n      - uses: marocchino/sticky-pull-request-comment@v2\n        if: always() && (steps.lint_pr_title.outputs.error_message != null)\n        with:\n          header: pr-title-lint-error\n          message: |\n            ⚠️ PR title format invalid.\n\n            superfile uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for squash merges.\n\n            Allowed types:\n            ```\n            feat, fix, refactor, chore, docs, test, style, perf, ci, build\n            ```\n\n            👉 While not required, it’s **recommended** to include a scope like:\n            ```\n            feat(file-preview): support Kitty image protocol\n            fix(renderer): correct ANSI fallback\n            ```\n\n            ---\n            ```\n            ${{ steps.lint_pr_title.outputs.error_message }}\n            ```\n\n      - if: ${{ steps.lint_pr_title.outputs.error_message == null }}\n        uses: marocchino/sticky-pull-request-comment@v2\n        with:\n          header: pr-title-lint-error\n          delete: true\n"
  },
  {
    "path": ".github/workflows/mirror.yml",
    "content": "name: Mirror to Codeberg\npermissions:\n  contents: read\n\non: [push]  # Trigger on push to ANY branch\n\njobs:\n  mirror:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n      \n      - uses: yesolutions/mirror-action@master\n        with:\n          REMOTE: 'https://codeberg.org/yorukot/superfile.git'\n          GIT_USERNAME: yorukot\n          GIT_PASSWORD: ${{ secrets.CODEBERG_TOKEN }}\n          PUSH_ALL_REFS: \"true\""
  },
  {
    "path": ".github/workflows/superfile-build-test.yml",
    "content": "name: Go CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main, develop]\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    name: Build and Test (${{ matrix.os }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n      - name: Setup dependencies (only on linux)\n        if: runner.os == 'Linux'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y exiftool\n          curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.25.5'\n\n      - name: Cache Go modules\n        uses: actions/cache@v5\n        with:\n          path: |\n            ~/.cache/go-build\n            ${{ runner.temp }}/gomodcache\n          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-go-\n\n      - name: Install dependencies\n        run: go mod download\n        env:\n          GOMODCACHE: ${{ runner.temp }}/gomodcache\n\n      - name: Build\n        run: go build -v ./...\n        env:\n          GOMODCACHE: ${{ runner.temp }}/gomodcache\n\n      - name: Test\n        run: go test -v ./...\n        env:\n          GOMODCACHE: ${{ runner.temp }}/gomodcache\n\n      - name: Check gofmt (skip on Windows)\n        if: runner.os != 'Windows'\n        run: |\n          go fmt ./...\n          git diff --exit-code\n\n      - name: golangci-lint (skip on Windows)\n        if: runner.os != 'Windows'\n        uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.8.0\n          args: --timeout 3m\n"
  },
  {
    "path": ".github/workflows/testsuite-run.yml",
    "content": "name: Python Testsuite Run\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main, develop ]\n\npermissions:\n  contents: read\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n\n      - name: Install tmux\n        run: sudo apt-get update && sudo apt-get install -y tmux\n\n      - name: Set up Go\n        uses: actions/setup-go@v6\n        with:\n          go-version: '1.25.5'\n      - name: Build superfile\n        run: ./build.sh\n\n      # timeout command just launches and kills spf, to create the config directories\n      - name: Check installation\n        run: tmux -V; ls; pwd; ls bin/; bin/spf path-list; timeout 1s bin/spf\n        continue-on-error: true\n      - name: set debug\n        run: sed -i 's/debug = false/debug = true/g' /home/runner/.config/superfile/config.toml\n\n      - name: Set up Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.14'\n    \n      - name: Install Dependencies\n        run: pip install -r testsuite/requirements.txt\n\n      - name: Run Tests\n        run: python testsuite/main.py -d\n      \n      - name: Print logs\n        if: always()\n        run: cat ~/.local/state/superfile/superfile.log\n"
  },
  {
    "path": ".github/workflows/update-gomod2nix.yml",
    "content": "name: Update gomod2nix.toml\non:\n  push:\n    paths:\n      - 'go.mod'\n      - 'go.sum'\n\npermissions:\n contents: write\n\njobs:\n dependabot:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - name: Install Nix\n        uses: cachix/install-nix-action@v31\n        with:\n          github_access_token: ${{ secrets.GITHUB_TOKEN }}\n          nix_path: nixpkgs=channel:nixos-unstable\n\n      - name: Update checksum\n        run: |\n          nix develop --extra-experimental-features \"nix-command flakes\" '.#' -c \"gomod2nix\"\n          # git push if we have a diff\n          if [[ -n $(git diff) ]]; then\n            git config --global user.email \"107802416+yorukot@users.noreply.github.com\"\n            git config --global user.name \"yorukot\"\n            git commit -am \"chore: update gomod2nix\"\n            BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's/refs\\/heads\\///g')\n            git push origin HEAD:$BRANCH_NAME\n          fi\n\n"
  },
  {
    "path": ".github/workflows/winget.yml",
    "content": "name: Publish to WinGet\n\non:\n  release:\n    types: [released]\n\njobs:\n  publish:\n    name: Publish WinGet package\n    runs-on: windows-latest\n    steps:\n      - name: Submit package to Windows Package Manager Community Repository\n        uses: vedantmgoyal2009/winget-releaser@v2\n        with:\n          identifier: yorukot.superfile\n          installers-regex: '\\.(zip|msi)$'\n          version: ${{ github.event.release.tag_name }}\n          release-tag: ${{ github.event.release.tag_name }}\n          token: ${{ secrets.WINGET_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.log\nbin/\ndist/\n.idea/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Go test coverage reports\ncoverage.out\ncoverage.html\n\n# environment variables\n.env\n.env.production\n.direnv\n\n# Python virtual environments\nvenv/\n.venv/\ntestsuite/venv/\ntestsuite/.venv/\n\n# macOS-specific files\n.DS_Store\n\n# Agent configs\nCLAUDE.md\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "# Based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322\n# Version used - \n# https://gist.githubusercontent.com/maratori/47a4d00457a92aa426dbd48a18776322/raw/933de4558de48c79322aee44b44a9619eeaba167/.golangci.yml\n\n# Note: Superfile doesn't ensure that this file's version is in sync with\n# the version we use in CI. It can be outdated.\n\n# Additional changes done for superfile\n# - govet shadow analyzer is set to \"strict : false\" settings.\n#   strict : true causes too much noise\n\n\n# This file is licensed under the terms of the MIT license https://opensource.org/license/mit\n# Copyright (c) 2021-2025 Marat Reymers\n\n## Golden config for golangci-lint v2.4.0\n#\n# This is the best config for golangci-lint based on my experience and opinion.\n# It is very strict, but not extremely strict.\n# Feel free to adapt it to suit your needs.\n# If this config helps you, please consider keeping a link to this file (see the next comment).\n\n# Based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322\n\nversion: \"2\"\n\nissues:\n  # Maximum count of issues with the same text.\n  # Set to 0 to disable.\n  # Default: 3\n  max-same-issues: 50\n\nformatters:\n  enable:\n    - goimports # checks if the code and import statements are formatted according to the 'goimports' command\n    - golines # checks if code is formatted, and fixes long lines\n\n    ## you may want to enable\n    # TODO\n    #- gci # checks if code and import statements are formatted, with additional rules\n    #- gofmt # checks if the code is formatted according to 'gofmt' command\n    #- gofumpt # enforces a stricter format than 'gofmt', while being backwards compatible\n    #- swaggo # formats swaggo comments\n\n  # All settings can be found here https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml\n  settings:\n    goimports:\n      # A list of prefixes, which, if set, checks import paths\n      # with the given prefixes are grouped after 3rd-party packages.\n      # Default: []\n      local-prefixes:\n        - github.com/yorukot/superfile\n\n    golines:\n      # Target maximum line length.\n      # Default: 100\n      max-len: 120\n\nlinters:\n  enable:\n    - asasalint # checks for pass []any as any in variadic func(...any)\n    - asciicheck # checks that your code does not contain non-ASCII identifiers\n    - bidichk # checks for dangerous unicode character sequences\n    - bodyclose # checks whether HTTP response body is closed successfully\n    - canonicalheader # checks whether net/http.Header uses canonical header\n    - copyloopvar # detects places where loop variables are copied (Go 1.22+)\n    - cyclop # checks function and package cyclomatic complexity\n    - depguard # checks if package imports are in a list of acceptable packages\n    - dupl # tool for code clone detection\n    - durationcheck # checks for two durations multiplied together\n    - embeddedstructfieldcheck # checks embedded types in structs\n    - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases\n    - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error\n    - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13\n    - exhaustive # checks exhaustiveness of enum switch statements\n    - exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions\n    - fatcontext # detects nested contexts in loops\n    # Not needed. this blocks only fmt.Print family and is not useful right now\n    # - forbidigo # forbids identifiers\n    - funcorder # checks the order of functions, methods, and constructors\n    - funlen # tool for detection of long functions\n    - gocheckcompilerdirectives # validates go compiler directive comments (//go:)\n    - gochecknoglobals # checks that no global variables exist\n    - gochecknoinits # checks that no init functions are present in Go code\n    - gochecksumtype # checks exhaustiveness on Go \"sum types\"\n    - gocognit # computes and checks the cognitive complexity of functions\n    - goconst # finds repeated strings that could be replaced by a constant\n    - gocritic # provides diagnostics that check for bugs, performance and style issues\n    - gocyclo # computes and checks the cyclomatic complexity of functions\n    # Not needed\n    # - godot # checks if comments end in a period\n    - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod\n    - goprintffuncname # checks that printf-like functions are named with f at the end\n    - gosec # inspects source code for security problems\n    - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string\n    - iface # checks the incorrect use of interfaces, helping developers avoid interface pollution\n    - ineffassign # detects when assignments to existing variables are not used\n    - intrange # finds places where for loops could make use of an integer range\n    - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)\n    - makezero # finds slice declarations with non-zero initial length\n    - mirror # reports wrong mirror patterns of bytes/strings usage\n    - mnd # detects magic numbers\n    - musttag # enforces field tags in (un)marshaled structs\n    - nakedret # finds naked returns in functions greater than a specified function length\n    # TODO enable : Many reports. A bit hard to understand the nesting value.\n    # - nestif # reports deeply nested if statements\n    - nilerr # finds the code that returns nil even if it checks that the error is not nil\n    - nilnesserr # reports that it checks for err != nil, but it returns a different nil value error (powered by nilness and nilerr)\n    - nilnil # checks that there is no simultaneous return of nil error and an invalid value\n    - noctx # finds sending http request without context.Context\n    - nolintlint # reports ill-formed or insufficient nolint directives\n    - nonamedreturns # reports all named returns\n    - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL\n    - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative\n    - predeclared # finds code that shadows one of Go's predeclared identifiers\n    - promlinter # checks Prometheus metrics naming via promlint\n    - protogetter # reports direct reads from proto message fields when getters should be used\n    - reassign # checks that package variables are not reassigned\n    - recvcheck # checks for receiver type consistency\n    - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint\n    - rowserrcheck # checks whether Err of rows is checked successfully\n    # Need to use non-root logger. Not really needed.\n    # - sloglint # ensure consistent code style when using log/slog\n    - spancheck # checks for mistakes with OpenTelemetry/Census spans\n    - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed\n    - staticcheck # is a go vet on steroids, applying a ton of static analysis checks\n    - testableexamples # checks if examples are testable (have an expected output)\n    - testifylint # checks usage of github.com/stretchr/testify\n    # [SPF Specific] Not needed - Makes it harder to test unexported package functions.\n    # - testpackage # makes you use a separate _test package\n\n    - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes\n    - unconvert # removes unnecessary type conversions\n    - unparam # reports unused function parameters\n    - unused # checks for unused constants, variables, functions and types\n    - usestdlibvars # detects the possibility to use variables/constants from the Go standard library\n    - usetesting # reports uses of functions with replacement inside the testing package\n    - wastedassign # finds wasted assignment statements\n    - whitespace # detects leading and trailing whitespace\n\n    ## you may want to enable\n    #- arangolint # opinionated best practices for arangodb client\n    - decorder # checks declaration order and count of types, constants, variables and functions\n    # TODO Enable: Many issues right now.\n    #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized\n    #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega\n    # TODO Enable. After fixing all TODOs\n    #- godox # detects usage of FIXME, TODO and other keywords inside comments\n    - goheader # checks is file header matches to pattern\n    #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters\n    - interfacebloat # checks the number of methods inside an interface\n    # TODO Enable: Function Update() is caught\n    #- ireturn # accept interfaces, return concrete types\n    #- noinlineerr # disallows inline error handling `if err := ...; err != nil {`\n    #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated\n    #- tagalign # checks that struct tags are well aligned\n    #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope\n    #- wrapcheck # checks that errors returned from external packages are wrapped\n    #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event\n\n    ## disabled\n    #- containedctx # detects struct contained context.Context field\n    #- contextcheck # [too many false positives] checks the function whether use a non-inherited context\n    #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())\n    #- dupword # [useless without config] checks for duplicate words in the source code\n    #- err113 # [too strict] checks the errors handling expressions\n    #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted\n    #- forcetypeassert # [replaced by errcheck] finds forced type assertions\n    #- gomodguard # [use more powerful depguard] allow and block lists linter for direct Go module dependencies\n    - gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase\n    - grouper # analyzes expression groups\n    - importas # enforces consistent import aliases\n    #- lll # [replaced by golines] reports long lines\n    - maintidx # measures the maintainability index of each function\n    - misspell # [useless] finds commonly misspelled English words in comments\n    #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity\n    #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test\n    #- tagliatelle # checks the struct tags\n    #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers\n    #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines\n    #- wsl_v5 # [too strict and mostly code is not more readable] add or remove empty lines\n\n  # All settings can be found here https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml\n  settings:\n    cyclop:\n      # The maximal code complexity to report.\n      # Default: 10\n      max-complexity: 30\n      # The maximal average package complexity.\n      # If it's higher than 0.0 (float) the check is enabled.\n      # Default: 0.0\n      package-average: 10.0\n\n    depguard:\n      # Rules to apply.\n      #\n      # Variables:\n      # - File Variables\n      #   Use an exclamation mark `!` to negate a variable.\n      #   Example: `!$test` matches any file that is not a go test file.\n      #\n      #   `$all` - matches all go files\n      #   `$test` - matches all go test files\n      #\n      # - Package Variables\n      #\n      #   `$gostd` - matches all of go's standard library (Pulled from `GOROOT`)\n      #\n      # Default (applies if no custom rules are defined): Only allow $gostd in all files.\n      rules:\n        \"deprecated\":\n          # List of file globs that will match this list of settings to compare against.\n          # By default, if a path is relative, it is relative to the directory where the golangci-lint command is executed.\n          # The placeholder '${base-path}' is substituted with a path relative to the mode defined with `run.relative-path-mode`.\n          # The placeholder '${config-path}' is substituted with a path relative to the configuration file.\n          # Default: $all\n          files:\n            - \"$all\"\n          # List of packages that are not allowed.\n          # Entries can be a variable (starting with $), a string prefix, or an exact match (if ending with $).\n          # Default: []\n          deny:\n            - pkg: github.com/golang/protobuf\n              desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules\n            - pkg: github.com/satori/go.uuid\n              desc: Use github.com/google/uuid instead, satori's package is not maintained\n            - pkg: github.com/gofrs/uuid$\n              desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5\n        \"non-test files\":\n          files:\n            - \"!$test\"\n          deny:\n            - pkg: math/rand$\n              desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2\n        \"non-main files\":\n          files:\n            - \"!**/main.go\"\n          deny:\n            - pkg: log$\n              desc: Use log/slog instead, see https://go.dev/blog/slog\n\n    embeddedstructfieldcheck:\n      # Checks that sync.Mutex and sync.RWMutex are not used as embedded fields.\n      # Default: false\n      forbid-mutex: true\n\n    errcheck:\n      # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.\n      # Such cases aren't reported by default.\n      # Default: false\n      check-type-assertions: true\n\n    exhaustive:\n      # Program elements to check for exhaustiveness.\n      # Default: [ switch ]\n      check:\n        - switch\n        - map\n\n    exhaustruct:\n      # List of regular expressions to match type names that should be excluded from processing.\n      # Anonymous structs can be matched by '<anonymous>' alias.\n      # Has precedence over `include`.\n      # Each regular expression must match the full type name, including package path.\n      # For example, to match type `net/http.Cookie` regular expression should be `.*/http\\.Cookie`,\n      # but not `http\\.Cookie`.\n      # Default: []\n      exclude:\n        # std libs\n        - ^net/http.Client$\n        - ^net/http.Cookie$\n        - ^net/http.Request$\n        - ^net/http.Response$\n        - ^net/http.Server$\n        - ^net/http.Transport$\n        - ^net/url.URL$\n        - ^os/exec.Cmd$\n        - ^reflect.StructField$\n        # public libs\n        - ^github.com/Shopify/sarama.Config$\n        - ^github.com/Shopify/sarama.ProducerMessage$\n        - ^github.com/mitchellh/mapstructure.DecoderConfig$\n        - ^github.com/prometheus/client_golang/.+Opts$\n        - ^github.com/spf13/cobra.Command$\n        - ^github.com/spf13/cobra.CompletionOptions$\n        - ^github.com/stretchr/testify/mock.Mock$\n        - ^github.com/testcontainers/testcontainers-go.+Request$\n        - ^github.com/testcontainers/testcontainers-go.FromDockerfile$\n        - ^golang.org/x/tools/go/analysis.Analyzer$\n        - ^google.golang.org/protobuf/.+Options$\n        - ^gopkg.in/yaml.v3.Node$\n      # Allows empty structures in return statements.\n      # Default: false\n      allow-empty-returns: true\n\n    funcorder:\n      # Checks if the exported methods of a structure are placed before the non-exported ones.\n      # Default: true\n      struct-method: false\n\n    funlen:\n      # Checks the number of lines in a function.\n      # If lower than 0, disable the check.\n      # Default: 60\n      lines: 100\n      # Checks the number of statements in a function.\n      # If lower than 0, disable the check.\n      # Default: 40\n      statements: 50\n\n    gochecksumtype:\n      # Presence of `default` case in switch statements satisfies exhaustiveness, if all members are not listed.\n      # Default: true\n      default-signifies-exhaustive: false\n\n    gocognit:\n      # Minimal code complexity to report.\n      # Default: 30 (but we recommend 10-20)\n      min-complexity: 20\n\n    gocritic:\n      # Settings passed to gocritic.\n      # The settings key is the name of a supported gocritic checker.\n      # The list of supported checkers can be found at https://go-critic.com/overview.\n      settings:\n        captLocal:\n          # Whether to restrict checker to params only.\n          # Default: true\n          paramsOnly: false\n        underef:\n          # Whether to skip (*x).method() calls where x is a pointer receiver.\n          # Default: true\n          skipRecvDeref: false\n\n    govet:\n      # Enable all analyzers.\n      # Default: false\n      enable-all: true\n      # Disable analyzers by name.\n      # Run `GL_DEBUG=govet golangci-lint run --enable=govet` to see default, all available analyzers, and enabled analyzers.\n      # Default: []\n      disable:\n        - fieldalignment # too strict\n      # Settings per analyzer.\n      settings:\n        shadow:\n          # Whether to be strict about shadowing; can be noisy.\n          # Default: false\n          strict: false\n\n    inamedparam:\n      # Skips check for interface methods with only a single parameter.\n      # Default: false\n      skip-single-param: true\n\n    mnd:\n      # List of function patterns to exclude from analysis.\n      # Values always ignored: `time.Date`,\n      # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,\n      # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.\n      # Default: []\n      ignored-functions:\n        - args.Error\n        - flag.Arg\n        - flag.Duration.*\n        - flag.Float.*\n        - flag.Int.*\n        - flag.Uint.*\n        - os.Chmod\n        - os.Mkdir.*\n        - os.OpenFile\n        - os.WriteFile\n        - prometheus.ExponentialBuckets.*\n        - prometheus.LinearBuckets\n\n    nakedret:\n      # Make an issue if func has more lines of code than this setting, and it has naked returns.\n      # Default: 30\n      max-func-lines: 0\n\n    nolintlint:\n      # Exclude following linters from requiring an explanation.\n      # Default: []\n      allow-no-explanation: [ funlen, gocognit, golines ]\n      # Enable to require an explanation of nonzero length after each nolint directive.\n      # Default: false\n      require-explanation: true\n      # Enable to require nolint directives to mention the specific linter being suppressed.\n      # Default: false\n      require-specific: true\n\n    perfsprint:\n      # Optimizes into strings concatenation.\n      # Default: true\n      strconcat: false\n\n    reassign:\n      # Patterns for global variable names that are checked for reassignment.\n      # See https://github.com/curioswitch/go-reassign#usage\n      # Default: [\"EOF\", \"Err.*\"]\n      patterns:\n        - \".*\"\n    revive:\n      rules:\n        - name: exported\n          disabled: true\n        - name: package-comments\n          disabled: true\n    rowserrcheck:\n      # database/sql is always checked.\n      # Default: []\n      packages:\n        - github.com/jmoiron/sqlx\n\n    sloglint:\n      # Enforce not using global loggers.\n      # Values:\n      # - \"\": disabled\n      # - \"all\": report all global loggers\n      # - \"default\": report only the default slog logger\n      # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global\n      # Default: \"\"\n      no-global: all\n      # Enforce using methods that accept a context.\n      # Values:\n      # - \"\": disabled\n      # - \"all\": report all contextless calls\n      # - \"scope\": report only if a context exists in the scope of the outermost function\n      # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only\n      # Default: \"\"\n      context: scope\n\n    staticcheck:\n      # SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks\n      # Example (to disable some checks): [ \"all\", \"-SA1000\", \"-SA1001\"]\n      # Default: [\"all\", \"-ST1000\", \"-ST1003\", \"-ST1016\", \"-ST1020\", \"-ST1021\", \"-ST1022\"]\n      checks:\n        - all\n        # Incorrect or missing package comment.\n        # https://staticcheck.dev/docs/checks/#ST1000\n        - -ST1000\n        # Use consistent method receiver names.\n        # https://staticcheck.dev/docs/checks/#ST1016\n        - -ST1016\n        # Omit embedded fields from selector expression.\n        # https://staticcheck.dev/docs/checks/#QF1008\n        - -QF1008\n\n    usetesting:\n      # Enable/disable `os.TempDir()` detections.\n      # Default: false\n      os-temp-dir: true\n\n  exclusions:\n    # Log a warning if an exclusion rule is unused.\n    # Default: false\n    warn-unused: false\n    # Predefined exclusion rules.\n    # Default: []\n    presets:\n      - std-error-handling\n      - common-false-positives\n    # Excluding configuration per-path, per-linter, per-text and per-source.\n    rules:\n      - source: 'TODO'\n        linters: [ godot ]\n      - text: 'should have a package comment'\n        linters: [ revive ]\n      - text: 'exported \\S+ \\S+ should have comment( \\(or a comment on this block\\))? or be unexported'\n        linters: [ revive ]\n      - text: 'package comment should be of the form \".+\"'\n        source: '// ?(nolint|TODO)'\n        linters: [ revive ]\n      - text: 'comment on exported \\S+ \\S+ should be of the form \".+\"'\n        source: '// ?(nolint|TODO)'\n        linters: [ revive, staticcheck ]\n      - path: '_test\\.go'\n        linters:\n          - bodyclose\n          - dupl\n          - errcheck\n          - funlen\n          - goconst\n          - gosec\n          - gosmopolitan\n          - noctx\n          - wrapcheck\n      - text: 'ST102[0-2]: comment.*should be of the form'\n        linters: [staticcheck]\n      # TODO: Fix this\n      - text: 'os/exec\\.Command must not be called'\n        linters: [noctx]\n      # Stores globally accessible icon strings\n      - path: 'src/config/icon/icon.go'\n        linters:\n          - gochecknoglobals\n      # Stores prerendered styles\n      - path: 'src/internal/common/style.go'\n        linters:\n          - gochecknoglobals\n      # Some fixed variables like default config paths, version, etc.\n      - path: 'src/config/fixed_variable.go'\n        linters:\n          - gochecknoglobals\n      # Stores predefined variables, like re-used non-const strings\n      - path: 'src/internal/common/predefined_variable.go'\n        linters:\n          - gochecknoglobals\n      # Global variables storing config\n      - path: 'src/internal/common/default_config.go'\n        linters:\n          - gochecknoglobals    \n      # Type defining config.toml and hotkey.toml\n      - path: 'src/internal/common/config_type.go'\n        linters:\n          - golines\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to superfile\n\nWelcome to **superfile**! This guide will help you get started contributing to the project, whether you're fixing bugs, building features, or just sharing ideas.\n\nThere are many ways to contribute:\n\n* Reporting bugs\n* Fixing issues\n* Adding a theme\n* Suggesting and implementing new features\n* Sharing ideas or feedback\n\n---\n\n## 🐞 Issues\n\n### Found a bug?\n\nCheck if there's already an open or closed issue for it. If not, open a new one and describe the problem clearly.\n\n### Want to fix an issue?\n\n1. Fork this repository\n2. Create a new branch for the issue you're working on\n3. Commit your changes with clear messages\n4. Open a pull request (PR) with a description of the problem and your solution\n\nMaintainers may request changes before merging.\n\n---\n\n## 🎨 Adding a Theme\n\nBefore starting, make sure the theme you want to add doesn’t already exist.\n\n1. Copy an existing theme's `.toml` file as a base\n2. Customize it to your needs\n3. Test it by editing your `~/.config/superfile/config/config.toml`\n4. When ready, submit a pull request\n5. To ensure the theme looks consistent and functions properly, please include the following screenshots in your PR:\n- Full view of superfile (Including sidebar, file previewer, process panel, metadata panel, and clipboard panel )\n    - Make sure that file previewer is non empty, process panel has at least one process, and clipboard has at least one entry\n- Add a screenshot of these individual panel being focused (To make sure border focus color is good)\n    - Sidebar\n    - Processbar\n- Add a screenshot of help menu (Press ?)\n- Add a screenshot of popup that opens when you create a new file (Ctrl+n)\n- Add a screenshot of image being preview using your theme.\n- Add a screenshot of successful and unsuccessful shell command.\n<details>\n<summary>Example:</summary>\n\n- Full view of superfile (Including sidebar, file previewer, process panel, metadata panel, and clipboard panel)\n\n  - Make sure that file previewer is non empty, process panel has at least one process, and clipboard has at least one entry\n\n  ![Full view of superfile](asset/theme-example/1.png)\n\n- Add a screenshot of these individual panels being focused (To make sure border focus color is good)\n\n  - Sidebar\n  - Processbar\n\n  ![Sidebar focused](asset/theme-example/2.png)\n\n  ![Processbar focused](asset/theme-example/3.png)\n\n- Add a screenshot of help menu (Press `?`)\n\n  ![Help menu](asset/theme-example/4.png)\n\n- Add a screenshot of popup that opens when you create a new file (Ctrl+n)\n\n  ![New file popup](asset/theme-example/5.png)\n\n- Add a screenshot of image being previewed using your theme\n\n  ![Image preview](asset/theme-example/6.png)\n\n- Add a screenshot of successful and unsuccessful shell command\n\n  ![Successful shell command](asset/theme-example/7.png)\n\n  ![Failed shell command](asset/theme-example/8.png)\n\n</details>\n\n\n---\n\n## 💡 Sharing Ideas\n\nGot a new idea? Awesome!\n\n1. Check if similar ideas exist in Discussions or Issues\n2. Open a discussion at: [https://github.com/yorukot/superfile/discussions](https://github.com/yorukot/superfile/discussions)\n3. If you want to implement it yourself, follow the PR steps above\n\n---\n\n## 🧩 Don’t Know Where to Start?\n\nCheck out GitHub’s official guide:\n[https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project)\n\nStill unsure? Open a discussion — we’re happy to help.\n\n---\n\n## ✅ Pull Request Checklist\n\nPlease make sure your PR follows these steps:\n\n* [ ] I have run `go fmt ./...` to format the code\n* [ ] I have run `golangci-lint run` and fixed any reported issues\n* [ ] I have tested my changes and verified they work as expected\n* [ ] I have reviewed the diff to make sure I’m not committing any debug logs or TODOs\n* [ ] I have filled out the PR template with description, context, and screenshots if needed\n- [ ] I have checked that the PR title follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format\n\n---\n\n## 🙏 Thank You\n\nThank you for contributing to superfile! We appreciate every issue, pull request, and idea. Your help makes this project better for everyone.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 - Yorukot\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: all build test lint clean dev testsuite help\n\n# Default target\nall: dev\n\n# Development workflow (equivalent to ./dev.sh)\ndev:\n\t@FORCE_COLOR=1 ./dev.sh\n\n# Build only\nbuild:\n\t@FORCE_COLOR=1 ./dev.sh --skip-tests\n\n# Run tests\ntest:\n\t@go test ./...\n\n# Run linter\nlint:\n\t@golangci-lint run\n\n# Run full testsuite\ntestsuite:\n\t@FORCE_COLOR=1 ./dev.sh --testsuite\n\n# Clean build artifacts\nclean:\n\t@rm -rf ./bin/\n\n# Show help\nhelp:\n\t@echo \"Available targets:\"\n\t@echo \"  all       - Run full development workflow (default)\"\n\t@echo \"  dev       - Run development workflow (./dev.sh)\"\n\t@echo \"  build     - Build only (skip tests)\"\n\t@echo \"  test      - Run unit tests only\"\n\t@echo \"  lint      - Run linter only\"\n\t@echo \"  testsuite - Run full testsuite\"\n\t@echo \"  clean     - Clean build artifacts\"\n\t@echo \"  help      - Show this help\"\n\t@echo \"\"\n\t@echo \"For more options, use: ./dev.sh --help\" "
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\n<p>\n  <h4>\n    <a href=\"https://ko-fi.com/yorukot\">superfile is supported by the community.</a>\n  </h4>\n<div align=\"center\" markdown=\"1\">\n   <sup>Special thanks to:</sup>\n   <br>\n   <br>\n   <a href=\"https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=superfile\">\n      <img alt=\"Warp sponsorship\" width=\"300\" src=\"/asset/warp.png\">\n   </a>\n\n### [Warp, the AI terminal for developers](https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=superfile)\n[Available for macOS, Linux, & Windows](https://www.warp.dev/?utm_source=github&utm_medium=referral&utm_campaign=superfile)<br>\n\n</div>\n<hr>\n\n</div>\n\n<div align=\"center\">\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"/asset/superfilelogowhite.png\" />\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"/asset/superfilelogoblack.png\" />\n  <img alt=\"superfile LOGO\" src=\"/asset/superfilelogowhite.png\" />\n</picture>\n\n[![Go Report Card](https://goreportcard.com/badge/github.com/yorukot/superfile)](https://goreportcard.com/report/github.com/yorukot/superfile)\n[![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/yorukot/superfile/refs/heads/main/LICENSE)\n[![Discord Link](https://img.shields.io/discord/1338415256875307110?label=discord&logo=discord&logoColor=white)](https://discord.gg/YYtJ23Du7B)\n[![Release](https://img.shields.io/github/v/release/yorukot/superfile.svg?style=flat-square)](https://github.com/yorukot/superfile/releases/latest)\n[![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/yorukot/superfile?utm_source=oss&utm_medium=github&utm_campaign=yorukot%2Fsuperfile&labelColor=171717&color=FF570A&&label=CodeRabbit+Reviews)](https://www.coderabbit.ai/)\n\n![](/asset/demo.png)\n\n</div>\n\n## Demo\n\n| Perform common operations |\n| ------------------------- |\n| ![](/asset/demo.gif)      |\n\n## Content\n\n- [Installation](#installation)\n- [Build](#build)\n- [Supported Systems](#supported-systems)\n- [Tutorial](#tutorial)\n- [Plugins](#plugins)\n- [Themes](#themes)\n- [Hotkeys](#hotkeys)\n- [Notes](#notes)\n- [Contributing](#contributing)\n- [Troubleshooting](#troubleshooting)\n- [Thanks](#thanks)\n  - [Support](#Support)\n  - [Core maintainer](#core-maintainer)\n  - [Contributors](#contributors)\n  - [Powered by](#powered-by)\n  - [Star History](#star-history)\n\n## Installation\n\n### macOS and Linux\n\n```bash\nbash -c \"$(curl -sLo- https://superfile.dev/install.sh)\"\n```\nIf you want to inspect the script, see : [install.sh](./website/public/install.sh)\n\n### Windows\n\n#### Powershell\n```powershell\npowershell -ExecutionPolicy Bypass -Command \"Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://superfile.dev/install.ps1'))\"\n```\nIf you want to inspect the script, see : [install.ps1](./website/public/install.ps1)\n\n#### [Winget](https://winget.run/)\n```powershell\nwinget install --id yorukot.superfile\n```\n\n#### [Scoop](https://scoop.sh/)\n```\nscoop install superfile\n```\n\n### More installation methods\n[Click me to check on how to install](https://superfile.dev/getting-started/installation/)\n\n## Build\n\nYou can build the source code yourself by using these steps:\n\n**Requirements**\n\n- [golang](https://go.dev/doc/install)\n\n**Build Steps**\n\nClone this repository using the following command:\n\n```\ngit clone https://github.com/yorukot/superfile.git --depth=1\n```\n\nEnter the downloaded directory:\n\n```bash\ncd superfile\n```\n\n### For macOS/Linux\nRun the `build.sh` file:\n\n```bash\n./build.sh\n```\n\nAdd the binary file to your $PATH, e.g., in `/usr/local/bin`:\n\n```bash\nsudo mv ./bin/spf /usr/local/bin\n```\n\n### For Windows\n\n```bash\ngo build -o bin/spf.exe\n```\n\nEdit System Environment Variables and add superfile repo's `bin` directory to your PATH  \n\n## Start superfile\n\n```bash\nspf\n```\n\n## Supported Systems\n\n- \\[x\\] Linux\n- \\[x\\] macOS\n- \\[x\\] Windows (Not fully supported yet)\n\n## Tutorial\n\nAfter you install superfile, you can go [here](https://superfile.dev/getting-started/tutorial/) to briefly understand how to use superfile!\n\n## Plugins\n\n[Click me to the plugins wiki](https://superfile.dev/list/plugin-list/)\n\n## Themes\n\n[Click me to the theme wiki](https://superfile.dev/configure/custom-theme/)\n\n## Hotkeys\n\n> [!WARNING]\n> If you are vim/nvim user please change your default hotkeys config to vim version!\n\n[**Click me to see the hotkey wiki**](https://superfile.dev/configure/custom-hotkeys/)\n\n## Notes\n\nWe have an auto update functionality, that fetches superfile's latest released version from github (if last timestamp of last version check was less than 24 hours) and prints a prompt to user, if there is a newer version available.\n\nYou can turn this off, by setting `auto_check_update` to false in superfile config. [**Click me to see the config wiki**](https://superfile.dev/configure/superfile-config/) \n\n## Troubleshooting\n\n[**Click me to see common problem fix**](https://superfile.dev/troubleshooting/)\n\n## Uninstalling\n\n### macOS and Linux\n\nOn macOS and Linux, you can uninstall superfile by simply removing the binary. If you installed superfile with sudo, run:\n\n```bash\nsudo rm /usr/local/bin/spf\n```\n\nIf you installed superfile without sudo, run:\n\n```bash\nrm ~/.local/bin/spf\n```\n\nIf you don't rember, just try removing both.\n\n\n### Window\n\nTo uninstall superfile on Windows, use this powershell script.\n\n```powershell\npowershell -ExecutionPolicy Bypass -Command \"Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://superfile.dev/uninstall.ps1'))\"\n```\n\n## Contributing\n\nIf you want to contribute please follow the [contribution guide](./CONTRIBUTING.md)\n\n[**Click me to see changelog**](https://superfile.dev/changelog)\n\n## Thanks\n\n### Support\n\n- a Star on my GitHub repository would be nice 🌟\n- You can buy a coffee for me 💖\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/G2G1JEGGC)\n\n### Core maintainer\n\n> We welcome anyone who wants to become a core maintainer. Feel free to reach out!\n\n- **[@yorukot](https://github.com/yorukot)** - Original author and maintainer\n- **[@lazysegtree](https://github.com/lazysegtree)** - Core maintainer\n\n### Contributors\n\n**Thanks to all the contributors for making this project even greater!**\n\n<a href=\"https://github.com/yorukot/superfile/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=yorukot/superfile\" />\n</a>\n\n### Powered by\n\n<a href=\"https://jb.gg/OpenSource\"><img alt=\"JetBrains logo\" align=\"right\" width=\"200\" src=\"https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg\"></a>\n\nThanks to JetBrains team for providing open-source licenses to support the maintenance of superfile.\n\n### Star History\n\n**THANKS FOR All OF YOUR STARS!**\nYour stars are my motivation to keep updating!\n\n<a href=\"https://star-history.com/#yorukot/superfile&Timeline\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=yorukot/superfile&type=Timeline&theme=dark\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=yorukot/superfile&type=Timeline\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=yorukot/superfile&type=Timeline\" />\n </picture>\n</a>\n\n<div align=\"center\">\n\n## ༼ つ ◕_◕ ༽つ  Please share.\n\n</div>\n"
  },
  {
    "path": "asset/spf.desktop",
    "content": "[Desktop Entry]\nName=spf\nGenericName=superfile\nComment=fancy and modern terminal file manager\nType=Application\nMimeType=inode/directory\nIcon=utilities-terminal\nTerminal=true\nTryExec=spf\nExec=spf %u\nCategories=Utility;System;FileTools;FileManager;Filesystem;ConsoleOnly\nKeywords=File;Manager;Explorer\n"
  },
  {
    "path": "build.sh",
    "content": "#!/usr/bin/env bash\n\n# build the app\nCGO_ENABLED=0 go build -o ./bin/spf\n"
  },
  {
    "path": "cd_on_quit/cd_on_quit.fish",
    "content": "function spf\n    set os $(uname -s)\n\n    if test \"$os\" = \"Linux\"\n        set spf_last_dir \"$HOME/.local/state/superfile/lastdir\"\n    end\n\n    if test \"$os\" = \"Darwin\"\n        set spf_last_dir \"$HOME/Library/Application Support/superfile/lastdir\"\n    end\n\n    command spf $argv\n\n    if test -f \"$spf_last_dir\"\n        source \"$spf_last_dir\"\n        rm -f -- \"$spf_last_dir\" >> /dev/null\n    end\nend\n"
  },
  {
    "path": "cd_on_quit/cd_on_quit.ps1",
    "content": "function spf() {\n    param (\n        [string[]]$Params\n    )\n    $spf_location = [Environment]::GetFolderPath(\"LocalApplicationData\") + \"\\Programs\\superfile\\spf.exe\"\n    $SPF_LAST_DIR_PATH = [Environment]::GetFolderPath(\"LocalApplicationData\") + \"\\superfile\\lastdir\"\n\n    & $spf_location @Params\n\n    if (Test-Path $SPF_LAST_DIR_PATH) {\n        $SPF_LAST_DIR = Get-Content -Path $SPF_LAST_DIR_PATH\n        Invoke-Expression $SPF_LAST_DIR\n        Remove-Item -Force $SPF_LAST_DIR_PATH\n    }\n}\n"
  },
  {
    "path": "cd_on_quit/cd_on_quit.sh",
    "content": "spf() {\n    os=$(uname -s)\n\n    # Linux\n    if [[ \"$os\" == \"Linux\" ]]; then\n        export SPF_LAST_DIR=\"${XDG_STATE_HOME:-$HOME/.local/state}/superfile/lastdir\"\n    fi\n\n    # macOS\n    if [[ \"$os\" == \"Darwin\" ]]; then\n        export SPF_LAST_DIR=\"$HOME/Library/Application Support/superfile/lastdir\"\n    fi\n\n    command spf \"$@\"\n\n    [ ! -f \"$SPF_LAST_DIR\" ] || {\n        . \"$SPF_LAST_DIR\"\n        rm -f -- \"$SPF_LAST_DIR\" > /dev/null\n    }\n}\n"
  },
  {
    "path": "dev.sh",
    "content": "#!/usr/bin/env bash\n\nset -e  # Exit on any error\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# Check if colors should be disabled\nif [ \"$FORCE_COLOR\" != \"1\" ] && ([ -n \"$MAKEFLAGS\" ] || [ \"$TERM\" = \"dumb\" ] || [ ! -t 1 ]); then\n    # Disable colors when running under Make or non-interactive\n    RED=''\n    GREEN=''\n    YELLOW=''\n    BLUE=''\n    NC=''\nfi\n\n# Default values\nRUN_TESTSUITE=false\nSKIP_TESTS=false\nVERBOSE=false\nUSE_GLOBAL_ENV=false\n\n# Function to print colored output\nprint_step() {\n    printf \"${BLUE}==>${NC} %s\\n\" \"$1\"\n}\n\nprint_success() {\n    printf \"${GREEN}✓${NC} %s\\n\" \"$1\"\n}\n\nprint_warning() {\n    printf \"${YELLOW}⚠${NC} %s\\n\" \"$1\"\n}\n\nprint_error() {\n    printf \"${RED}✗${NC} %s\\n\" \"$1\"\n}\n\n# Function to setup Python virtual environment\nsetup_venv() {\n    local venv_path=\"$1\"\n    \n    # Remove existing incomplete virtual environment if activate script is missing or pip is broken\n    if [ -d \"$venv_path\" ]; then\n        if [ ! -f \"$venv_path/bin/activate\" ]; then\n            print_warning \"Removing incomplete virtual environment (missing activate)...\"\n            rm -rf \"$venv_path\"\n        else\n            # Test if pip works in the existing virtual environment\n            if ! (source \"$venv_path/bin/activate\" && python -m pip --version > /dev/null 2>&1); then\n                print_warning \"Removing broken virtual environment (pip not working)...\"\n                rm -rf \"$venv_path\"\n            fi\n        fi\n    fi\n    \n    if [ ! -d \"$venv_path\" ]; then\n        print_step \"Creating Python virtual environment...\"\n        if python3 -m venv \"$venv_path\" --upgrade-deps; then\n            print_success \"Virtual environment created at $venv_path\"\n        else\n            print_error \"Failed to create virtual environment\"\n            return 1\n        fi\n    else\n        print_step \"Using existing virtual environment at $venv_path\"\n    fi\n    \n    # Check if activate script exists and has proper permissions\n    if [ ! -f \"$venv_path/bin/activate\" ]; then\n        print_error \"Virtual environment activate script not found at $venv_path/bin/activate\"\n        return 1\n    fi\n    \n    # Ensure activate script has execution permissions\n    chmod +x \"$venv_path/bin/activate\"\n    \n    # Activate virtual environment\n    source \"$venv_path/bin/activate\"\n    \n    # Verify that we're in the virtual environment\n    if [ -z \"$VIRTUAL_ENV\" ]; then\n        print_error \"Failed to activate virtual environment\"\n        return 1\n    fi\n    \n    # Upgrade pip to latest version\n    print_step \"Upgrading pip in virtual environment...\"\n    if python -m pip install --upgrade pip > /dev/null 2>&1; then\n        print_success \"Pip upgraded successfully\"\n    else\n        print_warning \"Failed to upgrade pip - continuing anyway\"\n    fi\n    \n    return 0\n}\n\n# Function to cleanup virtual environment\ncleanup_venv() {\n    if [ -n \"$VIRTUAL_ENV\" ]; then\n        deactivate 2>/dev/null || true\n    fi\n}\n\n# Setup trap for cleanup on exit/interruption\ntrap cleanup_venv EXIT INT TERM\n\n# Function to show usage\nusage() {\n    echo \"Usage: $0 [OPTIONS]\"\n    echo \"\"\n    echo \"A comprehensive script for formatting, testing, and building superfile\"\n    echo \"\"\n    echo \"OPTIONS:\"\n    echo \"  -t, --testsuite     Run integration testsuite after unit tests\"\n    echo \"  -s, --skip-tests    Skip unit tests (only format, lint, and build)\"\n    echo \"  -v, --verbose       Enable verbose output\"\n    echo \"  --use-global-env    Use global Python environment instead of virtual environment\"\n    echo \"  -h, --help          Show this help message\"\n    echo \"\"\n    echo \"STEPS PERFORMED:\"\n    echo \"  1. Tidy Go modules\"\n    echo \"  2. Format code with 'go fmt'\"\n    echo \"  3. Run golangci-lint\"\n    echo \"  4. Run unit tests (unless --skip-tests)\"\n    echo \"  5. Run integration testsuite (if --testsuite)\"\n    echo \"  6. Build spf binary\"\n}\n\n# Parse command line arguments\nwhile [[ $# -gt 0 ]]; do\n    case $1 in\n        -t|--testsuite)\n            RUN_TESTSUITE=true\n            shift\n            ;;\n        -s|--skip-tests)\n            SKIP_TESTS=true\n            shift\n            ;;\n        -v|--verbose)\n            VERBOSE=true\n            shift\n            ;;\n        --use-global-env)\n            USE_GLOBAL_ENV=true\n            shift\n            ;;\n        -h|--help)\n            usage\n            exit 0\n            ;;\n        *)\n            echo \"Unknown option: $1\"\n            usage\n            exit 1\n            ;;\n    esac\ndone\n\n# Set verbose flag for commands if requested\nVERBOSE_FLAG=\"\"\nif [ \"$VERBOSE\" = true ]; then\n    VERBOSE_FLAG=\"-v\"\nfi\n\nprintf \"${BLUE}🚀 Starting superfile development workflow${NC}\\n\"\necho \"\"\n\n# Step 1: Tidy up the go mod\nprint_step \"Tidying Go modules...\"\nif go mod tidy $VERBOSE_FLAG; then\n    print_success \"Go modules tidied\"\nelse\n    print_error \"Failed to tidy Go modules\"\n    exit 1\nfi\n\n# Step 2: Format the code\nprint_step \"Formatting Go code...\"\nif go fmt ./...; then\n    print_success \"Code formatted\"\nelse\n    print_error \"Failed to format code\"\n    exit 1\nfi\n\n# Step 3: Run the linter\nprint_step \"Running golangci-lint...\"\nif golangci-lint run; then\n    print_success \"Linting passed\"\nelse\n    print_error \"Linting failed\"\n    exit 1\nfi\n\n# Step 4: Run unit tests (unless skipped)\nif [ \"$SKIP_TESTS\" = false ]; then\n    print_step \"Running unit tests...\"\n    if [ \"$VERBOSE\" = true ]; then\n        if go test -v ./...; then\n            print_success \"Unit tests passed\"\n        else\n            print_error \"Unit tests failed\"\n            exit 1\n        fi\n    else\n        if go test ./...; then\n            print_success \"Unit tests passed\"\n        else\n            print_error \"Unit tests failed\"\n            exit 1\n        fi\n    fi\nelse\n    print_warning \"Skipping unit tests\"\nfi\n\n# Step 5: Run integration testsuite (if requested)\nif [ \"$RUN_TESTSUITE\" = true ]; then\n    print_step \"Running integration testsuite...\"\n\n    # Check if Python is available\n    if ! command -v python3 &> /dev/null; then\n        print_error \"Python3 is required for testsuite but not found\"\n        exit 1\n    fi\n\n    # Check if testsuite requirements are installed\n    if [ ! -f \"testsuite/requirements.txt\" ]; then\n        print_error \"testsuite/requirements.txt not found\"\n        exit 1\n    fi\n\n    cd testsuite\n\n    # Use virtual environment by default, global environment if requested\n    if [ \"$USE_GLOBAL_ENV\" = true ]; then\n        # Install requirements globally\n        print_step \"Installing testsuite requirements globally...\"\n        print_warning \"Using global Python environment - consider removing --use-global-env flag to use virtual environment\"\n        if python3 -m pip install -r requirements.txt > /dev/null 2>&1; then\n            print_success \"Testsuite requirements installed globally\"\n        else\n            print_warning \"Failed to install testsuite requirements - continuing anyway\"\n        fi\n    else\n        # Setup virtual environment (default behavior)\n        VENV_PATH=\"./venv\"\n        \n        if ! setup_venv \"$VENV_PATH\"; then\n            print_error \"Failed to setup virtual environment\"\n            cd ..\n            exit 1\n        fi\n        \n        # Install requirements in virtual environment\n        print_step \"Installing testsuite requirements in virtual environment...\"\n        if python -m pip install -r requirements.txt > /dev/null 2>&1; then\n            print_success \"Testsuite requirements installed in virtual environment\"\n        else\n            print_error \"Failed to install testsuite requirements in virtual environment\"\n            cd ..\n            exit 1\n        fi\n    fi\n\n    # Run the testsuite\n    if [ \"$VERBOSE\" = true ]; then\n        if python3 main.py --debug; then\n            print_success \"Integration testsuite passed\"\n        else\n            print_error \"Integration testsuite failed\"\n            cd ..\n            exit 1\n        fi\n    else\n        if python3 main.py; then\n            print_success \"Integration testsuite passed\"\n        else\n            print_error \"Integration testsuite failed\"\n            cd ..\n            exit 1\n        fi\n    fi\n\n    cd ..\nfi\n\n# Step 6: Build the app\nprint_step \"Building spf binary...\"\nif CGO_ENABLED=0 go build -o ./bin/spf; then\n    print_success \"Build completed successfully\"\nelse\n    print_error \"Build failed\"\n    exit 1\nfi\n\necho \"\"\nprintf \"${GREEN}🎉 All steps completed successfully!${NC}\\n\"\nprintf \"${BLUE}Binary location:${NC} ./bin/spf\\n\"\n\n# Show binary info\nif [ -f \"./bin/spf\" ]; then\n    BINARY_SIZE=$(du -h ./bin/spf | cut -f1)\n    printf \"${BLUE}Binary size:${NC} $BINARY_SIZE\\n\"\nfi\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"A fancy, pretty terminal file manager\";\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixos-unstable\";\n\n    flake-utils.url = \"github:numtide/flake-utils\";\n\n    flake-compat.url = \"github:edolstra/flake-compat\";\n    flake-compat.flake = false;\n\n    gomod2nix.url = \"github:nix-community/gomod2nix\";\n    gomod2nix.inputs.nixpkgs.follows = \"nixpkgs\";\n    gomod2nix.inputs.flake-utils.follows = \"flake-utils\";\n  };\n\n  outputs = inputs @ {...}:\n    inputs.flake-utils.lib.eachDefaultSystem\n    (\n      system: let\n        overlays = [\n          inputs.gomod2nix.overlays.default\n        ];\n        pkgs = import inputs.nixpkgs {\n          inherit system overlays;\n        };\n      in rec {\n        packages = rec {\n          superfile = pkgs.buildGoApplication {\n            pname = \"superfile\";\n            version = \"1.5.0\";\n            src = ./.;\n            modules = ./gomod2nix.toml;\n\n            nativeCheckInputs = with pkgs; [\n              zoxide\n              exiftool\n              writableTmpDirAsHomeHook\n            ];\n          };\n\n          default = superfile;\n        };\n\n        apps = rec {\n          superfile = {\n            type = \"app\";\n            program = \"${packages.superfile}/bin/superfile\";\n          };\n          default = superfile;\n        };\n\n        devShells = {\n          default = pkgs.mkShell {\n            packages = with pkgs; [\n              ## golang\n              delve\n              go-outline\n              go\n              golangci-lint\n              gopkgs\n              gopls\n              gotools\n              nix\n              gomod2nix\n              nixpkgs-fmt\n            ];\n          };\n        };\n      }\n    );\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/yorukot/superfile\n\ngo 1.25.5\n\nrequire (\n\tgithub.com/adrg/xdg v0.5.3\n\tgithub.com/alecthomas/chroma/v2 v2.21.1\n\tgithub.com/atotto/clipboard v0.1.4\n\tgithub.com/barasher/go-exiftool v1.10.0\n\tgithub.com/charmbracelet/bubbles v0.21.0\n\tgithub.com/charmbracelet/bubbletea v1.3.10\n\tgithub.com/charmbracelet/lipgloss v1.1.0\n\tgithub.com/charmbracelet/x/ansi v0.10.1\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/fvbommel/sortorder v1.1.0\n\tgithub.com/hymkor/trash-go v0.2.0\n\tgithub.com/lazysegtree/go-zoxide v0.1.0\n\tgithub.com/lithammer/shortuuid v3.0.0+incompatible\n\tgithub.com/muesli/termenv v0.16.0\n\tgithub.com/reinhrst/fzf-lib v0.9.0\n\tgithub.com/rkoesters/xdg v0.0.1\n\tgithub.com/shirou/gopsutil/v4 v4.25.12\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/urfave/cli/v3 v3.6.1\n\tgolang.org/x/image v0.35.0\n\tgolang.org/x/mod v0.31.0\n\tgolift.io/xtractr v0.2.2\n)\n\nrequire (\n\tgithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect\n\tgithub.com/charmbracelet/x/term v0.2.1 // indirect\n\tgithub.com/clipperhouse/stringish v0.1.1 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.3.0 // indirect\n\tgithub.com/ebitengine/purego v0.9.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect\n\tgolang.org/x/term v0.18.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\nrequire (\n\tgithub.com/andybalholm/brotli v1.0.5 // indirect\n\tgithub.com/bodgit/plumbing v1.3.0 // indirect\n\tgithub.com/bodgit/sevenzip v1.4.0 // indirect\n\tgithub.com/bodgit/windows v1.0.1 // indirect\n\tgithub.com/connesc/cipherio v0.2.1 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/kdomanski/iso9660 v0.3.3 // indirect\n\tgithub.com/klauspost/compress v1.16.3 // indirect\n\tgithub.com/nwaples/rardecode v1.1.3 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.17 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/yorukot/ansichroma v0.1.0\n\tgo4.org v0.0.0-20230225012048-214862532bf5 // indirect\n)\n\nrequire (\n\tgithub.com/BourgeoisBear/rasterm v1.1.2\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/charmbracelet/harmonica v0.2.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/disintegration/imaging v1.6.2\n\tgithub.com/go-ole/go-ole v1.2.6 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/reflow v0.3.0\n\tgithub.com/pelletier/go-toml/v2 v2.2.4\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgolang.org/x/sys v0.38.0 // indirect\n\tgolang.org/x/text v0.33.0\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/BourgeoisBear/rasterm v1.1.2 h1:hWHZBZ45N366uNSqxWFYBV0y19q8fXRXADhPkoLF4Ss=\ngithub.com/BourgeoisBear/rasterm v1.1.2/go.mod h1:Ifd+To5s/uyUiYx+B4fxhS8lUNwNLSxDBjskmC5pEyw=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=\ngithub.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=\ngithub.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=\ngithub.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=\ngithub.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=\ngithub.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=\ngithub.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=\ngithub.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=\ngithub.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=\ngithub.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=\ngithub.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=\ngithub.com/bodgit/sevenzip v1.4.0 h1:OPUy/dCA9KvDTxcwQQYkv/W7kHmxhP4B3vpW8S02WlA=\ngithub.com/bodgit/sevenzip v1.4.0/go.mod h1:0WaxeLofKpADVzngQXcMoXb98627kLDiqTZyDLaVxiA=\ngithub.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=\ngithub.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=\ngithub.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=\ngithub.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=\ngithub.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=\ngithub.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=\ngithub.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=\ngithub.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=\ngithub.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=\ngithub.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=\ngithub.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=\ngithub.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=\ngithub.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=\ngithub.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/hymkor/trash-go v0.2.0 h1:t51zidKT8WuMTeeiMBsJxx+BDwnJtaaf/ckpjle2GOE=\ngithub.com/hymkor/trash-go v0.2.0/go.mod h1:pZ07qBUuGdTWPdymNtE97NAXHDY5W/b5szvoBVOhJ3U=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kdomanski/iso9660 v0.3.3 h1:cNwM9L2L1Hzc5hZWGy6fPJ92UyWDccaY69DmEPlfDNY=\ngithub.com/kdomanski/iso9660 v0.3.3/go.mod h1:K+UlIGxKgtrdAWyoigPnFbeQLVs/Xudz4iztWFThBwo=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=\ngithub.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=\ngithub.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lazysegtree/go-zoxide v0.1.0 h1:gL11AWS9fJDuB7FYxsh+ohf8ozMXeK31E/PC6f9jNBc=\ngithub.com/lazysegtree/go-zoxide v0.1.0/go.mod h1:C1K2SDM4iHkQeFVrWBiRX3tU9jpAz6BrnoHQYSiHZl0=\ngithub.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w=\ngithub.com/lithammer/shortuuid v3.0.0+incompatible/go.mod h1:FR74pbAuElzOUuenUHTK2Tciko1/vKuIKS9dSkDrA4w=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=\ngithub.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=\ngithub.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/reinhrst/fzf-lib v0.9.0 h1:P57AkpmDOmRhuBTDclvasclcOY7kNVWJUVVEBEB9kCA=\ngithub.com/reinhrst/fzf-lib v0.9.0/go.mod h1:06+ssO8WzlXxOUT7RtLo2gXf9tSR7HU8fbBgFX1VP6k=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rkoesters/xdg v0.0.1 h1:RmfYxghVvIsb4d51u5LtNOcwqY5r3P44u6o86qqvBMA=\ngithub.com/rkoesters/xdg v0.0.1/go.mod h1:5DcbjvJkY00fIOKkaBnylbC/rmc1NNJP5dmUcnlcm7U=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=\ngithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=\ngithub.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=\ngithub.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=\ngithub.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=\ngithub.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yorukot/ansichroma v0.1.0 h1:S7mAB41CgSbYp2tcERMN/bT3cNfdNbexmLm5R0GftzA=\ngithub.com/yorukot/ansichroma v0.1.0/go.mod h1:Er9xbqeEBUScZnfUezMWl31vBYleQBxoZ0tBhOnqhwI=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=\ngo4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=\ngolang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=\ngolang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=\ngolang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=\ngolang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=\ngolang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=\ngolang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=\ngolang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=\ngolang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=\ngolang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=\ngolang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=\ngolang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolift.io/xtractr v0.2.2 h1:MvujxeuX629d1rQs2VJbbcvYMvMmN5SzIkEflU5ryOc=\ngolift.io/xtractr v0.2.2/go.mod h1:30CvLMUY3yOS2VoKZTTMtzeeljCzBcWkr8dU6EHqfh8=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "gomod2nix.toml",
    "content": "schema = 3\n\n[mod]\n  [mod.'github.com/BourgeoisBear/rasterm']\n    version = 'v1.1.2'\n    hash = 'sha256-fYV85hVcIAT01xriEpWn0f/YyGgsc7W+0SW6iz9K/+A='\n\n  [mod.'github.com/adrg/xdg']\n    version = 'v0.5.3'\n    hash = 'sha256-bo6tBgHS+3sl6f4oWpmdFrZjfV6eA/3xAlysSW0bIEs='\n\n  [mod.'github.com/alecthomas/chroma/v2']\n    version = 'v2.21.1'\n    hash = 'sha256-N6ske1LCEJy7MZS1L/0DEaWXnWlVqGaoU6oCwrM5teQ='\n\n  [mod.'github.com/andybalholm/brotli']\n    version = 'v1.0.5'\n    hash = 'sha256-/qS8wU8yZQJ+uTOg66rEl9s7spxq9VIXF5L1BcaEClc='\n\n  [mod.'github.com/atotto/clipboard']\n    version = 'v0.1.4'\n    hash = 'sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk='\n\n  [mod.'github.com/aymanbagabas/go-osc52/v2']\n    version = 'v2.0.1'\n    hash = 'sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg='\n\n  [mod.'github.com/barasher/go-exiftool']\n    version = 'v1.10.0'\n    hash = 'sha256-ed+Jhji3usyMROzjw7Xq++khLz3mDrlgfn+vrBXWZgg='\n\n  [mod.'github.com/bodgit/plumbing']\n    version = 'v1.3.0'\n    hash = 'sha256-nmLdJAB2sWpwHb2lFrYTcMR4yusMM1U629Pm2K9hfm0='\n\n  [mod.'github.com/bodgit/sevenzip']\n    version = 'v1.4.0'\n    hash = 'sha256-1y3jeuXQPEYWe8iuOhplbHtZjN2GgAXDnJaJohDMwLc='\n\n  [mod.'github.com/bodgit/windows']\n    version = 'v1.0.1'\n    hash = 'sha256-GSpAGboli54A5wDMpDEdZDYh55o1Zs8dZzFIf7hYdMY='\n\n  [mod.'github.com/charmbracelet/bubbles']\n    version = 'v0.21.0'\n    hash = 'sha256-cfjUHgy9eq5SretTHuYuRaeeT6QmJYQBB9dsI8QSnW0='\n\n  [mod.'github.com/charmbracelet/bubbletea']\n    version = 'v1.3.10'\n    hash = 'sha256-7wr85TLszu1CHNEMv+o4w+r24Z0xdzCgecPv+ZtRX/A='\n\n  [mod.'github.com/charmbracelet/colorprofile']\n    version = 'v0.2.3-0.20250311203215-f60798e515dc'\n    hash = 'sha256-D9E/bMOyLXAUVOHA1/6o3i+vVmLfwIMOWib6sU7A6+Q='\n\n  [mod.'github.com/charmbracelet/harmonica']\n    version = 'v0.2.0'\n    hash = 'sha256-fi5N0IXhSbbYHdSZFngCfpT4kdiEaKedqj8YpnlvX0o='\n\n  [mod.'github.com/charmbracelet/lipgloss']\n    version = 'v1.1.0'\n    hash = 'sha256-RHsRT2EZ1nDOElxAK+6/DC9XAaGVjDTgPvRh3pyCfY4='\n\n  [mod.'github.com/charmbracelet/x/ansi']\n    version = 'v0.10.1'\n    hash = 'sha256-nY4zkUGnuD+Lczwt+NMXdQ38cAsy5mtxzXrFSJmR0E4='\n\n  [mod.'github.com/charmbracelet/x/cellbuf']\n    version = 'v0.0.13-0.20250311204145-2c3ea96c31dd'\n    hash = 'sha256-XAhCOt8qJ2vR77lH1ez0IVU1/2CaLTq9jSmrHVg5HHU='\n\n  [mod.'github.com/charmbracelet/x/term']\n    version = 'v0.2.1'\n    hash = 'sha256-VBkCZLI90PhMasftGw3403IqoV7d3E5WEGAIVrN5xQM='\n\n  [mod.'github.com/clipperhouse/stringish']\n    version = 'v0.1.1'\n    hash = 'sha256-Mp8M1CRbwr6dcJ4BD9tXD5I78ZgCFEm0GDxJv0GYReg='\n\n  [mod.'github.com/clipperhouse/uax29/v2']\n    version = 'v2.3.0'\n    hash = 'sha256-dLL70mEOxrmYVoQjy2K7QvZiahAlJKOcFDz2mW/R/Do='\n\n  [mod.'github.com/connesc/cipherio']\n    version = 'v0.2.1'\n    hash = 'sha256-PeuFnTak2WADesP8YTBqHP/XyvBue9bdKHYser2v6LU='\n\n  [mod.'github.com/davecgh/go-spew']\n    version = 'v1.1.2-0.20180830191138-d8f796af33cc'\n    hash = 'sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc='\n\n  [mod.'github.com/disintegration/imaging']\n    version = 'v1.6.2'\n    hash = 'sha256-pSeMTPvSkxlthh65LjNYYhPLvCZDkBgVgAGYWW0Aguo='\n\n  [mod.'github.com/dlclark/regexp2']\n    version = 'v1.11.5'\n    hash = 'sha256-jN5+2ED+YbIoPIuyJ4Ou5pqJb2w1uNKzp5yTjKY6rEQ='\n\n  [mod.'github.com/ebitengine/purego']\n    version = 'v0.9.1'\n    hash = 'sha256-iVfU8vaJ7IPa92dUeHeuW+yKvUbe59F/eV7GlDRIAcE='\n\n  [mod.'github.com/erikgeiser/coninput']\n    version = 'v0.0.0-20211004153227-1c3628e74d0f'\n    hash = 'sha256-OWSqN1+IoL73rWXWdbbcahZu8n2al90Y3eT5Z0vgHvU='\n\n  [mod.'github.com/fatih/color']\n    version = 'v1.18.0'\n    hash = 'sha256-pP5y72FSbi4j/BjyVq/XbAOFjzNjMxZt2R/lFFxGWvY='\n\n  [mod.'github.com/fvbommel/sortorder']\n    version = 'v1.1.0'\n    hash = 'sha256-553Rg/amloO6nv67p0ARsKas9QNnTQucpi08vNLtjQU='\n\n  [mod.'github.com/go-ole/go-ole']\n    version = 'v1.2.6'\n    hash = 'sha256-+oxitLeJxYF19Z6g+6CgmCHJ1Y5D8raMi2Cb3M6nXCs='\n\n  [mod.'github.com/google/uuid']\n    version = 'v1.6.0'\n    hash = 'sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw='\n\n  [mod.'github.com/hashicorp/errwrap']\n    version = 'v1.1.0'\n    hash = 'sha256-6lwuMQOfBq+McrViN3maJTIeh4f8jbEqvLy2c9FvvFw='\n\n  [mod.'github.com/hashicorp/go-multierror']\n    version = 'v1.1.1'\n    hash = 'sha256-ANzPEUJIZIlToxR89Mn7Db73d9LGI51ssy7eNnUgmlA='\n\n  [mod.'github.com/hymkor/trash-go']\n    version = 'v0.2.0'\n    hash = 'sha256-p7zJYJpPjNAYKRNW7No+BXRemNabGp/No1mp9ItHXII='\n\n  [mod.'github.com/kdomanski/iso9660']\n    version = 'v0.3.3'\n    hash = 'sha256-iMyzZnZCKXUfBKJDdqwEYyzcFKo/VEkoo4Lm/Ct8tj4='\n\n  [mod.'github.com/klauspost/compress']\n    version = 'v1.16.3'\n    hash = 'sha256-dU0OgO5afQ1z5s83Y3w8Bg0ftvg+ikWbktUACEgY3OQ='\n\n  [mod.'github.com/lazysegtree/go-zoxide']\n    version = 'v0.1.0'\n    hash = 'sha256-PKEV+zCKf/7MsFAMMctL/pFSnEsri1yS8Bzxcj1dLWE='\n\n  [mod.'github.com/lithammer/shortuuid']\n    version = 'v3.0.0+incompatible'\n    hash = 'sha256-dD6ArCHGnpo84RBy6SI2kUjMqHS4IZU+K+DgAgmRakY='\n\n  [mod.'github.com/lucasb-eyer/go-colorful']\n    version = 'v1.3.0'\n    hash = 'sha256-6BKrJsfmxie+YFAWzTYVPQfrwjQEXRo+J8LY+50C1BU='\n\n  [mod.'github.com/mattn/go-colorable']\n    version = 'v0.1.13'\n    hash = 'sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8='\n\n  [mod.'github.com/mattn/go-isatty']\n    version = 'v0.0.20'\n    hash = 'sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ='\n\n  [mod.'github.com/mattn/go-localereader']\n    version = 'v0.0.1'\n    hash = 'sha256-JlWckeGaWG+bXK8l8WEdZqmSiTwCA8b1qbmBKa/Fj3E='\n\n  [mod.'github.com/mattn/go-runewidth']\n    version = 'v0.0.19'\n    hash = 'sha256-GpnbKplhX410Q/eIdknvWbYZgdav1keN+7wNUeOSMHE='\n\n  [mod.'github.com/muesli/ansi']\n    version = 'v0.0.0-20230316100256-276c6243b2f6'\n    hash = 'sha256-qRKn0Bh2yvP0QxeEMeZe11Vz0BPFIkVcleKsPeybKMs='\n\n  [mod.'github.com/muesli/cancelreader']\n    version = 'v0.2.2'\n    hash = 'sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ='\n\n  [mod.'github.com/muesli/reflow']\n    version = 'v0.3.0'\n    hash = 'sha256-Pou2ybE9SFSZG6YfZLVV1Eyfm+X4FuVpDPLxhpn47Cc='\n\n  [mod.'github.com/muesli/termenv']\n    version = 'v0.16.0'\n    hash = 'sha256-hGo275DJlyLtcifSLpWnk8jardOksdeX9lH4lBeE3gI='\n\n  [mod.'github.com/nwaples/rardecode']\n    version = 'v1.1.3'\n    hash = 'sha256-X7Cg0kEygyy6Xw6sxRF9HirgefkH9tn9UPPelxRaAGg='\n\n  [mod.'github.com/pelletier/go-toml/v2']\n    version = 'v2.2.4'\n    hash = 'sha256-8qQIPldbsS5RO8v/FW/se3ZsAyvLzexiivzJCbGRg2Q='\n\n  [mod.'github.com/pierrec/lz4/v4']\n    version = 'v4.1.17'\n    hash = 'sha256-36L+GNhRrHBCyhbHCqweCk5rfmggdRtHqH6g5m1ViQI='\n\n  [mod.'github.com/pmezard/go-difflib']\n    version = 'v1.0.1-0.20181226105442-5d4384ee4fb2'\n    hash = 'sha256-XA4Oj1gdmdV/F/+8kMI+DBxKPthZ768hbKsO3d9Gx90='\n\n  [mod.'github.com/power-devops/perfstat']\n    version = 'v0.0.0-20240221224432-82ca36839d55'\n    hash = 'sha256-ujzuJ1ttQgjHQJEij4O/2+I8DZaUVZQCQgA4ysfqulI='\n\n  [mod.'github.com/reinhrst/fzf-lib']\n    version = 'v0.9.0'\n    hash = 'sha256-UUe5g+oXopDj+JjvzAguqJ70/a4pqZhn7pdJiFE97yI='\n\n  [mod.'github.com/rivo/uniseg']\n    version = 'v0.4.7'\n    hash = 'sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo='\n\n  [mod.'github.com/rkoesters/xdg']\n    version = 'v0.0.1'\n    hash = 'sha256-+ckoKNaiEqQIpRFPr3CNRE3w8+OHmRKZ2cn6MquNIOE='\n\n  [mod.'github.com/rwcarlsen/goexif']\n    version = 'v0.0.0-20190401172101-9e8deecbddbd'\n    hash = 'sha256-AiY2T9hXj6jnfldYDoe4WNr3FldpVTxc3lScR++HOLc='\n\n  [mod.'github.com/shirou/gopsutil/v4']\n    version = 'v4.25.12'\n    hash = 'sha256-gzk9GW4+tXUWmxAVD3by/k4D/+l++TvajRVTkQJvwmM='\n\n  [mod.'github.com/stretchr/testify']\n    version = 'v1.11.1'\n    hash = 'sha256-sWfjkuKJyDllDEtnM8sb/pdLzPQmUYWYtmeWz/5suUc='\n\n  [mod.'github.com/ulikunitz/xz']\n    version = 'v0.5.15'\n    hash = 'sha256-L5KYLue5U14bxUuNyhZ6lIjbda6eCQsx1V6gToqfRdk='\n\n  [mod.'github.com/urfave/cli/v3']\n    version = 'v3.6.1'\n    hash = 'sha256-q1WeKEvWoSyA0Wcan+Edjm71e0/vnPmqVvlfjUix7+8='\n\n  [mod.'github.com/xo/terminfo']\n    version = 'v0.0.0-20220910002029-abceb7e1c41e'\n    hash = 'sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU='\n\n  [mod.'github.com/yorukot/ansichroma']\n    version = 'v0.1.0'\n    hash = 'sha256-aIQZ16auuGfGLltKt2lm2T83f4sALrmRpOujxQepkS4='\n\n  [mod.'github.com/yusufpapurcu/wmi']\n    version = 'v1.2.4'\n    hash = 'sha256-N+YDBjOW59YOsZ2lRBVtFsEEi48KhNQRb63/0ZSU3bA='\n\n  [mod.'go4.org']\n    version = 'v0.0.0-20230225012048-214862532bf5'\n    hash = 'sha256-8y7krSESdxZfxTzo16EDqmF8USRzGmLPYr3iKuVYzzE='\n\n  [mod.'golang.org/x/exp']\n    version = 'v0.0.0-20231006140011-7918f672742d'\n    hash = 'sha256-2SO1etTQ6UCUhADR5sgvDEDLHcj77pJKCIa/8mGDbAo='\n\n  [mod.'golang.org/x/image']\n    version = 'v0.35.0'\n    hash = 'sha256-l9GP7N78XsAxe+LFUrBERp16SnerF7I5/WSTvHLGe2M='\n\n  [mod.'golang.org/x/mod']\n    version = 'v0.31.0'\n    hash = 'sha256-ZVNmaZADgM3+30q9rW8q4gP6ySkT7r1eb4vrHIlpCjM='\n\n  [mod.'golang.org/x/sys']\n    version = 'v0.38.0'\n    hash = 'sha256-1+i5EaG3JwH3KMtefzJLG5R6jbOeJM4GK3/LHBVnSy0='\n\n  [mod.'golang.org/x/term']\n    version = 'v0.18.0'\n    hash = 'sha256-lpze9arFZIhBV8Ht3VZyoiUwqPkeH2IwfXt8M3xljiM='\n\n  [mod.'golang.org/x/text']\n    version = 'v0.33.0'\n    hash = 'sha256-XdA6D39ESuJkaaM/SRBnqZzjKUwi6Gbt1Si1nvauTr4='\n\n  [mod.'golift.io/xtractr']\n    version = 'v0.2.2'\n    hash = 'sha256-ihKdIrWG1DKADjQ4X1AW62EGZWGgYU+9dY3g260bNOY='\n\n  [mod.'gopkg.in/yaml.v3']\n    version = 'v3.0.1'\n    hash = 'sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU='\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"embed\"\n\n\t\"github.com/yorukot/superfile/src/cmd\"\n)\n\nvar (\n\t//go:embed src/superfile_config/*\n\tcontent embed.FS\n)\n\nfunc main() {\n\tcmd.Run(content)\n}\n"
  },
  {
    "path": "release/release.sh",
    "content": "#!/usr/bin/env -S bash -euo pipefail\n\nprojectName=\"superfile\"\nversion=\"v1.5.0\"\nosList=(\"darwin\" \"linux\" \"windows\")\narchList=(\"amd64\" \"arm64\")\nmkdir dist\n\n# Prevent macOS from adding ._* files to archives\nexport COPYFILE_DISABLE=1\n\nfor os in \"${osList[@]}\"; do\n    if [ \"$os\" = \"windows\" ]; then\n        for arch in \"${archList[@]}\"; do\n            echo \"$projectName-$os-$version-$arch\"\n            mkdir \"./dist/$projectName-$os-$version-$arch\"\n            cd ../ || exit\n            env GOOS=\"$os\" GOARCH=\"$arch\" CGO_ENABLED=0 go build -o \"./release/dist/$projectName-$os-$version-$arch/spf.exe\" main.go\n            cd ./release || exit\n            zip -r \"./dist/$projectName-$os-$version-$arch.zip\" \"./dist/$projectName-$os-$version-$arch\"\n        done\n    else\n        for arch in \"${archList[@]}\"; do\n            echo \"$projectName-$os-$version-$arch\"\n            mkdir \"./dist/$projectName-$os-$version-$arch\"\n            cd ../ || exit\n            env GOOS=\"$os\" GOARCH=\"$arch\" CGO_ENABLED=0 go build -o \"./release/dist/$projectName-$os-$version-$arch/spf\" main.go\n            cd ./release || exit\n            tar czf \"./dist/$projectName-$os-$version-$arch.tar.gz\" \"./dist/$projectName-$os-$version-$arch\"\n        done\n    fi\ndone"
  },
  {
    "path": "release/release_check.md",
    "content": "- [ ] check all plugins is disable\n- [ ] check update version and zip file"
  },
  {
    "path": "release/remove_all_spf_config.sh",
    "content": "#!/usr/bin/env bash\n\nrm -r \"$HOME/.config/superfile\"\nrm -r \"$HOME/.local/state/superfile\"\nrm -r \"$HOME/.local/share/superfile\""
  },
  {
    "path": "src/cmd/debug_info.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n)\n\nconst (\n\tkeyWidth         = 20\n\tmaxVersionLength = 50\n)\n\ntype debugPrinter struct {\n\ttitleColor   *color.Color\n\tflagColor    *color.Color\n\twarningColor *color.Color\n\tsuccessColor *color.Color\n}\n\nfunc newDebugPrinter() *debugPrinter {\n\treturn &debugPrinter{\n\t\ttitleColor:   color.New(color.FgGreen, color.Bold),\n\t\tflagColor:    color.New(color.FgCyan, color.Bold),\n\t\twarningColor: color.New(color.FgRed, color.Bold),\n\t\tsuccessColor: color.New(color.FgGreen),\n\t}\n}\n\nfunc printDebugInfo() {\n\tdp := newDebugPrinter()\n\n\tfmt.Println()\n\tdp.printHeader(\"Superfile\")\n\tdp.printKeyValue(\"Version\", variable.CurrentVersion+variable.PreReleaseSuffix)\n\n\tfmt.Println()\n\tdp.printHeader(\"System\")\n\tdp.printKeyValue(\"OS\", runtime.GOOS)\n\tdp.printKeyValue(\"Arch\", runtime.GOARCH)\n\tif kernel, err := getKernelVersion(); err == nil {\n\t\tdp.printKeyValue(\"Kernel\", kernel)\n\t}\n\n\tfmt.Println()\n\tdp.printHeader(\"Configuration\")\n\tdp.printKeyValue(\"Config File\", variable.ConfigFile)\n\tdp.printKeyValue(\"Hotkeys File\", variable.HotkeysFile)\n\tdp.printKeyValue(\"Theme Folder\", variable.ThemeFolder)\n\tdp.printKeyValue(\"Log File\", variable.LogFile)\n\tdp.printKeyValue(\"Data Dir\", variable.SuperFileDataDir)\n\n\tfmt.Println()\n\tdp.printHeader(\"Environment\")\n\tif runtime.GOOS == utils.OsWindows {\n\t\tdp.printEnv(\"COMSPEC\")\n\t\tdp.printEnv(\"APPDATA\")\n\t\tdp.printEnv(\"LOCALAPPDATA\")\n\t} else {\n\t\tdp.printEnv(\"TERM\")\n\t\tdp.printEnv(\"TERM_PROGRAM\")\n\t\tdp.printEnv(\"TERM_PROGRAM_VERSION\")\n\t\tdp.printEnv(\"SHELL\")\n\t\tdp.printEnv(\"EDITOR\")\n\t\tdp.printEnv(\"VISUAL\")\n\t\tdp.printEnv(\"XDG_SESSION_TYPE\")\n\t\tdp.printEnv(\"WAYLAND_DISPLAY\")\n\t\tdp.printEnv(\"DISPLAY\")\n\t}\n\n\tfmt.Println()\n\tdp.printHeader(\"Dependencies\")\n\tdp.checkDependency(\"ffmpeg\", \"-version\")\n\tdp.checkDependency(\"pdftoppm\", \"-v\")\n\tdp.checkDependency(\"exiftool\", \"-ver\")\n\tdp.checkDependency(\"bat\", \"--version\")\n\tdp.checkDependency(\"zoxide\", \"--version\")\n\tswitch runtime.GOOS {\n\tcase utils.OsDarwin:\n\t\tdp.checkDependency(\"open\", \"\")\n\t\tdp.checkDependency(\"pbcopy\", \"\")\n\tcase utils.OsWindows:\n\t\tdp.checkDependency(\"clip\", \"\")\n\tcase utils.OsLinux:\n\t\tdp.checkDependency(\"xdg-open\", \"--version\")\n\t\tdp.checkDependency(\"wl-copy\", \"--version\")\n\t\tdp.checkDependency(\"xclip\", \"-version\")\n\t\tdp.checkDependency(\"xsel\", \"--version\")\n\t}\n}\n\nfunc (dp *debugPrinter) printHeader(text string) {\n\t_, _ = dp.titleColor.Add(color.Underline).Println(text)\n}\n\nfunc (dp *debugPrinter) printKeyValue(key, value string) {\n\tif filepath.IsAbs(value) {\n\t\tif _, err := os.Stat(value); os.IsNotExist(err) {\n\t\t\tvalue = dp.warningColor.Sprint(value + \" (Not Found)\")\n\t\t}\n\t}\n\n\t// Use fixed width formatting for key\n\tkeyStr := fmt.Sprintf(\"%-*s\", keyWidth, key)\n\t_, _ = dp.flagColor.Print(keyStr)\n\tfmt.Printf(\": %s\\n\", value)\n}\n\nfunc (dp *debugPrinter) printEnv(key string) {\n\tval := os.Getenv(key)\n\tif val == \"\" {\n\t\tval = \"Not Set\"\n\t}\n\tdp.printKeyValue(key, val)\n}\n\nfunc (dp *debugPrinter) checkDependency(name string, flag string) {\n\tpath, err := exec.LookPath(name)\n\tvar status string\n\tif err != nil {\n\t\tstatus = dp.warningColor.Sprint(\"Not Found\")\n\t} else {\n\t\t// Try to get version\n\t\tversion := \"Found at \" + path\n\t\tif flag != \"\" {\n\t\t\t//nolint:gosec // flags are hardcoded strings\n\t\t\tcmd := exec.Command(name, strings.Split(flag, \" \")...)\n\t\t\tout, err := cmd.CombinedOutput()\n\t\t\tif err == nil {\n\t\t\t\tlines := strings.Split(string(out), \"\\n\")\n\t\t\t\tif len(lines) > 0 {\n\t\t\t\t\tv := strings.TrimSpace(lines[0])\n\t\t\t\t\tif len(v) > maxVersionLength {\n\t\t\t\t\t\tv = v[:maxVersionLength] + \"...\"\n\t\t\t\t\t}\n\t\t\t\t\tif v != \"\" {\n\t\t\t\t\t\tversion = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tstatus = dp.successColor.Sprint(version)\n\t}\n\n\tkeyStr := fmt.Sprintf(\"%-*s\", keyWidth, name)\n\t_, _ = dp.flagColor.Print(keyStr)\n\tfmt.Printf(\": %s\\n\", status)\n}\n\nfunc getKernelVersion() (string, error) {\n\tif runtime.GOOS == utils.OsWindows {\n\t\tcmd := exec.Command(\"cmd\", \"/c\", \"ver\")\n\t\tout, err := cmd.Output()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn strings.TrimSpace(string(out)), nil\n\t}\n\tout, err := exec.Command(\"uname\", \"-r\").Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(out)), nil\n}\n"
  },
  {
    "path": "src/cmd/help_printer.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/urfave/cli/v3\"\n)\n\n// CustomHelpPrinter provides cargo-style colored help output for superfile CLI\nfunc CustomHelpPrinter(w io.Writer, templ string, data interface{}) {\n\t// Define color styles matching superfile's aesthetic\n\ttitleColor := color.New(color.FgGreen, color.Bold)\n\tflagColor := color.New(color.FgCyan, color.Bold)\n\tcommandColor := color.New(color.FgBlue, color.Bold)\n\taccentColor := color.New(color.FgMagenta, color.Bold)\n\n\tswitch v := data.(type) {\n\tcase *cli.Command:\n\t\t// Get the actual binary name from os.Args[0]\n\t\tbinaryName := filepath.Base(os.Args[0])\n\t\tprintUsage(w, titleColor, accentColor, binaryName, v)\n\n\t\tprintCommands(w, titleColor, commandColor, v)\n\n\t\tprintFlags(w, titleColor, flagColor, v)\n\t\t// Print version info if available\n\t\tif v.Version != \"\" {\n\t\t\tfmt.Printf(\"Version: \")\n\t\t\t_, _ = accentColor.Fprintf(w, \"%s\\n\\n\", v.Version)\n\t\t}\n\n\t\t// Print help footer using the actual binary name\n\t\tfmt.Fprint(w, \"Use \\\"\")\n\t\t_, _ = accentColor.Fprintf(w, \"%s\", binaryName)\n\t\tfmt.Fprint(w, \" [COMMAND] --help\\\" for more information about a command.\\n\")\n\n\tdefault:\n\t\t// Fallback to default template rendering for other cases\n\t\tcli.HelpPrinterCustom(w, templ, data, nil)\n\t}\n}\n\nfunc printUsage(w io.Writer, titleColor *color.Color, accentColor *color.Color, binaryName string, v *cli.Command) {\n\t_, _ = titleColor.Fprintf(w, \"Usage:\")\n\tfmt.Fprint(w, \" \")\n\t_, _ = accentColor.Fprintf(w, \"%s\", binaryName)\n\tif len(v.Commands) > 0 {\n\t\tfmt.Fprint(w, \" [COMMAND]\")\n\t}\n\tif len(v.Flags) > 0 {\n\t\tfmt.Fprint(w, \" [OPTIONS]\")\n\t}\n\tif v.ArgsUsage != \"\" {\n\t\tfmt.Fprintf(w, \" %s\", v.ArgsUsage)\n\t}\n\tfmt.Fprintln(w)\n\tfmt.Fprintln(w)\n\tif v.Description != \"\" {\n\t\tfmt.Fprintf(w, \"%s\\n\\n\", strings.TrimSpace(v.Description))\n\t}\n}\n\nfunc printCommands(w io.Writer, titleColor *color.Color, commandColor *color.Color, v *cli.Command) {\n\tif len(v.Commands) == 0 {\n\t\treturn\n\t}\n\t_, _ = titleColor.Fprintf(w, \"Commands:\\n\")\n\tfor _, cmd := range v.Commands {\n\t\t// Format command name with aliases\n\t\tcmdDisplay := cmd.Name\n\t\tif len(cmd.Aliases) > 0 {\n\t\t\tcmdDisplay = fmt.Sprintf(\"%s, %s\", cmd.Name, strings.Join(cmd.Aliases, \", \"))\n\t\t}\n\n\t\t_, _ = commandColor.Fprintf(w, \"  %-20s\", cmdDisplay)\n\t\tfmt.Fprintf(w, \" %s\\n\", cmd.Usage)\n\t}\n\tfmt.Fprintln(w)\n}\n\nfunc printFlags(w io.Writer, titleColor *color.Color, flagColor *color.Color, v *cli.Command) {\n\tif len(v.Flags) == 0 {\n\t\treturn\n\t}\n\t_, _ = titleColor.Fprintf(w, \"Options:\\n\")\n\tfor _, flag := range v.Flags {\n\t\tnames := flag.Names()\n\n\t\t// Format flag names with proper prefixes and aliases\n\t\tvar flagParts []string\n\t\tvar valuePlaceholder string\n\t\tvar usage string\n\n\t\t// Determine flag type, value placeholder, and usage in one switch\n\t\tswitch f := flag.(type) {\n\t\tcase *cli.BoolFlag:\n\t\t\t// Boolean flags don't need values\n\t\t\tvaluePlaceholder = \"\"\n\t\t\tusage = f.Usage\n\t\tcase *cli.StringFlag:\n\t\t\tvaluePlaceholder = \" <value>\"\n\t\t\tusage = f.Usage\n\t\t\tif f.Value != \"\" {\n\t\t\t\tusage += fmt.Sprintf(\" (default: %q)\", f.Value)\n\t\t\t}\n\t\tcase *cli.StringSliceFlag:\n\t\t\tvaluePlaceholder = \" <value>...\"\n\t\t\tusage = f.Usage\n\t\tcase *cli.IntFlag:\n\t\t\tvaluePlaceholder = \" <number>\"\n\t\t\tusage = f.Usage\n\t\t\tif f.Value != 0 {\n\t\t\t\tusage += fmt.Sprintf(\" (default: %d)\", f.Value)\n\t\t\t}\n\t\tdefault:\n\t\t\tvaluePlaceholder = \" <value>\"\n\t\t\tusage = \"No description available\"\n\t\t}\n\n\t\tfor _, name := range names {\n\t\t\tif len(name) == 1 {\n\t\t\t\tflagParts = append(flagParts, \"-\"+name)\n\t\t\t} else {\n\t\t\t\tflagParts = append(flagParts, \"--\"+name)\n\t\t\t}\n\t\t}\n\t\tflagStr := strings.Join(flagParts, \", \") + valuePlaceholder\n\n\t\t_, _ = flagColor.Fprintf(w, \"  %-30s\", flagStr)\n\t\tfmt.Fprintf(w, \" %s\\n\", usage)\n\t}\n\tfmt.Fprintln(w)\n}\n"
  },
  {
    "path": "src/cmd/main.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/urfave/cli/v3\"\n\t\"golang.org/x/mod/semver\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\tinternal \"github.com/yorukot/superfile/src/internal\"\n)\n\n// Run superfile app\nfunc Run(content embed.FS) {\n\t// Enable custom colored help output\n\tcli.HelpPrinter = CustomHelpPrinter //nolint:reassign // Intentionally reassigning to customize help output\n\n\t// Before we open log file, set all \"non debug\" logs to stdout\n\tutils.SetRootLoggerToStdout(false)\n\n\tcommon.LoadInitialPrerenderedVariables()\n\tcommon.LoadAllDefaultConfig(content)\n\n\tapp := &cli.Command{\n\t\tName:        \"superfile\",\n\t\tVersion:     variable.CurrentVersion + variable.PreReleaseSuffix,\n\t\tDescription: \"Pretty fancy and modern terminal file manager \",\n\t\tArgsUsage:   \"[PATH]...\",\n\t\tCommands: []*cli.Command{\n\t\t\t{\n\t\t\t\tName:    \"path-list\",\n\t\t\t\tAliases: []string{\"pl\"},\n\t\t\t\tUsage:   \"Print the path to the configuration and directory\",\n\t\t\t\tAction: func(_ context.Context, c *cli.Command) error {\n\t\t\t\t\tif c.Bool(\"lastdir-file\") {\n\t\t\t\t\t\tfmt.Println(variable.LastDirFile)\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\t\t\t\t\tfmt.Printf(\"%-*s %s\\n\",\n\t\t\t\t\t\tcommon.HelpKeyColumnWidth,\n\t\t\t\t\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#66b2ff\")).Render(\"[Configuration file path]\"),\n\t\t\t\t\t\tvariable.ConfigFile,\n\t\t\t\t\t)\n\t\t\t\t\tfmt.Printf(\"%-*s %s\\n\",\n\t\t\t\t\t\tcommon.HelpKeyColumnWidth,\n\t\t\t\t\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#ffcc66\")).Render(\"[Hotkeys file path]\"),\n\t\t\t\t\t\tvariable.HotkeysFile,\n\t\t\t\t\t)\n\t\t\t\t\tlogStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"#66ff66\"))\n\t\t\t\t\tconfigStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"#ff9999\"))\n\t\t\t\t\tdataStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"#ff66ff\"))\n\t\t\t\t\tfmt.Printf(\"%-*s %s\\n\", common.HelpKeyColumnWidth,\n\t\t\t\t\t\tlogStyle.Render(\"[Log file path]\"), variable.LogFile)\n\t\t\t\t\tfmt.Printf(\"%-*s %s\\n\", common.HelpKeyColumnWidth,\n\t\t\t\t\t\tconfigStyle.Render(\"[Configuration directory path]\"), variable.SuperFileMainDir)\n\t\t\t\t\tfmt.Printf(\"%-*s %s\\n\", common.HelpKeyColumnWidth,\n\t\t\t\t\t\tdataStyle.Render(\"[Data directory path]\"), variable.SuperFileDataDir)\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tFlags: []cli.Flag{\n\t\t\t\t\t&cli.BoolFlag{\n\t\t\t\t\t\tName:    \"lastdir-file\",\n\t\t\t\t\t\tAliases: []string{\"ld\"},\n\t\t\t\t\t\tUsage:   \"Print path to lastdir file (Where last dir is written when cd_on_quit config is true)\",\n\t\t\t\t\t\tValue:   false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:    \"debug-info\",\n\t\t\t\tAliases: []string{\"di\"},\n\t\t\t\tUsage:   \"Print debug information\",\n\t\t\t\tValue:   false,\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:    \"fix-hotkeys\",\n\t\t\t\tAliases: []string{\"fh\"},\n\t\t\t\tUsage:   \"Adds any missing hotkeys to the hotkey config file\",\n\t\t\t\tValue:   false,\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:    \"fix-config-file\",\n\t\t\t\tAliases: []string{\"fch\"},\n\t\t\t\tUsage:   \"Adds any missing hotkeys to the hotkey config file\",\n\t\t\t\tValue:   false,\n\t\t\t},\n\t\t\t&cli.BoolFlag{\n\t\t\t\tName:    \"print-last-dir\",\n\t\t\t\tAliases: []string{\"pld\"},\n\t\t\t\tUsage:   \"Print the last dir to stdout on exit (to use for cd)\",\n\t\t\t\tValue:   false,\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:    \"config-file\",\n\t\t\t\tAliases: []string{\"c\"},\n\t\t\t\tUsage:   \"Specify the path to a different config file\",\n\t\t\t\tValue:   \"\", // Default to the blank string indicating non-usage of flag\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:    \"hotkey-file\",\n\t\t\t\tAliases: []string{\"hf\"},\n\t\t\t\tUsage:   \"Specify the path to a different hotkey file\",\n\t\t\t\tValue:   \"\", // Default to the blank string indicating non-usage of flag\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:    \"chooser-file\",\n\t\t\t\tAliases: []string{\"cf\"},\n\t\t\t\tUsage:   \"On trying to open any file, superfile will write to its path to this file, and exit\",\n\t\t\t\tValue:   \"\", // Default to the blank string indicating non-usage of flag\n\t\t\t},\n\t\t},\n\t\tAction: spfAppAction,\n\t}\n\n\terr := app.Run(context.Background(), os.Args)\n\tif err != nil {\n\t\tutils.PrintlnAndExit(err)\n\t}\n}\n\nfunc spfAppAction(_ context.Context, c *cli.Command) error {\n\tvariable.UpdateVarFromCliArgs(c)\n\n\tif c.Bool(\"debug-info\") {\n\t\tprintDebugInfo()\n\t\treturn nil\n\t}\n\t// If no args are called along with \"spf\" use current dir\n\tfirstPanelPaths := []string{\"\"}\n\tif c.Args().Present() {\n\t\tfirstPanelPaths = c.Args().Slice()\n\t}\n\n\tInitConfigFile()\n\n\tfirstUse := checkFirstUse()\n\n\tp := tea.NewProgram(internal.InitialModel(firstPanelPaths, firstUse),\n\t\ttea.WithAltScreen(), tea.WithMouseCellMotion())\n\tif _, err := p.Run(); err != nil {\n\t\tutils.PrintfAndExitf(\"Alas, there's been an error: %v\", err)\n\t}\n\n\t// This must be after calling internal.InitialModel()\n\t// so that we know `common.Config` is loaded\n\t// Should not be a goroutine, Otherwise the main\n\t// goroutine will exit first, and this will not be able to finish\n\tCheckForUpdates()\n\n\tif variable.PrintLastDir {\n\t\tfmt.Println(variable.LastDir)\n\t}\n\n\treturn nil\n}\n\n// Create proper directories for storing configuration and write default\n// configurations to Config and Hotkeys toml\nfunc InitConfigFile() {\n\t// Create directories\n\tif err := utils.CreateDirectories(\n\t\tvariable.SuperFileMainDir,\n\t\tvariable.SuperFileDataDir,\n\t\tvariable.SuperFileStateDir,\n\t\tvariable.ThemeFolder,\n\t); err != nil {\n\t\tutils.PrintlnAndExit(\"Error creating directories:\", err)\n\t}\n\n\t// Create files\n\tif err := utils.CreateFiles(\n\t\tvariable.ToggleDotFile,\n\t\tvariable.LogFile,\n\t\tvariable.ThemeFileVersion,\n\t\tvariable.ToggleFooter,\n\t); err != nil {\n\t\tutils.PrintlnAndExit(\"Error creating files:\", err)\n\t}\n\n\t// Write config file\n\tif err := writeConfigFile(variable.ConfigFile, common.ConfigTomlString); err != nil {\n\t\tutils.PrintlnAndExit(\"Error writing config file:\", err)\n\t}\n\n\tif err := writeConfigFile(variable.HotkeysFile, common.HotkeysTomlString); err != nil {\n\t\tutils.PrintlnAndExit(\"Error writing config file:\", err)\n\t}\n}\n\n// Check if is the first time initializing the app, if it is create\n// use check file\nfunc checkFirstUse() bool {\n\tfile := variable.FirstUseCheck\n\tfirstUse := false\n\tif _, err := os.Stat(file); os.IsNotExist(err) {\n\t\tfirstUse = true\n\t\tif err = os.WriteFile(file, nil, utils.ConfigFilePerm); err != nil {\n\t\t\tutils.PrintfAndExitf(\"Failed to create file: %v\", err)\n\t\t}\n\t}\n\treturn firstUse\n}\n\n// Write data to the path file if it does not exists\nfunc writeConfigFile(path, data string) error {\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\tif err = os.WriteFile(path, []byte(data), utils.ConfigFilePerm); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write config file %s: %w\", path, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc writeLastCheckTime(t time.Time) {\n\terr := os.WriteFile(variable.LastCheckVersion, []byte(t.Format(time.RFC3339)), utils.ConfigFilePerm)\n\tif err != nil {\n\t\tslog.Error(\"Error writing LastCheckVersion file\", \"error\", err)\n\t}\n}\n\n// Check for the need of updates if AutoCheckUpdate is on, if its the first time\n// that version is checked or if has more than 24h since the last version check,\n// look into the repo if  there's any more recent version\nfunc CheckForUpdates() {\n\tif !common.Config.AutoCheckUpdate {\n\t\treturn\n\t}\n\n\tcurrentTime := time.Now().UTC()\n\tlastCheckTime := readLastCheckTime()\n\n\tif !shouldCheckForUpdate(currentTime, lastCheckTime) {\n\t\treturn\n\t}\n\n\tdefer writeLastCheckTime(currentTime)\n\tcheckAndNotifyUpdate()\n}\n\n// Default to zero time if file doesn't exist, is empty, or has errors\nfunc readLastCheckTime() time.Time {\n\tcontent, err := os.ReadFile(variable.LastCheckVersion)\n\tif err != nil || len(content) == 0 {\n\t\treturn time.Time{}\n\t}\n\n\tparsedTime, parseErr := time.Parse(time.RFC3339, string(content))\n\tif parseErr != nil {\n\t\tslog.Error(\"Failed to parse LastCheckVersion timestamp\", \"error\", parseErr)\n\t\treturn time.Time{}\n\t}\n\n\treturn parsedTime.UTC()\n}\n\nfunc shouldCheckForUpdate(now, last time.Time) bool {\n\treturn last.IsZero() || now.Sub(last) >= 24*time.Hour\n}\n\nfunc checkAndNotifyUpdate() {\n\tctx, cancel := context.WithTimeout(context.Background(), common.DefaultCLIContextTimeout)\n\tdefer cancel()\n\n\tresp, err := fetchLatestRelease(ctx)\n\tif err != nil {\n\t\tslog.Error(\"Failed to fetch update\", \"error\", err)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\tslog.Error(\"Failed to read update response\", \"error\", err)\n\t\treturn\n\t}\n\n\ttype GitHubRelease struct {\n\t\tTagName string `json:\"tag_name\"`\n\t}\n\n\tvar release GitHubRelease\n\tif err := json.Unmarshal(body, &release); err != nil {\n\t\tslog.Error(\"Failed to parse GitHub JSON\", \"error\", err)\n\t\treturn\n\t}\n\n\tif semver.Compare(release.TagName, variable.CurrentVersion) > 0 {\n\t\tnotifyUpdateAvailable(release.TagName)\n\t}\n}\n\nfunc fetchLatestRelease(ctx context.Context) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, variable.LatestVersionURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn (&http.Client{}).Do(req)\n}\n\nfunc notifyUpdateAvailable(latest string) {\n\tfmt.Println(\n\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#FF69E1\")).Render(\"┃ \") +\n\t\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#FFBA52\")).Bold(true).Render(\"A new version \") +\n\t\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#00FFF2\")).Bold(true).Italic(true).Render(latest) +\n\t\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#FFBA52\")).Bold(true).Render(\" is available.\"),\n\t)\n\tfmt.Printf(\n\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#FF69E1\")).Render(\"┃ \")+\"Please update.\\n┏\\n\\n      => %s\\n\\n\",\n\t\tvariable.LatestVersionGithub,\n\t)\n\tfmt.Println(\"                                                               ┛\")\n}\n"
  },
  {
    "path": "src/config/fixed_variable.go",
    "content": "package variable\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/urfave/cli/v3\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/adrg/xdg\"\n)\n\nconst (\n\tCurrentVersion = \"v1.5.0\"\n\t// Allowing pre-releases with non production version\n\t// Set this to \"\" for production releases\n\tPreReleaseSuffix = \"\"\n\n\t// This gives most recent non-prerelease, non-draft release\n\tLatestVersionURL    = \"https://api.github.com/repos/yorukot/superfile/releases/latest\"\n\tLatestVersionGithub = \"github.com/yorukot/superfile/releases/latest\"\n\n\t// This will not break in windows. This is a relative path for Embed FS. It uses \"/\" only\n\tEmbedConfigDir           = \"src/superfile_config\"\n\tEmbedConfigFile          = EmbedConfigDir + \"/config.toml\"\n\tEmbedHotkeysFile         = EmbedConfigDir + \"/hotkeys.toml\"\n\tEmbedThemeDir            = EmbedConfigDir + \"/theme\"\n\tEmbedThemeCatppuccinFile = EmbedThemeDir + \"/catppuccin-mocha.toml\"\n)\n\nvar (\n\tHomeDir           = xdg.Home\n\tSuperFileMainDir  = filepath.Join(xdg.ConfigHome, \"superfile\")\n\tSuperFileCacheDir = filepath.Join(xdg.CacheHome, \"superfile\")\n\tSuperFileDataDir  = filepath.Join(xdg.DataHome, \"superfile\")\n\tSuperFileStateDir = filepath.Join(xdg.StateHome, \"superfile\")\n\n\t// MainDir files\n\tThemeFolder = filepath.Join(SuperFileMainDir, \"theme\")\n\n\t// DataDir files\n\tLastCheckVersion = filepath.Join(SuperFileDataDir, \"lastCheckVersion\")\n\tThemeFileVersion = filepath.Join(SuperFileDataDir, \"themeFileVersion\")\n\tFirstUseCheck    = filepath.Join(SuperFileDataDir, \"firstUseCheck\")\n\tPinnedFile       = filepath.Join(SuperFileDataDir, \"pinned.json\")\n\tToggleDotFile    = filepath.Join(SuperFileDataDir, \"toggleDotFile\")\n\tToggleFooter     = filepath.Join(SuperFileDataDir, \"toggleFooter\")\n\n\t// StateDir files\n\tLogFile     = filepath.Join(SuperFileStateDir, \"superfile.log\")\n\tLastDirFile = filepath.Join(SuperFileStateDir, \"lastdir\")\n\n\t// Trash Directories\n\tDarwinTrashDirectory = filepath.Join(HomeDir, \".Trash\")\n\n\t// These are used by github.com/rkoesters/xdg/trash package\n\t// We need to make sure that these directories exist\n\tLinuxTrashDirectory      = filepath.Join(xdg.DataHome, \"Trash\")\n\tLinuxTrashDirectoryFiles = filepath.Join(xdg.DataHome, \"Trash\", \"files\")\n\tLinuxTrashDirectoryInfo  = filepath.Join(xdg.DataHome, \"Trash\", \"info\")\n)\n\n// These variables are actually not fixed, they are sometimes updated dynamically\nvar (\n\tConfigFile  = filepath.Join(SuperFileMainDir, \"config.toml\")\n\tHotkeysFile = filepath.Join(SuperFileMainDir, \"hotkeys.toml\")\n\n\t// ChooserFile is the path where superfile will write the file's path, which is to be\n\t// opened, before exiting\n\tChooserFile = \"\"\n\n\t// Other state variables\n\tFixHotkeys    = false\n\tFixConfigFile = false\n\tLastDir       = \"\"\n\tPrintLastDir  = false\n)\n\n// Still we are preventing other packages to directly modify them via reassign linter\n\nfunc SetLastDir(path string) {\n\tLastDir = path\n}\n\nfunc SetChooserFile(path string) {\n\tChooserFile = path\n}\n\nfunc UpdateVarFromCliArgs(c *cli.Command) {\n\t// Setting the config file path\n\tconfigFileArg := c.String(\"config-file\")\n\n\t// Validate the config file exists\n\tif configFileArg != \"\" {\n\t\tif _, err := os.Stat(configFileArg); err != nil {\n\t\t\tutils.PrintfAndExitf(\"Error: While reading config file '%s' from argument : %v\", configFileArg, err)\n\t\t}\n\t\tConfigFile = configFileArg\n\t}\n\n\thotkeyFileArg := c.String(\"hotkey-file\")\n\n\tif hotkeyFileArg != \"\" {\n\t\tif _, err := os.Stat(hotkeyFileArg); err != nil {\n\t\t\tutils.PrintfAndExitf(\"Error: While reading hotkey file '%s' from argument : %v\", hotkeyFileArg, err)\n\t\t}\n\t\tHotkeysFile = hotkeyFileArg\n\t}\n\n\t// It could be non existent. We are writing to the file. If file doesn't exists, we would attempt to create it.\n\tSetChooserFile(c.String(\"chooser-file\"))\n\n\tFixHotkeys = c.Bool(\"fix-hotkeys\")\n\tFixConfigFile = c.Bool(\"fix-config-file\")\n\tPrintLastDir = c.Bool(\"print-last-dir\")\n}\n"
  },
  {
    "path": "src/config/icon/function.go",
    "content": "package icon\n\n// InitIcon initializes the icon configuration for the application.\n// It sets up different icons based on whether nerd fonts are enabled and configures directory icon colors.\n//\n// Parameters:\n//   - nerdfont: boolean flag to determine if nerd fonts should be used\n//     When false, uses simple ASCII characters for icons\n//     When true, uses nerd font icons (default behavior)\n//   - directoryIconColor: string representing the color for directory icons\n//     If empty, defaults to \"NONE\" (dark yellowish)\n//\n// The function configures various icons for:\n//   - System directories (Home, Download, Documents, etc.)\n//   - File operations (Compress, Extract, Copy, Cut, Delete)\n//   - UI elements (Cursor, Browser, Select, etc.)\n//   - Status indicators (Error, Warn, Done, InOperation)\n//   - Navigation and sorting (Directory, Search, SortAsc, SortDesc)\nfunc InitIcon(nerdfont bool, directoryIconColor string) {\n\t// Make sure that these alternatives are ASCII characters only.\n\t// Dont place any special unicode characters here.\n\tif !nerdfont {\n\t\t// When nerdfont is disabled, we use simple ASCII characters\n\t\t// Space is set to empty string because we don't need special spacing\n\t\t// for ASCII characters, unlike nerd fonts which often need proper spacing\n\t\t// to display correctly\n\t\tSpace = \"\"\n\t\tSuperfileIcon = \"\"\n\n\t\tHome = \"\"\n\t\tDownload = \"\"\n\t\tDocuments = \"\"\n\t\tPictures = \"\"\n\t\tVideos = \"\"\n\t\tMusic = \"\"\n\t\tTemplates = \"\"\n\t\tPublicShare = \"\"\n\n\t\t// file operations\n\t\tCompressFile = \"\"\n\t\tExtractFile = \"\"\n\t\tCopy = \"\"\n\t\tCut = \"\"\n\t\tDelete = \"\"\n\n\t\t// other\n\t\tCursor = \">\"\n\t\tBrowser = \"B\"\n\t\tSelect = \"S\"\n\t\tError = \"\"\n\t\tWarn = \"\"\n\t\tDone = \"\"\n\t\tInOperation = \"\"\n\t\tDirectory = \"\"\n\t\tSearch = \"\"\n\t\tSortAsc = \"^\"\n\t\tSortDesc = \"v\"\n\t\tTerminal = \"\"\n\t\tPinned = \"\"\n\t\tDisk = \"\"\n\t}\n\n\tif directoryIconColor == \"\" {\n\t\tdirectoryIconColor = \"NONE\" // Dark yellowish\n\t}\n\tFolders[\"folder\"] = Style{\n\t\tIcon:  \"\\uf07b\", // Printable Rune : \"\"\n\t\tColor: directoryIconColor,\n\t}\n}\n\nfunc GetCopyOrCutIcon(cut bool) string {\n\tif cut {\n\t\treturn Cut\n\t}\n\treturn Copy\n}\n"
  },
  {
    "path": "src/config/icon/icon.go",
    "content": "package icon\n\n// Style for icons\ntype Style struct {\n\tIcon  string\n\tColor string\n}\n\nvar (\n\tSpace         = \" \"\n\tSuperfileIcon = \"\\ue6ad\" // Printable Rune : \"\"\n\n\t// Well Known Directories\n\tHome        = \"\\U000f02dc\" // Printable Rune : \"󰋜\"\n\tDownload    = \"\\U000f03d4\" // Printable Rune : \"󰏔\"\n\tDocuments   = \"\\U000f0219\" // Printable Rune : \"󰈙\"\n\tPictures    = \"\\U000f02e9\" // Printable Rune : \"󰋩\"\n\tVideos      = \"\\U000f0381\" // Printable Rune : \"󰎁\"\n\tMusic       = \"♬\"          // Printable Rune : \"♬\"\n\tTemplates   = \"\\U000f03e2\" // Printable Rune : \"󰏢\"\n\tPublicShare = \"\\uf0ac\"     // Printable Rune : \"\"\n\tTrash       = \"\\uf1f8\"     // Printable Rune : \"\"\n\n\t// file operations\n\tCompressFile = \"\\U000f05c4\" // Printable Rune : \"󰗄\"\n\tExtractFile  = \"\\U000f06eb\" // Printable Rune : \"󰛫\"\n\tCopy         = \"\\U000f018f\" // Printable Rune : \"󰆏\"\n\tCut          = \"\\U000f0190\" // Printable Rune : \"󰆐\"\n\tDelete       = \"\\U000f01b4\" // Printable Rune : \"󰆴\"\n\n\t// other\n\tCursor          = \"\\uf054\"     // Printable Rune : \"\"\n\tBrowser         = \"\\U000f0208\" // Printable Rune : \"󰈈\"\n\tSelect          = \"\\U000f01bd\" // Printable Rune : \"󰆽\"\n\tCheckboxEmpty   = \"\\U000f0131\" // Printable Rune : \"󰄱\"\n\tCheckboxChecked = \"\\U000f0856\" // Printable Rune : \"󰡖\"\n\tError           = \"\\uf530\"     // Printable Rune : \"\"\n\tWarn            = \"\\uf071\"     // Printable Rune : \"\"\n\tDone            = \"\\uf4a4\"     // Printable Rune : \"\"\n\tInOperation     = \"\\U000f0954\" // Printable Rune : \"󰥔\"\n\tDirectory       = \"\\uf07b\"     // Printable Rune : \"\"\n\tSearch          = \"\\ue68f\"     // Printable Rune : \"\"\n\tSortAsc         = \"\\uf0de\"     // Printable Rune : \"\"\n\tSortDesc        = \"\\uf0dd\"     // Printable Rune : \"\"\n\tTerminal        = \"\\ue795\"     // Printable Rune : \"\"\n\tPinned          = \"\\U000f0403\" // Printable Rune : \"󰐃\"\n\tDisk            = \"\\U000f11f0\" // Printable Rune : \"󱇰\"\n\n)\n\n/*\nTHESE CODE BASE ON https://github.com/acarl005/ls-go\nthanks for the great work!!\n*/\n\nvar Icons = map[string]Style{\n\t\"ai\":           {Icon: \"\\ue669\", Color: \"#ce6f14\"},     // Printable Rune : \"\"\n\t\"android\":      {Icon: \"\\uf17b\", Color: \"#a7c83f\"},     // Printable Rune : \"\"\n\t\"apple\":        {Icon: \"\\ue711\", Color: \"#78909c\"},     // Printable Rune : \"\"\n\t\"asm\":          {Icon: \"\\U000f061a\", Color: \"#ff7844\"}, // Printable Rune : \"󰘚\"\n\t\"audio\":        {Icon: \"\\uf001\", Color: \"#ee524f\"},     // Printable Rune : \"\"\n\t\"binary\":       {Icon: \"\\uf471\", Color: \"#ff7844\"},     // Printable Rune : \"\"\n\t\"c\":            {Icon: \"\\ue649\", Color: \"#0188d2\"},     // Printable Rune : \"\"\n\t\"cfg\":          {Icon: \"\\ue615\", Color: \"#8B8B8B\"},     // Printable Rune : \"\"\n\t\"clj\":          {Icon: \"\\ue76a\", Color: \"#68b338\"},     // Printable Rune : \"\"\n\t\"conf\":         {Icon: \"\\ue615\", Color: \"#8B8B8B\"},     // Printable Rune : \"\"\n\t\"cpp\":          {Icon: \"\\ue646\", Color: \"#0188d2\"},     // Printable Rune : \"\"\n\t\"css\":          {Icon: \"\\uf13c\", Color: \"#2d53e5\"},     // Printable Rune : \"\"\n\t\"dart\":         {Icon: \"\\ue64c\", Color: \"#03589b\"},     // Printable Rune : \"\"\n\t\"db\":           {Icon: \"\\uf1c0\", Color: \"#FF8400\"},     // Printable Rune : \"\"\n\t\"deb\":          {Icon: \"\\ue77d\", Color: \"#ab0836\"},     // Printable Rune : \"\"\n\t\"doc\":          {Icon: \"\\ue6a5\", Color: \"#295394\"},     // Printable Rune : \"\"\n\t\"dockerfile\":   {Icon: \"\\U000f0868\", Color: \"#099cec\"}, // Printable Rune : \"󰡨\"\n\t\"ebook\":        {Icon: \"\\uf02d\", Color: \"#67b500\"},     // Printable Rune : \"\"\n\t\"env\":          {Icon: \"\\uf462\", Color: \"#eed645\"},     // Printable Rune : \"\"\n\t\"f\":            {Icon: \"\\U000f121a\", Color: \"#8e44ad\"}, // Printable Rune : \"󱈚\"\n\t\"file\":         {Icon: \"\\uf15b\", Color: \"NONE\"},        // Printable Rune : \"\"\n\t\"font\":         {Icon: \"\\uf031\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"fs\":           {Icon: \"\\ue7a7\", Color: \"#2ecc71\"},     // Printable Rune : \"\"\n\t\"gb\":           {Icon: \"\\ue272\", Color: \"#f1c40f\"},     // Printable Rune : \"\"\n\t\"gform\":        {Icon: \"\\uf298\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"git\":          {Icon: \"\\ue702\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"go\":           {Icon: \"\\ue627\", Color: \"#6ed8e5\"},     // Printable Rune : \"\"\n\t\"graphql\":      {Icon: \"\\ue662\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"glp\":          {Icon: \"\\U000f01a7\", Color: \"#3498db\"}, // Printable Rune : \"󰆧\"\n\t\"groovy\":       {Icon: \"\\ue775\", Color: \"#2ecc71\"},     // Printable Rune : \"\"\n\t\"gruntfile.js\": {Icon: \"\\ue74c\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"gulpfile.js\":  {Icon: \"\\ue610\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"gv\":           {Icon: \"\\ue225\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"h\":            {Icon: \"\\uf0fd\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"haml\":         {Icon: \"\\ue664\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"hs\":           {Icon: \"\\ue777\", Color: \"#2980b9\"},     // Printable Rune : \"\"\n\t\"html\":         {Icon: \"\\uf13b\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"hx\":           {Icon: \"\\ue666\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"ics\":          {Icon: \"\\uf073\", Color: \"#f1c40f\"},     // Printable Rune : \"\"\n\t\"image\":        {Icon: \"\\uf1c5\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"iml\":          {Icon: \"\\ue7b5\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"ini\":          {Icon: \"\\U000f016a\", Color: \"#f1c40f\"}, // Printable Rune : \"󰅪\"\n\t\"ino\":          {Icon: \"\\ue255\", Color: \"#2ecc71\"},     // Printable Rune : \"\"\n\t\"iso\":          {Icon: \"\\U000f02ca\", Color: \"#f1c40f\"}, // Printable Rune : \"󰋊\"\n\t\"jade\":         {Icon: \"\\ue66c\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"java\":         {Icon: \"\\ue738\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"jenkinsfile\":  {Icon: \"\\ue767\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"jl\":           {Icon: \"\\ue624\", Color: \"#2ecc71\"},     // Printable Rune : \"\"\n\t\"js\":           {Icon: \"\\ue781\", Color: \"#f39c12\"},     // Printable Rune : \"\"\n\t\"json\":         {Icon: \"\\ue60b\", Color: \"#f1c40f\"},     // Printable Rune : \"\"\n\t\"jsx\":          {Icon: \"\\ue7ba\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"key\":          {Icon: \"\\uf43d\", Color: \"#f1c40f\"},     // Printable Rune : \"\"\n\t\"ko\":           {Icon: \"\\uebc6\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"kt\":           {Icon: \"\\ue634\", Color: \"#2980b9\"},     // Printable Rune : \"\"\n\t\"less\":         {Icon: \"\\ue758\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"link_file\":    {Icon: \"\\uf481\", Color: \"NONE\"},        // Printable Rune : \"\"\n\t\"lock\":         {Icon: \"\\uf023\", Color: \"#f1c40f\"},     // Printable Rune : \"\"\n\t\"log\":          {Icon: \"\\uf18d\", Color: \"#7f8c8d\"},     // Printable Rune : \"\"\n\t\"lua\":          {Icon: \"\\ue620\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"maintainers\":  {Icon: \"\\uf0c0\", Color: \"#7f8c8d\"},     // Printable Rune : \"\"\n\t\"makefile\":     {Icon: \"\\ue20f\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"md\":           {Icon: \"\\uf48a\", Color: \"#7f8c8d\"},     // Printable Rune : \"\"\n\t\"mjs\":          {Icon: \"\\ue718\", Color: \"#f39c12\"},     // Printable Rune : \"\"\n\t\"ml\":           {Icon: \"\\U000f0627\", Color: \"#2ecc71\"}, // Printable Rune : \"󰘧\"\n\t\"mustache\":     {Icon: \"\\ue60f\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"nc\":           {Icon: \"\\U000f02c1\", Color: \"#f1c40\"},  // Printable Rune : \"󰋁\"\n\t\"nim\":          {Icon: \"\\ue677\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"nix\":          {Icon: \"\\uf313\", Color: \"#f39c12\"},     // Printable Rune : \"\"\n\t\"npmignore\":    {Icon: \"\\ue71e\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"package\":      {Icon: \"\\U000f03d7\", Color: \"#9b59b6\"}, // Printable Rune : \"󰏗\"\n\t\"passwd\":       {Icon: \"\\uf023\", Color: \"#f1c40f\"},     // Printable Rune : \"\"\n\t\"patch\":        {Icon: \"\\uf440\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"pdf\":          {Icon: \"\\uf1c1\", Color: \"#d35400\"},     // Printable Rune : \"\"\n\t\"php\":          {Icon: \"\\ue608\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"pl\":           {Icon: \"\\ue7a1\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"prisma\":       {Icon: \"\\ue684\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"ppt\":          {Icon: \"\\uf1c4\", Color: \"#c0392b\"},     // Printable Rune : \"\"\n\t\"psd\":          {Icon: \"\\ue7b8\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"py\":           {Icon: \"\\ue606\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"r\":            {Icon: \"\\ue68a\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"rb\":           {Icon: \"\\ue21e\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"rdb\":          {Icon: \"\\ue76d\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"rpm\":          {Icon: \"\\uf17c\", Color: \"#d35400\"},     // Printable Rune : \"\"\n\t\"rs\":           {Icon: \"\\ue7a8\", Color: \"#f39c12\"},     // Printable Rune : \"\"\n\t\"rss\":          {Icon: \"\\uf09e\", Color: \"#c0392b\"},     // Printable Rune : \"\"\n\t\"rst\":          {Icon: \"\\U000f016b\", Color: \"#2ecc71\"}, // Printable Rune : \"󰅫\"\n\t\"rubydoc\":      {Icon: \"\\ue73b\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"sass\":         {Icon: \"\\ue603\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"scala\":        {Icon: \"\\ue737\", Color: \"#e67e22\"},     // Printable Rune : \"\"\n\t\"shell\":        {Icon: \"\\uf489\", Color: \"#2ecc71\"},     // Printable Rune : \"\"\n\t\"shp\":          {Icon: \"\\U000f065e\", Color: \"#f1c40f\"}, // Printable Rune : \"󰙞\"\n\t\"sol\":          {Icon: \"\\U000f086a\", Color: \"#3498db\"}, // Printable Rune : \"󰡪\"\n\t\"sqlite\":       {Icon: \"\\ue7c4\", Color: \"#27ae60\"},     // Printable Rune : \"\"\n\t\"styl\":         {Icon: \"\\ue600\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n\t\"svelte\":       {Icon: \"\\ue697\", Color: \"#ff3e00\"},     // Printable Rune : \"\"\n\t\"swift\":        {Icon: \"\\ue755\", Color: \"#ff6f61\"},     // Printable Rune : \"\"\n\t\"tex\":          {Icon: \"\\u222b\", Color: \"#9b59b6\"},     // Printable Rune : \"∫\"\n\t\"tf\":           {Icon: \"\\ue69a\", Color: \"#2ecc71\"},     // Printable Rune : \"\"\n\t\"toml\":         {Icon: \"\\U000f016a\", Color: \"#f39c12\"}, // Printable Rune : \"󰅪\"\n\t\"ts\":           {Icon: \"\\U000f06e6\", Color: \"#2980b9\"}, // Printable Rune : \"󰛦\"\n\t\"twig\":         {Icon: \"\\ue61c\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"txt\":          {Icon: \"\\uf15c\", Color: \"#7f8c8d\"},     // Printable Rune : \"\"\n\t\"vagrantfile\":  {Icon: \"\\ue21e\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"video\":        {Icon: \"\\uf03d\", Color: \"#c0392b\"},     // Printable Rune : \"\"\n\t\"vim\":          {Icon: \"\\ue62b\", Color: \"#019833\"},     // Printable Rune : \"\"\n\t\"vue\":          {Icon: \"\\ue6a0\", Color: \"#41b883\"},     // Printable Rune : \"\"\n\t\"windows\":      {Icon: \"\\uf17a\", Color: \"#4a90e2\"},     // Printable Rune : \"\"\n\t\"xls\":          {Icon: \"\\uf1c3\", Color: \"#27ae60\"},     // Printable Rune : \"\"\n\t\"xml\":          {Icon: \"\\ue796\", Color: \"#3498db\"},     // Printable Rune : \"\"\n\t\"yml\":          {Icon: \"\\ue601\", Color: \"#f39c12\"},     // Printable Rune : \"\"\n\t\"zig\":          {Icon: \"\\ue6a9\", Color: \"#9b59b6\"},     // Printable Rune : \"\"\n\t\"zip\":          {Icon: \"\\uf410\", Color: \"#e74c3c\"},     // Printable Rune : \"\"\n}\n\nvar Aliases = map[string]string{\n\t\"dart\":             \"dart\",\n\t\"apk\":              \"android\",\n\t\"gradle\":           \"android\",\n\t\"ds_store\":         \"apple\",\n\t\"localized\":        \"apple\",\n\t\"m\":                \"apple\",\n\t\"mm\":               \"apple\",\n\t\"s\":                \"asm\",\n\t\"aac\":              \"audio\",\n\t\"alac\":             \"audio\",\n\t\"flac\":             \"audio\",\n\t\"m4a\":              \"audio\",\n\t\"mka\":              \"audio\",\n\t\"mp3\":              \"audio\",\n\t\"ogg\":              \"audio\",\n\t\"opus\":             \"audio\",\n\t\"wav\":              \"audio\",\n\t\"wma\":              \"audio\",\n\t\"bson\":             \"binary\",\n\t\"feather\":          \"binary\",\n\t\"mat\":              \"binary\",\n\t\"o\":                \"binary\",\n\t\"pb\":               \"binary\",\n\t\"pickle\":           \"binary\",\n\t\"pkl\":              \"binary\",\n\t\"tfrecord\":         \"binary\",\n\t\"conf\":             \"cfg\",\n\t\"config\":           \"cfg\",\n\t\"cljc\":             \"clj\",\n\t\"cljs\":             \"clj\",\n\t\"editorconfig\":     \"conf\",\n\t\"rc\":               \"conf\",\n\t\"c++\":              \"cpp\",\n\t\"cc\":               \"cpp\",\n\t\"cxx\":              \"cpp\",\n\t\"scss\":             \"css\",\n\t\"sql\":              \"db\",\n\t\"docx\":             \"doc\",\n\t\"gdoc\":             \"doc\",\n\t\"dockerignore\":     \"dockerfile\",\n\t\"epub\":             \"ebook\",\n\t\"ipynb\":            \"ebook\",\n\t\"mobi\":             \"ebook\",\n\t\"env\":              \"env\",\n\t\".env.local\":       \"env\",\n\t\"local\":            \"env\",\n\t\"f03\":              \"f\",\n\t\"f77\":              \"f\",\n\t\"f90\":              \"f\",\n\t\"f95\":              \"f\",\n\t\"for\":              \"f\",\n\t\"fpp\":              \"f\",\n\t\"ftn\":              \"f\",\n\t\"eot\":              \"font\",\n\t\"otf\":              \"font\",\n\t\"ttf\":              \"font\",\n\t\"woff\":             \"font\",\n\t\"woff2\":            \"font\",\n\t\"fsi\":              \"fs\",\n\t\"fsscript\":         \"fs\",\n\t\"fsx\":              \"fs\",\n\t\"dna\":              \"gb\",\n\t\"gitattributes\":    \"git\",\n\t\"gitconfig\":        \"git\",\n\t\"gitignore\":        \"git\",\n\t\"gitignore_global\": \"git\",\n\t\"gitmirrorall\":     \"git\",\n\t\"gitmodules\":       \"git\",\n\t\"gltf\":             \"glp\",\n\t\"gsh\":              \"groovy\",\n\t\"gvy\":              \"groovy\",\n\t\"gy\":               \"groovy\",\n\t\"h++\":              \"h\",\n\t\"hh\":               \"h\",\n\t\"hpp\":              \"h\",\n\t\"hxx\":              \"h\",\n\t\"lhs\":              \"hs\",\n\t\"htm\":              \"html\",\n\t\"xhtml\":            \"html\",\n\t\"bmp\":              \"image\",\n\t\"cbr\":              \"image\",\n\t\"cbz\":              \"image\",\n\t\"dvi\":              \"image\",\n\t\"eps\":              \"image\",\n\t\"gif\":              \"image\",\n\t\"ico\":              \"image\",\n\t\"jpeg\":             \"image\",\n\t\"jpg\":              \"image\",\n\t\"nef\":              \"image\",\n\t\"orf\":              \"image\",\n\t\"pbm\":              \"image\",\n\t\"pgm\":              \"image\",\n\t\"png\":              \"image\",\n\t\"pnm\":              \"image\",\n\t\"ppm\":              \"image\",\n\t\"pxm\":              \"image\",\n\t\"sixel\":            \"image\",\n\t\"stl\":              \"image\",\n\t\"svg\":              \"image\",\n\t\"tif\":              \"image\",\n\t\"tiff\":             \"image\",\n\t\"webp\":             \"image\",\n\t\"xpm\":              \"image\",\n\t\"disk\":             \"iso\",\n\t\"dmg\":              \"iso\",\n\t\"img\":              \"iso\",\n\t\"ipsw\":             \"iso\",\n\t\"smi\":              \"iso\",\n\t\"vhd\":              \"iso\",\n\t\"vhdx\":             \"iso\",\n\t\"vmdk\":             \"iso\",\n\t\"jar\":              \"java\",\n\t\"kts\":              \"kt\",\n\t\"cjs\":              \"js\",\n\t\"properties\":       \"json\",\n\t\"webmanifest\":      \"json\",\n\t\"tsx\":              \"jsx\",\n\t\"cjsx\":             \"jsx\",\n\t\"cer\":              \"key\",\n\t\"crt\":              \"key\",\n\t\"der\":              \"key\",\n\t\"gpg\":              \"key\",\n\t\"p7b\":              \"key\",\n\t\"pem\":              \"key\",\n\t\"pfx\":              \"key\",\n\t\"pgp\":              \"key\",\n\t\"license\":          \"key\",\n\t\"codeowners\":       \"maintainers\",\n\t\"credits\":          \"maintainers\",\n\t\"cmake\":            \"makefile\",\n\t\"justfile\":         \"makefile\",\n\t\"markdown\":         \"md\",\n\t\"mkd\":              \"md\",\n\t\"rdoc\":             \"md\",\n\t\"readme\":           \"md\",\n\t\"mli\":              \"ml\",\n\t\"sml\":              \"ml\",\n\t\"netcdf\":           \"nc\",\n\t\"brewfile\":         \"package\",\n\t\"cargo.toml\":       \"package\",\n\t\"cargo.lock\":       \"package\",\n\t\"go.mod\":           \"package\",\n\t\"go.sum\":           \"package\",\n\t\"pyproject.toml\":   \"package\",\n\t\"poetry.lock\":      \"package\",\n\t\"package.json\":     \"package\",\n\t\"pipfile\":          \"package\",\n\t\"pipfile.lock\":     \"package\",\n\t\"php3\":             \"php\",\n\t\"php4\":             \"php\",\n\t\"php5\":             \"php\",\n\t\"phpt\":             \"php\",\n\t\"phtml\":            \"php\",\n\t\"gslides\":          \"ppt\",\n\t\"pptx\":             \"ppt\",\n\t\"pxd\":              \"py\",\n\t\"pyc\":              \"py\",\n\t\"pyx\":              \"py\",\n\t\"whl\":              \"py\",\n\t\"rdata\":            \"r\",\n\t\"rds\":              \"r\",\n\t\"rmd\":              \"r\",\n\t\"gemfile\":          \"rb\",\n\t\"gemspec\":          \"rb\",\n\t\"guardfile\":        \"rb\",\n\t\"procfile\":         \"rb\",\n\t\"rakefile\":         \"rb\",\n\t\"rspec\":            \"rb\",\n\t\"rspec_parallel\":   \"rb\",\n\t\"rspec_status\":     \"rb\",\n\t\"ru\":               \"rb\",\n\t\"erb\":              \"rubydoc\",\n\t\"slim\":             \"rubydoc\",\n\t\"awk\":              \"shell\",\n\t\"bash\":             \"shell\",\n\t\"bash_history\":     \"shell\",\n\t\"bash_profile\":     \"shell\",\n\t\"bashrc\":           \"shell\",\n\t\"csh\":              \"shell\",\n\t\"fish\":             \"shell\",\n\t\"ksh\":              \"shell\",\n\t\"sh\":               \"shell\",\n\t\"zsh\":              \"shell\",\n\t\"zsh-theme\":        \"shell\",\n\t\"zshrc\":            \"shell\",\n\t\"plpgsql\":          \"sql\",\n\t\"plsql\":            \"sql\",\n\t\"psql\":             \"sql\",\n\t\"tsql\":             \"sql\",\n\t\"sl3\":              \"sqlite\",\n\t\"sqlite3\":          \"sqlite\",\n\t\"stylus\":           \"styl\",\n\t\"cls\":              \"tex\",\n\t\"avi\":              \"video\",\n\t\"flv\":              \"video\",\n\t\"m2v\":              \"video\",\n\t\"mkv\":              \"video\",\n\t\"mov\":              \"video\",\n\t\"mp4\":              \"video\",\n\t\"mpeg\":             \"video\",\n\t\"mpg\":              \"video\",\n\t\"ogm\":              \"video\",\n\t\"ogv\":              \"video\",\n\t\"vob\":              \"video\",\n\t\"webm\":             \"video\",\n\t\"vimrc\":            \"vim\",\n\t\"bat\":              \"windows\",\n\t\"cmd\":              \"windows\",\n\t\"exe\":              \"windows\",\n\t\"csv\":              \"xls\",\n\t\"gsheet\":           \"xls\",\n\t\"xlsx\":             \"xls\",\n\t\"plist\":            \"xml\",\n\t\"xul\":              \"xml\",\n\t\"yaml\":             \"yml\",\n\t\"7z\":               \"zip\",\n\t\"Z\":                \"zip\",\n\t\"bz2\":              \"zip\",\n\t\"gz\":               \"zip\",\n\t\"lzma\":             \"zip\",\n\t\"par\":              \"zip\",\n\t\"rar\":              \"zip\",\n\t\"tar\":              \"zip\",\n\t\"tc\":               \"zip\",\n\t\"tgz\":              \"zip\",\n\t\"txz\":              \"zip\",\n\t\"xz\":               \"zip\",\n\t\"z\":                \"zip\",\n}\n\nvar Folders = map[string]Style{\n\t\".atom\":   {Icon: \"\\ue764\", Color: \"#66595c\"}, // Atom folder - Dark gray // Printable Rune : \"\"\n\t\".aws\":    {Icon: \"\\ue7ad\", Color: \"#ff9900\"}, // AWS folder - Orange // Printable Rune : \"\"\n\t\".docker\": {Icon: \"\\ue7b0\", Color: \"#0db7ed\"}, // Docker folder - Blue // Printable Rune : \"\"\n\t\".gem\":    {Icon: \"\\ue21e\", Color: \"#e9573f\"}, // Gem folder - Red // Printable Rune : \"\"\n\t\".git\":    {Icon: \"\\ue5fb\", Color: \"#f14e32\"}, // Git folder - Red // Printable Rune : \"\"\n\t\".git-credential-cache\": {\n\t\tIcon:  \"\\ue5fb\",\n\t\tColor: \"#f14e32\",\n\t}, // Git credential cache folder - Red // Printable Rune : \"\"\n\t\".github\": {Icon: \"\\ue5fd\", Color: \"#000000\"}, // GitHub folder - Black // Printable Rune : \"\"\n\t\".npm\":    {Icon: \"\\ue5fa\", Color: \"#cb3837\"}, // npm folder - Red // Printable Rune : \"\"\n\t\".nvm\":    {Icon: \"\\ue718\", Color: \"#cb3837\"}, // nvm folder - Red // Printable Rune : \"\"\n\t\".rvm\":    {Icon: \"\\ue21e\", Color: \"#e9573f\"}, // rvm folder - Red // Printable Rune : \"\"\n\t\".Trash\":  {Icon: \"\\uf1f8\", Color: \"#7f8c8d\"}, // Trash folder - Light gray // Printable Rune : \"\"\n\t\".vscode\": {Icon: \"\\ue70c\", Color: \"#007acc\"}, // VSCode folder - Blue // Printable Rune : \"\"\n\t\".vim\":    {Icon: \"\\ue62b\", Color: \"#019833\"}, // Vim folder - Green // Printable Rune : \"\"\n\t\"config\":  {Icon: \"\\ue5fc\", Color: \"#ffb86c\"}, // Config folder - Light orange // Printable Rune : \"\"\n\t// Item for Generic folder, with key \"folder\" is initialized in InitIcon()\n\t\"hidden\":       {Icon: \"\\uf023\", Color: \"#75715e\"}, // Hidden folder - Dark yellowish // Printable Rune : \"\"\n\t\"node_modules\": {Icon: \"\\ue5fa\", Color: \"#cb3837\"}, // Node modules folder - Red // Printable Rune : \"\"\n\t\"link_folder\":  {Icon: \"\\uf482\", Color: \"NONE\"},    // link folder - None // Printable Rune : \"\"\n\n\t\"superfile\": {Icon: \"\\U000f069d\", Color: \"#FF6F00\"}, // Printable Rune : \"󰚝\"\n}\n"
  },
  {
    "path": "src/internal/backend/README.md",
    "content": "# Backend Package\nHandles operations on the User's OS.\nFor example, executing shell commands, performing file operations on user's files...\nReading OS-specific configurations like disk partitions.\n\nThe name 'backend' isn't the most appropriate, open to suggestions.\nThis would modularize the code, and would enable us to write unit tests \nwhere we would 'mock' the backend functionality with dummy interface \nimplementations\n\n# Dependencies\nShould not import any \"ui\" package\nCan import common and its subpackages\n\n# Implementation specifications\nTry to implement everything via interfaces, so that we can easily write unit tests\n"
  },
  {
    "path": "src/internal/common/README.md",
    "content": "# common package\nDefines common utilities for ui and file operations package\neveryone can use common package, but common package should not have any dependency on\nany other package. Currently, common package is a big monolith, but we plan to separate it into config,\n\n\n# Dependencies\n- src/config package"
  },
  {
    "path": "src/internal/common/config_type.go",
    "content": "package common\n\n// Theme configuration\ntype ThemeType struct {\n\t// Code syntax highlight theme\n\tCodeSyntaxHighlightTheme string `toml:\"code_syntax_highlight\"`\n\n\t// Border\n\tFilePanelBorder string `toml:\"file_panel_border\"`\n\tSidebarBorder   string `toml:\"sidebar_border\"`\n\tFooterBorder    string `toml:\"footer_border\"`\n\n\t// Border Active\n\tFilePanelBorderActive string `toml:\"file_panel_border_active\"`\n\tSidebarBorderActive   string `toml:\"sidebar_border_active\"`\n\tFooterBorderActive    string `toml:\"footer_border_active\"`\n\tModalBorderActive     string `toml:\"modal_border_active\"`\n\n\t// Background (bg)\n\tFullScreenBG string `toml:\"full_screen_bg\"`\n\tFilePanelBG  string `toml:\"file_panel_bg\"`\n\tSidebarBG    string `toml:\"sidebar_bg\"`\n\tFooterBG     string `toml:\"footer_bg\"`\n\tModalBG      string `toml:\"modal_bg\"`\n\n\t// Foreground (fg)\n\tFullScreenFG string `toml:\"full_screen_fg\"`\n\tFilePanelFG  string `toml:\"file_panel_fg\"`\n\tSidebarFG    string `toml:\"sidebar_fg\"`\n\tFooterFG     string `toml:\"footer_fg\"`\n\tModalFG      string `toml:\"modal_fg\"`\n\n\t// Special Color\n\tCursor  string `toml:\"cursor\"`\n\tCorrect string `toml:\"correct\"`\n\tError   string `toml:\"error\"`\n\tHint    string `toml:\"hint\"`\n\tCancel  string `toml:\"cancel\"`\n\t// Note: this is linked with `RequiredGradientColorCount` constant\n\tGradientColor      []string `toml:\"gradient_color\"`\n\tDirectoryIconColor string   `toml:\"directory_icon_color\"`\n\n\t// File Panel Special Items\n\tFilePanelTopDirectoryIcon string `toml:\"file_panel_top_directory_icon\"`\n\tFilePanelTopPath          string `toml:\"file_panel_top_path\"`\n\tFilePanelItemSelectedFG   string `toml:\"file_panel_item_selected_fg\"`\n\tFilePanelItemSelectedBG   string `toml:\"file_panel_item_selected_bg\"`\n\n\t// Sidebar Special Items\n\tSidebarTitle          string `toml:\"sidebar_title\"`\n\tSidebarItemSelectedFG string `toml:\"sidebar_item_selected_fg\"`\n\tSidebarItemSelectedBG string `toml:\"sidebar_item_selected_bg\"`\n\tSidebarDivider        string `toml:\"sidebar_divider\"`\n\n\t// Modal Special Items\n\tModalCancelFG  string `toml:\"modal_cancel_fg\"`\n\tModalCancelBG  string `toml:\"modal_cancel_bg\"`\n\tModalConfirmFG string `toml:\"modal_confirm_fg\"`\n\tModalConfirmBG string `toml:\"modal_confirm_bg\"`\n\n\tHelpMenuHotkey string `toml:\"help_menu_hotkey\"`\n\tHelpMenuTitle  string `toml:\"help_menu_title\"`\n}\n\n// Configuration settings\ntype ConfigType struct {\n\tTheme string `toml:\"theme\" comment:\"More details are at https://superfile.dev/configure/superfile-config/\\nchange your theme\"`\n\n\tEditor    string `toml:\"editor\" comment:\"\\nThe editor files will be opened with. (Leave blank to use the EDITOR environment variable).\"`\n\tDirEditor string `toml:\"dir_editor\" comment:\"\\nThe editor directories will be opened with. (Leave blank to use the default editors).\"`\n\t// The table (map) for editor by file extension\n\tOpenWith map[string]string `toml:\"open_with\" comment:\"\\nCustom open commands by file extension.\"`\n\n\tAutoCheckUpdate        bool   `toml:\"auto_check_update\" comment:\"\\nAuto check for update\"`\n\tCdOnQuit               bool   `toml:\"cd_on_quit\" comment:\"\\nCd on quit (For more details, please check out https://superfile.dev/configure/superfile-config/#cd_on_quit)\"`\n\tDefaultOpenFilePreview bool   `toml:\"default_open_file_preview\" comment:\"\\nWhether to open file preview automatically every time superfile is opened.\"`\n\tShowImagePreview       bool   `toml:\"show_image_preview\" comment:\"\\nWhether to show image preview.\"`\n\tShowPanelFooterInfo    bool   `toml:\"show_panel_footer_info\" comment:\"\\nWhether to show additional footer info for file panel.\"`\n\tDefaultDirectory       string `toml:\"default_directory\" comment:\"\\nThe path of the first file panel when superfile is opened.\"`\n\tFileSizeUseSI          bool   `toml:\"file_size_use_si\" comment:\"\\nDisplay file sizes using powers of 1000 (kB, MB, GB) instead of powers of 1024 (KiB, MiB, GiB).\"`\n\tDefaultSortType        int    `toml:\"default_sort_type\" comment:\"\\nDefault sort type (0: Name, 1: Size, 2: Date Modified, 3: Type, 4: Natural).\"`\n\tSortOrderReversed      bool   `toml:\"sort_order_reversed\" comment:\"\\nDefault sort order (false: Ascending, true: Descending).\"`\n\tCaseSensitiveSort      bool   `toml:\"case_sensitive_sort\" comment:\"\\nCase sensitive sort by name (capital \\\"B\\\" comes before \\\"a\\\" if true).\"`\n\tShellCloseOnSuccess    bool   `toml:\"shell_close_on_success\" comment:\"\\nWhether to close the shell on successful command execution.\"`\n\tDebug                  bool   `toml:\"debug\" comment:\"\\nWhether to enable debug mode.\"`\n\t// IgnoreMissingFields controls whether warnings about missing TOML fields are suppressed.\n\tIgnoreMissingFields   bool `toml:\"ignore_missing_fields\" comment:\"\\nWhether to ignore warnings about missing fields in the config file.\"`\n\tPageScrollSize        int  `toml:\"page_scroll_size\" comment:\"\\nNumber of lines to scroll for PgUp/PgDown keys (0: full page, default behavior).\"`\n\tFilePanelExtraColumns int  `toml:\"file_panel_extra_columns\" comment:\"\\nCount of extra columns in file panel in addition to file name. When option equal 0 then feature is disabled.\"`\n\tFilePanelNamePercent  int  `toml:\"file_panel_name_percent\" comment:\"\\nPercentage of file panel width allocated to file names (25-100). Higher values give more space to names, less to extra columns.\"`\n\n\tNerdfont                bool     `toml:\"nerdfont\" comment:\"\\n================   Style =================\\n\\n If you don't have or don't want Nerdfont installed you can turn this off\"`\n\tShowSelectIcons         bool     `toml:\"show_select_icons\" comment:\"\\nShow checkbox icons in select mode (requires nerdfont)\"`\n\tTransparentBackground   bool     `toml:\"transparent_background\" comment:\"\\nSet transparent background or not (this only work when your terminal background is transparent)\"`\n\tFilePreviewWidth        int      `toml:\"file_preview_width\" comment:\"\\nFile preview width allow '0' (this mean same as file panel),'x' x must be less than 10 and greater than 1 (This means that the width of the file preview will be one xth of the total width.)\"`\n\tEnableFilePreviewBorder bool     `toml:\"enable_file_preview_border\" comment:\"\\nEnable border around the file preview panel (default: false)\"`\n\tCodePreviewer           string   `toml:\"code_previewer\" comment:\"\\nWhether to use the builtin syntax highlighting with chroma or use bat. Values: \\\"\\\" for builtin chroma, \\\"bat\\\" for bat\"`\n\tSidebarWidth            int      `toml:\"sidebar_width\" comment:\"\\nThe length of the sidebar(excluding borders). If you don't find to display the sidebar, you can input 0 directly. If you want to display the value, please place it in the range of 5-20.\"`\n\tSidebarSections         []string `toml:\"sidebar_sections\" comment:\"\\nOrder of sidebar sections (valid values: \\\"home\\\", \\\"pinned\\\", \\\"disks\\\").\\nOnly sections included in this list will be displayed.\"`\n\n\tBorderTop         string `toml:\"border_top\" comment:\"\\nBorder style\"`\n\tBorderBottom      string `toml:\"border_bottom\"`\n\tBorderLeft        string `toml:\"border_left\"`\n\tBorderRight       string `toml:\"border_right\"`\n\tBorderTopLeft     string `toml:\"border_top_left\"`\n\tBorderTopRight    string `toml:\"border_top_right\"`\n\tBorderBottomLeft  string `toml:\"border_bottom_left\"`\n\tBorderBottomRight string `toml:\"border_bottom_right\"`\n\tBorderMiddleLeft  string `toml:\"border_middle_left\"`\n\tBorderMiddleRight string `toml:\"border_middle_right\"`\n\n\tMetadata          bool `toml:\"metadata\" comment:\"\\n==========PLUGINS========== #\\nPlugins means that you need to install some external dependencies to use them.\\n\\nShow more detailed metadata, please install exiftool before enabling this plugin!\"`\n\tEnableMD5Checksum bool `toml:\"enable_md5_checksum\" comment:\"Enable MD5 checksum generation for files\"`\n\tZoxideSupport     bool `toml:\"zoxide_support\" comment:\"Zoxide support for the fast navigation\"`\n}\n\n// GetIgnoreMissingFields reports whether warnings about missing TOML fields should be ignored.\nfunc (c *ConfigType) GetIgnoreMissingFields() bool {\n\treturn c.IgnoreMissingFields\n}\n\ntype HotkeysType struct {\n\tConfirm []string `toml:\"confirm\" comment:\"=================================================================================================\\nGlobal hotkeys (cannot conflict with other hotkeys)\"`\n\tQuit    []string `toml:\"quit\"`\n\tCdQuit  []string `toml:\"cd_quit\"`\n\n\t// movement\n\tListUp   []string `toml:\"list_up\" comment:\"movement\"`\n\tListDown []string `toml:\"list_down\"`\n\tPageUp   []string `toml:\"page_up\"`\n\tPageDown []string `toml:\"page_down\"`\n\n\tCloseFilePanel         []string `toml:\"close_file_panel\" comment:\"file panel control\"`\n\tCreateNewFilePanel     []string `toml:\"create_new_file_panel\"`\n\tSplitFilePanel         []string `toml:\"split_file_panel\"`\n\tNextFilePanel          []string `toml:\"next_file_panel\"`\n\tPreviousFilePanel      []string `toml:\"previous_file_panel\"`\n\tToggleFilePreviewPanel []string `toml:\"toggle_file_preview_panel\"`\n\tOpenSortOptionsMenu    []string `toml:\"open_sort_options_menu\"`\n\tToggleReverseSort      []string `toml:\"toggle_reverse_sort\"`\n\n\tFocusOnProcessBar []string `toml:\"focus_on_process_bar\" comment:\"change focus\"`\n\tFocusOnSidebar    []string `toml:\"focus_on_sidebar\"`\n\tFocusOnMetaData   []string `toml:\"focus_on_metadata\"`\n\n\tFilePanelItemCreate []string `toml:\"file_panel_item_create\" comment:\"create file/directory and rename \"`\n\tFilePanelItemRename []string `toml:\"file_panel_item_rename\"`\n\n\tCopyItems              []string `toml:\"copy_items\" comment:\"file operate\"`\n\tPasteItems             []string `toml:\"paste_items\"`\n\tCutItems               []string `toml:\"cut_items\"`\n\tDeleteItems            []string `toml:\"delete_items\"`\n\tPermanentlyDeleteItems []string `toml:\"permanently_delete_items\"`\n\n\tExtractFile  []string `toml:\"extract_file\" comment:\"compress and extract\"`\n\tCompressFile []string `toml:\"compress_file\"`\n\n\tOpenFileWithEditor             []string `toml:\"open_file_with_editor\" comment:\"editor\"`\n\tOpenCurrentDirectoryWithEditor []string `toml:\"open_current_directory_with_editor\"`\n\n\tPinnedDirectory []string `toml:\"pinned_directory\" comment:\"other\"`\n\tToggleDotFile   []string `toml:\"toggle_dot_file\"`\n\tChangePanelMode []string `toml:\"change_panel_mode\"`\n\tOpenHelpMenu    []string `toml:\"open_help_menu\"`\n\tOpenCommandLine []string `toml:\"open_command_line\"`\n\tOpenSPFPrompt   []string `toml:\"open_spf_prompt\"`\n\tOpenZoxide      []string `toml:\"open_zoxide\"`\n\n\tCopyPath []string `toml:\"copy_path\"`\n\tCopyPWD  []string `toml:\"copy_present_working_directory\"`\n\n\tToggleFooter []string `toml:\"toggle_footer\"`\n\n\tConfirmTyping []string `toml:\"confirm_typing\" comment:\"=================================================================================================\\nTyping hotkeys (can conflict with all hotkeys)\"`\n\tCancelTyping  []string `toml:\"cancel_typing\"`\n\n\tParentDirectory []string `toml:\"parent_directory\" comment:\"=================================================================================================\\nNormal mode hotkeys (can conflict with other modes, cannot conflict with global hotkeys)\"`\n\tSearchBar       []string `toml:\"search_bar\"`\n\n\tFilePanelSelectModeItemsSelectDown []string `toml:\"file_panel_select_mode_items_select_down\" comment:\"=================================================================================================\\nSelect mode hotkeys (can conflict with other modes, cannot conflict with global hotkeys)\"`\n\tFilePanelSelectModeItemsSelectUp   []string `toml:\"file_panel_select_mode_items_select_up\"`\n\tFilePanelSelectAllItem             []string `toml:\"file_panel_select_all_items\"`\n}\n"
  },
  {
    "path": "src/internal/common/default_config.go",
    "content": "package common\n\n// Variables for holding default configurations of each settings\nvar (\n\tHotkeysTomlString  string\n\tConfigTomlString   string\n\tDefaultThemeString string\n)\n\nvar Theme ThemeType\nvar Config ConfigType\nvar Hotkeys HotkeysType\n"
  },
  {
    "path": "src/internal/common/icon_utils.go",
    "content": "package common\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n)\n\nfunc getFileIcon(file string, isLink bool) icon.Style {\n\tif isLink {\n\t\treturn icon.Icons[\"link_file\"]\n\t}\n\text := strings.TrimPrefix(filepath.Ext(file), \".\")\n\t// default icon for all files. try to find a better one though...\n\tresultIcon := icon.Icons[\"file\"]\n\t// resolve aliased extensions\n\textKey := strings.ToLower(ext)\n\talias, hasAlias := icon.Aliases[extKey]\n\tif hasAlias {\n\t\textKey = alias\n\t}\n\n\t// see if we can find a better icon based on extension alone\n\tbetterIcon, hasBetterIcon := icon.Icons[extKey]\n\tif hasBetterIcon {\n\t\tresultIcon = betterIcon\n\t}\n\n\t// now look for icons based on full names\n\tfullName := file\n\n\tfullName = strings.ToLower(fullName)\n\tfullAlias, hasFullAlias := icon.Aliases[fullName]\n\tif hasFullAlias {\n\t\tfullName = fullAlias\n\t}\n\tbestIcon, hasBestIcon := icon.Icons[fullName]\n\tif hasBestIcon {\n\t\tresultIcon = bestIcon\n\t}\n\tif resultIcon.Color == \"NONE\" {\n\t\treturn icon.Style{\n\t\t\tIcon:  resultIcon.Icon,\n\t\t\tColor: Theme.FilePanelFG,\n\t\t}\n\t}\n\treturn resultIcon\n}\n\nfunc GetElementIcon(file string, isDir bool, isLink bool, nerdFont bool) icon.Style {\n\tif !nerdFont {\n\t\treturn icon.Style{\n\t\t\tIcon:  \"\",\n\t\t\tColor: Theme.FilePanelFG,\n\t\t}\n\t}\n\n\tif isDir {\n\t\tif isLink {\n\t\t\treturn icon.Folders[\"link_folder\"]\n\t\t}\n\t\tresultIcon := icon.Folders[\"folder\"]\n\t\tbetterIcon, hasBetterIcon := icon.Folders[file]\n\t\tif hasBetterIcon {\n\t\t\tresultIcon = betterIcon\n\t\t}\n\t\treturn resultIcon\n\t}\n\n\treturn getFileIcon(file, isLink)\n}\n"
  },
  {
    "path": "src/internal/common/icon_utils_test.go",
    "content": "package common\n\nimport (\n\t\"testing\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n)\n\nfunc TestGetElementIcon(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tfile     string\n\t\tisDir    bool\n\t\tisLink   bool\n\t\tnerdFont bool\n\t\texpected icon.Style\n\t}{\n\t\t{\n\t\t\tname:     \"Non-nerdfont returns empty icon\",\n\t\t\tfile:     \"test.txt\",\n\t\t\tisDir:    false,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: false,\n\t\t\texpected: icon.Style{\n\t\t\t\tIcon:  \"\",\n\t\t\t\tColor: Theme.FilePanelFG,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"Directory with nerd font\",\n\t\t\tfile:     \"folder\",\n\t\t\tisDir:    true,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Folders[\"folder\"],\n\t\t},\n\t\t{\n\t\t\tname:     \"File with known extension\",\n\t\t\tfile:     \"test.js\",\n\t\t\tisDir:    false,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Icons[\"js\"],\n\t\t},\n\t\t{\n\t\t\tname:     \"Full name takes priority over extension\",\n\t\t\tfile:     \"gulpfile.js\",\n\t\t\tisDir:    false,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Icons[\"gulpfile.js\"],\n\t\t},\n\t\t{\n\t\t\tname:     \".git directory\",\n\t\t\tfile:     \".git\",\n\t\t\tisDir:    true,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Folders[\".git\"],\n\t\t},\n\t\t{\n\t\t\tname:     \"superfile directory\",\n\t\t\tfile:     \"superfile\",\n\t\t\tisDir:    true,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Folders[\"superfile\"],\n\t\t},\n\t\t{\n\t\t\tname:     \"package.json file\",\n\t\t\tfile:     \"package.json\",\n\t\t\tisDir:    false,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Icons[\"package\"],\n\t\t},\n\t\t{\n\t\t\tname:     \"File with unknown extension\",\n\t\t\tfile:     \"test.xyz\",\n\t\t\tisDir:    false,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Style{\n\t\t\t\tIcon: icon.Icons[\"file\"].Icon,\n\t\t\t\t// Theme is not defined here, so this will be blank\n\t\t\t\tColor: Theme.FilePanelFG,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"File with aliased name\",\n\t\t\tfile:     \"dockerfile\",\n\t\t\tisDir:    false,\n\t\t\tisLink:   false,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Icons[\"dockerfile\"],\n\t\t},\n\t\t{\n\t\t\tname:     \"Link to Directory with nerd font\",\n\t\t\tfile:     \"folder\",\n\t\t\tisDir:    true,\n\t\t\tisLink:   true,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Folders[\"link_folder\"],\n\t\t},\n\t\t{\n\t\t\tname:     \"Link to File\",\n\t\t\tfile:     \"test.js\",\n\t\t\tisDir:    false,\n\t\t\tisLink:   true,\n\t\t\tnerdFont: true,\n\t\t\texpected: icon.Icons[\"link_file\"],\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := GetElementIcon(tt.file, tt.isDir, tt.isLink, tt.nerdFont)\n\t\t\tif result.Icon != tt.expected.Icon || result.Color != tt.expected.Color {\n\t\t\t\tt.Errorf(\"GetElementIcon() = %v, want %v\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/common/load_config.go",
    "content": "package common\n\nimport (\n\t\"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/pelletier/go-toml/v2\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/config/icon\"\n)\n\n// Load configurations from the configuration file. Compares the content\n// with the default values and modify the config file to include default configs\n// if the FixConfigFile flag is on\n// TODO : Fix the code duplication with LoadHotkeysFile().\nfunc LoadConfigFile() {\n\terr := utils.LoadTomlFile(variable.ConfigFile, ConfigTomlString, &Config, variable.FixConfigFile, false)\n\tif err != nil {\n\t\tuserMsg := fmt.Sprintf(\"%s%s\", LipglossError, err.Error())\n\n\t\ttoExit := true\n\t\tvar loadError *utils.TomlLoadError\n\t\tif errors.As(err, &loadError) && loadError != nil {\n\t\t\tif loadError.MissingFields() && !variable.FixConfigFile {\n\t\t\t\t// Had missing fields and we did not fix\n\t\t\t\tuserMsg += \"\\nTo add missing fields to configuration file automatically run superfile \" +\n\t\t\t\t\t\"with the --fix-config-file flag `spf --fix-config-file`\"\n\t\t\t}\n\t\t\ttoExit = loadError.IsFatal()\n\t\t}\n\t\tif toExit {\n\t\t\tutils.PrintfAndExitf(\"%s\\n\", userMsg)\n\t\t} else {\n\t\t\tfmt.Println(userMsg)\n\t\t}\n\t}\n\n\t// Even if there is a missing field, we want to validate fields that are present\n\tif err := ValidateConfig(&Config); err != nil {\n\t\t// If config is incorrect we cannot continue. We need to exit\n\t\tutils.PrintlnAndExit(err.Error())\n\t}\n}\n\nfunc ValidateConfig(c *ConfigType) error {\n\tif (c.FilePreviewWidth > 10 || c.FilePreviewWidth < 2) && c.FilePreviewWidth != 0 {\n\t\treturn errors.New(\n\t\t\tLoadConfigError(\"file_preview_width\", \"File preview width must be 2–10, or 0 to disable preview.\"),\n\t\t)\n\t}\n\n\tif c.SidebarWidth != 0 && (c.SidebarWidth < 5 || c.SidebarWidth > 20) {\n\t\treturn errors.New(LoadConfigError(\"sidebar_width\", \"Sidebar width must be 5–20, or 0 to hide the sidebar.\"))\n\t}\n\n\tfor _, order := range c.SidebarSections {\n\t\tif order != utils.SidebarSectionHome &&\n\t\t\torder != utils.SidebarSectionPinned &&\n\t\t\torder != utils.SidebarSectionDisks {\n\t\t\treturn errors.New(\n\t\t\t\tLoadConfigError(\n\t\t\t\t\t\"sidebar_sections\",\n\t\t\t\t\t\"Sidebar sections contain an unsupported value. Allowed values are: home, pinned, disks.\",\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t}\n\n\tif c.DefaultSortType < 0 || c.DefaultSortType > 4 {\n\t\treturn errors.New(LoadConfigError(\"default_sort_type\", \"Default sort type must be between 0 and 4.\"))\n\t}\n\n\tif c.FilePanelNamePercent < FileNameRatioMin || c.FilePanelNamePercent > FileNameRatioMax {\n\t\treturn errors.New(\n\t\t\tLoadConfigError(\"file_panel_name_percent\", \"File panel name percent is outside the supported range.\"),\n\t\t)\n\t}\n\n\tif ansi.StringWidth(c.BorderTop) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_top\", \"Border character must be exactly one cell wide.\"))\n\t}\n\n\treturn validateBorders(c)\n}\n\nfunc validateBorders(c *ConfigType) error {\n\tif ansi.StringWidth(c.BorderBottom) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_bottom\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderLeft) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_left\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderRight) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_right\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderBottomLeft) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_bottom_left\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderBottomRight) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_bottom_right\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderTopLeft) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_top_left\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderTopRight) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_top_right\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderMiddleLeft) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_middle_left\", \"Border character must be exactly one cell wide.\"))\n\t}\n\tif ansi.StringWidth(c.BorderMiddleRight) != 1 {\n\t\treturn errors.New(LoadConfigError(\"border_middle_right\", \"Border character must be exactly one cell wide.\"))\n\t}\n\n\treturn nil\n}\n\n// Load keybinds from the hotkeys file. Compares the content\n// with the default values and modify the hotkeys if the FixHotkeys flag is on.\nfunc LoadHotkeysFile(ignoreMissingFields bool) {\n\terr := utils.LoadTomlFile(\n\t\tvariable.HotkeysFile,\n\t\tHotkeysTomlString,\n\t\t&Hotkeys,\n\t\tvariable.FixHotkeys,\n\t\tignoreMissingFields,\n\t)\n\tif err != nil {\n\t\tuserMsg := fmt.Sprintf(\"%s%s\", LipglossError, err.Error())\n\n\t\ttoExit := true\n\t\tvar loadError *utils.TomlLoadError\n\t\tif errors.As(err, &loadError) {\n\t\t\tif loadError.MissingFields() && !variable.FixHotkeys {\n\t\t\t\t// Had missing fields and we did not fix\n\t\t\t\tuserMsg += \"\\nTo add missing fields to hotkeys file automatically run superfile \" +\n\t\t\t\t\t\"with the --fix-hotkeys flag `spf --fix-hotkeys`\"\n\t\t\t}\n\t\t\ttoExit = loadError.IsFatal()\n\t\t}\n\t\tif toExit {\n\t\t\tutils.PrintfAndExitf(\"%s\\n\", userMsg)\n\t\t} else {\n\t\t\tfmt.Println(userMsg)\n\t\t}\n\t}\n\n\t// Validate hotkey values\n\tval := reflect.ValueOf(Hotkeys)\n\tfor i := range val.NumField() {\n\t\tfield := val.Type().Field(i)\n\t\tvalue := val.Field(i)\n\n\t\t// Although this is redundant as Hotkey is always a slice\n\t\t// This adds a layer against accidental struct modifications\n\t\t// Makes sure its always be a string slice. It's somewhat like a unit test\n\t\tif value.Kind() != reflect.Slice || value.Type().Elem().Kind() != reflect.String {\n\t\t\tutils.PrintlnAndExit(\n\t\t\t\tLoadHotkeysError(\n\t\t\t\t\tfield.Name,\n\t\t\t\t\t\"Hotkey value must be a list of strings.\",\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\thotkeysList, ok := value.Interface().([]string)\n\t\tif !ok || len(hotkeysList) == 0 || hotkeysList[0] == \"\" {\n\t\t\tutils.PrintlnAndExit(\n\t\t\t\tLoadHotkeysError(\n\t\t\t\t\tfield.Name,\n\t\t\t\t\t\"Hotkey list is empty; at least one key binding is required.\",\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t}\n}\n\n// LoadThemeFile : Load configurations from theme file into &theme\n// set default values if we cant read user's theme file\nfunc LoadThemeFile() {\n\tthemeFile := filepath.Join(variable.ThemeFolder, Config.Theme+\".toml\")\n\tif err := LoadUserTheme(themeFile, &Theme); err != nil {\n\t\tslog.Error(\"Could not read user's theme file. Falling back to default theme\", \"error\", err)\n\t\terr = toml.Unmarshal([]byte(DefaultThemeString), &Theme)\n\t\tif err != nil {\n\t\t\tutils.PrintfAndExitf(\"Unexpected error while reading default theme file : %v. Exiting...\", err)\n\t\t}\n\t}\n\n\t// Validations\n\tif len(Theme.GradientColor) != RequiredGradientColorCount {\n\t\tutils.PrintlnAndExit(\n\t\t\tLoadThemeError(\n\t\t\t\t\"gradient_color\",\n\t\t\t\t\"Gradient color must contain exactly two values.\",\n\t\t\t),\n\t\t)\n\t}\n}\n\nfunc LoadUserTheme(themeFile string, obj *ThemeType) error {\n\tdata, err := os.ReadFile(themeFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not read user's theme file(%s), err : %w\", themeFile, err)\n\t}\n\tif err = toml.Unmarshal(data, obj); err != nil {\n\t\treturn fmt.Errorf(\"could not unmarshal user's theme file(%s) : %w\", themeFile, err)\n\t}\n\treturn nil\n}\n\n// LoadAllDefaultConfig : Load all default configurations from embedded superfile_config folder into global\n// configurations variables and write theme files if its needed.\nfunc LoadAllDefaultConfig(content embed.FS) {\n\terr := LoadConfigStringGlobals(content)\n\tif err != nil {\n\t\tslog.Error(\"Could not load default config from embed FS\", \"error\", err)\n\t\treturn\n\t}\n\n\tcurrentThemeVersion, err := os.ReadFile(variable.ThemeFileVersion)\n\tif err != nil && !os.IsNotExist(err) {\n\t\tslog.Error(\"Unexpected error reading from file:\", \"error\", err)\n\t\treturn\n\t}\n\n\tif string(currentThemeVersion) == variable.CurrentVersion {\n\t\t// We don't need to update themes as its already up to date\n\t\treturn\n\t}\n\n\t// Write theme files to theme directory\n\terr = WriteThemeFiles(content)\n\tif err != nil {\n\t\tslog.Error(\"Error while writing default theme directories\", \"error\", err)\n\t\treturn\n\t}\n\n\t// Prevent failure for first time app run by making sure parent directories exists\n\tif err = os.MkdirAll(filepath.Dir(variable.ThemeFileVersion), utils.ConfigDirPerm); err != nil {\n\t\tslog.Error(\"Error creating theme file parent directory\", \"error\", err)\n\t\treturn\n\t}\n\n\terr = os.WriteFile(variable.ThemeFileVersion, []byte(variable.CurrentVersion), utils.ConfigFilePerm)\n\tif err != nil {\n\t\tslog.Error(\"Error writing theme file version\", \"error\", err)\n\t}\n}\n\nfunc LoadConfigStringGlobals(content embed.FS) error {\n\thotkeyData, err := content.ReadFile(variable.EmbedHotkeysFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tHotkeysTomlString = string(hotkeyData)\n\n\tconfigData, err := content.ReadFile(variable.EmbedConfigFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tConfigTomlString = string(configData)\n\n\tthemeData, err := content.ReadFile(variable.EmbedThemeCatppuccinFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tDefaultThemeString = string(themeData)\n\treturn nil\n}\n\nfunc WriteThemeFiles(content embed.FS) error {\n\t_, err := os.Stat(variable.ThemeFolder)\n\n\tif os.IsNotExist(err) {\n\t\tif err = os.MkdirAll(variable.ThemeFolder, utils.ConfigDirPerm); err != nil {\n\t\t\tslog.Error(\"Error creating theme directory\", \"error\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfiles, err := content.ReadDir(variable.EmbedThemeDir)\n\tif err != nil {\n\t\tslog.Error(\"Error reading theme directory from embed\", \"error\", err)\n\t\treturn err\n\t}\n\n\tfor _, file := range files {\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\t// This will not break in windows. This is a relative path for Embed FS. It uses \"/\" only\n\t\tsrc, err := content.ReadFile(variable.EmbedThemeDir + \"/\" + file.Name())\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error reading theme file from embed\", \"error\", err)\n\t\t\treturn err\n\t\t}\n\n\t\tcurThemeFile, err := os.Create(filepath.Join(variable.ThemeFolder, file.Name()))\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error creating theme file from embed\", \"error\", err)\n\t\t\treturn err\n\t\t}\n\t\tdefer curThemeFile.Close()\n\t\t_, err = curThemeFile.Write(src)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error writing theme file from embed\", \"error\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Used only in unit tests\n// Populate config variables based on given file\nfunc PopulateGlobalConfigs() error {\n\t_, filename, _, ok := runtime.Caller(0)\n\tif !ok {\n\t\treturn errors.New(\"failed to determine source file location\")\n\t}\n\n\t// This is src/internal/common/load_config.go\n\t// we want src/superfile_config\n\tspfConfigDir := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(filename))),\n\t\t\"superfile_config\")\n\n\tconfigFilePath := filepath.Join(spfConfigDir, \"config.toml\")\n\thotkeyFilePath := filepath.Join(spfConfigDir, \"hotkeys.toml\")\n\tthemeFilePath := filepath.Join(spfConfigDir, \"theme\", \"monokai.toml\")\n\n\terr := PopulateConfigFromFile(configFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = PopulateHotkeyFromFile(hotkeyFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = PopulateThemeFromFile(themeFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Populate fixed variables\n\tLoadInitialPrerenderedVariables()\n\ticon.InitIcon(Config.Nerdfont, Theme.DirectoryIconColor)\n\tLoadPrerenderedVariables()\n\treturn nil\n}\n\n// No validation required\nfunc populateFromFile(filePath string, target interface{}) error {\n\tdata, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = toml.Unmarshal(data, target)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc PopulateConfigFromFile(configFilePath string) error {\n\treturn populateFromFile(configFilePath, &Config)\n}\n\nfunc PopulateHotkeyFromFile(hotkeyFilePath string) error {\n\treturn populateFromFile(hotkeyFilePath, &Hotkeys)\n}\n\nfunc PopulateThemeFromFile(themeFilePath string) error {\n\treturn populateFromFile(themeFilePath, &Theme)\n}\n\nfunc InitTrash() bool {\n\t// Create trash directories\n\tif runtime.GOOS != utils.OsLinux {\n\t\treturn true\n\t}\n\terr := utils.CreateDirectories(\n\t\tvariable.LinuxTrashDirectory,\n\t\tvariable.LinuxTrashDirectoryFiles,\n\t\tvariable.LinuxTrashDirectoryInfo,\n\t)\n\tif err != nil {\n\t\tslog.Warn(\"Failed to initialize XDG trash; falling back to permanent delete\",\n\t\t\t\"error\", err, \"trashDir\", variable.LinuxTrashDirectory)\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "src/internal/common/predefined_variable.go",
    "content": "package common\n\nimport (\n\t\"time\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n)\n\nconst (\n\tWheelRunTime          = 5\n\tDefaultCommandTimeout = 5000 * time.Millisecond\n\tDateModifiedOption    = \"Date Modified\"\n\tInvalidTypeString     = \"InvalidType\"\n)\n\nconst (\n\tSameRenameWarnTitle   = \"There is already a file or directory with that name\"\n\tSameRenameWarnContent = \"This operation will override the existing file\"\n)\n\nconst (\n\tTrashWarnTitle             = \"Are you sure you want to move this to trash can\"\n\tTrashWarnContent           = \"This operation will move file or directory to trash can.\"\n\tPermanentDeleteWarnTitle   = \"Are you sure you want to completely delete\"\n\tPermanentDeleteWarnContent = \"This operation cannot be undone and your data will be completely lost.\"\n)\n\nconst (\n\tMinimumHeight = 24\n\tMinimumWidth  = 60\n\n\t// TODO : These are model object properties, not global properties\n\t// We are modifying them in the code many time. They need to be part of model struct.\n\tMinFooterHeight = 6\n\tModalWidth      = 60\n\tModalHeight     = 7\n)\n\nvar (\n\tSideBarSuperfileTitle string\n\tSideBarHomeDivider    string\n\tSideBarPinnedDivider  string\n\tSideBarDisksDivider   string\n\tSideBarNoneText       string\n\n\tProcessBarNoneText string\n\tClipboardNoneText  string\n\n\tFilePanelTopDirectoryIcon string\n\tFilePanelNoneText         string\n\n\tFilePreviewNoFileInfoText               string\n\tFilePreviewNoContentText                string\n\tFilePreviewUnsupportedFormatText        string\n\tFilePreviewUnsupportedFileMode          string\n\tFilePreviewDirectoryUnreadableText      string\n\tFilePreviewEmptyText                    string\n\tFilePreviewError                        string\n\tFilePreviewPanelClosedText              string\n\tFilePreviewImagePreviewDisabledText     string\n\tFilePreviewUnsupportedImageFormatsText  string\n\tFilePreviewImageConversionErrorText     string\n\tFilePreviewBatNotInstalledText          string\n\tFilePreviewThumbnailGenerationErrorText string\n\n\tCheckboxChecked        string\n\tCheckboxCheckedFocused string\n\tCheckboxEmpty          string\n\tCheckboxEmptyFocused   string\n\n\tModalConfirmInputText string\n\tModalCancelInputText  string\n\tModalOkayInputText    string\n\tModalInputSpacingText string\n\tLipglossError         string\n)\n\nvar (\n\tUnsupportedPreviewFormats = []string{\".torrent\"}\n\tImageExtensions           = map[string]bool{\n\t\t\".jpg\":  true,\n\t\t\".jpeg\": true,\n\t\t\".png\":  true,\n\t\t\".gif\":  true,\n\t\t\".bmp\":  true,\n\t\t\".tiff\": true,\n\t\t\".svg\":  true,\n\t\t\".webp\": true,\n\t\t\".ico\":  true,\n\t}\n\tVideoExtensions = map[string]bool{\n\t\t\".mkv\":  true,\n\t\t\".mp4\":  true,\n\t\t\".mov\":  true,\n\t\t\".avi\":  true,\n\t\t\".flv\":  true,\n\t\t\".webm\": true,\n\t\t\".wmv\":  true,\n\t\t\".m4v\":  true,\n\t\t\".mpeg\": true,\n\t\t\".3gp\":  true,\n\t\t\".ogv\":  true,\n\t}\n)\n\n// No dependencies\nfunc LoadInitialPrerenderedVariables() {\n\tLipglossError = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#F93939\")).Render(\"Error\") +\n\t\tlipgloss.NewStyle().Foreground(lipgloss.Color(\"#00FFEE\")).Render(\" ┃ \")\n}\n\n// This should be used only after InitIcon() has been called.\nfunc wrapFilePreviewErrorMsg(msg string) string {\n\treturn \"\\n--- \" + icon.Error + icon.Space + msg + \" ---\"\n}\n\n// Dependecies - TODO We should programmatically guarantee these dependencies. And log error\n// if its not satisfied.\n// LoadThemeConfig() in style.go should be finished\n// loadConfigFile() in config_types.go should be finished\n// InitIcon() in config package in function.go should be finished\nfunc LoadPrerenderedVariables() {\n\tSideBarSuperfileTitle = SidebarTitleStyle.Render(\" \" + icon.SuperfileIcon + icon.Space + \"superfile\")\n\tSideBarHomeDivider = SidebarTitleStyle.Render(icon.Home+icon.Space+\"Home\") +\n\t\tSidebarDividerStyle.Render(\" ─────────────\")\n\n\tSideBarPinnedDivider = SidebarTitleStyle.Render(icon.Pinned+icon.Space+\"Pinned\") +\n\t\tSidebarDividerStyle.Render(\" ───────────\")\n\n\tSideBarDisksDivider = SidebarTitleStyle.Render(icon.Disk+icon.Space+\"Disks\") +\n\t\tSidebarDividerStyle.Render(\" ────────────\")\n\n\tSideBarNoneText = SidebarStyle.Render(\" \" + icon.Error + icon.Space + \"None\")\n\n\tProcessBarNoneText = icon.Error + icon.Space + \"No processes running\"\n\tClipboardNoneText = \" \" + icon.Error + icon.Space + \" No content in clipboard\"\n\n\tFilePanelTopDirectoryIcon = FilePanelTopDirectoryIconStyle.Render(\" \" + icon.Directory + icon.Space)\n\tFilePanelNoneText = FilePanelStyle.Render(\" \" + icon.Error + icon.Space + \"No such file or directory\")\n\n\tFilePreviewNoContentText = wrapFilePreviewErrorMsg(\n\t\t\"No content to preview\")\n\tFilePreviewNoFileInfoText = wrapFilePreviewErrorMsg(\n\t\t\"Could not get file info\")\n\tFilePreviewUnsupportedFormatText = wrapFilePreviewErrorMsg(\n\t\t\"Unsupported formats\")\n\tFilePreviewUnsupportedFileMode = wrapFilePreviewErrorMsg(\n\t\t\"Unsupported File Mode\")\n\tFilePreviewDirectoryUnreadableText = wrapFilePreviewErrorMsg(\n\t\t\"Cannot read directory\")\n\tFilePreviewError = wrapFilePreviewErrorMsg(\n\t\t\"Error\")\n\tFilePreviewEmptyText = wrapFilePreviewErrorMsg(\n\t\t\"Empty\")\n\tFilePreviewPanelClosedText = wrapFilePreviewErrorMsg(\n\t\t\"Preview panel is closed\")\n\tFilePreviewImagePreviewDisabledText = wrapFilePreviewErrorMsg(\n\t\t\"Image preview is disabled\")\n\tFilePreviewUnsupportedImageFormatsText = wrapFilePreviewErrorMsg(\n\t\t\"Unsupported image formats\")\n\tFilePreviewImageConversionErrorText = wrapFilePreviewErrorMsg(\n\t\t\"Error convert image to ansi\")\n\tFilePreviewBatNotInstalledText = wrapFilePreviewErrorMsg(\n\t\t\"'bat' is not installed or not found\")\n\tFilePreviewThumbnailGenerationErrorText = wrapFilePreviewErrorMsg(\n\t\t\"Thumbnail generation failed\")\n\n\tCheckboxChecked = FilePanelSelectBoxStyle.\n\t\tForeground(FilePanelBorderColor).\n\t\tRender(icon.CheckboxChecked + icon.Space)\n\tCheckboxCheckedFocused = FilePanelSelectBoxStyle.\n\t\tForeground(FilePanelBorderActiveColor).\n\t\tRender(icon.CheckboxChecked + icon.Space)\n\tCheckboxEmpty = FilePanelSelectBoxStyle.\n\t\tForeground(FilePanelBorderColor).\n\t\tRender(icon.CheckboxEmpty + icon.Space)\n\tCheckboxEmptyFocused = FilePanelSelectBoxStyle.\n\t\tForeground(FilePanelBorderActiveColor).\n\t\tRender(icon.CheckboxEmpty + icon.Space)\n\n\tModalOkayInputText = MainStyle.AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center).Render(\n\t\tModalConfirm.Render(\" (\" + Hotkeys.ConfirmTyping[0] + \") Okay \"))\n\tModalConfirmInputText = ModalConfirm.Render(\" (\" + Hotkeys.ConfirmTyping[0] + \") Confirm \")\n\tModalCancelInputText = ModalCancel.Render(\" (\" + Hotkeys.Quit[0] + \") Cancel \")\n\tModalInputSpacingText = lipgloss.NewStyle().Background(ModalBGColor).Render(\"           \")\n}\n"
  },
  {
    "path": "src/internal/common/string_function.go",
    "content": "package common\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"math\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\n// Size calculation constants\nconst (\n\tKilobyteSize      = 1000 // SI decimal unit\n\tKibibyteSize      = 1024 // Binary unit\n\tTabWidth          = 4    // Standard tab expansion width\n\tDefaultBufferSize = 1024 // Default buffer size for string operations\n\tNonBreakingSpace  = 0xa0 // Unicode non-breaking space\n\tEscapeChar        = 0x1b // ANSI escape character\n\tASCIIMax          = 0x7f // Maximum ASCII character value\n)\n\n// TODO: This has a bug. Remove its usage. Remove all custom truncation\n// And audit and evaluate any problem\n// The logic truncates to maxChars - len(tails) first, then checks if truncation occurred. This means:\n// - \"Hello\" with maxChars=5 gets truncated to 2 chars (5-3=2), producing \"He...\"\n// - \"Hello\" with maxChars=6 gets truncated to 3 chars (6-3=3), producing \"Hel...\"\n// Both cases are wrong - \"Hello\" fits within 5 and 6 characters, so it shouldn't be truncated at all.\nfunc TruncateText(text string, maxChars int, tails string) string {\n\ttruncatedText := ansi.Truncate(text, maxChars-len(tails), \"\")\n\tif text != truncatedText {\n\t\treturn truncatedText + tails\n\t}\n\n\treturn text\n}\n\nfunc TruncateTextBeginning(text string, maxChars int, tails string) string {\n\tif ansi.StringWidth(text) <= maxChars {\n\t\treturn text\n\t}\n\n\ttruncatedRunes := []rune(text)\n\n\ttruncatedWidth := ansi.StringWidth(string(truncatedRunes))\n\n\tfor truncatedWidth > maxChars {\n\t\ttruncatedRunes = truncatedRunes[1:]\n\t\ttruncatedWidth = ansi.StringWidth(string(truncatedRunes))\n\t}\n\n\tif len(truncatedRunes) > len(tails) {\n\t\ttruncatedRunes = append([]rune(tails), truncatedRunes[len(tails):]...)\n\t}\n\n\treturn string(truncatedRunes)\n}\n\nfunc TruncateMiddleText(text string, maxChars int, tails string) string {\n\tif utf8.RuneCountInString(text) <= maxChars {\n\t\treturn text\n\t}\n\n\t//nolint:mnd // standard halving for center truncation\n\thalfEllipsisLength := (maxChars - 3) / 2\n\t// TODO : Use ansi.Substring to correctly handle ANSI escape codes\n\ttruncatedText := text[:halfEllipsisLength] + tails + text[utf8.RuneCountInString(text)-halfEllipsisLength:]\n\n\treturn truncatedText\n}\n\nfunc FilePanelItemRenderWithIcon(\n\tname string,\n\twidth int,\n\tisDir bool,\n\tisLink bool,\n\tisSelected bool,\n\tbgColor lipgloss.Color,\n) string {\n\tstyle := GetElementIcon(name, isDir, isLink, Config.Nerdfont)\n\ticonData := style.Icon + \" \"\n\tfilenameWidth := width - ansi.StringWidth(iconData)\n\tif filenameWidth <= 0 {\n\t\t// This should never happen, unless there is extremely low size or programming bug\n\t\tslog.Debug(\"Too low width for rendering file name\", \"width\", width, \"filenameWidth\", filenameWidth)\n\t\treturn \"\"\n\t}\n\treturn StringColorRender(lipgloss.Color(style.Color), bgColor).\n\t\tBackground(bgColor).Render(iconData) +\n\t\tFilePanelItemRender(name, filenameWidth, isSelected, bgColor, lipgloss.Left)\n}\nfunc FilePanelItemRender(data string,\n\twidth int,\n\tisSelected bool,\n\tbgColor lipgloss.Color,\n\talignment lipgloss.Position,\n) string {\n\toutputData := ansi.Truncate(data, width, \"...\")\n\tstyle := FilePanelStyle\n\tif isSelected {\n\t\tstyle = FilePanelItemSelectedStyle\n\t}\n\treturn style.Background(bgColor).Width(width).Align(alignment).Render(outputData)\n}\n\nfunc ClipboardPrettierName(name string, width int, isDir bool, isLink bool, isSelected bool) string {\n\tstyle := GetElementIcon(filepath.Base(name), isDir, isLink, Config.Nerdfont)\n\tif isSelected {\n\t\treturn StringColorRender(lipgloss.Color(style.Color), FooterBGColor).\n\t\t\tBackground(FooterBGColor).\n\t\t\tRender(style.Icon+\" \") +\n\t\t\tFilePanelItemSelectedStyle.Render(TruncateTextBeginning(name, width, \"...\"))\n\t}\n\treturn StringColorRender(lipgloss.Color(style.Color), FooterBGColor).\n\t\tBackground(FooterBGColor).\n\t\tRender(style.Icon+\" \") +\n\t\tFilePanelStyle.Render(TruncateTextBeginning(name, width, \"...\"))\n}\n\nfunc FileNameWithoutExtension(fileName string) string {\n\tfor {\n\t\tpos := strings.LastIndexByte(fileName, '.')\n\t\tif pos <= 0 {\n\t\t\tbreak\n\t\t}\n\t\tfileName = fileName[:pos]\n\t}\n\treturn fileName\n}\n\nfunc unitsDec() [7]string {\n\treturn [...]string{\"B\", \"kB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\"}\n}\n\nfunc unitsBin() [7]string {\n\treturn [...]string{\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\"}\n}\n\nfunc formatSizeInternal(size int64, power int, unitlist [7]string) string {\n\tif size == 0 {\n\t\treturn \"0 B\"\n\t}\n\tunitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(float64(power))))\n\tif unitIndex == 0 {\n\t\treturn fmt.Sprintf(\"%d %s\", size, unitlist[unitIndex])\n\t}\n\tadjustedSize := float64(size) / math.Pow(float64(power), float64(unitIndex))\n\treturn fmt.Sprintf(\"%.2f %s\", adjustedSize, unitlist[unitIndex])\n}\n\nfunc FormatFileSize(size int64) string {\n\tunits := unitsBin()\n\tif Config.FileSizeUseSI {\n\t\tunits = unitsDec()\n\t}\n\treturn formatSizeInternal(size, KibibyteSize, units)\n}\n\nfunc GetHelpMenuHotkeyString(hotkeys []string) string {\n\tvar hotkey strings.Builder\n\tfor i, key := range hotkeys {\n\t\tif key == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif i != 0 {\n\t\t\thotkey.WriteString(\" | \")\n\t\t}\n\t\tif key == \" \" {\n\t\t\tkey = \"space\"\n\t\t}\n\t\thotkey.WriteString(key)\n\t}\n\treturn hotkey.String()\n}\n\n// Separated this out out for easy testing\nfunc IsBufferPrintable(buffer []byte) bool {\n\tfor _, b := range buffer {\n\t\t// This will also handle b==0\n\t\tif !unicode.IsPrint(rune(b)) && !unicode.IsSpace(rune(b)) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// IsExtensionExtractable checks if a string is a valid compressed archive file extension.\nfunc IsExtensionExtractable(ext string) bool {\n\t// Extensions based on the types that package: `xtractr` `ExtractFile` function handles.\n\tvalidExtensions := map[string]struct{}{\n\t\t\".zip\":     {},\n\t\t\".bz\":      {},\n\t\t\".gz\":      {},\n\t\t\".iso\":     {},\n\t\t\".rar\":     {},\n\t\t\".7z\":      {},\n\t\t\".tar\":     {},\n\t\t\".tar.gz\":  {},\n\t\t\".tar.bz2\": {},\n\t}\n\t_, exists := validExtensions[strings.ToLower(ext)]\n\treturn exists\n}\n\n// Check file is text file or not\nfunc IsTextFile(filename string) (bool, error) {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer file.Close()\n\n\treader := bufio.NewReader(file)\n\tbuffer := make([]byte, DefaultBufferSize)\n\tcnt, err := reader.Read(buffer)\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\treturn false, err\n\t}\n\treturn IsBufferPrintable(buffer[:cnt]), nil\n}\n\n// Although some characters like `\\x0b`(vertical tab) are printable,\n// previewing them breaks the layout.\n// So, among the \"non-graphic\" printable characters, we only need \\n and \\t\n// Space and NBSP are already considered graphic by unicode.\n// Allow Any rune that is above ASCII control characters range 0x7f\n// for valid unicodes like nerdfont \\uf410 \\U000f0868\n// Also allow \\x0b that is for escape sequences\n// This function should better not be broken into multiple functions\nfunc MakePrintableWithEscCheck(line string, allowEsc bool) string { //nolint: gocognit // See above\n\tvar sb strings.Builder\n\tfor _, r := range line {\n\t\tif r == utf8.RuneError {\n\t\t\tcontinue\n\t\t}\n\t\t// It needs to be handled separately since considered a space,\n\t\t// It is multi-byte in UTF-8, But it has zero display width\n\t\tif r == NonBreakingSpace {\n\t\t\tsb.WriteRune(r)\n\t\t\tcontinue\n\t\t}\n\t\t// It needs to be handled separately since considered a space,\n\t\t// Since we are using ansi.StringWidth() for truncation, and \\t is\n\t\t// considered zero width\n\t\tif r == '\\t' {\n\t\t\tsb.WriteString(\"    \")\n\t\t\tcontinue\n\t\t}\n\t\tif r == EscapeChar {\n\t\t\tif allowEsc {\n\t\t\t\tsb.WriteRune(r)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif r > ASCIIMax {\n\t\t\tif unicode.IsSpace(r) && utf8.RuneLen(r) > 1 {\n\t\t\t\t// See https://github.com/charmbracelet/x/issues/466\n\t\t\t\t// Space chacters spanning more than one bytes are not handled well by\n\t\t\t\t// ansi.Wrap(), and so lipgloss.Render() has issues\n\t\t\t\tr = ' '\n\t\t\t}\n\t\t\tsb.WriteRune(r)\n\t\t\tcontinue\n\t\t}\n\t\tif unicode.IsGraphic(r) || r == rune('\\n') {\n\t\t\tsb.WriteRune(r)\n\t\t}\n\t}\n\treturn sb.String()\n}\n\nfunc MakePrintable(line string) string {\n\t// We assume default behaviour of allowing ESC is not  a problem\n\t// If you disallow ESC, then you would see ansi codes afer \\x1b and it will look ugly\n\t// But thats only for files with that kind of data, and its rare.\n\t// And yazi does it too.\n\t// We will keep it false only if it can cause a rendering problem\n\treturn MakePrintableWithEscCheck(line, true)\n}\n"
  },
  {
    "path": "src/internal/common/string_function_test.go",
    "content": "package common\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestStringTruncate(t *testing.T) {\n\tvar inputs = []struct {\n\t\tfunction func(string, int, string) string\n\t\tfuncName string\n\t\tinput    string\n\t\tmaxSize  int\n\t\ttalis    string\n\t\texpected string\n\t}{\n\t\t{TruncateText, \"TruncateText\", \"Hello world\", 4, \"...\", \"H...\"},\n\t\t{TruncateText, \"TruncateText\", \"Hello world\", 6, \"...\", \"Hel...\"},\n\t\t{TruncateText, \"TruncateText\", \"Hello\", 100, \"...\", \"Hello\"},\n\t\t{TruncateTextBeginning, \"TruncateTextBeginning\", \"Hello world\", 4, \"...\", \"...d\"},\n\t\t{TruncateTextBeginning, \"TruncateTextBeginning\", \"Hello world\", 6, \"...\", \"...rld\"},\n\t\t{TruncateTextBeginning, \"TruncateTextBeginning\", \"Hello\", 100, \"...\", \"Hello\"},\n\t\t{TruncateMiddleText, \"TruncateMiddleText\", \"Hello world\", 5, \"...\", \"H...d\"},\n\t\t{TruncateMiddleText, \"TruncateMiddleText\", \"Hello world\", 7, \"...\", \"He...ld\"},\n\t\t{TruncateMiddleText, \"TruncateMiddleText\", \"Hello\", 100, \"...\", \"Hello\"},\n\t}\n\n\tfor _, tt := range inputs {\n\t\tt.Run(fmt.Sprintf(\"Run %s on string %s to %d chars\", tt.funcName, tt.input, tt.maxSize), func(t *testing.T) {\n\t\t\tresult := tt.function(tt.input, tt.maxSize, tt.talis)\n\t\t\texpected := tt.expected\n\t\t\tif result != expected {\n\t\t\t\tt.Errorf(\"got \\\"%s\\\", expected \\\"%s\\\"\", result, expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilenameWithouText(t *testing.T) {\n\tvar inputs = []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"hello\", \"hello\"},\n\t\t{\"hello.zip\", \"hello\"},\n\t\t{\"hello.tar.gz\", \"hello\"},\n\t\t{\".gitignore\", \".gitignore\"},\n\t\t{\"\", \"\"},\n\t}\n\n\tfor _, tt := range inputs {\n\t\tt.Run(fmt.Sprintf(\"Remove extension from %s\", tt.input), func(t *testing.T) {\n\t\t\tresult := FileNameWithoutExtension(tt.input)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %s, got %s\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHelpHotkeyString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Single key\",\n\t\t\tinput:    []string{\"a\"},\n\t\t\texpected: \"a\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Multiple keys\",\n\t\t\tinput:    []string{\"a\", \"b\", \"c\"},\n\t\t\texpected: \"a | b | c\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Empty key\",\n\t\t\tinput:    []string{\"a\", \"\", \"b\"},\n\t\t\texpected: \"a | b\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Trailing empty\",\n\t\t\tinput:    []string{\"a\", \"\"},\n\t\t\texpected: \"a\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Trailing empty with multiple keys\",\n\t\t\tinput:    []string{\"a\", \"b\", \"\"},\n\t\t\texpected: \"a | b\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Space key\",\n\t\t\tinput:    []string{\" \"},\n\t\t\texpected: \"space\",\n\t\t},\n\n\t\t// Starting with an empty key (\"\", \"a\") is not allowed by the file parser,\n\t\t// so a test is not needed\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := GetHelpMenuHotkeyString(tt.input)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestIsBufferPrintable(t *testing.T) {\n\tvar inputs = []struct {\n\t\tinput    string\n\t\texpected bool\n\t}{\n\t\t{\"\", true},\n\t\t{\"hello\", true},\n\t\t{\"abcdABCD0123~!@#$%^&*()_+-={}|:\\\"<>?,./;'[]\", true},\n\t\t{\"Horizontal Tab and NewLine\\t\\t\\n\\n\", true},\n\t\t{\"\\xa0(NBSP)\", true},\n\t\t{\"\\x0b(Vertical Tab)\", true},\n\t\t{\"\\x0d(CR)\", true},\n\t\t{\"ASCII control characters : \\x00(NULL)\", false},\n\t\t{\"\\x05(ENQ)\", false},\n\t\t{\"\\x0f(SI)\", false},\n\t\t{\"\\x1b(ESC)\", false},\n\t\t{\"\\x7f(DEL)\", false},\n\t}\n\tfor _, tt := range inputs {\n\t\tt.Run(fmt.Sprintf(\"Testing if buffer %q is printable\", tt.input), func(t *testing.T) {\n\t\t\tresult := IsBufferPrintable([]byte(tt.input))\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsExtensionExtractable(t *testing.T) {\n\tinputs := []struct {\n\t\text      string\n\t\texpected bool\n\t}{\n\t\t{\".zip\", true},\n\t\t{\".rar\", true},\n\t\t{\".7z\", true},\n\t\t{\".tar.gz\", true},\n\t\t{\".tar.bz2\", true},\n\t\t{\".exe\", false},\n\t\t{\".txt\", false},\n\t\t{\".tar\", true},\n\t\t{\"\", false},    // Empty string case\n\t\t{\".ZIP\", true}, // Case sensitivity check\n\t\t{\".Zip\", true}, // Case sensitivity check\n\t\t{\".bz\", true},\n\t\t{\".gz\", true},\n\t\t{\".iso\", true},\n\t}\n\n\tfor _, tt := range inputs {\n\t\tt.Run(tt.ext, func(t *testing.T) {\n\t\t\tresult := IsExtensionExtractable(tt.ext)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"IsExensionExtractable (%q) = %v; want %v\", tt.ext, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMakePrintable(t *testing.T) {\n\tvar inputs = []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"\", \"\"},\n\t\t{\"hello\", \"hello\"},\n\t\t{\"abcdABCD0123~!@#$%^&*()_+-={}|:\\\"<>?,./;'[]\", \"abcdABCD0123~!@#$%^&*()_+-={}|:\\\"<>?,./;'[]\"},\n\t\t{\"Horizontal Tab and NewLine\\t\\t\\n\\n\", \"Horizontal Tab and NewLine        \\n\\n\"},\n\t\t{\"(NBSP)\\u00a0\\u00a0\\u00a0\\u00a0;\", \"(NBSP)\\u00a0\\u00a0\\u00a0\\u00a0;\"},\n\t\t{\"\\x0b(Vertical Tab)\", \"(Vertical Tab)\"},\n\t\t{\"\\x0d(CR)\", \"(CR)\"},\n\t\t{\"ASCII control characters : \\x00(NULL)\", \"ASCII control characters : (NULL)\"},\n\t\t{\"\\x05(ENQ)\", \"(ENQ)\"},\n\t\t{\"\\x0f(SI)\", \"(SI)\"},\n\t\t{\"\\x1b(ESC)\", \"\\x1b(ESC)\"},\n\t\t{\"\\x7f(DEL)\", \"(DEL)\"},\n\t\t{\"\\x7f(DEL)\", \"(DEL)\"},\n\t\t{\"Valid unicodes like nerdfont \\uf410 \\U000f0868\", \"Valid unicodes like nerdfont \\uf410 \\U000f0868\"},\n\t\t{\"Invalid Unicodes\\ufffd\", \"Invalid Unicodes\"},\n\t\t{\"Invalid Unicodes\\xa0\", \"Invalid Unicodes\"},\n\t\t{\"Ascii color sequence\\x1b[38;2;230;219;116;48;2;39;40;34m\\ue68f \\x1b[0m\",\n\t\t\t\"Ascii color sequence\\x1b[38;2;230;219;116;48;2;39;40;34m\\ue68f \\x1b[0m\"},\n\t\t{\"Unicodes spaces\\u202f\\u205f\\u2029\", \"Unicodes spaces   \"},\n\t\t{\"IDEOGRAPHIC SPACE\\u3000\", \"IDEOGRAPHIC SPACE \"},\n\t}\n\tfor _, tt := range inputs {\n\t\tt.Run(fmt.Sprintf(\"Make %q printable\", tt.input), func(t *testing.T) {\n\t\t\tresult := MakePrintable(tt.input)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected '%v', got '%v' (input : '%v')\", tt.expected, result, tt.input)\n\t\t\t}\n\t\t})\n\t}\n\tt.Run(\"ESC is skipped\", func(t *testing.T) {\n\t\tassert.Equal(t, \"(ESC)\", MakePrintableWithEscCheck(\"\\x1b(ESC)\", false))\n\t})\n\tt.Run(\"ESC is not skipped\", func(t *testing.T) {\n\t\tassert.Equal(t, \"\\x1b(ESC)\", MakePrintableWithEscCheck(\"\\x1b(ESC)\", true))\n\t})\n}\n\nfunc TestFormatSizeInternal(t *testing.T) {\n\tt.Run(\"max int size\", func(t *testing.T) {\n\t\tactual := formatSizeInternal(math.MaxInt64, KilobyteSize, unitsDec())\n\t\tassert.Equal(t, \"9.22 EB\", actual)\n\t})\n\tt.Run(\"zero size\", func(t *testing.T) {\n\t\tactual := formatSizeInternal(0, KilobyteSize, unitsDec())\n\t\tassert.Equal(t, \"0 B\", actual)\n\t})\n\tt.Run(\"100 bytes size\", func(t *testing.T) {\n\t\tactual := formatSizeInternal(100, KilobyteSize, unitsDec())\n\t\tassert.Equal(t, \"100 B\", actual)\n\t})\n\tt.Run(\"1005 bytes size\", func(t *testing.T) {\n\t\tactual := formatSizeInternal(1005, KilobyteSize, unitsDec())\n\t\tassert.Equal(t, \"1.00 kB\", actual)\n\t})\n\tt.Run(\"1005 bytes size kibi\", func(t *testing.T) {\n\t\tactual := formatSizeInternal(1005, KibibyteSize, unitsBin())\n\t\tassert.Equal(t, \"1005 B\", actual)\n\t})\n\tt.Run(\"1025 bytes size kibi\", func(t *testing.T) {\n\t\tactual := formatSizeInternal(1025, KibibyteSize, unitsBin())\n\t\tassert.Equal(t, \"1.00 KiB\", actual)\n\t})\n\tt.Run(\"1035 bytes size kibi\", func(t *testing.T) {\n\t\tactual := formatSizeInternal(1035, KibibyteSize, unitsBin())\n\t\tassert.Equal(t, \"1.01 KiB\", actual)\n\t})\n}\n"
  },
  {
    "path": "src/internal/common/style.go",
    "content": "package common\n\nimport (\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nvar (\n\tBottomMiddleBorderSplit string\n)\nvar (\n\tTerminalTooSmall    lipgloss.Style\n\tTerminalCorrectSize lipgloss.Style\n)\n\nvar (\n\tMainStyle      lipgloss.Style\n\tFilePanelStyle lipgloss.Style\n\tSidebarStyle   lipgloss.Style\n\tFooterStyle    lipgloss.Style\n\tModalStyle     lipgloss.Style\n)\n\nvar (\n\tSidebarDividerStyle  lipgloss.Style\n\tSidebarTitleStyle    lipgloss.Style\n\tSidebarSelectedStyle lipgloss.Style\n)\n\nvar (\n\tFilePanelCursorStyle lipgloss.Style\n\tFooterCursorStyle    lipgloss.Style\n\tModalCursorStyle     lipgloss.Style\n)\n\nvar (\n\tFilePanelTopDirectoryIconStyle lipgloss.Style\n\tFilePanelTopPathStyle          lipgloss.Style\n\tFilePanelItemSelectedStyle     lipgloss.Style\n\tFilePanelSelectBoxStyle        lipgloss.Style\n)\n\nvar (\n\tProcessErrorStyle       lipgloss.Style\n\tProcessInOperationStyle lipgloss.Style\n\tProcessCancelStyle      lipgloss.Style\n\tProcessSuccessfulStyle  lipgloss.Style\n)\n\nvar (\n\tModalCancel     lipgloss.Style\n\tModalConfirm    lipgloss.Style\n\tModalTitleStyle lipgloss.Style\n\tModalErrorStyle lipgloss.Style\n)\n\nvar (\n\tHelpMenuHotkeyStyle lipgloss.Style\n\tHelpMenuTitleStyle  lipgloss.Style\n)\n\nvar (\n\tPromptSuccessStyle lipgloss.Style\n\tPromptFailureStyle lipgloss.Style\n)\nvar TransparentBackgroundColor string\n\nvar (\n\tFilePanelBorderColor lipgloss.Color\n\tSidebarBorderColor   lipgloss.Color\n\tFooterBorderColor    lipgloss.Color\n\n\tFilePanelBorderActiveColor lipgloss.Color\n\tSidebarBorderActiveColor   lipgloss.Color\n\tFooterBorderActiveColor    lipgloss.Color\n\tModalBorderActiveColor     lipgloss.Color\n\n\tFullScreenBGColor lipgloss.Color\n\tFilePanelBGColor  lipgloss.Color\n\tSidebarBGColor    lipgloss.Color\n\tFooterBGColor     lipgloss.Color\n\tModalBGColor      lipgloss.Color\n\n\tFullScreenFGColor lipgloss.Color\n\tFilePanelFGColor  lipgloss.Color\n\tSidebarFGColor    lipgloss.Color\n\tFooterFGColor     lipgloss.Color\n\tModalFGColor      lipgloss.Color\n\n\tcursorColor  lipgloss.Color\n\tcorrectColor lipgloss.Color\n\terrorColor   lipgloss.Color\n\thintColor    lipgloss.Color\n\tcancelColor  lipgloss.Color\n\n\tfilePanelTopDirectoryIconColor lipgloss.Color\n\tfilePanelTopPathColor          lipgloss.Color\n\tfilePanelItemSelectedFGColor   lipgloss.Color\n\tfilePanelItemSelectedBGColor   lipgloss.Color\n\n\tsidebarTitleColor          lipgloss.Color\n\tsidebarItemSelectedFGColor lipgloss.Color\n\tsidebarItemSelectedBGColor lipgloss.Color\n\tsidebarDividerColor        lipgloss.Color\n\n\tmodalCancelFGColor  lipgloss.Color\n\tmodalCancelBGColor  lipgloss.Color\n\tmodalConfirmFGColor lipgloss.Color\n\tmodalConfirmBGColor lipgloss.Color\n\n\thelpMenuHotkeyColor lipgloss.Color\n\thelpMenuTitleColor  lipgloss.Color\n\n\tpromptSuccessColor lipgloss.Color\n\tpromptFailureColor lipgloss.Color\n)\n\nfunc LoadThemeConfig() { //nolint: funlen // Variable initialization\n\tBottomMiddleBorderSplit = Config.BorderMiddleLeft + Config.BorderBottom + Config.BorderMiddleRight\n\n\tFilePanelBorderColor = lipgloss.Color(Theme.FilePanelBorder)\n\tSidebarBorderColor = lipgloss.Color(Theme.SidebarBorder)\n\tFooterBorderColor = lipgloss.Color(Theme.FooterBorder)\n\n\tFilePanelBorderActiveColor = lipgloss.Color(Theme.FilePanelBorderActive)\n\tSidebarBorderActiveColor = lipgloss.Color(Theme.SidebarBorderActive)\n\tFooterBorderActiveColor = lipgloss.Color(Theme.FooterBorderActive)\n\tModalBorderActiveColor = lipgloss.Color(Theme.ModalBorderActive)\n\n\tFullScreenBGColor = lipgloss.Color(Theme.FullScreenBG)\n\tFilePanelBGColor = lipgloss.Color(Theme.FilePanelBG)\n\tSidebarBGColor = lipgloss.Color(Theme.SidebarBG)\n\tFooterBGColor = lipgloss.Color(Theme.FooterBG)\n\tModalBGColor = lipgloss.Color(Theme.ModalBG)\n\n\tFullScreenFGColor = lipgloss.Color(Theme.FullScreenFG)\n\tFilePanelFGColor = lipgloss.Color(Theme.FilePanelFG)\n\tSidebarFGColor = lipgloss.Color(Theme.SidebarFG)\n\tFooterFGColor = lipgloss.Color(Theme.FooterFG)\n\tModalFGColor = lipgloss.Color(Theme.ModalFG)\n\n\tcursorColor = lipgloss.Color(Theme.Cursor)\n\tcorrectColor = lipgloss.Color(Theme.Correct)\n\terrorColor = lipgloss.Color(Theme.Error)\n\thintColor = lipgloss.Color(Theme.Hint)\n\tcancelColor = lipgloss.Color(Theme.Cancel)\n\n\tfilePanelTopDirectoryIconColor = lipgloss.Color(Theme.FilePanelTopDirectoryIcon)\n\tfilePanelTopPathColor = lipgloss.Color(Theme.FilePanelTopPath)\n\tfilePanelItemSelectedFGColor = lipgloss.Color(Theme.FilePanelItemSelectedFG)\n\tfilePanelItemSelectedBGColor = lipgloss.Color(Theme.FilePanelItemSelectedBG)\n\n\tsidebarTitleColor = lipgloss.Color(Theme.SidebarTitle)\n\tsidebarItemSelectedFGColor = lipgloss.Color(Theme.SidebarItemSelectedFG)\n\tsidebarItemSelectedBGColor = lipgloss.Color(Theme.SidebarItemSelectedBG)\n\tsidebarDividerColor = lipgloss.Color(Theme.SidebarDivider)\n\n\tmodalCancelFGColor = lipgloss.Color(Theme.ModalCancelFG)\n\tmodalCancelBGColor = lipgloss.Color(Theme.ModalCancelBG)\n\tmodalConfirmFGColor = lipgloss.Color(Theme.ModalConfirmFG)\n\tmodalConfirmBGColor = lipgloss.Color(Theme.ModalConfirmBG)\n\n\thelpMenuHotkeyColor = lipgloss.Color(Theme.HelpMenuHotkey)\n\thelpMenuTitleColor = lipgloss.Color(Theme.HelpMenuTitle)\n\n\tpromptSuccessColor = lipgloss.Color(Theme.Correct)\n\tpromptFailureColor = lipgloss.Color(Theme.Error)\n\n\tif Config.TransparentBackground {\n\t\tTransparentAllBackgroundColor()\n\t}\n\n\t// All Panel Main Color\n\t// (full screen and default color)\n\tMainStyle = lipgloss.NewStyle().Foreground(FullScreenFGColor).Background(FullScreenBGColor)\n\tFilePanelStyle = lipgloss.NewStyle().Foreground(FilePanelFGColor).Background(FilePanelBGColor)\n\tSidebarStyle = lipgloss.NewStyle().Foreground(SidebarFGColor).Background(SidebarBGColor)\n\tFooterStyle = lipgloss.NewStyle().Foreground(FooterFGColor).Background(FooterBGColor)\n\tModalStyle = lipgloss.NewStyle().Foreground(ModalFGColor).Background(ModalBGColor)\n\n\t// Terminal Size Error\n\tTerminalTooSmall = lipgloss.NewStyle().Foreground(errorColor).Background(FullScreenBGColor)\n\tTerminalCorrectSize = lipgloss.NewStyle().Foreground(cursorColor).Background(FullScreenBGColor)\n\n\t// Cursor\n\tFilePanelCursorStyle = lipgloss.NewStyle().Foreground(cursorColor).Background(FilePanelBGColor)\n\tFooterCursorStyle = lipgloss.NewStyle().Foreground(cursorColor).Background(FooterBGColor)\n\tModalCursorStyle = lipgloss.NewStyle().Foreground(cursorColor).Background(ModalBGColor)\n\n\t// File Panel Special Style\n\tFilePanelTopDirectoryIconStyle = lipgloss.NewStyle().Foreground(filePanelTopDirectoryIconColor).\n\t\tBackground(FilePanelBGColor)\n\tFilePanelTopPathStyle = lipgloss.NewStyle().Foreground(filePanelTopPathColor).Background(FilePanelBGColor)\n\tFilePanelItemSelectedStyle = lipgloss.NewStyle().Foreground(filePanelItemSelectedFGColor).\n\t\tBackground(filePanelItemSelectedBGColor)\n\tFilePanelSelectBoxStyle = lipgloss.NewStyle().Background(FilePanelBGColor)\n\n\t// Sidebar Special Style\n\tSidebarDividerStyle = lipgloss.NewStyle().Foreground(sidebarDividerColor).Background(SidebarBGColor)\n\tSidebarTitleStyle = lipgloss.NewStyle().Foreground(sidebarTitleColor).Background(SidebarBGColor)\n\tSidebarSelectedStyle = lipgloss.NewStyle().Foreground(sidebarItemSelectedFGColor).\n\t\tBackground(sidebarItemSelectedBGColor)\n\n\t// Footer Special Style\n\tProcessErrorStyle = lipgloss.NewStyle().Foreground(errorColor).Background(FooterBGColor)\n\tProcessInOperationStyle = lipgloss.NewStyle().Foreground(hintColor).Background(FooterBGColor)\n\tProcessCancelStyle = lipgloss.NewStyle().Foreground(cancelColor).Background(FooterBGColor)\n\tProcessSuccessfulStyle = lipgloss.NewStyle().Foreground(correctColor).Background(FooterBGColor)\n\n\t// Modal Special Style\n\tModalCancel = lipgloss.NewStyle().Foreground(modalCancelFGColor).Background(modalCancelBGColor)\n\tModalConfirm = lipgloss.NewStyle().Foreground(modalConfirmFGColor).Background(modalConfirmBGColor)\n\tModalTitleStyle = lipgloss.NewStyle().Foreground(hintColor).Background(ModalBGColor)\n\tModalErrorStyle = lipgloss.NewStyle().Foreground(errorColor).Background(ModalBGColor)\n\t// Help Menu Style\n\tHelpMenuHotkeyStyle = lipgloss.NewStyle().Foreground(helpMenuHotkeyColor).Background(ModalBGColor)\n\tHelpMenuTitleStyle = lipgloss.NewStyle().Foreground(helpMenuTitleColor).Background(ModalBGColor)\n\n\t// Prompt Style\n\tPromptSuccessStyle = lipgloss.NewStyle().Foreground(promptSuccessColor).Background(ModalBGColor)\n\tPromptFailureStyle = lipgloss.NewStyle().Foreground(promptFailureColor).Background(ModalBGColor)\n}\n\nfunc TransparentAllBackgroundColor() {\n\tif SidebarBGColor == sidebarItemSelectedBGColor {\n\t\tsidebarItemSelectedBGColor = lipgloss.Color(TransparentBackgroundColor)\n\t}\n\n\tif FilePanelBGColor == filePanelItemSelectedBGColor {\n\t\tfilePanelItemSelectedBGColor = lipgloss.Color(TransparentBackgroundColor)\n\t}\n\n\tFullScreenBGColor = lipgloss.Color(TransparentBackgroundColor)\n\tFilePanelBGColor = lipgloss.Color(TransparentBackgroundColor)\n\tSidebarBGColor = lipgloss.Color(TransparentBackgroundColor)\n\tFooterBGColor = lipgloss.Color(TransparentBackgroundColor)\n\tModalBGColor = lipgloss.Color(TransparentBackgroundColor)\n}\n"
  },
  {
    "path": "src/internal/common/style_function.go",
    "content": "package common\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/progress\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n)\n\nfunc ModalBorderStyle(height int, width int) lipgloss.Style {\n\treturn modalBorderStyleWithAlign(height, width, lipgloss.Center)\n}\n\n// Generate modal (pop up widnwos) border style\nfunc modalBorderStyleWithAlign(height int, width int, horizontalAlignment lipgloss.Position) lipgloss.Style {\n\tborder := GenerateBorder()\n\treturn lipgloss.NewStyle().Height(height).\n\t\tWidth(width).\n\t\tAlign(horizontalAlignment, lipgloss.Center).\n\t\tBorder(border).\n\t\tBorderForeground(ModalBorderActiveColor).\n\t\tBorderBackground(ModalBGColor).\n\t\tBackground(ModalBGColor).\n\t\tForeground(ModalFGColor)\n}\n\n// Generate first use modal style (This modal pop up when user first use superfile)\nfunc FirstUseModal(height int, width int) lipgloss.Style {\n\tborder := GenerateBorder()\n\treturn lipgloss.NewStyle().Height(height).\n\t\tWidth(width).\n\t\tAlign(lipgloss.Left, lipgloss.Center).\n\t\tBorder(border).\n\t\tBorderForeground(ModalBorderActiveColor).\n\t\tBorderBackground(ModalBGColor).\n\t\tBackground(ModalBGColor).\n\t\tForeground(ModalFGColor)\n}\n\n// Generate sort options modal border style\nfunc SortOptionsModalBorderStyle(height int, width int, borderBottom string) lipgloss.Style {\n\tborder := GenerateBorder()\n\tborder.Bottom = borderBottom\n\n\treturn lipgloss.NewStyle().\n\t\tBorder(border).\n\t\tBorderForeground(ModalBorderActiveColor).\n\t\tBorderBackground(ModalBGColor).\n\t\tWidth(width).\n\t\tHeight(height).\n\t\tBackground(ModalBGColor).\n\t\tForeground(ModalFGColor)\n}\n\n// Generate full screen style for terminal size too small etc\nfunc FullScreenStyle(height int, width int) lipgloss.Style {\n\treturn lipgloss.NewStyle().\n\t\tHeight(height).\n\t\tWidth(width).\n\t\tAlign(lipgloss.Center, lipgloss.Center).\n\t\tBackground(FullScreenBGColor).\n\t\tForeground(FullScreenFGColor)\n}\n\n// Return only fg and bg color style\nfunc StringColorRender(fgColor lipgloss.Color, bgColor lipgloss.Color) lipgloss.Style {\n\treturn lipgloss.NewStyle().\n\t\tForeground(fgColor).\n\t\tBackground(bgColor)\n}\n\n// Generate border style\nfunc GenerateBorder() lipgloss.Border {\n\treturn lipgloss.Border{\n\t\tTop:         Config.BorderTop,\n\t\tBottom:      Config.BorderBottom,\n\t\tLeft:        Config.BorderLeft,\n\t\tRight:       Config.BorderRight,\n\t\tTopLeft:     Config.BorderTopLeft,\n\t\tTopRight:    Config.BorderTopRight,\n\t\tBottomLeft:  Config.BorderBottomLeft,\n\t\tBottomRight: Config.BorderBottomRight,\n\t}\n}\n\nfunc LoadConfigError(value string, msg string) string {\n\treturn UserConfigInvalidationErrorString(value, \"Config\", msg)\n}\n\nfunc LoadHotkeysError(value string, msg string) string {\n\treturn UserConfigInvalidationErrorString(value, \"Hotkey\", msg)\n}\n\nfunc LoadThemeError(value string, msg string) string {\n\treturn UserConfigInvalidationErrorString(value, \"Theme\", msg)\n}\n\nfunc UserConfigInvalidationErrorString(value string, configType string, msg string) string {\n\treturn lipgloss.NewStyle().Foreground(lipgloss.Color(\"#FF5555\")).Render(\"■ ERROR: \") +\n\t\tconfigType + \" value for \\\"\" + lipgloss.NewStyle().Foreground(lipgloss.Color(\"#00D9FF\")).Render(value) +\n\t\t\"\\\" is invalid : \" + msg\n}\n\n// TODO : Fix Code duplication in textInput.Model creation\n// This eventually caused a bug, where we created new model for sidebar search, and\n// Didn't set `Width` in that. Take Width and other parameters as input in one function\n// Generate search bar for file panel\nfunc GenerateSearchBar() textinput.Model {\n\tti := textinput.New()\n\tti.Cursor.Style = FooterCursorStyle\n\tti.Cursor.TextStyle = FooterStyle\n\tti.TextStyle = FilePanelStyle\n\tti.Prompt = FilePanelTopDirectoryIconStyle.Render(icon.Search + icon.Space)\n\tti.Cursor.Blink = true\n\tti.PlaceholderStyle = FilePanelStyle\n\tti.Placeholder = \"(\" + Hotkeys.SearchBar[0] + \") Type something\"\n\tti.Blur()\n\tti.CharLimit = 156\n\treturn ti\n}\n\nfunc GeneratePromptTextInput() textinput.Model {\n\tt := textinput.New()\n\tt.Prompt = \"\"\n\tt.CharLimit = 156\n\tt.SetValue(\"\")\n\tt.Cursor.Style = ModalCursorStyle\n\tt.Cursor.TextStyle = ModalStyle\n\tt.TextStyle = ModalStyle\n\tt.PlaceholderStyle = ModalStyle\n\n\treturn t\n}\n\nfunc GenerateNewFileTextInput() textinput.Model {\n\tt := textinput.New()\n\tt.Cursor.Style = ModalCursorStyle\n\tt.Cursor.TextStyle = ModalStyle\n\tt.TextStyle = ModalStyle\n\tt.Cursor.Blink = true\n\tt.Placeholder = \"Add \\\"\" + string(filepath.Separator) + \"\\\" transcend folders\"\n\tt.PlaceholderStyle = ModalStyle\n\tt.Focus()\n\tt.CharLimit = 156\n\t//nolint:mnd // modal width minus padding\n\tt.Width = ModalWidth - 10\n\treturn t\n}\n\nfunc GenerateRenameTextInput(width int, cursorPos int, defaultValue string) textinput.Model {\n\tti := textinput.New()\n\tti.Cursor.Style = FilePanelCursorStyle\n\tti.Cursor.TextStyle = FilePanelStyle\n\tti.Prompt = FilePanelCursorStyle.Render(icon.Cursor + \" \")\n\tti.TextStyle = ModalStyle\n\tti.Cursor.Blink = true\n\tti.Placeholder = \"New name\"\n\tti.PlaceholderStyle = ModalStyle\n\tti.SetValue(defaultValue)\n\tti.SetCursor(cursorPos)\n\tti.Focus()\n\tti.CharLimit = 156\n\tti.Width = width\n\n\treturn ti\n}\n\nfunc GeneratePinnedRenameTextInput(cursorPos int, defaultValue string) textinput.Model {\n\tti := textinput.New()\n\tti.Cursor.Style = FilePanelCursorStyle\n\tti.Cursor.TextStyle = FilePanelStyle\n\tti.Prompt = FilePanelCursorStyle.Render(icon.Cursor + \" \")\n\tti.TextStyle = ModalStyle\n\tti.Cursor.Blink = true\n\tti.Placeholder = \"New name\"\n\tti.PlaceholderStyle = ModalStyle\n\tti.SetValue(defaultValue)\n\tti.SetCursor(cursorPos)\n\tti.Focus()\n\tti.CharLimit = 156\n\tti.Width = Config.SidebarWidth - PanelPadding\n\treturn ti\n}\n\nfunc GenerateGradientColor() progress.Option {\n\treturn progress.WithScaledGradient(Theme.GradientColor[0], Theme.GradientColor[1])\n}\n\nfunc GenerateFooterBorder(countString string, width int) string {\n\trepeatCount := width - len(countString)\n\tif repeatCount < 0 {\n\t\trepeatCount = 0\n\t}\n\treturn strings.Repeat(Config.BorderBottom, repeatCount) + Config.BorderMiddleRight +\n\t\tcountString + Config.BorderMiddleLeft\n}\n"
  },
  {
    "path": "src/internal/common/type.go",
    "content": "package common\n\n// Placeholder inteface for now, might later move 'model' type to commons and have\n// and add an execute(model) function to this\ntype ModelAction interface {\n\tString() string\n}\n\ntype NoAction struct {\n}\n\nfunc (n NoAction) String() string {\n\treturn \"NoAction\"\n}\n\ntype ShellCommandAction struct {\n\tCommand string\n}\n\nfunc (s ShellCommandAction) String() string {\n\treturn \"ShellCommandAction for command \" + s.Command\n}\n\n// We could later move 'model' type to commons and have\n// these actions implement an execute(model) interface\ntype SplitPanelAction struct{}\n\nfunc (s SplitPanelAction) String() string {\n\treturn \"SplitPanelAction\"\n}\n\ntype CDCurrentPanelAction struct {\n\tLocation string\n}\n\nfunc (c CDCurrentPanelAction) String() string {\n\treturn \"CDCurrentPanelAction to \" + c.Location\n}\n\ntype OpenPanelAction struct {\n\tLocation string\n}\n\nfunc (o OpenPanelAction) String() string {\n\treturn \"OpenPanelAction at \" + o.Location\n}\n"
  },
  {
    "path": "src/internal/common/ui_consts.go",
    "content": "package common\n\nimport \"time\"\n\n// Shared UI/layout constants to replace magic numbers flagged by mnd.\nconst (\n\tHelpKeyColumnWidth       = 55              // width of help key column in CLI help\n\tDefaultCLIContextTimeout = 5 * time.Second // default CLI context timeout for CLI ops\n\n\tPanelPadding    = 3 // rows reserved around file list (borders/header/footer)\n\tBorderPadding   = 2 // rows/cols for outer border frame\n\tInnerPadding    = 4 // cols for inner content padding (truncate widths)\n\tFooterGroupCols = 3 // columns per group in footer layout math\n\n\tDefaultFilePanelWidth    = 10 // default width for file panels\n\tFilePanelMax             = 10 // max number of file panels supported\n\tResponsiveWidthThreshold = 95 // width breakpoint for layout behavior\n\n\tHeightBreakA = 30 // responsive height tiers\n\tHeightBreakB = 35\n\tHeightBreakC = 40\n\tHeightBreakD = 45\n\n\tFilePanelWidthUnit    = 20                     // width unit used to calculate max file panels\n\tDefaultPreviewTimeout = 500 * time.Millisecond // preview operation timeout\n\n\tFileNameRatioMin = 25\n\tFileNameRatioMax = 100\n\n\tRequiredGradientColorCount = 2\n\n\t// UI positioning\n\tCenterDivisor = 2 // divisor for centering UI elements\n)\n"
  },
  {
    "path": "src/internal/config_function.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"os\"\n\t\"reflect\"\n\t\"runtime\"\n\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\n\t\"github.com/barasher/go-exiftool\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n\t\"github.com/yorukot/superfile/src/internal/ui/rendering\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sidebar\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// initialConfig load and handle all configuration files (spf config,Hotkeys\n// themes) setted up. Processes input directories and returns toggle states.\n\n// This is the only usecase of named returns, distinguish between multiple return values\nfunc initialConfig(firstPanelPaths []string) (toggleDotFile bool, //nolint: nonamedreturns // See above\n\ttoggleFooter bool, zClient *zoxidelib.Client) {\n\t// Open log stream\n\tfile, err := os.OpenFile(variable.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, utils.LogFilePerm)\n\n\t// TODO : This could be improved if we want to make superfile more resilient to errors\n\t// For example if the log file directories have access issues.\n\t// we could pass a dummy object to log.SetOutput() and the app would still function.\n\tif err != nil {\n\t\tutils.PrintfAndExitf(\"Error while opening superfile.log file : %v\", err)\n\t}\n\tcommon.LoadConfigFile()\n\n\tlogLevel := slog.LevelInfo\n\tif common.Config.Debug {\n\t\tlogLevel = slog.LevelDebug\n\t}\n\n\tslog.SetDefault(slog.New(slog.NewTextHandler(\n\t\tfile, &slog.HandlerOptions{Level: logLevel})))\n\n\tprintRuntimeInfo()\n\n\tcommon.LoadHotkeysFile(common.Config.IgnoreMissingFields)\n\n\tcommon.LoadThemeFile()\n\n\ticon.InitIcon(common.Config.Nerdfont, common.Theme.DirectoryIconColor)\n\n\tcommon.LoadThemeConfig()\n\tcommon.LoadPrerenderedVariables()\n\n\t// TODO: Make sure to clean it up. Via et.Close()\n\t// Note: All the tool we use to interact with OS, should be abstracted behind a struc\n\t// Have exiftool manager, Zoxide Manager, OS Manager, Xtractor, Zipper, Command Executor\n\tif common.Config.Metadata {\n\t\tet, err = exiftool.NewExiftool()\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while initial model function init exiftool error\", \"error\", err)\n\t\t}\n\t}\n\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\tslog.Error(\"cannot get current working directory\", \"error\", err)\n\t\tcwd = variable.HomeDir\n\t}\n\n\tif common.Config.ZoxideSupport {\n\t\tzClient, err = zoxidelib.New()\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error initializing zoxide client\", \"error\", err)\n\t\t}\n\t}\n\n\tupdateFirstFilePanelPaths(firstPanelPaths, cwd, zClient)\n\n\tslog.Debug(\"Directory configuration\", \"cwd\", cwd, \"start_paths\", firstPanelPaths)\n\tprintRuntimeInfo()\n\n\ttoggleDotFile = utils.ReadBoolFile(variable.ToggleDotFile, false)\n\ttoggleFooter = utils.ReadBoolFile(variable.ToggleFooter, true)\n\n\treturn toggleDotFile, toggleFooter, zClient\n}\n\nfunc updateFirstFilePanelPaths(firstPanelPaths []string, cwd string, zClient *zoxidelib.Client) {\n\tfor i := range firstPanelPaths {\n\t\tif firstPanelPaths[i] == \"\" {\n\t\t\tfirstPanelPaths[i] = common.Config.DefaultDirectory\n\t\t}\n\t\toriginalPath := firstPanelPaths[i]\n\t\tfirstPanelPaths[i] = utils.ResolveAbsPath(cwd, firstPanelPaths[i])\n\t\tif _, err := os.Stat(firstPanelPaths[i]); err != nil {\n\t\t\tslog.Error(\"cannot get stats\", \"path\", firstPanelPaths[i], \"error\", err)\n\t\t\t// In case the path provided did not exist, use zoxide query\n\t\t\t// else, fallback to home dir\n\t\t\tif common.Config.ZoxideSupport && zClient != nil {\n\t\t\t\tpath, err := attemptZoxideForInitPath(originalPath, zClient)\n\t\t\t\tif err != nil {\n\t\t\t\t\tslog.Error(\"Zoxide query error\", \"originalPath\", originalPath, \"error\", err)\n\t\t\t\t\tfirstPanelPaths[i] = variable.HomeDir\n\t\t\t\t} else {\n\t\t\t\t\tfirstPanelPaths[i] = path\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfirstPanelPaths[i] = variable.HomeDir\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc attemptZoxideForInitPath(originalPath string, zClient *zoxidelib.Client) (string, error) {\n\tpath, err := zClient.Query(originalPath)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif path == \"\" {\n\t\treturn \"\", errors.New(\"zoxide returned empty path\")\n\t}\n\tif stat, statErr := os.Stat(path); statErr != nil || !stat.IsDir() {\n\t\treturn \"\", errors.New(\"zoxide returned invalid path\")\n\t}\n\treturn path, nil\n}\n\nfunc printRuntimeInfo() {\n\tslog.Debug(\"Runtime information\", \"runtime.GOOS\", runtime.GOOS)\n\tvar memStats runtime.MemStats\n\truntime.ReadMemStats(&memStats)\n\tslog.Debug(\"Memory usage\",\n\t\t\"alloc_bytes\", memStats.Alloc,\n\t\t\"total_alloc_bytes\", memStats.TotalAlloc,\n\t\t\"heap_objects\", memStats.HeapObjects,\n\t\t\"sys_bytes\", memStats.Sys)\n\tslog.Debug(\"Object sizes\",\n\t\t\"model_size_bytes\", reflect.TypeOf(model{}).Size(),\n\t\t\"filePanel_size_bytes\", reflect.TypeOf(filepanel.Model{}).Size(),\n\t\t\"sidebarModel_size_bytes\", reflect.TypeOf(sidebar.Model{}).Size(),\n\t\t\"renderer_size_bytes\", reflect.TypeOf(rendering.Renderer{}).Size(),\n\t\t\"borderConfig_size_bytes\", reflect.TypeOf(rendering.BorderConfig{}).Size(),\n\t\t\"process_size_bytes\", reflect.TypeOf(processbar.Process{}).Size())\n}\n"
  },
  {
    "path": "src/internal/default_config.go",
    "content": "package internal\n\nimport (\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/helpmenu\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filemodel\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sortmodel\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/metadata\"\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sidebar\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/prompt\"\n\tzoxideui \"github.com/yorukot/superfile/src/internal/ui/zoxide\"\n)\n\n// Generate and return model containing default configurations for interface\n// Maybe we can replace slice of strings with var args - Should we ?\n// TODO: Move the configuration parameters to a ModelConfig struct.\n// Something like `RendererConfig` struct for `Renderer` struct in ui/renderer package\n// Or even better API like varargs lambda function opts\n// which can be WithFooter(), WithXYZ()\n// Lots of improvements are waiting on it\n//   - Allow Sending thumbnailGeneratorNeeded as false to preview.New()\n//     to prevent noise in test logs. Same with imagePreviewer\nfunc defaultModelConfig(toggleDotFile, toggleFooter, firstUse bool,\n\tfirstPanelPaths []string, zClient *zoxidelib.Client) *model {\n\treturn &model{\n\t\tfocusPanel:      nonePanelFocus,\n\t\tprocessBarModel: processbar.New(),\n\t\tsidebarModel:    sidebar.New(),\n\t\tfileMetaData:    metadata.New(),\n\t\tfileModel:       filemodel.New(firstPanelPaths, toggleDotFile),\n\t\thelpMenu:        helpmenu.New(),\n\t\tpromptModal:     prompt.DefaultModel(prompt.PromptMinHeight, prompt.PromptMinWidth),\n\t\tzoxideModal:     zoxideui.DefaultModel(zoxideui.ZoxideMinHeight, zoxideui.ZoxideMinWidth, zClient),\n\t\tsortModal:       sortmodel.New(),\n\t\tzClient:         zClient,\n\t\tmodelQuitState:  notQuitting,\n\t\ttoggleFooter:    toggleFooter,\n\t\tfirstUse:        firstUse,\n\t\thasTrash:        common.InitTrash(),\n\t}\n}\n"
  },
  {
    "path": "src/internal/file_operation_compress_test.go",
    "content": "package internal\n\nimport (\n\t\"archive/zip\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\nfunc TestZipSources(t *testing.T) {\n\tprocessBar := processbar.New()\n\tprocessBar.ListenForChannelUpdates()\n\tt.Cleanup(processBar.SendStopListeningMsgBlocking)\n\ttests := []struct {\n\t\tname          string\n\t\tsetupFunc     func(t *testing.T, tempDir string) ([]string, error)\n\t\texpectedFiles map[string]string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname: \"multiple directories with subdirectories\",\n\t\t\tsetupFunc: func(t *testing.T, tempDir string) ([]string, error) {\n\t\t\t\ttestDir1 := filepath.Join(tempDir, \"testdir1\")\n\t\t\t\ttestDir2 := filepath.Join(tempDir, \"testdir2\")\n\t\t\t\tsubDir := filepath.Join(testDir1, \"subdir\")\n\t\t\t\tutils.SetupDirectories(t, testDir1, testDir2, subDir)\n\t\t\t\tutils.SetupFilesWithData(t, []byte(\"Content of file1\"), filepath.Join(testDir1, \"file1.txt\"))\n\t\t\t\tutils.SetupFilesWithData(t, []byte(\"Content of file2\"), filepath.Join(subDir, \"file2.txt\"))\n\t\t\t\tutils.SetupFilesWithData(t, []byte(\"Content of file3\"), filepath.Join(testDir2, \"file3.txt\"))\n\n\t\t\t\treturn []string{testDir1, testDir2}, nil\n\t\t\t},\n\n\t\t\t// End for directory is always \"/\" regardless of windows and linux for zipReader library\n\t\t\texpectedFiles: map[string]string{\n\t\t\t\t\"testdir1/\":                                      \"\",\n\t\t\t\tfilepath.Join(\"testdir1\", \"file1.txt\"):           \"Content of file1\",\n\t\t\t\tfilepath.Join(\"testdir1\", \"subdir\") + \"/\":        \"\",\n\t\t\t\tfilepath.Join(\"testdir1\", \"subdir\", \"file2.txt\"): \"Content of file2\",\n\t\t\t\t\"testdir2/\":                            \"\",\n\t\t\t\tfilepath.Join(\"testdir2\", \"file3.txt\"): \"Content of file3\",\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"single file\",\n\t\t\tsetupFunc: func(t *testing.T, tempDir string) ([]string, error) {\n\t\t\t\ttestFile := filepath.Join(tempDir, \"single.txt\")\n\t\t\t\tutils.SetupFilesWithData(t, []byte(\"Single file content\"), testFile)\n\t\t\t\treturn []string{testFile}, nil\n\t\t\t},\n\t\t\texpectedFiles: map[string]string{\n\t\t\t\t\"single.txt\": \"Single file content\",\n\t\t\t},\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty list\",\n\t\t\tsetupFunc: func(_ *testing.T, _ string) ([]string, error) {\n\t\t\t\treturn []string{}, nil\n\t\t\t},\n\t\t\texpectedFiles: map[string]string{},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"non-existent source\",\n\t\t\tsetupFunc: func(_ *testing.T, _ string) ([]string, error) {\n\t\t\t\treturn []string{\"/non/existent/path\"}, nil\n\t\t\t},\n\t\t\texpectedFiles: nil,\n\t\t\texpectError:   true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttempDir := t.TempDir()\n\t\t\tsources, err := tt.setupFunc(t, tempDir)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Setup failed: %v\", err)\n\t\t\t}\n\n\t\t\ttargetZip := filepath.Join(tempDir, \"test.zip\")\n\t\t\terr = zipSources(sources, targetZip, &processBar)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err, \"zipSources should return error\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"zipSources should not return error\")\n\n\t\t\tzipReader, err := zip.OpenReader(targetZip)\n\t\t\trequire.NoError(t, err, \"should be able to open ZIP file\")\n\t\t\tdefer zipReader.Close()\n\t\t\tvalidateZipExtraction(t, zipReader, tt.expectedFiles)\n\t\t})\n\t}\n}\n\nfunc validateZipExtraction(t *testing.T, zipReader *zip.ReadCloser, expectedFiles map[string]string) {\n\trequire.Len(t, zipReader.File, len(expectedFiles), \"ZIP should contain expected number of files\")\n\n\tfoundFiles := make(map[string]string)\n\tfor _, file := range zipReader.File {\n\t\tfoundFiles[file.Name] = \"\"\n\t\tif !strings.HasSuffix(file.Name, \"/\") {\n\t\t\trc, err := file.Open()\n\t\t\trequire.NoError(t, err, \"should be able to open file %s in ZIP\", file.Name)\n\n\t\t\tcontent, err := io.ReadAll(rc)\n\t\t\trc.Close()\n\t\t\trequire.NoError(t, err, \"should be able to read file %s\", file.Name)\n\n\t\t\tfoundFiles[file.Name] = string(content)\n\t\t}\n\t}\n\n\tfor expectedFile, expectedContent := range expectedFiles {\n\t\tfoundContent, exists := foundFiles[expectedFile]\n\t\trequire.True(t, exists, \"expected file %s should be found in ZIP\", expectedFile)\n\t\tif expectedContent != \"\" {\n\t\t\trequire.Equal(t, expectedContent, foundContent, \"content should match for file %s\", expectedFile)\n\t\t}\n\t}\n\n\tfor foundFile := range foundFiles {\n\t\t_, expected := expectedFiles[foundFile]\n\t\trequire.True(t, expected, \"unexpected file %s found in ZIP\", foundFile)\n\t}\n}\n\nfunc TestZipSourcesInvalidTarget(t *testing.T) {\n\tprocessBar := processbar.New()\n\tprocessBar.ListenForChannelUpdates()\n\tt.Cleanup(processBar.SendStopListeningMsgBlocking)\n\ttempDir := t.TempDir()\n\ttestFile := filepath.Join(tempDir, \"test.txt\")\n\terr := os.WriteFile(testFile, []byte(\"test\"), 0644)\n\trequire.NoError(t, err, \"should be able to create test file\")\n\n\tinvalidTarget := \"/invalid/path/test.zip\"\n\terr = zipSources([]string{testFile}, invalidTarget, &processBar)\n\trequire.Error(t, err, \"zipSources should return error for invalid target\")\n}\n"
  },
  {
    "path": "src/internal/file_operations.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\ttrash_win \"github.com/hymkor/trash-go\"\n\t\"github.com/rkoesters/xdg/trash\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n)\n\n// isSamePartition checks if two paths are on the same filesystem partition\nfunc isSamePartition(path1, path2 string) (bool, error) {\n\t// Get the absolute path to handle relative paths\n\tabsPath1, err := filepath.Abs(path1)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to get absolute path of the first path: %w\", err)\n\t}\n\n\tabsPath2, err := filepath.Abs(path2)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to get absolute path of the second path: %w\", err)\n\t}\n\n\tif runtime.GOOS == utils.OsWindows {\n\t\t// On Windows, we can check if both paths are on the same drive (same letter)\n\t\tdrive1 := getDriveLetter(absPath1)\n\t\tdrive2 := getDriveLetter(absPath2)\n\t\treturn drive1 == drive2, nil\n\t}\n\n\t// For Unix-like systems, we use the same path to check the root partition\n\treturn filepath.VolumeName(absPath1) == filepath.VolumeName(absPath2), nil\n}\n\n// getDriveLetter extracts the drive letter from a Windows path\nfunc getDriveLetter(path string) string {\n\t// Windows paths are usually like \"C:\\path\\to\\file\"\n\t// So we need to extract the drive letter (e.g., \"C\")\n\treturn strings.ToUpper(string(path[0]))\n}\n\n// moveElement moves a file or directory efficiently\nfunc moveElement(src, dst string) error {\n\t// Check if source and destination are on the same partition\n\tsameDev, err := isSamePartition(src, dst)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to check partitions: %w\", err)\n\t}\n\n\t// If on the same partition, attempt to rename (which will use the same inode)\n\tif sameDev {\n\t\tif err = os.Rename(src, dst); err == nil {\n\t\t\treturn nil\n\t\t}\n\t\t// If rename fails, fall back to copy+delete\n\t}\n\n\t// If on different partitions or rename failed, fall back to copy+delete\n\terr = copyElement(src, dst)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to copy: %w\", err)\n\t}\n\n\terr = os.RemoveAll(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to remove source after copy: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// copyElement handles copying of both files and directories\nfunc copyElement(src, dst string) error {\n\tsrcInfo, err := os.Stat(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to stat source: %w\", err)\n\t}\n\n\tif srcInfo.IsDir() {\n\t\treturn copyDir(src, dst, srcInfo)\n\t}\n\treturn copyFile(src, dst, srcInfo)\n}\n\n// copyDir recursively copies a directory\nfunc copyDir(src, dst string, srcInfo os.FileInfo) error {\n\terr := os.MkdirAll(dst, srcInfo.Mode())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create destination directory: %w\", err)\n\t}\n\n\tentries, err := os.ReadDir(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read source directory: %w\", err)\n\t}\n\n\tfor _, entry := range entries {\n\t\tsrcPath := filepath.Join(src, entry.Name())\n\t\tdstPath := filepath.Join(dst, entry.Name())\n\n\t\tentryInfo, err := entry.Info()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get entry info: %w\", err)\n\t\t}\n\n\t\tif entryInfo.IsDir() {\n\t\t\terr = copyDir(srcPath, dstPath, entryInfo)\n\t\t} else {\n\t\t\terr = copyFile(srcPath, dstPath, entryInfo)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// copyFile copies a single file\nfunc copyFile(src, dst string, srcInfo os.FileInfo) error {\n\tsrcFile, err := os.Open(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open source file: %w\", err)\n\t}\n\tdefer srcFile.Close()\n\n\tdstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create destination file: %w\", err)\n\t}\n\tdefer dstFile.Close()\n\n\tif _, err := io.Copy(dstFile, srcFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy file contents: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc moveToTrash(src string) error {\n\tvar err error\n\tswitch runtime.GOOS {\n\tcase utils.OsDarwin:\n\t\terr = moveElement(src, filepath.Join(variable.DarwinTrashDirectory, filepath.Base(src)))\n\tcase utils.OsWindows:\n\t\terr = trash_win.Throw(src)\n\tdefault:\n\t\t// TODO: We should consider moving away from this package. Its not well written.\n\t\t// It uses package globals, It doesn't initializes trash directory, and we have to do it\n\t\t// separately outside of the this package. There is not documentation about this\n\t\t// It also uses deprecated libraries, and isn't well maintained.\n\t\terr = trash.Trash(src)\n\t}\n\tif err != nil {\n\t\tslog.Error(\"Error while deleting single item, in function to move file to trash can\", \"error\", err)\n\t}\n\treturn err\n}\n\n// pasteDir handles directory copying with progress tracking\nfunc pasteDir(src, dst string, p *processbar.Process, cut bool, processBarModel *processbar.Model) error {\n\tdst, err := renameIfDuplicate(dst)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if we can do a fast move within the same partition\n\tsameDev, err := isSamePartition(src, dst)\n\tif err == nil && sameDev && cut {\n\t\t// For cut operations on same partition, try fast rename first\n\t\terr = os.Rename(src, dst)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\t// If rename fails, fall back to manual copy\n\t}\n\n\terr = filepath.Walk(src, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trelPath, err := filepath.Rel(src, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnewPath := filepath.Join(dst, relPath)\n\t\treturn actualPasteOperation(info, path, newPath, cut, sameDev, p, processBarModel)\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If this was a cut operation and we had to do a manual copy, remove the source\n\tif cut && !sameDev {\n\t\terr = os.RemoveAll(src)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove source after move: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc actualPasteOperation(info os.FileInfo, path string, newPath string, cut bool, sameDev bool,\n\tp *processbar.Process, processBarModel *processbar.Model) error {\n\tvar err error\n\tif info.IsDir() {\n\t\t// TODO - this is likely not needed because we did\n\t\t// dst, err := renameIfDuplicate(dst) above\n\t\tnewPath, err = renameIfDuplicate(newPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = os.MkdirAll(newPath, info.Mode())\n\t\treturn err\n\t}\n\n\t// File\n\tp.CurrentFile = filepath.Base(path)\n\tif cut && sameDev {\n\t\terr = os.Rename(path, newPath)\n\t} else {\n\t\terr = copyFile(path, newPath, info)\n\t}\n\n\tif err != nil {\n\t\tp.State = processbar.Failed\n\t\tpSendErr := processBarModel.SendUpdateProcessMsg(*p, true)\n\t\tif pSendErr != nil {\n\t\t\tslog.Error(\"Error sending process update\", \"error\", pSendErr)\n\t\t}\n\t\treturn err\n\t}\n\n\tp.Done++\n\tprocessBarModel.TrySendingUpdateProcessMsg(*p)\n\treturn nil\n}\n\n// isAncestor checks if dst is the same as src or a subdirectory of src.\n// It handles symlinks by resolving them and applies case-insensitive comparison on Windows.\nfunc isAncestor(src, dst string) bool {\n\t// Resolve symlinks for both paths\n\tsrcResolved, err := filepath.EvalSymlinks(src)\n\tif err != nil {\n\t\t// If we can't resolve symlinks, fall back to original path\n\t\tsrcResolved = src\n\t}\n\n\tdstResolved, err := filepath.EvalSymlinks(dst)\n\tif err != nil {\n\t\t// If we can't resolve symlinks, fall back to original path\n\t\tdstResolved = dst\n\t}\n\n\t// Get absolute paths. Abs() also Cleans paths to normalize separators and resolve . and ..\n\tsrcAbs, err := filepath.Abs(srcResolved)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tdstAbs, err := filepath.Abs(dstResolved)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// On Windows, perform case-insensitive comparison\n\tif runtime.GOOS == \"windows\" {\n\t\tsrcAbs = strings.ToLower(srcAbs)\n\t\tdstAbs = strings.ToLower(dstAbs)\n\t}\n\n\t// Check if dst is the same as src\n\tif srcAbs == dstAbs {\n\t\treturn true\n\t}\n\n\t// Check if dst is a subdirectory of src\n\t// Use filepath.Rel to check the relationship\n\trel, err := filepath.Rel(srcAbs, dstAbs)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// If rel is \".\" or doesn't start with \"..\", then dst is inside src\n\treturn rel == \".\" || !strings.HasPrefix(rel, \"..\")\n}\n"
  },
  {
    "path": "src/internal/file_operations_compress.go",
    "content": "package internal\n\nimport (\n\t\"archive/zip\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\nfunc zipSources(sources []string, target string, processBar *processbar.Model) error {\n\tvar err error\n\n\ttotalFiles := 0\n\tfor _, src := range sources {\n\t\tif _, err = os.Stat(src); os.IsNotExist(err) {\n\t\t\treturn fmt.Errorf(\"source path does not exist: %s\", src)\n\t\t}\n\t\tcount, e := countFiles(src)\n\t\tif e != nil {\n\t\t\tslog.Error(\"Error while zip file count files \", \"error\", e)\n\t\t}\n\t\ttotalFiles += count\n\t}\n\tp, err := processBar.SendAddProcessMsg(filepath.Base(target), processbar.OpCompress, totalFiles, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot spawn process : %w\", err)\n\t}\n\t_, err = os.Stat(target)\n\tif err == nil {\n\t\tp.ErrorMsg = \"File already exists\"\n\t\tp.State = processbar.Cancelled\n\t\tp.DoneTime = time.Now()\n\t\tpSendErr := processBar.SendUpdateProcessMsg(p, true)\n\t\tif pSendErr != nil {\n\t\t\tslog.Error(\"Error sending process update\", \"error\", pSendErr)\n\t\t}\n\t\treturn errors.New(\"file already exists\")\n\t}\n\n\tf, err := os.Create(target)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\twriter := zip.NewWriter(f)\n\tdefer writer.Close()\n\n\tzipSourcesCore(sources, processBar, &p, writer)\n\n\tif p.State != processbar.Failed {\n\t\t// TODO: User p.SetSuccessful(), p.SetFailed()\n\t\tp.State = processbar.Successful\n\t\tp.Done = totalFiles\n\t}\n\tp.DoneTime = time.Now()\n\tpSendErr := processBar.SendUpdateProcessMsg(p, true)\n\tif pSendErr != nil {\n\t\tslog.Error(\"Error sending process update\", \"error\", pSendErr)\n\t}\n\treturn nil\n}\n\nfunc zipSourcesCore(sources []string, processBar *processbar.Model,\n\tp *processbar.Process, writer *zip.Writer) {\n\tfor _, src := range sources {\n\t\tsrcParentDir := filepath.Dir(src)\n\t\terr := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {\n\t\t\tp.CurrentFile = filepath.Base(path)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trelPath, err := filepath.Rel(srcParentDir, path)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terr = writeZipFile(path, relPath, info, writer)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tp.Done++\n\t\t\tprocessBar.TrySendingUpdateProcessMsg(*p)\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while zip file\", \"error\", err)\n\t\t\tp.State = processbar.Failed\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc writeZipFile(path string, relPath string, info os.FileInfo, writer *zip.Writer) error {\n\theader, err := zip.FileInfoHeader(info)\n\tif err != nil {\n\t\treturn err\n\t}\n\theader.Method = zip.Deflate\n\theader.Name = relPath\n\tif info.IsDir() {\n\t\theader.Name += \"/\"\n\t}\n\theaderWriter, err := writer.CreateHeader(header)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif info.IsDir() {\n\t\treturn nil\n\t}\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer file.Close()\n\t_, err = io.Copy(headerWriter, file)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc getZipArchiveName(base string) (string, error) {\n\tzipName := strings.TrimSuffix(base, filepath.Ext(base)) + \".zip\"\n\tzipName, err := renameIfDuplicate(zipName)\n\treturn zipName, err\n}\n"
  },
  {
    "path": "src/internal/file_operations_extract.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"golift.io/xtractr\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\nfunc extractCompressFile(src, dest string, processBar *processbar.Model) error {\n\tp, err := processBar.SendAddProcessMsg(filepath.Base(src), processbar.OpExtract, 1, true)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot spawn process : %w\", err)\n\t}\n\n\tx := &xtractr.XFile{\n\t\tFilePath:  src,\n\t\tOutputDir: dest,\n\t\tFileMode:  utils.ExtractedFileMode,\n\t\tDirMode:   utils.ExtractedDirMode,\n\t}\n\n\t_, _, _, err = xtractr.ExtractFile(x)\n\n\tif err != nil {\n\t\tp.State = processbar.Failed\n\t\tslog.Error(\"Error extracting\", \"path\", src, \"error\", err)\n\t} else {\n\t\tp.State = processbar.Successful\n\t\tp.Done = 1\n\t}\n\n\tp.DoneTime = time.Now()\n\tpSendErr := processBar.SendUpdateProcessMsg(p, true)\n\tif pSendErr != nil {\n\t\tslog.Error(\"Error sending process update\", \"error\", pSendErr)\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "src/internal/function.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\nvar suffixRegexp = regexp.MustCompile(`^(.*)\\((\\d+)\\)$`)\n\n// Check if the directory is external disk path\n// TODO : This function should be give two directories, and it should return\n// if the two share a different disk partition.\n// Ideally we shouldn't even try to figure that out in our file operations, and let OS handles it.\n// But at least right now its not okay. This returns if `path` is an External disk\n// from perspective of `/`, but it should tell from perspective of currently open directory\n// The usage of this function in cut/paste is not as expected.\nfunc isExternalDiskPath(path string) bool {\n\t// This is very vague. You cannot tell if a path is belonging to an external partition\n\t// if you dont define the source path to compare with\n\t// But making this true will cause slow file operations based on current implementation\n\tif runtime.GOOS == utils.OsWindows {\n\t\treturn false\n\t}\n\n\t// exclude timemachine on macOS\n\tif strings.HasPrefix(path, \"/Volumes/.timemachine\") {\n\t\treturn false\n\t}\n\n\t// to filter out mounted partitions like /, /boot etc\n\treturn strings.HasPrefix(path, \"/mnt\") ||\n\t\tstrings.HasPrefix(path, \"/media\") ||\n\t\tstrings.HasPrefix(path, \"/run/media\") ||\n\t\tstrings.HasPrefix(path, \"/Volumes\")\n}\n\nfunc checkFileNameValidity(name string) error {\n\tswitch {\n\tcase name == \".\", name == \"..\":\n\t\treturn errors.New(\"file name cannot be '.' or '..'\")\n\tcase strings.HasSuffix(name, fmt.Sprintf(\"%c.\", filepath.Separator)),\n\t\tstrings.HasSuffix(name, fmt.Sprintf(\"%c..\", filepath.Separator)):\n\t\treturn fmt.Errorf(\"file name cannot end with '%c.' or '%c..'\", filepath.Separator, filepath.Separator)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc renameIfDuplicate(destination string) (string, error) {\n\tif _, err := os.Stat(destination); os.IsNotExist(err) {\n\t\treturn destination, nil\n\t} else if err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdir := filepath.Dir(destination)\n\tbase := filepath.Base(destination)\n\text := filepath.Ext(base)\n\tname := base[:len(base)-len(ext)]\n\n\t// Extract base name without existing suffix\n\tcounter := 1\n\t//nolint:mnd // 3 = full match + 2 capture groups\n\tif match := suffixRegexp.FindStringSubmatch(name); len(match) == 3 {\n\t\tname = match[1] // base name without (N)\n\t\tif num, err := strconv.Atoi(match[2]); err == nil {\n\t\t\tcounter = num + 1 // start from next number\n\t\t}\n\t}\n\n\t// Find first available name\n\tfor i := counter; i < 10_000; i++ {\n\t\tnewName := fmt.Sprintf(\"%s(%d)%s\", name, i, ext)\n\t\tnewPath := filepath.Join(dir, newName)\n\t\tif _, err := os.Stat(newPath); os.IsNotExist(err) {\n\t\t\treturn newPath, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not find free name for %s after many attempts\", destination)\n}\n\n// Count how many file in the directory\nfunc countFiles(dirPath string) (int, error) {\n\tcount := 0\n\n\terr := filepath.Walk(dirPath, func(_ string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !info.IsDir() {\n\t\t\tcount++\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn count, err\n}\n\nfunc processCmdToTeaCmd(cmd processbar.Cmd) tea.Cmd {\n\tif cmd == nil {\n\t\t// To prevent us from running cmd() on nil cmd\n\t\treturn nil\n\t}\n\treturn func() tea.Msg {\n\t\tupdateMsg := cmd()\n\t\treturn ProcessBarUpdateMsg{\n\t\t\tpMsg: updateMsg,\n\t\t\tBaseMessage: BaseMessage{\n\t\t\t\treqID: updateMsg.GetReqID(),\n\t\t\t},\n\t\t}\n\t}\n}\n\nfunc getCopyOrCutOperationName(cut bool) string {\n\tif cut {\n\t\treturn \"cut\"\n\t}\n\treturn \"copy\"\n}\n"
  },
  {
    "path": "src/internal/function_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\nfunc TestCheckFileNameValidity(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tinput   string\n\t\twantErr bool\n\t\terrMsg  string\n\t}{\n\t\t{name: \"Invalid - single dot\",\n\t\t\tinput:   \".\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"file name cannot be '.' or '..'\",\n\t\t}, {\n\t\t\tname:    \"invalid - double dot\",\n\t\t\tinput:   \"..\",\n\t\t\twantErr: true,\n\t\t\terrMsg:  \"file name cannot be '.' or '..'\",\n\t\t}, {\n\t\t\tname:    \"invalid - ends with /.. (platform separator)\",\n\t\t\tinput:   fmt.Sprintf(\"testDir%c..\", filepath.Separator),\n\t\t\twantErr: true,\n\t\t\terrMsg:  fmt.Sprintf(\"file name cannot end with '%c.' or '%c..'\", filepath.Separator, filepath.Separator),\n\t\t}, {\n\t\t\tname:    \"invalid - ends with /. (platform separator)\",\n\t\t\tinput:   fmt.Sprintf(\"testDir%c.\", filepath.Separator),\n\t\t\twantErr: true,\n\t\t\terrMsg:  fmt.Sprintf(\"file name cannot end with '%c.' or '%c..'\", filepath.Separator, filepath.Separator),\n\t\t}, {\n\t\t\tname:    \"valid - normal file name\",\n\t\t\tinput:   \"valid_file.txt\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid - contains dot inside\",\n\t\t\tinput:   \"some.folder.name/file.txt\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid - ends with dot not after separator\",\n\t\t\tinput:   \"somefile.\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"valid - ends with .. not after separator\",\n\t\t\tinput:   \"somefile..\",\n\t\t\twantErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := checkFileNameValidity(tt.input)\n\n\t\t\tif !tt.wantErr {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.errMsg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_renameIfDuplicate(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\tf1NonExistent := filepath.Join(curTestDir, \"file.txt\")\n\tf2 := filepath.Join(curTestDir, \"file2.txt\")\n\tf3 := filepath.Join(curTestDir, \"file3(3).txt\")\n\td1 := filepath.Join(curTestDir, \"dir1\")\n\n\tutils.SetupFiles(t, f2, f3)\n\tutils.SetupDirectories(t, d1)\n\n\ttests := []struct {\n\t\tname     string\n\t\tfileName string\n\t\twant     string\n\t}{\n\t\t{\n\t\t\tname:     \"file does not exist\",\n\t\t\tfileName: f1NonExistent,\n\t\t\twant:     filepath.Base(f1NonExistent),\n\t\t},\n\t\t{\n\t\t\tname:     \"file exists without suffix\",\n\t\t\tfileName: f2,\n\t\t\twant:     \"file2(1).txt\",\n\t\t},\n\t\t{\n\t\t\tname:     \"file exists with suffix\",\n\t\t\tfileName: f3,\n\t\t\twant:     \"file3(4).txt\",\n\t\t},\n\t\t{\n\t\t\tname:     \"directory exists\",\n\t\t\tfileName: d1,\n\t\t\twant:     \"dir1(1)\", // without extension\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresults, err := renameIfDuplicate(tt.fileName)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, filepath.Base(tt.want), filepath.Base(results))\n\t\t})\n\t}\n}\n\nfunc Benchmark_renameIfDuplicate(b *testing.B) {\n\tdir := b.TempDir()\n\n\texistingFile := filepath.Join(dir, \"file.txt\")\n\terr := os.WriteFile(existingFile, utils.SampleDataBytes, 0644)\n\trequire.NoError(b, err)\n\n\texistingDir := filepath.Join(dir, \"docs\")\n\terr = os.Mkdir(existingDir, 0o755)\n\trequire.NoError(b, err)\n\n\tb.Run(\"file_exists\", func(b *testing.B) {\n\t\tfor range b.N {\n\t\t\t_, err := renameIfDuplicate(existingFile)\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tb.Run(\"dir_exists\", func(b *testing.B) {\n\t\tfor range b.N {\n\t\t\t_, err := renameIfDuplicate(existingDir)\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tb.Run(\"file_not_exists\", func(b *testing.B) {\n\t\tnonExistent := filepath.Join(dir, \"nofile.txt\")\n\t\tfor range b.N {\n\t\t\t_, err := renameIfDuplicate(nonExistent)\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "src/internal/handle_file_operation_test.go",
    "content": "package internal\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\nfunc TestCompressSelectedFiles(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tfile1 := filepath.Join(curTestDir, \"file1.txt\")\n\tfile2 := filepath.Join(dir1, \"file2.txt\")\n\n\tutils.SetupDirectories(t, curTestDir, dir1, dir2)\n\tutils.SetupFiles(t, file1, file2)\n\n\t// Note this is to validate the end to end user interface, not to extensively validate\n\t// that compress works as expected. For that, we have TestZipSources\n\n\t// Need to test that\n\t// 1 - Compress single file (Browser Mode)\n\t// 2 - Compress Single directory with files (Browser Mode)\n\t// 3 - Compress single file where cursor is pointed when nothing is selected (Select Mode)\n\t// 4 - Compress single selected file in Select Mode where cursor points to different file\n\t// 5 - Compress multiple selected files and directories\n\t// 6 - Pressing compress hotkey on empty panel doesn't do anything or crashes on both browser/select mode\n\n\t// Copied from CopyTest. TODO - work on it.\n\n\ttestdata := []struct {\n\t\tname             string\n\t\tstartDir         string\n\t\tcursor           int\n\t\tselectMode       bool\n\t\tselectedElem     []string\n\t\texpectedZipName  string\n\t\textractedDirName string\n\t\t// Relative to extractedDir\n\t\texpectedFilesAfterExtract []string\n\t}{\n\t\t{\n\t\t\tname:                      \"Single File Compress\",\n\t\t\tstartDir:                  curTestDir,\n\t\t\tcursor:                    2,\n\t\t\tselectMode:                false,\n\t\t\tselectedElem:              nil,\n\t\t\texpectedZipName:           \"file1.zip\",\n\t\t\textractedDirName:          \"file1\",\n\t\t\texpectedFilesAfterExtract: []string{\"file1.txt\"},\n\t\t},\n\t\t{\n\t\t\tname:                      \"Single Directory Compress\",\n\t\t\tstartDir:                  curTestDir,\n\t\t\tcursor:                    0,\n\t\t\tselectMode:                false,\n\t\t\tselectedElem:              nil,\n\t\t\texpectedZipName:           \"dir1.zip\",\n\t\t\textractedDirName:          \"dir1(1)\",\n\t\t\texpectedFilesAfterExtract: []string{filepath.Join(\"dir1\", \"file2.txt\")},\n\t\t},\n\t\t{\n\t\t\tname:                      \"Single File Compress with select mode without selection\",\n\t\t\tstartDir:                  curTestDir,\n\t\t\tcursor:                    2,\n\t\t\tselectMode:                true,\n\t\t\tselectedElem:              []string{},\n\t\t\texpectedZipName:           \"file1.zip\",\n\t\t\textractedDirName:          \"file1\",\n\t\t\texpectedFilesAfterExtract: []string{\"file1.txt\"},\n\t\t},\n\t\t{\n\t\t\tname:                      \"Single File Compress with select mode with different cursor and selection\",\n\t\t\tstartDir:                  curTestDir,\n\t\t\tcursor:                    0, // points to dir1\n\t\t\tselectMode:                true,\n\t\t\tselectedElem:              []string{file1},\n\t\t\texpectedZipName:           \"file1.zip\",\n\t\t\textractedDirName:          \"file1\",\n\t\t\texpectedFilesAfterExtract: []string{\"file1.txt\"},\n\t\t},\n\t\t{\n\t\t\tname:                      \"Multi file compression\",\n\t\t\tstartDir:                  curTestDir,\n\t\t\tcursor:                    0, // points to dir1\n\t\t\tselectMode:                true,\n\t\t\tselectedElem:              []string{dir2, dir1, file1},\n\t\t\texpectedZipName:           \"dir2.zip\",\n\t\t\textractedDirName:          \"dir2(1)\",\n\t\t\texpectedFilesAfterExtract: []string{\"dir2\", filepath.Join(\"dir1\", \"file2.txt\"), \"file1.txt\"},\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := defaultTestModel(tt.startDir)\n\t\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\t\t\trequire.Greater(t, m.getFocusedFilePanel().ElemCount(), tt.cursor)\n\t\t\t// Update cursor\n\t\t\tfor range tt.cursor {\n\t\t\t\tm.getFocusedFilePanel().ListDown()\n\t\t\t}\n\n\t\t\trequire.Equal(t, filepanel.BrowserMode, m.getFocusedFilePanel().PanelMode)\n\t\t\tif tt.selectMode {\n\t\t\t\tm.getFocusedFilePanel().ChangeFilePanelMode()\n\t\t\t\tm.getFocusedFilePanel().SetSelectedAll(tt.selectedElem)\n\t\t\t}\n\n\t\t\tp.SendKey(common.Hotkeys.CompressFile[0])\n\n\t\t\t// This is a bit of an indirect validation, but there aren't many ways.\n\t\t\t// We many add a process type later, and ensure that a process of\n\t\t\t// type compress was done\n\t\t\tensureOneProcessDone(t, m)\n\t\t\tzipFile := filepath.Join(tt.startDir, tt.expectedZipName)\n\t\t\trequire.FileExists(t, zipFile, \"Expected zip file does not exist after compression\")\n\n\t\t\tsetFilePanelSelectedItemByLocation(t, m.getFocusedFilePanel(), zipFile)\n\n\t\t\tselectedItemLocation := m.getFocusedFilePanel().GetFocusedItem().Location\n\t\t\tassert.Equal(t, zipFile, selectedItemLocation)\n\t\t\t// Ensure we are extracting the zip file, not a directory\n\t\t\tfileInfo, err := os.Stat(selectedItemLocation)\n\t\t\trequire.NoError(t, err, \"Failed to stat panel location before extraction\")\n\t\t\trequire.False(t, fileInfo.IsDir(),\n\t\t\t\t\"Panel location for extraction is a directory, expected a zip file: %s\", selectedItemLocation)\n\n\t\t\tp.SendKey(common.Hotkeys.ExtractFile[0])\n\t\t\t// File extraction is supposedly async. So function's return doesn't means its done.\n\t\t\textractedDir := filepath.Join(tt.startDir, tt.extractedDirName)\n\n\t\t\t// Setup cleanup to run even if test fails\n\t\t\tt.Cleanup(func() {\n\t\t\t\tcleanupWithRetry(t, extractedDir, \"extracted directory\")\n\t\t\t\tcleanupWithRetry(t, zipFile, \"zip file\")\n\t\t\t})\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\tfor _, f := range tt.expectedFilesAfterExtract {\n\t\t\t\t\t_, err := os.Stat(filepath.Join(extractedDir, f))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}, DefaultTestTimeout, DefaultTestTick, \"Extraction of files failed Required - [%s]+%v\",\n\t\t\t\textractedDir, tt.expectedFilesAfterExtract)\n\t\t})\n\t}\n\n\tt.Run(\"Compress on Empty panel\", func(t *testing.T) {\n\t\tNewTestTeaProgWithEventLoop(t, defaultTestModel(dir2)).\n\t\t\tSendKey(common.Hotkeys.CompressFile[0])\n\t\t// Should not crash. Nothing should happen. If there is a crash, it will be caught\n\t\tentries, err := os.ReadDir(dir2)\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, entries)\n\t})\n}\n\nfunc TestPasteItem(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\tsourceDir := filepath.Join(curTestDir, \"source\")\n\tdestDir := filepath.Join(curTestDir, \"dest\")\n\tsubDir := filepath.Join(sourceDir, \"subdir\")\n\tfile1 := filepath.Join(sourceDir, \"file1.txt\")\n\tfile2 := filepath.Join(sourceDir, \"file2.txt\")\n\tdirFile1 := filepath.Join(subDir, \"dirfile1.txt\")\n\n\tutils.SetupDirectories(t, curTestDir, sourceDir, destDir, subDir)\n\tutils.SetupFiles(t, file1, file2, dirFile1)\n\n\ttestdata := []struct {\n\t\tname                 string\n\t\tstartDir             string\n\t\ttargetDir            string\n\t\titemName             string\n\t\tisCut                bool\n\t\tselectMode           bool\n\t\tselectedItems        []string\n\t\tshouldClipboardClear bool\n\t\tshouldOriginalExist  bool\n\t\texpectedDestFiles    []string\n\t\tshouldPreventPaste   bool\n\t\tdescription          string\n\t}{\n\t\t{\n\t\t\tname:                 \"Copy Single File\",\n\t\t\tstartDir:             sourceDir,\n\t\t\ttargetDir:            destDir,\n\t\t\titemName:             \"file1.txt\",\n\t\t\tisCut:                false,\n\t\t\tselectMode:           false,\n\t\t\tselectedItems:        nil,\n\t\t\tshouldClipboardClear: false,\n\t\t\tshouldOriginalExist:  true,\n\t\t\texpectedDestFiles:    []string{\"file1.txt\"},\n\t\t\tshouldPreventPaste:   false,\n\t\t\tdescription:          \"Copy a single file from source to destination\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Cut Single File\",\n\t\t\tstartDir:             sourceDir,\n\t\t\ttargetDir:            destDir,\n\t\t\titemName:             \"file2.txt\",\n\t\t\tisCut:                true,\n\t\t\tselectMode:           false,\n\t\t\tselectedItems:        nil,\n\t\t\tshouldClipboardClear: true,\n\t\t\tshouldOriginalExist:  false,\n\t\t\texpectedDestFiles:    []string{\"file2.txt\"},\n\t\t\tshouldPreventPaste:   false,\n\t\t\tdescription:          \"Cut a single file from source to destination\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"Cut Directory into Same Location\",\n\t\t\tstartDir:             sourceDir,\n\t\t\ttargetDir:            sourceDir, // Same directory\n\t\t\titemName:             \"subdir\",\n\t\t\tisCut:                true,\n\t\t\tselectMode:           false,\n\t\t\tselectedItems:        nil,\n\t\t\tshouldClipboardClear: false,      // Should not clear because paste should be prevented\n\t\t\tshouldOriginalExist:  true,       // Should still exist because paste prevented\n\t\t\texpectedDestFiles:    []string{}, // No files should be created in dest\n\t\t\tshouldPreventPaste:   true,\n\t\t\tdescription:          \"Cutting directory into same location should be prevented\",\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := setupModelAndPerformOperation(t, tt.startDir, tt.selectMode, tt.itemName, tt.selectedItems, tt.isCut)\n\t\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\t\t\t// Navigate to target directory\n\t\t\tnavigateToTargetDir(t, m, tt.startDir, tt.targetDir)\n\n\t\t\t// Get original file path for existence check\n\t\t\toriginalPath := getOriginalPath(tt.selectMode, tt.itemName, tt.startDir)\n\n\t\t\t// Perform paste operation\n\t\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\n\t\t\t// Verify results based on whether paste should be prevented\n\t\t\tif tt.shouldPreventPaste {\n\t\t\t\tverifyPreventedPasteResults(t, m, originalPath)\n\t\t\t} else {\n\t\t\t\tverifySuccessfulPasteResults(\n\t\t\t\t\tt,\n\t\t\t\t\ttt.targetDir,\n\t\t\t\t\ttt.expectedDestFiles,\n\t\t\t\t\toriginalPath,\n\t\t\t\t\ttt.shouldOriginalExist,\n\t\t\t\t)\n\t\t\t}\n\t\t\t// Checking separately, as this is something independent of tt.shouldPreventPaste\n\t\t\tif tt.shouldClipboardClear {\n\t\t\t\t// Wait for the Paste Model message to reach to Model via bubble tea event loop, that will clear clipboard\n\t\t\t\t// TODO: There are two eventually here in this test. A bit inefficient.\n\t\t\t\t// We have different async activities, create of files, completion of process message reaching processbar, model msg reaching model. Ideally we would like to have one final check, that is process completion message reaching model, and then check all conditions serially one be one.\n\t\t\t\t// For that there, might be need to implement ioReqDone count in model message, or a count per process type, or even an full fledged async manager.\n\t\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\t\treturn len(p.m.clipboard.GetItems()) == 0\n\t\t\t\t}, DefaultTestTimeout, DefaultTestTick, \"Clipboard should be cleared after successful cut-paste\")\n\t\t\t} else {\n\t\t\t\tassert.NotEmpty(t, p.m.clipboard.GetItems(), \"Clipboard should remain after copy-paste\")\n\t\t\t}\n\t\t})\n\t}\n\n\t// Special test cases that don't fit the table-driven pattern\n\tt.Run(\"Paste with Empty Clipboard\", func(t *testing.T) {\n\t\temptyTestDir := filepath.Join(curTestDir, \"empty_test\")\n\t\tutils.SetupDirectories(t, emptyTestDir)\n\t\tm := defaultTestModel(emptyTestDir)\n\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\n\t\t// Ensure clipboard is empty\n\t\tm.clipboard.Reset(false)\n\n\t\t// Get initial count\n\t\tentriesBefore, err := os.ReadDir(emptyTestDir)\n\t\trequire.NoError(t, err)\n\n\t\t// Attempt to paste (should do nothing)\n\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\n\t\t// Should not crash and no new files should be created\n\t\tentriesAfter, err := os.ReadDir(emptyTestDir)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Len(t, entriesAfter, len(entriesBefore),\n\t\t\t\"No new files should be created when pasting with empty clipboard\")\n\t})\n\n\tt.Run(\"Multiple Items Copy and Paste\", func(t *testing.T) {\n\t\t// Create fresh files for this test\n\t\tmultiFile1 := filepath.Join(sourceDir, \"multi1.txt\")\n\t\tmultiFile2 := filepath.Join(sourceDir, \"multi2.txt\")\n\t\tutils.SetupFiles(t, multiFile1, multiFile2)\n\n\t\tselectedItems := []string{multiFile1, multiFile2}\n\t\tm := setupModelAndPerformOperation(t, sourceDir, true, \"\", selectedItems, false)\n\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\n\t\t// Navigate to destination\n\t\tnavigateToTargetDir(t, m, sourceDir, destDir)\n\n\t\t// Paste items\n\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\n\t\t// Verify both files were copied\n\t\texpectedDestFiles := []string{\"multi1.txt\", \"multi2.txt\"}\n\t\tverifyDestinationFiles(t, destDir, expectedDestFiles)\n\t})\n\n\tt.Run(\"Cut into Subdirectory Prevention\", func(t *testing.T) {\n\t\t// Create a separate subdirectory for this test to avoid conflicts with table-driven tests\n\t\ttestSubDir := filepath.Join(sourceDir, \"testsubdir\")\n\t\ttestDirFile := filepath.Join(testSubDir, \"testdirfile.txt\")\n\t\tutils.SetupDirectories(t, testSubDir)\n\t\tutils.SetupFiles(t, testDirFile)\n\n\t\t// Test the logic that prevents cutting a directory into its subdirectory\n\t\tm := setupModelAndPerformOperation(t, sourceDir, false, \"testsubdir\", nil, true)\n\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\n\t\t// Navigate into the subdirectory and try to paste there (should be prevented)\n\t\tnavigateToTargetDir(t, m, sourceDir, testSubDir)\n\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\n\t\t// Directory should still exist in original location after prevention\n\t\tassert.DirExists(t, testSubDir, \"Directory should still exist after failed paste into subdirectory\")\n\t})\n\n\tt.Run(\"Duplicate File Handling\", func(t *testing.T) {\n\t\t// Create a file to copy\n\t\tdupFile := filepath.Join(sourceDir, \"duplicate.txt\")\n\t\tutils.SetupFiles(t, dupFile)\n\n\t\tm := setupModelAndPerformOperation(t, sourceDir, false, \"duplicate.txt\", nil, false)\n\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\t\t// Navigate to destination and paste\n\t\tnavigateToTargetDir(t, m, sourceDir, destDir)\n\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\n\t\t// Verify first copy\n\t\tverifyDestinationFiles(t, destDir, []string{\"duplicate.txt\"})\n\n\t\t// Paste again to test duplicate handling\n\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\n\t\t// Verify duplicate file with different name\n\t\tverifyDestinationFiles(t, destDir, []string{\"duplicate(1).txt\"})\n\t})\n}\n\n// ------  Very specific utilities that are required for this test case file only\n\n// Helper function to setup model and perform copy/cut operation\nfunc setupModelAndPerformOperation(t *testing.T, startDir string, useSelectMode bool,\n\titemName string, selectedItems []string, isCut bool) *model {\n\tt.Helper()\n\tm := defaultTestModel(startDir)\n\tTeaUpdate(m, nil)\n\n\tsetupPanelModeAndSelection(t, m, useSelectMode, itemName, selectedItems)\n\tperformCopyOrCutOperation(t, m, isCut)\n\n\tselectedItemsCount := len(selectedItems)\n\tif !useSelectMode {\n\t\tselectedItemsCount = 1\n\t}\n\tverifyClipboardState(t, m, isCut, useSelectMode, selectedItemsCount)\n\n\treturn m\n}\n\nfunc ensureOneProcessDone(t *testing.T, m *model) {\n\t// Don't attempt to print\n\t// m.processBarModel.GetProcessesSlice() in the failure message\n\t// This will print values calculated at the beginning of the call\n\trequire.Eventually(t, func() bool {\n\t\tprocesses := m.processBarModel.GetProcessesSlice()\n\t\treturn len(processes) == 1 && processes[0].State == processbar.Successful\n\t}, DefaultTestTimeout, DefaultTestTick, \"Compress process not done\")\n}\n\n// Duct tape to have less flaky tests in windows\nfunc cleanupWithRetry(t *testing.T, path, label string) {\n\tvar lastErr error\n\tok := assert.Eventually(t, func() bool {\n\t\tlastErr = os.RemoveAll(path)\n\t\treturn lastErr == nil\n\t}, DefaultTestTimeout, DefaultTestTick)\n\tif !ok {\n\t\tt.Fatalf(\"Failed to remove %s %q: %v\", label, path, lastErr)\n\t}\n}\n"
  },
  {
    "path": "src/internal/handle_file_operations.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/notify\"\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n\n\t\"github.com/atotto/clipboard\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n)\n\n// Create a file in the currently focus file panel\n// TODO: Fix it. It doesn't creates a new file. It just opens a file model,\n// that allows you to create a file. Actual creation happens here - createItem() in handle_modal.go\nfunc (m *model) panelCreateNewFile() {\n\tpanel := m.getFocusedFilePanel()\n\n\tm.typingModal.location = panel.Location\n\tm.typingModal.open = true\n\tm.typingModal.textInput = common.GenerateNewFileTextInput()\n\tm.firstTextInput = true\n}\n\n// TODO : This function does not needs the entire model. Only pass the panel object\nfunc (m *model) IsRenamingConflicting() bool {\n\t// TODO : Replace this with m.getCurrentFilePanel() everywhere\n\tpanel := m.getFocusedFilePanel()\n\tif panel.ElemCount() == 0 {\n\t\tslog.Error(\"IsRenamingConflicting() being called on empty panel\")\n\t\treturn false\n\t}\n\toldPath := panel.GetFocusedItem().Location\n\tnewPath := filepath.Join(panel.Location, panel.Rename.Value())\n\n\tif oldPath == newPath {\n\t\treturn false\n\t}\n\n\t_, err := os.Stat(newPath)\n\treturn err == nil\n}\n\n// TODO: Remove channel messaging and use tea.Cmd\nfunc (m *model) warnModalForRenaming() tea.Cmd {\n\treqID := m.ioReqCnt\n\tm.ioReqCnt++\n\tslog.Debug(\"Submitting rename notify model request\", \"reqID\", reqID)\n\tres := func() tea.Msg {\n\t\tnotifyModel := notify.New(true,\n\t\t\tcommon.SameRenameWarnTitle,\n\t\t\tcommon.SameRenameWarnContent,\n\t\t\tnotify.RenameAction)\n\t\treturn NewNotifyModalMsg(notifyModel, reqID)\n\t}\n\treturn res\n}\n\n// Rename file where the cusror is located\n// TODO: Fix this. It doesn't do any rename, just opens the rename text input\n// Actual rename happens at confirmRename() in handle_modal.go\nfunc (m *model) panelItemRename() {\n\tpanel := m.getFocusedFilePanel()\n\tif panel.Empty() {\n\t\treturn\n\t}\n\n\tcursorPos := -1\n\tnameRunes := []rune(panel.GetFocusedItem().Name)\n\tnameLen := len(nameRunes)\n\tfor i := nameLen - 1; i >= 0; i-- {\n\t\tif nameRunes[i] == '.' {\n\t\t\tcursorPos = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif cursorPos == -1 || cursorPos == 0 && nameLen > 0 || panel.GetFocusedItem().Directory {\n\t\tcursorPos = nameLen\n\t}\n\n\tm.fileModel.Renaming = true\n\tpanel.Renaming = true\n\tm.firstTextInput = true\n\t// TODO: Don't re-create a new model on each rename. Don't create\n\t// unnecessary gargage for collection. Reuse the existing model.\n\t// Maintain its state, dimensions. Update its cursor and text when needed\n\tpanel.Rename = common.GenerateRenameTextInput(\n\t\tm.fileModel.SinglePanelWidth-common.InnerPadding,\n\t\tcursorPos,\n\t\tpanel.GetFocusedItem().Name)\n}\n\nfunc (m *model) getDeleteCmd(permDelete bool) tea.Cmd {\n\tpanel := m.getFocusedFilePanel()\n\tif panel.Empty() {\n\t\treturn nil\n\t}\n\n\tvar items []string\n\tif panel.PanelMode == filepanel.SelectMode {\n\t\titems = panel.GetSelectedLocations()\n\t} else {\n\t\titems = []string{panel.GetFocusedItem().Location}\n\t}\n\n\tuseTrash := m.hasTrash && !isExternalDiskPath(panel.Location) && !permDelete\n\n\treqID := m.ioReqCnt\n\tm.ioReqCnt++\n\tslog.Debug(\"Submitting delete request\", \"id\", reqID, \"items cnt\", len(items))\n\treturn func() tea.Msg {\n\t\tstate := deleteOperation(&m.processBarModel, items, useTrash)\n\t\treturn NewDeleteOperationMsg(state, reqID)\n\t}\n}\n\nfunc deleteOperation(processBarModel *processbar.Model, items []string, useTrash bool) processbar.ProcessState {\n\tif len(items) == 0 {\n\t\treturn processbar.Cancelled\n\t}\n\tp, err := processBarModel.SendAddProcessMsg(filepath.Base(items[0]), processbar.OpDelete, len(items), true)\n\tif err != nil {\n\t\tslog.Error(\"Cannot spawn a new process\", \"error\", err)\n\t\treturn processbar.Failed\n\t}\n\n\tdeleteFunc := os.RemoveAll\n\tif useTrash {\n\t\tdeleteFunc = moveToTrash\n\t}\n\tfor _, item := range items {\n\t\terr = deleteFunc(item)\n\t\tif err != nil {\n\t\t\tp.State = processbar.Failed\n\t\t\tslog.Error(\"Error in delete operation\", \"item\", item, \"useTrash\", useTrash, \"error\", err)\n\t\t\tbreak\n\t\t}\n\t\tp.CurrentFile = filepath.Base(item)\n\t\tp.Done++\n\t\tprocessBarModel.TrySendingUpdateProcessMsg(p)\n\t}\n\n\tif p.State != processbar.Failed {\n\t\tp.State = processbar.Successful\n\t}\n\tp.DoneTime = time.Now()\n\terr = processBarModel.SendUpdateProcessMsg(p, true)\n\tif err != nil {\n\t\tslog.Error(\"Failed to send final delete operation update\", \"error\", err)\n\t}\n\treturn p.State\n}\n\nfunc (m *model) getDeleteTriggerCmd(deletePermanent bool) tea.Cmd {\n\tpanel := m.getFocusedFilePanel()\n\tif (panel.PanelMode == filepanel.SelectMode && panel.SelectedCount() == 0) ||\n\t\t(panel.PanelMode == filepanel.BrowserMode && panel.Empty()) {\n\t\treturn nil\n\t}\n\n\treqID := m.ioReqCnt\n\tm.ioReqCnt++\n\n\treturn func() tea.Msg {\n\t\ttitle := common.TrashWarnTitle\n\t\tcontent := common.TrashWarnContent\n\t\taction := notify.DeleteAction\n\n\t\tif !m.hasTrash || isExternalDiskPath(panel.Location) || deletePermanent {\n\t\t\ttitle = common.PermanentDeleteWarnTitle\n\t\t\tcontent = common.PermanentDeleteWarnContent\n\t\t\taction = notify.PermanentDeleteAction\n\t\t}\n\t\treturn NewNotifyModalMsg(notify.New(true, title, content, action), reqID)\n\t}\n}\n\n// Copy directory or file's path to superfile's clipboard\n// set cut to true/false accordingly\nfunc (m *model) copySingleItem(cut bool) {\n\tpanel := m.getFocusedFilePanel()\n\tm.clipboard.Reset(cut)\n\tif panel.Empty() {\n\t\treturn\n\t}\n\tslog.Debug(\"handle_file_operations.copySingleItem\", \"cut\", cut,\n\t\t\"panel location\", panel.GetFocusedItem().Location)\n\tm.clipboard.Add(panel.GetFocusedItem().Location)\n}\n\n// Copy all selected file or directory's paths to the clipboard\nfunc (m *model) copyMultipleItem(cut bool) {\n\tpanel := m.getFocusedFilePanel()\n\tm.clipboard.Reset(cut)\n\tif panel.SelectedCount() == 0 {\n\t\treturn\n\t}\n\tslog.Debug(\"handle_file_operations.copyMultipleItem\", \"cut\", cut,\n\t\t\"panel selected files\", panel.GetSelectedLocations())\n\tm.clipboard.SetItems(panel.GetSelectedLocations())\n}\n\nfunc (m *model) getPasteItemCmd() tea.Cmd {\n\tcopyItems := m.clipboard.PruneInaccessibleItemsAndGet()\n\tcut := m.clipboard.IsCut()\n\tif len(copyItems) == 0 {\n\t\treturn nil\n\t}\n\n\t// TODO: Do it via m.getNewReqID()\n\t// TODO: Have an IO Req Management, collecting info about pending IO Req too\n\treqID := m.ioReqCnt\n\tm.ioReqCnt++\n\tpanelLocation := m.getFocusedFilePanel().Location\n\n\tslog.Debug(\"Submitting pasteItems request\", \"id\", reqID, \"items cnt\", len(copyItems), \"dest\", panelLocation)\n\treturn func() tea.Msg {\n\t\terr := validatePasteOperation(panelLocation, copyItems, cut)\n\t\tif err != nil {\n\t\t\treturn NewNotifyModalMsg(notify.New(true, \"Invalid paste location\", err.Error(), notify.NoAction),\n\t\t\t\treqID)\n\t\t}\n\t\tstate := executePasteOperation(&m.processBarModel, panelLocation, copyItems, cut)\n\t\treturn NewPasteOperationMsg(state, reqID)\n\t}\n}\n\nfunc validatePasteOperation(panelLocation string, copyItems []string, cut bool) error {\n\t// Check if trying to paste into source or subdirectory for both cut and copy operations\n\tfor _, srcPath := range copyItems {\n\t\t// Check if trying to cut and paste into the same directory - this would be a no-op\n\t\t// and could potentially cause issues, so we prevent it\n\t\tif filepath.Dir(srcPath) == panelLocation && cut {\n\t\t\treturn fmt.Errorf(\"cannot paste into parent directory of source, srcPath : %v, panelLocation : %v\",\n\t\t\t\tsrcPath, panelLocation)\n\t\t}\n\t\tif cut && srcPath == panelLocation {\n\t\t\treturn errors.New(\"cannot paste a directory into itself\")\n\t\t}\n\n\t\tif isAncestor(srcPath, panelLocation) {\n\t\t\treturn fmt.Errorf(\"cannot %s and paste a directory into itself or its subdirectory\",\n\t\t\t\tgetCopyOrCutOperationName(cut))\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// new func to check and return an error that will go in m.content\n// create a new error type\n\n// Paste all clipboard items\nfunc executePasteOperation(processBarModel *processbar.Model,\n\tpanelLocation string, copyItems []string, cut bool,\n) processbar.ProcessState {\n\tslog.Debug(\"executePasteOperation\", \"items\", copyItems, \"cut\", cut, \"panel location\", panelLocation)\n\n\tvar operation processbar.OperationType\n\tif cut {\n\t\toperation = processbar.OpCut\n\t} else {\n\t\toperation = processbar.OpCopy\n\t}\n\n\tp, err := processBarModel.SendAddProcessMsg(\n\t\tfilepath.Base(copyItems[0]),\n\t\toperation,\n\t\tgetTotalFilesCnt(copyItems), true)\n\tif err != nil {\n\t\tslog.Error(\"Cannot spawn a new process\", \"error\", err)\n\t\treturn processbar.Failed\n\t}\n\n\tfor _, filePath := range copyItems {\n\t\terrMessage := \"cut item error\"\n\t\tif cut && !isExternalDiskPath(filePath) {\n\t\t\terr = moveElement(filePath, filepath.Join(panelLocation, filepath.Base(filePath)))\n\t\t} else {\n\t\t\t// TODO : These error cases are hard to test. We have to somehow make the paste operations fail,\n\t\t\t// which is time consuming and manual. We should test these with automated testcases\n\t\t\terr = pasteDir(filePath, filepath.Join(panelLocation, filepath.Base(filePath)), &p, cut, processBarModel)\n\t\t\tif err != nil {\n\t\t\t\terrMessage = \"paste item error\"\n\t\t\t}\n\t\t}\n\n\t\tp.CurrentFile = filepath.Base(filePath)\n\t\tif err != nil {\n\t\t\tslog.Debug(\"model.pasteItem - paste failure\", \"error\", err,\n\t\t\t\t\"current item\", filePath, \"errMessage\", errMessage)\n\t\t\tp.State = processbar.Failed\n\t\t\tslog.Error(errMessage, \"error\", err)\n\t\t\tbreak\n\t\t}\n\t\tprocessBarModel.TrySendingUpdateProcessMsg(p)\n\t}\n\n\tif p.State != processbar.Failed {\n\t\tp.State = processbar.Successful\n\t\tp.Done = p.Total\n\t}\n\tp.DoneTime = time.Now()\n\terr = processBarModel.SendUpdateProcessMsg(p, true)\n\tif err != nil {\n\t\tslog.Error(\"Could not send final update for process Bar\", \"error\", err)\n\t}\n\n\treturn p.State\n}\n\nfunc getTotalFilesCnt(copyItems []string) int {\n\ttotalFiles := 0\n\tfor _, folderPath := range copyItems {\n\t\t// TODO : Fix this. This is inefficient\n\t\t// In case of a cut operations for a directory with a lot of files\n\t\t// we are unnecessarily walking the whole directory recursively\n\t\t// while os will just perform a rename\n\t\t// So instead of few operations this will cause the cut paste\n\t\t// to read the whole directory recursively\n\t\t// we should avoid doing this.\n\t\t// Although this allows us a more detailed progress tracking\n\t\t// this make the copy/cut more inefficient\n\t\t// instead, we could just track progress based on total items in\n\t\t// copyItems\n\t\t// efficiency should be prioritized over more detailed feedback.\n\t\tcount, err := countFiles(folderPath)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error in countFiles\", \"error\", err)\n\t\t\tcontinue\n\t\t}\n\t\ttotalFiles += count\n\t}\n\treturn totalFiles\n}\n\n// Extract compressed file\n// TODO : err should be returned and properly handled by the caller\nfunc (m *model) getExtractFileCmd() tea.Cmd {\n\tpanel := m.getFocusedFilePanel()\n\tif panel.Empty() {\n\t\treturn nil\n\t}\n\n\titem := panel.GetFocusedItem().Location\n\n\text := strings.ToLower(filepath.Ext(item))\n\tif !common.IsExtensionExtractable(ext) {\n\t\tslog.Error(\"Error unexpected file\", \"extension type\", ext, \"item\", item, \"error\", errors.ErrUnsupported)\n\t\treturn nil\n\t}\n\treqID := m.ioReqCnt\n\tm.ioReqCnt++\n\n\tslog.Debug(\"Submitting Extract file request\", \"reqID\", reqID, \"item\", item)\n\n\treturn func() tea.Msg {\n\t\toutputDir := common.FileNameWithoutExtension(item)\n\t\toutputDir, err := renameIfDuplicate(outputDir)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while renaming for duplicates\", \"error\", err)\n\t\t\treturn NewExtractOperationMsg(processbar.Failed, reqID)\n\t\t}\n\n\t\terr = os.MkdirAll(\n\t\t\toutputDir,\n\t\t\tutils.ExtractedDirMode,\n\t\t)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while making directory for extracting files\", \"error\", err)\n\t\t\treturn NewExtractOperationMsg(processbar.Failed, reqID)\n\t\t}\n\t\terr = extractCompressFile(item, outputDir, &m.processBarModel)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error extract file\", \"error\", err)\n\t\t\treturn NewExtractOperationMsg(processbar.Failed, reqID)\n\t\t}\n\t\treturn NewExtractOperationMsg(processbar.Successful, reqID)\n\t}\n}\n\nfunc (m *model) getCompressSelectedFilesCmd() tea.Cmd {\n\tpanel := m.getFocusedFilePanel()\n\n\tif panel.Empty() {\n\t\treturn nil\n\t}\n\tvar filesToCompress []string\n\tvar firstFile string\n\n\tif panel.SelectedCount() == 0 {\n\t\tfirstFile = panel.GetFocusedItem().Location\n\t\tfilesToCompress = append(filesToCompress, firstFile)\n\t} else {\n\t\tfirstFile = panel.GetFirstSelectedLocation()\n\t\tfilesToCompress = panel.GetSelectedLocations()\n\t}\n\n\treqID := m.ioReqCnt\n\tm.ioReqCnt++\n\n\treturn func() tea.Msg {\n\t\tzipName, err := getZipArchiveName(filepath.Base(firstFile))\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error in getZipArchiveName\", \"error\", err)\n\t\t\treturn NewCompressOperationMsg(processbar.Failed, reqID)\n\t\t}\n\t\tzipPath := filepath.Join(panel.Location, zipName)\n\t\tif err := zipSources(filesToCompress, zipPath, &m.processBarModel); err != nil {\n\t\t\tslog.Error(\"Error in zipping files\", \"error\", err)\n\t\t\treturn NewCompressOperationMsg(processbar.Failed, reqID)\n\t\t}\n\t\treturn NewCompressOperationMsg(processbar.Successful, reqID)\n\t}\n}\n\nfunc (m *model) chooserFileWriteAndQuit(path string) error {\n\t// Attempt to write to the file\n\terr := os.WriteFile(variable.ChooserFile, []byte(path), utils.ConfigFilePerm)\n\tif err != nil {\n\t\treturn err\n\t}\n\tm.modelQuitState = quitInitiated\n\treturn nil\n}\n\n// Open file with default editor\nfunc (m *model) openFileWithEditor() tea.Cmd {\n\tpanel := m.getFocusedFilePanel()\n\t// Check if panel is empty\n\tif panel.Empty() {\n\t\treturn nil\n\t}\n\n\tif variable.ChooserFile != \"\" {\n\t\terr := m.chooserFileWriteAndQuit(panel.GetFocusedItem().Location)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\t// Continue with preview if file is not writable\n\t\tslog.Error(\"Error while writing to chooser file, continuing with open via file editor\", \"error\", err)\n\t}\n\n\teditor := common.Config.Editor\n\tif editor == \"\" {\n\t\teditor = os.Getenv(\"EDITOR\")\n\t}\n\n\t// Make sure there is an editor\n\tif editor == \"\" {\n\t\tif runtime.GOOS == utils.OsWindows {\n\t\t\teditor = \"notepad\"\n\t\t} else {\n\t\t\teditor = \"nano\"\n\t\t}\n\t}\n\n\t// Split the editor command into command and arguments\n\tparts := strings.Fields(editor)\n\tcmd := parts[0]\n\n\t//nolint:gocritic // appendAssign: intentionally creating a new slice\n\targs := append(parts[1:], panel.GetFocusedItem().Location)\n\n\tc := exec.Command(cmd, args...)\n\n\treturn tea.ExecProcess(c, func(err error) tea.Msg {\n\t\treturn editorFinishedMsg{err}\n\t})\n}\n\n// Open directory with default editor\nfunc (m *model) openDirectoryWithEditor() tea.Cmd {\n\tif variable.ChooserFile != \"\" {\n\t\terr := m.chooserFileWriteAndQuit(m.getFocusedFilePanel().Location)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\t// Continue with preview if file is not writable\n\t\tslog.Error(\"Error while writing to chooser file, continuing with open via directory editor\", \"error\", err)\n\t}\n\n\teditor := common.Config.DirEditor\n\n\tif editor == \"\" {\n\t\tswitch runtime.GOOS {\n\t\tcase utils.OsWindows:\n\t\t\teditor = \"explorer\"\n\t\tcase utils.OsDarwin:\n\t\t\teditor = \"open\"\n\t\tdefault:\n\t\t\teditor = \"vi\"\n\t\t}\n\t}\n\n\t// Split the editor command into command and arguments\n\tparts := strings.Fields(editor)\n\tcmd := parts[0]\n\t//nolint:gocritic // appendAssign: intentionally creating a new slice\n\targs := append(parts[1:], m.getFocusedFilePanel().Location)\n\n\tc := exec.Command(cmd, args...)\n\treturn tea.ExecProcess(c, func(err error) tea.Msg {\n\t\treturn editorFinishedMsg{err}\n\t})\n}\n\n// Copy file path\n// TODO: This is also an IO operations, do it via tea.Cmd\nfunc (m *model) copyPath() {\n\tpanel := m.getFocusedFilePanel()\n\n\tif panel.Empty() {\n\t\treturn\n\t}\n\n\tif err := clipboard.WriteAll(panel.GetFocusedItem().Location); err != nil {\n\t\tslog.Error(\"Error while copy path\", \"error\", err)\n\t}\n}\n\n// TODO: This is also an IO operations, do it via tea.Cmd\nfunc (m *model) copyPWD() {\n\tpanel := m.getFocusedFilePanel()\n\tif err := clipboard.WriteAll(panel.Location); err != nil {\n\t\tslog.Error(\"Error while copy present working directory\", \"error\", err)\n\t}\n}\n"
  },
  {
    "path": "src/internal/handle_modal.go",
    "content": "package internal\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\n// Cancel typing modal e.g. create file or directory\nfunc (m *model) cancelTypingModal() {\n\tm.typingModal.textInput.Blur()\n\tm.typingModal.open = false\n}\n\n// Confirm to create file or directory\nfunc (m *model) createItem() {\n\tif err := checkFileNameValidity(m.typingModal.textInput.Value()); err != nil {\n\t\tm.typingModal.errorMesssage = err.Error()\n\t\tslog.Error(\"Errow while createItem during item creation\", \"error\", err)\n\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tm.typingModal.errorMesssage = \"\"\n\t\tm.typingModal.open = false\n\t\tm.typingModal.textInput.Blur()\n\t}()\n\n\tpath := filepath.Join(m.typingModal.location, m.typingModal.textInput.Value())\n\tif !strings.HasSuffix(m.typingModal.textInput.Value(), string(filepath.Separator)) {\n\t\tpath, _ = renameIfDuplicate(path)\n\t\tif err := os.MkdirAll(filepath.Dir(path), utils.UserDirPerm); err != nil {\n\t\t\tslog.Error(\"Error while createItem during directory creation\", \"error\", err)\n\t\t\treturn\n\t\t}\n\t\tf, err := os.Create(path)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while createItem during file creation\", \"error\", err)\n\t\t\treturn\n\t\t}\n\t\tdefer f.Close()\n\t} else {\n\t\terr := os.MkdirAll(path, utils.UserDirPerm)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while createItem during directory creation\", \"error\", err)\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Cancel rename file or directory\nfunc (m *model) cancelRename() {\n\tpanel := m.getFocusedFilePanel()\n\tpanel.Rename.Blur()\n\tpanel.Renaming = false\n\tm.fileModel.Renaming = false\n}\n\n// Connfirm rename file or directory\nfunc (m *model) confirmRename() {\n\tpanel := m.getFocusedFilePanel()\n\n\t// Although we dont expect this to happen based on our current flow\n\t// Just adding it here to be safe\n\tif panel.Empty() {\n\t\tslog.Error(\"confirmRename called on empty panel\")\n\t\treturn\n\t}\n\n\toldPath := panel.GetFocusedItem().Location\n\tnewPath := filepath.Join(panel.Location, panel.Rename.Value())\n\n\t// Rename the file\n\terr := os.Rename(oldPath, newPath)\n\tif err != nil {\n\t\tslog.Error(\"Error while confirmRename during rename\", \"error\", err)\n\t\t// Dont return. We have to also reset the panel and model information\n\t}\n\tm.fileModel.Renaming = false\n\tpanel.Rename.Blur()\n\tpanel.Renaming = false\n}\n\nfunc (m *model) confirmSortOptions() {\n\tpanel := m.getFocusedFilePanel()\n\tpanel.SortKind = m.sortModal.GetSelectedKind()\n\tm.sortModal.Close()\n}\n\n// Cancel search, this will clear all searchbar input\nfunc (m *model) cancelSearch() {\n\tpanel := m.getFocusedFilePanel()\n\tpanel.SearchBar.Blur()\n\tpanel.SearchBar.SetValue(\"\")\n}\n\n// Confirm search. This will exit the search bar and filter the files\nfunc (m *model) confirmSearch() {\n\tpanel := m.getFocusedFilePanel()\n\tpanel.SearchBar.Blur()\n}\n\nfunc (m *model) getFocusedFilePanel() *filepanel.Model {\n\treturn m.fileModel.GetFocusedFilePanel()\n}\n"
  },
  {
    "path": "src/internal/handle_panel_movement.go",
    "content": "package internal\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// Back to parent directory\nfunc (m *model) parentDirectory() {\n\terr := m.getFocusedFilePanel().ParentDirectory()\n\tif err != nil {\n\t\tslog.Error(\"Error while changing to parent directory\", \"error\", err)\n\t}\n}\n\n// Enter directory or open file with default application\n// TODO: Unit test this\nfunc (m *model) enterPanel() {\n\tpanel := m.getFocusedFilePanel()\n\n\tif panel.Empty() {\n\t\treturn\n\t}\n\tselectedItem := panel.GetFocusedItem()\n\tif selectedItem.Directory {\n\t\ttargetPath := selectedItem.Location\n\n\t\tif selectedItem.Info.Mode()&os.ModeSymlink != 0 {\n\t\t\tvar symlinkErr error\n\t\t\ttargetPath, symlinkErr = filepath.EvalSymlinks(targetPath)\n\t\t\tif symlinkErr != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// targetPath shouldn't be a link now, so Stat and Lstat should be same\n\t\t\tif targetInfo, lstatErr := os.Lstat(targetPath); lstatErr != nil || !targetInfo.IsDir() {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// TODO : Propagate error out from this this function. Return here, instead of logging\n\t\terr := m.updateCurrentFilePanelDir(targetPath)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while changing to directory\", \"error\", err, \"target\", targetPath)\n\t\t}\n\t\treturn\n\t}\n\n\tif variable.ChooserFile != \"\" {\n\t\tchooserErr := m.chooserFileWriteAndQuit(panel.GetFocusedItem().Location)\n\t\tif chooserErr == nil {\n\t\t\treturn\n\t\t}\n\t\t// Continue with preview if file is not writable\n\t\tslog.Error(\"Error while writing to chooser file, continuing with file open\", \"error\", chooserErr)\n\t}\n\tm.executeOpenCommand()\n}\n\nfunc (m *model) executeOpenCommand() {\n\tpanel := m.getFocusedFilePanel()\n\n\tfilePath := panel.GetFocusedItem().Location\n\n\topenCommand := \"xdg-open\"\n\tswitch runtime.GOOS {\n\tcase utils.OsDarwin:\n\t\topenCommand = \"open\"\n\tcase utils.OsWindows:\n\t\tdllpath := filepath.Join(os.Getenv(\"SYSTEMROOT\"), \"System32\", \"rundll32.exe\")\n\t\tdllfile := \"url.dll,FileProtocolHandler\"\n\n\t\tcmd := exec.Command(dllpath, dllfile, filePath)\n\t\terr := cmd.Start()\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while open file with\", \"error\", err)\n\t\t}\n\n\t\treturn\n\t}\n\n\t// For now open_with works only for mac and linux\n\t// TODO: Make it in parity with windows.\n\text := strings.ToLower(strings.TrimPrefix(filepath.Ext(filePath), \".\"))\n\tif extEditor, ok := common.Config.OpenWith[ext]; ok {\n\t\topenCommand = extEditor\n\t}\n\n\tcmd := exec.Command(openCommand, filePath)\n\tutils.DetachFromTerminal(cmd)\n\terr := cmd.Start()\n\tif err != nil {\n\t\t// TODO: This kind of errors should go to user facing pop ups\n\t\tslog.Error(\"Error while open file with\", \"error\", err)\n\t}\n}\n\n// Switch to the directory where the sidebar cursor is located\nfunc (m *model) sidebarSelectDirectory() {\n\t// We can't do this when we have only divider directories\n\t// m.sidebarModel.directories[m.sidebarModel.cursor].location would point to a divider dir.\n\tif m.sidebarModel.NoActualDir() {\n\t\treturn\n\t}\n\t// TODO(Refactor): Move this to a function m.ResetFocus()\n\tm.focusPanel = nonePanelFocus\n\tpanel := m.getFocusedFilePanel()\n\n\terr := m.updateCurrentFilePanelDir(m.sidebarModel.GetCurrentDirectoryLocation())\n\tif err != nil {\n\t\tslog.Error(\"Error switching to sidebar directory\", \"error\", err)\n\t}\n\tpanel.IsFocused = true\n}\n\n// Toggle dotfile display or not\nfunc (m *model) toggleDotFileController() {\n\tm.fileModel.ToggleDotFile()\n\terr := utils.WriteBoolFile(variable.ToggleDotFile, m.fileModel.DisplayDotFiles)\n\tif err != nil {\n\t\tslog.Error(\"Error while updating toggleDotFile data\", \"error\", err)\n\t}\n}\n\n// Toggle dotfile display or not\nfunc (m *model) toggleFooterController() tea.Cmd {\n\tm.toggleFooter = !m.toggleFooter\n\terr := utils.WriteBoolFile(variable.ToggleFooter, m.toggleFooter)\n\tif err != nil {\n\t\tslog.Error(\"Error while updating toggleFooter data\", \"error\", err)\n\t}\n\tm.setHeightValues()\n\treturn m.updateComponentDimensions()\n}\n\n// Focus on search bar\nfunc (m *model) searchBarFocus() {\n\tpanel := m.getFocusedFilePanel()\n\tif panel.SearchBar.Focused() {\n\t\tpanel.SearchBar.Blur()\n\t} else {\n\t\tpanel.SearchBar.Focus()\n\t\tm.firstTextInput = true\n\t}\n\n\t// config search bar width\n\tpanel.SearchBar.Width = m.fileModel.SinglePanelWidth - common.InnerPadding\n}\n\nfunc (m *model) sidebarSearchBarFocus() {\n\tif m.sidebarModel.SearchBarFocused() {\n\t\t// Ideally Code should never reach here. Once sidebar is focused, we should\n\t\t// not cause sidebarSearchBarFocus() event by pressing search key\n\t\tslog.Error(\"sidebarSearchBarFocus() called on Focused sidebar\")\n\t\tm.sidebarModel.SearchBarBlur()\n\t\treturn\n\t}\n\tm.sidebarModel.SearchBarFocus()\n\tm.firstTextInput = true\n}\n"
  },
  {
    "path": "src/internal/handle_panel_navigation.go",
    "content": "package internal\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// Pinned directory\nfunc (m *model) pinnedDirectory() {\n\tpanel := m.getFocusedFilePanel()\n\terr := m.sidebarModel.TogglePinnedDirectory(panel.Location)\n\tif err != nil {\n\t\tslog.Error(\"Error while toggling pinned directory\", \"error\", err)\n\t}\n}\n\n// Focus on sidebar\nfunc (m *model) focusOnSideBar() {\n\tif common.Config.SidebarWidth == 0 {\n\t\treturn\n\t}\n\tif m.focusPanel == sidebarFocus {\n\t\tm.focusPanel = nonePanelFocus\n\t\tm.getFocusedFilePanel().IsFocused = true\n\t} else {\n\t\tm.focusPanel = sidebarFocus\n\t\tm.getFocusedFilePanel().IsFocused = false\n\t}\n}\n\n// Focus on processbar\nfunc (m *model) focusOnProcessBar() {\n\tif !m.toggleFooter {\n\t\treturn\n\t}\n\n\tif m.focusPanel == processBarFocus {\n\t\tm.focusPanel = nonePanelFocus\n\t\tm.getFocusedFilePanel().IsFocused = true\n\t} else {\n\t\tm.focusPanel = processBarFocus\n\t\tm.getFocusedFilePanel().IsFocused = false\n\t}\n}\n\n// focus on metadata\nfunc (m *model) focusOnMetadata() {\n\tif !m.toggleFooter {\n\t\treturn\n\t}\n\n\tif m.focusPanel == metadataFocus {\n\t\tm.focusPanel = nonePanelFocus\n\t\tm.getFocusedFilePanel().IsFocused = true\n\t} else {\n\t\tm.focusPanel = metadataFocus\n\t\tm.getFocusedFilePanel().IsFocused = false\n\t}\n}\n"
  },
  {
    "path": "src/internal/key_function.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"slices\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/filemodel\"\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/notify\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n)\n\n// mainKey handles most of key commands in the regular state of the application. For\n// keys that performs actions in multiple panels, like going up or down,\n// check the state of model m and handle properly.\n// TODO: This function has grown too big. It needs to be fixed, via major\n// updates and fixes in key handling code\nfunc (m *model) mainKey(msg string) tea.Cmd { //nolint: gocyclo,cyclop,funlen // See above\n\tswitch {\n\t// If move up Key is pressed, check the current state and executes\n\tcase slices.Contains(common.Hotkeys.ListUp, msg):\n\t\tswitch m.focusPanel {\n\t\tcase sidebarFocus:\n\t\t\tm.sidebarModel.ListUp()\n\t\tcase processBarFocus:\n\t\t\tm.processBarModel.ListUp()\n\t\tcase metadataFocus:\n\t\t\tm.fileMetaData.ListUp()\n\t\tcase nonePanelFocus:\n\t\t\tm.getFocusedFilePanel().ListUp()\n\t\t}\n\n\t\t// If move down Key is pressed, check the current state and executes\n\tcase slices.Contains(common.Hotkeys.ListDown, msg):\n\t\tswitch m.focusPanel {\n\t\tcase sidebarFocus:\n\t\t\tm.sidebarModel.ListDown()\n\t\tcase processBarFocus:\n\t\t\tm.processBarModel.ListDown()\n\t\tcase metadataFocus:\n\t\t\tm.fileMetaData.ListDown()\n\t\tcase nonePanelFocus:\n\t\t\tm.getFocusedFilePanel().ListDown()\n\t\t}\n\n\tcase slices.Contains(common.Hotkeys.PageUp, msg):\n\t\tm.getFocusedFilePanel().PgUp()\n\n\tcase slices.Contains(common.Hotkeys.PageDown, msg):\n\t\tm.getFocusedFilePanel().PgDown()\n\n\tcase slices.Contains(common.Hotkeys.ChangePanelMode, msg):\n\t\tm.getFocusedFilePanel().ChangeFilePanelMode()\n\n\tcase slices.Contains(common.Hotkeys.NextFilePanel, msg):\n\t\tif m.focusPanel == nonePanelFocus {\n\t\t\tm.fileModel.NextFilePanel()\n\t\t}\n\n\tcase slices.Contains(common.Hotkeys.PreviousFilePanel, msg):\n\t\tif m.focusPanel == nonePanelFocus {\n\t\t\tm.fileModel.PreviousFilePanel()\n\t\t}\n\n\tcase slices.Contains(common.Hotkeys.CloseFilePanel, msg):\n\t\tcmd, err := m.fileModel.CloseFilePanel()\n\t\tif err != nil && !errors.Is(err, filemodel.ErrMinimumPanelCount) {\n\t\t\tslog.Error(\"unexpected error while closing new panel\", \"error\", err)\n\t\t}\n\t\treturn cmd\n\tcase slices.Contains(common.Hotkeys.CreateNewFilePanel, msg):\n\t\tcmd, err := m.fileModel.CreateNewFilePanel(variable.HomeDir)\n\t\tif err != nil && !errors.Is(err, filemodel.ErrMaximumPanelCount) {\n\t\t\tslog.Error(\"unexpected error while creating new panel\", \"error\", err)\n\t\t}\n\t\treturn cmd\n\tcase slices.Contains(common.Hotkeys.SplitFilePanel, msg):\n\t\tcmd, err := m.splitPanel()\n\t\tif err != nil && !errors.Is(err, filemodel.ErrMaximumPanelCount) {\n\t\t\tslog.Error(\"unexpected error while splitting panel\", \"error\", err)\n\t\t}\n\t\treturn cmd\n\tcase slices.Contains(common.Hotkeys.ToggleFilePreviewPanel, msg):\n\t\treturn m.fileModel.ToggleFilePreviewPanel()\n\n\tcase slices.Contains(common.Hotkeys.FocusOnSidebar, msg):\n\t\tm.focusOnSideBar()\n\n\tcase slices.Contains(common.Hotkeys.FocusOnProcessBar, msg):\n\t\tm.focusOnProcessBar()\n\n\tcase slices.Contains(common.Hotkeys.FocusOnMetaData, msg):\n\t\tm.focusOnMetadata()\n\n\tcase slices.Contains(common.Hotkeys.PasteItems, msg):\n\t\treturn m.getPasteItemCmd()\n\n\tcase slices.Contains(common.Hotkeys.FilePanelItemCreate, msg):\n\t\tm.panelCreateNewFile()\n\tcase slices.Contains(common.Hotkeys.PinnedDirectory, msg):\n\t\tm.pinnedDirectory()\n\n\tcase slices.Contains(common.Hotkeys.ToggleDotFile, msg):\n\t\tm.toggleDotFileController()\n\n\tcase slices.Contains(common.Hotkeys.ToggleFooter, msg):\n\t\treturn m.toggleFooterController()\n\n\tcase slices.Contains(common.Hotkeys.ExtractFile, msg):\n\t\treturn m.getExtractFileCmd()\n\n\tcase slices.Contains(common.Hotkeys.CompressFile, msg):\n\t\treturn m.getCompressSelectedFilesCmd()\n\n\tcase slices.Contains(common.Hotkeys.OpenCommandLine, msg):\n\t\tm.promptModal.Open(true)\n\tcase slices.Contains(common.Hotkeys.OpenSPFPrompt, msg):\n\t\tm.promptModal.Open(false)\n\tcase slices.Contains(common.Hotkeys.OpenZoxide, msg):\n\t\treturn m.zoxideModal.Open()\n\n\tcase slices.Contains(common.Hotkeys.OpenHelpMenu, msg):\n\t\tm.helpMenu.Open()\n\n\tcase slices.Contains(common.Hotkeys.OpenSortOptionsMenu, msg):\n\t\tm.sortModal.Open(m.getFocusedFilePanel().SortKind)\n\n\tcase slices.Contains(common.Hotkeys.ToggleReverseSort, msg):\n\t\tm.getFocusedFilePanel().ToggleReverseSort()\n\n\tcase slices.Contains(common.Hotkeys.OpenFileWithEditor, msg):\n\t\treturn m.openFileWithEditor()\n\n\tcase slices.Contains(common.Hotkeys.OpenCurrentDirectoryWithEditor, msg):\n\t\treturn m.openDirectoryWithEditor()\n\n\tdefault:\n\t\treturn m.normalAndBrowserModeKey(msg)\n\t}\n\n\treturn nil\n}\n\nfunc (m *model) normalAndBrowserModeKey(msg string) tea.Cmd {\n\t// if not focus on the filepanel return\n\tif !m.getFocusedFilePanel().IsFocused {\n\t\tif m.focusPanel == sidebarFocus && slices.Contains(common.Hotkeys.Confirm, msg) {\n\t\t\tm.sidebarSelectDirectory()\n\t\t}\n\t\tif m.focusPanel == sidebarFocus && slices.Contains(common.Hotkeys.FilePanelItemRename, msg) {\n\t\t\tm.sidebarModel.PinnedItemRename()\n\t\t}\n\t\tif m.focusPanel == sidebarFocus && slices.Contains(common.Hotkeys.SearchBar, msg) {\n\t\t\tm.sidebarSearchBarFocus()\n\t\t}\n\t\treturn nil\n\t}\n\t// Check if in the select mode and focusOn filepanel\n\tif m.getFocusedFilePanel().PanelMode == filepanel.SelectMode {\n\t\tswitch {\n\t\tcase slices.Contains(common.Hotkeys.Confirm, msg):\n\t\t\tm.getFocusedFilePanel().SingleItemSelect()\n\t\tcase slices.Contains(common.Hotkeys.FilePanelSelectModeItemsSelectUp, msg):\n\t\t\tm.getFocusedFilePanel().ItemSelectUp()\n\t\tcase slices.Contains(common.Hotkeys.FilePanelSelectModeItemsSelectDown, msg):\n\t\t\tm.getFocusedFilePanel().ItemSelectDown()\n\t\tcase slices.Contains(common.Hotkeys.DeleteItems, msg):\n\t\t\treturn m.getDeleteTriggerCmd(false)\n\t\tcase slices.Contains(common.Hotkeys.PermanentlyDeleteItems, msg):\n\t\t\treturn m.getDeleteTriggerCmd(true)\n\t\tcase slices.Contains(common.Hotkeys.CopyItems, msg):\n\t\t\tm.copyMultipleItem(false)\n\t\tcase slices.Contains(common.Hotkeys.CutItems, msg):\n\t\t\tm.copyMultipleItem(true)\n\t\tcase slices.Contains(common.Hotkeys.FilePanelSelectAllItem, msg):\n\t\t\tm.getFocusedFilePanel().SelectAllItem()\n\t\t}\n\t\treturn nil\n\t}\n\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.Confirm, msg):\n\t\tm.enterPanel()\n\tcase slices.Contains(common.Hotkeys.ParentDirectory, msg):\n\t\tm.parentDirectory()\n\tcase slices.Contains(common.Hotkeys.DeleteItems, msg):\n\t\treturn m.getDeleteTriggerCmd(false)\n\tcase slices.Contains(common.Hotkeys.PermanentlyDeleteItems, msg):\n\t\treturn m.getDeleteTriggerCmd(true)\n\tcase slices.Contains(common.Hotkeys.CopyItems, msg):\n\t\tm.copySingleItem(false)\n\tcase slices.Contains(common.Hotkeys.CutItems, msg):\n\t\tm.copySingleItem(true)\n\tcase slices.Contains(common.Hotkeys.FilePanelItemRename, msg):\n\t\tm.panelItemRename()\n\tcase slices.Contains(common.Hotkeys.SearchBar, msg):\n\t\tm.searchBarFocus()\n\tcase slices.Contains(common.Hotkeys.CopyPath, msg):\n\t\tm.copyPath()\n\tcase slices.Contains(common.Hotkeys.CopyPWD, msg):\n\t\tm.copyPWD()\n\t}\n\treturn nil\n}\n\n// Check the hotkey to cancel operation or create file\nfunc (m *model) typingModalOpenKey(msg string) {\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.CancelTyping, msg):\n\t\tm.typingModal.errorMesssage = \"\"\n\t\tm.cancelTypingModal()\n\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg):\n\t\tm.createItem()\n\t}\n}\n\nfunc (m *model) notifyModelOpenKey(msg string) tea.Cmd {\n\tisCancel := slices.Contains(common.Hotkeys.CancelTyping, msg) || slices.Contains(common.Hotkeys.Quit, msg)\n\tisConfirm := slices.Contains(common.Hotkeys.ConfirmTyping, msg)\n\n\tif !isCancel && !isConfirm {\n\t\tslog.Warn(\"Invalid keypress in notifyModel\", \"msg\", msg)\n\t\treturn nil\n\t}\n\tm.notifyModel.Close()\n\taction := m.notifyModel.GetConfirmAction()\n\tif isCancel {\n\t\treturn m.handleNotifyModelCancel(action)\n\t}\n\treturn m.handleNotifyModelConfirm(action)\n}\n\nfunc (m *model) handleNotifyModelCancel(action notify.ConfirmActionType) tea.Cmd {\n\tswitch action {\n\tcase notify.RenameAction:\n\t\tm.cancelRename()\n\tcase notify.QuitAction:\n\t\tm.modelQuitState = notQuitting\n\tcase notify.DeleteAction, notify.NoAction, notify.PermanentDeleteAction:\n\t\t// Do nothing\n\tdefault:\n\t\tslog.Error(\"Unknown type of action\", \"action\", action)\n\t}\n\treturn nil\n}\n\nfunc (m *model) handleNotifyModelConfirm(action notify.ConfirmActionType) tea.Cmd {\n\tswitch action {\n\tcase notify.DeleteAction:\n\t\treturn m.getDeleteCmd(false)\n\tcase notify.PermanentDeleteAction:\n\t\treturn m.getDeleteCmd(true)\n\tcase notify.RenameAction:\n\t\tm.confirmRename()\n\tcase notify.QuitAction:\n\t\tm.modelQuitState = quitConfirmationReceived\n\tcase notify.NoAction:\n\t\t// Ignore\n\tdefault:\n\t\tslog.Error(\"Unknown type of action\", \"action\", action)\n\t}\n\treturn nil\n}\n\n// Handles key inputs inside sort options menu\nfunc (m *model) sortOptionsKey(msg string) {\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.OpenSortOptionsMenu, msg):\n\t\tm.sortModal.Close()\n\tcase slices.Contains(common.Hotkeys.Quit, msg):\n\t\tm.sortModal.Close()\n\tcase slices.Contains(common.Hotkeys.Confirm, msg):\n\t\tm.confirmSortOptions()\n\tcase slices.Contains(common.Hotkeys.ListUp, msg):\n\t\tm.sortModal.ListUp()\n\tcase slices.Contains(common.Hotkeys.ListDown, msg):\n\t\tm.sortModal.ListDown()\n\t}\n}\n\nfunc (m *model) renamingKey(msg string) tea.Cmd {\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.CancelTyping, msg):\n\t\tm.cancelRename()\n\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg):\n\t\tif m.IsRenamingConflicting() {\n\t\t\treturn m.warnModalForRenaming()\n\t\t}\n\t\tm.confirmRename()\n\t}\n\n\treturn nil\n}\n\nfunc (m *model) sidebarRenamingKey(msg string) {\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.CancelTyping, msg):\n\t\tm.sidebarModel.CancelSidebarRename()\n\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg):\n\t\tm.sidebarModel.ConfirmSidebarRename()\n\t}\n}\n\n// Check the key input and cancel or confirms the search\nfunc (m *model) focusOnSearchbarKey(msg string) {\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.CancelTyping, msg):\n\t\tm.cancelSearch()\n\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg):\n\t\tm.confirmSearch()\n\t}\n}\n"
  },
  {
    "path": "src/internal/model.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"os\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/barasher/go-exiftool\"\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\t\"github.com/yorukot/superfile/src/internal/ui/metadata\"\n\t\"github.com/yorukot/superfile/src/internal/ui/notify\"\n\t\"github.com/yorukot/superfile/src/internal/ui/preview\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\tzoxideui \"github.com/yorukot/superfile/src/internal/ui/zoxide\"\n\tstringfunction \"github.com/yorukot/superfile/src/pkg/string_function\"\n)\n\n// These represent model's state information, its not a global preperty\nvar (\n\tLastTimeCursorMove = [2]int{int(time.Now().UnixMicro()), 0} //nolint: gochecknoglobals // TODO: Move to model struct\n\tet                 *exiftool.Exiftool                       //nolint: gochecknoglobals // TODO: Move to model struct\n)\n\n// Initialize and return model with default configs\n// It returns only tea.Model because when it used in main, the return value\n// is passed to tea.NewProgram() which accepts tea.Model\n// Either way type 'model' is not exported, so there is not way main package can\n// be aware of it, and use it directly\nfunc InitialModel(firstPanelPaths []string, firstUseCheck bool) tea.Model {\n\ttoggleDotFile, toggleFooter, zClient := initialConfig(firstPanelPaths)\n\treturn defaultModelConfig(toggleDotFile, toggleFooter, firstUseCheck, firstPanelPaths, zClient)\n}\n\n// Init function to be called by Bubble tea framework, sets windows title,\n// cursos blinking and starts message streamming channel\n// Note : What init should do, for example read file panel data, read sidebar directories, and\n// disk, is being done in at the creation of model of object. Right now creation of model object\n// and its initialization isn't well separated.\nfunc (m *model) Init() tea.Cmd {\n\treturn tea.Batch(\n\t\ttea.SetWindowTitle(\"superfile\"),\n\t\ttextinput.Blink, // Assuming textinput.Blink is a valid command\n\t\tprocessCmdToTeaCmd(m.processBarModel.GetListenCmd()),\n\t)\n}\n\n// Update function for bubble tea to provide internal communication to the\n// application\nfunc (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tslog.Debug(\"model.Update() called\", \"msgType\", reflect.TypeOf(msg))\n\n\tvar sidebarCmd, inputCmd, updateCmd, panelCmd,\n\t\tmetadataCmd, filePreviewCmd, helpMenuCmd, resizeCmd tea.Cmd\n\n\t// These are above the key message handing to prevent issues with firstKeyInput\n\t// if someone presses `/` to focus to searchBar, searchBar will otherwise\n\t// get `/` input too.\n\tsidebarCmd = m.sidebarModel.UpdateState(msg)\n\t// Necessary for blinking. Can't do this in HandleKey, as we only pass KeyMsg there\n\thelpMenuCmd = m.helpMenu.HandleTeaMsg(msg)\n\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\t\tresizeCmd = m.handleWindowResize(msg)\n\tcase tea.MouseMsg:\n\t\tm.handleMouseMsg(msg)\n\tcase tea.KeyMsg:\n\t\tinputCmd = m.handleKeyInput(msg)\n\n\t// Has to handle zoxide messages separately as they could be generated via\n\t// zoxide update commands, or batched commands from textinput\n\t// Cannot do it like processbar messages\n\tcase zoxideui.UpdateMsg:\n\t\tslog.Debug(\"Got ModelUpdate message\", \"id\", msg.GetReqID())\n\t\tupdateCmd = msg.Apply(&m.zoxideModal)\n\n\t// Its a pain to interconvert commands like processBar\n\tcase preview.UpdateMsg:\n\t\tslog.Debug(\"Got ModelUpdate message\", \"id\", msg.GetReqID())\n\t\tm.fileModel.UpdatePreviewPanel(msg)\n\tcase ModelUpdateMessage:\n\t\tslog.Debug(\"Got ModelUpdate message\", \"id\", msg.GetReqID())\n\t\tupdateCmd = msg.ApplyToModel(m)\n\n\tdefault:\n\t\tslog.Debug(\"Message of type that is not explicitly handled\")\n\t}\n\n\t// This is needed for blink, etc to work\n\tpanelCmd = m.updateComponentState(msg)\n\n\tm.updateModelStateAfterMsg()\n\tfilePreviewCmd = m.fileModel.GetFilePreviewCmd(false)\n\n\tmetadataCmd = m.getMetadataCmd()\n\n\treturn m, tea.Batch(sidebarCmd, helpMenuCmd, inputCmd, updateCmd,\n\t\tpanelCmd, metadataCmd, filePreviewCmd, resizeCmd)\n}\n\nfunc (m *model) handleMouseMsg(msg tea.MouseMsg) {\n\tmsgStr := msg.String()\n\tif msgStr == \"wheel up\" || msgStr == \"wheel down\" {\n\t\twheelMainAction(msgStr, m)\n\t} else {\n\t\tslog.Debug(\"Mouse event of type that is not handled\", \"msg\", msgStr)\n\t}\n}\n\nfunc (m *model) updateModelStateAfterMsg() {\n\tm.sidebarModel.UpdateDirectories()\n\tm.fileModel.UpdateFilePanelsIfNeeded(false)\n\t// TODO: Move to utility\n\tif m.focusPanel != metadataFocus {\n\t\tm.fileMetaData.ResetRender()\n\t}\n\t// TODO: Entirely remove the need of this variable, and handle first loading via Init()\n\t// Init() should return a basic model object with all IO waiting via a tea.Cmd\n\tif !m.firstLoadingComplete {\n\t\tm.firstLoadingComplete = true\n\t}\n}\n\n// Note : Maybe we should not trigger metadata fetch for updates\n// that dont change the currently selected file panel element\n// TODO : At least dont trigger metadata fetch when user is scrolling\n// through the metadata panel\nfunc (m *model) getMetadataCmd() tea.Cmd {\n\tif m.disableMetadata {\n\t\treturn nil\n\t}\n\tif m.getFocusedFilePanel().EmptyOrInvalid() {\n\t\tm.fileMetaData.SetBlank()\n\t\treturn nil\n\t}\n\tselectedItem := m.getFocusedFilePanel().GetFocusedItem()\n\tmetadataFocused := m.focusPanel == metadataFocus\n\t// Note : This will cause metadata not being refreshed there is any file update events.\n\t// We can have a cache with TTL or watch filesystem changes to fix this\n\tif selectedItem.Location == m.fileMetaData.GetMetadataLocation() &&\n\t\tmetadataFocused == m.fileMetaData.GetMetadataExpectedFocused() {\n\t\treturn nil\n\t}\n\tif m.fileMetaData.UpdateMetadataIfExistsInCache(selectedItem.Location, metadataFocused) {\n\t\treturn nil\n\t}\n\n\tm.fileMetaData.SetMetadataLocationAndFocused(selectedItem.Location, metadataFocused)\n\n\tif m.fileMetaData.IsBlank() {\n\t\tm.fileMetaData.SetInfoMsg(icon.InOperation + icon.Space + \"Loading metadata...\")\n\t}\n\n\treqCnt := m.ioReqCnt\n\tm.ioReqCnt++\n\t// If there are too many metadata fetches, we need to have a cache with path as a key\n\t// and timeout based eviction\n\tslog.Debug(\"Submitting metadata fetch request\", \"id\", reqCnt, \"path\", selectedItem.Location)\n\treturn func() tea.Msg {\n\t\treturn NewMetadataMsg(\n\t\t\tmetadata.GetMetadata(selectedItem.Location, metadataFocused, et), metadataFocused, reqCnt)\n\t}\n}\n\n// Adjust window size based on msg information\nfunc (m *model) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {\n\tm.fullHeight = msg.Height\n\tm.fullWidth = msg.Width\n\tm.setHeightValues()\n\treturn m.updateComponentDimensions()\n}\n\nfunc (m *model) setHeightValues() {\n\t//nolint: gocritic // This is to be separated out to a function, and made better later. No need to refactor here\n\tif !m.toggleFooter {\n\t\tm.footerHeight = 0\n\t} else if m.fullHeight < common.HeightBreakA {\n\t\tm.footerHeight = 6\n\t} else if m.fullHeight < common.HeightBreakB {\n\t\tm.footerHeight = 7\n\t} else if m.fullHeight < common.HeightBreakC {\n\t\tm.footerHeight = 8\n\t} else if m.fullHeight < common.HeightBreakD {\n\t\tm.footerHeight = 9\n\t} else {\n\t\tm.footerHeight = 10\n\t}\n\t// TODO : Make it grow even more for bigger screen sizes.\n\t// TODO : Calculate the value , instead of manually hard coding it.\n\n\t// Main panel height = Total terminal height- 2(file panel border) - footer height\n\tm.mainPanelHeight = m.fullHeight - common.BorderPadding - utils.FullFooterHeight(m.footerHeight, m.toggleFooter)\n}\n\nfunc (m *model) updateComponentDimensions() tea.Cmd {\n\tm.setHelpMenuSize()\n\tm.setPromptModelSize()\n\tm.setZoxideModelSize()\n\tm.setFooterComponentSize()\n\n\t// File preview panel requires explicit height update, unlike sidebar/file panels\n\t// which receive height as render parameters and update automatically on each frame\n\t// Force re-render of preview content with new dimensions\n\treturn m.setMainModelDimensions()\n}\n\nfunc (m *model) setMainModelDimensions() tea.Cmd {\n\tfileModelWidth := m.fullWidth\n\tif common.Config.SidebarWidth != 0 {\n\t\tfileModelWidth -= common.Config.SidebarWidth + common.BorderPadding\n\t}\n\tm.sidebarModel.SetHeight(m.mainPanelHeight + common.BorderPadding)\n\treturn m.fileModel.SetDimensions(fileModelWidth, m.mainPanelHeight+common.BorderPadding)\n}\n\n// Set help menu size\nfunc (m *model) setHelpMenuSize() {\n\theight := m.fullHeight - common.BorderPadding\n\twidth := m.fullWidth - common.BorderPadding\n\tif m.fullHeight > common.HeightBreakB {\n\t\theight = 30\n\t}\n\tif m.fullWidth > common.ResponsiveWidthThreshold {\n\t\twidth = 90\n\t}\n\tm.helpMenu.SetDimensions(width, height)\n}\n\nfunc (m *model) setPromptModelSize() {\n\t// Scale prompt model's maxHeight - 33% of total height\n\tm.promptModal.SetMaxHeight(m.fullHeight / 3) //nolint:mnd // modal uses third height for layout\n\n\t// Scale prompt model's maxHeight - 50% of total height\n\tm.promptModal.SetWidth(m.fullWidth / 2) //nolint:mnd // modal uses half width for layout\n}\n\nfunc (m *model) setZoxideModelSize() {\n\t// Scale zoxide model's maxHeight - 50% of total height to accommodate scroll indicators\n\tm.zoxideModal.SetMaxHeight(m.fullHeight / 2) //nolint:mnd // modal uses half height for layout\n\n\t// Scale zoxide model's width - 50% of total width\n\tm.zoxideModal.SetWidth(m.fullWidth / 2) //nolint:mnd // modal uses half width for layout\n}\n\nfunc (m *model) setFooterComponentSize() {\n\tvar width, clipBoardwidth, height int\n\theight = m.footerHeight + common.BorderPadding\n\twidth = m.fullWidth / utils.CntFooterPanels\n\tclipBoardwidth = width + m.fullWidth%utils.CntFooterPanels\n\tm.fileMetaData.SetDimensions(width, height)\n\tm.processBarModel.SetDimensions(width, height)\n\tm.clipboard.SetDimensions(clipBoardwidth, height)\n}\n\n// Identify the current state of the application m and properly handle the\n// msg keybind pressed\nfunc (m *model) handleKeyInput(msg tea.KeyMsg) tea.Cmd {\n\tslog.Debug(\"model.handleKeyInput\", \"msg\", msg, \"typestr\", msg.Type.String(),\n\t\t\"runes\", msg.Runes, \"type\", int(msg.Type), \"paste\", msg.Paste,\n\t\t\"alt\", msg.Alt)\n\tslog.Debug(\"model.handleKeyInput. model info. \",\n\t\t\"fileModel.FocusedPanelIndex\", m.fileModel.FocusedPanelIndex,\n\t\t\"filePanel.isFocused\", m.getFocusedFilePanel().IsFocused,\n\t\t\"filePanel.panelMode\", m.getFocusedFilePanel().PanelMode,\n\t\t\"typingModal.open\", m.typingModal.open,\n\t\t\"notifyModel.open\", m.notifyModel.IsOpen(),\n\t\t\"promptModal.open\", m.promptModal.IsOpen(),\n\t\t\"fileModel.renaming\", m.fileModel.Renaming,\n\t\t\"searchBar.focused\", m.getFocusedFilePanel().SearchBar.Focused(),\n\t\t\"helpMenu.open\", m.helpMenu.IsOpen(),\n\t\t\"firstTextInput\", m.firstTextInput,\n\t\t\"focusPanel\", m.focusPanel,\n\t)\n\tif m.firstUse {\n\t\tm.firstUse = false\n\t\treturn nil\n\t}\n\tvar cmd tea.Cmd\n\tcdOnQuit := common.Config.CdOnQuit\n\tswitch {\n\tcase m.typingModal.open:\n\t\tm.typingModalOpenKey(msg.String())\n\tcase m.promptModal.IsOpen():\n\t\t// Ignore keypress. It will be handled in Update call via\n\t\t// updateFilePanelState\n\t\t// TODO: Convert that to async via tea.Cmd\n\tcase m.zoxideModal.IsOpen():\n\t\t// Ignore keypress. It will be handled in Update call via\n\t\t// updateFilePanelState\n\n\t// Handles all warn models except the warn model for confirming to quit\n\tcase m.notifyModel.IsOpen():\n\t\tcmd = m.notifyModelOpenKey(msg.String())\n\n\t// If renaming a object\n\tcase m.fileModel.Renaming:\n\t\tcmd = m.renamingKey(msg.String())\n\tcase m.sidebarModel.IsRenaming():\n\t\tm.sidebarRenamingKey(msg.String())\n\t// If search bar is open\n\tcase m.getFocusedFilePanel().SearchBar.Focused():\n\t\tm.focusOnSearchbarKey(msg.String())\n\t// If sort options menu is open\n\tcase m.sidebarModel.SearchBarFocused():\n\t\tm.sidebarModel.HandleSearchBarKey(msg.String())\n\tcase m.sortModal.IsOpen():\n\t\tm.sortOptionsKey(msg.String())\n\t// If help menu is open\n\tcase m.helpMenu.IsOpen():\n\t\tm.helpMenu.HandleKey(msg.String())\n\n\tcase slices.Contains(common.Hotkeys.Quit, msg.String()):\n\t\tm.modelQuitState = quitInitiated\n\n\tcase slices.Contains(common.Hotkeys.CdQuit, msg.String()):\n\t\tm.modelQuitState = quitInitiated\n\t\tcdOnQuit = true\n\n\tdefault:\n\t\t// Handles general kinds of inputs in the regular state of the application\n\t\tcmd = m.mainKey(msg.String())\n\t}\n\n\t// If quiting input pressed, check if has any running process and displays a\n\t// warn. Otherwise just quits application\n\tif m.modelQuitState == quitInitiated {\n\t\tif m.processBarModel.HasRunningProcesses() {\n\t\t\t// Dont quit now, get a confirmation first.\n\t\t\tm.modelQuitState = quitConfirmationInitiated\n\t\t\tm.warnModalForQuit()\n\t\t\treturn cmd\n\t\t}\n\t\tm.modelQuitState = quitConfirmationReceived\n\t}\n\tif m.modelQuitState == quitConfirmationReceived {\n\t\tm.quitSuperfile(cdOnQuit)\n\t\treturn tea.Quit\n\t}\n\treturn cmd\n}\n\n// Update the file panel state. Change name of renamed files, filter out files\n// in search, update typingb bar, etc\nfunc (m *model) updateComponentState(msg tea.Msg) tea.Cmd {\n\tfocusPanel := m.getFocusedFilePanel()\n\tvar cmd tea.Cmd\n\tvar action common.ModelAction\n\tswitch {\n\tcase m.firstTextInput:\n\t\tm.firstTextInput = false\n\tcase m.fileModel.Renaming:\n\t\tfocusPanel.Rename, cmd = focusPanel.Rename.Update(msg)\n\tcase focusPanel.SearchBar.Focused():\n\t\tfocusPanel.SearchBar, cmd = focusPanel.SearchBar.Update(msg)\n\tcase m.typingModal.open:\n\t\tm.typingModal.textInput, cmd = m.typingModal.textInput.Update(msg)\n\tcase m.promptModal.IsOpen():\n\t\t// TODO : Separate this to a utility\n\t\tcwdLocation := m.getFocusedFilePanel().Location\n\t\taction, cmd = m.promptModal.HandleUpdate(msg, cwdLocation)\n\t\tcmd = tea.Batch(cmd, m.applyPromptModalAction(action))\n\tcase m.zoxideModal.IsOpen():\n\t\taction, cmd = m.zoxideModal.HandleUpdate(msg)\n\t\tcmd = tea.Batch(cmd, m.applyZoxideModalAction(action))\n\t}\n\treturn cmd\n}\n\n// Apply the Action and notify the promptModal\nfunc (m *model) applyPromptModalAction(action common.ModelAction) tea.Cmd {\n\tsuccessMsg, cmd, actionErr := m.logAndExecuteAction(action)\n\tif actionErr != nil {\n\t\tm.promptModal.HandleSPFActionResults(false, actionErr.Error())\n\t} else if successMsg != \"\" {\n\t\tm.promptModal.HandleSPFActionResults(true, successMsg)\n\t}\n\treturn cmd\n}\n\n// Utility function to log and execute actions, reducing duplication\nfunc (m *model) logAndExecuteAction(action common.ModelAction) (string, tea.Cmd, error) {\n\t// Only log actions that aren't NoAction to reduce debug noise\n\tif _, ok := action.(common.NoAction); !ok {\n\t\tslog.Debug(\"Applying model action\", \"action\", action)\n\t}\n\n\tswitch action := action.(type) {\n\tcase common.NoAction:\n\t\treturn \"\", nil, nil\n\tcase common.ShellCommandAction:\n\t\t// Shell commands are handled separately and don't return here\n\t\tm.applyShellCommandAction(action.Command)\n\t\treturn \"\", nil, nil\n\tcase common.SplitPanelAction:\n\t\tcmd, err := m.splitPanel()\n\t\treturn \"Panel successfully split\", cmd, err\n\tcase common.CDCurrentPanelAction:\n\t\treturn \"Panel directory changed\", nil, m.updateCurrentFilePanelDir(action.Location)\n\tcase common.OpenPanelAction:\n\t\tcmd, err := m.createNewFilePanelRelativeToCurrent(action.Location)\n\t\treturn \"New panel opened\", cmd, err\n\tdefault:\n\t\treturn \"\", nil, errors.New(\"unhandled action type\")\n\t}\n}\n\n// Apply the Action for zoxide modal (no result notifications needed)\nfunc (m *model) applyZoxideModalAction(action common.ModelAction) tea.Cmd {\n\t_, cmd, _ := m.logAndExecuteAction(action)\n\treturn cmd\n}\n\n// TODO : Move them around to appropriate places\nfunc (m *model) applyShellCommandAction(shellCommand string) {\n\tfocusPanelDir := m.getFocusedFilePanel().Location\n\n\tretCode, output, err := utils.ExecuteCommandInShell(common.DefaultCommandTimeout, focusPanelDir, shellCommand)\n\n\tm.promptModal.HandleShellCommandResults(retCode, output)\n\n\tif err != nil {\n\t\tslog.Error(\"Command execution failed\", \"retCode\", retCode,\n\t\t\t\"error\", err, \"output\", output)\n\t\treturn\n\t}\n}\n\nfunc (m *model) splitPanel() (tea.Cmd, error) {\n\treturn m.fileModel.CreateNewFilePanel(m.getFocusedFilePanel().Location)\n}\n\nfunc (m *model) createNewFilePanelRelativeToCurrent(path string) (tea.Cmd, error) {\n\tcurrentDir := m.getFocusedFilePanel().Location\n\treturn m.fileModel.CreateNewFilePanel(utils.ResolveAbsPath(currentDir, path))\n}\n\n// simulates a 'cd' action\nfunc (m *model) updateCurrentFilePanelDir(path string) error {\n\tpanel := m.getFocusedFilePanel()\n\terr := panel.UpdateCurrentFilePanelDir(path)\n\tif err == nil {\n\t\t// Track the directory change with zoxide\n\t\tm.trackDirectoryWithZoxide(panel.Location)\n\t}\n\treturn err\n}\n\n// trackDirectoryWithZoxide adds the directory to zoxide database if zoxide is available and enabled\nfunc (m *model) trackDirectoryWithZoxide(path string) {\n\tif !common.Config.ZoxideSupport || m.zClient == nil {\n\t\treturn\n\t}\n\n\terr := m.zClient.Add(path)\n\tif err != nil {\n\t\tslog.Debug(\"Failed to add directory to zoxide\", \"path\", path, \"error\", err)\n\t}\n}\n\n// Check if there's any processes running in background\n\n// Triggers a warn for confirm quiting\nfunc (m *model) warnModalForQuit() {\n\tm.notifyModel = notify.New(true, \"Confirm to quit superfile\",\n\t\t\"You still have files being processed. Are you sure you want to exit?\",\n\t\tnotify.QuitAction)\n}\n\n// Implement View function for bubble tea model to handle visualization.\nfunc (m *model) View() string {\n\tslog.Debug(\"model.View() called\", \"mainPanelHeight\", m.mainPanelHeight,\n\t\t\"footerHeight\", m.footerHeight, \"fullHeight\", m.fullHeight,\n\t\t\"fullWidth\", m.fullWidth, \"panelCount\", m.fileModel.PanelCount(),\n\t\t\"singlePanelWidth\", m.fileModel.SinglePanelWidth,\n\t\t\"maxPanels\", m.fileModel.MaxFilePanel,\n\t\t\"sideBarWidth\", common.Config.SidebarWidth,\n\t\t\"firstFilePanelWidth\", m.fileModel.FilePanels[0].GetWidth())\n\n\tif !m.firstLoadingComplete {\n\t\treturn \"Loading...\"\n\t}\n\n\t// check is the terminal size enough\n\tif m.fullHeight < common.MinimumHeight || m.fullWidth < common.MinimumWidth {\n\t\treturn m.terminalSizeWarnRender()\n\t}\n\tif m.fileModel.SinglePanelWidth < filepanel.MinWidth {\n\t\treturn m.terminalSizeWarnAfterFirstRender()\n\t}\n\n\t// Do validations after min size check above. Validations will fail if user give\n\t// too less size to the terminal program\n\tif err := m.validateLayout(); err != nil {\n\t\tslog.Error(\"Invalid layout\", \"error\", err)\n\t}\n\n\treturn m.updateRenderForOverlay(m.mainComponentsRender())\n}\n\nfunc (m *model) updateRenderForOverlay(finalRender string) string {\n\t// check if need pop up modal\n\tif m.helpMenu.IsOpen() {\n\t\thelpMenu := m.helpMenu.Render()\n\t\toverlayX := m.fullWidth/common.CenterDivisor - m.helpMenu.GetWidth()/common.CenterDivisor\n\t\toverlayY := m.fullHeight/common.CenterDivisor - m.helpMenu.GetHeight()/common.CenterDivisor\n\t\treturn stringfunction.PlaceOverlay(overlayX, overlayY, helpMenu, finalRender)\n\t}\n\n\tif m.promptModal.IsOpen() {\n\t\tpromptModal := m.promptModalRender()\n\t\toverlayX := m.fullWidth/common.CenterDivisor - m.promptModal.GetWidth()/common.CenterDivisor\n\t\toverlayY := m.fullHeight/common.CenterDivisor - m.promptModal.GetMaxHeight()/common.CenterDivisor\n\t\treturn stringfunction.PlaceOverlay(overlayX, overlayY, promptModal, finalRender)\n\t}\n\n\tif m.zoxideModal.IsOpen() {\n\t\tzoxideModal := m.zoxideModalRender()\n\t\toverlayX := m.fullWidth/common.CenterDivisor - m.zoxideModal.GetWidth()/common.CenterDivisor\n\t\toverlayY := m.fullHeight/common.CenterDivisor - m.zoxideModal.GetMaxHeight()/common.CenterDivisor\n\t\treturn stringfunction.PlaceOverlay(overlayX, overlayY, zoxideModal, finalRender)\n\t}\n\n\tif m.sortModal.IsOpen() {\n\t\tsortOptions := m.sortModal.Render()\n\t\toverlayX := m.fullWidth/common.CenterDivisor - m.sortModal.Width/common.CenterDivisor\n\t\toverlayY := m.fullHeight/common.CenterDivisor - m.sortModal.Height/common.CenterDivisor\n\t\treturn stringfunction.PlaceOverlay(overlayX, overlayY, sortOptions, finalRender)\n\t}\n\n\tif m.firstUse {\n\t\tintroduceModal := m.introduceModalRender()\n\t\toverlayX := m.fullWidth/common.CenterDivisor - m.helpMenu.GetWidth()/common.CenterDivisor\n\t\toverlayY := m.fullHeight/common.CenterDivisor - m.helpMenu.GetHeight()/common.CenterDivisor\n\t\treturn stringfunction.PlaceOverlay(overlayX, overlayY, introduceModal, finalRender)\n\t}\n\n\tif m.typingModal.open {\n\t\ttypingModal := m.typineModalRender()\n\t\toverlayX := m.fullWidth/common.CenterDivisor - common.ModalWidth/common.CenterDivisor\n\t\toverlayY := m.fullHeight/common.CenterDivisor - common.ModalHeight/common.CenterDivisor\n\t\treturn stringfunction.PlaceOverlay(overlayX, overlayY, typingModal, finalRender)\n\t}\n\n\tif m.notifyModel.IsOpen() {\n\t\tnotifyModal := m.notifyModel.Render()\n\t\toverlayX := m.fullWidth/common.CenterDivisor - common.ModalWidth/common.CenterDivisor\n\t\toverlayY := m.fullHeight/common.CenterDivisor - common.ModalHeight/common.CenterDivisor\n\t\treturn stringfunction.PlaceOverlay(overlayX, overlayY, notifyModal, finalRender)\n\t}\n\treturn finalRender\n}\n\nfunc (m *model) mainComponentsRender() string {\n\tsidebar := m.sidebarRender()\n\tfileModel := m.fileModel.Render()\n\tmainPanel := lipgloss.JoinHorizontal(0, sidebar, fileModel)\n\n\tif !m.toggleFooter {\n\t\treturn mainPanel\n\t}\n\n\tprocessBar := m.processBarRender()\n\tmetaData := m.fileMetaData.Render(m.focusPanel == metadataFocus)\n\tclipboardBar := m.clipboard.Render()\n\tfooter := lipgloss.JoinHorizontal(0, processBar, metaData, clipboardBar)\n\treturn lipgloss.JoinVertical(0, mainPanel, footer)\n}\n\n// Close superfile application. Cd into the current dir if CdOnQuit on and save\n// the path in state direcotory\nfunc (m *model) quitSuperfile(cdOnQuit bool) {\n\t// Resource cleanup\n\tif common.Config.Metadata && et != nil {\n\t\t_ = et.Close()\n\t}\n\tm.fileModel.FilePreview.CleanUp()\n\n\t// cd on quit\n\tcurrentDir := m.getFocusedFilePanel().Location\n\tvariable.SetLastDir(currentDir)\n\n\tif cdOnQuit {\n\t\t// escape single quote\n\t\tcurrentDir = strings.ReplaceAll(currentDir, \"'\", \"'\\\\''\")\n\t\terr := os.WriteFile(variable.LastDirFile, []byte(\"cd '\"+currentDir+\"'\"), utils.ConfigFilePerm)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error during writing lastdir file\", \"error\", err)\n\t\t}\n\t}\n\tm.modelQuitState = quitDone\n\tslog.Debug(\"Quitting superfile\", \"current dir\", currentDir)\n}\n"
  },
  {
    "path": "src/internal/model_file_operations_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/rkoesters/xdg/trash\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/notify\"\n)\n\n// TODO : Add test for model initialized with multiple directories\n// TODO : Add test for clipboard different variations, cut paste\n// TODO : Add test for tea resizing\n// TODO : Add test for quitting\n\nfunc TestCopy(t *testing.T) {\n\tcurTestDir := filepath.Join(testDir, \"TestCopy\")\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tfile1 := filepath.Join(dir1, \"file1.txt\")\n\tt.Run(\"Basic Copy\", func(t *testing.T) {\n\t\tutils.SetupDirectories(t, curTestDir, dir1, dir2)\n\t\tutils.SetupFiles(t, file1)\n\t\tt.Cleanup(func() {\n\t\t\tos.RemoveAll(curTestDir)\n\t\t})\n\n\t\tp := NewTestTeaProgWithEventLoop(t, defaultTestModel(dir1))\n\n\t\trequire.Equal(t, \"file1.txt\",\n\t\t\tp.getModel().getFocusedFilePanel().GetFocusedItem().Name)\n\t\tp.SendKeyDirectly(common.Hotkeys.CopyItems[0])\n\t\tassert.False(t, p.getModel().clipboard.IsCut())\n\t\tassert.Equal(t, file1, p.getModel().clipboard.GetFirstItem())\n\n\t\tp.getModel().updateCurrentFilePanelDir(\"../dir2\")\n\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\t_, err := os.Lstat(filepath.Join(dir2, \"file1.txt\"))\n\t\t\treturn err == nil\n\t\t}, DefaultTestTimeout, DefaultTestTick)\n\n\t\tassert.False(t, p.getModel().clipboard.IsCut())\n\t\tassert.Equal(t, file1, p.getModel().clipboard.GetFirstItem())\n\n\t\tp.SendKey(common.Hotkeys.PasteItems[0])\n\t\tassert.Eventually(t, func() bool {\n\t\t\t_, err := os.Lstat(filepath.Join(dir2, \"file1(1).txt\"))\n\t\t\treturn err == nil\n\t\t}, DefaultTestTimeout, DefaultTestTick)\n\t\tassert.FileExists(t, filepath.Join(dir2, \"file1(1).txt\"))\n\t\t//TODO: Also verify if there are only 2 items in process bar\n\t})\n}\n\nfunc TestFileCreation(t *testing.T) {\n\t// TODO Also add directory creation test to this\n\tcurTestDir := filepath.Join(testDir, \"TestNaming\")\n\ttestParentDir := filepath.Join(curTestDir, \"parentDir\")\n\ttestChildDir := filepath.Join(testParentDir, \"childDir\")\n\n\tutils.SetupDirectories(t, curTestDir, testParentDir, testChildDir)\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(curTestDir)\n\t})\n\n\ttestdata := []struct {\n\t\tname          string\n\t\tfileName      string\n\t\texpectedError bool\n\t}{\n\t\t{\"valid name\", \"file.txt\", false},\n\t\t{\"invalid single dot\", \".\", true},\n\t\t{\"invalid double dot\", \"..\", true},\n\t\t{\"invalid trailing slash-dot\", fmt.Sprintf(\"test%c.\", filepath.Separator), true},\n\t\t{\"invalid trailing slash-dot-dot\", fmt.Sprintf(\"test%c..\", filepath.Separator), true},\n\t\t{\"valid name with trailing .\", \"abc.\", false},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tm := defaultTestModel(testChildDir)\n\n\t\tTeaUpdate(m, nil)\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.FilePanelItemCreate[0]))\n\n\t\tassert.Empty(t, m.typingModal.errorMesssage)\n\n\t\tm.typingModal.textInput.SetValue(tt.fileName)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.ConfirmTyping[0]))\n\n\t\tif tt.expectedError {\n\t\t\tassert.NotEmpty(t, m.typingModal.errorMesssage, \"expected an error for input: %q\", tt.fileName)\n\t\t} else {\n\t\t\tassert.Empty(t, m.typingModal.errorMesssage, \"expected an error for input: %q\", tt.fileName)\n\t\t\tassert.FileExists(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(testChildDir, tt.fileName),\n\t\t\t\t\"expected file to be created: %q\",\n\t\t\t\ttt.fileName,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestFileRename(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\tfile1 := filepath.Join(curTestDir, \"file1.txt\")\n\tfile2 := filepath.Join(curTestDir, \"file2.txt\")\n\tfile3 := filepath.Join(curTestDir, \"file3.txt\")\n\n\tutils.SetupFilesWithData(t, []byte(\"f1\"), file1)\n\tutils.SetupFilesWithData(t, []byte(\"f2\"), file2)\n\tutils.SetupFilesWithData(t, []byte(\"f3\"), file3)\n\n\tfile1New := filepath.Join(curTestDir, \"file1_new.txt\")\n\n\tt.Run(\"Basic rename\", func(t *testing.T) {\n\t\tm := defaultTestModel(curTestDir)\n\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\t\tsetFilePanelSelectedItemByLocation(t, m.getFocusedFilePanel(), file1)\n\n\t\tp.SendKey(common.Hotkeys.FilePanelItemRename[0])\n\t\tp.SendKey(\"_new\")\n\t\tp.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\t_, err1 := os.Stat(file1)\n\t\t\t_, err1New := os.Stat(file1New)\n\t\t\treturn err1New == nil && os.IsNotExist(err1)\n\t\t}, DefaultTestTimeout, DefaultTestTick, \"File never got renamed\")\n\t})\n\n\tt.Run(\"Rename confirmation for same name\", func(t *testing.T) {\n\t\tactualTest := func(doRename bool) {\n\t\t\tm := defaultTestModel(curTestDir)\n\t\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\t\t\tsetFilePanelSelectedItemByLocation(t, m.getFocusedFilePanel(), file3)\n\n\t\t\tp.SendKey(common.Hotkeys.FilePanelItemRename[0])\n\t\t\tp.Send(tea.KeyMsg{Type: tea.KeyBackspace})\n\t\t\tp.SendKey(\"2\")\n\t\t\tp.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\t\t\trequire.Eventually(t, func() bool {\n\t\t\t\treturn m.notifyModel.IsOpen()\n\t\t\t}, DefaultTestTimeout, DefaultTestTick,\n\t\t\t\t\"Notify modal never opened, renaming text : %v\", m.getFocusedFilePanel().Rename.Value())\n\n\t\t\tassert.Equal(t, notify.New(true,\n\t\t\t\tcommon.SameRenameWarnTitle,\n\t\t\t\tcommon.SameRenameWarnContent,\n\t\t\t\tnotify.RenameAction), m.notifyModel, \"Notify model should be as expected\")\n\n\t\t\tif doRename {\n\t\t\t\tp.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\t} else {\n\t\t\t\tp.SendKey(common.Hotkeys.CancelTyping[0])\n\t\t\t}\n\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\t_, err2 := os.Stat(file2)\n\t\t\t\t_, err3 := os.Stat(file3)\n\t\t\t\tf2Data, err := os.ReadFile(file2)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tif doRename {\n\t\t\t\t\t// f3 should be gone. f2 should have content of f3\n\t\t\t\t\treturn os.IsNotExist(err3) && err2 == nil &&\n\t\t\t\t\t\tstring(f2Data) == \"f3\"\n\t\t\t\t}\n\t\t\t\treturn err2 == nil && err3 == nil\n\t\t\t}, DefaultTestTimeout, DefaultTestTick,\n\t\t\t\t\"Rename could not be done/not done appropriately\")\n\t\t}\n\n\t\tactualTest(false)\n\t\tactualTest(true)\n\t})\n}\n\nfunc isTrashed(fileAbsPath string) bool {\n\tfileName := filepath.Base(fileAbsPath)\n\tswitch runtime.GOOS {\n\tcase utils.OsDarwin:\n\t\t_, err := os.Stat(filepath.Join(variable.DarwinTrashDirectory, fileName))\n\t\treturn err == nil\n\tcase utils.OsLinux:\n\t\t_, err := trash.Stat(fileAbsPath)\n\t\treturn err == nil\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc TestFileDelete(t *testing.T) {\n\tif runtime.GOOS == utils.OsWindows {\n\t\tt.Skip(\"Skipping for windows\")\n\t}\n\tcurTestDir := t.TempDir()\n\tfile1 := filepath.Join(curTestDir, \"file1.txt\")\n\tfile2 := filepath.Join(curTestDir, \"file2.txt\")\n\n\tutils.SetupFilesWithData(t, []byte(\"f1\"), file1)\n\tutils.SetupFilesWithData(t, []byte(\"f2\"), file2)\n\n\ttestdata := []struct {\n\t\tname            string\n\t\tfilePath        string\n\t\tpermanentDelete bool\n\t}{\n\t\t{\n\t\t\tname:            \"Move to trash\",\n\t\t\tfilePath:        file1,\n\t\t\tpermanentDelete: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Permanently delete\",\n\t\t\tfilePath:        file2,\n\t\t\tpermanentDelete: true,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := defaultTestModel(curTestDir)\n\t\t\tm.hasTrash = common.InitTrash()\n\t\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\t\t\tsetFilePanelSelectedItemByLocation(t, m.getFocusedFilePanel(), tt.filePath)\n\t\t\tif tt.permanentDelete {\n\t\t\t\tp.SendKey(common.Hotkeys.PermanentlyDeleteItems[0])\n\t\t\t} else {\n\t\t\t\tp.SendKey(common.Hotkeys.DeleteItems[0])\n\t\t\t}\n\t\t\tassert.Eventually(t, m.notifyModel.IsOpen, DefaultTestTimeout,\n\t\t\t\tDefaultTestTick, \"Notify model never opened\")\n\t\t\texpectedTitle := common.TrashWarnTitle\n\t\t\texpectedAction := notify.DeleteAction\n\t\t\tif tt.permanentDelete {\n\t\t\t\texpectedTitle = common.PermanentDeleteWarnTitle\n\t\t\t\texpectedAction = notify.PermanentDeleteAction\n\t\t\t}\n\t\t\tassert.Equal(t, expectedTitle, m.notifyModel.GetTitle())\n\t\t\tassert.Equal(t, expectedAction, m.notifyModel.GetConfirmAction())\n\n\t\t\tp.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\t_, err := os.Stat(tt.filePath)\n\t\t\t\treturn err != nil && os.IsNotExist(err)\n\t\t\t}, DefaultTestTimeout, DefaultTestTick, \"File never removed from original location\")\n\n\t\t\t// Window's trash is not flexible enough for the check.\n\t\t\t// Sorry windows\n\t\t\tif runtime.GOOS == utils.OsDarwin || runtime.GOOS == utils.OsLinux {\n\t\t\t\tassert.Equal(t, tt.permanentDelete, !isTrashed(filepath.Base(tt.filePath)),\n\t\t\t\t\t\"Existence in trash status should be expected only of not permanently deleted\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/model_layout_test.go",
    "content": "package internal\n\n// TODO add two new tests for sidebar, a - with only one section, and b - without any sections.\n// note - we should update `testWithConfig` to take a new object of `common.ConfigType`, so that any custom config can be provided.\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\t\"github.com/yorukot/superfile/src/internal/ui/metadata\"\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\nconst ScrollDownCount = 10\nconst ScrollUpCount = 5\n\nfunc TestLayout(t *testing.T) {\n\t// This runs 800+ tests can be skipped via go test ./... -short\n\tif testing.Short() {\n\t\tt.Skip(\"skipping test in short mode.\")\n\t}\n\t// Uncomment this to debug locally.\n\t// This is to prevent too many logs in CICD\n\tutils.SetRootLoggerToDiscarded()\n\tt.Cleanup(func() {\n\t\tif testing.Verbose() {\n\t\t\tutils.SetRootLoggerToStdout(true)\n\t\t}\n\t})\n\n\tbaseTestDir := t.TempDir()\n\tsubDir := filepath.Join(baseTestDir, \"subdir\")\n\tsubDir2 := filepath.Join(baseTestDir, \"subdir2\")\n\tutils.SetupDirectories(t, baseTestDir, subDir, subDir2)\n\tutils.SetupFiles(t,\n\t\tfilepath.Join(baseTestDir, \"file1.txt\"),\n\t\tfilepath.Join(baseTestDir, \"file2.txt\"),\n\t\tfilepath.Join(baseTestDir, \"file3.txt\"),\n\t\tfilepath.Join(subDir, \"nested1.txt\"),\n\t\tfilepath.Join(subDir, \"nested2.txt\"),\n\t)\n\n\tsidebarWidths := []int{0, 5, 12, 20}\n\tpreviewWidths := []int{0, 2, 3, 10}\n\tpWDef := common.Config.FilePreviewWidth\n\tsWDef := common.Config.SidebarWidth\n\n\t// Note: These cannot run in parallel for now as they share the same\n\t// config global variable. Later we might fix that and use parallelization\n\tfor _, w := range sidebarWidths {\n\t\tt.Run(fmt.Sprintf(\"sW=%d;pW=%d\", w, pWDef), func(t *testing.T) {\n\t\t\ttestWithConfig(t, w, pWDef, false, baseTestDir)\n\t\t})\n\t}\n\tfor _, w := range previewWidths {\n\t\tt.Run(fmt.Sprintf(\"sW=%d;pW=%d\", sWDef, w), func(t *testing.T) {\n\t\t\ttestWithConfig(t, sWDef, w, false, baseTestDir)\n\t\t})\n\t}\n\n\t// One test for preview border enabled\n\tt.Run(\"sW=10;pW=5;previewWithBorder\", func(t *testing.T) {\n\t\ttestWithConfig(t, 10, 5, true, baseTestDir)\n\t})\n}\n\nfunc testWithConfig(t *testing.T, sidebarWidth int, previewWidth int,\n\tpreviewBorderEnabled bool, testPath string) {\n\t// Save original config values and restore them after test\n\torigSidebarWidth := common.Config.SidebarWidth\n\torigPreviewWidth := common.Config.FilePreviewWidth\n\torigPreviewBorderEnabled := common.Config.EnableFilePreviewBorder\n\tdefer func() {\n\t\tcommon.Config.SidebarWidth = origSidebarWidth\n\t\tcommon.Config.FilePreviewWidth = origPreviewWidth\n\t\tcommon.Config.EnableFilePreviewBorder = origPreviewBorderEnabled\n\t}()\n\n\t// Set test config\n\tcommon.Config.SidebarWidth = sidebarWidth\n\tcommon.Config.FilePreviewWidth = previewWidth\n\tcommon.Config.EnableFilePreviewBorder = previewBorderEnabled\n\n\tm := defaultTestModelWithFooterAndFilePreview(testPath)\n\tp := NewTestTeaProgWithEventLoop(t, m)\n\n\tresizeSizes := []struct {\n\t\twidth, height int\n\t}{\n\t\t{60, 24},   // Minimum\n\t\t{80, 30},   // Small HeightBreakC (<35)\n\t\t{100, 39},  // HeightBreakC\n\t\t{130, 44},  // HeightBreakD\n\t\t{200, 60},  // Large\n\t\t{400, 120}, // Extra large\n\t\t{91, 41},   // Back to medium\n\t\t{60, 24},   // Back to minimum\n\t}\n\n\t// Run resize tests\n\tfor _, size := range resizeSizes {\n\t\tt.Run(fmt.Sprintf(\"w=%d;h=%d\", size.width, size.height), func(t *testing.T) {\n\t\t\tupdateModelDimensionsAndValidate(t, p, size.width, size.height)\n\t\t})\n\t}\n\n\tt.Run(\"Edge cases\", func(t *testing.T) {\n\t\tedgeCases := []struct {\n\t\t\tname          string\n\t\t\twidth, height int\n\t\t}{\n\t\t\t{\"Ultra-narrow\", 70, 100},\n\t\t\t{\"Ultra-wide\", 500, 30},\n\t\t\t{\"Boundary-79\", 79, 30},\n\t\t\t{\"Boundary-80\", 80, 30},\n\t\t\t{\"Boundary-81\", 81, 30},\n\t\t\t{\"Below-minimum\", 59, 23},\n\t\t}\n\n\t\tfor _, tc := range edgeCases {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tp.SendDirectly(tea.WindowSizeMsg{Width: tc.width, Height: tc.height})\n\t\t\t\tassertLayoutValidity(t, p.m)\n\t\t\t})\n\t\t}\n\t})\n}\n\n// Note: this will create as many panels possible and leave the model in that state\n// This is to ensure that at time of resize operations, there are more panels\nfunc updateModelDimensionsAndValidate(t *testing.T, p *TeaProg, width int, height int) {\n\t// Set Footer OFF, Preview OFF via model state changes\n\t// if p.m.toggleFooter {\n\t//\tp.SendDirectly(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(common.Hotkeys.ToggleFooter[0])[0:1]})\n\t//}\n\t// File preview toggle - just send the key, no need to check state\n\t// Sending toggle key will turn it off if it's on\n\n\trequire.True(t, p.m.toggleFooter)\n\trequire.True(t, p.m.fileModel.FilePreview.IsOpen())\n\n\ttestdata := []struct {\n\t\tname string\n\t\tmsg  []tea.Msg\n\t}{\n\t\t{\n\t\t\tname: \"Resize\",\n\t\t\tmsg:  []tea.Msg{tea.WindowSizeMsg{Width: width, Height: height}},\n\t\t},\n\t\t{\n\t\t\tname: \"FooterOffPreviewOff\",\n\t\t\tmsg: []tea.Msg{\n\t\t\t\tutils.TeaRuneKeyMsg(common.Hotkeys.ToggleFooter[0]),\n\t\t\t\tutils.TeaRuneKeyMsg(common.Hotkeys.ToggleFilePreviewPanel[0]),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ToggleFooterOn\",\n\t\t\tmsg:  []tea.Msg{utils.TeaRuneKeyMsg(common.Hotkeys.ToggleFooter[0])},\n\t\t},\n\t\t{\n\t\t\tname: \"FooterOffPreviewOn\",\n\t\t\tmsg: []tea.Msg{\n\t\t\t\tutils.TeaRuneKeyMsg(common.Hotkeys.ToggleFooter[0]),\n\t\t\t\tutils.TeaRuneKeyMsg(common.Hotkeys.ToggleFilePreviewPanel[0]),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"FooterOnAgain\",\n\t\t\tmsg:  []tea.Msg{utils.TeaRuneKeyMsg(common.Hotkeys.ToggleFooter[0])},\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfor _, msg := range tt.msg {\n\t\t\t\tp.SendDirectly(msg)\n\t\t\t}\n\t\t\tassertLayoutValidity(t, p.m)\n\t\t})\n\t}\n\n\tt.Run(\"FilePanelRemoval\", func(t *testing.T) {\n\t\tfor {\n\t\t\tinitialCount := p.m.fileModel.PanelCount()\n\n\t\t\tp.SendDirectly(utils.TeaRuneKeyMsg(common.Hotkeys.CloseFilePanel[0]))\n\t\t\tassertLayoutValidity(t, p.m)\n\n\t\t\tif p.m.fileModel.PanelCount() == initialCount {\n\t\t\t\tbreak // No panel was removed\n\t\t\t}\n\t\t\trequire.Positive(t, p.m.fileModel.PanelCount())\n\t\t}\n\t})\n\n\ttestModelScrolling(t, p)\n\n\tt.Run(\"FilePanelCreation\", func(t *testing.T) {\n\t\tfor {\n\t\t\tinitialCount := p.m.fileModel.PanelCount()\n\t\t\tp.SendDirectly(utils.TeaRuneKeyMsg(common.Hotkeys.CreateNewFilePanel[0]))\n\n\t\t\tassertLayoutValidity(t, p.m)\n\n\t\t\tif p.m.fileModel.PanelCount() == initialCount {\n\t\t\t\tbreak // No new panel created\n\t\t\t}\n\n\t\t\trequire.LessOrEqual(t, p.m.fileModel.PanelCount(), common.FilePanelMax,\n\t\t\t\t\"Panel count should not exceed maximum\")\n\t\t}\n\t})\n\n\tassert.Equal(t, p.m.fileModel.MaxFilePanel, p.m.fileModel.PanelCount())\n}\n\nfunc testModelScrolling(t *testing.T, p *TeaProg) {\n\t// We are at Filepanel now\n\ttestModelScrollingCore(t, p)\n\n\t// Add dummy data to ProcessBar and Metadata\n\tfor i := range 10 {\n\t\tp.m.processBarModel.AddProcess(\n\t\t\tprocessbar.NewProcess(strconv.Itoa(i), \"test\", processbar.OpCopy, 1),\n\t\t)\n\t}\n\tdummyData := [][2]string{\n\t\t{\"a\", \"b\"},\n\t\t{\"a\", \"b\"},\n\t\t{\"a\", \"b\"},\n\t\t{\"a\", \"b\"},\n\t\t{\"a\", \"b\"},\n\t}\n\tp.m.fileMetaData.SetMetadata(metadata.NewMetadata(dummyData, \"\", \"\"), true)\n\n\tpanels := []struct {\n\t\tname     string\n\t\tfocusKey string\n\t}{\n\t\t{\"Sidebar\", common.Hotkeys.FocusOnSidebar[0]},\n\t\t{\"ProcessBar\", common.Hotkeys.FocusOnProcessBar[0]},\n\t\t{\"Metadata\", common.Hotkeys.FocusOnMetaData[0]},\n\t}\n\n\tfor _, panel := range panels {\n\t\tt.Run(panel.name+\"Scrolling\", func(t *testing.T) {\n\t\t\tp.SendKeyDirectly(panel.focusKey)\n\t\t\t// TODO: Add validation that we are actually at sidebar\n\t\t\ttestModelScrollingCore(t, p)\n\t\t})\n\t}\n}\n\nfunc testModelScrollingCore(t *testing.T, p *TeaProg) {\n\tfor range ScrollDownCount {\n\t\tp.SendDirectly(tea.KeyMsg{Type: tea.KeyDown})\n\t}\n\tassertLayoutValidity(t, p.m)\n\n\t// Scroll up\n\tfor range ScrollUpCount {\n\t\tp.SendDirectly(tea.KeyMsg{Type: tea.KeyUp})\n\t}\n\tassertLayoutValidity(t, p.m)\n}\n\nfunc assertLayoutValidity(t *testing.T, m *model) {\n\t// Skip for edge conditions where terminal is too small\n\tif m.fullHeight < common.MinimumHeight || m.fullWidth < common.MinimumWidth {\n\t\treturn // Terminal too small for valid layout\n\t}\n\tif m.fileModel.SinglePanelWidth < filepanel.MinWidth {\n\t\treturn // Panels too narrow for valid layout\n\t}\n\n\treturnFirstError := func() error {\n\t\tif err := m.validateLayout(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.validateComponentRender(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := m.validateFinalRender(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\terr := returnFirstError()\n\t// Not using assert to prevent `getLayoutInfoForDebug` getting called\n\t// in happy case. This is hot-path for 906 tests\n\tif err != nil {\n\t\tt.Errorf(\"validations failed, error : %v, layout info : %v\",\n\t\t\terr, getLayoutInfoForDebug(m))\n\t}\n}\n\nfunc getLayoutInfoForDebug(m *model) string {\n\tfirstPanel := m.fileModel.FilePanels[0]\n\tlastPanel := m.fileModel.FilePanels[m.fileModel.PanelCount()-1]\n\tlocation := m.getFocusedFilePanel().Location\n\twidth := fmt.Sprintf(\"width=%d[sidebar=%d,filemodel=%d\"+\n\t\t\"[firstpanel=%d,lastpanel=%d,previewExp=%d,previewActual=%d]]\"+\n\t\t\"[panelCount=%d,maxPanel=%d]\"+\n\t\t\"[processbarWidth=%d,clipboardWidth=%d]\",\n\t\tm.fullWidth, common.Config.SidebarWidth, m.fileModel.Width,\n\t\tfirstPanel.GetWidth(), lastPanel.GetWidth(), m.fileModel.ExpectedPreviewWidth,\n\t\tm.fileModel.FilePreview.GetContentWidth(),\n\t\tm.fileModel.PanelCount(), m.fileModel.MaxFilePanel,\n\t\tm.processBarModel.GetWidth(), m.clipboard.GetWidth())\n\n\theight := fmt.Sprintf(\"height=%d[fileModel=%d[firstPanel=%d,previewActual=%d],footer=%d]\",\n\t\tm.fullHeight, m.fileModel.Height, firstPanel.GetHeight(),\n\t\tm.fileModel.FilePreview.GetContentHeight(), m.footerHeight)\n\n\treturn fmt.Sprintf(\"%s %s location=%s\", width, height, location)\n}\n"
  },
  {
    "path": "src/internal/model_msg.go",
    "content": "package internal\n\nimport (\n\t\"log/slog\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/metadata\"\n\t\"github.com/yorukot/superfile/src/internal/ui/notify\"\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\ntype ModelUpdateMessage interface {\n\tApplyToModel(m *model) tea.Cmd\n\tGetReqID() int\n}\n\ntype BaseMessage struct {\n\treqID int\n}\n\nfunc (msg BaseMessage) GetReqID() int {\n\treturn msg.reqID\n}\n\ntype PasteOperationMsg struct {\n\tBaseMessage\n\n\tstate processbar.ProcessState\n}\n\nfunc NewPasteOperationMsg(state processbar.ProcessState, reqID int) PasteOperationMsg {\n\treturn PasteOperationMsg{\n\t\tstate: state,\n\t\tBaseMessage: BaseMessage{\n\t\t\treqID: reqID,\n\t\t},\n\t}\n}\n\nfunc (msg PasteOperationMsg) ApplyToModel(m *model) tea.Cmd {\n\tif (msg.state == processbar.Failed || msg.state == processbar.Successful) && m.clipboard.IsCut() {\n\t\tm.clipboard.Reset(false)\n\t}\n\treturn nil\n}\n\ntype DeleteOperationMsg struct {\n\tBaseMessage\n\n\tstate processbar.ProcessState\n}\n\nfunc NewDeleteOperationMsg(state processbar.ProcessState, reqID int) DeleteOperationMsg {\n\treturn DeleteOperationMsg{\n\t\tstate: state,\n\t\tBaseMessage: BaseMessage{\n\t\t\treqID: reqID,\n\t\t},\n\t}\n}\n\nfunc (msg DeleteOperationMsg) ApplyToModel(m *model) tea.Cmd {\n\t// Remove selection\n\tm.getFocusedFilePanel().ResetSelected()\n\treturn nil\n}\n\ntype ProcessBarUpdateMsg struct {\n\tBaseMessage\n\n\tpMsg processbar.UpdateMsg\n}\n\nfunc (msg ProcessBarUpdateMsg) ApplyToModel(m *model) tea.Cmd {\n\tcmd, err := msg.pMsg.Apply(&m.processBarModel)\n\tif err != nil {\n\t\tslog.Error(\"Error applying processbar update\", \"error\", err)\n\t}\n\treturn processCmdToTeaCmd(cmd)\n}\n\ntype CompressOperationMsg struct {\n\tBaseMessage\n\n\tstate processbar.ProcessState\n}\n\nfunc NewCompressOperationMsg(state processbar.ProcessState, reqID int) CompressOperationMsg {\n\treturn CompressOperationMsg{\n\t\tstate: state,\n\t\tBaseMessage: BaseMessage{\n\t\t\treqID: reqID,\n\t\t},\n\t}\n}\n\nfunc (msg CompressOperationMsg) ApplyToModel(_ *model) tea.Cmd {\n\treturn nil\n}\n\ntype ExtractOperationMsg struct {\n\tBaseMessage\n\n\tstate processbar.ProcessState\n}\n\nfunc NewExtractOperationMsg(state processbar.ProcessState, reqID int) ExtractOperationMsg {\n\treturn ExtractOperationMsg{\n\t\tstate: state,\n\t\tBaseMessage: BaseMessage{\n\t\t\treqID: reqID,\n\t\t},\n\t}\n}\n\nfunc (msg ExtractOperationMsg) ApplyToModel(_ *model) tea.Cmd {\n\treturn nil\n}\n\ntype MetadataMsg struct {\n\tBaseMessage\n\n\tmeta            metadata.Metadata\n\tmetadataFocused bool\n}\n\nfunc NewMetadataMsg(meta metadata.Metadata, metadataFocused bool, reqID int) MetadataMsg {\n\treturn MetadataMsg{\n\t\tmeta:            meta,\n\t\tmetadataFocused: metadataFocused,\n\t\tBaseMessage: BaseMessage{\n\t\t\treqID: reqID,\n\t\t},\n\t}\n}\n\nfunc (msg MetadataMsg) ApplyToModel(m *model) tea.Cmd {\n\tm.fileMetaData.SetMetadataCache(msg.meta, msg.metadataFocused)\n\tselectedItem := m.getFocusedFilePanel().GetFocusedItemPtr()\n\tif selectedItem == nil {\n\t\tslog.Debug(\"Panel empty or cursor invalid. Ignoring MetadataMsg\")\n\t\treturn nil\n\t}\n\tif selectedItem.Location != msg.meta.GetPath() {\n\t\tslog.Debug(\"MetadataMsg for older files. Ignoring\",\n\t\t\t\"currentItem\", selectedItem.Location, \"msgItem\", msg.meta.GetPath())\n\t\treturn nil\n\t}\n\tif (m.focusPanel == metadataFocus) != msg.metadataFocused {\n\t\tslog.Debug(\"MetadataMsg for older state. Ignoring\",\n\t\t\t\"actualFocus\", m.focusPanel, \"msgFocus\", msg.metadataFocused)\n\t\treturn nil\n\t}\n\tm.fileMetaData.SetMetadata(msg.meta, msg.metadataFocused)\n\treturn nil\n}\n\ntype NotifyModalUpdateMsg struct {\n\tBaseMessage\n\n\tm notify.Model\n}\n\nfunc NewNotifyModalMsg(m notify.Model, reqID int) NotifyModalUpdateMsg {\n\treturn NotifyModalUpdateMsg{\n\t\tm: m,\n\t\tBaseMessage: BaseMessage{\n\t\t\treqID: reqID,\n\t\t},\n\t}\n}\n\nfunc (msg NotifyModalUpdateMsg) ApplyToModel(m *model) tea.Cmd {\n\tm.notifyModel = msg.m\n\treturn nil\n}\n"
  },
  {
    "path": "src/internal/model_navigation_test.go",
    "content": "package internal\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc TestFilePanelNavigation(t *testing.T) {\n\t/*\n\t\tWe want to test\n\t\t(1) Switching to parent directory\n\t\t(2) Switching to parent on being at root \"/\"\n\t\t(3) Entering current directory\n\t\t(4) Entering via cd / command\n\t\t(5) Cd to itself via cd . command\n\n\t\tMake sure to validate\n\t\t- Search bar is cleared\n\t\t- The cursor and render values are restored correctly\n\t*/\n\n\tcurTestDir := t.TempDir()\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tfile1 := filepath.Join(curTestDir, \"file1.txt\")\n\t// We >=3 files in dir1 and >=2 files in dir2\n\t// so that cursor=2, and cursor=1 are valid values.\n\tfile2 := filepath.Join(dir1, \"file2.txt\")\n\tfile3 := filepath.Join(dir1, \"file3.txt\")\n\tfile4 := filepath.Join(dir1, \"file4.txt\")\n\tfile5 := filepath.Join(dir2, \"file5.txt\")\n\tfile6 := filepath.Join(dir2, \"file6.txt\")\n\n\trootDir := \"/\"\n\n\tif runtime.GOOS == utils.OsWindows {\n\t\trootDir = \"\\\\\"\n\t}\n\n\tutils.SetupDirectories(t, dir1, dir2)\n\tutils.SetupFiles(t, file1, file2, file3, file4, file5, file6)\n\n\ttestdata := []struct {\n\t\tname           string\n\t\tstartDir       string\n\t\tresultDir      string\n\t\tstartCursor    int\n\t\tkeyInput       []string\n\t\tsearchBarClear bool\n\t}{\n\t\t{\n\t\t\tname:        \"Switch to parent\",\n\t\t\tstartDir:    dir1,\n\t\t\tresultDir:   curTestDir,\n\t\t\tstartCursor: 1,\n\t\t\tkeyInput: []string{\n\t\t\t\tcommon.Hotkeys.ParentDirectory[0],\n\t\t\t},\n\t\t\tsearchBarClear: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Switch to parent when at root\",\n\t\t\tstartDir:    rootDir,\n\t\t\tresultDir:   rootDir,\n\t\t\tstartCursor: 0,\n\t\t\tkeyInput: []string{\n\t\t\t\tcommon.Hotkeys.ParentDirectory[0],\n\t\t\t},\n\t\t\tsearchBarClear: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"Enter current directory\",\n\t\t\tstartDir:    curTestDir,\n\t\t\tresultDir:   dir2,\n\t\t\tstartCursor: 1,\n\t\t\tkeyInput: []string{\n\t\t\t\tcommon.Hotkeys.Confirm[0],\n\t\t\t},\n\t\t\tsearchBarClear: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Enter via cd command first dir\",\n\t\t\tstartDir:    curTestDir,\n\t\t\tresultDir:   dir1,\n\t\t\tstartCursor: 0,\n\t\t\tkeyInput: []string{\n\t\t\t\tcommon.Hotkeys.OpenSPFPrompt[0],\n\t\t\t\t// TODO : Have it quoted, once cd command supports quoted paths\n\t\t\t\t\"cd \" + dir1,\n\t\t\t\tcommon.Hotkeys.ConfirmTyping[0],\n\t\t\t},\n\t\t\tsearchBarClear: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"cd . should be ignored\",\n\t\t\tstartDir:    curTestDir,\n\t\t\tresultDir:   curTestDir,\n\t\t\tstartCursor: 2,\n\t\t\tkeyInput: []string{\n\t\t\t\tcommon.Hotkeys.OpenSPFPrompt[0],\n\t\t\t\t// TODO : Have it quoted, once cd command supports quoted paths\n\t\t\t\t\"cd .\",\n\t\t\t\tcommon.Hotkeys.ConfirmTyping[0],\n\t\t\t},\n\t\t\tsearchBarClear: false,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := defaultTestModel(tt.startDir)\n\t\t\tfor range tt.startCursor {\n\t\t\t\tm.getFocusedFilePanel().ListDown()\n\t\t\t}\n\t\t\trequire.Equal(t, tt.startCursor, m.getFocusedFilePanel().GetCursor())\n\t\t\toriginalRenderIndex := m.getFocusedFilePanel().GetRenderIndex()\n\t\t\tfor _, s := range tt.keyInput {\n\t\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(s))\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.resultDir, m.getFocusedFilePanel().Location)\n\n\t\t\tif tt.searchBarClear {\n\t\t\t\tassert.Empty(t, m.getFocusedFilePanel().SearchBar.Value())\n\t\t\t}\n\n\t\t\t// Go back to original directory\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(\"cd \"+tt.startDir))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\n\t\t\t// Make sure we have original cursor and render\n\t\t\tassert.Equal(t, tt.startCursor, m.getFocusedFilePanel().GetCursor())\n\t\t\tassert.Equal(t, originalRenderIndex, m.getFocusedFilePanel().GetRenderIndex())\n\t\t})\n\t}\n\n\tt.Run(\"Focus on current directory on navigation to parent directory\", func(t *testing.T) {\n\t\tm := defaultTestModel(dir2)\n\t\tp := NewTestTeaProgWithEventLoop(t, m)\n\t\tp.SendKey(common.Hotkeys.ParentDirectory[0])\n\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn m.getFocusedFilePanel().GetFocusedItem().Location == dir2 &&\n\t\t\t\tm.getFocusedFilePanel().GetCursor() == 1\n\t\t}, DefaultTestTimeout, DefaultTestTick)\n\t})\n}\n\nfunc TestCursorOutOfBoundsAfterDirectorySwitch(t *testing.T) {\n\t// Create two directories with different file counts\n\ttempDir := t.TempDir()\n\tdir1 := filepath.Join(tempDir, \"dir1\")\n\tdir2 := filepath.Join(tempDir, \"dir2\")\n\tutils.SetupDirectories(t, dir1, dir2)\n\n\tvar files1, files2 []string\n\tfor i := range 10 {\n\t\tfiles1 = append(files1, filepath.Join(dir1, string('a'+rune(i))+\".txt\"))\n\t}\n\tfor i := range 5 {\n\t\tfiles2 = append(files2, filepath.Join(dir2, string('a'+rune(i))+\".txt\"))\n\t}\n\tutils.SetupFiles(t, files1...)\n\tutils.SetupFiles(t, files2...)\n\n\t// Start with dir1\n\tm := defaultTestModel(dir1)\n\tp := NewTestTeaProgWithEventLoop(t, m)\n\n\t// It will immediately load as defaultTestModel does one sync TeaUpdate\n\tassert.Equal(t, 10, m.getFocusedFilePanel().ElemCount(),\n\t\t\"Should load 10 files in dir1\")\n\n\t// Move cursor to position 8 (near end of list)\n\tpanel := m.getFocusedFilePanel()\n\tfor range 8 {\n\t\tp.Send(tea.KeyMsg{Type: tea.KeyDown})\n\t}\n\n\t// Verify cursor is at position 8\n\tassert.Eventually(t, func() bool {\n\t\treturn m.getFocusedFilePanel().GetCursor() == 8\n\t}, DefaultTestTimeout, DefaultTestTick, \"Cursor should be at position 8\")\n\tt.Logf(\"Cursor at position %d with %d elements\", panel.GetCursor(), panel.ElemCount())\n\n\t// Navigate to dir2 (this saves cursor=8 in directoryRecords)\n\tnavigateToTargetDir(t, m, dir1, dir2)\n\n\tassert.Equal(t, dir2, m.getFocusedFilePanel().Location, \"Should be in dir2\")\n\tassert.Equal(t, 5, m.getFocusedFilePanel().ElemCount())\n\n\tfor i := 4; i < 10; i++ {\n\t\terr := os.Remove(files1[i])\n\t\trequire.NoError(t, err)\n\t}\n\tt.Log(\"Deleted 6 files from dir1 externally\")\n\n\t// Navigate back to dir1 (this restores cursor=8 from cache)\n\tnavigateToTargetDir(t, m, dir2, dir1)\n\tassert.Equal(t, 0, panel.GetCursor(), \"Cursor not restored as is from directoryRecords cache\")\n\tassert.NoError(t, panel.ValidateCursorAndRenderIndex(), \"panel not valid\")\n}\n\nfunc TestCursorRemembersParentPosition(t *testing.T) {\n\t/*\n\t\tWe want to test that the cursor remembers its position in the parent directory in 3 different cases\n\t\t(1) jump back from child with more elements than parent and near top of list of parent\n\t\t(2) jump back from child with less elements than parent and near end of list of parent\n\t\t(3) jump back from child with no elements\n\t*/\n\n\tcurTestDir := t.TempDir()\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tdir3 := filepath.Join(curTestDir, \"dir3\")\n\tdir4 := filepath.Join(curTestDir, \"dir4\")\n\tdir5 := filepath.Join(curTestDir, \"dir5\")\n\tfile1 := filepath.Join(dir2, \"file1.txt\")\n\tfile2 := filepath.Join(dir2, \"file2.txt\")\n\tfile3 := filepath.Join(dir2, \"file3.txt\")\n\tfile4 := filepath.Join(dir2, \"file4.txt\")\n\tfile5 := filepath.Join(dir2, \"file5.txt\")\n\tfile6 := filepath.Join(dir2, \"file6.txt\")\n\tfile7 := filepath.Join(dir2, \"file7.txt\")\n\tfile8 := filepath.Join(dir4, \"file8.txt\")\n\tfile9 := filepath.Join(dir4, \"file9.txt\")\n\n\tutils.SetupDirectories(t, dir1, dir2, dir3, dir4, dir5)\n\tutils.SetupFiles(t, file1, file2, file3, file4, file5, file6, file7, file8, file9)\n\n\tcases := []struct {\n\t\tname           string\n\t\tmoveDowns      int\n\t\tchildDir       string\n\t\texpectedCursor int\n\t}{\n\t\t{\"case1\", 1, dir2, 1},\n\t\t{\"case2\", 3, dir4, 3},\n\t\t{\"case3\", 4, dir5, 4},\n\t}\n\n\t// if a case fails the next case(s) fail also\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tm := defaultTestModel(curTestDir)\n\n\t\t\tfor range tc.moveDowns {\n\t\t\t\tm.getFocusedFilePanel().ListDown()\n\t\t\t}\n\n\t\t\toriginalRenderIndex := m.getFocusedFilePanel().GetRenderIndex()\n\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\treturn m.getFocusedFilePanel().GetCursor() == tc.expectedCursor\n\t\t\t}, DefaultTestTimeout, DefaultTestTick, \"Cursor should be at correct position\")\n\n\t\t\t// Move into child directory\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.Confirm[0]))\n\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\treturn m.getFocusedFilePanel().Location == tc.childDir\n\t\t\t}, DefaultTestTimeout, DefaultTestTick, \"Should have stepped into child directory\")\n\n\t\t\t// Go back to original directory\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.ParentDirectory[0]))\n\n\t\t\tassert.Eventually(t, func() bool {\n\t\t\t\treturn m.getFocusedFilePanel().Location == curTestDir\n\t\t\t}, DefaultTestTimeout, DefaultTestTick, \"Should have stepped into parent directory curTestDir\")\n\n\t\t\t// Make sure we have original cursor and render\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\ttc.expectedCursor,\n\t\t\t\tm.getFocusedFilePanel().GetCursor(),\n\t\t\t\t\"Should have remembered cursor position in parent\",\n\t\t\t)\n\t\t\tassert.Equal(t, originalRenderIndex, m.getFocusedFilePanel().GetRenderIndex())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/model_process_test.go",
    "content": "package internal\n\nimport \"testing\"\n\nfunc TestProcess(_ *testing.T) {\n\t// TODO :\n\t// We need to test - We could implement these checks in other tests like Test Copy\n\t// 1 - Successful process for Copy, Delete, Compress, Extract\n\t// Validate donetime, done value, state, etc.\n\t// 2 - Failed processes\n\n\t// TODO : Figure out a way test the progress updating. The fact that progress gradually updates.\n\t// 3 - Process progress tracking\n\n}\n"
  },
  {
    "path": "src/internal/model_prompt_test.go",
    "content": "package internal\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/adrg/xdg\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/prompt\"\n)\n\nfunc TestModel_Update_Prompt(t *testing.T) {\n\tcurTestDir := filepath.Join(testDir, \"TestPrompt\")\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tfile1 := filepath.Join(dir1, \"file1.txt\")\n\n\tutils.SetupDirectories(t, curTestDir, dir1, dir2)\n\tutils.SetupFiles(t, file1)\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(curTestDir)\n\t})\n\n\t// We want to test these. TODO : complete important tests\n\t// 1. Being able to open prompt\n\t// 1a. Open in shell mode, 1b. Open in prompt mode 1c. Switching between then\n\n\t// 2. Being able to execute shell commands\n\t// 3. Shell command failure is handled and prompt stays open\n\t// 4. Successful Model actions - Split, Cd, Open new panel\n\t// 4a. Working split\n\t// 4b. Working cd : cd to abs path, cd to relative path, cd to home\n\t// 4c. Working open : open to abs path, open to relative path, open to home\n\t// 5. Split - Failure due to reaching max no. of panels\n\t// 6. cd - failure due to invalid path\n\t// 7. open - failure due to reaching max no. of panels\n\t// 8. open - failure due to invalid path\n\t// 9. cd and open - handling absolute and relative paths correctly\n\t// 10. Model closing\n\t// 10a. Pressing escape or ctrl+c and model closes\n\t// 10b. Autoclose based on config\n\n\t// Dont test shell command substitution here.\n\n\t// We might want to wrap os command execution in an interface and\n\t// ? Use a mock os command executor to have timeouts, and\n\t// custom command behaviour\n\n\t// Other tests cases\n\t// -- UI\n\t// 1. Entire model's rendering with promptModel open/closed\n\t// 2. Rendering not breaking when user pastes/enter special character or too much text\n\t// 3. Prompt gets resized based on total screen size. And always fits in\n\n\t// -- Functionality\n\t// 1. Shell command Timeout. Testing timeout is a pain. We should use async, and configure low timeout\n\t// like 1 sec for testing\n\t// 2. In case we plan to show output, we need to test case of\n\t// too big Shell command output\n\n\ttestBasicPromptFunctionality(t, dir1)\n\ttestPanelOperations(t, dir1, dir2, curTestDir)\n\ttestDirectoryHandlingWithQuotes(t, curTestDir, dir1)\n\ttestShellCommandsWithQuotes(t, curTestDir, dir1)\n}\n\n// testBasicPromptFunctionality tests opening, closing and basic command execution\nfunc testBasicPromptFunctionality(t *testing.T, dir1 string) {\n\tt.Run(\"Basic Prompt Opening\", func(t *testing.T) {\n\t\tm := defaultTestModel(dir1)\n\t\tassert.False(t, m.promptModal.IsOpen())\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenCommandLine[0]))\n\t\tassert.True(t, m.promptModal.IsOpen())\n\t\tassert.True(t, m.promptModal.IsShellMode())\n\n\t\t// Switching between modes\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\t\tassert.False(t, m.promptModal.IsShellMode(), \"Pressing prompt key should switch to prompt mode\")\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenCommandLine[0]))\n\t\tassert.True(t, m.promptModal.IsShellMode(), \"Pressing shell key should switch to shell mode\")\n\n\t\t// Closing and opening in prompt mode\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.CancelTyping[0]))\n\t\tassert.False(t, m.promptModal.IsOpen())\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\t\tassert.True(t, m.promptModal.IsOpen())\n\t\tassert.False(t, m.promptModal.IsShellMode())\n\t})\n\n\tt.Run(\"Shell command execution\", func(t *testing.T) {\n\t\tm := defaultTestModel(dir1)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenCommandLine[0]))\n\t\t// Prefer cross platform command\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(\"mkdir test_dir\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded())\n\t\tassert.DirExists(t, filepath.Join(dir1, \"test_dir\"))\n\n\t\t// Invalid command shouldn't cause issues.\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(\"xyz_non_exisiting_command\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.False(t, m.promptModal.LastActionSucceeded())\n\t\tassert.True(t, m.promptModal.IsOpen())\n\t})\n\n\tt.Run(\"Model closing\", func(t *testing.T) {\n\t\tm := defaultTestModel(dir1)\n\t\tfor _, key := range common.Hotkeys.CancelTyping {\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\t\t\tassert.True(t, m.promptModal.IsOpen())\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(key))\n\t\t\tassert.False(t, m.promptModal.IsOpen(), \"Prompt should get closed\")\n\t\t}\n\t})\n}\n\n// testPanelOperations tests split, cd, and open panel operations\nfunc testPanelOperations(t *testing.T, dir1, dir2, curTestDir string) {\n\tt.Run(\"Split Panel\", func(t *testing.T) {\n\t\tm := defaultTestModel(dir1)\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\t\trequire.True(t, m.promptModal.IsOpen())\n\t\tfor len(m.fileModel.FilePanels) < m.fileModel.MaxFilePanel {\n\t\t\tprevCnt := len(m.fileModel.FilePanels)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.SplitCommand))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\trequire.Len(t, m.fileModel.FilePanels, prevCnt+1)\n\t\t\tassert.Equal(t, dir1, m.fileModel.FilePanels[prevCnt].Location)\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded())\n\t\t}\n\n\t\t// Now doing a split should fail\n\t\tprevCnt := len(m.fileModel.FilePanels)\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.SplitCommand))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.False(t, m.promptModal.LastActionSucceeded())\n\t\tassert.Len(t, m.fileModel.FilePanels, prevCnt)\n\t})\n\n\tt.Run(\"cd Panel\", func(t *testing.T) {\n\t\tm := defaultTestModel(dir1)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+\" \"+dir2))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd using absolute path should work\")\n\t\tassert.Equal(t, dir2, m.getFocusedFilePanel().Location)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+\" ..\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd using relative path should work\")\n\t\tassert.Equal(t, curTestDir, m.getFocusedFilePanel().Location)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+\" \"+filepath.Base(dir2)))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd using relative path should work\")\n\t\tassert.Equal(t, dir2, m.getFocusedFilePanel().Location)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+\" \"+filepath.Join(dir2, \"non_existing_dir\")))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.False(t, m.promptModal.LastActionSucceeded(), \"cd invalid abs path should not work\")\n\t\tassert.Equal(t, dir2, m.getFocusedFilePanel().Location)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+\" non_existing_dir\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.False(t, m.promptModal.LastActionSucceeded(), \"cd invalid relative path should not work\")\n\t\tassert.Equal(t, dir2, m.getFocusedFilePanel().Location)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+\" ~\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd using tilde should work\")\n\t\tassert.Equal(t, xdg.Home, m.getFocusedFilePanel().Location)\n\t})\n\n\tt.Run(\"open Panel\", func(t *testing.T) {\n\t\tm := defaultTestModel(dir1)\n\t\torgCnt := len(m.fileModel.FilePanels)\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" \"+dir2))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"open using absolute path should work\")\n\t\tassert.Equal(t, dir2, m.getFocusedFilePanel().Location)\n\n\t\tm.fileModel.CloseFilePanel()\n\t\tassert.Len(t, m.fileModel.FilePanels, orgCnt)\n\t\tassert.Equal(t, dir1, m.getFocusedFilePanel().Location)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" ../dir2\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"open using relative path should work\")\n\t\tassert.Equal(t, dir2, m.getFocusedFilePanel().Location)\n\n\t\tm.fileModel.CloseFilePanel()\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" ~\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"open using tilde should work\")\n\t\tassert.Equal(t, xdg.Home, m.getFocusedFilePanel().Location)\n\n\t\tm.fileModel.CloseFilePanel()\n\n\t\tuserHomeEnv := \"HOME\"\n\t\tif runtime.GOOS == utils.OsWindows {\n\t\t\tuserHomeEnv = \"USERPROFILE\"\n\t\t}\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+fmt.Sprintf(\" ${%s}\", userHomeEnv)))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"open using variable substitution should work\")\n\t\tassert.Equal(t, xdg.Home, m.getFocusedFilePanel().Location)\n\n\t\tm.fileModel.CloseFilePanel()\n\n\t\t// Note : resolving shell subsitution is flaky in windows.\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" $(echo \\\"~\\\")\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"open using command substitution should work\")\n\t\tassert.Equal(t, xdg.Home, m.getFocusedFilePanel().Location)\n\n\t\tm.fileModel.CloseFilePanel()\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" non_existing_dir\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.False(t, m.promptModal.LastActionSucceeded(), \"open using invalid relative path should not work\")\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" \"+filepath.Join(dir2, \"non_existing_dir\")))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.False(t, m.promptModal.LastActionSucceeded(), \"open using invalid abs path should not work\")\n\n\t\tfor len(m.fileModel.FilePanels) < m.fileModel.MaxFilePanel {\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" .\"))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded())\n\t\t}\n\n\t\t// Now doing a open should fail\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+\" .\"))\n\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\tassert.False(t, m.promptModal.LastActionSucceeded())\n\t})\n}\n\n// testDirectoryHandlingWithQuotes tests handling directories with spaces and quotes\nfunc testDirectoryHandlingWithQuotes(t *testing.T, curTestDir, dir1 string) {\n\tt.Run(\"Directory names with spaces and quotes\", func(t *testing.T) {\n\t\t// Create test directories with spaces and special characters\n\t\tdirWithSpaces := filepath.Join(curTestDir, \"dir with spaces\")\n\t\tdirWithQuotes := filepath.Join(curTestDir, \"dir'with'quotes\")\n\n\t\t// Windows doesn't allow double quotes in directory names\n\t\tvar dirWithSpecialChars, dirWithMixed string\n\t\tvar directoriesToCreate []string\n\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\t// On Windows, use alternative characters that don't conflict with filesystem restrictions\n\t\t\tdirWithSpecialChars = filepath.Join(curTestDir, `dir[with]quotes`)\n\t\t\tdirWithMixed = filepath.Join(curTestDir, `dir with 'mixed' [quotes]`)\n\t\t\tdirectoriesToCreate = []string{dirWithSpaces, dirWithQuotes, dirWithSpecialChars, dirWithMixed}\n\t\t} else {\n\t\t\t// On Unix-like systems, double quotes are allowed in directory names\n\t\t\tdirWithSpecialChars = filepath.Join(curTestDir, `dir\"with\"quotes`)\n\t\t\tdirWithMixed = filepath.Join(curTestDir, `dir with 'mixed' \"quotes\"`)\n\t\t\tdirectoriesToCreate = []string{dirWithSpaces, dirWithQuotes, dirWithSpecialChars, dirWithMixed}\n\t\t}\n\n\t\tutils.SetupDirectories(t, directoriesToCreate...)\n\n\t\tt.Run(\"cd with double quotes\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+` \"`+dirWithSpaces+`\"`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd with double quotes should work\")\n\t\t\tassert.Equal(t, dirWithSpaces, m.getFocusedFilePanel().Location)\n\t\t})\n\n\t\tt.Run(\"cd with single quotes\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+` '`+dirWithSpaces+`'`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd with single quotes should work\")\n\t\t\tassert.Equal(t, dirWithSpaces, m.getFocusedFilePanel().Location)\n\t\t})\n\n\t\tt.Run(\"cd with single quotes in path using double quotes\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+` \"`+dirWithQuotes+`\"`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd with single quotes in path should work\")\n\t\t\tassert.Equal(t, dirWithQuotes, m.getFocusedFilePanel().Location)\n\t\t})\n\n\t\tt.Run(\"cd with double quotes in path using single quotes\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+` '`+dirWithSpecialChars+`'`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd with double quotes in path should work\")\n\t\t\tassert.Equal(t, dirWithSpecialChars, m.getFocusedFilePanel().Location)\n\t\t})\n\n\t\tt.Run(\"cd with escaped spaces\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tTeaUpdate(\n\t\t\t\tm,\n\t\t\t\tutils.TeaRuneKeyMsg(prompt.CdCommand+` `+strings.ReplaceAll(dirWithSpaces, \" \", `\\ `)),\n\t\t\t)\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd with escaped spaces should work\")\n\t\t\tassert.Equal(t, dirWithSpaces, m.getFocusedFilePanel().Location)\n\t\t})\n\n\t\tt.Run(\"open with double quotes\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.OpenCommand+` \"`+dirWithSpaces+`\"`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"open with double quotes should work\")\n\t\t\tassert.Equal(t, dirWithSpaces, m.getFocusedFilePanel().Location)\n\n\t\t\tm.fileModel.CloseFilePanel()\n\t\t})\n\n\t\tt.Run(\"cd with quoted environment variable\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tuserHomeEnv := \"HOME\"\n\t\t\tif runtime.GOOS == utils.OsWindows {\n\t\t\t\tuserHomeEnv = \"USERPROFILE\"\n\t\t\t}\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+` \"${`+userHomeEnv+`}\"`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"cd with quoted env var should work\")\n\t\t\tassert.Equal(t, xdg.Home, m.getFocusedFilePanel().Location)\n\t\t})\n\n\t\tt.Run(\"cd with single quoted environment variable\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0]))\n\n\t\t\tuserHomeEnv := \"HOME\"\n\t\t\tif runtime.GOOS == utils.OsWindows {\n\t\t\t\tuserHomeEnv = \"USERPROFILE\"\n\t\t\t}\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(prompt.CdCommand+` '${`+userHomeEnv+`}'`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(\n\t\t\t\tt,\n\t\t\t\tm.promptModal.LastActionSucceeded(),\n\t\t\t\t\"cd with single quoted env var works in superfile (unlike bash)\",\n\t\t\t)\n\t\t\tassert.Equal(t, xdg.Home, m.getFocusedFilePanel().Location)\n\t\t})\n\t})\n}\n\n// testShellCommandsWithQuotes tests shell command execution with quoted arguments\nfunc testShellCommandsWithQuotes(t *testing.T, curTestDir, dir1 string) {\n\tt.Run(\"Shell command with quotes\", func(t *testing.T) {\n\t\tdirWithSpaces := filepath.Join(curTestDir, \"test dir with spaces\")\n\t\tutils.SetupDirectories(t, dirWithSpaces)\n\n\t\tt.Run(\"shell command with double quotes\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenCommandLine[0]))\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(`mkdir \"`+filepath.Join(dir1, \"new dir with spaces\")+`\"`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"shell command with quotes should work\")\n\t\t\tassert.DirExists(t, filepath.Join(dir1, \"new dir with spaces\"))\n\t\t})\n\n\t\tt.Run(\"shell command with single quotes\", func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenCommandLine[0]))\n\n\t\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(`mkdir '`+filepath.Join(dir1, \"another dir with spaces\")+`'`))\n\t\t\tTeaUpdate(m, tea.KeyMsg{Type: tea.KeyEnter})\n\t\t\tassert.True(t, m.promptModal.LastActionSucceeded(), \"shell command with single quotes should work\")\n\t\t\tassert.DirExists(t, filepath.Join(dir1, \"another dir with spaces\"))\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "src/internal/model_render.go",
    "content": "package internal\n\nimport (\n\t\"path/filepath\"\n\t\"strconv\"\n\n\tfilepreview \"github.com/yorukot/superfile/src/pkg/file_preview\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n)\n\nfunc (m *model) sidebarRender() string {\n\treturn m.sidebarModel.Render(m.focusPanel == sidebarFocus,\n\t\tm.getFocusedFilePanel().Location)\n}\n\nfunc (m *model) processBarRender() string {\n\treturn m.processBarModel.Render(m.focusPanel == processBarFocus)\n}\n\nfunc (m *model) terminalSizeWarnRender() string {\n\tfullWidthString := strconv.Itoa(m.fullWidth)\n\tfullHeightString := strconv.Itoa(m.fullHeight)\n\tminimumWidthString := strconv.Itoa(common.MinimumWidth)\n\tminimumHeightString := strconv.Itoa(common.MinimumHeight)\n\tif m.fullHeight < common.MinimumHeight {\n\t\tfullHeightString = common.TerminalTooSmall.Render(fullHeightString)\n\t}\n\tif m.fullWidth < common.MinimumWidth {\n\t\tfullWidthString = common.TerminalTooSmall.Render(fullWidthString)\n\t}\n\tfullHeightString = common.TerminalCorrectSize.Render(fullHeightString)\n\tfullWidthString = common.TerminalCorrectSize.Render(fullWidthString)\n\n\theightString := common.MainStyle.Render(\" Height = \")\n\treturn common.FullScreenStyle(m.fullHeight, m.fullWidth).Render(`Terminal size too small:`+\"\\n\"+\n\t\t\"Width = \"+fullWidthString+\n\t\theightString+fullHeightString+\"\\n\\n\"+\n\t\t\"Needed for current config:\"+\"\\n\"+\n\t\t\"Width = \"+common.TerminalCorrectSize.Render(minimumWidthString)+\n\t\theightString+common.TerminalCorrectSize.Render(minimumHeightString)) + filepreview.ClearKittyImages()\n}\n\nfunc (m *model) terminalSizeWarnAfterFirstRender() string {\n\tminimumWidthInt := common.Config.SidebarWidth + common.FilePanelWidthUnit*len(\n\t\tm.fileModel.FilePanels,\n\t) + common.FilePanelWidthUnit - 1\n\tminimumWidthString := strconv.Itoa(minimumWidthInt)\n\tfullWidthString := strconv.Itoa(m.fullWidth)\n\tfullHeightString := strconv.Itoa(m.fullHeight)\n\tminimumHeightString := strconv.Itoa(common.MinimumHeight)\n\n\tif m.fullHeight < common.MinimumHeight {\n\t\tfullHeightString = common.TerminalTooSmall.Render(fullHeightString)\n\t}\n\tif m.fullWidth < minimumWidthInt {\n\t\tfullWidthString = common.TerminalTooSmall.Render(fullWidthString)\n\t}\n\tfullHeightString = common.TerminalCorrectSize.Render(fullHeightString)\n\tfullWidthString = common.TerminalCorrectSize.Render(fullWidthString)\n\n\theightString := common.MainStyle.Render(\" Height = \")\n\treturn common.FullScreenStyle(m.fullHeight, m.fullWidth).Render(`You change your terminal size too small:`+\"\\n\"+\n\t\t\"Width = \"+fullWidthString+\n\t\theightString+fullHeightString+\"\\n\\n\"+\n\t\t\"Needed for current config:\"+\"\\n\"+\n\t\t\"Width = \"+common.TerminalCorrectSize.Render(minimumWidthString)+\n\t\theightString+common.TerminalCorrectSize.Render(minimumHeightString)) + filepreview.ClearKittyImages()\n}\n\nfunc (m *model) typineModalRender() string {\n\tpreviewPath := filepath.Join(m.typingModal.location, m.typingModal.textInput.Value())\n\n\tfileLocation := common.FilePanelTopDirectoryIconStyle.Render(\" \"+icon.Directory+icon.Space) +\n\t\tcommon.FilePanelTopPathStyle.Render(\n\t\t\tcommon.TruncateTextBeginning(previewPath, common.ModalWidth-common.InnerPadding, \"...\"),\n\t\t) + \"\\n\"\n\n\tconfirm := common.ModalConfirm.Render(\" (\" + common.Hotkeys.ConfirmTyping[0] + \") Create \")\n\tcancel := common.ModalCancel.Render(\" (\" + common.Hotkeys.CancelTyping[0] + \") Cancel \")\n\n\ttip := confirm +\n\t\tlipgloss.NewStyle().Background(common.ModalBGColor).Render(\"           \") +\n\t\tcancel\n\n\tvar err string\n\tif m.typingModal.errorMesssage != \"\" {\n\t\terr = \"\\n\\n\" + common.ModalErrorStyle.Render(m.typingModal.errorMesssage)\n\t}\n\t// TODO : Move this all to rendering package to avoid specifying newlines manually\n\treturn common.ModalBorderStyle(common.ModalHeight, common.ModalWidth).\n\t\tRender(fileLocation + \"\\n\" + m.typingModal.textInput.View() + \"\\n\\n\" + tip + err)\n}\n\nfunc (m *model) introduceModalRender() string {\n\ttitle := common.SidebarTitleStyle.Render(\" Thanks for using superfile!!\") +\n\t\tcommon.ModalStyle.Render(\"\\n You can read the following information before starting to use it!\")\n\tvimUserWarn := common.ProcessErrorStyle.Render(\"  ** Very importantly ** If you are a Vim/Nvim user, go to:\\n\" +\n\t\t\"  https://superfile.dev/configure/custom-hotkeys/ to change your hotkey settings!\")\n\tsubOne := common.SidebarTitleStyle.Render(\"  (1)\") +\n\t\tcommon.ModalStyle.Render(\" If this is your first time, make sure you read:\\n\"+\n\t\t\t\"      https://superfile.dev/getting-started/tutorial/\")\n\tsubTwo := common.SidebarTitleStyle.Render(\"  (2)\") +\n\t\tcommon.ModalStyle.Render(\" If you forget the relevant keys during use,\\n\"+\n\t\t\t\"      you can press \\\"?\\\" (shift+/) at any time to query the keys!\")\n\tsubThree := common.SidebarTitleStyle.Render(\"  (3)\") +\n\t\tcommon.ModalStyle.Render(\" For more customization you can refer to:\\n\"+\n\t\t\t\"      https://superfile.dev/\")\n\tsubFour := common.SidebarTitleStyle.Render(\"  (4)\") +\n\t\tcommon.ModalStyle.Render(\" Thank you again for using superfile.\\n\"+\n\t\t\t\"      If you have any questions, please feel free to ask at:\\n\"+\n\t\t\t\"      https://github.com/yorukot/superfile\\n\"+\n\t\t\t\"      Of course, you can always open a new issue to share your idea \\n\"+\n\t\t\t\"      or report a bug!\")\n\treturn common.FirstUseModal(m.helpMenu.GetHeight(), m.helpMenu.GetWidth()).\n\t\tRender(title + \"\\n\\n\" + vimUserWarn + \"\\n\\n\" + subOne + \"\\n\\n\" +\n\t\t\tsubTwo + \"\\n\\n\" + subThree + \"\\n\\n\" + subFour + \"\\n\\n\")\n}\n\nfunc (m *model) promptModalRender() string {\n\treturn m.promptModal.Render()\n}\n\nfunc (m *model) zoxideModalRender() string {\n\treturn m.zoxideModal.Render()\n}\n"
  },
  {
    "path": "src/internal/model_test.go",
    "content": "package internal\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/x/ansi\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n)\n\n/*\nThe purpose of this test file is to have the\n(1) common global data for tests\n(2) common setup for tests, and cleanup\n(3) Basic model fuctionality tests\n    - Initialization\n\t- Resize\n\t- Update\n\t- Quitting\n*/\n\n// Helps to have centralized cleanup\nvar testDir string //nolint: gochecknoglobals // One-time initialized, and then read-only global test variable\n\nfunc cleanupTestDir() {\n\terr := os.RemoveAll(testDir)\n\tif err != nil {\n\t\tfmt.Printf(\"error while cleaning up test directory, err : %v\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc TestMain(m *testing.M) {\n\terr := common.PopulateGlobalConfigs()\n\tif err != nil {\n\t\tfmt.Printf(\"error while populating config, err : %v\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// A cleanup before is required in case the previous test run had a panic, and then\n\t// deferred cleanup never executed\n\n\t// Create testDir\n\ttestDir = filepath.Join(os.TempDir(), \"spf_testdir\")\n\tcleanupTestDir()\n\tif err := os.Mkdir(testDir, 0o755); err != nil {\n\t\tfmt.Printf(\"error while creating test directory, err : %v\", err)\n\t\tos.Exit(1)\n\t}\n\tdefer cleanupTestDir()\n\n\tflag.Parse()\n\tif testing.Verbose() {\n\t\tutils.SetRootLoggerToStdout(true)\n\t} else {\n\t\tutils.SetRootLoggerToDiscarded()\n\t}\n\tm.Run()\n\t// Maybe catch panic\n}\n\nfunc TestBasic(t *testing.T) {\n\tcurTestDir := filepath.Join(testDir, \"TestBasic\")\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tfile1 := filepath.Join(dir1, \"file1.txt\")\n\n\tt.Run(\"Basic Checks\", func(t *testing.T) {\n\t\tutils.SetupDirectories(t, curTestDir, dir1, dir2)\n\t\tutils.SetupFiles(t, file1)\n\t\tt.Cleanup(func() {\n\t\t\tos.RemoveAll(curTestDir)\n\t\t})\n\n\t\tm := defaultTestModel(dir1)\n\n\t\t// Validate the most of the data stored in model object\n\t\t// Inspect model struct to see what more can be validated.\n\t\t// 1 - File panel location, cursor, render index, etc.\n\t\t// 2 - Directory Items are listed\n\t\t// 3 - sidebar items pinned items are listed\n\t\t// 4 - process panel is empty\n\t\t// 5 - clipboard is empty\n\t\t// 6 - model's dimenstion\n\n\t\tassert.Equal(t, dir1, m.getFocusedFilePanel().Location)\n\t})\n}\n\nfunc TestInitialFilePathPositionsCursorWindow(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\n\tutils.SetupDirectories(t, curTestDir, dir1)\n\n\tvar file7 string\n\tvar file2 string\n\tfor i := range 10 {\n\t\tf := filepath.Join(dir1, fmt.Sprintf(\"file%d.txt\", i))\n\t\tutils.SetupFiles(t, f)\n\t\tif i == 7 {\n\t\t\tfile7 = f\n\t\t}\n\t\tif i == 2 {\n\t\t\tfile2 = f\n\t\t}\n\t}\n\n\tm := defaultTestModel(dir1, file2, file7)\n\t// View port of 5\n\tTeaUpdate(m, tea.WindowSizeMsg{Width: common.MinimumWidth, Height: 10})\n\t// Uncomment below to understand the distribution\n\t// t.Logf(\"Heights : %d [%d - [%d] %d]\\n\", m.fullHeight, m.footerHeight, m.mainPanelHeight,\n\t//\tpanelElementHeight(m.mainPanelHeight))\n\trequire.Len(t, m.fileModel.FilePanels, 3)\n\tassert.Equal(t, dir1, m.fileModel.FilePanels[0].Location)\n\tassert.Equal(t, file2, m.fileModel.FilePanels[1].GetFocusedItem().Location)\n\tassert.Equal(t, 2, m.fileModel.FilePanels[1].GetCursor())\n\tassert.Equal(t, 0, m.fileModel.FilePanels[1].GetRenderIndex())\n\tassert.Equal(t, file7, m.fileModel.FilePanels[2].GetFocusedItem().Location)\n\tassert.Equal(t, 7, m.fileModel.FilePanels[2].GetCursor())\n\tassert.Equal(t, 3, m.fileModel.FilePanels[2].GetRenderIndex())\n}\n\nfunc TestQuit(t *testing.T) {\n\t// Test\n\t// 1 - Normal quit\n\t// 2 - Normal quit with running process causing a warn modal\n\t//     2a - Cancelling quit\n\t//     2b - Proceeding with the quit\n\t// 3 - Cd on quit test that LastDir is written on\n\n\tt.Run(\"Normal Quit\", func(t *testing.T) {\n\t\tm := defaultTestModel(testDir)\n\t\tassert.Equal(t, notQuitting, m.modelQuitState)\n\t\tcmd := TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.Quit[0]))\n\t\tassert.Equal(t, quitDone, m.modelQuitState)\n\t\tassert.True(t, IsTeaQuit(cmd))\n\t})\n\tt.Run(\"Quit with running process\", func(t *testing.T) {\n\t\tm := defaultTestModel(testDir)\n\t\tm.processBarModel.AddOrUpdateProcess(processbar.Process{\n\t\t\tState: processbar.InOperation,\n\t\t\tDone:  0,\n\t\t\tTotal: 100,\n\t\t\tID:    \"1\",\n\t\t})\n\n\t\tassert.Equal(t, notQuitting, m.modelQuitState)\n\t\tcmd := TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.Quit[0]))\n\t\tassert.Equal(t, quitConfirmationInitiated, m.modelQuitState)\n\t\tassert.False(t, IsTeaQuit(cmd))\n\n\t\t// Now we would be asked for confirmation.\n\t\t// Cancel the quit\n\t\tcmd = TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.CancelTyping[0]))\n\t\tassert.Equal(t, notQuitting, m.modelQuitState)\n\t\tassert.False(t, IsTeaQuit(cmd))\n\n\t\t// Again trigger quit\n\t\tcmd = TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.Quit[0]))\n\t\tassert.Equal(t, quitConfirmationInitiated, m.modelQuitState)\n\t\tassert.False(t, IsTeaQuit(cmd))\n\n\t\t// Confirm this time\n\t\tcmd = TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.Confirm[0]))\n\t\tassert.Equal(t, quitDone, m.modelQuitState)\n\t\tassert.True(t, IsTeaQuit(cmd))\n\t})\n\n\tt.Run(\"Cd on quit test that LastDir is written on\", func(t *testing.T) {\n\t\tlastDirFile := filepath.Join(variable.SuperFileStateDir, \"lastdir\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(lastDirFile), 0o755))\n\t\tm := defaultTestModel(testDir)\n\n\t\tassert.Equal(t, notQuitting, m.modelQuitState)\n\n\t\tcmd := TeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.CdQuit[0]))\n\n\t\tassert.Equal(t, quitDone, m.modelQuitState)\n\t\tassert.True(t, IsTeaQuit(cmd))\n\n\t\tdata, err := os.ReadFile(lastDirFile)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"cd '\"+testDir+\"'\", string(data), \"LastDir file should contain the tempDir path\")\n\n\t\terr = os.Remove(lastDirFile)\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestChooserFile(t *testing.T) {\n\t// 1 - No quit - blank chooser file\n\t// 2 - Quit with valid chooser file\n\t//     2a - file preview\n\t//     2b - directory preview\n\t// 3 - No quit - invalid chooser file\n\tcurTestDir := filepath.Join(testDir, \"TestChooserFile\")\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tfile1 := filepath.Join(dir1, \"file1.txt\")\n\ttestChooserFile := filepath.Join(dir2, \"chooser_file.txt\")\n\tutils.SetupDirectories(t, curTestDir, dir1, dir2)\n\tutils.SetupFiles(t, file1)\n\n\ttestdata := []struct {\n\t\tname            string\n\t\tchooserFile     string\n\t\thotkey          string\n\t\texpectedQuit    bool\n\t\texpectedContent string\n\t}{\n\t\t{\n\t\t\tname:            \"Open with default app with valid chooser file\",\n\t\t\tchooserFile:     testChooserFile,\n\t\t\thotkey:          common.Hotkeys.Confirm[0],\n\t\t\texpectedQuit:    true,\n\t\t\texpectedContent: file1,\n\t\t},\n\t\t{\n\t\t\tname:            \"Open with file editor with valid chooser file\",\n\t\t\tchooserFile:     testChooserFile,\n\t\t\thotkey:          common.Hotkeys.OpenFileWithEditor[0],\n\t\t\texpectedQuit:    true,\n\t\t\texpectedContent: file1,\n\t\t},\n\t\t{\n\t\t\tname:            \"Open with directory editor valid chooser file\",\n\t\t\thotkey:          common.Hotkeys.OpenCurrentDirectoryWithEditor[0],\n\t\t\tchooserFile:     testChooserFile,\n\t\t\texpectedQuit:    true,\n\t\t\texpectedContent: dir1,\n\t\t},\n\t\t{\n\t\t\tname:            \"Open with file editor with Blank chooser file\",\n\t\t\tchooserFile:     \"\",\n\t\t\thotkey:          common.Hotkeys.OpenFileWithEditor[0],\n\t\t\texpectedQuit:    false,\n\t\t\texpectedContent: \"\",\n\t\t},\n\t\t{\n\t\t\tname:            \"Open with file editor with Invalid chooser file\",\n\t\t\tchooserFile:     filepath.Join(curTestDir, \"non_existent_dir\", \"file.txt\"),\n\t\t\thotkey:          common.Hotkeys.OpenFileWithEditor[0],\n\t\t\texpectedQuit:    false,\n\t\t\texpectedContent: \"\",\n\t\t},\n\t}\n\n\t// Must be sequential as we are using global variable chooserfile\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := defaultTestModel(dir1)\n\t\t\tif tt.expectedQuit {\n\t\t\t\terr := os.WriteFile(tt.chooserFile, []byte{}, 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tvariable.SetChooserFile(tt.chooserFile)\n\t\t\tcmd := TeaUpdate(m, utils.TeaRuneKeyMsg(tt.hotkey))\n\n\t\t\tif tt.expectedQuit {\n\t\t\t\tassert.Equal(t, quitDone, m.modelQuitState)\n\t\t\t\tassert.True(t, IsTeaQuit(cmd))\n\t\t\t\tassert.FileExists(t, tt.chooserFile)\n\t\t\t\tdata, err := os.ReadFile(tt.chooserFile)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expectedContent, string(data))\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, notQuitting, m.modelQuitState)\n\t\t\t\tassert.False(t, IsTeaQuit(cmd))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc eventuallyEnsurePreviewContent(t *testing.T, m *model, content string, msgAndArgs ...any) {\n\tcontains := false\n\tassert.Eventually(t, func() bool {\n\t\tcontains = strings.Contains(m.fileModel.FilePreview.GetContent(), content)\n\t\treturn contains\n\t}, DefaultTestTimeout, DefaultTestTick, msgAndArgs...)\n\tif !contains {\n\t\tpContent := ansi.Strip(m.fileModel.FilePreview.GetContent())\n\t\tpContent = pContent[:min(len(pContent), 20)]\n\t\tt.Logf(\"%s was not found in '%s'\", content, pContent)\n\t}\n}\n\nfunc TestAsyncPreviewPanelSync(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\n\toriginalPreviewWidth := common.Config.FilePreviewWidth\n\tcommon.Config.FilePreviewWidth = 0\n\tt.Cleanup(func() {\n\t\tcommon.Config.FilePreviewWidth = originalPreviewWidth\n\t})\n\n\tfile1, content1 := filepath.Join(curTestDir, \"file1.txt\"), \"File 1 content\"\n\tfile2, content2 := filepath.Join(curTestDir, \"file2.txt\"), \"File 2 content\"\n\tutils.SetupFilesWithData(t, []byte(content1), file1)\n\tutils.SetupFilesWithData(t, []byte(content2), file2)\n\n\tm := defaultTestModelWithFilePreview(curTestDir)\n\tp := NewTestTeaProgWithEventLoop(t, m)\n\n\t// We need to send message via event loop to ensure that preview load command\n\t// is actually processed, also we want a size bigger than default\n\t// to allow more number of panels\n\tp.Send(tea.WindowSizeMsg{Width: 4 * DefaultTestModelWidth, Height: 4 * DefaultTestModelHeight})\n\n\teventuallyEnsurePreviewContent(t, m, content1, \"file1 content should load initially\")\n\tpW := m.fileModel.FilePreview.GetContentWidth()\n\n\t// Create two panels\n\tsplitPanelAsync(p)\n\tsplitPanelAsync(p)\n\teventuallyEnsurePreviewContent(t, m, content1, \"file1 content should reload after new panel\")\n\n\tassert.NotEqual(t, pW, m.fileModel.FilePreview.GetContentWidth(),\n\t\t\"width should change on new panel creation\")\n\n\tp.Send(tea.KeyMsg{Type: tea.KeyDown})\n\tt.Logf(\"Current element : %s\", m.getFocusedFilePanel().GetFocusedItem().Location)\n\teventuallyEnsurePreviewContent(t, m, content2, \"content should update to file2\")\n\n\tp.SendKey(common.Hotkeys.CloseFilePanel[0])\n\teventuallyEnsurePreviewContent(t, m, content1, \"content should update to file1 after closing panel\")\n\n\t// Upscale\n\tp.Send(tea.WindowSizeMsg{Width: 8 * DefaultTestModelWidth,\n\t\tHeight: 8 * DefaultTestModelHeight})\n\teventuallyEnsurePreviewContent(t, m, content1, \"content should update to file1 after resize\")\n\n\t// Downscale\n\tp.Send(tea.WindowSizeMsg{Width: 6 * DefaultTestModelWidth,\n\t\tHeight: 6 * DefaultTestModelHeight})\n\teventuallyEnsurePreviewContent(t, m, content1, \"content should update to file1 after resize\")\n}\n"
  },
  {
    "path": "src/internal/model_zoxide_test.go",
    "content": "package internal\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc setupProgAndOpenZoxide(t *testing.T, zClient *zoxidelib.Client, dir string) *TeaProg {\n\tt.Helper()\n\tcommon.Config.ZoxideSupport = true\n\tm := defaultTestModelWithZClient(zClient, dir)\n\tp := NewTestTeaProgWithEventLoop(t, m)\n\n\tp.SendKey(common.Hotkeys.OpenZoxide[0])\n\tassert.Eventually(t, func() bool {\n\t\treturn p.getModel().zoxideModal.IsOpen()\n\t}, DefaultTestTimeout, DefaultTestTick, \"Zoxide modal should open\")\n\treturn p\n}\n\nfunc updateCurrentFilePanelDirOfTestModel(t *testing.T, p *TeaProg, dir string) {\n\terr := p.getModel().updateCurrentFilePanelDir(dir)\n\trequire.NoError(t, err, \"Failed to navigate to %s\", dir)\n\tassert.Equal(t, dir, p.getModel().getFocusedFilePanel().Location, \"Should be in %s after navigation\", dir)\n}\n\nfunc TestZoxide(t *testing.T) {\n\tzoxideDataDir := t.TempDir()\n\tzClient, err := zoxidelib.New(zoxidelib.WithDataDir(zoxideDataDir))\n\tif err != nil {\n\t\tif runtime.GOOS != utils.OsLinux {\n\t\t\tt.Skipf(\"Skipping zoxide tests in non-Linux because zoxide client cannot be initialized\")\n\t\t} else {\n\t\t\tt.Fatalf(\"zoxide initialization failed\")\n\t\t}\n\t}\n\n\toriginalZoxideSupport := common.Config.ZoxideSupport\n\tdefer func() {\n\t\tcommon.Config.ZoxideSupport = originalZoxideSupport\n\t}()\n\n\tcurTestDir := filepath.Join(testDir, \"TestZoxide\")\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tdir3 := filepath.Join(curTestDir, \"dir3\")\n\tmultiSpaceDir := filepath.Join(curTestDir, \"test  dir\")\n\tutils.SetupDirectories(t, curTestDir, dir1, dir2, dir3, multiSpaceDir)\n\n\tt.Run(\"Zoxide tracking and navigation\", func(t *testing.T) {\n\t\tp := setupProgAndOpenZoxide(t, zClient, dir1)\n\t\tupdateCurrentFilePanelDirOfTestModel(t, p, dir2)\n\t\tupdateCurrentFilePanelDirOfTestModel(t, p, dir3)\n\n\t\tp.SendKey(\"dir2\")\n\t\tassert.Eventually(t, func() bool {\n\t\t\tresults := p.getModel().zoxideModal.GetResults()\n\t\t\treturn len(results) == 1 && results[0].Path == dir2\n\t\t}, DefaultTestTimeout, DefaultTestTick, \"dir2 should be found by zoxide UI search\")\n\n\t\t// Press enter to navigate to dir2\n\t\tp.SendKey(common.Hotkeys.ConfirmTyping[0])\n\t\t// Wait for both modal to close AND location to change to avoid race condition\n\t\tassert.Eventually(t, func() bool {\n\t\t\treturn !p.getModel().zoxideModal.IsOpen() &&\n\t\t\t\tp.getModel().getFocusedFilePanel().Location == dir2\n\t\t}, DefaultTestTimeout, DefaultTestTick,\n\t\t\t\"Zoxide modal should close and navigate to %s (current location: %s)\",\n\t\t\tdir2, p.getModel().getFocusedFilePanel().Location)\n\t})\n\n\tt.Run(\"Zoxide disabled shows no results\", func(t *testing.T) {\n\t\tcommon.Config.ZoxideSupport = false\n\t\tm := defaultTestModelWithZClient(zClient, dir1)\n\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenZoxide[0]))\n\t\tassert.True(t, m.zoxideModal.IsOpen(), \"Zoxide modal should open even when ZoxideSupport is disabled\")\n\n\t\tresults := m.zoxideModal.GetResults()\n\t\tassert.Empty(t, results, \"Zoxide modal should show no results when ZoxideSupport is disabled\")\n\t})\n\n\tt.Run(\"Zoxide modal size on window resize\", func(t *testing.T) {\n\t\tp := setupProgAndOpenZoxide(t, zClient, dir1)\n\n\t\tinitialWidth := p.getModel().zoxideModal.GetWidth()\n\t\tinitialMaxHeight := p.getModel().zoxideModal.GetMaxHeight()\n\n\t\tp.SendDirectly(tea.WindowSizeMsg{Width: 2 * DefaultTestModelWidth, Height: 2 * DefaultTestModelHeight})\n\n\t\tupdatedWidth := p.getModel().zoxideModal.GetWidth()\n\t\tupdatedMaxHeight := p.getModel().zoxideModal.GetMaxHeight()\n\t\tassert.Greater(t, updatedWidth, initialWidth, \"Width should increase with larger window\")\n\t\tassert.Greater(t, updatedMaxHeight, initialMaxHeight, \"MaxHeight should increase with larger window\")\n\t})\n\n\tt.Run(\"Zoxide 'z' key suppression on open\", func(t *testing.T) {\n\t\tp := setupProgAndOpenZoxide(t, zClient, dir1)\n\t\tassert.Empty(t, p.getModel().zoxideModal.GetTextInputValue(),\n\t\t\t\"The 'z' key should not be added to textInput\")\n\t\tp.SendKeyDirectly(\"abc\")\n\t\tassert.Equal(t, \"abc\", p.getModel().zoxideModal.GetTextInputValue())\n\t})\n\n\tt.Run(\"Multi-space directory name navigation\", func(t *testing.T) {\n\t\tp := setupProgAndOpenZoxide(t, zClient, dir1)\n\n\t\tupdateCurrentFilePanelDirOfTestModel(t, p, multiSpaceDir)\n\t\tupdateCurrentFilePanelDirOfTestModel(t, p, dir1)\n\n\t\tp.SendKey(filepath.Base(multiSpaceDir))\n\t\tassert.Eventually(t, func() bool {\n\t\t\tresults := p.getModel().zoxideModal.GetResults()\n\t\t\tfor _, result := range results {\n\t\t\t\tif result.Path == multiSpaceDir {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}, DefaultTestTimeout, DefaultTestTick, \"Multi-space directory should be found by zoxide\")\n\n\t\t// Reset textinput via Close-Open\n\t\tp.SendKey(common.Hotkeys.Quit[0])\n\t\tp.SendKey(common.Hotkeys.OpenZoxide[0])\n\n\t\tp.SendKey(\"di r 1\")\n\t\tassert.Eventually(t, func() bool {\n\t\t\tresults := p.getModel().zoxideModal.GetResults()\n\t\t\tfor _, result := range results {\n\t\t\t\tif result.Path == dir1 {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}, DefaultTestTimeout, DefaultTestTick, \"dir1 should be found by zoxide\")\n\t})\n\n\tt.Run(\"Zoxide escape key closes modal\", func(t *testing.T) {\n\t\tp := setupProgAndOpenZoxide(t, zClient, dir1)\n\t\tp.SendKeyDirectly(common.Hotkeys.CancelTyping[0])\n\t\tassert.False(t, p.getModel().zoxideModal.IsOpen(),\n\t\t\t\"Zoxide modal should close on escape key\")\n\t})\n}\n"
  },
  {
    "path": "src/internal/test_utils.go",
    "content": "package internal\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nconst DefaultTestTick = 10 * time.Millisecond\nconst DefaultTestTimeout = time.Second\nconst DefaultTestModelWidth = 2 * common.MinimumWidth\nconst DefaultTestModelHeight = 2 * common.MinimumHeight\n\n// -------------------- Model setup utils\n\nfunc defaultTestModel(dirs ...string) *model {\n\tm := defaultModelConfig(false, false, false, dirs, nil)\n\treturn setModelParamsForTest(m, true)\n}\n\n// TODO: Change this to a better API. passing opts\n// WithZClient(), WithFooter()\nfunc defaultTestModelWithZClient(zClient *zoxidelib.Client, dirs ...string) *model {\n\tm := defaultModelConfig(false, false, false, dirs, zClient)\n\treturn setModelParamsForTest(m, true)\n}\n\nfunc defaultTestModelWithFooterAndFilePreview(dirs ...string) *model {\n\tm := defaultModelConfig(false, true, false, dirs, nil)\n\treturn setModelParamsForTest(m, false)\n}\n\nfunc defaultTestModelWithFilePreview(dirs ...string) *model {\n\tm := defaultModelConfig(false, false, false, dirs, nil)\n\treturn setModelParamsForTest(m, false)\n}\n\nfunc setModelParamsForTest(m *model, disablePreview bool) *model {\n\tm.disableMetadata = true\n\tif disablePreview {\n\t\tm.fileModel.FilePreview.Close()\n\t}\n\t// async size updates like preview panel content update\n\t// will not be done\n\tTeaUpdate(m, tea.WindowSizeMsg{Width: DefaultTestModelWidth, Height: DefaultTestModelHeight})\n\treturn m\n}\n\n// Helper function to setup panel mode and selection\nfunc setupPanelModeAndSelection(t *testing.T, m *model, useSelectMode bool, itemName string, selectedItems []string) {\n\tt.Helper()\n\tpanel := m.getFocusedFilePanel()\n\n\tif useSelectMode {\n\t\t// Switch to select mode and set selected items\n\t\tm.getFocusedFilePanel().ChangeFilePanelMode()\n\t\trequire.Equal(t, filepanel.SelectMode, panel.PanelMode)\n\t\tfor _, item := range selectedItems {\n\t\t\tpanel.SetSelected(item)\n\t\t}\n\t} else {\n\t\t// Find the item in browser mode\n\t\tsetFilePanelSelectedItemByName(t, panel, itemName)\n\t}\n}\n\n// --------------------  Bubletea utilities\n\n// TODO : Should we validate that returned value is of type *model ?\n// and equal to m ? We are assuming that to be true as of now\nfunc TeaUpdate(m *model, msg tea.Msg) tea.Cmd {\n\t_, cmd := m.Update(msg)\n\treturn cmd\n}\n\n// Is the command tea.quit, or a batch that contains tea.quit\nfunc IsTeaQuit(cmd tea.Cmd) bool {\n\tif cmd == nil {\n\t\treturn false\n\t}\n\t// Ignore commands with longer IO Operations, which waits on a channel\n\tmsg := ExecuteTeaCmdWithTimeout(cmd, time.Millisecond)\n\tswitch msg := msg.(type) {\n\tcase tea.QuitMsg:\n\t\treturn true\n\tcase tea.BatchMsg:\n\t\tfor _, curCmd := range msg {\n\t\t\tif IsTeaQuit(curCmd) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc ExecuteTeaCmdWithTimeout(cmd tea.Cmd, timeout time.Duration) tea.Msg {\n\tresult := make(chan tea.Msg, 1)\n\tgo func() {\n\t\tresult <- cmd()\n\t}()\n\tselect {\n\tcase msg := <-result:\n\t\treturn msg\n\tcase <-time.After(timeout):\n\t\treturn nil\n\t}\n}\n\n// Helper function to perform copy or cut operation\nfunc performCopyOrCutOperation(t *testing.T, m *model, isCut bool) {\n\tt.Helper()\n\tif isCut {\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.CutItems[0]))\n\t} else {\n\t\tTeaUpdate(m, utils.TeaRuneKeyMsg(common.Hotkeys.CopyItems[0]))\n\t}\n}\n\n// -------------- Validation Utilities\n\n// Helper function to verify clipboard state after copy/cut\nfunc verifyClipboardState(t *testing.T, m *model, isCut bool, useSelectMode bool, selectedItemsCount int) {\n\tt.Helper()\n\tassert.Equal(t, isCut, m.clipboard.IsCut(), \"Clipboard cut state should match operation\")\n\tif useSelectMode {\n\t\tassert.Len(t, m.clipboard.GetItems(), selectedItemsCount, \"Clipboard should contain all selected items\")\n\t} else {\n\t\tassert.Len(t, m.clipboard.GetItems(), 1, \"Clipboard should contain one item\")\n\t}\n}\n\n// Helper function to verify file or directory exists\nfunc verifyPathExists(t *testing.T, path, message string) {\n\tt.Helper()\n\tinfo, err := os.Stat(path)\n\trequire.NoError(t, err, message)\n\tif info.IsDir() {\n\t\tassert.DirExists(t, path, message)\n\t} else {\n\t\tassert.FileExists(t, path, message)\n\t}\n}\n\n// Helper function to verify file or directory doesn't exist after cut\nfunc verifyPathNotExistsEventually(t *testing.T, path, message string) {\n\tt.Helper()\n\tassert.Eventually(t, func() bool {\n\t\t_, err := os.Stat(path)\n\t\treturn os.IsNotExist(err)\n\t}, DefaultTestTimeout, DefaultTestTick, message)\n}\n\n// Helper function to verify expected destination files exist\nfunc verifyDestinationFiles(t *testing.T, targetDir string, expectedDestFiles []string) {\n\tt.Helper()\n\tfor _, expectedFile := range expectedDestFiles {\n\t\tdestPath := filepath.Join(targetDir, expectedFile)\n\t\tassert.Eventually(t, func() bool {\n\t\t\t_, err := os.Stat(destPath)\n\t\t\treturn err == nil\n\t\t}, DefaultTestTimeout, DefaultTestTick, \"%s should exist in destination\", expectedFile)\n\t}\n}\n\n// Helper function to verify prevented paste results\nfunc verifyPreventedPasteResults(t *testing.T, m *model, originalPath string) {\n\tt.Helper()\n\tif originalPath != \"\" {\n\t\tverifyPathExists(t, originalPath, \"Original file should still exist when paste is prevented\")\n\t}\n\t// Clipboard should not be cleared when paste is prevented\n\tassert.NotEmpty(t, m.clipboard.GetItems(), \"Clipboard should not be cleared when paste is prevented\")\n}\n\n// Helper function to verify successful paste results\nfunc verifySuccessfulPasteResults(t *testing.T, targetDir string, expectedDestFiles []string,\n\toriginalPath string, shouldOriginalExist bool) {\n\tt.Helper()\n\t// Verify expected files were created in destination\n\tverifyDestinationFiles(t, targetDir, expectedDestFiles)\n\n\t// Verify original file existence based on operation type\n\tif originalPath != \"\" {\n\t\tif shouldOriginalExist {\n\t\t\tverifyPathExists(t, originalPath, \"Original file should exist after copy operation\")\n\t\t} else {\n\t\t\tverifyPathNotExistsEventually(t, originalPath, \"Original file should not exist after cut operation\")\n\t\t}\n\t}\n}\n\n// -------------- Other utilities\n// Helper function to navigate to target directory if different from start\nfunc navigateToTargetDir(t *testing.T, m *model, startDir, targetDir string) {\n\tt.Helper()\n\tif targetDir != startDir {\n\t\terr := m.updateCurrentFilePanelDir(targetDir)\n\t\trequire.NoError(t, err)\n\t\tTeaUpdate(m, nil)\n\t}\n}\n\n// Helper function to get original path for existence check\nfunc getOriginalPath(useSelectMode bool, itemName, startDir string) string {\n\tif !useSelectMode && itemName != \"\" {\n\t\treturn filepath.Join(startDir, itemName)\n\t}\n\treturn \"\"\n}\n\nfunc setFilePanelSelectedItemByLocation(t *testing.T, panel *filepanel.Model, filePath string) {\n\tt.Helper()\n\tidx := panel.FindElementIndexByLocation(filePath)\n\trequire.NotEqual(t, -1, idx, \"%s should be found in panel\", filePath)\n\tpanel.SetCursorPosition(idx)\n}\n\nfunc setFilePanelSelectedItemByName(t *testing.T, panel *filepanel.Model, fileName string) {\n\tt.Helper()\n\tidx := panel.FindElementIndexByName(fileName)\n\trequire.NotEqual(t, -1, idx, \"%s should be found in panel\", fileName)\n\tpanel.SetCursorPosition(idx)\n}\n\nfunc splitPanelAsync(p *TeaProg) {\n\tp.SendKey(common.Hotkeys.OpenSPFPrompt[0])\n\tp.SendKey(\"split\")\n\tp.Send(tea.KeyMsg{Type: tea.KeyEnter})\n\tp.Send(tea.KeyMsg{Type: tea.KeyEsc})\n}\n"
  },
  {
    "path": "src/internal/test_utils_teaprog.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\ntype IgnorerWriter struct{}\n\nfunc (w IgnorerWriter) Write(p []byte) (int, error) {\n\treturn len(p), nil\n}\n\ntype TeaProg struct {\n\tm    *model\n\tprog *tea.Program\n}\n\n// If you use this, make sure to handle cleanup\nfunc NewTeaProg(m *model, eventLoop bool) *TeaProg {\n\tp := &TeaProg{m: m, prog: tea.NewProgram(m, tea.WithInput(nil), tea.WithOutput(IgnorerWriter{}))}\n\tif eventLoop {\n\t\tp.StartEventLoop()\n\t}\n\treturn p\n}\n\nfunc NewTestTeaProgWithEventLoop(t *testing.T, m *model) *TeaProg {\n\tp := NewTeaProg(m, true)\n\tt.Cleanup(func() {\n\t\tp.Close()\n\t})\n\treturn p\n}\n\nfunc (p *TeaProg) getModel() *model {\n\treturn p.m\n}\n\nfunc (p *TeaProg) StartEventLoop() {\n\tgo func() {\n\t\t_, err := p.prog.Run()\n\t\t// This will return only after Run() is completed\n\t\t// This will not be error if its due to p.Close() being called\n\t\tif err != nil && !errors.Is(err, tea.ErrProgramKilled) {\n\t\t\tslog.Error(\"TeaProg finished with error\", \"error\", err)\n\t\t}\n\t}()\n\t// Send nil to block for start of event loop\n\tp.prog.Send(nil)\n}\n\nfunc (p *TeaProg) Send(msgs ...tea.Msg) {\n\tfor _, msg := range msgs {\n\t\tp.prog.Send(msg)\n\t}\n}\n\nfunc (p *TeaProg) SendKey(key string) {\n\tp.Send(utils.TeaRuneKeyMsg(key))\n}\n\n// Dont use eventloop and dont care about the tea.Cmd returned by Update()\nfunc (p *TeaProg) SendDirectly(msgs ...tea.Msg) tea.Cmd {\n\tcmds := make([]tea.Cmd, len(msgs))\n\tfor i, msg := range msgs {\n\t\tvar retModel tea.Model\n\t\tretModel, cmds[i] = p.m.Update(msg)\n\t\tif m, ok := retModel.(*model); ok {\n\t\t\tp.m = m\n\t\t} else {\n\t\t\t// This should never happen as we return *model on Update()\n\t\t\tpanic(\"model is not of type *model\")\n\t\t}\n\t}\n\n\treturn tea.Batch(cmds...)\n}\n\nfunc (p *TeaProg) SendKeyDirectly(key string) tea.Cmd {\n\treturn p.SendDirectly(utils.TeaRuneKeyMsg(key))\n}\n\nfunc (p *TeaProg) Close() {\n\tp.prog.Kill()\n}\n"
  },
  {
    "path": "src/internal/type.go",
    "content": "package internal\n\nimport (\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/helpmenu\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/clipboard\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sortmodel\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filemodel\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/metadata\"\n\t\"github.com/yorukot/superfile/src/internal/ui/notify\"\n\t\"github.com/yorukot/superfile/src/internal/ui/processbar\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sidebar\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/prompt\"\n\tzoxideui \"github.com/yorukot/superfile/src/internal/ui/zoxide\"\n)\n\n// Type representing the type of focused panel\ntype focusPanelType int\n\ntype modelQuitStateType int\n\n// Constants for panel with no focus\nconst (\n\tnonePanelFocus focusPanelType = iota\n\tprocessBarFocus\n\tsidebarFocus\n\tmetadataFocus\n)\n\nconst (\n\tnotQuitting modelQuitStateType = iota\n\tquitInitiated\n\tquitConfirmationInitiated\n\tquitConfirmationReceived\n\tquitDone\n)\n\n// Main model\n// TODO : We could consider using *model as tea.Model, instead of model.\n// for reducing re-allocations. The struct is 20K bytes. But this could lead to\n// issues like race conditions and whatnot, which are hidden since we are creating\n// new model in each tea update.\ntype model struct {\n\t// Main Panels\n\tfileModel       filemodel.Model\n\tsidebarModel    sidebar.Model\n\tprocessBarModel processbar.Model\n\tclipboard       clipboard.Model\n\tfocusPanel      focusPanelType\n\n\t// Modals\n\tnotifyModel notify.Model\n\ttypingModal typingModal\n\thelpMenu    helpmenu.Model\n\tpromptModal prompt.Model\n\tzoxideModal zoxideui.Model\n\tsortModal   sortmodel.Model\n\n\t// Zoxide client for directory tracking\n\tzClient *zoxidelib.Client\n\n\tfileMetaData         metadata.Model\n\tioReqCnt             int\n\tmodelQuitState       modelQuitStateType\n\tfirstTextInput       bool\n\ttoggleFooter         bool\n\tfirstLoadingComplete bool\n\tfirstUse             bool\n\n\t// This entirely disables metadata fetching. Used in test model\n\tdisableMetadata bool\n\n\t// Height in number of lines of actual viewport of\n\t// main panel and sidebar excluding border\n\tmainPanelHeight int\n\n\t// Height in number of lines of actual viewport of\n\t// footer panels - process/metadata/clipboard - excluding border\n\tfooterHeight int\n\tfullWidth    int\n\tfullHeight   int\n\n\t// whether usable trash directory exists or not\n\thasTrash bool\n}\n\ntype typingModal struct {\n\tlocation      string\n\topen          bool\n\ttextInput     textinput.Model\n\terrorMesssage string\n}\n\ntype editorFinishedMsg struct{ err error }\n"
  },
  {
    "path": "src/internal/type_utils.go",
    "content": "package internal\n\nimport (\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// ================ String method for easy logging =====================\n\nfunc (f focusPanelType) String() string {\n\tswitch f {\n\tcase nonePanelFocus:\n\t\treturn \"nonePanelFocus\"\n\tcase processBarFocus:\n\t\treturn \"processBarFocus\"\n\tcase sidebarFocus:\n\t\treturn \"sidebarFocus\"\n\tcase metadataFocus:\n\t\treturn \"metadataFocus\"\n\tdefault:\n\t\treturn common.InvalidTypeString\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/README.md",
    "content": "# ui package\n\n# To-dos\n- Put model, filePanel, sidebarModel, etc. in separate packages like this"
  },
  {
    "path": "src/internal/ui/clipboard/model.go",
    "content": "package clipboard\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n)\n\n// The fact that its visible in UI or not, is controlled by the main model\ntype Model struct {\n\twidth  int\n\theight int\n\titems  copyItems\n}\n\n// Copied items\ntype copyItems struct {\n\titems []string\n\tcut   bool\n}\n\nfunc (m *Model) SetDimensions(width int, height int) {\n\tm.width = width\n\tm.height = height\n}\n\nfunc (m *Model) Render() string {\n\tr := ui.ClipboardRenderer(m.height, m.width)\n\tviewHeight := m.height - common.BorderPadding\n\tviewWidth := m.width - common.InnerPadding\n\tif len(m.items.items) == 0 {\n\t\t// TODO move this to a string\n\t\tr.AddLines(\"\", common.ClipboardNoneText)\n\t} else {\n\t\tfor i := 0; i < len(m.items.items) && i < viewHeight; i++ {\n\t\t\tif i == viewHeight-1 && i != len(m.items.items)-1 {\n\t\t\t\t// Last Entry we can render, but there are more that one left\n\t\t\t\tr.AddLines(strconv.Itoa(len(m.items.items)-i) + \" items left....\")\n\t\t\t} else {\n\t\t\t\t// TODO: Avoid Lstat during render for performance\n\t\t\t\t// Add IsDir/IsLink information in the item type or\n\t\t\t\t// better use filepanel's Element strcut as-is\n\t\t\t\tfileInfo, err := os.Lstat(m.items.items[i])\n\t\t\t\tif err != nil {\n\t\t\t\t\tslog.Error(\"Clipboard render function get item state \", \"error\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tisLink := fileInfo.Mode()&os.ModeSymlink != 0\n\t\t\t\tr.AddLines(common.ClipboardPrettierName(m.items.items[i],\n\t\t\t\t\tviewWidth, fileInfo.IsDir(), isLink, false))\n\t\t\t}\n\t\t}\n\t}\n\treturn r.Render()\n}\n\nfunc (m *Model) IsCut() bool {\n\treturn m.items.cut\n}\n\nfunc (m *Model) Reset(cut bool) {\n\tm.items.cut = cut\n\tm.items.items = m.items.items[:0]\n}\n\nfunc (m *Model) Add(location string) {\n\tm.items.items = append(m.items.items, location)\n}\n\nfunc (m *Model) SetItems(items []string) {\n\tm.items.items = make([]string, len(items))\n\tcopy(m.items.items, items)\n}\n\nfunc (m *Model) pruneInaccessibleItems() {\n\tm.items.items = slices.DeleteFunc(m.items.items, func(item string) bool {\n\t\t_, err := os.Lstat(item)\n\t\treturn err != nil\n\t})\n}\n\nfunc (m *Model) GetItems() []string {\n\t// return a copy to prevent external mutation\n\titems := make([]string, len(m.items.items))\n\tcopy(items, m.items.items)\n\treturn items\n}\n\n// Use this to use a copy that is in sync with current state of filesystem\nfunc (m *Model) PruneInaccessibleItemsAndGet() []string {\n\t// Clipboard items might becomes outdated with\n\t// externally/interally triggered changes\n\tm.pruneInaccessibleItems()\n\treturn m.GetItems()\n}\n\nfunc (m *Model) Len() int {\n\treturn len(m.items.items)\n}\n\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n\nfunc (m *Model) GetHeight() int {\n\treturn m.height\n}\n\nfunc (m *Model) GetFirstItem() string {\n\tif len(m.items.items) == 0 {\n\t\treturn \"\"\n\t}\n\treturn m.items.items[0]\n}\n"
  },
  {
    "path": "src/internal/ui/clipboard/model_test.go",
    "content": "package clipboard\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc TestMain(m *testing.M) {\n\t//nolint:reassign // Needed to tests\n\tcommon.ClipboardNoneText = \" \" + icon.Error + icon.Space + \" No content in clipboard\"\n\tflag.Parse()\n\tif testing.Verbose() {\n\t\tutils.SetRootLoggerToStdout(true)\n\t} else {\n\t\tutils.SetRootLoggerToDiscarded()\n\t}\n\tm.Run()\n}\n\nfunc TestClipboardRender_Empty(t *testing.T) {\n\tdir := t.TempDir()\n\tvar items []string\n\tfor i := range 5 {\n\t\tfp := filepath.Join(dir, \"f\"+strconv.Itoa(i)+\".txt\")\n\t\titems = append(items, fp)\n\t}\n\tm := &Model{}\n\tm.SetDimensions(15+len(items[0]), 6)\n\tt.Run(\"Empty\", func(t *testing.T) {\n\t\tout := ansi.Strip(m.Render())\n\t\tassert.Contains(t, out, common.ClipboardNoneText)\n\t})\n\n\tutils.CreateFiles(items[0])\n\tt.Run(\"Single Item\", func(t *testing.T) {\n\t\tm.SetItems([]string{items[0]})\n\t\tout := ansi.Strip(m.Render())\n\t\tassert.NotContains(t, out, common.ClipboardNoneText)\n\t\tassert.Contains(t, out, items[0])\n\t\tassert.NotContains(t, out, items[1])\n\t})\n\n\tutils.CreateFiles(items[1])\n\tt.Run(\"Only two items exist, rest don't\", func(t *testing.T) {\n\t\tm.SetItems(items)\n\t\tout := ansi.Strip(m.Render())\n\t\tassert.NotContains(t, out, common.ClipboardNoneText)\n\t\tassert.Contains(t, out, items[0])\n\t\tassert.Contains(t, out, items[1])\n\t\tfor i := 2; i < 5; i++ {\n\t\t\tassert.NotContains(t, out, items[i])\n\t\t}\n\t})\n\n\tutils.CreateFiles(items[2:]...)\n\tt.Run(\"Overflow\", func(t *testing.T) {\n\t\tm.SetItems(items)\n\t\tout := ansi.Strip(m.Render())\n\t\tassert.NotContains(t, out, common.ClipboardNoneText)\n\t\tfor i := range 3 {\n\t\t\tassert.Contains(t, out, items[i])\n\t\t}\n\t\tassert.Contains(t, out, \"2 items left....\", \"expected overflow indicator in render\")\n\t})\n}\n\nfunc TestPruneInaccessibleItemsAndGet(t *testing.T) {\n\tdir := t.TempDir()\n\tfiles := []string{filepath.Join(dir, \"f1\"), filepath.Join(dir, \"f2\")}\n\tutils.SetupFiles(t, files...)\n\n\tm := &Model{}\n\tm.SetItems(files)\n\tassert.Equal(t, files, m.PruneInaccessibleItemsAndGet())\n\trequire.NoError(t, os.Remove(files[1]))\n\tassert.Equal(t, []string{files[0]}, m.PruneInaccessibleItemsAndGet())\n}\n"
  },
  {
    "path": "src/internal/ui/filemodel/consts.go",
    "content": "package filemodel\n\nimport (\n\t\"errors\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n)\n\n// Now they are same doesn't means that they will be forever.\n// Explicitly stating here tells that they are derived from same\n// source, but have inherently different meaning\nconst (\n\tFileModelMinHeight      = filepanel.MinHeight\n\tFileModelMinWidth       = filepanel.MinWidth\n\tFilePreviewResizingText = \"Resizing...\"\n\tFilePreviewLoadingText  = \"Loading...\"\n)\n\nvar ErrMaximumPanelCount = errors.New(\"maximum panel count reached\")\n\nvar ErrMinimumPanelCount = errors.New(\"minimum panel count reached\")\n"
  },
  {
    "path": "src/internal/ui/filemodel/dimensions.go",
    "content": "package filemodel\n\nimport (\n\t\"log/slog\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n)\n\n// Use SetDimensions if you want to update both\n// it will prevent duplicate file preview commands and hence, is efficient\nfunc (m *Model) SetDimensions(width int, height int) tea.Cmd {\n\tm.Height = max(height, FileModelMinHeight)\n\tm.Width = max(width, FileModelMinWidth)\n\tm.updateChildComponentWidth()\n\tm.updateChildComponentHeight()\n\treturn m.ensurePreviewDimensionsSync()\n}\nfunc (m *Model) SetHeight(height int) tea.Cmd {\n\tm.Height = max(height, FileModelMinHeight)\n\tm.updateChildComponentHeight()\n\treturn m.ensurePreviewDimensionsSync()\n}\n\nfunc (m *Model) SetWidth(width int) tea.Cmd {\n\tm.Width = max(width, FileModelMinWidth)\n\tm.updateChildComponentWidth()\n\treturn m.ensurePreviewDimensionsSync()\n}\n\nfunc (m *Model) PanelCount() int {\n\treturn len(m.FilePanels)\n}\n\nfunc (m *Model) updateChildComponentHeight() {\n\tfor i := range m.FilePanels {\n\t\tm.FilePanels[i].SetHeight(m.Height)\n\t}\n}\n\nfunc (m *Model) updateChildComponentWidth() {\n\t// TODO: programatically ensure that this becomes impossible\n\tif m.PanelCount() == 0 {\n\t\tslog.Error(\"Unexpected error: fileModel with 0 panels\")\n\t\treturn\n\t}\n\tpanelCount := len(m.FilePanels)\n\twidthForPanels := m.Width\n\n\tif m.FilePreview.IsOpen() {\n\t\t// Need to give some width to preview\n\t\tif common.Config.FilePreviewWidth == 0 {\n\t\t\t// FileModel will be split among `panelCount+1`\n\t\t\tm.ExpectedPreviewWidth = m.Width / (panelCount + 1)\n\t\t} else {\n\t\t\tm.ExpectedPreviewWidth = m.Width / common.Config.FilePreviewWidth\n\t\t}\n\t\twidthForPanels -= m.ExpectedPreviewWidth\n\t}\n\n\tpanelWidth := widthForPanels / panelCount\n\tlastPanelWidth := widthForPanels - (panelCount-1)*panelWidth\n\n\tfor i := range panelCount {\n\t\tif i == panelCount-1 {\n\t\t\tm.FilePanels[i].SetWidth(lastPanelWidth)\n\t\t} else {\n\t\t\tm.FilePanels[i].SetWidth(panelWidth)\n\t\t}\n\t}\n\n\tm.SinglePanelWidth = panelWidth\n\tm.MaxFilePanel = widthForPanels / filepanel.MinWidth\n\t// Cap at the system maximum\n\tif m.MaxFilePanel > common.FilePanelMax {\n\t\tm.MaxFilePanel = common.FilePanelMax\n\t}\n}\n\nfunc (m *Model) ensurePreviewDimensionsSync() tea.Cmd {\n\tif m.FilePreview.GetContentWidth() != m.ExpectedPreviewWidth ||\n\t\tm.FilePreview.GetContentHeight() != m.Height {\n\t\treturn m.GetFilePreviewCmd(true)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/internal/ui/filemodel/navigation.go",
    "content": "package filemodel\n\nimport \"log/slog\"\n\nfunc (m *Model) NextFilePanel() {\n\tm.MoveFocusedPanelBy(1)\n}\n\nfunc (m *Model) PreviousFilePanel() {\n\tm.MoveFocusedPanelBy(-1)\n}\n\nfunc (m *Model) MoveFocusedPanelBy(delta int) {\n\tif m.PanelCount() == 0 {\n\t\tslog.Error(\"Unexpected error: fileModel with 0 panels\")\n\t\treturn\n\t}\n\tm.GetFocusedFilePanel().IsFocused = false\n\tm.FocusedPanelIndex = (m.FocusedPanelIndex + delta + m.PanelCount()) % m.PanelCount()\n\tm.FilePanels[m.FocusedPanelIndex].IsFocused = true\n}\n"
  },
  {
    "path": "src/internal/ui/filemodel/render.go",
    "content": "package filemodel\n\nimport \"github.com/charmbracelet/lipgloss\"\n\nfunc (m *Model) Render() string {\n\tf := make([]string, m.PanelCount()+1)\n\tfor i, filePanel := range m.FilePanels {\n\t\tf[i] = filePanel.Render(filePanel.IsFocused)\n\t}\n\tf[m.PanelCount()] = m.GetFilePreviewRender()\n\treturn lipgloss.JoinHorizontal(lipgloss.Top, f...)\n}\n\nfunc (m *Model) GetFilePreviewRender() string {\n\tif !m.FilePreview.IsOpen() {\n\t\treturn \"\"\n\t}\n\t// Check if width and height have been synced yet\n\tif m.FilePreview.GetContentHeight() == m.Height &&\n\t\tm.FilePreview.GetContentWidth() == m.ExpectedPreviewWidth {\n\t\tif m.FilePreview.IsLoading() {\n\t\t\treturn m.FilePreview.RenderText(FilePreviewLoadingText)\n\t\t}\n\t\treturn m.FilePreview.GetContent()\n\t}\n\n\t// Placeholder resizing text till they get synced\n\treturn m.FilePreview.RenderTextWithDimension(\n\t\tFilePreviewResizingText, m.Height, m.ExpectedPreviewWidth)\n}\n"
  },
  {
    "path": "src/internal/ui/filemodel/type.go",
    "content": "package filemodel\n\nimport (\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\t\"github.com/yorukot/superfile/src/internal/ui/preview\"\n)\n\n// TODO: Make the fields unexported, as much as possible\n// some fields like `Width` should not be updated directly, only via\n// Set functions. Having them exported is dangerous\ntype Model struct {\n\tFilePanels           []filepanel.Model\n\tSinglePanelWidth     int\n\tWidth                int\n\tExpectedPreviewWidth int\n\tHeight               int\n\tRenaming             bool\n\tMaxFilePanel         int\n\tFilePreview          preview.Model\n\tFocusedPanelIndex    int\n\tioReqCnt             int\n\tDisplayDotFiles      bool\n}\n"
  },
  {
    "path": "src/internal/ui/filemodel/update.go",
    "content": "package filemodel\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\t\"github.com/yorukot/superfile/src/internal/ui/preview\"\n)\n\nfunc (m *Model) CreateNewFilePanel(location string) (tea.Cmd, error) {\n\tif m.PanelCount() >= m.MaxFilePanel {\n\t\treturn nil, ErrMaximumPanelCount\n\t}\n\n\tif _, err := os.Stat(location); err != nil {\n\t\treturn nil, fmt.Errorf(\"cannot access location : %s\", location)\n\t}\n\n\tm.FilePanels = append(m.FilePanels, filepanel.New(\n\t\tlocation, false, \"\", m.GetFocusedFilePanel().SortKind,\n\t\tm.GetFocusedFilePanel().SortReversed))\n\n\tnewPanelIndex := m.PanelCount() - 1\n\n\tm.FilePanels[m.FocusedPanelIndex].IsFocused = false\n\tm.FilePanels[newPanelIndex].IsFocused = true\n\tm.FilePanels[newPanelIndex].SetHeight(m.Height)\n\tm.FocusedPanelIndex = newPanelIndex\n\n\tm.updateChildComponentWidth()\n\treturn m.ensurePreviewDimensionsSync(), nil\n}\n\nfunc (m *Model) CloseFilePanel() (tea.Cmd, error) {\n\tif m.PanelCount() <= 1 {\n\t\treturn nil, ErrMinimumPanelCount\n\t}\n\n\tm.FilePanels = append(m.FilePanels[:m.FocusedPanelIndex],\n\t\tm.FilePanels[m.FocusedPanelIndex+1:]...)\n\n\tif m.FocusedPanelIndex != 0 {\n\t\tm.FocusedPanelIndex--\n\t}\n\tm.FilePanels[m.FocusedPanelIndex].IsFocused = true\n\tm.updateChildComponentWidth()\n\n\treturn m.ensurePreviewDimensionsSync(), nil\n}\n\nfunc (m *Model) ToggleFilePreviewPanel() tea.Cmd {\n\tm.FilePreview.ToggleOpen()\n\tm.updateChildComponentWidth()\n\treturn m.ensurePreviewDimensionsSync()\n}\n\nfunc (m *Model) UpdatePreviewPanel(msg preview.UpdateMsg) {\n\tselectedItem := m.GetFocusedFilePanel().GetFocusedItemPtr()\n\tif selectedItem == nil {\n\t\tslog.Debug(\"Panel empty or cursor invalid. Ignoring FilePreviewUpdateMsg\")\n\t\treturn\n\t}\n\tif selectedItem.Location != msg.GetLocation() {\n\t\tslog.Debug(\"FilePreviewUpdateMsg for older files. Ignoring\",\n\t\t\t\"curLocation\", selectedItem.Location, \"msgLocation\", msg.GetLocation())\n\t\treturn\n\t}\n\n\tif m.ExpectedPreviewWidth != msg.GetContentWidth() ||\n\t\tm.Height != msg.GetContentHeight() {\n\t\tslog.Debug(\"FilePreviewUpdateMsg for older dimensions. Ignoring\",\n\t\t\t\"curW\", m.ExpectedPreviewWidth, \"curH\", m.Height,\n\t\t\t\"msgW\", msg.GetContentWidth(), \"msgH\", msg.GetContentHeight())\n\t\treturn\n\t}\n\tm.FilePreview.Apply(msg)\n}\n\nfunc (m *Model) GetFilePreviewCmd(forcePreviewRender bool) tea.Cmd {\n\tif !m.FilePreview.IsOpen() {\n\t\treturn nil\n\t}\n\tpanel := m.GetFocusedFilePanel()\n\tif panel.EmptyOrInvalid() {\n\t\t// Sync call because this will be fast\n\t\tm.FilePreview.SetEmptyWithDimensions(m.ExpectedPreviewWidth, m.Height)\n\t\treturn nil\n\t}\n\tselectedItem := panel.GetFocusedItem()\n\tif m.FilePreview.GetLocation() == selectedItem.Location && !forcePreviewRender {\n\t\treturn nil\n\t}\n\n\tm.FilePreview.SetLocation(selectedItem.Location)\n\tm.FilePreview.SetLoading()\n\n\t// HACK!!!. fileModel must not be aware of other dimensions. but...\n\t// Unfortunately, previewPanel isn't completely 'under' fileModel\n\t// Note: Must save the dimensions for the closure of the Cmd to avoid\n\t// problems\n\tfullModalWidth := m.Width + common.Config.SidebarWidth\n\tif common.Config.SidebarWidth != 0 {\n\t\tfullModalWidth += common.BorderPadding\n\t}\n\twidth := m.ExpectedPreviewWidth\n\theight := m.Height\n\n\treqCnt := m.ioReqCnt\n\tm.ioReqCnt++\n\tslog.Debug(\"Submitting file preview render request\", \"id\", reqCnt,\n\t\t\"path\", selectedItem.Location, \"w\", width, \"h\", height)\n\n\treturn func() tea.Msg {\n\t\tcontent := m.FilePreview.RenderWithPath(selectedItem.Location, width, height, fullModalWidth)\n\t\treturn preview.NewUpdateMsg(selectedItem.Location, content,\n\t\t\twidth, height, reqCnt)\n\t}\n}\n\nfunc (m *Model) ToggleDotFile() {\n\tm.DisplayDotFiles = !m.DisplayDotFiles\n\tm.UpdateFilePanelsIfNeeded(true)\n}\n\nfunc (m *Model) UpdateFilePanelsIfNeeded(force bool) {\n\tfor i := range m.FilePanels {\n\t\tm.FilePanels[i].UpdateElementsIfNeeded(force, m.DisplayDotFiles)\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/filemodel/utils.go",
    "content": "package filemodel\n\nimport (\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/filepanel\"\n\t\"github.com/yorukot/superfile/src/internal/ui/preview\"\n)\n\nfunc (m *Model) GetFocusedFilePanel() *filepanel.Model {\n\treturn &m.FilePanels[m.FocusedPanelIndex]\n}\n\nfunc New(firstPanelPaths []string, toggleDotFile bool) Model {\n\treturn Model{\n\t\tFilePanels:       filepanel.FilePanelSlice(firstPanelPaths),\n\t\tFilePreview:      preview.New(),\n\t\tSinglePanelWidth: common.DefaultFilePanelWidth,\n\t\tDisplayDotFiles:  toggleDotFile,\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/columns.go",
    "content": "package filepanel\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/charmbracelet/x/ansi\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// The renderer for the mandatory first column in the file panel, with a name, a cursor, and a select option.\nfunc (m *Model) renderFileName(indexElement int, columnWidth int) string {\n\telem := m.GetElementAtIdx(indexElement)\n\tisSelected := m.CheckSelected(elem.Location)\n\tcursor := emptyCursor\n\tif indexElement == m.GetCursor() && !m.SearchBar.Focused() {\n\t\tcursor = icon.Cursor\n\t}\n\n\tselectBox := m.renderSelectBox(isSelected)\n\n\t// Calculate the actual prefix width for proper alignment\n\tprefixWidth := ansi.StringWidth(cursor+\" \") + ansi.StringWidth(selectBox)\n\n\tisLink := elem.Info.Mode()&os.ModeSymlink != 0\n\trenderedName := common.FilePanelItemRenderWithIcon(\n\t\telem.Name,\n\t\tcolumnWidth-prefixWidth,\n\t\telem.Directory,\n\t\tisLink,\n\t\tisSelected,\n\t\tcommon.FilePanelBGColor,\n\t)\n\treturn common.FilePanelCursorStyle.Render(cursor+\" \") + selectBox + renderedName\n}\n\n// The renderer of delimiter spaces. It has a strict fixed size that depends only on the delimiter string.\nfunc (m *Model) renderDelimiter(indexElement int, columnWidth int) string {\n\tisSelected := m.CheckSelected(m.GetElementAtIdx(indexElement).Location)\n\treturn common.FilePanelItemRender(\n\t\tColumnDelimiter,\n\t\tcolumnWidth,\n\t\tisSelected,\n\t\tcommon.FilePanelBGColor,\n\t\tlipgloss.Left,\n\t)\n}\n\nfunc (m *Model) renderFileSize(indexElement int, columnWidth int) string {\n\telem := m.GetElementAtIdx(indexElement)\n\tisSelected := m.CheckSelected(elem.Location)\n\tsizeValue := common.FormatFileSize(elem.Info.Size())\n\tif elem.Info.IsDir() {\n\t\tsizeValue = \"\"\n\t}\n\treturn common.FilePanelItemRender(\n\t\tsizeValue,\n\t\tcolumnWidth,\n\t\tisSelected,\n\t\tcommon.FilePanelBGColor,\n\t\tlipgloss.Right,\n\t)\n}\n\n// TODO: make time template configurable\nfunc (m *Model) renderModifyTime(indexElement int, columnWidth int) string {\n\telem := m.GetElementAtIdx(indexElement)\n\tisSelected := m.CheckSelected(elem.Location)\n\tmodifyTime := elem.Info.ModTime().Format(\"2006-01-02 15:04\")\n\treturn common.FilePanelItemRender(\n\t\tmodifyTime,\n\t\tcolumnWidth,\n\t\tisSelected,\n\t\tcommon.FilePanelBGColor,\n\t\tlipgloss.Right,\n\t)\n}\n\nfunc (m *Model) renderPermissions(indexElement int, columnWidth int) string {\n\telem := m.GetElementAtIdx(indexElement)\n\tisSelected := m.CheckSelected(elem.Location)\n\treturn common.FilePanelItemRender(\n\t\telem.Info.Mode().Perm().String(),\n\t\tcolumnWidth,\n\t\tisSelected,\n\t\tcommon.FilePanelBGColor,\n\t\tlipgloss.Right,\n\t)\n}\n\nfunc (cd *columnDefinition) Render(index int) string {\n\treturn cd.columnRender(index, cd.Size)\n}\n\nfunc (cd *columnDefinition) RenderHeader() string {\n\treturn common.FilePanelItemRender(\n\t\tcd.Name,\n\t\tcd.Size,\n\t\tfalse,\n\t\tcommon.FilePanelBGColor,\n\t\tcd.HeaderAlign,\n\t)\n}\n\nfunc (m *Model) makeColumns(columnThreshold int, fileNameRatio int) []columnDefinition {\n\t// TODO: make column set configurable\n\t// Note: May use a predefined slice for efficiency. This content is static\n\textraColumns := []columnDefinition{\n\t\t{\n\t\t\tName:         \"Size\",\n\t\t\tcolumnRender: m.renderFileSize,\n\t\t\tSize:         FileSizeColumnWidth,\n\t\t\tHeaderAlign:  lipgloss.Center,\n\t\t},\n\t\t{\n\t\t\tName:         \"Modify time\",\n\t\t\tcolumnRender: m.renderModifyTime,\n\t\t\tSize:         ModifyTimeSizeColumnWidth,\n\t\t\tHeaderAlign:  lipgloss.Center,\n\t\t},\n\t\t{\n\t\t\tName:         \"Permission\",\n\t\t\tcolumnRender: m.renderPermissions,\n\t\t\tSize:         PermissionsColumnWidth,\n\t\t\tHeaderAlign:  lipgloss.Center,\n\t\t},\n\t}\n\tmaxColumns := min(columnThreshold, len(extraColumns))\n\tcolumns := []columnDefinition{\n\t\t{\n\t\t\tName:         strings.Repeat(\" \", ansi.StringWidth(emptyCursor+\" \")) + \"Name\",\n\t\t\tcolumnRender: m.renderFileName,\n\t\t\tSize:         m.GetContentWidth(),\n\t\t\tHeaderAlign:  lipgloss.Left,\n\t\t},\n\t}\n\n\tminWidthForNameColumn := int(float64(m.GetContentWidth() * fileNameRatio / common.FileNameRatioMax))\n\t// Worst case (5 * 100 / 100) could evaluate to 5.0001\n\t// Hence, we need this check. Our constraints on Width and ratio guarantee it to be > 0 though\n\tminWidthForNameColumn = min(minWidthForNameColumn, m.GetContentWidth())\n\n\tfor _, col := range extraColumns[0:maxColumns] {\n\t\twidthExtraColumn := ansi.StringWidth(ColumnDelimiter) + col.Size\n\n\t\t// This condition checks that can we borrow some width from first column for additional columns?\n\t\tif columns[0].Size-widthExtraColumn > minWidthForNameColumn {\n\t\t\tdelimiterCol := columnDefinition{\n\t\t\t\tName:         \"\",\n\t\t\t\tcolumnRender: m.renderDelimiter,\n\t\t\t\tSize:         ansi.StringWidth(ColumnDelimiter),\n\t\t\t}\n\t\t\tcolumns = append(columns, delimiterCol, col)\n\t\t\tcolumns[0].Size -= widthExtraColumn\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn columns\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/consts.go",
    "content": "package filepanel\n\nimport (\n\t\"time\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nconst (\n\tcontentPadding = 3 // Title + Searchbar + middle border line\n\tMinHeight      = contentPadding + common.BorderPadding + 1\n\tMinWidth       = 18 // minimal width for rename input to render\n\n\tFileSizeColumnWidth       = 15\n\tModifyTimeSizeColumnWidth = 18\n\tPermissionsColumnWidth    = 12\n\tColumnHeaderHeight        = 1\n\n\t// Delimiter between columns in the file panel.\n\tColumnDelimiter      = \"  \"\n\tReRenderChunkDivisor = 100\n\tReRenderMaxDelay     = 3\n\n\tnonFocussedPanelReRenderTime = 3 * time.Second\n\n\temptyCursor = \" \"\n)\n"
  },
  {
    "path": "src/internal/ui/filepanel/dimension.go",
    "content": "package filepanel\n\nimport (\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc (m *Model) UpdateDimensions(width, height int) {\n\tm.SetWidth(width)\n\tm.SetHeight(height)\n}\n\nfunc (m *Model) SetWidth(width int) {\n\tif width < MinWidth {\n\t\twidth = MinWidth\n\t}\n\tm.width = width\n\tm.SearchBar.Width = m.width - common.InnerPadding\n\tm.columns = m.makeColumns(common.Config.FilePanelExtraColumns, common.Config.FilePanelNamePercent)\n}\n\nfunc (m *Model) SetHeight(height int) {\n\tif height < MinHeight {\n\t\theight = MinHeight\n\t}\n\tm.height = height\n\t// Adjust scroll if needed\n\tm.scrollToCursor(m.GetCursor())\n}\n\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n\nfunc (m *Model) GetHeight() int {\n\treturn m.height\n}\n\nfunc (m *Model) GetMainPanelHeight() int {\n\treturn m.height - common.BorderPadding\n}\n\nfunc (m *Model) GetContentWidth() int {\n\treturn m.width - common.BorderPadding\n}\n\nfunc (m *Model) NeedRenderHeaders() bool {\n\treturn common.Config.FilePanelExtraColumns > 0 && len(m.columns) > 1\n}\n\n// PanelElementHeight calculates the number of visible elements in content area\nfunc (m *Model) PanelElementHeight() int {\n\theaderHeight := 0\n\tif m.NeedRenderHeaders() {\n\t\theaderHeight = ColumnHeaderHeight\n\t}\n\treturn m.GetMainPanelHeight() - contentPadding - headerHeight\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/get_elements.go",
    "content": "package filepanel\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\n// TODO : Take common.Config.CaseSensitiveSort as a function parameter\n// and also consider testing this caseSensitive with both true and false in\n// our unit_test TestReturnDirElement\n// getDirectoryElements returns the directory elements for the panel's current location\nfunc (m *Model) getDirectoryElements(displayDotFile bool) []Element {\n\tdirEntries, err := os.ReadDir(m.Location)\n\tif err != nil {\n\t\tslog.Error(\"Error while returning folder elements\", \"error\", err)\n\t\treturn nil\n\t}\n\n\tdirEntries = slices.DeleteFunc(dirEntries, func(e os.DirEntry) bool {\n\t\t// Entries not needed to be considered\n\t\t_, err := e.Info()\n\t\treturn err != nil || (strings.HasPrefix(e.Name(), \".\") && !displayDotFile)\n\t})\n\n\t// No files/directories to process\n\tif len(dirEntries) == 0 {\n\t\treturn nil\n\t}\n\treturn sortFileElement(m.SortKind, m.SortReversed, dirEntries, m.Location)\n}\n\n// getDirectoryElementsBySearch returns filtered directory elements based on search string\nfunc (m *Model) getDirectoryElementsBySearch(displayDotFile bool) []Element {\n\tsearchString := m.SearchBar.Value()\n\titems, err := os.ReadDir(m.Location)\n\tif err != nil {\n\t\tslog.Error(\"Error while return folder element function\", \"error\", err)\n\t\treturn nil\n\t}\n\n\tif len(items) == 0 {\n\t\treturn nil\n\t}\n\n\tfolderElementMap := map[string]os.DirEntry{}\n\tfileAndDirectories := []string{}\n\n\tfor _, item := range items {\n\t\tfileInfo, err := item.Info()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif !displayDotFile && strings.HasPrefix(fileInfo.Name(), \".\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tfileAndDirectories = append(fileAndDirectories, item.Name())\n\t\tfolderElementMap[item.Name()] = item\n\t}\n\t// https://github.com/reinhrst/fzf-lib/blob/main/core.go#L43\n\t// fzf returns matches ordered by score; we subsequently sort by the chosen sort option.\n\tfzfResults := utils.FzfSearch(searchString, fileAndDirectories)\n\tdirElements := make([]os.DirEntry, 0, len(fzfResults))\n\tfor _, item := range fzfResults {\n\t\tresultItem := folderElementMap[item.Key]\n\t\tdirElements = append(dirElements, resultItem)\n\t}\n\n\treturn sortFileElement(m.SortKind, m.SortReversed, dirElements, m.Location)\n}\n\n// Helper to decide whether to skip updating a panel this tick.\nfunc (m *Model) shouldSkipPanelUpdate(nowTime time.Time) bool {\n\tif !m.IsFocused {\n\t\treturn nowTime.Sub(m.LastTimeGetElement) < nonFocussedPanelReRenderTime\n\t}\n\n\treRenderTime := int(float64(m.ElemCount()) / ReRenderChunkDivisor)\n\treRenderTime = min(reRenderTime, ReRenderMaxDelay)\n\treturn !m.NeedsReRender() &&\n\t\tnowTime.Sub(m.LastTimeGetElement) < time.Duration(reRenderTime)*time.Second\n}\n\nfunc (m *Model) UpdateElementsIfNeeded(force bool, displayDotFile bool) {\n\tnowTime := time.Now()\n\tif force || !m.shouldSkipPanelUpdate(nowTime) {\n\t\t// Load elements for this panel (with/without search filter)\n\t\tm.element = m.getElements(displayDotFile)\n\t\t// Update file panel list\n\t\tm.LastTimeGetElement = nowTime\n\n\t\t// For hover to file on first time loading\n\t\tif m.TargetFile != \"\" {\n\t\t\tm.applyTargetFileCursor()\n\t\t}\n\n\t\t// If cursor becomes invalid due to element update, reset\n\t\tif m.ValidateCursorAndRenderIndex() != nil {\n\t\t\tm.scrollToCursor(0)\n\t\t}\n\t}\n}\n\n// Retrieves elements for a panel based on search bar value and sort options.\nfunc (m *Model) getElements(displayDotFile bool) []Element {\n\tif m.SearchBar.Value() != \"\" {\n\t\treturn m.getDirectoryElementsBySearch(displayDotFile)\n\t}\n\treturn m.getDirectoryElements(displayDotFile)\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/get_elements_test.go",
    "content": "package filepanel\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/sortmodel\"\n)\n\nfunc TestReturnDirElement(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\tdir1 := filepath.Join(curTestDir, \"dir1\")\n\tdir2 := filepath.Join(curTestDir, \"dir2\")\n\tdirNatural := filepath.Join(curTestDir, \"dirNatural\")\n\tutils.SetupDirectories(t, curTestDir, dir1, dir2, dirNatural)\n\n\tcreationDelay := time.Millisecond * 5\n\t// Cleanup is handled by TestMain\n\n\t// Setup files\n\t// All files with 10 bytes of text\n\n\t// dir1\n\t// - file1.txt\n\t// dir2 (Empty)\n\t// .xyz\n\t// 1.json\n\t// abc - Add 15 bytes of text\n\t// aBcD\n\t// file1.txt\n\t// file2.txt - Add 20 bytes of text\n\t// xyz.json\n\n\tfileSetup := []struct {\n\t\tpath string\n\t\tdata []byte\n\t}{\n\t\t{filepath.Join(curTestDir, \".xyz\"), []byte(\"0123456789\")},\n\t\t{filepath.Join(dir1, \"file1.txt\"), []byte(\"0123456789\")},\n\t\t{filepath.Join(curTestDir, \"aBcD\"), []byte(\"0123456789\")},\n\t\t{filepath.Join(curTestDir, \"file1.txt\"), []byte(\"0123456789\")},\n\t\t{filepath.Join(curTestDir, \"xyz.json\"), []byte(\"0123456789\")},\n\t\t{filepath.Join(curTestDir, \"abc\"), []byte(\"012345678901234\")},\n\t\t{filepath.Join(curTestDir, \"file2.txt\"), []byte(\"01234567890123456789\")},\n\t\t{filepath.Join(curTestDir, \"1.json\"), []byte(\"0123456789\")},\n\t\t{filepath.Join(dirNatural, \"file1.txt\"), []byte(\"a\")},\n\t\t{filepath.Join(dirNatural, \"file2.txt\"), []byte(\"b\")},\n\t\t{filepath.Join(dirNatural, \"file10.txt\"), []byte(\"c\")},\n\t\t{filepath.Join(dirNatural, \"file20.txt\"), []byte(\"d\")},\n\t}\n\n\tfor _, f := range fileSetup {\n\t\tutils.SetupFilesWithData(t, f.data, f.path)\n\t\ttime.Sleep(creationDelay)\n\t}\n\n\ttestdata := []struct {\n\t\tname              string\n\t\tlocation          string\n\t\tdotFiles          bool\n\t\tsortKind          sortmodel.SortKind\n\t\treversed          bool\n\t\tsearchString      string\n\t\texpectedElemNames []string\n\t}{\n\t\t{\n\t\t\tname:              \"Empty Directory\",\n\t\t\tlocation:          dir2,\n\t\t\tdotFiles:          false,\n\t\t\tsortKind:          sortmodel.SortByName,\n\t\t\treversed:          false,\n\t\t\texpectedElemNames: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"Sort by Name\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: false,\n\t\t\tsortKind: sortmodel.SortByName,\n\t\t\treversed: false,\n\t\t\texpectedElemNames: []string{\"dir1\", \"dir2\", \"dirNatural\", \"1.json\", \"abc\", \"aBcD\", \"file1.txt\",\n\t\t\t\t\"file2.txt\", \"xyz.json\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Sort by Name, with dotfiles\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: true,\n\t\t\tsortKind: sortmodel.SortByName,\n\t\t\treversed: false,\n\t\t\texpectedElemNames: []string{\"dir1\", \"dir2\", \"dirNatural\", \".xyz\", \"1.json\", \"abc\", \"aBcD\",\n\t\t\t\t\"file1.txt\", \"file2.txt\", \"xyz.json\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Sort by Name Reversed\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: false,\n\t\t\tsortKind: sortmodel.SortByName,\n\t\t\treversed: true,\n\t\t\texpectedElemNames: []string{\"dirNatural\", \"dir2\", \"dir1\", \"xyz.json\", \"file2.txt\",\n\t\t\t\t\"file1.txt\", \"aBcD\", \"abc\", \"1.json\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Sort by Size\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: false,\n\t\t\tsortKind: sortmodel.SortBySize,\n\t\t\treversed: false,\n\t\t\texpectedElemNames: []string{\"dir2\", \"dir1\", \"dirNatural\", \"1.json\", \"aBcD\",\n\t\t\t\t\"file1.txt\", \"xyz.json\", \"abc\", \"file2.txt\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Sort by Size Reversed\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: false,\n\t\t\tsortKind: sortmodel.SortBySize,\n\t\t\treversed: true,\n\t\t\texpectedElemNames: []string{\"dirNatural\", \"dir1\", \"dir2\", \"file2.txt\", \"abc\", \"xyz.json\",\n\t\t\t\t\"file1.txt\", \"aBcD\", \"1.json\"},\n\t\t},\n\t\t// This one could be flakey if files are created to quickly, or maybe created in\n\t\t// parallel\n\t\t{\n\t\t\tname:     \"Sort by Date\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: false,\n\t\t\tsortKind: sortmodel.SortByDate,\n\t\t\treversed: false,\n\t\t\texpectedElemNames: []string{\"dirNatural\", \"1.json\", \"file2.txt\", \"abc\",\n\t\t\t\t\"xyz.json\", \"file1.txt\", \"aBcD\", \"dir1\", \"dir2\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Sort by Type\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: false,\n\t\t\tsortKind: sortmodel.SortByType,\n\t\t\treversed: false,\n\t\t\texpectedElemNames: []string{\"dir1\", \"dir2\", \"dirNatural\", \"abc\", \"aBcD\", \"1.json\", \"xyz.json\",\n\t\t\t\t\"file1.txt\", \"file2.txt\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"Sort by Type Reversed and dotfiles\",\n\t\t\tlocation: curTestDir,\n\t\t\tdotFiles: true,\n\t\t\tsortKind: sortmodel.SortByType,\n\t\t\treversed: true,\n\t\t\texpectedElemNames: []string{\"dirNatural\", \"dir2\", \"dir1\", \".xyz\", \"file2.txt\", \"file1.txt\",\n\t\t\t\t\"xyz.json\", \"1.json\", \"aBcD\", \"abc\"},\n\t\t},\n\t\t{\n\t\t\tname:              \"Sort by Type Reversed and dotfiles with search\",\n\t\t\tlocation:          curTestDir,\n\t\t\tdotFiles:          true,\n\t\t\tsortKind:          sortmodel.SortByType,\n\t\t\treversed:          true,\n\t\t\tsearchString:      \"x\",\n\t\t\texpectedElemNames: []string{\".xyz\", \"file2.txt\", \"file1.txt\", \"xyz.json\"},\n\t\t},\n\t\t{\n\t\t\tname:              \"Sort by Size Reversed with search ftt\",\n\t\t\tlocation:          curTestDir,\n\t\t\tdotFiles:          false,\n\t\t\tsortKind:          sortmodel.SortBySize,\n\t\t\treversed:          true,\n\t\t\tsearchString:      \"ftt\",\n\t\t\texpectedElemNames: []string{\"file2.txt\", \"file1.txt\"},\n\t\t},\n\t\t{\n\t\t\tname:              \"Sort by Size Reversed with search d\",\n\t\t\tlocation:          curTestDir,\n\t\t\tdotFiles:          false,\n\t\t\tsortKind:          sortmodel.SortBySize,\n\t\t\treversed:          true,\n\t\t\tsearchString:      \"d\",\n\t\t\texpectedElemNames: []string{\"dirNatural\", \"dir1\", \"dir2\", \"aBcD\"},\n\t\t},\n\t\t{\n\t\t\tname:              \"Sort by Natural\",\n\t\t\tlocation:          dirNatural,\n\t\t\tdotFiles:          false,\n\t\t\tsortKind:          sortmodel.SortByNatural,\n\t\t\treversed:          false,\n\t\t\texpectedElemNames: []string{\"file1.txt\", \"file2.txt\", \"file10.txt\", \"file20.txt\"},\n\t\t},\n\t\t{\n\t\t\tname:              \"Sort by Natural Reversed\",\n\t\t\tlocation:          dirNatural,\n\t\t\tdotFiles:          false,\n\t\t\tsortKind:          sortmodel.SortByNatural,\n\t\t\treversed:          true,\n\t\t\texpectedElemNames: []string{\"file20.txt\", \"file10.txt\", \"file2.txt\", \"file1.txt\"},\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tpanel := testModel(0, 0, 0, BrowserMode, nil)\n\t\t\tpanel.Location = tt.location\n\t\t\tpanel.SortKind = tt.sortKind\n\t\t\tpanel.SortReversed = tt.reversed\n\t\t\tpanel.SearchBar.SetValue(tt.searchString)\n\t\t\tvar res []Element\n\t\t\tif tt.searchString == \"\" {\n\t\t\t\tres = panel.getDirectoryElements(tt.dotFiles)\n\t\t\t} else {\n\t\t\t\tres = panel.getDirectoryElementsBySearch(tt.dotFiles)\n\t\t\t}\n\n\t\t\tassert.Len(t, res, len(tt.expectedElemNames))\n\t\t\tactualNames := []string{}\n\t\t\tfor i := range res {\n\t\t\t\tactualNames = append(actualNames, res[i].Name)\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedElemNames, actualNames)\n\t\t})\n\t}\n}\n\nfunc TestSingleItemSelect(t *testing.T) {\n\ttestdata := []struct {\n\t\tname             string\n\t\tpanel            Model\n\t\tpanelToSelect    []string\n\t\texpectedSelected map[string]int\n\t}{\n\t\t{\n\t\t\tname: \"Select unselected item\",\n\t\t\tpanel: testModel(0, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\texpectedSelected: map[string]int{\"/tmp/file1.txt\": 1},\n\t\t},\n\t\t{\n\t\t\tname: \"Deselect selected item\",\n\t\t\tpanel: testModel(0, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{\"/tmp/file1.txt\"},\n\t\t\texpectedSelected: map[string]int{},\n\t\t},\n\t\t{\n\t\t\tname: \"Out of bounds cursor negative\",\n\t\t\tpanel: testModel(-1, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\texpectedSelected: map[string]int{},\n\t\t},\n\t\t{\n\t\t\tname: \"Out of bounds cursor beyond count\",\n\t\t\tpanel: testModel(5, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\texpectedSelected: map[string]int{},\n\t\t},\n\t\t{\n\t\t\tname:             \"Empty element list\",\n\t\t\tpanel:            testModel(0, 0, 12, SelectMode, []Element{}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\texpectedSelected: map[string]int{},\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.panel.SetSelectedAll(tt.panelToSelect)\n\t\t\ttt.panel.SingleItemSelect()\n\t\t\tassert.Equal(t, tt.expectedSelected, tt.panel.selected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/misc.go",
    "content": "package filepanel\n\nimport \"github.com/yorukot/superfile/src/internal/common\"\n\nfunc (p PanelMode) String() string {\n\tswitch p {\n\tcase SelectMode:\n\t\treturn \"selectMode\"\n\tcase BrowserMode:\n\t\treturn \"browserMode\"\n\tdefault:\n\t\treturn common.InvalidTypeString\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/model.go",
    "content": "package filepanel\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sortmodel\"\n)\n\n// FilePanelSlice creates a slice of FilePanels from the given paths\nfunc FilePanelSlice(paths []string) []Model {\n\tres := make([]Model, len(paths))\n\tfor i := range paths {\n\t\t// Making the first panel as the focused\n\t\tisFocus := i == 0\n\t\tres[i] = defaultFilePanel(paths[i], isFocus)\n\t}\n\treturn res\n}\n\n// defaultFilePanel creates a new FilePanel with default settings\nfunc defaultFilePanel(path string, focused bool) Model {\n\ttargetFile := \"\"\n\tpanelPath := path\n\t// If path refers to a file, switch to its parent and remember the filename\n\tif stat, err := os.Stat(panelPath); err == nil && !stat.IsDir() {\n\t\ttargetFile = filepath.Base(panelPath)\n\t\tpanelPath = filepath.Dir(panelPath)\n\t}\n\treturn New(panelPath, focused, targetFile, sortmodel.SortKind(common.Config.DefaultSortType),\n\t\tcommon.Config.SortOrderReversed)\n}\n\nfunc New(location string, focused bool, targetFile string, sortKind sortmodel.SortKind, sortReversed bool) Model {\n\treturn Model{\n\t\tcursor:           0,\n\t\trenderIndex:      0,\n\t\tLocation:         location,\n\t\tSortKind:         sortKind,\n\t\tSortReversed:     sortReversed,\n\t\tPanelMode:        BrowserMode,\n\t\tIsFocused:        focused,\n\t\tDirectoryRecords: make(map[string]directoryRecord),\n\t\tSearchBar:        common.GenerateSearchBar(),\n\t\tTargetFile:       targetFile,\n\t\twidth:            MinWidth,\n\t\theight:           MinHeight,\n\t\tselected:         make(map[string]int),\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/navigation.go",
    "content": "package filepanel\n\nimport (\n\t\"fmt\"\n)\n\nfunc (m *Model) scrollToCursor(cursor int) {\n\tif cursor < 0 || cursor >= m.ElemCount() {\n\t\treturn\n\t}\n\tm.cursor = cursor\n\n\t// Modify renderIndex if needed\n\trenderCount := m.PanelElementHeight()\n\tif m.cursor < m.renderIndex {\n\t\t// Due to size change, when last element is selected, we might have\n\t\t// empty space (renderIndex ... ElemCount()-1 spans less then renderCount)\n\t\t// Even with >0 renderIndex\n\t\tm.renderIndex = m.cursor\n\t} else if m.cursor > m.renderIndex+renderCount-1 {\n\t\tm.renderIndex = m.cursor - renderCount + 1\n\t}\n}\n\nfunc (m *Model) moveCursorBy(delta int) {\n\tif m.Empty() {\n\t\treturn\n\t}\n\t// Wrap cursor\n\tcursor := (m.cursor + delta + m.ElemCount()) % m.ElemCount()\n\tm.scrollToCursor(cursor)\n}\n\n// Control file panel list up\nfunc (m *Model) ListUp() {\n\tm.moveCursorBy(-1)\n}\n\n// Control file panel list down\nfunc (m *Model) ListDown() {\n\tm.moveCursorBy(1)\n}\n\nfunc (m *Model) PgUp() {\n\tm.moveCursorBy(-m.getPageScrollSize())\n}\n\nfunc (m *Model) PgDown() {\n\tm.moveCursorBy(m.getPageScrollSize())\n}\n\n// Handles the action of selecting an item in the file panel upwards. (only work on select mode)\n// This basically just toggles the \"selected\" status of element that is pointed by the cursor\n// and then moves the cursor up\n// TODO : Add unit tests for ItemSelectUp and singleItemSelect\nfunc (m *Model) ItemSelectUp() {\n\tm.SingleItemSelect()\n\tm.ListUp()\n}\n\n// Handles the action of selecting an item in the file panel downwards. (only work on select mode)\nfunc (m *Model) ItemSelectDown() {\n\tm.SingleItemSelect()\n\tm.ListDown()\n}\n\n// Applies targetFile cursor positioning, if configured for the panel.\nfunc (m *Model) applyTargetFileCursor() {\n\tidx := m.FindElementIndexByName(m.TargetFile)\n\tif idx != -1 {\n\t\tm.scrollToCursor(idx)\n\t}\n\tm.TargetFile = \"\"\n}\n\nfunc (m *Model) ValidateCursorAndRenderIndex() error {\n\tif m.cursor < 0 || m.ElemCount() <= m.cursor {\n\t\treturn fmt.Errorf(\"invalid cursor : %d, element count : %d\", m.cursor, m.ElemCount())\n\t}\n\trenderCount := m.PanelElementHeight()\n\tif (m.cursor < m.renderIndex) || (m.cursor > m.renderIndex+renderCount-1) {\n\t\treturn fmt.Errorf(\"invalid renderIndex : %d, cursor : %d, renderCount : %d\",\n\t\t\tm.renderIndex, m.cursor, renderCount)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/navigation_test.go",
    "content": "package filepanel\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc testModelWithElemCount(cursor int, renderIndex int, height int, elemCount int) Model {\n\treturn testModel(cursor, renderIndex, height, BrowserMode, make([]Element, elemCount))\n}\n\nfunc testModel(cursor int, renderIndex int, height int, mode PanelMode,\n\telements []Element) Model {\n\treturn Model{\n\t\telement:     elements,\n\t\tcursor:      cursor,\n\t\trenderIndex: renderIndex,\n\t\theight:      height,\n\t\tselected:    make(map[string]int),\n\t\tPanelMode:   mode,\n\t}\n}\n\nfunc Test_filePanelUpDown(t *testing.T) {\n\ttestdata := []struct {\n\t\tname           string\n\t\tpanel          Model\n\t\tlistDown       bool\n\t\texpectedCursor int\n\t\texpectedRender int\n\t}{\n\t\t{\n\t\t\tname:           \"Down movement within renderable range\",\n\t\t\tpanel:          testModelWithElemCount(0, 0, 12, 10),\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 1,\n\t\t\texpectedRender: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"Down movement when cursor is at bottom\",\n\t\t\tpanel:          testModelWithElemCount(6, 0, 12, 10),\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 7,\n\t\t\texpectedRender: 1,\n\t\t},\n\t\t{\n\t\t\tname:           \"Down movement causing wrap to top\",\n\t\t\tpanel:          testModelWithElemCount(9, 3, 12, 10),\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up movement within renderable range\",\n\t\t\tpanel:          testModelWithElemCount(2, 0, 12, 10),\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 1,\n\t\t\texpectedRender: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up movement when cursor is at top\",\n\t\t\tpanel:          testModelWithElemCount(3, 3, 12, 10),\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 2,\n\t\t\texpectedRender: 2,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up movement causing wrap to bottom\",\n\t\t\tpanel:          testModelWithElemCount(0, 0, 12, 10),\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 9,\n\t\t\texpectedRender: 3,\n\t\t},\n\t\t{\n\t\t\tname:           \"Down movement on empty panel\",\n\t\t\tpanel:          testModelWithElemCount(0, 0, 12, 0),\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up movement on empty panel\",\n\t\t\tpanel:          testModelWithElemCount(0, 0, 12, 0),\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.listDown {\n\t\t\t\ttt.panel.ListDown()\n\t\t\t} else {\n\t\t\t\ttt.panel.ListUp()\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedCursor, tt.panel.GetCursor())\n\t\t\tassert.Equal(t, tt.expectedRender, tt.panel.GetRenderIndex())\n\t\t})\n\t}\n}\n\nfunc TestPgUpDown(t *testing.T) {\n\ttestdata := []struct {\n\t\tname           string\n\t\tpanel          Model\n\t\tpageDown       bool\n\t\texpectedCursor int\n\t\texpectedRender int\n\t}{\n\t\t{\n\t\t\tname:           \"Page down with full page of items\",\n\t\t\tpanel:          testModelWithElemCount(0, 0, 12, 20),\n\t\t\tpageDown:       true,\n\t\t\texpectedCursor: 7,\n\t\t\texpectedRender: 1,\n\t\t},\n\t\t{\n\t\t\tname:           \"Page down near end wraps to start\",\n\t\t\tpanel:          testModelWithElemCount(18, 12, 12, 20),\n\t\t\tpageDown:       true,\n\t\t\texpectedCursor: 5, // (18 + 7) % 20 = 5\n\t\t\texpectedRender: 5,\n\t\t},\n\t\t{\n\t\t\tname:           \"Page up from middle\",\n\t\t\tpanel:          testModelWithElemCount(10, 4, 12, 20),\n\t\t\tpageDown:       false,\n\t\t\texpectedCursor: 3, // 10 - 7 = 3\n\t\t\texpectedRender: 3,\n\t\t},\n\t\t{\n\t\t\tname:           \"Page up near beginning wraps to end\",\n\t\t\tpanel:          testModelWithElemCount(2, 0, 12, 20),\n\t\t\tpageDown:       false,\n\t\t\texpectedCursor: 15, // (2 - 7 + 20) % 20 = 15\n\t\t\texpectedRender: 9,\n\t\t},\n\t\t{\n\t\t\tname:           \"Page navigation with small element count\",\n\t\t\tpanel:          testModelWithElemCount(0, 0, 12, 5),\n\t\t\tpageDown:       true,\n\t\t\texpectedCursor: 2, // (0 + 7) % 5 = 2\n\t\t\texpectedRender: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"Page down on empty panel\",\n\t\t\tpanel:          testModelWithElemCount(0, 0, 12, 0),\n\t\t\tpageDown:       true,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.pageDown {\n\t\t\t\ttt.panel.PgDown()\n\t\t\t} else {\n\t\t\t\ttt.panel.PgUp()\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedCursor, tt.panel.GetCursor())\n\t\t\tassert.Equal(t, tt.expectedRender, tt.panel.GetRenderIndex())\n\t\t})\n\t}\n}\n\nfunc TestItemSelectUpDown(t *testing.T) {\n\ttestdata := []struct {\n\t\tname             string\n\t\tpanel            Model\n\t\tpanelToSelect    []string\n\t\tselectDown       bool\n\t\texpectedCursor   int\n\t\texpectedRender   int\n\t\texpectedSelected map[string]int\n\t}{\n\t\t{\n\t\t\tname: \"Select and move down\",\n\t\t\tpanel: testModel(0, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t\t\t{Name: \"file3.txt\", Location: \"/tmp/file3.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\tselectDown:       true,\n\t\t\texpectedCursor:   1,\n\t\t\texpectedRender:   0,\n\t\t\texpectedSelected: map[string]int{\"/tmp/file1.txt\": 1},\n\t\t},\n\t\t{\n\t\t\tname: \"Select and move up\",\n\t\t\tpanel: testModel(2, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t\t\t{Name: \"file3.txt\", Location: \"/tmp/file3.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\tselectDown:       false,\n\t\t\texpectedCursor:   1,\n\t\t\texpectedRender:   0,\n\t\t\texpectedSelected: map[string]int{\"/tmp/file3.txt\": 1},\n\t\t},\n\t\t{\n\t\t\tname: \"Deselect already selected item\",\n\t\t\tpanel: testModel(0, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{\"/tmp/file1.txt\"},\n\t\t\tselectDown:       true,\n\t\t\texpectedCursor:   1,\n\t\t\texpectedRender:   0,\n\t\t\texpectedSelected: map[string]int{},\n\t\t},\n\t\t{\n\t\t\tname: \"Selection at boundary with wrap\",\n\t\t\tpanel: testModel(1, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\tselectDown:       true,\n\t\t\texpectedCursor:   0, // wraps to beginning\n\t\t\texpectedRender:   0,\n\t\t\texpectedSelected: map[string]int{\"/tmp/file2.txt\": 1},\n\t\t},\n\t\t{\n\t\t\tname: \"Selection persistence across moves\",\n\t\t\tpanel: testModel(1, 0, 12, SelectMode, []Element{\n\t\t\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t\t\t{Name: \"file3.txt\", Location: \"/tmp/file3.txt\"},\n\t\t\t}),\n\t\t\tpanelToSelect:    []string{\"/tmp/file1.txt\"},\n\t\t\tselectDown:       true,\n\t\t\texpectedCursor:   2,\n\t\t\texpectedRender:   0,\n\t\t\texpectedSelected: map[string]int{\"/tmp/file1.txt\": 1, \"/tmp/file2.txt\": 2},\n\t\t},\n\t\t{\n\t\t\tname:             \"Empty panel selection\",\n\t\t\tpanel:            testModel(0, 0, 12, SelectMode, []Element{}),\n\t\t\tpanelToSelect:    []string{},\n\t\t\tselectDown:       true,\n\t\t\texpectedCursor:   0,\n\t\t\texpectedRender:   0,\n\t\t\texpectedSelected: map[string]int{},\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.panel.SetSelectedAll(tt.panelToSelect)\n\n\t\t\tif tt.selectDown {\n\t\t\t\ttt.panel.ItemSelectDown()\n\t\t\t} else {\n\t\t\t\ttt.panel.ItemSelectUp()\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedCursor, tt.panel.GetCursor())\n\t\t\tassert.Equal(t, tt.expectedRender, tt.panel.GetRenderIndex())\n\t\t\tassert.Equal(t, tt.expectedSelected, tt.panel.selected)\n\t\t})\n\t}\n}\n\nfunc TestScrollToCursor(t *testing.T) {\n\ttestdata := []struct {\n\t\tname           string\n\t\tpanel          Model\n\t\tcursorPos      int\n\t\texpectedCursor int\n\t\texpectedRender int\n\t}{\n\t\t{\n\t\t\tname:           \"Jump to visible cursor no change\",\n\t\t\tpanel:          testModelWithElemCount(5, 3, 12, 20),\n\t\t\tcursorPos:      4,\n\t\t\texpectedCursor: 4,\n\t\t\texpectedRender: 3,\n\t\t},\n\t\t{\n\t\t\tname:           \"Jump above view\",\n\t\t\tpanel:          testModelWithElemCount(10, 5, 12, 20),\n\t\t\tcursorPos:      2,\n\t\t\texpectedCursor: 2,\n\t\t\texpectedRender: 2,\n\t\t},\n\t\t{\n\t\t\tname:           \"Jump below view\",\n\t\t\tpanel:          testModelWithElemCount(5, 0, 12, 20),\n\t\t\tcursorPos:      15,\n\t\t\texpectedCursor: 15,\n\t\t\texpectedRender: 9, // 15 - 7 + 1\n\t\t},\n\t\t{\n\t\t\tname:           \"Jump above view with empty space\",\n\t\t\tpanel:          testModelWithElemCount(19, 18, 12, 20),\n\t\t\tcursorPos:      17,\n\t\t\texpectedCursor: 17,\n\t\t\texpectedRender: 17,\n\t\t},\n\t\t{\n\t\t\tname:           \"Invalid cursor negative\",\n\t\t\tpanel:          testModelWithElemCount(5, 2, 12, 10),\n\t\t\tcursorPos:      -1,\n\t\t\texpectedCursor: 5, // unchanged\n\t\t\texpectedRender: 2, // unchanged\n\t\t},\n\t\t{\n\t\t\tname:           \"Invalid cursor beyond count\",\n\t\t\tpanel:          testModelWithElemCount(5, 2, 12, 10),\n\t\t\tcursorPos:      15,\n\t\t\texpectedCursor: 5, // unchanged\n\t\t\texpectedRender: 2, // unchanged\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.panel.scrollToCursor(tt.cursorPos)\n\t\t\tassert.Equal(t, tt.expectedCursor, tt.panel.GetCursor())\n\t\t\tassert.Equal(t, tt.expectedRender, tt.panel.GetRenderIndex())\n\t\t})\n\t}\n}\n\nfunc TestApplyTargetFileCursor(t *testing.T) {\n\tpanel := testModel(0, 0, 8, BrowserMode, []Element{\n\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t{Name: \"file3.txt\", Location: \"/tmp/file3.txt\"},\n\t\t{Name: \"file4.txt\", Location: \"/tmp/file4.txt\"},\n\t\t{Name: \"target.txt\", Location: \"/tmp/target.txt\"},\n\t\t{Name: \"file6.txt\", Location: \"/tmp/file6.txt\"},\n\t})\n\tpanel.TargetFile = \"target.txt\"\n\n\texpCursor := 4\n\texpRender := 2\n\n\tpanel.applyTargetFileCursor()\n\tassert.Equal(t, expCursor, panel.GetCursor())\n\tassert.Equal(t, expRender, panel.GetRenderIndex())\n\tassert.Empty(t, panel.TargetFile)\n\n\t// Shouldn't do anything\n\tpanel.applyTargetFileCursor()\n\tassert.Equal(t, expCursor, panel.GetCursor())\n\tassert.Equal(t, expRender, panel.GetRenderIndex())\n}\n\nfunc TestPageScrollSizeConfig(t *testing.T) {\n\toriginalPageScrollSize := common.Config.PageScrollSize\n\tdefer func() {\n\t\tcommon.Config.PageScrollSize = originalPageScrollSize\n\t}()\n\n\ttests := []struct {\n\t\tname           string\n\t\tpageScrollSize int\n\t\ttotalElements  int\n\t\tinitialCursor  int\n\t\tpanelHeight    int\n\t\texpectedCursor int\n\t\tpgUp           bool\n\t}{\n\t\t{\n\t\t\tname:           \"Default full page scroll (PageScrollSize = 0)\",\n\t\t\tpageScrollSize: 0,\n\t\t\ttotalElements:  30,\n\t\t\tinitialCursor:  0,\n\t\t\tpanelHeight:    10, // panelElementHeight = 10 - 3 = 7\n\t\t\texpectedCursor: 7,  // Should move by 7 (full page)\n\t\t},\n\t\t{\n\t\t\tname:           \"Custom scroll size 5\",\n\t\t\tpageScrollSize: 5,\n\t\t\ttotalElements:  30,\n\t\t\tinitialCursor:  0,\n\t\t\tpanelHeight:    10,\n\t\t\texpectedCursor: 5, // Should move by 5\n\t\t},\n\t\t{\n\t\t\tname:           \"Custom scroll size 10\",\n\t\t\tpageScrollSize: 10,\n\t\t\ttotalElements:  30,\n\t\t\tinitialCursor:  0,\n\t\t\tpanelHeight:    10,\n\t\t\texpectedCursor: 10, // Should move by 10\n\t\t},\n\t\t{\n\t\t\tname:           \"PgUp with custom scroll size\",\n\t\t\tpageScrollSize: 3,\n\t\t\ttotalElements:  30,\n\t\t\tinitialCursor:  10,\n\t\t\tpanelHeight:    10,\n\t\t\texpectedCursor: 7, // 10 - 3 = 7\n\t\t\tpgUp:           true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcommon.Config.PageScrollSize = tt.pageScrollSize\n\n\t\t\t// Create model with elements\n\t\t\tm := testModelWithElemCount(tt.initialCursor, 0, tt.panelHeight+2, tt.totalElements)\n\t\t\tif tt.pgUp {\n\t\t\t\tm.PgUp()\n\t\t\t} else {\n\t\t\t\tm.PgDown()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expectedCursor, m.GetCursor(),\n\t\t\t\t\"Cursor position should match expected after PgUp/PgDown\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/render.go",
    "content": "package filepanel\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n\t\"github.com/yorukot/superfile/src/internal/ui/rendering\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sortmodel\"\n)\n\n/*\n- TODO: Write File Panel Specific unit test\n  - Individual panel resizes\n  - Footer content of filepanel changes due to resizing\n  - i Only mode icons remains on smaller\n  - ii Other things that change too\n  - Other panels like clipboard and metadata's content changes too on resize\n*/\nfunc (m *Model) Render(focused bool) string {\n\tr := ui.FilePanelRenderer(m.height, m.width, focused)\n\n\tm.renderTopBar(r)\n\tm.renderSearchBar(r)\n\tm.renderFooter(r, m.SelectedCount())\n\tif m.NeedRenderHeaders() {\n\t\tm.renderColumnHeaders(r)\n\t}\n\tm.renderFileEntries(r)\n\treturn r.Render()\n}\n\nfunc (m *Model) renderTopBar(r *rendering.Renderer) {\n\t// TODO - Add ansitruncate left in renderer and remove truncation here\n\ttruncatedPath := common.TruncateTextBeginning(m.Location, m.GetContentWidth()-common.InnerPadding, \"...\")\n\tr.AddLines(common.FilePanelTopDirectoryIcon + common.FilePanelTopPathStyle.Render(truncatedPath))\n\tr.AddSection()\n}\n\nfunc (m *Model) renderSearchBar(r *rendering.Renderer) {\n\tr.AddLines(\" \" + m.SearchBar.View())\n}\n\n// TODO : Unit test this\nfunc (m *Model) renderFooter(r *rendering.Renderer, selectedCount uint) {\n\tsortLabel, sortIcon := m.getSortInfo()\n\tmodeLabel, modeIcon := m.getPanelModeInfo(selectedCount)\n\tcursorStr := m.getCursorString()\n\n\tif common.Config.Nerdfont {\n\t\tsortLabel = sortIcon + icon.Space + sortLabel\n\t\tmodeLabel = modeIcon + icon.Space + modeLabel\n\t} else {\n\t\t// TODO : Figure out if we can set icon.Space to \" \" if nerdfont is false\n\t\t// That would simplify code\n\t\tsortLabel = sortIcon + \" \" + sortLabel\n\t}\n\n\tif common.Config.ShowPanelFooterInfo {\n\t\tr.SetBorderInfoItems(sortLabel, modeLabel, cursorStr)\n\t\tif r.AreInfoItemsTruncated() {\n\t\t\tr.SetBorderInfoItems(sortIcon, modeIcon, cursorStr)\n\t\t}\n\t} else {\n\t\tr.SetBorderInfoItems(cursorStr)\n\t}\n}\n\nfunc (m *Model) renderColumnHeaders(r *rendering.Renderer) {\n\tvar builder strings.Builder\n\tfor _, column := range m.columns {\n\t\tbuilder.WriteString(column.RenderHeader())\n\t}\n\tr.AddLines(builder.String())\n}\n\nfunc (m *Model) renderFileEntries(r *rendering.Renderer) {\n\tif m.Empty() {\n\t\tr.AddLines(common.FilePanelNoneText)\n\t\treturn\n\t}\n\tend := min(m.renderIndex+m.PanelElementHeight(), m.ElemCount())\n\n\tfor itemIndex := m.renderIndex; itemIndex < end; itemIndex++ {\n\t\tif m.Renaming && itemIndex == m.GetCursor() {\n\t\t\tr.AddLines(m.Rename.View())\n\t\t\tcontinue\n\t\t}\n\t\tvar builder strings.Builder\n\t\tfor _, column := range m.columns {\n\t\t\tcolData := column.Render(itemIndex)\n\t\t\tbuilder.WriteString(colData)\n\t\t}\n\t\tr.AddLines(builder.String())\n\t}\n}\n\nfunc (m *Model) getSortInfo() (string, string) {\n\ticonStr := icon.SortAsc\n\tif m.SortReversed {\n\t\ticonStr = icon.SortDesc\n\t}\n\treturn sortmodel.SortOptionsShortStr[m.SortKind], iconStr\n}\n\nfunc (m *Model) getPanelModeInfo(selectedCount uint) (string, string) {\n\tswitch m.PanelMode {\n\tcase BrowserMode:\n\t\treturn \"Browser\", icon.Browser\n\tcase SelectMode:\n\t\treturn \"Select\" + icon.Space + fmt.Sprintf(\"(%d)\", selectedCount), icon.Select\n\tdefault:\n\t\treturn \"\", \"\"\n\t}\n}\n\nfunc (m *Model) getCursorString() string {\n\tcursor := m.GetCursor()\n\tif !m.Empty() {\n\t\tcursor++ // Convert to 1-based\n\t}\n\treturn fmt.Sprintf(\"%d/%d\", cursor, m.ElemCount())\n}\n\nfunc (m *Model) renderSelectBox(isSelected bool) string {\n\tif !common.Config.ShowSelectIcons || !common.Config.Nerdfont || m.PanelMode != SelectMode {\n\t\treturn \"\"\n\t}\n\n\tif m.IsFocused {\n\t\tif isSelected {\n\t\t\treturn common.CheckboxCheckedFocused\n\t\t}\n\t\treturn common.CheckboxEmptyFocused\n\t}\n\tif isSelected {\n\t\treturn common.CheckboxChecked\n\t}\n\treturn common.CheckboxEmpty\n}\n\n// Checks whether a panel needs re-render due to being invalid or due to directory change\nfunc (m *Model) NeedsReRender() bool {\n\tif !m.EmptyOrInvalid() {\n\t\treturn filepath.Dir(m.GetFirstElement().Location) != m.Location\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/selection_test.go",
    "content": "package filepanel\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPanelSelectionLifeCycle(t *testing.T) {\n\tpanel := testModel(0, 0, 0, BrowserMode, []Element{\n\t\t{Name: \"file1.txt\", Location: \"/tmp/file1.txt\"},\n\t\t{Name: \"file2.txt\", Location: \"/tmp/file2.txt\"},\n\t\t{Name: \"file3.txt\", Location: \"/tmp/file3.txt\"},\n\t\t{Name: \"file4.txt\", Location: \"/tmp/file4.txt\"},\n\t\t{Name: \"file5.txt\", Location: \"/tmp/file5.txt\"}})\n\tassert.Equal(t, uint(0), panel.SelectedCount())\n\n\t// first added\n\tpanel.SetSelected(\"/tmp/file1.txt\")\n\tassert.Equal(t, uint(1), panel.SelectedCount())\n\tassert.Equal(t, map[string]int{\"/tmp/file1.txt\": 1}, panel.selected)\n\n\t// second added\n\tpanel.SetSelected(\"/tmp/file2.txt\")\n\tassert.Equal(t, map[string]int{\"/tmp/file1.txt\": 1, \"/tmp/file2.txt\": 2}, panel.selected)\n\tassert.Equal(t, uint(2), panel.SelectedCount())\n\tcurrentFirst := panel.GetFirstSelectedLocation()\n\tassert.Equal(t, \"/tmp/file1.txt\", currentFirst)\n\n\t// first removed\n\tpanel.SetUnSelected(\"/tmp/file1.txt\")\n\tassert.Equal(t, uint(1), panel.SelectedCount())\n\tassert.Equal(t, map[string]int{\"/tmp/file2.txt\": 2}, panel.selected)\n\tcurrentFirst = panel.GetFirstSelectedLocation()\n\tassert.Equal(t, \"/tmp/file2.txt\", currentFirst)\n\n\t// multi select\n\tpanel.SetSelectedAll([]string{\"/tmp/file3.txt\", \"/tmp/file4.txt\"})\n\tassert.Equal(t, map[string]int{\"/tmp/file2.txt\": 2, \"/tmp/file3.txt\": 3, \"/tmp/file4.txt\": 4}, panel.selected)\n\tassert.Equal(t, uint(3), panel.SelectedCount())\n\n\t// multi unselect\n\tpanel.SetUnSelected(\"/tmp/file2.txt\")\n\tpanel.SetUnSelected(\"/tmp/file4.txt\")\n\tassert.Equal(t, map[string]int{\"/tmp/file3.txt\": 3}, panel.selected)\n\tassert.Equal(t, uint(1), panel.SelectedCount())\n\tcurrentFirst = panel.GetFirstSelectedLocation()\n\tassert.Equal(t, \"/tmp/file3.txt\", currentFirst)\n\n\t// reset selection\n\tpanel.ResetSelected()\n\tassert.Equal(t, uint(0), panel.SelectedCount())\n\tassert.Equal(t, map[string]int{}, panel.selected)\n\tassert.Equal(t, 0, panel.selectOrderCounter)\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/sort.go",
    "content": "package filepanel\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/fvbommel/sortorder\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/sortmodel\"\n)\n\nfunc getOrderingFunc(elements []Element, reversed bool, sortKind sortmodel.SortKind) sliceOrderFunc {\n\tvar order func(i, j int) bool\n\tswitch sortKind {\n\tcase sortmodel.SortByName:\n\t\torder = func(i, j int) bool {\n\t\t\t// One of them is a directory, and other is not\n\t\t\tif elements[i].Directory != elements[j].Directory {\n\t\t\t\treturn elements[i].Directory\n\t\t\t}\n\t\t\tif common.Config.CaseSensitiveSort {\n\t\t\t\treturn elements[i].Name < elements[j].Name != reversed\n\t\t\t}\n\t\t\treturn strings.ToLower(elements[i].Name) < strings.ToLower(elements[j].Name) != reversed\n\t\t}\n\tcase sortmodel.SortBySize:\n\t\torder = getSizeOrderingFunc(elements, reversed)\n\tcase sortmodel.SortByDate:\n\t\torder = func(i, j int) bool {\n\t\t\treturn elements[i].Info.ModTime().After(elements[j].Info.ModTime()) != reversed\n\t\t}\n\tcase sortmodel.SortByType:\n\t\torder = getTypeOrderingFunc(elements, reversed)\n\tcase sortmodel.SortByNatural:\n\t\torder = func(i, j int) bool {\n\t\t\t// One of them is a directory, and other is not\n\t\t\tif elements[i].Directory != elements[j].Directory {\n\t\t\t\treturn elements[i].Directory\n\t\t\t}\n\t\t\tif common.Config.CaseSensitiveSort {\n\t\t\t\treturn sortorder.NaturalLess(elements[i].Name, elements[j].Name) != reversed\n\t\t\t}\n\t\t\treturn sortorder.NaturalLess(\n\t\t\t\tstrings.ToLower(elements[i].Name),\n\t\t\t\tstrings.ToLower(elements[j].Name),\n\t\t\t) != reversed\n\t\t}\n\t}\n\treturn order\n}\n\nfunc getSizeOrderingFunc(elements []Element, reversed bool) sliceOrderFunc {\n\treturn func(i, j int) bool {\n\t\t// Directories at the top sorted by direct child count (not recursive)\n\t\t// Files sorted by size\n\n\t\t// One of them is a directory, and other is not\n\t\tif elements[i].Directory != elements[j].Directory {\n\t\t\treturn elements[i].Directory\n\t\t}\n\n\t\t// This needs to be improved, and we should sort by actual size only\n\t\t// Repeated recursive read would be slow, so we could cache\n\t\tif elements[i].Directory && elements[j].Directory {\n\t\t\tfilesI, err := os.ReadDir(elements[i].Location)\n\t\t\t// No need of early return, we only call len() on filesI, so nil would\n\t\t\t// just result in 0\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Error when reading directory during sort\", \"error\", err)\n\t\t\t}\n\t\t\tfilesJ, err := os.ReadDir(elements[j].Location)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Error when reading directory during sort\", \"error\", err)\n\t\t\t}\n\t\t\treturn len(filesI) < len(filesJ) != reversed\n\t\t}\n\t\treturn elements[i].Info.Size() < elements[j].Info.Size() != reversed\n\t}\n}\n\nfunc getTypeOrderingFunc(elements []Element, reversed bool) sliceOrderFunc {\n\treturn func(i, j int) bool {\n\t\t// One of them is a directory, and the other is not\n\t\tif elements[i].Directory != elements[j].Directory {\n\t\t\treturn elements[i].Directory\n\t\t}\n\n\t\tvar extI, extJ string\n\t\tif !elements[i].Directory {\n\t\t\textI = strings.ToLower(filepath.Ext(elements[i].Name))\n\t\t}\n\t\tif !elements[j].Directory {\n\t\t\textJ = strings.ToLower(filepath.Ext(elements[j].Name))\n\t\t}\n\n\t\t// Compare by extension/type\n\t\tif extI != extJ {\n\t\t\treturn (extI < extJ) != reversed\n\t\t}\n\n\t\t// If same type, fall back to name\n\t\tif common.Config.CaseSensitiveSort {\n\t\t\treturn (elements[i].Name < elements[j].Name) != reversed\n\t\t}\n\t\treturn (strings.ToLower(elements[i].Name) < strings.ToLower(elements[j].Name)) != reversed\n\t}\n}\n\nfunc sortFileElement(sortKind sortmodel.SortKind, reversed bool, dirEntries []os.DirEntry, location string) []Element {\n\telements := make([]Element, 0, len(dirEntries))\n\tfor _, item := range dirEntries {\n\t\tinfo, err := item.Info()\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while retrieving file info during sort\",\n\t\t\t\t\"error\", err, \"path\", filepath.Join(location, item.Name()))\n\t\t\tcontinue\n\t\t}\n\n\t\telements = append(elements, Element{\n\t\t\tName:      item.Name(),\n\t\t\tDirectory: item.IsDir() || isSymlinkToDir(location, info, item.Name()),\n\t\t\tLocation:  filepath.Join(location, item.Name()),\n\t\t\tInfo:      info,\n\t\t})\n\t}\n\n\tsort.Slice(elements, getOrderingFunc(elements, reversed, sortKind))\n\n\treturn elements\n}\n\n// Symlinks to directories are to be identified as directories\nfunc isSymlinkToDir(location string, info os.FileInfo, name string) bool {\n\tif info.Mode()&os.ModeSymlink != 0 {\n\t\ttargetInfo, errStat := os.Stat(filepath.Join(location, name))\n\t\treturn errStat == nil && targetInfo.IsDir()\n\t}\n\treturn false\n}\n\nfunc (m *Model) getPageScrollSize() int {\n\tscrollSize := common.Config.PageScrollSize\n\tif scrollSize <= 0 {\n\t\t// Use default full page behavior\n\t\tscrollSize = m.PanelElementHeight()\n\t}\n\treturn scrollSize\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/types.go",
    "content": "package filepanel\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui/sortmodel\"\n)\n\n// Make sure to use New() to ensure that maps are initialized\n// zero value `Model{}`, or direct initialization should be avoided\n// or used very carefully if needed\ntype Model struct {\n\n\t// Note: We have tried to minimize direct access to cursor,\n\t// and read it via GetCursor() at most places, to make it easier\n\t// to find and harder to cause bugs of invalid value getting set to cursor\n\tcursor      int\n\trenderIndex int\n\tIsFocused   bool\n\tLocation    string\n\t// Dimension fields\n\twidth  int // Total width including borders\n\theight int // Total height including borders\n\n\tSortKind     sortmodel.SortKind\n\tSortReversed bool\n\n\tPanelMode PanelMode\n\t// key is file location, value order of selection\n\tselected           map[string]int\n\tselectOrderCounter int\n\telement            []Element\n\tDirectoryRecords   map[string]directoryRecord\n\tRename             textinput.Model\n\tRenaming           bool\n\tSearchBar          textinput.Model\n\tLastTimeGetElement time.Time\n\tTargetFile         string             // filename to position cursor on after load\n\tcolumns            []columnDefinition // columns for rendering\n}\n\n// Record for directory navigation\ntype directoryRecord struct {\n\tdirectoryCursor int\n\tdirectoryRender int\n}\n\n// Element within a file panel\ntype Element struct {\n\tName      string\n\tLocation  string\n\tDirectory bool\n\tInfo      os.FileInfo\n}\n\n// Type representing the mode of the panel\ntype PanelMode uint\n\n// Constants for select mode or browser mode\nconst (\n\tSelectMode PanelMode = iota\n\tBrowserMode\n)\n\ntype sliceOrderFunc func(i, j int) bool\n\ntype columnRenderer func(indexElement int, columnWidth int) string\n\ntype columnDefinition struct {\n\tName         string\n\tSize         int\n\tHeaderAlign  lipgloss.Position\n\tcolumnRender columnRenderer\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/update.go",
    "content": "package filepanel\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\nfunc (m *Model) ChangeFilePanelMode() {\n\tswitch m.PanelMode {\n\tcase SelectMode:\n\t\tm.ResetSelected()\n\t\tm.PanelMode = BrowserMode\n\tcase BrowserMode:\n\t\tm.PanelMode = SelectMode\n\tdefault:\n\t\tslog.Error(\"Unexpected panelMode\", \"panelMode\", m.PanelMode)\n\t}\n}\n\n// This should be the function that is always called whenever we are updating a directory.\nfunc (m *Model) UpdateCurrentFilePanelDir(path string) error {\n\tslog.Debug(\"updateCurrentFilePanelDir\", \"panel.location\", m.Location, \"path\", path)\n\t// In case non Absolute path is passed, make sure to resolve it.\n\tpath = utils.ResolveAbsPath(m.Location, path)\n\n\t// Ignore if its the same directory. It prevents resetting of searchBar\n\tif path == m.Location {\n\t\treturn nil\n\t}\n\n\t// NOTE: This could be a configurable feature\n\t// Update the cursor and render status in case we switch back to this.\n\tm.DirectoryRecords[m.Location] = directoryRecord{\n\t\tdirectoryCursor: m.cursor,\n\t\tdirectoryRender: m.renderIndex,\n\t}\n\n\tif info, err := os.Stat(path); err != nil {\n\t\treturn fmt.Errorf(\"%s : no such file or directory, stats err : %w\", path, err)\n\t} else if !info.IsDir() {\n\t\treturn fmt.Errorf(\"%s is not a directory\", path)\n\t}\n\n\t// In case of switching to parent, explicitly set focus.\n\t// This is to handle when there isn't a DirectoryRecord, yet.\n\tif filepath.Dir(m.Location) == path {\n\t\tm.TargetFile = filepath.Base(m.Location)\n\t}\n\t// Switch to \"path\"\n\tm.Location = path\n\n\t// NOTE: We are fetching the cursor and render from cache, but this could become invalid\n\t// in case user deletes some items in the directory via another file manager and then switch back\n\t// Basically this directoryRecords cache can be invalid. On each Update(), on dire change\n\t// we do a element fetch and validate the cursor and render values. But the filepane could\n\t// stay in invalid state till that and operations done before the update may fail\n\tcurDirectoryRecord, hasRecord := m.DirectoryRecords[m.Location]\n\tif hasRecord {\n\t\tm.cursor = curDirectoryRecord.directoryCursor\n\t\tm.renderIndex = curDirectoryRecord.directoryRender\n\t} else {\n\t\tm.cursor = 0\n\t\tm.renderIndex = 0\n\t}\n\n\tslog.Debug(\"updateCurrentFilePanelDir : After update\", \"cursor\", m.cursor, \"render\", m.renderIndex)\n\n\t// Reset the searchbar Value\n\t// TODO(Refactoring) : Have a common searchBar type for sidebar and this search bar.\n\tm.SearchBar.SetValue(\"\")\n\n\treturn nil\n}\n\nfunc (m *Model) ParentDirectory() error {\n\treturn m.UpdateCurrentFilePanelDir(\"..\")\n}\n\n// Select all item in the file panel (only work on select mode)\nfunc (m *Model) SelectAllItem() {\n\tfor _, item := range m.element {\n\t\tm.SetSelected(item.Location)\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/filepanel/utils.go",
    "content": "package filepanel\n\nimport \"math\"\n\nfunc (m *Model) GetCursor() int {\n\treturn m.cursor\n}\n\nfunc (m *Model) GetRenderIndex() int {\n\treturn m.renderIndex\n}\n\nfunc (m *Model) GetFocusedItem() Element {\n\treturn m.GetElementAtIdx(m.GetCursor())\n}\n\nfunc (m *Model) GetElementAtIdx(idx int) Element {\n\tif idx < 0 || m.ElemCount() <= idx {\n\t\treturn Element{}\n\t}\n\treturn m.element[idx]\n}\n\nfunc (m *Model) GetFirstElement() Element {\n\treturn m.GetElementAtIdx(0)\n}\n\nfunc (m *Model) ResetSelected() {\n\tm.selectOrderCounter = 0\n\tm.selected = make(map[string]int)\n}\n\n// For modification. Make sure to do a nil check\nfunc (m *Model) GetFocusedItemPtr() *Element {\n\tif m.GetCursor() < 0 || m.ElemCount() <= m.GetCursor() {\n\t\treturn nil\n\t}\n\treturn &m.element[m.GetCursor()]\n}\n\n// Note : If this is called on an already selected element\n// it will make its order last. This is expected behaviour\nfunc (m *Model) SetSelected(location string) {\n\tm.selectOrderCounter++\n\tm.selected[location] = m.selectOrderCounter\n}\n\nfunc (m *Model) SetUnSelected(location string) {\n\tif m.CheckSelected(location) {\n\t\tdelete(m.selected, location)\n\t}\n}\n\nfunc (m *Model) ToggleSelected(location string) {\n\tif m.CheckSelected(location) {\n\t\tdelete(m.selected, location)\n\t\treturn\n\t}\n\tm.SetSelected(location)\n}\n\n// Only used in tests, including tests outside this package\nfunc (m *Model) SetSelectedAll(locations []string) {\n\tfor _, location := range locations {\n\t\tm.SetSelected(location)\n\t}\n}\n\nfunc (m *Model) CheckSelected(location string) bool {\n\t_, isSelected := m.selected[location]\n\treturn isSelected\n}\n\n// Returns an unordered list of selected locations\nfunc (m *Model) GetSelectedLocations() []string {\n\tresult := make([]string, 0, len(m.selected))\n\tfor k := range m.selected {\n\t\tresult = append(result, k)\n\t}\n\treturn result\n}\n\nfunc (m *Model) GetFirstSelectedLocation() string {\n\tif len(m.selected) == 0 {\n\t\treturn \"\"\n\t}\n\tresult := \"\"\n\tminOrder := math.MaxInt\n\tfor location, order := range m.selected {\n\t\tif minOrder > order {\n\t\t\tresult = location\n\t\t\tminOrder = order\n\t\t}\n\t}\n\treturn result\n}\n\n// Select the item where cursor located (only work on select mode)\nfunc (m *Model) SingleItemSelect() {\n\tif !m.EmptyOrInvalid() {\n\t\tm.ToggleSelected(m.GetFocusedItem().Location)\n\t}\n}\n\nfunc (m *Model) ElemCount() int {\n\treturn len(m.element)\n}\n\nfunc (m *Model) SelectedCount() uint {\n\treturn uint(len(m.selected))\n}\n\nfunc (m *Model) Empty() bool {\n\treturn m.ElemCount() == 0\n}\n\nfunc (m *Model) EmptyOrInvalid() bool {\n\treturn m.Empty() || m.ValidateCursorAndRenderIndex() != nil\n}\n\nfunc (m *Model) ToggleReverseSort() {\n\tm.SortReversed = !m.SortReversed\n}\n\n// SetCursorPosition sets cursor and updates renderIndex accordingly.\n// Note: Intended for test utilities only!!!!!\nfunc (m *Model) SetCursorPosition(cursor int) {\n\tm.scrollToCursor(cursor)\n}\n\nfunc (m *Model) FindElementIndexByName(name string) int {\n\tfor i, elem := range m.element {\n\t\tif elem.Name == name {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (m *Model) FindElementIndexByLocation(location string) int {\n\tfor i, elem := range m.element {\n\t\tif elem.Location == location {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n"
  },
  {
    "path": "src/internal/ui/helpmenu/data.go",
    "content": "package helpmenu\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\n// Return help menu for Hotkeys\nfunc getData() []hotkeydata { //nolint: funlen // This should be self contained\n\tdata := []hotkeydata{\n\t\t{\n\t\t\tsubTitle: \"General\",\n\t\t},\n\t\t{\n\t\t\thotkey:         []string{\"spf\", \"\"},\n\t\t\tdescription:    \"Open superfile\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.Confirm,\n\t\t\tdescription:    \"Confirm your select or typing\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.Quit,\n\t\t\tdescription:    \"Quit typing, modal or superfile\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CdQuit,\n\t\t\tdescription:    \"Quit superfile and change directory to current folder\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ConfirmTyping,\n\t\t\tdescription:    \"Confirm typing\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CancelTyping,\n\t\t\tdescription:    \"Cancel typing\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.OpenHelpMenu,\n\t\t\tdescription:    \"Open help menu (hotkeylist)\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.OpenCommandLine,\n\t\t\tdescription:    \"Open command line\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.OpenSPFPrompt,\n\t\t\tdescription:    \"Open SPF prompt\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.OpenZoxide,\n\t\t\tdescription:    \"Open zoxide navigation\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\tsubTitle: \"Panel navigation\",\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CreateNewFilePanel,\n\t\t\tdescription:    \"Create new file panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.SplitFilePanel,\n\t\t\tdescription:    \"Split file panel (open new panel in same directory)\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CloseFilePanel,\n\t\t\tdescription:    \"Close the focused file panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ToggleFilePreviewPanel,\n\t\t\tdescription:    \"Toggle file preview panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.OpenSortOptionsMenu,\n\t\t\tdescription:    \"Open sort options menu\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ToggleReverseSort,\n\t\t\tdescription:    \"Toggle reverse sort\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ToggleFooter,\n\t\t\tdescription:    \"Toggle footer\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.NextFilePanel,\n\t\t\tdescription:    \"Focus on the next file panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.PreviousFilePanel,\n\t\t\tdescription:    \"Focus on the previous file panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FocusOnProcessBar,\n\t\t\tdescription:    \"Focus on the processbar panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FocusOnSidebar,\n\t\t\tdescription:    \"Focus on the sidebar\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FocusOnMetaData,\n\t\t\tdescription:    \"Focus on the metadata panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\tsubTitle: \"Panel movement\",\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ListUp,\n\t\t\tdescription:    \"Up\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ListDown,\n\t\t\tdescription:    \"Down\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.PageUp,\n\t\t\tdescription:    \"Page up\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.PageDown,\n\t\t\tdescription:    \"Page down\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ParentDirectory,\n\t\t\tdescription:    \"Return to parent folder\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FilePanelSelectAllItem,\n\t\t\tdescription:    \"Select all items in focused file panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FilePanelSelectModeItemsSelectUp,\n\t\t\tdescription:    \"Select up with your course\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FilePanelSelectModeItemsSelectDown,\n\t\t\tdescription:    \"Select down with your course\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ToggleDotFile,\n\t\t\tdescription:    \"Toggle dot file display\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.SearchBar,\n\t\t\tdescription:    \"Toggle active search bar\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ChangePanelMode,\n\t\t\tdescription:    \"Change between selection mode or normal mode\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.PinnedDirectory,\n\t\t\tdescription:    \"Pin or Unpin folder to sidebar (can be auto saved)\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\tsubTitle: \"File operations\",\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FilePanelItemCreate,\n\t\t\tdescription:    \"Create file or folder(end with \" + string(filepath.Separator) + \" to create a folder)\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.FilePanelItemRename,\n\t\t\tdescription:    \"Rename file or folder\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CopyItems,\n\t\t\tdescription:    \"Copy selected items to the clipboard\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CutItems,\n\t\t\tdescription:    \"Cut selected items to the clipboard\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.PasteItems,\n\t\t\tdescription:    \"Paste clipboard items into the current file panel\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.DeleteItems,\n\t\t\tdescription:    \"Delete selected items\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.PermanentlyDeleteItems,\n\t\t\tdescription:    \"Permanently delete selected items\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CopyPath,\n\t\t\tdescription:    \"Copy current file or directory path\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CopyPWD,\n\t\t\tdescription:    \"Copy current working directory\",\n\t\t\thotkeyWorkType: globalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.ExtractFile,\n\t\t\tdescription:    \"Extract compressed file\",\n\t\t\thotkeyWorkType: normalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.CompressFile,\n\t\t\tdescription:    \"Zip file or folder to .zip file\",\n\t\t\thotkeyWorkType: normalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.OpenFileWithEditor,\n\t\t\tdescription:    \"Open file with your default editor\",\n\t\t\thotkeyWorkType: normalType,\n\t\t},\n\t\t{\n\t\t\thotkey:         common.Hotkeys.OpenCurrentDirectoryWithEditor,\n\t\t\tdescription:    \"Open current directory with default editor\",\n\t\t\thotkeyWorkType: normalType,\n\t\t},\n\t}\n\n\treturn data\n}\n\nfunc removeOrphanSections(items []hotkeydata) []hotkeydata {\n\tvar result []hotkeydata\n\t// Since we can't know beforehand which section are we actually filtering\n\t// we may end up in a scenario where there are two sections (General, Panel navigation)\n\t// with no hotkeys between them, so we need to remove the section which its hotkeys was\n\t// completely filtered out (Orphan sections)\n\tfor i := range items {\n\t\tif items[i].subTitle != \"\" {\n\t\t\t// Look ahead: is the next item a real hotkey?\n\t\t\tif i+1 < len(items) && items[i+1].subTitle == \"\" {\n\t\t\t\tresult = append(result, items[i])\n\t\t\t}\n\t\t\t// Else: skip this subtitle because no children\n\t\t} else {\n\t\t\tresult = append(result, items[i])\n\t\t}\n\t}\n\treturn result\n}\n\nfunc (m *Model) filter(query string) {\n\tfiltered := fuzzySearch(query, m.data)\n\tfiltered = removeOrphanSections(filtered)\n\n\tm.filteredData = filtered\n\tif len(filtered) == 0 {\n\t\tm.cursor = 0\n\t} else {\n\t\tm.cursor = 1\n\t}\n\tm.renderIndex = 0\n}\n\n// Fuzzy search function for a list of helpMenuModalData.\n// inspired from: sidebar/directory_utils.go\nfunc fuzzySearch(query string, data []hotkeydata) []hotkeydata {\n\tif len(data) == 0 {\n\t\treturn []hotkeydata{}\n\t}\n\n\t// Optimization - This haystack can be kept precomputed based on description\n\t// instead of re computing it in each call\n\thaystack := []string{}\n\tidxMap := []int{}\n\tfor i, item := range data {\n\t\tif item.subTitle != \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tsearchText := strings.Join(item.hotkey, \" \") + \" \" + item.description\n\t\thaystack = append(haystack, searchText)\n\t\tidxMap = append(idxMap, i)\n\t}\n\n\tmatchedIdx := map[int]struct{}{}\n\tfor _, match := range utils.FzfSearch(query, haystack) {\n\t\tmatchedIdx[idxMap[match.HayIndex]] = struct{}{}\n\t}\n\n\tresults := []hotkeydata{}\n\tfor i, d := range data {\n\t\t_, isMatch := matchedIdx[i]\n\t\tif d.subTitle != \"\" || isMatch {\n\t\t\tresults = append(results, d)\n\t\t}\n\t}\n\n\treturn results\n}\n"
  },
  {
    "path": "src/internal/ui/helpmenu/model_state.go",
    "content": "package helpmenu\n\nimport (\n\t\"slices\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc New() Model {\n\tdata := getData()\n\n\treturn Model{\n\t\trenderIndex:  0,\n\t\tcursor:       1,\n\t\tdata:         data,\n\t\tfilteredData: data,\n\t\topened:       false,\n\t\tsearchBar:    common.GenerateSearchBar(),\n\t}\n}\n\n// Toggle help menu\nfunc (m *Model) Open() {\n\tif m.opened {\n\t\tm.searchBar.Reset()\n\t\tm.opened = false\n\t\treturn\n\t}\n\n\t// Reset filteredData to the full data whenever the helpMenu is opened\n\tm.filteredData = m.data\n\tm.opened = true\n}\n\n// Quit help menu\nfunc (m *Model) Close() {\n\tm.searchBar.Reset()\n\tm.opened = false\n}\n\n// Check hotkey input in help menu. Possible actions are moving up, down\n// and quiting the menu\nfunc (m *Model) HandleKey(msg string) {\n\tif m.searchBar.Focused() {\n\t\tswitch {\n\t\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg), slices.Contains(common.Hotkeys.CancelTyping, msg):\n\t\t\tm.searchBar.Blur()\n\t\tdefault:\n\t\t\tm.filter(m.searchBar.Value())\n\t\t}\n\t} else {\n\t\tm.handleNavKeys(msg)\n\t}\n}\n\nfunc (m *Model) handleNavKeys(msg string) {\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.ListUp, msg):\n\t\tm.ListUp()\n\tcase slices.Contains(common.Hotkeys.ListDown, msg):\n\t\tm.ListDown()\n\tcase slices.Contains(common.Hotkeys.Quit, msg):\n\t\tm.Close()\n\tcase slices.Contains(common.Hotkeys.SearchBar, msg):\n\t\tm.searchBar.Focus()\n\t}\n}\n\nfunc (m *Model) HandleTeaMsg(msg tea.Msg) tea.Cmd {\n\tvar cmd tea.Cmd\n\tif m.searchBar.Focused() {\n\t\tm.searchBar, cmd = m.searchBar.Update(msg)\n\t}\n\treturn cmd\n}\n\nfunc (m *Model) SetDimensions(width int, height int) {\n\tm.width = width\n\tm.height = height\n\t// 2 for border, 1 for left padding, 2 for placeholder icon of searchbar\n\t// 1 for additional character that View() of search bar function mysteriously adds.\n\tm.searchBar.Width = m.width - (common.InnerPadding + common.BorderPadding)\n}\n"
  },
  {
    "path": "src/internal/ui/helpmenu/navigation.go",
    "content": "package helpmenu\n\nimport \"github.com/yorukot/superfile/src/internal/common\"\n\n// Help menu panel list up\nfunc (m *Model) ListUp() {\n\tif m.cursor > 1 {\n\t\tm.cursor--\n\t\tif m.cursor < m.renderIndex {\n\t\t\tm.renderIndex = m.cursor\n\t\t}\n\t\tif m.filteredData[m.cursor].subTitle != \"\" {\n\t\t\tm.cursor--\n\t\t}\n\t} else {\n\t\t// Set the cursor to the last item in the list.\n\t\t// We use max(..., 0) as a safeguard to prevent a negative cursor index\n\t\t// in case the filtered list is empty.\n\t\tm.cursor = max(len(m.filteredData)-1, 0)\n\n\t\t// Adjust the render index to show the bottom of the list.\n\t\t// Similarly, we use max(..., 0) to ensure the renderIndex doesn't become negative,\n\t\t// which can happen if the number of items is less than the view height.\n\t\t// This prevents a potential out-of-bounds panic during rendering.\n\t\tm.renderIndex = max(len(m.filteredData)-(m.height-common.InnerPadding), 0)\n\t}\n}\n\n// Help menu panel list down\nfunc (m *Model) ListDown() {\n\tif len(m.filteredData) == 0 {\n\t\treturn\n\t}\n\n\tif m.cursor < len(m.filteredData)-1 {\n\t\t// Compute the next selectable row (skip subtitles).\n\t\tnext := m.cursor + 1\n\t\tfor next < len(m.filteredData) && m.filteredData[next].subTitle != \"\" {\n\t\t\tnext++\n\t\t}\n\t\tif next >= len(m.filteredData) {\n\t\t\t// Wrap if no more selectable rows.\n\t\t\tm.cursor = 1\n\t\t\tm.renderIndex = 0\n\t\t\treturn\n\t\t}\n\t\tm.cursor = next\n\n\t\t// Scroll down if cursor moved past the viewport.\n\t\tif m.cursor > m.renderIndex+m.height-5 {\n\t\t\tm.renderIndex++\n\t\t}\n\t\t// Clamp renderIndex to bottom.\n\t\tbottom := len(m.filteredData) - (m.height - common.InnerPadding)\n\t\tif bottom < 0 {\n\t\t\tbottom = 0\n\t\t}\n\t\tif m.renderIndex > bottom {\n\t\t\tm.renderIndex = bottom\n\t\t}\n\t} else {\n\t\tm.cursor = 1\n\t\tm.renderIndex = 0\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/helpmenu/render.go",
    "content": "package helpmenu\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n\t\"github.com/yorukot/superfile/src/internal/ui/rendering\"\n)\n\nfunc (m *Model) Render() string {\n\tr := ui.HelpMenuRenderer(m.height, m.width)\n\tr.AddLines(\" \" + m.searchBar.View())\n\tr.AddLines(\"\") // one-line separation between searchbar and content\n\n\t// TODO : This computation should not happen at render time. Move this to update\n\t// TODO : Move these computations to a utility function\n\tmaxKeyLength := 0\n\tfor _, data := range m.filteredData {\n\t\ttotalKeyLen := 0\n\t\tfor _, key := range data.hotkey {\n\t\t\ttotalKeyLen += len(key)\n\t\t}\n\n\t\tseparatorLen := max(0, (len(data.hotkey)-1)) * common.FooterGroupCols\n\t\tif data.subTitle == \"\" && totalKeyLen+separatorLen > maxKeyLength {\n\t\t\tmaxKeyLength = totalKeyLen + separatorLen\n\t\t}\n\t}\n\n\tvalueLength := m.width - maxKeyLength - common.BorderPadding\n\tif valueLength < m.width/common.CenterDivisor {\n\t\tvalueLength = m.width/common.CenterDivisor - common.BorderPadding\n\t}\n\n\ttotalTitleCount := 0\n\tcursorBeenTitleCount := 0\n\n\tfor i, data := range m.filteredData {\n\t\tif data.subTitle != \"\" {\n\t\t\tif i < m.cursor {\n\t\t\t\tcursorBeenTitleCount++\n\t\t\t}\n\t\t\ttotalTitleCount++\n\t\t}\n\t}\n\n\trenderHotkeyLength := m.getRenderHotkeyLength()\n\tm.getContent(r, renderHotkeyLength, valueLength)\n\n\tcurrent := m.cursor + 1 - cursorBeenTitleCount\n\tif len(m.filteredData) == 0 {\n\t\tcurrent = 0\n\t}\n\tr.SetBorderInfoItems(fmt.Sprintf(\"%s/%s\",\n\t\tstrconv.Itoa(current),\n\t\tstrconv.Itoa(len(m.filteredData)-totalTitleCount)))\n\treturn r.Render()\n}\n\nfunc (m *Model) getRenderHotkeyLength() int {\n\trenderHotkeyLength := 0\n\tfor i := m.renderIndex; i < m.renderIndex+(m.height-common.InnerPadding) && i < len(m.filteredData); i++ {\n\t\tif m.filteredData[i].subTitle != \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\thotkey := common.GetHelpMenuHotkeyString(m.filteredData[i].hotkey)\n\n\t\trenderHotkeyLength = max(renderHotkeyLength, len(common.HelpMenuHotkeyStyle.Render(hotkey)))\n\t}\n\treturn renderHotkeyLength + 1\n}\n\nfunc (m *Model) getContent(r *rendering.Renderer, renderHotkeyLength int, valueLength int) {\n\tfor i := m.renderIndex; i < m.renderIndex+(m.height-common.InnerPadding) && i < len(m.filteredData); i++ {\n\t\tif m.filteredData[i].subTitle != \"\" {\n\t\t\tr.AddLines(common.HelpMenuTitleStyle.Render(\" \" + m.filteredData[i].subTitle))\n\t\t\tcontinue\n\t\t}\n\n\t\thotkey := common.GetHelpMenuHotkeyString(m.filteredData[i].hotkey)\n\t\tdescription := common.TruncateText(m.filteredData[i].description, valueLength, \"...\")\n\n\t\tcursor := \"  \"\n\t\tif m.cursor == i {\n\t\t\tcursor = common.FilePanelCursorStyle.Render(icon.Cursor + \" \")\n\t\t}\n\t\tr.AddLines(cursor + common.ModalStyle.Render(fmt.Sprintf(\"%*s%s\", renderHotkeyLength,\n\t\t\tcommon.HelpMenuHotkeyStyle.Render(hotkey+\" \"), common.ModalStyle.Render(description))))\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/helpmenu/type.go",
    "content": "package helpmenu\n\nimport \"github.com/charmbracelet/bubbles/textinput\"\n\ntype hotkeyType int\n\nconst (\n\tglobalType hotkeyType = iota\n\tnormalType\n\tselectType\n)\n\n// Modal\ntype Model struct {\n\theight       int\n\twidth        int\n\topened       bool\n\trenderIndex  int\n\tcursor       int\n\tdata         []hotkeydata\n\tfilteredData []hotkeydata\n\tsearchBar    textinput.Model\n}\n\ntype hotkeydata struct {\n\thotkey         []string\n\tdescription    string\n\thotkeyWorkType hotkeyType\n\tsubTitle       string\n}\n"
  },
  {
    "path": "src/internal/ui/helpmenu/utils.go",
    "content": "package helpmenu\n\nfunc (m *Model) IsOpen() bool {\n\treturn m.opened\n}\n\nfunc (m *Model) GetHeight() int {\n\treturn m.height\n}\n\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/README.md",
    "content": "# metadata package\nThis is for the metadata panel, fetching and rendering metadata.\nSince metadata fetching is not fully contained, some part of functionality is\noffloaded to main model\n\n# To-dos\n- Add unit tests\n- Finish required TODOs\n- Update coverage stats\n\n# Coverage\n\n```bash\ncd /path/to/ui/metadata\ngo test -cover\n```\nCurrent coverage is 0%"
  },
  {
    "path": "src/internal/ui/metadata/architecture.go",
    "content": "package metadata\n\nimport (\n\t\"debug/elf\"\n\t\"debug/macho\"\n\t\"debug/pe\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst (\n\tarchI386    = \"i386\"\n\tarchX8664   = \"x86-64\"\n\tarchARM     = \"ARM\"\n\tarchARM64   = \"ARM64\"\n\tarchPPC     = \"PowerPC\"\n\tarchPPC64   = \"PowerPC64\"\n\tarchRISCV   = \"RISC-V\"\n\tarchS390x   = \"s390x\"\n\tarchSPARC64 = \"SPARC64\"\n\tarchMIPS    = \"MIPS\"\n)\n\nvar errNotBinary = errors.New(\"not a recognized binary format\")\n\nfunc GetBinaryArchitecture(filePath string) (string, error) {\n\tif arch, err := getELFArchitecture(filePath); err == nil {\n\t\treturn arch, nil\n\t}\n\n\tif arch, err := getPEArchitecture(filePath); err == nil {\n\t\treturn arch, nil\n\t}\n\n\tif arch, err := getMachOArchitecture(filePath); err == nil {\n\t\treturn arch, nil\n\t}\n\n\treturn \"\", errNotBinary\n}\n\nfunc getELFArchitecture(filePath string) (string, error) {\n\tf, err := elf.Open(filePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\n\tarch := elfMachineToString(f.Machine)\n\treturn fmt.Sprintf(\"ELF %s\", arch), nil\n}\n\nfunc getPEArchitecture(filePath string) (string, error) {\n\tf, err := pe.Open(filePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer f.Close()\n\n\tarch := peArchitectureToString(f.Machine)\n\treturn fmt.Sprintf(\"PE %s\", arch), nil\n}\n\nfunc getMachOArchitecture(filePath string) (string, error) {\n\tf, err := macho.Open(filePath)\n\tif err == nil {\n\t\tdefer f.Close()\n\t\tarch := machoCPUToString(f.Cpu)\n\t\treturn fmt.Sprintf(\"Mach-O %s\", arch), nil\n\t}\n\n\tfat, err := macho.OpenFat(filePath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer fat.Close()\n\n\tarchs := make([]string, 0, len(fat.Arches))\n\tfor _, arch := range fat.Arches {\n\t\tarchs = append(archs, machoCPUToString(arch.Cpu))\n\t}\n\n\tif len(archs) == 1 {\n\t\treturn fmt.Sprintf(\"Mach-O %s\", archs[0]), nil\n\t}\n\treturn fmt.Sprintf(\"Mach-O Universal (%s)\", strings.Join(archs, \", \")), nil\n}\n\n//nolint:exhaustive // common architectures only\nfunc elfMachineToString(machine elf.Machine) string {\n\tswitch machine {\n\tcase elf.EM_386:\n\t\treturn archI386\n\tcase elf.EM_X86_64:\n\t\treturn archX8664\n\tcase elf.EM_ARM:\n\t\treturn archARM\n\tcase elf.EM_AARCH64:\n\t\treturn archARM64\n\tcase elf.EM_MIPS:\n\t\treturn archMIPS\n\tcase elf.EM_PPC:\n\t\treturn archPPC\n\tcase elf.EM_PPC64:\n\t\treturn archPPC64\n\tcase elf.EM_RISCV:\n\t\treturn archRISCV\n\tcase elf.EM_S390:\n\t\treturn archS390x\n\tcase elf.EM_SPARCV9:\n\t\treturn archSPARC64\n\tdefault:\n\t\treturn machine.String()\n\t}\n}\n\nfunc peArchitectureToString(machine uint16) string {\n\tswitch machine {\n\tcase pe.IMAGE_FILE_MACHINE_I386:\n\t\treturn archI386\n\tcase pe.IMAGE_FILE_MACHINE_AMD64:\n\t\treturn archX8664\n\tcase pe.IMAGE_FILE_MACHINE_ARM:\n\t\treturn archARM\n\tcase pe.IMAGE_FILE_MACHINE_ARM64:\n\t\treturn archARM64\n\tdefault:\n\t\treturn fmt.Sprintf(\"Unknown (0x%x)\", machine)\n\t}\n}\n\nfunc machoCPUToString(cpu macho.Cpu) string {\n\tswitch cpu {\n\tcase macho.Cpu386:\n\t\treturn archI386\n\tcase macho.CpuAmd64:\n\t\treturn archX8664\n\tcase macho.CpuArm:\n\t\treturn archARM\n\tcase macho.CpuArm64:\n\t\treturn archARM64\n\tcase macho.CpuPpc:\n\t\treturn archPPC\n\tcase macho.CpuPpc64:\n\t\treturn archPPC64\n\tdefault:\n\t\treturn cpu.String()\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/architecture_test.go",
    "content": "package metadata\n\nimport (\n\t\"debug/elf\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetBinaryArchitecture_NonBinaryFile(t *testing.T) {\n\ttmpFile := t.TempDir() + \"/test.txt\"\n\n\terr := os.WriteFile(tmpFile, []byte(\"This is not a binary file\"), 0o644)\n\trequire.NoError(t, err)\n\n\tarch, err := GetBinaryArchitecture(tmpFile)\n\trequire.Error(t, err)\n\tassert.Empty(t, arch)\n}\n\nfunc TestGetBinaryArchitecture_NonExistentFile(t *testing.T) {\n\tarch, err := GetBinaryArchitecture(\"/nonexistent/file/path\")\n\trequire.Error(t, err)\n\tassert.Empty(t, arch)\n}\n\nfunc TestGetBinaryArchitecture_CurrentBinary(t *testing.T) {\n\texecutable, err := os.Executable()\n\tif err != nil {\n\t\tt.Skip(\"Could not get current executable path\")\n\t}\n\n\tarch, err := GetBinaryArchitecture(executable)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, arch)\n\n\thasValidPrefix := strings.HasPrefix(arch, \"ELF\") ||\n\t\tstrings.HasPrefix(arch, \"PE\") ||\n\t\tstrings.HasPrefix(arch, \"Mach-O\")\n\tassert.True(t, hasValidPrefix,\n\t\t\"Architecture should start with a known format prefix, got: %s\", arch)\n}\n\nfunc TestElfMachineToString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    uint16\n\t\texpected string\n\t}{\n\t\t{\"x86-64\", 0x3E, archX8664},\n\t\t{\"i386\", 0x03, archI386},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, elfMachineToString(elf.Machine(tt.input)))\n\t\t})\n\t}\n}\n\nfunc TestPeArchitectureToString(t *testing.T) {\n\tassert.Equal(t, archI386, peArchitectureToString(0x14c))\n\tassert.Equal(t, archX8664, peArchitectureToString(0x8664))\n\tassert.Equal(t, archARM, peArchitectureToString(0x1c0))\n\tassert.Equal(t, archARM64, peArchitectureToString(0xaa64))\n\tassert.Contains(t, peArchitectureToString(0x9999), \"Unknown\")\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/const.go",
    "content": "package metadata\n\nimport \"time\"\n\n// Spacing between Key and Value while rendering\nconst keyValueSpacing = \" \"\nconst keyValueSpacingLen = 1\n\nconst fileStatErrorMsg = \"Cannot load file stats\"\nconst linkFileBrokenMsg = \"Link file is broken!\"\nconst etFetchErrorMsg = \"Errors while fetching metadata via exiftool\"\n\nconst keyName = \"Name\"\nconst keySize = \"Size\"\nconst keyDataModified = \"Date Modified\"\nconst keyDataAccessed = \"Date Accessed\"\nconst keyPermissions = \"Permissions\"\nconst keyMd5Checksum = \"MD5Checksum\"\nconst keyOwner = \"Owner\"\nconst keyGroup = \"Group\"\nconst keyPath = \"Path\"\nconst keyArchitecture = \"Architecture\"\nconst borderSize = 2\n\n// Cache configuration\nconst defaultCacheSize = 300\nconst defaultCacheExpiration = 5 * time.Minute\n\nvar sortPriority = map[string]int{ //nolint: gochecknoglobals // This is effectively const.\n\t// Metadata field priority indices for display ordering\n\tkeyName:         0,\n\tkeySize:         1,\n\tkeyDataModified: 2, //nolint:mnd // display order index\n\tkeyDataAccessed: 3, //nolint:mnd // display order index\n\tkeyPermissions:  4, //nolint:mnd // display order index\n\tkeyOwner:        5, //nolint:mnd // display order index\n\tkeyGroup:        6, //nolint:mnd // display order index\n\tkeyPath:         7, //nolint:mnd // display order index\n\tkeyArchitecture: 8, //nolint:mnd // display order index\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/metadata.go",
    "content": "package metadata\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\n\t\"github.com/barasher/go-exiftool\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\ntype Metadata struct {\n\t// Stores key value pairs\n\tdata     [][2]string\n\tinfoMsg  string\n\tfilepath string\n}\n\nfunc NewMetadata(data [][2]string, filepath string, infoMsg string) Metadata {\n\treturn Metadata{\n\t\tdata:     data,\n\t\tfilepath: filepath,\n\t\tinfoMsg:  infoMsg,\n\t}\n}\n\nfunc (m Metadata) GetPath() string {\n\treturn m.filepath\n}\n\nfunc (m Metadata) GetData() [][2]string {\n\treturn m.data\n}\n\nfunc (m Metadata) GetValue(key string) (string, error) {\n\tfor _, pair := range m.data {\n\t\tif pair[0] == key {\n\t\t\treturn pair[1], nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"key %s not found\", key)\n}\n\n// Note : We dont use map[string]string, as metadata\n// 1 -> We dont need to support get(key) yet. Only usage is via iterating the whole list\n// 2 -> We need custom ordering\n\nfunc sortMetadata(meta [][2]string) {\n\tsort.SliceStable(meta, func(i, j int) bool {\n\t\tpi, iOkay := sortPriority[meta[i][0]]\n\t\tpj, jOkay := sortPriority[meta[j][0]]\n\n\t\t// Both are priority fields\n\t\tif iOkay && jOkay {\n\t\t\treturn pi < pj\n\t\t}\n\t\t// i is a priority field, and j is not\n\t\tif iOkay {\n\t\t\treturn true\n\t\t}\n\n\t\t// j is a priority field, and i is not\n\t\tif jOkay {\n\t\t\treturn false\n\t\t}\n\n\t\t// None of them are priority fields, sort with name\n\t\treturn meta[i][0] < meta[j][0]\n\t})\n}\n\nfunc GetMetadata(filePath string, metadataFocused bool, et *exiftool.Exiftool) Metadata {\n\tmeta := getMetaDataUnsorted(filePath, metadataFocused, et)\n\tsortMetadata(meta.data)\n\treturn meta\n}\n\nfunc getSymLinkMetaData(filePath string) Metadata {\n\tres := Metadata{\n\t\tfilepath: filePath,\n\t}\n\tlinkPath, symlinkErr := filepath.EvalSymlinks(filePath)\n\tif symlinkErr != nil {\n\t\tres.infoMsg = linkFileBrokenMsg\n\t} else {\n\t\tpath := [2]string{keyPath, linkPath}\n\t\tres.data = append(res.data, path)\n\t}\n\treturn res\n}\n\nfunc getMetaDataUnsorted(filePath string, metadataFocused bool, et *exiftool.Exiftool) Metadata {\n\tres := Metadata{\n\t\tfilepath: filePath,\n\t}\n\n\tfileInfo, err := os.Lstat(filePath)\n\tif err != nil {\n\t\tres.infoMsg = fileStatErrorMsg\n\t\treturn res\n\t}\n\tif fileInfo.Mode()&os.ModeSymlink != 0 {\n\t\treturn getSymLinkMetaData(filePath)\n\t}\n\t// Add basic metadata information irrespective of what is fetched from exiftool\n\t// Note : we prioritize these while sorting Metadata\n\tname := [2]string{keyName, fileInfo.Name()}\n\tsize := [2]string{keySize, common.FormatFileSize(fileInfo.Size())}\n\tmodifyDate := [2]string{keyDataModified, fileInfo.ModTime().String()}\n\tpermissions := [2]string{keyPermissions, fileInfo.Mode().String()}\n\townerVal, groupVal := getOwnerAndGroup(fileInfo)\n\towner := [2]string{keyOwner, ownerVal}\n\tgroup := [2]string{keyGroup, groupVal}\n\n\tif fileInfo.IsDir() && metadataFocused {\n\t\t// TODO : Calling dirSize() could be expensive for large directories, as it recursively\n\t\t// walks the entire tree. For now we have async approach of loading metadata,\n\t\t// and its only loaded when metadata panel is focused.\n\t\tsize = [2]string{keySize, common.FormatFileSize(utils.DirSize(filePath))}\n\t}\n\tres.data = append(res.data, name, size, modifyDate, permissions, owner, group)\n\n\tif fileInfo.Mode().IsRegular() {\n\t\tif arch, err := GetBinaryArchitecture(filePath); err == nil {\n\t\t\tarchData := [2]string{keyArchitecture, arch}\n\t\t\tres.data = append(res.data, archData)\n\t\t}\n\t}\n\n\tupdateExiftoolMetadata(filePath, et, &res)\n\n\tif fileInfo.Mode().IsRegular() && common.Config.EnableMD5Checksum {\n\t\t// Calculate MD5 checksum\n\t\tchecksum, err := calculateMD5Checksum(filePath)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error calculating MD5 checksum\", \"error\", err)\n\t\t} else {\n\t\t\tmd5Data := [2]string{keyMd5Checksum, checksum}\n\t\t\tres.data = append(res.data, md5Data)\n\t\t}\n\t}\n\n\treturn res\n}\n\nfunc updateExiftoolMetadata(filePath string, et *exiftool.Exiftool, res *Metadata) {\n\tif !common.Config.Metadata || et == nil {\n\t\treturn\n\t}\n\tfileInfos := et.ExtractMetadata(filePath)\n\n\tfor _, fileInfo := range fileInfos {\n\t\tif fileInfo.Err != nil {\n\t\t\tres.infoMsg = etFetchErrorMsg\n\t\t\tslog.Error(\"Error while return metadata function\", \"fileInfo\", fileInfo, \"error\", fileInfo.Err)\n\t\t\tcontinue\n\t\t}\n\t\tfor k, v := range fileInfo.Fields {\n\t\t\tres.data = append(res.data, [2]string{k, fmt.Sprintf(\"%v\", v)})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/metadata_test.go",
    "content": "package metadata\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/barasher/go-exiftool\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\nfunc TestGetMetadata(t *testing.T) {\n\tif runtime.GOOS != utils.OsLinux {\n\t\tt.Skip(\"Skipping metatada fetch test in windows and macOS\")\n\t}\n\tet, err := exiftool.NewExiftool()\n\trequire.NoError(t, err)\n\t_, curFilename, _, ok := runtime.Caller(0)\n\ttestdataDir := filepath.Join(filepath.Dir(curFilename), \"testdata\")\n\n\tdefaultKeys := []string{keyName, keySize, keyDataModified, keyPermissions}\n\n\trequire.True(t, ok)\n\ttestdata := []struct {\n\t\tname            string\n\t\tfilepath        string\n\t\tmetadataFocused bool\n\t}{\n\t\t{\n\t\t\tname:            \"Basic Metadata fetching\",\n\t\t\tfilepath:        filepath.Join(testdataDir, \"file1.txt\"),\n\t\t\tmetadataFocused: true,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tmeta := GetMetadata(tt.filepath, tt.metadataFocused, et)\n\t\t\tassert.Empty(t, meta.infoMsg)\n\t\t\tassert.Equal(t, tt.filepath, meta.filepath)\n\t\t\tfor _, key := range defaultKeys {\n\t\t\t\t_, err := meta.GetValue(key)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/metadata_unix.go",
    "content": "//go:build !windows\n\npackage metadata\n\nimport (\n\t\"os\"\n\t\"os/user\"\n\t\"strconv\"\n\t\"syscall\"\n)\n\nfunc getOwnerAndGroup(fileInfo os.FileInfo) (string, string) {\n\tusr := \"\"\n\tgrp := \"\"\n\tif stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok {\n\t\tuid := strconv.FormatUint(uint64(stat.Uid), 10)\n\t\tgid := strconv.FormatUint(uint64(stat.Gid), 10)\n\t\tif userData, err := user.LookupId(uid); err == nil {\n\t\t\tusr = userData.Username\n\t\t}\n\t\tif groupData, err := user.LookupGroupId(gid); err == nil {\n\t\t\tgrp = groupData.Name\n\t\t}\n\t}\n\treturn usr, grp\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/metadata_windows.go",
    "content": "//go:build windows\n\npackage metadata\n\nimport (\n\t\"os\"\n)\n\nfunc getOwnerAndGroup(_ os.FileInfo) (string, string) {\n\treturn \"\", \"\"\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/model.go",
    "content": "package metadata\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n\t\"github.com/yorukot/superfile/src/pkg/cache\"\n)\n\ntype Model struct {\n\tmetadata Metadata // current metadata\n\tcache    *cache.Cache[Metadata]\n\n\t// It tells what the metadata should have. Its used to prevent additional requests\n\t// if one is already underway\n\texpectedLocation string\n\texpectedFocused  bool\n\n\t// Render state\n\trenderIndex int\n\n\t// Model Dimensions, including borders\n\twidth  int\n\theight int\n}\n\nfunc New() Model {\n\treturn Model{\n\t\tcache: cache.New[Metadata](defaultCacheSize, defaultCacheExpiration),\n\t}\n}\n\n// Should be at least 2x2\n// TODO : Validate this\nfunc (m *Model) SetDimensions(width int, height int) {\n\tm.width = width\n\tm.height = height\n}\n\nfunc (m *Model) GetHeight() int {\n\treturn m.height\n}\n\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n\nfunc (m *Model) ResetRenderIfInvalid() {\n\tif m.renderIndex >= m.MetadataLen() {\n\t\tm.ResetRender()\n\t}\n}\n\nfunc (m *Model) ResetRender() {\n\tm.renderIndex = 0\n}\n\nfunc (m *Model) MetadataLen() int {\n\treturn len(m.metadata.data)\n}\n\n// Control metadata panel up\nfunc (m *Model) ListUp() {\n\tif m.MetadataLen() == 0 {\n\t\treturn\n\t}\n\tif m.renderIndex > 0 {\n\t\tm.renderIndex--\n\t} else {\n\t\tm.renderIndex = m.MetadataLen() - 1\n\t}\n}\n\n// Control metadata panel down\nfunc (m *Model) ListDown() {\n\tif m.renderIndex < m.MetadataLen()-1 {\n\t\tm.renderIndex++\n\t} else {\n\t\tm.renderIndex = 0\n\t}\n}\n\nfunc (m *Model) SetBlank() {\n\tm.metadata.filepath = \"\"\n\tm.metadata.data = m.metadata.data[:0]\n\tm.metadata.infoMsg = \"No metadata present\"\n}\n\nfunc (m *Model) IsBlank() bool {\n\treturn m.MetadataLen() == 0 && m.metadata.infoMsg == \"\"\n}\n\nfunc (m *Model) SetInfoMsg(msg string) {\n\tm.metadata.infoMsg = msg\n}\n\nfunc (m *Model) Render(metadataFocused bool) string {\n\tr := ui.MetadataRenderer(m.height, m.width, metadataFocused)\n\tif m.MetadataLen() == 0 {\n\t\tr.AddLines(\"\", \" \"+m.metadata.infoMsg)\n\t\treturn r.Render()\n\t}\n\tkeyLen, valueLen := computeRenderDimensions(m.metadata.data, m.width-2-keyValueSpacingLen)\n\tr.SetBorderInfoItems(fmt.Sprintf(\"%d/%d\", m.renderIndex+1, len(m.metadata.data)))\n\tlines := formatMetadataLines(m.metadata.data, m.renderIndex, m.height-borderSize, keyLen, valueLen)\n\tr.AddLines(lines...)\n\treturn r.Render()\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/model_test.go",
    "content": "package metadata\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUpDown(t *testing.T) {\n\tdefaultMetadata := Metadata{\n\t\tdata: make([][2]string, 5),\n\t}\n\ttestdata := []struct {\n\t\tname                string\n\t\tm                   Model\n\t\tlistDown            bool // Whether to do listDown or listUp\n\t\texpectedRenderIndex int\n\t}{\n\t\t{\n\t\t\tname: \"Basic down movement 1\",\n\t\t\tm: Model{\n\t\t\t\tmetadata:    defaultMetadata,\n\t\t\t\trenderIndex: 0,\n\t\t\t},\n\t\t\tlistDown:            true,\n\t\t\texpectedRenderIndex: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"Down wraps to top\",\n\t\t\tm: Model{\n\t\t\t\tmetadata:    defaultMetadata,\n\t\t\t\trenderIndex: 4,\n\t\t\t},\n\t\t\tlistDown:            true,\n\t\t\texpectedRenderIndex: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"Basic up movement 1\",\n\t\t\tm: Model{\n\t\t\t\tmetadata:    defaultMetadata,\n\t\t\t\trenderIndex: 4,\n\t\t\t},\n\t\t\tlistDown:            false,\n\t\t\texpectedRenderIndex: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"Up wraps to top\",\n\t\t\tm: Model{\n\t\t\t\tmetadata:    defaultMetadata,\n\t\t\t\trenderIndex: 0,\n\t\t\t},\n\t\t\tlistDown:            false,\n\t\t\texpectedRenderIndex: 4,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.listDown {\n\t\t\t\ttt.m.ListDown()\n\t\t\t} else {\n\t\t\t\ttt.m.ListUp()\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedRenderIndex, tt.m.renderIndex)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/testdata/file1.txt",
    "content": "12345678"
  },
  {
    "path": "src/internal/ui/metadata/update.go",
    "content": "package metadata\n\nimport \"strconv\"\n\nfunc (m *Model) SetMetadataCache(metadata Metadata, metadataFocused bool) {\n\tm.cache.Set(cacheKey(metadata.filepath, metadataFocused), metadata)\n}\n\nfunc (m *Model) SetMetadata(metadata Metadata, metadataFocused bool) {\n\tm.metadata = metadata\n\tm.SetMetadataLocationAndFocused(metadata.filepath, metadataFocused)\n\t// Note : Dont always reset render to 0\n\t// We would have update requests coming in during user scrolling through metadata\n\tm.ResetRenderIfInvalid()\n}\n\nfunc (m *Model) GetMetadataLocation() string {\n\treturn m.expectedLocation\n}\n\nfunc (m *Model) GetMetadataExpectedFocused() bool {\n\treturn m.expectedFocused\n}\n\nfunc (m *Model) SetMetadataLocationAndFocused(filepath string, metadataFocused bool) {\n\tm.expectedLocation = filepath\n\tm.expectedFocused = metadataFocused\n}\n\nfunc cacheKey(filePath string, metadataFocused bool) string {\n\treturn filePath + \":\" + strconv.FormatBool(metadataFocused)\n}\n\nfunc (m *Model) UpdateMetadataIfExistsInCache(filepath string, metadataFocused bool) bool {\n\tif meta, ok := m.cache.Get(cacheKey(filepath, metadataFocused)); ok {\n\t\tm.SetMetadata(meta, metadataFocused)\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "src/internal/ui/metadata/utils.go",
    "content": "package metadata\n\nimport (\n\t\"crypto/md5\" //nolint:gosec // MD5 used for file checksum display only, not for security\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc getMaxKeyLength(meta [][2]string) int {\n\tmaxLen := 0\n\tfor _, pair := range meta {\n\t\tif len(pair[0]) > maxLen {\n\t\t\tmaxLen = len(pair[0])\n\t\t}\n\t}\n\treturn maxLen\n}\n\nfunc computeMetadataWidths(viewWidth, maxKeyLen int) (int, int) {\n\tkeyLen := maxKeyLen\n\tvalueLen := viewWidth - keyLen\n\tif valueLen < viewWidth/2 {\n\t\t//nolint:mnd // standard halving for center split\n\t\tvalueLen = viewWidth / 2\n\t\tkeyLen = viewWidth - valueLen\n\t}\n\n\treturn keyLen, valueLen\n}\n\n// TODO : Simplify these mystic calculations, or add explanation comments.\n// TODO : unit test and fix this mess\nfunc formatMetadataLines(meta [][2]string, startIdx, height, keyLen, valueLen int) []string {\n\tlines := []string{}\n\tendIdx := min(startIdx+height, len(meta))\n\tfor i := startIdx; i < endIdx; i++ {\n\t\tvalue := common.TruncateMiddleText(meta[i][1], valueLen, \"...\")\n\t\tkey := common.TruncateMiddleText(meta[i][0], keyLen, \"...\")\n\t\tline := fmt.Sprintf(\"%-*s%s%s\", keyLen, key, keyValueSpacing, value)\n\t\tlines = append(lines, line)\n\t}\n\treturn lines\n}\n\nfunc computeRenderDimensions(metadata [][2]string, viewWidth int) (int, int) {\n\t// Compute dimension based values\n\tmaxKeyLen := getMaxKeyLength(metadata)\n\treturn computeMetadataWidths(viewWidth, maxKeyLen)\n}\n\n// TODO : Unit test this\nfunc calculateMD5Checksum(filePath string) (string, error) {\n\tfile, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to open file: %w\", err)\n\t}\n\tdefer file.Close()\n\n\thash := md5.New() //nolint:gosec // MD5 is sufficient for file integrity display, not used for security\n\tif _, err := io.Copy(hash, file); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to calculate MD5 checksum: %w\", err)\n\t}\n\n\tchecksum := hex.EncodeToString(hash.Sum(nil))\n\treturn checksum, nil\n}\n"
  },
  {
    "path": "src/internal/ui/notify/model.go",
    "content": "package notify\n\nimport (\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\ntype Model struct {\n\topen          bool\n\ttitle         string\n\tcontent       string\n\tconfirmAction ConfirmActionType\n}\n\nfunc New(open bool, title string, content string, confirmAction ConfirmActionType) Model {\n\treturn Model{\n\t\topen:          open,\n\t\ttitle:         title,\n\t\tcontent:       content,\n\t\tconfirmAction: confirmAction,\n\t}\n}\n\nfunc (m *Model) GetTitle() string {\n\treturn m.title\n}\n\nfunc (m *Model) GetContent() string {\n\treturn m.content\n}\n\nfunc (m *Model) IsOpen() bool {\n\treturn m.open\n}\n\nfunc (m *Model) Open() {\n\tm.open = true\n}\n\nfunc (m *Model) Close() {\n\tm.open = false\n}\n\nfunc (m *Model) GetConfirmAction() ConfirmActionType {\n\treturn m.confirmAction\n}\n\n// TODO: Remove code duplication with typineModalRender\nfunc (m *Model) Render() string {\n\tvar inputKeysText string\n\tif m.confirmAction == NoAction {\n\t\tinputKeysText = common.ModalOkayInputText\n\t} else {\n\t\tinputKeysText = common.ModalConfirmInputText + common.ModalInputSpacingText + common.ModalCancelInputText\n\t}\n\treturn common.ModalBorderStyle(common.ModalHeight, common.ModalWidth).\n\t\tRender(m.title + \"\\n\\n\" + m.content + \"\\n\\n\" + inputKeysText)\n}\n"
  },
  {
    "path": "src/internal/ui/notify/type.go",
    "content": "package notify\n\ntype ConfirmActionType int\n\nconst (\n\tRenameAction ConfirmActionType = iota\n\tDeleteAction\n\tQuitAction\n\tNoAction\n\tPermanentDeleteAction\n)\n"
  },
  {
    "path": "src/internal/ui/preview/model.go",
    "content": "package preview\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\n\tfilepreview \"github.com/yorukot/superfile/src/pkg/file_preview\"\n)\n\ntype Model struct {\n\topen bool\n\n\t// Location denotes what is supposed to be in model.\n\t// Might not be always in sync with content\n\tlocation      string\n\tcontent       string\n\tcontentWidth  int\n\tcontentHeight int\n\n\tloading            bool\n\timagePreviewer     *filepreview.ImagePreviewer\n\tbatCmd             string\n\tthumbnailGenerator *filepreview.ThumbnailGenerator\n}\n\nfunc New() Model {\n\tgenerator, err := filepreview.NewThumbnailGenerator()\n\tif err != nil {\n\t\tslog.Error(\"Could not NewThumbnailGenerator object\", \"error\", err)\n\t}\n\n\treturn Model{\n\t\topen: common.Config.DefaultOpenFilePreview,\n\t\t// TODO: This causes unnecessary terminal cell size detection\n\t\t// logs in tests, we should not be initializing it in tests\n\t\t// when  `DefaultOpenFilePreview` is false\n\t\t// And only initialize these objects when Open() is called\n\t\t// Still them being nil should be handled well, right we don't\n\t\t// have code with good defensive programming\n\t\t// Some of these processes are IO operations so maybe it should\n\t\t// be done via an Init() function\n\t\timagePreviewer:     filepreview.NewImagePreviewer(),\n\t\tthumbnailGenerator: generator,\n\t\t// TODO:  This is an IO operation, move to async ?\n\t\tbatCmd: checkBatCmd(),\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/preview/model_utils.go",
    "content": "package preview\n\nimport \"log/slog\"\n\nfunc (m *Model) GetContent() string {\n\treturn m.content\n}\n\nfunc (m *Model) GetContentWidth() int {\n\treturn m.contentWidth\n}\n\nfunc (m *Model) GetContentHeight() int {\n\treturn m.contentHeight\n}\n\nfunc (m *Model) GetLocation() string {\n\treturn m.location\n}\n\nfunc (m *Model) SetOpen(open bool) {\n\tm.open = open\n}\n\nfunc (m *Model) SetLocation(location string) {\n\tm.location = location\n}\n\nfunc (m *Model) SetLoading() {\n\tm.loading = true\n}\n\n// All content change happen via this only, to ensure the sync between\n// content and width x height, and the loading variable reset\nfunc (m *Model) setContent(content string, width int, height int, location string) {\n\tm.content = content\n\tm.contentWidth = width\n\tm.contentHeight = height\n\tm.location = location\n\tm.loading = false\n}\n\nfunc (m *Model) SetEmptyWithDimensions(width int, height int) {\n\tm.setContent(m.RenderTextWithDimension(\"\", height, width), width, height, \"\")\n}\n\nfunc (m *Model) IsLoading() bool {\n\treturn m.loading\n}\n\nfunc (m *Model) ToggleOpen() {\n\tm.open = !m.open\n}\n\nfunc (m *Model) CleanUp() {\n\tif m.thumbnailGenerator != nil {\n\t\terr := m.thumbnailGenerator.CleanUp()\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error While cleaning up TempDirectory\", \"error\", err)\n\t\t}\n\t}\n}\n\nfunc (m *Model) IsOpen() bool {\n\treturn m.open\n}\n\nfunc (m *Model) Open() {\n\tm.open = true\n}\n\nfunc (m *Model) Close() {\n\tm.open = false\n}\n"
  },
  {
    "path": "src/internal/ui/preview/render.go",
    "content": "package preview\n\nimport (\n\t\"errors\"\n\t\"image\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/chroma/v2/lexers\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/yorukot/ansichroma\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n\t\"github.com/yorukot/superfile/src/internal/ui/rendering\"\n)\n\nfunc renderDirectoryPreview(r *rendering.Renderer, itemPath string, previewHeight int) string {\n\tfiles, err := os.ReadDir(itemPath)\n\tif err != nil {\n\t\tslog.Error(\"Error render directory preview\", \"error\", err)\n\t\tr.AddLines(common.FilePreviewDirectoryUnreadableText)\n\t\treturn r.Render()\n\t}\n\n\tif len(files) == 0 {\n\t\tr.AddLines(common.FilePreviewEmptyText)\n\t\treturn r.Render()\n\t}\n\n\tsort.Slice(files, func(i, j int) bool {\n\t\tif files[i].IsDir() && !files[j].IsDir() {\n\t\t\treturn true\n\t\t}\n\t\tif !files[i].IsDir() && files[j].IsDir() {\n\t\t\treturn false\n\t\t}\n\t\treturn files[i].Name() < files[j].Name()\n\t})\n\n\tfor i := 0; i < previewHeight && i < len(files); i++ {\n\t\tfile := files[i]\n\t\tisLink := false\n\t\tif info, err := file.Info(); err == nil {\n\t\t\tisLink = info.Mode()&os.ModeSymlink != 0\n\t\t}\n\t\tstyle := common.GetElementIcon(file.Name(), file.IsDir(), isLink, common.Config.Nerdfont)\n\t\tres := lipgloss.NewStyle().Foreground(lipgloss.Color(style.Color)).Background(common.FilePanelBGColor).\n\t\t\tRender(style.Icon+\" \") + common.FilePanelStyle.Render(file.Name())\n\t\tr.AddLines(res)\n\t}\n\treturn r.Render()\n}\n\nfunc (m *Model) renderImagePreview(r *rendering.Renderer, itemPath string, previewWidth,\n\tpreviewHeight int, sideAreaWidth int, clearCmd string,\n) string {\n\tif !m.open {\n\t\treturn r.AddLines(common.FilePreviewPanelClosedText).Render() + clearCmd\n\t}\n\n\tif !common.Config.ShowImagePreview {\n\t\treturn r.AddLines(common.FilePreviewImagePreviewDisabledText).Render() + clearCmd\n\t}\n\n\t// Use the new auto-detection function to choose the best renderer\n\timageRender, err := m.imagePreviewer.ImagePreview(itemPath, previewWidth, previewHeight,\n\t\tcommon.Theme.FilePanelBG, sideAreaWidth)\n\tif errors.Is(err, image.ErrFormat) {\n\t\treturn r.AddLines(common.FilePreviewUnsupportedImageFormatsText).Render() + clearCmd\n\t}\n\n\tif err != nil {\n\t\tslog.Error(\"Error convert image to ansi\", \"error\", err)\n\t\treturn r.AddLines(common.FilePreviewImageConversionErrorText).Render() + clearCmd\n\t}\n\n\t// Check if this looks like Kitty protocol output (starts with escape sequences)\n\t// For Kitty protocol, avoid using lipgloss alignment to prevent layout drift\n\tif strings.HasPrefix(imageRender, \"\\x1b_G\") {\n\t\tr.AddLines(imageRender)\n\t\treturn r.Render()\n\t}\n\n\t// For ANSI output, we can safely use vertical alignment\n\treturn r.AddStyleModifier(func(s lipgloss.Style) lipgloss.Style {\n\t\treturn s.AlignHorizontal(lipgloss.Center).AlignVertical(lipgloss.Center)\n\t}).AddLines(imageRender).Render() + clearCmd\n}\n\nfunc (m *Model) renderTextPreview(r *rendering.Renderer, itemPath string,\n\tpreviewWidth, previewHeight int,\n) string {\n\tformat := lexers.Match(filepath.Base(itemPath))\n\tif format == nil {\n\t\tisText, err := common.IsTextFile(itemPath)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error while checking text file\", \"error\", err)\n\t\t\treturn r.AddLines(common.FilePreviewError).Render()\n\t\t} else if !isText {\n\t\t\treturn r.AddLines(common.FilePreviewUnsupportedFormatText).Render()\n\t\t}\n\t}\n\n\tfileContent, err := utils.ReadFileContent(itemPath, previewWidth, previewHeight)\n\tif err != nil {\n\t\tslog.Error(\"Error open file\", \"error\", err)\n\t\treturn r.AddLines(common.FilePreviewError).Render()\n\t}\n\n\tif fileContent == \"\" {\n\t\treturn r.AddLines(common.FilePreviewEmptyText).Render()\n\t}\n\n\tif format != nil {\n\t\tbackground := \"\"\n\t\tif !common.Config.TransparentBackground {\n\t\t\tbackground = common.Theme.FilePanelBG\n\t\t}\n\t\tif common.Config.CodePreviewer == \"bat\" {\n\t\t\tif m.batCmd == \"\" {\n\t\t\t\treturn r.AddLines(common.FilePreviewBatNotInstalledText).Render()\n\t\t\t}\n\t\t\tfileContent, err = getBatSyntaxHighlightedContent(itemPath, previewHeight, background, m.batCmd)\n\t\t} else {\n\t\t\tfileContent, err = ansichroma.HightlightString(fileContent, format.Config().Name,\n\t\t\t\tcommon.Theme.CodeSyntaxHighlightTheme, background)\n\t\t}\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error render code highlight\", \"error\", err)\n\t\t\treturn r.AddLines(common.FilePreviewError).Render()\n\t\t}\n\t}\n\n\tr.AddLines(fileContent)\n\treturn r.Render()\n}\n\n// Only use this when height and width are synced with filemodel's expectations\nfunc (m *Model) RenderText(text string) string {\n\treturn m.RenderTextWithDimension(text, m.contentHeight, m.contentWidth)\n}\n\nfunc (m *Model) RenderTextWithDimension(text string, height int, width int) string {\n\t// For zero size, don't need to render anything. Its kinda hack, but\n\t// its to prevent error logs\n\tclearCmd := m.imagePreviewer.ClearKittyImages()\n\tif width == 0 && height == 0 {\n\t\treturn clearCmd\n\t}\n\treturn ui.FilePreviewPanelRenderer(height, width).\n\t\tAddLines(text).\n\t\tRender() + clearCmd\n}\n\nfunc (m *Model) RenderWithPath(itemPath string, previewWidth int, previewHeight int, fullModelWidth int) string {\n\tr := ui.FilePreviewPanelRenderer(previewHeight, previewWidth)\n\tclearCmd := m.imagePreviewer.ClearKittyImages()\n\n\t// Adjust dimensions if border is enabled\n\tcontentWidth := previewWidth\n\tcontentHeight := previewHeight\n\tif common.Config.EnableFilePreviewBorder {\n\t\tcontentWidth = previewWidth - common.BorderPadding\n\t\tcontentHeight = previewHeight - common.BorderPadding\n\t}\n\n\tfileInfo, infoErr := os.Stat(itemPath)\n\tif infoErr != nil {\n\t\tslog.Error(\"Error get file info\", \"error\", infoErr)\n\t\treturn r.AddLines(common.FilePreviewNoFileInfoText).Render() + clearCmd\n\t}\n\tslog.Debug(\"Attempting to render preview\", \"itemPath\", itemPath,\n\t\t\"mode\", fileInfo.Mode().String(), \"isRegular\", fileInfo.Mode().IsRegular())\n\n\t// For non regular files which are not directories Dont try to read them\n\t// See Issue #876\n\tif !fileInfo.Mode().IsRegular() && (fileInfo.Mode()&fs.ModeDir) == 0 {\n\t\treturn r.AddLines(common.FilePreviewUnsupportedFileMode).Render() + clearCmd\n\t}\n\n\text := filepath.Ext(itemPath)\n\tif slices.Contains(common.UnsupportedPreviewFormats, ext) {\n\t\treturn r.AddLines(common.FilePreviewUnsupportedFormatText).Render() + clearCmd\n\t}\n\n\tif fileInfo.IsDir() {\n\t\treturn renderDirectoryPreview(r, itemPath, contentHeight) + clearCmd\n\t}\n\n\tif m.thumbnailGenerator != nil && m.thumbnailGenerator.SupportsExt(ext) {\n\t\tthumbnailPath, err := m.thumbnailGenerator.GetThumbnailOrGenerate(itemPath)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Error generating thumbnail\", \"error\", err)\n\t\t\treturn r.AddLines(common.FilePreviewThumbnailGenerationErrorText).Render() + clearCmd\n\t\t}\n\t\t// Notes : If renderImagePreview fails, and return some error message\n\t\t// render, then we dont apply clearCmd. This might cause issues.\n\t\t// same for below usage of renderImagePreview\n\t\treturn m.renderImagePreview(\n\t\t\tr, thumbnailPath, contentWidth, contentHeight,\n\t\t\tfullModelWidth-previewWidth, clearCmd)\n\t}\n\n\tif isImageFile(itemPath) {\n\t\treturn m.renderImagePreview(\n\t\t\tr, itemPath, contentWidth, contentHeight,\n\t\t\tfullModelWidth-previewWidth, clearCmd)\n\t}\n\n\treturn m.renderTextPreview(r, itemPath, contentWidth, contentHeight) + clearCmd\n}\n"
  },
  {
    "path": "src/internal/ui/preview/render_test.go",
    "content": "package preview\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\n/*\n- TODO Tests\n  - testdata with 10-15 small files(< 100 bytes each) with all kind of contents\n  - ascii control chars\n  - bin content\n  - video, pdf, image, corrupted files, files with bad perms?,\n  - symlinks, directories,\n*/\n\nfunc TestFilePreviewRenderWithDimensions(t *testing.T) {\n\ttestDir := t.TempDir()\n\t// Test that\n\t// 1 - we can truncate width and height\n\t// 2 - We add extra whitespace to make up for width and height\n\t// 3 - Emojis and special unicodes characters can be rendered and Special characters - ~!@#$%^&*()_+-={}\\\"\"\n\t// 4 - File with spaces, tabs, unicode spaces, etc, is rendered correctly\n\t// 5 - File with problematic characters like ascii control char, invalid unicodes etc,\n\t//     is cleaned up\n\n\t// Additional tests\n\t// 1 - File with ascii color sequences can be rendered correctly\n\t// 2 - Test all cases - unsupported file, non text file\n\tcurTestDir := filepath.Join(testDir, \"TestFilePreviewRender\")\n\n\t// Cleanup is taken care by TestMain()\n\tutils.SetupDirectories(t, curTestDir)\n\n\ttestdata := []struct {\n\t\tname            string\n\t\tfileContent     string\n\t\tfileName        string\n\t\theight          int\n\t\twidth           int\n\t\texpectedPreview string\n\t}{\n\t\t{\n\t\t\tname: \"Basic test\",\n\t\t\tfileContent: \"\" +\n\t\t\t\t\"abcd\\n\" +\n\t\t\t\t\"1234\",\n\t\t\tfileName: \"basic.txt\",\n\t\t\theight:   2,\n\t\t\twidth:    4,\n\t\t\texpectedPreview: \"\" +\n\t\t\t\t\"abcd\\n\" +\n\t\t\t\t\"1234\",\n\t\t},\n\t\t{\n\t\t\tname: \"Width and height truncation\",\n\t\t\tfileContent: \"\" +\n\t\t\t\t\"abcd\\n\" +\n\t\t\t\t\"1234\\n\" +\n\t\t\t\t\"WXYZ\",\n\t\t\tfileName: \"truncate.txt\",\n\t\t\theight:   2,\n\t\t\twidth:    3,\n\t\t\texpectedPreview: \"\" +\n\t\t\t\t\"abc\\n\" +\n\t\t\t\t\"123\",\n\t\t},\n\t\t{\n\t\t\tname: \"Whitespace filling\",\n\t\t\tfileContent: \"\" +\n\t\t\t\t\"abc\\n\" +\n\t\t\t\t\"123\",\n\t\t\tfileName: \"fill.txt\",\n\t\t\theight:   3,\n\t\t\twidth:    4,\n\t\t\texpectedPreview: \"\" +\n\t\t\t\t\"abc \\n\" +\n\t\t\t\t\"123 \\n\" +\n\t\t\t\t\"    \",\n\t\t},\n\t\t{\n\t\t\tname: \"Special char, Emojies and special unicodes\",\n\t\t\tfileContent: \"\" +\n\t\t\t\t\"✅\\uf410\\U000f0868abcdABCD0123~\\n\" +\n\t\t\t\t\"!@#$%^&*()_+-={}|:\\\"<>?,./;'[]\",\n\t\t\tfileName: \"special.txt\",\n\t\t\theight:   2,\n\t\t\twidth:    30,\n\t\t\texpectedPreview: \"\" +\n\t\t\t\t\"✅\\uf410\\U000f0868abcdABCD0123~             \\n\" +\n\t\t\t\t\"!@#$%^&*()_+-={}|:\\\"<>?,./;'[] \",\n\t\t},\n\t\t{\n\t\t\t// Contains various Unicode whitespace characters:\n\t\t\t// U+00A0 (NO-BREAK SPACE)\n\t\t\t// U+202F (NARROW NO-BREAK SPACE)\n\t\t\t// U+205F (MEDIUM MATHEMATICAL SPACE)\n\t\t\t// U+2029 (PARAGRAPH SEPARATOR)\n\t\t\tname: \"Whitespace handling\",\n\t\t\tfileContent: \"\" +\n\t\t\t\t\"\\n\" +\n\t\t\t\t\"\\t1\\t\\t2\\t\\n\" +\n\t\t\t\t\"0\\u00a01\\u00a02\\u202f3\\u205f4\\u20295\\u202f6\\u205f7\\u2029\\n\" +\n\t\t\t\t\"0\\u30001\\u30002\",\n\t\t\tfileName: \"whitespace.txt\",\n\t\t\theight:   5,\n\t\t\twidth:    12,\n\t\t\texpectedPreview: \"\" +\n\t\t\t\t\"            \\n\" +\n\t\t\t\t\"    1       \\n\" +\n\t\t\t\t\"0\\u00a01\\u00a02 3 4 5 \\n\" +\n\t\t\t\t\"0 1 2       \\n\" +\n\t\t\t\t\"            \",\n\t\t},\n\t\t{\n\t\t\t// Contains control characters:\n\t\t\t// \\x0b (Vertical Tab)\n\t\t\t// \\x0d (Carriage Return)\n\t\t\t// \\x00 (Null)\n\t\t\t// \\x05 (Enquiry)\n\t\t\t// \\x0f (Shift In)\n\t\t\t// \\x7f (Delete)\n\t\t\t// \\xa0 (Non-breaking space)\n\t\t\t// \\ufffd (Replacement character)\n\t\t\tname: \"Invalid character cleanup\",\n\t\t\tfileContent: \"\" +\n\t\t\t\t\"\\x0b\\x0d\\x00\\x05\\x0f\\x7f\\xa0\\ufffd\",\n\t\t\tfileName: \"invalid.txt\",\n\t\t\theight:   2,\n\t\t\twidth:    10,\n\t\t\texpectedPreview: \"\" +\n\t\t\t\t\"          \\n\" +\n\t\t\t\t\"          \",\n\t\t},\n\t}\n\n\tfor i, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcurDir := filepath.Join(curTestDir, \"dir\"+strconv.Itoa(i))\n\t\t\tutils.SetupDirectories(t, curDir)\n\t\t\tfilePath := filepath.Join(curDir, tt.fileName)\n\t\t\terr := os.WriteFile(filePath, []byte(tt.fileContent), 0o644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tm := New()\n\t\t\tres := ansi.Strip(m.RenderWithPath(filePath, tt.width, tt.height, tt.width))\n\n\t\t\tassert.Equal(t, tt.expectedPreview, res, \"filePath = %s\", filePath)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/preview/render_unix_test.go",
    "content": "//go:build !windows\n\npackage preview\n\nimport (\n\t\"path/filepath\"\n\t\"syscall\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc TestFilePreviewWithInvalidMode(t *testing.T) {\n\tcurTestDir := t.TempDir()\n\tfile := filepath.Join(curTestDir, \"testf\")\n\n\terr := syscall.Mkfifo(file, 0644)\n\trequire.NoError(t, err)\n\n\tm := New()\n\tres := m.RenderWithPath(file, 20, 10, 20)\n\tassert.Contains(t, res, common.FilePreviewUnsupportedFileMode)\n}\n"
  },
  {
    "path": "src/internal/ui/preview/render_utils.go",
    "content": "package preview\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc getBatSyntaxHighlightedContent(\n\titemPath string,\n\tpreviewLine int,\n\tbackground string,\n\tbatCmd string,\n) (string, error) {\n\t// --plain: use the plain style without line numbers and decorations\n\t// --force-colorization: force colorization for non-interactive shell output\n\t// --line-range <:m>: only read from line 1 to line \"m\"\n\tbatArgs := []string{itemPath, \"--plain\", \"--force-colorization\", \"--line-range\", fmt.Sprintf(\":%d\", previewLine)}\n\n\t// set timeout for the external command execution to 500ms max\n\tctx, cancel := context.WithTimeout(context.Background(), common.DefaultPreviewTimeout)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, batCmd, batArgs...)\n\n\tfileContentBytes, err := cmd.Output()\n\tif err != nil {\n\t\tslog.Error(\"Error render code highlight\", \"error\", err)\n\t\treturn \"\", err\n\t}\n\n\tfileContent := string(fileContentBytes)\n\tif !common.Config.TransparentBackground {\n\t\tfileContent = setBatBackground(fileContent, background)\n\t}\n\treturn fileContent, nil\n}\n\nfunc setBatBackground(input string, background string) string {\n\ttokens := strings.Split(input, \"\\x1b[0m\")\n\tbackgroundStyle := lipgloss.NewStyle().Background(lipgloss.Color(background))\n\tfor idx, token := range tokens {\n\t\ttokens[idx] = backgroundStyle.Render(token)\n\t}\n\treturn strings.Join(tokens, \"\\x1b[0m\")\n}\n\n// Check if bat is an executable in PATH and whether to use bat or batcat as command\nfunc checkBatCmd() string {\n\tif _, err := exec.LookPath(\"bat\"); err == nil {\n\t\treturn \"bat\"\n\t}\n\t// on ubuntu bat executable is called batcat\n\tif _, err := exec.LookPath(\"batcat\"); err == nil {\n\t\treturn \"batcat\"\n\t}\n\treturn \"\"\n}\n\nfunc isImageFile(filename string) bool {\n\treturn common.ImageExtensions[strings.ToLower(filepath.Ext(filename))]\n}\n"
  },
  {
    "path": "src/internal/ui/preview/update.go",
    "content": "package preview\n\n// UpdateMsg represents an async query result\n\ntype UpdateMsg struct {\n\t// location can contain either the path of current content's file\n\t// or path of file whose preview request is in flight.\n\t// It should not have past data\n\tlocation string\n\n\t// preview panel's content needs to be in sync with its width/height\n\t// you cannot update width/height without updating the content\n\tcontent       string\n\tcontentWidth  int\n\tcontentHeight int\n\treqID         int\n}\n\nfunc NewUpdateMsg(location string, content string, width int, height int, reqID int) UpdateMsg {\n\treturn UpdateMsg{\n\t\tlocation:      location,\n\t\tcontent:       content,\n\t\tcontentWidth:  width,\n\t\tcontentHeight: height,\n\t\treqID:         reqID,\n\t}\n}\n\nfunc (msg UpdateMsg) GetReqID() int {\n\treturn msg.reqID\n}\n\nfunc (m *Model) Apply(msg UpdateMsg) {\n\tm.setContent(msg.content, msg.contentWidth, msg.contentHeight, msg.location)\n}\n\nfunc (msg UpdateMsg) GetLocation() string {\n\treturn msg.location\n}\n\nfunc (msg UpdateMsg) GetContentWidth() int {\n\treturn msg.contentWidth\n}\n\nfunc (msg UpdateMsg) GetContentHeight() int {\n\treturn msg.contentHeight\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/README.md",
    "content": "# processbar package\nThis package is for processbar. \nThis should not import internal package, and should not be aware of main 'model'\n\n\n# To-do\n- Finish code TODOs\n- Add end to end test with model\n- Add unit tests for Render(), and getSortedProcesses()\n"
  },
  {
    "path": "src/internal/ui/processbar/const.go",
    "content": "package processbar\n\nconst (\n\t// Min width and height for borders\n\tminHeight = 2\n\tminWidth  = 2\n\n\t// This should allow smooth tracking of 5-10 active processes\n\t// In case we have issues in future, we could attempt to change this\n\tmsgChannelSize = 50\n\n\t// UI dimension constants for process bar rendering\n\t// borderSize is the border width for the process bar panel\n\tborderSize = 2\n\n\t// progressBarRightPadding is padding after progress bar\n\tprogressBarRightPadding = 3\n\n\t// processNameTruncatePadding is the space reserved for ellipsis and icon in process name\n\tprocessNameTruncatePadding = 7\n\n\t// linesPerProcess is the number of lines needed to render one process\n\tlinesPerProcess = 3\n)\n"
  },
  {
    "path": "src/internal/ui/processbar/error.go",
    "content": "package processbar\n\ntype ProcessChannelFullError struct {\n}\n\nfunc (p *ProcessChannelFullError) Error() string {\n\treturn \"process channel is full\"\n}\n\ntype NoProcessFoundError struct {\n\tid string\n}\n\nfunc (p *NoProcessFoundError) Error() string {\n\treturn \"no process with id : \" + p.id\n}\n\ntype ProcessAlreadyExistsError struct {\n\tid string\n}\n\nfunc (p *ProcessAlreadyExistsError) Error() string {\n\treturn \"process already exists with id : \" + p.id\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model.go",
    "content": "package processbar\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n)\n\n// Model for process bar internal\ntype Model struct {\n\trenderIndex int\n\tcursor      int\n\n\t// Including borders\n\theight int\n\twidth  int\n\n\t// TODO: Fix this. No mechanism to remove completed processes from memory\n\t// processes map grows indefinitely\n\t// Maybe, TTL or cleanup mechanism for successful/failed processes\n\tprocesses map[string]Process\n\tmsgChan   chan UpdateMsg\n\treqCnt    int\n}\n\nfunc New() Model {\n\treturn NewModelWithOptions(minWidth, minHeight)\n}\n\n// Note: We should considering our internal models, they\n// should be returning pointer object, and implement tea.Model\nfunc NewModelWithOptions(width int, height int) Model {\n\tm := Model{\n\t\trenderIndex: 0,\n\t\tcursor:      0,\n\t\tprocesses:   make(map[string]Process),\n\t\tmsgChan:     make(chan UpdateMsg, msgChannelSize),\n\t\treqCnt:      0,\n\t}\n\tm.SetDimensions(width, height)\n\treturn m\n}\n\nfunc (m *Model) SetDimensions(width int, height int) {\n\tif width < minWidth {\n\t\tslog.Warn(\"Invalid width, using minimum\", \"provided\", width, \"minimum\", minWidth)\n\t\twidth = minWidth\n\t}\n\tif height < minHeight {\n\t\tslog.Warn(\"Invalid height, using minimum\", \"provided\", height, \"minimum\", minHeight)\n\t\theight = minHeight\n\t}\n\tm.width = width\n\tm.height = height\n}\n\nfunc (m *Model) AddProcess(p Process) error {\n\tif _, ok := m.processes[p.ID]; ok {\n\t\treturn &ProcessAlreadyExistsError{id: p.ID}\n\t}\n\tm.processes[p.ID] = p\n\treturn nil\n}\n\nfunc (m *Model) AddOrUpdateProcess(p Process) {\n\tm.processes[p.ID] = p\n}\n\nfunc (m *Model) UpdateExistingProcess(p Process) error {\n\tif _, ok := m.processes[p.ID]; !ok {\n\t\treturn &NoProcessFoundError{id: p.ID}\n\t}\n\tm.processes[p.ID] = p\n\treturn nil\n}\n\nfunc (m *Model) GetByID(id string) (Process, bool) {\n\tp, ok := m.processes[id]\n\treturn p, ok\n}\n\nfunc (m *Model) HasRunningProcesses() bool {\n\tfor _, data := range m.processes {\n\t\tif data.State == InOperation && data.Done != data.Total {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (m *Model) Render(processBarFocused bool) string {\n\tr := ui.ProcessBarRenderer(m.height, m.width, processBarFocused)\n\tif !m.isValid() {\n\t\tslog.Error(\"processBar in invalid state\", \"render\", m.renderIndex,\n\t\t\t\"cursor\", m.cursor, \"Height\", m.height)\n\t\tr.AddLines(\"Invalid state\")\n\t\treturn r.Render()\n\t}\n\tif m.cntProcesses() == 0 {\n\t\tr.AddLines(\"\", \" \"+common.ProcessBarNoneText)\n\t\treturn r.Render()\n\t}\n\n\tr.SetBorderInfoItems(fmt.Sprintf(\"%d/%d\", m.cursor+1, m.cntProcesses()))\n\n\trenderedHeight := 0\n\tprocesses := m.getSortedProcesses()\n\tfor i := m.renderIndex; i < len(processes); i++ {\n\t\t// We allow rendering of a process if we have at least 2 lines left\n\t\tif m.viewHeight() < renderedHeight+2 {\n\t\t\tbreak\n\t\t}\n\t\trenderedHeight += 3\n\n\t\t// Note : We will be updating this on each Render, although harmless from performance\n\t\t// perspective. We are rendering modified version of the data.\n\t\t// TODO: We could, save pointer of process in map and update progressbar of each\n\t\t// map on each SetWidth. This would be cleaner and more efficient.\n\t\tcurProcess := processes[i]\n\t\tcurProcess.Progress.Width = m.viewWidth() - progressBarRightPadding\n\n\t\t// TODO : get them via a separate function.\n\t\tvar cursor string\n\t\tif i == m.cursor {\n\t\t\t// TODO : Prerender it.\n\t\t\tcursor = common.FooterCursorStyle.Render(\"┃ \")\n\t\t} else {\n\t\t\tcursor = common.FooterCursorStyle.Render(\"  \")\n\t\t}\n\n\t\tr.AddLines(cursor + common.FooterStyle.Render(\n\t\t\tcommon.TruncateText(curProcess.GetDisplayName(), m.viewWidth()-processNameTruncatePadding, \"...\")+\" \") +\n\t\t\tcurProcess.State.Icon())\n\n\t\t// We add two lines here, and let the renderer take care of\n\t\t// dropping the second line if it exceeds height\n\t\tif curProcess.Total != 0 {\n\t\t\tprogressPercentage := float64(curProcess.Done) / float64(curProcess.Total)\n\t\t\tr.AddLines(cursor+curProcess.Progress.ViewAs(progressPercentage), \"\")\n\t\t} else {\n\t\t\t// if the total is 0, that means the process only have directory\n\t\t\t// so we can set the progress to 100%\n\t\t\tr.AddLines(cursor+curProcess.Progress.ViewAs(1), \"\")\n\t\t}\n\t}\n\n\treturn r.Render()\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model_navigation.go",
    "content": "package processbar\n\nimport (\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// Control processbar panel list up\n// There is a shadowing happening here, but it will be removed\n// Once we make footerHeight part of model struct\nfunc (m *Model) ListUp() {\n\tcntP := m.cntProcesses()\n\tif cntP == 0 {\n\t\treturn\n\t}\n\tif m.cursor > 0 {\n\t\tm.cursor--\n\t\tif m.cursor < m.renderIndex {\n\t\t\tm.renderIndex--\n\t\t}\n\t} else {\n\t\tm.cursor = cntP - 1\n\t\t// Either start from beginning or\n\t\t// from a process so that we could render last one\n\t\tm.renderIndex = max(0, cntP-m.cntRenderableProcess())\n\t}\n}\n\n// Control processbar panel list down\nfunc (m *Model) ListDown() {\n\tcntP := m.cntProcesses()\n\tif cntP == 0 {\n\t\treturn\n\t}\n\tif m.cursor < cntP-1 {\n\t\tm.cursor++\n\t\tif m.cursor > m.renderIndex+m.cntRenderableProcess()-1 {\n\t\t\tm.renderIndex++\n\t\t}\n\t} else {\n\t\tm.renderIndex = 0\n\t\tm.cursor = 0\n\t}\n}\n\nfunc (m *Model) cntRenderableProcess() int {\n\tfooterHeight := m.height - common.BorderPadding\n\treturn cntRenderableProcess(footerHeight)\n}\n\nfunc cntRenderableProcess(footerHeight int) int {\n\t// We can render one process in three lines\n\t// And last process in two or three lines ( with/without a line separtor)\n\treturn (footerHeight + 1) / linesPerProcess\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model_navigation_test.go",
    "content": "package processbar\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_cntRenderableProcess(t *testing.T) {\n\tassert.Equal(t, 1, cntRenderableProcess(4))\n\tassert.Equal(t, 2, cntRenderableProcess(5))\n\tassert.Equal(t, 2, cntRenderableProcess(6))\n\tassert.Equal(t, 2, cntRenderableProcess(7))\n\tassert.Equal(t, 3, cntRenderableProcess(8))\n\tassert.Equal(t, 3, cntRenderableProcess(9))\n\tassert.Equal(t, 3, cntRenderableProcess(10))\n\tassert.Equal(t, 4, cntRenderableProcess(11))\n}\n\nfunc genProcessBarModel(count int, cursor int, render int, viewHeight int) Model {\n\tpMap := map[string]Process{}\n\tfor i := range count {\n\t\tpID := strconv.Itoa(i)\n\t\tpMap[pID] = Process{\n\t\t\tID:          pID,\n\t\t\tCurrentFile: pID,\n\t\t}\n\t}\n\treturn Model{\n\t\tprocesses:   pMap,\n\t\tcursor:      cursor,\n\t\trenderIndex: render,\n\t\twidth:       minWidth,\n\t\theight:      viewHeight + 2,\n\t}\n}\n\nfunc Test_processBarModelUpDown(t *testing.T) {\n\ttestdata := []struct {\n\t\tname           string\n\t\tprocessCnt     int\n\t\tcursor         int\n\t\trender         int\n\t\tlistDown       bool // Whether to do ListDown or ListUp\n\t\texpectedCursor int\n\t\texpectedRender int\n\t\tfooterHeight   int\n\t}{\n\t\t{\n\t\t\tname:           \"Basic down movement 1\",\n\t\t\tprocessCnt:     10,\n\t\t\tcursor:         0,\n\t\t\trender:         0,\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 1,\n\t\t\texpectedRender: 0,\n\t\t\tfooterHeight:   10,\n\t\t},\n\t\t{\n\t\t\tname:           \"Down at the last process - Footer height is plenty\",\n\t\t\tprocessCnt:     3,\n\t\t\tcursor:         2,\n\t\t\trender:         0,\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t\tfooterHeight:   10,\n\t\t},\n\t\t{\n\t\t\tname:           \"Down at the last process - Footer height just enough\",\n\t\t\tprocessCnt:     3,\n\t\t\tcursor:         2,\n\t\t\trender:         0,\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t\tfooterHeight:   8,\n\t\t},\n\t\t{\n\t\t\tname:           \"Down at the last process - Footer height is small\",\n\t\t\tprocessCnt:     10,\n\t\t\tcursor:         9,\n\t\t\trender:         7,\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t\tfooterHeight:   8,\n\t\t},\n\t\t{\n\t\t\tname:           \"Down at the process causing render index to move\",\n\t\t\tprocessCnt:     10,\n\t\t\tcursor:         3,\n\t\t\trender:         0,\n\t\t\tlistDown:       true,\n\t\t\texpectedCursor: 4,\n\t\t\texpectedRender: 1,\n\t\t\tfooterHeight:   11, // Can hold 4 processes\n\t\t},\n\t\t{\n\t\t\tname:           \"Basic up movement 1\",\n\t\t\tprocessCnt:     10,\n\t\t\tcursor:         1,\n\t\t\trender:         0,\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 0,\n\t\t\texpectedRender: 0,\n\t\t\tfooterHeight:   10,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up at top wraps to last and adjusts render\",\n\t\t\tprocessCnt:     10,\n\t\t\tcursor:         0,\n\t\t\trender:         0,\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 9,\n\t\t\texpectedRender: 6, // 10 processes , 4 renderable\n\t\t\tfooterHeight:   11,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up causes render index decrement\",\n\t\t\tprocessCnt:     10,\n\t\t\tcursor:         3,\n\t\t\trender:         3,\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 2,\n\t\t\texpectedRender: 2, // Cursor moved above render start\n\t\t\tfooterHeight:   8, // Renders 3 processes\n\t\t},\n\t\t{\n\t\t\tname:           \"Up on short list wraps correctly\",\n\t\t\tprocessCnt:     3,\n\t\t\tcursor:         0,\n\t\t\trender:         0,\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 2,\n\t\t\texpectedRender: 0, // 3 processes, 3 renderable\n\t\t\tfooterHeight:   11,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up within render window maintains position\",\n\t\t\tprocessCnt:     8,\n\t\t\tcursor:         5,\n\t\t\trender:         3,\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 4,\n\t\t\texpectedRender: 3, // Remain in render window\n\t\t\tfooterHeight:   11,\n\t\t},\n\t\t{\n\t\t\tname:           \"Up with minimal footer height\",\n\t\t\tprocessCnt:     5,\n\t\t\tcursor:         0,\n\t\t\trender:         0,\n\t\t\tlistDown:       false,\n\t\t\texpectedCursor: 4,\n\t\t\texpectedRender: 3,\n\t\t\tfooterHeight:   5,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tpModel := genProcessBarModel(tt.processCnt, tt.cursor, tt.render, tt.footerHeight)\n\t\t\tassert.True(t, pModel.isValid())\n\t\t\tif tt.listDown {\n\t\t\t\tpModel.ListDown()\n\t\t\t} else {\n\t\t\t\tpModel.ListUp()\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expectedCursor, pModel.cursor)\n\t\t\tassert.Equal(t, tt.expectedRender, pModel.renderIndex)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model_test.go",
    "content": "package processbar\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// TODO: This is duplicated in tests of prompt package, internal package too.\n// Fix this code duplication\n\n// Initialize the globals we need for testing\nfunc initGlobals() {\n\t// Updating globals for test is not a good idea and can lead to all sorts of issue\n\t// When multiple tests depend on same global variable and want different values\n\t// Since this is config that would likely stay same, maybe this is okay.\n\t// Also, this is done in main model's test too.\n\t// We need to find a better way to do this\n\terr := common.PopulateGlobalConfigs()\n\tif err != nil {\n\t\tfmt.Printf(\"error while populating config, err : %v\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc TestMain(m *testing.M) {\n\tflag.Parse()\n\tif testing.Verbose() {\n\t\tutils.SetRootLoggerToStdout(true)\n\t} else {\n\t\tutils.SetRootLoggerToDiscarded()\n\t}\n\tinitGlobals()\n\tm.Run()\n}\n\nfunc TestModelProcessUtils(t *testing.T) {\n\tm := New()\n\tp1 := NewProcess(\"1\", \"test\", OpCopy, 10)\n\tp2 := NewProcess(\"2\", \"test2\", OpDelete, 11)\n\n\t// ------- Testing AddProcess\n\n\terr := m.AddProcess(p1)\n\trequire.NoError(t, err, \"Add should succeed without errors\")\n\n\terr = m.AddProcess(p2)\n\trequire.NoError(t, err, \"Add should succeed without errors for second process\")\n\n\tpRes, ok := m.GetByID(p1.ID)\n\trequire.True(t, ok, \"Should be able to get the process we just added\")\n\tassert.Equal(t, p1, pRes, \"Should get the correct process value\")\n\n\tp2Dup := NewProcess(\"2\", \"test2_dup\", OpCopy, 1)\n\terr = m.AddProcess(p2Dup)\n\tvar errExp *ProcessAlreadyExistsError\n\trequire.ErrorAs(t, err, &errExp, \"Should get ProcessAlreadyExistsError\")\n\tassert.Equal(t, errExp.id, p2Dup.ID, \"ID in the error should match with what we sent\")\n\n\t// ------ Testing AddOrUpdate process\n\tm.AddOrUpdateProcess(p2Dup)\n\tpRes, ok = m.GetByID(p2Dup.ID)\n\trequire.True(t, ok)\n\tassert.Equal(t, p2Dup, pRes, \"Should get the correct process value after update\")\n\n\tp3 := NewProcess(\"3\", \"test3\", OpExtract, 1)\n\n\t// ------ Testing UpdateExisting\n\n\terr = m.UpdateExistingProcess(p3)\n\tvar errExpUpdate *NoProcessFoundError\n\trequire.ErrorAs(t, err, &errExpUpdate, \"Should get NoProcessFoundError\")\n\tassert.Equal(t, p3.ID, errExpUpdate.id, \"ID in the error should match with what we sent\")\n\n\tassert.True(t, m.HasRunningProcesses())\n\n\t// Update all to done\n\tp1.State = Successful\n\tp2Dup.Done = p2Dup.Total\n\tp3.State = Failed\n\t_ = m.UpdateExistingProcess(p1)\n\t_ = m.UpdateExistingProcess(p2Dup)\n\t_ = m.UpdateExistingProcess(p3)\n\n\tassert.False(t, m.HasRunningProcesses())\n}\n\nfunc TestModelSetDimenstions(t *testing.T) {\n\tm := New()\n\n\tm.SetDimensions(5, 6)\n\tassert.Equal(t, 5, m.width, \"Correct value should be set\")\n\tassert.Equal(t, 6, m.height, \"Correct value should be set\")\n\n\tm.SetDimensions(minWidth+1, minHeight-1)\n\tassert.Equal(t, minHeight, m.height, \"Min value should be set\")\n\tassert.Equal(t, minWidth+1, m.width, \"Given value should be set\")\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model_update.go",
    "content": "package processbar\n\nimport (\n\t\"log/slog\"\n)\n\n// Only used in tests, to have processbar used in a standalone way without model\nfunc (m *Model) ListenForChannelUpdates() {\n\t// A goroutine running forever\n\tgo func() {\n\t\tfor {\n\t\t\tmsg, ok := <-m.msgChan\n\t\t\tif !ok {\n\t\t\t\tslog.Debug(\"Channel closed, stopping listener\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif _, ok := msg.(stopListeningMsg); ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_, err := msg.Apply(m)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Could not apply update to processbar\", \"error\", err)\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// An IO Operation, that will wait forever on msgChannel\nfunc (m *Model) GetListenCmd() Cmd {\n\treturn func() UpdateMsg {\n\t\treturn <-m.msgChan\n\t}\n}\n\n// Might add options to drain the channel in case msg is high priority.\nfunc (m *Model) trySendMsgToChannel(msg UpdateMsg) error {\n\tselect {\n\tcase m.msgChan <- msg:\n\t\treturn nil\n\tdefault:\n\t\t// Process queue full with messages. Cannot add new process\n\t\treturn &ProcessChannelFullError{}\n\t}\n}\n\n// Block till message is sent\nfunc (m *Model) sendMsgToChannelBlocking(msg UpdateMsg) {\n\tm.msgChan <- msg\n}\n\nfunc (m *Model) sendMsgToChannel(msg UpdateMsg, blocking bool) error {\n\tif blocking {\n\t\tm.sendMsgToChannelBlocking(msg)\n\t\treturn nil\n\t}\n\treturn m.trySendMsgToChannel(msg)\n}\n\nfunc (m *Model) SendAddProcessMsg(\n\tcurrentFile string, operation OperationType, total int, blockingSend bool,\n) (Process, error) {\n\tid := m.newUUIDForProcess()\n\tp := NewProcess(id, currentFile, operation, total)\n\tmsg := newProcessMsg{\n\t\tNewProcess: p,\n\t\tBaseMsg:    BaseMsg{reqID: m.newReqCnt()},\n\t}\n\terr := m.sendMsgToChannel(msg, blockingSend)\n\tif err != nil {\n\t\t// Return zero-value process to indicate failure\n\t\treturn Process{}, err\n\t}\n\treturn p, nil\n}\n\nfunc (m *Model) SendUpdateProcessMsg(p Process, blockingSend bool) error {\n\tmsg := updateProcessMsg{NewProcess: p, BaseMsg: BaseMsg{reqID: m.newReqCnt()}}\n\treturn m.sendMsgToChannel(msg, blockingSend)\n}\n\n// Non Blocking and can fail\nfunc (m *Model) TrySendingUpdateProcessMsg(p Process) {\n\tmsg := updateProcessMsg{NewProcess: p, BaseMsg: BaseMsg{reqID: m.newReqCnt()}}\n\terr := m.sendMsgToChannel(msg, false)\n\tif err != nil {\n\t\tslog.Error(\"Failed to send message to channel\", \"reqID\", msg.GetReqID(), \"error\", err)\n\t}\n}\n\nfunc (m *Model) SendStopListeningMsgBlocking() {\n\tm.sendMsgToChannelBlocking(stopListeningMsg{BaseMsg: BaseMsg{reqID: m.newReqCnt()}})\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model_update_test.go",
    "content": "package processbar\n\nimport \"testing\"\n\nfunc TestUpdateMsg(_ *testing.T) {\n\t// TODO\n\t// Test these\n\t// 1 - Sending messages without starting to listen\n\t//   - messages fail after limit (tests trySendMsgToChannel())\n\t//   - blocking messages stuck forever - timeout after 0.5sec (Just do 1)\n\t//     -  Tests sendMsgToChannelBlocking()\n\t// 2 - Use SendAddProcessMsg() and verify that new process is added soon\n\t// 3 - Use SendUpdateProcessNameMsg() and verify update\n\t// 4 - Verify that stopListeningMsg works. Use SendStopListeningMsgBlocking()\n\t//     - Test m.IsListeningForUpdates()\n\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model_utils.go",
    "content": "package processbar\n\nimport (\n\t\"sort\"\n\n\t\"github.com/lithammer/shortuuid\"\n)\n\nfunc (m *Model) cntProcesses() int {\n\treturn len(m.processes)\n}\n\nfunc (m *Model) isValid() bool {\n\treturn m.renderIndex <= m.cursor &&\n\t\tm.cursor <= m.renderIndex+cntRenderableProcess(m.height-borderSize)-1\n}\n\nfunc (m *Model) viewHeight() int {\n\treturn m.height - borderSize\n}\n\nfunc (m *Model) viewWidth() int {\n\treturn m.width - borderSize\n}\n\nfunc (m *Model) GetHeight() int {\n\treturn m.height\n}\n\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n\nfunc (m *Model) getSortedProcesses() []Process {\n\t// save process in the array and sort the process by finished or not,\n\t// completion percetage, or finish time\n\t// TODO : This is very inefficient and can be improved.\n\t// The whole design needs to be changed so that we dont need to recreate the slice\n\t// and sort on each render. Idea : Maintain two slices - completed, ongoing\n\t// Processes should be added / removed to the slice on correct time, and we dont\n\t// need to redo slice formation and sorting on each render.\n\t// TODO : One idea is that we can use google/btree to store processes\n\t// have process implement a Less() method, and we can do O(logn) inserts/deletes\n\t// To make sure its always stored in an order we want. And then iterate in O(n)\n\t// in render()\n\tprocesses := m.GetProcessesSlice()\n\t// sort by the process\n\tsort.Slice(processes, func(i, j int) bool {\n\t\tdoneI := (processes[i].State == Successful || processes[i].State == Failed)\n\t\tdoneJ := (processes[j].State == Successful || processes[j].State == Failed)\n\n\t\t// sort by done or not\n\t\tif doneI != doneJ {\n\t\t\treturn !doneI\n\t\t}\n\n\t\t// if both not done\n\t\tif !doneI {\n\t\t\tcompletionI := float64(processes[i].Done) / float64(processes[i].Total)\n\t\t\tcompletionJ := float64(processes[j].Done) / float64(processes[j].Total)\n\t\t\treturn completionI < completionJ // Those who finish first will be ranked later.\n\t\t}\n\n\t\t// if both done sort by the doneTime\n\t\treturn processes[j].DoneTime.Before(processes[i].DoneTime)\n\t})\n\n\treturn processes\n}\n\nfunc (m *Model) newReqCnt() int {\n\tm.reqCnt++\n\treturn m.reqCnt\n}\n\n// TODO: Maybe make sure that there isn't any existing process with this UUID\nfunc (m *Model) newUUIDForProcess() string {\n\treturn shortuuid.New()\n}\n\n// Copy of the current processes for read only purpose\nfunc (m *Model) GetProcessesSlice() []Process {\n\tvar processes []Process\n\tfor _, p := range m.processes {\n\t\tprocesses = append(processes, p)\n\t}\n\treturn processes\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/model_utils_test.go",
    "content": "package processbar\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestModelUtils(t *testing.T) {\n\tm := NewModelWithOptions(14, 10)\n\tassert.Equal(t, 8, m.viewHeight())\n\tassert.Equal(t, 12, m.viewWidth())\n\tassert.Equal(t, 0, m.cntProcesses())\n\tassert.Equal(t, 1, m.newReqCnt())\n\tassert.Equal(t, 2, m.newReqCnt())\n\tassert.True(t, m.isValid())\n\n\tp1 := NewProcess(\"1\", \"test\", OpCopy, 10)\n\tp2 := NewProcess(\"2\", \"test2\", OpCompress, 11)\n\n\t_ = m.AddProcess(p1)\n\t_ = m.AddProcess(p2)\n\n\tassert.Equal(t, 2, m.cntProcesses())\n\tassert.True(t, m.isValid())\n\n\tm.cursor = -1\n\tassert.False(t, m.isValid())\n\tm.cursor = 0\n\tm.renderIndex = 1\n\tassert.False(t, m.isValid())\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/operation.go",
    "content": "package processbar\n\nimport \"github.com/yorukot/superfile/src/config/icon\"\n\ntype OperationType int\n\nconst (\n\tOpCopy OperationType = iota\n\tOpCut\n\tOpDelete\n\tOpCompress\n\tOpExtract\n)\n\n// GetIcon returns the appropriate icon for the operation type\nfunc (op OperationType) GetIcon() string {\n\tswitch op {\n\tcase OpCopy:\n\t\treturn icon.Copy\n\tcase OpCut:\n\t\treturn icon.Cut\n\tcase OpDelete:\n\t\treturn icon.Delete\n\tcase OpCompress:\n\t\treturn icon.CompressFile\n\tcase OpExtract:\n\t\treturn icon.ExtractFile\n\tdefault:\n\t\treturn icon.InOperation\n\t}\n}\n\n// GetVerb returns the present tense verb for the operation\nfunc (op OperationType) GetVerb() string {\n\tswitch op {\n\tcase OpCopy:\n\t\treturn \"Copying\"\n\tcase OpCut:\n\t\treturn \"Moving\"\n\tcase OpDelete:\n\t\treturn \"Deleting\"\n\tcase OpCompress:\n\t\treturn \"Compressing\"\n\tcase OpExtract:\n\t\treturn \"Extracting\"\n\tdefault:\n\t\treturn \"Processing\"\n\t}\n}\n\n// GetPastVerb returns the past tense verb for the operation\nfunc (op OperationType) GetPastVerb() string {\n\tswitch op {\n\tcase OpCopy:\n\t\treturn \"Copied\"\n\tcase OpCut:\n\t\treturn \"Moved\"\n\tcase OpDelete:\n\t\treturn \"Deleted\"\n\tcase OpCompress:\n\t\treturn \"Compressed\"\n\tcase OpExtract:\n\t\treturn \"Extracted\"\n\tdefault:\n\t\treturn \"Processed\"\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/process.go",
    "content": "package processbar\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/progress\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// Model for an individual process\n// Note : Its size is ~ 800 bytes\ntype Process struct {\n\tID          string\n\tCurrentFile string\n\t// TODO : We always want ErrorMsg to be set when State is\n\t// moved to Cancelled or Failed. To ensure it, we need to only allow state\n\t// change via helper functions and ask for the  errorMsg\n\tErrorMsg  string\n\tOperation OperationType\n\tProgress  progress.Model\n\tState     ProcessState\n\tTotal     int\n\tDone      int\n\tDoneTime  time.Time\n}\n\nfunc NewProcess(id string, currentFile string, operation OperationType, total int) Process {\n\tprog := progress.New(common.GenerateGradientColor())\n\tprog.PercentageStyle = common.FooterStyle\n\treturn Process{\n\t\tID:          id,\n\t\tCurrentFile: currentFile,\n\t\tOperation:   operation,\n\t\tProgress:    prog,\n\t\tState:       InOperation,\n\t\tTotal:       total,\n\t\tDone:        0,\n\t}\n}\n\ntype ProcessState int\n\nconst (\n\tInOperation ProcessState = iota\n\tSuccessful\n\tCancelled\n\tFailed\n)\n\n// TODO : Should we store in a global map for efficiency ? At least need to prerender\n// Yes, this is a Render() call, which is expensive\nfunc (p ProcessState) Icon() string {\n\tswitch p {\n\tcase Failed:\n\t\treturn common.ProcessErrorStyle.Render(icon.Warn)\n\tcase Successful:\n\t\treturn common.ProcessSuccessfulStyle.Render(icon.Done)\n\tcase InOperation:\n\t\treturn common.ProcessInOperationStyle.Render(icon.InOperation)\n\tcase Cancelled:\n\t\tfallthrough\n\tdefault:\n\t\treturn common.ProcessCancelStyle.Render(icon.Error)\n\t}\n}\n\n// GetDisplayName returns the appropriate display name for the process\nfunc (p *Process) GetDisplayName() string {\n\treturn p.Operation.GetIcon() + icon.Space + p.displayNameWithoutIcon()\n}\n\nfunc (p *Process) displayNameWithoutIcon() string {\n\tif p.State == Cancelled {\n\t\treturn p.Operation.GetVerb() + \" cancelled : \" + p.ErrorMsg\n\t}\n\tif p.State == Failed {\n\t\treturn p.Operation.GetVerb() + \" failed : \" + p.ErrorMsg\n\t}\n\n\tif p.State == InOperation {\n\t\treturn p.Operation.GetVerb() + \" \" + p.CurrentFile\n\t}\n\n\tif p.Total > 1 {\n\t\treturn fmt.Sprintf(\"%s %d files\", p.Operation.GetPastVerb(), p.Total)\n\t}\n\treturn p.Operation.GetPastVerb() + \" \" + p.CurrentFile\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/process_test.go",
    "content": "package processbar\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n)\n\nfunc TestGetDisplayName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tprocess  Process\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Cancelled\",\n\t\t\tprocess: Process{\n\t\t\t\tCurrentFile: \"file.txt\",\n\t\t\t\tErrorMsg:    \"File already exists\",\n\t\t\t\tOperation:   OpCompress,\n\t\t\t\tTotal:       1,\n\t\t\t\tState:       Cancelled,\n\t\t\t},\n\t\t\texpected: icon.CompressFile + icon.Space + \"Compressing cancelled : File already exists\",\n\t\t},\n\t\t{\n\t\t\tname: \"Failed without error Msg\",\n\t\t\tprocess: Process{\n\t\t\t\tCurrentFile: \"file.txt\",\n\t\t\t\tOperation:   OpCompress,\n\t\t\t\tTotal:       1,\n\t\t\t\tState:       Failed,\n\t\t\t},\n\t\t\texpected: icon.CompressFile + icon.Space + \"Compressing failed : \",\n\t\t},\n\t\t{\n\t\t\tname: \"Error message during operations\",\n\t\t\tprocess: Process{\n\t\t\t\tCurrentFile: \"file.txt\",\n\t\t\t\tErrorMsg:    \"File already exists\",\n\t\t\t\tOperation:   OpCompress,\n\t\t\t\tTotal:       1,\n\t\t\t\tState:       InOperation,\n\t\t\t},\n\t\t\texpected: icon.CompressFile + icon.Space + \"Compressing file.txt\",\n\t\t},\n\t\t{\n\t\t\tname: \"Single file during operation\",\n\t\t\tprocess: Process{\n\t\t\t\tCurrentFile: \"file.txt\",\n\t\t\t\tOperation:   OpCopy,\n\t\t\t\tTotal:       1,\n\t\t\t\tState:       InOperation,\n\t\t\t},\n\t\t\texpected: icon.Copy + icon.Space + \"Copying file.txt\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple files during operation\",\n\t\t\tprocess: Process{\n\t\t\t\tCurrentFile: \"file2.txt\",\n\t\t\t\tOperation:   OpDelete,\n\t\t\t\tTotal:       10,\n\t\t\t\tState:       InOperation,\n\t\t\t},\n\t\t\texpected: icon.Delete + icon.Space + \"Deleting file2.txt\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple files after completion\",\n\t\t\tprocess: Process{\n\t\t\t\tCurrentFile: \"file.txt\",\n\t\t\t\tOperation:   OpCopy,\n\t\t\t\tTotal:       5,\n\t\t\t\tState:       Successful,\n\t\t\t},\n\t\t\texpected: icon.Copy + icon.Space + \"Copied 5 files\",\n\t\t},\n\t\t{\n\t\t\tname: \"Single file after completion\",\n\t\t\tprocess: Process{\n\t\t\t\tCurrentFile: \"file.txt\",\n\t\t\t\tOperation:   OpDelete,\n\t\t\t\tTotal:       1,\n\t\t\t\tState:       Successful,\n\t\t\t},\n\t\t\texpected: icon.Delete + icon.Space + \"Deleted file.txt\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, tt.process.GetDisplayName())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/processbar/process_update_msg.go",
    "content": "package processbar\n\ntype Cmd func() UpdateMsg\n\ntype UpdateMsg interface {\n\tApply(m *Model) (Cmd, error)\n\tGetReqID() int\n}\n\n// TODO: Can we remove this duplication with model_msg ?\ntype BaseMsg struct {\n\treqID int\n}\n\nfunc (msg BaseMsg) GetReqID() int {\n\treturn msg.reqID\n}\n\ntype newProcessMsg struct {\n\tBaseMsg\n\n\tNewProcess Process\n}\n\nfunc (msg newProcessMsg) Apply(m *Model) (Cmd, error) {\n\treturn m.GetListenCmd(), m.AddProcess(msg.NewProcess)\n}\n\ntype updateProcessMsg struct {\n\tBaseMsg\n\n\tNewProcess Process\n}\n\nfunc (msg updateProcessMsg) Apply(m *Model) (Cmd, error) {\n\treturn m.GetListenCmd(), m.UpdateExistingProcess(msg.NewProcess)\n}\n\n// Construction will be options UpdateName(), UpdateDone(), etc..\n\ntype stopListeningMsg struct {\n\tBaseMsg\n}\n\nfunc (msg stopListeningMsg) Apply(_ *Model) (Cmd, error) {\n\t//nolint:nilnil // This is a no-op apply.\n\treturn nil, nil\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/README.md",
    "content": "# prompt package\nThis is for the Prompt modal of superfile\n\nHandles user input updates, spf model updates, and returns a PromptAction to model. \n\n\n# Coverage\n\n```bash\ncd /path/to/ui/prompt\n# Basic coverage\ngo test -cover\n\n# HTML report\ngo test -coverprofile=coverage.out && go tool cover -html=coverage.out -o coverage.html\n```\nCurrent coverage is 91.3%.\n"
  },
  {
    "path": "src/internal/ui/prompt/consts.go",
    "content": "package prompt\n\nimport \"time\"\n\n// These could as well be property of prompt Model vs being global consts\n// But its fine\nconst (\n\tpromptHeadlineText = \"superfile Prompt\"\n\n\tOpenCommand  = \"open\"\n\tSplitCommand = \"split\"\n\tCdCommand    = \"cd\"\n\n\t// We could later make this configurable. But, not needed now.\n\tspfPromptChar   = \">\"\n\tshellPromptChar = \":\"\n\n\tsuccessMessagePrefix = \"Success\"\n\tfailureMessagePrefix = \"Error\"\n\n\tshellModeString = \"(Shell Mode)\"\n\tspfModeString   = \"(SPF Mode)\"\n\n\t// Error message string\n\ttokenizationError    = \"Failed during tokenization\"\n\tsplitCommandArgError = \"split command should not be given arguments\"\n\n\t// Timeout for command executed for shell substitution\n\tshellSubTimeout        = 1000 * time.Millisecond\n\tshellSubTimeoutInTests = 100 * time.Millisecond\n\n\tdefaultTestCwd = \"/\"\n\n\tPromptMinWidth  = 10\n\tPromptMinHeight = 3\n\n\tdefaultTestWidth     = 100\n\tdefaultTestMaxHeight = 100\n\n\t// UI dimension constants for prompt modal\n\t// promptInputPadding is total padding for prompt input fields\n\tpromptInputPadding = 6 // 2 + 1 + 2 + 1 (borders and spacing)\n\n\t// expectedArgCount is the expected number of prompt arguments\n\texpectedArgCount = 2\n)\n\nfunc modeString(shellMode bool) string {\n\tif shellMode {\n\t\treturn shellModeString\n\t}\n\treturn spfModeString\n}\n\nfunc shellPrompt(shellMode bool) string {\n\tif shellMode {\n\t\treturn shellPromptChar\n\t}\n\treturn spfPromptChar\n}\n\nfunc defaultCommandSlice() []promptCommand {\n\treturn []promptCommand{\n\t\t{\n\t\t\tcommand:     OpenCommand,\n\t\t\tusage:       OpenCommand + \" <PATH>\",\n\t\t\tdescription: \"Open a new panel at a specified path\",\n\t\t},\n\t\t{\n\t\t\tcommand:     SplitCommand,\n\t\t\tusage:       SplitCommand,\n\t\t\tdescription: \"Open a new panel at a current file panel's path\",\n\t\t},\n\t\t{\n\t\t\tcommand:     CdCommand,\n\t\t\tusage:       CdCommand + \" <PATH>\",\n\t\t\tdescription: \"Change directory of current panel\",\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/error.go",
    "content": "package prompt\n\nimport \"fmt\"\n\n// This is to generate error objects that can be nicely printed to UI\ntype invalidCmdError struct {\n\tuiMsg        string\n\twrappedError error\n}\n\nfunc (e invalidCmdError) Error() string {\n\tif e.wrappedError == nil {\n\t\treturn e.uiMsg\n\t}\n\treturn e.wrappedError.Error()\n}\n\nfunc (e invalidCmdError) Unwrap() error {\n\treturn e.wrappedError\n}\n\nfunc (e invalidCmdError) uiMessage() string {\n\treturn e.uiMsg\n}\n\ntype envVarNotFoundError struct {\n\tvarName string\n}\n\nfunc (e envVarNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"env var %s not found\", e.varName)\n}\n\ntype bracketMatchError struct {\n\topenChar  rune\n\tcloseChar rune\n}\n\nfunc (p bracketMatchError) Error() string {\n\treturn fmt.Sprintf(\"could not find matching %c for %c\", p.closeChar, p.openChar)\n}\n\nfunc roundBracketMatchError() bracketMatchError {\n\treturn bracketMatchError{openChar: '(', closeChar: ')'}\n}\n\nfunc curlyBracketMatchError() bracketMatchError {\n\treturn bracketMatchError{openChar: '{', closeChar: '}'}\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/model.go",
    "content": "package prompt\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc DefaultModel(maxHeight int, width int) Model {\n\treturn GenerateModel(common.Hotkeys.OpenSPFPrompt[0],\n\t\tcommon.Hotkeys.OpenCommandLine[0], common.Config.ShellCloseOnSuccess, maxHeight, width)\n}\n\nfunc GenerateModel(spfPromptHotkey string, shellPromptHotkey string, closeOnSuccess bool,\n\tmaxHeight int, width int) Model {\n\tm := Model{\n\t\theadline:          icon.Terminal + icon.Space + promptHeadlineText,\n\t\topen:              false,\n\t\tshellMode:         true,\n\t\ttextInput:         common.GeneratePromptTextInput(),\n\t\tcommands:          defaultCommandSlice(),\n\t\tspfPromptHotkey:   spfPromptHotkey,\n\t\tshellPromptHotkey: shellPromptHotkey,\n\t\tactionSuccess:     true,\n\t\tcloseOnSuccess:    closeOnSuccess,\n\t}\n\tm.SetMaxHeight(maxHeight)\n\tm.SetWidth(width)\n\treturn m\n}\n\nfunc (m *Model) HandleUpdate(msg tea.Msg, cwdLocation string) (common.ModelAction, tea.Cmd) {\n\tvar action common.ModelAction\n\taction = common.NoAction{}\n\tvar cmd tea.Cmd\n\tif !m.IsOpen() {\n\t\tslog.Error(\"HandleUpdate called on closed prompt\")\n\t\treturn action, cmd\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch {\n\t\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg.String()):\n\t\t\taction = m.handleConfirm(cwdLocation)\n\t\tcase slices.Contains(common.Hotkeys.CancelTyping, msg.String()):\n\t\t\tm.Close()\n\t\tdefault:\n\t\t\tcmd = m.handleNormalKeyInput(msg)\n\t\t}\n\tdefault:\n\t\t// Non keypress updates like Cursor Blink\n\t\tm.textInput, cmd = m.textInput.Update(msg)\n\t}\n\treturn action, cmd\n}\n\nfunc (m *Model) handleConfirm(cwdLocation string) common.ModelAction {\n\t// Pressing confirm on empty prompt will trigger close\n\tif m.textInput.Value() == \"\" {\n\t\tm.CloseOnSuccessIfNeeded()\n\t}\n\n\t// Create Action based on input\n\tvar err error\n\taction, err := getPromptAction(m.shellMode, m.textInput.Value(), cwdLocation)\n\tif err == nil {\n\t\tm.resultMsg = \"\"\n\t\tm.actionSuccess = true\n\t} else if cmdErr, ok := err.(invalidCmdError); ok { //nolint: errorlint // We don't expect a wrapped error here\n\t\tslog.Error(\"Error from getPromptAction\", \"error\", cmdErr, \"uiMsg\", cmdErr.uiMsg)\n\t\tm.resultMsg = cmdErr.uiMessage()\n\t\tm.actionSuccess = false\n\t} else {\n\t\tslog.Error(\"Unexpected error from getPromptAction\", \"error\", err)\n\t\tm.resultMsg = err.Error()\n\t\tm.actionSuccess = false\n\t}\n\tm.textInput.SetValue(\"\")\n\treturn action\n}\n\nfunc (m *Model) handleNormalKeyInput(msg tea.KeyMsg) tea.Cmd {\n\tvar cmd tea.Cmd\n\tswitch {\n\tcase m.textInput.Value() == \"\" && msg.String() == m.spfPromptHotkey:\n\t\tm.setShellMode(false)\n\tcase m.textInput.Value() == \"\" && msg.String() == m.shellPromptHotkey:\n\t\tm.setShellMode(true)\n\tdefault:\n\t\tm.textInput, cmd = m.textInput.Update(msg)\n\t}\n\tm.resultMsg = \"\"\n\tm.actionSuccess = true\n\treturn cmd\n}\n\n// After action is performed, model will update the Model with results\nfunc (m *Model) HandleShellCommandResults(retCode int, output string) {\n\tm.actionSuccess = retCode == 0\n\tm.resultMsg = fmt.Sprintf(\"Command exited with status %d\", retCode)\n\n\toutput = strings.TrimSpace(common.MakePrintableWithEscCheck(output, false))\n\tif output != \"\" {\n\t\tm.resultMsg += \", Output:\\n\" + output\n\t} else {\n\t\tm.resultMsg += \" (No output)\"\n\t}\n\tm.CloseOnSuccessIfNeeded()\n}\n\n// After action is performed, model will update the prompt.Model with results\n// In case of NoAction, this method should not be called.\nfunc (m *Model) HandleSPFActionResults(success bool, msg string) {\n\tm.actionSuccess = success\n\tm.resultMsg = msg\n\tm.CloseOnSuccessIfNeeded()\n}\n\nfunc (m *Model) Render() string {\n\tr := ui.PromptRenderer(m.maxHeight, m.width)\n\tr.SetBorderTitle(m.headline + \" \" + modeString(m.shellMode))\n\tr.AddLines(\" \" + m.textInput.View())\n\n\tif !m.shellMode {\n\t\t// To make sure its added one time only per render call\n\t\thintSectionAdded := false\n\t\tif m.textInput.Value() == \"\" {\n\t\t\tif !hintSectionAdded {\n\t\t\t\tr.AddSection()\n\t\t\t\thintSectionAdded = true\n\t\t\t}\n\t\t\tr.AddLines(\" '\" + m.shellPromptHotkey + \"' - Get into Shell mode\")\n\t\t}\n\t\tcommand := getFirstToken(m.textInput.Value())\n\t\tfor _, cmd := range m.commands {\n\t\t\tif strings.HasPrefix(cmd.command, command) {\n\t\t\t\tif !hintSectionAdded {\n\t\t\t\t\tr.AddSection()\n\t\t\t\t\thintSectionAdded = true\n\t\t\t\t}\n\t\t\t\tr.AddLines(\" '\" + cmd.usage + \"' - \" + cmd.description)\n\t\t\t}\n\t\t}\n\t} else if m.textInput.Value() == \"\" {\n\t\tr.AddSection()\n\t\tr.AddLines(\" '\" + m.spfPromptHotkey + \"' - Get into SPF mode\")\n\t}\n\n\tif m.resultMsg != \"\" {\n\t\tmsgPrefix := successMessagePrefix\n\t\tresultStyle := common.PromptSuccessStyle\n\t\tif !m.actionSuccess {\n\t\t\tresultStyle = common.PromptFailureStyle\n\t\t\tmsgPrefix = failureMessagePrefix\n\t\t}\n\t\tr.AddSection()\n\t\tr.AddLines(resultStyle.Render(\" \" + msgPrefix + \" : \" + m.resultMsg))\n\t}\n\treturn r.Render()\n}\n\nfunc (m *Model) Open(shellMode bool) {\n\tm.open = true\n\tm.setShellMode(shellMode)\n\t_ = m.textInput.Focus()\n}\n\nfunc (m *Model) setShellMode(shellMode bool) {\n\tm.shellMode = shellMode\n\tm.textInput.Prompt = shellPrompt(m.shellMode) + \" \"\n}\n\nfunc (m *Model) Close() {\n\tm.open = false\n\tm.setShellMode(true)\n\tm.textInput.SetValue(\"\")\n}\n\nfunc (m *Model) IsOpen() bool {\n\treturn m.open\n}\n\nfunc (m *Model) IsShellMode() bool {\n\treturn m.shellMode\n}\n\nfunc (m *Model) LastActionSucceeded() bool {\n\treturn m.actionSuccess\n}\n\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n\nfunc (m *Model) GetMaxHeight() int {\n\treturn m.maxHeight\n}\n\nfunc (m *Model) SetWidth(width int) {\n\tif width < PromptMinWidth {\n\t\tslog.Warn(\"Prompt initialized with too less width\", \"width\", width)\n\t\twidth = PromptMinWidth\n\t}\n\tm.width = width\n\t// Excluding borders(2), SpacePadding(1), Prompt(2), and one extra character that is appended\n\t// by textInput.View()\n\tm.textInput.Width = width - promptInputPadding\n}\n\nfunc (m *Model) SetMaxHeight(maxHeight int) {\n\tif maxHeight < PromptMinHeight {\n\t\tslog.Warn(\"Prompt initialized with too less maxHeight\", \"maxHeight\", maxHeight)\n\t\tmaxHeight = PromptMinHeight\n\t}\n\tm.maxHeight = maxHeight\n}\n\nfunc (m *Model) validate() bool {\n\t// Prompt was closed, but textInput was not cleared\n\tif !m.open && m.textInput.Value() != \"\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (m *Model) CloseOnSuccessIfNeeded() {\n\tif m.closeOnSuccess && m.actionSuccess {\n\t\tm.Close()\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/model_test.go",
    "content": "package prompt\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// Initialize the globals we need for testing\nfunc initGlobals() {\n\t// Updating globals for test is not a good idea and can lead to all sorts of issue\n\t// When multiple tests depend on same global variable and want different values\n\t// Since this is config that would likely stay same, maybe this is okay.\n\t// Also, this is done in main model's test too.\n\t// We need to find a better way to do this\n\terr := common.PopulateGlobalConfigs()\n\tif err != nil {\n\t\tfmt.Printf(\"error while populating config, err : %v\", err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc TestMain(m *testing.M) {\n\tfor env, val := range testEnvValues {\n\t\terr := os.Setenv(env, val)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Could not set env variables, error : %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\tflag.Parse()\n\tif testing.Verbose() {\n\t\tutils.SetRootLoggerToStdout(true)\n\t} else {\n\t\tutils.SetRootLoggerToDiscarded()\n\t}\n\n\tinitGlobals()\n\tm.Run()\n}\n\nfunc defaultTestModel() Model {\n\treturn GenerateModel(spfPromptChar, shellPromptChar, true, defaultTestMaxHeight, defaultTestWidth)\n}\n\nfunc TestModel_HandleUpdate(t *testing.T) {\n\t// We don't test getPromptAction here. It is a separate test\n\tt.Run(\"Handle update called on closed Model\", func(t *testing.T) {\n\t\tm := defaultTestModel()\n\t\taction, _ := m.HandleUpdate(utils.TeaRuneKeyMsg(\"x\"), defaultTestCwd)\n\t\tassert.Empty(t, m.textInput.Value())\n\t\tassert.True(t, m.validate())\n\t\tassert.False(t, m.IsOpen())\n\t\tassert.Equal(t, common.NoAction{}, action)\n\t})\n\n\tt.Run(\"Pressing confirm on empty input\", func(t *testing.T) {\n\t\tactualTest := func(closeOnSuccess bool, openAfterEnter bool) {\n\t\t\tm := GenerateModel(spfPromptChar, shellPromptChar, closeOnSuccess, defaultTestMaxHeight, defaultTestWidth)\n\t\t\tm.Open(true)\n\t\t\tassert.True(t, m.IsOpen())\n\n\t\t\taction, _ := m.HandleUpdate(tea.KeyMsg{Type: tea.KeyEnter}, defaultTestCwd)\n\t\t\tassert.Equal(t, openAfterEnter, m.IsOpen())\n\t\t\tassert.Equal(t, common.NoAction{}, action)\n\t\t\tassert.Empty(t, m.resultMsg)\n\t\t\tassert.True(t, m.LastActionSucceeded())\n\t\t\tassert.True(t, m.validate())\n\t\t}\n\n\t\tactualTest(true, false)\n\t\tactualTest(false, true)\n\t})\n\n\tt.Run(\"Validate Prompt Actions\", func(t *testing.T) {\n\t\tm := defaultTestModel()\n\t\tm.Open(false)\n\n\t\taction, _ := m.HandleUpdate(utils.TeaRuneKeyMsg(SplitCommand), defaultTestCwd)\n\t\tassert.Equal(t, common.NoAction{}, action)\n\n\t\taction, _ = m.HandleUpdate(tea.KeyMsg{Type: tea.KeyEnter}, defaultTestCwd)\n\t\tassert.Equal(t, common.SplitPanelAction{}, action)\n\n\t\t_, _ = m.HandleUpdate(utils.TeaRuneKeyMsg(\"bad_command\"), defaultTestCwd)\n\t\taction, _ = m.HandleUpdate(tea.KeyMsg{Type: tea.KeyEnter}, defaultTestCwd)\n\t\tassert.Equal(t, common.NoAction{}, action)\n\t\tassert.False(t, m.LastActionSucceeded())\n\t\tassert.NotEmpty(t, m.resultMsg)\n\n\t\tm.setShellMode(true)\n\t\tcommand := \"abc def /xyz\"\n\t\t_, _ = m.HandleUpdate(utils.TeaRuneKeyMsg(command), defaultTestCwd)\n\t\taction, _ = m.HandleUpdate(tea.KeyMsg{Type: tea.KeyEnter}, defaultTestCwd)\n\t\tassert.Equal(t, common.ShellCommandAction{Command: command}, action)\n\t})\n\n\tt.Run(\"Validate Cancel typing\", func(t *testing.T) {\n\t\tm := defaultTestModel()\n\n\t\tactualTest := func(closeKey tea.KeyMsg, shouldBeOpen bool) {\n\t\t\tm.Open(true)\n\t\t\t_, _ = m.HandleUpdate(utils.TeaRuneKeyMsg(\"xyz\"), defaultTestCwd)\n\t\t\taction, _ := m.HandleUpdate(closeKey, defaultTestCwd)\n\t\t\tassert.Equal(t, common.NoAction{}, action)\n\t\t\tassert.Equal(t, shouldBeOpen, m.IsOpen())\n\t\t}\n\n\t\tactualTest(tea.KeyMsg{Type: tea.KeyCtrlC}, false)\n\t\tactualTest(tea.KeyMsg{Type: tea.KeyEscape}, false)\n\t\tactualTest(tea.KeyMsg{Type: tea.KeyCtrlD}, true)\n\t})\n\n\tt.Run(\"Switching between shell and SPF mode\", func(t *testing.T) {\n\t\tactualTest := func(promptChar string, shellChar string) {\n\t\t\tm := GenerateModel(promptChar, shellChar, true, defaultTestMaxHeight, defaultTestWidth)\n\t\t\tm.Open(true)\n\t\t\tassert.True(t, m.IsShellMode())\n\n\t\t\t// Shell to prompt\n\t\t\taction, _ := m.HandleUpdate(utils.TeaRuneKeyMsg(promptChar), defaultTestCwd)\n\t\t\tassert.False(t, m.IsShellMode())\n\t\t\tassert.True(t, m.LastActionSucceeded())\n\t\t\tassert.Equal(t, common.NoAction{}, action)\n\n\t\t\t// Prompt to shell\n\t\t\taction, _ = m.HandleUpdate(utils.TeaRuneKeyMsg(shellChar), defaultTestCwd)\n\t\t\tassert.True(t, m.IsShellMode())\n\t\t\tassert.True(t, m.LastActionSucceeded())\n\t\t\tassert.Equal(t, common.NoAction{}, action)\n\n\t\t\t// Pressing shellChar when you are already on shell shouldn't to anything\n\t\t\taction, _ = m.HandleUpdate(utils.TeaRuneKeyMsg(shellChar), defaultTestCwd)\n\t\t\tassert.True(t, m.IsShellMode())\n\t\t\tassert.True(t, m.LastActionSucceeded())\n\t\t\tassert.Equal(t, common.NoAction{}, action)\n\t\t}\n\t\tactualTest(\">\", \":\")\n\t\tactualTest(\"$\", \"#\")\n\t})\n\n\tt.Run(\"Validate Cursor Blink update\", func(t *testing.T) {\n\t\tm := defaultTestModel()\n\t\tm.Open(true)\n\t\tassert.False(t, m.textInput.Cursor.Blink)\n\n\t\tblinkMsg := m.textInput.Cursor.BlinkCmd()()\n\t\taction, _ := m.HandleUpdate(blinkMsg, defaultTestCwd)\n\t\tassert.Equal(t, common.NoAction{}, action)\n\t\tassert.True(t, m.textInput.Cursor.Blink)\n\n\t\tblinkMsg = m.textInput.Cursor.BlinkCmd()()\n\t\taction, _ = m.HandleUpdate(blinkMsg, defaultTestCwd)\n\t\tassert.Equal(t, common.NoAction{}, action)\n\t\tassert.False(t, m.textInput.Cursor.Blink)\n\n\t\tblinkMsg = m.textInput.Cursor.BlinkCmd()()\n\t\taction, _ = m.HandleUpdate(blinkMsg, defaultTestCwd)\n\t\tassert.Equal(t, common.NoAction{}, action)\n\t\tassert.True(t, m.textInput.Cursor.Blink)\n\n\t\t// We could test BlinkCancelled and initialBlink as well, but that's too much for now\n\t})\n}\n\nfunc TestModel_HandleResults(t *testing.T) {\n\tt.Run(\"Verify Shell results update\", func(t *testing.T) {\n\t\tm := defaultTestModel()\n\t\tm.Open(true)\n\t\tm.HandleShellCommandResults(0, \"\")\n\n\t\t// Validate close happens when closeOnSuccess is true\n\t\tassert.True(t, m.LastActionSucceeded())\n\t\tassert.Equal(t, \"Command exited with status 0 (No output)\", m.resultMsg)\n\t\tassert.False(t, m.IsOpen())\n\n\t\tm.Open(true)\n\t\tm.HandleShellCommandResults(1, \"\")\n\t\tassert.False(t, m.LastActionSucceeded())\n\t\tassert.Equal(t, \"Command exited with status 1 (No output)\", m.resultMsg)\n\t\tassert.True(t, m.IsOpen())\n\n\t\tm.closeOnSuccess = false\n\t\tm.HandleShellCommandResults(0, \"\")\n\t\t// Validate that close does not happen when closeOnSuccess is true\n\t\tassert.True(t, m.LastActionSucceeded())\n\t\tassert.Equal(t, \"Command exited with status 0 (No output)\", m.resultMsg)\n\t\tassert.True(t, m.IsOpen())\n\t})\n\n\tt.Run(\"Verify Shell command output is displayed\", func(t *testing.T) {\n\t\tm := defaultTestModel()\n\t\tm.closeOnSuccess = false\n\t\tm.Open(true)\n\n\t\t// Test with single line output\n\t\tm.HandleShellCommandResults(0, \"hello world\")\n\t\tassert.Equal(t, \"Command exited with status 0, Output:\\nhello world\", m.resultMsg)\n\n\t\t// Test with multi-line output\n\t\tm.HandleShellCommandResults(0, \"line1\\nline2\\nline3\")\n\t\tassert.Equal(t, \"Command exited with status 0, Output:\\nline1\\nline2\\nline3\", m.resultMsg)\n\n\t\t// Test output is trimmed\n\t\tm.HandleShellCommandResults(0, \"  trimmed output  \\n\")\n\t\tassert.Equal(t, \"Command exited with status 0, Output:\\ntrimmed output\", m.resultMsg)\n\n\t\tm.HandleShellCommandResults(0, \"ESC SEQ\\x1b[2;6H\")\n\t\tassert.Equal(t, \"Command exited with status 0, Output:\\nESC SEQ[2;6H\", m.resultMsg)\n\n\t\t// Test with failed command and output\n\t\tm.HandleShellCommandResults(1, \"error message\")\n\t\tassert.False(t, m.LastActionSucceeded())\n\t\tassert.Equal(t, \"Command exited with status 1, Output:\\nerror message\", m.resultMsg)\n\t})\n\n\tt.Run(\"Verify SPF results update\", func(t *testing.T) {\n\t\tm := GenerateModel(spfPromptChar, shellPromptChar, false, defaultTestMaxHeight, defaultTestWidth)\n\t\tm.Open(true)\n\t\tmsg := \"Test message\"\n\t\tm.HandleSPFActionResults(true, msg)\n\n\t\tassert.True(t, m.LastActionSucceeded())\n\t\tassert.Equal(t, msg, m.resultMsg)\n\t\tassert.True(t, m.IsOpen())\n\n\t\tm.closeOnSuccess = true\n\t\t// Validate close happens when closeOnSuccess is true\n\t\tm.HandleSPFActionResults(true, \"\")\n\t\tassert.False(t, m.IsOpen())\n\t})\n}\n\nfunc TestModel_Render(t *testing.T) {\n\t// Test\n\t// 1 - Default view with shell mode and spf prompt mode\n\t// 2 - User input\n\t// 3 - User input, that is truncated due to being too large\n\t// 4 - User input with special characters, emojies, etc.\n\t// 5 - Prompt mode suggestion with these prefixes\n\t//   - \"cd\"\n\t//   - \"c\"\n\t//   - \"open <PATH>\"\n\t//   - \"open <PATH> <Extra arg>\"\n\t//   - \"non_existent_command\"\n\n\t// 6 - Model with result message (Without is tested above)\n\t// 7 - Color of result message green on success, red on failure\n\t// This one is hard, and we will likely not do it soon.\n\t// Needs global style variables\n\n\t// Challenges - needs border config strings for render test\n\tt.Run(\"Basic Render Checks\", func(t *testing.T) {\n\t\tm := GenerateModel(spfPromptChar, shellPromptChar, true, 10, 40)\n\t\tm.setShellMode(true)\n\t\tres := ansi.Strip(m.Render())\n\t\texp := \"\" +\n\t\t\t\"╭─┤ \" + icon.Terminal + \" superfile Prompt (Shell Mode) ├──╮\\n\" +\n\t\t\t// 23--------4------------56789012345678901234567890123456789\n\t\t\t\"│ :                                    │\\n\" +\n\t\t\t// 23456789012345678901234567890123456789\n\t\t\t\"├──────────────────────────────────────┤\\n\" +\n\t\t\t\"│ '>' - Get into SPF mode              │\\n\" +\n\t\t\t\"╰──────────────────────────────────────╯\"\n\t\tassert.Equal(t, exp, res)\n\t\tm.setShellMode(false)\n\t\tres = ansi.Strip(m.Render())\n\t\texp = \"\" +\n\t\t\t\"╭─┤ \" + icon.Terminal + \" superfile Prompt (SPF Mode) ├────╮\\n\" +\n\t\t\t// 23--------4------------56789012345678901234567890123456789\n\t\t\t\"│ >                                    │\\n\" +\n\t\t\t\"├──────────────────────────────────────┤\\n\" +\n\t\t\t\"│ ':' - Get into Shell mode            │\\n\" +\n\t\t\t\"│ 'open <PATH>' - Open a new panel at a│\\n\" +\n\t\t\t\"│ 'split' - Open a new panel at a curre│\\n\" +\n\t\t\t\"│ 'cd <PATH>' - Change directory of cur│\\n\" +\n\t\t\t\"╰──────────────────────────────────────╯\"\n\t\tassert.Equal(t, exp, res)\n\t})\n\n\tt.Run(\"Test User Input\", func(t *testing.T) {\n\t\texecute := func(input string, expected string) {\n\t\t\t// Changing this will need test adjustments\n\t\t\twidth := 10\n\t\t\tm := GenerateModel(spfPromptChar, shellPromptChar, true, 10, width)\n\t\t\tm.Open(true)\n\t\t\tm.textInput.SetValue(input)\n\t\t\tm.textInput.Cursor.Blink = false\n\t\t\tres := ansi.Strip(m.Render())\n\t\t\tinputLine := strings.Split(res, \"\\n\")[1]\n\t\t\trequire.Equal(t, width, ansi.StringWidth(inputLine))\n\t\t\t// | : xxxx |\n\t\t\t// 0123456789\n\t\t\tcontent := strings.TrimPrefix(inputLine, \"│ : \")\n\t\t\tcontent = strings.TrimSuffix(content, \" │\")\n\n\t\t\tassert.Equal(t, expected, content)\n\t\t}\n\t\texecute(\"abc\", \"abc \")\n\t\texecute(\"0123456789\", \"6789\")\n\t\texecute(\"✅1✅2\", \"1✅2\")\n\t\texecute(\"✅1✅2✅\", \"2✅ \")\n\t})\n\n\tt.Run(\"Result Message\", func(t *testing.T) {\n\t\tm := GenerateModel(spfPromptChar, shellPromptChar, true, 10, 50)\n\t\tm.setShellMode(true)\n\t\tm.HandleShellCommandResults(0, \"\")\n\t\tres := ansi.Strip(m.Render())\n\t\texp := \"\" +\n\t\t\t\"╭─┤ \" + icon.Terminal + \" superfile Prompt (Shell Mode) ├────────────╮\\n\" +\n\t\t\t// 23--------4------------567890123456789012345678901234567890123456789\n\t\t\t\"│ :                                              │\\n\" +\n\t\t\t// 234567890123456789012345678901234567890123456789\n\t\t\t\"├────────────────────────────────────────────────┤\\n\" +\n\t\t\t\"│ '>' - Get into SPF mode                        │\\n\" +\n\t\t\t\"├────────────────────────────────────────────────┤\\n\" +\n\t\t\t\"│ Success : Command exited with status 0 (No outp│\\n\" +\n\t\t\t\"╰────────────────────────────────────────────────╯\"\n\t\tassert.Equal(t, exp, res)\n\t\tm.HandleShellCommandResults(1, \"\")\n\t\tres = ansi.Strip(m.Render())\n\t\texp = \"\" +\n\t\t\t\"╭─┤ \" + icon.Terminal + \" superfile Prompt (Shell Mode) ├────────────╮\\n\" +\n\t\t\t// 23--------4------------567890123456789012345678901234567890123456789\n\t\t\t\"│ :                                              │\\n\" +\n\t\t\t// 234567890123456789012345678901234567890123456789\n\t\t\t\"├────────────────────────────────────────────────┤\\n\" +\n\t\t\t\"│ '>' - Get into SPF mode                        │\\n\" +\n\t\t\t\"├────────────────────────────────────────────────┤\\n\" +\n\t\t\t\"│ Error : Command exited with status 1 (No output│\\n\" +\n\t\t\t\"╰────────────────────────────────────────────────╯\"\n\t\tassert.Equal(t, exp, res)\n\t})\n\tshellModeSuggestion := \"':' - Get into Shell mode\"\n\tvar openCmdSuggestion string\n\tvar splitCmdSuggestion string\n\tvar cdCmdSuggestion string\n\tfor _, cmd := range defaultCommandSlice() {\n\t\tcurSuggestion := \"'\" + cmd.usage + \"' - \" + cmd.description\n\n\t\tswitch cmd.command {\n\t\tcase OpenCommand:\n\t\t\topenCmdSuggestion = curSuggestion\n\t\tcase SplitCommand:\n\t\t\tsplitCmdSuggestion = curSuggestion\n\t\tcase CdCommand:\n\t\t\tcdCmdSuggestion = curSuggestion\n\t\tdefault:\n\t\t\tassert.Fail(t, \"Unknow command\")\n\t\t}\n\t}\n\n\ttestdataSuggestions := []struct {\n\t\tname                string\n\t\ttextInput           string\n\t\texpectedSuggestions []string\n\t}{\n\t\t{\n\t\t\tname:      \"No Input\",\n\t\t\ttextInput: \"\",\n\t\t\texpectedSuggestions: []string{\n\t\t\t\tshellModeSuggestion,\n\t\t\t\topenCmdSuggestion,\n\t\t\t\tsplitCmdSuggestion,\n\t\t\t\tcdCmdSuggestion,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Command without args\",\n\t\t\ttextInput: \"cd\",\n\t\t\texpectedSuggestions: []string{\n\t\t\t\tcdCmdSuggestion,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Incomplete Command\",\n\t\t\ttextInput: \"c\",\n\t\t\texpectedSuggestions: []string{\n\t\t\t\tcdCmdSuggestion,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Command with args\",\n\t\t\ttextInput: \"open /abc\",\n\t\t\texpectedSuggestions: []string{\n\t\t\t\topenCmdSuggestion,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"Command with extra args\",\n\t\t\ttextInput: \"open /abc /abc\",\n\t\t\texpectedSuggestions: []string{\n\t\t\t\topenCmdSuggestion,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:                \"Invalid command\",\n\t\t\ttextInput:           \"non_existent_command\",\n\t\t\texpectedSuggestions: []string{},\n\t\t},\n\t}\n\n\tfor _, tt := range testdataSuggestions {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := DefaultModel(defaultTestMaxHeight, defaultTestWidth)\n\t\t\tm.Open(false)\n\t\t\tm.textInput.SetValue(tt.textInput)\n\t\t\tres := ansi.Strip(m.Render())\n\t\t\tresLines := strings.Split(res, \"\\n\")\n\t\t\tif len(tt.expectedSuggestions) == 0 {\n\t\t\t\trequire.Len(t, resLines, 3)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Len(t, resLines, 4+len(tt.expectedSuggestions))\n\t\t\tsuggestionLines := resLines[3 : len(resLines)-1]\n\t\t\trequire.Len(t, suggestionLines, len(tt.expectedSuggestions))\n\n\t\t\tfor i := range tt.expectedSuggestions {\n\t\t\t\texp := tt.expectedSuggestions[i]\n\t\t\t\tactualLine := suggestionLines[i]\n\t\t\t\tactualLine = strings.TrimPrefix(actualLine, \"│ \")\n\t\t\t\tactualLine = strings.TrimSuffix(actualLine, \"│\")\n\t\t\t\tactualLine = strings.TrimSpace(actualLine)\n\t\t\t\tassert.Equal(t, exp, actualLine)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/tokenize.go",
    "content": "package prompt\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\n// split into tokens\nfunc tokenizePromptCommand(command string, cwdLocation string) ([]string, error) {\n\tcommand, err := resolveShellSubstitution(shellSubTimeout, command, cwdLocation)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn tokenizeWithQuotes(command)\n}\n\n// Replace ${} and $() with values\nfunc resolveShellSubstitution(subCmdTimeout time.Duration, command string, cwdLocation string) (string, error) {\n\tresCommand := strings.Builder{}\n\tcmdRunes := []rune(command)\n\ti := 0\n\tfor i < len(cmdRunes) {\n\t\tif i+1 < len(cmdRunes) && cmdRunes[i] == '$' {\n\t\t\tif !isOpenBracket(cmdRunes[i+1]) {\n\t\t\t\tresCommand.WriteRune(cmdRunes[i])\n\t\t\t\ti++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\topenChar := cmdRunes[i+1]\n\t\t\tcloseChar := getClosingBracket(openChar)\n\t\t\tend := findEndingBracket(cmdRunes, i+1, openChar, closeChar)\n\t\t\tif end == -1 {\n\t\t\t\treturn \"\", errors.New(\"unexpected error in tokenization\")\n\t\t\t}\n\t\t\tif end == len(cmdRunes) {\n\t\t\t\treturn \"\", bracketMatchError{openChar: openChar, closeChar: closeChar}\n\t\t\t}\n\n\t\t\terr := updateResCommand(&resCommand, openChar, string(cmdRunes[i+2:end]),\n\t\t\t\tsubCmdTimeout, cwdLocation)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\ti = end + 1\n\t\t} else {\n\t\t\tresCommand.WriteRune(cmdRunes[i])\n\t\t\ti++\n\t\t}\n\t}\n\n\treturn resCommand.String(), nil\n}\n\nfunc updateResCommand(resCommand *strings.Builder, openChar rune, token string,\n\tsubCmdTimeout time.Duration, cwdLocation string) error {\n\tswitch openChar {\n\tcase '{':\n\t\tvalue, ok := os.LookupEnv(token)\n\t\tif !ok {\n\t\t\treturn envVarNotFoundError{varName: token}\n\t\t}\n\t\t// Might Handle values being too big, or having multiple lines\n\t\t// But this is based on user input, so it is probably okay for now\n\t\t// Same comment for command substitution\n\t\tresCommand.WriteString(value)\n\tcase '(':\n\t\tretCode, output, err := utils.ExecuteCommandInShell(subCmdTimeout, cwdLocation, token)\n\n\t\tif retCode == -1 {\n\t\t\treturn fmt.Errorf(\"could not execute shell substitution command : %s : %w\", token, err)\n\t\t}\n\t\t// We are allowing commands that exit with non zero status code\n\t\t// We still use its output\n\t\tif retCode != 0 {\n\t\t\tslog.Debug(\"substitution command exited with non zero status\", \"retCode\", retCode,\n\t\t\t\t\"command\", token)\n\t\t}\n\t\tresCommand.WriteString(output)\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected openChar %v in tokenization\", openChar)\n\t}\n\treturn nil\n}\n\nfunc findEndingBracket(r []rune, openIdx int, openParan rune, closeParan rune) int {\n\tif openIdx < 0 || openIdx >= len(r) || r[openIdx] != openParan {\n\t\treturn -1\n\t}\n\n\topenCount := 1\n\ti := openIdx + 1\n\tfor i < len(r) && openCount != 0 {\n\t\tswitch r[i] {\n\t\tcase openParan:\n\t\t\topenCount++\n\t\tcase closeParan:\n\t\t\topenCount--\n\t\t}\n\t\tif openCount != 0 {\n\t\t\ti++\n\t\t}\n\t}\n\treturn i\n}\n\nfunc isOpenBracket(r rune) bool {\n\tswitch r {\n\tcase '(', '{':\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc getClosingBracket(r rune) rune {\n\tswitch r {\n\tcase '(':\n\t\treturn ')'\n\tcase '{':\n\t\treturn '}'\n\tdefault:\n\t\treturn ' '\n\t}\n}\n\n// splits command into tokens while respecting quotes and escapes\nfunc tokenizeWithQuotes(command string) ([]string, error) {\n\tvar (\n\t\ttokens    []string\n\t\tbuffer    strings.Builder\n\t\tquoteOpen rune // 0:none, '\\'' or '\"'\n\t\tescaped   bool\n\t)\n\n\t// Initialize tokens as empty slice instead of nil\n\ttokens = []string{}\n\n\t// Helper function to flush the current buffer into tokens\n\tflush := func() {\n\t\ttokens = append(tokens, buffer.String())\n\t\tbuffer.Reset()\n\t}\n\n\tfor _, r := range command {\n\t\tswitch {\n\t\tcase escaped:\n\t\t\t// Only allow escaping of specific characters that have special meaning\n\t\t\tswitch r {\n\t\t\tcase '\"', '\\'', '\\\\', ' ':\n\t\t\t\t// These are valid escape sequences\n\t\t\t\tbuffer.WriteRune(r)\n\t\t\tdefault:\n\t\t\t\t// Invalid escape sequence - treat backslash as literal\n\t\t\t\tbuffer.WriteRune('\\\\')\n\t\t\t\tbuffer.WriteRune(r)\n\t\t\t}\n\t\t\tescaped = false\n\t\tcase r == '\\\\':\n\t\t\tescaped = true\n\t\tcase quoteOpen == 0 && (r == '\"' || r == '\\''):\n\t\t\tquoteOpen = r\n\t\tcase quoteOpen == r:\n\t\t\t// End of quoted section - always flush (even if empty)\n\t\t\tflush()\n\t\t\tquoteOpen = 0\n\t\tcase unicode.IsSpace(r) && quoteOpen == 0:\n\t\t\t// Only flush if we have content\n\t\t\tif buffer.Len() > 0 {\n\t\t\t\tflush()\n\t\t\t}\n\t\tdefault:\n\t\t\tbuffer.WriteRune(r)\n\t\t}\n\t}\n\n\tif escaped || quoteOpen != 0 {\n\t\treturn nil, errors.New(\"unmatched quotes or escape characters in command\")\n\t}\n\n\t// Flush any remaining content\n\tif buffer.Len() > 0 {\n\t\tflush()\n\t}\n\n\treturn tokens, nil\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/tokenize_test.go",
    "content": "package prompt\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tspfTestEnvVar1 = \"SPF_TEST_ENV_VAR1\"\n\tspfTestEnvVar2 = \"SPF_TEST_ENV_VAR2\"\n\tspfTestEnvVar3 = \"SPF_TEST_ENV_VAR3\"\n\tspfTestEnvVar4 = \"SPF_TEST_ENV_VAR4\"\n)\n\nvar testEnvValues = map[string]string{ //nolint:gochecknoglobals // This is more like a const.\n\tspfTestEnvVar1: \"1\",\n\tspfTestEnvVar2: \"hello\",\n\tspfTestEnvVar3: \"\",\n}\n\nfunc Test_tokenizePromptCommand(t *testing.T) {\n\t// Just test that we can split as expected\n\t// Don't try to test shell substitution in this. This is just\n\t// to test that tokenize function can handle the results of shell\n\t// substitution as expected\n\n\ttestdata := []struct {\n\t\tname            string\n\t\tcommand         string\n\t\texpectedRes     []string\n\t\tisErrorExpected bool\n\t}{\n\t\t{\n\t\t\tname:            \"Empty String\",\n\t\t\tcommand:         \"\",\n\t\t\texpectedRes:     []string{},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Parenthesis issue\",\n\t\t\tcommand:         \"abcd $(xyz\",\n\t\t\texpectedRes:     nil,\n\t\t\tisErrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"Parenthesis issue - But no dollar\",\n\t\t\tcommand:         \"abcd (xyz\",\n\t\t\texpectedRes:     []string{\"abcd\", \"(xyz\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Whitespace\",\n\t\t\tcommand:         \"    a b  c  \",\n\t\t\texpectedRes:     []string{\"a\", \"b\", \"c\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Single token\",\n\t\t\tcommand:         \"()\",\n\t\t\texpectedRes:     []string{\"()\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Special characters\",\n\t\t\tcommand:         \"() \\t\\n\\t a $5^&*\\v\\a\\n\\uF0AC\",\n\t\t\texpectedRes:     []string{\"()\", \"a\", \"$5^&*\", \"\\a\", \"\\uF0AC\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres, err := tokenizePromptCommand(tt.command, defaultTestCwd)\n\t\t\tassert.Equal(t, tt.expectedRes, res)\n\t\t\tassert.Equal(t, tt.isErrorExpected, err != nil)\n\t\t})\n\t}\n}\n\n// Note : resolving shell subsitution is flaky in windows.\n// It usually times out, and environment variables sometimes dont work.\nfunc Test_resolveShellSubstitution(t *testing.T) {\n\ttimeout := shellSubTimeoutInTests\n\tnewLineSuffix := \"\\n\"\n\tnoopCommand := \"true\"\n\tif runtime.GOOS == \"windows\" {\n\t\t// Substitution is slow in windows\n\t\ttimeout = 2 * time.Second\n\t\t// Windows uses \\r\\n as new line for echo\n\t\tnewLineSuffix = \"\\r\\n\"\n\t\tnoopCommand = \"cd .\"\n\t}\n\n\ttestdata := []struct {\n\t\tname            string\n\t\tcommand         string\n\t\texpectedResult  string\n\t\tisErrorExpected bool\n\t\terrorToMatch    error\n\t}{\n\t\t// Test with no substitution being performed\n\t\t{\n\t\t\tname:            \"Empty String\",\n\t\t\tcommand:         \"\",\n\t\t\texpectedResult:  \"\",\n\t\t\tisErrorExpected: false,\n\t\t\terrorToMatch:    nil,\n\t\t},\n\t\t{\n\t\t\tname:            \"String without substitution requirement\",\n\t\t\tcommand:         \"   a b c $%^ () {} \\a\\v\\t \\u0087\",\n\t\t\texpectedResult:  \"   a b c $%^ () {} \\a\\v\\t \\u0087\",\n\t\t\tisErrorExpected: false,\n\t\t\terrorToMatch:    nil,\n\t\t},\n\t\t{\n\t\t\tname:            \"Ill formed command 1\",\n\t\t\tcommand:         \"abc $(abc\",\n\t\t\texpectedResult:  \"\",\n\t\t\tisErrorExpected: true,\n\t\t\terrorToMatch:    roundBracketMatchError(),\n\t\t},\n\t\t{\n\t\t\tname:            \"Ill formed command 2\",\n\t\t\tcommand:         \"abc $(echo abc) syt ${ sdfc ( {)}\",\n\t\t\texpectedResult:  \"\",\n\t\t\tisErrorExpected: true,\n\t\t\terrorToMatch:    curlyBracketMatchError(),\n\t\t},\n\n\t\t// Test with substitution being performed\n\t\t{\n\t\t\tname:            \"Basic substitution\",\n\t\t\tcommand:         \"$(echo abc)\",\n\t\t\texpectedResult:  \"abc\" + newLineSuffix,\n\t\t\tisErrorExpected: false,\n\t\t\terrorToMatch:    nil,\n\t\t},\n\t\t// Might not work on windows ?\n\t\t{\n\t\t\tname:            \"Command with internal substitution\",\n\t\t\tcommand:         \"$(echo $(echo abc))\",\n\t\t\texpectedResult:  \"abc\" + newLineSuffix,\n\t\t\tisErrorExpected: false,\n\t\t\terrorToMatch:    nil,\n\t\t},\n\t\t{\n\t\t\tname:            \"Multiple substitution\",\n\t\t\tcommand:         fmt.Sprintf(\"$(echo $(echo hi)) ${%s}\", spfTestEnvVar2),\n\t\t\texpectedResult:  fmt.Sprintf(\"hi%s %s\", newLineSuffix, testEnvValues[spfTestEnvVar2]),\n\t\t\tisErrorExpected: false,\n\t\t\terrorToMatch:    nil,\n\t\t},\n\t\t{\n\t\t\tname:            \"Non Existing env var\",\n\t\t\tcommand:         fmt.Sprintf(\"${%s}\", spfTestEnvVar4),\n\t\t\texpectedResult:  \"\",\n\t\t\tisErrorExpected: true,\n\t\t\terrorToMatch:    envVarNotFoundError{varName: spfTestEnvVar4},\n\t\t},\n\t\t{\n\t\t\tname:            \"Shell substitution inside env var substitution\",\n\t\t\tcommand:         \"${$(pwd)}\",\n\t\t\texpectedResult:  \"\",\n\t\t\tisErrorExpected: true,\n\t\t\terrorToMatch:    envVarNotFoundError{varName: \"$(pwd)\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"Empty output\",\n\t\t\tcommand:         \"cd abc $(\" + noopCommand + \")\",\n\t\t\texpectedResult:  \"cd abc \",\n\t\t\tisErrorExpected: false,\n\t\t\terrorToMatch:    nil,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := resolveShellSubstitution(timeout, tt.command, defaultTestCwd)\n\n\t\t\tassert.Equal(t, tt.expectedResult, result)\n\t\t\tif err != nil {\n\t\t\t\tassert.True(t, tt.isErrorExpected)\n\t\t\t\tif tt.errorToMatch != nil {\n\t\t\t\t\tassert.ErrorIs(t, err, tt.errorToMatch)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Testing shell substitution timeout\", func(t *testing.T) {\n\t\tresult, err := resolveShellSubstitution(timeout, \"$(sleep 2)\", defaultTestCwd)\n\t\tassert.Empty(t, result)\n\t\trequire.Error(t, err)\n\t\trequire.ErrorIs(t, err, context.DeadlineExceeded)\n\t})\n}\n\nfunc Test_findEndingParenthesis(t *testing.T) {\n\ttestdata := []struct {\n\t\tname        string\n\t\tvalue       string\n\t\topenIdx     int\n\t\topenPar     rune\n\t\tclosePar    rune\n\t\texpectedRes int\n\t}{\n\t\t{\n\t\t\tname:        \"Empty String\",\n\t\t\tvalue:       \"\",\n\t\t\topenIdx:     0,\n\t\t\topenPar:     '(',\n\t\t\tclosePar:    ')',\n\t\t\texpectedRes: -1,\n\t\t},\n\t\t{\n\t\t\tname:        \"Invalid input\",\n\t\t\tvalue:       \"abc\",\n\t\t\topenIdx:     0,\n\t\t\topenPar:     '(',\n\t\t\tclosePar:    ')',\n\t\t\texpectedRes: -1,\n\t\t},\n\t\t{\n\t\t\tname:        \"Simple\",\n\t\t\tvalue:       \"abc(def)\",\n\t\t\topenIdx:     3,\n\t\t\topenPar:     '(',\n\t\t\tclosePar:    ')',\n\t\t\texpectedRes: 7,\n\t\t},\n\t\t{\n\t\t\tname:  \"Nesting Example 1\",\n\t\t\tvalue: \"abc(d(e{f})gh)\",\n\t\t\t//------01234567890123\n\t\t\topenIdx:     3,\n\t\t\topenPar:     '(',\n\t\t\tclosePar:    ')',\n\t\t\texpectedRes: 13,\n\t\t},\n\t\t{\n\t\t\tname:  \"Nesting Example 2\",\n\t\t\tvalue: \"abc(d(e{f})gh)\",\n\t\t\t//------01234567890123\n\t\t\topenIdx:     5,\n\t\t\topenPar:     '(',\n\t\t\tclosePar:    ')',\n\t\t\texpectedRes: 10,\n\t\t},\n\t\t{\n\t\t\tname:  \"Nesting Example 2\",\n\t\t\tvalue: \"abc(d(e{f(x}))gh)\",\n\t\t\t//------01234567890123456\n\t\t\topenIdx:     7,\n\t\t\topenPar:     '{',\n\t\t\tclosePar:    '}',\n\t\t\texpectedRes: 11,\n\t\t},\n\t\t{\n\t\t\tname:  \"No Closing Parenthesis 1\",\n\t\t\tvalue: \"abc(def}\",\n\t\t\t//------012345678901234\n\t\t\topenIdx:     3,\n\t\t\topenPar:     '(',\n\t\t\tclosePar:    ')',\n\t\t\texpectedRes: 8,\n\t\t},\n\t\t{\n\t\t\tname:  \"No Closing Parenthesis 2\",\n\t\t\tvalue: \"abc((d(e{f})gh)\",\n\t\t\t//------012345678901234\n\t\t\topenIdx:     3,\n\t\t\topenPar:     '(',\n\t\t\tclosePar:    ')',\n\t\t\texpectedRes: 15,\n\t\t},\n\t\t{\n\t\t\tname:  \"Asymmetric Parenthesis\",\n\t\t\tvalue: \"abc((d(e{f}>gh)\",\n\t\t\t//------012345678901234\n\t\t\topenIdx:     8,\n\t\t\topenPar:     '{',\n\t\t\tclosePar:    '>',\n\t\t\texpectedRes: 11,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := findEndingBracket([]rune(tt.value), tt.openIdx, tt.openPar, tt.closePar)\n\t\t\tassert.Equal(t, tt.expectedRes, res)\n\t\t})\n\t}\n}\n\nfunc Test_tokenizeWithQuotes(t *testing.T) {\n\ttestdata := []struct {\n\t\tname            string\n\t\tcommand         string\n\t\texpectedRes     []string\n\t\tisErrorExpected bool\n\t}{\n\t\t// Basic cases\n\t\t{\n\t\t\tname:            \"Empty String\",\n\t\t\tcommand:         \"\",\n\t\t\texpectedRes:     []string{},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Simple tokens\",\n\t\t\tcommand:         \"a b c\",\n\t\t\texpectedRes:     []string{\"a\", \"b\", \"c\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Whitespace handling\",\n\t\t\tcommand:         \"    a   b   c    \",\n\t\t\texpectedRes:     []string{\"a\", \"b\", \"c\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Tab and newline handling\",\n\t\t\tcommand:         \"a\\tb\\nc\",\n\t\t\texpectedRes:     []string{\"a\", \"b\", \"c\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Multiple spaces\",\n\t\t\tcommand:         \"command    arg\",\n\t\t\texpectedRes:     []string{\"command\", \"arg\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\n\t\t// Basic quoting\n\t\t{\n\t\t\tname:            \"Double quotes\",\n\t\t\tcommand:         `\"hello world\"`,\n\t\t\texpectedRes:     []string{\"hello world\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Single quotes\",\n\t\t\tcommand:         `'hello world'`,\n\t\t\texpectedRes:     []string{\"hello world\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Mixed quotes and unquoted\",\n\t\t\tcommand:         `command \"arg with spaces\" normal`,\n\t\t\texpectedRes:     []string{\"command\", \"arg with spaces\", \"normal\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Leading and trailing quotes\",\n\t\t\tcommand:         `\"command\" arg \"trailing\"`,\n\t\t\texpectedRes:     []string{\"command\", \"arg\", \"trailing\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\n\t\t// Empty quotes\n\t\t{\n\t\t\tname:            \"Empty double quotes\",\n\t\t\tcommand:         `command \"\"`,\n\t\t\texpectedRes:     []string{\"command\", \"\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Empty single quotes\",\n\t\t\tcommand:         `command ''`,\n\t\t\texpectedRes:     []string{\"command\", \"\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Only empty quotes\",\n\t\t\tcommand:         `\"\"`,\n\t\t\texpectedRes:     []string{\"\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\n\t\t// Nested different quotes\n\t\t{\n\t\t\tname:            \"Single quotes inside double quotes\",\n\t\t\tcommand:         `\"it's working\"`,\n\t\t\texpectedRes:     []string{\"it's working\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Double quotes inside single quotes\",\n\t\t\tcommand:         `'he said \"hello\"'`,\n\t\t\texpectedRes:     []string{`he said \"hello\"`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\n\t\t// Escaping\n\t\t{\n\t\t\tname:            \"Escaped double quote\",\n\t\t\tcommand:         `\"escaped \\\" quote\"`,\n\t\t\texpectedRes:     []string{`escaped \" quote`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Escaped single quote\",\n\t\t\tcommand:         `'can\\'t'`,\n\t\t\texpectedRes:     []string{`can't`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Escaped backslash\",\n\t\t\tcommand:         `\"path\\\\to\\\\file\"`,\n\t\t\texpectedRes:     []string{`path\\to\\file`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Multiple escaped backslashes\",\n\t\t\tcommand:         `\"\\\\\\\\\"`,\n\t\t\texpectedRes:     []string{`\\\\`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Escaped characters outside quotes\",\n\t\t\tcommand:         `a\\ b c`,\n\t\t\texpectedRes:     []string{`a b`, `c`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\n\t\t// Special characters\n\t\t{\n\t\t\tname:            \"Special characters in quotes\",\n\t\t\tcommand:         `\"$HOME\" '${USER}' \"$(pwd)\"`,\n\t\t\texpectedRes:     []string{\"$HOME\", \"${USER}\", \"$(pwd)\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Unicode in quotes\",\n\t\t\tcommand:         `\"こんにちは\" '世界'`,\n\t\t\texpectedRes:     []string{\"こんにちは\", \"世界\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\n\t\t// Error cases\n\t\t{\n\t\t\tname:            \"Unmatched double quote\",\n\t\t\tcommand:         `abcd \"sdf`,\n\t\t\texpectedRes:     nil,\n\t\t\tisErrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"Unmatched single quote\",\n\t\t\tcommand:         `\"abcd'`,\n\t\t\texpectedRes:     nil,\n\t\t\tisErrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"Unmatched quotes mixed\",\n\t\t\tcommand:         `abc \"def' ghi`,\n\t\t\texpectedRes:     nil,\n\t\t\tisErrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"Trailing escape\",\n\t\t\tcommand:         `abc\\`,\n\t\t\texpectedRes:     nil,\n\t\t\tisErrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"Escape at end of quoted string\",\n\t\t\tcommand:         `\"abc\\`,\n\t\t\texpectedRes:     nil,\n\t\t\tisErrorExpected: true,\n\t\t},\n\n\t\t// Complex cases\n\t\t{\n\t\t\tname:            \"Multiple quoted sections\",\n\t\t\tcommand:         `\"first part\" \"second part\" third`,\n\t\t\texpectedRes:     []string{\"first part\", \"second part\", \"third\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Quotes with no spaces\",\n\t\t\tcommand:         `\"hello\"\"world\"`,\n\t\t\texpectedRes:     []string{\"hello\", \"world\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Mixed quotes no spaces\",\n\t\t\tcommand:         `\"hello\"'world'`,\n\t\t\texpectedRes:     []string{\"hello\", \"world\"},\n\t\t\tisErrorExpected: false,\n\t\t},\n\n\t\t// Invalid escape sequences (should preserve backslash)\n\t\t{\n\t\t\tname:            \"Invalid escape sequence \\\\n\",\n\t\t\tcommand:         `\"hello\\nworld\"`,\n\t\t\texpectedRes:     []string{`hello\\nworld`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Invalid escape sequence \\\\t\",\n\t\t\tcommand:         `\"hello\\tworld\"`,\n\t\t\texpectedRes:     []string{`hello\\tworld`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Invalid escape sequence \\\\x\",\n\t\t\tcommand:         `\"hello\\xworld\"`,\n\t\t\texpectedRes:     []string{`hello\\xworld`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname:            \"Invalid escape sequence \\\\$\",\n\t\t\tcommand:         `\"hello\\$world\"`,\n\t\t\texpectedRes:     []string{`hello\\$world`},\n\t\t\tisErrorExpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres, err := tokenizeWithQuotes(tt.command)\n\t\t\tassert.Equal(t, tt.expectedRes, res)\n\t\t\tassert.Equal(t, tt.isErrorExpected, err != nil)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/type.go",
    "content": "package prompt\n\nimport \"github.com/charmbracelet/bubbles/textinput\"\n\n// No need to name it as PromptModel. It will me imported as prompt.Model\ntype Model struct {\n\n\t// Configuration\n\theadline          string\n\tcommands          []promptCommand\n\tspfPromptHotkey   string\n\tshellPromptHotkey string\n\tcloseOnSuccess    bool\n\n\t// State\n\topen bool\n\t// whether its shellMode or spfMode\n\t// Always use setShellMode to adjust\n\tshellMode bool\n\ttextInput textinput.Model\n\tresultMsg string\n\n\t// Whether the user intended action was successful\n\tactionSuccess bool\n\n\t// Dimensions - Exported, since model will be dynamically adjusting them\n\twidth int\n\t// Height is dynamically adjusted based on content\n\tmaxHeight int\n}\n\n// This is only used to render suggestions\n// Should not be exported\ntype promptCommand struct {\n\tcommand     string\n\tusage       string\n\tdescription string\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/utils.go",
    "content": "package prompt\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc getPromptAction(shellMode bool, value string, cwdLocation string) (common.ModelAction, error) {\n\tnoAction := common.NoAction{}\n\tif value == \"\" {\n\t\treturn noAction, nil\n\t}\n\tif shellMode {\n\t\treturn common.ShellCommandAction{\n\t\t\tCommand: value,\n\t\t}, nil\n\t}\n\n\tpromptArgs, err := tokenizePromptCommand(value, cwdLocation)\n\tif err != nil {\n\t\treturn noAction, invalidCmdError{\n\t\t\tuiMsg:        tokenizationError + \" : \" + err.Error(),\n\t\t\twrappedError: fmt.Errorf(\"error during tokenization : %w\", err),\n\t\t}\n\t}\n\n\tswitch promptArgs[0] {\n\tcase \"split\":\n\t\tif len(promptArgs) != 1 {\n\t\t\treturn noAction, invalidCmdError{\n\t\t\t\tuiMsg: splitCommandArgError,\n\t\t\t}\n\t\t}\n\t\treturn common.SplitPanelAction{}, nil\n\tcase \"cd\":\n\t\tif len(promptArgs) != expectedArgCount {\n\t\t\treturn noAction, invalidCmdError{\n\t\t\t\tuiMsg: fmt.Sprintf(\"cd command needs exactly one argument, received %d\",\n\t\t\t\t\tlen(promptArgs)-1),\n\t\t\t}\n\t\t}\n\t\treturn common.CDCurrentPanelAction{\n\t\t\tLocation: promptArgs[1],\n\t\t}, nil\n\tcase \"open\":\n\t\tif len(promptArgs) != expectedArgCount {\n\t\t\treturn noAction, invalidCmdError{\n\t\t\t\tuiMsg: fmt.Sprintf(\"open command needs exactly one argument, received %d\",\n\t\t\t\t\tlen(promptArgs)-1),\n\t\t\t}\n\t\t}\n\t\treturn common.OpenPanelAction{\n\t\t\tLocation: promptArgs[1],\n\t\t}, nil\n\n\tdefault:\n\t\treturn noAction, invalidCmdError{\n\t\t\tuiMsg: \"Invalid spf command : \" + promptArgs[0],\n\t\t}\n\t}\n}\n\n// Only allocates memory proportional to first token's size\n// Only works for space right now. Does not splits command based on\n// \\n or \\t , etc\nfunc getFirstToken(command string) string {\n\tcommand = strings.TrimSpace(command)\n\tspaceIndex := strings.IndexByte(command, ' ')\n\tif spaceIndex == -1 {\n\t\treturn command\n\t}\n\treturn command[:spaceIndex]\n}\n"
  },
  {
    "path": "src/internal/ui/prompt/utils_test.go",
    "content": "package prompt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc TestModel_getPromptAction(t *testing.T) {\n\t// Notes of Things we tested\n\t// About Tokenization failure. Don't test all failures,\n\t// it will be in tokenize_test.go\n\n\ttestdata := []struct {\n\t\tname           string\n\t\ttext           string\n\t\tshellMode      bool\n\t\texpectecAction common.ModelAction\n\t\texpectedErr    bool\n\t\texpectedErrMsg string\n\t}{\n\t\t{\n\t\t\tname:           \"No Action\",\n\t\t\ttext:           \"\",\n\t\t\tshellMode:      true,\n\t\t\texpectecAction: common.NoAction{},\n\t\t\texpectedErr:    false,\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"Shell command\",\n\t\t\ttext:      \"abc xyz /def\",\n\t\t\tshellMode: true,\n\t\t\texpectecAction: common.ShellCommandAction{\n\t\t\t\tCommand: \"abc xyz /def\",\n\t\t\t},\n\t\t\texpectedErr:    false,\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Tokenization failure\",\n\t\t\ttext:           \"cd ${sdfdsf\", // Missing \"}\"\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.NoAction{},\n\t\t\texpectedErr:    true,\n\t\t\texpectedErrMsg: tokenizationError + \" : \" + curlyBracketMatchError().Error(),\n\t\t},\n\t\t{\n\t\t\tname:           \"Split with extra arguments\",\n\t\t\ttext:           SplitCommand + \" xyz\",\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.NoAction{},\n\t\t\texpectedErr:    true,\n\t\t\texpectedErrMsg: splitCommandArgError,\n\t\t},\n\t\t{\n\t\t\tname:           \"cd with 0 arguments\",\n\t\t\ttext:           CdCommand,\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.NoAction{},\n\t\t\texpectedErr:    true,\n\t\t\texpectedErrMsg: \"cd command needs exactly one argument, received 0\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Invalid command\",\n\t\t\ttext:           \"abcd\",\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.NoAction{},\n\t\t\texpectedErr:    true,\n\t\t\texpectedErrMsg: \"Invalid spf command : abcd\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Correct split command\",\n\t\t\ttext:           SplitCommand,\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.SplitPanelAction{},\n\t\t\texpectedErr:    false,\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Correct cd command\",\n\t\t\ttext:           CdCommand + \" /abc\",\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.CDCurrentPanelAction{Location: \"/abc\"},\n\t\t\texpectedErr:    false,\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Correct open command\",\n\t\t\ttext:           OpenCommand + \" /abc\",\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.OpenPanelAction{Location: \"/abc\"},\n\t\t\texpectedErr:    false,\n\t\t\texpectedErrMsg: \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"open with three arguments\",\n\t\t\ttext:           OpenCommand + \" /abc /xyz\",\n\t\t\tshellMode:      false,\n\t\t\texpectecAction: common.NoAction{},\n\t\t\texpectedErr:    true,\n\t\t\texpectedErrMsg: \"open command needs exactly one argument, received 2\",\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\taction, err := getPromptAction(tt.shellMode, tt.text, \"/\")\n\t\t\tif err != nil {\n\t\t\t\tassert.True(t, tt.expectedErr)\n\t\t\t\t//nolint: errorlint // We don't expect a wrapped error here, so using type assertion\n\t\t\t\tcmdErr, ok := err.(invalidCmdError)\n\t\t\t\tassert.True(t, ok)\n\t\t\t\tif tt.expectedErrMsg != \"\" {\n\t\t\t\t\tassert.Equal(t, tt.expectedErrMsg, cmdErr.uiMessage())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expectecAction, action)\n\t\t})\n\t}\n}\n\nfunc Test_getFirstToken(t *testing.T) {\n\tt.Run(\"Basic test\", func(t *testing.T) {\n\t\tassert.Equal(t, \"abc\", getFirstToken(\"abc\"))\n\t\tassert.Equal(t, \"abc\", getFirstToken(\"abc a b c\"))\n\t\tassert.Equal(t, \"abc\", getFirstToken(\"abc \"))\n\t\tassert.Equal(t, \"abc\", getFirstToken(\"  abc \"))\n\t\tassert.Equal(t, \"abc\\n\\ta\", getFirstToken(\"abc\\n\\ta\"))\n\t})\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/README.md",
    "content": "# renderer package\nResponsible for rendering\n\n# Dependencies\nThis 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. \nIt 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.\n\n# To-dos\n- [ ] Use rendering package for other models like sort Menu, Help menu, etc.\n\n# Notes\n- 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\n"
  },
  {
    "path": "src/internal/ui/rendering/border.go",
    "content": "package rendering\n\nimport (\n\t\"strings\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\ntype BorderConfig struct {\n\t// ANSI encoded strings are not allowed in border title and info items, for now.\n\t// The style is overridden with border's style.\n\ttitle string\n\n\t// Optional info items at the bottom of the border\n\tinfoItems []string\n\n\t// Section dividers - A slice of values within [0,height-2]\n\t// Signifying usage of MiddleLeft and MiddleRight borders in Left and Right borders for\n\t// Section divider line.\n\tdividerIdx []int\n\n\t// Including corners. Both should be >= 2\n\twidth  int\n\theight int\n\n\ttitleLeftMargin int\n}\n\nfunc NewBorderConfig(height int, width int) BorderConfig {\n\treturn BorderConfig{\n\t\theight:          height,\n\t\twidth:           width,\n\t\ttitleLeftMargin: 1,\n\t}\n}\n\nfunc (b *BorderConfig) SetTitle(title string) {\n\tb.title = ansi.Strip(title)\n}\n\nfunc (b *BorderConfig) SetInfoItems(infoItems ...string) {\n\tfor i := range infoItems {\n\t\tinfoItems[i] = ansi.Strip(infoItems[i])\n\t}\n\tb.infoItems = infoItems\n}\n\nfunc (b *BorderConfig) AreInfoItemsTruncated() bool {\n\tcnt := len(b.infoItems)\n\tif cnt == 0 {\n\t\treturn false\n\t}\n\n\tactualWidth := b.width - borderCornerWidth\n\t// border.MiddleLeft <content> border.MiddleRight border.Bottom\n\tavailWidth := actualWidth/cnt - borderDividerWidth\n\tfor i := range b.infoItems {\n\t\tif ansi.StringWidth(b.infoItems[i]) > availWidth {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (b *BorderConfig) AddDivider(idx int) {\n\tb.dividerIdx = append(b.dividerIdx, idx)\n}\n\n// border.Top with something that takes up more than 1 runewidth will not work, so\n// we only allow 1 runewidth for now, in the config. multiple things like\n// border corner characters must be single rune, or else it would break rendering.\n// This is all filled in one function to prevent passing around too many values\n// in helper functions\nfunc (b *BorderConfig) GetBorder(borderStrings lipgloss.Border) lipgloss.Border {\n\tres := borderStrings\n\n\t// excluding corners. Maybe we can move this to a utility function\n\tactualWidth := b.width - borderCornerWidth\n\tactualHeight := b.height - borderCornerWidth\n\n\t// Min 5 width is needed for title so that at least one character can be\n\t// rendered\n\tif b.title != \"\" && actualWidth >= minTitleWidth {\n\t\t// We need to plain truncate the title if needed.\n\t\t// topWidth - 1( for BorderMiddleLeft) - 1 (for BorderMiddleRight) - 2 (padding)\n\t\ttitleAvailWidth := actualWidth - borderCornerWidth - borderPaddingWidth\n\n\t\t// Basic Right truncation\n\t\ttruncatedTitle := ansi.Truncate(b.title, titleAvailWidth, \"\")\n\t\tremainingWidth := actualWidth - borderCornerWidth - borderPaddingWidth - ansi.StringWidth(truncatedTitle)\n\n\t\tmargin := \"\"\n\t\tif remainingWidth > b.titleLeftMargin {\n\t\t\tmargin = strings.Repeat(borderStrings.Top, b.titleLeftMargin)\n\t\t\tremainingWidth -= b.titleLeftMargin\n\t\t}\n\n\t\t// Title alignment is by default Left for now\n\t\tres.Top = margin + borderStrings.MiddleRight + \" \" + truncatedTitle + \" \" + borderStrings.MiddleLeft +\n\t\t\tstrings.Repeat(borderStrings.Top, remainingWidth)\n\t}\n\n\tcnt := len(b.infoItems)\n\t// Minimum 4 character for each info item so that at least first character is rendered\n\tif cnt > 0 && actualWidth >= cnt*minInfoItemWidth {\n\t\t// Max available width for each item's actual content\n\t\t// border.MiddleLeft <content> border.MiddleRight border.Bottom\n\t\tavailWidth := actualWidth/cnt - borderDividerWidth\n\t\tvar infoText strings.Builder\n\t\tfor _, item := range b.infoItems {\n\t\t\titem = ansi.Truncate(item, availWidth, \"\")\n\t\t\tinfoText.WriteString(\n\t\t\t\tborderStrings.MiddleRight + item + borderStrings.MiddleLeft + borderStrings.Bottom,\n\t\t\t)\n\t\t}\n\n\t\t// Fill the rest with border char.\n\t\tremainingWidth := actualWidth - ansi.StringWidth(infoText.String())\n\n\t\tres.Bottom = strings.Repeat(borderStrings.Bottom, remainingWidth) + infoText.String()\n\t}\n\n\tif len(b.dividerIdx) > 0 {\n\t\t// Update res.Left and res.Right\n\t\tleftBorder := strings.Builder{}\n\t\trightBorder := strings.Builder{}\n\t\tdi := 0\n\t\tfor i := range actualHeight {\n\t\t\tif di < len(b.dividerIdx) && b.dividerIdx[di] == i {\n\t\t\t\tdi++\n\t\t\t\tleftBorder.WriteString(borderStrings.MiddleLeft)\n\t\t\t\trightBorder.WriteString(borderStrings.MiddleRight)\n\t\t\t} else {\n\t\t\t\tleftBorder.WriteString(borderStrings.Left)\n\t\t\t\trightBorder.WriteString(borderStrings.Right)\n\t\t\t}\n\t\t}\n\n\t\tres.Left = leftBorder.String()\n\t\tres.Right = rightBorder.String()\n\t}\n\n\treturn res\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/constants.go",
    "content": "package rendering\n\n// Border rendering constants\nconst (\n\t// borderCornerWidth is the width occupied by border corners\n\tborderCornerWidth = 2\n\n\t// borderPaddingWidth is padding around border title/content\n\tborderPaddingWidth = 2\n\n\t// borderDividerWidth is width for middle dividers (MiddleLeft + MiddleRight + Bottom)\n\tborderDividerWidth = 3\n\n\t// minTitleWidth is minimum width needed to render at least 1 char of title\n\tminTitleWidth = 5\n\n\t// minInfoItemWidth is minimum width for each info item to render at least 1 char\n\tminInfoItemWidth = 4\n\n\trendererNameMax = 1000\n\n\tMinWidthForBorder  = 2\n\tMinHeightForBorder = 2\n)\n"
  },
  {
    "path": "src/internal/ui/rendering/content_renderer.go",
    "content": "package rendering\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\ntype ContentRenderer struct {\n\tlines []string\n\n\t// Allow at max this many lines. If there are lesser lines\n\tmaxLines int\n\t// Every line should have at most this many characters\n\tmaxLineWidth    int\n\tsanitizeContent bool\n\n\t// We can add alignStyle if needed\n\ttruncateStyle TruncateStyle\n\n\tname string\n}\n\nfunc NewContentRenderer(maxLines int, maxLineWidth int, truncateStyle TruncateStyle, name string) ContentRenderer {\n\treturn ContentRenderer{\n\t\tlines:           make([]string, 0),\n\t\tmaxLines:        maxLines,\n\t\tmaxLineWidth:    maxLineWidth,\n\t\ttruncateStyle:   truncateStyle,\n\t\tsanitizeContent: true,\n\t\tname:            name,\n\t}\n}\n\nfunc (r *ContentRenderer) CntLines() int {\n\treturn len(r.lines)\n}\n\nfunc (r *ContentRenderer) AddLines(lines ...string) {\n\tfor _, line := range lines {\n\t\tr.AddLineWithCustomTruncate(line, r.truncateStyle)\n\t}\n}\n\nfunc (r *ContentRenderer) ClearLines() {\n\tr.lines = r.lines[:0]\n}\n\n// Maybe better return an error ?\n// AddLineWithCustomTruncate adds lines to the renderer, truncating each line according to the specified style.\n// It does not trims whitespace, and its possible to add multiple empty lines using this.\nfunc (r *ContentRenderer) AddLineWithCustomTruncate(lineStr string, truncateStyle TruncateStyle) {\n\t// If string is multiline, add individual lines separately\n\t// We dont use strings.Lines() we need to allow adding empty strings \"\" as line.\n\tfor line := range strings.SplitSeq(lineStr, \"\\n\") {\n\t\tif len(r.lines) >= r.maxLines {\n\t\t\tslog.Debug(\"Max lines reached\", \"name\", r.name, \"maxLines\", r.maxLines)\n\t\t\treturn\n\t\t}\n\t\t// Sanitazation should be done before truncate. Sanitization can increase width\n\t\t// For ex: Converting problematic unicode nbsp to spaces.\n\t\tif r.sanitizeContent {\n\t\t\tline = common.MakePrintableWithEscCheck(line, true)\n\t\t}\n\t\t// Some characters like \"\\t\" are considered 1 width\n\t\tline = TruncateBasedOnStyle(line, r.maxLineWidth, truncateStyle)\n\n\t\tr.lines = append(r.lines, line)\n\t}\n}\n\nfunc (r *ContentRenderer) Render() string {\n\treturn strings.Join(r.lines, \"\\n\")\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/content_renderer_test.go",
    "content": "package rendering\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestContentRendererBasic(t *testing.T) {\n\tt.Run(\"Basic test\", func(t *testing.T) {\n\t\tr := NewContentRenderer(6, 5, PlainTruncateRight, \"\")\n\t\tr.AddLines(\"123456\")\n\t\tr.AddLines(\"12345\\n12345\", \"123\")\n\t\tassert.Equal(t, 4, r.CntLines())\n\t\tr.AddLineWithCustomTruncate(\"123456\", TailsTruncateRight)\n\t\tr.AddLines(\"\\t1234\")\n\t\t// Should be ignored\n\t\tr.AddLines(\"1234\")\n\n\t\tres := r.Render()\n\t\texpected := \"12345\\n\" +\n\t\t\t\"12345\\n\" +\n\t\t\t\"12345\\n\" +\n\t\t\t\"123\\n\" +\n\t\t\t\"12...\\n\" +\n\t\t\t\"    1\"\n\t\tassert.Equal(t, expected, res, \"Basic truncation, and adding lines\")\n\n\t\tr.ClearLines()\n\t\tassert.Zero(t, r.CntLines(), \"ClearLines should remove all content\")\n\n\t\tr.AddLines(\"\\x00\\x11\\x1babc\")\n\t\tassert.Equal(t, \"\\x1babc\", r.Render())\n\n\t\tr.sanitizeContent = false\n\t\tr.ClearLines()\n\n\t\tr.AddLines(\"\\x00\\x11\\x1babc\")\n\n\t\tassert.Equal(t, \"\\x00\\x11\\x1babc\", r.Render())\n\n\t\tr = NewContentRenderer(0, 0, PlainTruncateRight, \"\")\n\t\tr.AddLines(\"L1\")\n\t\tr.AddLines(\"L2\")\n\t\tassert.Empty(t, r.Render())\n\t})\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/renderer.go",
    "content": "package rendering\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math/rand/v2\"\n\t\"strconv\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\ntype StyleModifier func(lipgloss.Style) lipgloss.Style\n\n// For now we are not allowing to add/update/remove lines to previous sections\n// We may allow that later.\n// Also we could have functions about getting sections count, line count, adding updating a\n// specific line in a specific section, and adjusting section sizes. But not needed now.\n// NOTE: Renderer's zero value isn't safe to use, always use NewRenderer()\ntype Renderer struct {\n\n\t// Current sectionization will not allow to predefine section\n\t// but only allow adding them via AddSection(). Hence trucateWill be applicable to\n\t// last section only.\n\tcontentSections []ContentRenderer\n\n\t// Empty for last section . len(sectionDividers) should be equal to len(contentSections) - 1\n\tsectionDividers []string\n\tcurSectionIdx   int\n\t// Including Dividers - Count of actual lines that were added. It maybe <= totalHeight - 2\n\tactualContentHeight int\n\tdefTruncateStyle    TruncateStyle\n\n\t// Whether to reduce rendered height to fit number of lines\n\ttruncateHeight bool\n\n\tborder BorderConfig\n\n\t// Should this go in contentRenderer - No . ContentRenderer is not for storing style configs\n\tcontentFGColor lipgloss.TerminalColor\n\tcontentBGColor lipgloss.TerminalColor\n\n\t// Should this go in borderConfig ?\n\tborderFGColor lipgloss.TerminalColor\n\tborderBGColor lipgloss.TerminalColor\n\n\t// Use this to add additional style modifications\n\t// This is applied before any style update that are defined by other configurations,\n\t// like border, height, width. Hence if conflicting styles are used, they can get\n\t// overridden\n\tstyleModifiers []StyleModifier\n\n\t// Maybe better rename these to maxHeight\n\t// Final rendered string should have exactly this many lines, including borders\n\t// But if truncateHeight is true, it maybe be <= totalHeight\n\ttotalHeight int\n\t// Every line should have at most this many characters, including borders\n\ttotalWidth int\n\n\tcontentHeight int\n\tcontentWidth  int\n\n\t// Note: Must pass non empty borderStrings if borderRequired is set as true\n\t// TODO: Have ansi.StringWidth checks in `ValidateConfig`\n\t// If you silently pass empty border, rendering will be unexpectd and,\n\t// it might take some time to RCA.\n\tborderRequired bool\n\tborderStrings  lipgloss.Border\n\t// for logging\n\tname string\n}\n\ntype RendererConfig struct {\n\tTotalHeight int\n\tTotalWidth  int\n\n\tDefTruncateStyle TruncateStyle\n\tTruncateHeight   bool\n\tBorderRequired   bool\n\n\tContentFGColor lipgloss.TerminalColor\n\tContentBGColor lipgloss.TerminalColor\n\n\tBorderFGColor lipgloss.TerminalColor\n\tBorderBGColor lipgloss.TerminalColor\n\n\tBorder       lipgloss.Border\n\tRendererName string\n}\n\nfunc DefaultRendererConfig(totalHeight int, totalWidth int) RendererConfig {\n\treturn RendererConfig{\n\t\tTotalHeight:      totalHeight,\n\t\tTotalWidth:       totalWidth,\n\t\tTruncateHeight:   false,\n\t\tBorderRequired:   false,\n\t\tDefTruncateStyle: PlainTruncateRight,\n\t\tContentFGColor:   lipgloss.NoColor{},\n\t\tContentBGColor:   lipgloss.NoColor{},\n\t\tBorderFGColor:    lipgloss.NoColor{},\n\t\tBorderBGColor:    lipgloss.NoColor{},\n\t\t//nolint: gosec // Not for security purpose, only for logging\n\t\tRendererName: \"R-\" + strconv.Itoa(rand.IntN(rendererNameMax)),\n\t}\n}\n\nfunc NewRenderer(cfg RendererConfig) (*Renderer, error) {\n\tif err := validate(cfg); err != nil {\n\t\treturn nil, err\n\t}\n\treturn createRendererWithValidatedConfig(cfg), nil\n}\n\nfunc NewRendererWithAutoFixConfig(cfg RendererConfig) *Renderer {\n\tvalidateAndAutoFix(&cfg)\n\treturn createRendererWithValidatedConfig(cfg)\n}\n\nfunc createRendererWithValidatedConfig(cfg RendererConfig) *Renderer {\n\tcontentHeight := cfg.TotalHeight\n\tif cfg.BorderRequired {\n\t\tcontentHeight -= 2\n\t}\n\tcontentWidth := cfg.TotalWidth\n\tif cfg.BorderRequired {\n\t\tcontentWidth -= 2\n\t}\n\n\treturn &Renderer{\n\n\t\tcontentSections: []ContentRenderer{\n\t\t\tNewContentRenderer(contentHeight, contentWidth, cfg.DefTruncateStyle, cfg.RendererName),\n\t\t},\n\t\tsectionDividers:     nil,\n\t\tcurSectionIdx:       0,\n\t\tactualContentHeight: 0,\n\t\tdefTruncateStyle:    cfg.DefTruncateStyle,\n\t\ttruncateHeight:      cfg.TruncateHeight,\n\n\t\tborder: NewBorderConfig(cfg.TotalHeight, cfg.TotalWidth),\n\n\t\tcontentFGColor: cfg.ContentFGColor,\n\t\tcontentBGColor: cfg.ContentBGColor,\n\t\tborderFGColor:  cfg.BorderFGColor,\n\t\tborderBGColor:  cfg.BorderBGColor,\n\n\t\ttotalHeight:   cfg.TotalHeight,\n\t\ttotalWidth:    cfg.TotalWidth,\n\t\tcontentHeight: contentHeight,\n\t\tcontentWidth:  contentWidth,\n\n\t\tborderRequired: cfg.BorderRequired,\n\t\tborderStrings:  cfg.Border,\n\t\tname:           cfg.RendererName,\n\t}\n}\n\n// There is code duplication with `validate` but, I can't think of any clean design pattern to fix that.\n// Note: Having a function validate(cfg,autoFix) error and ensure err is not nil via panic is not clean.\nfunc validateAndAutoFix(cfg *RendererConfig) {\n\tif cfg.TotalHeight < 0 || cfg.TotalWidth < 0 {\n\t\tslog.Debug(\"AutoFixConfig: clamping negative dimensions\", \"h\", cfg.TotalHeight, \"w\", cfg.TotalWidth)\n\t\tcfg.TotalHeight = max(0, cfg.TotalHeight)\n\t\tcfg.TotalWidth = max(0, cfg.TotalWidth)\n\t}\n\tif cfg.BorderRequired {\n\t\tif cfg.TotalWidth < MinWidthForBorder || cfg.TotalHeight < MinHeightForBorder {\n\t\t\tslog.Debug(\"AutoFixConfig: disabling border due to insufficient dimensions\",\n\t\t\t\t\"h\", cfg.TotalHeight, \"w\", cfg.TotalWidth)\n\t\t\tcfg.BorderRequired = false\n\t\t}\n\t}\n}\n\nfunc validate(cfg RendererConfig) error {\n\tif cfg.TotalHeight < 0 || cfg.TotalWidth < 0 {\n\t\treturn fmt.Errorf(\"dimensions must be non-negative (h=%d, w=%d)\", cfg.TotalHeight, cfg.TotalWidth)\n\t}\n\tif cfg.BorderRequired {\n\t\tif cfg.TotalWidth < MinWidthForBorder || cfg.TotalHeight < MinHeightForBorder {\n\t\t\treturn errors.New(\"need at least 2 width and height for borders\")\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/renderer_core.go",
    "content": "package rendering\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\n// Add lines as much as the remaining capacity allows\nfunc (r *Renderer) AddLines(lines ...string) *Renderer {\n\tr.contentSections[r.curSectionIdx].AddLines(lines...)\n\treturn r\n}\n\n// Lines until now will belong to current section, and\n// Any new lines will belong to a new section\nfunc (r *Renderer) AddSection() {\n\t// r.actualContentHeight before this point only includes sections\n\t// before r.curSectionIdx\n\tr.actualContentHeight += r.contentSections[r.curSectionIdx].CntLines()\n\n\t// Silently Fail if cannot add\n\tif r.contentHeight <= r.actualContentHeight {\n\t\tslog.Error(\"Cannot add any more sections\", \"name\", r.name, \"actualHeight\", r.actualContentHeight,\n\t\t\t\"contentHeight\", r.contentHeight)\n\t\treturn\n\t}\n\n\t// Add divider\n\tr.border.AddDivider(r.actualContentHeight)\n\t// sectionDivider should be of borderstyle\n\tr.sectionDividers = append(r.sectionDividers, lipgloss.NewStyle().\n\t\tForeground(r.borderFGColor).\n\t\tBackground(r.borderBGColor).\n\t\tRender(strings.Repeat(r.borderStrings.Top, r.contentWidth)))\n\tr.actualContentHeight++\n\n\tremainingHeight := r.contentHeight - r.actualContentHeight\n\tr.contentSections = append(r.contentSections,\n\t\tNewContentRenderer(remainingHeight, r.contentWidth, r.defTruncateStyle, r.name))\n\t// Adjust index\n\tr.curSectionIdx++\n}\n\n// Truncate would always preserve ansi codes.\nfunc (r *Renderer) AddLineWithCustomTruncate(line string, truncateStyle TruncateStyle) {\n\tr.contentSections[r.curSectionIdx].AddLineWithCustomTruncate(line, truncateStyle)\n}\n\nfunc (r *Renderer) AddStyleModifier(modifier StyleModifier) *Renderer {\n\tr.styleModifiers = append(r.styleModifiers, modifier)\n\treturn r\n}\n\nfunc (r *Renderer) SetBorderTitle(title string) {\n\tr.border.SetTitle(title)\n}\n\nfunc (r *Renderer) SetBorderInfoItems(infoItems ...string) {\n\tr.border.SetInfoItems(infoItems...)\n}\n\nfunc (r *Renderer) AreInfoItemsTruncated() bool {\n\treturn r.border.AreInfoItemsTruncated()\n}\n\n// Should not do any updates on 'r'\nfunc (r *Renderer) Render() string {\n\tcontent := strings.Builder{}\n\tfor i := range r.contentSections {\n\t\t// After every iteration, current cursor will be on next newline\n\t\tcurContent := r.contentSections[i].Render()\n\t\tcontent.WriteString(curContent)\n\t\t// == \"\" check cant differentiate between no data, vs empty line\n\t\tif r.contentSections[i].CntLines() > 0 {\n\t\t\tcontent.WriteString(\"\\n\")\n\t\t}\n\n\t\tif i < len(r.contentSections)-1 {\n\t\t\t// True for all except last section\n\t\t\tcontent.WriteString(r.sectionDividers[i])\n\t\t\tcontent.WriteString(\"\\n\")\n\t\t}\n\t}\n\tcontentStr := strings.TrimSuffix(content.String(), \"\\n\")\n\tres := r.Style().Render(contentStr)\n\t// Post rendering validations - Maybe we can return an error instead of logging\n\t// TODO(perf): This can be disabled to improve performance\n\tmaxW := 0\n\tfor line := range strings.Lines(res) {\n\t\tmaxW = max(maxW, ansi.StringWidth(line))\n\t}\n\n\tlineCnt := strings.Count(res, \"\\n\") + 1\n\tif maxW > r.totalWidth || lineCnt > r.totalHeight {\n\t\tslog.Error(\n\t\t\t\"Rendered output data inconsistency\",\n\t\t\t\"name\",\n\t\t\tr.name,\n\t\t\t\"lineCnt\",\n\t\t\tlineCnt,\n\t\t\t\"totalHeight\",\n\t\t\tr.totalHeight,\n\t\t\t\"totalWidth\",\n\t\t\tr.totalWidth,\n\t\t\t\"maxW\",\n\t\t\tmaxW,\n\t\t)\n\t\t// lipgloss Render() doesn't always respects the \"height\" value,\n\t\t// so res can have more height than intended. In that case, we must truncate lines here.\n\t\tnewRes := strings.Builder{}\n\t\tcurCnt := 0\n\t\t// Dont use strings.Lines(), that wont allow us to have empty lines\n\t\tfor line := range strings.SplitSeq(res, \"\\n\") {\n\t\t\tif curCnt == r.totalHeight {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tnewRes.WriteString(ansi.Truncate(line, r.totalWidth, \"\"))\n\t\t\tcurCnt++\n\t\t\tif curCnt < r.totalHeight {\n\t\t\t\tnewRes.WriteByte('\\n')\n\t\t\t}\n\t\t}\n\t\treturn newRes.String()\n\t}\n\n\treturn res\n}\n\nfunc (r *Renderer) Style() lipgloss.Style {\n\tcontentHeight := r.contentHeight\n\tif r.truncateHeight {\n\t\tcontentHeight = r.actualContentHeight\n\t}\n\ts := lipgloss.NewStyle()\n\n\tfor _, modifier := range r.styleModifiers {\n\t\ts = modifier(s)\n\t}\n\n\ts = s.Width(r.contentWidth).\n\t\tHeight(contentHeight).\n\t\tBackground(r.contentBGColor).\n\t\tForeground(r.contentFGColor)\n\n\tif r.borderRequired {\n\t\ts = s.Border(r.border.GetBorder(r.borderStrings))\n\t\ts = s.BorderForeground(r.borderFGColor).\n\t\t\tBorderBackground(r.borderBGColor)\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/renderer_test.go",
    "content": "package rendering\n\nimport (\n\t\"flag\"\n\t\"testing\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\nconst (\n\tsectionStr = \"<SECTION>\"\n)\n\nfunc TestMain(m *testing.M) {\n\tflag.Parse()\n\tif testing.Verbose() {\n\t\tutils.SetRootLoggerToStdout(true)\n\t} else {\n\t\tutils.SetRootLoggerToDiscarded()\n\t}\n\tm.Run()\n}\n\nfunc getDefaultTestRendererConfig(totalHeight int, totalWidth int, borderRequired bool,\n\ttruncateHeight bool) RendererConfig {\n\tcfg := DefaultRendererConfig(totalHeight, totalWidth)\n\tif borderRequired {\n\t\tcfg.BorderRequired = true\n\t\tcfg.Border = lipgloss.Border{\n\t\t\tTop:    \"─\",\n\t\t\tBottom: \"─\",\n\t\t\tLeft:   \"│\",\n\t\t\tRight:  \"│\",\n\n\t\t\tTopLeft:  \"╭\",\n\t\t\tTopRight: \"╮\",\n\n\t\t\tBottomLeft:  \"╰\",\n\t\t\tBottomRight: \"╯\",\n\n\t\t\tMiddleLeft:  \"├\",\n\t\t\tMiddleRight: \"┤\",\n\t\t}\n\t}\n\tcfg.TruncateHeight = truncateHeight\n\treturn cfg\n}\n\nfunc getDefaultTestRenderer(totalHeight int, totalWidth int, borderRequired bool) *Renderer {\n\tr, _ := NewRenderer(getDefaultTestRendererConfig(totalHeight, totalWidth, borderRequired, false))\n\treturn r\n}\n\nfunc TestRendererBasic(t *testing.T) {\n\tt.Run(\"Basic test\", func(t *testing.T) {\n\t\tr := getDefaultTestRenderer(4, 4, true)\n\t\tr.AddLines(\"L1\")\n\t\tr.AddLines(\"L2--Extra line should truncated\")\n\t\tr.AddLines(\"L3--Extra line should not be added\")\n\t\tres := r.Render()\n\t\texpected := \"\" +\n\t\t\t\"╭──╮\\n\" +\n\t\t\t\"│L1│\\n\" +\n\t\t\t\"│L2│\\n\" +\n\t\t\t\"╰──╯\"\n\t\tassert.Equal(t, expected, res)\n\t})\n\n\tt.Run(\"Empty Renderer\", func(t *testing.T) {\n\t\tr := getDefaultTestRenderer(0, 0, false)\n\t\tr.AddLines(\"L1\")\n\t\tr.AddLines(\"L2--Extra line should truncated\")\n\t\tr.AddLines(\"L3--Extra line should not be added\")\n\t\tres := r.Render()\n\t\texpected := \"\"\n\t\tassert.Equal(t, expected, res)\n\t})\n\n\tt.Run(\"Invalid config Renderer\", func(t *testing.T) {\n\t\tcfg := getDefaultTestRendererConfig(0, 0, true, false)\n\t\tr, err := NewRenderer(cfg)\n\t\tassert.Nil(t, r)\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestSections(t *testing.T) {\n\tsectionTests := []struct {\n\t\tname           string\n\t\ttotalHeight    int\n\t\ttotalWidth     int\n\t\tborderRequired bool\n\t\t// Test expects only single line strings.\n\t\tlines         []string\n\t\ttrucateheight bool\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\tname:           \"Basic Sections\",\n\t\t\ttotalHeight:    7,\n\t\t\ttotalWidth:     4,\n\t\t\tborderRequired: true,\n\t\t\tlines:          []string{\"L1\", sectionStr, \"L2\", sectionStr, sectionStr, \"L3\", sectionStr},\n\t\t\ttrucateheight:  false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──╮\\n\" +\n\t\t\t\t\"│L1│\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"│L2│\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"╰──╯\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Only Sections, with empty lines\",\n\t\t\ttotalHeight:    7,\n\t\t\ttotalWidth:     4,\n\t\t\tborderRequired: true,\n\t\t\tlines:          []string{sectionStr, sectionStr, \"\", sectionStr, sectionStr},\n\t\t\ttrucateheight:  false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──╮\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"│  │\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"╰──╯\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Single line at the end\",\n\t\t\ttotalHeight:    7,\n\t\t\ttotalWidth:     4,\n\t\t\tborderRequired: true,\n\t\t\tlines:          []string{sectionStr, sectionStr, sectionStr, sectionStr, \"L1\"},\n\t\t\ttrucateheight:  false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──╮\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"│L1│\\n\" +\n\t\t\t\t\"╰──╯\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Only sections\",\n\t\t\ttotalHeight:    3,\n\t\t\ttotalWidth:     4,\n\t\t\tborderRequired: true,\n\t\t\tlines:          []string{sectionStr},\n\t\t\ttrucateheight:  false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──╮\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"╰──╯\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Minimal width\",\n\t\t\ttotalHeight:    4,\n\t\t\ttotalWidth:     2,\n\t\t\tborderRequired: true,\n\t\t\tlines:          []string{sectionStr, \"L1\", sectionStr, sectionStr},\n\t\t\ttrucateheight:  false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭╮\\n\" +\n\t\t\t\t\"├┤\\n\" +\n\t\t\t\t\"││\\n\" +\n\t\t\t\t\"╰╯\",\n\t\t},\n\t\t{\n\t\t\tname:           \"Minimal height\",\n\t\t\ttotalHeight:    2,\n\t\t\ttotalWidth:     8,\n\t\t\tborderRequired: true,\n\t\t\tlines:          []string{sectionStr, \"L1\", sectionStr, sectionStr},\n\t\t\ttrucateheight:  false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──────╮\\n\" +\n\t\t\t\t\"│      │\",\n\t\t\t// Border breaks here, because lipgloss creates a 3 line string, and\n\t\t\t// our renderer, than manually adjusts it.\n\t\t},\n\t\t{\n\t\t\tname:           \"Minimal heightBorderless\",\n\t\t\ttotalHeight:    0,\n\t\t\ttotalWidth:     8,\n\t\t\tborderRequired: false,\n\t\t\tlines:          []string{sectionStr, \"L1\", sectionStr, sectionStr},\n\t\t\ttrucateheight:  false,\n\t\t\texpected:       \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"No Border\",\n\t\t\ttotalHeight:    4,\n\t\t\ttotalWidth:     4,\n\t\t\tborderRequired: false,\n\t\t\tlines:          []string{sectionStr, \"L1\", sectionStr},\n\t\t\ttrucateheight:  false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"    \\n\" +\n\t\t\t\t\"L1  \\n\" +\n\t\t\t\t\"    \\n\" +\n\t\t\t\t\"    \",\n\t\t},\n\t}\n\n\tfor _, tt := range sectionTests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr, _ := NewRenderer(getDefaultTestRendererConfig(\n\t\t\t\ttt.totalHeight, tt.totalWidth, tt.borderRequired, tt.trucateheight))\n\t\t\t// maxL := r.contentWidth\n\t\t\t// if i >= maxL, check for errors here\n\t\t\tfor _, l := range tt.lines {\n\t\t\t\tif l == sectionStr {\n\t\t\t\t\tr.AddSection()\n\t\t\t\t} else {\n\t\t\t\t\tr.AddLines(l)\n\t\t\t\t}\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expected, r.Render())\n\t\t})\n\t}\n}\n\nfunc TestDynamicHeight(t *testing.T) {\n\tdynamicHeightTests := []struct {\n\t\tname          string\n\t\ttotalHeight   int\n\t\tlines         []string\n\t\ttrucateheight bool\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\tname:          \"No truncate\",\n\t\t\ttotalHeight:   5,\n\t\t\tlines:         []string{\"L1\"},\n\t\t\ttrucateheight: false,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──╮\\n\" +\n\t\t\t\t\"│L1│\\n\" +\n\t\t\t\t\"│  │\\n\" +\n\t\t\t\t\"│  │\\n\" +\n\t\t\t\t\"╰──╯\",\n\t\t},\n\t\t{\n\t\t\tname:          \"Basic truncate\",\n\t\t\ttotalHeight:   7,\n\t\t\tlines:         []string{\"L1\", \"\"},\n\t\t\ttrucateheight: true,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──╮\\n\" +\n\t\t\t\t\"│L1│\\n\" +\n\t\t\t\t\"│  │\\n\" +\n\t\t\t\t\"╰──╯\",\n\t\t},\n\t\t{\n\t\t\tname:          \"Basic truncate with Sections\",\n\t\t\ttotalHeight:   100,\n\t\t\tlines:         []string{\"L1\", \"\", sectionStr, \"L2\", \"\", \"L3\"},\n\t\t\ttrucateheight: true,\n\t\t\texpected: \"\" +\n\t\t\t\t\"╭──╮\\n\" +\n\t\t\t\t\"│L1│\\n\" +\n\t\t\t\t\"│  │\\n\" +\n\t\t\t\t\"├──┤\\n\" +\n\t\t\t\t\"│L2│\\n\" +\n\t\t\t\t\"│  │\\n\" +\n\t\t\t\t\"│L3│\\n\" +\n\t\t\t\t\"╰──╯\",\n\t\t},\n\t}\n\n\tfor _, tt := range dynamicHeightTests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr, _ := NewRenderer(getDefaultTestRendererConfig(\n\t\t\t\ttt.totalHeight, 4, true, tt.trucateheight))\n\t\t\tfor _, l := range tt.lines {\n\t\t\t\tif l == sectionStr {\n\t\t\t\t\tr.AddSection()\n\t\t\t\t} else {\n\t\t\t\t\tr.AddLines(l)\n\t\t\t\t}\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expected, r.Render())\n\t\t})\n\t}\n}\nfunc TestBorders(t *testing.T) {\n\tt.Run(\"Basic test\", func(t *testing.T) {\n\t\tr := getDefaultTestRenderer(4, 10, true)\n\t\tr.AddLines(\"L1\")\n\t\tr.AddLines(\"L2\")\n\t\tr.SetBorderTitle(\"Title\")\n\t\tres := r.Render()\n\t\texpected := \"\" +\n\t\t\t\"╭┤ Titl ├╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰────────╯\"\n\t\tassert.False(t, r.AreInfoItemsTruncated())\n\t\tassert.Equal(t, expected, res, \"No margin if title is too big\")\n\t\tr.SetBorderTitle(\"T\")\n\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭─┤ T ├──╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰────────╯\"\n\t\tassert.Equal(t, expected, res, \"Margin should be there if title fits well\")\n\n\t\tr.border.SetInfoItems(\"A\", \"B\")\n\t\tassert.False(t, r.AreInfoItemsTruncated())\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭─┤ T ├──╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰┤A├─┤B├─╯\"\n\t\tassert.Equal(t, expected, res)\n\n\t\tr.border.SetInfoItems(\"A1\", \"B2\")\n\t\tassert.True(t, r.AreInfoItemsTruncated())\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭─┤ T ├──╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰┤A├─┤B├─╯\"\n\t\tassert.Equal(t, expected, res)\n\n\t\tr.border.SetInfoItems(\"A12345\")\n\t\tassert.True(t, r.AreInfoItemsTruncated())\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭─┤ T ├──╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰┤A1234├─╯\"\n\t\tassert.Equal(t, expected, res, \"Info Items Truncation\")\n\n\t\tr.SetBorderTitle(\"✅1✅2✅3\")\n\t\tr.SetBorderInfoItems()\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭┤ ✅1 ├─╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰────────╯\"\n\t\tassert.Equal(t, expected, res, \"Double terminal width characters in Title\")\n\n\t\ttestStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"#0000ff\"))\n\t\ttitle := testStyle.Render(\"Title\")\n\n\t\tr.SetBorderTitle(title)\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭┤ Titl ├╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰────────╯\"\n\n\t\tassert.Equal(t, expected, res, \"Ansi escapes are not preserved\")\n\n\t\tr.SetBorderTitle(\"\")\n\t\tr.SetBorderInfoItems(\"A\", \"\")\n\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭────────╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰─┤A├─┤├─╯\"\n\n\t\tassert.Equal(t, expected, res, \"Empty title is ignored, but not empty infoitems\")\n\n\t\tr.SetBorderInfoItems(\"AA\", \"\")\n\n\t\tres = r.Render()\n\t\texpected = \"\" +\n\t\t\t\"╭────────╮\\n\" +\n\t\t\t\"│L1      │\\n\" +\n\t\t\t\"│L2      │\\n\" +\n\t\t\t\"╰─┤A├─┤├─╯\"\n\t\tassert.True(t, r.AreInfoItemsTruncated())\n\t\tassert.Equal(t, expected, res, \"Truncated even if there was enough space because one item was too big\")\n\t})\n\n\tt.Run(\"Different Border\", func(t *testing.T) {\n\t\tcfg := getDefaultTestRendererConfig(6, 10, true, false)\n\t\tcfg.Border = lipgloss.Border{\n\t\t\tTop:    \"─\",\n\t\t\tBottom: \"*\",\n\t\t\tLeft:   \"+\",\n\t\t\tRight:  \"│\",\n\n\t\t\tTopLeft:  \"╭\",\n\t\t\tTopRight: \"╮\",\n\n\t\t\tBottomLeft:  \"╰\",\n\t\t\tBottomRight: \"╯\",\n\n\t\t\tMiddleLeft:  \"├\",\n\t\t\tMiddleRight: \"┤\",\n\t\t}\n\n\t\tr, _ := NewRenderer(cfg)\n\t\tr.SetBorderTitle(\"Title\")\n\t\tr.SetBorderInfoItems(\"A\")\n\t\tr.AddLines(\"L1\")\n\t\tr.AddSection()\n\t\tr.AddLines(\"\")\n\t\tr.AddLines(\"L2\")\n\n\t\tres := r.Render()\n\t\texpected := \"\" +\n\t\t\t\"╭┤ Titl ├╮\\n\" +\n\t\t\t\"+L1      │\\n\" +\n\t\t\t\"├────────┤\\n\" +\n\t\t\t\"+        │\\n\" +\n\t\t\t\"+L2      │\\n\" +\n\t\t\t\"╰****┤A├*╯\"\n\n\t\tassert.Equal(t, expected, res, \"Ansi escape is preserved\")\n\t})\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/truncate.go",
    "content": "package rendering\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n)\n\ntype TruncateStyle int\n\n// These truncate styles must preserve ansi escape codes. If something doesn't preserves\n// it shouldn't be here\nconst (\n\tPlainTruncateRight = iota\n\tTailsTruncateRight\n)\n\nfunc TruncateBasedOnStyle(line string, maxWidth int, truncateStyle TruncateStyle) string {\n\tswitch truncateStyle {\n\tcase PlainTruncateRight:\n\t\treturn ansi.Truncate(line, maxWidth, \"\")\n\tcase TailsTruncateRight:\n\t\treturn ansi.Truncate(line, maxWidth, \"...\")\n\tdefault:\n\t\tslog.Error(\"Invalid truncate style\", \"style\", truncateStyle)\n\t\treturn \"\"\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/rendering/truncate_test.go",
    "content": "package rendering\n\nimport (\n\t\"testing\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTruncate(t *testing.T) {\n\ttestStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(\"#0000ff\"))\n\ttestdata := []struct {\n\t\tname     string\n\t\tline     string\n\t\tmaxWidth int\n\t\tstyle    TruncateStyle\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"No truncate\",\n\t\t\tline:     \"abc\",\n\t\t\tmaxWidth: 10,\n\t\t\tstyle:    PlainTruncateRight,\n\t\t\texpected: \"abc\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Plain truncate\",\n\t\t\tline:     \"abcdefgh\",\n\t\t\tmaxWidth: 5,\n\t\t\tstyle:    PlainTruncateRight,\n\t\t\texpected: \"abcde\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Tails truncate\",\n\t\t\tline:     \"abcdefgh\",\n\t\t\tmaxWidth: 5,\n\t\t\tstyle:    TailsTruncateRight,\n\t\t\texpected: \"ab...\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Invalid style\",\n\t\t\tline:     \"abcdefgh\",\n\t\t\tmaxWidth: 5,\n\t\t\tstyle:    10,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Tails truncate with too less width\",\n\t\t\tline:     \"abcdefgh\",\n\t\t\tmaxWidth: 2,\n\t\t\tstyle:    TailsTruncateRight,\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Wide characters\",\n\t\t\tline:     \"✅1✅2✅3\",\n\t\t\tmaxWidth: 3,\n\t\t\tstyle:    PlainTruncateRight,\n\t\t\texpected: \"✅1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Wide characters 2\",\n\t\t\tline:     \"✅1✅2✅3\",\n\t\t\tmaxWidth: 4,\n\t\t\tstyle:    PlainTruncateRight,\n\t\t\texpected: \"✅1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Wide characters 3\",\n\t\t\tline:     \"✅1✅2✅3\",\n\t\t\tmaxWidth: 4,\n\t\t\tstyle:    TailsTruncateRight,\n\t\t\texpected: \"...\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Ansi color sequence\",\n\t\t\tline:     testStyle.Render(\"12345\"),\n\t\t\tmaxWidth: 4,\n\t\t\tstyle:    TailsTruncateRight,\n\t\t\texpected: testStyle.Render(\"1...\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"Ansi color sequence with just enough widht\",\n\t\t\tline:     testStyle.Render(\"1234\"),\n\t\t\tmaxWidth: 4,\n\t\t\tstyle:    TailsTruncateRight,\n\t\t\texpected: testStyle.Render(\"1234\"),\n\t\t},\n\t}\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, TruncateBasedOnStyle(tt.line, tt.maxWidth, tt.style))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/README.md",
    "content": "# sidebar package\nThis is for the sidebar UI, and for fetching and updating sidebar directories\n\n# To-dos\n- Add missing unit tests\n- Separate out implementation of file I/O operations. (Disk listing, Reading and Updating pinned.json)\n  This package should only be concerned with UI/UX.\n- Implementing a proper state transitioning for the sidebar's different modes (normal, search, rename)\n- Some methods could be made more pure by reducing side effects\n\n# Coverage\n\n```bash\ncd /path/to/ui/sidebar\ngo test -cover\n```\nCurrent coverage is 29.3%."
  },
  {
    "path": "src/internal/ui/sidebar/consts.go",
    "content": "package sidebar\n\nimport (\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\n// These are effectively consts\n// Had to use `var` as go doesn't allows const structs\nvar homeDividerDir = directory{ //nolint: gochecknoglobals // This is more like a const.\n\tName:     \"\",\n\tLocation: \"Home+-*/=?\",\n}\n\nvar pinnedDividerDir = directory{ //nolint: gochecknoglobals // This is more like a const.\n\tName:     \"\",\n\tLocation: \"Pinned+-*/=?\",\n}\n\nvar diskDividerDir = directory{ //nolint: gochecknoglobals // This is more like a const.\n\tName:     \"\",\n\tLocation: \"Disks+-*/=?\",\n}\n\nvar defaultSectionSlice = []string{ //nolint: gochecknoglobals // This is more like a const.\n\tutils.SidebarSectionHome, utils.SidebarSectionPinned, utils.SidebarSectionDisks,\n}\n\n// superfile logo + blank line + search bar\nconst sideBarInitialHeight = 3\n\n// UI dimension constants for sidebar\nconst (\n\t// searchBarPadding is the total padding for search bar (borders + prompt + extra char)\n\tsearchBarPadding = 5 // 2 (borders) + 2 (prompt) + 1 (extra char)\n\n\tdirectoryCapacityForDividers = 2\n\n\t// dividerDirHeight is the default height when no height is available\n\tdividerDirHeight = 3\n\n\tminHeight = 5\n\tminWidth  = 7\n)\n"
  },
  {
    "path": "src/internal/ui/sidebar/directory_utils.go",
    "content": "package sidebar\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"slices\"\n\n\t\"github.com/adrg/xdg\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// Fuzzy search function for a list of directories.\nfunc fuzzySearch(query string, dirs []directory) []directory {\n\tif len(dirs) == 0 {\n\t\treturn []directory{}\n\t}\n\n\tvar filteredDirs []directory\n\n\t// Optimization - This haystack can be kept precomputed based on directories\n\t// instead of re computing it in each call\n\thaystack := make([]string, len(dirs))\n\tdirMap := make(map[string]directory, len(dirs))\n\tfor i, dir := range dirs {\n\t\thaystack[i] = dir.Name\n\t\tdirMap[dir.Name] = dir\n\t}\n\n\tfor _, match := range utils.FzfSearch(query, haystack) {\n\t\tif d, ok := dirMap[match.Key]; ok {\n\t\t\tfilteredDirs = append(filteredDirs, d)\n\t\t}\n\t}\n\n\treturn filteredDirs\n}\n\n// getDirectories returns the list of directories to display in the sidebar.\nfunc getDirectories(pinnedMgr *PinnedManager, sections []string) []directory {\n\treturn formDirctorySlice(\n\t\tgetWellKnownDirectories(),\n\t\tgetPinnedDirectoriesWithIcon(pinnedMgr),\n\t\tgetExternalMediaFolders(),\n\t\tsections,\n\t)\n}\n\n// Return system default directory e.g. Home, Downloads, etc\nfunc getWellKnownDirectories() []directory {\n\twellKnownDirectories := []directory{\n\t\t{Location: xdg.Home, Name: icon.Home + icon.Space + \"Home\"},\n\t\t{Location: xdg.UserDirs.Download, Name: icon.Download + icon.Space + \"Downloads\"},\n\t\t{Location: xdg.UserDirs.Documents, Name: icon.Documents + icon.Space + \"Documents\"},\n\t\t{Location: xdg.UserDirs.Pictures, Name: icon.Pictures + icon.Space + \"Pictures\"},\n\t\t{Location: xdg.UserDirs.Videos, Name: icon.Videos + icon.Space + \"Videos\"},\n\t\t{Location: xdg.UserDirs.Music, Name: icon.Music + icon.Space + \"Music\"},\n\t\t{Location: xdg.UserDirs.Templates, Name: icon.Templates + icon.Space + \"Templates\"},\n\t\t{Location: xdg.UserDirs.PublicShare, Name: icon.PublicShare + icon.Space + \"PublicShare\"},\n\t}\n\n\t// Add Trash directory for Linux only\n\tif runtime.GOOS == utils.OsLinux {\n\t\twellKnownDirectories = append(wellKnownDirectories, directory{\n\t\t\tLocation: variable.LinuxTrashDirectory,\n\t\t\tName:     icon.Trash + icon.Space + \"Trash\",\n\t\t})\n\t}\n\n\treturn slices.DeleteFunc(wellKnownDirectories, func(d directory) bool {\n\t\t_, err := os.Stat(d.Location)\n\t\treturn err != nil\n\t})\n}\n\nfunc getPinnedDirectoriesWithIcon(pinnedMgr *PinnedManager) []directory {\n\tdirs := pinnedMgr.Load()\n\tfor i := range dirs {\n\t\ticonInfo := common.GetElementIcon(dirs[i].Name, true, false, common.Config.Nerdfont)\n\t\tdirs[i].Name = iconInfo.Icon + icon.Space + dirs[i].Name\n\t}\n\treturn dirs\n}\n\n// Get filtered directories using fuzzy search logic with three haystacks.\nfunc getFilteredDirectories(query string, pinnedMgr *PinnedManager, sections []string) []directory {\n\treturn formDirctorySlice(\n\t\tfuzzySearch(query, getWellKnownDirectories()),\n\t\tfuzzySearch(query, getPinnedDirectoriesWithIcon(pinnedMgr)),\n\t\tfuzzySearch(query, getExternalMediaFolders()),\n\t\tsections,\n\t)\n}\n\nfunc formDirctorySlice(homeDirectories []directory, pinnedDirectories []directory,\n\tdiskDirectories []directory, sections []string) []directory {\n\t// Preallocation for efficiency\n\ttotalCapacity := len(homeDirectories) + len(pinnedDirectories) + len(diskDirectories) + directoryCapacityForDividers\n\tdirectories := make([]directory, 0, totalCapacity)\n\n\tfor _, section := range sections {\n\t\tswitch section {\n\t\tcase utils.SidebarSectionHome:\n\t\t\tif len(directories) > 0 {\n\t\t\t\tdirectories = append(directories, homeDividerDir)\n\t\t\t}\n\t\t\tdirectories = append(directories, homeDirectories...)\n\t\tcase utils.SidebarSectionPinned:\n\t\t\tdirectories = append(directories, pinnedDividerDir)\n\t\t\tdirectories = append(directories, pinnedDirectories...)\n\t\tcase utils.SidebarSectionDisks:\n\t\t\tdirectories = append(directories, diskDividerDir)\n\t\t\tdirectories = append(directories, diskDirectories...)\n\t\t}\n\t}\n\n\treturn directories\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/disk_utils.go",
    "content": "package sidebar\n\nimport (\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/shirou/gopsutil/v4/disk\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\n// Get external media directories\nfunc getExternalMediaFolders() []directory {\n\t// only get physical drives\n\tparts, err := disk.Partitions(false)\n\n\tif err != nil {\n\t\tslog.Error(\"Error while getting external media: \", \"error\", err)\n\t\treturn nil\n\t}\n\tvar disks []directory\n\tfor _, disk := range parts {\n\t\t// ShouldListDisk, DiskName, and DiskLocation, each has runtime.GOOS checks\n\t\t// We can ideally reduce it to one check only.\n\t\tif shouldListDisk(disk.Mountpoint) {\n\t\t\tdisks = append(disks, directory{\n\t\t\t\tName:     diskName(disk.Mountpoint),\n\t\t\t\tLocation: diskLocation(disk.Mountpoint),\n\t\t\t})\n\t\t}\n\t}\n\treturn disks\n}\n\nfunc shouldListDisk(mountPoint string) bool {\n\tif runtime.GOOS == utils.OsWindows {\n\t\t// We need to get C:, D: drive etc in the list\n\t\treturn true\n\t}\n\n\t// Should always list the main disk\n\tif mountPoint == \"/\" {\n\t\treturn true\n\t}\n\n\t// TODO : make a configurable field in config.yaml\n\t// excluded_disk_mounts = [\"/Volumes/.timemachine\"]\n\t// Mountpoints that are in subdirectory of disk_mounts\n\t// but still are to be excluded in disk section of sidebar\n\tif strings.HasPrefix(mountPoint, \"/Volumes/.timemachine\") {\n\t\treturn false\n\t}\n\n\t// We avoid listing all mounted partitions (Otherwise listed disk could get huge)\n\t// but only a few partitions that usually corresponds to external physical devices\n\t// For example : mounts like /boot, /var/ will get skipped\n\t// This can be inaccurate based on your system setup if you mount any external devices\n\t// on other directories, or if you have some extra mounts on these directories\n\t// TODO : make a configurable field in config.yaml\n\t// disk_mounts = [\"/mnt\", \"/media\", \"/run/media\", \"/Volumes\"]\n\t// Only block devicies that are mounted on these or any subdirectory of these Mountpoints\n\t// Will be shown in disk sidebar\n\treturn strings.HasPrefix(mountPoint, \"/mnt\") ||\n\t\tstrings.HasPrefix(mountPoint, \"/media\") ||\n\t\tstrings.HasPrefix(mountPoint, \"/run/media\") ||\n\t\tstrings.HasPrefix(mountPoint, \"/Volumes\")\n}\n\nfunc diskName(mountPoint string) string {\n\t// In windows we dont want to use filepath.Base as it returns \"\\\" for when\n\t// mountPoint is any drive root \"C:\", \"D:\", etc. Hence causing same name\n\t// for each drive\n\tif runtime.GOOS == utils.OsWindows {\n\t\treturn mountPoint\n\t}\n\n\t// This might cause duplicate names in case you mount two devices in\n\t// /mnt/usb and /mnt/dir2/usb . Full mountpoint is a more accurate way\n\t// but that results in messy UI, hence we do this.\n\treturn filepath.Base(mountPoint)\n}\n\nfunc diskLocation(mountPoint string) string {\n\t// In windows if you are in \"C:\\some\\path\", \"cd C:\" will not cd to root of C: drive\n\t// but \"cd C:\\\" will\n\tif runtime.GOOS == utils.OsWindows {\n\t\treturn filepath.Join(mountPoint, \"\\\\\")\n\t}\n\treturn mountPoint\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/navigation.go",
    "content": "package sidebar\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc (s *Model) ListUp() {\n\tslog.Debug(\"controlListUp called\", \"cursor\", s.cursor,\n\t\t\"renderIndex\", s.renderIndex, \"directory count\", len(s.directories))\n\tif s.NoActualDir() {\n\t\treturn\n\t}\n\tif s.cursor > 0 {\n\t\t// Not at the top, can safely decrease\n\t\ts.cursor--\n\t} else {\n\t\t// We are at the top. Move to the bottom\n\t\ts.cursor = len(s.directories) - 1\n\t}\n\t// We should update even if cursor is at divider for now\n\t// Otherwise dividers are sometimes skipped in render in case of\n\t// large pinned directories\n\ts.updateRenderIndex()\n\tif s.directories[s.cursor].isDivider() {\n\t\t// cause another listUp trigger to move up.\n\t\ts.ListUp()\n\t}\n}\n\nfunc (s *Model) ListDown() {\n\tslog.Debug(\"controlListDown called\", \"cursor\", s.cursor,\n\t\t\"renderIndex\", s.renderIndex, \"directory count\", len(s.directories))\n\tif s.NoActualDir() {\n\t\treturn\n\t}\n\tif s.cursor < len(s.directories)-1 {\n\t\t// Not at the bottom, can safely increase\n\t\ts.cursor++\n\t} else {\n\t\t// We are at the bottom. Move to the top\n\t\ts.cursor = 0\n\t}\n\n\t// We should update even if cursor is at divider for now\n\t// Otherwise dividers are sometimes skipped in render in case of\n\t// large pinned directories\n\ts.updateRenderIndex()\n\n\t// Move below special divider directories\n\tif s.directories[s.cursor].isDivider() {\n\t\t// cause another listDown trigger to move down.\n\t\ts.ListDown()\n\t}\n}\n\n// Return till what indexes we will render, if we start from startIndex\n// if returned value is `startIndex - 1`, that means nothing can be rendered\n// This could be made constant time by keeping Indexes ot special directories saved,\n// but that too much.\nfunc (s *Model) lastRenderedIndex(startIndex int) int {\n\tmainPanelHeight := s.height - common.BorderPadding\n\tcurHeight := sideBarInitialHeight\n\tendIndex := startIndex - 1\n\tfor i := startIndex; i < len(s.directories); i++ {\n\t\tcurHeight += s.directories[i].requiredHeight()\n\t\tif curHeight > mainPanelHeight {\n\t\t\tbreak\n\t\t}\n\t\tendIndex = i\n\t}\n\treturn endIndex\n}\n\n// Return what will be the startIndex, if we end at endIndex\n// if returned value is `endIndex + 1`, that means nothing can be rendered\nfunc (s *Model) firstRenderedIndex(endIndex int) int {\n\tmainPanelHeight := s.height - common.BorderPadding\n\n\t// This should ideally never happen. Maybe we should panic ?\n\tif endIndex >= len(s.directories) {\n\t\treturn endIndex + 1\n\t}\n\n\tcurHeight := sideBarInitialHeight\n\tstartIndex := endIndex + 1\n\tfor i := endIndex; i >= 0; i-- {\n\t\tcurHeight += s.directories[i].requiredHeight()\n\t\tif curHeight > mainPanelHeight {\n\t\t\tbreak\n\t\t}\n\t\tstartIndex = i\n\t}\n\treturn startIndex\n}\n\nfunc (s *Model) updateRenderIndex() {\n\t// Case I : New cursor moved above current renderable range\n\tif s.cursor < s.renderIndex {\n\t\t// We will start rendering from there\n\t\ts.renderIndex = s.cursor\n\t\treturn\n\t}\n\n\tcurEndIndex := s.lastRenderedIndex(s.renderIndex)\n\n\t// Case II : new cursor also comes in range of rendered directories\n\t// Taking this case later avoid extra lastRenderedIndex() call\n\tif s.renderIndex <= s.cursor && s.cursor <= curEndIndex {\n\t\t// no need to update s.renderIndex\n\t\treturn\n\t}\n\n\t// Case III : New cursor is too below\n\tif curEndIndex < s.cursor {\n\t\ts.renderIndex = s.firstRenderedIndex(s.cursor)\n\t\treturn\n\t}\n\n\t// Code should never reach here\n\tslog.Error(\"Unexpected situation in updateRenderIndex\", \"cursor\", s.cursor,\n\t\t\"renderIndex\", s.renderIndex, \"directory count\", len(s.directories))\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/navigation_test.go",
    "content": "package sidebar\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc Test_lastRenderIndex(t *testing.T) {\n\t// Setup test data\n\tsidebarA := defaultTestModel(0, 0, 0, 10, 10, 10)\n\tsidebarB := defaultTestModel(0, 0, 0, 1, 0, 5)\n\n\ttestCases := []struct {\n\t\tname              string\n\t\tsidebar           Model\n\t\tmainPanelHeight   int\n\t\tstartIndex        int\n\t\texpectedLastIndex int\n\t\texplanation       string\n\t}{\n\t\t{\n\t\t\tname:              \"Small viewport with home directories\",\n\t\t\tsidebar:           sidebarA,\n\t\t\tmainPanelHeight:   10,\n\t\t\tstartIndex:        0,\n\t\t\texpectedLastIndex: 6,\n\t\t\texplanation:       \"3(initialHeight) + 7 (0-6 home dirs)\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Medium viewport showing home and some pinned\",\n\t\t\tsidebar:           sidebarA,\n\t\t\tmainPanelHeight:   20,\n\t\t\tstartIndex:        0,\n\t\t\texpectedLastIndex: 14,\n\t\t\texplanation:       \"3(initialHeight) + 10 (0-9 home dirs) + 3 (10-pinned divider) + 4 (11-14 pinned dirs)\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Medium viewport starting from pinned dirs\",\n\t\t\tsidebar:           sidebarA,\n\t\t\tmainPanelHeight:   20,\n\t\t\tstartIndex:        11,\n\t\t\texpectedLastIndex: 25,\n\t\t\texplanation:       \"3(initialHeight) + 10 (11-20 pinned dirs) + 3 (21-disk divider) + 4 (22-25 disk dirs)\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Large viewport showing all directories\",\n\t\t\tsidebar:           sidebarA,\n\t\t\tmainPanelHeight:   100,\n\t\t\tstartIndex:        11,\n\t\t\texpectedLastIndex: 31,\n\t\t\texplanation:       \"Last dir index is 31\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Start index beyond directory count\",\n\t\t\tsidebar:           sidebarA,\n\t\t\tmainPanelHeight:   100,\n\t\t\tstartIndex:        32,\n\t\t\texpectedLastIndex: 31,\n\t\t\texplanation:       \"When startIndex > len(directories), return last valid index\",\n\t\t},\n\t\t{\n\t\t\tname:              \"Asymmetric directory distribution\",\n\t\t\tsidebar:           sidebarB,\n\t\t\tmainPanelHeight:   12,\n\t\t\tstartIndex:        0,\n\t\t\texpectedLastIndex: 4,\n\t\t\texplanation:       \"3(initialHeight) + 1 (0-homedir) + 3(1-pinned divider) + 3 (2-diskdivider) + 2 (3-4 diskdirs)\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.sidebar.SetHeight(tt.mainPanelHeight + common.BorderPadding)\n\n\t\t\tresult := tt.sidebar.lastRenderedIndex(tt.startIndex)\n\t\t\tassert.Equal(t, tt.expectedLastIndex, result,\n\t\t\t\t\"lastRenderedIndex failed: %s\", tt.explanation)\n\t\t})\n\t}\n}\n\nfunc Test_firstRenderIndex(t *testing.T) {\n\tsidebarA := defaultTestModel(0, 0, 0, 10, 10, 10)\n\tsidebarB := defaultTestModel(0, 0, 0, 1, 0, 5)\n\tsidebarC := defaultTestModel(0, 0, 0, 0, 5, 5)\n\tsidebarD := defaultTestModel(0, 0, 0, 0, 0, 3)\n\n\t// Empty sidebar with only dividers\n\tsidebarE := defaultTestModel(0, 0, 0, 0, 0, 0)\n\n\ttestCases := []struct {\n\t\tname               string\n\t\tsidebar            Model\n\t\tmainPanelHeight    int\n\t\tendIndex           int\n\t\texpectedFirstIndex int\n\t\texplanation        string\n\t}{\n\t\t{\n\t\t\tname:               \"Basic calculation from end index\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    10,\n\t\t\tendIndex:           10,\n\t\t\texpectedFirstIndex: 6,\n\t\t\texplanation:        \"3(InitialHeight) + 4 (6-9 homedirs) + 3 (10-pinned divider)\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Small panel height\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    5,\n\t\t\tendIndex:           15,\n\t\t\texpectedFirstIndex: 14,\n\t\t\texplanation:        \"3(InitialHeight) + 2(14-15 pinned dirs)\",\n\t\t},\n\t\t{\n\t\t\tname:               \"End index near beginning\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    20,\n\t\t\tendIndex:           3,\n\t\t\texpectedFirstIndex: 0,\n\t\t\texplanation:        \"When end index is near beginning, first index should be 0\",\n\t\t},\n\t\t{\n\t\t\tname:               \"End index at disk divider\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    15,\n\t\t\tendIndex:           21, // Disk divider in sidebar_a\n\t\t\texpectedFirstIndex: 12,\n\t\t\texplanation:        \"3(InitialHeight) + 9(12-20 pinned dirs) + 3(21-disk divider)\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Very large panel height showing all items\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    100,\n\t\t\tendIndex:           31, // Last disk dir in sidebar_a\n\t\t\texpectedFirstIndex: 0,\n\t\t\texplanation:        \"Large panel should show all directories from start\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Asymetric sidebar with few directories\",\n\t\t\tsidebar:            sidebarB,\n\t\t\tmainPanelHeight:    12,\n\t\t\tendIndex:           4, // Last disk dir in sidebar_b\n\t\t\texpectedFirstIndex: 0,\n\t\t\texplanation:        \"Small sidebar should fit in panel height\",\n\t\t},\n\t\t{\n\t\t\tname:               \"No home directories case\",\n\t\t\tsidebar:            sidebarC,\n\t\t\tmainPanelHeight:    10,\n\t\t\tendIndex:           6, // Disk dir in sidebar_c\n\t\t\texpectedFirstIndex: 2, // Pinned divider\n\t\t\texplanation:        \"3(InitialHeight) + 4(2-5 pinned dirs) + 3(6-disk divider)\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Only disk directories case\",\n\t\t\tsidebar:            sidebarD,\n\t\t\tmainPanelHeight:    8,\n\t\t\tendIndex:           4, // Last disk dir\n\t\t\texpectedFirstIndex: 2, // Disk divider\n\t\t\texplanation:        \"3(InitialHeight) + 3(2-4 disk dirs)\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Empty sidebar case\",\n\t\t\tsidebar:            sidebarE,\n\t\t\tmainPanelHeight:    10,\n\t\t\tendIndex:           1, // Disk divider\n\t\t\texpectedFirstIndex: 0, // Pinned divider\n\t\t\texplanation:        \"Empty sidebar should show all dividers\",\n\t\t},\n\t\t{\n\t\t\tname:               \"End index at the start\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    5,\n\t\t\tendIndex:           0,\n\t\t\texpectedFirstIndex: 0,\n\t\t\texplanation:        \"When end index is at start, first index should be the same\",\n\t\t},\n\t\t{\n\t\t\tname:               \"End index out of bounds\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    20,\n\t\t\tendIndex:           32, // Out of bounds for sidebar_a\n\t\t\texpectedFirstIndex: 33, // endIndex + 1\n\t\t\texplanation:        \"When end index is out of bounds, should return endIndex+1\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Very small panel height\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    2, // Too small to fit anything\n\t\t\tendIndex:           10,\n\t\t\texpectedFirstIndex: 11,\n\t\t\texplanation:        \"With panel height less than initialHeight, first index is invalid\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Panel height exactly matches divider\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    6,  // Just enough for initialHeight + divider\n\t\t\tendIndex:           10, // Pinned divider\n\t\t\texpectedFirstIndex: 10,\n\t\t\texplanation:        \"When panel height only fits the divider, start index should be the same\",\n\t\t},\n\t\t{\n\t\t\tname:               \"Boundary case between directory types\",\n\t\t\tsidebar:            sidebarA,\n\t\t\tmainPanelHeight:    7,\n\t\t\tendIndex:           11, // First pinned dir\n\t\t\texpectedFirstIndex: 10, // Pinned divider\n\t\t\texplanation:        \"3(InitialHeight) + 3(10-pinned divider) + 1(11-pinned dir)\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.sidebar.SetHeight(tt.mainPanelHeight + common.BorderPadding)\n\t\t\tresult := tt.sidebar.firstRenderedIndex(tt.endIndex)\n\t\t\tassert.Equal(t, tt.expectedFirstIndex, result,\n\t\t\t\t\"firstRenderedIndex failed: %s\", tt.explanation)\n\t\t})\n\t}\n}\n\nfunc Test_updateRenderIndex(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                string\n\t\tsidebar             Model\n\t\tmainPanelHeight     int\n\t\tinitialRenderIndex  int\n\t\tinitialCursor       int\n\t\texpectedRenderIndex int\n\t\texplanation         string\n\t}{\n\t\t{\n\t\t\tname:                \"Case I: Cursor moved above render range\",\n\t\t\tsidebar:             defaultTestModel(5, 10, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     15,\n\t\t\texpectedRenderIndex: 5,\n\t\t\texplanation:         \"When cursor moves above render range, renderIndex should be set to cursor\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Case II: Cursor within render range\",\n\t\t\tsidebar:             defaultTestModel(8, 5, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     15,\n\t\t\texpectedRenderIndex: 5, // No change expected\n\t\t\texplanation:         \"When cursor is within render range, renderIndex should not change\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Case III: Cursor moved below render range\",\n\t\t\tsidebar:             defaultTestModel(20, 0, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedRenderIndex: 14, // Should adjust to make cursor visible\n\t\t\t// 3(Initial height) + 7(14-20 pinned dirs)\n\t\t\texplanation: \"When cursor moves below render range, renderIndex should adjust to make cursor visible\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Edge case: Small panel with cursor at end\",\n\t\t\tsidebar:             defaultTestModel(31, 0, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     5,\n\t\t\texpectedRenderIndex: 30, // Should show only the last couple items\n\t\t\texplanation:         \"With small panel and cursor at end, should adjust renderIndex to show cursor\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Edge case: Large panel showing everything\",\n\t\t\tsidebar:             defaultTestModel(4, 2, 0, 1, 0, 5),\n\t\t\tmainPanelHeight:     50, // Large enough to show all directories\n\t\t\texpectedRenderIndex: 2,  // No change needed as everything is visible\n\t\t\texplanation:         \"With large panel showing all items, renderIndex should remain unchanged\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Edge case: Empty sidebar\",\n\t\t\tsidebar:             defaultTestModel(1, 0, 0, 0, 0, 0),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedRenderIndex: 0, // No change needed for empty sidebar\n\t\t\texplanation:         \"With empty sidebar, renderIndex should remain at 0\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Case I and III overlap: Cursor exactly at current renderIndex\",\n\t\t\tsidebar:             defaultTestModel(15, 15, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedRenderIndex: 15, // No change needed, Case I takes precedence\n\t\t\texplanation: \"When cursor is exactly at renderIndex, \" +\n\t\t\t\t\"Case I takes precedence and renderIndex remains unchanged\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Boundary case: Cursor at edge of visible range\",\n\t\t\tsidebar:             defaultTestModel(9, 5, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     8,\n\t\t\texpectedRenderIndex: 5, // Still visible, no change needed\n\t\t\texplanation:         \"When cursor is at the edge of visible range, renderIndex should not change\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Boundary case: Cursor just beyond visible range\",\n\t\t\tsidebar:             defaultTestModel(11, 5, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedRenderIndex: 7, // Adjust to make cursor visible\n\t\t\texplanation:         \"When cursor is just beyond visible range, renderIndex should adjust\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a copy of the sidebar to avoid modifying the original\n\t\t\tsidebar := tt.sidebar\n\t\t\tsidebar.SetHeight(tt.mainPanelHeight + common.BorderPadding)\n\n\t\t\t// Update render index\n\t\t\tsidebar.updateRenderIndex()\n\n\t\t\t// Check the result\n\t\t\tassert.Equal(t, tt.expectedRenderIndex, sidebar.renderIndex,\n\t\t\t\t\"updateRenderIndex failed: %s\", tt.explanation)\n\t\t})\n\t}\n}\n\nfunc Test_listUp(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                string\n\t\tsidebar             Model\n\t\tmainPanelHeight     int\n\t\texpectedCursor      int\n\t\texpectedRenderIndex int\n\t\texplanation         string\n\t}{\n\t\t{\n\t\t\tname:                \"Basic cursor movement from middle position\",\n\t\t\tsidebar:             defaultTestModel(5, 5, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     15,\n\t\t\texpectedCursor:      4, // Should move up one position\n\t\t\texpectedRenderIndex: 4, // Render index should follow cursor\n\t\t\texplanation:         \"When cursor is in the middle, it should move up one position\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Skip divider when moving up\",\n\t\t\tsidebar:             defaultTestModel(11, 8, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      9, // Should skip divider (10) and move to home dir (9)\n\t\t\texpectedRenderIndex: 8,\n\t\t\texplanation:         \"When moving up to a divider, cursor should skip it and move to previous item\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Wrap around from top to bottom\",\n\t\t\tsidebar:             defaultTestModel(0, 0, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      31, // Should wrap to last directory (index 31)\n\t\t\texpectedRenderIndex: 25, // Should adjust render to show cursor\n\t\t\t// 3(Initial Height) + 7(25-31 disk dirs)\n\t\t\texplanation: \"When at the top, cursor should wrap to the bottom\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Skip multiple consecutive dividers\",\n\t\t\tsidebar:             defaultTestModel(7, 5, 0, 5, 0, 5),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      4, // Should skip all dividers and move to item before dividers\n\t\t\texpectedRenderIndex: 4, // Should adjust render index accordingly\n\t\t\texplanation:         \"When encountering multiple consecutive dividers, cursor should skip all of them\",\n\t\t},\n\t\t{\n\t\t\tname:                \"No actual directories case\",\n\t\t\tsidebar:             defaultTestModel(0, 0, 0, 0, 0, 0),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      0, // Should remain unchanged\n\t\t\texpectedRenderIndex: 0, // Should remain unchanged\n\t\t\texplanation:         \"When there are no actual directories, cursor should not move\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Large panel showing all directories\",\n\t\t\tsidebar:             defaultTestModel(3, 0, 0, 2, 2, 2),\n\t\t\tmainPanelHeight:     50, // Large enough to show all directories\n\t\t\texpectedCursor:      1,  // Should move up one position\n\t\t\texpectedRenderIndex: 0,  // No change needed as everything is visible\n\t\t\texplanation:         \"With large panel showing all items, cursor should move up and renderIndex remain unchanged\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a copy of the sidebar to avoid modifying the original\n\t\t\tsidebar := tt.sidebar\n\t\t\tsidebar.SetHeight(tt.mainPanelHeight + common.BorderPadding)\n\n\t\t\t// Call the function to test\n\t\t\tsidebar.ListUp()\n\n\t\t\t// Check the results\n\t\t\tassert.Equal(t, tt.expectedCursor, sidebar.cursor,\n\t\t\t\t\"listUp cursor position: %s\", tt.explanation)\n\t\t\tassert.Equal(t, tt.expectedRenderIndex, sidebar.renderIndex,\n\t\t\t\t\"listUp render index: %s\", tt.explanation)\n\t\t})\n\t}\n}\n\nfunc Test_listDown(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                string\n\t\tsidebar             Model\n\t\tmainPanelHeight     int\n\t\texpectedCursor      int\n\t\texpectedRenderIndex int\n\t\texplanation         string\n\t}{\n\t\t{\n\t\t\tname:                \"Basic cursor movement from middle position\",\n\t\t\tsidebar:             defaultTestModel(5, 5, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     15,\n\t\t\texpectedCursor:      6, // Should move down one position\n\t\t\texpectedRenderIndex: 5, // Render index should remain the same as cursor is still visible\n\t\t\texplanation:         \"When cursor is in the middle, it should move down one position\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Skip divider when moving down\",\n\t\t\tsidebar:             defaultTestModel(9, 8, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      11, // Should skip divider (10) and move to pinned dir (11)\n\t\t\texpectedRenderIndex: 8,  // Should adjust render index to keep cursor visible\n\t\t\texplanation:         \"When moving down to a divider, cursor should skip it and move to next item\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Wrap around from bottom to top\",\n\t\t\tsidebar:             defaultTestModel(31, 26, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      0, // Should wrap to first directory (index 0)\n\t\t\texpectedRenderIndex: 0, // Should adjust render to show cursor\n\t\t\texplanation:         \"When at the bottom, cursor should wrap to the top\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Skip multiple consecutive dividers\",\n\t\t\tsidebar:             defaultTestModel(4, 0, 0, 5, 0, 5),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      7, // Should skip all dividers and move to item after dividers\n\t\t\texpectedRenderIndex: 5, // Should adjust render index accordingly\n\t\t\t// 3 (Initial Height) 6(5,6 - pinned and disk divider), 1 (7-Disk dir)\n\t\t\texplanation: \"When encountering multiple consecutive dividers, cursor should skip all of them\",\n\t\t},\n\t\t{\n\t\t\tname:                \"No actual directories case\",\n\t\t\tsidebar:             defaultTestModel(0, 0, 0, 0, 0, 0),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      0, // Should remain unchanged\n\t\t\texpectedRenderIndex: 0, // Should remain unchanged\n\t\t\texplanation:         \"When there are no actual directories, cursor should not move\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Move down from home to pinned section\",\n\t\t\tsidebar:             defaultTestModel(9, 6, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     10,\n\t\t\texpectedCursor:      11, // Should move to first pinned directory\n\t\t\texpectedRenderIndex: 7,  // Should adjust render index to show cursor\n\t\t\texplanation: \"When moving down from last home directory,\" +\n\t\t\t\t\" cursor should skip divider and go to first pinned directory\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Large panel showing all directories\",\n\t\t\tsidebar:             defaultTestModel(3, 0, 0, 2, 2, 2),\n\t\t\tmainPanelHeight:     50, // Large enough to show all directories\n\t\t\texpectedCursor:      4,  // Should move down one position\n\t\t\texpectedRenderIndex: 0,  // No change needed as everything is visible\n\t\t\texplanation:         \"With large panel showing all items, cursor should move down and renderIndex remain unchanged\",\n\t\t},\n\t\t{\n\t\t\tname:                \"Cursor at the end of visible range\",\n\t\t\tsidebar:             defaultTestModel(14, 5, 0, 10, 10, 10),\n\t\t\tmainPanelHeight:     15,\n\t\t\texpectedCursor:      15, // Should move down one position\n\t\t\texpectedRenderIndex: 6,  // Should increase render index to keep cursor visible\n\t\t\texplanation:         \"When cursor is at the end of visible range, moving down should adjust renderIndex\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a copy of the sidebar to avoid modifying the original\n\t\t\tsidebar := tt.sidebar\n\t\t\tsidebar.SetHeight(tt.mainPanelHeight + common.BorderPadding)\n\t\t\t// Call the function to test\n\t\t\tsidebar.ListDown()\n\n\t\t\t// Check the results\n\t\t\tassert.Equal(t, tt.expectedCursor, sidebar.cursor,\n\t\t\t\t\"listDown cursor position: %s\", tt.explanation)\n\t\t\tassert.Equal(t, tt.expectedRenderIndex, sidebar.renderIndex,\n\t\t\t\t\"listDown render index: %s\", tt.explanation)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/pinned.go",
    "content": "package sidebar\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\ntype PinnedManager struct {\n\tfilePath string\n}\n\nfunc NewPinnedFileManager(filePath string) PinnedManager {\n\tif err := utils.InitJSONFile(filePath); err != nil {\n\t\tslog.Error(\"Error initializing pinned JSON file\", \"error\", err)\n\t}\n\n\treturn PinnedManager{\n\t\tfilePath: filePath,\n\t}\n}\n\n// Load reads the pinned directories from file and cleans non-existing ones\nfunc (mgr *PinnedManager) Load() []directory {\n\tdirectories := []directory{}\n\n\tjsonData, err := os.ReadFile(mgr.filePath)\n\tif err != nil {\n\t\tslog.Error(\"Error reading pinned directories file\", \"error\", err)\n\t\treturn directories\n\t}\n\n\t// Check for the old format has been dropped in this manager\n\tif err := json.Unmarshal(jsonData, &directories); err != nil {\n\t\tslog.Error(\"Error parsing pinned directories data\", \"error\", err)\n\t\treturn directories\n\t}\n\n\t// Clean non-existing directories\n\tcleanedDirs := mgr.Clean(directories)\n\n\treturn cleanedDirs\n}\n\n// Save marshals and writes the pinned directories to file.\nfunc (mgr *PinnedManager) Save(dirs []directory) error {\n\tdata, err := json.Marshal(dirs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshaling pinned directories: %w\", err)\n\t}\n\n\tif err := os.WriteFile(mgr.filePath, data, utils.ConfigFilePerm); err != nil {\n\t\treturn fmt.Errorf(\"error writing pinned directories file: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Toggle adds or removes a directory from the pinned directories list\nfunc (mgr *PinnedManager) Toggle(dir string) error {\n\tdirs := mgr.Load()\n\tunPinned := false\n\n\tfor i, other := range dirs {\n\t\tif other.Location == dir {\n\t\t\tdirs = append(dirs[:i], dirs[i+1:]...)\n\t\t\tunPinned = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !unPinned {\n\t\tdirs = append(dirs, directory{\n\t\t\tLocation: dir,\n\t\t\tName:     filepath.Base(dir),\n\t\t})\n\t}\n\n\tif err := mgr.Save(dirs); err != nil {\n\t\treturn fmt.Errorf(\"error saving pinned directories: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Clean removes non-existing directories and optionally saves the updated list\nfunc (mgr *PinnedManager) Clean(dirs []directory) []directory {\n\tcleanedDirs := make([]directory, 0, len(dirs))\n\tfor _, dir := range dirs {\n\t\tif _, err := os.Stat(dir.Location); err == nil {\n\t\t\tcleanedDirs = append(cleanedDirs, dir)\n\t\t} else if !os.IsNotExist(err) {\n\t\t\tslog.Warn(\"error while checking pinned directory\", \"directory\", dir.Location, \"error\", err)\n\t\t}\n\t}\n\n\tif len(cleanedDirs) == len(dirs) {\n\t\treturn cleanedDirs\n\t}\n\n\tif err := mgr.Save(cleanedDirs); err != nil {\n\t\tslog.Error(\"error saving pinned directories\", \"error\", err)\n\t}\n\n\treturn cleanedDirs\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/pinned_test.go",
    "content": "package sidebar\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\nfunc Test_Load(t *testing.T) {\n\ttempDir := t.TempDir()\n\tpDirName := \"pinnedDir\"\n\tpinnedDir := filepath.Join(tempDir, pDirName)\n\tutils.SetupDirectories(t, pinnedDir)\n\n\temptyBytes, err := json.Marshal([]directory{})\n\trequire.NoError(t, err)\n\temptyPath := filepath.Join(pinnedDir, \"empty.json\")\n\tutils.SetupFilesWithData(t, emptyBytes, emptyPath)\n\n\tinvalidPath := filepath.Join(pinnedDir, \"invalid.json\")\n\tutils.SetupFilesWithData(t, []byte(\"{ invalid json }\"), invalidPath)\n\n\tvalidData := []directory{\n\t\t{\n\t\t\tLocation: pinnedDir,\n\t\t\tName:     pDirName,\n\t\t},\n\t}\n\tvalidBytes, err := json.Marshal(validData)\n\trequire.NoError(t, err)\n\tvalidPath := filepath.Join(pinnedDir, \"valid.json\")\n\tutils.SetupFilesWithData(t, validBytes, validPath)\n\n\tnonexistData := []directory{\n\t\t{\n\t\t\tLocation: pinnedDir,\n\t\t\tName:     pDirName,\n\t\t},\n\t\t{\n\t\t\tLocation: filepath.Join(pinnedDir, \"nonexistent9\"),\n\t\t\tName:     \"nonexistent9\",\n\t\t},\n\t}\n\tnonexistBytes, err := json.Marshal(nonexistData)\n\trequire.NoError(t, err)\n\tnonexistentPath := filepath.Join(pinnedDir, \"nonexistent.json\")\n\tutils.SetupFilesWithData(t, nonexistBytes, nonexistentPath)\n\n\tcleanDirs := []directory{\n\t\t{\n\t\t\tLocation: pinnedDir,\n\t\t\tName:     pDirName,\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tname      string\n\t\tpinnedMgr PinnedManager\n\t\texpected  []directory\n\t}{\n\t\t{\n\t\t\tname:      \"Empty No Pinned Directories\",\n\t\t\tpinnedMgr: PinnedManager{filePath: emptyPath},\n\t\t\texpected:  []directory{},\n\t\t},\n\t\t{\n\t\t\tname:      \"Invalid Format File\",\n\t\t\tpinnedMgr: PinnedManager{filePath: invalidPath},\n\t\t\texpected:  []directory{},\n\t\t},\n\t\t{\n\t\t\tname:      \"Valid With No Non-Existent Directories\",\n\t\t\tpinnedMgr: PinnedManager{filePath: validPath},\n\t\t\texpected:  cleanDirs,\n\t\t},\n\t\t{\n\t\t\tname:      \"Valid With One Non-Existent Directory\",\n\t\t\tpinnedMgr: PinnedManager{filePath: nonexistentPath},\n\t\t\texpected:  cleanDirs,\n\t\t},\n\t\t{\n\t\t\tname:      \"Invalid filePath\",\n\t\t\tpinnedMgr: PinnedManager{filePath: filepath.Join(pinnedDir, \"pinned_not_exists.json\")},\n\t\t\texpected:  []directory{},\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, tt.pinnedMgr.Load())\n\t\t})\n\t}\n}\n\nfunc Test_Save(t *testing.T) {\n\ttempDir := t.TempDir()\n\tpDirName := \"pinnedDir\"\n\tpinnedDir := filepath.Join(tempDir, pDirName)\n\tutils.SetupDirectories(t, pinnedDir)\n\n\tsavePath := filepath.Join(pinnedDir, \"pinned.json\")\n\n\tdirs := []directory{\n\t\t{\n\t\t\tLocation: pinnedDir,\n\t\t\tName:     pDirName,\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tname      string\n\t\tpinnedMgr PinnedManager\n\t\tnoError   bool\n\t\texpected  []directory\n\t\targDirs   []directory\n\t}{\n\t\t{\n\t\t\tname:      \"Valid Normal Case\",\n\t\t\tpinnedMgr: PinnedManager{filePath: savePath},\n\t\t\tnoError:   true,\n\t\t\texpected:  dirs,\n\t\t\targDirs:   dirs,\n\t\t},\n\t\t{\n\t\t\tname:      \"Empty Slice\",\n\t\t\tpinnedMgr: PinnedManager{filePath: savePath},\n\t\t\tnoError:   true,\n\t\t\texpected:  []directory{},\n\t\t\targDirs:   []directory{},\n\t\t},\n\t\t{\n\t\t\tname:      \"Write Failure\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedDir},\n\t\t\tnoError:   false,\n\t\t\texpected:  nil,\n\t\t\targDirs:   dirs,\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.pinnedMgr.Save(tt.argDirs)\n\t\t\tif tt.noError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, tt.pinnedMgr.Load())\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Toggle(t *testing.T) {\n\ttempDir := t.TempDir()\n\tpDirName := \"pinnedDir\"\n\tpinnedDir := filepath.Join(tempDir, pDirName)\n\tutils.SetupDirectories(t, pinnedDir)\n\n\tpinnedFile := filepath.Join(pinnedDir, \"pinned.json\")\n\n\ttestCases := []struct {\n\t\tname      string\n\t\tpinnedMgr PinnedManager\n\t\texpected  []directory\n\t\tnoError   bool\n\t\targDir    string\n\t}{\n\t\t{\n\t\t\tname:      \"Add Non-Existing Directory to Pinned\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedFile},\n\t\t\texpected:  []directory{},\n\t\t\tnoError:   true,\n\t\t\targDir:    filepath.Join(tempDir, \"nonExistentDir\"),\n\t\t},\n\t\t{\n\t\t\tname:      \"Add a Directory to Pinned\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedFile},\n\t\t\texpected: []directory{\n\t\t\t\t{\n\t\t\t\t\tLocation: pinnedDir,\n\t\t\t\t\tName:     pDirName,\n\t\t\t\t},\n\t\t\t},\n\t\t\tnoError: true,\n\t\t\targDir:  pinnedDir,\n\t\t},\n\t\t{\n\t\t\tname:      \"Remove a Directory from Pinned\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedFile},\n\t\t\texpected:  []directory{},\n\t\t\tnoError:   true,\n\t\t\targDir:    pinnedDir,\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.pinnedMgr.Toggle(tt.argDir)\n\t\t\tif tt.noError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, tt.pinnedMgr.Load())\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_Clean(t *testing.T) {\n\ttempDir := t.TempDir()\n\tpDirName := \"pinnedDir\"\n\tpinnedDir := filepath.Join(tempDir, pDirName)\n\tutils.SetupDirectories(t, pinnedDir)\n\n\tpinnedFile := filepath.Join(pinnedDir, \"pinned.json\")\n\n\tcleanDirs := []directory{\n\t\t{\n\t\t\tLocation: pinnedDir,\n\t\t\tName:     pDirName,\n\t\t},\n\t}\n\tbadDirs := append([]directory{}, cleanDirs...)\n\tbadDirs = append(badDirs, directory{\n\t\tLocation: filepath.Join(tempDir, \"nonexistentDir\"),\n\t\tName:     \"nonexistentDir\",\n\t})\n\n\ttestCases := []struct {\n\t\tname      string\n\t\tpinnedMgr PinnedManager\n\t\tmodified  bool\n\t\texpected  []directory\n\t\targDirs   []directory\n\t}{\n\t\t{\n\t\t\tname:      \"All Directories Exist\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedFile},\n\t\t\tmodified:  false,\n\t\t\texpected:  cleanDirs,\n\t\t\targDirs:   cleanDirs,\n\t\t},\n\t\t{\n\t\t\tname:      \"Some Directories Exist\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedFile},\n\t\t\tmodified:  true,\n\t\t\texpected:  cleanDirs,\n\t\t\targDirs:   badDirs,\n\t\t},\n\t\t{\n\t\t\tname:      \"Save Fails\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedDir},\n\t\t\tmodified:  false,\n\t\t\texpected:  cleanDirs,\n\t\t\targDirs:   badDirs,\n\t\t},\n\t\t{\n\t\t\tname:      \"Empty Input Slice\",\n\t\t\tpinnedMgr: PinnedManager{filePath: pinnedFile},\n\t\t\tmodified:  false,\n\t\t\texpected:  []directory{},\n\t\t\targDirs:   []directory{},\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsubjectPath := tt.pinnedMgr.filePath\n\t\t\tif subjectPath == pinnedFile {\n\t\t\t\t_ = tt.pinnedMgr.Save(tt.argDirs)\n\t\t\t}\n\t\t\tbeforeInfo, beforeErr := os.Stat(subjectPath)\n\n\t\t\tcleaned := tt.pinnedMgr.Clean(tt.argDirs)\n\n\t\t\tafterInfo, afterErr := os.Stat(subjectPath)\n\t\t\tif beforeErr == nil && afterErr == nil && !tt.modified {\n\t\t\t\trequire.Equal(t, beforeInfo.ModTime(), afterInfo.ModTime())\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expected, cleaned)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/render.go",
    "content": "package sidebar\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/rendering\"\n)\n\n// Render returns the rendered sidebar string.\nfunc (s *Model) Render(sidebarFocused bool, currentFilePanelLocation string) string {\n\tif s.Disabled() {\n\t\treturn \"\"\n\t}\n\n\tr := ui.SidebarRenderer(s.height, s.width, sidebarFocused)\n\n\tr.AddLines(common.SideBarSuperfileTitle, \"\")\n\n\tif s.searchBar.Focused() || s.searchBar.Value() != \"\" || sidebarFocused {\n\t\tr.AddLines(s.searchBar.View())\n\t}\n\n\tif s.NoActualDir() {\n\t\tr.AddLines(common.SideBarNoneText)\n\t} else {\n\t\ts.directoriesRender(currentFilePanelLocation, sidebarFocused, r)\n\t}\n\treturn r.Render()\n}\n\n// directoriesRender handles the iterative rendering of directories within the sidebar model.\nfunc (s *Model) directoriesRender(curFilePanelFileLocation string,\n\tsideBarFocused bool, r *rendering.Renderer) {\n\t// Cursor should always point to a valid directory at this point\n\tif s.isCursorInvalid() {\n\t\tslog.Error(\"Unexpected situation in sideBar Model. \"+\n\t\t\t\"Cursor is at invalid position, while there are valid directories\", \"cursor\", s.cursor,\n\t\t\t\"directory count\", len(s.directories))\n\t}\n\n\t// TODO : This is not true when searchbar is not rendered(totalHeight is 2, not 3),\n\t// so we end up underutilizing one line for our render. But it wont break anything.\n\ttotalHeight := sideBarInitialHeight\n\tmainPanelHeight := s.height - common.BorderPadding\n\tfor i := s.renderIndex; i < len(s.directories); i++ {\n\t\tif totalHeight+s.directories[i].requiredHeight() > mainPanelHeight {\n\t\t\tbreak\n\t\t}\n\n\t\ttotalHeight += s.directories[i].requiredHeight()\n\n\t\tswitch s.directories[i] {\n\t\tcase homeDividerDir:\n\t\t\tr.AddLines(\"\", common.SideBarHomeDivider, \"\")\n\t\tcase pinnedDividerDir:\n\t\t\tr.AddLines(\"\", common.SideBarPinnedDivider, \"\")\n\t\tcase diskDividerDir:\n\t\t\tr.AddLines(\"\", common.SideBarDisksDivider, \"\")\n\t\tdefault:\n\t\t\tcursor := \" \"\n\t\t\tif s.cursor == i && sideBarFocused && !s.searchBar.Focused() {\n\t\t\t\tcursor = icon.Cursor\n\t\t\t}\n\t\t\tif s.renaming && s.cursor == i {\n\t\t\t\tr.AddLines(s.rename.View())\n\t\t\t} else {\n\t\t\t\trenderStyle := common.SidebarStyle\n\t\t\t\tif s.directories[i].Location == curFilePanelFileLocation {\n\t\t\t\t\trenderStyle = common.SidebarSelectedStyle\n\t\t\t\t}\n\t\t\t\tline := common.FilePanelCursorStyle.Render(cursor+\" \") + renderStyle.Render(s.directories[i].Name)\n\t\t\t\tr.AddLineWithCustomTruncate(line, rendering.TailsTruncateRight)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/sidebar.go",
    "content": "package sidebar\n\nimport (\n\t\"log/slog\"\n\t\"slices\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\tvariable \"github.com/yorukot/superfile/src/config\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// PinnedItemRename initiates the rename process for the currently selected pinned directory.\nfunc (s *Model) PinnedItemRename() {\n\tpinnedBegin, pinnedEnd := s.pinnedIndexRange()\n\t// We have not selected a pinned directory, rename is not allowed\n\tif s.cursor < pinnedBegin || s.cursor > pinnedEnd {\n\t\treturn\n\t}\n\n\tnameLen := len(s.directories[s.cursor].Name)\n\tcursorPos := nameLen\n\n\ts.renaming = true\n\ts.rename = common.GeneratePinnedRenameTextInput(cursorPos, s.directories[s.cursor].Name)\n}\n\n// CancelSidebarRename aborts the rename process for a pinned directory.\nfunc (s *Model) CancelSidebarRename() {\n\ts.rename.Blur()\n\ts.renaming = false\n}\n\n// ConfirmSidebarRename finalizes the rename process and saves changes to the pinned directories file.\nfunc (s *Model) ConfirmSidebarRename() {\n\titemLocation := s.directories[s.cursor].Location\n\tnewItemName := s.rename.Value()\n\t// This is needed to update the current pinned directory data loaded into memory\n\ts.directories[s.cursor].Name = newItemName\n\n\t// recover the state of rename\n\ts.CancelSidebarRename()\n\n\tpinnedDirs := s.pinnedMgr.Load()\n\t// Call getPinnedDirectories, instead of using what is stored in sidebar.directories\n\t// sidebar.directories could have less directories in case a search filter is used\n\tfor i := range pinnedDirs {\n\t\t// Considering the situation when many\n\t\tif pinnedDirs[i].Location == itemLocation {\n\t\t\tpinnedDirs[i].Name = newItemName\n\t\t}\n\t}\n\n\tif err := s.pinnedMgr.Save(pinnedDirs); err != nil {\n\t\tslog.Error(\"error saving pinned directories\", \"error\", err)\n\t}\n}\n\n// UpdateState handles the sidebar's state updates in response to Bubble Tea messages.\nfunc (s *Model) UpdateState(msg tea.Msg) tea.Cmd {\n\tvar cmd tea.Cmd\n\tif s.renaming {\n\t\ts.rename, cmd = s.rename.Update(msg)\n\t} else if s.searchBar.Focused() {\n\t\ts.searchBar, cmd = s.searchBar.Update(msg)\n\t}\n\n\tif s.cursor < 0 {\n\t\ts.cursor = 0\n\t}\n\treturn cmd\n}\n\n// HandleSearchBarKey processes key events specifically for the sidebar's search bar.\nfunc (s *Model) HandleSearchBarKey(msg string) {\n\tswitch {\n\tcase slices.Contains(common.Hotkeys.CancelTyping, msg):\n\t\ts.SearchBarBlur()\n\t\ts.searchBar.SetValue(\"\")\n\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg):\n\t\ts.SearchBarBlur()\n\t\ts.resetCursor()\n\t}\n}\n\n// UpdateDirectories refreshes the list of directories based on the search query or section configuration.\nfunc (s *Model) UpdateDirectories() {\n\tif s.Disabled() {\n\t\treturn\n\t}\n\tif s.searchBar.Value() != \"\" {\n\t\ts.directories = getFilteredDirectories(s.searchBar.Value(), s.pinnedMgr, s.sections)\n\t} else {\n\t\ts.directories = getDirectories(s.pinnedMgr, s.sections)\n\t}\n\t// This is needed, as due to filtering, the cursor might be invalid\n\tif s.isCursorInvalid() {\n\t\ts.resetCursor()\n\t}\n}\n\n// TogglePinnedDirectory adds or removes a directory from the pinned list.\nfunc (s *Model) TogglePinnedDirectory(dir string) error {\n\treturn s.pinnedMgr.Toggle(dir)\n}\n\n// New initializes and returns a new Model for the sidebar correctly set up with configuration.\nfunc New() Model {\n\tif common.Config.SidebarWidth == 0 {\n\t\treturn Model{\n\t\t\tdisabled: true,\n\t\t}\n\t}\n\t// pinnedMgr is created here, can be done higher up in the call chain\n\tpinnedMgr := NewPinnedFileManager(variable.PinnedFile)\n\ts := Model{\n\t\trenderIndex: 0,\n\t\tsearchBar:   common.GenerateSearchBar(),\n\t\tpinnedMgr:   &pinnedMgr,\n\t\twidth:       common.Config.SidebarWidth + common.BorderPadding,\n\t\theight:      minHeight,\n\t\tdisabled:    false,\n\t\tsections:    common.Config.SidebarSections,\n\t}\n\n\ts.directories = getDirectories(&pinnedMgr, s.sections)\n\ts.searchBar.Width = s.width - common.BorderPadding - searchBarPadding\n\ts.searchBar.Placeholder = \"(\" + common.Hotkeys.SearchBar[0] + \")\" + \" Search\"\n\treturn s\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/type.go",
    "content": "package sidebar\n\nimport \"github.com/charmbracelet/bubbles/textinput\"\n\ntype directory struct {\n\tLocation string `json:\"location\"`\n\tName     string `json:\"name\"`\n}\n\ntype Model struct {\n\tdirectories []directory\n\trenderIndex int\n\tcursor      int\n\trename      textinput.Model\n\trenaming    bool\n\tsearchBar   textinput.Model\n\tpinnedMgr   *PinnedManager\n\twidth       int\n\theight      int\n\tdisabled    bool\n\tsections    []string\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/utils.go",
    "content": "package sidebar\n\nimport \"log/slog\"\n\n// isDivider returns true if the directory is one of the section dividers.\nfunc (d directory) isDivider() bool {\n\treturn d == homeDividerDir || d == pinnedDividerDir || d == diskDividerDir\n}\n\n// requiredHeight returns the number of terminal lines required to render this item.\nfunc (d directory) requiredHeight() int {\n\tif d.isDivider() {\n\t\treturn dividerDirHeight\n\t}\n\treturn 1\n}\n\n// NoActualDir returns true if the sidebar contains only dividers and no actual directories.\nfunc (s *Model) NoActualDir() bool {\n\tfor _, d := range s.directories {\n\t\tif !d.isDivider() {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// isCursorInvalid returns true if the current cursor position is out of bounds or points to a divider.\nfunc (s *Model) isCursorInvalid() bool {\n\treturn s.cursor < 0 || s.cursor >= len(s.directories) || s.directories[s.cursor].isDivider()\n}\n\n// resetCursor moves the cursor to the first selectable directory in the sidebar.\nfunc (s *Model) resetCursor() {\n\ts.cursor = 0\n\t// Move to first non Divider dir\n\tfor i, d := range s.directories {\n\t\tif !d.isDivider() {\n\t\t\ts.cursor = i\n\t\t\treturn\n\t\t}\n\t}\n\t// If all directories are divider, code will reach here. and s.cursor will stay 0\n\t// Or s.directories is empty\n}\n\n// SearchBarFocused returns whether the search bar is focused\nfunc (s *Model) SearchBarFocused() bool {\n\treturn s.searchBar.Focused()\n}\n\n// SearchBarBlur removes focus from the search bar\nfunc (s *Model) SearchBarBlur() {\n\ts.searchBar.Blur()\n}\n\n// SearchBarFocus sets focus on the search bar\nfunc (s *Model) SearchBarFocus() {\n\ts.searchBar.Focus()\n}\n\n// IsRenaming returns whether the sidebar is currently in renaming mode\nfunc (s *Model) IsRenaming() bool {\n\treturn s.renaming\n}\n\n// GetCurrentDirectoryLocation returns the location of the currently selected directory\nfunc (s *Model) GetCurrentDirectoryLocation() string {\n\tif s.isCursorInvalid() || s.NoActualDir() {\n\t\treturn \"\"\n\t}\n\treturn s.directories[s.cursor].Location\n}\n\n// pinnedIndexRange calculates the start and end indices of the pinned directories section.\n// Returns (-1, -1) if the section is missing or empty.\nfunc (s *Model) pinnedIndexRange() (int, int) {\n\t// pinned directories start after well-known directories and the divider\n\t// Can't use getPinnedDirectories() here, as if we are in search mode, we would be showing\n\t// and having less directories in sideBar.directories slice\n\n\t// TODO : This is inefficient to iterate each time for this.\n\t// This information can be kept precomputed\n\tpinnedDividerIdx := -1\n\tfor i, d := range s.directories {\n\t\tif d == pinnedDividerDir {\n\t\t\tpinnedDividerIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif pinnedDividerIdx == -1 {\n\t\treturn -1, -1\n\t}\n\n\tpinnedEndIdx := len(s.directories) - 1\n\tfor i := pinnedDividerIdx + 1; i < len(s.directories); i++ {\n\t\tif s.directories[i].isDivider() {\n\t\t\tpinnedEndIdx = i - 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif pinnedDividerIdx+1 > pinnedEndIdx {\n\t\treturn -1, -1\n\t}\n\n\treturn pinnedDividerIdx + 1, pinnedEndIdx\n}\n\n// GetWidth returns the current width of the sidebar.\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n\n// GetHeight returns the current height of the sidebar.\nfunc (m *Model) GetHeight() int {\n\treturn m.height\n}\n\n// SetHeight updates the height of the sidebar, ensuring it meets the minimum requirement.\nfunc (m *Model) SetHeight(height int) {\n\tif height < minHeight {\n\t\tslog.Error(\"Attempted to set too low height to sidebar\", \"height\", height)\n\t\treturn\n\t}\n\tm.height = height\n}\n\n// Disabled returns true if the sidebar is currently disabled.\nfunc (m *Model) Disabled() bool {\n\treturn m.disabled\n}\n"
  },
  {
    "path": "src/internal/ui/sidebar/utils_test.go",
    "content": "package sidebar\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\nfunc defaultTestModel(cursor int, renderIndex int, height int,\n\tcntHome int, cntPinned int, cntDisk int) Model {\n\treturn testModel(cursor, renderIndex, height, defaultSectionSlice,\n\t\tformDirctorySlice(dirSlice(cntHome), dirSlice(cntPinned), dirSlice(cntDisk), defaultSectionSlice))\n}\n\nfunc testModel(cursor int, renderIndex int, height int, sections []string,\n\tdirectories []directory) Model {\n\treturn Model{\n\t\tdirectories: directories,\n\t\tcursor:      cursor,\n\t\trenderIndex: renderIndex,\n\t\theight:      height,\n\t\tsections:    sections,\n\t}\n}\n\nfunc dirSlice(count int) []directory {\n\tres := make([]directory, count)\n\tfor i := range count {\n\t\tres[i] = directory{Name: \"Dir\" + strconv.Itoa(i), Location: \"/a/\" + strconv.Itoa(i)}\n\t}\n\treturn res\n}\n\nfunc Test_noActualDir(t *testing.T) {\n\ttestcases := []struct {\n\t\tname     string\n\t\tsidebar  Model\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"Empty invalid sidebar should have no actual directories\",\n\t\t\tModel{},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"Empty sidebar should have no actual directories\",\n\t\t\tdefaultTestModel(0, 0, 10, 0, 0, 0),\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"Non-Empty Sidebar with only pinned directories\",\n\t\t\tdefaultTestModel(0, 0, 10, 0, 10, 0),\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"Non-Empty Sidebar with all directories\",\n\t\t\tdefaultTestModel(0, 0, 10, 10, 10, 10),\n\t\t\tfalse,\n\t\t},\n\t}\n\tfor _, tt := range testcases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, tt.sidebar.NoActualDir())\n\t\t})\n\t}\n}\n\nfunc Test_isCursorInvalid(t *testing.T) {\n\ttestcases := []struct {\n\t\tname     string\n\t\tsidebar  Model\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"Empty invalid sidebar\",\n\t\t\tModel{},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"Cursor after all directories\",\n\t\t\tdefaultTestModel(32, 0, 10, 10, 10, 10),\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"Curson points to pinned divider\",\n\t\t\tdefaultTestModel(10, 0, 10, 10, 10, 10),\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"Non-Empty Sidebar with all directories\",\n\t\t\tdefaultTestModel(5, 0, 10, 10, 10, 10),\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range testcases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expected, tt.sidebar.isCursorInvalid())\n\t\t})\n\t}\n}\n\nfunc Test_resetCursor(t *testing.T) {\n\tdata := []struct {\n\t\tname              string\n\t\tcurSideBar        Model\n\t\texpectedCursorPos int\n\t}{\n\t\t{\n\t\t\tname:              \"Only Pinned directories\",\n\t\t\tcurSideBar:        defaultTestModel(0, 0, 10, 0, 10, 0),\n\t\t\texpectedCursorPos: 1, // After pinned divider\n\t\t},\n\t\t{\n\t\t\tname:              \"All kind of directories\",\n\t\t\tcurSideBar:        defaultTestModel(0, 0, 10, 10, 10, 10),\n\t\t\texpectedCursorPos: 0, // First home\n\t\t},\n\t\t{\n\t\t\tname:              \"Only Disk\",\n\t\t\tcurSideBar:        defaultTestModel(0, 0, 10, 0, 0, 10),\n\t\t\texpectedCursorPos: 2, // After pinned and dist divider\n\t\t},\n\t\t{\n\t\t\tname:              \"Empty Sidebar\",\n\t\t\tcurSideBar:        defaultTestModel(0, 0, 10, 0, 0, 0),\n\t\t\texpectedCursorPos: 0, // Empty sidebar, cursor should reset to 0\n\t\t},\n\t}\n\n\tfor _, tt := range data {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttt.curSideBar.resetCursor()\n\t\t\tassert.Equal(t, tt.expectedCursorPos, tt.curSideBar.cursor)\n\t\t})\n\t}\n}\n\nfunc TestSidebarSectionsVisibility(t *testing.T) {\n\ttestcases := []struct {\n\t\tname          string\n\t\tsections      []string\n\t\thomeDirs      int\n\t\tpinnedDirs    int\n\t\tdiskDirs      int\n\t\texpectedLen   int\n\t\texpectHomeDiv bool\n\t}{\n\t\t{\n\t\t\tname:        \"Only one section (pinned)\",\n\t\t\tsections:    []string{utils.SidebarSectionPinned},\n\t\t\tpinnedDirs:  5,\n\t\t\texpectedLen: 6, // divider + 5 dirs\n\t\t},\n\t\t{\n\t\t\tname:        \"No sections\",\n\t\t\tsections:    []string{},\n\t\t\texpectedLen: 0,\n\t\t},\n\t\t{\n\t\t\tname:          \"Reordered sections (pinned, home)\",\n\t\t\tsections:      []string{utils.SidebarSectionPinned, utils.SidebarSectionHome},\n\t\t\thomeDirs:      3,\n\t\t\tpinnedDirs:    3,\n\t\t\texpectedLen:   1 + 3 + 1 + 3, // pinned divider + 3 pinned + home divider + 3 home\n\t\t\texpectHomeDiv: true,\n\t\t},\n\t}\n\n\tfor _, tt := range testcases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdirs := formDirctorySlice(\n\t\t\t\tdirSlice(tt.homeDirs),\n\t\t\t\tdirSlice(tt.pinnedDirs),\n\t\t\t\tdirSlice(tt.diskDirs),\n\t\t\t\ttt.sections,\n\t\t\t)\n\t\t\tassert.Len(t, dirs, tt.expectedLen)\n\t\t\tif tt.expectHomeDiv {\n\t\t\t\tfoundHomeDiv := false\n\t\t\t\tfor _, d := range dirs {\n\t\t\t\t\tif d == homeDividerDir {\n\t\t\t\t\t\tfoundHomeDiv = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tassert.True(t, foundHomeDiv, \"Expected home divider to be present\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/sortmodel/const.go",
    "content": "package sortmodel\n\nconst (\n\tsortOptionsDefaultWidth  = 20\n\tsortOptionsDefaultHeight = 4\n\tSortTypeCount            = 4\n)\n"
  },
  {
    "path": "src/internal/ui/sortmodel/model.go",
    "content": "package sortmodel\n\nfunc New() Model {\n\treturn Model{\n\t\tHeight: sortOptionsDefaultHeight,\n\t\tWidth:  sortOptionsDefaultWidth,\n\t\tCursor: 0,\n\t\topen:   false,\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/sortmodel/navigation.go",
    "content": "package sortmodel\n\nfunc (m *Model) ListUp() {\n\tm.Cursor = (m.Cursor - 1 + SortTypeCount) % SortTypeCount\n}\n\nfunc (m *Model) ListDown() {\n\tm.Cursor = (m.Cursor + 1 + SortTypeCount) % SortTypeCount\n}\n"
  },
  {
    "path": "src/internal/ui/sortmodel/render.go",
    "content": "package sortmodel\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc (m *Model) Render() string {\n\tvar sortOptionsContent strings.Builder\n\tsortOptionsContent.WriteString(common.ModalTitleStyle.Render(\" Sort Options\") + \"\\n\\n\")\n\tfor i, option := range SortOptionsStr {\n\t\tcursor := \" \"\n\t\tif i == m.Cursor {\n\t\t\tcursor = common.FilePanelCursorStyle.Render(icon.Cursor)\n\t\t}\n\t\tsortOptionsContent.WriteString(cursor + common.ModalStyle.Render(\" \"+option) + \"\\n\")\n\t}\n\tbottomBorder := common.GenerateFooterBorder(\n\t\tfmt.Sprintf(\"%s/%s\", strconv.Itoa(m.Cursor+1),\n\t\t\tstrconv.Itoa(len(SortOptionsStr))), m.Width-common.BorderPadding)\n\n\treturn common.SortOptionsModalBorderStyle(m.Height, m.Width,\n\t\tbottomBorder).Render(sortOptionsContent.String())\n}\n"
  },
  {
    "path": "src/internal/ui/sortmodel/types.go",
    "content": "package sortmodel\n\ntype SortKind int\n\n// NOTE: Update the validation of DefaultSortType config if you make changes here\nconst (\n\tSortByName SortKind = iota\n\tSortBySize\n\tSortByDate\n\tSortByType\n\tSortByNatural\n)\n\nvar SortOptionsStr = []string{ //nolint: gochecknoglobals // Effectively const\n\t\"Name\", \"Size\", \"Date Modified\", \"Type\", \"Natural\",\n}\n\nvar SortOptionsShortStr = []string{ //nolint: gochecknoglobals // Effectively const\n\t\"Name\", \"Size\", \"Date\", \"Type\", \"Natural\",\n}\n\n// Sort options\ntype Model struct {\n\tWidth  int\n\tHeight int\n\topen   bool\n\n\t// Cursor has meaning only during open state, its lost on close\n\tCursor int\n}\n"
  },
  {
    "path": "src/internal/ui/sortmodel/utils.go",
    "content": "package sortmodel\n\nfunc (m *Model) IsOpen() bool {\n\treturn m.open\n}\n\nfunc (m *Model) Open(curSortKind SortKind) {\n\tm.Cursor = int(curSortKind)\n\tm.open = true\n}\n\nfunc (m *Model) Close() {\n\tm.open = false\n\tm.Cursor = 0\n}\n\nfunc (m *Model) GetSelectedKind() SortKind {\n\treturn SortKind(m.Cursor)\n}\n"
  },
  {
    "path": "src/internal/ui/spf_renderers.go",
    "content": "package ui\n\nimport (\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui/rendering\"\n)\n\nfunc SidebarRenderer(totalHeight int, totalWidth int, sidebarFocused bool) *rendering.Renderer {\n\tcfg := rendering.DefaultRendererConfig(totalHeight, totalWidth)\n\n\tcfg.ContentFGColor = common.SidebarFGColor\n\tcfg.ContentBGColor = common.SidebarBGColor\n\n\tcfg.BorderRequired = true\n\tcfg.BorderBGColor = common.SidebarBGColor\n\tcfg.BorderFGColor = common.SidebarBorderColor\n\tif sidebarFocused {\n\t\tcfg.BorderFGColor = common.SidebarBorderActiveColor\n\t}\n\tcfg.Border = DefaultLipglossBorder()\n\tcfg.RendererName += \"-sidebar\"\n\n\tr := rendering.NewRendererWithAutoFixConfig(cfg)\n\n\treturn r\n}\n\nfunc FilePanelRenderer(totalHeight int, totalWidth int, filePanelFocused bool) *rendering.Renderer {\n\tcfg := rendering.DefaultRendererConfig(totalHeight, totalWidth)\n\n\tcfg.ContentFGColor = common.FilePanelFGColor\n\tcfg.ContentBGColor = common.FilePanelBGColor\n\n\tcfg.BorderRequired = true\n\tcfg.BorderBGColor = common.FilePanelBGColor\n\tcfg.BorderFGColor = common.FilePanelBorderColor\n\tif filePanelFocused {\n\t\tcfg.BorderFGColor = common.FilePanelBorderActiveColor\n\t}\n\tcfg.Border = DefaultLipglossBorder()\n\tcfg.RendererName += \"-filepanel\"\n\n\tr := rendering.NewRendererWithAutoFixConfig(cfg)\n\treturn r\n}\n\nfunc FilePreviewPanelRenderer(totalHeight int, totalWidth int) *rendering.Renderer {\n\tcfg := rendering.DefaultRendererConfig(totalHeight, totalWidth)\n\tcfg.ContentFGColor = common.FilePanelFGColor\n\tcfg.ContentBGColor = common.FilePanelBGColor\n\n\tif common.Config.EnableFilePreviewBorder {\n\t\tcfg.BorderRequired = true\n\t\tcfg.BorderBGColor = common.FilePanelBGColor\n\t\tcfg.BorderFGColor = common.FilePanelBorderColor\n\t\tcfg.Border = DefaultLipglossBorder()\n\t}\n\tcfg.RendererName += \"-preview\"\n\n\tr := rendering.NewRendererWithAutoFixConfig(cfg)\n\treturn r\n}\n\nfunc PromptRenderer(totalHeight int, totalWidth int) *rendering.Renderer {\n\tcfg := rendering.DefaultRendererConfig(totalHeight, totalWidth)\n\tcfg.TruncateHeight = true\n\tcfg.ContentFGColor = common.ModalFGColor\n\tcfg.ContentBGColor = common.ModalBGColor\n\n\tcfg.BorderRequired = true\n\tcfg.BorderBGColor = common.ModalBGColor\n\tcfg.BorderFGColor = common.ModalBorderActiveColor\n\n\tcfg.Border = DefaultLipglossBorder()\n\n\tr := rendering.NewRendererWithAutoFixConfig(cfg)\n\treturn r\n}\n\nfunc ZoxideRenderer(totalHeight int, totalWidth int) *rendering.Renderer {\n\treturn PromptRenderer(totalHeight, totalWidth)\n}\n\nfunc HelpMenuRenderer(totalHeight int, totalWidth int) *rendering.Renderer {\n\tcfg := rendering.DefaultRendererConfig(totalHeight, totalWidth)\n\tcfg.ContentFGColor = common.ModalFGColor\n\tcfg.ContentBGColor = common.ModalBGColor\n\n\tcfg.BorderRequired = true\n\tcfg.BorderBGColor = common.ModalBGColor\n\tcfg.BorderFGColor = common.ModalBorderActiveColor\n\n\tcfg.Border = DefaultLipglossBorder()\n\n\tr := rendering.NewRendererWithAutoFixConfig(cfg)\n\treturn r\n}\n\nfunc DefaultFooterRenderer(totalHeight int, totalWidth int, focused bool, name string) *rendering.Renderer {\n\tcfg := rendering.DefaultRendererConfig(totalHeight, totalWidth)\n\n\tcfg.ContentFGColor = common.FooterFGColor\n\tcfg.ContentBGColor = common.FooterBGColor\n\n\tcfg.BorderRequired = true\n\tcfg.BorderBGColor = common.FooterBGColor\n\tcfg.BorderFGColor = common.FooterBorderColor\n\tif focused {\n\t\tcfg.BorderFGColor = common.FooterBorderActiveColor\n\t}\n\tcfg.Border = DefaultLipglossBorder()\n\tcfg.RendererName = name\n\n\tr := rendering.NewRendererWithAutoFixConfig(cfg)\n\n\tr.SetBorderTitle(name)\n\treturn r\n}\n\nfunc ProcessBarRenderer(totalHeight int, totalWidth int, processBarFocused bool) *rendering.Renderer {\n\treturn DefaultFooterRenderer(totalHeight, totalWidth, processBarFocused, \"Processes\")\n}\n\nfunc MetadataRenderer(totalHeight int, totalWidth int, metadataFocused bool) *rendering.Renderer {\n\treturn DefaultFooterRenderer(totalHeight, totalWidth, metadataFocused, \"Metadata\")\n}\n\nfunc ClipboardRenderer(totalHeight int, totalWidth int) *rendering.Renderer {\n\treturn DefaultFooterRenderer(totalHeight, totalWidth, false, \"Clipboard\")\n}\n\nfunc DefaultLipglossBorder() lipgloss.Border {\n\treturn lipgloss.Border{\n\t\tTop:         common.Config.BorderTop,\n\t\tBottom:      common.Config.BorderBottom,\n\t\tLeft:        common.Config.BorderLeft,\n\t\tRight:       common.Config.BorderRight,\n\t\tTopLeft:     common.Config.BorderTopLeft,\n\t\tTopRight:    common.Config.BorderTopRight,\n\t\tBottomLeft:  common.Config.BorderBottomLeft,\n\t\tBottomRight: common.Config.BorderBottomRight,\n\t\tMiddleLeft:  common.Config.BorderMiddleLeft,\n\t\tMiddleRight: common.Config.BorderMiddleRight,\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/README.md",
    "content": "# zoxide package\nThis is for the Zoxide navigation modal of superfile\n\nHandles user input for zoxide queries, integrates with the go-zoxide library, and returns navigation actions to the model.\n\n## Features\n\n- Interactive zoxide directory search and navigation\n- Real-time suggestions with scores from zoxide database  \n- Keyboard navigation with standard superfile hotkeys\n- Integration with existing file panel navigation system\n\n## Usage\n\nThe zoxide modal is opened by pressing the `z` hotkey and allows users to:\n1. Type directory names to search zoxide's database\n2. See top 5 matching directories with relevance scores\n3. Navigate to selected directory in the current file panel\n4. Close the modal with Escape or successful navigation\n\n## Architecture\n\n- `Model`: Main zoxide modal state and behavior\n- `HandleUpdate()`: Processes keyboard input and zoxide queries\n- `Render()`: Displays search interface and suggestions\n- Integration with `*zoxidelib.Client` for zoxide database queries\n\n## Coverage\n\nCurrent test coverage: **0%**\n\nNo tests have been implemented yet for this package. The package is functional and integrated, but lacks unit test coverage.\n\n```bash\ncd /path/to/ui/zoxide\n# Basic coverage (when tests exist)\ngo test -cover\n\n# HTML report (when tests exist)\ngo test -coverprofile=coverage.out && go tool cover -html=coverage.out -o coverage.html\n```"
  },
  {
    "path": "src/internal/ui/zoxide/consts.go",
    "content": "package zoxide\n\nconst (\n\tzoxideHeadlineText = \"Zoxide Navigation\"\n\n\tZoxideMinWidth  = 15\n\tZoxideMinHeight = 3\n\n\tmaxVisibleResults = 5 // Maximum number of results visible at once\n\n\t// UI dimension constants for zoxide modal\n\t// scoreColumnWidth is width reserved for score display (including padding and separator)\n\tscoreColumnWidth = 13 // borders(2) + padding(2) + score(6) + separator(3)\n\n\t// modalInputPadding is total padding for modal input fields\n\tmodalInputPadding = 6 // 2 + 1 + 2 + 1 (borders and spacing)\n)\n"
  },
  {
    "path": "src/internal/ui/zoxide/model.go",
    "content": "package zoxide\n\nimport (\n\t\"log/slog\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\n\t\"github.com/yorukot/superfile/src/config/icon\"\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc DefaultModel(maxHeight int, width int, zClient *zoxidelib.Client) Model {\n\treturn GenerateModel(zClient, maxHeight, width)\n}\n\nfunc GenerateModel(zClient *zoxidelib.Client, maxHeight int, width int) Model {\n\tm := Model{\n\t\theadline:  icon.Search + icon.Space + zoxideHeadlineText,\n\t\topen:      false,\n\t\ttextInput: common.GeneratePromptTextInput(),\n\t\tzClient:   zClient,\n\t\tresults:   []zoxidelib.Result{},\n\t}\n\tm.SetMaxHeight(maxHeight)\n\tm.SetWidth(width)\n\tm.textInput.Prompt = \"\"\n\treturn m\n}\n\nfunc (m *Model) HandleUpdate(msg tea.Msg) (common.ModelAction, tea.Cmd) {\n\tslog.Debug(\"zoxide.Model HandleUpdate()\", \"msg\", msg,\n\t\t\"msgType\", reflect.TypeOf(msg),\n\t\t\"textInput\", m.textInput.Value(),\n\t\t\"cursorBlink\", m.textInput.Cursor.Blink)\n\tvar action common.ModelAction\n\taction = common.NoAction{}\n\tvar cmd tea.Cmd\n\tif !m.IsOpen() {\n\t\tslog.Error(\"HandleUpdate called on closed zoxide\")\n\t\treturn action, cmd\n\t}\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\t// If zoxide is not available, only allow confirm/cancel to close modal\n\t\tif m.zClient == nil {\n\t\t\tswitch {\n\t\t\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg.String()),\n\t\t\t\tslices.Contains(common.Hotkeys.CancelTyping, msg.String()),\n\t\t\t\tslices.Contains(common.Hotkeys.Quit, msg.String()):\n\t\t\t\tm.Close()\n\t\t\t}\n\t\t\treturn action, cmd\n\t\t}\n\n\t\tswitch {\n\t\tcase slices.Contains(common.Hotkeys.ConfirmTyping, msg.String()):\n\t\t\taction = m.handleConfirm()\n\t\t\tm.Close()\n\t\tcase slices.Contains(common.Hotkeys.CancelTyping, msg.String()):\n\t\t\tm.Close()\n\t\t// We dont want keys like `j` and `k` to get stuck here\n\t\t// So if its a navigation key, lets specifically ignore\n\t\t// the alphanumeric keys as zoxide panel is in text input\n\t\t// mode by default\n\t\tcase slices.Contains(common.Hotkeys.ListUp, msg.String()) && !isKeyAlphaNum(msg):\n\t\t\tm.navigateUp()\n\t\tcase slices.Contains(common.Hotkeys.ListDown, msg.String()) && !isKeyAlphaNum(msg):\n\t\t\tm.navigateDown()\n\t\tcase slices.Contains(common.Hotkeys.OpenZoxide, msg.String()) && m.justOpened:\n\t\t\t// Ignore the 'z' key that just opened this modal to prevent it from appearing in text input\n\t\t\tm.justOpened = false\n\t\tdefault:\n\t\t\tcmd = m.handleNormalKeyInput(msg)\n\t\t}\n\tdefault:\n\t\t// Non keypress updates like Cursor Blink\n\t\t// Only update text input if zoxide is available\n\t\tif m.zClient != nil {\n\t\t\tm.textInput, cmd = m.textInput.Update(msg)\n\t\t}\n\t}\n\treturn action, cmd\n}\n\nfunc (m *Model) handleConfirm() common.ModelAction {\n\t// If we have results and a valid selection, navigate to selected result\n\tif len(m.results) > 0 && m.cursor >= 0 && m.cursor < len(m.results) {\n\t\tselectedResult := m.results[m.cursor]\n\t\treturn common.CDCurrentPanelAction{\n\t\t\tLocation: selectedResult.Path,\n\t\t}\n\t}\n\n\t// No results or invalid selection - close modal\n\treturn common.NoAction{}\n}\n\nfunc (m *Model) handleNormalKeyInput(msg tea.KeyMsg) tea.Cmd {\n\tvar cmd tea.Cmd\n\tm.textInput, cmd = m.textInput.Update(msg)\n\treturn tea.Batch(cmd, m.GetQueryCmd(m.textInput.Value()))\n}\n\nfunc (m *Model) GetQueryCmd(query string) tea.Cmd {\n\tif m.zClient == nil || !common.Config.ZoxideSupport {\n\t\treturn nil\n\t}\n\n\treqID := m.reqCnt\n\tm.reqCnt++\n\n\tslog.Debug(\"Submitting zoxide query request\", \"query\", query, \"id\", reqID)\n\n\treturn func() tea.Msg {\n\t\tqueryFields := strings.Fields(query)\n\t\tresults, err := m.zClient.QueryAll(queryFields...)\n\t\tif err != nil {\n\t\t\tslog.Debug(\"Zoxide query failed\", \"query\", query, \"error\", err, \"id\", reqID)\n\t\t\treturn NewUpdateMsg(query, []zoxidelib.Result{}, reqID)\n\t\t}\n\t\treturn NewUpdateMsg(query, results, reqID)\n\t}\n}\n\n// Apply updates the zoxide modal with query results\nfunc (msg UpdateMsg) Apply(m *Model) tea.Cmd {\n\t// Ignore stale results - only apply if query matches current input\n\tcurrentQuery := m.textInput.Value()\n\tif msg.query != currentQuery {\n\t\tslog.Debug(\"Ignoring stale zoxide query result\",\n\t\t\t\"msgQuery\", msg.query,\n\t\t\t\"currentQuery\", currentQuery,\n\t\t\t\"id\", msg.reqID)\n\t\treturn nil\n\t}\n\n\tm.results = msg.results\n\tm.cursor = 0\n\tm.renderIndex = 0\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/model_test.go",
    "content": "package zoxide\n\nimport (\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc TestMain(m *testing.M) {\n\toriginalZoxideSupport := common.Config.ZoxideSupport\n\tcommon.Config.ZoxideSupport = true\n\tdefer func() {\n\t\tcommon.Config.ZoxideSupport = originalZoxideSupport\n\t}()\n\tm.Run()\n}\n\nfunc TestHandleConfirmWithValidSelection(t *testing.T) {\n\tm := setupTestModelWithResults(3)\n\tm.cursor = 1\n\n\taction := m.handleConfirm()\n\n\tcdAction, ok := action.(common.CDCurrentPanelAction)\n\trequire.True(t, ok, \"action should be CDCurrentPanelAction\")\n\tassert.Equal(t, m.results[1].Path, cdAction.Location, \"action should navigate to results[1].Path\")\n}\n\nfunc TestHandleConfirmWithNoResults(t *testing.T) {\n\tm := setupTestModel()\n\n\taction := m.handleConfirm()\n\n\t_, ok := action.(common.NoAction)\n\tassert.True(t, ok, \"action should be NoAction when there are no results\")\n}\n\nfunc TestHandleConfirmWithInvalidCursor(t *testing.T) {\n\tm := setupTestModelWithResults(3)\n\tm.cursor = 5\n\n\taction := m.handleConfirm()\n\n\t_, ok := action.(common.NoAction)\n\tassert.True(t, ok, \"action should be NoAction when cursor is out of bounds\")\n}\n\nfunc TestJKKeyHandling(t *testing.T) {\n\tm := setupTestModelWithClient(t)\n\n\tm.Open()\n\n\toriginalHotkeys := common.Hotkeys.ListDown\n\tcommon.Hotkeys.ListDown = []string{\"j\", \"down\"}\n\tdefer func() {\n\t\tcommon.Hotkeys.ListDown = originalHotkeys\n\t}()\n\n\taction, cmd := m.HandleUpdate(utils.TeaRuneKeyMsg(\"j\"))\n\n\tassert.NotNil(t, cmd, \"HandleUpdate should return cmd for text input update\")\n\t_, isNoAction := action.(common.NoAction)\n\tassert.True(t, isNoAction, \"action should be NoAction for text input\")\n\tassert.Equal(t, \"j\", m.textInput.Value(), \"'j' should be added to textInput\")\n\n\taction, cmd = m.HandleUpdate(utils.TeaRuneKeyMsg(\"k\"))\n\tassert.NotNil(t, cmd, \"HandleUpdate should return cmd for text input update\")\n\t_, isNoAction = action.(common.NoAction)\n\tassert.True(t, isNoAction, \"action should be NoAction for text input\")\n\tassert.Equal(t, \"jk\", m.textInput.Value(), \"'k' should be added to textInput\")\n\n\tm.textInput.SetValue(\"\")\n\tm.results = []zoxidelib.Result{\n\t\t{Path: \"/test/path1\", Score: 100},\n\t\t{Path: \"/test/path2\", Score: 90},\n\t}\n\tm.cursor = 0\n\n\taction, cmd = m.HandleUpdate(tea.KeyMsg{Type: tea.KeyDown})\n\tassert.Nil(t, cmd, \"HandleUpdate with down arrow should not return cmd\")\n\t_, isNoAction = action.(common.NoAction)\n\tassert.True(t, isNoAction, \"action should be NoAction for navigation\")\n\tassert.Equal(t, 1, m.cursor, \"down arrow should navigate down\")\n\tassert.Empty(t, m.textInput.Value(), \"down arrow should not add to textInput\")\n}\n\nfunc TestApplyWithMatchingQuery(t *testing.T) {\n\tm := setupTestModel()\n\tm.textInput.SetValue(\"test\")\n\tm.cursor = 5\n\tm.renderIndex = 2\n\n\tresults := []zoxidelib.Result{\n\t\t{Path: \"/test/path1\", Score: 100},\n\t\t{Path: \"/test/path2\", Score: 90},\n\t\t{Path: \"/test/path3\", Score: 80},\n\t}\n\tmsg := NewUpdateMsg(\"test\", results, 1)\n\n\tcmd := msg.Apply(&m)\n\tassert.Nil(t, cmd)\n\tassert.Len(t, m.results, 3, \"results should be updated to 3 items\")\n\tassert.Equal(t, 0, m.cursor, \"cursor should be reset to 0\")\n\tassert.Equal(t, 0, m.renderIndex, \"renderIndex should be reset to 0\")\n\tassert.Equal(t, results, m.results, \"results should match the update message\")\n}\n\nfunc TestApplyWithStaleQuery(t *testing.T) {\n\tm := setupTestModel()\n\tm.textInput.SetValue(\"new\")\n\tm.cursor = 1\n\tm.renderIndex = 1\n\toriginalResults := []zoxidelib.Result{\n\t\t{Path: \"/original/path\", Score: 50},\n\t}\n\tm.results = originalResults\n\n\tstaleResults := []zoxidelib.Result{\n\t\t{Path: \"/test/path1\", Score: 100},\n\t\t{Path: \"/test/path2\", Score: 90},\n\t\t{Path: \"/test/path3\", Score: 80},\n\t}\n\tmsg := NewUpdateMsg(\"old\", staleResults, 1)\n\n\tcmd := msg.Apply(&m)\n\tassert.Nil(t, cmd)\n\tassert.Equal(t, originalResults, m.results, \"results should remain unchanged\")\n\tassert.Equal(t, 1, m.cursor, \"cursor should remain unchanged\")\n\tassert.Equal(t, 1, m.renderIndex, \"renderIndex should remain unchanged\")\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/navigation.go",
    "content": "package zoxide\n\nfunc (m *Model) navigateUp() {\n\tif len(m.results) == 0 {\n\t\treturn\n\t}\n\tif m.cursor > 0 {\n\t\tm.cursor--\n\t} else {\n\t\tm.cursor = len(m.results) - 1 // Wrap to bottom\n\t}\n\tm.updateRenderIndex()\n}\n\nfunc (m *Model) navigateDown() {\n\tif len(m.results) == 0 {\n\t\treturn\n\t}\n\tif m.cursor < len(m.results)-1 {\n\t\tm.cursor++\n\t} else {\n\t\tm.cursor = 0 // Wrap to top\n\t}\n\tm.updateRenderIndex()\n}\n\nfunc (m *Model) updateRenderIndex() {\n\tif len(m.results) == 0 {\n\t\tm.renderIndex = 0\n\t\treturn\n\t}\n\n\t// If cursor is above visible range, scroll up\n\tif m.cursor < m.renderIndex {\n\t\tm.renderIndex = m.cursor\n\t}\n\n\t// If cursor is below visible range, scroll down\n\tif m.cursor >= m.renderIndex+maxVisibleResults {\n\t\tm.renderIndex = m.cursor - maxVisibleResults + 1\n\t}\n\n\t// Ensure renderIndex is within bounds\n\tif m.renderIndex < 0 {\n\t\tm.renderIndex = 0\n\t}\n\tmaxRenderIndex := len(m.results) - maxVisibleResults\n\tif maxRenderIndex < 0 {\n\t\tmaxRenderIndex = 0\n\t}\n\tif m.renderIndex > maxRenderIndex {\n\t\tm.renderIndex = maxRenderIndex\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/navigation_test.go",
    "content": "package zoxide\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNavigation(t *testing.T) {\n\ttestdata := []struct {\n\t\tname           string\n\t\tresultCnt      int\n\t\tstartCursor    int\n\t\tnavigateUp     bool\n\t\texpectedCursor int\n\t}{\n\t\t{\n\t\t\tname:           \"navigateUp at position 0 wraps to last position\",\n\t\t\tresultCnt:      5,\n\t\t\tstartCursor:    0,\n\t\t\tnavigateUp:     true,\n\t\t\texpectedCursor: 4,\n\t\t},\n\t\t{\n\t\t\tname:           \"navigateDown at position 0 moves to next position\",\n\t\t\tresultCnt:      5,\n\t\t\tstartCursor:    0,\n\t\t\tnavigateUp:     false,\n\t\t\texpectedCursor: 1,\n\t\t},\n\t\t{\n\t\t\tname:           \"navigateDown at last position wraps to first position\",\n\t\t\tresultCnt:      5,\n\t\t\tstartCursor:    4,\n\t\t\tnavigateUp:     false,\n\t\t\texpectedCursor: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"navigateUp with empty results keeps cursor at 0\",\n\t\t\tresultCnt:      0,\n\t\t\tstartCursor:    0,\n\t\t\tnavigateUp:     true,\n\t\t\texpectedCursor: 0,\n\t\t},\n\t\t{\n\t\t\tname:           \"navigateDown with empty results keeps cursor at 0\",\n\t\t\tresultCnt:      0,\n\t\t\tstartCursor:    0,\n\t\t\tnavigateUp:     false,\n\t\t\texpectedCursor: 0,\n\t\t},\n\t}\n\n\tfor _, td := range testdata {\n\t\tt.Run(td.name, func(t *testing.T) {\n\t\t\tvar m Model\n\t\t\tif td.resultCnt == 0 {\n\t\t\t\tm = setupTestModel()\n\t\t\t} else {\n\t\t\t\tm = setupTestModelWithResults(td.resultCnt)\n\t\t\t}\n\t\t\tm.cursor = td.startCursor\n\n\t\t\tif td.navigateUp {\n\t\t\t\tm.navigateUp()\n\t\t\t} else {\n\t\t\t\tm.navigateDown()\n\t\t\t}\n\n\t\t\tassert.Equal(t, td.expectedCursor, m.cursor)\n\t\t})\n\t}\n}\n\nfunc TestUpdateRenderIndex(t *testing.T) {\n\ttestdata := []struct {\n\t\tname                string\n\t\tresultCnt           int\n\t\tcursor              int\n\t\texpectedRenderIndex int\n\t}{\n\t\t{\n\t\t\tname:                \"cursor at 0 has renderIndex 0\",\n\t\t\tresultCnt:           10,\n\t\t\tcursor:              0,\n\t\t\texpectedRenderIndex: 0,\n\t\t},\n\t\t{\n\t\t\tname:                \"cursor at 5 has renderIndex 1 (visible at bottom)\",\n\t\t\tresultCnt:           10,\n\t\t\tcursor:              5,\n\t\t\texpectedRenderIndex: 1,\n\t\t},\n\t\t{\n\t\t\tname:                \"cursor at 9 has renderIndex 5 (last page)\",\n\t\t\tresultCnt:           10,\n\t\t\tcursor:              9,\n\t\t\texpectedRenderIndex: 5,\n\t\t},\n\t\t{\n\t\t\tname:                \"cursor back at 0 scrolls back up to renderIndex 0\",\n\t\t\tresultCnt:           10,\n\t\t\tcursor:              0,\n\t\t\texpectedRenderIndex: 0,\n\t\t},\n\t\t{\n\t\t\tname:                \"renderIndex stays 0 with 3 results, cursor at 0\",\n\t\t\tresultCnt:           3,\n\t\t\tcursor:              0,\n\t\t\texpectedRenderIndex: 0,\n\t\t},\n\t\t{\n\t\t\tname:                \"renderIndex stays 0 with 3 results, cursor at 1\",\n\t\t\tresultCnt:           3,\n\t\t\tcursor:              1,\n\t\t\texpectedRenderIndex: 0,\n\t\t},\n\t\t{\n\t\t\tname:                \"renderIndex stays 0 with 3 results, cursor at 2\",\n\t\t\tresultCnt:           3,\n\t\t\tcursor:              2,\n\t\t\texpectedRenderIndex: 0,\n\t\t},\n\t}\n\n\tfor _, td := range testdata {\n\t\tt.Run(td.name, func(t *testing.T) {\n\t\t\tm := setupTestModelWithResults(td.resultCnt)\n\t\t\tm.cursor = td.cursor\n\t\t\tm.updateRenderIndex()\n\t\t\tassert.Equal(t, td.expectedRenderIndex, m.renderIndex)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/render.go",
    "content": "package zoxide\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/internal/ui\"\n\t\"github.com/yorukot/superfile/src/internal/ui/rendering\"\n)\n\nfunc (m *Model) Render() string {\n\tr := ui.ZoxideRenderer(m.maxHeight, m.width)\n\tr.SetBorderTitle(m.headline)\n\n\tif m.zClient == nil {\n\t\tr.AddSection()\n\t\tr.AddLines(\" Zoxide not available (check zoxide_support in config)\")\n\t\treturn r.Render()\n\t}\n\n\tr.AddLines(\" \" + m.textInput.View())\n\tr.AddSection()\n\tif len(m.results) > 0 {\n\t\tm.renderResultList(r)\n\t} else {\n\t\tr.AddLines(\" No zoxide results found\")\n\t}\n\treturn r.Render()\n}\n\nfunc (m *Model) renderResultList(r *rendering.Renderer) {\n\t// Calculate visible range\n\tendIndex := m.renderIndex + maxVisibleResults\n\tif endIndex > len(m.results) {\n\t\tendIndex = len(m.results)\n\t}\n\t// Show visible results\n\tm.renderVisibleResults(r, endIndex)\n\n\t// Show scroll indicators if needed\n\tm.renderScrollIndicators(r, endIndex)\n}\n\nfunc (m *Model) renderVisibleResults(r *rendering.Renderer, endIndex int) {\n\tfor i := m.renderIndex; i < endIndex; i++ {\n\t\tresult := m.results[i]\n\n\t\t// Truncate path if too long (account for score, separator, and padding)\n\t\t// Available width: modal width\n\t\t// - borders(2) - padding(2) - score(6)\n\t\t// - separator(3) = width - 13\n\t\t// 0123456789012345678 => 19 width, path gets 6\n\t\t// | 9999.9 | <path> |\n\t\tavailablePathWidth := m.width - scoreColumnWidth\n\t\tpath := common.TruncateTextBeginning(result.Path, availablePathWidth, \"...\")\n\n\t\tline := fmt.Sprintf(\" %6.1f | %s\", result.Score, path)\n\n\t\t// Highlight the selected item\n\t\tif i == m.cursor {\n\t\t\tline = common.ModalCursorStyle.Render(line)\n\t\t}\n\t\tr.AddLines(line)\n\t}\n}\n\nfunc (m *Model) renderScrollIndicators(r *rendering.Renderer, endIndex int) {\n\tif len(m.results) <= maxVisibleResults {\n\t\treturn\n\t}\n\n\tif m.renderIndex > 0 {\n\t\tr.AddSection()\n\t\tr.AddLines(\" ↑ More results above\")\n\t}\n\tif endIndex < len(m.results) {\n\t\tif m.renderIndex == 0 {\n\t\t\tr.AddSection()\n\t\t}\n\t\tr.AddLines(\" ↓ More results below\")\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/render_test.go",
    "content": "package zoxide\n\nimport (\n\t\"testing\"\n\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRenderWithNilZClient(t *testing.T) {\n\tm := setupTestModel()\n\n\toutput := m.Render()\n\n\tassert.Contains(t, output, \"Zoxide not available\", \"output should contain 'Zoxide not available'\")\n}\n\nfunc TestRenderWithEmptyResults(t *testing.T) {\n\tm := setupTestModelWithClient(t)\n\n\toutput := m.Render()\n\n\tassert.Contains(t, output, \"No zoxide results found\", \"output should contain 'No zoxide results found'\")\n}\n\nfunc TestRenderWithResults(t *testing.T) {\n\tm := setupTestModelWithClient(t)\n\tm.results = []zoxidelib.Result{\n\t\t{Path: \"/dir1\", Score: 100},\n\t\t{Path: \"/dir2\", Score: 90},\n\t\t{Path: \"/dir3\", Score: 80},\n\t}\n\n\toutput := m.Render()\n\n\tassert.Contains(t, output, \"/dir1\", \"output should contain /dir1\")\n\tassert.Contains(t, output, \"/dir2\", \"output should contain /dir2\")\n\tassert.Contains(t, output, \"/dir3\", \"output should contain /dir3\")\n\tassert.Contains(t, output, \"100.0\", \"output should contain score 100.0\")\n\tassert.Contains(t, output, \"90.0\", \"output should contain score 90.0\")\n\tassert.Contains(t, output, \"80.0\", \"output should contain score 80.0\")\n}\n\nfunc TestRenderWithTextInput(t *testing.T) {\n\tm := setupTestModelWithClient(t)\n\tm.textInput.SetValue(\"test query\")\n\n\toutput := m.Render()\n\n\tassert.Contains(t, output, \"test query\", \"output should contain text input value\")\n}\n\nfunc TestRenderScrollIndicator(t *testing.T) {\n\ttestdata := []struct {\n\t\tname       string\n\t\tresultCnt  int\n\t\tcursor     int\n\t\texpectUp   bool\n\t\texpectDown bool\n\t}{\n\t\t{\n\t\t\tname:       \"More above\",\n\t\t\tresultCnt:  10,\n\t\t\tcursor:     9,\n\t\t\texpectUp:   true,\n\t\t\texpectDown: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"More below\",\n\t\t\tresultCnt:  10,\n\t\t\tcursor:     0,\n\t\t\texpectUp:   false,\n\t\t\texpectDown: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"Both directions\",\n\t\t\tresultCnt:  10,\n\t\t\tcursor:     5,\n\t\t\texpectUp:   true,\n\t\t\texpectDown: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"No scroll needed\",\n\t\t\tresultCnt:  3,\n\t\t\tcursor:     1,\n\t\t\texpectUp:   false,\n\t\t\texpectDown: false,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := setupTestModelWithClient(t)\n\t\t\tm.results = setupTestModelWithResults(tt.resultCnt).results\n\t\t\tm.cursor = tt.cursor\n\t\t\tm.updateRenderIndex()\n\n\t\t\trendered := m.Render()\n\n\t\t\tif tt.expectUp {\n\t\t\t\tassert.Contains(t, rendered, \"↑ More results above\")\n\t\t\t} else {\n\t\t\t\tassert.NotContains(t, rendered, \"↑ More results above\")\n\t\t\t}\n\n\t\t\tif tt.expectDown {\n\t\t\t\tassert.Contains(t, rendered, \"↓ More results below\")\n\t\t\t} else {\n\t\t\t\tassert.NotContains(t, rendered, \"↓ More results below\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/test_helpers.go",
    "content": "package zoxide\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n)\n\nfunc setupTestModel() Model {\n\treturn GenerateModel(nil, 50, 80) //nolint:mnd // test dimensions\n}\n\nfunc setupTestModelWithClient(t *testing.T) Model {\n\tt.Helper()\n\tzClient, err := zoxidelib.New(zoxidelib.WithDataDir(t.TempDir()))\n\tif err != nil {\n\t\tif runtime.GOOS != utils.OsLinux {\n\t\t\tt.Skipf(\"Skipping zoxide tests in non-Linux because zoxide client cannot be initialized\")\n\t\t} else {\n\t\t\tt.Fatalf(\"zoxide initialization failed\")\n\t\t}\n\t}\n\treturn GenerateModel(zClient, 50, 80) //nolint:mnd // test dimensions\n}\n\nfunc setupTestModelWithResults(resultCount int) Model {\n\tm := setupTestModel()\n\tm.results = make([]zoxidelib.Result, resultCount)\n\tfor i := range resultCount {\n\t\tm.results[i] = zoxidelib.Result{\n\t\t\tPath:  \"/test/path\" + string(rune('0'+i)),\n\t\t\tScore: float64(100 - i*10), //nolint:mnd // test scores\n\t\t}\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/type.go",
    "content": "package zoxide\n\nimport (\n\t\"github.com/charmbracelet/bubbles/textinput\"\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n)\n\n// No need to name it as ZoxideModel. It will me imported as zoxide.Model\ntype Model struct {\n\n\t// Configuration\n\theadline string\n\tzClient  *zoxidelib.Client\n\n\t// State\n\topen        bool\n\tjustOpened  bool // Flag to ignore the opening keystroke\n\ttextInput   textinput.Model\n\tresults     []zoxidelib.Result\n\tcursor      int // Index of currently selected result for keyboard navigation\n\trenderIndex int // Index of first visible result in scrollable list\n\n\t// Dimensions - Exported, since model will be dynamically adjusting them\n\twidth int\n\t// Height is dynamically adjusted based on content\n\tmaxHeight int\n\n\t// Request tracking for async queries\n\treqCnt int\n}\n\n// UpdateMsg represents an async query result\ntype UpdateMsg struct {\n\tquery   string\n\tresults []zoxidelib.Result\n\treqID   int\n}\n\nfunc NewUpdateMsg(query string, results []zoxidelib.Result, reqID int) UpdateMsg {\n\treturn UpdateMsg{\n\t\tquery:   query,\n\t\tresults: results,\n\t\treqID:   reqID,\n\t}\n}\n\nfunc (msg UpdateMsg) GetReqID() int {\n\treturn msg.reqID\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/utils.go",
    "content": "package zoxide\n\nimport (\n\t\"log/slog\"\n\t\"unicode\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\tzoxidelib \"github.com/lazysegtree/go-zoxide\"\n)\n\nfunc (m *Model) Open() tea.Cmd {\n\tm.open = true\n\tm.justOpened = true\n\tm.textInput.SetValue(\"\")\n\t_ = m.textInput.Focus()\n\n\t// Return async command for initial query instead of blocking\n\treturn m.GetQueryCmd(\"\")\n}\n\nfunc (m *Model) Close() {\n\tm.open = false\n\tm.textInput.Blur()\n\tm.textInput.SetValue(\"\")\n\tm.results = []zoxidelib.Result{}\n\tm.cursor = 0\n\tm.renderIndex = 0\n}\n\nfunc (m *Model) IsOpen() bool {\n\treturn m.open\n}\n\nfunc (m *Model) GetWidth() int {\n\treturn m.width\n}\n\nfunc (m *Model) GetMaxHeight() int {\n\treturn m.maxHeight\n}\n\nfunc (m *Model) SetWidth(width int) {\n\tif width < ZoxideMinWidth {\n\t\tslog.Warn(\"Zoxide initialized with too less width\", \"width\", width)\n\t\twidth = ZoxideMinWidth\n\t}\n\tm.width = width\n\t// Excluding borders(2), SpacePadding(1), Prompt(2), and one extra character that is appended\n\t// by textInput.View()\n\tm.textInput.Width = width - modalInputPadding\n}\n\nfunc (m *Model) SetMaxHeight(maxHeight int) {\n\tif maxHeight < ZoxideMinHeight {\n\t\tslog.Warn(\"Zoxide initialized with too less maxHeight\", \"maxHeight\", maxHeight)\n\t\tmaxHeight = ZoxideMinHeight\n\t}\n\tm.maxHeight = maxHeight\n}\n\nfunc (m *Model) GetResults() []zoxidelib.Result {\n\tout := make([]zoxidelib.Result, len(m.results))\n\tcopy(out, m.results)\n\treturn out\n}\n\nfunc (m *Model) GetTextInputValue() string {\n\treturn m.textInput.Value()\n}\n\nfunc isKeyAlphaNum(msg tea.KeyMsg) bool {\n\tr := []rune(msg.String())\n\tif len(r) != 1 {\n\t\treturn false\n\t}\n\treturn unicode.IsLetter(r[0]) || unicode.IsNumber(r[0])\n}\n"
  },
  {
    "path": "src/internal/ui/zoxide/utils_test.go",
    "content": "package zoxide\n\nimport (\n\t\"testing\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsKeyAlphaNum(t *testing.T) {\n\tassert.True(t, isKeyAlphaNum(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'j'}}), \"'j' should be alphanumeric\")\n\tassert.True(t, isKeyAlphaNum(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}}), \"'k' should be alphanumeric\")\n\tassert.True(t, isKeyAlphaNum(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'5'}}), \"'5' should be alphanumeric\")\n\tassert.True(t, isKeyAlphaNum(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'A'}}), \"'A' should be alphanumeric\")\n\tassert.False(t, isKeyAlphaNum(tea.KeyMsg{Type: tea.KeyUp}), \"up arrow should not be alphanumeric\")\n\tassert.False(t, isKeyAlphaNum(tea.KeyMsg{Type: tea.KeyEnter}), \"enter should not be alphanumeric\")\n\tassert.False(\n\t\tt,\n\t\tisKeyAlphaNum(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}}),\n\t\t\"space should not be alphanumeric\",\n\t)\n}\n\nfunc TestOpenResetsState(t *testing.T) {\n\tm := setupTestModelWithClient(t)\n\tm.textInput.SetValue(\"old\")\n\tm.cursor = 2\n\tm.renderIndex = 1\n\n\tcmd := m.Open()\n\n\tassert.True(t, m.open, \"open should be true after Open()\")\n\tassert.True(t, m.justOpened, \"justOpened should be true after Open()\")\n\tassert.Empty(t, m.textInput.Value(), \"textInput should be empty after Open()\")\n\tassert.NotNil(t, cmd, \"Open() should return non-nil Cmd for async query\")\n}\n\nfunc TestCloseClearsState(t *testing.T) {\n\tm := setupTestModelWithResults(5)\n\tm.open = true\n\tm.cursor = 2\n\tm.renderIndex = 1\n\tm.textInput.SetValue(\"test\")\n\n\tm.Close()\n\n\tassert.False(t, m.open, \"open should be false after Close()\")\n\tassert.Empty(t, m.results, \"results should be empty after Close()\")\n\tassert.Equal(t, 0, m.cursor, \"cursor should be 0 after Close()\")\n\tassert.Equal(t, 0, m.renderIndex, \"renderIndex should be 0 after Close()\")\n\tassert.Empty(t, m.textInput.Value(), \"textInput should be empty after Close()\")\n}\n\nfunc TestGetResultsReturnsCopy(t *testing.T) {\n\tm := setupTestModelWithResults(3)\n\toriginalPath := m.results[0].Path\n\n\tresults := m.GetResults()\n\tresults[0].Path = \"/modified/path\"\n\n\tassert.Equal(\n\t\tt,\n\t\toriginalPath,\n\t\tm.results[0].Path,\n\t\t\"modifying returned results should not affect original model.results\",\n\t)\n}\n\nfunc TestSetWidthBoundsChecking(t *testing.T) {\n\tm := setupTestModel()\n\n\tm.SetWidth(5)\n\tassert.Equal(t, ZoxideMinWidth, m.width, \"width should be set to ZoxideMinWidth when value < ZoxideMinWidth\")\n\n\tm.SetWidth(100)\n\tassert.Equal(t, 100, m.width, \"width should be set to provided value when >= ZoxideMinWidth\")\n}\n\nfunc TestSetMaxHeightBoundsChecking(t *testing.T) {\n\tm := setupTestModel()\n\n\tm.SetMaxHeight(1)\n\tassert.Equal(\n\t\tt,\n\t\tZoxideMinHeight,\n\t\tm.maxHeight,\n\t\t\"maxHeight should be set to ZoxideMinHeight when value < ZoxideMinHeight\",\n\t)\n\n\tm.SetMaxHeight(50)\n\tassert.Equal(t, 50, m.maxHeight, \"maxHeight should be set to provided value when >= ZoxideMinHeight\")\n}\n"
  },
  {
    "path": "src/internal/validation.go",
    "content": "package internal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n\n\t\"github.com/yorukot/superfile/src/pkg/utils\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nconst minLinesForBorder = 3\n\n// Non fatal Validations. This indicates bug / programming errors, not user configuration mistake\nfunc (m *model) validateLayout() error { //nolint:gocognit // cumilation of validations\n\t// Validate footer height\n\tif 0 < m.footerHeight && m.footerHeight < common.MinFooterHeight {\n\t\treturn fmt.Errorf(\"footerHeight %v is too small\", m.footerHeight)\n\t}\n\tif !m.toggleFooter && m.footerHeight != 0 {\n\t\treturn fmt.Errorf(\"footer closed and footerHeight %v is non zero\", m.footerHeight)\n\t}\n\tif m.toggleFooter && m.footerHeight == 0 {\n\t\treturn errors.New(\"footer open but footerHeight is 0\")\n\t}\n\n\t// PanelHeight + 2 lines (main border) + actual footer height\n\tif m.fullHeight != (m.mainPanelHeight+common.BorderPadding)+utils.FullFooterHeight(m.footerHeight, m.toggleFooter) {\n\t\treturn fmt.Errorf(\n\t\t\t\"invalid model layout, total height doesn't sum correctly, fullHeight : %v, mainPanelHeight : %v, footerHeight : %v\",\n\t\t\tm.fullHeight,\n\t\t\tm.mainPanelHeight,\n\t\t\tm.footerHeight,\n\t\t)\n\t}\n\n\t// Validate width constraints\n\tif m.fullWidth < common.MinimumWidth {\n\t\treturn fmt.Errorf(\"fullWidth %v is below minimum %v\", m.fullWidth, common.MinimumWidth)\n\t}\n\n\t// Check that file panel width is positive if we have file panels\n\tif m.fileModel.PanelCount() == 0 {\n\t\treturn errors.New(\"file model is empty\")\n\t}\n\n\t// Check total width calculation consistency\n\tif m.fullWidth != m.sidebarModel.GetWidth()+m.fileModel.Width {\n\t\treturn fmt.Errorf(\n\t\t\t\"width layout inconsistent: fullWidth=%v, sidebar=%v filemodel=%v\",\n\t\t\tm.fullWidth, m.sidebarModel.GetWidth(), m.fileModel.Width)\n\t}\n\n\t// Check file panels count\n\tif m.fileModel.PanelCount() > m.fileModel.MaxFilePanel {\n\t\treturn fmt.Errorf(\n\t\t\t\"too many file panels: %v exceeds maximum %v\",\n\t\t\tm.fileModel.PanelCount(), m.fileModel.MaxFilePanel)\n\t}\n\n\ttotalFileModelWidth := 0\n\t// Check preview panel dimensions if open\n\tif m.fileModel.FilePreview.IsOpen() {\n\t\tif m.fileModel.ExpectedPreviewWidth <= 0 {\n\t\t\treturn fmt.Errorf(\"preview panel is open but width is %v\", m.fileModel.ExpectedPreviewWidth)\n\t\t}\n\t\tif m.fileModel.Height <= 0 {\n\t\t\treturn fmt.Errorf(\"preview panel is open but height is %v\", m.fileModel.Height)\n\t\t}\n\t\ttotalFileModelWidth += m.fileModel.ExpectedPreviewWidth\n\t}\n\n\t// Check each file panel has correct dimensions set\n\tfor i, panel := range m.fileModel.FilePanels {\n\t\ttotalFileModelWidth += panel.GetWidth()\n\t\tif panel.GetHeight() != m.fileModel.Height {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"file panel %v height mismatch: expected %v, got %v\",\n\t\t\t\ti, m.fileModel.Height, panel.GetHeight())\n\t\t}\n\n\t\tif err := panel.ValidateCursorAndRenderIndex(); err != nil {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"file panel %v error : %w\", i, err)\n\t\t}\n\n\t\t// Validate search bar width matches panel width minus padding\n\t\tif panel.SearchBar.Width != panel.GetWidth()-common.InnerPadding {\n\t\t\treturn fmt.Errorf(\"file panel %v search bar width mismatch: expected %v, got %v\",\n\t\t\t\ti, panel.GetWidth()-common.InnerPadding, panel.SearchBar.Width)\n\t\t}\n\t}\n\n\tif m.fileModel.Width != totalFileModelWidth {\n\t\treturn fmt.Errorf(\n\t\t\t\"file model width mismatch: expected %v, got %v\",\n\t\t\tm.fileModel.Width, totalFileModelWidth)\n\t}\n\n\t// Validate focus panel index is within valid range\n\tif m.fileModel.FocusedPanelIndex < 0 || m.fileModel.FocusedPanelIndex >= m.fileModel.PanelCount() {\n\t\treturn fmt.Errorf(\"FocusedPanelIndex %v is out of range [0, %v)\",\n\t\t\tm.fileModel.FocusedPanelIndex, m.fileModel.PanelCount())\n\t}\n\n\t// Validate overlay panels have less width and height than total\n\tif m.helpMenu.IsOpen() {\n\t\tif m.helpMenu.GetWidth() >= m.fullWidth {\n\t\t\treturn fmt.Errorf(\"help menu width %v exceeds full width %v\", m.helpMenu.GetWidth(), m.fullWidth)\n\t\t}\n\t\tif m.helpMenu.GetHeight() >= m.fullHeight {\n\t\t\treturn fmt.Errorf(\"help menu height %v exceeds full height %v\", m.helpMenu.GetHeight(), m.fullHeight)\n\t\t}\n\t}\n\n\tif m.promptModal.IsOpen() {\n\t\tif m.promptModal.GetWidth() >= m.fullWidth {\n\t\t\treturn fmt.Errorf(\"prompt modal width %v exceeds full width %v\", m.promptModal.GetWidth(), m.fullWidth)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc validateRender(out string, height int, width int, border bool) error {\n\tstrippedOut := ansi.Strip(out)\n\n\t// Empty content is not handled correctly\n\t// strings.Split(\"\", \"\\n\") will return [\"\"], not [].\n\t// Hence we need this separate handling\n\tif height == 0 {\n\t\t// zero lines\n\t\tif strippedOut != \"\" {\n\t\t\treturn fmt.Errorf(\"render height mismatch: expected empty string for 0 height, got '%v'\", strippedOut)\n\t\t}\n\t\treturn nil\n\t}\n\n\tlines := strings.Split(strippedOut, \"\\n\")\n\n\tif len(lines) != height {\n\t\treturn fmt.Errorf(\"render height mismatch: expected %v lines, got %v\", height, len(lines))\n\t}\n\n\tfor i, line := range lines {\n\t\tlineWidth := ansi.StringWidth(line)\n\t\tif lineWidth != width {\n\t\t\treturn fmt.Errorf(\n\t\t\t\t\"render line %v, expected %v width, got %v(line - '%v')\",\n\t\t\t\ti,\n\t\t\t\twidth,\n\t\t\t\tlineWidth,\n\t\t\t\tlines[i],\n\t\t\t)\n\t\t}\n\t}\n\n\tif !border {\n\t\treturn nil\n\t}\n\n\treturn validateRenderBorderValidations(lines)\n}\n\nfunc validateRenderBorderValidations(lines []string) error {\n\tif len(lines) < minLinesForBorder {\n\t\treturn fmt.Errorf(\"too few lines for border : %v\", len(lines))\n\t}\n\t// Check first line starts with TopLeft and ends with TopRight\n\tif !strings.HasPrefix(lines[0], common.Config.BorderTopLeft) {\n\t\treturn fmt.Errorf(\"render missing top left border, expected %q\", common.Config.BorderTopLeft)\n\t}\n\tif !strings.HasSuffix(lines[0], common.Config.BorderTopRight) {\n\t\treturn fmt.Errorf(\"render missing top right border, expected %q\", common.Config.BorderTopRight)\n\t}\n\n\t// Check last line starts with BottomLeft and ends with BottomRight\n\tlastLine := lines[len(lines)-1]\n\tif !strings.HasPrefix(lastLine, common.Config.BorderBottomLeft) {\n\t\treturn fmt.Errorf(\"render missing bottom left border, expected %q\", common.Config.BorderBottomLeft)\n\t}\n\tif !strings.HasSuffix(lastLine, common.Config.BorderBottomRight) {\n\t\treturn fmt.Errorf(\"render missing bottom right border, expected %q\", common.Config.BorderBottomRight)\n\t}\n\n\t// Check middle lines wrapped with BorderLeft and BorderRight\n\tfor i := 1; i < len(lines)-1; i++ {\n\t\tif !strings.HasPrefix(lines[i], common.Config.BorderLeft) &&\n\t\t\t!strings.HasPrefix(lines[i], common.Config.BorderMiddleLeft) {\n\t\t\treturn fmt.Errorf(\"render line '%v' missing left border\", lines[i])\n\t\t}\n\t\tif !strings.HasSuffix(lines[i], common.Config.BorderRight) &&\n\t\t\t!strings.HasSuffix(lines[i], common.Config.BorderMiddleRight) {\n\t\t\treturn fmt.Errorf(\"render line '%v' missing right border\", lines[i])\n\t\t}\n\t}\n\n\t// Check top line contains BorderTop\n\tif !strings.Contains(lines[0], common.Config.BorderTop) {\n\t\treturn fmt.Errorf(\"render missing top border character %q\", common.Config.BorderTop)\n\t}\n\n\t// Check bottom line contains BorderBottom\n\tif !strings.Contains(lastLine, common.Config.BorderBottom) {\n\t\treturn fmt.Errorf(\"render missing bottom border character %q\", common.Config.BorderBottom)\n\t}\n\n\treturn nil\n}\n\n// validateComponentRender validates render output of all components\nfunc (m *model) validateComponentRender() error {\n\t// Validate sidebar render\n\tif common.Config.SidebarWidth > 0 {\n\t\tsidebarRender := m.sidebarRender()\n\t\tif err := validateRender(\n\t\t\tsidebarRender,\n\t\t\tm.mainPanelHeight+common.BorderPadding,\n\t\t\tcommon.Config.SidebarWidth+common.BorderPadding,\n\t\t\ttrue,\n\t\t); err != nil {\n\t\t\treturn fmt.Errorf(\"sidebar render validation failed: %w\", err)\n\t\t}\n\t}\n\n\tfor i := range m.fileModel.FilePanels {\n\t\tpanel := &m.fileModel.FilePanels[i]\n\t\tpanelRender := panel.Render(i == m.fileModel.FocusedPanelIndex)\n\t\tif err := validateRender(panelRender, panel.GetHeight(), panel.GetWidth(), true); err != nil {\n\t\t\treturn fmt.Errorf(\"file panel %v render validation failed: %w\", i, err)\n\t\t}\n\t}\n\n\tp := &m.fileModel.FilePreview\n\tif err := validateRender(\n\t\tp.GetContent(),\n\t\tp.GetContentHeight(),\n\t\tp.GetContentWidth(),\n\t\tcommon.Config.EnableFilePreviewBorder,\n\t); err != nil {\n\t\treturn fmt.Errorf(\"file preview render validation failed: %w\", err)\n\t}\n\n\tif err := validateRender(m.fileModel.Render(), m.fileModel.Height, m.fileModel.Width, false); err != nil {\n\t\treturn fmt.Errorf(\"file model render validation failed: %w\", err)\n\t}\n\n\t// Validate footer components if visible\n\tif m.toggleFooter {\n\t\tif err := validateRender(\n\t\t\tm.processBarRender(),\n\t\t\tm.processBarModel.GetHeight(),\n\t\t\tm.processBarModel.GetWidth(),\n\t\t\ttrue,\n\t\t); err != nil {\n\t\t\treturn fmt.Errorf(\"process bar render validation failed: %w\", err)\n\t\t}\n\t\tif err := validateRender(\n\t\t\tm.fileMetaData.Render(true),\n\t\t\tm.fileMetaData.GetHeight(),\n\t\t\tm.fileMetaData.GetWidth(),\n\t\t\ttrue,\n\t\t); err != nil {\n\t\t\treturn fmt.Errorf(\"metadata render validation failed: %w\", err)\n\t\t}\n\t\tif err := validateRender(\n\t\t\tm.clipboard.Render(),\n\t\t\tm.clipboard.GetHeight(),\n\t\t\tm.clipboard.GetWidth(),\n\t\t\ttrue,\n\t\t); err != nil {\n\t\t\treturn fmt.Errorf(\"clipboard render validation failed: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (m *model) validateFinalRender() error { //nolint:gocognit // cumilation of validations\n\tmainRender := m.mainComponentsRender()\n\tif err := validateRender(mainRender, m.fullHeight, m.fullWidth, false); err != nil {\n\t\treturn fmt.Errorf(\"model rendering failures : %w\", err)\n\t}\n\n\tstrippedOut := ansi.Strip(mainRender)\n\tlines := strings.Split(strippedOut, \"\\n\")\n\tif common.Config.SidebarWidth != 0 {\n\t\tsidebarPos := compPosition{\n\t\t\tstRow:  0,\n\t\t\tstCol:  0,\n\t\t\tendRow: m.sidebarModel.GetHeight() - 1,\n\t\t\tendCol: m.sidebarModel.GetWidth() - 1,\n\t\t}\n\t\t// Note: This wont work when any overlay model is open\n\t\tif err := m.validateComponentPlacement(lines, sidebarPos, true); err != nil {\n\t\t\treturn fmt.Errorf(\"sidebar position validation failed: %w\", err)\n\t\t}\n\t}\n\n\tfilePanelColStart := 0\n\tif common.Config.SidebarWidth != 0 {\n\t\tfilePanelColStart += common.BorderPadding + common.Config.SidebarWidth\n\t}\n\tfor i := range m.fileModel.FilePanels {\n\t\tpanel := &m.fileModel.FilePanels[i]\n\t\tpanelPos := compPosition{\n\t\t\tstRow:  0,\n\t\t\tendRow: m.mainPanelHeight + 1,\n\t\t\tstCol:  filePanelColStart,\n\t\t\tendCol: filePanelColStart + panel.GetWidth() - 1,\n\t\t}\n\t\tfilePanelColStart += panel.GetWidth()\n\t\t// Note: This wont work when any overlay model is open\n\t\tif err := m.validateComponentPlacement(lines, panelPos, true); err != nil {\n\t\t\treturn fmt.Errorf(\"file panel %v position validation failed: %w\", i, err)\n\t\t}\n\t}\n\n\tif m.fileModel.FilePreview.IsOpen() {\n\t\tpreviewPanelPos := compPosition{\n\t\t\tstRow:  0,\n\t\t\tendRow: m.mainPanelHeight + 1,\n\t\t\tstCol:  m.fullWidth - m.fileModel.ExpectedPreviewWidth,\n\t\t\tendCol: m.fullWidth - 1,\n\t\t}\n\t\tif err := m.validateComponentPlacement(\n\t\t\tlines,\n\t\t\tpreviewPanelPos,\n\t\t\tcommon.Config.EnableFilePreviewBorder,\n\t\t); err != nil {\n\t\t\treturn fmt.Errorf(\"preview panel position validation failed: %w\", err)\n\t\t}\n\t}\n\n\tif m.toggleFooter {\n\t\tprocessBarPos := compPosition{\n\t\t\tstRow:  m.mainPanelHeight + common.BorderPadding,\n\t\t\tstCol:  0,\n\t\t\tendRow: m.fullHeight - 1,\n\t\t\tendCol: m.processBarModel.GetWidth() - 1,\n\t\t}\n\t\tif err := m.validateComponentPlacement(lines, processBarPos, true); err != nil {\n\t\t\treturn fmt.Errorf(\"process bar position validation failed: %w\", err)\n\t\t}\n\t\tmetadataPos := compPosition{\n\t\t\tstRow:  m.mainPanelHeight + common.BorderPadding,\n\t\t\tstCol:  m.processBarModel.GetWidth(),\n\t\t\tendRow: m.fullHeight - 1,\n\t\t\tendCol: m.processBarModel.GetWidth() + m.fileMetaData.GetWidth() - 1,\n\t\t}\n\t\tif err := m.validateComponentPlacement(lines, metadataPos, true); err != nil {\n\t\t\treturn fmt.Errorf(\"metadata bar position validation failed: %w\", err)\n\t\t}\n\t\tclipboardPos := compPosition{\n\t\t\tstRow:  m.mainPanelHeight + common.BorderPadding,\n\t\t\tstCol:  m.processBarModel.GetWidth() + m.fileMetaData.GetWidth(),\n\t\t\tendRow: m.fullHeight - 1,\n\t\t\tendCol: m.fullWidth - 1,\n\t\t}\n\t\tif err := m.validateComponentPlacement(lines, clipboardPos, true); err != nil {\n\t\t\treturn fmt.Errorf(\"clipboard position validation failed: %w\", err)\n\t\t}\n\t}\n\n\t// TODO: programatically ensure that only one of them is open at a time\n\t// We may need some sort of overlay model management\n\tif m.IsOverlayModelOpen() {\n\t\tfinalRender := m.updateRenderForOverlay(mainRender)\n\t\tif err := validateRender(finalRender, m.fullHeight, m.fullWidth, false); err != nil {\n\t\t\treturn fmt.Errorf(\"model rendering failures : %w\", err)\n\t\t}\n\n\t\t// TODO: Add validations for overlay models\n\t}\n\n\treturn nil\n}\n\n// Inclusive\nfunc (m *model) validateComponentPlacement(lines []string, pos compPosition, border bool) error {\n\textractedLines, err := m.extractComponent(lines, pos)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failure while extracting content : %w\", err)\n\t}\n\n\tcntRow := pos.endRow - pos.stRow + 1\n\tcntCol := pos.endCol - pos.stCol + 1\n\textractedOut := strings.Join(extractedLines, \"\\n\")\n\tif err := validateRender(extractedOut, cntRow, cntCol, border); err != nil {\n\t\treturn fmt.Errorf(\"failure in extracted content : %w\", err)\n\t}\n\treturn nil\n}\n\n// Inclusive\nfunc (m *model) extractComponent(lines []string, pos compPosition) ([]string, error) {\n\tif 0 > pos.stRow || pos.stRow > pos.endRow || pos.endRow >= len(lines) {\n\t\treturn nil, fmt.Errorf(\"invalid row range [%v, %v], line count : %v\",\n\t\t\tpos.stRow, pos.endRow, len(lines))\n\t}\n\tfirstLineWidth := ansi.StringWidth(lines[0])\n\tif 0 > pos.stCol || pos.stCol > pos.endCol || pos.endCol >= firstLineWidth {\n\t\treturn nil, fmt.Errorf(\"invalid col range [%v, %v], first line width : %v\",\n\t\t\tpos.stCol, pos.endCol, firstLineWidth)\n\t}\n\n\tcntRow := pos.endRow - pos.stRow + 1\n\textractedLines := make([]string, cntRow)\n\tfor i := range cntRow {\n\t\torgIdx := pos.stRow + i\n\t\textractedLines[i] = ansi.Cut(lines[orgIdx], pos.stCol, pos.endCol+1)\n\t}\n\treturn extractedLines, nil\n}\n\ntype compPosition struct {\n\tstRow  int\n\tstCol  int\n\tendRow int\n\tendCol int\n}\n\nfunc (m *model) IsOverlayModelOpen() bool {\n\treturn m.zoxideModal.IsOpen() || m.helpMenu.IsOpen() || m.promptModal.IsOpen() ||\n\t\tm.sortModal.IsOpen() || m.firstUse || m.typingModal.open ||\n\t\tm.notifyModel.IsOpen()\n}\n"
  },
  {
    "path": "src/internal/wheel_function.go",
    "content": "package internal\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\nfunc wheelMainAction(msg string, m *model) {\n\tslog.Debug(\"wheelMainAction called\", \"msg\", msg, \"focusPanel\", m.focusPanel)\n\tvar action func()\n\tswitch msg {\n\tcase \"wheel up\":\n\t\tswitch m.focusPanel {\n\t\tcase sidebarFocus:\n\t\t\taction = func() { m.sidebarModel.ListUp() }\n\t\tcase processBarFocus:\n\t\t\taction = func() { m.processBarModel.ListUp() }\n\t\tcase metadataFocus:\n\t\t\taction = func() { m.fileMetaData.ListUp() }\n\t\tcase nonePanelFocus:\n\t\t\taction = func() { m.getFocusedFilePanel().ListUp() }\n\t\t}\n\n\tcase \"wheel down\":\n\t\tswitch m.focusPanel {\n\t\tcase sidebarFocus:\n\t\t\taction = func() { m.sidebarModel.ListDown() }\n\t\tcase processBarFocus:\n\t\t\taction = func() { m.processBarModel.ListDown() }\n\t\tcase metadataFocus:\n\t\t\taction = func() { m.fileMetaData.ListDown() }\n\t\tcase nonePanelFocus:\n\t\t\taction = func() { m.getFocusedFilePanel().ListDown() }\n\t\t}\n\tdefault:\n\t\tslog.Error(\"Unexpected type of mouse action in wheelMainAction\", \"msg\", msg)\n\t\treturn\n\t}\n\n\tfor range common.WheelRunTime {\n\t\taction()\n\t}\n}\n"
  },
  {
    "path": "src/pkg/cache/cache.go",
    "content": "package cache\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\ntype cacheItemInternal[T any] struct {\n\tobj       T\n\tTimestamp time.Time\n}\n\ntype Cache[T any] struct {\n\tcache      map[string]cacheItemInternal[T]\n\tmutex      sync.RWMutex\n\tmaxEntries int\n\texpiration time.Duration\n}\n\nfunc New[T any](maxEntries int, expiration time.Duration) *Cache[T] {\n\tcache := &Cache[T]{\n\t\tcache:      make(map[string]cacheItemInternal[T]),\n\t\tmaxEntries: maxEntries,\n\t\texpiration: expiration,\n\t}\n\n\t// Start a cleanup goroutine\n\tgo cache.periodicCleanup()\n\n\treturn cache\n}\n\n// periodicCleanup removes expired entries periodically\nfunc (c *Cache[T]) periodicCleanup() {\n\t//nolint:mnd // half of expiration for cleanup interval\n\tticker := time.NewTicker(c.expiration / 2)\n\tdefer ticker.Stop()\n\n\tfor range ticker.C {\n\t\tc.cleanupExpired()\n\t}\n}\n\n// cleanupExpired removes expired cache entries\nfunc (c *Cache[T]) cleanupExpired() {\n\tnow := time.Now()\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\n\tfor key, entry := range c.cache {\n\t\tif now.Sub(entry.Timestamp) > c.expiration {\n\t\t\tdelete(c.cache, key)\n\t\t}\n\t}\n}\n\nfunc (c *Cache[T]) Get(key string) (T, bool) {\n\tc.mutex.RLock()\n\tdefer c.mutex.RUnlock()\n\n\tif entry, exists := c.cache[key]; exists {\n\t\treturn entry.obj, true\n\t}\n\tvar res T\n\treturn res, false\n}\n\nfunc (c *Cache[T]) Set(key string, obj T) {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\n\t// Check if we need to evict entries\n\tif len(c.cache) >= c.maxEntries {\n\t\tc.evictOldest()\n\t}\n\n\tc.cache[key] = cacheItemInternal[T]{\n\t\tobj:       obj,\n\t\tTimestamp: time.Now(),\n\t}\n}\n\n// evictOldest removes the oldest entry from the cache\nfunc (c *Cache[T]) evictOldest() {\n\tvar oldestKey string\n\tvar oldestTime time.Time\n\n\t// Find the oldest entry\n\tfor key, entry := range c.cache {\n\t\tif oldestKey == \"\" || entry.Timestamp.Before(oldestTime) {\n\t\t\toldestKey = key\n\t\t\toldestTime = entry.Timestamp\n\t\t}\n\t}\n\n\t// Remove the oldest entry\n\tif oldestKey != \"\" {\n\t\tdelete(c.cache, oldestKey)\n\t}\n}\n"
  },
  {
    "path": "src/pkg/file_preview/ansi.go",
    "content": "package filepreview\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"strings\"\n\n\t\"github.com/muesli/termenv\"\n)\n\n// ConvertImageToANSI converts an image to ANSI escape codes with proper aspect ratio\nfunc ConvertImageToANSI(img image.Image, defaultBGColor color.Color) string {\n\twidth := img.Bounds().Dx()\n\theight := img.Bounds().Dy()\n\n\t// TODO: Use renderer here to prevent newline management,and overflows\n\tvar output strings.Builder\n\tcache := newColorCache()\n\tdefaultBGHex := colorToHex(defaultBGColor)\n\n\tfor y := 0; y < height; y += 2 {\n\t\tfor x := range width {\n\t\t\tupperColor := cache.getTermenvColor(img.At(x, y), defaultBGHex)\n\t\t\tlowerColor := cache.getTermenvColor(defaultBGColor, \"\")\n\n\t\t\tif y+1 < height {\n\t\t\t\tlowerColor = cache.getTermenvColor(img.At(x, y+1), defaultBGHex)\n\t\t\t}\n\n\t\t\t// Using the \"▄\" character which fills the lower half\n\t\t\tcell := termenv.String(\"▄\").Foreground(lowerColor).Background(upperColor)\n\t\t\toutput.WriteString(cell.String())\n\t\t}\n\t\t// Only add newline if this is not the last row\n\t\tif y+2 < height {\n\t\t\toutput.WriteByte('\\n')\n\t\t}\n\t}\n\n\treturn output.String()\n}\n\n// Convert image to ansi\nfunc (p *ImagePreviewer) ANSIRenderer(img image.Image, defaultBGColor string,\n\tmaxWidth int, maxHeight int) (string, error) {\n\tbgColor, err := hexToColor(defaultBGColor)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid background color: %w\", err)\n\t}\n\n\t// For ANSI rendering, resize image appropriately\n\tfittedImg := resizeForANSI(img, maxWidth, maxHeight)\n\treturn ConvertImageToANSI(fittedImg, bgColor), nil\n}\n\ntype colorCache struct {\n\trgbaToTermenv map[color.RGBA]termenv.RGBColor\n}\n\nfunc newColorCache() *colorCache {\n\treturn &colorCache{\n\t\trgbaToTermenv: make(map[color.RGBA]termenv.RGBColor),\n\t}\n}\n\nfunc (c *colorCache) getTermenvColor(col color.Color, fallbackColor string) termenv.RGBColor {\n\trgba, ok := color.RGBAModel.Convert(col).(color.RGBA)\n\tif !ok || rgba.A == 0 {\n\t\treturn termenv.RGBColor(fallbackColor)\n\t}\n\n\tif termenvColor, exists := c.rgbaToTermenv[rgba]; exists {\n\t\treturn termenvColor\n\t}\n\n\ttermenvColor := termenv.RGBColor(fmt.Sprintf(\"#%02x%02x%02x\", rgba.R, rgba.G, rgba.B))\n\tc.rgbaToTermenv[rgba] = termenvColor\n\treturn termenvColor\n}\n"
  },
  {
    "path": "src/pkg/file_preview/constants.go",
    "content": "package filepreview\n\nimport \"time\"\n\n// Image preview constants\nconst (\n\t// Cache configuration\n\tdefaultImagePreviewCacheSize = 100\n\tdefaultCacheExpiration       = 5 * time.Minute\n\n\t// Image processing\n\theightScaleFactor = 2  // Factor for height scaling in terminal display\n\trgbShift16        = 16 // Bit shift for red channel in RGB operations\n\trgbShift8         = 8  // Bit shift for green channel in RGB operations\n\n\t// Kitty protocol\n\tkittyHashSeed      = 42     // Seed for kitty image ID hashing\n\tkittyHashPrime     = 31     // Prime multiplier for hash calculation\n\tkittyMaxID         = 0xFFFF // Maximum ID value for kitty images\n\tkittyNonZeroOffset = 1000   // Offset to ensure non-zero IDs\n\n\t// RGB color masks\n\trgbMask     = 0xFF // Mask for extracting 8-bit RGB channel values\n\talphaOpaque = 255  // Fully opaque alpha channel value\n\n\tmaxVideoFileSizeForThumb = \"104857600\" // 100MB limit\n\tthumbOutputExt           = \".jpg\"\n\tthumbGenerationTimeout   = 30 * time.Second\n)\n"
  },
  {
    "path": "src/pkg/file_preview/image_preview.go",
    "content": "package filepreview\n\nimport (\n\t\"fmt\"\n\t_ \"image/gif\"  // Register GIF decoder\n\t_ \"image/jpeg\" // Register JPEG decoder\n\t_ \"image/png\"  // Register PNG decoder\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\t_ \"golang.org/x/image/webp\" // Register WebP decoder\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n\t\"github.com/yorukot/superfile/src/pkg/cache\"\n)\n\ntype ImageRenderer int\n\nconst (\n\tRendererANSI ImageRenderer = iota\n\tRendererKitty\n)\n\nfunc (f ImageRenderer) String() string {\n\tswitch f {\n\tcase RendererANSI:\n\t\treturn \"ANSI\"\n\tcase RendererKitty:\n\t\treturn \"Kitty\"\n\tdefault:\n\t\treturn common.InvalidTypeString\n\t}\n}\n\nfunc getPreviewObjKey(path string, dim string, renderer ImageRenderer) string {\n\treturn fmt.Sprintf(\"%s:%s:%s\", path, dim, renderer)\n}\n\n// ImagePreviewer encapsulates image preview functionality with caching\ntype ImagePreviewer struct {\n\tcache       *cache.Cache[string]\n\tterminalCap *TerminalCapabilities\n}\n\n// NewImagePreviewer creates a new ImagePreviewer with default cache settings\nfunc NewImagePreviewer() *ImagePreviewer {\n\treturn NewImagePreviewerWithConfig(defaultImagePreviewCacheSize, defaultCacheExpiration)\n}\n\n// NewImagePreviewerWithConfig creates a new ImagePreviewer with custom cache configuration\nfunc NewImagePreviewerWithConfig(maxEntries int, expiration time.Duration) *ImagePreviewer {\n\tpreviewer := &ImagePreviewer{\n\t\tcache:       cache.New[string](maxEntries, expiration),\n\t\tterminalCap: NewTerminalCapabilities(),\n\t}\n\n\t// Initialize terminal capabilities\n\tpreviewer.terminalCap.InitTerminalCapabilities()\n\n\treturn previewer\n}\n\n// ImagePreview generates a preview of an image file\nfunc (p *ImagePreviewer) ImagePreview(path string, maxWidth int, maxHeight int,\n\tdefaultBGColor string, sideAreaWidth int) (string, error) {\n\t// Validate dimensions\n\tif maxWidth <= 0 || maxHeight <= 0 {\n\t\treturn \"\", fmt.Errorf(\"dimensions must be positive (maxWidth=%d, maxHeight=%d)\", maxWidth, maxHeight)\n\t}\n\n\t// Create dimensions string for cache key\n\tdimensions := fmt.Sprintf(\"%d,%d,%s,%d\", maxWidth, maxHeight, defaultBGColor, sideAreaWidth)\n\n\t// Try Kitty first as it's more modern\n\tif p.IsKittyCapable() {\n\t\t// Check cache for Kitty renderer\n\t\tif preview, exists := p.cache.Get(getPreviewObjKey(path, dimensions, RendererKitty)); exists {\n\t\t\treturn preview, nil\n\t\t}\n\n\t\tpreview, err := p.ImagePreviewWithRenderer(\n\t\t\tpath,\n\t\t\tmaxWidth,\n\t\t\tmaxHeight,\n\t\t\tdefaultBGColor,\n\t\t\tRendererKitty,\n\t\t\tsideAreaWidth,\n\t\t)\n\t\tif err == nil {\n\t\t\t// Cache the successful result\n\t\t\tp.cache.Set(getPreviewObjKey(path, dimensions, RendererKitty), preview)\n\t\t\treturn preview, nil\n\t\t}\n\n\t\t// Fall through to ANSI if Kitty fails\n\t\tslog.Error(\"Kitty renderer failed, falling back to ANSI\", \"error\", err)\n\t}\n\n\t// Check cache for ANSI renderer\n\tif preview, found := p.cache.Get(getPreviewObjKey(path, dimensions, RendererANSI)); found {\n\t\treturn preview, nil\n\t}\n\n\t// Fall back to ANSI\n\tpreview, err := p.ImagePreviewWithRenderer(path, maxWidth, maxHeight, defaultBGColor, RendererANSI, sideAreaWidth)\n\tif err == nil {\n\t\t// Cache the successful result\n\t\tp.cache.Set(getPreviewObjKey(path, dimensions, RendererANSI), preview)\n\t}\n\treturn preview, err\n}\n\n// ImagePreviewWithRenderer generates an image preview using the specified renderer\nfunc (p *ImagePreviewer) ImagePreviewWithRenderer(path string, maxWidth int, maxHeight int,\n\tdefaultBGColor string, renderer ImageRenderer, sideAreaWidth int) (string, error) {\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tconst maxFileSize = 100 * 1024 * 1024 // 100MB limit\n\tif info.Size() > maxFileSize {\n\t\treturn \"\", fmt.Errorf(\"image file too large: %d bytes\", info.Size())\n\t}\n\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Use the new image preparation pipeline\n\timg, originalWidth, originalHeight, err := prepareImageForPreview(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tswitch renderer {\n\tcase RendererKitty:\n\t\tresult, err := p.renderWithKittyUsingTermCap(img, path, originalWidth,\n\t\t\toriginalHeight, maxWidth, maxHeight, sideAreaWidth)\n\t\tif err != nil {\n\t\t\t// If kitty fails, fall back to ANSI renderer\n\t\t\tslog.Error(\"Kitty renderer failed, falling back to ANSI\", \"error\", err)\n\t\t\treturn p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight)\n\t\t}\n\t\treturn result, nil\n\n\tcase RendererANSI:\n\t\treturn p.ANSIRenderer(img, defaultBGColor, maxWidth, maxHeight)\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"invalid renderer : %v\", renderer)\n\t}\n}\n"
  },
  {
    "path": "src/pkg/file_preview/image_resize.go",
    "content": "package filepreview\n\nimport (\n\t\"bytes\"\n\t\"image\"\n\t\"log/slog\"\n\n\t\"github.com/disintegration/imaging\"\n\t\"github.com/rwcarlsen/goexif/exif\"\n)\n\n// prepareImageForPreview handles the complete image preparation pipeline\nfunc prepareImageForPreview(data []byte) (image.Image, int, int, error) {\n\timgReader := bytes.NewReader(data)\n\n\timg, _, err := image.Decode(imgReader)\n\tif err != nil {\n\t\treturn nil, 0, 0, err\n\t}\n\n\t// Store original dimensions\n\toriginalWidth := img.Bounds().Dx()\n\toriginalHeight := img.Bounds().Dy()\n\n\t// Adjust orientation based on EXIF data\n\texifReader := bytes.NewReader(data)\n\timg = adjustImageOrientation(exifReader, img)\n\n\t// Limit resolution to 1080p\n\timg = limitImageResolution(img, originalWidth, originalHeight)\n\n\treturn img, originalWidth, originalHeight, nil\n}\n\n// limitImageResolution limits image resolution to 1080p while maintaining aspect ratio\nfunc limitImageResolution(img image.Image, originalWidth, originalHeight int) image.Image {\n\tconst maxImageWidth = 1920\n\tconst maxImageHeight = 1080\n\n\t// Only resize if the image is larger than 1080p\n\tif originalWidth > maxImageWidth || originalHeight > maxImageHeight {\n\t\tresizedImg := imaging.Fit(img, maxImageWidth, maxImageHeight, imaging.Lanczos)\n\t\treturn resizedImg\n\t}\n\n\treturn img\n}\n\n// adjustImageOrientation adjusts image orientation based on EXIF data\nfunc adjustImageOrientation(r *bytes.Reader, img image.Image) image.Image {\n\texifData, err := exif.Decode(r)\n\tif err != nil {\n\t\tslog.Error(\"exif error\", \"error\", err)\n\t\treturn img\n\t}\n\ttag, err := exifData.Get(exif.Orientation)\n\tif err != nil {\n\t\tslog.Error(\"exif orientation error\", \"error\", err)\n\t\treturn img\n\t}\n\torientation, err := tag.Int(0)\n\tif err != nil {\n\t\tslog.Error(\"exif orientation value error\", \"error\", err)\n\t\treturn img\n\t}\n\treturn adjustOrientation(img, orientation)\n}\n\n// adjustOrientation applies the specified orientation transformation to the image\nfunc adjustOrientation(img image.Image, orientation int) image.Image {\n\tswitch orientation {\n\tcase 1:\n\t\treturn img\n\tcase 2: //nolint:mnd // EXIF orientation: horizontal flip\n\t\treturn imaging.FlipH(img)\n\tcase 3: //nolint:mnd // EXIF orientation: 180 rotation\n\t\treturn imaging.Rotate180(img)\n\tcase 4: //nolint:mnd // EXIF orientation: vertical flip\n\t\treturn imaging.FlipV(img)\n\tcase 5: //nolint:mnd // EXIF orientation: transpose\n\t\treturn imaging.Transpose(img)\n\tcase 6: //nolint:mnd // EXIF orientation: 270 rotation\n\t\treturn imaging.Rotate270(img)\n\tcase 7: //nolint:mnd // EXIF orientation: transverse\n\t\treturn imaging.Transverse(img)\n\tcase 8: //nolint:mnd // EXIF orientation: 90 rotation\n\t\treturn imaging.Rotate90(img)\n\tdefault:\n\t\tslog.Error(\"Invalid orientation value\", \"error\", orientation)\n\t\treturn img\n\t}\n}\n\n// resizeForANSI resizes image specifically for ANSI rendering\nfunc resizeForANSI(img image.Image, maxWidth, maxHeight int) image.Image {\n\t// Use maxHeight*2 because each terminal row represents 2 pixel rows in ANSI rendering\n\treturn imaging.Fit(img, maxWidth, maxHeight*heightScaleFactor, imaging.Lanczos)\n}\n"
  },
  {
    "path": "src/pkg/file_preview/kitty.go",
    "content": "package filepreview\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/BourgeoisBear/rasterm\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\n// isKittyCapable checks if the terminal supports Kitty graphics protocol\nfunc isKittyCapable() bool {\n\tisCapable := rasterm.IsKittyCapable()\n\n\t// Additional detection for terminals that might not be detected by rasterm\n\tif !isCapable {\n\t\ttermProgram := os.Getenv(\"TERM_PROGRAM\")\n\t\tterm := os.Getenv(\"TERM\")\n\n\t\t// List of known terminal identifiers that support Kitty protocol\n\t\tknownTerminals := []string{\n\t\t\t\"ghostty\",\n\t\t\t\"WezTerm\",\n\t\t\t\"iTerm2\",\n\t\t\t\"xterm-kitty\",\n\t\t\t\"kitty\",\n\t\t\t\"Konsole\",\n\t\t\t\"WarpTerminal\",\n\t\t}\n\n\t\tfor _, knownTerm := range knownTerminals {\n\t\t\tif strings.EqualFold(termProgram, knownTerm) || strings.EqualFold(term, knownTerm) {\n\t\t\t\tisCapable = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn isCapable\n}\n\n// ClearKittyImages clears all Kitty protocol images from the terminal\nfunc ClearKittyImages() string {\n\tif !isKittyCapable() {\n\t\treturn \"\" // No need to clear if terminal doesn't support Kitty protocol\n\t}\n\n\treturn generateKittyClearCommands()\n}\n\n// ClearKittyImages clears all Kitty protocol images from the terminal\nfunc (p *ImagePreviewer) ClearKittyImages() string {\n\tif !p.IsKittyCapable() {\n\t\treturn \"\" // No need to clear if terminal doesn't support Kitty protocol\n\t}\n\n\treturn generateKittyClearCommands()\n}\n\n// generateKittyClearCommands generates the clearing commands for Kitty protocol\nfunc generateKittyClearCommands() string {\n\tvar buf bytes.Buffer\n\n\t// Clear all images first\n\tclearAllCmd := \"\\x1b_Ga=d\\x1b\\\\\"\n\tbuf.WriteString(clearAllCmd)\n\n\t// Clear all placements\n\tclearPlacementsCmd := \"\\x1b_Ga=d,p=1\\x1b\\\\\"\n\tbuf.WriteString(clearPlacementsCmd)\n\n\t// Reset text formatting to default\n\tbuf.WriteString(\"\\x1b[0m\")\n\n\treturn buf.String()\n}\n\n// generatePlacementID generates a unique placement ID based on file path\nfunc generatePlacementID(path string) uint32 {\n\tif len(path) == 0 {\n\t\treturn kittyHashSeed // Default fallback\n\t}\n\n\thash := 0\n\tfor _, c := range path {\n\t\thash = hash*kittyHashPrime + int(c)\n\t}\n\treturn uint32(hash&kittyMaxID) + //nolint:gosec // Hash is bounded by kittyMaxID mask before conversion\n\t\tkittyNonZeroOffset\n}\n\n// renderWithKittyUsingTermCap renders an image using Kitty graphics protocol with terminal capabilities\nfunc (p *ImagePreviewer) renderWithKittyUsingTermCap(img image.Image, path string,\n\toriginalWidth, originalHeight, maxWidth, maxHeight int, sideAreaWidth int,\n) (string, error) {\n\t// Validate dimensions\n\tif maxWidth <= 0 || maxHeight <= 0 {\n\t\treturn \"\", fmt.Errorf(\"dimensions must be positive (maxWidth=%d, maxHeight=%d)\", maxWidth, maxHeight)\n\t}\n\n\tvar buf bytes.Buffer\n\n\t// Add clearing commands\n\tbuf.WriteString(generateKittyClearCommands())\n\n\topts := rasterm.KittyImgOpts{\n\t\tPlacementId: generatePlacementID(path),\n\t}\n\n\t// Get terminal cell size from ImagePreviewer's terminal capabilities\n\tcellSize := p.terminalCap.GetTerminalCellSize()\n\tpixelsPerColumn := cellSize.PixelsPerColumn\n\tpixelsPerRow := cellSize.PixelsPerRow\n\n\tslog.Debug(\"pixelsPerColumn\", \"pixelsPerColumn\", pixelsPerColumn, \"pixelsPerRow\", pixelsPerRow)\n\n\timgRatio := float64(originalWidth) / float64(originalHeight)\n\ttermRatio := float64(maxWidth*pixelsPerColumn) / float64(maxHeight*pixelsPerRow)\n\n\tslog.Debug(\"imgRatio\", \"imgRatio\", imgRatio, \"termRatio\", termRatio)\n\n\tif imgRatio > termRatio {\n\t\tdstCols := maxWidth\n\t\tdstRows := int(float64(dstCols*pixelsPerColumn) / imgRatio / float64(pixelsPerRow))\n\t\topts.DstCols = uint32(dstCols) //nolint:gosec // Terminal dimensions are bounded by maxWidth/maxHeight\n\t\topts.DstRows = uint32(dstRows) //nolint:gosec // Terminal dimensions are bounded by maxWidth/maxHeight\n\t} else {\n\t\tdstRows := maxHeight\n\t\tdstCols := int(float64(dstRows*pixelsPerRow) * imgRatio / float64(pixelsPerColumn))\n\t\topts.DstRows = uint32(dstRows) //nolint:gosec // Terminal dimensions are bounded by maxWidth/maxHeight\n\t\topts.DstCols = uint32(dstCols) //nolint:gosec // Terminal dimensions are bounded by maxWidth/maxHeight\n\t}\n\n\t// Write image using Kitty protocol\n\tif err := rasterm.KittyWriteImage(&buf, img, opts); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// TODO: using internal/common package in pkg package is against the standards\n\t// We shouldn't use that here.\n\t// Other usage of common in `file_preview` should be removed too.\n\t// common.VideoExtensions should be moved to fixed_variables\n\t// and internal/common/utils shoud move to pkg/utils so that it can\n\t// be used by everyone\n\n\t// TODO : Ideally we should not need the kitty previewer to be\n\t// aware of full modal width and make decisions based on global config\n\t// A better solutions than this is needed for it.\n\trow := 1\n\tcol := sideAreaWidth + 1\n\tif common.Config.EnableFilePreviewBorder {\n\t\trow++\n\t\tcol++\n\t}\n\tbuf.WriteString(fmt.Sprintf(\"\\x1b[%d;%dH\", row, col))\n\n\treturn buf.String(), nil\n}\n\n// IsKittyCapable checks if the terminal supports Kitty graphics protocol\nfunc (p *ImagePreviewer) IsKittyCapable() bool {\n\treturn isKittyCapable()\n}\n"
  },
  {
    "path": "src/pkg/file_preview/thumbnail_generator.go",
    "content": "package filepreview\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/yorukot/superfile/src/internal/common\"\n)\n\ntype thumbnailGeneratorInterface interface {\n\tsupportsExt(ext string) bool\n\tgenerateThumbnail(inputPath string, outputPathWithoutExt string) (string, error)\n}\n\ntype VideoGenerator struct{}\n\nfunc newVideoGenerator() (*VideoGenerator, error) {\n\tif !isFFmpegInstalled() {\n\t\treturn nil, errors.New(\"ffmpeg is not installed\")\n\t}\n\n\treturn &VideoGenerator{}, nil\n}\n\nfunc (g *VideoGenerator) supportsExt(ext string) bool {\n\treturn common.VideoExtensions[strings.ToLower(ext)]\n}\n\nfunc (g *VideoGenerator) generateThumbnail(inputPath string, outputPathWithoutExt string) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), thumbGenerationTimeout)\n\tdefer cancel()\n\toutputPath := outputPathWithoutExt + thumbOutputExt\n\n\t// ffmpeg -v warning -t 60 -hwaccel auto -an -sn -dn -skip_frame nokey -i input.mkv -vf scale='min(1024,iw)':'min(720,ih)':force_original_aspect_ratio=decrease:flags=fast_bilinear -vf \"thumbnail\" -frames:v 1 -y thumb.jpg\n\tffmpeg := exec.CommandContext(ctx, \"ffmpeg\",\n\t\t\"-v\", \"warning\", // set log level to warning\n\t\t\"-an\",       // disable Audio stream\n\t\t\"-sn\",       // disable Subtitle stream\n\t\t\"-dn\",       // disable data stream\n\t\t\"-t\", \"180\", // process maximum 180s of the video (the first 3 min)\n\t\t\"-hwaccel\", \"auto\", // Use Hardware Acceleration if available\n\t\t\"-skip_frame\", \"nokey\", // skip non-key frames\n\t\t\"-i\", inputPath, // set input file\n\t\t\"-vf\", \"thumbnail\", // use ffmpeg default thumbnail filter\n\t\t\"-frames:v\", \"1\", // output only one frame (one image)\n\t\t\"-f\", \"image2\", // set format to image2\n\t\t\"-fs\", maxVideoFileSizeForThumb, // limit the max file size to match image previewer limit\n\t\t\"-y\", outputPath, // set the outputFile and overwrite it without confirmation if already exists\n\t)\n\n\terr := ffmpeg.Run()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error generating video thumbnail, outputPath: %s : %w\", outputPath, err)\n\t}\n\n\treturn outputPath, nil\n}\n\ntype pdfGenerator struct{}\n\nfunc newPdfGenerator() (*pdfGenerator, error) {\n\tif !isPopplerInstalled() {\n\t\treturn nil, errors.New(\"poppler is not installed\")\n\t}\n\n\treturn &pdfGenerator{}, nil\n}\n\nfunc (g *pdfGenerator) supportsExt(ext string) bool {\n\treturn strings.ToLower(ext) == \".pdf\"\n}\n\nfunc (g *pdfGenerator) generateThumbnail(inputPath string, outputPathWithoutExt string) (string, error) {\n\toutputPath := outputPathWithoutExt + thumbOutputExt\n\tctx, cancel := context.WithTimeout(context.Background(), thumbGenerationTimeout)\n\tdefer cancel()\n\n\t// pdftoppm -singlefile -png prefixFilename\n\tpdftoppm := exec.CommandContext(ctx, \"pdftoppm\",\n\t\t\"-singlefile\",        // output only the first page as image\n\t\t\"-jpeg\",              // Image extension\n\t\tinputPath,            // Set input file\n\t\toutputPathWithoutExt, // The output prefix. (pdftoppm will add the .jpg ext)\n\t)\n\n\terr := pdftoppm.Run()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error generating pdf thumbnail, outputPath: %s : %w\",\n\t\t\toutputPath, err)\n\t}\n\n\treturn outputPath, nil\n}\n\ntype ThumbnailGenerator struct {\n\t// This is a cache. Key -> Video file path, Value -> Thumbnail file path\n\t// TODO: We can potentially make it persistent, preventing generation\n\t// of thumbnail on every launch or superfile\n\ttempFilesCache map[string]string\n\ttempDirectory  string\n\tmu             sync.Mutex\n\tgenerators     []thumbnailGeneratorInterface\n}\n\nfunc NewThumbnailGenerator() (*ThumbnailGenerator, error) {\n\ttmp, err := os.MkdirTemp(\"\", \"superfiles-*\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgenerators := []thumbnailGeneratorInterface{}\n\n\tpdf, err := newPdfGenerator()\n\tif err != nil {\n\t\tslog.Debug(\"Error while trying to create pdfGenerator\", \"error\", err)\n\t} else {\n\t\tgenerators = append(generators, pdf)\n\t}\n\n\tvideo, err := newVideoGenerator()\n\tif err != nil {\n\t\tslog.Debug(\"Error while trying to create videoGenerator\", \"error\", err)\n\t} else {\n\t\tgenerators = append(generators, video)\n\t}\n\n\tthumbnailGenerator := &ThumbnailGenerator{\n\t\ttempFilesCache: make(map[string]string),\n\t\ttempDirectory:  tmp,\n\t\tgenerators:     generators,\n\t}\n\n\treturn thumbnailGenerator, nil\n}\n\nfunc (g *ThumbnailGenerator) SupportsExt(ext string) bool {\n\tfor i := range g.generators {\n\t\tif g.generators[i].supportsExt(ext) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (g *ThumbnailGenerator) GetThumbnailOrGenerate(path string) (string, error) {\n\tg.mu.Lock()\n\tfile, ok := g.tempFilesCache[path]\n\tg.mu.Unlock()\n\n\tif ok {\n\t\t_, err := os.Stat(file)\n\t\tif err == nil {\n\t\t\treturn file, nil\n\t\t}\n\n\t\tg.mu.Lock()\n\t\tdelete(g.tempFilesCache, path)\n\t\tg.mu.Unlock()\n\t}\n\n\tgeneratedThumbnailPath, err := g.generateThumbnail(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tg.mu.Lock()\n\tg.tempFilesCache[path] = generatedThumbnailPath\n\tg.mu.Unlock()\n\n\treturn generatedThumbnailPath, nil\n}\n\nfunc (g *ThumbnailGenerator) generateThumbnail(path string) (string, error) {\n\tfileExt := filepath.Ext(path)\n\tfor index := range g.generators {\n\t\tgenerator := g.generators[index]\n\n\t\tif !generator.supportsExt(fileExt) {\n\t\t\tcontinue\n\t\t}\n\t\tfilename := filepath.Base(path)\n\t\tbaseName := filename[:len(filename)-len(fileExt)]\n\n\t\toutputPathWithoutExt := filepath.Join(g.tempDirectory,\n\t\t\tfmt.Sprintf(\"%s-%d\", baseName, time.Now().UnixNano()))\n\n\t\toutputPath, err := generator.generateThumbnail(path, outputPathWithoutExt)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn outputPath, nil\n\t}\n\n\treturn \"\", errors.New(\"unsupported file format\")\n}\n\nfunc (g *ThumbnailGenerator) CleanUp() error {\n\treturn os.RemoveAll(g.tempDirectory)\n}\n\nfunc isPopplerInstalled() bool {\n\t_, err := exec.LookPath(\"pdftoppm\")\n\treturn err == nil\n}\n\nfunc isFFmpegInstalled() bool {\n\t_, err := exec.LookPath(\"ffmpeg\")\n\treturn err == nil\n}\n"
  },
  {
    "path": "src/pkg/file_preview/utils.go",
    "content": "package filepreview\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"image/color\"\n\t\"log/slog\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"sync\"\n)\n\n// Terminal cell to pixel conversion constants\n// These approximate the pixel dimensions of terminal cells\nconst (\n\tDefaultPixelsPerColumn = 10 // approximate pixels per terminal column\n\tDefaultPixelsPerRow    = 20 // approximate pixels per terminal row\n\tWindowsPixelsPerColumn = 8  // Windows Terminal/CMD typical width\n\tWindowsPixelsPerRow    = 16 // Windows Terminal/CMD typical height\n)\n\n// TerminalCellSize represents the pixel dimensions of terminal cells\ntype TerminalCellSize struct {\n\tPixelsPerColumn int\n\tPixelsPerRow    int\n}\n\n// TerminalCapabilities encapsulates terminal capability detection\ntype TerminalCapabilities struct {\n\tcellSize       TerminalCellSize\n\tcellSizeInit   sync.Once\n\tdetectionMutex sync.Mutex\n}\n\n// NewTerminalCapabilities creates a new TerminalCapabilities instance\nfunc NewTerminalCapabilities() *TerminalCapabilities {\n\treturn &TerminalCapabilities{\n\t\tcellSize: TerminalCellSize{\n\t\t\tPixelsPerColumn: DefaultPixelsPerColumn,\n\t\t\tPixelsPerRow:    DefaultPixelsPerRow,\n\t\t},\n\t}\n}\n\n// InitTerminalCapabilities initializes all terminal capabilities detection\n// including cell size and Kitty Graphics Protocol support\n// This should be called early in the application startup\nfunc (tc *TerminalCapabilities) InitTerminalCapabilities() {\n\t// Use a goroutine to avoid blocking the application startup\n\tgo func() {\n\t\t// Initialize cell size detection\n\t\ttc.cellSizeInit.Do(func() {\n\t\t\ttc.cellSize = tc.detectTerminalCellSize()\n\t\t\tslog.Info(\"Terminal cell size detection\",\n\t\t\t\t\"pixels_per_column\", tc.cellSize.PixelsPerColumn,\n\t\t\t\t\"pixels_per_row\", tc.cellSize.PixelsPerRow)\n\t\t})\n\t}()\n}\n\n// GetTerminalCellSize returns the current terminal cell size\n// If detection hasn't been initialized, it performs detection first\nfunc (tc *TerminalCapabilities) GetTerminalCellSize() TerminalCellSize {\n\ttc.cellSizeInit.Do(func() {\n\t\ttc.cellSize = tc.detectTerminalCellSize()\n\t\tslog.Info(\"Terminal cell size detection (lazy init)\",\n\t\t\t\"pixels_per_column\", tc.cellSize.PixelsPerColumn,\n\t\t\t\"pixels_per_row\", tc.cellSize.PixelsPerRow)\n\t})\n\n\treturn tc.cellSize\n}\n\n// winsize struct for ioctl TIOCGWINSZ\ntype winsize struct {\n\tRow    uint16\n\tCol    uint16\n\tXpixel uint16\n\tYpixel uint16\n}\n\n// detectTerminalCellSize detects the terminal cell size using ioctl system calls\n// This method is non-blocking and doesn't interfere with stdin\nfunc (tc *TerminalCapabilities) detectTerminalCellSize() TerminalCellSize {\n\ttc.detectionMutex.Lock()\n\tdefer tc.detectionMutex.Unlock()\n\n\t// Try platform-specific detection\n\tif runtime.GOOS == \"windows\" {\n\t\tif cellSize, ok := getTerminalCellSizeWindows(); ok {\n\t\t\tslog.Info(\"Successfully detected terminal cell size on Windows\",\n\t\t\t\t\"pixels_per_column\", cellSize.PixelsPerColumn,\n\t\t\t\t\"pixels_per_row\", cellSize.PixelsPerRow)\n\t\t\treturn cellSize\n\t\t}\n\t} else {\n\t\t// Unix-like systems (Linux, macOS, etc.)\n\t\tif cellSize, ok := getTerminalCellSizeViaIoctl(); ok {\n\t\t\tslog.Info(\"Successfully detected terminal cell size via ioctl\",\n\t\t\t\t\"pixels_per_column\", cellSize.PixelsPerColumn,\n\t\t\t\t\"pixels_per_row\", cellSize.PixelsPerRow)\n\t\t\treturn cellSize\n\t\t}\n\t}\n\n\t// Fallback to default values\n\tslog.Info(\"Using default terminal cell size\", \"os\", runtime.GOOS)\n\treturn getDefaultCellSize()\n}\n\n// getDefaultCellSize returns default fallback terminal cell size\nfunc getDefaultCellSize() TerminalCellSize {\n\treturn TerminalCellSize{\n\t\tPixelsPerColumn: DefaultPixelsPerColumn,\n\t\tPixelsPerRow:    DefaultPixelsPerRow,\n\t}\n}\n\n// InitTerminalCapabilities initializes terminal capabilities for the ImagePreviewer\nfunc (p *ImagePreviewer) InitTerminalCapabilities() {\n\tp.terminalCap.InitTerminalCapabilities()\n}\n\n// Windows-specific terminal detection functions\n// getTerminalCellSizeWindows uses Windows Console API to detect terminal cell size\nfunc getTerminalCellSizeWindows() (TerminalCellSize, bool) {\n\tif runtime.GOOS != \"windows\" {\n\t\treturn TerminalCellSize{}, false\n\t}\n\n\t// For Windows, just return reasonable defaults\n\t// Windows terminal detection is complex and varies greatly between\n\t// different terminal emulators (Windows Terminal, ConEmu, etc.)\n\tslog.Info(\"Using Windows default terminal cell size\")\n\t// TODO: Implement actual Windows Console API calls when running on Windows\n\treturn getWindowsDefaultCellSize(), true\n}\n\n// getWindowsDefaultCellSize returns reasonable defaults for Windows\nfunc getWindowsDefaultCellSize() TerminalCellSize {\n\treturn TerminalCellSize{\n\t\tPixelsPerColumn: WindowsPixelsPerColumn, // Windows Terminal/CMD typical width\n\t\tPixelsPerRow:    WindowsPixelsPerRow,    // Windows Terminal/CMD typical height\n\t}\n}\n\nfunc hexToColor(hex string) (color.RGBA, error) {\n\tif len(hex) != 7 || hex[0] != '#' {\n\t\treturn color.RGBA{}, errors.New(\"invalid hex color format\")\n\t}\n\tvalues, err := strconv.ParseUint(hex[1:], 16, 32)\n\tif err != nil {\n\t\treturn color.RGBA{}, err\n\t}\n\treturn color.RGBA{\n\t\tR: uint8(values >> rgbShift16),            //nolint:gosec // RGB values are masked to 8-bit range\n\t\tG: uint8((values >> rgbShift8) & rgbMask), //nolint:gosec // RGB values are masked to 8-bit range\n\t\tB: uint8(values & rgbMask),                //nolint:gosec // RGB values are masked to 8-bit range\n\t\tA: alphaOpaque,\n\t}, nil\n}\n\nfunc colorToHex(color color.Color) string {\n\tr, g, b, _ := color.RGBA()\n\treturn fmt.Sprintf(\n\t\t\"#%02x%02x%02x\",\n\t\tuint8(r>>rgbShift8), //nolint:gosec // RGBA() returns 16-bit values, shifting by 8 gives 8-bit\n\t\tuint8(g>>rgbShift8), //nolint:gosec // RGBA() returns 16-bit values, shifting by 8 gives 8-bit\n\t\tuint8(b>>rgbShift8), //nolint:gosec // RGBA() returns 16-bit values, shifting by 8 gives 8-bit\n\t)\n}\n"
  },
  {
    "path": "src/pkg/file_preview/utils_unix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage filepreview\n\nimport (\n\t\"syscall\"\n\t\"unsafe\"\n)\n\n// getTerminalCellSizeViaIoctl uses ioctl system call to get terminal size\nfunc getTerminalCellSizeViaIoctl() (TerminalCellSize, bool) {\n\t// Try different file descriptors in order of preference\n\tfds := []uintptr{\n\t\t1, // stdout\n\t\t0, // stdin\n\t\t2, // stderr\n\t}\n\n\tfor _, fd := range fds {\n\t\tif cellSize, ok := getTerminalSizeFromFd(fd); ok {\n\t\t\treturn cellSize, true\n\t\t}\n\t}\n\n\treturn TerminalCellSize{}, false\n}\n\n// getTerminalSizeFromFd gets terminal size from a specific file descriptor\nfunc getTerminalSizeFromFd(fd uintptr) (TerminalCellSize, bool) {\n\tvar ws winsize\n\n\t// TIOCGWINSZ ioctl call to get window size\n\t_, _, errno := syscall.Syscall(\n\t\tsyscall.SYS_IOCTL,\n\t\tfd,\n\t\tsyscall.TIOCGWINSZ,\n\t\tuintptr(unsafe.Pointer(&ws)),\n\t)\n\n\tif errno != 0 {\n\t\treturn TerminalCellSize{}, false\n\t}\n\n\t// Check if we got valid pixel dimensions\n\tif ws.Xpixel > 0 && ws.Ypixel > 0 && ws.Col > 0 && ws.Row > 0 {\n\t\tpixelsPerColumn := int(ws.Xpixel) / int(ws.Col)\n\t\tpixelsPerRow := int(ws.Ypixel) / int(ws.Row)\n\n\t\t// Sanity check the values\n\t\tif pixelsPerColumn > 0 && pixelsPerRow > 0 &&\n\t\t\tpixelsPerColumn < 100 && pixelsPerRow < 100 {\n\t\t\treturn TerminalCellSize{\n\t\t\t\tPixelsPerColumn: pixelsPerColumn,\n\t\t\t\tPixelsPerRow:    pixelsPerRow,\n\t\t\t}, true\n\t\t}\n\t}\n\n\treturn TerminalCellSize{}, false\n}\n"
  },
  {
    "path": "src/pkg/file_preview/utils_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage filepreview\n\n// getTerminalCellSizeViaIoctl is not supported on Windows, so always return false\nfunc getTerminalCellSizeViaIoctl() (TerminalCellSize, bool) {\n\treturn TerminalCellSize{}, false\n}\n"
  },
  {
    "path": "src/pkg/string_function/overplace.go",
    "content": "// ====================== overplace these is from the lipgloss PR ==============\n\n// These code is from the https://github.com/charmbracelet/lipgloss/pull/102\n// Thanks a lot!!!!!\n\n// Edit - cutLeft has been replaced with charmansi.TruncateLeft.\n// See https://github.com/charmbracelet/lipgloss/pull/102#issuecomment-2900110821\n\n// =============================================================================\npackage stringfunction\n\nimport (\n\t\"strings\"\n\n\tcharmansi \"github.com/charmbracelet/x/ansi\"\n\tansi \"github.com/muesli/reflow/ansi\"\n\t\"github.com/muesli/reflow/truncate\"\n\t\"github.com/muesli/termenv\"\n)\n\n// whitespace is a whitespace renderer.\ntype whitespace struct {\n\tstyle termenv.Style\n\tchars string\n}\n\ntype WhitespaceOption func(*whitespace)\n\n// Render whitespaces.\nfunc (w whitespace) render(width int) string {\n\tif w.chars == \"\" {\n\t\tw.chars = \" \"\n\t}\n\n\tr := []rune(w.chars)\n\tj := 0\n\tb := strings.Builder{}\n\n\t// Cycle through runes and print them into the whitespace.\n\tfor i := 0; i < width; {\n\t\tb.WriteRune(r[j])\n\t\tj++\n\t\tif j >= len(r) {\n\t\t\tj = 0\n\t\t}\n\t\ti += charmansi.StringWidth(string(r[j]))\n\t}\n\n\t// Fill any extra gaps white spaces. This might be necessary if any runes\n\t// are more than one cell wide, which could leave a one-rune gap.\n\tshort := width - charmansi.StringWidth(b.String())\n\tif short > 0 {\n\t\tb.WriteString(strings.Repeat(\" \", short))\n\t}\n\n\treturn w.style.Styled(b.String())\n}\n\n// PlaceOverlay places fg on top of bg.\nfunc PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string {\n\tfgLines, fgWidth := getLines(fg)\n\tbgLines, bgWidth := getLines(bg)\n\tbgHeight := len(bgLines)\n\tfgHeight := len(fgLines)\n\n\tif fgWidth >= bgWidth && fgHeight >= bgHeight {\n\t\t// FIXME: return fg or bg?\n\t\treturn fg\n\t}\n\t// TODO: allow placement outside of the bg box?\n\tx = clamp(x, 0, bgWidth-fgWidth)\n\ty = clamp(y, 0, bgHeight-fgHeight)\n\n\tws := &whitespace{}\n\tfor _, opt := range opts {\n\t\topt(ws)\n\t}\n\n\tvar b strings.Builder\n\tfor i, bgLine := range bgLines {\n\t\tif i > 0 {\n\t\t\tb.WriteByte('\\n')\n\t\t}\n\t\tif i < y || i >= y+fgHeight {\n\t\t\tb.WriteString(bgLine)\n\t\t\tcontinue\n\t\t}\n\n\t\tpos := 0\n\t\tif x > 0 {\n\t\t\tleft := truncate.String(bgLine, uint(x))\n\t\t\tpos = ansi.PrintableRuneWidth(left)\n\t\t\tb.WriteString(left)\n\t\t\tif pos < x {\n\t\t\t\tb.WriteString(ws.render(x - pos))\n\t\t\t\tpos = x\n\t\t\t}\n\t\t}\n\n\t\tfgLine := fgLines[i-y]\n\t\tb.WriteString(fgLine)\n\t\tpos += ansi.PrintableRuneWidth(fgLine)\n\n\t\tright := charmansi.TruncateLeft(bgLine, pos, \"\")\n\t\tbgWidth = ansi.PrintableRuneWidth(bgLine)\n\t\trightWidth := ansi.PrintableRuneWidth(right)\n\t\tif rightWidth <= bgWidth-pos {\n\t\t\tb.WriteString(ws.render(bgWidth - rightWidth - pos))\n\t\t}\n\n\t\tb.WriteString(right)\n\t}\n\n\treturn b.String()\n}\n\nfunc clamp(v, lower, upper int) int {\n\treturn min(max(v, lower), upper)\n}\n\n// Split a string into lines, additionally returning the size of the widest\n// line.\nfunc getLines(s string) ([]string, int) {\n\tlines := strings.Split(s, \"\\n\")\n\twidest := 0\n\tfor _, l := range lines {\n\t\tw := charmansi.StringWidth(l)\n\t\tif widest < w {\n\t\t\twidest = w\n\t\t}\n\t}\n\n\treturn lines, widest\n}\n"
  },
  {
    "path": "src/pkg/utils/README.md",
    "content": "# utils package\nIndependent utilities with zero dependencies with other packages\n"
  },
  {
    "path": "src/pkg/utils/bool_file_store.go",
    "content": "package utils\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"strconv\"\n)\n\n// This file provides utilities for storing boolean values in a file\n\n// Read file with \"true\" / \"false\" as content. In case of issues, return defaultValue\nfunc ReadBoolFile(path string, defaultValue bool) bool {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\tslog.Error(\"Error in readBoolFile\", \"path\", path, \"error\", err)\n\t\treturn defaultValue\n\t}\n\n\t// Not using strconv.ParseBool() as it allows other values like : \"TRUE\"\n\t// Using exact string comparison with predefined constants ensures\n\t// consistent behavior and prevents issues with case-insensitivity or\n\t// unexpected values like \"yes\", \"on\", etc. that ParseBool would accept\n\tswitch string(data) {\n\tcase TrueString:\n\t\treturn true\n\tcase FalseString:\n\t\treturn false\n\tdefault:\n\t\treturn defaultValue\n\t}\n}\n\nfunc WriteBoolFile(path string, value bool) error {\n\treturn os.WriteFile(path, []byte(strconv.FormatBool(value)), ConfigFilePerm)\n}\n"
  },
  {
    "path": "src/pkg/utils/bool_file_store_test.go",
    "content": "package utils\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReadBoolFile(t *testing.T) {\n\ttempDir := t.TempDir()\n\n\ttests := []struct {\n\t\tname         string\n\t\tfileContent  string\n\t\tdefaultValue bool\n\t\tcreateFile   bool\n\t\texpected     bool\n\t}{\n\t\t{\n\t\t\tname:         \"file contains true\",\n\t\t\tfileContent:  TrueString,\n\t\t\tdefaultValue: false,\n\t\t\tcreateFile:   true,\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tname:         \"file contains false\",\n\t\t\tfileContent:  FalseString,\n\t\t\tdefaultValue: true,\n\t\t\tcreateFile:   true,\n\t\t\texpected:     false,\n\t\t},\n\t\t{\n\t\t\tname:         \"file contains invalid value\",\n\t\t\tfileContent:  \"invalid\",\n\t\t\tdefaultValue: true,\n\t\t\tcreateFile:   true,\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tname:         \"file contains invalid value with default false\",\n\t\t\tfileContent:  \"invalid\",\n\t\t\tdefaultValue: false,\n\t\t\tcreateFile:   true,\n\t\t\texpected:     false,\n\t\t},\n\t\t{\n\t\t\tname:         \"file contains TRUE (uppercase)\",\n\t\t\tfileContent:  \"TRUE\",\n\t\t\tdefaultValue: false,\n\t\t\tcreateFile:   true,\n\t\t\texpected:     false, // Should not accept uppercase\n\t\t},\n\t\t{\n\t\t\tname:         \"file contains empty string\",\n\t\t\tfileContent:  \"\",\n\t\t\tdefaultValue: true,\n\t\t\tcreateFile:   true,\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tname:         \"file does not exist - default true\",\n\t\t\tfileContent:  \"\",\n\t\t\tdefaultValue: true,\n\t\t\tcreateFile:   false,\n\t\t\texpected:     true,\n\t\t},\n\t\t{\n\t\t\tname:         \"file does not exist - default false\",\n\t\t\tfileContent:  \"\",\n\t\t\tdefaultValue: false,\n\t\t\tcreateFile:   false,\n\t\t\texpected:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a unique file path for each test\n\t\t\tfilePath := filepath.Join(tempDir, tt.name+\".txt\")\n\n\t\t\t// Create and write to the file if needed\n\t\t\tif tt.createFile {\n\t\t\t\terr := os.WriteFile(filePath, []byte(tt.fileContent), 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Call the function\n\t\t\tresult := ReadBoolFile(filePath, tt.defaultValue)\n\n\t\t\t// Assert the result\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestWriteBoolFile(t *testing.T) {\n\ttempDir := t.TempDir()\n\n\ttests := []struct {\n\t\tname  string\n\t\tvalue bool\n\t}{\n\t\t{\n\t\t\tname:  \"write true value\",\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\tname:  \"write false value\",\n\t\t\tvalue: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t// Create a unique file path for each test\n\t\t\tfilePath := filepath.Join(tempDir, tt.name+\".txt\")\n\n\t\t\t// Call the function\n\t\t\terr := WriteBoolFile(filePath, tt.value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify file content\n\t\t\tcontent, err := os.ReadFile(filePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\texpected := FalseString\n\t\t\tif tt.value {\n\t\t\t\texpected = TrueString\n\t\t\t}\n\n\t\t\tassert.Equal(t, expected, string(content))\n\n\t\t\t// Verify permissions (Unix only)\n\t\t\tif runtime.GOOS != OsWindows {\n\t\t\t\tinfo, err := os.Stat(filePath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, os.FileMode(ConfigFilePerm), info.Mode().Perm())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteBoolFileError(t *testing.T) {\n\ttempDir := t.TempDir()\n\tnonExistentDir := filepath.Join(tempDir, \"non_existent_dir\", \"file.txt\")\n\n\terr := WriteBoolFile(nonExistentDir, true)\n\tassert.Error(t, err)\n}\n\nfunc TestReadBoolFilePermissionDenied(t *testing.T) {\n\t// Skip on Windows as permission handling differs\n\tif runtime.GOOS == OsWindows {\n\t\tt.Skip(\"Skipping permission test on Windows\")\n\t}\n\n\t// Skip when running as root since root can read files with 000 permissions\n\tif os.Geteuid() == 0 {\n\t\tt.Skip(\"Skipping permission test when running as root\")\n\t}\n\n\ttempDir := t.TempDir()\n\n\t// Create a file\n\tfilePath := filepath.Join(tempDir, \"no_read_perm.txt\")\n\terr := os.WriteFile(filePath, []byte(TrueString), ConfigFilePerm)\n\trequire.NoError(t, err)\n\n\t// Remove read permissions\n\terr = os.Chmod(filePath, 0)\n\trequire.NoError(t, err)\n\tdefer os.Chmod(filePath, ConfigFilePerm) // Reset permissions for cleanup\n\n\t// The function should return the default value when it can't read the file\n\tresult := ReadBoolFile(filePath, false)\n\tassert.False(t, result)\n\n\tresult = ReadBoolFile(filePath, true)\n\tassert.True(t, result)\n}\n\nfunc TestWriteBoolFilePermissionDenied(t *testing.T) {\n\t// Skip on Windows as permission handling differs\n\tif runtime.GOOS == OsWindows {\n\t\tt.Skip(\"Skipping permission test on Windows\")\n\t}\n\n\t// Skip when running as root since root can write to read-only directories\n\tif os.Geteuid() == 0 {\n\t\tt.Skip(\"Skipping permission test when running as root\")\n\t}\n\n\ttempDir := t.TempDir()\n\n\t// Make the directory read-only\n\terr := os.Chmod(tempDir, 0500)\n\trequire.NoError(t, err)\n\tdefer os.Chmod(tempDir, 0700) // Reset permissions for cleanup\n\n\tfilePath := filepath.Join(tempDir, \"readonly.txt\")\n\terr = WriteBoolFile(filePath, true)\n\tassert.Error(t, err)\n}\n\nfunc TestReadBoolFile_CornerCases(t *testing.T) {\n\ttempDir := t.TempDir()\n\n\t// Test with a directory instead of a file\n\tdirPath := filepath.Join(tempDir, \"directory\")\n\terr := os.Mkdir(dirPath, 0755)\n\trequire.NoError(t, err)\n\n\t// Should return the default value when path is a directory\n\tresult := ReadBoolFile(dirPath, true)\n\tassert.True(t, result)\n}\n"
  },
  {
    "path": "src/pkg/utils/config_interface.go",
    "content": "package utils\n\n// MissingFieldIgnorer defines the interface for configuration types that can ignore missing fields\n// during TOML file loading. Types implementing this interface can control whether missing field\n// warnings are suppressed when parsing configuration files.\ntype MissingFieldIgnorer interface {\n\tGetIgnoreMissingFields() bool\n}\n"
  },
  {
    "path": "src/pkg/utils/consts.go",
    "content": "package utils\n\nconst (\n\tTrueString  = \"true\"\n\tFalseString = \"false\"\n\t// These are used while comparing with runtime.GOOS\n\t// OsWindows represents the Windows operating system identifier\n\tOsWindows = \"windows\"\n\t// OsDarwin represents the macOS (Darwin) operating system identifier\n\tOsDarwin = \"darwin\"\n\tOsLinux  = \"linux\"\n\n\t// File permissions\n\tConfigFilePerm = 0600 // configuration files (owner read/write only)\n\tUserFilePerm   = 0644 // user-created files (owner rw, others r)\n\tLogFilePerm    = 0600 // log files (owner read/write only)\n\n\t// Directory permissions\n\tConfigDirPerm = 0700 // configuration directories (owner only)\n\tUserDirPerm   = 0755 // user-created directories (owner rwx, others rx)\n\n\t// Extracted file permissions (from archives)\n\tExtractedFileMode = 0644 // extracted files\n\tExtractedDirMode  = 0755 // extracted directories\n\n\t// Sidebar sections\n\tSidebarSectionHome   = \"home\"\n\tSidebarSectionPinned = \"pinned\"\n\tSidebarSectionDisks  = \"disks\"\n)\n"
  },
  {
    "path": "src/pkg/utils/detach_unix.go",
    "content": "//go:build !windows\n\npackage utils\n\nimport (\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc DetachFromTerminal(cmd *exec.Cmd) {\n\t// Start new session so child isn't tied to the TTY (prevents SIGHUP on terminal close).\n\t// This also prevents programs like sudo to read/write to tty\n\tcmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}\n\t// Optionally, redirect stdio to avoid terminal hangups\n\tcmd.Stdin = nil\n\tcmd.Stdout = nil\n\tcmd.Stderr = nil\n}\n"
  },
  {
    "path": "src/pkg/utils/detach_windows.go",
    "content": "//go:build windows\n\npackage utils\n\nimport \"os/exec\"\n\nfunc DetachFromTerminal(cmd *exec.Cmd) {\n\t// No-op: current Windows path uses rundll32 and returns immediately.\n\t// If needed later, set CreationFlags/HideWindow via syscall.SysProcAttr.\n}\n"
  },
  {
    "path": "src/pkg/utils/error.go",
    "content": "package utils\n\nimport (\n\t\"errors\"\n)\n\ntype TomlLoadError struct {\n\tuserMessage   string\n\twrappedError  error\n\tisFatal       bool\n\tmissingFields bool\n}\n\nfunc (t *TomlLoadError) Error() string {\n\tres := t.userMessage\n\tif t.wrappedError != nil {\n\t\tres += \" : \" + t.wrappedError.Error()\n\t}\n\treturn res\n}\n\nfunc (t *TomlLoadError) IsFatal() bool {\n\treturn t.isFatal\n}\n\nfunc (t *TomlLoadError) MissingFields() bool {\n\treturn t.missingFields\n}\n\nfunc (t *TomlLoadError) Unwrap() error {\n\treturn t.wrappedError\n}\n\nfunc (t *TomlLoadError) UpdateMessageAndError(msg string, err error) {\n\tt.userMessage = msg\n\tt.wrappedError = err\n}\n\n// Include another msg. For now we dont need to have this as wrapped error.\nfunc (t *TomlLoadError) AddMessageAndError(msg string, err error) {\n\tt.userMessage += \" \" + msg\n\tt.wrappedError = errors.Join(t.wrappedError, err)\n}\n"
  },
  {
    "path": "src/pkg/utils/file_utils.go",
    "content": "package utils\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/pelletier/go-toml/v2\"\n\n\t\"github.com/charmbracelet/x/ansi\"\n\t\"golang.org/x/text/encoding/unicode\"\n\t\"golang.org/x/text/transform\"\n)\n\n// Utility functions related to file operations\n// Note : This is not used anymore as we use os.WriteAt to\n// fix toml files now, but we will still keep it for later use.\nfunc WriteTomlData(filePath string, data interface{}) error {\n\ttomlData, err := toml.Marshal(data)\n\tif err != nil {\n\t\t// return a wrapped error\n\t\treturn fmt.Errorf(\"error encoding data : %w\", err)\n\t}\n\terr = os.WriteFile(filePath, tomlData, ConfigFilePerm)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing file : %w\", err)\n\t}\n\treturn nil\n}\n\n// Helper function to load and validate TOML files with field checking\n// errorPrefix is appended before every error message\nfunc LoadTomlFile(filePath string, defaultData string, target interface{},\n\tfixFlag bool, ignoreMissingFields bool) error {\n\t// Initialize with default config\n\t_ = toml.Unmarshal([]byte(defaultData), target)\n\n\tdata, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn &TomlLoadError{\n\t\t\tuserMessage:  \"config file doesn't exist\",\n\t\t\twrappedError: err,\n\t\t}\n\t}\n\n\t// Create a map to track which fields are present\n\tvar rawData map[string]interface{}\n\terr = toml.Unmarshal(data, &rawData)\n\tif err != nil {\n\t\treturn &TomlLoadError{\n\t\t\tuserMessage:  \"error decoding TOML file\",\n\t\t\twrappedError: err,\n\t\t\tisFatal:      true,\n\t\t}\n\t}\n\n\t// Replace default values with file values\n\terr = toml.Unmarshal(data, target)\n\tif err != nil {\n\t\tvar decodeErr *toml.DecodeError\n\t\tif errors.As(err, &decodeErr) {\n\t\t\trow, col := decodeErr.Position()\n\t\t\treturn &TomlLoadError{\n\t\t\t\tuserMessage:  fmt.Sprintf(\"error in field at line %d column %d\", row, col),\n\t\t\t\twrappedError: decodeErr,\n\t\t\t\tisFatal:      true,\n\t\t\t}\n\t\t}\n\t\treturn &TomlLoadError{\n\t\t\tuserMessage:  \"error unmarshalling data\",\n\t\t\twrappedError: err,\n\t\t\tisFatal:      true,\n\t\t}\n\t}\n\n\t// Override the default value if it exists default value to false\n\tif config, ok := target.(MissingFieldIgnorer); ok {\n\t\tignoreMissingFields = config.GetIgnoreMissingFields()\n\t}\n\n\t// Check for missing fields\n\ttargetType := reflect.TypeOf(target).Elem()\n\tmissingFields := []string{}\n\n\tfor i := range targetType.NumField() {\n\t\tfield := targetType.Field(i)\n\t\tvar fieldName string\n\t\ttag := field.Tag.Get(\"toml\")\n\t\tif tag != \"\" {\n\t\t\t// Discard options such as \",omitempty\"\n\t\t\tfieldName = strings.Split(tag, \",\")[0]\n\t\t} else {\n\t\t\tfieldName = field.Name\n\t\t}\n\t\t// Skip open_with field as it's an optional table\n\t\tif fieldName == \"open_with\" {\n\t\t\tcontinue\n\t\t}\n\t\tif _, exists := rawData[fieldName]; !exists {\n\t\t\tmissingFields = append(missingFields, fieldName)\n\t\t}\n\t}\n\n\tif len(missingFields) == 0 {\n\t\treturn nil\n\t}\n\tif !fixFlag && ignoreMissingFields {\n\t\t// nil error if we dont wanna fix, and dont wanna print\n\t\treturn nil\n\t}\n\n\tresultErr := &TomlLoadError{\n\t\tmissingFields: true,\n\t}\n\tif !fixFlag {\n\t\tresultErr.userMessage = fmt.Sprintf(\"missing fields: %v\", missingFields)\n\t\treturn resultErr\n\t}\n\n\t// Start fixing\n\treturn fixTomlFile(resultErr, filePath, target)\n}\n\nfunc fixTomlFile(resultErr *TomlLoadError, filePath string, target interface{}) error {\n\tresultErr.isFatal = true\n\t// Create a unique backup of the current config file\n\tbackupFile, err := os.CreateTemp(filepath.Dir(filePath), filepath.Base(filePath)+\".bak-\")\n\tif err != nil {\n\t\tresultErr.UpdateMessageAndError(\"failed to create backup file\", err)\n\t\treturn resultErr\n\t}\n\n\tbackupPath := backupFile.Name()\n\tneedsBackupFileRemoval := true\n\tdefer func() {\n\t\tif closeErr := backupFile.Close(); closeErr != nil {\n\t\t\tif resultErr.wrappedError == nil {\n\t\t\t\tresultErr.UpdateMessageAndError(\"failed to close backup file\", closeErr)\n\t\t\t}\n\t\t}\n\t\t// Remove backup in case of unsuccessful write\n\t\tif needsBackupFileRemoval {\n\t\t\tif errRem := os.Remove(backupPath); errRem != nil {\n\t\t\t\t// Modify result Error\n\t\t\t\tresultErr.AddMessageAndError(\"warning: failed to remove backup file, backupPath : \"+backupPath, errRem)\n\t\t\t}\n\t\t}\n\t}()\n\t// Copy the original file to the backup\n\t// Open it in read write mode\n\torigFile, err := os.OpenFile(filePath, os.O_RDWR, ConfigFilePerm)\n\tif err != nil {\n\t\tresultErr.UpdateMessageAndError(\"failed to open original file for backup\", err)\n\t\treturn resultErr\n\t}\n\tdefer origFile.Close()\n\n\t_, err = io.Copy(backupFile, origFile)\n\tif err != nil {\n\t\tresultErr.UpdateMessageAndError(\"failed to copy original file to backup\", err)\n\t\treturn resultErr\n\t}\n\n\ttomlData, err := toml.Marshal(target)\n\tif err != nil {\n\t\tresultErr.UpdateMessageAndError(\"failed to marshal config to TOML\", err)\n\t\treturn resultErr\n\t}\n\t_, err = origFile.WriteAt(tomlData, 0)\n\tif err != nil {\n\t\tresultErr.UpdateMessageAndError(\"failed to write TOML data to original file\", err)\n\t\treturn resultErr\n\t}\n\n\t// Fix done\n\t// Inform user about backup location\n\tresultErr.userMessage = \"config file had issues. Its fixed successfully. Original backed up to : \" + backupPath\n\tresultErr.isFatal = false\n\t// Do not remove backup; user may want to restore manually\n\tneedsBackupFileRemoval = false\n\n\treturn resultErr\n}\n\n// If path is not absolute, then append to currentDir and get absolute path\n// Resolve paths starting with \"~\"\n// currentDir should be an absolute path\nfunc ResolveAbsPath(currentDir string, path string) string {\n\tif !filepath.IsAbs(currentDir) {\n\t\tslog.Warn(\"currentDir is not absolute\", \"currentDir\", currentDir)\n\t}\n\tif strings.HasPrefix(path, \"~\") {\n\t\t// We dont use variable.HomeDir here, as the util package cannot have dependency\n\t\t// on variable package\n\t\tpath = strings.Replace(path, \"~\", xdg.Home, 1)\n\t}\n\tif !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(currentDir, path)\n\t}\n\treturn filepath.Clean(path)\n}\n\n// Get directory total size\n// TODO: Uni test this\nfunc DirSize(path string) int64 {\n\tvar size int64\n\t// Its named walkErr to prevent shadowing\n\twalkErr := filepath.WalkDir(path, func(_ string, entry os.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\tslog.Error(\"Dir size function error\", \"error\", err)\n\t\t}\n\t\tif !entry.IsDir() {\n\t\t\tinfo, infoErr := entry.Info()\n\t\t\tif infoErr == nil {\n\t\t\t\tsize += info.Size()\n\t\t\t}\n\t\t}\n\t\treturn err\n\t})\n\tif walkErr != nil {\n\t\tslog.Error(\"errors during WalkDir\", \"error\", walkErr)\n\t}\n\treturn size\n}\n\n// Helper functions\n// Create all dirs that does not already exists\nfunc CreateDirectories(dirs ...string) error {\n\tfor _, dir := range dirs {\n\t\tif dir == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif err := os.MkdirAll(dir, ConfigDirPerm); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create directory %s: %w\", dir, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Create all files if they do not exists yet\nfunc CreateFiles(files ...string) error {\n\tfor _, file := range files {\n\t\tif _, err := os.Stat(file); os.IsNotExist(err) {\n\t\t\tif err = os.WriteFile(file, nil, ConfigFilePerm); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create file %s: %w\", file, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ReadFileContent(filepath string, maxLineLength int, previewLine int) (string, error) {\n\tvar resultBuilder strings.Builder\n\tfile, err := os.Open(filepath)\n\tif err != nil {\n\t\treturn resultBuilder.String(), err\n\t}\n\tdefer file.Close()\n\n\treader := transform.NewReader(file, unicode.BOMOverride(unicode.UTF8.NewDecoder()))\n\tscanner := bufio.NewScanner(reader)\n\tlineCount := 0\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tline = ansi.Truncate(line, maxLineLength, \"\")\n\t\tresultBuilder.WriteString(line)\n\t\tresultBuilder.WriteRune('\\n')\n\t\tlineCount++\n\t\tif previewLine > 0 && lineCount >= previewLine {\n\t\t\tbreak\n\t\t}\n\t}\n\t// returns the first non-EOF error that was encountered by the [Scanner]\n\treturn resultBuilder.String(), scanner.Err()\n}\n\nfunc InitJSONFile(path string) error {\n\tif _, err := os.Stat(path); os.IsNotExist(err) {\n\t\tif err = os.WriteFile(path, []byte(\"null\"), ConfigFilePerm); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to initialize json file %s: %w\", path, err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/pkg/utils/file_utils_test.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/pelletier/go-toml/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestResolveAbsPath(t *testing.T) {\n\tsep := string(filepath.Separator)\n\tdir1 := \"abc\"\n\tdir2 := \"def\"\n\n\tabsPrefix := \"\"\n\tif runtime.GOOS == \"windows\" {\n\t\tabsPrefix = \"C:\" // Windows absolute path prefix\n\t}\n\troot := absPrefix + sep\n\n\ttestdata := []struct {\n\t\tname        string\n\t\tcwd         string\n\t\tpath        string\n\t\texpectedRes string\n\t}{\n\t\t{\n\t\t\tname:        \"Path cleaup Test 1\",\n\t\t\tcwd:         absPrefix + sep,\n\t\t\tpath:        absPrefix + strings.Repeat(sep, 10),\n\t\t\texpectedRes: absPrefix + sep,\n\t\t},\n\t\t{\n\t\t\tname:        \"Basic test\",\n\t\t\tcwd:         filepath.Join(root, dir1),\n\t\t\tpath:        dir2,\n\t\t\texpectedRes: filepath.Join(root, dir1, dir2),\n\t\t},\n\t\t{\n\t\t\tname:        \"Ignore cwd for abs path\",\n\t\t\tcwd:         filepath.Join(root, dir1),\n\t\t\tpath:        filepath.Join(root, dir2),\n\t\t\texpectedRes: filepath.Join(root, dir2),\n\t\t},\n\t\t{\n\t\t\tname:        \"Path cleanup Test 2\",\n\t\t\tcwd:         absPrefix + strings.Repeat(sep, 4) + dir1,\n\t\t\tpath:        \".\" + sep + \".\" + sep + dir2,\n\t\t\texpectedRes: filepath.Join(root, dir1, dir2),\n\t\t},\n\t\t{\n\t\t\tname:        \"Basic test with ~\",\n\t\t\tcwd:         root,\n\t\t\tpath:        \"~\",\n\t\t\texpectedRes: xdg.Home,\n\t\t},\n\t\t{\n\t\t\tname:        \"~ should not be resolved if not first\",\n\t\t\tcwd:         dir1,\n\t\t\tpath:        filepath.Join(dir2, \"~\"),\n\t\t\texpectedRes: filepath.Join(dir1, dir2, \"~\"),\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.expectedRes, ResolveAbsPath(tt.cwd, tt.path))\n\t\t})\n\t}\n}\n\n// We cannot use ConfigType here, as that is not accessible by \"utils\" package\nfunc TestLoadTomlFile(t *testing.T) {\n\t_, curFilename, _, ok := runtime.Caller(0)\n\trequire.True(t, ok)\n\ttestdataDir := filepath.Join(filepath.Dir(curFilename), \"testdata\", \"load_toml\")\n\n\tdefaultDataBytes, err := os.ReadFile(filepath.Join(testdataDir, \"default.toml\"))\n\trequire.NoError(t, err)\n\n\tdefaultData := string(defaultDataBytes)\n\tvar defaultTomlVal TestTOMLType\n\terr = toml.Unmarshal(defaultDataBytes, &defaultTomlVal)\n\trequire.NoError(t, err)\n\n\ttestdata := []struct {\n\t\tname string\n\t\t// Relative to corr\n\t\tconfigName string\n\t\tfixFlag    bool\n\t\tnoError    bool\n\n\t\t// If we have error. It should be TomlLoadError\n\t\texpectedError *TomlLoadError\n\n\t\t// For checking the result value\n\t\tcheckTomlVal    bool\n\t\texpectedTomlVal TestTOMLType\n\t}{\n\t\t{\n\t\t\tname:            \"Config1 Load Default\",\n\t\t\tconfigName:      \"default.toml\",\n\t\t\tfixFlag:         false,\n\t\t\tnoError:         true,\n\t\t\tcheckTomlVal:    true,\n\t\t\texpectedTomlVal: defaultTomlVal,\n\t\t},\n\t\t{\n\t\t\tname:       \"Config1 Missing fields\",\n\t\t\tconfigName: \"missing_str.toml\",\n\t\t\tfixFlag:    false,\n\t\t\tnoError:    false,\n\t\t\texpectedError: &TomlLoadError{\n\t\t\t\tuserMessage:   \"missing fields: [sample_str]\",\n\t\t\t\twrappedError:  nil,\n\t\t\t\tisFatal:       false,\n\t\t\t\tmissingFields: true,\n\t\t\t},\n\t\t\tcheckTomlVal:    true,\n\t\t\texpectedTomlVal: defaultTomlVal,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tomlVal TestTOMLType\n\t\t\terr = LoadTomlFile(filepath.Join(testdataDir, tt.configName), defaultData, &tomlVal,\n\t\t\t\ttt.fixFlag, false)\n\t\t\tif tt.noError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.expectedError, err)\n\t\t\t}\n\n\t\t\tif tt.checkTomlVal {\n\t\t\t\tassert.Equal(t, tt.expectedTomlVal, tomlVal)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLoadTomlFileIgnorer(t *testing.T) {\n\t_, curFilename, _, ok := runtime.Caller(0)\n\trequire.True(t, ok)\n\ttestdataDir := filepath.Join(filepath.Dir(curFilename), \"testdata\", \"load_toml\", \"ignorer\")\n\n\tdefaultDataBytes, err := os.ReadFile(filepath.Join(testdataDir, \"default.toml\"))\n\trequire.NoError(t, err)\n\n\tdefaultData := string(defaultDataBytes)\n\tvar defaultTomlVal TestTOMLMissingIgnorerType\n\terr = toml.Unmarshal(defaultDataBytes, &defaultTomlVal)\n\trequire.NoError(t, err)\n\t// This is for Ignorer Type\n\ttestdata := []struct {\n\t\tname string\n\t\t// Relative to corr\n\t\tconfigName string\n\t\tfixFlag    bool\n\t\tnoError    bool\n\n\t\t// If we have error. It should be TomlLoadError\n\t\texpectedError    *TomlLoadError\n\t\tverifyWrappedErr bool\n\t\t// For checking the result value\n\t\tcheckTomlVal    bool\n\t\texpectedTomlVal TestTOMLMissingIgnorerType\n\t}{\n\t\t{\n\t\t\tname:            \"Config2 Load Default\",\n\t\t\tconfigName:      \"default.toml\",\n\t\t\tfixFlag:         false,\n\t\t\tnoError:         true,\n\t\t\tcheckTomlVal:    true,\n\t\t\texpectedTomlVal: defaultTomlVal,\n\t\t},\n\t\t{\n\t\t\tname:            \"Config2 Extra Fields ignored\",\n\t\t\tconfigName:      \"default_extra_fields.toml\",\n\t\t\tfixFlag:         false,\n\t\t\tnoError:         true,\n\t\t\tcheckTomlVal:    true,\n\t\t\texpectedTomlVal: defaultTomlVal,\n\t\t},\n\t\t{\n\t\t\tname:       \"Config2 Missing fields Not Ignored\",\n\t\t\tconfigName: \"missing_str_int.toml\",\n\t\t\tfixFlag:    false,\n\t\t\tnoError:    false,\n\t\t\texpectedError: &TomlLoadError{\n\t\t\t\tuserMessage:   \"missing fields: [sample_int sample_str]\",\n\t\t\t\twrappedError:  nil,\n\t\t\t\tisFatal:       false,\n\t\t\t\tmissingFields: true,\n\t\t\t},\n\t\t\tcheckTomlVal:    true,\n\t\t\texpectedTomlVal: defaultTomlVal,\n\t\t},\n\t\t{\n\t\t\tname:            \"Config2 Missing fields Ignored\",\n\t\t\tconfigName:      \"missing_str_ignore.toml\",\n\t\t\tfixFlag:         false,\n\t\t\tnoError:         true,\n\t\t\tcheckTomlVal:    true,\n\t\t\texpectedTomlVal: defaultTomlVal.WithIgnoreMissing(true),\n\t\t},\n\t\t{\n\t\t\tname:       \"Config2 Non Existent config\",\n\t\t\tconfigName: \"non_existent_config.toml\",\n\t\t\tfixFlag:    false,\n\t\t\tnoError:    false,\n\t\t\texpectedError: &TomlLoadError{\n\t\t\t\tuserMessage: \"config file doesn't exist\",\n\t\t\t},\n\t\t\tverifyWrappedErr: false,\n\t\t\tcheckTomlVal:     false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Config2 Invalid format\",\n\t\t\tconfigName: \"invalid_format.toml\",\n\t\t\tfixFlag:    false,\n\t\t\tnoError:    false,\n\t\t\texpectedError: &TomlLoadError{\n\t\t\t\tuserMessage: \"error decoding TOML file\",\n\t\t\t\tisFatal:     true,\n\t\t\t},\n\t\t\tverifyWrappedErr: false,\n\t\t\tcheckTomlVal:     false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Config2 Invalid Value Type\",\n\t\t\tconfigName: \"invalid_value_type.toml\",\n\t\t\tfixFlag:    false,\n\t\t\tnoError:    false,\n\t\t\texpectedError: &TomlLoadError{\n\t\t\t\tuserMessage: \"error in field at line 2 column 14\",\n\t\t\t\tisFatal:     true,\n\t\t\t},\n\t\t\tverifyWrappedErr: false,\n\t\t\tcheckTomlVal:     false,\n\t\t},\n\t}\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tomlVal TestTOMLMissingIgnorerType\n\t\t\terr := LoadTomlFile(filepath.Join(testdataDir, tt.configName), defaultData, &tomlVal,\n\t\t\t\ttt.fixFlag, false)\n\t\t\tif tt.noError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tvar tomlErr *TomlLoadError\n\t\t\t\trequire.ErrorAs(t, err, &tomlErr)\n\t\t\t\tif tt.verifyWrappedErr {\n\t\t\t\t\tassert.Equal(t, tt.expectedError, tomlErr)\n\t\t\t\t} else {\n\t\t\t\t\tassert.Equal(t, tt.expectedError.userMessage, tomlErr.userMessage)\n\t\t\t\t\tassert.Equal(t, tt.expectedError.isFatal, tomlErr.isFatal)\n\t\t\t\t\tassert.Equal(t, tt.expectedError.missingFields, tomlErr.missingFields)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tt.checkTomlVal {\n\t\t\t\tassert.Equal(t, tt.expectedTomlVal, tomlVal)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Tests for fixing config file\n\n\tt.Run(\"Config2 Fixing config file\", func(t *testing.T) {\n\t\t// To make sure that other values are kept.\n\t\texpectedVal1 := defaultTomlVal\n\t\texpectedVal2 := defaultTomlVal\n\t\texpectedVal1.SampleInt = -1\n\t\texpectedVal2.SampleInt = -1\n\t\texpectedVal2.IgnoreMissing = true\n\n\t\ttempDir := t.TempDir()\n\n\t\tactualTest := func(fileName string, expectedVal TestTOMLMissingIgnorerType) {\n\t\t\tvar tomlVal TestTOMLMissingIgnorerType\n\t\t\ttestFile := filepath.Join(testdataDir, fileName)\n\t\t\torgFile := filepath.Join(tempDir, fileName)\n\n\t\t\ttestContent, err := os.ReadFile(testFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Copy to temp directory first to avoid permission errors\n\t\t\terr = os.WriteFile(orgFile, testContent, 0644)\n\t\t\trequire.NoError(t, err, \"Error writing config file to temp directory\")\n\n\t\t\terr = LoadTomlFile(orgFile, defaultData, &tomlVal, true, false)\n\t\t\tvar tomlErr *TomlLoadError\n\t\t\trequire.ErrorAs(t, err, &tomlErr)\n\n\t\t\tassert.True(t, tomlErr.missingFields)\n\t\t\tassert.Equal(t, expectedVal, tomlVal)\n\n\t\t\tpref := \"config file had issues. Its fixed successfully. Original backed up to : \"\n\n\t\t\tassert.True(t, strings.HasPrefix(tomlErr.userMessage, pref), \"Unexpectd error : \"+tomlErr.Error())\n\n\t\t\tbackupFile := strings.TrimPrefix(tomlErr.userMessage, pref)\n\n\t\t\tassert.FileExists(t, backupFile)\n\t\t\tbackupContent, err := os.ReadFile(backupFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, testContent, backupContent)\n\n\t\t\t// Validate that if you Load Original File again, it loads without any errors\n\t\t\terr = LoadTomlFile(orgFile, defaultData, &tomlVal, true, false)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.WriteFile(orgFile, backupContent, 0644)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\t\tactualTest(\"missing_str2.toml\", expectedVal1)\n\t\tactualTest(\"missing_str_ignore2.toml\", expectedVal2)\n\t})\n}\n\nfunc TestReadFileContent(t *testing.T) {\n\ttestDir := t.TempDir()\n\tcurTestDir := filepath.Join(testDir, \"TestReadFileContent\")\n\tSetupDirectories(t, curTestDir)\n\n\ttestdata := []struct {\n\t\tname          string\n\t\tcontent       []byte\n\t\tmaxLineLength int\n\t\tpreviewLine   int\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\tname:          \"regular UTF-8 file\",\n\t\t\tcontent:       []byte(\"line1\\nline2\\nline3\"),\n\t\t\tmaxLineLength: 100,\n\t\t\tpreviewLine:   5,\n\t\t\texpected:      \"line1\\nline2\\nline3\\n\",\n\t\t},\n\t\t{\n\t\t\tname:          \"UTF-8 BOM file\",\n\t\t\tcontent:       []byte(\"\\xEF\\xBB\\xBFline1\\nline2\\nline3\"),\n\t\t\tmaxLineLength: 100,\n\t\t\tpreviewLine:   5,\n\t\t\texpected:      \"line1\\nline2\\nline3\\n\",\n\t\t},\n\t\t{\n\t\t\tname:          \"limited preview lines\",\n\t\t\tcontent:       []byte(\"line1\\nline2\\nline3\\nline4\"),\n\t\t\tmaxLineLength: 100,\n\t\t\tpreviewLine:   2,\n\t\t\texpected:      \"line1\\nline2\\n\",\n\t\t},\n\t}\n\n\tfor i, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttestFile := filepath.Join(curTestDir, fmt.Sprintf(\"test_file_%d.txt\", i))\n\t\t\tSetupFilesWithData(t, tt.content, testFile)\n\n\t\t\tresult, err := ReadFileContent(testFile, tt.maxLineLength, tt.previewLine)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestReadFileContentBOMHandling(t *testing.T) {\n\ttestDir := t.TempDir()\n\tcurTestDir := filepath.Join(testDir, \"TestBOMHandling\")\n\tSetupDirectories(t, curTestDir)\n\n\t// Write a file prefixed with UTF-8 BOM\n\tbomContent := []byte(\"\\xEF\\xBB\\xBFHello, World!\\nSecond line\")\n\tbomFile := filepath.Join(curTestDir, \"bom_file.txt\")\n\tSetupFilesWithData(t, bomContent, bomFile)\n\n\tresult, err := ReadFileContent(bomFile, 100, 10)\n\trequire.NoError(t, err)\n\n\t// Verify BOM is removed and content is correct\n\tassert.True(t, strings.HasPrefix(result, \"Hello, World!\"),\n\t\t\"Content should start with expected text, got: %q\", result)\n\tassert.NotContains(t, result, \"\\uFEFF\",\n\t\t\"BOM character should be removed from output: %q\", result)\n}\n"
  },
  {
    "path": "src/pkg/utils/fzf_utils.go",
    "content": "package utils\n\nimport \"github.com/reinhrst/fzf-lib\"\n\n// Returning a string slice causes inefficiency in current usage\nfunc FzfSearch(query string, source []string) []fzf.MatchResult {\n\tfzfSearcher := fzf.New(source, fzf.DefaultOptions())\n\tfzfSearcher.Search(query)\n\t// TODO : This is a blocking call, which will cause the UI to freeze if the query is slow.\n\t// Need to put a timeout on this\n\tfzfResults := <-fzfSearcher.GetResultChannel()\n\tfzfSearcher.End()\n\treturn fzfResults.Matches\n}\n"
  },
  {
    "path": "src/pkg/utils/log_utils.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n)\n\n// Print line to stderr and exit with status 1\n// Cannot use log.Fataln() as slog.SetDefault() causes those lines to\n// go into log file\nfunc PrintlnAndExit(args ...any) {\n\tfmt.Fprintln(os.Stderr, args...)\n\tos.Exit(1)\n}\n\n// Print formatted output line to stderr and exit with status 1\n// Cannot use log.Fataln() as slog.SetDefault() causes those lines to\n// go into log file\nfunc PrintfAndExitf(format string, args ...any) {\n\tfmt.Fprintf(os.Stderr, format, args...)\n\tos.Exit(1)\n}\n\n// Used in unit test\nfunc SetRootLoggerToStdout(debug bool) {\n\tlevel := slog.LevelInfo\n\tif debug {\n\t\tlevel = slog.LevelDebug\n\t}\n\tslog.SetDefault(slog.New(slog.NewTextHandler(\n\t\tos.Stdout, &slog.HandlerOptions{Level: level})))\n}\n\n// Used in unit test\nfunc SetRootLoggerToDiscarded() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n"
  },
  {
    "path": "src/pkg/utils/shell_utils.go",
    "content": "package utils\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"time\"\n)\n\n// Choose correct shell as per OS\nfunc ExecuteCommandInShell(timeLimit time.Duration, cmdDir string, shellCommand string) (int, string, error) {\n\t// Linux and Darwin\n\tbaseCmd := \"/bin/sh\"\n\targs := []string{\"-c\", shellCommand}\n\n\tif runtime.GOOS == OsWindows {\n\t\tbaseCmd = \"powershell.exe\"\n\t\targs[0] = \"-Command\"\n\t}\n\n\treturn ExecuteCommand(timeLimit, cmdDir, baseCmd, args...)\n}\n\nfunc ExecuteCommand(timeLimit time.Duration, cmdDir string, baseCmd string, args ...string) (int, string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), timeLimit)\n\tdefer cancel()\n\n\tcmd := exec.CommandContext(ctx, baseCmd, args...)\n\tcmd.Dir = cmdDir\n\tDetachFromTerminal(cmd)\n\toutputBytes, err := cmd.CombinedOutput()\n\tretCode := -1\n\n\tif errors.Is(ctx.Err(), context.DeadlineExceeded) {\n\t\tslog.Error(\"User's command timed out\", \"outputBytes\", outputBytes,\n\t\t\t\"cmd error\", err, \"ctx error\", ctx.Err())\n\t\treturn retCode, string(outputBytes), ctx.Err()\n\t}\n\n\tif err == nil {\n\t\tretCode = 0\n\t} else if exitErr, ok := err.(*exec.ExitError); ok { //nolint: errorlint // We dont expect error to be Wrapped here\n\t\tretCode = exitErr.ExitCode()\n\t} else {\n\t\terr = fmt.Errorf(\"unexpected Error in command execution : %w\", err)\n\t}\n\n\treturn retCode, string(outputBytes), err\n}\n"
  },
  {
    "path": "src/pkg/utils/tea_utils.go",
    "content": "package utils\n\nimport tea \"github.com/charmbracelet/bubbletea\"\n\nfunc TeaRuneKeyMsg(msg string) tea.KeyMsg {\n\treturn tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(msg),\n\t}\n}\n"
  },
  {
    "path": "src/pkg/utils/test_utils.go",
    "content": "package utils\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar SampleDataBytes = []byte(\"This is sample\") //nolint: gochecknoglobals // Effectively const\n\ntype TestTOMLType struct {\n\tSampleBool  bool     `toml:\"sample_bool\"`\n\tSampleInt   int      `toml:\"sample_int\"`\n\tSampleStr   string   `toml:\"sample_str\"`\n\tSampleSlice []string `toml:\"sample_slice\"`\n}\n\ntype TestTOMLMissingIgnorerType struct {\n\tSampleBool    bool     `toml:\"sample_bool\"`\n\tSampleInt     int      `toml:\"sample_int\"`\n\tSampleStr     string   `toml:\"sample_str\"`\n\tSampleSlice   []string `toml:\"sample_slice\"`\n\tIgnoreMissing bool     `toml:\"ignore_missing\"`\n}\n\nfunc (t TestTOMLMissingIgnorerType) GetIgnoreMissingFields() bool {\n\treturn t.IgnoreMissing\n}\n\nfunc (t TestTOMLMissingIgnorerType) WithIgnoreMissing(val bool) TestTOMLMissingIgnorerType {\n\tt.IgnoreMissing = val\n\treturn t\n}\n\nfunc SetupDirectories(t *testing.T, dirs ...string) {\n\tt.Helper()\n\tfor _, dir := range dirs {\n\t\tif dir == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\terr := os.MkdirAll(dir, UserDirPerm)\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc SetupFilesWithData(t *testing.T, data []byte, files ...string) {\n\tt.Helper()\n\tfor _, file := range files {\n\t\terr := os.WriteFile(file, data, UserFilePerm)\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc SetupFiles(t *testing.T, files ...string) {\n\tSetupFilesWithData(t, SampleDataBytes, files...)\n}\n"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/default.toml",
    "content": "sample_bool = true \nsample_int = 2\nsample_str = \"hello\"\nsample_slice = ['a', 'b']"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/.gitignore",
    "content": "missing_str2.toml.bak*\nmissing_str_ignore2.toml.bak*"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/default.toml",
    "content": "sample_bool = true \nsample_int = 2\nsample_str = \"hello\"\nsample_slice = ['a', 'b']\nignore_missing = false"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/default_extra_fields.toml",
    "content": "sample_bool = true \nsample_int = 2\nsample_str = \"hello\"\nsample_slice = ['a', 'b']\nignore_missing = false\nsample_extra = 1"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/invalid_format.toml",
    "content": "sample_bool = true \nsample_int = 2\nsample_str = \"hello\"\nsample_slice =\nignore_missing = false"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/invalid_value_type.toml",
    "content": "sample_bool = true \nsample_int = \"hi\"\nsample_str = \"hello\"\nignore_missing = false"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/missing_str2.toml",
    "content": "sample_bool = true\nsample_int = -1\nsample_slice = ['a', 'b']\nignore_missing = false"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/missing_str_ignore.toml",
    "content": "sample_bool = true \nsample_int = 2\nsample_slice = ['a', 'b']\nignore_missing = true"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/missing_str_ignore2.toml",
    "content": "sample_bool = true\nsample_int = -1\nsample_slice = ['a', 'b']\nignore_missing = true"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/ignorer/missing_str_int.toml",
    "content": "sample_bool = true \nsample_slice = ['a', 'b']\nignore_missing = false"
  },
  {
    "path": "src/pkg/utils/testdata/load_toml/missing_str.toml",
    "content": "sample_bool = true \nsample_int = 2\nsample_slice = ['a', 'b']"
  },
  {
    "path": "src/pkg/utils/ui_utils.go",
    "content": "package utils\n\nconst CntFooterPanels = 3\n\nconst BorderPaddingForFooter = 2\n\n// Including borders\nfunc FullFooterHeight(footerHeight int, toggleFooter bool) int {\n\tif toggleFooter {\n\t\treturn footerHeight + BorderPaddingForFooter\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "src/superfile_config/config.toml",
    "content": "##############################################\n#                                            #\n#           Superfile Configuration          #\n#                                            #\n##############################################\n\n# This contains the root config file for superfile! More details can be found at\n# https://superfile.dev/configure/superfile-config/.\n\n###############################################################################\n#                                   Defaults                                  #\n###############################################################################\n\n#-- File Editor\n# Default: $EDITOR\neditor = \"\"\n\n#-- Directory Editor\n# \ndir_editor = \"\"\n\n#-- Auto check for update\nauto_check_update = true\n\n#-- cd on quit\n# Should we cd the shell to the last directory open in superfile when the\n# program exits? \ncd_on_quit = false\n\n#-- File Preview\n# Should we open a file preview by default whenever selection-hovering over a\n# file?\ndefault_open_file_preview = true\n\n#-- Image Preview\n# Should we open an image preview by default whenever selection-hovering over an\n# image?\nshow_image_preview = true\n\n#-- File Info Footer\n# Should we display a footer in the file panel that provides more file information?\nshow_panel_footer_info = true\n\n#-- Default Directory\n# The initial path that the file panel should navigate to when superfile is\n# opened. This setting understands relative paths such as \".\", \"..\", etc.\ndefault_directory = \".\"\n\n#-- File Size Units\n# true: SI decimal units of 1000 (kB, MB, GB).\n# false: IEC binary units of 1024 (KiB, MiB, GiB).\nfile_size_use_si = false\n\n#-- Default File Sort Type\n# (0: Name, 1: Size, 2: Date Modified, 3: Type, 4: Natural).\n# Natural sort treats numeric sequences as numbers (e.g., file2 before file10).\ndefault_sort_type = 0\n\n#-- Sort Order Reversing\n# true: Descending.\n# false: Ascending.\nsort_order_reversed = false\n\n#-- Case-Sensitive Sorting\n# An uppercase \"B\" comes before a lowercase \"a\" if true.\ncase_sensitive_sort = false\n\n#-- Exit Shell on Success\n# Whether to exit the shell on successful command execution.\nshell_close_on_success = false\n\n#-- Page Scroll Size\n# Number of lines to scroll for PgUp/PgDown keys (0: full page, default behavior).\npage_scroll_size = 0\n\n#-- Debug Mode\ndebug = false\n\n#-- Ignore Missing Config Fields\n# Whether to silence any warnings about missing config fields.\nignore_missing_fields = false\n\n#-- File Panel Extra Columns Count\n# Count of extra columns in file panel in addition to file name. When option equal 0 then feature is disabled.\nfile_panel_extra_columns = 0\n\n#-- File name width in File Panel\n# Percentage of file panel width allocated to file names (25-100). Higher values give more space to names, less to extra columns.\nfile_panel_name_percent = 50\n\n\n###############################################################################\n#                                   Styling                                   #\n###############################################################################\n\n#-- Theme\n# Put your theme's name here!\ntheme = \"catppuccin-mocha\"\n\n#-- Code Previewer\n# Whether to use the builtin syntax highlighting with chroma or use bat. Values: \"\" for builtin chroma, \"bat\" for bat\ncode_previewer = \"\"\n\n#-- Nerd Fonts Support\n# Whether to enable support for Nerd Fonts symbols.\n# Requires:  Font patched with the Nerd Fonts patch.\nnerdfont = true\n\n#-- Show checkbox icons in select mode\n# Requires: nerdfont = true\nshow_select_icons = true\n\n#-- Transparent Background Support\n# Set to true to enable background transparency.\n# Requires: terminal support for colour transparency\ntransparent_background = false\n\n#-- File Preview Panel Width\n# Width of the file preview panel will be 1/n of the total width.\n# Values recommended to be in 2–10.\n# Default (0): Use the same width as file picker panel.\nfile_preview_width = 0\n\n#-- File Preview Border\n# Enable border around the file preview panel for better visual separation.\n# Default: false (no border)\nenable_file_preview_border = false\n\n#-- Sidebar Width\n# If you don't want to display the sidebar, you can input 0 directly.\n# Values recommended to be in 5–20.\nsidebar_width = 20\n\n#-- Sidebar Section Order\n# Order of sidebar sections (valid values: \"home\", \"pinned\", \"disks\")\n# Only enabled sections will be displayed\nsidebar_sections = [\"home\", \"pinned\", \"disks\"]\n\n#-- Border\n# Make sure to add strings that are exactly one character wide!\n# Use ' ' for borderless.\nborder_top = '─'\nborder_bottom = '─'\nborder_left = '│'\nborder_right = '│'\nborder_top_left = '╭'\nborder_top_right = '╮'\nborder_bottom_left = '╰'\nborder_bottom_right = '╯'\nborder_middle_left = '├'\nborder_middle_right = '┤'\n\n###############################################################################\n#                                   Plugins                                   #\n###############################################################################\n\n# This section is for using plugins with superfile, external addons that extend\n# the default capabilities of the program! More info can be found at\n# https://superfile.dev/list/plugin-list/.\n\n#-- Detailed Metadata\n# Requires: exiftool\nmetadata = false\n\n#-- MD5 Checksum Generation\n# Requires: md5sum\nenable_md5_checksum = false\n#\n#-- Zoxide Support - Smart directory navigation!\n# Requires: zoxide\nzoxide_support = false\n\n#-- File opening rules\n# Map file extensions to commands used to open them.\n# The file path will be appended as the last argument.\n# MUST BE IN THE VERY END OF THE FILE BECAUSE TOML CANNOT CLOSE TABLES\n# Example:\n#   png = \"feh\"\n#   pdf = \"zathura\"\n#   conf = \"nvim\"\n[open_with]\n"
  },
  {
    "path": "src/superfile_config/hotkeys.toml",
    "content": "##############################################\n#                                            #\n#           Superfile Configuration          #\n#                                            #\n##############################################\n\n# This contains the hotkey config file for superfile! More details can be found at\n# https://superfile.dev/configure/custom-hotkeys/.\n\n###############################################################################\n#                                Global hotkeys                               #\n###############################################################################\n\n# Note: These hotkeys should be unique.\n\n#-- Basic Actions\nconfirm = ['enter', 'right', 'l']\ncd_quit = ['Q', '']\nquit = ['q', 'esc']\n\n#-- Navigation\nlist_down = ['down', 'j']\nlist_up = ['up', 'k']\npage_down = ['pgdown','']\npage_up = ['pgup','']\n\n#-- File Panel Controls\nclose_file_panel = ['w', '']\ncreate_new_file_panel = ['n', '']\nnext_file_panel = ['tab', 'L']\nopen_sort_options_menu = ['o', '']\npinned_directory = ['P', '']\nprevious_file_panel = ['shift+left', 'H']\nsplit_file_panel = ['N', '']\ntoggle_file_preview_panel = ['f', '']\ntoggle_reverse_sort = ['R', '']\n\n#-- Focus Manipulation\nfocus_on_metadata = ['m', '']\nfocus_on_process_bar = ['p', '']\nfocus_on_sidebar = ['s', '']\n\n#-- File/Dir Creation/Renaming\nfile_panel_item_create = ['ctrl+n', '']\nfile_panel_item_rename = ['ctrl+r', '']\n\n#-- Main File Operations\ncopy_items = ['ctrl+c', '']\ncut_items = ['ctrl+x', '']\ndelete_items = ['ctrl+d', 'delete', '']\npaste_items = ['ctrl+v', 'ctrl+w', '']\npermanently_delete_items = ['D', '']\n\n#-- Archive Manipulation\ncompress_file = ['ctrl+a', '']\nextract_file = ['ctrl+e', '']\n\n#-- Editor Actions\nopen_current_directory_with_editor = ['E', '']\nopen_file_with_editor = ['e', '']\n\n#-- Other Actions\nchange_panel_mode = ['v', '']\ncopy_path = ['ctrl+p', '']\ncopy_present_working_directory = ['c', '']\nopen_command_line = [':', '']\nopen_help_menu = ['?', '']\nopen_spf_prompt = ['>', '']\nopen_zoxide = ['z', '']\ntoggle_dot_file = ['.', '']\ntoggle_footer = ['F', '']\n\n###############################################################################\n#                                Typing hotkeys                               #\n###############################################################################\n\n# Note: These hotkeys can override all hotkeys.\n\nconfirm_typing = ['enter', '']\ncancel_typing = ['ctrl+c', 'esc']\n\n###############################################################################\n#                            Mode-Specific Hotkeys                            #\n###############################################################################\n\n# Note: These hotkeys can conflict with other modes, but not with global\n# hotkeys.\n\n#-- Normal Mode Actions\nparent_directory = ['h', 'left', 'backspace']\nsearch_bar = ['/', '']\n\n#-- Selection Mode Actions\nfile_panel_select_mode_items_select_down = ['shift+down', 'J']\nfile_panel_select_mode_items_select_up = ['shift+up', 'K']\nfile_panel_select_all_items = ['A', '']\n"
  },
  {
    "path": "src/superfile_config/theme/0x96f.toml",
    "content": "##############################################\n#                                            #\n#                 0x96f Theme                #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/filipjanevski ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"monokai\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#fcfcfc\"\nfull_screen_bg = \"#262427\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#ff7272\", \"#bcdf59\"]\n\n#-- File Panel\nfile_panel_fg = \"#fcfcfc\"\nfile_panel_bg = \"#262427\"\nfile_panel_border = \"#757075\"\nfile_panel_border_active = \"#ffca58\"\nfile_panel_top_directory_icon = \"#64d2e8\"\nfile_panel_top_path = \"#fcfcfc\"\nfile_panel_item_selected_fg = \"#c6e472\"\nfile_panel_item_selected_bg = \"#262427\"\n\n#-- Footer\nfooter_fg = \"#fcfcfc\"\nfooter_bg = \"#262427\"\nfooter_border = \"#757075\"\nfooter_border_active = \"#ffca58\"\n\n#-- Sidebar\nsidebar_fg = \"#fcfcfc\"\nsidebar_bg = \"#262427\"\nsidebar_title = \"#64d2e8\"\nsidebar_border = \"#757075\"\nsidebar_border_active = \"#baebf6\"\nsidebar_item_selected_fg = \"#c6e472\"\nsidebar_item_selected_bg = \"#262427\"\nsidebar_divider = \"#757075\"\n\n#-- Modals\nmodal_fg = \"#fcfcfc\"\nmodal_bg = \"#262427\"\nmodal_border_active = \"#ffca58\"\nmodal_cancel_fg = \"#262427\"\nmodal_cancel_bg = \"#ff8787\"\nmodal_confirm_fg = \"#262427\"\nmodal_confirm_bg = \"#bcdf59\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#ff8787\"\nhelp_menu_title = \"#64d2e8\"\n\n#-- Special\ncursor = \"#ffca58\"\ncorrect = \"#c6e472\"\nerror = \"#ff8787\"\nhint = \"#baebf6\"\ncancel = \"#fcfcfc\"\n"
  },
  {
    "path": "src/superfile_config/theme/ayu-dark.toml",
    "content": "##############################################\n#                                            #\n#               Ayu Dark Theme               #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/rustnomicon ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"ayu-dark\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#b3b1ad\"\nfull_screen_bg = \"#0f1419\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#FFB454\", \"#36A3D9\"]\n\n#-- File Panel\nfile_panel_fg = \"#b3b1ad\"\nfile_panel_bg = \"#0f1419\"\nfile_panel_border = \"#242936\"\nfile_panel_border_active = \"#ffcc66\"\nfile_panel_top_directory_icon = \"#aad94c\"\nfile_panel_top_path = \"#ffcc66\"\nfile_panel_item_selected_fg = \"#36a3d9\"\nfile_panel_item_selected_bg = \"#1f2430\"\n\n#-- Footer\nfooter_fg = \"#b3b1ad\"\nfooter_bg = \"#0f1419\"\nfooter_border = \"#242936\"\nfooter_border_active = \"#aad94c\"\n\n#-- Sidebar\nsidebar_fg = \"#b3b1ad\"\nsidebar_bg = \"#0f1419\"\nsidebar_title = \"#36a3d9\"\nsidebar_border = \"#1f2430\"\nsidebar_border_active = \"#ff7733\"\nsidebar_item_selected_fg = \"#36a3d9\"\nsidebar_item_selected_bg = \"#1f2430\"\nsidebar_divider = \"#242936\"\n\n#-- Modals\nmodal_fg = \"#b3b1ad\"\nmodal_bg = \"#0f1419\"\nmodal_border_active = \"#5c6773\"\nmodal_cancel_fg = \"#0f1419\"\nmodal_cancel_bg = \"#f07178\"\nmodal_confirm_fg = \"#0f1419\"\nmodal_confirm_bg = \"#36a3d9\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#36a3d9\"\nhelp_menu_title = \"#f07178\"\n\n#-- Special\ncursor = \"#ffcc66\"\ncorrect = \"#aad94c\"\nerror = \"#ff3333\"\nhint = \"#36a3d9\"\ncancel = \"#f07178\"\n"
  },
  {
    "path": "src/superfile_config/theme/blood.toml",
    "content": "##############################################\n#                                            #\n#                 Blood Theme                #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/charlesrocket ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"onedark\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#f8f8f2\"\nfull_screen_bg = \"#000000\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#720000\", \"#ff0000\"]\n\n#-- File Panel\nfile_panel_fg = \"#f8f8f2\"\nfile_panel_bg = \"#000000\"\nfile_panel_border = \"#9a0000\"\nfile_panel_border_active = \"#ff0000\"\nfile_panel_top_directory_icon = \"#ff522e\"\nfile_panel_top_path = \"#ff9999\"\nfile_panel_item_selected_fg = \"#ff8d34\"\nfile_panel_item_selected_bg = \"#524549\"\n\n#-- Footer\nfooter_fg = \"#f8f8f2\"\nfooter_bg = \"#000000\"\nfooter_border = \"#790000\"\nfooter_border_active = \"#ff0000\"\n\n#-- Sidebar\nsidebar_fg = \"#f8f8f2\"\nsidebar_bg = \"#000000\"\nsidebar_title = \"#dd0000\"\nsidebar_border = \"#790000\"\nsidebar_border_active = \"#ff0000\"\nsidebar_item_selected_fg = \"#000000\"\nsidebar_item_selected_bg = \"#ff8d34\"\nsidebar_divider = \"#615250\"\n\n#-- Modals\nmodal_fg = \"#f8f8f2\"\nmodal_bg = \"#000000\"\nmodal_border_active = \"#ff0000\"\nmodal_cancel_fg = \"#f9f9fe\"\nmodal_cancel_bg = \"#000042\"\nmodal_confirm_fg = \"#f9f9fe\"\nmodal_confirm_bg = \"#ffb86c\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#ff8d34\"\nhelp_menu_title = \"#ff6666\"\n\n#-- Special\ncursor = \"#ff0000\"\ncorrect = \"#47ef7d\"\nerror = \"#d70000\"\nhint = \"#5bd9f3\"\ncancel = \"#6575ab\"\n"
  },
  {
    "path": "src/superfile_config/theme/catppuccin-frappe.toml",
    "content": "##############################################\n#                                            #\n#           Catppuccin Frappe Theme          #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/GV14982 ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-frappe\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#a5adce\"\nfull_screen_bg = \"#303446\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#8caaee\", \"#ca9ee6\"]\n\n#-- File Panel\nfile_panel_fg = \"#a5adce\"\nfile_panel_bg = \"#303446\"\nfile_panel_border = \"#737994\"\nfile_panel_border_active = \"#babbf1\"\nfile_panel_top_directory_icon = \"#a6d189\"\nfile_panel_top_path = \"#89b5fa\"\nfile_panel_item_selected_fg = \"#99d1db\"\nfile_panel_item_selected_bg = \"#303446\"\n\n#-- Footer\nfooter_fg = \"#a5adce\"\nfooter_bg = \"#303446\"\nfooter_border = \"#737994\"\nfooter_border_active = \"#a6d189\"\n\n#-- Sidebar\nsidebar_fg = \"#a5adce\"\nsidebar_bg = \"#303446\"\nsidebar_title = \"#85c1dc\"\nsidebar_border = \"#303446\"\nsidebar_border_active = \"#e78284\"\nsidebar_item_selected_fg = \"#99d1db\"\nsidebar_item_selected_bg = \"#303446\"\nsidebar_divider = \"#949cbb\"\n\n#-- Modals\nmodal_fg = \"#a5adce\"\nmodal_bg = \"#303446\"\nmodal_border_active = \"#949cbb\"\nmodal_cancel_fg = \"#414559\"\nmodal_cancel_bg = \"#ea999c\"\nmodal_confirm_fg = \"#414559\"\nmodal_confirm_bg = \"#99d1db\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#99d1db\"\nhelp_menu_title = \"#ea999c\"\n\n#-- Special\ncursor = \"#f2d5cf\"\ncorrect = \"#a6d189\"\nerror = \"#e78284\"\nhint = \"#85c1dc\"\ncancel = \"#ea999c\"\n"
  },
  {
    "path": "src/superfile_config/theme/catppuccin-latte.toml",
    "content": "##############################################\n#                                            #\n#           Catppuccin Latte Theme           #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/GV14982 ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-latte\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#4c4f69\"\nfull_screen_bg = \"#eff1f5\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#1e66f5\", \"#ca9ee6\"]\n\n#-- File Panel\nfile_panel_fg = \"#4c4f69\"\nfile_panel_bg = \"#eff1f5\"\nfile_panel_border = \"#9ca0b0\"\nfile_panel_border_active = \"#7287fd\"\nfile_panel_top_directory_icon = \"#40a02b\"\nfile_panel_top_path = \"#89b5fa\"\nfile_panel_item_selected_fg = \"#04a5e5\"\nfile_panel_item_selected_bg = \"#eff1f5\"\n\n#-- Footer\nfooter_fg = \"#4c4f69\"\nfooter_bg = \"#eff1f5\"\nfooter_border = \"#9ca0b0\"\nfooter_border_active = \"#40a02b\"\n\n#-- Sidebar\nsidebar_fg = \"#4c4f69\"\nsidebar_bg = \"#eff1f5\"\nsidebar_title = \"#209fb5\"\nsidebar_border = \"#eff1f5\"\nsidebar_border_active = \"#40a02b\"\nsidebar_item_selected_fg = \"#04a5e5\"\nsidebar_item_selected_bg = \"#eff1f5\"\nsidebar_divider = \"#7c7f93\"\n\n#-- Modals\nmodal_fg = \"#4c4f69\"\nmodal_bg = \"#eff1f5\"\nmodal_border_active = \"#7c7f93\"\nmodal_cancel_fg = \"#eff1f5\"\nmodal_cancel_bg = \"#e64553\"\nmodal_confirm_fg = \"#eff1f5\"\nmodal_confirm_bg = \"#04a5e5\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#04a5e5\"\nhelp_menu_title = \"#fe640b\"\n\n#-- Special\ncursor = \"#dc8a78\"\ncorrect = \"#40a02b\"\nerror = \"#d20f39\"\nhint = \"#209fb5\"\ncancel = \"#e64553\"\n"
  },
  {
    "path": "src/superfile_config/theme/catppuccin-macchiato.toml",
    "content": "##############################################\n#                                            #\n#         Catppuccin Macchiato Theme         #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/GV14982 ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-macchiato\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#a5adcb\"\nfull_screen_bg = \"#24273a\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#8aadf4\", \"#c6a0f6\"]\n\n#-- File Panel\nfile_panel_fg = \"#a5adcb\"\nfile_panel_bg = \"#24273a\"\nfile_panel_border = \"#6e738d\"\nfile_panel_border_active = \"#b7bdf8\"\nfile_panel_top_directory_icon = \"#a6da95\"\nfile_panel_top_path = \"#8aadf4\"\nfile_panel_item_selected_fg = \"#91d7e3\"\nfile_panel_item_selected_bg = \"#24273a\"\n\n#-- Footer\nfooter_fg = \"#a5adcb\"\nfooter_bg = \"#24273a\"\nfooter_border = \"#6e738d\"\nfooter_border_active = \"#a6da95\"\n\n#-- Sidebar\nsidebar_fg = \"#a5adcb\"\nsidebar_bg = \"#24273a\"\nsidebar_title = \"#7dc4e4\"\nsidebar_border = \"#24273a\"\nsidebar_border_active = \"#ed8796\"\nsidebar_item_selected_fg = \"#91d7e3\"\nsidebar_item_selected_bg = \"#24273a\"\nsidebar_divider = \"#939ab7\"\n\n#-- Modals\nmodal_fg = \"#a5adcb\"\nmodal_bg = \"#24273a\"\nmodal_border_active = \"#939ab7\"\nmodal_cancel_fg = \"#363a4f\"\nmodal_cancel_bg = \"#ee99a0\"\nmodal_confirm_fg = \"#363a4f\"\nmodal_confirm_bg = \"#91d7e3\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#91d7e3\"\nhelp_menu_title = \"#ee99a0\"\n\n#-- Special\ncursor = \"#f4dbd6\"\ncorrect = \"#a6da95\"\nerror = \"#ed8796\"\nhint = \"#7dc4e4\"\ncancel = \"#ee99a0\"\n"
  },
  {
    "path": "src/superfile_config/theme/catppuccin-mocha.toml",
    "content": "##############################################\n#                                            #\n#           Catppuccin Mocha Theme           #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/AnshumanNeon ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-mocha\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#a6adc8\"\nfull_screen_bg = \"#1e1e2e\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#89b4fa\", \"#cba6f7\"]\n\n#-- File Panel\nfile_panel_fg = \"#a6adc8\"\nfile_panel_bg = \"#1e1e2e\"\nfile_panel_border = \"#6c7086\"\nfile_panel_border_active = \"#b4befe\"\nfile_panel_top_directory_icon = \"#a6e3a1\"\nfile_panel_top_path = \"#89b5fa\"\nfile_panel_item_selected_fg = \"#98D0FD\"\nfile_panel_item_selected_bg = \"#1e1e2e\"\n\n#-- Footer\nfooter_fg = \"#a6adc8\"\nfooter_bg = \"#1e1e2e\"\nfooter_border = \"#6c7086\"\nfooter_border_active = \"#a6e3a1\"\n\n#-- Sidebar\nsidebar_fg = \"#a6adc8\"\nsidebar_bg = \"#1e1e2e\"\nsidebar_title = \"#74c7ec\"\nsidebar_border = \"#1e1e2e\"\nsidebar_border_active = \"#f38ba8\"\nsidebar_item_selected_fg = \"#A6DBF7\"\nsidebar_item_selected_bg = \"#1e1e2e\"\nsidebar_divider = \"#868686\"\n\n#-- Modals\nmodal_fg = \"#a6adc8\"\nmodal_bg = \"#1e1e2e\"\nmodal_border_active = \"#868686\"\nmodal_cancel_fg = \"#383838\"\nmodal_cancel_bg = \"#eba0ac\"\nmodal_confirm_fg = \"#383838\"\nmodal_confirm_bg = \"#89dceb\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#89dceb\"\nhelp_menu_title = \"#eba0ac\"\n\n#-- Special\ncursor = \"#f5e0dc\"\ncorrect = \"#a6e3a1\"\nerror = \"#f38ba8\"\nhint = \"#73c7ec\"\ncancel = \"#eba0ac\"\n"
  },
  {
    "path": "src/superfile_config/theme/dracula.toml",
    "content": "##############################################\n#                                            #\n#                Dracula Theme               #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/BeanieBarrow ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"dracula\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#f8f8f2\"\nfull_screen_bg = \"#282a36\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#50fa7b\", \"#ff5555\"]\n\n#-- File Panel\nfile_panel_fg = \"#f8f8f2\"\nfile_panel_bg = \"#282a36\"\nfile_panel_border = \"#6272a4\"\nfile_panel_border_active = \"#44475a\"\nfile_panel_top_directory_icon = \"#50fa7b\"\nfile_panel_top_path = \"#8be9fd\"\nfile_panel_item_selected_fg = \"#ffb86c\"\nfile_panel_item_selected_bg = \"#282a36\"\n\n#-- Footer\nfooter_fg = \"#f8f8f2\"\nfooter_bg = \"#282a36\"\nfooter_border = \"#6272a4\"\nfooter_border_active = \"#44475a\"\n\n#-- Sidebar\nsidebar_fg = \"#f8f8f2\"\nsidebar_bg = \"#282a36\"\nsidebar_title = \"#bd93f9\"\nsidebar_border = \"#282a36\"\nsidebar_border_active = \"#44475a\"\nsidebar_item_selected_fg = \"#ffb86c\"\nsidebar_item_selected_bg = \"#282a36\"\nsidebar_divider = \"#868686\"\n\n#-- Modals\nmodal_fg = \"#f8f8f2\"\nmodal_bg = \"#282a36\"\nmodal_border_active = \"#44475a\"\nmodal_cancel_fg = \"#f8f8f2\"\nmodal_cancel_bg = \"#6272a4\"\nmodal_confirm_fg = \"#f8f8f2\"\nmodal_confirm_bg = \"#ffb86c\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#ffb86c\"\nhelp_menu_title = \"#bd93f9\"\n\n#-- Special\ncursor = \"#ff79c6\"\ncorrect = \"#50fa7b\"\nerror = \"#ff5555\"\nhint = \"#8be9fd\"\ncancel = \"#6272a4\"\n"
  },
  {
    "path": "src/superfile_config/theme/everforest-dark-hard.toml",
    "content": "##############################################\n#                                            #\n#         Everforest Dark Hard Theme         #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/fzahner ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"evergarden\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#d3c6aa\"\nfull_screen_bg = \"#272e33\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#a7c080\", \"#e67e80\"]\n\n#-- File Panel\nfile_panel_fg = \"#d3c6aa\"\nfile_panel_bg = \"#272e33\"\nfile_panel_border = \"#859289\"\nfile_panel_border_active = \"#dbbc7f\"\nfile_panel_top_directory_icon = \"#a7c080\"\nfile_panel_top_path = \"#7fbbb3\"\nfile_panel_item_selected_fg = \"#d699b6\"\nfile_panel_item_selected_bg = \"#232a2e\"\n\n#-- Footer\nfooter_fg = \"#d3c6aa\"\nfooter_bg = \"#272e33\"\nfooter_border = \"#859289\"\nfooter_border_active = \"#d3c6aa\"\n\n#-- Sidebar\nsidebar_fg = \"#d3c6aa\"\nsidebar_bg = \"#272e33\"\nsidebar_title = \"#d699b6\"\nsidebar_border = \"#2d353b\"\nsidebar_border_active = \"#d3c6aa\"\nsidebar_item_selected_fg = \"#e69875\"\nsidebar_item_selected_bg = \"#2d353b\"\nsidebar_divider = \"#859289\"\n\n#-- Modals\nmodal_fg = \"#d3c6aa\"\nmodal_bg = \"#272e33\"\nmodal_border_active = \"#859289\"\nmodal_cancel_fg = \"#d3c6aa\"\nmodal_cancel_bg = \"#232a2e\"\nmodal_confirm_fg = \"#d3c6aa\"\nmodal_confirm_bg = \"#e69875\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#a7c080\"\nhelp_menu_title = \"#e69875\"\n\n#-- Special\ncursor = \"#a7c080\"\ncorrect = \"#a7c080\"\nerror = \"#e67e80\"\nhint = \"#7fbbb3\"\ncancel = \"#859289\"\n"
  },
  {
    "path": "src/superfile_config/theme/everforest-dark-medium.toml",
    "content": "##############################################\n#                                            #\n#        Everforest Dark Medium Theme        #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/dotintegral ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-macchiato\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#d3c6aa\"\nfull_screen_bg = \"#2d353b\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#a7c080\", \"#e67e80\"]\n\n#-- File Panel\nfile_panel_fg = \"#d3c6aa\"\nfile_panel_bg = \"#2d353b\"\nfile_panel_border = \"#859289\"\nfile_panel_border_active = \"#fff1c5\"\nfile_panel_top_directory_icon = \"#a7c080\"\nfile_panel_top_path = \"#7fbbb3\"\nfile_panel_item_selected_fg = \"#d699b6\"\nfile_panel_item_selected_bg = \"#232a2e\"\n\n#-- Footer\nfooter_fg = \"#d3c6aa\"\nfooter_bg = \"#2d353b\"\nfooter_border = \"#859289\"\nfooter_border_active = \"#dbbc7f\"\n\n#-- Sidebar\nsidebar_fg = \"#d3c6aa\"\nsidebar_bg = \"#2d353b\"\nsidebar_title = \"#d699b6\"\nsidebar_border = \"#2d353b\"\nsidebar_border_active = \"#dbbc7f\"\nsidebar_item_selected_fg = \"#e69875\"\nsidebar_item_selected_bg = \"#2d353b\"\nsidebar_divider = \"#859289\"\n\n#-- Modals\nmodal_fg = \"#d3c6aa\"\nmodal_bg = \"#2d353b\"\nmodal_border_active = \"#859289\"\nmodal_cancel_fg = \"#d3c6aa\"\nmodal_cancel_bg = \"#232a2e\"\nmodal_confirm_fg = \"#d3c6aa\"\nmodal_confirm_bg = \"#e69875\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#a7c080\"\nhelp_menu_title = \"#e69875\"\n\n#-- Special\ncursor = \"#a7c080\"\ncorrect = \"#a7c080\"\nerror = \"#e67e80\"\nhint = \"#7fbbb3\"\ncancel = \"#859289\"\n"
  },
  {
    "path": "src/superfile_config/theme/gruvbox-dark-hard.toml",
    "content": "##############################################\n#                                            #\n#           Gruvbox Dark Hard Theme          #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/frost-phoenix ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"gruvbox\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#fbf1c7\"\nfull_screen_bg = \"#1d2021\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#fb4934\", \"#b8bb26\"]\n\n#-- File Panel\nfile_panel_fg = \"#fbf1c7\"\nfile_panel_bg = \"#1d2021\"\nfile_panel_border = \"#fbf1c7\"\nfile_panel_border_active = \"#98971a\"\nfile_panel_top_directory_icon = \"#689d6a\"\nfile_panel_top_path = \"#458588\"\nfile_panel_item_selected_fg = \"#d65d0e\"\nfile_panel_item_selected_bg = \"\"\n\n#-- Footer\nfooter_fg = \"#fbf1c7\"\nfooter_bg = \"#1d2021\"\nfooter_border = \"#928374\"\nfooter_border_active = \"#d79921\"\n\n#-- Sidebar\nsidebar_fg = \"#fbf1c7\"\nsidebar_bg = \"#1d2021\"\nsidebar_title = \"#b16286\"\nsidebar_border = \"#928374\"\nsidebar_border_active = \"#b16286\"\nsidebar_item_selected_fg = \"#d65d0e\"\nsidebar_item_selected_bg = \"\"\nsidebar_divider = \"#928374\"\n\n#-- Modals\nmodal_fg = \"#fbf1c7\"\nmodal_bg = \"#1d2021\"\nmodal_border_active = \"#689d6a\"\nmodal_cancel_fg = \"#fb4934\"\nmodal_cancel_bg = \"\"\nmodal_confirm_fg = \"#b8bb26\"\nmodal_confirm_bg = \"\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#689d6a\"\nhelp_menu_title = \"#b16286\"\n\n#-- Special\ncursor = \"#689d6a\"\ncorrect = \"#98971a\"\nerror = \"#ff6969\"\nhint = \"#468588\"\ncancel = \"#838383\"\n"
  },
  {
    "path": "src/superfile_config/theme/gruvbox.toml",
    "content": "##############################################\n#                                            #\n#                Gruvbox Theme               #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/yorukot ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"gruvbox\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#ebdbb2\"\nfull_screen_bg = \"#282828\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#689d6a\", \"#fb4934\"]\n\n#-- File Panel\nfile_panel_fg = \"#ebdbb2\"\nfile_panel_bg = \"#282828\"\nfile_panel_border = \"#868686\"\nfile_panel_border_active = \"#fff1c5\"\nfile_panel_top_directory_icon = \"#8ec07c\"\nfile_panel_top_path = \"#458588\"\nfile_panel_item_selected_fg = \"#d3869b\"\nfile_panel_item_selected_bg = \"#282828\"\n\n#-- Footer\nfooter_fg = \"#ebdbb2\"\nfooter_bg = \"#282828\"\nfooter_border = \"#868686\"\nfooter_border_active = \"#d79921\"\n\n#-- Sidebar\nsidebar_fg = \"#ebdbb2\"\nsidebar_bg = \"#282828\"\nsidebar_title = \"#cc241d\"\nsidebar_border = \"#282828\"\nsidebar_border_active = \"#d79921\"\nsidebar_item_selected_fg = \"#e8751a\"\nsidebar_item_selected_bg = \"#282828\"\nsidebar_divider = \"#868686\"\n\n#-- Modals\nmodal_fg = \"#ebdbb2\"\nmodal_bg = \"#282828\"\nmodal_border_active = \"#868686\"\nmodal_cancel_fg = \"#ebdbb2\"\nmodal_cancel_bg = \"#6d6d6d\"\nmodal_confirm_fg = \"#ebdbb2\"\nmodal_confirm_bg = \"#ff4d00\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#8ec07c\"\nhelp_menu_title = \"#ff4d00\"\n\n#-- Special\ncursor = \"#8ec07c\"\ncorrect = \"#8ec07c\"\nerror = \"#ff6969\"\nhint = \"#458588\"\ncancel = \"#838383\"\n"
  },
  {
    "path": "src/superfile_config/theme/hacks.toml",
    "content": "##############################################\n#                                            #\n#                 Hacks Theme                #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/charlesrocket ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"onedark\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#f8f8f2\"\nfull_screen_bg = \"#000000\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#00ff00\", \"#afff00\"]\n\n#-- File Panel\nfile_panel_fg = \"#f8f8f2\"\nfile_panel_bg = \"#000000\"\nfile_panel_border = \"#afff00\"\nfile_panel_border_active = \"#6532ff\"\nfile_panel_top_directory_icon = \"#afff00\"\nfile_panel_top_path = \"#afff00\"\nfile_panel_item_selected_fg = \"#ff8d34\"\nfile_panel_item_selected_bg = \"#524549\"\n\n#-- Footer\nfooter_fg = \"#f8f8f2\"\nfooter_bg = \"#000000\"\nfooter_border = \"#afff00\"\nfooter_border_active = \"#6532ff\"\n\n#-- Sidebar\nsidebar_fg = \"#f8f8f2\"\nsidebar_bg = \"#000000\"\nsidebar_title = \"#afff00\"\nsidebar_border = \"#afff00\"\nsidebar_border_active = \"#6532ff\"\nsidebar_item_selected_fg = \"#000000\"\nsidebar_item_selected_bg = \"#ff8d34\"\nsidebar_divider = \"#615250\"\n\n#-- Modals\nmodal_fg = \"#f8f8f2\"\nmodal_bg = \"#000000\"\nmodal_border_active = \"#6532ff\"\nmodal_cancel_fg = \"#f9f9fe\"\nmodal_cancel_bg = \"#000042\"\nmodal_confirm_fg = \"#f9f9fe\"\nmodal_confirm_bg = \"#ffb86c\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#ff8d34\"\nhelp_menu_title = \"#afff00\"\n\n#-- Special\ncursor = \"#ff0000\"\ncorrect = \"#47ef7d\"\nerror = \"#d70000\"\nhint = \"#5bd9f3\"\ncancel = \"#6575ab\"\n"
  },
  {
    "path": "src/superfile_config/theme/kaolin.toml",
    "content": "##############################################\n#                                            #\n#                Kaolin Theme                #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/AnshumanNeon ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-macchiato\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#efefef\"\nfull_screen_bg = \"#17171a\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#74b09a\", \"#c74a4d\"]\n\n#-- File Panel\nfile_panel_fg = \"#efefef\"\nfile_panel_bg = \"#17171a\"\nfile_panel_border = \"#74b09a\"\nfile_panel_border_active = \"#74b09a\"\nfile_panel_top_directory_icon = \"#f5c791\"\nfile_panel_top_path = \"#d7936d\"\nfile_panel_item_selected_fg = \"#4fa8a3\"\nfile_panel_item_selected_bg = \"#17171a\"\n\n#-- Footer\nfooter_fg = \"#efefef\"\nfooter_bg = \"#17171a\"\nfooter_border = \"#74b09a\"\nfooter_border_active = \"#57b2c2\"\n\n#-- Sidebar\nsidebar_fg = \"#efefef\"\nsidebar_bg = \"#17171a\"\nsidebar_title = \"#f5c791\"\nsidebar_border = \"#17171a\"\nsidebar_border_active = \"#57b2c2\"\nsidebar_item_selected_fg = \"#ba667d\"\nsidebar_item_selected_bg = \"#17171a\"\nsidebar_divider = \"#868686\"\n\n#-- Modals\nmodal_fg = \"#efefef\"\nmodal_bg = \"#17171a\"\nmodal_border_active = \"#868686\"\nmodal_cancel_fg = \"#eedcc1\"\nmodal_cancel_bg = \"#c74a4d\"\nmodal_confirm_fg = \"#eedcc1\"\nmodal_confirm_bg = \"#3e594a\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#4fa8a3\"\nhelp_menu_title = \"#f5c791\"\n\n#-- Special\ncursor = \"#f5c791\"\ncorrect = \"#74b09a\"\nerror = \"#c74a4d\"\nhint = \"#4fa8a3\"\ncancel = \"#d7936d\"\n"
  },
  {
    "path": "src/superfile_config/theme/monokai.toml",
    "content": "##############################################\n#                                            #\n#                Monokai Theme               #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/CommandJoo ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"monokai\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#f8f8f2\"\nfull_screen_bg = \"#272822\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#66d9ef\", \"#ae81ff\"]\n\n#-- File Panel\nfile_panel_fg = \"#f8f8f2\"\nfile_panel_bg = \"#272822\"\nfile_panel_border = \"#75715e\"\nfile_panel_border_active = \"#66d9ef\"\nfile_panel_top_directory_icon = \"#e6db74\"\nfile_panel_top_path = \"#e6db74\"\nfile_panel_item_selected_fg = \"#66d9ef\"\nfile_panel_item_selected_bg = \"#2e2e2e\"\n\n#-- Footer\nfooter_fg = \"#f8f8f2\"\nfooter_bg = \"#272822\"\nfooter_border = \"#75715e\"\nfooter_border_active = \"#a9dc76\"\n\n#-- Sidebar\nsidebar_fg = \"#f8f8f2\"\nsidebar_bg = \"#272822\"\nsidebar_title = \"#66d9ef\"\nsidebar_border = \"#75715e\"\nsidebar_border_active = \"#f92672\"\nsidebar_item_selected_fg = \"#66d9ef\"\nsidebar_item_selected_bg = \"#272822\"\nsidebar_divider = \"#75715e\"\n\n#-- Modals\nmodal_fg = \"#f8f8f2\"\nmodal_bg = \"#272822\"\nmodal_border_active = \"#66d9ef\"\nmodal_cancel_fg = \"#75715e\"\nmodal_cancel_bg = \"#f92672\"\nmodal_confirm_fg = \"#75715e\"\nmodal_confirm_bg = \"#66d9ef\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#66d9ef\"\nhelp_menu_title = \"#ae81ff\"\n\n#-- Special\ncursor = \"#66d9ef\"\ncorrect = \"#a6e22e\"\nerror = \"#f92672\"\nhint = \"#66d9ef\"\ncancel = \"#e6db74\"\n"
  },
  {
    "path": "src/superfile_config/theme/nord.toml",
    "content": "##############################################\n#                                            #\n#                 Nord Theme                 #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/rames-eltany ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"nord\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#e5e9f0\"\nfull_screen_bg = \"#2e3440\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#81a1c1\", \"#bf616a\"]\n\n#-- File Panel\nfile_panel_fg = \"#e5e9f0\"\nfile_panel_bg = \"#2e3440\"\nfile_panel_border = \"#4c566a\"\nfile_panel_border_active = \"#d8dee9\"\nfile_panel_top_directory_icon = \"#88c0d0\"\nfile_panel_top_path = \"#88c0d0\"\nfile_panel_item_selected_fg = \"#bf616a\"\nfile_panel_item_selected_bg = \"#2e3440\"\n\n#-- Footer\nfooter_fg = \"#e5e9f0\"\nfooter_bg = \"#2e3440\"\nfooter_border = \"#4c566a\"\nfooter_border_active = \"#b48ead\"\n\n#-- Sidebar\nsidebar_fg = \"#e5e9f0\"\nsidebar_bg = \"#2e3440\"\nsidebar_title = \"#81a1c1\"\nsidebar_border = \"#2e3440\"\nsidebar_border_active = \"#b48ead\"\nsidebar_item_selected_fg = \"#88c0d0\"\nsidebar_item_selected_bg = \"#2e3440\"\nsidebar_divider = \"#868686\"\n\n#-- Modals\nmodal_fg = \"#e5e9f0\"\nmodal_bg = \"#2e3440\"\nmodal_border_active = \"#868686\"\nmodal_cancel_fg = \"#e5e9f0\"\nmodal_cancel_bg = \"#4c566a\"\nmodal_confirm_fg = \"#e5e9f0\"\nmodal_confirm_bg = \"#bf616a\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#8fbcbb\"\nhelp_menu_title = \"#81a1c1\"\n\n#-- Special\ncursor = \"#88c0d0\"\ncorrect = \"#88c0d0\"\nerror = \"#bf616a\"\nhint = \"#8fbcbb\"\ncancel = \"#d8dee9\"\n"
  },
  {
    "path": "src/superfile_config/theme/onedark.toml",
    "content": "##############################################\n#                                            #\n#                OneDark Theme               #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/CommandJoo ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"onedark\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#a7aab0\"\nfull_screen_bg = \"#2c2d31\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#68aee8\", \"#bb70d2\"]\n\n#-- File Panel\nfile_panel_fg = \"#a7aab0\"\nfile_panel_bg = \"#232326\"\nfile_panel_border = \"#737994\"\nfile_panel_border_active = \"#57a5e5\"\nfile_panel_top_directory_icon = \"#dbb671\"\nfile_panel_top_path = \"#dbb671\"\nfile_panel_item_selected_fg = \"#51a8b3\"\nfile_panel_item_selected_bg = \"#2c2d31\"\n\n#-- Footer\nfooter_fg = \"#a7aab0\"\nfooter_bg = \"#232326\"\nfooter_border = \"#737994\"\nfooter_border_active = \"#8fb573\"\n\n#-- Sidebar\nsidebar_fg = \"#a7aab0\"\nsidebar_bg = \"#232326\"\nsidebar_title = \"#57a5e5\"\nsidebar_border = \"#737994\"\nsidebar_border_active = \"#de5d68\"\nsidebar_item_selected_fg = \"#51a8b3\"\nsidebar_item_selected_bg = \"#2c2d31\"\nsidebar_divider = \"#818387\"\n\n#-- Modals\nmodal_fg = \"#a7aab0\"\nmodal_bg = \"#35363b\"\nmodal_border_active = \"#51a8b3\"\nmodal_cancel_fg = \"#414559\"\nmodal_cancel_bg = \"#de5d68\"\nmodal_confirm_fg = \"#414559\"\nmodal_confirm_bg = \"#51a8b3\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#51a8b3\"\nhelp_menu_title = \"#bb70d2\"\n\n#-- Special\ncursor = \"#68aee8\"\ncorrect = \"#a6d189\"\nerror = \"#de5d68\"\nhint = \"#68aee8\"\ncancel = \"#c49060\"\n"
  },
  {
    "path": "src/superfile_config/theme/poimandres.toml",
    "content": "##############################################\n#                                            #\n#              Poimandres Theme              #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/Myles-J ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-mocha\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#cdd6f4\"\nfull_screen_bg = \"#1b1d24\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#a6e3a1\", \"#f38ba8\"]\n\n#-- File Panel\nfile_panel_fg = \"#cdd6f4\"\nfile_panel_bg = \"#1b1d24\"\nfile_panel_border = \"#6c7086\"\nfile_panel_border_active = \"#7fbbb3\"\nfile_panel_top_directory_icon = \"#a6e3a1\"\nfile_panel_top_path = \"#89b4fa\"\nfile_panel_item_selected_fg = \"#a6e3a1\"\nfile_panel_item_selected_bg = \"#2a303c\"\n\n#-- Footer\nfooter_fg = \"#cdd6f4\"\nfooter_bg = \"#1b1d24\"\nfooter_border = \"#6c7086\"\nfooter_border_active = \"#7fbbb3\"\n\n#-- Sidebar\nsidebar_fg = \"#cdd6f4\"\nsidebar_bg = \"#1b1d24\"\nsidebar_title = \"#cba6f7\"\nsidebar_border = \"#2a303c\"\nsidebar_border_active = \"#7fbbb3\"\nsidebar_item_selected_fg = \"#a6e3a1\"\nsidebar_item_selected_bg = \"#2a303c\"\nsidebar_divider = \"#6c7086\"\n\n#-- Modals\nmodal_fg = \"#cdd6f4\"\nmodal_bg = \"#1b1d24\"\nmodal_border_active = \"#7fbbb3\"\nmodal_cancel_fg = \"#cdd6f4\"\nmodal_cancel_bg = \"#6c7086\"\nmodal_confirm_fg = \"#cdd6f4\"\nmodal_confirm_bg = \"#4a4e69\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#a6e3a1\"\nhelp_menu_title = \"#cba6f7\"\n\n#-- Special\ncursor = \"#74c7ec\"\ncorrect = \"#a6e3a1\"\nerror = \"#f38ba8\"\nhint = \"#89b4fa\"\ncancel = \"#6c7086\"\n"
  },
  {
    "path": "src/superfile_config/theme/rose-pine.toml",
    "content": "##############################################\n#                                            #\n#               Rose Pine Theme              #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/pearcidar ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"rose-pine\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#e0def4\"\nfull_screen_bg = \"#191724\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#31784f\", \"#eb6f92\"]\n\n#-- File Panel\nfile_panel_fg = \"#e0def4\"\nfile_panel_bg = \"#191724\"\nfile_panel_border = \"#403d52\"\nfile_panel_border_active = \"#6e6e86\"\nfile_panel_top_directory_icon = \"#9ccfd8\"\nfile_panel_top_path = \"#ebbcba\"\nfile_panel_item_selected_fg = \"#c4a7e7\"\nfile_panel_item_selected_bg = \"#191724\"\n\n#-- Footer\nfooter_fg = \"#e0def4\"\nfooter_bg = \"#191724\"\nfooter_border = \"#403d52\"\nfooter_border_active = \"#f6c177\"\n\n#-- Sidebar\nsidebar_fg = \"#e0def4\"\nsidebar_bg = \"#191724\"\nsidebar_title = \"#6e6e86\"\nsidebar_border = \"#191724\"\nsidebar_border_active = \"#c4a7e7\"\nsidebar_item_selected_fg = \"#f6c177\"\nsidebar_item_selected_bg = \"#191724\"\nsidebar_divider = \"#868686\"\n\n#-- Modals\nmodal_fg = \"#e0def4\"\nmodal_bg = \"#191724\"\nmodal_border_active = \"#868686\"\nmodal_cancel_fg = \"#e0def4\"\nmodal_cancel_bg = \"#524f67\"\nmodal_confirm_fg = \"#e0def4\"\nmodal_confirm_bg = \"#eb6f92\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#f6c177\"\nhelp_menu_title = \"#9ccfd8\"\n\n#-- Special\ncursor = \"#9ccfd8\"\ncorrect = \"#8ec07c\"\nerror = \"#ff6969\"\nhint = \"#31784f\"\ncancel = \"#838383\"\n"
  },
  {
    "path": "src/superfile_config/theme/sugarplum.toml",
    "content": "##############################################\n#                                            #\n#               Sugarplum Theme              #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/lemonlime0x3C33 ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-macchiato\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#db7ddd\"\nfull_screen_bg = \"#111147\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#249a84\", \"#5ca8dc\"]\n\n#-- File Panel\nfile_panel_fg = \"#db7ddd\"\nfile_panel_bg = \"#111147\"\nfile_panel_border = \"#a175d4\"\nfile_panel_border_active = \"#53aaa1\"\nfile_panel_top_directory_icon = \"#249a84\"\nfile_panel_top_path = \"#249a84\"\nfile_panel_item_selected_fg = \"#53b397\"\nfile_panel_item_selected_bg = \"#53b397\"\n\n#-- Footer\nfooter_fg = \"#5ca8dc\"\nfooter_bg = \"#111147\"\nfooter_border = \"#a175d4\"\nfooter_border_active = \"#53aaa1\"\n\n#-- Sidebar\nsidebar_fg = \"#d0beee\"\nsidebar_bg = \"#111147\"\nsidebar_title = \"#db7ddd\"\nsidebar_border = \"#a175d4\"\nsidebar_border_active = \"#53aaa1\"\nsidebar_item_selected_fg = \"#249a84\"\nsidebar_item_selected_bg = \"#111147\"\nsidebar_divider = \"#565f89\"\n\n#-- Modals\nmodal_fg = \"#98c7a3\"\nmodal_bg = \"#111147\"\nmodal_border_active = \"#53aaa1\"\nmodal_cancel_fg = \"#7c4094\"\nmodal_cancel_bg = \"#7c4094\"\nmodal_confirm_fg = \"#7c4094\"\nmodal_confirm_bg = \"#7c4094\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#7dcfff\"\nhelp_menu_title = \"#73daca\"\n\n#-- Special\ncursor = \"#53b397\"\ncorrect = \"#524094\"\nerror = \"#2082a6\"\nhint = \"#91d4c2\"\ncancel = \"#b53dff\"\n"
  },
  {
    "path": "src/superfile_config/theme/tokyonight.toml",
    "content": "##############################################\n#                                            #\n#              Tokyo Night Theme             #\n#                                            #\n##############################################\n\n# This theme was created by: https://github.com/pearcidar ! Thank you <3\n\n# This contains the theme config file for superfile! For more details see:\n# https://superfile.dev/configure/custom-theme/\n\n###############################################################################\n#                           Code Syntax Highlighting                          #\n###############################################################################\n\n# Find one you like at: https://github.com/alecthomas/chroma/blob/master/styles.\n\ncode_syntax_highlight = \"catppuccin-macchiato\"\n\n###############################################################################\n#                                 Base Colors                                 #\n###############################################################################\n\n#-- Full Screen\nfull_screen_fg = \"#a9b1d6\"\nfull_screen_bg = \"#1a1b26\"\n\n#-- Gradient\n# Note: This currently only supports two colors.\ngradient_color = [\"#7aa2f7\", \"#bb9af7\"]\n\n#-- File Panel\nfile_panel_fg = \"#a9b1d6\"\nfile_panel_bg = \"#1a1b26\"\nfile_panel_border = \"#414868\"\nfile_panel_border_active = \"#b4befe\"\nfile_panel_top_directory_icon = \"#73daca\"\nfile_panel_top_path = \"#7aa2f7\"\nfile_panel_item_selected_fg = \"#2ac3de\"\nfile_panel_item_selected_bg = \"#1a1b26\"\n\n#-- Footer\nfooter_fg = \"#a9b1d6\"\nfooter_bg = \"#1a1b26\"\nfooter_border = \"#414868\"\nfooter_border_active = \"#73daca\"\n\n#-- Sidebar\nsidebar_fg = \"#a9b1d6\"\nsidebar_bg = \"#1a1b26\"\nsidebar_title = \"#73daca\"\nsidebar_border = \"#24283b\"\nsidebar_border_active = \"#f7768e\"\nsidebar_item_selected_fg = \"#7dcfff\"\nsidebar_item_selected_bg = \"#1a1b26\"\nsidebar_divider = \"#565f89\"\n\n#-- Modals\nmodal_fg = \"#a9b1d6\"\nmodal_bg = \"#1a1b26\"\nmodal_border_active = \"#73daca\"\nmodal_cancel_fg = \"#24383b\"\nmodal_cancel_bg = \"#e0af68\"\nmodal_confirm_fg = \"#24283b\"\nmodal_confirm_bg = \"#9ece6a\"\n\n#-- Help Menu\nhelp_menu_hotkey = \"#7dcfff\"\nhelp_menu_title = \"#73daca\"\n\n#-- Special\ncursor = \"#ff9e64\"\ncorrect = \"#9ece6a\"\nerror = \"#f7768e\"\nhint = \"#7dcfff\"\ncancel = \"#ff9e64\"\n"
  },
  {
    "path": "src/superfile_config/vimHotkeys.toml",
    "content": "##############################################\n#                                            #\n#         Superfile vim-like Hotkeys         #\n#                                            #\n##############################################\n\n#-- Maintainer: nonepork <https://github.com/nonepork>\n\n# This contains a hotkey config file for superfile, that's themed around vim\n# controls! More details can be found at\n# https://superfile.dev/configure/custom-hotkeys/.\n\n###############################################################################\n#                                Global hotkeys                               #\n###############################################################################\n\n# Note: These hotkeys should be unique.\n\n#-- Basic Actions\nconfirm = ['enter', '']\nquit = ['ctrl+c', ''] # a.k.a. \"theprimeagen troller\"\ncd_quit = ['Q', '']\n\n#-- Navigation\nlist_up = ['k', '']\nlist_down = ['j', '']\npage_up = ['pgup','']\npage_down = ['pgdown','']\n\n#-- File Panel Controls\ncreate_new_file_panel = ['n', '']\nclose_file_panel = ['q', '']\nnext_file_panel = ['tab', '']\nprevious_file_panel = ['shift+tab', '']\nsplit_file_panel = ['N', '']\ntoggle_file_preview_panel = ['f', '']\nopen_sort_options_menu = ['o', '']\ntoggle_reverse_sort = ['R', '']\n\n#-- Focus Manipulation\nfocus_on_process_bar = ['ctrl+p', '']\nfocus_on_sidebar = ['ctrl+s', '']\nfocus_on_metadata = ['ctrl+d', '']\n\n#-- File/Dir Creation/Renaming\nfile_panel_item_create = ['a', '']\nfile_panel_item_rename = ['r', '']\n\n#-- Main File Operations\ncopy_items = ['y', '']\ncut_items = ['x', '']\npaste_items = ['p', '']\ndelete_items = ['d', '']\npermanently_delete_items = ['D', '']\n\n#-- Archive Manipulation\nextract_file = ['ctrl+e', '']\ncompress_file = ['ctrl+a', '']\n\n#-- Editor Actions\nopen_file_with_editor = ['e', '']\nopen_current_directory_with_editor = ['E', '']\n\n#-- Other Actions\npinned_directory = ['P', '']\ntoggle_dot_file = ['.', '']\nchange_panel_mode = ['m', '']\nopen_help_menu = ['?', '']\nopen_spf_prompt = ['>', '']\nopen_command_line = [':', '']\nopen_zoxide = ['z', '']\ncopy_path = ['Y', '']\ncopy_present_working_directory = ['c', '']\ntoggle_footer = ['ctrl+f', '']\n\n###############################################################################\n#                                Typing hotkeys                               #\n###############################################################################\n\n# Note: These hotkeys can override all hotkeys.\n\nconfirm_typing = ['enter', '']\ncancel_typing = ['esc', '']\n\n###############################################################################\n#                            Mode-Specific Hotkeys                            #\n###############################################################################\n\n# Note: These hotkeys can conflict with other modes, but not with global\n# hotkeys.\n\n#-- Normal Mode Actions\nparent_directory = ['-', '']\nsearch_bar = ['/', '']\n\n#-- Selection Mode Actions\nfile_panel_select_mode_items_select_down = ['J', '']\nfile_panel_select_mode_items_select_up = ['K', '']\nfile_panel_select_all_items = ['A', '']\n"
  },
  {
    "path": "testsuite/.gitignore",
    "content": "# python venv site packages\nsite-packages/\n\n#python venvs\n.venv/\n\n# python pycache\n__pycache__/\n*.pyc"
  },
  {
    "path": "testsuite/Notes.md",
    "content": "# Implementation notes\n\n- The `pyautogui` sends input to the process in focus, which is the `spf` subprocess.\n- If `spf` is not exited correcly via `q`, it causes weird vertical tabs in print statements from python\n- There is some flakiness in sending of input. Many times, `Ctrl+C` is received as `C` in `spf`\n  - If first key is `Ctrl+C`, its always received as `C`\n- Note : You must keep your focus on the terminal for the entire duration of test run. `pyautogui` sends keypress to process on focus.\n\n# To-dos\n- Write testsuite to validate new files getting created on first launch\n  - We recently had a bug slip into main, where lastVersionFile would not get written\n\n## Input to spf\n\n### Pyautogui alternatives\nPOC with pyautogui as a lot of issues, stated above.\n\n#### Linux / macOS\n\n- xdotool\n  - Seems complicated. It wont be able to manage spf process that well\n- mkfifo / Manual linux piping\n  - Too much manual work to send inputs, even if it works\n- tmux\n  - Supports full terminal programs and has a python wrapper library\n  - See `docs/tmux.md`\n  - Not available for windows\n- References\n  - https://superuser.com/questions/585398/sending-simulated-keystrokes-in-bash\n\n#### Windows\n\n- Autohotkey\n  - No better than pyautogui\n- ControlSend and SendInput utility in windows\n  - Isn't that just for C# / C++ code ?\n- Python ctypes\n  - https://stackoverflow.com/questions/62189991/how-to-wrap-the-sendinput-function-to-python-using-ctypes\n- pywin32 library\n  - Create a new GUI window for test\n  - Use `win32gui.SendMessage` or `win32gui.PostMessage`\n  - Probably the correct way, but I havent been able to get it working.\n  - First we need to get it send input to a sample window like notepad, etc. Then we can make superfile work\n- pywinpty\n  - Heavy installations requirements. Needs Rust, and Visual studio build tools.\n  - Rust cargo not found\n    - Needs rust \n  - link.exe not found (` the msvc targets depend on the msvc linker but link.exe was not found` )\n    - Needs to install Visual Studio Build Tools (build tools and spectre mitigated libs)\n    - Had to manually find link.exe and put it on the PATH\n  - You might get error of unable to find mspdbcore.dll (I havent been able to solve it so far)\n    - https://stackoverflow.com/questions/67328795/c1356-unable-to-find-mspdbcore-dll\n- References\n  - https://www.reddit.com/r/tmux/comments/l580mi/is_there_a_tmuxlike_equivalent_for_windows/\n\n## Directory setup\n- Programmatic setup is better.\n- We could keep test directory setup as a config file - json/yaml/toml \n- or as a hardcoded python dict\n- Turns out, a in-memory fs is better. We have utilities like copy to actual fs and print tree\n  - Although it has a limitation of not being able to work with large files, as that would consume a lot of RAM\n  - For large files, we could do actually make them only on the actual filesystem, and not use in-memory fs\n  - https://docs.pyfilesystem.org/en/latest/reference/memoryfs.html\n  - https://docs.pyfilesystem.org/en/latest/guide.html \n\n\n## Tests and Validation\n- Each tests starts independently, so there is no strict order\n- Hardcoded validations . Predefined test, where each test has start dir, key press, and validations\n- We could have a base Class test. where check(), input(), init(), methods would be overrided\n- It allows greater flexibility in terms of testcases.\n- Abstraction layer for spf init, teardown and inputm"
  },
  {
    "path": "testsuite/README.md",
    "content": "## Coding style rules\n- Prefer using strong typing\n- Prefer using type hinting for the first time the variable is declared, and for functions paremeters and return types\n- Use `-> None` to explicitly indicate no return value\n\n### Ideas\n- Recommended to integrate your IDE with PEP8 to highlight PEP8 violations in real-time\n- Enforcing PEP8 via `pylint flake8 pycodestyle` and via pre commit hooks\n\n## Writing New testcases\n- Just create a file ending with `_test.py` in `tests` directory\n  - Any subclass of BaseTest with name ending with `Test` will be executed\n  - see `run_tests` and `get_testcases` in `core/runner.py` for more info\n\n## Setup\nRequires python 3.9 or later.\n\n## Setup for macOS / Linux\n\n### Install tmux\n- You need to have tmux installed. See https://github.com/tmux/tmux/wiki\n\n### Python virtual env setup\n```\n# cd to this directory\ncd <path/to/here>\npython3 -m venv .venv\n.venv/bin/pip install --upgrade pip\n.venv/bin/pip install -r requirements.txt\n```\n\n### Make sure you build spf\n```\n# cd to the superfile repo root (parent of this)\ncd <superfile_root>\n./build.sh\n```\n\n### Running testsuite\n```\n.venv/bin/python3 main.py\n```\n## Setup for Windows\nComing soon.\n\n\n\n### Python virtual env setup\n```\n# cd to this directory\ncd <path/to/here>\n\n# If your python command refers to python3, you can use 'python' below\npython3 -m venv .venv\n.venv\\Scripts\\python -m pip install --upgrade pip\n.venv\\Scripts\\pip install -r requirements.txt\n```\n\n### Make sure you build spf\n```\n# cd to the superfile repo root (parent of this)\ncd <superfile_root>\ngo build -o bin/spf.exe\n```\n\n### Running testsuite\nNotes\n- You must keep your focus on the terminal for the entire duration of test run. `pyautogui` sends keypress to process on focus.\n\n```\n.venv\\Scripts\\python main.py\n```\n\n## Tips while running tests\n- Use `-d` or `--debug` to enable debug logs during test run.\n- 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 enforcing superfile to close within a specific time window in tests to reduce test flakiness\n- Make sure that your hotkeys are set to default hotkeys. Tests use default hotkeys for now.\n- Use `-t` or `--tests` to only run specific tests\n  - Example `python main.py -d -t RenameTest CopyTest`\n- 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\n"
  },
  {
    "path": "testsuite/core/__init__.py",
    "content": ""
  },
  {
    "path": "testsuite/core/base_test.py",
    "content": "import logging\nimport time\nfrom abc import ABC, abstractmethod\nfrom core.environment import Environment\nfrom pathlib import Path\nfrom typing import Union, List, Tuple\nimport core.keys as keys\nimport core.test_constants as tconst\n\n\nclass BaseTest(ABC):\n    \"\"\"Base class for all tests\n    The idea is to have independency among each test.\n    And for each test to have full control on its environment, execution, and validation.\n    \"\"\"\n    def __init__(self, test_env : Environment):\n        self.env = test_env\n        self.logger = logging.getLogger()\n\n    @abstractmethod\n    def setup(self) -> None:\n        \"\"\"Set up the required things for test\n        \"\"\"\n\n    @abstractmethod\n    def test_execute(self) -> None:\n        \"\"\"Execute the test\n        \"\"\"\n\n    @abstractmethod\n    def validate(self) -> bool:\n        \"\"\"Validate that test passed. Log exception if failed.\n        Returns:\n            bool: True if validation passed\n        \"\"\"\n    @abstractmethod\n    def cleanup(self) -> None:\n        \"\"\"Any required cleanup after test is done\n        \"\"\"\n\n\nclass GenericTestImpl(BaseTest):\n    def __init__(self, *, # Barrier to explicitly require keyword arguements only\n        test_env : Environment,\n        test_root : Path,\n        start_dir : Path,\n        test_dirs : List[Path],\n        key_inputs : List[Union[keys.Keys,str]] = None,\n        test_files : List[Tuple[Path, str]] = None,\n        validate_exists : List[Path] = None,\n        validate_not_exists : List[Path] = None,\n        validate_spf_closed: bool = False,\n        validate_spf_running: bool = False,\n        start_wait_time : float = tconst.START_WAIT_TIME,\n        close_wait_time : float = tconst.CLOSE_WAIT_TIME ):\n\n        super().__init__(test_env)\n        self.test_root = test_root\n        self.start_dir = start_dir\n        self.spf_opts : List[str] = [\"-c\", str(tconst.CONFIG_FILE), \"--hf\", str(tconst.HOTKEY_FILE)]\n        \n        self.test_dirs = test_dirs\n        self.test_files = test_files\n        \n        # TODO fix it : For now first keypress in not being registered, \n        # Need Additional no-operation key press as the first keypress\n        if key_inputs is None:\n            key_inputs = []\n        key_inputs= ['a'] + key_inputs\n\n        self.key_inputs = key_inputs\n        self.validate_exists = validate_exists\n        self.validate_not_exists = validate_not_exists\n        self.validate_spf_closed = validate_spf_closed\n        self.validate_spf_running = validate_spf_running\n\n        if start_wait_time < 0 or close_wait_time < 0:\n            raise ValueError(\"wait times must be non-negative\")\n        self.start_wait_time = start_wait_time\n        self.close_wait_time = close_wait_time\n    \n    def setup(self) -> None:\n        for dir_path in self.test_dirs:\n            self.env.fs_mgr.makedirs(dir_path)\n        \n        if self.test_files is not None:\n            for file_path, data in self.test_files:\n                self.env.fs_mgr.create_file(file_path, data)\n        \n        self.logger.debug(\"Current file structure : \\n%s\",\n            self.env.fs_mgr.tree(self.test_root))\n        \n    \n    def start_spf(self) -> None:\n        self.env.spf_mgr.start_spf(self.env.fs_mgr.abspath(self.start_dir), self.spf_opts)\n        time.sleep(self.start_wait_time)\n        assert self.env.spf_mgr.is_spf_running(), \"superfile is not running\"\n\n    def end_execution(self) -> None:\n        self.env.spf_mgr.send_special_input(keys.KEY_ESC)    \n        time.sleep(self.close_wait_time)\n        self.logger.debug(\"Finished Execution\")\n\n    def send_input(self) -> None:\n        if self.key_inputs is not None:\n            for cur_input in self.key_inputs:\n                if isinstance(cur_input, keys.Keys):\n                    self.env.spf_mgr.send_special_input(cur_input)\n                else:\n                    assert isinstance(cur_input, str), \"Invalid input type\"\n                    self.env.spf_mgr.send_text_input(cur_input)\n                time.sleep(tconst.KEY_DELAY)\n\n    def test_execute(self) -> None:\n        \"\"\"Execute the test\n        \"\"\"\n        self.start_spf()\n        self.send_input()    \n        time.sleep(tconst.OPERATION_DELAY)\n        self.end_execution()\n        \n\n    def validate(self) -> bool:\n        \"\"\"Validate that test passed. Log exception if failed.\n        Returns:\n            bool: True if validation passed\n        \"\"\"\n        self.logger.debug(\"spf_manager info : %s, Current file structure : \\n%s\",\n            self.env.spf_mgr.runtime_info(), self.env.fs_mgr.tree(self.test_root))\n        try:\n            if self.validate_spf_closed :\n                assert not self.env.spf_mgr.is_spf_running(), \"superfile is still running\"\n            if self.validate_spf_running :\n                assert self.env.spf_mgr.is_spf_running(), \"superfile is not running\"\n\n            if self.validate_exists is not None:\n                for file_path in self.validate_exists:\n                    assert self.env.fs_mgr.check_exists(file_path), f\"File {file_path} does not exists\"\n\n            if self.validate_not_exists is not None:\n                for file_path in self.validate_not_exists:\n                    assert not self.env.fs_mgr.check_exists(file_path), f\"File {file_path} exists\" \n        except AssertionError as ae:\n            self.logger.debug(\"Test assertion failed : %s\", ae, exc_info=True)\n            return False\n                \n        return True\n\n    def cleanup(self) -> None:\n        # Cleanup after test is done\n        if self.env.spf_mgr.is_spf_running():\n            self.env.spf_mgr.close_spf()\n    \n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}\"\n\n"
  },
  {
    "path": "testsuite/core/environment.py",
    "content": "from core.spf_manager import BaseSPFManager\nfrom core.fs_manager import TestFSManager\n\nclass Environment:\n    \"\"\"Manage test environment\n    Manage cleanup of environment and other stuff at a single place\n    \"\"\"    \n    def __init__(self, spf_manager : BaseSPFManager, fs_manager : TestFSManager ):\n        self.spf_mgr = spf_manager\n        self.fs_mgr = fs_manager\n\n    def cleanup(self) -> None:\n        self.spf_mgr.close_spf()\n        self.fs_mgr.cleanup()"
  },
  {
    "path": "testsuite/core/fs_manager.py",
    "content": "import logging\nfrom  tempfile import TemporaryDirectory\nfrom pathlib import Path\nimport os\nfrom io import StringIO\n\nclass TestFSManager:\n    \"\"\"Manage the temporary files for test and the cleanup\n    \"\"\"    \n    def __init__(self):\n        self.logger = logging.getLogger()\n        self.logger.debug(\"Initialized %s\", self.__class__.__name__) \n        self.temp_dir_obj = TemporaryDirectory()\n        self.temp_dir = Path(self.temp_dir_obj.name)\n    \n    def abspath(self, relative_path : Path) -> Path:\n        return self.temp_dir / relative_path\n    \n    def check_exists(self, relative_path : Path) -> bool:\n        return self.abspath(relative_path).exists()\n    \n    def read_file(self, relative_path: Path) -> str:\n        content = \"\"\n        try:\n            with open(self.abspath(relative_path), 'r', encoding=\"utf-8\") as f:\n                content = f.read()\n        except FileNotFoundError:\n            self.logger.error(\"File not found: %s\", relative_path)\n        except PermissionError:\n            self.logger.error(\"Permission denied when reading file: %s\", relative_path)\n        return content\n\n    def makedirs(self, relative_path : Path) -> None:\n        # Overloaded '/' operator\n        os.makedirs(self.temp_dir / relative_path, exist_ok=True)\n    \n    def create_file(self, relative_path : Path, data : str = \"\") -> None:\n        \"\"\"Create files\n        Make sure directories exist\n        Args:\n            relative_path (Path): Relative path from test root\n        \"\"\"\n        with open(self.temp_dir / relative_path, 'w', encoding=\"utf-8\") as f:\n            f.write(data)\n\n    def tree(self, relative_root : Path = None) -> str:\n        if relative_root is None:\n            root = self.temp_dir\n        else:\n            root = self.temp_dir / relative_root\n        res = StringIO()\n        for item in root.rglob('*'):\n            path_str = str(item.relative_to(root))\n            if item.is_dir():\n                res.write(f\"D-{path_str}\\n\")\n            else:\n                res.write(f\"F-{path_str}\\n\")\n        return res.getvalue()\n\n    def cleanup(self) -> None:\n        \"\"\"Cleaup the temporary directory\n        Its okay to forget it though, it will be cleaned on program exit then.\n        \"\"\"\n        self.temp_dir_obj.cleanup()\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(temp_dir = {self.temp_dir})\"\n"
  },
  {
    "path": "testsuite/core/keys.py",
    "content": "from abc import ABC\nimport platform\n\nclass Keys(ABC):\n    def __init__(self, ascii_code : int):\n        self.ascii_code = ascii_code\n    \n    def __repr__(self) -> str:\n        return f\"Key(code={self.ascii_code})\"\n\n# Will isinstance of Keys work for object of CtrlKeys ?\nclass CtrlKeys(Keys):\n    def __init__(self, char : str):\n        # Only allowing single alphabetic character\n        # assert is good here as all objects are defined statically\n        assert len(char) == 1\n        assert char.isalpha() and char.islower()\n        self.char = char\n        # Ctrl + A starts at 1\n        super().__init__(ord(char) - ord('a') +  1)\n\n# Maybe have keycode\nclass SpecialKeys(Keys):\n    def __init__(self, ascii_code : int, key_name : str):\n        super().__init__(ascii_code)\n        self.key_name = key_name\n\n\n\nKEY_CTRL_A : Keys = CtrlKeys('a')\nKEY_CTRL_C : Keys = CtrlKeys('c')\nKEY_CTRL_E : Keys = CtrlKeys('e')\nKEY_CTRL_D : Keys = CtrlKeys('d')\nKEY_CTRL_M : Keys = CtrlKeys('m')\nKEY_CTRL_P : Keys = CtrlKeys('p')\nKEY_CTRL_R : Keys = CtrlKeys('r')\nKEY_CTRL_V : Keys = CtrlKeys('v')\nKEY_CTRL_W : Keys = CtrlKeys('w')\nKEY_CTRL_X : Keys = CtrlKeys('x')\n\n# Platform specific keys\nKEY_PASTE : Keys = KEY_CTRL_V\nif platform.system() == \"Windows\" :\n    KEY_PASTE = KEY_CTRL_W\n\n# See https://vimdoc.sourceforge.net/htmldoc/digraph.html#digraph-table for key codes\n# If keyname is not the same string as key code in pyautogui, need to handle separately\nKEY_BACKSPACE   : Keys = SpecialKeys(8 , \"Backspace\")\nKEY_ENTER       : Keys = SpecialKeys(13, \"Enter\")\nKEY_ESC         : Keys = SpecialKeys(27, \"Esc\")\nKEY_DELETE      : Keys = SpecialKeys(127 , \"Delete\")\n\n\nNO_ASCII = -1\n\n# Some keys dont have ascii codes, they have to be handled separately\n# Make sure key name is the same string as key code for Tmux\nKEY_DOWN        : Keys = SpecialKeys(NO_ASCII, \"Down\")\nKEY_UP          : Keys = SpecialKeys(NO_ASCII, \"Up\")\nKEY_LEFT        : Keys = SpecialKeys(NO_ASCII, \"Left\")\nKEY_RIGHT       : Keys = SpecialKeys(NO_ASCII, \"Right\")\n"
  },
  {
    "path": "testsuite/core/pyautogui_manager.py",
    "content": "import time \nimport subprocess\nimport pyautogui\nimport core.keys as keys\nfrom core.spf_manager import BaseSPFManager\n\nclass PyAutoGuiSPFManager(BaseSPFManager):\n    \"\"\"Manage SPF via subprocesses and pyautogui\n    Cross platform, but it globally takes over the input, so you need the terminal \n    constantly on focus during test run\n    \"\"\"\n    SPF_START_DELAY : float = 0.5\n    def __init__(self, spf_path : str):\n        super().__init__(spf_path)\n        self.spf_process = None\n\n\n    def start_spf(self, start_dir : str = None, args : list[str] = None) -> None:\n        spf_args = [self.spf_path]\n        if args :\n            spf_args += args\n        spf_args.append(start_dir)\n\n        self.spf_process = subprocess.Popen(spf_args,\n            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n        time.sleep(PyAutoGuiSPFManager.SPF_START_DELAY)\n\n        # Need to send a sample keypress otherwise it ignores first keypress\n        self.send_text_input('x')\n        \n    \n    def send_text_input(self, text : str, all_at_once : bool = False) -> None:\n        if all_at_once :\n            pyautogui.write(text)\n        else:\n            for c in text:\n                pyautogui.write(c)\n\n    def send_special_input(self, key : keys.Keys) -> None:\n        if isinstance(key, keys.CtrlKeys):\n            pyautogui.hotkey('ctrl', key.char)\n        elif isinstance(key, keys.SpecialKeys):\n            pyautogui.press(key.key_name.lower())\n        else:\n            raise Exception(f\"Unknown key : {key}\") \n\n    def get_rendered_output(self) -> str:\n        return \"[Not supported yet]\" \n    \n    \n    def is_spf_running(self) -> bool:\n        self._is_spf_running = (self.spf_process is not None) and (self.spf_process.poll() is None)\n        return self._is_spf_running\n    \n    def close_spf(self) -> None:\n        if self.spf_process is not None:\n            self.spf_process.terminate()\n    \n    # Override\n    def runtime_info(self) -> str:\n        if self.spf_process is None:\n            return \"[No process]\"\n        else:\n            return f\"[PID : {self.spf_process.pid}, poll : {self.spf_process.poll()}]\"  \n\n\n\n"
  },
  {
    "path": "testsuite/core/runner.py",
    "content": "from core.spf_manager import BaseSPFManager\nfrom core.fs_manager import TestFSManager\nfrom core.environment import Environment\nfrom core.base_test import BaseTest\n\nimport logging\nimport platform\nimport importlib\nfrom pathlib import Path\nfrom typing import List\n\n\n# Preferred importing at the top level\nif platform.system() == \"Windows\" :\n    # Conditional import is needed to make it work on linux\n    # importing pyautogui on linux can cause errors.\n    from core.pyautogui_manager import PyAutoGuiSPFManager\nelse:\n    from core.tmux_manager import TmuxSPFManager\n\nlogger = logging.getLogger()\n\ndef get_testcases(test_env : Environment, only_run_tests : List[str] = None) -> List[BaseTest]:\n    res : List[BaseTest] = []\n    test_dir = Path(__file__).parent.parent / \"tests\"\n    for test_file in test_dir.glob(\"*_test.py\"):\n        # Import dynamically\n        module_name = test_file.stem \n        module = importlib.import_module(f\"tests.{module_name}\")\n        for attr_name in dir(module):\n            if only_run_tests is not None and attr_name not in only_run_tests:\n                continue\n            attr = getattr(module, attr_name)\n            if isinstance(attr, type) and attr is not BaseTest and issubclass(attr, BaseTest) \\\n                and  attr_name.endswith(\"Test\"):\n                logger.debug(\"Found a testcase %s, in module %s\", attr_name, module_name)\n                res.append(attr(test_env))\n    return res\n\ndef run_tests(spf_path : Path, stop_on_fail : bool = True, only_run_tests : List[str] = None) -> bool:\n    \"\"\"Runs tests\n\n    Args:\n        spf_path (Path): Path of spf binary under test\n        stop_on_fail (bool, optional): Whether to stop on failures. Defaults to True.\n        only_run_tests (List[str], optional): Only specific test to run. Defaults to None.\n\n    Returns:\n        bool: Whether run was successful\n    \"\"\"    \n    # is this str conversion needed ?\n\n    spf_manager : BaseSPFManager = None \n    if platform.system() == \"Windows\" :\n        spf_manager = PyAutoGuiSPFManager(str(spf_path))\n    else:\n        spf_manager = TmuxSPFManager(str(spf_path))\n        \n    fs_manager = TestFSManager()\n\n    test_env = Environment(spf_manager, fs_manager)\n    cnt_passed : int = 0\n    cnt_executed : int = 0\n    try:    \n        testcases : List[BaseTest] = get_testcases(test_env, only_run_tests=only_run_tests)\n        logger.info(\"Testcases : %s\", testcases)\n        for t in testcases:\n            logger.info(\"Running test %s\", t)\n            t.setup()\n            t.test_execute()\n            cnt_executed += 1\n            passed : bool = t.validate()\n            t.cleanup()\n\n            if passed:\n                logger.info(\"Passed test %s\", t)\n                cnt_passed += 1\n            else:\n                logger.error(\"Failed test %s\", t)\n                if stop_on_fail:\n                    break\n        \n        logger.info(\"Finished running %s test. %s passed\", cnt_executed, cnt_passed)\n    finally:\n        # Make sure of cleanup\n        # This is still not full proof, as if what happens when TestFSManager __init__ fails ?\n        test_env.cleanup()\n\n    return cnt_passed == cnt_executed\n        \n\n\n"
  },
  {
    "path": "testsuite/core/spf_manager.py",
    "content": "from abc import ABC, abstractmethod\nimport core.keys as keys\n\nclass BaseSPFManager(ABC):\n\n    def __init__(self, spf_path : str):\n        self.spf_path = spf_path\n        # _ denotes the internal variables, anyone should not directly read/modify\n        self._is_spf_running : bool = False\n\n    @abstractmethod\n    def start_spf(self, start_dir : str = None, args : list[str] = None) -> None:\n        pass \n    \n    @abstractmethod\n    def send_text_input(self, text : str, all_at_once : bool = False) -> None:\n        pass \n\n    @abstractmethod\n    def send_special_input(self, key : keys.Keys) -> None:\n        pass \n\n    @abstractmethod\n    def get_rendered_output(self) -> str:\n        pass\n    \n    \n    @abstractmethod\n    def is_spf_running(self) -> bool:\n        \"\"\"\n        We allow using _is_spf_running variable for efficiency\n        But this method should give the true state, although this might have some calculations\n        \"\"\"\n        return self._is_spf_running\n    \n    @abstractmethod\n    def close_spf(self) -> None:\n        \"\"\"\n        Close spf if its running and cleanup any other resources\n        \"\"\"\n    \n    def runtime_info(self) -> str:\n        return \"[No runtime info]\"\n\n"
  },
  {
    "path": "testsuite/core/test_constants.py",
    "content": "import platform\nfrom pathlib import Path\nFILE_TEXT1 : str = \"This is a sample Text\\n\"\n\nKEY_DELAY : float       = 0.05 # seconds\nOPERATION_DELAY : float = 0.3 # seconds\n\n# 0.3 second was too less for windows\n# 0.5 second Github workflow failed for with superfile is still running errors\nSTART_WAIT_TIME : float     = 0.5 # seconds\nCLOSE_WAIT_TIME : float     = 0.5 # seconds\n\n# Platform specific consts\nFILE_CREATE_COMMAND : str   = \"touch\"\nif platform.system() == \"Windows\" :\n    FILE_CREATE_COMMAND = \"ni\"\n\nCONF_DIR = Path(__file__).parent.parent.parent / \"src\" / \"superfile_config\"\n\nCONFIG_FILE = CONF_DIR / \"config.toml\"\nHOTKEY_FILE = CONF_DIR / \"hotkeys.toml\"\n"
  },
  {
    "path": "testsuite/core/tmux_manager.py",
    "content": "import libtmux\nimport time \nimport logging\nimport core.keys as keys\nfrom core.spf_manager import BaseSPFManager\n\nclass TmuxSPFManager(BaseSPFManager):\n    \"\"\"\n    Tmux based Manager\n    After running spf, you can connect to the session via\n    tmux -L superfile attach -t spf_session\n    Wont work in windows\n    \"\"\"\n    # Class variables\n    SPF_START_DELAY : float = 0.1 # seconds\n    SPF_SOCKET_NAME : str = \"superfile\"\n\n    # Init should not allocate any resources\n    def __init__(self, spf_path : str):\n        super().__init__(spf_path)\n        \n        # Check libtmux version requirement\n        min_version = (0, 31, 0)\n        current_version_str = libtmux.__version__\n        \n        # Parse version string to tuple for comparison\n        try:\n            current_version = tuple(map(int, current_version_str.split('.')[:3]))\n        except (ValueError, AttributeError):\n            current_version = (0, 0, 0)\n        \n        if current_version < min_version:\n            raise RuntimeError(\n                f\"libtmux version 0.31.0 or higher is required. \"\n                f\"Current version: {current_version_str}. \"\n                f\"Please upgrade with: pip install 'libtmux>=0.31.0'\"\n            )\n        \n        self.logger = logging.getLogger()\n        self.server = libtmux.Server(socket_name=TmuxSPFManager.SPF_SOCKET_NAME)\n        self.logger.debug(\"server object : %s\", self.server)\n        self.spf_session : libtmux.Session = None\n        self.spf_pane : libtmux.Pane = None\n\n    def start_spf(self, start_dir : str = None, args : list[str] = None) -> None:\n        spf_command = self.spf_path\n        if args:\n            spf_command += \" \" + \" \".join(args)\n\n        self.logger.debug(\"windows_command : %s\", spf_command)\n        \n\n        self.spf_session= self.server.new_session('spf_session',\n                window_command=spf_command, \n                start_directory=start_dir)\n        time.sleep(TmuxSPFManager.SPF_START_DELAY)\n        self.logger.debug(\"spf_session initialised : %s\", self.spf_session)\n\n        # If libtmux version is less than 0.3.1, active_pane does not exist.\n        self.spf_pane = self.spf_session.active_pane\n        self._is_spf_running = True\n\n    def _send_key(self, key : str) -> None:\n        self.logger.debug(\"sending key : %s\", repr(key))\n        self.spf_pane.send_keys(key, enter=False)\n\n    def send_text_input(self, text : str, all_at_once : bool = True) -> None:\n        if all_at_once:\n            self._send_key(text)\n        else:\n            for c in text:\n                self._send_key(c)\n\n    def send_special_input(self, key : keys.Keys) -> str:\n        if key.ascii_code != keys.NO_ASCII:\n            self._send_key(chr(key.ascii_code))\n        elif isinstance(key, keys.SpecialKeys):\n            self._send_key(key.key_name)\n        else:\n            raise Exception(f\"Unknown key : {key}\") \n            \n    def get_rendered_output(self) -> str:\n        return \"[Not supported yet]\"\n\n    def is_spf_running(self) -> bool:\n        self._is_spf_running = (self.spf_session is not None) \\\n            and (self.spf_session in self.server.sessions)\n\n        return self._is_spf_running\n\n    def close_spf(self) -> None:\n        if self.is_spf_running():\n            self.server.kill_session(self.spf_session.name)\n\n    # Override\n    def runtime_info(self) -> str:\n        return str(self.server.sessions)\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}(server : {self.server}, \" + \\\n            f\"session : {self.spf_session}, running : {self._is_spf_running})\"\n"
  },
  {
    "path": "testsuite/core/utils.py",
    "content": "import pyperclip\n# ------ Clipboard utils\n\n# This creates a layer of abstraction.\n# Now the user of the fuction doesn't need to import pyperclip\n# or need to even know what pyperclip was used.\ndef get_sys_clipboard_text() -> str :\n    return pyperclip.paste()"
  },
  {
    "path": "testsuite/docs/tmux.md",
    "content": "# Overview\nThis is to document the behviour of tmux, and how could we use it in testsuite\n\n# Tmux concepts and working info\n- Tmux creates a main server process, and one new process for each session.\n<img width=\"1147\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c701a56f-49ef-43a2-9b76-8f613f5e219a\" />\n- `-s` and `-n` for window naming.\n- We have prefix keys to send commands to tmux. \n\n# Sample usage with spf\n\n## Sending keys to termux and controlling from outside.\n<img width=\"1147\" alt=\"image\" src=\"https://github.com/user-attachments/assets/8ca67e0c-ece6-465c-84b1-755c8fa0a67e\" />\n<img width=\"1147\" alt=\"image\" src=\"https://github.com/user-attachments/assets/e4aeb0b9-e184-41a0-89eb-9174082eb884\" />\n\n# Knowledge sharing\n- `tmux new 'spf'` - Run spf in tmux\n- `tmux attach -t <session_name>` attach to an existing session. You can have two windows duplicating same behaviour.\n- `tmux kill-session -t <session_name>` kill session\n- `Ctrl+B`+`:` - Enter commands\n- `Ctrl+B`+`D` - Detach from session\n- `:source ~/.tmux.conf` - Change the config of running server\n- We have already a wrapper library for termux in python !!!!!\n- How to send key press/tmux commands to the process ?\n\n\n# References\n- https://github.com/tmux/tmux/wiki/Getting-Started\n- https://tao-of-tmux.readthedocs.io/en/latest/manuscript/10-scripting.html#controlling-tmux-send-keys\n- https://github.com/tmux-python/libtmux\n"
  },
  {
    "path": "testsuite/main.py",
    "content": "import argparse\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom core.runner import run_tests\nimport core.test_constants as tconst\n\n\ndef configure_logging(debug : bool = False) -> None:\n    # Prefer stdout instead of default stderr\n    handler = logging.StreamHandler(sys.stdout)\n    \n    # 7s to align all log levelnames - WARNING is the largest level, with size 7\n    handler.setFormatter(logging.Formatter(\n        '[%(asctime)s - %(levelname)7s] %(message)s',\n        datefmt='%Y-%m-%d %H:%M:%S'\n    ))\n\n\n    logger = logging.getLogger()\n    logger.addHandler(handler)\n\n    if debug:\n        logger.setLevel(logging.DEBUG)\n    else:\n        logger.setLevel(logging.INFO)\n\n    logging.getLogger(\"libtmux\").setLevel(logging.WARNING)\n\ndef main():\n    # Setup argument parser\n    parser = argparse.ArgumentParser(description='superfile testsuite')\n    parser.add_argument('-d', '--debug',action='store_true',\n                        help='Enable debug logging')\n    parser.add_argument('--close-wait-time', type=float,\n                        help='Override default wait time after closing spf')\n    parser.add_argument('--spf-path', type=str,\n                        help='Override the default spf executable path(../bin/spf) under test')\n    parser.add_argument('-t', '--tests', nargs='+',\n                        help='Specify one or more than one space separated testcases to be run')\n    # Parse arguments\n    args = parser.parse_args()\n    if args.close_wait_time is not None:\n        tconst.CLOSE_WAIT_TIME = args.close_wait_time\n    \n    configure_logging(args.debug)\n        \n    # Default path\n    # We maybe should run this only in main.py file.\n    spf_path = Path(__file__).parent.parent / \"bin\" / \"spf\"\n\n    if args.spf_path is not None:\n        spf_path = Path(args.spf_path)\n    # Resolve any symlinks, and make it absolute\n    spf_path = spf_path.resolve()\n\n    success = run_tests(spf_path, only_run_tests=args.tests)\n    if success:\n        sys.exit(0)\n    else:\n        sys.exit(1)\n\nmain()\n"
  },
  {
    "path": "testsuite/requirements.txt",
    "content": "pyautogui; sys_platform == \"win32\"\nlibtmux; sys_platform == \"linux\" or sys_platform == \"darwin\"\npyperclip\nassertpy"
  },
  {
    "path": "testsuite/tests/__init__.py",
    "content": ""
  },
  {
    "path": "testsuite/tests/chooser_file_test.py",
    "content": "from pathlib import Path\nimport time\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\n\nTESTROOT = Path(\"chooser_file_ops\")\nDIR1 = TESTROOT / \"dir1\"\nDIR2 = TESTROOT / \"dir2\"\nFILE1 = DIR1 / \"file1.txt\"\nCHOOSER_FILE = DIR2 / \"chooser_file.txt\"\n\n\n\nclass ChooserFileTest(GenericTestImpl):\n\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=DIR1,\n            test_dirs=[DIR1, DIR2],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=['e'],\n            validate_spf_closed=True,\n            close_wait_time=3\n        )\n\n        self.spf_opts += [\"--chooser-file\", str(self.env.fs_mgr.abspath(CHOOSER_FILE))]\n\n    # Override\n    def end_execution(self) -> None:\n        self.logger.debug(\"Skipping esc key press for Chooser file test\")\n        time.sleep(self.close_wait_time)\n        self.logger.debug(\"Finished Execution\")\n    # Override\n    def validate(self) -> bool:\n        if not super().validate():\n            return False\n        \n        try:\n            assert self.env.fs_mgr.check_exists(CHOOSER_FILE), f\"File {CHOOSER_FILE} does not exists\"\n            chooser_file_content = self.env.fs_mgr.read_file(CHOOSER_FILE)\n            assert chooser_file_content == str(self.env.fs_mgr.abspath(FILE1)), \\\n                f\"Expected '{self.env.fs_mgr.abspath(FILE1)}', got '{chooser_file_content}'\"\n\n        except AssertionError as ae:\n            self.logger.debug(\"Test assertion failed : %s\", ae, exc_info=True)\n            return False\n                \n        return True"
  },
  {
    "path": "testsuite/tests/command_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.keys as keys\nimport core.test_constants as tconst\n\nTESTROOT = Path(\"cmd_ops\")\nDIR1 = TESTROOT / \"dir1\"\nFILE1 = TESTROOT / \"file1\"\n\nclass CommandTest(GenericTestImpl):\n    \"\"\"Test compression and extraction\n    \"\"\"\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=TESTROOT,\n            test_dirs=[TESTROOT],\n            key_inputs=[':', 'mkdir dir1', keys.KEY_ENTER, ':', tconst.FILE_CREATE_COMMAND + ' file1', keys.KEY_ENTER],\n            validate_exists=[DIR1, FILE1]\n        )\n"
  },
  {
    "path": "testsuite/tests/compress_extract_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"ce_ops\")\nDIR1 = TESTROOT / \"dir1\"\nFILE1 = DIR1 / \"file1\"\nFILE2 = DIR1 / \"file2\"\n\nDIR1_ZIPPED = TESTROOT / \"dir1.zip\"\n\nDIR1_EXTRACTED = TESTROOT / \"dir1(1)\" / \"dir1\"\nFILE1_EXTRACTED = DIR1_EXTRACTED / \"file1\"\nFILE2_EXTRACTED = DIR1_EXTRACTED / \"file2\"\n\n\nclass CompressExtractTest(GenericTestImpl):\n    \"\"\"Test compression and extraction\n\n    Args:\n        GenericTestImpl (_type_): _description_\n    \"\"\"\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=TESTROOT,\n            test_dirs=[DIR1],\n            test_files=[(FILE1, tconst.FILE_TEXT1), (FILE2, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_A, keys.KEY_DOWN, keys.KEY_CTRL_E],\n            validate_exists=[DIR1, DIR1_ZIPPED, DIR1_EXTRACTED, FILE1_EXTRACTED, FILE2_EXTRACTED]\n        )\n"
  },
  {
    "path": "testsuite/tests/copy_dir_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"copy_dir\")\nDIR1 = TESTROOT / \"dir1\" \nNESTED_DIR1 = DIR1 / \"nested1\"\nNESTED_DIR2 = DIR1 / \"nested2\"\nFILE1 = NESTED_DIR1 / \"file1.txt\"\n\nDIR2 = TESTROOT / \"dir2\"\n\nDIR1_COPIED = DIR2 / \"dir1\"\nFILE1_COPIED = DIR1_COPIED / \"nested1\" / \"file1.txt\"\n\n\n\nclass CopyDirTest(GenericTestImpl):\n\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=TESTROOT,\n            test_dirs=[DIR1, DIR2, NESTED_DIR1, NESTED_DIR2],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_C, keys.KEY_DOWN, keys.KEY_ENTER, keys.KEY_PASTE],\n            validate_exists=[DIR1_COPIED, FILE1_COPIED, DIR1, FILE1]\n        )"
  },
  {
    "path": "testsuite/tests/copy_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"copy_ops\")\nDIR1 = TESTROOT / \"dir1\"\nDIR2 = TESTROOT / \"dir2\"\nFILE1 = DIR1 / \"file1.txt\"\nFILE1_COPY1 = DIR1 / \"file1(1).txt\"\nFILE1_COPY2 = DIR2 / \"file1.txt\"\n\n\n\nclass CopyTest(GenericTestImpl):\n\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=DIR1,\n            test_dirs=[DIR1, DIR2],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_C, keys.KEY_PASTE],\n            validate_exists=[FILE1, FILE1_COPY1],\n            # If you want to validate spf being close, wait time needs to be high\n            # Otherwise tests are flaky\n            validate_spf_closed=True,\n            close_wait_time=3\n        )"
  },
  {
    "path": "testsuite/tests/copyw_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"copyw_ops\")\nFILE1 = TESTROOT / \"file1.txt\"\nFILE1_COPY1 = TESTROOT / \"file1(1).txt\"\n\nclass CopyWTest(GenericTestImpl):\n    \"\"\"Testcase to validate copying with Ctrl+W shortcut \n    \"\"\"\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=TESTROOT,\n            test_dirs=[TESTROOT],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_C, keys.KEY_CTRL_W],\n            validate_exists=[FILE1, FILE1_COPY1]\n        )"
  },
  {
    "path": "testsuite/tests/cut_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"cut_ops\")\nDIR1 = TESTROOT / \"dir1\"\nDIR2 = TESTROOT / \"dir2\"\nFILE1 = DIR1 / \"file1.txt\"\nFILE1_CUT1 = DIR2 / \"file1.txt\"\n\n\n\nclass CutTest(GenericTestImpl):\n\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=DIR1,\n            test_dirs=[DIR1, DIR2],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_X, keys.KEY_LEFT, keys.KEY_DOWN, \n                keys.KEY_ENTER, keys.KEY_PASTE],\n            validate_exists=[FILE1_CUT1],\n            validate_not_exists=[FILE1]\n        )\n"
  },
  {
    "path": "testsuite/tests/delete_dir_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"delete_dir\")\nDIR1 = TESTROOT / \"dir1\"\nNESTED_DIR1 = DIR1 / \"nested1\"\nNESTED_DIR2 = DIR1 / \"nested2\"\nFILE1 = NESTED_DIR1 / \"file1.txt\"\n\n\nclass DeleteDirTest(GenericTestImpl):\n\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=TESTROOT,\n            test_dirs=[TESTROOT, DIR1, NESTED_DIR1, NESTED_DIR2],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_D, keys.KEY_ENTER],\n            validate_not_exists=[DIR1, NESTED_DIR1, NESTED_DIR2, FILE1]\n        )\n"
  },
  {
    "path": "testsuite/tests/delete_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"delete_ops\")\nFILE1 = TESTROOT / \"file_to_delete.txt\"\n\n\n\nclass DeleteTest(GenericTestImpl):\n\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=TESTROOT,\n            test_dirs=[TESTROOT],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_D, keys.KEY_ENTER],\n            validate_not_exists=[FILE1]\n        )\n"
  },
  {
    "path": "testsuite/tests/empty_panel_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\nimport time\n\nTESTROOT = Path(\"empty_panel_ops\")\nDIR1 = TESTROOT / \"dir1\"\n\n\nclass EmptyPanelTest(GenericTestImpl):\n    \"\"\"\n    Validate that spf doesn't crashes when we try to \n    perform operations on empty file panel\n    \"\"\"\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=DIR1,\n            test_dirs=[DIR1],\n            key_inputs=[ \n                keys.KEY_CTRL_C,    # Try copy\n                keys.KEY_CTRL_X,    # Try cut\n                keys.KEY_CTRL_D,    # Try delete\n                keys.KEY_PASTE,     # Try paste\n                keys.KEY_CTRL_R,    # Try rename\n                keys.KEY_CTRL_P,    # Try copy location\n                'e',                # Try open with editor\n                keys.KEY_ENTER,\n                keys.KEY_RIGHT,\n                keys.KEY_CTRL_A,    # Try archiving\n                keys.KEY_CTRL_E,    # Try extract\n                'v',                # Try going to Select mode\n                'J',                # Try select down  \n                'K',                # Try select up\n                'A',                # select all\n                'v',\n                '.',                # Try toggle dotfiles                 \n                ],\n            # Makes sure spf doesn't crashes\n            validate_spf_running=True\n        )\n\n    # Override\n    def test_execute(self) -> None:\n        self.start_spf()\n        self.send_input()    \n        time.sleep(tconst.OPERATION_DELAY)\n        # Intentionally not closing spf to ensure it remains running,\n        # which is verified by the validate_spf_running flag which is set \n        # to true for this testcase\n    \n    \n        \n    \n"
  },
  {
    "path": "testsuite/tests/nav_and_copy_path_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nfrom core.utils import get_sys_clipboard_text\nimport core.test_constants as tconst\nimport core.keys as keys\nfrom assertpy import assert_that\nimport time\n\nTESTROOT = Path(\"nav_ops\")\nDIR1 = TESTROOT / \"dir1\"\nFILE1 = TESTROOT / \"file1\"\nFILE2 = TESTROOT / \"file2\"\n\n# Temporarily disabled, till we fix xclip does not works in github actions \nclass NavCopyPathTest_Disabled(GenericTestImpl):\n    \"\"\"Test navigation, and Copying of path\n    \"\"\"\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=TESTROOT,\n            test_dirs=[TESTROOT, DIR1],\n            test_files=[(FILE1, tconst.FILE_TEXT1), (FILE2, tconst.FILE_TEXT1)]\n        )\n    \n    # Override\n    def test_execute(self) -> None:\n        self.start_spf()\n        time.sleep(tconst.OPERATION_DELAY)\n        # > dir1 \n        #   file1\n        #   file2\n\n        self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P)\n        time.sleep(tconst.KEY_DELAY)\n        assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(DIR1)))\n\n        self.env.spf_mgr.send_special_input(keys.KEY_DOWN)\n        time.sleep(tconst.KEY_DELAY)\n        #   dir1 \n        # > file1\n        #   file2\n        self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P)\n        time.sleep(tconst.KEY_DELAY)\n        assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(FILE1)))\n\n        self.env.spf_mgr.send_special_input(keys.KEY_DOWN)\n        time.sleep(tconst.KEY_DELAY)\n        #   dir1 \n        #   file1\n        # > file2\n        self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P)\n        time.sleep(tconst.KEY_DELAY)\n        assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(FILE2)))\n\n        self.env.spf_mgr.send_special_input(keys.KEY_UP)\n        time.sleep(tconst.KEY_DELAY)\n        #   dir1 \n        # > file1\n        #   file2\n        self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P)\n        time.sleep(tconst.KEY_DELAY)\n        assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(FILE1)))\n\n        self.env.spf_mgr.send_special_input(keys.KEY_DOWN)\n        time.sleep(tconst.KEY_DELAY)\n        self.env.spf_mgr.send_special_input(keys.KEY_DOWN)\n        time.sleep(tconst.KEY_DELAY)\n        # > dir1 \n        #   file1\n        #   file2\n        self.env.spf_mgr.send_special_input(keys.KEY_CTRL_P)\n        time.sleep(tconst.KEY_DELAY)\n        assert_that(get_sys_clipboard_text()).is_equal_to(str(self.env.fs_mgr.abspath(DIR1)))\n        \n        self.end_execution()\n"
  },
  {
    "path": "testsuite/tests/rename_test.py",
    "content": "from pathlib import Path\n\nfrom core.base_test import GenericTestImpl\nfrom core.environment import Environment\nimport core.test_constants as tconst\nimport core.keys as keys\n\nTESTROOT = Path(\"rename_ops\")\nDIR1 = TESTROOT / \"dir1\"\n\n# No extension, as in case of extension, the edit cursor appears before the dot, \n# not at the end of filename\nFILE1 = DIR1 / \"file1\"\nFILE1_RENAMED = DIR1 / \"file2\"\n\n\n\nclass RenameTest(GenericTestImpl):\n\n    def __init__(self, test_env : Environment):\n        super().__init__(\n            test_env=test_env,\n            test_root=TESTROOT,\n            start_dir=DIR1,\n            test_dirs=[DIR1],\n            test_files=[(FILE1, tconst.FILE_TEXT1)],\n            key_inputs=[keys.KEY_CTRL_R, keys.KEY_BACKSPACE, '2', keys.KEY_ENTER],\n            validate_exists=[FILE1_RENAMED],\n            validate_not_exists=[FILE1]\n        )\n"
  },
  {
    "path": "vhs/demo.tape",
    "content": "Output asset/demo.gif\n\nSet Shell \"base\"\nSet FontSize 20\nSet Width 1920 \nSet Height 1080\nSet FontFamily \"Comic Mono, RobotoMono Nerd Font\"\nSet Framerate 15\n\nType \"spf\"\nSleep 1500ms\nEnter\nSleep 700ms\n\nType \"b\"\nSleep 500ms\nDown@600ms 1\nType \"l\"\n\nCtrl+p\nSleep 600ms\n\nCtrl+n\nSleep 300ms\nCtrl+n\nSleep 300ms\nCtrl+n\nSleep 300ms\n\nCtrl+w\nSleep 300ms\nCtrl+w\nSleep 700ms\n\nType \"b\"\n\nDown@600ms 2\n\nSleep 600ms\nType \"l\"\n\nSleep 600ms\nTab\n\nSleep 600ms\nType \"c\"\nSleep 500ms\nType \"test.txt\"\nSleep 500ms\nEnter\nSleep 600ms\n\nDown@700ms 1\n\nSleep 600ms\nType \"d\"\n\nSleep 700ms\nType \"f\"\nSleep 600ms\nType \"test folder\"\nSleep 700ms\nEnter\nSleep 700ms\nType \"r\"\nSleep 600ms\nBackspace 12\nType \"rename this folder\"\nSleep 500ms\nEnter\nSleep 700ms\nType \"d\"\nSleep 600ms\nTab\nSleep 600ms\nCtrl+c\nSleep 600ms\nTab\nSleep 600ms\nCtrl+v\nSleep 600ms\nType \"l\"\nSleep 600ms\nType \"d\"\nSleep 300ms\nType \"d\"\nSleep 300ms\nType \"d\"\nSleep 300ms\nType \"v\"\nSleep 600ms\nType \"J\"\nSleep 500ms\nType \"J\"\nSleep 500ms\nType \"J\"\nSleep 600ms\n\nCtrl+d\nSleep 600ms\n\nType \"J\"\nSleep 400ms\nType \"j\"\nSleep 400ms\nType \"J\"\nSleep 400ms\nType \"j\"\nSleep 400ms\nType \"J\"\nSleep 400ms\nType \"j\"\nSleep 700ms\n\nCtrl+x\nSleep 700ms\nTab\nSleep 600ms\nCtrl+v\nSleep 700ms\n\nCtrl+w\nSleep 500ms\n\nEscape\n\nType \"Thanks for watching\"\nSleep 5s\n"
  },
  {
    "path": "vhs/open_spf_and_quit.tape",
    "content": "Output asset/demo.gif\n\nSet Shell \"base\"\nSet FontSize 30\nSet Width 2560\nSet Height 1500\nSet FontFamily \"Comic Mono, RobotoMono Nerd Font\"\nSet PlaybackSpeed 0.6\nSet Padding 50\nSet 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\" }\n\nType \"spf\"\nSleep 1500ms\nEnter\nSleep 1500ms\nType \"q\"\nSleep 1500ms"
  },
  {
    "path": "vhs/spf_file_panel_movement.tape",
    "content": "Output asset/demo.gif\n\nSet Shell \"base\"\nSet FontSize 30\nSet Framerate 60\nSet Width 2560\nSet Height 1500\nSet FontFamily \"Comic Mono, RobotoMono Nerd Font\"\nSet PlaybackSpeed 0.6\nSet Padding 50\nSet 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\" }\n\nType \"spf\"\nSleep 1500ms\nEnter\nSleep 1500ms\nDown@150ms 5\nSleep 150ms\nUp@150ms 3\nSleep 700ms\nEnter\nSleep 1500ms\n"
  },
  {
    "path": "vhs/spf_file_panel_navigation.tape",
    "content": "Output asset/demo.gif\n\nSet Shell \"base\"\nSet FontSize 30\nSet Framerate 60\nSet Width 2560\nSet Height 1500\nSet FontFamily \"Comic Mono, RobotoMono Nerd Font\"\nSet PlaybackSpeed 0.6\nSet Padding 50\nSet 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\" }\n\nType \"spf\"\nSleep 1500ms\nEnter\nSleep 1500ms\nCtrl+N\nSleep 500ms\nCtrl+N\nSleep 500ms\nCtrl+N\nSleep 500ms\nCtrl+N\nSleep 500ms\n\nType \"L\"\nSleep 200ms\nType \"L\"\nSleep 200ms\nType \"L\"\nSleep 200ms\nType \"L\"\nSleep 200ms\n\nType \"H\"\nSleep 200ms\nType \"H\"\nSleep 200ms\nType \"H\"\nSleep 200ms\nType \"H\"\nSleep 200ms\n\nCtrl+W\nSleep 500ms\nCtrl+W\nSleep 500ms\nCtrl+W\nSleep 500ms\nCtrl+W\nSleep 500ms"
  },
  {
    "path": "vhs/spf_file_panel_selection_mode.tape",
    "content": "Output asset/demo.gif\n\nSet Shell \"base\"\nSet FontSize 30\nSet Framerate 60\nSet Width 2560\nSet Height 1500\nSet FontFamily \"Comic Mono, RobotoMono Nerd Font\"\nSet PlaybackSpeed 0.6\nSet Padding 50\nSet 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\" }\n\nType \"spf\"\nSleep 1500ms\nEnter\nSleep 1500ms\nType \"v\"\n\nSleep 600ms\n\nType \"l\"\nSleep 300ms\nType \"l\"\nSleep 300ms\n\nType \"J\"\nSleep 300ms\nType \"J\"\nSleep 300ms\nType \"J\"\nSleep 300ms\nType \"J\"\nSleep 300ms\nType \"J\"\nSleep 300ms\n\nType \"l\"\nSleep 300ms\n\nType \"K\"\nSleep 300ms\nType \"K\"\nSleep 300ms\nType \"K\"\nSleep 300ms\nType \"K\"\nSleep 300ms\n\nCtrl+A\nSleep 1500ms\nType \"v\"\nSleep 1500ms\n"
  },
  {
    "path": "vhs/spf_panel_navigation.tape",
    "content": "Output asset/demo.gif\n\nSet Shell \"base\"\nSet FontSize 30\nSet Width 2560\nSet Height 1500\nSet FontFamily \"Comic Mono, RobotoMono Nerd Font\"\nSet PlaybackSpeed 0.6\nSet Padding 50\nSet 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\" }\n\nType \"spf\"\nSleep 1500ms\nEnter\nSleep 1500ms\nType \"b\"\nSleep 1500ms\nType \"p\"\nSleep 1500ms\nType \"m\"\nSleep 1500ms\nType \"m\"\nSleep 1500ms"
  },
  {
    "path": "website/README.md",
    "content": "# Starlight Starter Kit: Basics\n\n```\nnpm create astro@latest -- --template starlight\n```\n\n[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)\n[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)\n\n> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!\n\n## 🚀 Project Structure\n\nInside of your Astro + Starlight project, you'll see the following folders and files:\n\n```\n.\n├── public/\n├── src/\n│   ├── assets/\n│   ├── content/\n│   │   ├── docs/\n│   │   └── config.ts\n│   └── env.d.ts\n├── astro.config.mjs\n├── package.json\n└── tsconfig.json\n```\n\nStarlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.\n\nImages can be added to `src/assets/` and embedded in Markdown with a relative link.\n\nStatic assets, like favicons, can be placed in the `public/` directory.\n\n## 🧞 Commands\n\nAll commands are run from the root of the project, from a terminal:\n\n| Command                   | Action                                           |\n| :------------------------ | :----------------------------------------------- |\n| `npm install`             | Installs dependencies                            |\n| `npm run dev`             | Starts local dev server at `localhost:3000`      |\n| `npm run build`           | Build your production site to `./dist/`          |\n| `npm run preview`         | Preview your build locally, before deploying     |\n| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |\n| `npm run astro -- --help` | Get help using the Astro CLI                     |\n\n## 👀 Want to learn more?\n\nCheck 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).\n"
  },
  {
    "path": "website/astro.config.mjs",
    "content": "import { defineConfig } from \"astro/config\";\nimport starlight from \"@astrojs/starlight\";\nimport { pluginLineNumbers } from \"@expressive-code/plugin-line-numbers\";\nimport starlightGiscus from \"starlight-giscus\";\nimport sitemap from \"@astrojs/sitemap\";\n\nconst site = \"https://superfile.dev/\";\n\n// https://astro.build/config\nexport default defineConfig({\n  site: site,\n  integrations: [\n    sitemap(),\n    starlight({\n      title: \"superfile\",\n      description: `superfile is a very fancy and modern terminal file manager that can complete the file operations you need!`,\n      expressiveCode: {\n        themes: [\"dracula\", \"solarized-light\"],\n      },\n      logo: {\n        light: \"/src/assets/superfile-day.svg\",\n        dark: \"/src/assets/superfile-night.svg\",\n        replacesTitle: true,\n      },\n      components: {\n        LastUpdated: \"./src/components/LastUpdated.astro\",\n      },\n      plugins: [\n        starlightGiscus({\n          repo: \"yorukot/superfile\",\n          repoId: \"R_kgDOLil1MA\",\n          category: \"Docs Comments\",\n          categoryId: \"DIC_kwDOLil1MM4CfbH7\",\n          mapping: \"title\",\n          strict: false,\n          reactionsEnabled: true,\n          emitMetadata: false,\n          inputPosition: \"top\",\n          theme: \"preferred_color_scheme\",\n          lang: \"en\",\n          loading: \"lazy\",\n        }),\n      ],\n      social: [\n        {\n          icon: \"github\",\n          label: \"GitHub\",\n          href: \"https://github.com/yorukot/superfile\",\n        },\n        {\n          icon: \"discord\",\n          label: \"Discord\",\n          href: \"https://discord.gg/YYtJ23Du7B\",\n        },\n      ],\n      head: [\n        {\n          tag: \"meta\",\n          attrs: { property: \"og:image\", content: site + \"og.jpg?v=1\" },\n        },\n        {\n          tag: \"meta\",\n          attrs: { property: \"twitter:image\", content: site + \"og.jpg?v=1\" },\n        },\n        {\n          tag: \"link\",\n          attrs: { rel: \"preconnect\", href: \"https://fonts.googleapis.com\" },\n        },\n        {\n          tag: \"link\",\n          attrs: {\n            rel: \"preconnect\",\n            href: \"https://fonts.gstatic.com\",\n            crossorigin: true,\n          },\n        },\n        {\n          tag: \"link\",\n          attrs: {\n            rel: \"preload\",\n            href: \"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&display=swap\",\n            as: \"style\",\n            onload: \"this.onload=null;this.rel='stylesheet'\",\n          },\n        },\n        {\n          tag: \"noscript\",\n          content:\n            '<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&display=swap\">',\n        },\n        {\n          tag: \"script\",\n          attrs: {\n            src: \"https://cdn.jsdelivr.net/npm/@minimal-analytics/ga4/dist/index.js\",\n            async: true,\n          },\n        },\n        {\n          tag: \"script\",\n          content: ` window.minimalAnalytics = {\n            trackingId: 'G-WFLBCRZ7MC',\n            autoTrack: true,\n          };`,\n        },\n        {\n          tag: \"script\",\n          attrs: {\n            defer: true,\n            src: \"https://umami.yorukot.me/script.js\",\n            \"data-website-id\": \"8792ee93-9a4a-47be-9e7f-2b1587c3a3d1\",\n          },\n        },\n      ],\n      editLink: {\n        baseUrl: \"https://github.com/yorukot/superfile/edit/main/website/\",\n      },\n      sidebar: [\n        {\n          label: \"Overview\",\n          link: \"/overview\",\n        },\n        {\n          label: \"Start Here\",\n          items: [\n            {\n              label: \"Installation\",\n              link: \"/getting-started/installation/\",\n            },\n            {\n              label: \"Tutorial\",\n              link: \"/getting-started/tutorial/\",\n            },\n            {\n              label: \"Image Preview\",\n              link: \"/getting-started/image-preview/\",\n            },\n          ],\n        },\n        {\n          label: \"Configure\",\n          items: [\n            {\n              label: \"All config file path\",\n              link: \"/configure/config-file-path\",\n            },\n            {\n              label: \"superfile config\",\n              link: \"/configure/superfile-config/\",\n            },\n            {\n              label: \"Custom hotkeys\",\n              link: \"/configure/custom-hotkeys/\",\n            },\n            {\n              label: \"Custom theme\",\n              link: \"/configure/custom-theme\",\n            },\n            {\n              label: \"Enable plugin\",\n              link: \"/configure/enable-plugin\",\n            },\n          ],\n        },\n        {\n          label: \"List\",\n          items: [\n            {\n              label: \"Hotkey list\",\n              link: \"/list/hotkey-list/\",\n            },\n            {\n              label: \"Theme list\",\n              link: \"/list/theme-list/\",\n            },\n            {\n              label: \"Plugin list\",\n              link: \"/list/plugin-list/\",\n            },\n          ],\n        },\n        {\n          label: \"Contribute\",\n          items: [\n            {\n              label: \"How to contribute\",\n              link: \"/contribute/how-to-contribute\",\n            },\n            {\n              label: \"File structure\",\n              link: \"/contribute/file-struct\",\n            },\n            {\n              label: \"Implementation Info\",\n              link: \"/contribute/implementation-info\",\n            },\n          ],\n        },\n        {\n          label: \"Troubleshooting\",\n          link: \"/troubleshooting\",\n        },\n        {\n          label: \"Special thanks\",\n          link: \"/special-thanks\",\n        },\n        {\n          label: \"Changelog\",\n          link: \"/changelog\",\n        },\n      ],\n      customCss: [\"./src/styles/custom.css\"],\n      lastUpdated: true,\n    }),\n  ],\n  // Process images with sharp: https://docs.astro.build/en/guides/assets/#using-sharp\n  image: {\n    service: {\n      entrypoint: \"astro/assets/services/sharp\",\n      config: {\n        limitInputPixels: false,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "website/ec.config.mjs",
    "content": "import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections';\nimport { pluginLineNumbers } from '@expressive-code/plugin-line-numbers';\n\n/** @type {import('@astrojs/starlight/expressive-code').StarlightExpressiveCodeOptions} */\nexport default {\n  // Example: Using a custom plugin (which makes this `ec.config.mjs` file necessary)\n  // plugins: [pluginCollapsibleSections(), pluginLineNumbers()],\n  // ... any other options you want to configure\n};\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"ossified-orbit\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/sitemap\": \"^3.4.1\",\n    \"@astrojs/starlight\": \"^0.37.0\",\n    \"@expressive-code/plugin-collapsible-sections\": \"^0.41.2\",\n    \"@expressive-code/plugin-line-numbers\": \"^0.41.2\",\n    \"@fontsource/ibm-plex-mono\": \"^5.2.5\",\n    \"@fontsource/ibm-plex-serif\": \"^5.2.5\",\n    \"astro\": \"^5.15.6\",\n    \"hast-util-to-html\": \"^9.0.5\",\n    \"sharp\": \"^0.34.1\",\n    \"starlight-giscus\": \"^0.8.0\"\n  }\n}\n"
  },
  {
    "path": "website/public/_redirects",
    "content": "# redirect all /docs requests to the root domain\n\n/docs/\\* /:splat 301\n"
  },
  {
    "path": "website/public/google0fdf22175b8dde4d.html",
    "content": "google-site-verification: google0fdf22175b8dde4d.html"
  },
  {
    "path": "website/public/install.ps1",
    "content": "param(\n    [switch]\n    $AllUsers\n)\n\nfunction FolderIsInPATH($Path_to_directory) {\n    return ([Environment]::GetEnvironmentVariable(\"PATH\", \"User\") -split ';').TrimEnd('\\') -contains $Path_to_directory.TrimEnd('\\')\n}\n\nWrite-Host -ForegroundColor DarkRed     \"                                                    ______   __  __           \"\nWrite-Host -ForegroundColor Red         \"                                                   /      \\ /  |/  |          \"\nWrite-Host -ForegroundColor DarkYellow  \"  _______  __    __   ______    ______    ______  /`$`$`$`$`$`$  |`$`$/ `$`$ |  ______  \"\nWrite-Host -ForegroundColor Yellow      \" /       |/  |  /  | /      \\  /      \\  /      \\ `$`$ |_ `$`$/ /  |`$`$ | /      \\ \"\nWrite-Host -ForegroundColor DarkGreen   \"/`$`$`$`$`$`$`$/ `$`$ |  `$`$ |/`$`$`$`$`$`$  |/`$`$`$`$`$`$  |/`$`$`$`$`$`$  |`$`$   |    `$`$ |`$`$ |/`$`$`$`$`$`$  |\"\nWrite-Host -ForegroundColor Green       \"`$`$      \\ `$`$ |  `$`$ |`$`$ |  `$`$ |`$`$    `$`$ |`$`$ |  `$`$/ `$`$`$`$/     `$`$ |`$`$ |`$`$    `$`$ |\"\nWrite-Host -ForegroundColor DarkBlue    \" `$`$`$`$`$`$  |`$`$ \\__`$`$ |`$`$ |__`$`$ |`$`$`$`$`$`$`$`$/ `$`$ |      `$`$ |      `$`$ |`$`$ |`$`$`$`$`$`$`$`$/ \"\nWrite-Host -ForegroundColor Blue        \"      `$`$/ `$`$    `$`$/ `$`$    `$`$/ `$`$       |`$`$ |      `$`$ |      `$`$ |`$`$ |`$`$       |\"\nWrite-Host -ForegroundColor DarkMagenta \"`$`$`$`$`$`$`$/   `$`$`$`$`$`$/  `$`$`$`$`$`$`$/   `$`$`$`$`$`$`$/ `$`$/       `$`$/       `$`$/ `$`$/  `$`$`$`$`$`$`$/ \"\nWrite-Host -ForegroundColor Magenta     \"                    `$`$ |                                                      \"\nWrite-Host -ForegroundColor DarkRed     \"                    `$`$ |                                                      \"\nWrite-Host -ForegroundColor Red         \"                    `$`$/                                                       \"\nWrite-Host \"\"\n\nfunction Get-LatestVersion {\n    try {\n        $release = Invoke-RestMethod -Uri \"https://api.github.com/repos/yorukot/superfile/releases/latest\" -TimeoutSec 5\n        $version = $release.tag_name -replace '^v', ''\n        if ([string]::IsNullOrEmpty($version)) {\n            Write-Host \"Failed to parse version from GitHub API\"\n            exit 1\n        }\n        return $version\n    } catch {\n        Write-Host \"Failed to fetch latest version from GitHub API: $_\"\n        exit 1\n    }\n}\n\n$package = \"superfile\"\n$version = if ($env:SPF_INSTALL_VERSION) { $env:SPF_INSTALL_VERSION } else { Get-LatestVersion }\n\n$installInstructions = @'\nThis installer is only available for Windows.\nIf you're looking for installation instructions for your operating system,\nplease visit the following link:\n'@\nif ($IsMacOS) {\n    Write-Host @\"\n$installInstructions\n\nhttps://github.com/yorukot/superfile?tab=readme-ov-file#installation\n\"@\n    exit\n}\nif ($IsLinux) {\n    Write-Host @\"\n$installInstructions\n\nhttps://github.com/yorukot/superfile?tab=readme-ov-file#installation\n\"@\n    exit\n}\n\n$arch = (Get-CimInstance -Class Win32_Processor -Property Architecture).Architecture | Select-Object -First 1\nswitch ($arch) {\n    5 { $arch = \"arm64\" } # ARM\n    9 {\n        if ([Environment]::Is64BitOperatingSystem) {\n            $arch = \"amd64\"\n        }\n    }\n    12 { $arch = \"arm64\" } # Surface Pro X\n}\nif ([string]::IsNullOrEmpty($arch)) {\n    Write-Host @\"\nThe installer for system arch ($arch) is not available.\n\"@\n    exit\n}\n$filename = \"$package-windows-v$version-$arch.zip\"\n\n$ProgressPreference = 'SilentlyContinue' #speeds up Download massively, but doesnt show Bits written\n\nWrite-Host \"Checking for superfile installation...\"\n\n$superfileProgramPath = [Environment]::GetFolderPath(\"LocalApplicationData\") + \"\\Programs\\superfile\"\n$superfileExePath = $superfileProgramPath + \"\\spf.exe\"\n\nif (-not (Test-Path $superfileProgramPath)) {\n    New-Item -Path $superfileProgramPath -ItemType Directory -Verbose:$false | Out-Null\n} else {\n    if (Test-Path $superfileExePath) {\n        $versionOutput = & $superfileExePath --version\n        $versionOutput = $versionOutput.Replace('superfile version v', '')\n\n        $currentVersionParts = $version -split '\\.' | ForEach-Object { [int]$_ }\n        $installedVersionParts = $versionOutput -split '\\.' | ForEach-Object { [int]$_ }\n\n        # Compare versions part by part\n        $isUpToDate = $true\n        for ($i = 0; $i -lt $currentVersionParts.Count; $i++) {\n            if ($currentVersionParts[$i] -gt $installedVersionParts[$i]) {\n                $isUpToDate = $false\n                break\n            } elseif ($currentVersionParts[$i] -lt $installedVersionParts[$i]) {\n                continue\n            }\n        }\n        if ($isUpToDate) {\n            Write-Host \"superfile already installed, quitting...\"\n        } else {\n            Write-Host \"Old version (superfile v$versionOutput) found, removing...\"\n            try {\n                if (Test-Path $superfileExePath) {\n                    Remove-Item -Path $superfileExePath -Force\n                }\n            }\n            catch {\n                Write-Host \"An error occurred: $_\"\n                exit\n            }\n        }\n    } else {\n        Write-Host \"superfile folder found but not executable :/, please check your %localappdata%\\Programs\\superfile for conflict.\"\n        exit\n    }\n}\n\nWrite-Host \"Downloading superfile...(Version v$version)\"\n\n$url = \"https://github.com/yorukot/superfile/releases/download/v$version/$filename\"\ntry {\n    Invoke-WebRequest -OutFile \"$superfileProgramPath/$filename\" $url\n} catch {\n    Write-Host \"An error occurred: $_\"\n    exit\n}\n\nWrite-Host \"Extracting compressed file...\"\n\ntry {\n    $tempDirectory = \"$superfileProgramPath\\temp\"\n    New-Item -ItemType Directory -Path $tempDirectory -Force | Out-Null\n    Expand-Archive -Path \"$superfileProgramPath\\$filename\" -DestinationPath $tempDirectory\n    Remove-Item -Path \"$superfileProgramPath\\$filename\"\n    $thisisredundant = (Get-ChildItem -Path $tempDirectory -Directory | Sort-Object Name -Descending | Select-Object -First 1).Name\n    $lastFolderName = (Get-ChildItem -Path \"$tempDirectory\\$thisisredundant\" -Directory | Sort-Object Name -Descending | Select-Object -First 1).Name\n    Move-Item -Path \"$tempDirectory\\$thisisredundant\\$lastFolderName\\*\" -Destination $superfileProgramPath -Force\n    Remove-Item -Path $tempDirectory -Recurse -Force\n} catch {\n    Write-Host \"An error occurred: $_\"\n    exit\n}\nif (-not (FolderIsInPATH \"$superfileProgramPath\\\")) {\n    $envPath = [Environment]::GetEnvironmentVariable(\"PATH\", \"User\")\n    $newPath = \"$superfileProgramPath\\\"\n    $updatedPath = $envPath.TrimEnd(\";\") + \";\" + $newPath + \";\"\n    [Environment]::SetEnvironmentVariable(\"PATH\", $updatedPath, \"User\")\n}\n\nWrite-Host @'\nDone!\n\nRestart you terminal, and for the love of Get-Command\nTake a look at tutorial :)\n\nhttps://superfile.dev/getting-started/tutorial/\n'@\n"
  },
  {
    "path": "website/public/install.sh",
    "content": "#!/bin/bash\n\ngreen='\\033[0;32m'\nred='\\033[0;31m'\nyellow='\\033[0;33m'\nblue='\\033[0;34m'\npurple='\\033[0;35m'\ncyan='\\033[0;36m'\nwhite='\\033[0;37m'\nbright_red='\\033[1;31m'\nbright_green='\\033[1;32m'\nbright_yellow='\\033[1;33m'\nbright_blue='\\033[1;34m'\nbright_purple='\\033[1;35m'\nbright_cyan='\\033[1;36m'\nbright_white='\\033[1;37m'\nnc='\\033[0m' # No Color\n\necho -e '\n\\033[0;31m                                                    ______   __  __           \n\\033[1;31m                                                   /      \\ /  |/  |          \n\\033[0;33m  _______  __    __   ______    ______    ______  /$$$$$$  |$$/ $$ |  ______  \n\\033[1;33m /       |/  |  /  | /      \\  /      \\  /      \\ $$ |_ $$/ /  |$$ | /      \\ \n\\033[0;32m/$$$$$$$/ $$ |  $$ |/$$$$$$  |/$$$$$$  |/$$$$$$  |$$   |    $$ |$$ |/$$$$$$  |\n\\033[1;32m$$      \\ $$ |  $$ |$$ |  $$ |$$    $$ |$$ |  $$/ $$$$/     $$ |$$ |$$    $$ |\n\\033[0;34m $$$$$$  |$$ \\__$$ |$$ |__$$ |$$$$$$$$/ $$ |      $$ |      $$ |$$ |$$$$$$$$/ \n\\033[1;34m/     $$/ $$    $$/ $$    $$/ $$       |$$ |      $$ |      $$ |$$ |$$       |\n\\033[0;35m$$$$$$$/   $$$$$$/  $$$$$$$/   $$$$$$$/ $$/       $$/       $$/ $$/  $$$$$$$/ \n\\033[1;35m                    $$ |                                                      \n\\033[0;31m                    $$ |                                                      \n\\033[1;31m                    $$/                                                       \n'\n\n\ntemp_dir=$(mktemp -d)\nif [ $? -ne 0 ]; then\n    echo -e \"${red}❌ Fail install superfile: ${yellow}Unable to create temporary directory${nc}\"\n    exit 1\nfi\n\nfetch_latest_version() {\n    local response\n    if response=$(curl -s --max-time 5 \"https://api.github.com/repos/yorukot/superfile/releases/latest\"); then\n        local version\n        version=$(echo \"$response\" | grep '\"tag_name\"' | cut -d'\"' -f4 | sed 's/^v//')\n        if [ -n \"$version\" ]; then\n            echo \"$version\"\n        else\n            echo -e \"${red}❌ Failed to parse version from GitHub API${nc}\" >&2\n            exit 1\n        fi\n    else\n        echo -e \"${red}❌ Failed to fetch latest version from GitHub API${nc}\" >&2\n        exit 1\n    fi\n}\n\npackage=superfile\nversion=${SPF_INSTALL_VERSION:-$(fetch_latest_version)}\narch=$(uname -m)\nos=$(uname -s)\n\ncd \"${temp_dir}\"\n\nif [[ \"$arch\" == \"x86_64\" || \"$arch\" == \"amd64\" ]]; then\n    arch=\"amd64\"\nelif [[ \"$arch\" == \"arm\"* || \"$arch\" == \"aarch64\" || \"$arch\" == \"arm64\" ]]; then\n    arch=\"arm64\"\nelse\n    echo -e \"${red}❌ Fail install superfile: ${yellow}Unsupported architecture${nc}\"\n    exit 1\nfi\n\nif [[ \"$os\" == \"Linux\" ]]; then\n    os=\"linux\"\nelif [[ \"$os\" == \"Darwin\" ]]; then\n    os=\"darwin\"\nelse\n    echo -e \"${red}❌ Fail install superfile: ${yellow}Unsupported operating system${nc}\"\n    exit 1\nfi\n\nfile_name=${package}-${os}-v${version}-${arch}\n\nurl=\"https://github.com/yorukot/superfile/releases/download/v${version}/${file_name}.tar.gz\"\n\nif command -v curl &> /dev/null; then\n    echo -e \"${bright_yellow}Downloading ${cyan}${package} v${version} for ${os} (${arch})...${nc}\"\n    curl -sLO \"$url\"\nelse\n    echo -e \"${bright_yellow}Downloading ${cyan}${package} v${version} for ${os} (${arch})...${nc}\"\n    wget -q \"$url\"\nfi\n\necho -e \"${bright_yellow}Extracting ${cyan}${package}...${nc}\"\ntar -xzf \"${file_name}.tar.gz\"\n\necho -e \"${bright_yellow}Installing ${cyan}${package}...${nc}\"\ncd ./dist/${file_name}\nchmod +x ./spf\necho -e \"${yellow}Press ctrl+C to not install as sudo and try locally.${nc}\"\nif ! sudo mv ./spf /usr/local/bin/; then\n  echo -e \"${yellow}Unable to move binary to /usr/local/bin. Do you have sudo permissions?${nc}\"\n  mkdir -p ~/.local/bin\n  if ! mv ./spf ~/.local/bin/; then\n    echo -e \"${red}❌ Failed to install superfile: Unable to move to ~/.local/bin as well.${nc}\"\n  else\n    if ! [[ \":$PATH:\" == *\":$HOME/.local/bin:\"* ]]; then\n      shell_found_and_not_bash=1\n      case $SHELL in\n        */bash)\n          echo 'export PATH=\"${HOME}/.local/bin\":${PATH}' >> ~/.bashrc\n          shell_found_and_not_bash=0\n          ;;\n        */zsh)\n          echo 'export PATH=\"${HOME}/.local/bin\":${PATH}' >> ~/.zshrc\n          ;;\n        */fish)\n          echo 'fish_add_path \"${HOME}/.local/bin\"' >> ~/.config/fish/config.fish \n          ;;\n        */ksh)\n          echo 'export PATH=\"${HOME}/.local/bin\":${PATH}' >> ~/.kshrc\n          ;;\n        */xonsh)\n          echo '$PATH.prepend(\"${HOME}/.local/bin\")' >> ~/.xonshrc\n          ;;\n        */csh)\n          echo 'setenv PATH \"${HOME}/.local/bin\":${PATH}' >> ~/.cshrc\n          ;;\n        */tcsh)\n          echo 'setenv PATH \"${HOME}/.local/bin\":${PATH}' >> ~/.tshrc\n          ;;\n        *)\n          echo -e \"${red}Unsupported shell: ${SHELL}. Please add ${white}\\\"${bright_cyan}\\${HOME}/.local/bin${white}\\\" ${red}to PATH in your shell's config file.${red}\"\n          shell_found_and_not_bash=0\n          ;;\n      esac\n      if [ $shell_found_and_not_bash == 1 ]; then\n        echo -e \"${white}\\\"${bright_purple}${HOME}/.local/bin${white}\\\"${yellow} has been added to your PATH.${nc}\"\n        echo -e \"${yellow}Please source your config file/relogin.${nc}\"\n      fi\n    fi\n    echo -e \"🎉 ${bright_cyan}Local ${bright_green}Installation complete!${nc}\"\n    echo -e \"${bright_cyan}You can type ${white}\\\"${bright_yellow}spf${white}\\\" ${bright_cyan}to start!${nc}\"\n  fi\nelse\n  echo -e \"🎉 ${bright_green}Installation complete!${nc}\"\n  echo -e \"${bright_cyan}You can type ${white}\\\"${bright_yellow}spf${white}\\\" ${bright_cyan}to start!${nc}\"\nfi\n\nrm -rf \"$temp_dir\"\n"
  },
  {
    "path": "website/public/uninstall.ps1",
    "content": "param(\n    [switch]\n    $AllUsers\n)\n\nfunction FolderIsInPATH($Path_to_directory) {\n    return ([Environment]::GetEnvironmentVariable(\"PATH\", \"User\") -split ';').TrimEnd('\\') -contains $Path_to_directory.TrimEnd('\\')\n}\n\nWrite-Host -ForegroundColor DarkRed     \"                                                    ______   __  __           \"\nWrite-Host -ForegroundColor Red         \"                                                   /      \\ /  |/  |          \"\nWrite-Host -ForegroundColor DarkYellow  \"  _______  __    __   ______    ______    ______  /`$`$`$`$`$`$  |`$`$/ `$`$ |  ______  \"\nWrite-Host -ForegroundColor Yellow      \" /       |/  |  /  | /      \\  /      \\  /      \\ `$`$ |_ `$`$/ /  |`$`$ | /      \\ \"\nWrite-Host -ForegroundColor DarkGreen   \"/`$`$`$`$`$`$`$/ `$`$ |  `$`$ |/`$`$`$`$`$`$  |/`$`$`$`$`$`$  |/`$`$`$`$`$`$  |`$`$   |    `$`$ |`$`$ |/`$`$`$`$`$`$  |\"\nWrite-Host -ForegroundColor Green       \"`$`$      \\ `$`$ |  `$`$ |`$`$ |  `$`$ |`$`$    `$`$ |`$`$ |  `$`$/ `$`$`$`$/     `$`$ |`$`$ |`$`$    `$`$ |\"\nWrite-Host -ForegroundColor DarkBlue    \" `$`$`$`$`$`$  |`$`$ \\__`$`$ |`$`$ |__`$`$ |`$`$`$`$`$`$`$`$/ `$`$ |      `$`$ |      `$`$ |`$`$ |`$`$`$`$`$`$`$`$/ \"\nWrite-Host -ForegroundColor Blue        \"      `$`$/ `$`$    `$`$/ `$`$    `$`$/ `$`$       |`$`$ |      `$`$ |      `$`$ |`$`$ |`$`$       |\"\nWrite-Host -ForegroundColor DarkMagenta \"`$`$`$`$`$`$`$/   `$`$`$`$`$`$/  `$`$`$`$`$`$`$/   `$`$`$`$`$`$`$/ `$`$/       `$`$/       `$`$/ `$`$/  `$`$`$`$`$`$`$/ \"\nWrite-Host -ForegroundColor Magenta     \"                    `$`$ |                                                      \"\nWrite-Host -ForegroundColor DarkRed     \"                    `$`$ |                                                      \"\nWrite-Host -ForegroundColor Red         \"                    `$`$/                                                       \"\nWrite-Host \"\"\n\n$package = \"superfile\"\n\n$installInstructions = @'\nThis uninstaller is only available for Windows.\n'@\nif ($IsMacOS) {\n    Write-Host \"$installInstructions\"\n    exit\n}\nif ($IsLinux) {\n    Write-Host \"$installInstructions\"\n    exit\n}\n\nWrite-Host \"Removing folder...\"\n\n$superfileProgramPath = [Environment]::GetFolderPath(\"LocalApplicationData\") + \"\\Programs\\superfile\"\ntry {\n    if (Test-Path $superfileProgramPath) {\n        Remove-Item -Path $superfileProgramPath -Recurse -Force\n    }\n}\ncatch {\n    Write-Host \"An error occurred: $_\"\n    exit\n}\n\nWrite-Host \"Removing environment path...\"\n\ntry {\n    if (FolderIsInPATH \"$superfileProgramPath\\\") {\n        $envPath = [Environment]::GetEnvironmentVariable(\"PATH\", \"User\")\n        $updatedPath =($envPath.Split(';') | Where-Object { $_ -ne \"$superfileProgramPath\" }) -join ';' \n        [Environment]::SetEnvironmentVariable(\"PATH\", $updatedPath, \"User\")\n    }\n}\ncatch {\n    Write-Host \"An error occurred: $_\"\n    exit\n}\n\nWrite-Host @'\nUninstall Done!\n'@\n\n"
  },
  {
    "path": "website/src/components/GithubStar.astro",
    "content": "---\nimport { Icon } from '@astrojs/starlight/components';\n\n---\n\n<style>\n  @keyframes spinner {\n    to {\n      transform: rotate(360deg);\n    }\n  }\n\n  @keyframes slide-down {\n    0% {\n      transform: translate(-50%, -140%);\n      opacity: 0;\n    }\n    100% {\n      transform: translate(-50%, 0%);\n      opacity: 1;\n    }\n  }\n  .spinner:before {\n    content: \"\";\n    box-sizing: border-box;\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    width: 12px;\n    height: 12px;\n    margin-top: -6px;\n    margin-left: -7px;\n    border-radius: 50%;\n    border: 2px solid #fff;\n    border-top-color: #333;\n    animation: spinner 0.6s linear infinite;\n  }\n  .container{\n    position: absolute;\n    top: 80px;\n    left: 50%;\n    transform: translate(-50%, 0%);\n    background-color: transparent;\n    drop-shadow: drop-shadow(0 20px 13px rgba(0, 0, 0, 0.03)) drop-shadow(0 8px 5px rgba(0, 0, 0, 0.08));\n    border-radius: 20px;\n    border: 1px solid var(--sl-color-accent-high);\n    animation: slide-down 0.5s ease-in-out;\n  }\n  @media screen and (max-width: 768px) {\n    .container{\n      display: none;\n    }\n  }\n  .link {\n    display: flex;\n    padding: 0.5rem;\n    padding-left: 0.75rem;\n    padding-right: 0.75rem;\n    gap: 0.5rem;\n    align-items: center;\n    border-radius: 9999px;\n    border-width: 1px;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 100;\n    background-image: background-image: linear-gradient(to right, var(--tw-gradient-stops));\n    transition-duration: 300ms;\n    text-decoration: none;\n  }\n  .link:hover {\n    text-decoration: none;\n    color: var(--sl-color-accent-high);\n  }\n  .star-count{\n    font-weight: 700;\n    min-width: 20px;\n    color: transparent;\n    background-clip: text;\n    background-image: background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));\n    background-color: #F59E0B;\n    filter: drop-shadow(0 0 3rem var(--overlay-blurple));\n    drop-shadow: drop-shadow(0 20px 13px rgba(0, 0, 0, 0.03)) drop-shadow(0 8px 5px rgba(0, 0, 0, 0.08));\n  }\n\n</style>\n\n<script>\n  document.addEventListener(\"DOMContentLoaded\", () => {\n    const starCountElement = document.getElementById(\"star-count\");\n    if (starCountElement) {\n      starCountElement.classList.add(\"spinner\");\n\n      fetch(\"https://api.github.com/repos/yorukot/superfile\")\n        .then((response) => response.json())\n        .then((data) => {\n          starCountElement.classList.remove(\"spinner\");\n          const starCount = data.stargazers_count;\n          starCountElement.textContent = starCount;\n        })\n        .catch((error) => {\n          starCountElement.classList.remove(\"spinner\");\n          console.error(\"Error:\", error);\n        });\n    }\n  });\n</script>\n<div class=\"container\">\n<a\n  target=\"_blank\"\n  rel=\"noreferrer noopener\"\n  href=\"https://github.com/yorukot/superfile\"\n  class=\"link\">\n  <span\n    id=\"star-count\"\n    class=\"star-count\"\n  ></span>\n  <Icon name=\"star\" color=\"goldenrod\" size=\"1rem\" />\n  <span class=\"opacity-50\">|</span>\n  <span class=\"\">Give us a star on Github</span>\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"16\"\n    height=\"16\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    stroke-width=\"2\"\n    stroke-linecap=\"round\"\n    stroke-linejoin=\"round\"\n  >\n    <path d=\"m9 18 6-6-6-6\"></path>\n  </svg>\n</a>\n</div>"
  },
  {
    "path": "website/src/components/LastUpdated.astro",
    "content": "---\nimport type { Props } from '@astrojs/starlight/props';\nimport Default from '@astrojs/starlight/components/LastUpdated.astro';\n\nconst { lastUpdated } = Astro.props;\n\n---\n\n{lastUpdated && (\n  <div class=\"last-updated\">\n    <Default {...Astro.props}><slot /></Default>\n  </div>\n)}\n\n<style>\n\t.last-updated {\n    width: 100%;\n    text-align: right;\n\t}\n</style>"
  },
  {
    "path": "website/src/components/about.astro",
    "content": "---\ninterface Props {\n  title: string;\n}\n\nconst { title } = Astro.props;\n---\n\n<article class=\"sl-flex not-content\" aria-labelledby=\"about-obytes-heading\">\n  <small id=\"about-obytes-heading\">\n    {title}\n    <span class=\"sr-only\">Obytes</span>\n  </small>\n  <a href=\"https://github.com/obytes/react-native-template-obytes\" target=\"_blank\">\n  <small id=\"about-obytes-heading\">\n    Site := \"heavily inspired\" from starter.obytes.com Docs\n    <span class=\"sr-only\">Obytes</span>\n  </small></a>\n  <slot />\n</article>\n\n<style>\n  article {\n    max-width: 50rem;\n    margin-inline: auto;\n    padding-block: 5rem;\n    flex-direction: column;\n    align-items: center;\n    text-align: center;\n    gap: 0.5rem;\n  }\n  article > :global(*) {\n    max-width: 54ch;\n  }\n  small {\n    color: var(--sl-color-gray-3);\n  }\n  svg {\n    width: 15rem;\n  }\n</style>\n"
  },
  {
    "path": "website/src/components/code.astro",
    "content": "---\nimport { Code as SCode } from '@astrojs/starlight/components'\n\nimport fs from 'node:fs/promises';\n\ninterface Props {\n  file: string;\n  language?: string;\n  meta?: string;\n}\n\nconst { file, language, meta } = Astro.props;\nconst fileNamePath = '../' + file;\nconst fileEtension = file.split('.').pop() ?? 'js';\nconst code = await fs.readFile(fileNamePath, 'utf-8');\nconst lang  = language ?? fileEtension;\nconst metaa =  `title=\"${file}\"` + (meta ? ` ${meta}` : '')\n\n\n---\n\n<SCode code={code} lang={lang} meta={metaa} />\n\n<!-- <SCode code=\"console.log('Hello world!')\" lang=\"js\" /> -->\n"
  },
  {
    "path": "website/src/content/config.ts",
    "content": "import { defineCollection } from 'astro:content';\nimport { docsSchema, i18nSchema } from '@astrojs/starlight/schema';\n\nexport const collections = {\n  docs: defineCollection({ schema: docsSchema() }),\n  i18n: defineCollection({ type: 'data', schema: i18nSchema() }),\n};\n"
  },
  {
    "path": "website/src/content/docs/changelog.md",
    "content": "---\ntitle: CHANGELOG\ndescription: New features, improvements, and bug fixes for the superfile.\nhead:\n  - tag: title\n    content: superfile ChangeLog | superfile\n---\n\n# ChangeLog\n\nAll notable changes to this project will be documented in this file. Dates are displayed in UTC(YYYY-MM-DD).\n\n# [**v1.5.0**](https://github.com/yorukot/superfile/releases/tag/v1.5.0)\n\n> 2026-01-11\n\n#### Update\n- allow hover to file [`#1177`](https://github.com/yorukot/superfile/pull/1177)\n- show count selected items in select mode [`#1187`](https://github.com/yorukot/superfile/pull/1187)\n- Add icon alias for kts to kt [`#1153`](https://github.com/yorukot/superfile/pull/1153)\n- link icon and metadata [`#1171`](https://github.com/yorukot/superfile/pull/1171)\n- user configuration of editors by file extension [`#1197`](https://github.com/yorukot/superfile/pull/1197)\n- add video preview support [`#1178`](https://github.com/yorukot/superfile/pull/1178)\n- Add pdf preview support [`#1198`](https://github.com/yorukot/superfile/pull/1198)\n- Add icons in pinned directories [`#1215`](https://github.com/yorukot/superfile/pull/1215)\n- Enable fast configurable navigation [`#1220`](https://github.com/yorukot/superfile/pull/1220)\n- add Trash bin to default directories for Linux [`#1236`](https://github.com/yorukot/superfile/pull/1236)\n- add terminal stdout support for shell commands [`#1250`](https://github.com/yorukot/superfile/pull/1250)\n- More columns in file panel (MVP) [`#1268`](https://github.com/yorukot/superfile/pull/1268)\n\n#### Bug Fix\n- only calculate checksum on files [`#1119`](https://github.com/yorukot/superfile/pull/1119)\n- Linter issue with PrintfAndExit [`#1133`](https://github.com/yorukot/superfile/pull/1133)\n- Remove repeated os.ReadDir calls [`#1155`](https://github.com/yorukot/superfile/pull/1155)\n- Disable COPYFILE in macOS [`#1194`](https://github.com/yorukot/superfile/pull/1194)\n- add missing hotkeys to help menu [`#1192`](https://github.com/yorukot/superfile/pull/1192)\n- Fetch latest version automatically [`#1127`](https://github.com/yorukot/superfile/pull/1127)\n- Use async methods to prevent test race conditions [`#1201`](https://github.com/yorukot/superfile/pull/1201)\n- update metadata and process bar sizes when toggling footer [`#1218`](https://github.com/yorukot/superfile/pull/1218)\n- File panel dimension management [`#1222`](https://github.com/yorukot/superfile/pull/1222)\n- Layout fixes with full end-to-end tests [`#1227`](https://github.com/yorukot/superfile/pull/1227)\n- Fix flaky tests [`#1233`](https://github.com/yorukot/superfile/pull/1233)\n- modal confirmation bug with arrow keys [`#1243`](https://github.com/yorukot/superfile/pull/1243)\n- small file panel optimization [`#1241`](https://github.com/yorukot/superfile/pull/1241)\n- use ExtractOperationMsg for extraction [`#1248`](https://github.com/yorukot/superfile/pull/1248)\n- skip open_with from missing field validation [`#1251`](https://github.com/yorukot/superfile/pull/1251)\n- border height validation fixes [`#1267`](https://github.com/yorukot/superfile/pull/1267)\n- fix case with two active panes [`#1271`](https://github.com/yorukot/superfile/pull/1271)\n- help model formatting [`#1277`](https://github.com/yorukot/superfile/pull/1277)\n\n#### Optimization\n- simplify renameIfDuplicate logic [`#1100`](https://github.com/yorukot/superfile/pull/1100)\n- separate FilePanel into dedicated package [`#1195`](https://github.com/yorukot/superfile/pull/1195)\n- File model separation [`#1223`](https://github.com/yorukot/superfile/pull/1223)\n- Dimension validations [`#1224`](https://github.com/yorukot/superfile/pull/1224)\n- layout validation and sidebar dimension fixes [`#1228`](https://github.com/yorukot/superfile/pull/1228)\n- user rendering package and removal of unused preview code [`#1245`](https://github.com/yorukot/superfile/pull/1245)\n- user rendering package for file preview [`#1249`](https://github.com/yorukot/superfile/pull/1249)\n\n#### Documentation\n- update Fish shell setup docs [`#1142`](https://github.com/yorukot/superfile/pull/1142)\n- fix macOS typo [`#1212`](https://github.com/yorukot/superfile/pull/1212)\n- stylistic and linguistic cleanup of config documentation [`#1184`](https://github.com/yorukot/superfile/pull/1184)\n\n#### Dependencies\n- update astro monorepo [`#1010`](https://github.com/yorukot/superfile/pull/1010)\n- update starlight-giscus [`#1020`](https://github.com/yorukot/superfile/pull/1020)\n- bump astro versions [`#1138`](https://github.com/yorukot/superfile/pull/1138), [`#1157`](https://github.com/yorukot/superfile/pull/1157), [`#1158`](https://github.com/yorukot/superfile/pull/1158)\n- bump vite [`#1134`](https://github.com/yorukot/superfile/pull/1134)\n- update setup-go action [`#1038`](https://github.com/yorukot/superfile/pull/1038)\n- update expressive-code plugins [`#1189`](https://github.com/yorukot/superfile/pull/1189), [`#1246`](https://github.com/yorukot/superfile/pull/1246)\n- update sharp [`#1256`](https://github.com/yorukot/superfile/pull/1256)\n- update fontsource monorepo [`#1257`](https://github.com/yorukot/superfile/pull/1257)\n- update urfave/cli [`#1136`](https://github.com/yorukot/superfile/pull/1136), [`#1190`](https://github.com/yorukot/superfile/pull/1190)\n- update astro / starlight / ansi / toolchain deps [`#1275`](https://github.com/yorukot/superfile/pull/1275), [`#1278`](https://github.com/yorukot/superfile/pull/1278), [`#1280`](https://github.com/yorukot/superfile/pull/1280)\n- update python and go versions [`#1276`](https://github.com/yorukot/superfile/pull/1276), [`#1191`](https://github.com/yorukot/superfile/pull/1191)\n- update golangci-lint action [`#1286`](https://github.com/yorukot/superfile/pull/1286)\n\n#### Misc\n- update CI input names [`#1120`](https://github.com/yorukot/superfile/pull/1120)\n- Everforest Dark Hard theme [`#1114`](https://github.com/yorukot/superfile/pull/1114)\n- migrate tutorial demo assets to local [`#1140`](https://github.com/yorukot/superfile/pull/1140)\n- new logo asset [`#1145`](https://github.com/yorukot/superfile/pull/1145)\n- mirror repository to codeberg [`#1141`](https://github.com/yorukot/superfile/pull/1141)\n- sync package lock [`#1143`](https://github.com/yorukot/superfile/pull/1143)\n- bump golangci-lint version [`#1135`](https://github.com/yorukot/superfile/pull/1135)\n- add gosec linter [`#1185`](https://github.com/yorukot/superfile/pull/1185)\n- enable MND linter and clean magic numbers [`#1180`](https://github.com/yorukot/superfile/pull/1180)\n- skip permission tests when running as root [`#1186`](https://github.com/yorukot/superfile/pull/1186)\n- release v1.4.1-rc [`#1203`](https://github.com/yorukot/superfile/pull/1203)\n- 1.5.0-rc1 housekeeping changes [`#1264`](https://github.com/yorukot/superfile/pull/1264)\n\n\n# [**v1.4.0**](https://github.com/yorukot/superfile/releases/tag/v1.4.0)\n\n> 2025-10-10\n\n#### Update\n- feat: File operation via tea cmd by [`#963`](https://github.com/yorukot/superfile/pull/963)\n- feat: processbar improvements, package separation, better channel management by [`#970`](https://github.com/yorukot/superfile/pull/970)\n- feat: processbar improvements, package separation, better channel management by [`#973`](https://github.com/yorukot/superfile/pull/973)\n- feat: enable lll and recvcheck linter, fix tests, more refactors by [`#977`](https://github.com/yorukot/superfile/pull/977)\n- feat: Remove channel for notification models by [`#979`](https://github.com/yorukot/superfile/pull/979)\n- feat: enable cyclop, funlen, gocognit, gocyclo linters, and refactor large functions by [`#984`](https://github.com/yorukot/superfile/pull/984)\n- feat: Add a new hotkey to handle cd-on-quit whenever needed by [`#924`](https://github.com/yorukot/superfile/pull/924)\n- feat: added option to permanently delete files by [`#987`](https://github.com/yorukot/superfile/pull/987)\n- feat: Preview panel separation by [`#1021`](https://github.com/yorukot/superfile/pull/1021)\n- feat: Add search functionality to help menu by [`#1011`](https://github.com/yorukot/superfile/pull/1011)\n- feat: Use zoxide lib by [`#1036`](https://github.com/yorukot/superfile/pull/1036)\n- feat: Add zoxide directory tracking on navigation by [`#1041`](https://github.com/yorukot/superfile/pull/1041)\n- feat: Zoxide integration by [`#1039`](https://github.com/yorukot/superfile/pull/1039)\n- feat: Select mode with better feedback by [`#1074`](https://github.com/yorukot/superfile/pull/1074)\n- feat: owner/group in the metadata by [`#1093`](https://github.com/yorukot/superfile/pull/1093)\n- feat: Async zoxide by [`#1104`](https://github.com/yorukot/superfile/pull/1104)\n\n#### Bug Fix\n- fix: sorting in searchbar by [`#985`](https://github.com/yorukot/superfile/pull/985)\n- fix: Async rendering, Include clipboard check in paste items, and update linter configs by [`#997`](https://github.com/yorukot/superfile/pull/997)\n- fix: Move utility functions to utils package by [`#1012`](https://github.com/yorukot/superfile/pull/1012)\n- fix: Refactoring and separation of preview panel and searchbar in help menu by [`#1013`](https://github.com/yorukot/superfile/pull/1013)\n- fix(filePanel): allow focusType to be set correctly by [`#1033`](https://github.com/yorukot/superfile/pull/1033)\n- fix(ci): Update gomod2nix.toml, allow pre release in version output, release 1.4.0-rc1, bug fixes, and improvements by [`#1054`](https://github.com/yorukot/superfile/pull/1054)\n- fix(nix): resolve build failures in the nix flake by [`#1068`](https://github.com/yorukot/superfile/pull/1068)\n- fix: Retry the file deletion to prevent flakies (#938) by [`#1076`](https://github.com/yorukot/superfile/pull/1076)\n- fix(issue-1066): Fixed issue where enter was not searchable by [`#1078`](https://github.com/yorukot/superfile/pull/1078)\n- fix(#1073): Tech debt fix by [`#1077`](https://github.com/yorukot/superfile/pull/1077)\n- fix: fix deleted directory not able to remove from pins (#1067) by [`#1081`](https://github.com/yorukot/superfile/pull/1081)\n- fix: fix child process spawning attached by [`#1084`](https://github.com/yorukot/superfile/pull/1084)\n- fix: always clear images when showing a FullScreenStyle by [`#1094`](https://github.com/yorukot/superfile/pull/1094)\n- fix: Allow j and k keys in zoxide by [`#1102`](https://github.com/yorukot/superfile/pull/1102)\n- fix: Zoxide improvements and 1.4.0-rc2 by [`#1105`](https://github.com/yorukot/superfile/pull/1105)\n- fix: rename cursor beginning on wrong character because of multiple dots in name (#813) by [`#1112`](https://github.com/yorukot/superfile/pull/1112)\n- fix: check and fix file panel scroll position on height changes by [`#1095`](https://github.com/yorukot/superfile/pull/1095)\n\n#### Optimization\n- perf(website): optimize font loading and asset organization by [`#1089`](https://github.com/yorukot/superfile/pull/1089)\n\n#### Documentation\n- docs: fix incorrect zoxide plugin config name by [`#1049`](https://github.com/yorukot/superfile/pull/1049)\n- docs(hotkeys): Fix typo in vimHotkeys.toml comments by [`#1080`](https://github.com/yorukot/superfile/pull/1080)\n- docs: add section for core maintainers in README.md by [`#1088`](https://github.com/yorukot/superfile/pull/1088)\n- chore: add winget install instruction to readme and website by [`#943`](https://github.com/yorukot/superfile/pull/943)\n\n#### Dependencies\n- chore(deps): update dependency go to v1.25.0, golangci-lint to v2, golangci-lint actions to v8 by [`#750`](https://github.com/yorukot/superfile/pull/750)\n- chore(deps): update amannn/action-semantic-pull-request action to v6 by [`#1006`](https://github.com/yorukot/superfile/pull/1006)\n- chore(deps): update actions/first-interaction action to v3 by [`#1005`](https://github.com/yorukot/superfile/pull/1005)\n- chore(deps): update actions/checkout action to v5 by [`#1004`](https://github.com/yorukot/superfile/pull/1004)\n- chore(deps): bump astro from 5.10.1 to 5.12.8 by [`#982`](https://github.com/yorukot/superfile/pull/982)\n- fix(deps): update module golang.org/x/mod to v0.27.0 by [`#989`](https://github.com/yorukot/superfile/pull/989)\n- fix(deps): update dependency @expressive-code/plugin-collapsible-sections to v0.41.3 by [`#990`](https://github.com/yorukot/superfile/pull/990)\n- fix(deps): update dependency sharp to v0.34.3 by [`#992`](https://github.com/yorukot/superfile/pull/992)\n- fix(deps): update dependency @expressive-code/plugin-line-numbers to v0.41.3 by [`#991`](https://github.com/yorukot/superfile/pull/991)\n- chore(deps): update dependency go to v1.25.0 by [`#994`](https://github.com/yorukot/superfile/pull/994)\n- fix(deps): update astro monorepo by [`#995`](https://github.com/yorukot/superfile/pull/995)\n- fix(deps): update dependency @astrojs/starlight to ^0.35.0 by [`#1000`](https://github.com/yorukot/superfile/pull/1000)\n- fix(deps): update module github.com/urfave/cli/v3 to v3.4.1 by [`#1001`](https://github.com/yorukot/superfile/pull/1001)\n- fix(deps): update module golang.org/x/text to v0.28.0 by [`#1003`](https://github.com/yorukot/superfile/pull/1003)\n\n#### Misc\n- chore: migrate from superfile.netlify.app to superfile.dev by [`#1087`](https://github.com/yorukot/superfile/pull/1087)\n- refactor(filepanel): replace filePanelFocusType with isFocused boolean by [`#1040`](https://github.com/yorukot/superfile/pull/1040)\n- refactor(ansi): Migrate from github.com/charmbracelet/x/exp/term/ansi to github.com/charmbracelet/x/ansi by [`#1044`](https://github.com/yorukot/superfile/pull/1044)\n- refactor: common operation on pinned directory file using PinnedManager by [`#1085`](https://github.com/yorukot/superfile/pull/1085)\n- test: unit tests for pinned manager by [`#1090`](https://github.com/yorukot/superfile/pull/1090)\n\n\n# [**v1.3.3**](https://github.com/yorukot/superfile/releases/tag/v1.3.3)\n\n> 2025-07-25\n\n#### Update\n- feat: Metadata loading via bubbletea's tea.Cmd method, removed usage channels and custom goroutines by [`#947`](https://github.com/yorukot/superfile/pull/947)\n- feat: Metadata panel into separate package, UI bug fixes, Code improvements[`#950`](https://github.com/yorukot/superfile/pull/950)\n\n#### Bug Fix\n- fix: windows test ci by [`#941`](https://github.com/yorukot/superfile/pull/941)\n- fix: fixing `config.toml` by [`#952`](https://github.com/yorukot/superfile/pull/952)\n\n#### Misc\n- chore: update pnpm-lcok.yaml by [`#937`](https://github.com/yorukot/superfile/pull/937)\n- feat: add support for Python virtual environment in testsuite setup[`#956`](https://github.com/yorukot/superfile/pull/956)\n\n# [**v1.3.2**](https://github.com/yorukot/superfile/releases/tag/v1.3.2)\n\n> 2025-07-16\n\n#### Update\n- Normalize user-facing naming to superfile [`#880`](https://github.com/yorukot/superfile/pull/880)\n- Add kitty protocol for image preview [`#841`](https://github.com/yorukot/superfile/pull/841)\n- feat: add Zoxide support for path resolution in initial configuration [`#892`](https://github.com/yorukot/superfile/pull/892)\n- feat: update superfile's help output [`#908`](https://github.com/yorukot/superfile/pull/908)\n- feat: Add Action to Publish to Winget [`#925`](https://github.com/yorukot/superfile/pull/925)\n- feat: update superfile build test for the windows and macOS [`#922`](https://github.com/yorukot/superfile/pull/922)\n- Compress all files selected [`#821`](https://github.com/yorukot/superfile/pull/821)\n- Theme: add 0x96f theme [`#860`](https://github.com/yorukot/superfile/pull/860)\n\n#### Bug fix\n- fix: outdated and broken nix flake [`#846`](https://github.com/yorukot/superfile/pull/846)\n- fix: handle UTF-8 BOM in file reader [`#865`](https://github.com/yorukot/superfile/pull/865)\n- fix icon displayed on spf prompt when nerdfont disabled [`#878`](https://github.com/yorukot/superfile/pull/878)\n- fix: create item check for dot-entries [`#817`](https://github.com/yorukot/superfile/pull/817)\n- fix: prevent pasting a directory into itself, avoiding infinite loop [`#887`](https://github.com/yorukot/superfile/pull/887)\n- fix: clear search bar value on parent directory reset [`#906`](https://github.com/yorukot/superfile/pull/906)\n- fix: enhance terminal pixel detection and response handling [`#904`](https://github.com/yorukot/superfile/pull/904)\n- fix: Cannot Build superfile on Windows [`#921`](https://github.com/yorukot/superfile/pull/921)\n- fix: Improve command tokenization to handle quotes and escapes [`#931`](https://github.com/yorukot/superfile/pull/931)\n- fix: Dont read special files, and prevent freeze [`#932`](https://github.com/yorukot/superfile/pull/932)\n\n#### Optimization\n- Metadata and filepanel rendering refactor [`#867`](https://github.com/yorukot/superfile/pull/867)\n- refactor: simplify panel mode handling in file movement logic [`#907`](https://github.com/yorukot/superfile/pull/907)\n- refactor: standardize TODO comments and ReadMe to README [`#913`](https://github.com/yorukot/superfile/pull/913)\n\n#### Documentation\n- enhance: add detailed documentation for InitIcon function and update … [`#879`](https://github.com/yorukot/superfile/pull/879)\n- docs: add documentation for image preview [`#882`](https://github.com/yorukot/superfile/pull/882)\n- docs: update contributing guide and PR template [`#885`](https://github.com/yorukot/superfile/pull/885)\n- docs: update README and plugin documentation for clarity and structure [`#902`](https://github.com/yorukot/superfile/pull/902)\n- feat(docs): Update arch install package docs [`#929`](https://github.com/yorukot/superfile/pull/929)\n\n#### CI/CD\n- ci: add PR title linting with semantic-pull-request action [`#884`](https://github.com/yorukot/superfile/pull/884)\n- ci: improve PR workflows with contributor greeting and title linter fix [`#886`](https://github.com/yorukot/superfile/pull/886)\n\n#### Dependencies\n- build(deps): bump prismjs from 1.29.0 to 1.30.0 in /website [`#786`](https://github.com/yorukot/superfile/pull/786)\n- fix(deps): update dependency astro to v5.8.0 [`#787`](https://github.com/yorukot/superfile/pull/787)\n- chore(deps): bump vite from 6.3.3 to 6.3.5 in /website [`#822`](https://github.com/yorukot/superfile/pull/822)\n- fix(deps): update dependency sharp to v0.34.2 [`#909`](https://github.com/yorukot/superfile/pull/909)\n- fix(deps): update astro monorepo [`#894`](https://github.com/yorukot/superfile/pull/894)\n- fix(deps): update fontsource monorepo to v5.2.6 [`#910`](https://github.com/yorukot/superfile/pull/910)\n\n#### Misc\n- chore(license): update copyright year [`#895`](https://github.com/yorukot/superfile/pull/895)\n- feat: add ignore missing field flag [`#881`](https://github.com/yorukot/superfile/pull/881)\n- feat: add sitemap integration and update giscus input position [`#912`](https://github.com/yorukot/superfile/pull/912)\n\n\n# [**v1.3.1**](https://github.com/yorukot/superfile/releases/tag/v1.3.1)\n\n> 2025-05-27\n\n#### Update\n\n- Replace custom giscus implementation with official starlight-giscus plugin [`#843`](https://github.com/yorukot/superfile/pull/843)\n- Add 'Type' option for sorting by file extension with fallback [`#829`](https://github.com/yorukot/superfile/pull/829)\n\n#### Bug Fixes\n\n- Correct icons for clipboard files [`#845`](https://github.com/yorukot/superfile/pull/845)\n- Replace mattn/rundwidth with ansi package for more robust StringWidth [`#848`](https://github.com/yorukot/superfile/pull/848)  \n- Purego package update [`#837`](https://github.com/yorukot/superfile/pull/837)\n\n#### Optimization\n\n- Update main.go [`#839`](https://github.com/yorukot/superfile/pull/839)\n\n# [**v1.3.0**](https://github.com/yorukot/superfile/releases/tag/v1.3.0)\n\n> 2025-05-22\n\n#### Update\n\n- Added a Command-Prompt for SuperFile specific actions [`#752`](https://github.com/yorukot/superfile/pull/752)\n- Allow specifying multiple panels at startup [`#759`](https://github.com/yorukot/superfile/pull/759)\n- Initial draft of rendering package [`#775`](https://github.com/yorukot/superfile/pull/775)\n- Render unit tests for prompt model [`#809`](https://github.com/yorukot/superfile/pull/809)\n- Chooser file option, --lastdir-file option, and improvements in quit, and bug fixes [`#812`](https://github.com/yorukot/superfile/pull/812)\n- Prompt feature leftover items [`#804`](https://github.com/yorukot/superfile/pull/804)\n- SPF Prompt tutorial and fixes [`#814`](https://github.com/yorukot/superfile/pull/814)\n- Write prompt tutorial, rename prompt mode to spf mode, add develop branch in GitHub workflow, show_panel_footer_info flag [`#815`](https://github.com/yorukot/superfile/pull/815)\n- Theme: Add gruvbox-dark-hard [`#828`](https://github.com/yorukot/superfile/pull/828)\n- Sidebar separation [`#767`](https://github.com/yorukot/superfile/pull/767)\n- Sidebar code separation [`#770`](https://github.com/yorukot/superfile/pull/770)\n- Rendering package and rendering bug fixes [`#781`](https://github.com/yorukot/superfile/pull/781)\n- Refactor CheckForUpdates [`#797`](https://github.com/yorukot/superfile/pull/797)\n- Rename metadata strings [`#731`](https://github.com/yorukot/superfile/pull/731)\n\n#### Bug Fixes\n\n- Fix crash with opening file with editor on an empty panel [`#730`](https://github.com/yorukot/superfile/pull/730)\n- Fix: Add some of the remaining linter and fix errors [`#756`](https://github.com/yorukot/superfile/pull/756)\n- Golangci lint fixes [`#757`](https://github.com/yorukot/superfile/pull/757)\n- Fix: Remove redundant function containsKey [`#765`](https://github.com/yorukot/superfile/pull/765)\n- Fix: Correctly resolve path in open and cd prompt actions [`#802`](https://github.com/yorukot/superfile/pull/802)\n- Prompt dynamic dimensions and unit tests fix [`#805`](https://github.com/yorukot/superfile/pull/805)\n- Fix: Convert unicode space to normal space, use rendered in file preview to fix layout bugs, Release 1.3.0 [`#825`](https://github.com/yorukot/superfile/pull/825)\n\n#### Optimization\n\n- Adding linter to CI/CD and fix some lint issues [`#739`](https://github.com/yorukot/superfile/pull/739)\n- Linter fixes, new feature of allowing multiple directories at startup, other code improvements [`#764`](https://github.com/yorukot/superfile/pull/764)\n- Model unit tests [`#803`](https://github.com/yorukot/superfile/pull/803)\n\n\n# [**v1.2.1**](https://github.com/yorukot/superfile/releases/tag/v1.2.1)\n\n> 2025-03-26\n\n#### Update\n- Add show_image_preview flag [`#728`](https://github.com/yorukot/superfile/pull/728)\n- Allow specifying directory icon color in theme files [`#709`](https://github.com/yorukot/superfile/pull/709)\n- --hotkey-file flag and fix in configFileFlag [`#700`](https://github.com/yorukot/superfile/pull/700)\n- File preview: Add bat as plugin [`#686`](https://github.com/yorukot/superfile/pull/686)\n- Monokai Theme [`#673`](https://github.com/yorukot/superfile/pull/673)\n\n#### Bug fix\n- Fix broken link in website causing 404 [`#714`](https://github.com/yorukot/superfile/pull/714)\n- Fix sidebar disk listing [`#708`](https://github.com/yorukot/superfile/pull/708)\n- Switch to semver for newer 1.2.1 release [`#687`](https://github.com/yorukot/superfile/pull/687)\n\n#### Optimization\n- Fix: icon consts [`#719`](https://github.com/yorukot/superfile/pull/719)\n- Refactor and unit tests for scrolling [`#710`](https://github.com/yorukot/superfile/pull/710)\n- Refactor of wheel functions [`#695`](https://github.com/yorukot/superfile/pull/695)\n\n#### Documentation\n- Add info about auto update [`#721`](https://github.com/yorukot/superfile/pull/721)\n- add cd_on_quit for fish shell [`#696`](https://github.com/yorukot/superfile/pull/696)\n- Add Pixi installation instructions [`#690`](https://github.com/yorukot/superfile/pull/690)\n\n# [**v1.2.0.0**](https://github.com/yorukot/superfile/releases/tag/v1.2.0.0)\n\n> 2025-03-05\n\n#### Update\n- Added direnv support for nix flake dev shell [`#568`](https://github.com/yorukot/superfile/pull/568)\n- Move rename cursor to start before the extension [`#565`](https://github.com/yorukot/superfile/pull/565)\n- Renaming feature for pinned directories [`#579`](https://github.com/yorukot/superfile/pull/579)\n- Add python testsuite [`#581`](https://github.com/yorukot/superfile/pull/581)\n- Add build instructions for windows [`#583`](https://github.com/yorukot/superfile/pull/583)\n- Add `--config-file` flag support [`#592`](https://github.com/yorukot/superfile/pull/592)\n- Document Windows scoop installation option [`#595`](https://github.com/yorukot/superfile/pull/595)\n- Rotate image using EXIF metadata [`#607`](https://github.com/yorukot/superfile/pull/607)\n- Upgrade sidebar search [`#614`](https://github.com/yorukot/superfile/pull/614)\n- Change all outPutLog to slog.Error or slog.Info [`#628`](https://github.com/yorukot/superfile/pull/628)\n- Add install.sh files link for more trust [`#645`](https://github.com/yorukot/superfile/pull/645)\n- Update README.md and added a Run the app title [`#550`](https://github.com/yorukot/superfile/pull/550)\n\n#### Bug fix\n- Fix sort options hotkey [`#548`](https://github.com/yorukot/superfile/pull/548)\n- Fix wrong log line, Fatalln was used with formatting verbs [`#555`](https://github.com/yorukot/superfile/pull/555)\n- Fix incorrect failure reporting in delete operation [`#558`](https://github.com/yorukot/superfile/pull/558)\n- Fix previews for text file with control characters [`#557`](https://github.com/yorukot/superfile/pull/557)\n- Fix search field key blocking [`#569`](https://github.com/yorukot/superfile/pull/569)\n- Fix windows operations and other improvements [`#564`](https://github.com/yorukot/superfile/pull/564)\n- Fix crash when searching on WSL mounted drives [`#576`](https://github.com/yorukot/superfile/pull/576)\n- Fix arch install instructions [`#580`](https://github.com/yorukot/superfile/pull/580)\n- Fix windows delete, open file and other improvements [`#584`](https://github.com/yorukot/superfile/pull/584)\n- Fix UI issue of spf stuck with terminal size too small [`#594`](https://github.com/yorukot/superfile/pull/594)\n- Fix wrong path separator in windows [`#597`](https://github.com/yorukot/superfile/pull/597)\n- Fix command line not working for windows [`#601`](https://github.com/yorukot/superfile/pull/601)\n- Fix error while reading last check version file in new time zone [`#634`](https://github.com/yorukot/superfile/pull/634)\n- Fix discrete timeout for HTTP get version [`#632`](https://github.com/yorukot/superfile/pull/632)\n- Fix initial pinned.json having invalid JSON [`#652`](https://github.com/yorukot/superfile/pull/652)\n- Fix loadConfigFile and loadHotkeysFile functions [`#650`](https://github.com/yorukot/superfile/pull/650)\n- Fix issue when trying to extract a file with .zip_ extension [`#636`](https://github.com/yorukot/superfile/pull/636)\n- Fix openFileWithEditor bug [`#635`](https://github.com/yorukot/superfile/pull/635)\n- Fix partial overwrite issue by ensuring full file rewrite [`#665`](https://github.com/yorukot/superfile/pull/665)\n\n#### Optimization\n- Improving file panel rendering [`#589`](https://github.com/yorukot/superfile/pull/589)\n- Improve formatting, error handling, and fix typos [`#600`](https://github.com/yorukot/superfile/pull/600)\n- Go formatting fixes [`#618`](https://github.com/yorukot/superfile/pull/618)\n- Testsuite in GitHub Actions [`#602`](https://github.com/yorukot/superfile/pull/602)\n\n#### Documentation\n- Revert changes in website that were not yet released [`#611`](https://github.com/yorukot/superfile/pull/611)\n- Docs contribute [`#610`](https://github.com/yorukot/superfile/pull/610)\n- Remove godocs badge [`#627`](https://github.com/yorukot/superfile/pull/627)\n- Update installation.md to note setting nerd-font in terminal application [`#658`](https://github.com/yorukot/superfile/pull/658)\n- Fix README typos [`#653`](https://github.com/yorukot/superfile/pull/653)\n\n# [**v1.1.7.1**](https://github.com/yorukot/superfile/releases/tag/v1.1.7)\n\n> 2024-01-06\n\nNOTE: This release is a hotfix to resolve an unusual issue on Windows.\n\n#### Bug fix\n- Fix can't run on windows [`#534`](https://github.com/yorukot/superfile/issues/534)\n\n# [**v1.1.7**](https://github.com/yorukot/superfile/releases/tag/v1.1.7)\n\n> 2024-01-05\n\n#### Update\n\n- OneDark Theme added [`#477`](https://github.com/yorukot/superfile/pull/477)\n- Add keys PageUp and PageDown for better navigation [`#498`](https://github.com/yorukot/superfile/pull/498)\n- Add hotkey for copying PWD to clipboard [`#510`](https://github.com/yorukot/superfile/pull/510)\n- Add desktop entry [`#501`](https://github.com/yorukot/superfile/pull/501)\n- Enable cd_on_quit when current directory is home directory [`#518`](https://github.com/yorukot/superfile/pull/518)\n- Edit superfile config [`#509`](https://github.com/yorukot/superfile/pull/509)\n\n#### Bug fix\n- Fix rendering directory symlinks as directories, not files [`#481`](https://github.com/yorukot/superfile/pull/481)\n- Fix opening files on Windows [`#496`](https://github.com/yorukot/superfile/pull/496)\n- Fix lag in dotfile toggle with multiple panels [`#499`](https://github.com/yorukot/superfile/pull/499)\n- Fix parent directory navigation on Windows [`#502`](https://github.com/yorukot/superfile/pull/502)\n- Fix panic when deleting last file in directory [`#529`](https://github.com/yorukot/superfile/pull/529)\n- Fix panic when scrolling through an empty metadata list [`#531`](https://github.com/yorukot/superfile/pull/531)\n- Fix panic when trying to get folder size without needed permissions [`#532`](https://github.com/yorukot/superfile/pull/532)\n- Fix lag when navigating directories with large image files [`#525`](https://github.com/yorukot/superfile/pull/525)\n- Fix typo in welcome message [`#494`](https://github.com/yorukot/superfile/pull/494)\n\n#### Optimization\n- Optimize file move operation [`#522`](https://github.com/yorukot/superfile/pull/522)\n- Optimize file extraction [`#524`](https://github.com/yorukot/superfile/pull/524)\n- Warn overwrite when renaming files [`#526`](https://github.com/yorukot/superfile/pull/526)\n- Work without trash [`#527`](https://github.com/yorukot/superfile/pull/527)\n\n# [**v1.1.6**](https://github.com/yorukot/superfile/releases/tag/v1.1.6)\n\n> 2024-11-21\n\n#### Update\n- Add sort case toggle [`#469`](https://github.com/yorukot/superfile/issues/469)\n- Add Sort options [`#420`](https://github.com/yorukot/superfile/pull/420)\n- Fix flashing when switching between panels [`#122`](https://github.com/yorukot/superfile/issues/122)\n\n#### Bug fix\n- Fix some hotkey broken\n- Fix the searchbar to automatically put the open key into the searchbar [`ec9e256`](https://github.com/yorukot/superfile/commit/b20bc70fe9d4e0ee96931092a6522e8604cc017b)\n\n# [**v1.1.5**](https://github.com/yorukot/superfile/releases/tag/v1.1.5)\n\n> 2024-10-03\n\n#### Update\n- Stop automatically updating config file. Add fix-hotkeys flag, feedback for missing hotkeys [`#333`](https://github.com/yorukot/superfile/issues/333)\n- Update installation.md: Add x-cmd method to install superfile [`#371`](https://github.com/yorukot/superfile/issues/333)\n- Added option to change default editor [`#396`](https://github.com/yorukot/superfile/pull/396)\n- Support Shell access but cant read history [`#127`](https://github.com/yorukot/superfile/issues/127)\n- shortcut to copy path to currently selected file [`#196`](https://github.com/yorukot/superfile/issues/196)\n\n#### Bug fix\n- fixed typo in hotkeys.toml [`#341`](https://github.com/yorukot/superfile/issues/341)\n- Fixes issue #360 + Typo fixes by [`#379`](https://github.com/yorukot/superfile/pull/379)\n- fixed spelling mistake : varibale to variable [`#394`](https://github.com/yorukot/superfile/pull/394)\n- fixed exiftool session left open after use [`#400`](https://github.com/yorukot/superfile/pull/400)\n- Show unsupported format in preview panel over a torrent file [`#408`](https://github.com/yorukot/superfile/pull/408)\n- Vim bindings in docs cause error on nixos [`#325`](https://github.com/yorukot/superfile/issues/325)\n- fix spf help flag error [`#368`](https://github.com/yorukot/superfile/issues/368)\n- You cannot access the disks section in the side panel when only have one disk [`#409`](https://github.com/yorukot/superfile/issues/409)\n- \"Unsupported formats\" message has an extra space for .pdf files [`#392`](https://github.com/yorukot/superfile/issues/392)\n\n# [**v1.1.4**](https://github.com/yorukot/superfile/releases/tag/v1.1.4)\n\n> 2024-08-01\n\n#### Update\n- Added option to change default directory [`#211`](https://github.com/yorukot/superfile/issues/211)\n- Added quotes around dir in lastdir to support special characters [`#218`](https://github.com/yorukot/superfile/pull/218)\n- Make Hotkey settings unlimited [`423a96a`](https://github.com/yorukot/superfile/commit/423a96a0aeca4ea2c30447d8b4010868045bb7e8)\n- Selection should start on currently positioned/pointed item [`#226`](https://github.com/yorukot/superfile/issues/226)\n- Make Nerdfont optional [`#6`](https://github.com/yorukot/superfile/issues/6)\n- Confirm before quit [`#155`](https://github.com/yorukot/superfile/issues/155)\n- Added file permissions to metadata [`#279`](https://github.com/yorukot/superfile/pull/279)\n- Better fuzzy file search [`#115`](https://github.com/yorukot/superfile/issues/115)\n- MD5 checksum in Metadata [`#255`](https://github.com/yorukot/superfile/pull/225)\n- An option to display the filesize in decimal or binary sizes [`#220`](https://github.com/yorukot/superfile/issues/220)\n\n#### Bug fix\n- An option to display the filesize in decimal or binary sizes [`#220`](https://github.com/yorukot/superfile/issues/220)\n- Fix Transparent Background issue [`#76`](https://github.com/yorukot/superfile/issues/76)\n- Big text file makes the program freeze for a while [`#255`](https://github.com/yorukot/superfile/issues/255)\n- Text in file preview has a background color behind it when using transparency [`#76`](https://github.com/yorukot/superfile/issues/76)\n\n# [**v1.1.3**](https://github.com/yorukot/superfile/releases/tag/v1.1.3)\n\n> 2024-05-26\n\n#### Update\n- Update print path list [`37c8864`](https://github.com/yorukot/superfile/commit/37c8864eb2b0dc73fbf8928dd40b3b7573e9a11dw)\n- Make theme files embed [`0f53a12`](https://github.com/yorukot/superfile/commit/7fa775dd7db175fef694e514bd77ebd75c801fae)\n- Disable update check via config [`#131`](https://github.com/yorukot/superfile/issues/131)\n- Redesign hotkeys [`#116`](https://github.com/yorukot/superfile/issues/116)\n- Create file or folder using same hotkey [`#116`](https://github.com/yorukot/superfile/issues/116)\n- More dynamic footer height adaptive [`66a3fb4`](https://github.com/yorukot/superfile/commit/66a3fb4feba31ead2224938b1a18a431a55ac9cc)\n- Confirm delete files [``]()\n- Support windows for get well known directories [`d4db820`](https://github.com/yorukot/superfile/commit/d4db820ba839603df209dcce05468902739f301f)\n- Support text file preview [`#26`](https://github.com/yorukot/superfile/issues/26)\n- Support directory preview [`#26`](https://github.com/yorukot/superfile/issues/26)\n- Improve mouse scrolling delay [`f734292`](https://github.com/yorukot/superfile/commit/f7342921d49d87f1bc633c9f8e19fe6845fbbf26)\n- Support image preview with ansi [`#26`](https://github.com/yorukot/superfile/issues/26)\n- Clear search after opening directory  [`#146`](https://github.com/yorukot/superfile/issues/146)\n\n#### Bug fix\n- Recursive symlink crashes superfile [`#109`](https://github.com/yorukot/superfile/issues/109)\n- Timemachine snapshots listed in Disks section [`#126`](https://github.com/yorukot/superfile/issues/126)\n- There will be a bug in the layout under a specific terminal height [`#105`](https://github.com/yorukot/superfile/issues/105)\n- Fix lag when there are a lot of files [`#124`](https://github.com/yorukot/superfile/issues/124)\n- Rendering will be blocked while executing a task that uses a progress bar [`#104`](https://github.com/yorukot/superfile/issues/104)\n\n# [**v1.1.2**](https://github.com/yorukot/superfile/releases/tag/v1.1.2)\n\n> 2024-05-08\n\n#### Update\n- Update help menu [`#75`](https://github.com/yorukot/superfile/issues/75)\n- Update all modal, make other panel still show on background [`#79`](https://github.com/yorukot/superfile/pull/79)\n- Support extract gz tar file [`b9aed84`](https://github.com/yorukot/superfile/commit/b9aed847804421e1fc4f03dcaefb0e27f1260ea3)\n- Support transparent background [`4108d40`](https://github.com/yorukot/superfile/commit/4108d40bc0b93656eca2da98253a83dbc0cb27a9)\n- Support custom border style [`6ff0576`](https://github.com/yorukot/superfile/commit/6ff05765823cbd25e6fdc4d3f7370e435114acbb)\n- Enhancement when cutting and pasting, the file should be moved instead of copied and deleted. [`#100`](https://github.com/yorukot/superfile/issues/100)\n- Support extract almost compression formats [`e57cb78`](https://github.com/yorukot/superfile/commit/e57cb78d602d62b47662e2069b75059d908147db)\n- Update XDG_CACHE to XDG_STATE_HOME [`#90`](https://github.com/yorukot/superfile/issues/90)\n\n#### Bug fix\n- Fix Cut -> Paste file causes go panic [`#77`](https://github.com/yorukot/superfile/issues/77)\n- Fix symlinked folders don't open within superfile [`#88`](https://github.com/yorukot/superfile/issues/88)\n\n# [**v1.1.1**](https://github.com/yorukot/superfile/releases/tag/v1.1.1)\n\n> 2024-04-23\n\n#### Update\n- Open directory with default application [`#33`](https://github.com/yorukot/superfile/issues/33)\n- Auto update config file if missing config [`1498c92`](https://github.com/yorukot/superfile/commit/1498c92d2166c8c25989be9ce5a15dc6d1ffb073)\n\n#### Bug fix\n- key `l` deletes files in macOS [`#72`](https://github.com/yorukot/superfile/issues/72)\n\n# [**v1.1.0**](https://github.com/yorukot/superfile/releases/tag/v1.1.0)\n\n> 2024-04-20\n\n#### Update\n\n- Update data folder from `$XDG_CONFIG_HOME/superfile/data` to `$XDG_DATA_HOME/superfile` [`9fff97a`](https://github.com/yorukot/superfile/commit/9fff97a362bcd5bec1c19709b7a5aeb59cdeaa34)\n- Toggle dot file display [`9fff97a`](https://github.com/yorukot/superfile/commit/9fff97a362bcd5bec1c19709b7a5aeb59cdeaa34/9fff97a362bcd5bec1c19709b7a5aeb59cdeaa34)\n- Update log file from `$XDG_CONFIG_HOME/superfile/data/superfile.log` to `$XDG_CACHE_DATA` [`#27`](https://github.com/yorukot/superfile/pull/27)\n- Update theme background [`#42`](https://github.com/yorukot/superfile/pull/42)\n- Update unzip function [`#55`](https://github.com/yorukot/superfile/pull/55)\n- Update zip function [`60c490a`](https://github.com/yorukot/superfile/commit/60c490aa06019fb1a5382b1e241c6b0a72ec51a4)\n- Update all config file from `json` to `toml` format file [`a018128`](https://github.com/yorukot/superfile/commit/a018128ffd431d76a06f379fffbe0aa20d3e78cc)\n- Update search bar [`#61`](https://github.com/yorukot/superfile/pull/61)\n- Update theme config format [`#66`](https://github.com/yorukot/superfile/pull/66)\n- Update metadata to plugins [`c1f942d`](https://github.com/yorukot/superfile/commit/c1f942da366919f114b094ce512ff95002b6a08c)\n\n#### Bug fix\n\n- Fix interface lag when selecting zip files or large files [`#29`](https://github.com/yorukot/superfile/issues/29)\n- Fix external media error [`#46`](https://github.com/yorukot/superfile/pull/46)\n- Fix can't find trash can folder [`396674f`](https://github.com/yorukot/superfile/commit/396674f33e302369790bcb88d84df0d3830d3543)\n- Fix Crashes when truncating metadata [`#50`](https://github.com/yorukot/superfile/issues/50)\n\n# [**v1.0.1**](https://github.com/yorukot/superfile/releases/tag/v1.0.1)\n\n> 2024-04-08\n\n#### Update\n\n- Update `$HOME/.superfile` to `$XDG_CONFIG_HOME/superfile` [`886dbfb`](https://github.com/yorukot/superfile/commit/886dbfb276407db36e9fb7369ec31053e7aabcf4)\n- Follow [The FreeDesktop.org Trash specification](https://specifications.freedesktop.org/trash-spec/trashspec-1.0.html) to update the trash bin path in local path [`886dbfb`](https://github.com/yorukot/superfile/commit/886dbfb276407db36e9fb7369ec31053e7aabcf4)\n- The external hard drive will be deleted directly ,But macOS for now not support trash can[`a4232a8`](https://github.com/yorukot/superfile/commit/a4232a88bef4b5c3e99456fd198eabb953dc324c)\n- The user can enter the path, which will be the path of the first file panel [`14620b3`](https://github.com/yorukot/superfile/commit/14620b33b09edfce80a95e1f52f7f66b3686a9d0)\n- Make user can open file with default browser text-editor etc [`f47d291`](https://github.com/yorukot/superfile/commit/f47d2915bf637da0cf99a4b15fa0bea8edc8d380)\n- Can open terminal in focused file panel path [`f47d291`](https://github.com/yorukot/superfile/commit/f47d2915bf637da0cf99a4b15fa0bea8edc8d380)\n\n#### Bug fix\n\n- Fix processes bar cursor index display error [`f6eb9d8`](https://github.com/yorukot/superfile/commit/f6eb9d879f9f7ef31859e3f84c8792e2f0fc543a)\n- Fix [Crash when selecting a broken symlink](https://github.com/yorukot/superfile/issues/9) [`e89722b`](https://github.com/yorukot/superfile/commit/e89722b3717cc669c2e14bb310d1b96c1727b63f)\n\n# [**v1.0.0**](https://github.com/yorukot/superfile/releases/tag/v1.0.0)\n\n> 2024-04-06\n\n##### Update\n\n- Auto download folder [`96a3a71`](https://github.com/yorukot/superfile/commit/96a3a7108eb7c4327bad3424ed55e472ec78049f)\n- Auto initialize configuration [`96a3a71`](https://github.com/yorukot/superfile/commit/96a3a7108eb7c4327bad3424ed55e472ec78049f)\n- Add version sub-command [`ee22df3`](https://github.com/yorukot/superfile/commit/ee22df3c7700adddb859ada8623f6c8b038e8087)\n\n##### Bug fix\n\n- Fix creating an Item when the file panel has no Item will cause an error [`9ee1d86`](https://github.com/yorukot/superfile/commit/9ee1d860192182803d408c5046ca9f5255121698)\n- Fix delete mupulate Item will cause cursor error [`ee22df3`](https://github.com/yorukot/superfile/commit/ee22df3c7700adddb859ada8623f6c8b038e8087)\n\n# [**Beta 0.1.0**](https://github.com/yorukot/superfile/releases/tag/v0.1.0-beta)\n\n> 2024-04-06\n\n- FIRST RELEASE COME UP! NO ANY CHANGE\n"
  },
  {
    "path": "website/src/content/docs/configure/config-file-path.md",
    "content": "---\ntitle: Config file path\ndescription: All superfile config file path\nhead:\n  - tag: title\n    content: Config file path | superfile\n---\n\n:::tip\nIf you want to get the set path you can try `spf pl` which will print out the file locations of all superfile.\n:::\n\n## Directories\n\n#### Config directory\n\n|         Linux         |              macOS              |          Windows           |\n| :-------------------: | :-----------------------------: | :------------------------: |\n| `~/.config/superfile` | `~/Library/Application Support/superfile` | `%LOCALAPPDATA%/superfile` |\n\n#### Theme directory\n\n|            Linux            |                      macOS                      |             Windows              |\n| :-------------------------: | :---------------------------------------------: | :------------------------------: |\n| `~/.config/superfile/theme` | `~/Library/Application Support/superfile/theme` | `%LOCALAPPDATA%/superfile/theme` |\n\n#### Data directory\n\n|           Linux            |                   macOS                    |          Windows           |\n| :------------------------: | :----------------------------------------: | :------------------------: |\n| `~/.local/share/superfile` | `~/Library/Application Support/superfile/` | `%LOCALAPPDATA%/superfile` |\n\n### Changing Config File Path\n\nYou can use the `-c` or `--config-file` flag to specify a different path for the `config.toml` file:\n\n```bash\nspf -c /path/to/your/config.toml\n```\n\nYou can use the `--hotkey-file` flag to specify a different path for the `hotkey.toml` file:\n\n```bash\nspf --hotkey-file /path/to/your/hotkey.toml\n```\n\n#### Log directory\n\n|           Linux            |                   macOS                   |          Windows           |\n| :------------------------: | :---------------------------------------: | :------------------------: |\n| `~/.local/state/superfile` | `~/Library/Application Support/superfile` | `%LOCALAPPDATA%/superfile` |\n\n---\n\n## All config file path\n\n#### Config\n\n|               Linux               |                    macOS                    |                Windows                 |\n| :-------------------------------: | :-----------------------------------------: | :------------------------------------: |\n| `~/.config/superfile/config.toml` | `~/Library/Application Support/superfile/config.toml` | `%LOCALAPPDATA%/superfile/config.toml` |\n\n#### Hotkeys\n\n|               Linux                |                    macOS                     |                 Windows                 |\n| :--------------------------------: | :------------------------------------------: | :-------------------------------------: |\n| `~/.config/superfile/hotkeys.toml` | `~/Library/Application Support/superfile/hotkeys.toml` | `%LOCALAPPDATA%/superfile/hotkeys.toml` |\n\n#### Log file\n\n|                  Linux                   |                          macOS                          |                 Windows                  |\n| :--------------------------------------: | :-----------------------------------------------------: | :--------------------------------------: |\n| `~/.local/state/superfile/superfile.log` | `~/Library/Application Support/superfile/superfile.log` | `%LOCALAPPDATA%/superfile/superfile.log` |\n"
  },
  {
    "path": "website/src/content/docs/configure/custom-hotkeys.mdx",
    "content": "---\ntitle: Custom hotkeys\ndescription: Customize your own hotkeys\nhead:\n  - tag: title\n    content: Custom hotkeys | superfile\n---\n\nimport CodeBlock from '../../../components/code.astro';\n\nYou can enter the following command to set it up;\n\n[Click me to know where is HOTKEYS_PATH](/configure/config-file-path#hotkeys)\n\n```bash\n$EDITOR HOTKEYS_PATH\n```\n\n:::caution\nPlease do not use hotkeys with ascii codes conflicting with keys used to control superfile : \n- `Ctrl+M` - conflicts with `Enter` Key\n- `Ctrl+I` - conflicts with `Tab` Key\n- `Ctrl+?`, `Ctrl+[` - conflicts with `Delete` and `Backspace` Key\n:::\n\n### Default superfile hotkeys\n\n:::caution\nIf you are a vim user, the default hotkeys may make you hate superfile.\n:::\n\nsuperfile default hotkeys design concept:\n- All hotkeys that will change to files use `ctrl+key` (As long as you don't press ctrl your files will always be safe).\n- Non-control file classes use the first letters of words as hotkeys.\n\n<CodeBlock file=\"src/superfile_config/hotkeys.toml\" />\n\n### Vim like superfile hotkeys\n\n<CodeBlock file=\"src/superfile_config/vimHotkeys.toml\" />"
  },
  {
    "path": "website/src/content/docs/configure/custom-theme.mdx",
    "content": "---\ntitle: Custom theme\ndescription: Custom your own superfile theme\nhead:\n  - tag: title\n    content: Custom theme | superfile\n---\n\nimport CodeBlock from '../../../components/code.astro';\n\n### Use an existing theme\n\nYou can enter the following command to set it up;\n\n[Click me to know where is CONFIG_PATH](/configure/config-file-path#config)\n\n```bash\n$EDITOR CONFIG_PATH\n```\n\nYou 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!)\n\nOnce you find one you like, copy it and paste it into the theme in the config_path file.\n\n```diff\n- theme = 'catppuccin'\n+ theme = 'theme_name_you_like'\n```\n\n### Create your own theme\n\n[Click me to know where is THEME_DIRECTORY](/configure/config-file-path#config)\n\nIf 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\n\nDon't forget to change the `theme` variable in `config.toml` to your theme name.\n\n[If you are satisfied with your theme, you might as well put it into the default theme list!](/how-to-contribute)\n\n### Default theme\n\n<CodeBlock file=\"src/superfile_config/theme/catppuccin-mocha.toml\" />"
  },
  {
    "path": "website/src/content/docs/configure/enable-plugin.md",
    "content": "---\ntitle: Enable Plugin\ndescription: How to enable and configure superfile plugins\nhead:\n  - tag: title\n    content: Enable Plugins | superfile\n---\n\nPlugins extend superfile's functionality by integrating with external tools. This guide shows you how to enable and configure plugins.\n\n## Prerequisites\n\nBefore enabling any plugin, ensure you have:\n\n1. **Installed the required dependencies** for the specific plugin\n2. **Located your config file** - see [config file path guide](/configure/config-file-path#config)\n\n## How to Enable Plugins\n\n### Step 1: Install Required Dependencies\n\nEach plugin has specific requirements. Check the [plugin list](/list/plugin-list) for the dependencies needed for your desired plugin.\n\n### Step 2: Edit Configuration File\n\nOpen your `config.toml` file:\n\n```bash\n$EDITOR CONFIG_PATH\n```\n\n### Step 3: Enable the Plugin\n\nFind the plugin section in your config and change its value from `false` to `true`:\n\n```diff\n[plugins]\n- metadata = false\n+ metadata = true\n```\n\n### Example: Enabling Metadata Plugin\n\n1. **Install exiftool** (required for metadata plugin)\n2. **Edit your config file:**\n   ```bash\n   $EDITOR CONFIG_PATH\n   ```\n3. **Enable the plugin:**\n   ```toml\n   metadata = true\n   ```\n\n## Configuration Format\n\n```toml\nmetadata = false\nzoxide_support = false\n```\n\nSet any plugin to `true` to enable it, or `false` to disable it.\n\n## Available Plugins\n\nFor a complete list of available plugins and their requirements, see the [plugin list](/list/plugin-list).\n\n## Troubleshooting\n\nIf a plugin isn't working after enabling it:\n\n1. **Verify dependencies** - Make sure all required tools are installed and accessible in your PATH\n2. **Restart superfile** - Changes require restarting the application\n3. **Check configuration** - Ensure the plugin name is spelled correctly in your config file"
  },
  {
    "path": "website/src/content/docs/configure/superfile-config.mdx",
    "content": "---\ntitle: superfile config\ndescription: Configure your superfile\nhead:\n  - tag: title\n    content: superfile config | superfile\n---\n\nimport CodeBlock from \"../../../components/code.astro\";\n\nYou can edit your superfile config file with the following command:\n\n```bash\n$EDITOR config_path\n```\n\n:::tip\nTo see the path locations of your superfile files, use the command `spf pl`.\n:::\n\n### Setting\n\n- ###### theme\n\n[Click here](/configure/custom-theme) for instructions to edit the theme.\n\n- ###### editor\n\nThe editor your files will be opened with (Leave blank to use the EDITOR environment variable. If EDITOR environment variable is not set, it will default to `nano` for macOS/Linux, and `notepad` for Windows.\n\n- ###### dir_editor\n\nThe editor your directories will be opened with (Leave blank to use defaults : `vi` - Linux, `open` - macOS, `explorer` - Windows).\n\n- ###### auto_check_update\n\n`true` => Checks whether updates are needed when you exit superfile (only checks once a day).\n\n`false` => No checks performed.\n\n- ###### cd_on_quit\n\n`true` => When you exit superfile, changes the terminal path to the last file panel you used.\n\n`false` => When you exit superfile, the terminal path remains the same prior to superfile.\n\nAfter setting to `true`, you need to update your shell config file. Sample changes :\n\n##### macOS/Linux (bash or fish)\n\n###### Bash\n\nOpen the file:\n\n```bash\n$EDITOR ~/.bashrc\n```\n\nCopy the following code into the file:\n\n<CodeBlock file=\"cd_on_quit/cd_on_quit.sh\" />\n\nSave, exit, and reload your `.bashrc` file:\n\n```bash\nsource ~/.bashrc\n```\n\n###### Fish\n\nOpen the file:\n\n```bash\n$EDITOR ~/.config/fish/config.fish\n```\n\nIf you suspect your `config.fish` file is located somewhere else, read [the Fish shell documentation](https://fishshell.com/docs/current/language.html#configuration)\n\nCopy the following code into the file:\n\n<CodeBlock file=\"cd_on_quit/cd_on_quit.fish\" />\n\nSave, exit, and reload `config.fish`:\n\n```bash\nsource ~/.config/fish/config.fish\n```\n\n##### Windows (Powershell)\n\nOpen the file:\n\n```powershell\nnotepad $PROFILE\n```\n\nCopy the following code into the file:\n\n<CodeBlock file=\"cd_on_quit/cd_on_quit.ps1\" />\n\nSave, exit, and reload your profile.\n\n```powershell\n. $PROFILE\n```\n\n:::note\nYou need to make sure powershell is allowed to execute script. If you get error like `running\nscripts is disabled on this system`. You need to allow it.\n\nExample command to enable - `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned`\n:::\n\n- ###### default_open_file_preview\n\n`true` => Shows the file preview window when you run superfile.\n\n`false` => Hides the file preview window when you run a superfile.\n\n- ###### show_image_preview\n\n`true` => Shows the image preview in file preview panel when an image file is selected.\n\n`false` => Does not show the image preview.\n\n- ###### show_panel_footer_info\n\n`true` => Shows additional footer info for file panel like panel mode and sort type.\n\n`false` => Does not show additional footer info for file panel\n\n- ###### file_size_use_si\n\n`true` => Displays the file/directory sizes using powers of 1000 (kB, MB, GB).\n\n`false` => Displays the file/directory sizes using powers of 1024 (KiB, MiB, GiB).\n\n- ###### default_directory\n\nThe default location every time superfile is opened. Supports `~` and `.`\n\n- ###### default_sort_type\n\nFile panel sorting type. Directories will always be displayed at the top.\n\n`0` => Name\n\n`1` => Size\n\n`2` => Date Modified\n\n`3` => Type\n\n`4` => Natural\n\n- ###### sort_order_reversed\n\nFile panel sorting order.\n\n`false` => Ascending (a-z)\n\n`true` => Descending (z-a)\n\n- ###### case_sensitive_sort\n\nFile panel sorting case sensitivity (if `true`, uppercase letters come before lowercase letters).\n\n`true` => Case sensitive (\"B\" comes before \"a\")\n\n`false` => Case insensitive (\"a\" comes before \"B\")\n\n- ###### debug\n\nWhether to enable debug mode. (if `true`, more verbose logs are written in log file).\n\n`true` => DEBUG, INFO, WARN, ERROR logs are written to log file\n\n`false` => INFO, WARN, ERROR logs are written to log file\n\n- ###### ignore_missing_fields\n\nControls whether warnings about missing fields in the config file are displayed.\n\n`true` => No warnings will be shown when fields are missing from the config file\n\n`false` => Warnings will be shown for any missing fields in the config file\n\n- ###### page_scroll_size\n\nNumber of lines to scroll when using PgUp/PgDown keys.\n\n`0` => Full page scroll (default behavior)\n\n`n` (where n > 0) => Scroll exactly n lines\n\n- ###### file_panel_extra_columns\n\nCount of extra columns in file panel in addition to file name.\n\n`0` => Extra columns feature is disabled\n`1` => Also show size column\n`2` => Also show modify date\n`3` => Also show permission column\n\n:::caution\nEven though the extra columns are enabled, they will be hidden if the size of the filepanel is small.\nPlease either increase the size or adjust the file_panel_name_percent config if you don't see the columns.\n:::\n\n- ###### file_panel_name_percent\n\nPercentage of file panel width allocated to file names (25-100). Higher values give more space to names, less to extra columns.\n\n### Style\n\n- ###### code_previewer\n\n`''` => Use the builtin syntax highlighting for code files with _chroma_.\n`'bat'` => Use syntax highlighting provided by the [`bat`](https://github.com/sharkdp/bat) command line tool.\n\n- ###### nerdfont\n\n`true` => Use nerdfont for directories and file icons.\n\n`false` => Dont use nerdfont. If you don't have or don't want Nerdfont installed you can turn this off\n\n- ###### show_select_icons\n\n`true` => Show checkbox icons in select mode.\n\n`false` => Don't show checkbox icons in select mode.\n\n:::note\nThis setting is ignored unless `nerdfont = true`. Both `nerdfont` and `show_select_icons` must be `true` for checkbox icons to appear.\n:::\n\n- ###### transparent_background\n\n`true` => The background color is not rendered (transparent). This is useful if your terminal background is transparent.\n\n`false` => The background is rendered (with color) to maintain theme consistency.\n\n- ###### file_preview_width\n\nThis setting is an integer.\n\n`0` => The width of the file preview window is the same as the file panel.\n\n`X` => The width of the file preview window is 1/`X` of the terminal width (minus the sidebar width). It is calculated as: (terminal width - sidebar width) / `X`\n\n:::caution\n`X` must be from 2 to 10.\n:::\n\n- ###### enable_file_preview_border\n\n`true` => Enable border around the file preview panel\n\n`false` => Disable border around the file preview panel\n\n- ###### sidebar_width\n\nThis setting is an integer.\n\n`0` => The sidebar will not display.\n\n`X` => The width of the sidebar(excluding borders).\n\n:::caution\n`X` must be from 5 to 20.\n:::\n\n- ###### sidebar_sections\n\nOrder of sidebar sections.\n\n`[\"home\", \"pinned\", \"disks\"]` => Default order.\n\nOnly sections included in this list will be displayed. You can remove sections or change their order, for example: `[\"pinned\", \"home\"]`.\n\n- ###### Border style\n\nHere are a few suggested styles, of course you can change them to your own:\n\n:::caution\nMake sure to add strings exactly one character wide. Use ' ' for borderless\n:::\n\n```toml\n# ...\nborder_top = \"━\"\nborder_bottom = \"━\"\nborder_left = \"┃\"\nborder_right = \"┃\"\nborder_top_left = \"┏\"\nborder_top_right = \"┓\"\nborder_bottom_left = \"┗\"\nborder_bottom_right = \"┛\"\nborder_middle_left = \"┣\"\nborder_middle_right = \"┫\"\n#...\n```\n\n```toml\n# ...\nborder_top = \"─\"\nborder_bottom = \"─\"\nborder_left = \"│\"\nborder_right = \"│\"\nborder_top_left = \"╭\"\nborder_top_right = \"╮\"\nborder_bottom_left = \"╰\"\nborder_bottom_right = \"╯\"\nborder_middle_left = \"├\"\nborder_middle_right = \"┤\"\n#...\n```\n\n- ###### open_with\n\nAllows users to map file extensions to commands used to open them.\nThe file path will be appended as the last argument.\n\n:::caution\nMust be at the very end of the file\n:::\n\n```toml\n[open_with]\nxopp = \"xournalpp\"\nconf = \"nvim\"\n```\n\n### Default superfile config\n\n<CodeBlock file=\"src/superfile_config/config.toml\" />\n"
  },
  {
    "path": "website/src/content/docs/contribute/file-struct.md",
    "content": "---\ntitle: superfile Project Structure Guide\ndescription: A detailed guide to understanding superfile's codebase organization\nhead:\n  - tag: title\n    content: superfile Project Structure Guide | superfile\n---\n\n# superfile Project Structure Guide\n\nThe project follows a standard Go project layout with clear separation of concerns. Here's a detailed breakdown of the main directories and their purposes:\n\n## Core Directories\n\n### `src/` - Main Source Code\n\nThe main source code is organized into several key directories:\n\n#### `cmd/` - Entry Point\n\n- `main.go` - The main entry point of the application that handles:\n  - CLI argument parsing\n  - Configuration initialization\n  - Application startup\n\n#### `config/` - Configuration Management\n\n- `fixed_variable.go` - Contains constant values and configuration paths\n- `icon/` - Icon-related configuration\n  - `function.go` - Icon initialization and management functions\n  - `icon.go` - Icon definitions and mappings\n\n#### `internal/` - Core Application Logic\n\nContains the main business logic of the application, organized by functionality:\n\n**Configuration & Types:**\n\n- `config_function.go` - Configuration loading and management\n- `config_type.go` - Configuration-related type definitions\n- `default_config.go` - Default configuration values\n- `type.go` - Core type definitions\n\n**File Operations:**\n\n- `file_operations.go` - Basic file operation functions\n- `file_operations_compress.go` - File compression functionality\n- `file_operations_extract.go` - File extraction functionality\n- `handle_file_operations.go` - File operation handlers\n\n**UI & Interaction:**\n\n- `handle_modal.go` - Modal dialog management\n- `handle_panel_movement.go` - Panel navigation logic\n- `handle_panel_navigation.go` - Panel focus management\n- `handle_pinned_operations.go` - Pinned items functionality\n- `key_function.go` - Keyboard input handling\n- `model.go` - Core application model\n- `model_render.go` - UI rendering logic\n\n**Utilities:**\n\n- `function.go` - General utility functions\n- `get_data.go` - Data retrieval functions\n- `string_function.go` - String manipulation utilities\n- `string_function_test.go` - String utility tests\n- `style.go` - UI styling definitions\n- `style_function.go` - UI styling functions\n- `string_function_test.go` - String utility tests\n- `style.go` - UI styling definitions\n- `style_function.go` - UI styling functions\n\n### `testsuite/` - superfile's testsuite written in Python\n\n- Automatically tests superfile's functionality.\n- See `testsuite/ReadMe.md` for more info\n\n## Code Organization Principles\n\n1. **Separation of Concerns:**\n\n   - Configuration management is isolated in the `config/` directory\n   - Core business logic lives in `internal/`\n   - UI-related code is separated from business logic\n\n2. **Modular Design:**\n\n   - Each file has a specific responsibility\n   - Related functionality is grouped together\n   - Clear dependencies between components\n\n3. **Testing:**\n   - Test files are placed alongside the code they test\n   - Example: `string_function_test.go` tests `string_function.go`\n\n## Contributing Guidelines\n\nWhen contributing to superfile:\n\n1. **Adding New Features:**\n\n   - Place new business logic in appropriate `internal/` subdirectories\n   - Keep UI-related code separate from business logic\n   - Follow existing naming conventions\n\n2. **Making Changes:**\n\n   - Maintain the existing file structure\n   - Add tests for new functionality\n   - Update configuration files if needed\n\n3. **Code Style:**\n   - Follow Go best practices\n   - Maintain consistent formatting\n   - Add appropriate documentation\n\nThis structure helps maintain code organization and makes it easier for new contributors to understand where to make changes.\n"
  },
  {
    "path": "website/src/content/docs/contribute/how-to-contribute.md",
    "content": "---\ntitle: How to Contribute\ndescription: How to contribute to the project, including ways to show your support, report bugs, and more.\nhead:\n  - tag: title\n    content: How to Contribute | superfile\n---\n\n# Contributing to superfile\n\nWelcome to **superfile**! This guide will help you get started contributing to the project, whether you're fixing bugs, building features, or just sharing ideas.\n\nThere are many ways to contribute:\n\n* Reporting bugs\n* Fixing issues\n* Adding a theme\n* Suggesting and implementing new features\n* Sharing ideas or feedback\n\n---\n\n## 🐞 Issues\n\n### Found a bug?\n\nCheck if there's already an open or closed issue for it. If not, open a new one and describe the problem clearly.\n\n### Want to fix an issue?\n\n1. Fork this repository\n2. Create a new branch for the issue you're working on\n3. Commit your changes with clear messages\n4. Open a pull request (PR) with a description of the problem and your solution\n\nMaintainers may request changes before merging.\n\n---\n\n## 🎨 Adding a Theme\n\nBefore starting, make sure the theme you want to add doesn’t already exist.\n\n1. Copy an existing theme's `.toml` file as a base\n2. Customize it to your needs\n3. Test it by editing your `~/.config/superfile/config/config.toml`\n4. When ready, submit a pull request\n5. To ensure the theme looks consistent and functions properly, please include the following screenshots in your PR:\n- Full view of superfile (Including sidebar, file previewer, process panel, metadata panel, and clipboard panel )\n    - Make sure that file previewer is non empty, process panel has at least one process, and clipboard has at least one entry\n- Add a screenshot of these individual panel being focused (To make sure border focus color is good)\n    - Sidebar\n    - Processbar\n- Add a screenshot of help menu (Press ?)\n- Add a screenshot of popup that opens when you create a new file (Ctrl+n)\n- Add a screenshot of image being preview using your theme.\n- Add a screenshot of successful and unsuccessful shell command.\n<details>\n<summary>Example:</summary>\n\n- Full view of superfile (Including sidebar, file previewer, process panel, metadata panel, and clipboard panel)\n\n  - Make sure that file previewer is non empty, process panel has at least one process, and clipboard has at least one entry\n\n  ![Full view of superfile](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/1.png)\n\n- Add a screenshot of these individual panels being focused (To make sure border focus color is good)\n\n  - Sidebar\n  - Processbar\n\n  ![Sidebar focused](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/2.png)\n\n  ![Processbar focused](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/3.png)\n\n- Add a screenshot of help menu (Press `?`)\n\n  ![Help menu](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/4.png)\n\n- Add a screenshot of popup that opens when you create a new file (Ctrl+n)\n\n  ![New file popup](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/5.png)\n\n- Add a screenshot of image being previewed using your theme\n\n  ![Image preview](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/6.png)\n\n- Add a screenshot of successful and unsuccessful shell command\n\n  ![Successful shell command](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/7.png)\n\n  ![Failed shell command](https://raw.githubusercontent.com/yorukot/superfile/main/asset/theme-example/8.png)\n\n</details>\n\n---\n\n## 💡 Sharing Ideas\n\nGot a new idea? Awesome!\n\n1. Check if similar ideas exist in Discussions or Issues\n2. Open a discussion at: [https://github.com/yorukot/superfile/discussions](https://github.com/yorukot/superfile/discussions)\n3. If you want to implement it yourself, follow the PR steps above\n\n---\n\n## 🧩 Don’t Know Where to Start?\n\nCheck out GitHub’s official guide:\n[https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project)\n\nStill unsure? Open a discussion — we’re happy to help.\n\n---\n\n## ✅ Pull Request Checklist\n\nPlease make sure your PR follows these steps:\n\n* [ ] I have run `go fmt ./...` to format the code\n* [ ] I have run `golangci-lint run` and fixed any reported issues\n* [ ] I have tested my changes and verified they work as expected\n* [ ] I have reviewed the diff to make sure I’m not committing any debug logs or TODOs\n* [ ] I have filled out the PR template with description, context, and screenshots if needed\n- [ ] I have checked that the PR title follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format\n\n---\n\n## 🙏 Thank You\n\nThank you for contributing to superfile! We appreciate every issue, pull request, and idea. Your help makes this project better for everyone.\n"
  },
  {
    "path": "website/src/content/docs/contribute/implementation-info.md",
    "content": "---\ntitle: Implementation info\ndescription: A collection of general information regarding how various things work\nhead:\n  - tag: title\n    content: Implementation info | superfile\n---\n\n# Implmentation info\nThe 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. \n\n## How default configuration files are packaged with app\nWe 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.\n"
  },
  {
    "path": "website/src/content/docs/getting-started/image-preview.md",
    "content": "---\ntitle: Image Preview\ndescription: Learn how image preview works in superfile and how terminal compatibility is determined.\nhead:\n  - tag: title\n    content: Image Preview | superfile\n---\n\nThis tutorial will teach you how to use superfile’s image preview feature step by step.\n\n## What is Image Preview?\n\nsuperfile supports image previews directly in your terminal using several display protocols. When supported, images can be shown inline without any external viewer.\n\n---\n\n## Terminal Compatibility\n\nsuperfile automatically detects your terminal using the `$TERM` and `$TERM_PROGRAM` environment variables. We support rendering on the following terminals:\n\n| Terminal              | Protocol         | Image Preview Support |\n|-----------------------|------------------|------------------------|\n| **kitty**             | Kitty protocol   | ✅                     |\n| **WezTerm**           | Kitty protocol   | ✅                     |\n| **Ghostty**           | Kitty protocol   | ✅                     |\n| **iTerm2**            | Inline images    | ❌                     |\n| **Konsole**           | Inline images    | ❌                     |\n| **VSCode**            | Inline images    | ❌                     |\n| **Tabby**             | Inline images    | ❌                     |\n| **Hyper**             | Inline images    | ❌                     |\n| **Mintty**            | Inline images    | ❌                     |\n| **foot**              | Sixel graphics   | ❌                     |\n| **Black Box**         | Sixel graphics   | ❌                     |\n\n> ✅ means full support for inline image preview using Kitty protocol  \n> ❌ means image preview is currently not supported\n\n---\n\n## Supported Protocols\n\nsuperfile supports the following rendering protocols and will automatically choose the best one based on your terminal:\n\n| Protocol Name     | Description                                                                                   | Status      |\n|-------------------|-----------------------------------------------------------------------------------------------|-------------|\n| **Kitty protocol** | Most capable, pixel-accurate rendering with transparency and scaling support.                | ✅ Preferred|\n| **Sixel**          | Old standard used in DEC terminals and some modern ones like foot.                           | ❌          |\n| **iTerm2 inline**  | iTerm2’s proprietary image format, used in Tabby, Hyper, etc.                                | ❌          |\n| **ANSI**           | Fallback text rendering using ANSI blocks or metadata only.                                  | ✅ Always   |\n\n---\n\n## Terminal Detection and Pixel Size\n\nsuperfile detects terminal capabilities by inspecting:\n\n- `$TERM`\n- `$TERM_PROGRAM`\n\nThese variables help us decide whether advanced rendering might be possible. However, real support is confirmed at runtime using terminal queries.\n\nTo scale images correctly, superfile sends the following escape code:\n\n```\n\\x1b[16t\n```\n\nThis sequence queries the terminal for the size of each **cell in pixels**. superfile uses the result to:\n\n- Maintain correct image aspect ratio\n- Avoid distortions in previews\n- Adapt to terminal resizes\n\nIf your terminal does not support `\\x1b[16t`, we fallback to default assumptions like `10×20 px per cell`.\n\n## Graceful Fallback to ANSI\n\nWhen advanced image preview isn't supported (for example, when the terminal doesn't support the Kitty protocol), superfile gracefully falls back to an ANSI-based preview using color-coded blocks.\n\nThis ensures a consistent and reliable experience across all terminal environments."
  },
  {
    "path": "website/src/content/docs/getting-started/installation.md",
    "content": "---\ntitle: Install superfile\ndescription: Let's install superfile to your computer..\nhead:\n  - tag: title\n    content: Install superfile | superfile\n---\n\n## Before install\n\nFirst make sure you have the following tools installed on your machine:\n\n- [Any Nerd-font ](https://www.nerdfonts.com/font-downloads), and set the font for your terminal application to use the installed Nerd-font\n\n:::tip\nIf you don't install `Nerd font`, superfile will still work, but the UI may look a bit off. It's recommended to disable the Nerd font option to avoid this issue.\n:::\n\n## Installation Scripts\n\nCopy and paste the following one-line command into your machine's terminal.\n\n### Linux / MacOs\n\nWith `curl`:\n\n```bash\nbash -c \"$(curl -sLo- https://superfile.dev/install.sh)\"\n```\n\nOr with `wget`:\n```bash\nbash -c \"$(wget -qO- https://superfile.dev/install.sh)\"\n```\n\nUse `SPF_INSTALL_VERSION` to specify a version :\n\n```bash\nSPF_INSTALL_VERSION=1.2.1 bash -c \"$(curl -sLo- https://superfile.dev/install.sh)\"\n```\n\n### Windows\n\nWith `powershell`:\n\n```bash\npowershell -ExecutionPolicy Bypass -Command \"Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://superfile.dev/install.ps1'))\"\n```\n\n:::note\nTo uninstall, run the above `powershell` command with the modified URL:\n\n`https://superfile.dev/uninstall.ps1`\n:::\n\nUse `SPF_INSTALL_VERSION` to specify a version :\n\n```bash\npowershell -ExecutionPolicy Bypass -Command \"$env:SPF_INSTALL_VERSION=1.2.1; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://superfile.dev/install.ps1'))\"\n```\n\nWith [Winget](https://winget.run/):\n\n```powershell\nwinget install --id yorukot.superfile\n``````\n\nWith [Scoop](https://scoop.sh/):\n\n```bash\nscoop install superfile\n```\n\n## Community maintained packages\n\n[![Packaging status](https://repology.org/badge/vertical-allrepos/superfile.svg)](https://repology.org/project/superfile/versions)\n\n> Sort by letter\n\n### Arch\n\n###### Builds package from sources\n\n```bash\nsudo pacman -S superfile\n```\n\n###### Builds most recent version from GitHub\n\n```bash\nyay -S superfile-git\n```\n\n### Homebrew\n\nInstall [Homebrew](https://brew.sh/) and then run the following command:\n\n```bash\nbrew install superfile\n```\n\n### NixOS\n\n###### Install with nix command-line\n\n```bash\nnix profile install github:yorukot/superfile#superfile\n```\n\n###### Install with flake\n\nAdd superfile to your flake inputs:\n\n```nix\ninputs = {\n  superfile = {\n    url = \"github:yorukot/superfile\";\n  };\n  # ...\n};\n```\n\nThen you can add it to your packages:\n\n```nix\nlet\n  system = \"x86_64-linux\";\nin {\n  environment.systemPackages = with pkgs; [\n    # ...\n    inputs.superfile.packages.${system}.default  ];\n}\n```\n\n### Pixi\n\nInstall [Pixi](https://pixi.sh/latest/) and then run the following command:\n\n```bash\npixi global install superfile\n```\n\n### X-CMD\n\n[x-cmd](https://www.x-cmd.com/) is a **toolbox for Posix Shell**, offering a lightweight package manager built using shell and awk.\n```sh\nx env use superfile\n```\n\n## Start superfile\n\nAfter completing the installation, you can restart the terminal (if necessary).\n\nRun `spf` to start superfile\n\n```bash\nspf\n```\n\n## Next steps\n\n- [Tutorial](/getting-started/tutorial)\n- [Hotkey list](/list/hotkey-list)\n"
  },
  {
    "path": "website/src/content/docs/getting-started/tutorial.md",
    "content": "---\ntitle: Learn how to use tutorial\ndescription: Quickly get started with superfile\nhead:\n  - tag: title\n    content: Tutorial | superfile\n---\n\nThis tutorial will teach you how to use superfile step by step.\n\n:::caution\nIf you haven't installed superfile yet, please [click here](/getting-started/installation).\n:::\n\n:::tip\nA full list of hotkeys are available [here](/list/hotkey-list)\n:::\n\n## Hotkeys tutorial\n\nLet's start by running superfile! Open a terminal, type `spf` and press `enter`.\n\nTo exit, press `q` or `esc`.\n\n![hotkeys-demo](../../../assets/demo/hotkeys-demo.gif)\n\n### Panel navigation\n\nOnce superfile is running, it displays five panels:\n\n- sidebar\n- file\n- processes\n- metadata\n- clipboard\n- command execution bar\n\nThe file panel is the focused view by default. You can change focus onto three other panels.\n\nPress `s` to focus on the sidebar.\n\nPress `p` to focus on the processes.\n\nPress `m` to focus on the metadata.\n\nPress `:` to open command execution bar.\n\nTo return focus back onto the file panel, press the same hotkey again.\n\n> For command execution bar you need press `esc` or `ctrl+c`\n\nYou can also press `f` to show or hide the preview window.\n\nAlso press `F` to hide or show all footer panel.\n\n![panel-navigation-demo](../../../assets/demo/panel-navigation-demo.gif)\n\n:::tip\nThe size of the folder will only be shown when you focus on the metadata.\n\nFor more detailed metadata, [click here](/configure/enable-plugin) to install the metadata plugin.\n:::\n\nTo create more file panels, press `n`. Press `w` to close the focused file panel.\n\nTo move through multiple file panels, press `tab` or `L` (shift+l). To move to the previous panel, press `shift`+`left` or `H` (shift+h).\n\n![multiple-panels-demo](../../../assets/demo/multiple-panels-demo.gif)\n\n### Panel movement\n\nsuperfile provides multiple hotkeys to move through directories. The angle bracket cursor `>` tells you where you are.\n\nWhile focused on the file panel, move the cursor up with `up` or `k` and down with `down` or `j`.\n\nAfter navigating to the your file/folder, press `enter` or `l` to confirm your selection. Files are opened with your default application (if none set, there will be no response) and folders are opened for viewing. Press `h` or `backspace` to return to the parent directory.\n\n![panel-movement-demo](../../../assets/demo/panel-movement-demo.gif)\n\nFolders can be pinned to the sidebar panel. Navigate to and open your folder. Press `P` (shift+p) to pin or unpin it.\n\nPress `o` to bring up the sort options menu. You can sort by:\n\n- `Name`\n- `Size`\n- `Date Modified`\n\nPress `enter` to confirm your sort option. Press `esc`, `o`, or `ctrl`+`c` to cancel. To reverse the order of the sort, press `R` (shift+r).\n\nPress `/` to bring up the search bar. Type the name (you may need to first delete the `/` if it auto-populates). superfile searches in the current directory and dynamically displays the results. To exit the search bar, press `ctrl`+`c` or `esc`.\n\nPress `.` to show or hide dotfiles.\n\n#### Selection mode\n\nUse selection mode for bulk operations. If you are familiar with Vim, selection mode is similar to Vim's [visual mode](https://vimhelp.org/visual.txt.html#Visual).\n\nPress `v` to toggle between selection mode and normal (browser) mode.\n\nOnce in selection mode, you can perform [file operations](#file-operations) on all selected files/folders. [Panel movement](#panel-movement) hotkeys also work in selection mode.\n\n:::tip\nThe following operations can only be performed while in selection mode. Your current mode is displayed in the lower-right corner of the file panel (Select or Browser).\n:::\n\nTo make selections, navigate to your file/folder and press `enter` or `L` (shift+l). Press the same key again to deselect.\n\nThis may become tedious when you have a large number of items. Instead, you can press `shift`+`up` or `K` (shift+k) to select everything above the cursor. Press `shift`+`down` or `J` (shift+j) to select everything below the cursor.\n\nYou can also press `A` (shift+a) to select everything in the current directory.\n\n![selection-mode-demo](../../../assets/demo/selection-mode-demo.gif)\n\n### File operations\n\n:::note\nOnly copy, cut and delete can be used in selection mode.\n:::\n\nNow let's learn how to perform file operations.\n\nCreate a new file with `ctrl`+`n`. Type your new file's name and press `enter`. To create a new folder, add `/` to the end of the name.\n\n:::tip\nYou can create a directory, subdirectory and file in one string. For example:\n\n`directory/subdirectory/filename`\n:::\n\nTo rename, point your cursor at a file/folder and press `ctrl`+`r`.\n\nTo copy, you can press `ctrl`+`c`.\n\nTo cut, you can press `ctrl`+`x`.\n\nBoth cut and copied items are shown in the clipboard panel (lower-right corner). The progress of your operations is displayed in the processes panel (lower-left corner).\n\nTo paste, you can press `ctrl`+`v`.\n\n:::note\nIn some terminals, for example Windows Powershell, `ctrl`+`v` pastes input from clipboard to terminal. So, `ctrl`+`v` might not work for paste. Either you can add `ctrl`+`w` hotkey for paste, or override default behaviour of `ctrl`+`v` on your terminal.\n:::\n\nTo delete, you can press `ctrl`+`d`\n\n:::note\nThe deletion here is not direct deletion, but will be placed in the trash can. However, when you use an external hard drive, it will be deleted directly.\n:::\n\nTo compress, press `ctrl`+`a`. To decompress, press `ctrl`+`e`.\n\nTo open a file with an editor, press `e`.\n\nTo open the current directory with an editor, press `E` (shift+e).\n\nTo change the default file editor, you can set the `EDITOR` environment variable in your terminal or you can use the `editor` config option (take priority over `EDITOR` environment variable). \nTo change the default directory editor, you can use the `dir_editor` config option.\nFor example:\n\n```bash\nEDITOR=nvim\n```\n\nThis will set Neovim as your default editor. After setting this, Neovim will be used when opening files with the `e` key bindings.\n\n```\neditor = \"nano\"\ndir_editor = \"vi\"\n```\n\nThese are changes in config file. See [superfile-config](/configure/superfile-config) for more info.\nThis will set `nano` as your default editor, and `vi` as your default directory editor. After setting this, `nano` will be used when opening files with the `e` key bindings, and `vi` will be used to open current directory with `E` key bindings.\n\n:::caution\nIf your directory editor does not support opening the current directory with an editor, you may encounter an error when pressing `E`.\n:::\n\n![file-operations-demo](../../../assets/demo/file-operations-demo.gif)\n\n### SPF Prompt\n#### Shell Mode\nPress `:` to open the prompt in shell mode, and execute any shell command in the current directory.\n![Prompt-Shell-Mode](../../../assets/git-assets/prompt_shell_mode.png)\n\n:::note\nYou won't receive any stdout outputs.\nFor now, this is meant for executing more complex file manipulations via the shell,\nrather than handling interactive outputs.\nYou will be able to see the exit code of the command.\n:::\n\n#### SPF Mode\nPress `>` to open the prompt in SPF mode. \n![Prompt-SPF-Mode](../../../assets/git-assets/prompt_spf_mode.png)\n\nIn this mode, you can execute these spf commands :\n- `split` - Open a new panel at a current file panel's path.\n- `open <PATH>` - Open a new panel at a specified path.\n- `cd <PATH>` - Change directory of current panel.\n\nIn this mode, You can substitute shell environment variables via `${}`, shell commands via `$()` and prefix path with `~` to get substituted to home directory \nFor example \n- `cd ${HOME}` or `cd ~/xyz`\n- `open $(dirname $(which bash))`\n\nPress `esc` or `ctrl`+`c` to exit Prompt."
  },
  {
    "path": "website/src/content/docs/index.mdx",
    "content": "---\ntitle: superfile | terminal-based file manager\ndescription: \"superfile is a very fancy and modern terminal file manager that can complete the file operations you need!!\"\ntemplate: splash\nlastUpdated: false\neditUrl: false\nhero:\n  title: Perfect Terminal-based file manager 🚀!\n  tagline: \"superfile is a very fancy and modern terminal file manager that can complete the file operations you need!!\"\n  image:\n    file: ../../assets/logo.png\n  actions:\n    - text: Get Started\n      link: /overview\n      icon: right-arrow\n      variant: primary\n    - text: View on GitHub\n      link: https://github.com/yorukot/superfile\n      icon: external\n---\n\nimport { Card, CardGrid } from '@astrojs/starlight/components';\nimport GithubStar from '../../components/GithubStar.astro';\nimport About from '../../components/about.astro';\n\n<GithubStar />\n\n## Features\n\n<CardGrid stagger>\n\t<Card title=\"Exquisite and beautiful UI\" icon=\"star\">\n    It can be said that good-looking is the original intention of superfile, so the entire superfile should be as beautiful as possible.\n\t</Card>\n  <Card title=\"Complete functions\" icon=\"seti:nunjucks\">\n\t\tThis file manager allows you to do almost everything you want to do on a file manager.\n\t</Card>\n  <Card title=\"Fully customizable\" icon=\"setting\">\n    From basic Hotkey, the entire theme color and even the border Style can be customized.\n  </Card>\n  <Card title=\"Multiple panel\" icon=\"seti:plan\">\n\t\tMultiple 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.\n  </Card>\n</CardGrid>\n<About title=\"Built with ❤️ by Yorukot and all contributor\">\n\n</About>\n"
  },
  {
    "path": "website/src/content/docs/list/hotkey-list.md",
    "content": "---\ntitle: Hotkey list\ndescription: superfile hotkey list\nhead:\n  - tag: title\n    content: Hotkey list | superfile\n---\n\n:::tip\nThese are the default hotkeys and you can [change](/configure/custom-hotkeys) them all!\n:::\n\n## General\n\n| Function                                | Key              | Variable name    |\n| --------------------------------------- | ---------------- | ---------------- |\n| Open superfile                          | `spf`            |                  |\n| Confirm your select or typing           | `enter`, `right` | `confirm_typing` |\n| Quit typing, modal or superfile         | `esc`, `q`       | `quit`           |\n| Quit superfile and cd to current folder | `Q`              | `cd_quit`        |\n| Cancel typing                           | `ctrl+c`, `esc`  | `cancel_typing`  |\n| Open help menu(hotkeylist)              | `?`              | `open_help_menu` |\n| Toggle footer                           | `F`              | `toggle_footer`  |\n\n:::note\nQuit superfile and cd to current folder \"cd_quit\" require the same scripts as [\"cd_on_quit\"](/configure/superfile-config/#cd_on_quit) setting\n:::\n\n## Panel navigation\n\n| Function                         | Key                        | Variable name               |\n| -------------------------------- | -------------------------- | --------------------------- |\n| Create new file panel            | `n`                        | `create_new_file_panel`     |\n| Split focused file panel         | `N` (shift+n)              | `split_file_panel`          |\n| Close the focused file panel     | `w`                        | `close_file_panel`          |\n| Toggle file preview panel        | `f`                        | `toggle_file_preview_panel` |\n| Focus on the next file panel     | `tab`, `L`(shift+l)        | `next_file_panel`           |\n| Focus on the previous file panel | `shift+left`, `H`(shift+h) | `previous_file_panel`       |\n| Focus on the processbar panel    | `p`                        | `focus_on_process_bar`      |\n| Focus on the sidebar             | `s`                        | `focus_on_side_bar`         |\n| Focus on the metadata panel      | `m`                        | `focus_on_metadata`         |\n| Open prompt in shell mode        | `:`                        | `open_command_line`         |\n| Open prompt in spf mode          | `>`                        | `open_spf_prompt`           |\n| Open zoxide navigation modal     | `z`                        | `open_zoxide`               |\n\n## Panel movement\n\n| Function                                           | Key                         | Variable name                                                   |\n| -------------------------------------------------- | --------------------------- | --------------------------------------------------------------- |\n| Up                                                 | `up`, `k`                   | `list_up`                                                       |\n| Down                                               | `down`, `j`                 | `list_down`                                                     |\n| Return to parent folder                            | `h`, `left`, `backspace`    | `parent_folder`                                                 |\n| Toggle sort options menu                           | `o`                         | `open_sort_options_menu`                                        |\n| Select all items in focused file panel             | `A` (shift+a)               | `file_panel_select_all_item` (selection mode only)              |\n| Select up with your course                         | `shift+up`, `K` (shift+k)   | `file_panel_select_mode_item_select_up` (selection mode only)   |\n| Select down with your course                       | `shift+down`, `J` (shift+j) | `file_panel_select_mode_item_select_down` (selection mode only) |\n| Toggle dot file display                            | `.`                         | `toggle_dot_file`                                               |\n| Toggle active search bar                           | `/`                         | `search_bar`                                                    |\n| Change between selection mode or normal mode       | `v`                         | `change_panel_mode`                                             |\n| Pin or Unpin folder to sidebar (can be auto saved) | `P` (shift+p)               | `pinned_folder`                                                 |\n\n## File operations\n\n| Function                                             | Key                | Variable name                                                                          |\n| ---------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------- |\n| Create file or folder(/ ends with creating a folder) | `ctrl+n`           | `file_panel_item_create`                                                               |\n| Rename file or folder                                | `ctrl+r`           | `file_panel_item_rename`                                                               |\n| Copy file or folder (or both)                        | `ctrl+c`           | `copy_single_item` (normal mode) <br> `file_panel_select_mode_item_copy` (select mode) |\n| Cut file or folder (or both)                         | `ctrl+x`           | `file_panel_select_mode_item_cut`                                                      |\n| Paste all items in your clipboard                    | `ctrl+v`, `ctrl+w` | `paste_item`                                                                           |\n| Delete file or folder (or both)                      | `ctrl+d`, `delete` | `delete_item` (normal mode) <br> `file_panel_select_mode_item_delete` (select mode)    |\n| Copy current file or directory path                  | `ctrl+p`           | `copy_path`                                                                            |\n| Extract zip file                                     | `ctrl+e`           | `extract_file` (normal mode)                                                           |\n| Zip file or folder to .zip file                      | `ctrl+a`           | `compress_file` (normal mode)                                                          |\n| Open file with your default editor                   | `e`                | `open_file_with_editor` (normal node)                                                  |\n| Open current directory with default editor           | `E` (shift+e)      | `current_directory_with_editor` (normal node)                                          |\n| Permanently Delete file or folder (or both)          | `D` (shift+d) | `permanently_delete_items` (normal mode) <br> `file_panel_select_mode_item_delete` (select mode)    |\n"
  },
  {
    "path": "website/src/content/docs/list/plugin-list.md",
    "content": "---\ntitle: Plugin List\ndescription: Complete list of available superfile plugins\nhead:\n  - tag: title\n    content: Plugin List | superfile\n---\n\nSuperfile supports various plugins to extend its functionality. Below is a complete list of available plugins and their requirements.\n\n### Metadata\n\n- **Description:** Show more detailed metadata for files and directories\n\n- **Requirements:** [`exiftool`](https://exiftool.org)\n\n- **Config name:** `metadata`\n\n### Zoxide\n\n- **Description:** Smart directory jumping integration with zoxide. Navigate to frequently used directories quickly with a searchable modal interface.\n\n- **Requirements:** [`zoxide`](https://github.com/ajeetdsouza/zoxide)\n\n- **Config name:** `zoxide_support`\n\n- **Usage:** Press `z` to open the zoxide navigation modal. Start typing to search directories, use arrow keys to navigate results, and press Enter to jump to a directory."
  },
  {
    "path": "website/src/content/docs/list/theme-list.md",
    "content": "---\ntitle: Theme list\ndescription: List themes currently owned by superfile\nhead:\n  - tag: title\n    content: Theme list | superfile\n---\n\n> Sort by A-Z\n\n## 0x96f\n\n- Theme name: `0x96f`\n- Ported by: https://github.com/filipjanevski\n- Original Author: https://github.com/filipjanevski/\n\n![0x96f theme preview showing dark color scheme with blue accents](../../../assets/git-assets/theme/0x96f.png)\n\n## Ayu Dark\n\n- Theme name: `ayu-dark`\n- Ported by: https://github.com/rustnomicon\n- Original Author: https://github.com/ayu-theme/\n\n![Ayu Dark theme preview showing warm dark color palette](../../../assets/git-assets/theme/ayu-dark.png)\n\n## Blood\n\n- Theme name: `blood`\n- Ported by: https://github.com/charlesrocket\n- Original Author: https://github.com/charlesrocket\n\n![Blood theme preview showing dark red color scheme](../../../assets/git-assets/theme/blood.png)\n\n## Catppuccin Frappe\n\n- Theme name: `catppuccin-frappe`\n- Ported by: https://github.com/GV14982\n- Original Author: https://github.com/catppuccin\n\n![Catppuccin Frappe theme preview showing muted dark colors](../../../assets/git-assets/theme/catppuccin-frappe.png)\n\n## Catppuccin Latte\n\n- Theme name: `catppuccin-latte`\n- Ported by: https://github.com/GV14982\n- Original Author: https://github.com/catppuccin\n\n![Catppuccin Latte theme preview showing light color scheme](../../../assets/git-assets/theme/catppuccin-latte.png)\n\n## Catppuccin Macchiato\n\n- Theme name: `catppuccin-macchiato`\n- Ported by: https://github.com/GV14982\n- Original Author: https://github.com/catppuccin\n\n![Catppuccin Macchiato theme preview showing medium dark colors](../../../assets/git-assets/theme/catppuccin-macchiato.png)\n\n## Catppuccin Mocha\n\n- Theme name: `catppuccin-mocha`\n- Ported by: https://github.com/AnshumanNeon\n- Original Author: https://github.com/catppuccin\n\n![Catppuccin theme preview showing pastel color palette](../../../assets/git-assets/theme/catppuccin.png)\n\n## Dracula\n\n- Theme name: `dracula`\n- Ported by: https://github.com/BeanieBarrow\n- Original Author: https://github.com/zenorocha\n\n![Dracula theme preview showing purple and pink dark color scheme](../../../assets/git-assets/theme/dracula.png)\n\n## Everforest Dark Medium\n\n- Theme name: `everforest-dark-medium`\n- Ported by: https://github.com/dotintegral\n- Original Author: https://github.com/sainnhe/\n\n![Everforest Dark Medium theme preview showing nature-inspired green colors](../../../assets/git-assets/theme/everforest-dark-medium.png)\n\n## Everforest Dark Hard\n\n- Theme name: `everforest-dark-hard`\n- Ported by: https://github.com/fzahner\n- Original Author: https://github.com/sainnhe/\n\n![Everforest Dark hard theme preview showing nature-inspired green colors](../../../assets/git-assets/theme/everforest-dark-hard.png)\n\n## Gruvbox\n\n- Theme name: `gruvbox`\n- Ported by: https://github.com/yorukot\n- Original Author: https://github.com/morhetz/\n\n![Gruvbox theme preview showing retro warm color palette](../../../assets/git-assets/theme/gruvbox.png)\n\n## Gruvbox Dark Hard\n\n- Theme name: `gruvbox-dark-hard`\n- Ported by: https://github.com/frost-phoenix\n- Original Author: https://github.com/morhetz/\n\n![Gruvbox Dark Hard theme preview showing high contrast warm colors](../../../assets/git-assets/theme/gruvbox-dark-hard.png)\n\n## Hacks\n\n- Theme name: `hacks`\n- Ported by: https://github.com/charlesrocket\n- Original Author: https://github.com/charlesrocket\n\n![Hacks theme preview showing cyberpunk-inspired color scheme](../../../assets/git-assets/theme/hacks.png)\n\n## Kaolin\n\n- Theme name: `kaolin`\n- Ported by: https://github.com/AnshumqanNeon\n- Original Author: https://github.com/ogdenwebb/\n\n![Kaolin theme preview showing brown and orange earth tones](../../../assets/git-assets/theme/kaolin.png)\n\n## Monokai\n\n- Theme name: `monokai`\n- Ported by: https://github.com/CommandJoo\n- Original Author: https://github.com/monokai\n\n![Monokai theme preview showing classic dark syntax highlighting colors](../../../assets/git-assets/theme/monokai.png)\n\n## Nord\n\n- Theme name: `nord`\n- Ported by: https://github.com/ramses-eltany\n- Original Author: https://github.com/nordtheme\n\n![Nord theme preview showing cool blue and white arctic colors](../../../assets/git-assets/theme/nord.png)\n\n## OneDark\n\n- Theme name: `onedark`\n- Ported by: https://github.com/CommandJoo\n- Original Author: https://github.com/one-dark\n\n![OneDark theme preview showing dark background with blue accents](../../../assets/git-assets/theme/onedark.png)\n\n## Poimandres\n\n- Theme name: `poimandres`\n- Ported by: https://github.com/Myles-J\n- Original Author: https://github.com/drcmda/\n\n![Poimandres theme preview showing dark purple and teal color scheme](../../../assets/git-assets/theme/poimandres.png)\n\n## Rosé Pine\n\n- Theme name: `rose-pine`\n- Ported by: https://github.com/pearcidar\n- Original Author: https://github.com/rose-pine\n\n![Rosé Pine theme preview showing soft pink and purple colors](../../../assets/git-assets/theme/rose-pine.png)\n\n## Sugarplum\n\n- Theme name: `sugarplum`\n- Ported by: https://github.com/lemonlime0x3C33\n- Original Author: https://github.com/lemonlime0x3C33\n\n![Sugarplum theme preview showing sweet purple and pink color palette](../../../assets/git-assets/theme/sugarplum.png)\n\n## Tokyonight\n\n- Theme name: `tokyonight`\n- Ported by: https://github.com/pearcidar\n- Original Author: https://github.com/enkia/\n\n![Tokyonight theme preview showing dark blue nighttime color scheme](../../../assets/git-assets/theme/tokyonight.png)\n"
  },
  {
    "path": "website/src/content/docs/overview.md",
    "content": "---\ntitle: Overview\ndescription: An overview of why we built this starter, including its features, the libraries used, and more.\nhead:\n  - tag: title\n    content: Overview | superfile\n---\n\n![Demo of superfile terminal file manager interface](../../assets/git-assets/demo.png)\n\n# What is superfile?\nsuperfile 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.\n\n# Why was superfile built?\nBefore 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.\n\n# Why should I use superfile?\nsuperfile 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."
  },
  {
    "path": "website/src/content/docs/special-thanks.mdx",
    "content": "---\ntitle: Special Thanks\ndescription: A special thanks to the people, projects, and media that made superfile possible.\nhead:\n  - tag: title\n    content: Special Thanks | superfile\n---\n\n## Core Team\n- [Yorukot](https://github.com/yorukot) - Creator of superfile.\n- [Lazysegtree](https://github.com/lazysegtree) - Currently a primary contributor to superfile.\n\n## Contributors\n\n<a href=\"https://github.com/yorukot/superfile/graphs/contributors\">\n  <img alt=\"Contributors\" src=\"https://contrib.rocks/image?repo=yorukot/superfile\" />\n</a>\n\n## Sponsors\n\n- [Supporter at Ko-fi](https://ko-fi.com/yorukot) - Thank you to all the supporters who bought me a coffee on Ko-fi.\n- [Warp](https://www.warp.dev/) - Special thanks to Warp for sponsoring the development of superfile.\n- [JetBrains](https://www.jetbrains.com/) - Special thanks to JetBrains for providing free licenses for open-source projects.\n\n## Media\n\n- Thanks to all the bloggers, writers, streamers, and YouTubers who created content about superfile—your support has helped spread the word and grow the community!\n"
  },
  {
    "path": "website/src/content/docs/troubleshooting.md",
    "content": "---\ntitle: Troubleshooting\ndescription: Have you encountered any problems? Come here and take a look.\nhead:\n  - tag: title\n    content: Troubleshooting | superfile\n---\n\n## My superfile icon doesn't display correctly\n\nTry these things below:\n\n- Make sure you already install [nerdfont](https://www.nerdfonts.com/font-downloads) (You can choose whatever font you like!)\n- Apply this font to your terminal,This may require different settings depending on the terminal.You can check how to set it up!\n\n## Help! My superfile's rendering is all messed up!\n\nTry these things below:\n\n- Set your locale to utf-8  \n- chcp 65001 ( If that's an option for your shell )  \n- Set environment variable RUNEWIDTH_EASTASIAN to 0 (`RUNEWIDTH_EASTASIAN=0`)"
  },
  {
    "path": "website/src/env.d.ts",
    "content": "/// <reference path=\"../.astro/types.d.ts\" />\n/// <reference types=\"astro/client\" />\n"
  },
  {
    "path": "website/src/styles/custom.css",
    "content": ":root {\n  --sl-font: 'IBM Plex Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n}\n\n/* Dark mode colors. */\n:root {\n  --sl-color-accent-low: #2c230a;\n  --sl-color-accent: #846500;\n  --sl-color-accent-high: #d4c8ab;\n  --sl-color-white: #ffffff;\n  --sl-color-gray-1: #eceef2;\n  --sl-color-gray-2: #c0c2c7;\n  --sl-color-gray-3: #888b96;\n  --sl-color-gray-4: #545861;\n  --sl-color-gray-5: #353841;\n  --sl-color-gray-6: #24272f;\n  --sl-color-black: #17181c;\n}\n/* Light mode colors. */\n:root[data-theme='light'] {\n  --sl-color-accent-low: #dfd6c0;\n  --sl-color-accent: #866700;\n  --sl-color-accent-high: #3f3003;\n  --sl-color-white: #17181c;\n  --sl-color-gray-1: #24272f;\n  --sl-color-gray-2: #353841;\n  --sl-color-gray-3: #545861;\n  --sl-color-gray-4: #888b96;\n  --sl-color-gray-5: #c0c2c7;\n  --sl-color-gray-6: #eceef2;\n  --sl-color-gray-7: #f5f6f8;\n  --sl-color-black: #ffffff;\n}\n\n:root {\n  --purple-hsl: 205, 60%, 60%;\n  --overlay-blurple: hsla(var(--purple-hsl), 0.4);\n}\n\n:root[data-theme='light'] {\n  --purple-hsl: 255, 85%, 65%;\n}\n\n[data-has-hero] .page {\n  background: linear-gradient(215deg, var(--overlay-blurple), transparent 40%),\n    radial-gradient(var(--overlay-blurple), transparent 40%) no-repeat -60vw -40vh /\n      105vw 200vh,\n    radial-gradient(var(--overlay-blurple), transparent 65%) no-repeat 50%\n      calc(100% + 20rem) / 60rem 30rem;\n}\n\n[data-has-hero] header {\n  border-bottom: 1px solid transparent;\n  background-color: transparent;\n  -webkit-backdrop-filter: blur(16px);\n  backdrop-filter: blur(16px);\n}\n\n[data-has-hero] .hero > img {\n  filter: drop-shadow(0 0 3rem var(--overlay-blurple));\n}\n\n[data-page-title] {\n  font-size: 3rem;\n}\n\n/* date page title onl 2.5rem on mobile devices */\n@media (max-width: 768px) {\n  [data-page-title] {\n    font-size: 2.5rem;\n  }\n}\n\n.card-grid > .card {\n  border-radius: 10px;\n}\n\n.card > .title {\n  font-size: 1.3rem;\n  font-weight: 600;\n  line-height: 1.2;\n}\n"
  },
  {
    "path": "website/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\"\n}\n"
  }
]