Skip to content

Commit 3ba5109

Browse files
authored
Merge pull request #825 from yorukot/develop
fix: convert unicode space to normal space, use rendered in file preview to fix layout bugs, Release 1.3.0
2 parents 1a940d7 + 8313020 commit 3ba5109

File tree

17 files changed

+285
-49
lines changed

17 files changed

+285
-49
lines changed

flake.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
packages = rec {
2828
superfile = pkgs.buildGoApplication {
2929
pname = "superfile";
30-
version = "1.2.1";
30+
version = "1.3.0";
3131
src = ./.;
3232
modules = ./gomod2nix.toml;
3333
};

release/release.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env -S bash -euo pipefail
22

33
projectName="superfile"
4-
version="v1.2.1"
4+
version="v1.3.0"
55
osList=("darwin" "linux" "windows")
66
archList=("amd64" "arm64")
77
mkdir dist

src/config/fixed_variable.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
)
1212

1313
const (
14-
CurrentVersion = "v1.2.1"
14+
CurrentVersion = "v1.3.0"
1515
LatestVersionURL = "https://api.github.com/repos/yorukot/superfile/releases/latest"
1616
LatestVersionGithub = "github.com/yorukot/superfile/releases/latest"
1717

src/internal/common/predefined_variable.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,19 @@ var (
3232
FilePanelTopDirectoryIcon string
3333
FilePanelNoneText string
3434

35+
FilePreviewNoContentText string
36+
FilePreviewNoFileInfoText string
37+
FilePreviewUnsupportedFormatText string
38+
FilePreviewDirectoryUnreadableText string
39+
FilePreviewEmptyText string
40+
3541
LipglossError string
3642
)
3743

44+
var (
45+
UnsupportedPreviewFormats = []string{".pdf", ".torrent"}
46+
)
47+
3848
// No dependencies
3949
func LoadInitialPrerenderedVariables() {
4050
LipglossError = lipgloss.NewStyle().Foreground(lipgloss.Color("#F93939")).Render("Error") +
@@ -59,4 +69,10 @@ func LoadPrerenderedVariables() {
5969

6070
FilePanelTopDirectoryIcon = FilePanelTopDirectoryIconStyle.Render(" " + icon.Directory + icon.Space)
6171
FilePanelNoneText = FilePanelStyle.Render(" " + icon.Error + " No such file or directory")
72+
73+
FilePreviewNoContentText = "--- " + icon.Error + " No content to preview ---"
74+
FilePreviewNoFileInfoText = "--- " + icon.Error + " Could not get file info ---"
75+
FilePreviewUnsupportedFormatText = "--- " + icon.Error + " Unsupported formats ---"
76+
FilePreviewDirectoryUnreadableText = "--- " + icon.Error + " Cannot read directory ---"
77+
FilePreviewEmptyText = "--- Empty ---"
6278
}

src/internal/common/string_function.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,36 @@ func MakePrintableWithEscCheck(line string, allowEsc bool) string {
200200
if r == utf8.RuneError {
201201
continue
202202
}
203-
if r > 0x7f || unicode.IsGraphic(r) ||
204-
r == rune('\t') || r == rune('\n') || (allowEsc && r == rune('\x1b')) {
203+
// It needs to be handled separately since considered a space,
204+
// It is multi-byte in UTF-8, But it has zero display width
205+
if r == 0xa0 {
206+
sb.WriteRune(r)
207+
continue
208+
}
209+
// It needs to be handled separately since considered a space,
210+
// Since we are using ansi.StringWidth() for truncation, and \t is
211+
// considered zero width
212+
if r == '\t' {
213+
sb.WriteString(" ")
214+
continue
215+
}
216+
if r == 0x1b {
217+
if allowEsc {
218+
sb.WriteRune(r)
219+
}
220+
continue
221+
}
222+
if r > 0x7f {
223+
if unicode.IsSpace(r) && utf8.RuneLen(r) > 1 {
224+
// See https://github.com/charmbracelet/x/issues/466
225+
// Space chacters spanning more than one bytes are not handled well by
226+
// ansi.Wrap(), and so lipgloss.Render() has issues
227+
r = ' '
228+
}
229+
sb.WriteRune(r)
230+
continue
231+
}
232+
if unicode.IsGraphic(r) || r == rune('\n') {
205233
sb.WriteRune(r)
206234
}
207235
}

src/internal/common/string_function_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func TestMakePrintable(t *testing.T) {
127127
{"", ""},
128128
{"hello", "hello"},
129129
{"abcdABCD0123~!@#$%^&*()_+-={}|:\"<>?,./;'[]", "abcdABCD0123~!@#$%^&*()_+-={}|:\"<>?,./;'[]"},
130-
{"Horizontal Tab and NewLine\t\t\n\n", "Horizontal Tab and NewLine\t\t\n\n"},
130+
{"Horizontal Tab and NewLine\t\t\n\n", "Horizontal Tab and NewLine \n\n"},
131131
{"(NBSP)\u00a0\u00a0\u00a0\u00a0;", "(NBSP)\u00a0\u00a0\u00a0\u00a0;"},
132132
{"\x0b(Vertical Tab)", "(Vertical Tab)"},
133133
{"\x0d(CR)", "(CR)"},
@@ -141,12 +141,14 @@ func TestMakePrintable(t *testing.T) {
141141
{"Invalid Unicodes\ufffd", "Invalid Unicodes"},
142142
{"Invalid Unicodes\xa0", "Invalid Unicodes"},
143143
{"Ascii color sequence\x1b[38;2;230;219;116;48;2;39;40;34m\ue68f \x1b[0m", "Ascii color sequence\x1b[38;2;230;219;116;48;2;39;40;34m\ue68f \x1b[0m"},
144+
{"Unicodes spaces\u202f\u205f\u2029", "Unicodes spaces "},
145+
{"IDEOGRAPHIC SPACE\u3000", "IDEOGRAPHIC SPACE "},
144146
}
145147
for _, tt := range inputs {
146148
t.Run(fmt.Sprintf("Make %q printable", tt.input), func(t *testing.T) {
147149
result := MakePrintable(tt.input)
148150
if result != tt.expected {
149-
t.Errorf("Expected %v, got %v", tt.expected, result)
151+
t.Errorf("Expected '%v', got '%v' (input : '%v')", tt.expected, result, tt.input)
150152
}
151153
})
152154
}

src/internal/model_render.go

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"os/exec"
1212
"path/filepath"
13+
"slices"
1314
"sort"
1415
"strconv"
1516
"strings"
@@ -22,6 +23,7 @@ import (
2223

2324
"github.com/alecthomas/chroma/v2/lexers"
2425
"github.com/charmbracelet/lipgloss"
26+
"github.com/charmbracelet/x/exp/term/ansi"
2527
"github.com/yorukot/ansichroma"
2628
"github.com/yorukot/superfile/src/config/icon"
2729
filepreview "github.com/yorukot/superfile/src/pkg/file_preview"
@@ -537,8 +539,6 @@ func (m *model) sortOptionsRender() string {
537539
}
538540

539541
func readFileContent(filepath string, maxLineLength int, previewLine int) (string, error) {
540-
// String builder is much better for efficiency
541-
// See - https://stackoverflow.com/questions/1760757/how-to-efficiently-concatenate-strings-in-go/47798475#47798475
542542
var resultBuilder strings.Builder
543543
file, err := os.Open(filepath)
544544
if err != nil {
@@ -550,12 +550,9 @@ func readFileContent(filepath string, maxLineLength int, previewLine int) (strin
550550
lineCount := 0
551551
for scanner.Scan() {
552552
line := scanner.Text()
553-
if len(line) > maxLineLength {
554-
line = line[:maxLineLength]
555-
}
556-
// This is critical to avoid layout break, removes non Printable ASCII control characters.
557-
line = common.MakePrintable(line)
558-
resultBuilder.WriteString(line + "\n")
553+
line = ansi.Truncate(line, maxLineLength, "")
554+
resultBuilder.WriteString(line)
555+
resultBuilder.WriteRune('\n')
559556
lineCount++
560557
if previewLine > 0 && lineCount >= previewLine {
561558
break
@@ -566,14 +563,21 @@ func readFileContent(filepath string, maxLineLength int, previewLine int) (strin
566563
}
567564

568565
func (m *model) filePreviewPanelRender() string {
569-
previewLine := m.mainPanelHeight + 2
566+
// Todo : This width adjustment must not be done inside render function. It should
567+
// only be triggered via Update()
570568
m.fileModel.filePreview.width += m.fullWidth - common.Config.SidebarWidth - m.fileModel.filePreview.width - ((m.fileModel.width + 2) * len(m.fileModel.filePanels)) - 2
571569

570+
return m.filePreviewPanelRenderWithDimensions(m.mainPanelHeight+2, m.fileModel.filePreview.width)
571+
}
572+
573+
func (m *model) filePreviewPanelRenderWithDimensions(previewHeight int, previewWidth int) string {
572574
panel := m.fileModel.filePanels[m.filePanelFocusIndex]
573-
box := common.FilePreviewBox(previewLine, m.fileModel.filePreview.width)
575+
box := common.FilePreviewBox(previewHeight, previewWidth)
576+
r := ui.FilePreviewPanelRenderer(previewHeight, previewWidth)
574577

575578
if len(panel.element) == 0 {
576-
return box.Render("\n --- " + icon.Error + " No content to preview ---")
579+
r.AddLines(common.FilePreviewNoContentText)
580+
return r.Render()
577581
}
578582
// This could create errors if panel.cursor ever becomes negative, or goes out of bounds
579583
// We should have a panel validation function in our View() function
@@ -591,27 +595,30 @@ func (m *model) filePreviewPanelRender() string {
591595

592596
if infoErr != nil {
593597
slog.Error("Error get file info", "error", infoErr)
594-
return box.Render("\n --- " + icon.Error + " Error get file info ---")
598+
r.AddLines(common.FilePreviewNoFileInfoText)
599+
return r.Render()
595600
}
596601

597602
ext := filepath.Ext(itemPath)
598603
// check if the file is unsupported file, cuz pdf will cause error
599-
if ext == ".pdf" || ext == ".torrent" {
600-
return box.Render("\n --- " + icon.Error + " Unsupported formats ---")
604+
if slices.Contains(common.UnsupportedPreviewFormats, ext) {
605+
r.AddLines(common.FilePreviewUnsupportedFormatText)
606+
return r.Render()
601607
}
602608

603609
if fileInfo.IsDir() {
604-
directoryContent := ""
605610
dirPath := itemPath
606611

607612
files, err := os.ReadDir(dirPath)
608613
if err != nil {
609614
slog.Error("Error render directory preview", "error", err)
610-
return box.Render("\n --- " + icon.Error + " Error render directory preview ---")
615+
r.AddLines(common.FilePreviewDirectoryUnreadableText)
616+
return r.Render()
611617
}
612618

613619
if len(files) == 0 {
614-
return box.Render("\n --- empty ---")
620+
r.AddLines(common.FilePreviewEmptyText)
621+
return r.Render()
615622
}
616623

617624
sort.Slice(files, func(i, j int) bool {
@@ -624,15 +631,17 @@ func (m *model) filePreviewPanelRender() string {
624631
return files[i].Name() < files[j].Name()
625632
})
626633

627-
for i := 0; i < previewLine && i < len(files); i++ {
634+
for i := 0; i < previewHeight && i < len(files); i++ {
628635
file := files[i]
629-
directoryContent += common.PrettierDirectoryPreviewName(file.Name(), file.IsDir(), common.FilePanelBGColor)
630-
if i != previewLine-1 && i != len(files)-1 {
631-
directoryContent += "\n"
632-
}
636+
637+
style := common.GetElementIcon(file.Name(), file.IsDir(), common.Config.Nerdfont)
638+
639+
res := lipgloss.NewStyle().Foreground(lipgloss.Color(style.Color)).Background(common.FilePanelBGColor).
640+
Render(style.Icon+" ") + common.FilePanelStyle.Render(file.Name())
641+
642+
r.AddLines(res)
633643
}
634-
directoryContent = common.CheckAndTruncateLineLengths(directoryContent, m.fileModel.filePreview.width)
635-
return box.Render(directoryContent)
644+
return r.Render()
636645
}
637646

638647
if isImageFile(itemPath) {
@@ -645,7 +654,7 @@ func (m *model) filePreviewPanelRender() string {
645654
return box.Render("\n --- Image preview is disabled ---")
646655
}
647656

648-
ansiRender, err := filepreview.ImagePreview(itemPath, m.fileModel.filePreview.width, previewLine, common.Theme.FilePanelBG)
657+
ansiRender, err := filepreview.ImagePreview(itemPath, previewWidth, previewHeight, common.Theme.FilePanelBG)
649658
if errors.Is(err, image.ErrFormat) {
650659
return box.Render("\n --- " + icon.Error + " Unsupported image formats ---")
651660
}
@@ -671,7 +680,7 @@ func (m *model) filePreviewPanelRender() string {
671680
}
672681

673682
// At this point either format is not nil, or we can read the file
674-
fileContent, err := readFileContent(itemPath, m.fileModel.width+20, previewLine)
683+
fileContent, err := readFileContent(itemPath, previewWidth, previewHeight)
675684
if err != nil {
676685
slog.Error("Error open file", "error", err)
677686
return box.Render("\n --- " + icon.Error + " Error open file ---")
@@ -691,7 +700,7 @@ func (m *model) filePreviewPanelRender() string {
691700
if batCmd == "" {
692701
return box.Render("\n --- " + icon.Error + " 'bat' is not installed or not found. ---\n --- Cannot render file preview. ---")
693702
}
694-
fileContent, err = getBatSyntaxHighlightedContent(itemPath, previewLine, background)
703+
fileContent, err = getBatSyntaxHighlightedContent(itemPath, previewHeight, background)
695704
} else {
696705
fileContent, err = ansichroma.HightlightString(fileContent, format.Config().Name, common.Theme.CodeSyntaxHighlightTheme, background)
697706
}
@@ -700,9 +709,8 @@ func (m *model) filePreviewPanelRender() string {
700709
return box.Render("\n --- " + icon.Error + " Error render code highlight ---")
701710
}
702711
}
703-
704-
fileContent = common.CheckAndTruncateLineLengths(fileContent, m.fileModel.filePreview.width)
705-
return box.Render(fileContent)
712+
r.AddLines(fileContent)
713+
return r.Render()
706714
}
707715

708716
func getBatSyntaxHighlightedContent(itemPath string, previewLine int, background string) (string, error) {

0 commit comments

Comments
 (0)