-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dc1d338
commit f00a0df
Showing
2 changed files
with
181 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,215 @@ | ||
package filetree | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/charmbracelet/bubbles/filepicker" | ||
"github.com/charmbracelet/bubbles/key" | ||
"github.com/charmbracelet/bubbles/viewport" | ||
tea "github.com/charmbracelet/bubbletea" | ||
"github.com/charmbracelet/lipgloss" | ||
"github.com/mistakenelf/teacup/filesystem" | ||
) | ||
|
||
type Model struct { | ||
filepicker filepicker.Model | ||
selectedFile string | ||
quitting bool | ||
err error | ||
active bool | ||
} | ||
const ( | ||
thousand = 1000 | ||
ten = 10 | ||
fivePercent = 0.0499 | ||
) | ||
|
||
type clearErrorMsg struct{} | ||
var ( | ||
selectedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true) | ||
) | ||
|
||
func clearErrorAfter(t time.Duration) tea.Cmd { | ||
return tea.Tick(t, func(_ time.Time) tea.Msg { | ||
return clearErrorMsg{} | ||
}) | ||
type KeyMap struct { | ||
Down key.Binding | ||
Up key.Binding | ||
} | ||
|
||
func New(currentDirectory string, active bool) Model { | ||
fp := filepicker.New() | ||
fp.CurrentDirectory = currentDirectory | ||
|
||
return Model{ | ||
filepicker: fp, | ||
selectedFile: "", | ||
quitting: false, | ||
err: nil, | ||
active: active, | ||
} | ||
type DirectoryItem struct { | ||
name string | ||
details string | ||
path string | ||
extension string | ||
isDirectory bool | ||
currentDirectory string | ||
} | ||
|
||
func (m Model) Init() tea.Cmd { | ||
return m.filepicker.Init() | ||
type getDirectoryListingMsg []DirectoryItem | ||
type errorMsg error | ||
|
||
type Model struct { | ||
viewport viewport.Model | ||
cursor int | ||
files []DirectoryItem | ||
active bool | ||
keyMap KeyMap | ||
} | ||
|
||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { | ||
switch msg := msg.(type) { | ||
case clearErrorMsg: | ||
m.err = nil | ||
case tea.WindowSizeMsg: | ||
m.filepicker.Height = msg.Height | ||
func DefaultKeyMap() KeyMap { | ||
return KeyMap{ | ||
Down: key.NewBinding(key.WithKeys("j", "down", "ctrl+n"), key.WithHelp("j", "down")), | ||
Up: key.NewBinding(key.WithKeys("k", "up", "ctrl+p"), key.WithHelp("k", "up")), | ||
} | ||
} | ||
|
||
var cmd tea.Cmd | ||
func New() Model { | ||
viewPort := viewport.New(0, 0) | ||
|
||
if m.active { | ||
m.filepicker, cmd = m.filepicker.Update(msg) | ||
return Model{ | ||
viewport: viewPort, | ||
cursor: 0, | ||
active: true, | ||
keyMap: DefaultKeyMap(), | ||
} | ||
} | ||
|
||
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { | ||
m.selectedFile = path | ||
// ConvertBytesToSizeString converts a byte count to a human readable string. | ||
func ConvertBytesToSizeString(size int64) string { | ||
if size < thousand { | ||
return fmt.Sprintf("%dB", size) | ||
} | ||
|
||
if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { | ||
m.err = errors.New(path + " is not valid.") | ||
m.selectedFile = "" | ||
suffix := []string{ | ||
"K", // kilo | ||
"M", // mega | ||
"G", // giga | ||
"T", // tera | ||
"P", // peta | ||
"E", // exa | ||
"Z", // zeta | ||
"Y", // yotta | ||
} | ||
|
||
return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) | ||
curr := float64(size) / thousand | ||
for _, s := range suffix { | ||
if curr < ten { | ||
return fmt.Sprintf("%.1f%s", curr-fivePercent, s) | ||
} else if curr < thousand { | ||
return fmt.Sprintf("%d%s", int(curr), s) | ||
} | ||
curr /= thousand | ||
} | ||
|
||
return m, cmd | ||
return "" | ||
} | ||
|
||
func (m Model) View() string { | ||
var s strings.Builder | ||
// SetSize sets the size of the bubble. | ||
func (m *Model) SetSize(w, h int) { | ||
m.viewport.Width = w | ||
m.viewport.Height = h | ||
} | ||
|
||
// SetIsActive sets if the bubble is currently active. | ||
func (m *Model) SetIsActive(active bool) { | ||
m.active = active | ||
} | ||
|
||
// GotoTop jumps to the top of the viewport. | ||
func (m *Model) GotoTop() { | ||
m.viewport.GotoTop() | ||
} | ||
|
||
if m.quitting { | ||
return "" | ||
// getDirectoryListingCmd updates the directory listing based on the name of the directory provided. | ||
func getDirectoryListingCmd(directoryName string, showHidden bool) tea.Cmd { | ||
return func() tea.Msg { | ||
var err error | ||
var directoryItems []DirectoryItem | ||
|
||
if directoryName == filesystem.HomeDirectory { | ||
directoryName, err = filesystem.GetHomeDirectory() | ||
if err != nil { | ||
return errorMsg(err) | ||
} | ||
} | ||
|
||
directoryInfo, err := os.Stat(directoryName) | ||
if err != nil { | ||
return errorMsg(err) | ||
} | ||
|
||
if !directoryInfo.IsDir() { | ||
return nil | ||
} | ||
|
||
files, err := filesystem.GetDirectoryListing(directoryName, showHidden) | ||
if err != nil { | ||
return errorMsg(err) | ||
} | ||
|
||
err = os.Chdir(directoryName) | ||
if err != nil { | ||
return errorMsg(err) | ||
} | ||
|
||
workingDirectory, err := filesystem.GetWorkingDirectory() | ||
if err != nil { | ||
return errorMsg(err) | ||
} | ||
|
||
for _, file := range files { | ||
fileInfo, err := file.Info() | ||
if err != nil { | ||
continue | ||
} | ||
|
||
status := fmt.Sprintf("%s %s %s", | ||
fileInfo.ModTime().Format("2006-01-02 15:04:05"), | ||
fileInfo.Mode().String(), | ||
ConvertBytesToSizeString(fileInfo.Size())) | ||
|
||
directoryItems = append(directoryItems, DirectoryItem{ | ||
name: file.Name(), | ||
details: status, | ||
path: filepath.Join(workingDirectory, file.Name()), | ||
extension: filepath.Ext(fileInfo.Name()), | ||
isDirectory: fileInfo.IsDir(), | ||
currentDirectory: workingDirectory, | ||
}) | ||
} | ||
|
||
return getDirectoryListingMsg(directoryItems) | ||
} | ||
} | ||
|
||
func (m Model) Init() tea.Cmd { | ||
return getDirectoryListingCmd(filesystem.CurrentDirectory, true) | ||
} | ||
|
||
if m.err != nil { | ||
s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) | ||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { | ||
|
||
switch msg := msg.(type) { | ||
case tea.WindowSizeMsg: | ||
m.viewport.Width = msg.Width | ||
m.viewport.Height = msg.Height | ||
case getDirectoryListingMsg: | ||
if msg != nil { | ||
m.files = msg | ||
} | ||
case tea.KeyMsg: | ||
switch { | ||
case key.Matches(msg, m.keyMap.Up): | ||
m.cursor-- | ||
case key.Matches(msg, m.keyMap.Down): | ||
m.cursor++ | ||
} | ||
} | ||
|
||
s.WriteString("\n" + m.filepicker.View()) | ||
return m, nil | ||
} | ||
|
||
func (m Model) View() string { | ||
var fileList strings.Builder | ||
|
||
for i, file := range m.files { | ||
if i == m.cursor { | ||
fileList.WriteString(selectedItemStyle.Render(file.name) + "\n") | ||
} else { | ||
fileList.WriteString(file.name + "\n") | ||
} | ||
} | ||
|
||
return s.String() | ||
return fileList.String() | ||
} |