diff --git a/.golangci.yaml b/.golangci.yaml index 9143d9b5..615de58d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -128,7 +128,6 @@ linters-settings: # Ignore comments when counting lines. # Default false ignore-comments: true - gochecksumtype: # Presence of `default` case in switch statements satisfies exhaustiveness, if all members are not listed. # Default: true @@ -276,8 +275,7 @@ linters: - dupl # tool for code clone detection - durationcheck # checks for two durations multiplied together - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error - # Todo enable - #- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 - exhaustive # checks exhaustiveness of enum switch statements - exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions - fatcontext # detects nested contexts in loops @@ -286,18 +284,16 @@ linters: #- funlen # tool for detection of long functions - gocheckcompilerdirectives # validates go compiler directive comments (//go:) # Todo enable - This is too much for us right now - # - gochecknoglobals # checks that no global variables exist + - gochecknoglobals # checks that no global variables exist - gochecknoinits # checks that no init functions are present in Go code - gochecksumtype # checks exhaustiveness on Go "sum types" # Todo enable - Need to refactor many functions # - gocognit # computes and checks the cognitive complexity of functions - goconst # finds repeated strings that could be replaced by a constant - # Todo enable - Some few changes. There are good suggestions - # - gocritic # provides diagnostics that check for bugs, performance and style issues + - gocritic # provides diagnostics that check for bugs, performance and style issues # Todo enable #- gocyclo # computes and checks the cyclomatic complexity of functions - # Todo enable - too much for us - #- godot # checks if comments end in a period + #- godot # checks if comments end in a period # Too much for us right now - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod - goprintffuncname # checks that printf-like functions are named with f at the end @@ -305,8 +301,7 @@ linters: # weak md5 cryptography, Subprocess launched with a potential tainted input # - gosec # inspects source code for security problems - iface # checks the incorrect use of interfaces, helping developers avoid interface pollution - # Todo enable : Need fixes - # - intrange # finds places where for loops could make use of an integer range + - intrange # finds places where for loops could make use of an integer range # Todo enable : Need many fixes # - lll # reports long lines - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) @@ -323,39 +318,32 @@ linters: - nilnil # checks that there is no simultaneous return of nil error and an invalid value - noctx # finds sending http request without context.Context - nolintlint # reports ill-formed or insufficient nolint directives - # Todo enable - maybe too strict for us - # - nonamedreturns # reports all named returns + - nonamedreturns # reports all named returns - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative - predeclared # finds code that shadows one of Go's predeclared identifiers - promlinter # checks Prometheus metrics naming via promlint - protogetter # reports direct reads from proto message fields when getters should be used - # Todo enable - Some hard to fix things - # - reassign # checks that package variables are not reassigned + - reassign # checks that package variables are not reassigned # Todo enable - Issue due to model struct # - recvcheck # checks for receiver type consistency - # Todo enable : Many changes - # - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint - rowserrcheck # checks whether Err of rows is checked successfully # Todo enable - Need to use non-root logger # - sloglint # ensure consistent code style when using log/slog - spancheck # checks for mistakes with OpenTelemetry/Census spans - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - # Todo enable : Many changes, but good changes. - # - stylecheck # is a replacement for golint + - stylecheck # is a replacement for golint - testableexamples # checks if examples are testable (have an expected output) - testifylint # checks usage of github.com/stretchr/testify - # Todo enable - Might be too strict for us - # - testpackage # makes you use a separate _test package + # - testpackage # makes you use a separate _test package # Not needed - Makes it harder to test unexported package functions. - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # removes unnecessary type conversions - unparam # reports unused function parameters - usestdlibvars # detects the possibility to use variables/constants from the Go standard library - usetesting # reports uses of functions with replacement inside the testing package - # Todo enable - many required fixes - # - wastedassign # finds wasted assignment statements - # Todo enable - many fixes - # - whitespace # detects leading and trailing whitespace + - wastedassign # finds wasted assignment statements + - whitespace # detects leading and trailing whitespace ## Todo : Enable some of them ## you may want to enable @@ -390,13 +378,12 @@ linters: # - dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) # False positives # - dupword # [useless without config] checks for duplicate words in the source code - # Todo : It gave some useful errors. Should fix - # - err113 # [too strict] checks the errors handling expressions + + # It gave some useful errors optimization. Should fix eventually. But this is low priority + # - err113 # [too strict] checks the errors handling expressions # # - 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 # - forcetypeassert # [replaced by errcheck] finds forced type assertions - # Todo : rely on this, and skip separate go fmt check in cicd # - gofmt # [replaced by goimports] checks whether code was gofmt-ed - # Todo : Check this #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed #- gomodguard # [use more powerful depguard] allow and block lists linter for direct Go module dependencies - gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase @@ -405,14 +392,34 @@ linters: - maintidx # measures the maintainability index of each function - misspell # [useless] finds commonly misspelled English words in comments # - nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity - # Todo : might make tests parallel #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test - # Todo : Check this. There are some issues to be fixed # - tagliatelle # checks the struct tags # - tenv # [deprecated, replaced by usetesting] detects using os.Setenv instead of t.Setenv since Go1.17 #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers # - wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines - + exclusions: + rules: + # Stores globally accessible icon strings + - path: 'src/config/icon/icon.go' + linters: + - gochecknoglobals + # Stores prerendered styles + - path: 'src/internal/common/style.go' + linters: + - gochecknoglobals + # Some fixed variables like default config paths, version, etc. + - path: 'src/config/fixed_variable.go' + linters: + - gochecknoglobals + # Stores predefined variables, like re-used non-const strings + - path: 'src/internal/common/predefined_variable.go' + linters: + - gochecknoglobals + # Global variables storing config + - path: 'src/internal/common/default_config.go' + linters: + - gochecknoglobals + issues: # Maximum count of issues with the same text. diff --git a/src/cmd/main.go b/src/cmd/main.go index 6f95dd84..3ea6800b 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "log" "log/slog" "net/http" "os" @@ -26,11 +25,10 @@ import ( // Run superfile app func Run(content embed.FS) { - // Before we open log file, set all "non debug" logs to stdout utils.SetRootLoggerToStdout(false) - common.LoadInitial_PrerenderedVariables() + common.LoadInitialPrerenderedVariables() common.LoadAllDefaultConfig(content) app := &cli.App{ @@ -43,7 +41,7 @@ func Run(content embed.FS) { Name: "path-list", Aliases: []string{"pl"}, Usage: "Print the path to the configuration and directory", - Action: func(c *cli.Context) error { + Action: func(_ *cli.Context) error { fmt.Printf("%-*s %s\n", 55, lipgloss.NewStyle().Foreground(lipgloss.Color("#66b2ff")).Render("[Configuration file path]"), variable.ConfigFile) fmt.Printf("%-*s %s\n", 55, lipgloss.NewStyle().Foreground(lipgloss.Color("#ffcc66")).Render("[Hotkeys file path]"), variable.HotkeysFile) fmt.Printf("%-*s %s\n", 55, lipgloss.NewStyle().Foreground(lipgloss.Color("#66ff66")).Render("[Log file path]"), variable.LogFile) @@ -86,32 +84,12 @@ func Run(content embed.FS) { }, Action: func(c *cli.Context) error { // If no args are called along with "spf" use current dir - path := "" + firstFilePanelDirs := []string{""} if c.Args().Present() { - path = c.Args().First() - } - - // Setting the config file path - configFileArg := c.String("config-file") - - // Validate the config file exists - if configFileArg != "" { - if _, err := os.Stat(configFileArg); err != nil { - log.Fatalf("Error: While reading config file '%s' from argument : %v", configFileArg, err) - } else { - variable.ConfigFile = configFileArg - } + firstFilePanelDirs = c.Args().Slice() } - hotkeyFileArg := c.String("hotkey-file") - - if hotkeyFileArg != "" { - if _, err := os.Stat(hotkeyFileArg); err != nil { - log.Fatalf("Error: While reading hotkey file '%s' from argument : %v", hotkeyFileArg, err) - } else { - variable.HotkeysFile = hotkeyFileArg - } - } + variable.UpdateVarFromCliArgs(c) InitConfigFile() @@ -120,15 +98,11 @@ func Run(content embed.FS) { hasTrash = false } - variable.FixHotkeys = c.Bool("fix-hotkeys") - variable.FixConfigFile = c.Bool("fix-config-file") - variable.PrintLastDir = c.Bool("print-last-dir") - firstUse := checkFirstUse() - p := tea.NewProgram(internal.InitialModel(path, firstUse, hasTrash), tea.WithAltScreen(), tea.WithMouseCellMotion()) + p := tea.NewProgram(internal.InitialModel(firstFilePanelDirs, firstUse, hasTrash), tea.WithAltScreen(), tea.WithMouseCellMotion()) if _, err := p.Run(); err != nil { - log.Fatalf("Alas, there's been an error: %v", err) + utils.PrintfAndExit("Alas, there's been an error: %v", err) } // This must be after calling internal.InitialModel() @@ -147,7 +121,7 @@ func Run(content embed.FS) { err := app.Run(os.Args) if err != nil { - log.Fatalln(err) + utils.PrintlnAndExit(err) } } @@ -161,7 +135,7 @@ func InitConfigFile() { variable.SuperFileStateDir, variable.ThemeFolder, ); err != nil { - log.Fatalln("Error creating directories:", err) + utils.PrintlnAndExit("Error creating directories:", err) } // Create files @@ -171,27 +145,27 @@ func InitConfigFile() { variable.ThemeFileVersion, variable.ToggleFooter, ); err != nil { - log.Fatalln("Error creating files:", err) + utils.PrintlnAndExit("Error creating files:", err) } // Write config file if err := writeConfigFile(variable.ConfigFile, common.ConfigTomlString); err != nil { - log.Fatalln("Error writing config file:", err) + utils.PrintlnAndExit("Error writing config file:", err) } if err := writeConfigFile(variable.HotkeysFile, common.HotkeysTomlString); err != nil { - log.Fatalln("Error writing config file:", err) + utils.PrintlnAndExit("Error writing config file:", err) } - if err := initJsonFile(variable.PinnedFile); err != nil { - log.Fatalln("Error initializing json file:", err) + if err := initJSONFile(variable.PinnedFile); err != nil { + utils.PrintlnAndExit("Error initializing json file:", err) } } // We are initializing these, but not sure if we are ever using them func InitTrash() error { // Create trash directories - if runtime.GOOS != variable.OS_DARWIN { + if runtime.GOOS != utils.OsDarwin { err := createDirectories( variable.CustomTrashDirectory, variable.CustomTrashDirectoryFiles, @@ -240,7 +214,7 @@ func checkFirstUse() bool { if _, err := os.Stat(file); os.IsNotExist(err) { firstUse = true if err = os.WriteFile(file, nil, 0644); err != nil { - log.Fatalf("Failed to create file: %v", err) + utils.PrintfAndExit("Failed to create file: %v", err) } } return firstUse @@ -256,7 +230,7 @@ func writeConfigFile(path, data string) error { return nil } -func initJsonFile(path string) error { +func initJSONFile(path string) error { if _, err := os.Stat(path); os.IsNotExist(err) { if err = os.WriteFile(path, []byte("null"), 0644); err != nil { return fmt.Errorf("failed to initialize json file %s: %w", path, err) diff --git a/src/config/fixed_variable.go b/src/config/fixed_variable.go index 1ebc977a..ad72e975 100644 --- a/src/config/fixed_variable.go +++ b/src/config/fixed_variable.go @@ -1,63 +1,97 @@ package variable import ( + "os" "path/filepath" + "github.com/urfave/cli/v2" + "github.com/yorukot/superfile/src/internal/common/utils" + "github.com/adrg/xdg" ) const ( - CurrentVersion string = "v1.2.1" - LatestVersionURL string = "https://api.github.com/repos/yorukot/superfile/releases/latest" - LatestVersionGithub string = "github.com/yorukot/superfile/releases/latest" + CurrentVersion = "v1.2.1" + LatestVersionURL = "https://api.github.com/repos/yorukot/superfile/releases/latest" + LatestVersionGithub = "github.com/yorukot/superfile/releases/latest" // This will not break in windows. This is a relative path for Embed FS. It uses "/" only - EmbedConfigDir string = "src/superfile_config" - EmbedConfigFile string = EmbedConfigDir + "/config.toml" - EmbedHotkeysFile string = EmbedConfigDir + "/hotkeys.toml" - EmbedThemeDir string = EmbedConfigDir + "/theme" - EmbedThemeCatppuccinFile string = EmbedThemeDir + "/catppuccin.toml" - - // These are used while comparing with runtime.GOOS - // OS_WINDOWS represents the Windows operating system identifier - OS_WINDOWS = "windows" - // OS_DARWIN represents the macOS (Darwin) operating system identifier - OS_DARWIN = "darwin" + EmbedConfigDir = "src/superfile_config" + EmbedConfigFile = EmbedConfigDir + "/config.toml" + EmbedHotkeysFile = EmbedConfigDir + "/hotkeys.toml" + EmbedThemeDir = EmbedConfigDir + "/theme" + EmbedThemeCatppuccinFile = EmbedThemeDir + "/catppuccin.toml" ) var ( - HomeDir string = xdg.Home - SuperFileMainDir string = filepath.Join(xdg.ConfigHome, "superfile") - SuperFileCacheDir string = filepath.Join(xdg.CacheHome, "superfile") - SuperFileDataDir string = filepath.Join(xdg.DataHome, "superfile") - SuperFileStateDir string = filepath.Join(xdg.StateHome, "superfile") + HomeDir = xdg.Home + SuperFileMainDir = filepath.Join(xdg.ConfigHome, "superfile") + SuperFileCacheDir = filepath.Join(xdg.CacheHome, "superfile") + SuperFileDataDir = filepath.Join(xdg.DataHome, "superfile") + SuperFileStateDir = filepath.Join(xdg.StateHome, "superfile") // MainDir files - ThemeFolder string = filepath.Join(SuperFileMainDir, "theme") - ConfigFile string = filepath.Join(SuperFileMainDir, "config.toml") - HotkeysFile string = filepath.Join(SuperFileMainDir, "hotkeys.toml") + ThemeFolder = filepath.Join(SuperFileMainDir, "theme") // DataDir files - LastCheckVersion string = filepath.Join(SuperFileDataDir, "lastCheckVersion") - ThemeFileVersion string = filepath.Join(SuperFileDataDir, "themeFileVersion") - FirstUseCheck string = filepath.Join(SuperFileDataDir, "firstUseCheck") - PinnedFile string = filepath.Join(SuperFileDataDir, "pinned.json") - ToggleDotFile string = filepath.Join(SuperFileDataDir, "toggleDotFile") - ToggleFooter string = filepath.Join(SuperFileDataDir, "toggleFooter") + LastCheckVersion = filepath.Join(SuperFileDataDir, "lastCheckVersion") + ThemeFileVersion = filepath.Join(SuperFileDataDir, "themeFileVersion") + FirstUseCheck = filepath.Join(SuperFileDataDir, "firstUseCheck") + PinnedFile = filepath.Join(SuperFileDataDir, "pinned.json") + ToggleDotFile = filepath.Join(SuperFileDataDir, "toggleDotFile") + ToggleFooter = filepath.Join(SuperFileDataDir, "toggleFooter") // StateDir files - LogFile string = filepath.Join(SuperFileStateDir, "superfile.log") - LastDirFile string = filepath.Join(SuperFileStateDir, "lastdir") + LogFile = filepath.Join(SuperFileStateDir, "superfile.log") + LastDirFile = filepath.Join(SuperFileStateDir, "lastdir") // Trash Directories - DarwinTrashDirectory string = filepath.Join(HomeDir, ".Trash") - CustomTrashDirectory string = filepath.Join(xdg.DataHome, "Trash") - CustomTrashDirectoryFiles string = filepath.Join(xdg.DataHome, "Trash", "files") - CustomTrashDirectoryInfo string = filepath.Join(xdg.DataHome, "Trash", "info") + DarwinTrashDirectory = filepath.Join(HomeDir, ".Trash") + CustomTrashDirectory = filepath.Join(xdg.DataHome, "Trash") + CustomTrashDirectoryFiles = filepath.Join(xdg.DataHome, "Trash", "files") + CustomTrashDirectoryInfo = filepath.Join(xdg.DataHome, "Trash", "info") +) + +// These variables are actually not fixed, they are sometimes updated dynamically +var ( + ConfigFile = filepath.Join(SuperFileMainDir, "config.toml") + HotkeysFile = filepath.Join(SuperFileMainDir, "hotkeys.toml") // Other state variables - FixHotkeys bool = false - FixConfigFile bool = false - LastDir string = "" - PrintLastDir bool = false + FixHotkeys = false + FixConfigFile = false + LastDir = "" + PrintLastDir = false ) + +// Still we are preventing other packages to directly modify them via reassign linter + +func SetLastDir(path string) { + LastDir = path +} + +func UpdateVarFromCliArgs(c *cli.Context) { + // Setting the config file path + configFileArg := c.String("config-file") + + // Validate the config file exists + if configFileArg != "" { + if _, err := os.Stat(configFileArg); err != nil { + utils.PrintfAndExit("Error: While reading config file '%s' from argument : %v", configFileArg, err) + } + ConfigFile = configFileArg + } + + hotkeyFileArg := c.String("hotkey-file") + + if hotkeyFileArg != "" { + if _, err := os.Stat(hotkeyFileArg); err != nil { + utils.PrintfAndExit("Error: While reading hotkey file '%s' from argument : %v", hotkeyFileArg, err) + } + HotkeysFile = hotkeyFileArg + } + + FixHotkeys = c.Bool("fix-hotkeys") + FixConfigFile = c.Bool("fix-config-file") + PrintLastDir = c.Bool("print-last-dir") +} diff --git a/src/config/icon/function.go b/src/config/icon/function.go index 3d5e66dd..6cb118e4 100644 --- a/src/config/icon/function.go +++ b/src/config/icon/function.go @@ -38,7 +38,7 @@ func InitIcon(nerdfont bool, directoryIconColor string) { if directoryIconColor == "" { directoryIconColor = "NONE" // Dark yellowish } - Folders["folder"] = IconStyle{ + Folders["folder"] = Style{ Icon: "\uf07b", // Printable Rune : "" Color: directoryIconColor, } diff --git a/src/config/icon/icon.go b/src/config/icon/icon.go index c503b4ff..eb67cd1e 100644 --- a/src/config/icon/icon.go +++ b/src/config/icon/icon.go @@ -1,45 +1,45 @@ package icon // Style for icons -type IconStyle struct { +type Style struct { Icon string Color string } var ( - Space string = " " - SuperfileIcon string = "\ue6ad" // Printable Rune : "" + Space = " " + SuperfileIcon = "\ue6ad" // Printable Rune : "" // Well Known Directories - Home string = "\U000f02dc" // Printable Rune : "󰋜" - Download string = "\U000f03d4" // Printable Rune : "󰏔" - Documents string = "\U000f0219" // Printable Rune : "󰈙" - Pictures string = "\U000f02e9" // Printable Rune : "󰋩" - Videos string = "\U000f0381" // Printable Rune : "󰎁" - Music string = "♬" // Printable Rune : "♬" - Templates string = "\U000f03e2" // Printable Rune : "󰏢" - PublicShare string = "\uf0ac" // Printable Rune : "" + Home = "\U000f02dc" // Printable Rune : "󰋜" + Download = "\U000f03d4" // Printable Rune : "󰏔" + Documents = "\U000f0219" // Printable Rune : "󰈙" + Pictures = "\U000f02e9" // Printable Rune : "󰋩" + Videos = "\U000f0381" // Printable Rune : "󰎁" + Music = "♬" // Printable Rune : "♬" + Templates = "\U000f03e2" // Printable Rune : "󰏢" + PublicShare = "\uf0ac" // Printable Rune : "" // file operations - CompressFile string = "\U000f05c4" // Printable Rune : "󰗄" - ExtractFile string = "\U000f06eb" // Printable Rune : "󰛫" - Copy string = "\U000f018f" // Printable Rune : "󰆏" - Cut string = "\U000f0190" // Printable Rune : "󰆐" - Delete string = "\U000f01b4" // Printable Rune : "󰆴" + CompressFile = "\U000f05c4" // Printable Rune : "󰗄" + ExtractFile = "\U000f06eb" // Printable Rune : "󰛫" + Copy = "\U000f018f" // Printable Rune : "󰆏" + Cut = "\U000f0190" // Printable Rune : "󰆐" + Delete = "\U000f01b4" // Printable Rune : "󰆴" // other - Cursor string = "\uf054" // Printable Rune : "" - Browser string = "\U000f0208" // Printable Rune : "󰈈" - Select string = "\U000f01bd" // Printable Rune : "󰆽" - Error string = "\uf530" // Printable Rune : "" - Warn string = "\uf071" // Printable Rune : "" - Done string = "\uf4a4" // Printable Rune : "" - InOperation string = "\U000f0954" // Printable Rune : "󰥔" - Directory string = "\uf07b" // Printable Rune : "" - Search string = "\ue68f" // Printable Rune : "" - SortAsc string = "\uf0de" // Printable Rune : "" - SortDesc string = "\uf0dd" // Printable Rune : "" - Terminal string = "\ue795" // Printable Rune : "" + Cursor = "\uf054" // Printable Rune : "" + Browser = "\U000f0208" // Printable Rune : "󰈈" + Select = "\U000f01bd" // Printable Rune : "󰆽" + Error = "\uf530" // Printable Rune : "" + Warn = "\uf071" // Printable Rune : "" + Done = "\uf4a4" // Printable Rune : "" + InOperation = "\U000f0954" // Printable Rune : "󰥔" + Directory = "\uf07b" // Printable Rune : "" + Search = "\ue68f" // Printable Rune : "" + SortAsc = "\uf0de" // Printable Rune : "" + SortDesc = "\uf0dd" // Printable Rune : "" + Terminal = "\ue795" // Printable Rune : "" ) /* @@ -47,7 +47,7 @@ THESE CODE BASE ON https://github.com/acarl005/ls-go thanks for the great work!! */ -var Icons = map[string]IconStyle{ +var Icons = map[string]Style{ "ai": { Icon: "\ue669", // Printable Rune : "" Color: "#ce6f14", @@ -394,7 +394,7 @@ var Aliases = map[string]string{ "z": "zip", } -var Folders = map[string]IconStyle{ +var Folders = map[string]Style{ ".atom": {Icon: "\ue764", Color: "#66595c"}, // Atom folder - Dark gray // Printable Rune : "" ".aws": {Icon: "\ue7ad", Color: "#ff9900"}, // AWS folder - Orange // Printable Rune : "" ".docker": {Icon: "\ue7b0", Color: "#0db7ed"}, // Docker folder - Blue // Printable Rune : "" diff --git a/src/internal/common/load_config.go b/src/internal/common/load_config.go index 272d5b9b..c2d88279 100644 --- a/src/internal/common/load_config.go +++ b/src/internal/common/load_config.go @@ -27,9 +27,8 @@ func LoadConfigFile() { if err := ValidateConfig(&Config); err != nil { // If config is incorrect we cannot continue. We need to exit - utils.LogAndExit(err.Error()) + utils.PrintlnAndExit(err.Error()) } - } func ValidateConfig(c *ConfigType) error { @@ -58,7 +57,7 @@ func LoadHotkeysFile() { // Validate hotkey values val := reflect.ValueOf(Hotkeys) - for i := 0; i < val.NumField(); i++ { + for i := range val.NumField() { field := val.Type().Field(i) value := val.Field(i) @@ -66,12 +65,12 @@ func LoadHotkeysFile() { // This adds a layer against accidental struct modifications // Makes sure its always be a string slice. It's somewhat like a unit test if value.Kind() != reflect.Slice || value.Type().Elem().Kind() != reflect.String { - utils.LogAndExit(LoadHotkeysError(field.Name)) + utils.PrintlnAndExit(LoadHotkeysError(field.Name)) } hotkeysList, ok := value.Interface().([]string) if !ok || len(hotkeysList) == 0 || hotkeysList[0] == "" { - utils.LogAndExit(LoadHotkeysError(field.Name)) + utils.PrintlnAndExit(LoadHotkeysError(field.Name)) } } } @@ -82,26 +81,25 @@ func LoadThemeFile() { themeFile := filepath.Join(variable.ThemeFolder, Config.Theme+".toml") data, err := os.ReadFile(themeFile) if err == nil { - if unmarshalErr := toml.Unmarshal(data, &Theme); unmarshalErr == nil { + unmarshalErr := toml.Unmarshal(data, &Theme) + if unmarshalErr == nil { return - } else { - slog.Error("Could not unmarshal theme file. Falling back to default theme", - "unmarshalErr", unmarshalErr) } + slog.Error("Could not unmarshal theme file. Falling back to default theme", + "unmarshalErr", unmarshalErr) } else { slog.Error("Could not read user's theme file. Falling back to default theme", "path", themeFile, "error", err) } err = toml.Unmarshal([]byte(DefaultThemeString), &Theme) if err != nil { - utils.LogAndExit("Unexpected error while reading default theme file. Exiting...", "error", err) + utils.PrintfAndExit("Unexpected error while reading default theme file : %v. Exiting...", err) } } // LoadAllDefaultConfig : Load all default configurations from embedded superfile_config folder into global // configurations variables and write theme files if its needed. func LoadAllDefaultConfig(content embed.FS) { - err := LoadConfigStringGlobals(content) if err != nil { slog.Error("Could not load default config from embed FS", "error", err) diff --git a/src/internal/common/predefined_variable.go b/src/internal/common/predefined_variable.go index 0d7601b8..bc103f3f 100644 --- a/src/internal/common/predefined_variable.go +++ b/src/internal/common/predefined_variable.go @@ -31,7 +31,7 @@ var ( ) // No dependencies -func LoadInitial_PrerenderedVariables() { +func LoadInitialPrerenderedVariables() { LipglossError = lipgloss.NewStyle().Foreground(lipgloss.Color("#F93939")).Render("Error") + lipgloss.NewStyle().Foreground(lipgloss.Color("#00FFEE")).Render(" ┃ ") } diff --git a/src/internal/common/style.go b/src/internal/common/style.go index e4708368..375911a8 100644 --- a/src/internal/common/style.go +++ b/src/internal/common/style.go @@ -215,7 +215,6 @@ func LoadThemeConfig() { } func TransparentAllBackgroundColor() { - if SidebarBGColor == sidebarItemSelectedBGColor { sidebarItemSelectedBGColor = lipgloss.Color(TransparentBackgroundColor) } diff --git a/src/internal/common/style_function.go b/src/internal/common/style_function.go index eed9d008..9d7e531e 100644 --- a/src/internal/common/style_function.go +++ b/src/internal/common/style_function.go @@ -15,7 +15,8 @@ func FilePanelBorderStyle(height int, width int, filePanelFocussed bool, borderB border := GenerateBorder() border.Left = "" border.Right = "" - for i := 0; i < height; i++ { + + for i := range height { if i == 1 { border.Left += Config.BorderMiddleLeft border.Right += Config.BorderMiddleRight diff --git a/src/internal/common/utils/bool_file_store.go b/src/internal/common/utils/bool_file_store.go index 6a4bfb5d..493bd47c 100644 --- a/src/internal/common/utils/bool_file_store.go +++ b/src/internal/common/utils/bool_file_store.go @@ -21,9 +21,9 @@ func ReadBoolFile(path string, defaultValue bool) bool { // consistent behavior and prevents issues with case-insensitivity or // unexpected values like "yes", "on", etc. that ParseBool would accept switch string(data) { - case TRUE_STRING: + case TrueString: return true - case FALSE_STRING: + case FalseString: return false default: return defaultValue @@ -32,5 +32,4 @@ func ReadBoolFile(path string, defaultValue bool) bool { func WriteBoolFile(path string, value bool) error { return os.WriteFile(path, []byte(strconv.FormatBool(value)), 0644) - } diff --git a/src/internal/common/utils/bool_file_store_test.go b/src/internal/common/utils/bool_file_store_test.go index 279d7c4f..d52309b3 100644 --- a/src/internal/common/utils/bool_file_store_test.go +++ b/src/internal/common/utils/bool_file_store_test.go @@ -6,8 +6,6 @@ import ( "runtime" "testing" - variable "github.com/yorukot/superfile/src/config" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,14 +22,14 @@ func TestReadBoolFile(t *testing.T) { }{ { name: "file contains true", - fileContent: TRUE_STRING, + fileContent: TrueString, defaultValue: false, createFile: true, expected: true, }, { name: "file contains false", - fileContent: FALSE_STRING, + fileContent: FalseString, defaultValue: true, createFile: true, expected: false, @@ -130,15 +128,15 @@ func TestWriteBoolFile(t *testing.T) { content, err := os.ReadFile(filePath) require.NoError(t, err) - expected := FALSE_STRING + expected := FalseString if tt.value { - expected = TRUE_STRING + expected = TrueString } assert.Equal(t, expected, string(content)) // Verify permissions (Unix only) - if runtime.GOOS != variable.OS_WINDOWS { + if runtime.GOOS != OsWindows { info, err := os.Stat(filePath) require.NoError(t, err) assert.Equal(t, os.FileMode(0644), info.Mode().Perm()) @@ -157,7 +155,7 @@ func TestWriteBoolFileError(t *testing.T) { func TestReadBoolFilePermissionDenied(t *testing.T) { // Skip on Windows as permission handling differs - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == OsWindows { t.Skip("Skipping permission test on Windows") } @@ -165,7 +163,7 @@ func TestReadBoolFilePermissionDenied(t *testing.T) { // Create a file filePath := filepath.Join(tempDir, "no_read_perm.txt") - err := os.WriteFile(filePath, []byte(TRUE_STRING), 0644) + err := os.WriteFile(filePath, []byte(TrueString), 0644) require.NoError(t, err) // Remove read permissions @@ -183,7 +181,7 @@ func TestReadBoolFilePermissionDenied(t *testing.T) { func TestWriteBoolFilePermissionDenied(t *testing.T) { // Skip on Windows as permission handling differs - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == OsWindows { t.Skip("Skipping permission test on Windows") } diff --git a/src/internal/common/utils/consts.go b/src/internal/common/utils/consts.go index 3b216156..2c6db235 100644 --- a/src/internal/common/utils/consts.go +++ b/src/internal/common/utils/consts.go @@ -1,6 +1,11 @@ package utils const ( - TRUE_STRING = "true" - FALSE_STRING = "false" + TrueString = "true" + FalseString = "false" + // These are used while comparing with runtime.GOOS + // OsWindows represents the Windows operating system identifier + OsWindows = "windows" + // OsDarwin represents the macOS (Darwin) operating system identifier + OsDarwin = "darwin" ) diff --git a/src/internal/common/utils/file_utils.go b/src/internal/common/utils/file_utils.go index 768ac646..03078bdf 100644 --- a/src/internal/common/utils/file_utils.go +++ b/src/internal/common/utils/file_utils.go @@ -14,26 +14,27 @@ func WriteTomlData(filePath string, data interface{}) error { tomlData, err := toml.Marshal(data) if err != nil { // return a wrapped error - return fmt.Errorf("Error encoding data : %w", err) + return fmt.Errorf("error encoding data : %w", err) } err = os.WriteFile(filePath, tomlData, 0644) if err != nil { - return fmt.Errorf("Error writing file : %w", err) + return fmt.Errorf("error writing file : %w", err) } return nil } // Helper function to load and validate TOML files with field checking // errorPrefix is appended before every error message -func LoadTomlFile(filePath string, defaultData string, target interface{}, fixFlag bool, errorPrefix string) (hasError bool) { +func LoadTomlFile(filePath string, defaultData string, target interface{}, fixFlag bool, errorPrefix string) bool { // Initialize with default config _ = toml.Unmarshal([]byte(defaultData), target) data, err := os.ReadFile(filePath) if err != nil { - LogAndExit("Config file doesn't exist", "error", err) + PrintfAndExit("Config file doesn't exist. Error : %v", err) } errMsg := "" + hasError := false // Create a map to track which fields are present // Have to do this manually as toml.Unmarshal does not return an error when it encounters a TOML key @@ -50,6 +51,7 @@ func LoadTomlFile(filePath string, defaultData string, target interface{}, fixFl if !hasError { if err = toml.Unmarshal(data, target); err != nil { hasError = true + //nolint: errorlint // Type assertion is better here, and we need to read data from error if decodeErr, ok := err.(*toml.DecodeError); ok { row, col := decodeErr.Position() errMsg = errorPrefix + fmt.Sprintf("Error in field at line %d column %d: %s\n", @@ -64,7 +66,7 @@ func LoadTomlFile(filePath string, defaultData string, target interface{}, fixFl // Check for missing fields if no errors yet targetType := reflect.TypeOf(target).Elem() - for i := 0; i < targetType.NumField(); i++ { + for i := range targetType.NumField() { field := targetType.Field(i) if _, exists := rawData[field.Tag.Get("toml")]; !exists { hasError = true @@ -89,7 +91,7 @@ func LoadTomlFile(filePath string, defaultData string, target interface{}, fixFl // Now we are fixing the file, we would not return hasError=true even if there was error // Fix the file by writing all fields if err := WriteTomlData(filePath, target); err != nil { - LogAndExit("Error while writing config file", "error", err) + PrintfAndExit("Error while writing config file : %v", err) } return false } diff --git a/src/internal/common/utils/log_utils.go b/src/internal/common/utils/log_utils.go index 41ef7017..a4af98e5 100644 --- a/src/internal/common/utils/log_utils.go +++ b/src/internal/common/utils/log_utils.go @@ -1,13 +1,24 @@ package utils import ( + "fmt" "log/slog" "os" ) -// Todo : Eventually we want to remove all such usage that can result in app exiting abruptly -func LogAndExit(msg string, values ...any) { - slog.Error(msg, values...) +// Print line to stderr and exit with status 1 +// Cannot use log.Fataln() as slog.SetDefault() causes those lines to +// go into log file +func PrintlnAndExit(args ...any) { + fmt.Fprintln(os.Stderr, args...) + os.Exit(1) +} + +// Print formatted output line to stderr and exit with status 1 +// Cannot use log.Fataln() as slog.SetDefault() causes those lines to +// go into log file +func PrintfAndExit(format string, args ...any) { + fmt.Fprintf(os.Stderr, format, args...) os.Exit(1) } diff --git a/src/internal/common/utils/shell_utils.go b/src/internal/common/utils/shell_utils.go index 86af8c6a..83600705 100644 --- a/src/internal/common/utils/shell_utils.go +++ b/src/internal/common/utils/shell_utils.go @@ -8,8 +8,6 @@ import ( "os/exec" "runtime" "time" - - variable "github.com/yorukot/superfile/src/config" ) // Choose correct shell as per OS @@ -18,7 +16,7 @@ func ExecuteCommandInShell(timeLimit time.Duration, cmdDir string, shellCommand baseCmd := "/bin/sh" args := []string{"-c", shellCommand} - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == OsWindows { baseCmd = "powershell.exe" args[0] = "-Command" } @@ -43,9 +41,7 @@ func ExecuteCommand(timeLimit time.Duration, cmdDir string, baseCmd string, args if err == nil { retCode = 0 - } else if exitErr, ok := err.(*exec.ExitError); ok { - // We dont expect error to be Wrapped here, so we are using type - // assertion not errors.As + } else if exitErr, ok := err.(*exec.ExitError); ok { //nolint: errorlint // We dont expect error to be Wrapped here, so we are using type assertion not errors.As retCode = exitErr.ExitCode() } else { err = fmt.Errorf("unexpected Error in command execution : %w", err) diff --git a/src/internal/config_function.go b/src/internal/config_function.go index 79f9c05a..1b83733f 100644 --- a/src/internal/config_function.go +++ b/src/internal/config_function.go @@ -16,8 +16,8 @@ import ( ) // initialConfig load and handle all configuration files (spf config,Hotkeys -// themes) setted up. Returns absolute path of dir pointing to the file Panel -func initialConfig(dir string) (toggleDotFile bool, toggleFooter bool, firstFilePanelDir string) { +// themes) setted up. Processes input directories and returns toggle states. +func initialConfig(firstFilePanelDirs []string) (toggleDotFile bool, toggleFooter bool) { //nolint: nonamedreturns // This is the only usecase of named returns, distinguish between multiple return values // Open log stream file, err := os.OpenFile(variable.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) @@ -25,8 +25,7 @@ func initialConfig(dir string) (toggleDotFile bool, toggleFooter bool, firstFile // For example if the log file directories have access issues. // we could pass a dummy object to log.SetOutput() and the app would still function. if err != nil { - // At this point, it will go to stdout since log file is not initilized - utils.LogAndExit("Error while opening superfile.log file", "error", err) + utils.PrintfAndExit("Error while opening superfile.log file : %v", err) } common.LoadConfigFile() @@ -44,10 +43,6 @@ func initialConfig(dir string) (toggleDotFile bool, toggleFooter bool, firstFile icon.InitIcon(common.Config.Nerdfont, common.Theme.DirectoryIconColor) - toggleDotFile = utils.ReadBoolFile(variable.ToggleDotFile, false) - - toggleFooter = utils.ReadBoolFile(variable.ToggleFooter, true) - common.LoadThemeConfig() common.LoadPrerenderedVariables() @@ -58,19 +53,29 @@ func initialConfig(dir string) (toggleDotFile bool, toggleFooter bool, firstFile } } - if dir != "" { - firstFilePanelDir, err = filepath.Abs(dir) - } else { - common.Config.DefaultDirectory = strings.Replace(common.Config.DefaultDirectory, "~", variable.HomeDir, -1) - firstFilePanelDir, err = filepath.Abs(common.Config.DefaultDirectory) - } + for i := range firstFilePanelDirs { + if firstFilePanelDirs[i] == "" { + firstFilePanelDirs[i] = common.Config.DefaultDirectory + } - if err != nil { - firstFilePanelDir = variable.HomeDir + if strings.HasPrefix(firstFilePanelDirs[i], "~") { + // We only need to replace the first ~ , not all of them + // And only if its a prefix + firstFilePanelDirs[i] = strings.Replace(firstFilePanelDirs[i], "~", variable.HomeDir, 1) + } + firstFilePanelDirs[i], err = filepath.Abs(firstFilePanelDirs[i]) + // In case of unexpected path error, fallback to home dir + if err != nil { + slog.Error("Unexpected error while calculating firstFilePanelDir", "error", err) + firstFilePanelDirs[i] = variable.HomeDir + } } slog.Debug("Runtime information", "runtime.GOOS", runtime.GOOS, - "start directory", firstFilePanelDir) + "start directories", firstFilePanelDirs) + + toggleDotFile = utils.ReadBoolFile(variable.ToggleDotFile, false) + toggleFooter = utils.ReadBoolFile(variable.ToggleFooter, true) - return toggleDotFile, toggleFooter, firstFilePanelDir + return toggleDotFile, toggleFooter } diff --git a/src/internal/default_config.go b/src/internal/default_config.go index 9241fae0..51991330 100644 --- a/src/internal/default_config.go +++ b/src/internal/default_config.go @@ -8,7 +8,7 @@ import ( ) // Generate and return model containing default configurations for interface -func defaultModelConfig(toggleDotFile bool, toggleFooter bool, firstFilePanelDir string) model { +func defaultModelConfig(toggleDotFile bool, toggleFooter bool, firstFilePanelDirs []string) model { return model{ filePanelFocusIndex: 0, focusPanel: nonePanelFocus, @@ -23,28 +23,7 @@ func defaultModelConfig(toggleDotFile bool, toggleFooter bool, firstFilePanelDir searchBar: common.GenerateSearchBar(), }, fileModel: fileModel{ - filePanels: []filePanel{ - { - render: 0, - cursor: 0, - location: firstFilePanelDir, - sortOptions: sortOptionsModel{ - width: 20, - height: 4, - open: false, - cursor: common.Config.DefaultSortType, - data: sortOptionsModelData{ - options: []string{"Name", "Size", "Date Modified"}, - selected: common.Config.DefaultSortType, - reversed: common.Config.SortOrderReversed, - }, - }, - panelMode: browserMode, - focusType: focus, - directoryRecords: make(map[string]directoryRecord), - searchBar: common.GenerateSearchBar(), - }, - }, + filePanels: filePanelSlice(firstFilePanelDirs), filePreview: filePreviewPanel{ open: common.Config.DefaultOpenFilePreview, }, diff --git a/src/internal/file_operations.go b/src/internal/file_operations.go index 33de7186..a6096f43 100644 --- a/src/internal/file_operations.go +++ b/src/internal/file_operations.go @@ -9,6 +9,8 @@ import ( "runtime" "strings" + "github.com/yorukot/superfile/src/internal/common/utils" + trash_win "github.com/hymkor/trash-go" "github.com/rkoesters/xdg/trash" variable "github.com/yorukot/superfile/src/config" @@ -20,15 +22,15 @@ func isSamePartition(path1, path2 string) (bool, error) { // Get the absolute path to handle relative paths absPath1, err := filepath.Abs(path1) if err != nil { - return false, fmt.Errorf("failed to get absolute path of the first path: %v", err) + return false, fmt.Errorf("failed to get absolute path of the first path: %w", err) } absPath2, err := filepath.Abs(path2) if err != nil { - return false, fmt.Errorf("failed to get absolute path of the second path: %v", err) + return false, fmt.Errorf("failed to get absolute path of the second path: %w", err) } - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == utils.OsWindows { // On Windows, we can check if both paths are on the same drive (same letter) drive1 := getDriveLetter(absPath1) drive2 := getDriveLetter(absPath2) @@ -51,7 +53,7 @@ func moveElement(src, dst string) error { // Check if source and destination are on the same partition sameDev, err := isSamePartition(src, dst) if err != nil { - return fmt.Errorf("failed to check partitions: %v", err) + return fmt.Errorf("failed to check partitions: %w", err) } // If on the same partition, attempt to rename (which will use the same inode) @@ -65,12 +67,12 @@ func moveElement(src, dst string) error { // If on different partitions or rename failed, fall back to copy+delete err = copyElement(src, dst) if err != nil { - return fmt.Errorf("failed to copy: %v", err) + return fmt.Errorf("failed to copy: %w", err) } err = os.RemoveAll(src) if err != nil { - return fmt.Errorf("failed to remove source after copy: %v", err) + return fmt.Errorf("failed to remove source after copy: %w", err) } return nil @@ -80,7 +82,7 @@ func moveElement(src, dst string) error { func copyElement(src, dst string) error { srcInfo, err := os.Stat(src) if err != nil { - return fmt.Errorf("failed to stat source: %v", err) + return fmt.Errorf("failed to stat source: %w", err) } if srcInfo.IsDir() { @@ -93,12 +95,12 @@ func copyElement(src, dst string) error { func copyDir(src, dst string, srcInfo os.FileInfo) error { err := os.MkdirAll(dst, srcInfo.Mode()) if err != nil { - return fmt.Errorf("failed to create destination directory: %v", err) + return fmt.Errorf("failed to create destination directory: %w", err) } entries, err := os.ReadDir(src) if err != nil { - return fmt.Errorf("failed to read source directory: %v", err) + return fmt.Errorf("failed to read source directory: %w", err) } for _, entry := range entries { @@ -107,7 +109,7 @@ func copyDir(src, dst string, srcInfo os.FileInfo) error { entryInfo, err := entry.Info() if err != nil { - return fmt.Errorf("failed to get entry info: %v", err) + return fmt.Errorf("failed to get entry info: %w", err) } if entryInfo.IsDir() { @@ -126,18 +128,18 @@ func copyDir(src, dst string, srcInfo os.FileInfo) error { func copyFile(src, dst string, srcInfo os.FileInfo) error { srcFile, err := os.Open(src) if err != nil { - return fmt.Errorf("failed to open source file: %v", err) + return fmt.Errorf("failed to open source file: %w", err) } defer srcFile.Close() dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode()) if err != nil { - return fmt.Errorf("failed to create destination file: %v", err) + return fmt.Errorf("failed to create destination file: %w", err) } defer dstFile.Close() if _, err := io.Copy(dstFile, srcFile); err != nil { - return fmt.Errorf("failed to copy file contents: %v", err) + return fmt.Errorf("failed to copy file contents: %w", err) } return nil } @@ -145,15 +147,16 @@ func copyFile(src, dst string, srcInfo os.FileInfo) error { // Move file to trash can and can auto switch macos trash can or linux trash can func trashMacOrLinux(src string) error { var err error - if runtime.GOOS == variable.OS_DARWIN { + switch runtime.GOOS { + case utils.OsDarwin: err = moveElement(src, filepath.Join(variable.DarwinTrashDirectory, filepath.Base(src))) - } else if runtime.GOOS == variable.OS_WINDOWS { + case utils.OsWindows: err = trash_win.Throw(src) - } else { + default: err = trash.Trash(src) } if err != nil { - slog.Error("Error while delete single item function move file to trash can", "error", err) + slog.Error("Error while deleting single item, in function to move file to trash can", "error", err) } return err } @@ -200,7 +203,7 @@ func pasteDir(src, dst string, id string, m *model) error { } else { p := m.processBarModel.process[id] message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: p, } @@ -248,7 +251,7 @@ func pasteDir(src, dst string, id string, m *model) error { if m.copyItems.cut && !sameDev { err = os.RemoveAll(src) if err != nil { - return fmt.Errorf("failed to remove source after move: %v", err) + return fmt.Errorf("failed to remove source after move: %w", err) } } diff --git a/src/internal/file_operations_compress.go b/src/internal/file_operations_compress.go index 6f2e4041..9889bb8a 100644 --- a/src/internal/file_operations_compress.go +++ b/src/internal/file_operations_compress.go @@ -33,7 +33,7 @@ func zipSource(source, target string) error { } message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: p, } @@ -102,7 +102,6 @@ func zipSource(source, target string) error { } p.done++ if len(channel) < 5 { - message.processNewState = p channel <- message } diff --git a/src/internal/file_operations_extract.go b/src/internal/file_operations_extract.go index c001f1c8..9d66de51 100644 --- a/src/internal/file_operations_extract.go +++ b/src/internal/file_operations_extract.go @@ -26,7 +26,7 @@ func extractCompressFile(src, dest string) error { doneTime: time.Time{}, } message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: p, } diff --git a/src/internal/function.go b/src/internal/function.go index db5b2a66..6343288f 100644 --- a/src/internal/function.go +++ b/src/internal/function.go @@ -16,8 +16,9 @@ import ( "strings" "time" + "github.com/yorukot/superfile/src/internal/common/utils" + tea "github.com/charmbracelet/bubbletea" - variable "github.com/yorukot/superfile/src/config" "github.com/yorukot/superfile/src/internal/common" "github.com/lithammer/shortuuid" @@ -36,7 +37,7 @@ func isExternalDiskPath(path string) bool { // This is very vague. You cannot tell if a path is belonging to an external partition // if you dont define the source path to compare with // But making this true will cause slow file operations based on current implementation - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == utils.OsWindows { return false } @@ -53,7 +54,7 @@ func isExternalDiskPath(path string) bool { } func shouldListDisk(mountPoint string) bool { - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == utils.OsWindows { // We need to get C:, D: drive etc in the list return true } @@ -90,7 +91,7 @@ func diskName(mountPoint string) string { // In windows we dont want to use filepath.Base as it returns "\" for when // mountPoint is any drive root "C:", "D:", etc. Hence causing same name // for each drive - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == utils.OsWindows { return mountPoint } @@ -103,7 +104,7 @@ func diskName(mountPoint string) string { func diskLocation(mountPoint string) string { // In windows if you are in "C:\some\path", "cd C:" will not cd to root of C: drive // but "cd C:\" will - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == utils.OsWindows { return filepath.Join(mountPoint, "\\") } return mountPoint @@ -116,11 +117,11 @@ func returnFocusType(focusPanel focusPanelType) filePanelFocusType { return secondFocus } -func returnDirElement(location string, displayDotFile bool, sortOptions sortOptionsModelData) (directoryElement []element) { +func returnDirElement(location string, displayDotFile bool, sortOptions sortOptionsModelData) []element { dirEntries, err := os.ReadDir(location) if err != nil { slog.Error("Error while return folder element function", "error", err) - return directoryElement + return nil } dirEntries = slices.DeleteFunc(dirEntries, func(e os.DirEntry) bool { @@ -131,7 +132,7 @@ func returnDirElement(location string, displayDotFile bool, sortOptions sortOpti // No files/directoes to process if len(dirEntries) == 0 { - return directoryElement + return nil } // Sort files @@ -148,9 +149,8 @@ func returnDirElement(location string, displayDotFile bool, sortOptions sortOpti } if common.Config.CaseSensitiveSort { return dirEntries[i].Name() < dirEntries[j].Name() != reversed - } else { - return strings.ToLower(dirEntries[i].Name()) < strings.ToLower(dirEntries[j].Name()) != reversed } + return strings.ToLower(dirEntries[i].Name()) < strings.ToLower(dirEntries[j].Name()) != reversed } case "Size": order = func(i, j int) bool { @@ -176,13 +176,11 @@ func returnDirElement(location string, displayDotFile bool, sortOptions sortOpti slog.Error("Error when reading directory during sort", "error", err) } return len(filesI) < len(filesJ) != reversed - } else { - // No need for err check, we already filtered out dirEntries with err != nil in Info() call - fileInfoI, _ := dirEntries[i].Info() - fileInfoJ, _ := dirEntries[j].Info() - return fileInfoI.Size() < fileInfoJ.Size() != reversed } - + // No need for err check, we already filtered out dirEntries with err != nil in Info() call + fileInfoI, _ := dirEntries[i].Info() + fileInfoJ, _ := dirEntries[j].Info() + return fileInfoI.Size() < fileInfoJ.Size() != reversed } case "Date Modified": order = func(i, j int) bool { @@ -194,6 +192,8 @@ func returnDirElement(location string, displayDotFile bool, sortOptions sortOpti } sort.Slice(dirEntries, order) + // Preallocate for efficiency + directoryElement := make([]element, 0, len(dirEntries)) for _, item := range dirEntries { directoryElement = append(directoryElement, element{ name: item.Name(), @@ -204,8 +204,7 @@ func returnDirElement(location string, displayDotFile bool, sortOptions sortOpti return directoryElement } -func returnDirElementBySearchString(location string, displayDotFile bool, searchString string) (dirElement []element) { - +func returnDirElementBySearchString(location string, displayDotFile bool, searchString string) []element { items, err := os.ReadDir(location) if err != nil { slog.Error("Error while return folder element function", "error", err) @@ -239,7 +238,9 @@ func returnDirElementBySearchString(location string, displayDotFile bool, search } // https://github.com/reinhrst/fzf-lib/blob/main/core.go#L43 // No sorting needed. fzf.DefaultOptions() already return values ordered on Score - for _, item := range fzfSearch(searchString, fileAndDirectories) { + fzfResults := fzfSearch(searchString, fileAndDirectories) + dirElement := make([]element, 0, len(fzfResults)) + for _, item := range fzfResults { resultItem := folderElementMap[item.Key] dirElement = append(dirElement, resultItem) } @@ -343,7 +344,7 @@ func (m *model) returnMetaData() { id := shortuuid.New() message := channelMessage{ - messageId: id, + messageID: id, messageType: sendMetadata, metadata: m.fileMetaData.metaData, } @@ -374,8 +375,8 @@ func (m *model) returnMetaData() { fileInfo, err := os.Stat(filePath) if isSymlink(filePath) { - _, symlink_err := filepath.EvalSymlinks(filePath) - if symlink_err != nil { + _, symlinkErr := filepath.EvalSymlinks(filePath) + if symlinkErr != nil { m.fileMetaData.metaData = append(m.fileMetaData.metaData, [2]string{"Link file is broken!", ""}) } else { m.fileMetaData.metaData = append(m.fileMetaData.metaData, [2]string{"This is a link file.", ""}) @@ -383,7 +384,6 @@ func (m *model) returnMetaData() { message.metadata = m.fileMetaData.metaData channel <- message return - } if err != nil { @@ -409,7 +409,6 @@ func (m *model) returnMetaData() { } if common.Config.Metadata && checkIsSymlinked.Mode()&os.ModeSymlink == 0 && et != nil { - fileInfos := et.ExtractMetadata(filePath) for _, fileInfo := range fileInfos { @@ -452,13 +451,13 @@ func (m *model) returnMetaData() { func calculateMD5Checksum(filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { - return "", fmt.Errorf("failed to open file: %v", err) + return "", fmt.Errorf("failed to open file: %w", err) } defer file.Close() hash := md5.New() if _, err := io.Copy(hash, file); err != nil { - return "", fmt.Errorf("failed to calculate MD5 checksum: %v", err) + return "", fmt.Errorf("failed to calculate MD5 checksum: %w", err) } checksum := hex.EncodeToString(hash.Sum(nil)) @@ -468,21 +467,21 @@ func calculateMD5Checksum(filePath string) (string, error) { // Get directory total size func dirSize(path string) int64 { var size int64 - // Its named walk_err to prevent shadowing - walk_err := filepath.WalkDir(path, func(_ string, entry os.DirEntry, err error) error { + // Its named walkErr to prevent shadowing + walkErr := filepath.WalkDir(path, func(_ string, entry os.DirEntry, err error) error { if err != nil { slog.Error("Dir size function error", "error", err) } if !entry.IsDir() { - info, info_err := entry.Info() - if info_err == nil { + info, infoErr := entry.Info() + if infoErr == nil { size += info.Size() } } return err }) - if walk_err != nil { - slog.Error("errors during WalkDir", "error", walk_err) + if walkErr != nil { + slog.Error("errors during WalkDir", "error", walkErr) } return size } @@ -491,7 +490,7 @@ func dirSize(path string) int64 { func countFiles(dirPath string) (int, error) { count := 0 - err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(dirPath, func(_ string, info os.FileInfo, err error) error { if err != nil { return err } @@ -506,7 +505,6 @@ func countFiles(dirPath string) (int, error) { // Check whether is symlinks func isSymlink(filePath string) bool { - fileInfo, err := os.Lstat(filePath) if err != nil { return true @@ -531,60 +529,59 @@ func isImageFile(filename string) bool { return imageExtensions[ext] } -func getElementIcon(file string, IsDir bool) icon.IconStyle { +func getElementIcon(file string, isDir bool) icon.Style { ext := strings.TrimPrefix(filepath.Ext(file), ".") name := file if !common.Config.Nerdfont { - return icon.IconStyle{ + return icon.Style{ Icon: "", Color: common.Theme.FilePanelFG, } } - if IsDir { + if isDir { resultIcon := icon.Folders["folder"] betterIcon, hasBetterIcon := icon.Folders[name] if hasBetterIcon { resultIcon = betterIcon } return resultIcon - } else { - // 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 - } + } + // 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 - } + // 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 + // 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.IconStyle{ - Icon: resultIcon.Icon, - Color: common.Theme.FilePanelFG, - } + 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: common.Theme.FilePanelFG, } - return resultIcon } + return resultIcon } // TeaUpdate : Utility to send update to model , majorly used in tests @@ -595,7 +592,6 @@ func TeaUpdate(m *model, msg tea.Msg) (tea.Cmd, error) { mObj, ok := resModel.(model) if !ok { - return cmd, fmt.Errorf("unexpected model type: %T", resModel) } *m = mObj diff --git a/src/internal/get_data.go b/src/internal/get_data.go index 5142d685..b43e8ed3 100644 --- a/src/internal/get_data.go +++ b/src/internal/get_data.go @@ -19,7 +19,12 @@ func getDirectories() []directory { } func formDirctorySlice(homeDirectories []directory, pinnedDirectories []directory, diskDirectories []directory) []directory { - directories := append(homeDirectories, pinnedDividerDir) + // Preallocation for efficiency + totalCapacity := len(homeDirectories) + len(pinnedDirectories) + len(diskDirectories) + 2 + directories := make([]directory, 0, totalCapacity) + + directories = append(directories, homeDirectories...) + directories = append(directories, pinnedDividerDir) directories = append(directories, pinnedDirectories...) directories = append(directories, diskDividerDir) directories = append(directories, diskDirectories...) @@ -81,25 +86,19 @@ func getPinnedDirectories() []directory { } // Get external media directories -func getExternalMediaFolders() (disks []directory) { +func getExternalMediaFolders() []directory { // only get physical drives parts, err := disk.Partitions(false) if err != nil { slog.Error("Error while getting external media: ", "error", err) - return disks + return nil } - + var disks []directory for _, disk := range parts { - // Todo : We need to evaluate if more debug logs are a performance problem - // even when user had set debug=false in config. We dont write those to log file - // But we do make a functions call, and pass around some strings. So it might/might not be - // a problem. It could be a problem in a hot path though. - // shouldListDisk, diskName, and diskLocation, each has runtime.GOOS checks // We can ideally reduce it to one check only. if shouldListDisk(disk.Mountpoint) { - disks = append(disks, directory{ name: diskName(disk.Mountpoint), location: diskLocation(disk.Mountpoint), diff --git a/src/internal/handle_file_operations.go b/src/internal/handle_file_operations.go index 8dfece9b..034f1524 100644 --- a/src/internal/handle_file_operations.go +++ b/src/internal/handle_file_operations.go @@ -11,7 +11,8 @@ import ( "strings" "time" - variable "github.com/yorukot/superfile/src/config" + "github.com/yorukot/superfile/src/internal/common/utils" + "github.com/yorukot/superfile/src/internal/common" "github.com/atotto/clipboard" @@ -55,7 +56,7 @@ func (m *model) IsRenamingConflicting() bool { func (m *model) warnModalForRenaming() { id := shortuuid.New() message := channelMessage{ - messageId: id, + messageID: id, messageType: sendWarnModal, } @@ -96,7 +97,7 @@ func (m *model) deleteItemWarn() { id := shortuuid.New() message := channelMessage{ - messageId: id, + messageID: id, messageType: sendWarnModal, } @@ -109,16 +110,14 @@ func (m *model) deleteItemWarn() { } channel <- message return - } else { - message.warnModal = warnModal{ - open: true, - title: "Are you sure you want to move this to trash can", - content: "This operation will move file or directory to trash can.", - warnType: confirmDeleteItem, - } - channel <- message - return } + message.warnModal = warnModal{ + open: true, + title: "Are you sure you want to move this to trash can", + content: "This operation will move file or directory to trash can.", + warnType: confirmDeleteItem, + } + channel <- message } // Move file or directory to the trash can @@ -142,7 +141,7 @@ func (m *model) deleteSingleItem() { m.processBarModel.process[id] = newProcess message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: newProcess, } @@ -165,10 +164,8 @@ func (m *model) deleteSingleItem() { } if len(panel.element) == 0 { panel.cursor = 0 - } else { - if panel.cursor >= len(panel.element) { - panel.cursor = len(panel.element) - 1 - } + } else if panel.cursor >= len(panel.element) { + panel.cursor = len(panel.element) - 1 } } @@ -191,7 +188,7 @@ func (m *model) deleteMultipleItems() { m.processBarModel.process[id] = newProcess message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: newProcess, } @@ -199,7 +196,6 @@ func (m *model) deleteMultipleItems() { channel <- message for _, filePath := range panel.selected { - p := m.processBarModel.process[id] p.name = icon.Delete + icon.Space + filepath.Base(filePath) p.done++ @@ -217,14 +213,13 @@ func (m *model) deleteMultipleItems() { slog.Error("Error while delete multiple item function", "error", err) m.processBarModel.process[id] = p break - } else { - if p.done == p.total { - p.state = successful - message.processNewState = p - channel <- message - } - m.processBarModel.process[id] = p } + if p.done == p.total { + p.state = successful + message.processNewState = p + channel <- message + } + m.processBarModel.process[id] = p } } @@ -261,7 +256,7 @@ func (m *model) completelyDeleteSingleItem() { m.processBarModel.process[id] = newProcess message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: newProcess, } @@ -286,12 +281,11 @@ func (m *model) completelyDeleteSingleItem() { message.processNewState = p channel <- message } + // Todo : This is duplicated code fragment. Remove this duplication if len(panel.element) == 0 { panel.cursor = 0 - } else { - if panel.cursor >= len(panel.element) { - panel.cursor = len(panel.element) - 1 - } + } else if panel.cursor >= len(panel.element) { + panel.cursor = len(panel.element) - 1 } } @@ -313,14 +307,13 @@ func (m *model) completelyDeleteMultipleItems() { m.processBarModel.process[id] = newProcess message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: newProcess, } channel <- message for _, filePath := range panel.selected { - p := m.processBarModel.process[id] p.name = icon.Delete + icon.Space + filepath.Base(filePath) p.done++ @@ -341,14 +334,13 @@ func (m *model) completelyDeleteMultipleItems() { slog.Error("Error while completely delete multiple item function", "error", err) m.processBarModel.process[id] = p break - } else { - if p.done == p.total { - p.state = successful - message.processNewState = p - channel <- message - } - m.processBarModel.process[id] = p } + if p.done == p.total { + p.state = successful + message.processNewState = p + channel <- message + } + m.processBarModel.process[id] = p } } @@ -438,7 +430,7 @@ func (m *model) pasteItem() { m.processBarModel.process[id] = newProcess message := channelMessage{ - messageId: id, + messageID: id, messageType: sendProcess, processNewState: newProcess, } @@ -461,15 +453,13 @@ func (m *model) pasteItem() { if m.copyItems.cut && !isExternalDiskPath(filePath) { err = moveElement(filePath, filepath.Join(panel.location, filepath.Base(filePath))) } else { + // Todo : These error cases are hard to test. We have to somehow make the paste operations fail, + // which is time consuming and manual. We should test these with automated testcases err = pasteDir(filePath, filepath.Join(panel.location, filepath.Base(filePath)), id, m) if err != nil { errMessage = "paste item error" - } else { - // Todo : These error cases are hard to test. We have to somehow make the paste operations fail, - // which is time consuming and manual. We should test these with automated testcases - if m.copyItems.cut { - os.RemoveAll(filePath) - } + } else if m.copyItems.cut { + os.RemoveAll(filePath) } } p = m.processBarModel.process[id] @@ -579,7 +569,7 @@ func (m *model) openFileWithEditor() tea.Cmd { // Make sure there is an editor if editor == "" { - if runtime.GOOS == variable.OS_WINDOWS { + if runtime.GOOS == utils.OsWindows { editor = "notepad" } else { editor = "nano" @@ -589,6 +579,8 @@ func (m *model) openFileWithEditor() tea.Cmd { // Split the editor command into command and arguments parts := strings.Fields(editor) cmd := parts[0] + + //nolint:gocritic // appendAssign: intentionally creating a new slice args := append(parts[1:], panel.element[panel.cursor].location) c := exec.Command(cmd, args...) @@ -603,12 +595,12 @@ func (m *model) openDirectoryWithEditor() tea.Cmd { editor := common.Config.DirEditor if editor == "" { - if runtime.GOOS == variable.OS_WINDOWS { + switch runtime.GOOS { + case utils.OsWindows: editor = "explorer" - } else if runtime.GOOS == variable.OS_DARWIN { - // open is command for MacOS Finder + case utils.OsDarwin: editor = "open" - } else { + default: editor = "vi" } } @@ -616,6 +608,7 @@ func (m *model) openDirectoryWithEditor() tea.Cmd { // Split the editor command into command and arguments parts := strings.Fields(editor) cmd := parts[0] + //nolint:gocritic // appendAssign: intentionally creating a new slice args := append(parts[1:], m.fileModel.filePanels[m.filePanelFocusIndex].location) c := exec.Command(cmd, args...) diff --git a/src/internal/handle_modal.go b/src/internal/handle_modal.go index 3c9bb0fe..71ee356f 100644 --- a/src/internal/handle_modal.go +++ b/src/internal/handle_modal.go @@ -58,7 +58,6 @@ func (m *model) cancelRename() { // Connfirm rename file or directory func (m *model) confirmRename() { - panel := &m.fileModel.filePanels[m.filePanelFocusIndex] // Although we dont expect this to happen based on our current flow diff --git a/src/internal/handle_panel_movement.go b/src/internal/handle_panel_movement.go index 80c962fb..d6d7544d 100644 --- a/src/internal/handle_panel_movement.go +++ b/src/internal/handle_panel_movement.go @@ -74,14 +74,14 @@ func (m *model) enterPanel() { } if fileInfo.Mode()&os.ModeSymlink != 0 { - targetPath, symlink_err := filepath.EvalSymlinks(panel.element[panel.cursor].location) - if symlink_err != nil { + targetPath, symlinkErr := filepath.EvalSymlinks(panel.element[panel.cursor].location) + if symlinkErr != nil { return } - targetInfo, lstat_err := os.Lstat(targetPath) + targetInfo, lstatErr := os.Lstat(targetPath) - if lstat_err != nil { + if lstatErr != nil { return } @@ -93,10 +93,9 @@ func (m *model) enterPanel() { } openCommand := "xdg-open" - if runtime.GOOS == variable.OS_DARWIN { + if runtime.GOOS == utils.OsDarwin { openCommand = "open" - } else if runtime.GOOS == variable.OS_WINDOWS { - + } else if runtime.GOOS == utils.OsWindows { dllpath := filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe") dllfile := "url.dll,FileProtocolHandler" @@ -114,9 +113,7 @@ func (m *model) enterPanel() { if err != nil { slog.Error("Error while open file with", "error", err) } - } - } // Switch to the directory where the sidebar cursor is located @@ -156,7 +153,6 @@ func (m *model) selectAllItem() { // Select the item where cursor located (only work on select mode) func (panel *filePanel) singleItemSelect() { - if len(panel.element) > 0 && panel.cursor >= 0 && panel.cursor < len(panel.element) { elementLocation := panel.element[panel.cursor].location @@ -180,7 +176,6 @@ func (m *model) toggleDotFileController() { if err != nil { slog.Error("Error while updating toggleDotFile data", "error", err) } - } // Toggle dotfile display or not @@ -209,7 +204,6 @@ func (m *model) searchBarFocus() { } func (m *model) sidebarSearchBarFocus() { - if m.sidebarModel.searchBar.Focused() { // Ideally Code should never reach here. Once sidebar is focussed, we should // not cause sidebarSearchBarFocus() event by pressing search key diff --git a/src/internal/handle_panel_navigation.go b/src/internal/handle_panel_navigation.go index 09c1bd3c..80b00bfb 100644 --- a/src/internal/handle_panel_navigation.go +++ b/src/internal/handle_panel_navigation.go @@ -116,7 +116,6 @@ func (m *model) closeFilePanel() { if common.Config.FilePreviewWidth == 0 { m.fileModel.filePreview.width = (m.fullWidth - common.Config.SidebarWidth - (4 + (len(m.fileModel.filePanels))*2)) / (len(m.fileModel.filePanels) + 1) } else { - m.fileModel.filePreview.width = (m.fullWidth - common.Config.SidebarWidth) / common.Config.FilePreviewWidth } } @@ -154,7 +153,6 @@ func (m *model) toggleFilePreviewPanel() { for i := range m.fileModel.filePanels { m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - 4 } - } // Focus on next file panel diff --git a/src/internal/handle_panel_up_down_test.go b/src/internal/handle_panel_up_down_test.go index 26d0a466..0b37e2d5 100644 --- a/src/internal/handle_panel_up_down_test.go +++ b/src/internal/handle_panel_up_down_test.go @@ -10,7 +10,7 @@ import ( func genProcessBarModel(count int, cursor int, render int) processBarModel { pList := make([]string, count) pMap := map[string]process{} - for i := 0; i < count; i++ { + for i := range count { pList[i] = strconv.Itoa(i) pMap[pList[i]] = process{ name: pList[i], diff --git a/src/internal/internal_consts.go b/src/internal/internal_consts.go index 7edebfd4..c7d587fe 100644 --- a/src/internal/internal_consts.go +++ b/src/internal/internal_consts.go @@ -2,11 +2,13 @@ package internal // Todo , merge this and predefined variables file // These are effectively consts -var pinnedDividerDir = directory{ +// Had to use `var` as go doesn't allows const structs +var pinnedDividerDir = directory{ //nolint: gochecknoglobals // This is more like a const. name: "", location: "Pinned+-*/=?", } -var diskDividerDir = directory{ + +var diskDividerDir = directory{ //nolint: gochecknoglobals // This is more like a const. name: "", location: "Disks+-*/=?", } diff --git a/src/internal/key_function.go b/src/internal/key_function.go index 112a50cd..060297f7 100644 --- a/src/internal/key_function.go +++ b/src/internal/key_function.go @@ -10,31 +10,21 @@ import ( variable "github.com/yorukot/superfile/src/config" ) -// Todo : Replace usage of this with direct slices.Contains call -// This function is not required -func containsKey(v string, a []string) string { - if slices.Contains(a, v) { - return v - } - return "" -} - // mainKey handles most of key commands in the regular state of the application. For // keys that performs actions in multiple panels, like going up or down, // check the state of model m and handle properly. func (m *model) mainKey(msg string, cmd tea.Cmd) tea.Cmd { - - switch msg { - + switch { // If move up Key is pressed, check the current state and executes - case containsKey(msg, common.Hotkeys.ListUp): - if m.focusPanel == sidebarFocus { + case slices.Contains(common.Hotkeys.ListUp, msg): + switch m.focusPanel { + case sidebarFocus: m.sidebarModel.listUp(m.mainPanelHeight) - } else if m.focusPanel == processBarFocus { + case processBarFocus: m.processBarModel.listUp(m.footerHeight) - } else if m.focusPanel == metadataFocus { + case metadataFocus: m.fileMetaData.listUp() - } else if m.focusPanel == nonePanelFocus { + case nonePanelFocus: m.fileModel.filePanels[m.filePanelFocusIndex].listUp(m.mainPanelHeight) m.fileMetaData.renderIndex = 0 go func() { @@ -43,14 +33,15 @@ func (m *model) mainKey(msg string, cmd tea.Cmd) tea.Cmd { } // If move down Key is pressed, check the current state and executes - case containsKey(msg, common.Hotkeys.ListDown): - if m.focusPanel == sidebarFocus { + case slices.Contains(common.Hotkeys.ListDown, msg): + switch m.focusPanel { + case sidebarFocus: m.sidebarModel.listDown(m.mainPanelHeight) - } else if m.focusPanel == processBarFocus { + case processBarFocus: m.processBarModel.listDown(m.footerHeight) - } else if m.focusPanel == metadataFocus { + case metadataFocus: m.fileMetaData.listDown() - } else if m.focusPanel == nonePanelFocus { + case nonePanelFocus: m.fileModel.filePanels[m.filePanelFocusIndex].listDown(m.mainPanelHeight) m.fileMetaData.renderIndex = 0 go func() { @@ -58,88 +49,88 @@ func (m *model) mainKey(msg string, cmd tea.Cmd) tea.Cmd { }() } - case containsKey(msg, common.Hotkeys.PageUp): + case slices.Contains(common.Hotkeys.PageUp, msg): m.fileModel.filePanels[m.filePanelFocusIndex].pgUp(m.mainPanelHeight) - case containsKey(msg, common.Hotkeys.PageDown): + case slices.Contains(common.Hotkeys.PageDown, msg): m.fileModel.filePanels[m.filePanelFocusIndex].pgDown(m.mainPanelHeight) - case containsKey(msg, common.Hotkeys.ChangePanelMode): + case slices.Contains(common.Hotkeys.ChangePanelMode, msg): m.changeFilePanelMode() - case containsKey(msg, common.Hotkeys.NextFilePanel): + case slices.Contains(common.Hotkeys.NextFilePanel, msg): m.nextFilePanel() - case containsKey(msg, common.Hotkeys.PreviousFilePanel): + case slices.Contains(common.Hotkeys.PreviousFilePanel, msg): m.previousFilePanel() - case containsKey(msg, common.Hotkeys.CloseFilePanel): + case slices.Contains(common.Hotkeys.CloseFilePanel, msg): m.closeFilePanel() - case containsKey(msg, common.Hotkeys.CreateNewFilePanel): + case slices.Contains(common.Hotkeys.CreateNewFilePanel, msg): err := m.createNewFilePanel(variable.HomeDir) if err != nil { slog.Error("error while creating new panel", "error", err) } - case containsKey(msg, common.Hotkeys.ToggleFilePreviewPanel): + case slices.Contains(common.Hotkeys.ToggleFilePreviewPanel, msg): m.toggleFilePreviewPanel() - case containsKey(msg, common.Hotkeys.FocusOnSidebar): + case slices.Contains(common.Hotkeys.FocusOnSidebar, msg): m.focusOnSideBar() - case containsKey(msg, common.Hotkeys.FocusOnProcessBar): + case slices.Contains(common.Hotkeys.FocusOnProcessBar, msg): m.focusOnProcessBar() - case containsKey(msg, common.Hotkeys.FocusOnMetaData): + case slices.Contains(common.Hotkeys.FocusOnMetaData, msg): m.focusOnMetadata() go func() { m.returnMetaData() }() - case containsKey(msg, common.Hotkeys.PasteItems): + case slices.Contains(common.Hotkeys.PasteItems, msg): go func() { m.pasteItem() }() - case containsKey(msg, common.Hotkeys.FilePanelItemCreate): + case slices.Contains(common.Hotkeys.FilePanelItemCreate, msg): m.panelCreateNewFile() - case containsKey(msg, common.Hotkeys.PinnedDirectory): + case slices.Contains(common.Hotkeys.PinnedDirectory, msg): m.pinnedDirectory() - case containsKey(msg, common.Hotkeys.ToggleDotFile): + case slices.Contains(common.Hotkeys.ToggleDotFile, msg): m.toggleDotFileController() - case containsKey(msg, common.Hotkeys.ToggleFooter): + case slices.Contains(common.Hotkeys.ToggleFooter, msg): m.toggleFooterController() - case containsKey(msg, common.Hotkeys.ExtractFile): + case slices.Contains(common.Hotkeys.ExtractFile, msg): go func() { m.extractFile() }() - case containsKey(msg, common.Hotkeys.CompressFile): + case slices.Contains(common.Hotkeys.CompressFile, msg): go func() { m.compressFile() }() - case containsKey(msg, common.Hotkeys.OpenCommandLine): + case slices.Contains(common.Hotkeys.OpenCommandLine, msg): m.promptModal.Open(true) - case containsKey(msg, common.Hotkeys.OpenSPFPrompt): + case slices.Contains(common.Hotkeys.OpenSPFPrompt, msg): m.promptModal.Open(false) - case containsKey(msg, common.Hotkeys.OpenHelpMenu): + case slices.Contains(common.Hotkeys.OpenHelpMenu, msg): m.openHelpMenu() - case containsKey(msg, common.Hotkeys.OpenSortOptionsMenu): + case slices.Contains(common.Hotkeys.OpenSortOptionsMenu, msg): m.openSortOptionsMenu() - case containsKey(msg, common.Hotkeys.ToggleReverseSort): + case slices.Contains(common.Hotkeys.ToggleReverseSort, msg): m.toggleReverseSort() - case containsKey(msg, common.Hotkeys.OpenFileWithEditor): + case slices.Contains(common.Hotkeys.OpenFileWithEditor, msg): cmd = m.openFileWithEditor() - case containsKey(msg, common.Hotkeys.OpenCurrentDirectoryWithEditor): + case slices.Contains(common.Hotkeys.OpenCurrentDirectoryWithEditor, msg): cmd = m.openDirectoryWithEditor() default: @@ -152,85 +143,84 @@ func (m *model) mainKey(msg string, cmd tea.Cmd) tea.Cmd { func (m *model) normalAndBrowserModeKey(msg string) { // if not focus on the filepanel return if m.fileModel.filePanels[m.filePanelFocusIndex].focusType != focus { - if m.focusPanel == sidebarFocus && (msg == containsKey(msg, common.Hotkeys.Confirm)) { + if m.focusPanel == sidebarFocus && slices.Contains(common.Hotkeys.Confirm, msg) { m.sidebarSelectDirectory() } - if m.focusPanel == sidebarFocus && (msg == containsKey(msg, common.Hotkeys.FilePanelItemRename)) { + if m.focusPanel == sidebarFocus && slices.Contains(common.Hotkeys.FilePanelItemRename, msg) { m.pinnedItemRename() } - if m.focusPanel == sidebarFocus && (msg == containsKey(msg, common.Hotkeys.SearchBar)) { + if m.focusPanel == sidebarFocus && slices.Contains(common.Hotkeys.SearchBar, msg) { m.sidebarSearchBarFocus() } return } // Check if in the select mode and focusOn filepanel if m.fileModel.filePanels[m.filePanelFocusIndex].panelMode == selectMode { - switch msg { - case containsKey(msg, common.Hotkeys.Confirm): + switch { + case slices.Contains(common.Hotkeys.Confirm, msg): m.fileModel.filePanels[m.filePanelFocusIndex].singleItemSelect() - case containsKey(msg, common.Hotkeys.FilePanelSelectModeItemsSelectUp): + case slices.Contains(common.Hotkeys.FilePanelSelectModeItemsSelectUp, msg): m.fileModel.filePanels[m.filePanelFocusIndex].itemSelectUp(m.mainPanelHeight) - case containsKey(msg, common.Hotkeys.FilePanelSelectModeItemsSelectDown): + case slices.Contains(common.Hotkeys.FilePanelSelectModeItemsSelectDown, msg): m.fileModel.filePanels[m.filePanelFocusIndex].itemSelectDown(m.mainPanelHeight) - case containsKey(msg, common.Hotkeys.DeleteItems): + case slices.Contains(common.Hotkeys.DeleteItems, msg): go func() { m.deleteItemWarn() }() - case containsKey(msg, common.Hotkeys.CopyItems): + case slices.Contains(common.Hotkeys.CopyItems, msg): m.copyMultipleItem(false) - case containsKey(msg, common.Hotkeys.CutItems): + case slices.Contains(common.Hotkeys.CutItems, msg): m.copyMultipleItem(true) - case containsKey(msg, common.Hotkeys.FilePanelSelectAllItem): + case slices.Contains(common.Hotkeys.FilePanelSelectAllItem, msg): m.selectAllItem() } return } - switch msg { - case containsKey(msg, common.Hotkeys.Confirm): + switch { + case slices.Contains(common.Hotkeys.Confirm, msg): m.enterPanel() - case containsKey(msg, common.Hotkeys.ParentDirectory): + case slices.Contains(common.Hotkeys.ParentDirectory, msg): m.parentDirectory() - case containsKey(msg, common.Hotkeys.DeleteItems): + case slices.Contains(common.Hotkeys.DeleteItems, msg): go func() { m.deleteItemWarn() }() - case containsKey(msg, common.Hotkeys.CopyItems): + case slices.Contains(common.Hotkeys.CopyItems, msg): m.copySingleItem(false) - case containsKey(msg, common.Hotkeys.CutItems): + case slices.Contains(common.Hotkeys.CutItems, msg): m.copySingleItem(true) - case containsKey(msg, common.Hotkeys.FilePanelItemRename): + case slices.Contains(common.Hotkeys.FilePanelItemRename, msg): m.panelItemRename() - case containsKey(msg, common.Hotkeys.SearchBar): + case slices.Contains(common.Hotkeys.SearchBar, msg): m.searchBarFocus() - case containsKey(msg, common.Hotkeys.CopyPath): + case slices.Contains(common.Hotkeys.CopyPath, msg): m.copyPath() - case containsKey(msg, common.Hotkeys.CopyPWD): + case slices.Contains(common.Hotkeys.CopyPWD, msg): m.copyPWD() } } // Check the hotkey to cancel operation or create file func (m *model) typingModalOpenKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.CancelTyping): + switch { + case slices.Contains(common.Hotkeys.CancelTyping, msg): m.cancelTypingModal() - case containsKey(msg, common.Hotkeys.ConfirmTyping): + case slices.Contains(common.Hotkeys.ConfirmTyping, msg): m.createItem() } - } // Todo : There is a lot of duplication for these models, each one of them has to handle // ConfirmTyping and CancleTyping in a similar way. There is a scope of some good refactoring here. func (m *model) warnModalOpenKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.Quit), containsKey(msg, common.Hotkeys.CancelTyping): + switch { + case slices.Contains(common.Hotkeys.CancelTyping, msg) || slices.Contains(common.Hotkeys.Quit, msg): m.cancelWarnModal() if m.warnModal.warnType == confirmRenameItem { m.cancelRename() } - case containsKey(msg, common.Hotkeys.Confirm): + case slices.Contains(common.Hotkeys.Confirm, msg): m.warnModal.open = false switch m.warnModal.warnType { case confirmDeleteItem: @@ -257,7 +247,6 @@ func (m *model) warnModalOpenKey(msg string) { m.deleteSingleItem() }() } - } case confirmRenameItem: m.confirmRename() @@ -267,12 +256,12 @@ func (m *model) warnModalOpenKey(msg string) { // Handle key input to confirm or cancel and close quiting warn in SPF func (m *model) confirmToQuitSuperfile(msg string) bool { - switch msg { - case containsKey(msg, common.Hotkeys.Quit), containsKey(msg, common.Hotkeys.CancelTyping): + switch { + case slices.Contains(common.Hotkeys.CancelTyping, msg) || slices.Contains(common.Hotkeys.Quit, msg): m.cancelWarnModal() m.confirmToQuit = false return false - case containsKey(msg, common.Hotkeys.Confirm): + case slices.Contains(common.Hotkeys.Confirm, msg): return true default: return false @@ -281,25 +270,25 @@ func (m *model) confirmToQuitSuperfile(msg string) bool { // Handles key inputs inside sort options menu func (m *model) sortOptionsKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.OpenSortOptionsMenu): + switch { + case slices.Contains(common.Hotkeys.OpenSortOptionsMenu, msg): m.cancelSortOptions() - case containsKey(msg, common.Hotkeys.Quit): + case slices.Contains(common.Hotkeys.Quit, msg): m.cancelSortOptions() - case containsKey(msg, common.Hotkeys.Confirm): + case slices.Contains(common.Hotkeys.Confirm, msg): m.confirmSortOptions() - case containsKey(msg, common.Hotkeys.ListUp): + case slices.Contains(common.Hotkeys.ListUp, msg): m.sortOptionsListUp() - case containsKey(msg, common.Hotkeys.ListDown): + case slices.Contains(common.Hotkeys.ListDown, msg): m.sortOptionsListDown() } } func (m *model) renamingKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.CancelTyping): + switch { + case slices.Contains(common.Hotkeys.CancelTyping, msg): m.cancelRename() - case containsKey(msg, common.Hotkeys.ConfirmTyping): + case slices.Contains(common.Hotkeys.ConfirmTyping, msg): if m.IsRenamingConflicting() { m.warnModalForRenaming() } else { @@ -309,20 +298,20 @@ func (m *model) renamingKey(msg string) { } func (m *model) sidebarRenamingKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.CancelTyping): + switch { + case slices.Contains(common.Hotkeys.CancelTyping, msg): m.cancelSidebarRename() - case containsKey(msg, common.Hotkeys.ConfirmTyping): + case slices.Contains(common.Hotkeys.ConfirmTyping, msg): m.confirmSidebarRename() } } func (m *model) sidebarSearchBarKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.CancelTyping): + switch { + case slices.Contains(common.Hotkeys.CancelTyping, msg): m.sidebarModel.searchBar.Blur() m.sidebarModel.searchBar.SetValue("") - case containsKey(msg, common.Hotkeys.ConfirmTyping): + case slices.Contains(common.Hotkeys.ConfirmTyping, msg): m.sidebarModel.searchBar.Blur() m.sidebarModel.resetCursor() } @@ -330,10 +319,10 @@ func (m *model) sidebarSearchBarKey(msg string) { // Check the key input and cancel or confirms the search func (m *model) focusOnSearchbarKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.CancelTyping): + switch { + case slices.Contains(common.Hotkeys.CancelTyping, msg): m.cancelSearch() - case containsKey(msg, common.Hotkeys.ConfirmTyping): + case slices.Contains(common.Hotkeys.ConfirmTyping, msg): m.confirmSearch() } } @@ -341,12 +330,12 @@ func (m *model) focusOnSearchbarKey(msg string) { // Check hotkey input in help menu. Possible actions are moving up, down // and quiting the menu func (m *model) helpMenuKey(msg string) { - switch msg { - case containsKey(msg, common.Hotkeys.ListUp): + switch { + case slices.Contains(common.Hotkeys.ListUp, msg): m.helpMenuListUp() - case containsKey(msg, common.Hotkeys.ListDown): + case slices.Contains(common.Hotkeys.ListDown, msg): m.helpMenuListDown() - case containsKey(msg, common.Hotkeys.Quit): + case slices.Contains(common.Hotkeys.Quit, msg): m.quitHelpMenu() } } diff --git a/src/internal/model.go b/src/internal/model.go index 165ea5c3..79143762 100644 --- a/src/internal/model.go +++ b/src/internal/model.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "reflect" + "slices" "strings" "time" @@ -22,25 +23,27 @@ import ( stringfunction "github.com/yorukot/superfile/src/pkg/string_function" ) -var LastTimeCursorMove = [2]int{int(time.Now().UnixMicro()), 0} -var ListeningMessage = true - -var firstUse = false -var hasTrash = true -var batCmd = "" - -var et *exiftool.Exiftool - -var channel = make(chan channelMessage, 1000) -var progressBarLastRenderTime time.Time = time.Now() +// These represent model's state information, its not a global preperty +var LastTimeCursorMove = [2]int{int(time.Now().UnixMicro()), 0} //nolint: gochecknoglobals // Todo : Move to model struct +var ListeningMessage = true //nolint: gochecknoglobals // Todo : Move to model struct +var firstUse = false //nolint: gochecknoglobals // Todo : Move to model struct +var hasTrash = true //nolint: gochecknoglobals // Todo : Move to model struct +var batCmd = "" //nolint: gochecknoglobals // Todo : Move to model struct +var et *exiftool.Exiftool //nolint: gochecknoglobals // Todo : Move to model struct +var channel = make(chan channelMessage, 1000) //nolint: gochecknoglobals // Todo : Move to model struct +var progressBarLastRenderTime = time.Now() //nolint: gochecknoglobals // Todo : Move to model struct // Initialize and return model with default configs -func InitialModel(dir string, firstUseCheck, hasTrashCheck bool) model { - toggleDotFile, toggleFooter, firstFilePanelDir := initialConfig(dir) +// It returns only tea.Model because when it used in main, the return value +// is passed to tea.NewProgram() which accepts tea.Model +// Either way type 'model' is not exported, so there is not way main package can +// be aware of it, and use it directly +func InitialModel(firstFilePanelDirs []string, firstUseCheck, hasTrashCheck bool) tea.Model { + toggleDotFile, toggleFooter := initialConfig(firstFilePanelDirs) firstUse = firstUseCheck hasTrash = hasTrashCheck batCmd = checkBatCmd() - return defaultModelConfig(toggleDotFile, toggleFooter, firstFilePanelDir) + return defaultModelConfig(toggleDotFile, toggleFooter, firstFilePanelDirs) } // Init function to be called by Bubble tea framework, sets windows title, @@ -117,10 +120,10 @@ func (m *model) handleChannelMessage(msg channelMessage) { case sendMetadata: m.fileMetaData.metaData = msg.metadata case sendProcess: - if !arrayContains(m.processBarModel.processList, msg.messageId) { - m.processBarModel.processList = append(m.processBarModel.processList, msg.messageId) + if !arrayContains(m.processBarModel.processList, msg.messageID) { + m.processBarModel.processList = append(m.processBarModel.processList, msg.messageID) } - m.processBarModel.process[msg.messageId] = msg.processNewState + m.processBarModel.process[msg.messageID] = msg.processNewState default: slog.Error("Unhandled channelMessageType in handleChannelMessage()", "messageType", msg.messageType) @@ -150,8 +153,6 @@ func (m *model) handleWindowResize(msg tea.WindowSizeMsg) { func (m *model) setFilePreviewWidth(width int) { if common.Config.FilePreviewWidth == 0 { m.fileModel.filePreview.width = (width - common.Config.SidebarWidth - (4 + (len(m.fileModel.filePanels))*2)) / (len(m.fileModel.filePanels) + 1) - } else if common.Config.FilePreviewWidth > 10 || common.Config.FilePreviewWidth == 1 { - utils.LogAndExit("Config file file_preview_width invalidation") } else { m.fileModel.filePreview.width = (width - common.Config.SidebarWidth) / common.Config.FilePreviewWidth } @@ -168,6 +169,7 @@ func (m *model) setFilePanelsSize(width int) { } func (m *model) setHeightValues(height int) { + //nolint: gocritic // This is to be separated out to a function, and made better later. No need to refactor here if !m.toggleFooter { m.footerHeight = 0 } else if height < 30 { @@ -205,7 +207,6 @@ func (m *model) setHelpMenuSize() { // Identify the current state of the application m and properly handle the // msg keybind pressed func (m *model) handleKeyInput(msg tea.KeyMsg, cmd tea.Cmd) tea.Cmd { - slog.Debug("model.handleKeyInput", "msg", msg, "typestr", msg.Type.String(), "runes", msg.Runes, "type", int(msg.Type), "paste", msg.Paste, "alt", msg.Alt) @@ -227,35 +228,34 @@ func (m *model) handleKeyInput(msg tea.KeyMsg, cmd tea.Cmd) tea.Cmd { firstUse = false return cmd } - - if m.typingModal.open { + switch { + case m.typingModal.open: m.typingModalOpenKey(msg.String()) - - } else if m.promptModal.IsOpen() { + case m.promptModal.IsOpen(): // Ignore keypress. It will be handled in Update call via // updateFilePanelState - } else if m.warnModal.open { + case m.warnModal.open: m.warnModalOpenKey(msg.String()) // If renaming a object - } else if m.fileModel.renaming { + case m.fileModel.renaming: m.renamingKey(msg.String()) - } else if m.sidebarModel.renaming { + case m.sidebarModel.renaming: m.sidebarRenamingKey(msg.String()) // If search bar is open - } else if m.fileModel.filePanels[m.filePanelFocusIndex].searchBar.Focused() { + case m.fileModel.filePanels[m.filePanelFocusIndex].searchBar.Focused(): m.focusOnSearchbarKey(msg.String()) // If sort options menu is open - } else if m.sidebarModel.searchBar.Focused() { + case m.sidebarModel.searchBar.Focused(): m.sidebarSearchBarKey(msg.String()) // If sort options menu is open - } else if m.fileModel.filePanels[m.filePanelFocusIndex].sortOptions.open { + case m.fileModel.filePanels[m.filePanelFocusIndex].sortOptions.open: m.sortOptionsKey(msg.String()) // If help menu is open - } else if m.helpMenu.open { + case m.helpMenu.open: m.helpMenuKey(msg.String()) // If asking to confirm quiting - } else if m.confirmToQuit { + case m.confirmToQuit: quit := m.confirmToQuitSuperfile(msg.String()) if quit { m.quitSuperfile() @@ -263,7 +263,7 @@ func (m *model) handleKeyInput(msg tea.KeyMsg, cmd tea.Cmd) tea.Cmd { } // If quiting input pressed, check if has any running process and displays a // warn. Otherwise just quits application - } else if msg.String() == containsKey(msg.String(), common.Hotkeys.Quit) { + case slices.Contains(common.Hotkeys.Quit, msg.String()): if m.hasRunningProcesses() { m.warnModalForQuit() return cmd @@ -271,7 +271,7 @@ func (m *model) handleKeyInput(msg tea.KeyMsg, cmd tea.Cmd) tea.Cmd { m.quitSuperfile() return tea.Quit - } else { + default: // Handles general kinds of inputs in the regular state of the application cmd = m.mainKey(msg.String(), cmd) } @@ -282,15 +282,16 @@ func (m *model) handleKeyInput(msg tea.KeyMsg, cmd tea.Cmd) tea.Cmd { // in search, update typingb bar, etc func (m *model) updateFilePanelsState(msg tea.Msg, cmd *tea.Cmd) { focusPanel := &m.fileModel.filePanels[m.filePanelFocusIndex] - if m.firstTextInput { + switch { + case m.firstTextInput: m.firstTextInput = false - } else if m.fileModel.renaming { + case m.fileModel.renaming: focusPanel.rename, *cmd = focusPanel.rename.Update(msg) - } else if focusPanel.searchBar.Focused() { + case focusPanel.searchBar.Focused(): focusPanel.searchBar, *cmd = focusPanel.searchBar.Update(msg) - } else if m.typingModal.open { + case m.typingModal.open: m.typingModal.textInput, *cmd = m.typingModal.textInput.Update(msg) - } else if m.promptModal.IsOpen() { + case m.promptModal.IsOpen(): // *cmd is a non-name, and cannot be used on left of := var action common.ModelAction // Taking returned cmd is necessary for blinking @@ -585,7 +586,7 @@ func (m *model) quitSuperfile() { } // cd on quit currentDir := m.fileModel.filePanels[m.filePanelFocusIndex].location - variable.LastDir = currentDir + variable.SetLastDir(currentDir) if common.Config.CdOnQuit { // escape single quote diff --git a/src/internal/model_prompt_test.go b/src/internal/model_prompt_test.go index 25c3fc3a..0da27f22 100644 --- a/src/internal/model_prompt_test.go +++ b/src/internal/model_prompt_test.go @@ -43,7 +43,6 @@ func TestMain(m *testing.M) { // Model is huge. Just one test file ain't enough func TestModel_Update_Prompt(t *testing.T) { - // We want to test these. Todo : complete important tests // 1. Being able to open prompt // 2. Being able to execute shell commands @@ -71,7 +70,7 @@ func TestModel_Update_Prompt(t *testing.T) { // too big Shell command output t.Run("Basic Prompt Opening", func(t *testing.T) { - m := defaultModelConfig(false, false, "/") + m := defaultModelConfig(false, false, []string{"/"}) firstUse = false _, err := TeaUpdate(&m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenCommandLine[0])) require.NoError(t, err, "Opening the prompt should not produce an error") @@ -79,7 +78,7 @@ func TestModel_Update_Prompt(t *testing.T) { }) t.Run("Split Panel", func(t *testing.T) { - m := defaultModelConfig(false, false, "/") + m := defaultModelConfig(false, false, []string{"/"}) firstUse = false assert.Len(t, m.fileModel.filePanels, 1) _, _ = TeaUpdate(&m, utils.TeaRuneKeyMsg(common.Hotkeys.OpenSPFPrompt[0])) diff --git a/src/internal/model_render.go b/src/internal/model_render.go index c9653b12..d36f5f0a 100644 --- a/src/internal/model_render.go +++ b/src/internal/model_render.go @@ -3,6 +3,7 @@ package internal import ( "bufio" "context" + "errors" "fmt" "image" "log/slog" @@ -50,7 +51,6 @@ func (m *model) sidebarRender() string { } func (s *sidebarModel) directoriesRender(mainPanelHeight int, curFilePanelFileLocation string, sideBarFocussed bool) string { - // Cursor should always point to a valid directory at this point if s.isCursorInvalid() { slog.Error("Unexpected situation in sideBar Model. "+ @@ -69,11 +69,12 @@ func (s *sidebarModel) directoriesRender(mainPanelHeight int, curFilePanelFileLo totalHeight += s.directories[i].requiredHeight() - if s.directories[i] == pinnedDividerDir { + switch s.directories[i] { + case pinnedDividerDir: res += "\n" + common.SideBarPinnedDivider - } else if s.directories[i] == diskDividerDir { + case diskDividerDir: res += "\n" + common.SideBarDisksDivider - } else { + default: cursor := " " if s.cursor == i && sideBarFocussed && !s.searchBar.Focused() { cursor = icon.Cursor @@ -100,7 +101,6 @@ func (m *model) filePanelRender() string { // file panel f := make([]string, 10) for i, filePanel := range m.fileModel.filePanels { - // check if cursor or render out of range if filePanel.cursor > len(filePanel.element)-1 { filePanel.cursor = 0 @@ -109,22 +109,22 @@ func (m *model) filePanelRender() string { m.fileModel.filePanels[i] = filePanel f[i] += common.FilePanelTopDirectoryIconStyle.Render(" "+icon.Directory+icon.Space) + common.FilePanelTopPathStyle.Render(truncateTextBeginning(filePanel.location, m.fileModel.width-4, "...")) + "\n" - filePanelWidth := 0 - footerBorderWidth := 0 + var filePanelWidth int + footerBorderWidth := m.fileModel.width + 15 + // Todo : Move this to a utility function and clarify the calculation via comments + // Maybe even write unit tests if (m.fullWidth-common.Config.SidebarWidth-(4+(len(m.fileModel.filePanels)-1)*2))%len(m.fileModel.filePanels) != 0 && i == len(m.fileModel.filePanels)-1 { if m.fileModel.filePreview.open { filePanelWidth = m.fileModel.width } else { filePanelWidth = (m.fileModel.width + (m.fullWidth-common.Config.SidebarWidth-(4+(len(m.fileModel.filePanels)-1)*2))%len(m.fileModel.filePanels)) } - footerBorderWidth = m.fileModel.width + 15 } else { filePanelWidth = m.fileModel.width - footerBorderWidth = m.fileModel.width + 15 } - sortDirectionString := "" + var sortDirectionString string if filePanel.sortOptions.data.reversed { if common.Config.Nerdfont { sortDirectionString = icon.SortDesc @@ -138,7 +138,7 @@ func (m *model) filePanelRender() string { sortDirectionString = "A" } } - sortTypeString := "" + var sortTypeString string if filePanelWidth < 23 { sortTypeString = sortDirectionString } else { @@ -284,8 +284,8 @@ func (m *model) processBarRender() string { curProcess := processes[i] curProcess.progress.Width = utils.FooterWidth(m.fullWidth) - 3 - symbol := "" - cursor := "" + var symbol string + var cursor string if i == m.processBarModel.cursor { cursor = common.FooterCursorStyle.Render("┃ ") } else { @@ -312,9 +312,7 @@ func (m *model) processBarRender() string { func (m *model) wrapProcessBardBorder(processRender string) string { courseNumber := 0 - if len(m.processBarModel.processList) == 0 { - courseNumber = 0 - } else { + if len(m.processBarModel.processList) != 0 { courseNumber = m.processBarModel.cursor + 1 } bottomBorder := common.GenerateFooterBorder(fmt.Sprintf("%s/%s", strconv.Itoa(courseNumber), strconv.Itoa(len(m.processBarModel.processList))), utils.FooterWidth(m.fullWidth)-3) @@ -381,7 +379,6 @@ func (m *model) metadataRender() string { metadataName = truncateMiddleText(m.fileMetaData.metaData[i][0], valueLength, "...") } metaDataBar += fmt.Sprintf("%-*s %s", sprintfLength, metadataName, data) - } bottomBorder := common.GenerateFooterBorder(fmt.Sprintf("%s/%s", strconv.Itoa(m.fileMetaData.renderIndex+1), strconv.Itoa(len(m.fileMetaData.metaData))), utils.FooterWidth(m.fullWidth)-3) metaDataBar = common.MetadataBorder(m.footerHeight, utils.FooterWidth(m.fullWidth), bottomBorder, m.focusPanel == metadataFocus).Render(metaDataBar) @@ -390,7 +387,6 @@ func (m *model) metadataRender() string { } func (m *model) clipboardRender() string { - // render clipboardRender := "" if len(m.copyItems.items) == 0 { @@ -415,8 +411,8 @@ func (m *model) clipboardRender() string { } } } - bottomWidth := 0 + var bottomWidth int if m.fullWidth%3 != 0 { bottomWidth = utils.FooterWidth(m.fullWidth + m.fullWidth%3 + 2) } else { @@ -511,9 +507,7 @@ func (m *model) warnModalRender() string { } func (m *model) promptModalRender() string { - return m.promptModal.Render(m.helpMenu.width) - } func (m *model) helpMenuRender() string { @@ -569,7 +563,6 @@ func (m *model) helpMenuRender() string { } for i := m.helpMenu.renderIndex; i < m.helpMenu.height+m.helpMenu.renderIndex && i < len(m.helpMenu.data); i++ { - if i != m.helpMenu.renderIndex { helpMenuContent += "\n" } @@ -667,10 +660,10 @@ func (m *model) filePreviewPanelRender() string { itemPath := panel.element[panel.cursor].location // Renamed it to info_err to prevent shadowing with err below - fileInfo, info_err := os.Stat(itemPath) + fileInfo, infoErr := os.Stat(itemPath) - if info_err != nil { - slog.Error("Error get file info", "error", info_err) + if infoErr != nil { + slog.Error("Error get file info", "error", infoErr) return box.Render("\n --- " + icon.Error + " Error get file info ---") } @@ -726,7 +719,7 @@ func (m *model) filePreviewPanelRender() string { } ansiRender, err := filepreview.ImagePreview(itemPath, m.fileModel.filePreview.width, previewLine, common.Theme.FilePanelBG) - if err == image.ErrFormat { + if errors.Is(err, image.ErrFormat) { return box.Render("\n --- " + icon.Error + " Unsupported image formats ---") } @@ -786,7 +779,6 @@ func (m *model) filePreviewPanelRender() string { } func getBatSyntaxHighlightedContent(itemPath string, previewLine int, background string) (string, error) { - fileContent := "" // --plain: use the plain style without line numbers and decorations // --force-colorization: force colorization for non-interactive shell output // --line-range <:m>: only read from line 1 to line "m" @@ -804,7 +796,7 @@ func getBatSyntaxHighlightedContent(itemPath string, previewLine int, background return "", err } - fileContent = string(fileContentBytes) + fileContent := string(fileContentBytes) if !common.Config.TransparentBackground { fileContent = setBatBackground(fileContent, background) } diff --git a/src/internal/sidebar.go b/src/internal/sidebar.go index b95879ea..5380af14 100644 --- a/src/internal/sidebar.go +++ b/src/internal/sidebar.go @@ -37,7 +37,6 @@ func (s *sidebarModel) resetCursor() { // This could be made constant time by keeping Indexes ot special directories saved, // but that too much. func (s *sidebarModel) lastRenderedIndex(mainPanelHeight int, startIndex int) int { - curHeight := sideBarInitialHeight endIndex := startIndex - 1 for i := startIndex; i < len(s.directories); i++ { @@ -121,7 +120,6 @@ func (s *sidebarModel) listUp(mainPanelHeight int) { // cause another listUp trigger to move up. s.listUp(mainPanelHeight) } - } func (s *sidebarModel) listDown(mainPanelHeight int) { diff --git a/src/internal/sidebar_test.go b/src/internal/sidebar_test.go index ad6b6282..a1025fe2 100644 --- a/src/internal/sidebar_test.go +++ b/src/internal/sidebar_test.go @@ -9,7 +9,7 @@ import ( func dirSlice(count int) []directory { res := make([]directory, count) - for i := 0; i < count; i++ { + for i := range count { res[i] = directory{name: "Dir" + strconv.Itoa(i), location: "/a/" + strconv.Itoa(i)} } return res @@ -67,7 +67,6 @@ func Test_noActualDir(t *testing.T) { } func Test_isCursorInvalid(t *testing.T) { - testcases := []struct { name string sidebar sidebarModel @@ -158,12 +157,12 @@ func Test_resetCursor(t *testing.T) { func Test_lastRenderIndex(t *testing.T) { // Setup test data - sidebar_a := sidebarModel{ + sidebarA := sidebarModel{ directories: formDirctorySlice( dirSlice(10), dirSlice(10), dirSlice(10), ), } - sidebar_b := sidebarModel{ + sidebarB := sidebarModel{ directories: formDirctorySlice( dirSlice(1), nil, dirSlice(5), ), @@ -179,7 +178,7 @@ func Test_lastRenderIndex(t *testing.T) { }{ { name: "Small viewport with home directories", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 10, startIndex: 0, expectedLastIndex: 6, @@ -187,7 +186,7 @@ func Test_lastRenderIndex(t *testing.T) { }, { name: "Medium viewport showing home and some pinned", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 20, startIndex: 0, expectedLastIndex: 14, @@ -195,7 +194,7 @@ func Test_lastRenderIndex(t *testing.T) { }, { name: "Medium viewport starting from pinned dirs", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 20, startIndex: 11, expectedLastIndex: 25, @@ -203,7 +202,7 @@ func Test_lastRenderIndex(t *testing.T) { }, { name: "Large viewport showing all directories", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 100, startIndex: 11, expectedLastIndex: 31, @@ -211,7 +210,7 @@ func Test_lastRenderIndex(t *testing.T) { }, { name: "Start index beyond directory count", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 100, startIndex: 32, expectedLastIndex: 31, @@ -219,7 +218,7 @@ func Test_lastRenderIndex(t *testing.T) { }, { name: "Asymmetric directory distribution", - sidebar: sidebar_b, + sidebar: sidebarB, mainPanelHeight: 12, startIndex: 0, expectedLastIndex: 4, @@ -234,31 +233,30 @@ func Test_lastRenderIndex(t *testing.T) { "lastRenderedIndex failed: %s", tt.explanation) }) } - } func Test_firstRenderIndex(t *testing.T) { - sidebar_a := sidebarModel{ + sidebarA := sidebarModel{ directories: fullDirSlice(10), } - sidebar_b := sidebarModel{ + sidebarB := sidebarModel{ directories: formDirctorySlice( dirSlice(1), nil, dirSlice(5), ), } - sidebar_c := sidebarModel{ + sidebarC := sidebarModel{ directories: formDirctorySlice( nil, dirSlice(5), dirSlice(5), ), } - sidebar_d := sidebarModel{ + sidebarD := sidebarModel{ directories: formDirctorySlice( nil, nil, dirSlice(3), ), } // Empty sidebar with only dividers - sidebar_e := sidebarModel{ + sidebarE := sidebarModel{ directories: fullDirSlice(0), } @@ -272,7 +270,7 @@ func Test_firstRenderIndex(t *testing.T) { }{ { name: "Basic calculation from end index", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 10, endIndex: 10, expectedFirstIndex: 6, @@ -280,7 +278,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Small panel height", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 5, endIndex: 15, expectedFirstIndex: 14, @@ -288,7 +286,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "End index near beginning", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 20, endIndex: 3, expectedFirstIndex: 0, @@ -296,7 +294,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "End index at disk divider", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 15, endIndex: 21, // Disk divider in sidebar_a expectedFirstIndex: 12, @@ -304,7 +302,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Very large panel height showing all items", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 100, endIndex: 31, // Last disk dir in sidebar_a expectedFirstIndex: 0, @@ -312,7 +310,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Asymetric sidebar with few directories", - sidebar: sidebar_b, + sidebar: sidebarB, mainPanelHeight: 12, endIndex: 4, // Last disk dir in sidebar_b expectedFirstIndex: 0, @@ -320,7 +318,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "No home directories case", - sidebar: sidebar_c, + sidebar: sidebarC, mainPanelHeight: 10, endIndex: 6, // Disk dir in sidebar_c expectedFirstIndex: 2, // Pinned divider @@ -328,7 +326,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Only disk directories case", - sidebar: sidebar_d, + sidebar: sidebarD, mainPanelHeight: 8, endIndex: 4, // Last disk dir expectedFirstIndex: 2, // Disk divider @@ -336,7 +334,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Empty sidebar case", - sidebar: sidebar_e, + sidebar: sidebarE, mainPanelHeight: 10, endIndex: 1, // Disk divider expectedFirstIndex: 0, // Pinned divider @@ -344,7 +342,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "End index at the start", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 5, endIndex: 0, expectedFirstIndex: 0, @@ -352,7 +350,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "End index out of bounds", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 20, endIndex: 32, // Out of bounds for sidebar_a expectedFirstIndex: 33, // endIndex + 1 @@ -360,7 +358,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Very small panel height", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 2, // Too small to fit anything endIndex: 10, expectedFirstIndex: 11, @@ -368,7 +366,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Panel height exactly matches divider", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 6, // Just enough for initialHeight + divider endIndex: 10, // Pinned divider expectedFirstIndex: 10, @@ -376,7 +374,7 @@ func Test_firstRenderIndex(t *testing.T) { }, { name: "Boundary case between directory types", - sidebar: sidebar_a, + sidebar: sidebarA, mainPanelHeight: 7, endIndex: 11, // First pinned dir expectedFirstIndex: 10, // Pinned divider diff --git a/src/internal/string_function.go b/src/internal/string_function.go index 3187c301..e8b8881b 100644 --- a/src/internal/string_function.go +++ b/src/internal/string_function.go @@ -2,6 +2,7 @@ package internal import ( "bufio" + "errors" "fmt" "io" "math" @@ -17,7 +18,6 @@ import ( ) func truncateText(text string, maxChars int, talis string) string { - truncatedText := ansi.Truncate(text, maxChars-len(talis), "") if text != truncatedText { return truncatedText + talis @@ -67,12 +67,11 @@ func prettierName(name string, width int, isDir bool, isSelected bool, bgColor l Render(style.Icon+" ") + common.FilePanelItemSelectedStyle. Render(truncateText(name, width, "...")) - } else { - return common.StringColorRender(lipgloss.Color(style.Color), bgColor). - Background(bgColor). - Render(style.Icon+" ") + - common.FilePanelStyle.Render(truncateText(name, width, "...")) } + return common.StringColorRender(lipgloss.Color(style.Color), bgColor). + Background(bgColor). + Render(style.Icon+" ") + + common.FilePanelStyle.Render(truncateText(name, width, "...")) } func prettierDirectoryPreviewName(name string, isDir bool, bgColor lipgloss.Color) string { @@ -90,12 +89,11 @@ func clipboardPrettierName(name string, width int, isDir bool, isSelected bool) Background(common.FooterBGColor). Render(style.Icon+" ") + common.FilePanelItemSelectedStyle.Render(truncateTextBeginning(name, width, "...")) - } else { - return common.StringColorRender(lipgloss.Color(style.Color), common.FooterBGColor). - Background(common.FooterBGColor). - Render(style.Icon+" ") + - common.FilePanelStyle.Render(truncateTextBeginning(name, width, "...")) } + return common.StringColorRender(lipgloss.Color(style.Color), common.FooterBGColor). + Background(common.FooterBGColor). + Render(style.Icon+" ") + + common.FilePanelStyle.Render(truncateTextBeginning(name, width, "...")) } func fileNameWithoutExtension(fileName string) string { @@ -117,15 +115,15 @@ func formatFileSize(size int64) string { unitsDec := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} unitsBin := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} + // Todo : Remove duplication here if common.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]) - } else { - unitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(1024))) - adjustedSize := float64(size) / math.Pow(1024, float64(unitIndex)) - return fmt.Sprintf("%.2f %s", adjustedSize, unitsBin[unitIndex]) } + unitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(1024))) + adjustedSize := float64(size) / math.Pow(1024, float64(unitIndex)) + return fmt.Sprintf("%.2f %s", adjustedSize, unitsBin[unitIndex]) } // Truncate line lengths and keep ANSI @@ -185,7 +183,7 @@ func isTextFile(filename string) (bool, error) { reader := bufio.NewReader(file) buffer := make([]byte, 1024) cnt, err := reader.Read(buffer) - if err != nil && err != io.EOF { + if err != nil && !errors.Is(err, io.EOF) { return false, err } return isBufferPrintable(buffer[:cnt]), nil @@ -200,7 +198,7 @@ func makePrintable(line string) string { // This has to be looped byte-wise, looping it rune-wise // or by using strings.Map would cause issues with strings like // "(NBSP)\xa0" - for i := 0; i < len(line); i++ { + for i := range len(line) { r := rune(line[i]) if unicode.IsGraphic(r) || r == rune('\t') || r == rune('\n') { sb.WriteByte(line[i]) diff --git a/src/internal/string_function_test.go b/src/internal/string_function_test.go index c4302f45..438376e1 100644 --- a/src/internal/string_function_test.go +++ b/src/internal/string_function_test.go @@ -77,7 +77,6 @@ func TestIsBufferPrintable(t *testing.T) { {"\x7f(DEL)", false}, } for _, tt := range inputs { - t.Run(fmt.Sprintf("Testing if buffer %q is printable", tt.input), func(t *testing.T) { result := isBufferPrintable([]byte(tt.input)) if result != tt.expected { @@ -137,7 +136,6 @@ func TestMakePrintable(t *testing.T) { {"\x7f(DEL)", "(DEL)"}, } for _, tt := range inputs { - t.Run(fmt.Sprintf("Make %q printable", tt.input), func(t *testing.T) { result := makePrintable(tt.input) if result != tt.expected { diff --git a/src/internal/type.go b/src/internal/type.go index d8583e03..bbf4c223 100644 --- a/src/internal/type.go +++ b/src/internal/type.go @@ -248,7 +248,7 @@ type process struct { // Message for process bar type channelMessage struct { - messageId string + messageID string messageType channelMessageType processNewState process warnModal warnModal diff --git a/src/internal/type_utils.go b/src/internal/type_utils.go index f236faf0..fa081c04 100644 --- a/src/internal/type_utils.go +++ b/src/internal/type_utils.go @@ -25,7 +25,7 @@ func (m *model) validateLayout() error { } // PanelHeight + 2 lines (main border) + actual footer height if m.fullHeight != (m.mainPanelHeight+2)+utils.FullFooterHeight(m.footerHeight, m.toggleFooter) { - return fmt.Errorf("Invalid model layout, fullHeight : %v, mainPanelHeight : %v, footerHeight : %v\n", + return fmt.Errorf("invalid model layout, fullHeight : %v, mainPanelHeight : %v, footerHeight : %v", m.fullHeight, m.mainPanelHeight, m.footerHeight) } // Todo : Add check for width as well @@ -44,6 +44,39 @@ func (d directory) requiredHeight() int { return 1 } +// ================ filepanel + +func filePanelSlice(dir []string) []filePanel { + res := make([]filePanel, len(dir)) + for i := range dir { + res[i] = defaultFilePanel(dir[i]) + } + return res +} + +func defaultFilePanel(dir string) filePanel { + return filePanel{ + render: 0, + cursor: 0, + location: dir, + sortOptions: sortOptionsModel{ + width: 20, + height: 4, + open: false, + cursor: common.Config.DefaultSortType, + data: sortOptionsModelData{ + options: []string{"Name", "Size", "Date Modified"}, + selected: common.Config.DefaultSortType, + reversed: common.Config.SortOrderReversed, + }, + }, + panelMode: browserMode, + focusType: focus, + directoryRecords: make(map[string]directoryRecord), + searchBar: common.GenerateSearchBar(), + } +} + // ================ String method for easy logging ===================== func (f focusPanelType) String() string { @@ -72,7 +105,6 @@ func (f filePanelFocusType) String() string { default: return invalidTypeString } - } func (p panelMode) String() string { diff --git a/src/internal/ui/prompt/model.go b/src/internal/ui/prompt/model.go index ef36e093..1456e5b8 100644 --- a/src/internal/ui/prompt/model.go +++ b/src/internal/ui/prompt/model.go @@ -56,7 +56,6 @@ func (m *Model) HandleUpdate(msg tea.Msg, cwdLocation string) (common.ModelActio default: // Non keypress updates like Cursor Blink m.textInput, cmd = m.textInput.Update(msg) - } return action, cmd } @@ -73,8 +72,7 @@ func (m *Model) handleConfirm(cwdLocation string) common.ModelAction { if err == nil { m.resultMsg = "" m.actionSuccess = true - } else if cmdErr, ok := err.(invalidCmdError); ok { - // We don't expect a wrapped error here, so using type assertion + } else if cmdErr, ok := err.(invalidCmdError); ok { //nolint: errorlint // We don't expect a wrapped error here, so using type assertion m.resultMsg = cmdErr.uiMessage() m.actionSuccess = false } else { @@ -88,11 +86,12 @@ func (m *Model) handleConfirm(cwdLocation string) common.ModelAction { func (m *Model) handleNormalKeyInput(msg tea.KeyMsg) tea.Cmd { var cmd tea.Cmd - if m.textInput.Value() == "" && msg.String() == m.spfPromptHotkey { + switch { + case m.textInput.Value() == "" && msg.String() == m.spfPromptHotkey: m.shellMode = false - } else if m.textInput.Value() == "" && msg.String() == m.shellPromptHotkey { + case m.textInput.Value() == "" && msg.String() == m.shellPromptHotkey: m.shellMode = true - } else { + default: m.textInput, cmd = m.textInput.Update(msg) } m.resultMsg = "" @@ -124,7 +123,6 @@ func (m *Model) HandleSPFActionResults(success bool, msg string) { // a generic way. We would have all our components use that. // And we could unit test this Render() easility. func (m *Model) Render(width int) string { - divider := strings.Repeat(common.Config.BorderTop, width) content := " " + m.headline + modeString(m.shellMode) content += "\n" + divider diff --git a/src/internal/ui/prompt/model_test.go b/src/internal/ui/prompt/model_test.go index 6de8e8e6..e2b32c5d 100644 --- a/src/internal/ui/prompt/model_test.go +++ b/src/internal/ui/prompt/model_test.go @@ -19,7 +19,6 @@ func initGlobals() { // Since this is config that would likely stay same, maybe this is okay. common.Hotkeys.ConfirmTyping = []string{"enter"} common.Hotkeys.CancelTyping = []string{"ctrl+c", "esc"} - } func TestMain(m *testing.M) { @@ -53,7 +52,6 @@ func TestModel_HandleUpdate(t *testing.T) { }) t.Run("Pressing confirm on empty input", func(t *testing.T) { - actualTest := func(closeOnSuccess bool, openAfterEnter bool) { m := GenerateModel(spfPromptChar, shellPromptChar, closeOnSuccess) m.Open(true) @@ -69,7 +67,6 @@ func TestModel_HandleUpdate(t *testing.T) { actualTest(true, false) actualTest(false, true) - }) t.Run("Validate Prompt Actions", func(t *testing.T) { @@ -109,11 +106,9 @@ func TestModel_HandleUpdate(t *testing.T) { actualTest(tea.KeyMsg{Type: tea.KeyCtrlC}, false) actualTest(tea.KeyMsg{Type: tea.KeyEscape}, false) actualTest(tea.KeyMsg{Type: tea.KeyCtrlD}, true) - }) t.Run("Switching between shell and SPF mode", func(t *testing.T) { - actualTest := func(promptChar string, shellChar string) { m := GenerateModel(promptChar, shellChar, true) m.Open(true) @@ -188,7 +183,6 @@ func TestMode_HandleResults(t *testing.T) { assert.True(t, m.actionSuccess) assert.Equal(t, "Command exited with status 0", m.resultMsg) assert.True(t, m.IsOpen()) - }) t.Run("Verify SPF results update", func(t *testing.T) { @@ -205,6 +199,5 @@ func TestMode_HandleResults(t *testing.T) { // Validate close happens when closeOnSuccess is true m.HandleSPFActionResults(true, "") assert.False(t, m.IsOpen()) - }) } diff --git a/src/internal/ui/prompt/tokenize.go b/src/internal/ui/prompt/tokenize.go index 059c364c..5bffcd1d 100644 --- a/src/internal/ui/prompt/tokenize.go +++ b/src/internal/ui/prompt/tokenize.go @@ -13,7 +13,6 @@ import ( // split into tokens func tokenizePromptCommand(command string, cwdLocation string) ([]string, error) { - command, err := resolveShellSubstitution(shellSubTimeout, command, cwdLocation) if err != nil { return nil, err @@ -27,10 +26,10 @@ func resolveShellSubstitution(subCmdTimeout time.Duration, command string, cwdLo cmdRunes := []rune(command) i := 0 for i < len(cmdRunes) { - if i+1 < len(cmdRunes) && cmdRunes[i] == '$' { + switch cmdRunes[i+1] { // ${ spotted - if cmdRunes[i+1] == '{' { + case '{': // Look for Ending '}' end := findEndingBracket(cmdRunes, i+1, '{', '}') if end == -1 { @@ -43,18 +42,17 @@ func resolveShellSubstitution(subCmdTimeout time.Duration, command string, cwdLo envVarName := string(cmdRunes[i+2 : end]) // We can add a layer of abstraction for better unit testing - if value, ok := os.LookupEnv(envVarName); !ok { + value, ok := os.LookupEnv(envVarName) + if !ok { return "", envVarNotFoundError{varName: envVarName} - } else { - // Might Handle values being too big, or having multiple lines - // But this is based on user input, so it is probably okay for now - // Same comment for command substitution - resCommand.WriteString(value) } + // Might Handle values being too big, or having multiple lines + // But this is based on user input, so it is probably okay for now + // Same comment for command substitution + resCommand.WriteString(value) i = end + 1 - - } else if cmdRunes[i+1] == '(' { + case '(': // Look for ending ')' end := findEndingBracket(cmdRunes, i+1, '(', ')') if end == -1 { @@ -70,18 +68,17 @@ func resolveShellSubstitution(subCmdTimeout time.Duration, command string, cwdLo if retCode == -1 { return "", fmt.Errorf("could not execute shell substitution command : %s : %w", subCmd, err) - } else { - // We are allowing commands that exit with non zero status code - // We still use its output - if retCode != 0 { - slog.Debug("substitution command exited with non zero status", "retCode", retCode, - "command", subCmd) - } - resCommand.WriteString(output) } + // We are allowing commands that exit with non zero status code + // We still use its output + if retCode != 0 { + slog.Debug("substitution command exited with non zero status", "retCode", retCode, + "command", subCmd) + } + resCommand.WriteString(output) i = end + 1 - } else { + default: resCommand.WriteRune(cmdRunes[i]) i++ } @@ -89,7 +86,6 @@ func resolveShellSubstitution(subCmdTimeout time.Duration, command string, cwdLo resCommand.WriteRune(cmdRunes[i]) i++ } - } return resCommand.String(), nil diff --git a/src/internal/ui/prompt/tokenize_test.go b/src/internal/ui/prompt/tokenize_test.go index 1dca5aff..ca3c4018 100644 --- a/src/internal/ui/prompt/tokenize_test.go +++ b/src/internal/ui/prompt/tokenize_test.go @@ -16,7 +16,7 @@ const ( spfTestEnvVar4 = "SPF_TEST_ENV_VAR4" ) -var testEnvValues = map[string]string{ +var testEnvValues = map[string]string{ //nolint:gochecknoglobals // This is more like a const. Had to use `var` as go doesn't allows const maps spfTestEnvVar1: "1", spfTestEnvVar2: "hello", spfTestEnvVar3: "", @@ -187,7 +187,6 @@ func Test_resolveShellSubstitution(t *testing.T) { } func Test_findEndingParenthesis(t *testing.T) { - testdata := []struct { name string value string diff --git a/src/internal/ui/prompt/utils.go b/src/internal/ui/prompt/utils.go index d5138e7c..3f30ff3a 100644 --- a/src/internal/ui/prompt/utils.go +++ b/src/internal/ui/prompt/utils.go @@ -60,7 +60,6 @@ func getPromptAction(shellMode bool, value string, cwdLocation string) (common.M uiMsg: "Invalid spf prompt command : " + promptArgs[0], } } - } // Only allocates memory proportional to first token's size diff --git a/src/internal/ui/prompt/utils_test.go b/src/internal/ui/prompt/utils_test.go index 71a64298..8fae3d94 100644 --- a/src/internal/ui/prompt/utils_test.go +++ b/src/internal/ui/prompt/utils_test.go @@ -109,6 +109,7 @@ func TestModel_getPromptAction(t *testing.T) { action, err := getPromptAction(tt.shellMode, tt.text, "/") if err != nil { assert.True(t, tt.expectedErr) + //nolint: errorlint // We don't expect a wrapped error here, so using type assertion cmdErr, ok := err.(invalidCmdError) assert.True(t, ok) if tt.expectedErrMsg != "" { @@ -117,10 +118,8 @@ func TestModel_getPromptAction(t *testing.T) { } assert.Equal(t, tt.expectecAction, action) - }) } - } func Test_getFirstToken(t *testing.T) { diff --git a/src/internal/wheel_function.go b/src/internal/wheel_function.go index 0161f2b1..ef07cf9e 100644 --- a/src/internal/wheel_function.go +++ b/src/internal/wheel_function.go @@ -10,31 +10,35 @@ func wheelMainAction(msg string, m *model) { slog.Debug("wheelMainAction called", "msg", msg, "focusPanel", m.focusPanel) var action func() switch msg { - case "wheel up": - if m.focusPanel == sidebarFocus { + switch m.focusPanel { + case sidebarFocus: action = func() { m.sidebarModel.listUp(m.mainPanelHeight) } - } else if m.focusPanel == processBarFocus { + case processBarFocus: action = func() { m.processBarModel.listUp(m.footerHeight) } - } else if m.focusPanel == metadataFocus { + case metadataFocus: action = func() { m.fileMetaData.listUp() } - } else if m.focusPanel == nonePanelFocus { + case nonePanelFocus: action = func() { m.fileModel.filePanels[m.filePanelFocusIndex].listUp(m.mainPanelHeight) } } case "wheel down": - if m.focusPanel == sidebarFocus { + switch m.focusPanel { + case sidebarFocus: action = func() { m.sidebarModel.listDown(m.mainPanelHeight) } - } else if m.focusPanel == processBarFocus { + case processBarFocus: action = func() { m.processBarModel.listDown(m.footerHeight) } - } else if m.focusPanel == metadataFocus { + case metadataFocus: action = func() { m.fileMetaData.listDown() } - } else if m.focusPanel == nonePanelFocus { + case nonePanelFocus: action = func() { m.fileModel.filePanels[m.filePanelFocusIndex].listDown(m.mainPanelHeight) } } + default: + slog.Error("Unexpected type of mouse action in wheelMainAction", "msg", msg) + return } - for i := 0; i < common.WheelRunTime; i++ { + for range common.WheelRunTime { action() } diff --git a/src/pkg/file_preview/image_preview.go b/src/pkg/file_preview/image_preview.go index 7795b273..89eaa545 100644 --- a/src/pkg/file_preview/image_preview.go +++ b/src/pkg/file_preview/image_preview.go @@ -52,7 +52,7 @@ func ConvertImageToANSI(img image.Image, defaultBGColor color.Color) string { defaultBGHex := colorToHex(defaultBGColor) for y := 0; y < height; y += 2 { - for x := 0; x < width; x++ { + for x := range width { upperColor := cache.getTermenvColor(img.At(x, y), defaultBGHex) lowerColor := cache.getTermenvColor(defaultBGColor, "") @@ -99,7 +99,7 @@ func ImagePreview(path string, maxWidth, maxHeight int, defaultBGColor string) ( // Convert image to ANSI bgColor, err := hexToColor(defaultBGColor) if err != nil { - return "", fmt.Errorf("invalid background color: %v", err) + return "", fmt.Errorf("invalid background color: %w", err) } ansiImage := ConvertImageToANSI(resizedImg, bgColor) @@ -163,10 +163,7 @@ func hexToColor(hex string) (color.RGBA, error) { return color.RGBA{R: uint8(values >> 16), G: uint8((values >> 8) & 0xFF), B: uint8(values & 0xFF), A: 255}, nil } -func colorToHex(color color.Color) (fullbackHex string) { +func colorToHex(color color.Color) string { r, g, b, _ := color.RGBA() - - fullbackHex = fmt.Sprintf("#%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8)) - return fullbackHex - + return fmt.Sprintf("#%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8)) } diff --git a/src/pkg/string_function/overplace.go b/src/pkg/string_function/overplace.go index fec1a904..6e71adfe 100644 --- a/src/pkg/string_function/overplace.go +++ b/src/pkg/string_function/overplace.go @@ -160,9 +160,9 @@ func clamp(v, lower, upper int) int { // Split a string into lines, additionally returning the size of the widest // line. -func getLines(s string) (lines []string, widest int) { - lines = strings.Split(s, "\n") - +func getLines(s string) ([]string, int) { + lines := strings.Split(s, "\n") + widest := 0 for _, l := range lines { w := charmansi.StringWidth(l) if widest < w {