Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"time"

"github.com/yorukot/superfile/src/internal/common"
"github.com/yorukot/superfile/src/internal/common/utils"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai
Please generate a task description for this PR. I will give someone this task. He doesn't know anything about this product. we have a sidebar with implementation scattered across.

move stuff to sidebar package.
Make side functions exported so that they are accessible outside
Move src/internal/common/utils to a separate src/internal/utils package
Ensure nothing breaks
Organize code into correct files.
Any other refactoring that I missed ?

Add Anything else that I missed, which is done in this PR, and should be in the ask description.

Keep it in 6-7 points or less. Format should be markdown. Make sure to highlight what needs to be done, not how or where.

Give basic context about Superfile and what the requirements are.The problem description should be stated such that a reasonably competent software engineer could write the correct PR for it without too much familiarity with the repo. Write it in a way that a LLM could generate a correct PR for it.

Also, generate a TL;DR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lazysegtree

Task Description: Sidebar Code Consolidation and Refactoring

TL;DR

Consolidate scattered sidebar implementation into a dedicated sidebar package, export necessary functions, reorganize utilities, and ensure backward compatibility throughout the codebase.


Context

Superfile is a modern terminal file manager written in Go. It features a sidebar UI component that displays pinned directories, well-known system directories (like Documents, Downloads), and external media/disk partitions. Users can navigate, search, and rename items within the sidebar.

Problem

The sidebar implementation is currently scattered across multiple files in the main internal package. Functions are not properly encapsulated, utilities are mixed with other code, and the architecture makes maintenance and testing difficult.

Requirements

  1. Create a dedicated sidebar package at src/internal/ui/sidebar and consolidate all sidebar-related code (state management, rendering, navigation, directory operations) into this package.

  2. Export functions that need external access by converting them from lowercase to PascalCase naming convention (e.g., listUpListUp, sidebarModelModel).

  3. Reorganize utility functions by moving src/internal/common/utils to src/internal/utils, making it a standalone package independent of the common package.

  4. Update all import paths and references throughout the entire codebase to reflect the new package structure without breaking existing functionality.

  5. Organize code logically within the sidebar package by separating concerns into appropriate files (e.g., rendering logic, navigation, directory operations, disk utilities, type definitions).

  6. Maintain backward compatibility by ensuring all existing features (sidebar navigation, search, rename, pinned directories) continue to work exactly as before.


This refactoring improves code organization, testability, and maintainability without introducing new features or changing user-facing behavior.


🧠 Learnings used
Learnt from: lazysegtree
PR: yorukot/superfile#0
File: :0-0
Timestamp: 2025-04-12T12:00:32.688Z
Learning: In PR #767 for yorukot/superfile, the focus is on moving code (especially sidebar-related functionality) to a more organized structure without changing functionality. Pre-existing issues should be ignored since the purpose is code reorganization, not fixing existing problems.

Learnt from: lazysegtree
PR: yorukot/superfile#1013
File: src/internal/model_render.go:329-341
Timestamp: 2025-08-29T14:11:21.380Z
Learning: lazysegtree prefers to defer help menu rendering optimizations and other technical debt improvements to separate GitHub issues when the current PR scope has grown too large, maintaining focus on the primary refactoring objectives while tracking performance improvements for future work.

Learnt from: lazysegtree
PR: yorukot/superfile#0
File: :0-0
Timestamp: 2025-04-12T12:25:06.352Z
Learning: PR #770 "Sidebar code separation" is a refactoring and code reorganization effort with no new features. It involves moving code, updating import paths, renaming functions to follow PascalCase convention, and consolidating sidebar-related functionality into a dedicated package.

Learnt from: lazysegtree
PR: yorukot/superfile#963
File: src/internal/default_config.go:16-16
Timestamp: 2025-07-27T07:40:51.938Z
Learning: lazysegtree prefers simpler implementation approaches when the alternatives are significantly more complex, even if the alternatives might be architecturally cleaner, prioritizing maintainability and avoiding over-engineering.

Learnt from: lazysegtree
PR: yorukot/superfile#963
File: src/internal/default_config.go:16-16
Timestamp: 2025-07-27T07:40:51.938Z
Learning: lazysegtree prefers simpler implementation approaches when the alternatives are significantly more complex, even if the alternatives might be architecturally cleaner, prioritizing maintainability and avoiding over-engineering.

Learnt from: lazysegtree
PR: yorukot/superfile#969
File: src/internal/key_function.go:40-40
Timestamp: 2025-08-03T09:34:55.721Z
Learning: lazysegtree emphasizes proper dependency direction in software architecture, preferring that low-level components (like modal handlers) should not depend on high-level components (like the main model object). He also prioritizes performance considerations, noting that creating objects on every keypress in hot code paths like key handling is inefficient and should be avoided.

Learnt from: lazysegtree
PR: yorukot/superfile#985
File: src/internal/model.go:0-0
Timestamp: 2025-08-11T01:49:30.040Z
Learning: lazysegtree prefers maintaining code correctness through proper design and invariants rather than adding defensive bounds checks at every slice access point, viewing such defensive programming as "duct taping" that can mask actual bugs instead of fixing them at their source.

Learnt from: lazysegtree
PR: yorukot/superfile#973
File: src/internal/ui/processbar/model_update.go:7-27
Timestamp: 2025-08-03T14:49:31.221Z
Learning: lazysegtree prefers to keep test-only code simple without adding production-level concerns like goroutine synchronization, cancellation contexts, or complex lifecycle management, even when such patterns might prevent potential issues, since the complexity isn't justified for test utilities.

Learnt from: lazysegtree
PR: yorukot/superfile#1044
File: src/internal/utils/file_utils.go:149-149
Timestamp: 2025-09-09T13:29:11.771Z
Learning: lazysegtree prefers to keep PR scope focused on the primary objectives and considers pre-existing technical debt issues as out of scope for migration/refactoring PRs, preferring to defer such issues to separate GitHub issues rather than expanding the current PR scope.

Learnt from: lazysegtree
PR: yorukot/superfile#950
File: src/internal/ui/metadata/README.md:7-7
Timestamp: 2025-07-24T03:46:29.516Z
Learning: lazysegtree prefers to defer comprehensive unit testing to separate PRs when the current PR has grown too large, maintaining focus on the primary refactoring objectives while tracking testing requirements in dedicated GitHub issues.

Learnt from: lazysegtree
PR: yorukot/superfile#967
File: src/internal/key_function.go:45-47
Timestamp: 2025-08-02T11:47:07.713Z
Learning: lazysegtree prefers to track technical debt and architectural improvements in dedicated GitHub issues when they are identified during PR reviews but are beyond the scope of the current PR, particularly for complex refactoring needs like input handling architecture that would require significant changes.

Learnt from: lazysegtree
PR: yorukot/superfile#963
File: src/internal/handle_file_operations.go:567-570
Timestamp: 2025-07-27T08:49:09.687Z
Learning: lazysegtree prefers to defer technical debt issues like model mutation concerns to later PRs when the current PR has already grown too large, maintaining focus on the primary objectives while acknowledging the need to track such issues for future work.

Learnt from: lazysegtree
PR: yorukot/superfile#979
File: src/internal/common/predefined_variable.go:47-47
Timestamp: 2025-08-06T10:54:31.444Z
Learning: When lazysegtree says a review is "inaccurate and pre-existing issues. Not much useful," it means I should focus specifically on bugs or problems introduced by the current PR changes, not architectural concerns or code quality issues that were already present. The refactoring work in superfile PRs is generally well-implemented and should be evaluated on whether the specific changes work correctly, not on broader codebase architecture.

"github.com/yorukot/superfile/src/internal/utils"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
Expand Down
2 changes: 1 addition & 1 deletion src/config/fixed_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"path/filepath"

"github.com/urfave/cli/v2"
"github.com/yorukot/superfile/src/internal/common/utils"
"github.com/yorukot/superfile/src/internal/utils"

"github.com/adrg/xdg"
)
Expand Down
63 changes: 63 additions & 0 deletions src/internal/common/icon_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package common

import (
"path/filepath"
"strings"

"github.com/yorukot/superfile/src/config/icon"
)

func GetElementIcon(file string, isDir bool, nerdFont bool) icon.Style {
ext := strings.TrimPrefix(filepath.Ext(file), ".")
name := file

if !nerdFont {
return icon.Style{
Icon: "",
Color: Theme.FilePanelFG,
}
}

if isDir {
resultIcon := icon.Folders["folder"]
betterIcon, hasBetterIcon := icon.Folders[name]
if hasBetterIcon {
resultIcon = betterIcon
}
return resultIcon
}
// default icon for all files. try to find a better one though...
resultIcon := icon.Icons["file"]
// resolve aliased extensions
extKey := strings.ToLower(ext)
alias, hasAlias := icon.Aliases[extKey]
if hasAlias {
extKey = alias
}

// see if we can find a better icon based on extension alone
betterIcon, hasBetterIcon := icon.Icons[extKey]
if hasBetterIcon {
resultIcon = betterIcon
}

// now look for icons based on full names
fullName := name

fullName = strings.ToLower(fullName)
fullAlias, hasFullAlias := icon.Aliases[fullName]
if hasFullAlias {
fullName = fullAlias
}
bestIcon, hasBestIcon := icon.Icons[fullName]
if hasBestIcon {
resultIcon = bestIcon
}
if resultIcon.Color == "NONE" {
return icon.Style{
Icon: resultIcon.Icon,
Color: Theme.FilePanelFG,
}
}
return resultIcon
}
97 changes: 97 additions & 0 deletions src/internal/common/icon_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package common

import (
"testing"

"github.com/yorukot/superfile/src/config/icon"
)

func TestGetElementIcon(t *testing.T) {
tests := []struct {
name string
file string
isDir bool
nerdFont bool
expected icon.Style
}{
{
name: "Non-nerdfont returns empty icon",
file: "test.txt",
isDir: false,
nerdFont: false,
expected: icon.Style{
Icon: "",
Color: Theme.FilePanelFG,
},
},
{
name: "Directory with nerd font",
file: "folder",
isDir: true,
nerdFont: true,
expected: icon.Folders["folder"],
},
{
name: "File with known extension",
file: "test.js",
isDir: false,
nerdFont: true,
expected: icon.Icons["js"],
},
{
name: "Full name takes priority over extension",
file: "gulpfile.js",
isDir: false,
nerdFont: true,
expected: icon.Icons["gulpfile.js"],
},
{
name: ".git directory",
file: ".git",
isDir: true,
nerdFont: true,
expected: icon.Folders[".git"],
},
{
name: "superfile directory",
file: "superfile",
isDir: true,
nerdFont: true,
expected: icon.Folders["superfile"],
},
{
name: "package.json file",
file: "package.json",
isDir: false,
nerdFont: true,
expected: icon.Icons["package"],
},
{
name: "File with unknown extension",
file: "test.xyz",
isDir: false,
nerdFont: true,
expected: icon.Style{
Icon: icon.Icons["file"].Icon,
// Theme is not defined here, so this will be blank
Color: Theme.FilePanelFG,
},
},
{
name: "File with aliased name",
file: "dockerfile",
isDir: false,
nerdFont: true,
expected: icon.Icons["dockerfile"],
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetElementIcon(tt.file, tt.isDir, tt.nerdFont)
if result.Icon != tt.expected.Icon || result.Color != tt.expected.Color {
t.Errorf("GetElementIcon() = %v, want %v", result, tt.expected)
}
})
}
}
2 changes: 1 addition & 1 deletion src/internal/common/load_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"reflect"

"github.com/pelletier/go-toml/v2"
"github.com/yorukot/superfile/src/internal/common/utils"
"github.com/yorukot/superfile/src/internal/utils"

variable "github.com/yorukot/superfile/src/config"
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package internal
package common

import (
"bufio"
Expand All @@ -11,22 +11,20 @@ import (
"unicode"
"unicode/utf8"

"github.com/yorukot/superfile/src/internal/common"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/exp/term/ansi"
)

func truncateText(text string, maxChars int, talis string) string {
truncatedText := ansi.Truncate(text, maxChars-len(talis), "")
func TruncateText(text string, maxChars int, tails string) string {
truncatedText := ansi.Truncate(text, maxChars-len(tails), "")
if text != truncatedText {
return truncatedText + talis
return truncatedText + tails
}

return text
}

func truncateTextBeginning(text string, maxChars int, talis string) string {
func TruncateTextBeginning(text string, maxChars int, tails string) string {
if ansi.StringWidth(text) <= maxChars {
return text
}
Expand All @@ -40,63 +38,63 @@ func truncateTextBeginning(text string, maxChars int, talis string) string {
truncatedWidth = ansi.StringWidth(string(truncatedRunes))
}

if len(truncatedRunes) > len(talis) {
truncatedRunes = append([]rune(talis), truncatedRunes[len(talis):]...)
if len(truncatedRunes) > len(tails) {
truncatedRunes = append([]rune(tails), truncatedRunes[len(tails):]...)
}

return string(truncatedRunes)
}

func truncateMiddleText(text string, maxChars int, talis string) string {
func TruncateMiddleText(text string, maxChars int, tails string) string {
if utf8.RuneCountInString(text) <= maxChars {
return text
}

halfEllipsisLength := (maxChars - 3) / 2

truncatedText := text[:halfEllipsisLength] + talis + text[utf8.RuneCountInString(text)-halfEllipsisLength:]
// Todo : Use ansi.Substring to correctly handle ANSI escape codes
truncatedText := text[:halfEllipsisLength] + tails + text[utf8.RuneCountInString(text)-halfEllipsisLength:]

return truncatedText
}

func prettierName(name string, width int, isDir bool, isSelected bool, bgColor lipgloss.Color) string {
style := getElementIcon(name, isDir)
func PrettierName(name string, width int, isDir bool, isSelected bool, bgColor lipgloss.Color) string {
style := GetElementIcon(name, isDir, Config.Nerdfont)
if isSelected {
return common.StringColorRender(lipgloss.Color(style.Color), bgColor).
return StringColorRender(lipgloss.Color(style.Color), bgColor).
Background(bgColor).
Render(style.Icon+" ") +
common.FilePanelItemSelectedStyle.
Render(truncateText(name, width, "..."))
FilePanelItemSelectedStyle.
Render(TruncateText(name, width, "..."))
}
return common.StringColorRender(lipgloss.Color(style.Color), bgColor).
return StringColorRender(lipgloss.Color(style.Color), bgColor).
Background(bgColor).
Render(style.Icon+" ") +
common.FilePanelStyle.Render(truncateText(name, width, "..."))
FilePanelStyle.Render(TruncateText(name, width, "..."))
}

func prettierDirectoryPreviewName(name string, isDir bool, bgColor lipgloss.Color) string {
style := getElementIcon(name, isDir)
return common.StringColorRender(lipgloss.Color(style.Color), bgColor).
func PrettierDirectoryPreviewName(name string, isDir bool, bgColor lipgloss.Color) string {
style := GetElementIcon(name, isDir, Config.Nerdfont)
return StringColorRender(lipgloss.Color(style.Color), bgColor).
Background(bgColor).
Render(style.Icon+" ") +
common.FilePanelStyle.Render(name)
FilePanelStyle.Render(name)
}

func clipboardPrettierName(name string, width int, isDir bool, isSelected bool) string {
style := getElementIcon(name, isDir)
func ClipboardPrettierName(name string, width int, isDir bool, isSelected bool) string {
style := GetElementIcon(name, isDir, Config.Nerdfont)
if isSelected {
return common.StringColorRender(lipgloss.Color(style.Color), common.FooterBGColor).
Background(common.FooterBGColor).
return StringColorRender(lipgloss.Color(style.Color), FooterBGColor).
Background(FooterBGColor).
Render(style.Icon+" ") +
common.FilePanelItemSelectedStyle.Render(truncateTextBeginning(name, width, "..."))
FilePanelItemSelectedStyle.Render(TruncateTextBeginning(name, width, "..."))
}
return common.StringColorRender(lipgloss.Color(style.Color), common.FooterBGColor).
Background(common.FooterBGColor).
return StringColorRender(lipgloss.Color(style.Color), FooterBGColor).
Background(FooterBGColor).
Render(style.Icon+" ") +
common.FilePanelStyle.Render(truncateTextBeginning(name, width, "..."))
FilePanelStyle.Render(TruncateTextBeginning(name, width, "..."))
}

func fileNameWithoutExtension(fileName string) string {
func FileNameWithoutExtension(fileName string) string {
for {
pos := strings.LastIndexByte(fileName, '.')
if pos <= 0 {
Expand All @@ -107,7 +105,7 @@ func fileNameWithoutExtension(fileName string) string {
return fileName
}

func formatFileSize(size int64) string {
func FormatFileSize(size int64) string {
if size == 0 {
return "0B"
}
Expand All @@ -116,7 +114,7 @@ func formatFileSize(size int64) string {
unitsBin := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}

// Todo : Remove duplication here
if common.Config.FileSizeUseSI {
if Config.FileSizeUseSI {
unitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(1000)))
adjustedSize := float64(size) / math.Pow(1000, float64(unitIndex))
return fmt.Sprintf("%.2f %s", adjustedSize, unitsDec[unitIndex])
Expand All @@ -127,7 +125,7 @@ func formatFileSize(size int64) string {
}

// Truncate line lengths and keep ANSI
func checkAndTruncateLineLengths(text string, maxLength int) string {
func CheckAndTruncateLineLengths(text string, maxLength int) string {
lines := strings.Split(text, "\n")
var result strings.Builder

Expand All @@ -144,7 +142,7 @@ func checkAndTruncateLineLengths(text string, maxLength int) string {
}

// Separated this out out for easy testing
func isBufferPrintable(buffer []byte) bool {
func IsBufferPrintable(buffer []byte) bool {
for _, b := range buffer {
// This will also handle b==0
if !unicode.IsPrint(rune(b)) && !unicode.IsSpace(rune(b)) {
Expand All @@ -154,8 +152,8 @@ func isBufferPrintable(buffer []byte) bool {
return true
}

// isExensionExtractable checks if a string is a valid compressed archive file extension.
func isExensionExtractable(ext string) bool {
// IsExtensionExtractable checks if a string is a valid compressed archive file extension.
func IsExtensionExtractable(ext string) bool {
// Extensions based on the types that package: `xtractr` `ExtractFile` function handles.
validExtensions := map[string]struct{}{
".zip": {},
Expand All @@ -173,7 +171,7 @@ func isExensionExtractable(ext string) bool {
}

// Check file is text file or not
func isTextFile(filename string) (bool, error) {
func IsTextFile(filename string) (bool, error) {
file, err := os.Open(filename)
if err != nil {
return false, err
Expand All @@ -186,14 +184,14 @@ func isTextFile(filename string) (bool, error) {
if err != nil && !errors.Is(err, io.EOF) {
return false, err
}
return isBufferPrintable(buffer[:cnt]), nil
return IsBufferPrintable(buffer[:cnt]), nil
}

// Although some characters like `\x0b`(vertical tab) are printable,
// previewing them breaks the layout.
// So, among the "non-graphic" printable characters, we only need \n and \t
// Space and NBSP are already considered graphic by unicode.
func makePrintable(line string) string {
func MakePrintable(line string) string {
var sb strings.Builder
// This has to be looped byte-wise, looping it rune-wise
// or by using strings.Map would cause issues with strings like
Expand Down
Loading
Loading