add:send functionality

This commit is contained in:
2026-04-07 04:57:53 +09:00
parent d7d437d877
commit a20749d297
5 changed files with 96 additions and 25 deletions

View File

@@ -68,11 +68,17 @@ func (s *StorageService) Delete(filename string) error {
return nil return nil
} }
// Send transfers one or more local files to the remote storage. // Send transfers a single local file to the remote storage directory.
// Multiple files are archived into a temp tarball first. func (s *StorageService) Send(localPath string) error {
func (s *StorageService) Send(localPaths []string) error { dst := RemoteStorageRoot(s.server)
// TODO: implement cmd := RsyncCmd(s.server, localPath, dst)
return fmt.Errorf("send: not yet implemented") debugLog("Send | args: %v", cmd.Args)
out, err := cmd.CombinedOutput()
debugLog("Send | exit_err: %v | output: %q", err, strings.TrimSpace(string(out)))
if err != nil {
return fmt.Errorf("send failed: %w\n%s", err, strings.TrimSpace(string(out)))
}
return nil
} }
// CleanAll removes all files from remote storage. // CleanAll removes all files from remote storage.

View File

@@ -164,6 +164,10 @@ var (
Foreground(lipgloss.Color("75")). Foreground(lipgloss.Color("75")).
MarginBottom(1) MarginBottom(1)
PickerQueryBlurredStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("240")).
MarginBottom(1)
pickerItemBase = lipgloss.NewStyle(). pickerItemBase = lipgloss.NewStyle().
PaddingLeft(4). PaddingLeft(4).
Width(44) Width(44)

View File

@@ -14,15 +14,16 @@ type entry struct {
// picker is the state for the send file picker page. // picker is the state for the send file picker page.
type picker struct { type picker struct {
dir string // current directory being browsed dir string // current directory being browsed
entries []entry // unfiltered entries in dir entries []entry // unfiltered entries in dir
filtered []entry // entries matching query filtered []entry // entries matching query
query string // current filter string query string // current filter string
cursor int // index within filtered cursor int // index within filtered
queryFocused bool // true = typing goes to query; false = list navigation only
} }
func newPicker(startDir string) picker { func newPicker(startDir string) picker {
p := picker{dir: startDir} p := picker{dir: startDir, queryFocused: false}
p.entries = readDir(startDir) p.entries = readDir(startDir)
p.filtered = p.entries p.filtered = p.entries
return p return p

View File

@@ -1,6 +1,7 @@
package tui package tui
import ( import (
"path/filepath"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -86,6 +87,25 @@ func cleanAllCmd(store *services.ServicesStore, serverName string) tea.Cmd {
} }
} }
type sendFileMsg struct {
filename string
err error
}
func sendFileCmd(store *services.ServicesStore, serverName, localPath string) tea.Cmd {
return func() tea.Msg {
storage, err := store.NewStorageService(serverName)
if err != nil {
return sendFileMsg{err: err}
}
filename := filepath.Base(localPath)
if err := storage.Send(localPath); err != nil {
return sendFileMsg{err: err}
}
return sendFileMsg{filename: filename}
}
}
func newCleanInput() textinput.Model { func newCleanInput() textinput.Model {
ti := textinput.New() ti := textinput.New()
ti.Placeholder = "yes" ti.Placeholder = "yes"
@@ -261,6 +281,23 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
clearFlashAfter(0), // immediate clear of any stale flash clearFlashAfter(0), // immediate clear of any stale flash
) )
case sendFileMsg:
if msg.err != nil {
// return to server actions with error flash
server := m.ActiveServer
m.FlashMsg = "✗ send failed: " + msg.err.Error()
return m, tea.Batch(
func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} },
clearFlashAfter(4*time.Second),
)
}
server := m.ActiveServer
m.FlashMsg = "✓ \"" + msg.filename + "\" sent."
return m, tea.Batch(
func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} },
clearFlashAfter(3*time.Second),
)
case storageFilesMsg: case storageFilesMsg:
m.StorageLoading = false m.StorageLoading = false
m.StorageFiles = msg.files m.StorageFiles = msg.files
@@ -482,6 +519,11 @@ func (m TUIInterface) updateSend(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
p := m.Picker p := m.Picker
switch msg.String() { switch msg.String() {
case "tab":
p.queryFocused = !p.queryFocused
m.Picker = p
return m, nil
case "up", "k": case "up", "k":
if p.cursor > 0 { if p.cursor > 0 {
p.cursor-- p.cursor--
@@ -506,16 +548,16 @@ func (m TUIInterface) updateSend(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
// file selected — send it // file selected — send it
path := p.selectedPath() localPath := p.selectedPath()
_ = path // TODO: wire to send service
server := m.ActiveServer server := m.ActiveServer
return m, func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} } return m, sendFileCmd(m.Services, server, localPath)
case "backspace": case "backspace":
newP, consumed := p.backspace() if p.queryFocused {
m.Picker = newP newP, _ := p.backspace()
_ = consumed m.Picker = newP
return m, nil return m, nil
}
case "ctrl+c": case "ctrl+c":
m.Quitting = true m.Quitting = true
@@ -526,8 +568,8 @@ func (m TUIInterface) updateSend(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
return m, func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} } return m, func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} }
default: default:
// printable single rune → append to query // printable rune → only append to query when query pane is focused
if msg.Text != "" { if msg.Text != "" && p.queryFocused {
m.Picker = p.typeRune([]rune(msg.Text)[0]) m.Picker = p.typeRune([]rune(msg.Text)[0])
return m, nil return m, nil
} }

View File

@@ -2,6 +2,7 @@ package tui
import ( import (
"fmt" "fmt"
"strings"
"filepass/internal/styles" "filepass/internal/styles"
@@ -141,12 +142,12 @@ func (m TUIInterface) View() tea.View {
footerSep() + footerSep() +
footerHint("esc", "back") footerHint("esc", "back")
case pageSend: case pageSend:
footerStr = footerHint("↑↓", "navigate") + footerStr = footerHint("tab", "switch pane") +
footerSep() +
footerHint("↑↓", "navigate") +
footerSep() + footerSep() +
footerHint("enter", "open/send") + footerHint("enter", "open/send") +
footerSep() + footerSep() +
footerHint("backspace", "up a level") +
footerSep() +
footerHint("esc", "back") footerHint("esc", "back")
default: default:
footerStr = footerHint("↑↓", "navigate") + footerStr = footerHint("↑↓", "navigate") +
@@ -191,6 +192,12 @@ func (m TUIInterface) viewMenu() string {
statusLine = styles.StatusWarnStyle.Render("⚠ No servers configured. Select Config to add one.") statusLine = styles.StatusWarnStyle.Render("⚠ No servers configured. Select Config to add one.")
case m.FlashMsg != "" && m.Page == pageConfig: case m.FlashMsg != "" && m.Page == pageConfig:
statusLine = styles.StatusOKStyle.Render(m.FlashMsg) statusLine = styles.StatusOKStyle.Render(m.FlashMsg)
case m.FlashMsg != "" && m.Page == pageServerActions:
if strings.HasPrefix(m.FlashMsg, "✗") {
statusLine = styles.StatusErrStyle.Render(m.FlashMsg)
} else {
statusLine = styles.StatusOKStyle.Render(m.FlashMsg)
}
} }
if statusLine != "" { if statusLine != "" {
@@ -266,8 +273,19 @@ func (m TUIInterface) viewSend() string {
// breadcrumb showing current directory // breadcrumb showing current directory
crumb := styles.LocalDirStyle.Render(" " + p.dir) crumb := styles.LocalDirStyle.Render(" " + p.dir)
// search input // search input — shows cursor block and accent colour when focused, dim when not
queryLine := styles.PickerQueryStyle.Render(" / " + p.query + "█") var queryLine string
if p.queryFocused {
queryLine = styles.PickerQueryStyle.Render(" / " + p.query + "█")
} else {
var queryHint string
if p.query != "" {
queryHint = " / " + p.query
} else {
queryHint = " / (tab to filter)"
}
queryLine = styles.PickerQueryBlurredStyle.Render(queryHint)
}
// file/dir entries // file/dir entries
var rows []string var rows []string