diff --git a/.golangci.yaml b/.golangci.yaml index cc98f1c2d..f7397598a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -99,7 +99,7 @@ linters: - makezero # finds slice declarations with non-zero initial length - mirror # reports wrong mirror patterns of bytes/strings usage # TODO enable - # - mnd # detects magic numbers + - mnd # detects magic numbers - musttag # enforces field tags in (un)marshaled structs - nakedret # finds naked returns in functions greater than a specified function length # TODO enable : Many reports. A bit hard to understand the nesting value. diff --git a/src/cmd/main.go b/src/cmd/main.go index aadac6d32..b1f0bce06 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -49,16 +49,25 @@ func Run(content embed.FS) { fmt.Println(variable.LastDirFile) return nil } - 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) - fmt.Printf("%-*s %s\n", 55, lipgloss.NewStyle().Foreground(lipgloss.Color("#ff9999")). - Render("[Configuration directory path]"), variable.SuperFileMainDir) - fmt.Printf("%-*s %s\n", 55, lipgloss.NewStyle().Foreground(lipgloss.Color("#ff66ff")). - Render("[Data directory path]"), variable.SuperFileDataDir) + fmt.Printf("%-*s %s\n", + common.HelpKeyColumnWidth, + lipgloss.NewStyle().Foreground(lipgloss.Color("#66b2ff")).Render("[Configuration file path]"), + variable.ConfigFile, + ) + fmt.Printf("%-*s %s\n", + common.HelpKeyColumnWidth, + lipgloss.NewStyle().Foreground(lipgloss.Color("#ffcc66")).Render("[Hotkeys file path]"), + variable.HotkeysFile, + ) + logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#66ff66")) + configStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#ff9999")) + dataStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#ff66ff")) + fmt.Printf("%-*s %s\n", common.HelpKeyColumnWidth, + logStyle.Render("[Log file path]"), variable.LogFile) + fmt.Printf("%-*s %s\n", common.HelpKeyColumnWidth, + configStyle.Render("[Configuration directory path]"), variable.SuperFileMainDir) + fmt.Printf("%-*s %s\n", common.HelpKeyColumnWidth, + dataStyle.Render("[Data directory path]"), variable.SuperFileDataDir) return nil }, Flags: []cli.Flag{ @@ -267,7 +276,7 @@ func shouldCheckForUpdate(now, last time.Time) bool { } func checkAndNotifyUpdate() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), common.DefaultCLIContextTimeout) defer cancel() resp, err := fetchLatestRelease(ctx) diff --git a/src/internal/common/string_function.go b/src/internal/common/string_function.go index 23593ae48..216e8fd62 100644 --- a/src/internal/common/string_function.go +++ b/src/internal/common/string_function.go @@ -16,6 +16,17 @@ import ( "github.com/charmbracelet/x/ansi" ) +// Size calculation constants +const ( + KilobyteSize = 1000 // SI decimal unit + KibibyteSize = 1024 // Binary unit + TabWidth = 4 // Standard tab expansion width + DefaultBufferSize = 1024 // Default buffer size for string operations + NonBreakingSpace = 0xa0 // Unicode non-breaking space + EscapeChar = 0x1b // ANSI escape character + ASCIIMax = 0x7f // Maximum ASCII character value +) + func TruncateText(text string, maxChars int, tails string) string { truncatedText := ansi.Truncate(text, maxChars-len(tails), "") if text != truncatedText { @@ -51,6 +62,7 @@ func TruncateMiddleText(text string, maxChars int, tails string) string { return text } + //nolint:mnd // standard halving for center truncation halfEllipsisLength := (maxChars - 3) / 2 // TODO : Use ansi.Substring to correctly handle ANSI escape codes truncatedText := text[:halfEllipsisLength] + tails + text[utf8.RuneCountInString(text)-halfEllipsisLength:] @@ -116,12 +128,12 @@ func FormatFileSize(size int64) string { // TODO : Remove duplication here if Config.FileSizeUseSI { - unitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(1000))) - adjustedSize := float64(size) / math.Pow(1000, float64(unitIndex)) + unitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(KilobyteSize))) + adjustedSize := float64(size) / math.Pow(KilobyteSize, float64(unitIndex)) return fmt.Sprintf("%.2f %s", adjustedSize, unitsDec[unitIndex]) } - unitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(1024))) - adjustedSize := float64(size) / math.Pow(1024, float64(unitIndex)) + unitIndex := int(math.Floor(math.Log(float64(size)) / math.Log(KibibyteSize))) + adjustedSize := float64(size) / math.Pow(KibibyteSize, float64(unitIndex)) return fmt.Sprintf("%.2f %s", adjustedSize, unitsBin[unitIndex]) } @@ -132,7 +144,7 @@ func CheckAndTruncateLineLengths(text string, maxLength int) string { for _, line := range lines { // Replace tabs with spaces - expandedLine := strings.ReplaceAll(line, "\t", strings.Repeat(" ", 4)) + expandedLine := strings.ReplaceAll(line, "\t", strings.Repeat(" ", TabWidth)) truncatedLine := ansi.Truncate(expandedLine, maxLength, "") result.WriteString(truncatedLine + "\n") } @@ -180,7 +192,7 @@ func IsTextFile(filename string) (bool, error) { defer file.Close() reader := bufio.NewReader(file) - buffer := make([]byte, 1024) + buffer := make([]byte, DefaultBufferSize) cnt, err := reader.Read(buffer) if err != nil && !errors.Is(err, io.EOF) { return false, err @@ -204,7 +216,7 @@ func MakePrintableWithEscCheck(line string, allowEsc bool) string { //nolint: go } // It needs to be handled separately since considered a space, // It is multi-byte in UTF-8, But it has zero display width - if r == 0xa0 { + if r == NonBreakingSpace { sb.WriteRune(r) continue } @@ -215,13 +227,13 @@ func MakePrintableWithEscCheck(line string, allowEsc bool) string { //nolint: go sb.WriteString(" ") continue } - if r == 0x1b { + if r == EscapeChar { if allowEsc { sb.WriteRune(r) } continue } - if r > 0x7f { + if r > ASCIIMax { if unicode.IsSpace(r) && utf8.RuneLen(r) > 1 { // See https://github.com/charmbracelet/x/issues/466 // Space chacters spanning more than one bytes are not handled well by diff --git a/src/internal/common/style_function.go b/src/internal/common/style_function.go index 41516b2c4..5aeb3a407 100644 --- a/src/internal/common/style_function.go +++ b/src/internal/common/style_function.go @@ -269,6 +269,7 @@ func GenerateNewFileTextInput() textinput.Model { t.PlaceholderStyle = ModalStyle t.Focus() t.CharLimit = 156 + //nolint:mnd // modal width minus padding t.Width = ModalWidth - 10 return t } @@ -304,7 +305,7 @@ func GeneratePinnedRenameTextInput(cursorPos int, defaultValue string) textinput ti.SetCursor(cursorPos) ti.Focus() ti.CharLimit = 156 - ti.Width = Config.SidebarWidth - 4 + ti.Width = Config.SidebarWidth - PanelPadding return ti } diff --git a/src/internal/common/ui_consts.go b/src/internal/common/ui_consts.go new file mode 100644 index 000000000..54d45c564 --- /dev/null +++ b/src/internal/common/ui_consts.go @@ -0,0 +1,36 @@ +package common + +import "time" + +// Shared UI/layout constants to replace magic numbers flagged by mnd. +const ( + HelpKeyColumnWidth = 55 // width of help key column in CLI help + DefaultCLIContextTimeout = 5 * time.Second // default CLI context timeout for CLI ops + + PanelPadding = 3 // rows reserved around file list (borders/header/footer) + BorderPadding = 2 // rows/cols for outer border frame + InnerPadding = 4 // cols for inner content padding (truncate widths) + FooterGroupCols = 3 // columns per group in footer layout math + + DefaultFilePanelWidth = 10 // default width for file panels + FilePanelMax = 10 // max number of file panels supported + MinWidthForRename = 18 // minimal width for rename input to render + ResponsiveWidthThreshold = 95 // width breakpoint for layout behavior + + HeightBreakA = 30 // responsive height tiers + HeightBreakB = 35 + HeightBreakC = 40 + HeightBreakD = 45 + + ReRenderChunkDivisor = 100 // divisor for re-render throttling + + FilePanelWidthUnit = 20 // width unit used to calculate max file panels + DefaultPreviewTimeout = 500 * time.Millisecond // preview operation timeout + + // File permissions + ExtractedFileMode = 0644 // default permissions for extracted files + ExtractedDirMode = 0755 // default permissions for extracted directories + + // UI positioning + CenterDivisor = 2 // divisor for centering UI elements +) diff --git a/src/internal/default_config.go b/src/internal/default_config.go index 8db1d2a2b..ff746148a 100644 --- a/src/internal/default_config.go +++ b/src/internal/default_config.go @@ -30,7 +30,7 @@ func defaultModelConfig(toggleDotFile, toggleFooter, firstUse bool, fileModel: fileModel{ filePanels: filePanelSlice(firstPanelPaths), filePreview: preview.New(), - width: 10, + width: common.DefaultFilePanelWidth, }, helpMenu: newHelpMenuModal(), promptModal: prompt.DefaultModel(prompt.PromptMinHeight, prompt.PromptMinWidth), diff --git a/src/internal/file_operations_extract.go b/src/internal/file_operations_extract.go index 0056cb2a8..1a3844618 100644 --- a/src/internal/file_operations_extract.go +++ b/src/internal/file_operations_extract.go @@ -8,6 +8,7 @@ import ( "golift.io/xtractr" "github.com/yorukot/superfile/src/config/icon" + "github.com/yorukot/superfile/src/internal/common" "github.com/yorukot/superfile/src/internal/ui/processbar" ) @@ -20,8 +21,8 @@ func extractCompressFile(src, dest string, processBar *processbar.Model) error { x := &xtractr.XFile{ FilePath: src, OutputDir: dest, - FileMode: 0644, - DirMode: 0755, + FileMode: common.ExtractedFileMode, + DirMode: common.ExtractedDirMode, } _, _, _, err = xtractr.ExtractFile(x) diff --git a/src/internal/function.go b/src/internal/function.go index e34ad9c07..198a1113f 100644 --- a/src/internal/function.go +++ b/src/internal/function.go @@ -242,7 +242,7 @@ func getTypeOrderingFunc(elements []element, reversed bool) sliceOrderFunc { } func panelElementHeight(mainPanelHeight int) int { - return mainPanelHeight - 3 + return mainPanelHeight - common.PanelPadding } // TODO : replace usage of this with slices.contains @@ -291,6 +291,7 @@ func renameIfDuplicate(destination string) (string, error) { // Extract base name without existing suffix counter := 1 + //nolint:mnd // 3 = full match + 2 capture groups if match := suffixRegexp.FindStringSubmatch(name); len(match) == 3 { name = match[1] // base name without (N) if num, err := strconv.Atoi(match[2]); err == nil { diff --git a/src/internal/handle_file_operations.go b/src/internal/handle_file_operations.go index e4fe5ed56..a631d02c3 100644 --- a/src/internal/handle_file_operations.go +++ b/src/internal/handle_file_operations.go @@ -97,7 +97,10 @@ func (m *model) panelItemRename() { m.fileModel.renaming = true panel.renaming = true m.firstTextInput = true - panel.rename = common.GenerateRenameTextInput(m.fileModel.width-4, cursorPos, panel.element[panel.cursor].name) + panel.rename = common.GenerateRenameTextInput( + m.fileModel.width-common.InnerPadding, + cursorPos, + panel.element[panel.cursor].name) } func (m *model) getDeleteCmd(permDelete bool) tea.Cmd { diff --git a/src/internal/handle_modal.go b/src/internal/handle_modal.go index 71745a50b..aa4bb0aa1 100644 --- a/src/internal/handle_modal.go +++ b/src/internal/handle_modal.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/yorukot/superfile/src/internal/common" "github.com/yorukot/superfile/src/internal/utils" ) @@ -160,7 +161,7 @@ func (m *model) helpMenuListUp() { // Similarly, we use max(..., 0) to ensure the renderIndex doesn't become negative, // which can happen if the number of items is less than the view height. // This prevents a potential out-of-bounds panic during rendering. - m.helpMenu.renderIndex = max(len(m.helpMenu.filteredData)-(m.helpMenu.height-4), 0) + m.helpMenu.renderIndex = max(len(m.helpMenu.filteredData)-(m.helpMenu.height-common.InnerPadding), 0) } } @@ -189,7 +190,7 @@ func (m *model) helpMenuListDown() { m.helpMenu.renderIndex++ } // Clamp renderIndex to bottom. - bottom := len(m.helpMenu.filteredData) - (m.helpMenu.height - 4) + bottom := len(m.helpMenu.filteredData) - (m.helpMenu.height - common.InnerPadding) if bottom < 0 { bottom = 0 } diff --git a/src/internal/handle_panel_movement.go b/src/internal/handle_panel_movement.go index d6125fadd..249d94943 100644 --- a/src/internal/handle_panel_movement.go +++ b/src/internal/handle_panel_movement.go @@ -9,6 +9,7 @@ import ( tea "github.com/charmbracelet/bubbletea" + "github.com/yorukot/superfile/src/internal/common" "github.com/yorukot/superfile/src/internal/utils" variable "github.com/yorukot/superfile/src/config" @@ -175,7 +176,7 @@ func (m *model) searchBarFocus() { } // config search bar width - panel.searchBar.Width = m.fileModel.width - 4 + panel.searchBar.Width = m.fileModel.width - common.InnerPadding } func (m *model) sidebarSearchBarFocus() { diff --git a/src/internal/handle_panel_navigation.go b/src/internal/handle_panel_navigation.go index 9c979965c..474a13587 100644 --- a/src/internal/handle_panel_navigation.go +++ b/src/internal/handle_panel_navigation.go @@ -51,7 +51,7 @@ func (m *model) createNewFilePanel(location string) error { // File preview panel width same as file panel if common.Config.FilePreviewWidth == 0 { m.fileModel.filePreview.SetWidth((m.fullWidth - common.Config.SidebarWidth - - (4 + (len(m.fileModel.filePanels))*2)) / (len(m.fileModel.filePanels) + 1)) + (common.InnerPadding + (len(m.fileModel.filePanels))*common.BorderPadding)) / (len(m.fileModel.filePanels) + 1)) } else { m.fileModel.filePreview.SetWidth((m.fullWidth - common.Config.SidebarWidth) / common.Config.FilePreviewWidth) } @@ -60,13 +60,13 @@ func (m *model) createNewFilePanel(location string) error { m.fileModel.filePanels[m.filePanelFocusIndex].isFocused = false m.fileModel.filePanels[m.filePanelFocusIndex+1].isFocused = returnFocusType(m.focusPanel) m.fileModel.width = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth() - - (4 + (len(m.fileModel.filePanels)-1)*2)) / len(m.fileModel.filePanels) + (common.InnerPadding + (len(m.fileModel.filePanels)-1)*common.BorderPadding)) / len(m.fileModel.filePanels) m.filePanelFocusIndex++ - m.fileModel.maxFilePanel = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / 20 + m.fileModel.maxFilePanel = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / common.FilePanelWidthUnit for i := range m.fileModel.filePanels { - m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - 4 + m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - common.InnerPadding } return nil } @@ -84,7 +84,7 @@ func (m *model) closeFilePanel() { // File preview panel width same as file panel if common.Config.FilePreviewWidth == 0 { m.fileModel.filePreview.SetWidth((m.fullWidth - common.Config.SidebarWidth - - (4 + (len(m.fileModel.filePanels))*2)) / (len(m.fileModel.filePanels) + 1)) + (common.InnerPadding + (len(m.fileModel.filePanels))*common.BorderPadding)) / (len(m.fileModel.filePanels) + 1)) } else { m.fileModel.filePreview.SetWidth((m.fullWidth - common.Config.SidebarWidth) / common.Config.FilePreviewWidth) } @@ -95,37 +95,37 @@ func (m *model) closeFilePanel() { } m.fileModel.width = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth() - - (4 + (len(m.fileModel.filePanels)-1)*2)) / len(m.fileModel.filePanels) + (common.InnerPadding + (len(m.fileModel.filePanels)-1)*common.BorderPadding)) / len(m.fileModel.filePanels) m.fileModel.filePanels[m.filePanelFocusIndex].isFocused = returnFocusType(m.focusPanel) - m.fileModel.maxFilePanel = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / 20 + m.fileModel.maxFilePanel = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / common.FilePanelWidthUnit for i := range m.fileModel.filePanels { - m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - 4 + m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - common.InnerPadding } } func (m *model) toggleFilePreviewPanel() { m.fileModel.filePreview.ToggleOpen() m.fileModel.filePreview.SetWidth(0) - m.fileModel.filePreview.SetHeight(m.mainPanelHeight + 2) + m.fileModel.filePreview.SetHeight(m.mainPanelHeight + common.BorderPadding) if m.fileModel.filePreview.IsOpen() { // File preview panel width same as file panel if common.Config.FilePreviewWidth == 0 { m.fileModel.filePreview.SetWidth((m.fullWidth - common.Config.SidebarWidth - - (4 + (len(m.fileModel.filePanels))*2)) / (len(m.fileModel.filePanels) + 1)) + (common.InnerPadding + (len(m.fileModel.filePanels))*common.BorderPadding)) / (len(m.fileModel.filePanels) + 1)) } else { m.fileModel.filePreview.SetWidth((m.fullWidth - common.Config.SidebarWidth) / common.Config.FilePreviewWidth) } } m.fileModel.width = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth() - - (4 + (len(m.fileModel.filePanels)-1)*2)) / len(m.fileModel.filePanels) + (common.InnerPadding + (len(m.fileModel.filePanels)-1)*common.BorderPadding)) / len(m.fileModel.filePanels) - m.fileModel.maxFilePanel = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / 20 + m.fileModel.maxFilePanel = (m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / common.FilePanelWidthUnit for i := range m.fileModel.filePanels { - m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - 4 + m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - common.InnerPadding } } diff --git a/src/internal/handle_panel_up_down.go b/src/internal/handle_panel_up_down.go index fae1581e9..d89ac2202 100644 --- a/src/internal/handle_panel_up_down.go +++ b/src/internal/handle_panel_up_down.go @@ -60,7 +60,7 @@ func (panel *filePanel) pgUp(mainPanelHeight int) { } panHeight := panelElementHeight(mainPanelHeight) - panCenter := panHeight / 2 // For making sure the cursor is at the center of the panel + panCenter := panHeight / 2 //nolint:mnd // For making sure the cursor is at the center of the panel if panHeight >= panlen { panel.cursor = 0 @@ -86,7 +86,7 @@ func (panel *filePanel) pgDown(mainPanelHeight int) { } panHeight := panelElementHeight(mainPanelHeight) - panCenter := panHeight / 2 // For making sure the cursor is at the center of the panel + panCenter := panHeight / 2 //nolint:mnd // For making sure the cursor is at the center of the panel if panHeight >= panlen { panel.cursor = panlen - 1 diff --git a/src/internal/model.go b/src/internal/model.go index 219e6bf4a..010076185 100644 --- a/src/internal/model.go +++ b/src/internal/model.go @@ -234,21 +234,21 @@ func (m *model) handleWindowResize(msg tea.WindowSizeMsg) { m.setPromptModelSize() m.setZoxideModelSize() - if m.fileModel.maxFilePanel >= 10 { - m.fileModel.maxFilePanel = 10 + if m.fileModel.maxFilePanel >= common.FilePanelMax { + m.fileModel.maxFilePanel = common.FilePanelMax } } func (m *model) setFilePreviewPanelSize() { m.fileModel.filePreview.SetWidth(m.getFilePreviewWidth()) - m.fileModel.filePreview.SetHeight(m.mainPanelHeight + 2) + m.fileModel.filePreview.SetHeight(m.mainPanelHeight + common.BorderPadding) } // Set file preview panel Widht to width. Assure that func (m *model) getFilePreviewWidth() int { if common.Config.FilePreviewWidth == 0 { return (m.fullWidth - common.Config.SidebarWidth - - (4 + (len(m.fileModel.filePanels))*2)) / (len(m.fileModel.filePanels) + 1) + (common.InnerPadding + (len(m.fileModel.filePanels))*common.BorderPadding)) / (len(m.fileModel.filePanels) + 1) } return (m.fullWidth - common.Config.SidebarWidth) / common.Config.FilePreviewWidth } @@ -257,10 +257,10 @@ func (m *model) getFilePreviewWidth() int { func (m *model) setFilePanelsSize(width int) { // set each file panel size and max file panel amount m.fileModel.width = (width - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth() - - (4 + (len(m.fileModel.filePanels)-1)*2)) / len(m.fileModel.filePanels) - m.fileModel.maxFilePanel = (width - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / 20 + (common.InnerPadding + (len(m.fileModel.filePanels)-1)*common.BorderPadding)) / len(m.fileModel.filePanels) + m.fileModel.maxFilePanel = (width - common.Config.SidebarWidth - m.fileModel.filePreview.GetWidth()) / common.FilePanelWidthUnit for i := range m.fileModel.filePanels { - m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - 4 + m.fileModel.filePanels[i].searchBar.Width = m.fileModel.width - common.InnerPadding } } @@ -268,13 +268,13 @@ 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 { + } else if height < common.HeightBreakA { m.footerHeight = 6 - } else if height < 35 { + } else if height < common.HeightBreakB { m.footerHeight = 7 - } else if height < 40 { + } else if height < common.HeightBreakC { m.footerHeight = 8 - } else if height < 45 { + } else if height < common.HeightBreakD { m.footerHeight = 9 } else { m.footerHeight = 10 @@ -283,7 +283,7 @@ func (m *model) setHeightValues(height int) { // TODO : Calculate the value , instead of manually hard coding it. // Main panel height = Total terminal height- 2(file panel border) - footer height - m.mainPanelHeight = height - 2 - utils.FullFooterHeight(m.footerHeight, m.toggleFooter) + m.mainPanelHeight = height - common.BorderPadding - utils.FullFooterHeight(m.footerHeight, m.toggleFooter) for index := range m.fileModel.filePanels { m.fileModel.filePanels[index].handleResize(m.mainPanelHeight) @@ -292,44 +292,48 @@ func (m *model) setHeightValues(height int) { // Set help menu size func (m *model) setHelpMenuSize() { - m.helpMenu.height = m.fullHeight - 2 - m.helpMenu.width = m.fullWidth - 2 + m.helpMenu.height = m.fullHeight - common.BorderPadding + m.helpMenu.width = m.fullWidth - common.BorderPadding - if m.fullHeight > 35 { + if m.fullHeight > common.HeightBreakB { m.helpMenu.height = 30 } - if m.fullWidth > 95 { + if m.fullWidth > common.ResponsiveWidthThreshold { m.helpMenu.width = 90 } // 2 for border, 1 for left padding, 2 for placeholder icon of searchbar // 1 for additional character that View() of search bar function mysteriously adds. - m.helpMenu.searchBar.Width = m.helpMenu.width - 6 + m.helpMenu.searchBar.Width = m.helpMenu.width - (common.InnerPadding + common.BorderPadding) } func (m *model) setPromptModelSize() { // Scale prompt model's maxHeight - 33% of total height - m.promptModal.SetMaxHeight(m.fullHeight / 3) + m.promptModal.SetMaxHeight(m.fullHeight / 3) //nolint:mnd // modal uses third height for layout // Scale prompt model's maxHeight - 50% of total height - m.promptModal.SetWidth(m.fullWidth / 2) + m.promptModal.SetWidth(m.fullWidth / 2) //nolint:mnd // modal uses half width for layout } func (m *model) setZoxideModelSize() { // Scale zoxide model's maxHeight - 50% of total height to accommodate scroll indicators - m.zoxideModal.SetMaxHeight(m.fullHeight / 2) + m.zoxideModal.SetMaxHeight(m.fullHeight / 2) //nolint:mnd // modal uses half height for layout // Scale zoxide model's width - 50% of total width - m.zoxideModal.SetWidth(m.fullWidth / 2) + m.zoxideModal.SetWidth(m.fullWidth / 2) //nolint:mnd // modal uses half width for layout } func (m *model) setMetadataModelSize() { - m.fileMetaData.SetDimensions(utils.FooterWidth(m.fullWidth)+2, m.footerHeight+2) + m.fileMetaData.SetDimensions( + utils.FooterWidth(m.fullWidth)+common.BorderPadding, + m.footerHeight+common.BorderPadding) } // TODO: Remove this code duplication with footer models func (m *model) setProcessBarModelSize() { - m.processBarModel.SetDimensions(utils.FooterWidth(m.fullWidth)+2, m.footerHeight+2) + m.processBarModel.SetDimensions( + utils.FooterWidth(m.fullWidth)+common.BorderPadding, + m.footerHeight+common.BorderPadding) } // Identify the current state of the application m and properly handle the @@ -563,7 +567,7 @@ func (m *model) View() string { if m.fullHeight < common.MinimumHeight || m.fullWidth < common.MinimumWidth { return m.terminalSizeWarnRender() } - if m.fileModel.width < 18 { + if m.fileModel.width < common.MinWidthForRename { return m.terminalSizeWarnAfterFirstRender() } @@ -614,22 +618,22 @@ func (m *model) updateRenderForOverlay(finalRender string) string { // check if need pop up modal if m.helpMenu.open { helpMenu := m.helpMenuRender() - overlayX := m.fullWidth/2 - m.helpMenu.width/2 - overlayY := m.fullHeight/2 - m.helpMenu.height/2 + overlayX := m.fullWidth/common.CenterDivisor - m.helpMenu.width/common.CenterDivisor + overlayY := m.fullHeight/common.CenterDivisor - m.helpMenu.height/common.CenterDivisor return stringfunction.PlaceOverlay(overlayX, overlayY, helpMenu, finalRender) } if m.promptModal.IsOpen() { promptModal := m.promptModalRender() - overlayX := m.fullWidth/2 - m.promptModal.GetWidth()/2 - overlayY := m.fullHeight/2 - m.promptModal.GetMaxHeight()/2 + overlayX := m.fullWidth/common.CenterDivisor - m.promptModal.GetWidth()/common.CenterDivisor + overlayY := m.fullHeight/common.CenterDivisor - m.promptModal.GetMaxHeight()/common.CenterDivisor return stringfunction.PlaceOverlay(overlayX, overlayY, promptModal, finalRender) } if m.zoxideModal.IsOpen() { zoxideModal := m.zoxideModalRender() - overlayX := m.fullWidth/2 - m.zoxideModal.GetWidth()/2 - overlayY := m.fullHeight/2 - m.zoxideModal.GetMaxHeight()/2 + overlayX := m.fullWidth/common.CenterDivisor - m.zoxideModal.GetWidth()/common.CenterDivisor + overlayY := m.fullHeight/common.CenterDivisor - m.zoxideModal.GetMaxHeight()/common.CenterDivisor return stringfunction.PlaceOverlay(overlayX, overlayY, zoxideModal, finalRender) } @@ -637,29 +641,29 @@ func (m *model) updateRenderForOverlay(finalRender string) string { if panel.sortOptions.open { sortOptions := m.sortOptionsRender() - overlayX := m.fullWidth/2 - panel.sortOptions.width/2 - overlayY := m.fullHeight/2 - panel.sortOptions.height/2 + overlayX := m.fullWidth/common.CenterDivisor - panel.sortOptions.width/common.CenterDivisor + overlayY := m.fullHeight/common.CenterDivisor - panel.sortOptions.height/common.CenterDivisor return stringfunction.PlaceOverlay(overlayX, overlayY, sortOptions, finalRender) } if m.firstUse { introduceModal := m.introduceModalRender() - overlayX := m.fullWidth/2 - m.helpMenu.width/2 - overlayY := m.fullHeight/2 - m.helpMenu.height/2 + overlayX := m.fullWidth/common.CenterDivisor - m.helpMenu.width/common.CenterDivisor + overlayY := m.fullHeight/common.CenterDivisor - m.helpMenu.height/common.CenterDivisor return stringfunction.PlaceOverlay(overlayX, overlayY, introduceModal, finalRender) } if m.typingModal.open { typingModal := m.typineModalRender() - overlayX := m.fullWidth/2 - common.ModalWidth/2 - overlayY := m.fullHeight/2 - common.ModalHeight/2 + overlayX := m.fullWidth/common.CenterDivisor - common.ModalWidth/common.CenterDivisor + overlayY := m.fullHeight/common.CenterDivisor - common.ModalHeight/common.CenterDivisor return stringfunction.PlaceOverlay(overlayX, overlayY, typingModal, finalRender) } if m.notifyModel.IsOpen() { notifyModal := m.notifyModel.Render() - overlayX := m.fullWidth/2 - common.ModalWidth/2 - overlayY := m.fullHeight/2 - common.ModalHeight/2 + overlayX := m.fullWidth/common.CenterDivisor - common.ModalWidth/common.CenterDivisor + overlayY := m.fullHeight/common.CenterDivisor - common.ModalHeight/common.CenterDivisor return stringfunction.PlaceOverlay(overlayX, overlayY, notifyModal, finalRender) } return finalRender @@ -728,7 +732,7 @@ func (m *model) shouldSkipPanelUpdate(filePanel *filePanel, focusPanel *filePane } focusPanelReRender := focusPanel.needsReRender() - reRenderTime := int(float64(len(filePanel.element)) / 100) + reRenderTime := int(float64(len(filePanel.element)) / common.ReRenderChunkDivisor) if filePanel.isFocused && !focusPanelReRender && nowTime.Sub(filePanel.lastTimeGetElement) < time.Duration(reRenderTime)*time.Second { return true diff --git a/src/internal/model_render.go b/src/internal/model_render.go index 73931a83d..9d17f5bde 100644 --- a/src/internal/model_render.go +++ b/src/internal/model_render.go @@ -42,7 +42,7 @@ func (m *model) filePanelRender() string { // TODO : Move this to a utility function and clarify the calculation via comments // Maybe even write unit tests var filePanelWidth int - if (m.fullWidth-common.Config.SidebarWidth-(4+(len(m.fileModel.filePanels)-1)*2))%len( + if (m.fullWidth-common.Config.SidebarWidth-(common.InnerPadding+(len(m.fileModel.filePanels)-1)*common.BorderPadding))%len( m.fileModel.filePanels, ) != 0 && i == len(m.fileModel.filePanels)-1 { @@ -50,7 +50,7 @@ func (m *model) filePanelRender() string { 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)) + (common.InnerPadding+(len(m.fileModel.filePanels)-1)*common.BorderPadding))%len(m.fileModel.filePanels)) } } else { filePanelWidth = m.fileModel.width @@ -62,7 +62,7 @@ func (m *model) filePanelRender() string { } func (panel *filePanel) Render(mainPanelHeight int, filePanelWidth int, focussed bool) string { - r := ui.FilePanelRenderer(mainPanelHeight+2, filePanelWidth+2, focussed) + r := ui.FilePanelRenderer(mainPanelHeight+common.BorderPadding, filePanelWidth+common.BorderPadding, focussed) panel.renderTopBar(r, filePanelWidth) panel.renderSearchBar(r) @@ -74,7 +74,7 @@ func (panel *filePanel) Render(mainPanelHeight int, filePanelWidth int, focussed func (panel *filePanel) renderTopBar(r *rendering.Renderer, filePanelWidth int) { // TODO - Add ansitruncate left in renderer and remove truncation here - truncatedPath := common.TruncateTextBeginning(panel.location, filePanelWidth-4, "...") + truncatedPath := common.TruncateTextBeginning(panel.location, filePanelWidth-common.InnerPadding, "...") r.AddLines(common.FilePanelTopDirectoryIcon + common.FilePanelTopPathStyle.Render(truncatedPath)) r.AddSection() } @@ -210,11 +210,11 @@ func (m *model) clipboardRender() string { // render var bottomWidth int if m.fullWidth%3 != 0 { - bottomWidth = utils.FooterWidth(m.fullWidth + m.fullWidth%3 + 2) + bottomWidth = utils.FooterWidth(m.fullWidth + m.fullWidth%common.FooterGroupCols + common.BorderPadding) } else { bottomWidth = utils.FooterWidth(m.fullWidth) } - r := ui.ClipboardRenderer(m.footerHeight+2, bottomWidth+2) + r := ui.ClipboardRenderer(m.footerHeight+common.BorderPadding, bottomWidth+common.BorderPadding) if len(m.copyItems.items) == 0 { // TODO move this to a string r.AddLines("", " "+icon.Error+" No content in clipboard") @@ -233,7 +233,7 @@ func (m *model) clipboardRender() string { // TODO : There is an inconsistency in parameter that is being passed, // and its name in ClipboardPrettierName function r.AddLines(common.ClipboardPrettierName(m.copyItems.items[i], - utils.FooterWidth(m.fullWidth)-3, fileInfo.IsDir(), isLink, false)) + utils.FooterWidth(m.fullWidth)-common.PanelPadding, fileInfo.IsDir(), isLink, false)) } } } @@ -265,7 +265,9 @@ func (m *model) terminalSizeWarnRender() string { } func (m *model) terminalSizeWarnAfterFirstRender() string { - minimumWidthInt := common.Config.SidebarWidth + 20*len(m.fileModel.filePanels) + 20 - 1 + minimumWidthInt := common.Config.SidebarWidth + common.FilePanelWidthUnit*len( + m.fileModel.filePanels, + ) + common.FilePanelWidthUnit - 1 minimumWidthString := strconv.Itoa(minimumWidthInt) fullWidthString := strconv.Itoa(m.fullWidth) fullHeightString := strconv.Itoa(m.fullHeight) @@ -294,7 +296,7 @@ func (m *model) typineModalRender() string { fileLocation := common.FilePanelTopDirectoryIconStyle.Render(" "+icon.Directory+icon.Space) + common.FilePanelTopPathStyle.Render( - common.TruncateTextBeginning(previewPath, common.ModalWidth-4, "..."), + common.TruncateTextBeginning(previewPath, common.ModalWidth-common.InnerPadding, "..."), ) + "\n" confirm := common.ModalConfirm.Render(" (" + common.Hotkeys.ConfirmTyping[0] + ") Create ") @@ -360,15 +362,15 @@ func (m *model) helpMenuRender() string { totalKeyLen += len(key) } - separatorLen := max(0, (len(data.hotkey)-1)) * 3 + separatorLen := max(0, (len(data.hotkey)-1)) * common.FooterGroupCols if data.subTitle == "" && totalKeyLen+separatorLen > maxKeyLength { maxKeyLength = totalKeyLen + separatorLen } } - valueLength := m.helpMenu.width - maxKeyLength - 2 - if valueLength < m.helpMenu.width/2 { - valueLength = m.helpMenu.width/2 - 2 + valueLength := m.helpMenu.width - maxKeyLength - common.BorderPadding + if valueLength < m.helpMenu.width/common.CenterDivisor { + valueLength = m.helpMenu.width/common.CenterDivisor - common.BorderPadding } totalTitleCount := 0 @@ -398,7 +400,7 @@ func (m *model) helpMenuRender() string { func (m *model) getRenderHotkeyLengthHelpmenuModal() int { renderHotkeyLength := 0 - for i := m.helpMenu.renderIndex; i < m.helpMenu.renderIndex+(m.helpMenu.height-4) && i < len(m.helpMenu.filteredData); i++ { + for i := m.helpMenu.renderIndex; i < m.helpMenu.renderIndex+(m.helpMenu.height-common.InnerPadding) && i < len(m.helpMenu.filteredData); i++ { hotkey := "" if m.helpMenu.filteredData[i].subTitle != "" { @@ -418,7 +420,7 @@ func (m *model) getRenderHotkeyLengthHelpmenuModal() int { } func (m *model) getHelpMenuContent(r *rendering.Renderer, renderHotkeyLength int, valueLength int) { - for i := m.helpMenu.renderIndex; i < m.helpMenu.renderIndex+(m.helpMenu.height-4) && i < len(m.helpMenu.filteredData); i++ { + for i := m.helpMenu.renderIndex; i < m.helpMenu.renderIndex+(m.helpMenu.height-common.InnerPadding) && i < len(m.helpMenu.filteredData); i++ { if m.helpMenu.filteredData[i].subTitle != "" { r.AddLines(common.HelpMenuTitleStyle.Render(" " + m.helpMenu.filteredData[i].subTitle)) continue @@ -454,7 +456,7 @@ func (m *model) sortOptionsRender() string { sortOptionsContent += cursor + common.ModalStyle.Render(" "+option) + "\n" } bottomBorder := common.GenerateFooterBorder(fmt.Sprintf("%s/%s", strconv.Itoa(panel.sortOptions.cursor+1), - strconv.Itoa(len(panel.sortOptions.data.options))), panel.sortOptions.width-2) + strconv.Itoa(len(panel.sortOptions.data.options))), panel.sortOptions.width-common.BorderPadding) return common.SortOptionsModalBorderStyle(panel.sortOptions.height, panel.sortOptions.width, bottomBorder).Render(sortOptionsContent) diff --git a/src/internal/type_utils.go b/src/internal/type_utils.go index 0e85fbc36..36aa380c5 100644 --- a/src/internal/type_utils.go +++ b/src/internal/type_utils.go @@ -28,7 +28,7 @@ func (m *model) validateLayout() error { return fmt.Errorf("footer closed and footerHeight %v is non zero", m.footerHeight) } // PanelHeight + 2 lines (main border) + actual footer height - if m.fullHeight != (m.mainPanelHeight+2)+utils.FullFooterHeight(m.footerHeight, m.toggleFooter) { + if m.fullHeight != (m.mainPanelHeight+common.BorderPadding)+utils.FullFooterHeight(m.footerHeight, m.toggleFooter) { return fmt.Errorf("invalid model layout, fullHeight : %v, mainPanelHeight : %v, footerHeight : %v", m.fullHeight, m.mainPanelHeight, m.footerHeight) } @@ -62,7 +62,9 @@ func defaultFilePanel(path string, focused bool) filePanel { cursor: 0, location: panelPath, sortOptions: sortOptionsModel{ - width: 20, + //nolint:mnd // default sort options dimensions + width: 20, + //nolint:mnd // default sort options dimensions height: 4, open: false, cursor: common.Config.DefaultSortType, diff --git a/src/internal/ui/metadata/const.go b/src/internal/ui/metadata/const.go index a4fea4183..2c68b9108 100644 --- a/src/internal/ui/metadata/const.go +++ b/src/internal/ui/metadata/const.go @@ -17,14 +17,16 @@ const keyMd5Checksum = "MD5Checksum" const keyOwner = "Owner" const keyGroup = "Group" const keyPath = "Path" +const borderSize = 2 var sortPriority = map[string]int{ //nolint: gochecknoglobals // This is effectively const. + // Metadata field priority indices for display ordering keyName: 0, keySize: 1, - keyDataModified: 2, - keyDataAccessed: 3, - keyPermissions: 4, - keyOwner: 5, - keyGroup: 6, - keyPath: 7, + keyDataModified: 2, //nolint:mnd // display order index + keyDataAccessed: 3, //nolint:mnd // display order index + keyPermissions: 4, //nolint:mnd // display order index + keyOwner: 5, //nolint:mnd // display order index + keyGroup: 6, //nolint:mnd // display order index + keyPath: 7, //nolint:mnd // display order index } diff --git a/src/internal/ui/metadata/model.go b/src/internal/ui/metadata/model.go index 4de3eb27d..ce7eaa99c 100644 --- a/src/internal/ui/metadata/model.go +++ b/src/internal/ui/metadata/model.go @@ -93,7 +93,7 @@ func (m *Model) Render(metadataFocussed bool) string { } keyLen, valueLen := computeRenderDimensions(m.metadata.data, m.width-2-keyValueSpacingLen) r.SetBorderInfoItems(fmt.Sprintf("%d/%d", m.renderIndex+1, len(m.metadata.data))) - lines := formatMetadataLines(m.metadata.data, m.renderIndex, m.height-2, keyLen, valueLen) + lines := formatMetadataLines(m.metadata.data, m.renderIndex, m.height-borderSize, keyLen, valueLen) r.AddLines(lines...) return r.Render() } diff --git a/src/internal/ui/metadata/utils.go b/src/internal/ui/metadata/utils.go index f511555ba..e8acd0954 100644 --- a/src/internal/ui/metadata/utils.go +++ b/src/internal/ui/metadata/utils.go @@ -24,6 +24,7 @@ func computeMetadataWidths(viewWidth, maxKeyLen int) (int, int) { keyLen := maxKeyLen valueLen := viewWidth - keyLen if valueLen < viewWidth/2 { + //nolint:mnd // standard halving for center split valueLen = viewWidth / 2 keyLen = viewWidth - valueLen } diff --git a/src/internal/ui/preview/model.go b/src/internal/ui/preview/model.go index bacf56079..69ea6cdc9 100644 --- a/src/internal/ui/preview/model.go +++ b/src/internal/ui/preview/model.go @@ -13,7 +13,6 @@ import ( "slices" "sort" "strings" - "time" "github.com/yorukot/superfile/src/internal/ui" "github.com/yorukot/superfile/src/internal/ui/rendering" @@ -323,7 +322,7 @@ func getBatSyntaxHighlightedContent( batArgs := []string{itemPath, "--plain", "--force-colorization", "--line-range", fmt.Sprintf(":%d", previewLine-1)} // set timeout for the external command execution to 500ms max - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), common.DefaultPreviewTimeout) defer cancel() cmd := exec.CommandContext(ctx, batCmd, batArgs...) diff --git a/src/internal/ui/processbar/const.go b/src/internal/ui/processbar/const.go index 65dcb6454..3c5e97c97 100644 --- a/src/internal/ui/processbar/const.go +++ b/src/internal/ui/processbar/const.go @@ -8,4 +8,17 @@ const ( // This should allow smooth tracking of 5-10 active processes // In case we have issues in future, we could attempt to change this msgChannelSize = 50 + + // UI dimension constants for process bar rendering + // borderSize is the border width for the process bar panel + borderSize = 2 + + // progressBarRightPadding is padding after progress bar + progressBarRightPadding = 3 + + // processNameTruncatePadding is the space reserved for ellipsis and icon in process name + processNameTruncatePadding = 7 + + // linesPerProcess is the number of lines needed to render one process + linesPerProcess = 3 ) diff --git a/src/internal/ui/processbar/model.go b/src/internal/ui/processbar/model.go index 6c39e2156..c55fdcff8 100644 --- a/src/internal/ui/processbar/model.go +++ b/src/internal/ui/processbar/model.go @@ -119,7 +119,7 @@ func (m *Model) Render(processBarFocussed bool) string { // TODO: We could, save pointer of process in map and update progressbar of each // map on each SetWidth. This would be cleaner and more efficient. curProcess := processes[i] - curProcess.Progress.Width = m.viewWidth() - 3 + curProcess.Progress.Width = m.viewWidth() - progressBarRightPadding // TODO : get them via a separate function. var cursor string @@ -131,7 +131,7 @@ func (m *Model) Render(processBarFocussed bool) string { } r.AddLines(cursor + common.FooterStyle.Render( - common.TruncateText(curProcess.Name, m.viewWidth()-7, "...")+" ") + + common.TruncateText(curProcess.Name, m.viewWidth()-processNameTruncatePadding, "...")+" ") + curProcess.State.Icon()) // calculate progress percentage diff --git a/src/internal/ui/processbar/model_navigation.go b/src/internal/ui/processbar/model_navigation.go index 1999606b5..8c4828e25 100644 --- a/src/internal/ui/processbar/model_navigation.go +++ b/src/internal/ui/processbar/model_navigation.go @@ -46,5 +46,5 @@ func (m *Model) ListDown(footerHeight int) { func cntRenderableProcess(footerHeight int) int { // We can render one process in three lines // And last process in two or three lines ( with/without a line separtor) - return (footerHeight + 1) / 3 + return (footerHeight + 1) / linesPerProcess } diff --git a/src/internal/ui/processbar/model_utils.go b/src/internal/ui/processbar/model_utils.go index aae17e87d..7301a0e4d 100644 --- a/src/internal/ui/processbar/model_utils.go +++ b/src/internal/ui/processbar/model_utils.go @@ -12,15 +12,15 @@ func (m *Model) cntProcesses() int { func (m *Model) isValid() bool { return m.renderIndex <= m.cursor && - m.cursor <= m.renderIndex+cntRenderableProcess(m.height-2)-1 + m.cursor <= m.renderIndex+cntRenderableProcess(m.height-borderSize)-1 } func (m *Model) viewHeight() int { - return m.height - 2 + return m.height - borderSize } func (m *Model) viewWidth() int { - return m.width - 2 + return m.width - borderSize } func (m *Model) getSortedProcesses() []Process { diff --git a/src/internal/ui/prompt/consts.go b/src/internal/ui/prompt/consts.go index 202e5264a..39a73a34d 100644 --- a/src/internal/ui/prompt/consts.go +++ b/src/internal/ui/prompt/consts.go @@ -36,6 +36,13 @@ const ( defaultTestWidth = 100 defaultTestMaxHeight = 100 + + // UI dimension constants for prompt modal + // promptInputPadding is total padding for prompt input fields + promptInputPadding = 6 // 2 + 1 + 2 + 1 (borders and spacing) + + // expectedArgCount is the expected number of prompt arguments + expectedArgCount = 2 ) func modeString(shellMode bool) string { diff --git a/src/internal/ui/prompt/model.go b/src/internal/ui/prompt/model.go index e6d19f7db..6eb6b7ac5 100644 --- a/src/internal/ui/prompt/model.go +++ b/src/internal/ui/prompt/model.go @@ -212,7 +212,7 @@ func (m *Model) SetWidth(width int) { m.width = width // Excluding borders(2), SpacePadding(1), Prompt(2), and one extra character that is appended // by textInput.View() - m.textInput.Width = width - 2 - 1 - 2 - 1 + m.textInput.Width = width - promptInputPadding } func (m *Model) SetMaxHeight(maxHeight int) { diff --git a/src/internal/ui/prompt/utils.go b/src/internal/ui/prompt/utils.go index 3ce311dfb..92aaedc8b 100644 --- a/src/internal/ui/prompt/utils.go +++ b/src/internal/ui/prompt/utils.go @@ -35,7 +35,7 @@ func getPromptAction(shellMode bool, value string, cwdLocation string) (common.M } return common.SplitPanelAction{}, nil case "cd": - if len(promptArgs) != 2 { + if len(promptArgs) != expectedArgCount { return noAction, invalidCmdError{ uiMsg: fmt.Sprintf("cd command needs exactly one argument, received %d", len(promptArgs)-1), @@ -45,7 +45,7 @@ func getPromptAction(shellMode bool, value string, cwdLocation string) (common.M Location: promptArgs[1], }, nil case "open": - if len(promptArgs) != 2 { + if len(promptArgs) != expectedArgCount { return noAction, invalidCmdError{ uiMsg: fmt.Sprintf("open command needs exactly one argument, received %d", len(promptArgs)-1), diff --git a/src/internal/ui/rendering/border.go b/src/internal/ui/rendering/border.go index 0bca15b8b..d9ee7b149 100644 --- a/src/internal/ui/rendering/border.go +++ b/src/internal/ui/rendering/border.go @@ -53,9 +53,9 @@ func (b *BorderConfig) AreInfoItemsTruncated() bool { return false } - actualWidth := b.width - 2 + actualWidth := b.width - borderCornerWidth // border.MiddleLeft border.MiddleRight border.Bottom - availWidth := actualWidth/cnt - 3 + availWidth := actualWidth/cnt - borderDividerWidth for i := range b.infoItems { if ansi.StringWidth(b.infoItems[i]) > availWidth { return true @@ -77,19 +77,19 @@ func (b *BorderConfig) GetBorder(borderStrings lipgloss.Border) lipgloss.Border res := borderStrings // excluding corners. Maybe we can move this to a utility function - actualWidth := b.width - 2 - actualHeight := b.height - 2 + actualWidth := b.width - borderCornerWidth + actualHeight := b.height - borderCornerWidth // Min 5 width is needed for title so that at least one character can be // rendered - if b.title != "" && actualWidth >= 5 { + if b.title != "" && actualWidth >= minTitleWidth { // We need to plain truncate the title if needed. // topWidth - 1( for BorderMiddleLeft) - 1 (for BorderMiddleRight) - 2 (padding) - titleAvailWidth := actualWidth - 4 + titleAvailWidth := actualWidth - borderCornerWidth - borderPaddingWidth // Basic Right truncation truncatedTitle := ansi.Truncate(b.title, titleAvailWidth, "") - remainingWidth := actualWidth - 4 - ansi.StringWidth(truncatedTitle) + remainingWidth := actualWidth - borderCornerWidth - borderPaddingWidth - ansi.StringWidth(truncatedTitle) margin := "" if remainingWidth > b.titleLeftMargin { @@ -104,10 +104,10 @@ func (b *BorderConfig) GetBorder(borderStrings lipgloss.Border) lipgloss.Border cnt := len(b.infoItems) // Minimum 4 character for each info item so that at least first character is rendered - if cnt > 0 && actualWidth >= cnt*4 { + if cnt > 0 && actualWidth >= cnt*minInfoItemWidth { // Max available width for each item's actual content // border.MiddleLeft border.MiddleRight border.Bottom - availWidth := actualWidth/cnt - 3 + availWidth := actualWidth/cnt - borderDividerWidth infoText := "" for _, item := range b.infoItems { item = ansi.Truncate(item, availWidth, "") diff --git a/src/internal/ui/rendering/constants.go b/src/internal/ui/rendering/constants.go new file mode 100644 index 000000000..8ecbd089e --- /dev/null +++ b/src/internal/ui/rendering/constants.go @@ -0,0 +1,19 @@ +package rendering + +// Border rendering constants +const ( + // borderCornerWidth is the width occupied by border corners + borderCornerWidth = 2 + + // borderPaddingWidth is padding around border title/content + borderPaddingWidth = 2 + + // borderDividerWidth is width for middle dividers (MiddleLeft + MiddleRight + Bottom) + borderDividerWidth = 3 + + // minTitleWidth is minimum width needed to render at least 1 char of title + minTitleWidth = 5 + + // minInfoItemWidth is minimum width for each info item to render at least 1 char + minInfoItemWidth = 4 +) diff --git a/src/internal/ui/sidebar/consts.go b/src/internal/ui/sidebar/consts.go index 34d9959b3..945564912 100644 --- a/src/internal/ui/sidebar/consts.go +++ b/src/internal/ui/sidebar/consts.go @@ -14,3 +14,15 @@ var diskDividerDir = directory{ //nolint: gochecknoglobals // This is more like // superfile logo + blank line + search bar const sideBarInitialHeight = 3 + +// UI dimension constants for sidebar +const ( + // searchBarPadding is the total padding for search bar (borders + prompt + extra char) + searchBarPadding = 5 // 2 (borders) + 2 (prompt) + 1 (extra char) + + // directoryCapacityExtra is extra capacity for separator lines in directory list + directoryCapacityExtra = 2 + + // defaultRenderHeight is the default height when no height is available + defaultRenderHeight = 3 +) diff --git a/src/internal/ui/sidebar/directory_utils.go b/src/internal/ui/sidebar/directory_utils.go index 36ffc0d7a..2a7ab0eb3 100644 --- a/src/internal/ui/sidebar/directory_utils.go +++ b/src/internal/ui/sidebar/directory_utils.go @@ -72,7 +72,7 @@ func getFilteredDirectories(query string, pinnedMgr *PinnedManager) []directory func formDirctorySlice(homeDirectories []directory, pinnedDirectories []directory, diskDirectories []directory) []directory { // Preallocation for efficiency - totalCapacity := len(homeDirectories) + len(pinnedDirectories) + len(diskDirectories) + 2 + totalCapacity := len(homeDirectories) + len(pinnedDirectories) + len(diskDirectories) + directoryCapacityExtra directories := make([]directory, 0, totalCapacity) directories = append(directories, homeDirectories...) diff --git a/src/internal/ui/sidebar/render.go b/src/internal/ui/sidebar/render.go index 468179f2d..65e81faaa 100644 --- a/src/internal/ui/sidebar/render.go +++ b/src/internal/ui/sidebar/render.go @@ -19,7 +19,10 @@ func (s *Model) Render(mainPanelHeight int, sidebarFocussed bool, currentFilePan "renderIndex", s.renderIndex, "dirs count", len(s.directories), "sidebar focused", sidebarFocussed) - r := ui.SidebarRenderer(mainPanelHeight+2, common.Config.SidebarWidth+2, sidebarFocussed) + r := ui.SidebarRenderer( + mainPanelHeight+common.BorderPadding, + common.Config.SidebarWidth+common.BorderPadding, + sidebarFocussed) r.AddLines(common.SideBarSuperfileTitle, "") diff --git a/src/internal/ui/sidebar/sidebar.go b/src/internal/ui/sidebar/sidebar.go index f08b7da18..1c2805797 100644 --- a/src/internal/ui/sidebar/sidebar.go +++ b/src/internal/ui/sidebar/sidebar.go @@ -116,7 +116,7 @@ func New() Model { // Excluding borders(2), Searchbar Prompt(2), and one extra character than is appended // by searchBar.View() - res.searchBar.Width = common.Config.SidebarWidth - 2 - 2 - 1 + res.searchBar.Width = common.Config.SidebarWidth - searchBarPadding res.searchBar.Placeholder = "(" + common.Hotkeys.SearchBar[0] + ")" + " Search" return res } diff --git a/src/internal/ui/sidebar/utils.go b/src/internal/ui/sidebar/utils.go index 6f051af52..d11f242af 100644 --- a/src/internal/ui/sidebar/utils.go +++ b/src/internal/ui/sidebar/utils.go @@ -5,7 +5,7 @@ func (d directory) IsDivider() bool { } func (d directory) RequiredHeight() int { if d.IsDivider() { - return 3 + return defaultRenderHeight } return 1 } diff --git a/src/internal/ui/zoxide/consts.go b/src/internal/ui/zoxide/consts.go index fd1d41b92..75dabaa45 100644 --- a/src/internal/ui/zoxide/consts.go +++ b/src/internal/ui/zoxide/consts.go @@ -7,4 +7,11 @@ const ( ZoxideMinHeight = 3 maxVisibleResults = 5 // Maximum number of results visible at once + + // UI dimension constants for zoxide modal + // scoreColumnWidth is width reserved for score display (including padding and separator) + scoreColumnWidth = 13 // borders(2) + padding(2) + score(6) + separator(3) + + // modalInputPadding is total padding for modal input fields + modalInputPadding = 6 // 2 + 1 + 2 + 1 (borders and spacing) ) diff --git a/src/internal/ui/zoxide/render.go b/src/internal/ui/zoxide/render.go index a4d42ede0..9e55f0d83 100644 --- a/src/internal/ui/zoxide/render.go +++ b/src/internal/ui/zoxide/render.go @@ -51,7 +51,7 @@ func (m *Model) renderVisibleResults(r *rendering.Renderer, endIndex int) { // - separator(3) = width - 13 // 0123456789012345678 => 19 width, path gets 6 // | 9999.9 | | - availablePathWidth := m.width - 13 + availablePathWidth := m.width - scoreColumnWidth path := common.TruncateTextBeginning(result.Path, availablePathWidth, "...") line := fmt.Sprintf(" %6.1f | %s", result.Score, path) diff --git a/src/internal/ui/zoxide/test_helpers.go b/src/internal/ui/zoxide/test_helpers.go index e9624b698..a809202fe 100644 --- a/src/internal/ui/zoxide/test_helpers.go +++ b/src/internal/ui/zoxide/test_helpers.go @@ -10,7 +10,7 @@ import ( ) func setupTestModel() Model { - return GenerateModel(nil, 50, 80) + return GenerateModel(nil, 50, 80) //nolint:mnd // test dimensions } func setupTestModelWithClient(t *testing.T) Model { @@ -23,7 +23,7 @@ func setupTestModelWithClient(t *testing.T) Model { t.Fatalf("zoxide initialization failed") } } - return GenerateModel(zClient, 50, 80) + return GenerateModel(zClient, 50, 80) //nolint:mnd // test dimensions } func setupTestModelWithResults(resultCount int) Model { @@ -32,7 +32,7 @@ func setupTestModelWithResults(resultCount int) Model { for i := range resultCount { m.results[i] = zoxidelib.Result{ Path: "/test/path" + string(rune('0'+i)), - Score: float64(100 - i*10), + Score: float64(100 - i*10), //nolint:mnd // test scores } } return m diff --git a/src/internal/ui/zoxide/utils.go b/src/internal/ui/zoxide/utils.go index 0df45e6bc..f9c162e1c 100644 --- a/src/internal/ui/zoxide/utils.go +++ b/src/internal/ui/zoxide/utils.go @@ -47,7 +47,7 @@ func (m *Model) SetWidth(width int) { m.width = width // Excluding borders(2), SpacePadding(1), Prompt(2), and one extra character that is appended // by textInput.View() - m.textInput.Width = width - 2 - 1 - 2 - 1 + m.textInput.Width = width - modalInputPadding } func (m *Model) SetMaxHeight(maxHeight int) { diff --git a/src/internal/utils/ui_utils.go b/src/internal/utils/ui_utils.go index ca1490733..e1a55a3ad 100644 --- a/src/internal/utils/ui_utils.go +++ b/src/internal/utils/ui_utils.go @@ -1,16 +1,22 @@ package utils +// FilePanelMax defines the maximum number of file panels +const FilePanelMax = 3 + +// BorderPaddingForFooter is the border padding for footer calculations +const BorderPaddingForFooter = 2 + // We have three panels, so 6 characters for border // <---><---><---> // Hence we have (fullWidth - 6) / 3 = fullWidth/3 - 2 func FooterWidth(fullWidth int) int { - return fullWidth/3 - 2 + return fullWidth/FilePanelMax - BorderPaddingForFooter } // Including borders func FullFooterHeight(footerHeight int, toggleFooter bool) int { if toggleFooter { - return footerHeight + 2 + return footerHeight + BorderPaddingForFooter } return 0 } diff --git a/src/pkg/file_preview/constants.go b/src/pkg/file_preview/constants.go new file mode 100644 index 000000000..7e9988875 --- /dev/null +++ b/src/pkg/file_preview/constants.go @@ -0,0 +1,29 @@ +package filepreview + +import "time" + +// Image preview constants +const ( + // Cache configuration + defaultThumbnailCacheSize = 100 // Default number of thumbnails to cache + defaultCacheExpiration = 5 * time.Minute + + // Image processing + heightScaleFactor = 2 // Factor for height scaling in terminal display + rgbShift16 = 16 // Bit shift for red channel in RGB operations + rgbShift8 = 8 // Bit shift for green channel in RGB operations + + // Kitty protocol + kittyHashSeed = 42 // Seed for kitty image ID hashing + kittyHashPrime = 31 // Prime multiplier for hash calculation + kittyMaxID = 0xFFFF // Maximum ID value for kitty images + kittyNonZeroOffset = 1000 // Offset to ensure non-zero IDs + + // RGB color masks + rgbMask = 0xFF // Mask for extracting 8-bit RGB channel values + alphaOpaque = 255 // Fully opaque alpha channel value + + maxVideoFileSizeForThumb = "104857600" // 100MB limit + thumbOutputExt = ".jpg" + thumbGenerationTimeout = 30 * time.Second +) diff --git a/src/pkg/file_preview/image_preview.go b/src/pkg/file_preview/image_preview.go index c3be5eded..0d1eb191a 100644 --- a/src/pkg/file_preview/image_preview.go +++ b/src/pkg/file_preview/image_preview.go @@ -49,7 +49,7 @@ type ImagePreviewer struct { // NewImagePreviewer creates a new ImagePreviewer with default cache settings func NewImagePreviewer() *ImagePreviewer { - return NewImagePreviewerWithConfig(100, 5*time.Minute) + return NewImagePreviewerWithConfig(defaultThumbnailCacheSize, defaultCacheExpiration) } // NewImagePreviewerWithConfig creates a new ImagePreviewer with custom cache configuration @@ -81,6 +81,7 @@ func NewImagePreviewCache(maxEntries int, expiration time.Duration) *ImagePrevie // periodicCleanup removes expired entries periodically func (c *ImagePreviewCache) periodicCleanup() { + //nolint:mnd // half of expiration for cleanup interval ticker := time.NewTicker(c.expiration / 2) defer ticker.Stop() @@ -323,10 +324,15 @@ func hexToColor(hex string) (color.RGBA, error) { if err != nil { return color.RGBA{}, err } - return color.RGBA{R: uint8(values >> 16), G: uint8((values >> 8) & 0xFF), B: uint8(values & 0xFF), A: 255}, nil + return color.RGBA{ + R: uint8(values >> rgbShift16), + G: uint8((values >> rgbShift8) & rgbMask), + B: uint8(values & rgbMask), + A: alphaOpaque, + }, nil } func colorToHex(color color.Color) string { r, g, b, _ := color.RGBA() - return fmt.Sprintf("#%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8)) + return fmt.Sprintf("#%02x%02x%02x", uint8(r>>rgbShift8), uint8(g>>rgbShift8), uint8(b>>rgbShift8)) } diff --git a/src/pkg/file_preview/image_resize.go b/src/pkg/file_preview/image_resize.go index 0ac228b52..af4e66f85 100644 --- a/src/pkg/file_preview/image_resize.go +++ b/src/pkg/file_preview/image_resize.go @@ -71,19 +71,19 @@ func adjustOrientation(img image.Image, orientation int) image.Image { switch orientation { case 1: return img - case 2: + case 2: //nolint:mnd // EXIF orientation: horizontal flip return imaging.FlipH(img) - case 3: + case 3: //nolint:mnd // EXIF orientation: 180 rotation return imaging.Rotate180(img) - case 4: + case 4: //nolint:mnd // EXIF orientation: vertical flip return imaging.FlipV(img) - case 5: + case 5: //nolint:mnd // EXIF orientation: transpose return imaging.Transpose(img) - case 6: + case 6: //nolint:mnd // EXIF orientation: 270 rotation return imaging.Rotate270(img) - case 7: + case 7: //nolint:mnd // EXIF orientation: transverse return imaging.Transverse(img) - case 8: + case 8: //nolint:mnd // EXIF orientation: 90 rotation return imaging.Rotate90(img) default: slog.Error("Invalid orientation value", "error", orientation) @@ -94,5 +94,5 @@ func adjustOrientation(img image.Image, orientation int) image.Image { // resizeForANSI resizes image specifically for ANSI rendering func resizeForANSI(img image.Image, maxWidth, maxHeight int) image.Image { // Use maxHeight*2 because each terminal row represents 2 pixel rows in ANSI rendering - return imaging.Fit(img, maxWidth, maxHeight*2, imaging.Lanczos) + return imaging.Fit(img, maxWidth, maxHeight*heightScaleFactor, imaging.Lanczos) } diff --git a/src/pkg/file_preview/kitty.go b/src/pkg/file_preview/kitty.go index 52f134558..0602e0288 100644 --- a/src/pkg/file_preview/kitty.go +++ b/src/pkg/file_preview/kitty.go @@ -82,14 +82,14 @@ func generateKittyClearCommands() string { // generatePlacementID generates a unique placement ID based on file path func generatePlacementID(path string) uint32 { if len(path) == 0 { - return 42 // Default fallback + return kittyHashSeed // Default fallback } hash := 0 for _, c := range path { - hash = hash*31 + int(c) + hash = hash*kittyHashPrime + int(c) } - return uint32(hash&0xFFFF) + 1000 // Ensure it's not 0 and avoid low numbers + return uint32(hash&kittyMaxID) + kittyNonZeroOffset // Ensure it's not 0 and avoid low numbers } // renderWithKittyUsingTermCap renders an image using Kitty graphics protocol with terminal capabilities diff --git a/src/pkg/file_preview/thumbnail_generator.go b/src/pkg/file_preview/thumbnail_generator.go index ce8f57dab..6d6b5fd7e 100644 --- a/src/pkg/file_preview/thumbnail_generator.go +++ b/src/pkg/file_preview/thumbnail_generator.go @@ -7,13 +7,6 @@ import ( "os/exec" "path/filepath" "sync" - "time" -) - -const ( - maxFileSize = "104857600" // 100MB limit - outputExt = ".jpg" - generationTimeout = 30 * time.Second ) type ThumbnailGenerator struct { @@ -76,14 +69,14 @@ func (g *ThumbnailGenerator) generateThumbnail(inputPath string) (string, error) filename := filepath.Base(inputPath) baseName := filename[:len(filename)-len(fileExt)] - outputFile, err := os.CreateTemp(g.tempDirectory, "*-"+baseName+outputExt) + outputFile, err := os.CreateTemp(g.tempDirectory, "*-"+baseName+thumbOutputExt) if err != nil { return "", err } outputFilePath := outputFile.Name() outputFile.Close() - ctx, cancel := context.WithTimeout(context.Background(), generationTimeout) + ctx, cancel := context.WithTimeout(context.Background(), thumbGenerationTimeout) defer cancel() // ffmpeg -v warning -t 60 -hwaccel auto -an -sn -dn -skip_frame nokey -i input.mkv -vf scale='min(1024,iw)':'min(720,ih)':force_original_aspect_ratio=decrease:flags=fast_bilinear -vf "thumbnail" -frames:v 1 -y thumb.jpg @@ -99,7 +92,7 @@ func (g *ThumbnailGenerator) generateThumbnail(inputPath string) (string, error) "-vf", "thumbnail", // use ffmpeg default thumbnail filter "-frames:v", "1", // output only one frame (one image) "-f", "image2", // set format to image2 - "-fs", maxFileSize, // limit the max file size to match image previewer limit + "-fs", maxVideoFileSizeForThumb, // limit the max file size to match image previewer limit "-y", outputFilePath, // set the outputFile and overwrite it without confirmation if already exists ) diff --git a/src/pkg/file_preview/utils.go b/src/pkg/file_preview/utils.go index e4ad19da6..29f38f307 100644 --- a/src/pkg/file_preview/utils.go +++ b/src/pkg/file_preview/utils.go @@ -11,6 +11,8 @@ import ( const ( DefaultPixelsPerColumn = 10 // approximate pixels per terminal column DefaultPixelsPerRow = 20 // approximate pixels per terminal row + WindowsPixelsPerColumn = 8 // Windows Terminal/CMD typical width + WindowsPixelsPerRow = 16 // Windows Terminal/CMD typical height ) // TerminalCellSize represents the pixel dimensions of terminal cells @@ -133,7 +135,7 @@ func getTerminalCellSizeWindows() (TerminalCellSize, bool) { // getWindowsDefaultCellSize returns reasonable defaults for Windows func getWindowsDefaultCellSize() TerminalCellSize { return TerminalCellSize{ - PixelsPerColumn: 8, // Windows Terminal/CMD typical width - PixelsPerRow: 16, // Windows Terminal/CMD typical height + PixelsPerColumn: WindowsPixelsPerColumn, // Windows Terminal/CMD typical width + PixelsPerRow: WindowsPixelsPerRow, // Windows Terminal/CMD typical height } }