Skip to content

Commit

Permalink
feat: support file selection
Browse files Browse the repository at this point in the history
  • Loading branch information
ALX99 committed May 19, 2024
1 parent a5aaf03 commit 0c7d457
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 22 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ go install github.com/alx99/sail/cmd/sail@latest
- [x] Customizable keybindings
- [x] *Sail* into directories
- [x] Delete files (*partial* support)
- [x] Select files
- [ ] Move files
- [ ] Copy files
- [ ] Rename files
Expand Down Expand Up @@ -65,6 +66,7 @@ settings:
out: ","
go_home: "~"
delete: "d"
select: " "
```
### Using sail as a cd replacement
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Keymap struct {
NavOut string `yaml:"out"`
NavHome string `yaml:"go_home"`
Delete string `yaml:"delete"`
Select string `yaml:"select"`
}

// GetConfig reads, pareses and returns the configuration
Expand All @@ -41,6 +42,7 @@ func GetConfig() (Config, error) {
NavOut: ",",
NavHome: "~",
Delete: "d",
Select: " ",
},
},
}
Expand Down
79 changes: 57 additions & 22 deletions internal/models/main_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import (

const defaultMaxRows = 10

var pathAnimDuration = 250 * time.Millisecond
var (
pathAnimDuration = 250 * time.Millisecond
white = lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff"))
red = lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0000"))
)

type dirLoaded struct {
path string
Expand All @@ -38,6 +42,7 @@ type Model struct {
files []fs.DirEntry // current files in that directory
cursor position // cursor
cachedDirSelections map[string]string // cached file names for directories
selectedFiles map[string]any // selected files
maxRows int // the maximum number of rows to display
lastError error // last error that occurred

Expand All @@ -51,6 +56,7 @@ func NewMain(cwd string, cfg config.Config) Model {
cfg: cfg,
maxRows: defaultMaxRows,
cachedDirSelections: make(map[string]string, 100),
selectedFiles: make(map[string]any, 100),
sb: strings.Builder{},
}
}
Expand Down Expand Up @@ -130,19 +136,36 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
},
m.loadDir(m.cwd),
)
if len(m.files) > 0 {
// we optimistically believe that the file will be deleted
delete(m.cachedDirSelections, m.cwd)

return m, sequentially(
func() tea.Msg {
return osi.RemoveAll(path.Join(m.cwd, m.files[m.cursorOffset()].Name()))
},
m.loadDir(m.cwd),
)

case m.cfg.Settings.Keymap.Select:
if len(m.files) <= 0 {
return m, nil
}

fName := path.Join(m.cwd, m.files[m.cursorOffset()].Name())
if _, ok := m.selectedFiles[fName]; ok {
delete(m.selectedFiles, fName)
log.Debug().Msgf("Deselected %s", fName)
} else {
m.selectedFiles[fName] = nil
log.Debug().Msgf("Selected %s", fName)
}

prevCursor := m.cursor
m = m.goDown()
if prevCursor != m.cursor {
return m, nil
}
return m, nil

// If the cursor did not move, it HAS to mean we are at the
// end of a row. Try to move to the next column
if m.cursorOffset()+1 < len(m.files) {
m.setCursor(0, m.cursor.c+1)
} else {
// here we MUST be at the end of the list
m.setCursor(0, 0)
}
return m, nil
}
case tea.WindowSizeMsg:
var fName string
Expand Down Expand Up @@ -233,7 +256,7 @@ func (m Model) View() string {

if m.prevCWD != "" {
if strings.HasPrefix(m.prevCWD, m.cwd) && len(m.cwd) < len(m.prevCWD) {
m.sb.WriteString(m.cwd + lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0000")).Render(strings.TrimPrefix(m.prevCWD+"/", m.cwd)))
m.sb.WriteString(m.cwd + red.Render(strings.TrimPrefix(m.prevCWD+"/", m.cwd)))
} else if strings.HasPrefix(m.cwd, m.prevCWD) && len(m.cwd) > len(m.prevCWD) {
// some eye candy; directories end with a slash
if m.prevCWD != "/" {
Expand Down Expand Up @@ -264,29 +287,36 @@ func (m Model) View() string {
for row := range len(grid) {
for col, f := range grid[row] {
if m.cursor.r == row && m.cursor.c == col {
m.sb.WriteString(">")
m.sb.WriteString(white.Render(">"))
}

extraPadding := 0
rightPad := 0

// only pad if the column is not the last column
if col < len(grid[row]) {
extraPadding = maxColNameLen[col] - len(f.Name()) + 2
// +3 because we want at least one space between the file name and the next column
// and we can get +2 extra characters before the name (cursor + selection)
rightPad = maxColNameLen[col] - len(f.Name()) + 3

if m.cursor.r == row && m.cursor.c == col {
extraPadding--
rightPad--
}
}

if m.isSelected(f.Name()) {
m.sb.WriteString(white.Render(">"))
rightPad--
}

m.sb.WriteString(util.GetStyle(f).
PaddingRight(extraPadding).
PaddingRight(rightPad).
Render(f.Name()))
}
m.sb.WriteString("\n")

if row == len(grid)-1 {
if m.lastError != nil {
m.sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#ff0000")).Render(m.lastError.Error()))
m.sb.WriteString(red.Render(m.lastError.Error()))
}
}
}
Expand All @@ -309,8 +339,8 @@ func (m Model) logCursor() {
}

func (m *Model) setCursor(r, c int) {
m.cursor.c = r
m.cursor.r = c
m.cursor.c = c
m.cursor.r = r
}

// trySelectFile tries to select a file by name or sets the cursor to the first file
Expand All @@ -321,7 +351,7 @@ func (m *Model) trySelectFile(fName string) {
})

if index != -1 {
m.setCursor(index/m.maxRows, index%m.maxRows)
m.setCursor(index%m.maxRows, index/m.maxRows)
} else {
m.setCursor(0, 0)
}
Expand Down Expand Up @@ -349,6 +379,11 @@ func (m Model) goDown() Model {
return m
}

func (m Model) isSelected(name string) bool {
_, ok := m.selectedFiles[path.Join(m.cwd, name)]
return ok
}

// sequentially produces a command that sequentially executes the given
// commands.
// The tea.Msg returned is the first non-nil message returned by a Cmd.
Expand Down
116 changes: 116 additions & 0 deletions internal/models/main_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestModel_Update(t *testing.T) {
maxRows int
sb strings.Builder
lastError error
selectedFiles map[string]any
}
type args struct {
msg tea.Msg
Expand Down Expand Up @@ -594,6 +595,120 @@ func TestModel_Update(t *testing.T) {
},
},
},
{
name: "Select file",
fields: fields{
cfg: config.Config{
Settings: config.Settings{Keymap: config.Keymap{Select: " "}},
},
cwd: "/test",
files: []fs.DirEntry{
dirEntry{name: "file1", isDir: false},
},
maxRows: 3,
selectedFiles: map[string]any{},
},
args: args{
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}},
},
wantFunc: func(m Model) Model {
m.selectedFiles = map[string]any{
"/test/file1": nil,
}
return m
},
},
{
name: "Select file last file (wrap around)",
fields: fields{
cfg: config.Config{
Settings: config.Settings{Keymap: config.Keymap{Select: " "}},
},
cwd: "/test",
files: []fs.DirEntry{
dirEntry{name: "file1", isDir: false},
dirEntry{name: "file2", isDir: false},
},
cursor: position{c: 1},
maxRows: 1,
selectedFiles: map[string]any{},
},
args: args{
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}},
},
wantFunc: func(m Model) Model {
m.cursor = position{}
m.selectedFiles = map[string]any{
"/test/file2": nil,
}
return m
},
},
{
name: "Select file last file (wrap to next col)",
fields: fields{
cfg: config.Config{
Settings: config.Settings{Keymap: config.Keymap{Select: " "}},
},
cwd: "/test",
files: []fs.DirEntry{
dirEntry{name: "file1", isDir: false},
dirEntry{name: "file2", isDir: false},
},
maxRows: 1,
selectedFiles: map[string]any{},
},
args: args{
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}},
},
wantFunc: func(m Model) Model {
m.cursor = position{c: 1}
m.selectedFiles = map[string]any{
"/test/file1": nil,
}
return m
},
},
{
name: "Deselect file (next row move)",
fields: fields{
cfg: config.Config{
Settings: config.Settings{Keymap: config.Keymap{Select: " "}},
},
cwd: "/test",
files: []fs.DirEntry{
dirEntry{name: "file1", isDir: false},
dirEntry{name: "file2", isDir: false},
},
maxRows: 2,
selectedFiles: map[string]any{"/test/file1": nil},
},
args: args{
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}},
},
wantFunc: func(m Model) Model {
m.cursor = position{r: 1}
m.selectedFiles = map[string]any{}
return m
},
},
{
name: "Select file (no files)",
fields: fields{
cfg: config.Config{
Settings: config.Settings{Keymap: config.Keymap{Select: " "}},
},
cwd: "/test",
files: []fs.DirEntry{},
maxRows: 2,
},
args: args{
msg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{' '}},
},
wantFunc: func(m Model) Model {
return m
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -606,6 +721,7 @@ func TestModel_Update(t *testing.T) {
maxRows: tt.fields.maxRows,
sb: tt.fields.sb,
lastError: tt.fields.lastError,
selectedFiles: tt.fields.selectedFiles,
}

if mock, ok := tt.mocks.fs.(mockOS); ok {
Expand Down

0 comments on commit 0c7d457

Please sign in to comment.