add:send func
This commit is contained in:
47
build.sh
Normal file
47
build.sh
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LOCAL_BIN="$HOME/.local/bin"
|
||||||
|
EXPORT_LINE='export PATH="$HOME/.local/bin:$PATH"'
|
||||||
|
|
||||||
|
# Files to check (macOS + Linux)
|
||||||
|
FILES=(
|
||||||
|
"$HOME/.bashrc"
|
||||||
|
"$HOME/.bash_profile"
|
||||||
|
"$HOME/.zshrc"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Ensuring ~/.local/bin exists..."
|
||||||
|
mkdir -p "$LOCAL_BIN"
|
||||||
|
|
||||||
|
add_to_file() {
|
||||||
|
local file="$1"
|
||||||
|
|
||||||
|
# Create file if it doesn't exist
|
||||||
|
[ -f "$file" ] || touch "$file"
|
||||||
|
|
||||||
|
if grep -qxF "$EXPORT_LINE" "$file"; then
|
||||||
|
echo "✓ PATH already set in $(basename "$file")"
|
||||||
|
else
|
||||||
|
echo "→ Adding PATH to $(basename "$file")"
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "# Add local bin to PATH"
|
||||||
|
echo "$EXPORT_LINE"
|
||||||
|
} >> "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Updating shell config files..."
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
add_to_file "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Done!"
|
||||||
|
echo "Restart your shell or run:"
|
||||||
|
echo " source ~/.zshrc # for zsh"
|
||||||
|
echo " source ~/.bashrc # for bash"
|
||||||
5
internal/pages/send.go
Normal file
5
internal/pages/send.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
type SendPageMsg struct {
|
||||||
|
ServerName string
|
||||||
|
}
|
||||||
@@ -146,6 +146,31 @@ var (
|
|||||||
FilenameLabelStyle = lipgloss.NewStyle().
|
FilenameLabelStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("243")).
|
Foreground(lipgloss.Color("243")).
|
||||||
MarginBottom(1)
|
MarginBottom(1)
|
||||||
|
|
||||||
|
// Local directory label (above file list and in picker breadcrumb)
|
||||||
|
LocalDirStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("243")).
|
||||||
|
Italic(true).
|
||||||
|
MarginBottom(1)
|
||||||
|
|
||||||
|
// File picker
|
||||||
|
PickerQueryStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("75")).
|
||||||
|
MarginBottom(1)
|
||||||
|
|
||||||
|
pickerItemBase = lipgloss.NewStyle().
|
||||||
|
PaddingLeft(4).
|
||||||
|
Width(44)
|
||||||
|
|
||||||
|
pickerItemActive = lipgloss.NewStyle().
|
||||||
|
PaddingLeft(2).
|
||||||
|
Foreground(lipgloss.Color("75")).
|
||||||
|
Bold(true).
|
||||||
|
Width(44).
|
||||||
|
SetString("▸ ")
|
||||||
|
|
||||||
|
pickerDirColor = lipgloss.Color("75")
|
||||||
|
pickerFileColor = lipgloss.Color("252")
|
||||||
)
|
)
|
||||||
|
|
||||||
func MenuItemStyle(active, disabled bool) lipgloss.Style {
|
func MenuItemStyle(active, disabled bool) lipgloss.Style {
|
||||||
@@ -187,6 +212,21 @@ func FileItemStyle(active bool) lipgloss.Style {
|
|||||||
return fileItemInactive
|
return fileItemInactive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PickerItemStyle returns the style for a file picker entry.
|
||||||
|
// Directories are coloured differently from files.
|
||||||
|
func PickerItemStyle(active, isDir bool) lipgloss.Style {
|
||||||
|
if active {
|
||||||
|
if isDir {
|
||||||
|
return pickerItemActive.Foreground(pickerDirColor)
|
||||||
|
}
|
||||||
|
return pickerItemActive.Foreground(lipgloss.Color("255"))
|
||||||
|
}
|
||||||
|
if isDir {
|
||||||
|
return pickerItemBase.Foreground(pickerDirColor)
|
||||||
|
}
|
||||||
|
return pickerItemBase.Foreground(pickerFileColor)
|
||||||
|
}
|
||||||
|
|
||||||
// ServerRowStyle renders a single-line server list entry showing only the server name.
|
// ServerRowStyle renders a single-line server list entry showing only the server name.
|
||||||
func ServerRowStyle(active bool, name string) string {
|
func ServerRowStyle(active bool, name string) string {
|
||||||
if active {
|
if active {
|
||||||
|
|||||||
123
internal/tui/picker.go
Normal file
123
internal/tui/picker.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// entry is a single item in the file picker list.
|
||||||
|
type entry struct {
|
||||||
|
name string
|
||||||
|
isDir bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// picker is the state for the send file picker page.
|
||||||
|
type picker struct {
|
||||||
|
dir string // current directory being browsed
|
||||||
|
entries []entry // unfiltered entries in dir
|
||||||
|
filtered []entry // entries matching query
|
||||||
|
query string // current filter string
|
||||||
|
cursor int // index within filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPicker(startDir string) picker {
|
||||||
|
p := picker{dir: startDir}
|
||||||
|
p.entries = readDir(startDir)
|
||||||
|
p.filtered = p.entries
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// readDir lists the entries of a directory, dirs first then files.
|
||||||
|
func readDir(dir string) []entry {
|
||||||
|
infos, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var dirs, files []entry
|
||||||
|
for _, d := range infos {
|
||||||
|
name := d.Name()
|
||||||
|
if strings.HasPrefix(name, ".") {
|
||||||
|
continue // skip hidden
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
dirs = append(dirs, entry{name: name + "/", isDir: true})
|
||||||
|
} else {
|
||||||
|
files = append(files, entry{name: name, isDir: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(dirs, files...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyFilter rebuilds filtered from entries using query.
|
||||||
|
func (p picker) applyFilter() picker {
|
||||||
|
if p.query == "" {
|
||||||
|
p.filtered = p.entries
|
||||||
|
} else {
|
||||||
|
q := strings.ToLower(p.query)
|
||||||
|
var out []entry
|
||||||
|
for _, e := range p.entries {
|
||||||
|
if strings.Contains(strings.ToLower(e.name), q) {
|
||||||
|
out = append(out, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.filtered = out
|
||||||
|
}
|
||||||
|
p.cursor = 0
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// descend enters a subdirectory.
|
||||||
|
func (p picker) descend(name string) picker {
|
||||||
|
// strip trailing slash added for display
|
||||||
|
name = strings.TrimSuffix(name, "/")
|
||||||
|
next := filepath.Join(p.dir, name)
|
||||||
|
p.dir = next
|
||||||
|
p.entries = readDir(next)
|
||||||
|
p.query = ""
|
||||||
|
p.filtered = p.entries
|
||||||
|
p.cursor = 0
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// ascend goes up one directory level.
|
||||||
|
func (p picker) ascend() picker {
|
||||||
|
parent := filepath.Dir(p.dir)
|
||||||
|
if parent == p.dir {
|
||||||
|
return p // already at root
|
||||||
|
}
|
||||||
|
p.dir = parent
|
||||||
|
p.entries = readDir(parent)
|
||||||
|
p.query = ""
|
||||||
|
p.filtered = p.entries
|
||||||
|
p.cursor = 0
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectedPath returns the full path of the currently highlighted entry,
|
||||||
|
// or empty string if the list is empty.
|
||||||
|
func (p picker) selectedPath() string {
|
||||||
|
if len(p.filtered) == 0 || p.cursor < 0 || p.cursor >= len(p.filtered) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
e := p.filtered[p.cursor]
|
||||||
|
name := strings.TrimSuffix(e.name, "/")
|
||||||
|
return filepath.Join(p.dir, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeRune appends a rune to the query and re-filters.
|
||||||
|
func (p picker) typeRune(r rune) picker {
|
||||||
|
p.query += string(r)
|
||||||
|
return p.applyFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// backspace removes the last rune from the query.
|
||||||
|
// If query is already empty, ascend instead.
|
||||||
|
func (p picker) backspace() (picker, bool) {
|
||||||
|
if p.query == "" {
|
||||||
|
return p.ascend(), false // false = did not consume (went up)
|
||||||
|
}
|
||||||
|
runes := []rune(p.query)
|
||||||
|
p.query = string(runes[:len(runes)-1])
|
||||||
|
return p.applyFilter(), true
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ const (
|
|||||||
pageSelectServer
|
pageSelectServer
|
||||||
pageServerActions
|
pageServerActions
|
||||||
pageFileAction
|
pageFileAction
|
||||||
|
pageSend
|
||||||
)
|
)
|
||||||
|
|
||||||
type TUIInterface struct {
|
type TUIInterface struct {
|
||||||
@@ -33,6 +34,7 @@ type TUIInterface struct {
|
|||||||
WindowHeight int
|
WindowHeight int
|
||||||
// server actions page
|
// server actions page
|
||||||
ActiveServer string
|
ActiveServer string
|
||||||
|
LocalDir string // user's cwd, destination for received files
|
||||||
StorageFiles []string
|
StorageFiles []string
|
||||||
StorageLoading bool
|
StorageLoading bool
|
||||||
StorageErr error
|
StorageErr error
|
||||||
@@ -40,12 +42,15 @@ type TUIInterface struct {
|
|||||||
FileFocused bool // true = ↑↓ drives file list, false = action menu
|
FileFocused bool // true = ↑↓ drives file list, false = action menu
|
||||||
// file action page
|
// file action page
|
||||||
ActiveFile string
|
ActiveFile string
|
||||||
|
// send / file picker page
|
||||||
|
Picker picker
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTUIInterface(store *services.ServicesStore) TUIInterface {
|
func NewTUIInterface(store *services.ServicesStore, localDir string) TUIInterface {
|
||||||
return TUIInterface{
|
return TUIInterface{
|
||||||
Services: store,
|
Services: store,
|
||||||
Page: pageHome,
|
Page: pageHome,
|
||||||
MenuItems: pages.HomeMenuItems(),
|
MenuItems: pages.HomeMenuItems(),
|
||||||
|
LocalDir: localDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.Selected = 0
|
m.Selected = 0
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case pages.SendPageMsg:
|
||||||
|
m.Page = pageSend
|
||||||
|
m.Picker = newPicker(m.LocalDir)
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case storageFilesMsg:
|
case storageFilesMsg:
|
||||||
m.StorageLoading = false
|
m.StorageLoading = false
|
||||||
m.StorageFiles = msg.files
|
m.StorageFiles = msg.files
|
||||||
@@ -166,6 +171,9 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.Page == pageFileAction {
|
if m.Page == pageFileAction {
|
||||||
return m.updateFileAction(msg)
|
return m.updateFileAction(msg)
|
||||||
}
|
}
|
||||||
|
if m.Page == pageSend {
|
||||||
|
return m.updateSend(msg)
|
||||||
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
@@ -218,8 +226,7 @@ func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
func (m TUIInterface) updateServerActions(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
func (m TUIInterface) updateServerActions(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "tab":
|
case "tab":
|
||||||
// toggle focus between action menu and file list
|
if len(m.StorageFiles) > 0 || !m.StorageLoading {
|
||||||
if len(m.StorageFiles) > 0 {
|
|
||||||
m.FileFocused = !m.FileFocused
|
m.FileFocused = !m.FileFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,9 +260,10 @@ func (m TUIInterface) updateServerActions(msg tea.KeyPressMsg) (tea.Model, tea.C
|
|||||||
return pages.FileActionPageMsg{ServerName: server, Filename: file}
|
return pages.FileActionPageMsg{ServerName: server, Filename: file}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
server := m.ActiveServer
|
||||||
switch m.MenuItems[m.Selected].Key {
|
switch m.MenuItems[m.Selected].Key {
|
||||||
case "send":
|
case "send":
|
||||||
// TODO: navigate to send page
|
return m, func() tea.Msg { return pages.SendPageMsg{ServerName: server} }
|
||||||
case "clean":
|
case "clean":
|
||||||
// TODO: navigate to clean all page
|
// TODO: navigate to clean all page
|
||||||
}
|
}
|
||||||
@@ -304,6 +312,64 @@ func (m TUIInterface) updateFileAction(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m TUIInterface) updateSend(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||||
|
p := m.Picker
|
||||||
|
|
||||||
|
switch msg.String() {
|
||||||
|
case "up", "k":
|
||||||
|
if p.cursor > 0 {
|
||||||
|
p.cursor--
|
||||||
|
}
|
||||||
|
m.Picker = p
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case "down", "j":
|
||||||
|
if p.cursor < len(p.filtered)-1 {
|
||||||
|
p.cursor++
|
||||||
|
}
|
||||||
|
m.Picker = p
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case "enter":
|
||||||
|
if len(p.filtered) == 0 {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
selected := p.filtered[p.cursor]
|
||||||
|
if selected.isDir {
|
||||||
|
m.Picker = p.descend(selected.name)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
// file selected — send it
|
||||||
|
path := p.selectedPath()
|
||||||
|
_ = path // TODO: wire to send service
|
||||||
|
server := m.ActiveServer
|
||||||
|
return m, func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} }
|
||||||
|
|
||||||
|
case "backspace":
|
||||||
|
newP, consumed := p.backspace()
|
||||||
|
m.Picker = newP
|
||||||
|
_ = consumed
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case "ctrl+c":
|
||||||
|
m.Quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "esc":
|
||||||
|
server := m.ActiveServer
|
||||||
|
return m, func() tea.Msg { return pages.ServerActionsPageMsg{ServerName: server} }
|
||||||
|
|
||||||
|
default:
|
||||||
|
// printable single rune → append to query
|
||||||
|
if msg.Text != "" {
|
||||||
|
m.Picker = p.typeRune([]rune(msg.Text)[0])
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m TUIInterface) updateSelectServer(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
func (m TUIInterface) updateSelectServer(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||||
last := len(m.ServerNames) - 1
|
last := len(m.ServerNames) - 1
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ func (m TUIInterface) subtitle() string {
|
|||||||
return "Server"
|
return "Server"
|
||||||
case pageFileAction:
|
case pageFileAction:
|
||||||
return m.ActiveFile
|
return m.ActiveFile
|
||||||
|
case pageSend:
|
||||||
|
return "Send File"
|
||||||
default:
|
default:
|
||||||
return "Secure file transfer"
|
return "Secure file transfer"
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,8 @@ func (m TUIInterface) View() tea.View {
|
|||||||
body = m.viewServerActions()
|
body = m.viewServerActions()
|
||||||
case pageFileAction:
|
case pageFileAction:
|
||||||
body = m.viewFileAction()
|
body = m.viewFileAction()
|
||||||
|
case pageSend:
|
||||||
|
body = m.viewSend()
|
||||||
default:
|
default:
|
||||||
body = m.viewMenu()
|
body = m.viewMenu()
|
||||||
}
|
}
|
||||||
@@ -104,6 +108,14 @@ func (m TUIInterface) View() tea.View {
|
|||||||
footerHint("enter", "confirm") +
|
footerHint("enter", "confirm") +
|
||||||
footerSep() +
|
footerSep() +
|
||||||
footerHint("esc", "back")
|
footerHint("esc", "back")
|
||||||
|
case pageSend:
|
||||||
|
footerStr = footerHint("↑↓", "navigate") +
|
||||||
|
footerSep() +
|
||||||
|
footerHint("enter", "open/send") +
|
||||||
|
footerSep() +
|
||||||
|
footerHint("backspace", "up a level") +
|
||||||
|
footerSep() +
|
||||||
|
footerHint("esc", "back")
|
||||||
default:
|
default:
|
||||||
footerStr = footerHint("↑↓", "navigate") +
|
footerStr = footerHint("↑↓", "navigate") +
|
||||||
footerSep() +
|
footerSep() +
|
||||||
@@ -156,7 +168,7 @@ func (m TUIInterface) viewMenu() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m TUIInterface) viewServerActions() string {
|
func (m TUIInterface) viewServerActions() string {
|
||||||
// action menu — single column, unfocused when file pane is active
|
// action menu — single column, cursor only shown when pane is focused
|
||||||
var actionRows []string
|
var actionRows []string
|
||||||
for i, item := range m.MenuItems {
|
for i, item := range m.MenuItems {
|
||||||
active := !m.FileFocused && i == m.Selected
|
active := !m.FileFocused && i == m.Selected
|
||||||
@@ -164,7 +176,10 @@ func (m TUIInterface) viewServerActions() string {
|
|||||||
}
|
}
|
||||||
actions := lipgloss.JoinVertical(lipgloss.Left, actionRows...)
|
actions := lipgloss.JoinVertical(lipgloss.Left, actionRows...)
|
||||||
|
|
||||||
// file list section below, separated by a top border
|
// static local dir label — always visible above the file list
|
||||||
|
localDirLabel := styles.LocalDirStyle.Render(" ↓ " + m.LocalDir)
|
||||||
|
|
||||||
|
// file list rows
|
||||||
var fileRows []string
|
var fileRows []string
|
||||||
switch {
|
switch {
|
||||||
case m.StorageLoading:
|
case m.StorageLoading:
|
||||||
@@ -180,13 +195,14 @@ func (m TUIInterface) viewServerActions() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileList := lipgloss.JoinVertical(lipgloss.Left, fileRows...)
|
fileList := lipgloss.JoinVertical(lipgloss.Left, fileRows...)
|
||||||
fileSection := styles.StorageFileSectionStyle.Render(fileList)
|
fileSection := styles.StorageFileSectionStyle.Render(
|
||||||
|
lipgloss.JoinVertical(lipgloss.Left, localDirLabel, fileList),
|
||||||
|
)
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, actions, fileSection)
|
return lipgloss.JoinVertical(lipgloss.Left, actions, fileSection)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m TUIInterface) viewFileAction() string {
|
func (m TUIInterface) viewFileAction() string {
|
||||||
// filename shown as a dim label above the menu
|
|
||||||
filenameLabel := styles.FilenameLabelStyle.Render(m.ActiveFile)
|
filenameLabel := styles.FilenameLabelStyle.Render(m.ActiveFile)
|
||||||
|
|
||||||
var menuRows []string
|
var menuRows []string
|
||||||
@@ -198,6 +214,30 @@ func (m TUIInterface) viewFileAction() string {
|
|||||||
return lipgloss.JoinVertical(lipgloss.Left, filenameLabel, menu)
|
return lipgloss.JoinVertical(lipgloss.Left, filenameLabel, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m TUIInterface) viewSend() string {
|
||||||
|
p := m.Picker
|
||||||
|
|
||||||
|
// breadcrumb showing current directory
|
||||||
|
crumb := styles.LocalDirStyle.Render(" " + p.dir)
|
||||||
|
|
||||||
|
// search input
|
||||||
|
queryLine := styles.PickerQueryStyle.Render(" / " + p.query + "█")
|
||||||
|
|
||||||
|
// file/dir entries
|
||||||
|
var rows []string
|
||||||
|
if len(p.filtered) == 0 {
|
||||||
|
rows = append(rows, styles.StorageEmptyStyle.Render(" no matches"))
|
||||||
|
} else {
|
||||||
|
for i, e := range p.filtered {
|
||||||
|
active := i == p.cursor
|
||||||
|
rows = append(rows, styles.PickerItemStyle(active, e.isDir).Render(e.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list := lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||||
|
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Left, crumb, queryLine, list)
|
||||||
|
}
|
||||||
|
|
||||||
func (m TUIInterface) viewSelectServer() string {
|
func (m TUIInterface) viewSelectServer() string {
|
||||||
if len(m.ServerNames) == 0 {
|
if len(m.ServerNames) == 0 {
|
||||||
return styles.StatusWarnStyle.Render("⚠ No servers configured.")
|
return styles.StatusWarnStyle.Render("⚠ No servers configured.")
|
||||||
|
|||||||
11
main.go
11
main.go
@@ -15,8 +15,13 @@ func main() {
|
|||||||
if _, err := exec.LookPath("rsync"); err != nil {
|
if _, err := exec.LookPath("rsync"); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "error: rsync is required but was not found in PATH")
|
fmt.Fprintln(os.Stderr, "error: rsync is required but was not found in PATH")
|
||||||
fmt.Fprintln(os.Stderr, "install it with your package manager, e.g.:")
|
fmt.Fprintln(os.Stderr, "install it with your package manager, e.g.:")
|
||||||
fmt.Fprintln(os.Stderr, " brew install rsync")
|
fmt.Fprintln(os.Stderr, " sudo pacman -S rsync")
|
||||||
fmt.Fprintln(os.Stderr, " apt install rsync")
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
localDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "failed to get working directory:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +31,7 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
m := tui.NewTUIInterface(store)
|
m := tui.NewTUIInterface(store, localDir)
|
||||||
p := tea.NewProgram(m)
|
p := tea.NewProgram(m)
|
||||||
if _, err := p.Run(); err != nil {
|
if _, err := p.Run(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user