diff --git a/cmd/agentbbs/main.go b/cmd/agentbbs/main.go index 6b3506e..7c9d89b 100644 --- a/cmd/agentbbs/main.go +++ b/cmd/agentbbs/main.go @@ -70,6 +70,7 @@ import ( "github.com/profullstack/agentbbs/internal/store" "github.com/profullstack/agentbbs/internal/tor" "github.com/profullstack/agentbbs/plugins/about" + "github.com/profullstack/agentbbs/plugins/files" "github.com/profullstack/agentbbs/plugins/agentgames" "github.com/profullstack/agentbbs/plugins/arcade" qryptinviteplugin "github.com/profullstack/agentbbs/plugins/qryptinvite" @@ -171,7 +172,7 @@ func main() { a.mm = games.NewMatchmaker(a.gamesReg, a.st, time.Duration(envInt("AGENTBBS_GAME_MOVE_TIMEOUT", 15))*time.Second, time.Duration(envInt("AGENTBBS_GAME_QUEUE_WAIT", 120))*time.Second) - a.registry = []plugin.Plugin{arcade.Plugin{}, agentgames.New(a.gamesReg), qryptinviteplugin.Plugin{}, about.Plugin{}} + a.registry = []plugin.Plugin{arcade.Plugin{}, agentgames.New(a.gamesReg), qryptinviteplugin.Plugin{}, about.Plugin{}, files.Plugin{}} // Custom domains: maintain the symlink farm Caddy serves and answer its // on-demand-TLS "ask" query so certs are only issued for mapped domains. diff --git a/plugins/files/files.go b/plugins/files/files.go new file mode 100644 index 0000000..0537034 --- /dev/null +++ b/plugins/files/files.go @@ -0,0 +1,100 @@ +package files + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/profullstack/agentbbs/internal/auth" + "github.com/profullstack/agentbbs/internal/plugin" +) + +type Plugin struct{} + +func (Plugin) ID() string { return "files" } +func (Plugin) Title() string { return "Files" } +func (Plugin) Description() string { return "Browse your personal pod files" } +func (Plugin) RequiresAuth() bool { return true } + +func (Plugin) New(user auth.User, ctx plugin.Context) tea.Model { + return model{user: user, dataDir: ctx.DataDir} +} + +type model struct { + user auth.User + dataDir string + files []os.DirEntry + cursor int + err error +} + +func (m model) Init() tea.Cmd { + return func() tea.Msg { + if m.dataDir == "" { + return fmt.Errorf("no data directory configured") + } + files, err := os.ReadDir(m.dataDir) + if err != nil { + return err + } + return files + } +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case error: + m.err = msg + return m, nil + case []os.DirEntry: + m.files = msg + return m, nil + case tea.KeyMsg: + switch msg.String() { + case "up", "k": + if m.cursor > 0 { + m.cursor-- + } + case "down", "j": + if m.cursor < len(m.files)-1 { + m.cursor++ + } + case "q", "esc", "ctrl+c": + return m, plugin.Exit + } + } + return m, nil +} + +var ( + titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#4ade80")) + itemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) + selStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#4ade80")) +) + +func (m model) View() string { + if m.err != nil { + return fmt.Sprintf("Error reading files: %v\n\npress any key to return", m.err) + } + s := titleStyle.Render("Files in your pod workspace:") + "\n\n" + if len(m.files) == 0 { + s += " (no files found)\n" + } + for i, file := range m.files { + cur := " " + style := itemStyle + if i == m.cursor { + cur = "โฏ " + style = selStyle + } + icon := "๐Ÿ“„ " + if file.IsDir() { + icon = "๐Ÿ“ " + } + s += fmt.Sprintf("%s%s%s\n", cur, icon, style.Render(file.Name())) + } + s += "\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render("โ†‘/โ†“ move ยท q back") + return lipgloss.NewStyle().Padding(1, 2).Render(s) +}