add:send functionality
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ type picker struct {
|
|||||||
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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
newP, _ := p.backspace()
|
||||||
m.Picker = newP
|
m.Picker = newP
|
||||||
_ = consumed
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user