init
This commit is contained in:
16
internal/pages/home.go
Normal file
16
internal/pages/home.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package pages
|
||||
|
||||
type HomePageMsg struct{}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Key string
|
||||
}
|
||||
|
||||
func HomeMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Select Server", Key: "server"},
|
||||
{Label: "Config", Key: "config"},
|
||||
{Label: "Exit", Key: "exit"},
|
||||
}
|
||||
}
|
||||
56
internal/services/config.go
Normal file
56
internal/services/config.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Host string `json:"host"`
|
||||
User string `json:"user"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
Port string `json:"port"`
|
||||
}
|
||||
|
||||
type ConfigService struct {
|
||||
path string
|
||||
servers map[string]Server
|
||||
}
|
||||
|
||||
func NewConfigService() (*ConfigService, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dir := filepath.Join(configDir, "filepass")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, "servers.json")
|
||||
|
||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.WriteFile(path, []byte("{}"), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var servers map[string]Server
|
||||
if err := json.Unmarshal(data, &servers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ConfigService{path: path, servers: servers}, nil
|
||||
}
|
||||
|
||||
func (c *ConfigService) Servers() map[string]Server {
|
||||
return c.servers
|
||||
}
|
||||
13
internal/services/services_store.go
Normal file
13
internal/services/services_store.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package services
|
||||
|
||||
type ServicesStore struct {
|
||||
Config *ConfigService
|
||||
}
|
||||
|
||||
func NewServicesStore() (*ServicesStore, error) {
|
||||
cfg, err := NewConfigService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ServicesStore{Config: cfg}, nil
|
||||
}
|
||||
66
internal/styles/styles.go
Normal file
66
internal/styles/styles.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package styles
|
||||
|
||||
import lipgloss "charm.land/lipgloss/v2"
|
||||
|
||||
var (
|
||||
// Card / box
|
||||
CardStyle = lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("62")).
|
||||
Width(52)
|
||||
|
||||
CardInnerStyle = lipgloss.NewStyle().
|
||||
Padding(1, 3)
|
||||
|
||||
CardTitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("86")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
CardSubtitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("245")).
|
||||
MarginBottom(1)
|
||||
|
||||
// Menu items
|
||||
menuItemBase = lipgloss.NewStyle().
|
||||
PaddingLeft(2).
|
||||
Width(44)
|
||||
|
||||
menuItemActive = menuItemBase.
|
||||
Foreground(lipgloss.Color("75")).
|
||||
Bold(true).
|
||||
SetString("▸ ")
|
||||
|
||||
menuItemInactive = menuItemBase.
|
||||
Foreground(lipgloss.Color("245"))
|
||||
|
||||
// Status lines
|
||||
StatusWarnStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("221")).
|
||||
MarginTop(1)
|
||||
|
||||
StatusErrStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("203")).
|
||||
MarginTop(1)
|
||||
|
||||
// Footer
|
||||
FooterStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")).
|
||||
BorderTop(true).
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("237")).
|
||||
Padding(0, 1).
|
||||
Width(50)
|
||||
|
||||
FooterKeyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("86")).Bold(true)
|
||||
FooterSepStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("237"))
|
||||
FooterDescStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
|
||||
)
|
||||
|
||||
// MenuItemStyle returns the appropriate style for a menu row.
|
||||
func MenuItemStyle(active bool) lipgloss.Style {
|
||||
if active {
|
||||
return menuItemActive
|
||||
}
|
||||
return menuItemInactive
|
||||
}
|
||||
22
internal/tui/init.go
Normal file
22
internal/tui/init.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"filepass/internal/pages"
|
||||
"filepass/internal/services"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
type configLoadedMsg struct {
|
||||
servers map[string]services.Server
|
||||
err error
|
||||
}
|
||||
|
||||
func (m TUIInterface) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
func() tea.Msg { return pages.HomePageMsg{} },
|
||||
func() tea.Msg {
|
||||
return configLoadedMsg{servers: m.Services.Config.Servers()}
|
||||
},
|
||||
)
|
||||
}
|
||||
25
internal/tui/tui.go
Normal file
25
internal/tui/tui.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"filepass/internal/pages"
|
||||
"filepass/internal/services"
|
||||
)
|
||||
|
||||
type TUIInterface struct {
|
||||
Services *services.ServicesStore
|
||||
MenuItems []pages.MenuItem
|
||||
Selected int
|
||||
Servers map[string]services.Server
|
||||
NoServers bool
|
||||
InitErr error
|
||||
Quitting bool
|
||||
WindowWidth int
|
||||
WindowHeight int
|
||||
}
|
||||
|
||||
func NewTUIInterface(store *services.ServicesStore) TUIInterface {
|
||||
return TUIInterface{
|
||||
Services: store,
|
||||
MenuItems: pages.HomeMenuItems(),
|
||||
}
|
||||
}
|
||||
51
internal/tui/update.go
Normal file
51
internal/tui/update.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"filepass/internal/pages"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case pages.HomePageMsg:
|
||||
return m, nil
|
||||
|
||||
case configLoadedMsg:
|
||||
if msg.err != nil {
|
||||
m.InitErr = msg.err
|
||||
return m, nil
|
||||
}
|
||||
m.Servers = msg.servers
|
||||
m.NoServers = len(msg.servers) == 0
|
||||
return m, nil
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.WindowWidth = msg.Width
|
||||
m.WindowHeight = msg.Height
|
||||
return m, nil
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.Selected > 0 {
|
||||
m.Selected--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.Selected < len(m.MenuItems)-1 {
|
||||
m.Selected++
|
||||
}
|
||||
case "enter":
|
||||
if m.MenuItems[m.Selected].Key == "exit" {
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
// TODO: dispatch to server/config pages
|
||||
case "ctrl+c", "esc":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
91
internal/tui/view.go
Normal file
91
internal/tui/view.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"filepass/internal/styles"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
lipgloss "charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
func footerHint(key, desc string) string {
|
||||
return styles.FooterKeyStyle.Render(key) +
|
||||
" " +
|
||||
styles.FooterDescStyle.Render(desc)
|
||||
}
|
||||
|
||||
func footerSep() string {
|
||||
return styles.FooterSepStyle.Render(" · ")
|
||||
}
|
||||
|
||||
func (m TUIInterface) View() tea.View {
|
||||
if m.Quitting {
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
w := m.WindowWidth
|
||||
h := m.WindowHeight
|
||||
if w == 0 {
|
||||
w = 80
|
||||
}
|
||||
if h == 0 {
|
||||
h = 24
|
||||
}
|
||||
|
||||
// menu rows
|
||||
var menuRows []string
|
||||
for i, item := range m.MenuItems {
|
||||
menuRows = append(menuRows, styles.MenuItemStyle(i == m.Selected).Render(item.Label))
|
||||
}
|
||||
menu := lipgloss.JoinVertical(lipgloss.Left, menuRows...)
|
||||
|
||||
// status line — error takes priority over no-servers hint
|
||||
var statusLine string
|
||||
switch {
|
||||
case m.InitErr != nil:
|
||||
statusLine = styles.StatusErrStyle.Render("✗ " + m.InitErr.Error())
|
||||
case m.NoServers:
|
||||
statusLine = styles.StatusWarnStyle.Render("⚠ No servers configured. Select Config to add one.")
|
||||
}
|
||||
|
||||
// top content
|
||||
innerRows := []string{
|
||||
styles.CardTitleStyle.Render("✦ filepass"),
|
||||
styles.CardSubtitleStyle.Render("Secure file transfer"),
|
||||
menu,
|
||||
}
|
||||
if statusLine != "" {
|
||||
innerRows = append(innerRows, statusLine)
|
||||
}
|
||||
topContent := styles.CardInnerStyle.Render(
|
||||
lipgloss.JoinVertical(lipgloss.Left, innerRows...),
|
||||
)
|
||||
|
||||
// footer
|
||||
hints := footerHint("↑↓", "navigate") +
|
||||
footerSep() +
|
||||
footerHint("enter", "select") +
|
||||
footerSep() +
|
||||
footerHint("esc", "quit")
|
||||
footer := styles.FooterStyle.Render(hints)
|
||||
|
||||
// card
|
||||
card := styles.CardStyle.Render(
|
||||
lipgloss.JoinVertical(lipgloss.Left,
|
||||
topContent,
|
||||
footer,
|
||||
),
|
||||
)
|
||||
|
||||
cardHeight := lipgloss.Height(card)
|
||||
topPad := max((h-cardHeight)/2, 0)
|
||||
|
||||
centeredCard := lipgloss.NewStyle().
|
||||
Width(w).
|
||||
Align(lipgloss.Center).
|
||||
PaddingTop(topPad).
|
||||
Render(card)
|
||||
|
||||
v := tea.NewView(centeredCard)
|
||||
v.AltScreen = true
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user