diff --git a/app.go b/app.go index a2af313..2018823 100644 --- a/app.go +++ b/app.go @@ -744,6 +744,52 @@ func (a *App) collectCIFSMountConfig(defaults CIFSMountConfig) (CIFSMountConfig, }, nil } +func defaultCIFSMountConfig() CIFSMountConfig { + uid, gid := defaultLocalMountOwnerGroup(os.Getenv, user.Current, user.Lookup, user.LookupGroupId) + return CIFSMountConfig{ + UID: uid, + GID: gid, + } +} + +func defaultLocalMountOwnerGroup( + getenv func(string) string, + currentUser func() (*user.User, error), + lookupUser func(string) (*user.User, error), + lookupGroupID func(string) (*user.Group, error), +) (uid string, gid string) { + resolveGroup := func(groupID string) string { + if strings.TrimSpace(groupID) == "" { + return "" + } + group, err := lookupGroupID(groupID) + if err != nil { + return strings.TrimSpace(groupID) + } + return strings.TrimSpace(group.Name) + } + + if sudoUser := strings.TrimSpace(getenv("SUDO_USER")); sudoUser != "" { + uid = sudoUser + if sudoGID := strings.TrimSpace(getenv("SUDO_GID")); sudoGID != "" { + return uid, resolveGroup(sudoGID) + } + if u, err := lookupUser(sudoUser); err == nil { + return uid, resolveGroup(u.Gid) + } + return uid, "" + } + + u, err := currentUser() + if err != nil { + return "", "" + } + + uid = strings.TrimSpace(u.Username) + gid = resolveGroup(u.Gid) + return uid, gid +} + func (a *App) ensureMountPoint(path string) error { info, err := os.Stat(path) if err == nil { @@ -1545,7 +1591,7 @@ func (a *App) addClientMount() error { return err } - cfg, err := a.collectCIFSMountConfig(CIFSMountConfig{}) + cfg, err := a.collectCIFSMountConfig(defaultCIFSMountConfig()) if err != nil { return err } diff --git a/app_test.go b/app_test.go index 5ab3f9f..8154cd1 100644 --- a/app_test.go +++ b/app_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "os/user" + "testing" +) func TestParseCIFSMountEntries(t *testing.T) { contents := `# comment @@ -86,3 +89,56 @@ func TestUpdateFstabContentsAddEditDelete(t *testing.T) { t.Fatalf("unexpected delete result:\nwant: %q\ngot: %q", wantDeleted, deleted) } } + +func TestDefaultLocalMountOwnerGroupUsesCurrentUser(t *testing.T) { + getenv := func(string) string { return "" } + currentUser := func() (*user.User, error) { + return &user.User{Username: "alice", Gid: "1000"}, nil + } + lookupUser := func(string) (*user.User, error) { + t.Fatal("lookupUser should not be called without SUDO_USER") + return nil, nil + } + lookupGroupID := func(id string) (*user.Group, error) { + if id != "1000" { + t.Fatalf("unexpected group id lookup: %q", id) + } + return &user.Group{Name: "alice"}, nil + } + + uid, gid := defaultLocalMountOwnerGroup(getenv, currentUser, lookupUser, lookupGroupID) + if uid != "alice" || gid != "alice" { + t.Fatalf("unexpected defaults: uid=%q gid=%q", uid, gid) + } +} + +func TestDefaultLocalMountOwnerGroupUsesSudoUser(t *testing.T) { + getenv := func(key string) string { + switch key { + case "SUDO_USER": + return "carol" + case "SUDO_GID": + return "2000" + default: + return "" + } + } + currentUser := func() (*user.User, error) { + return &user.User{Username: "root", Gid: "0"}, nil + } + lookupUser := func(string) (*user.User, error) { + t.Fatal("lookupUser should not be called when SUDO_GID is set") + return nil, nil + } + lookupGroupID := func(id string) (*user.Group, error) { + if id != "2000" { + t.Fatalf("unexpected group id lookup: %q", id) + } + return &user.Group{Name: "developers"}, nil + } + + uid, gid := defaultLocalMountOwnerGroup(getenv, currentUser, lookupUser, lookupGroupID) + if uid != "carol" || gid != "developers" { + t.Fatalf("unexpected defaults: uid=%q gid=%q", uid, gid) + } +} diff --git a/samba-configer b/samba-configer index fe5ad31..5fa600d 100755 Binary files a/samba-configer and b/samba-configer differ diff --git a/tui.go b/tui.go index 4780095..16d6389 100644 --- a/tui.go +++ b/tui.go @@ -12,7 +12,9 @@ import ( type tuiTheme struct { outer lipgloss.Style + headerBar lipgloss.Style badge lipgloss.Style + chrome lipgloss.Style title lipgloss.Style subtitle lipgloss.Style sectionTitle lipgloss.Style @@ -20,10 +22,15 @@ type tuiTheme struct { muted lipgloss.Style footer lipgloss.Style panel lipgloss.Style + panelAccent lipgloss.Style row lipgloss.Style rowSelected lipgloss.Style + rowNumber lipgloss.Style + rowNumberOn lipgloss.Style rowKey lipgloss.Style rowKeySelected lipgloss.Style + rowTitle lipgloss.Style + rowTitleOn lipgloss.Style rowHint lipgloss.Style rowHintActive lipgloss.Style statusInfo lipgloss.Style @@ -34,35 +41,44 @@ type tuiTheme struct { } func newTUITheme() tuiTheme { - border := lipgloss.Color("#4B5D6B") - text := lipgloss.Color("#E9E1D4") - muted := lipgloss.Color("#9E9385") - accent := lipgloss.Color("#D97745") - accentSoft := lipgloss.Color("#F4D8BD") - panel := lipgloss.Color("#192126") - panelAlt := lipgloss.Color("#232E36") - info := lipgloss.Color("#7FB0C2") - warn := lipgloss.Color("#D7A55A") - success := lipgloss.Color("#8AAC83") + border := lipgloss.Color("#5A6772") + borderSoft := lipgloss.Color("#31414A") + text := lipgloss.Color("#ECE4D8") + muted := lipgloss.Color("#A49788") + accent := lipgloss.Color("#D9733F") + accentSoft := lipgloss.Color("#F2D2B5") + panel := lipgloss.Color("#161C20") + panelAlt := lipgloss.Color("#212A30") + panelLift := lipgloss.Color("#2B363D") + info := lipgloss.Color("#86B7C6") + warn := lipgloss.Color("#D8AB63") + success := lipgloss.Color("#91B57F") return tuiTheme{ outer: lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(border). Padding(1, 2), + headerBar: lipgloss.NewStyle(). + Foreground(borderSoft), + chrome: lipgloss.NewStyle(). + Foreground(muted), badge: lipgloss.NewStyle(). Foreground(panel). Background(accentSoft). Bold(true). Padding(0, 1), + subtitle: lipgloss.NewStyle(). + Foreground(muted), title: lipgloss.NewStyle(). Foreground(text). Bold(true), - subtitle: lipgloss.NewStyle(). - Foreground(muted), sectionTitle: lipgloss.NewStyle(). Foreground(accentSoft). Bold(true). + Border(lipgloss.NormalBorder(), false, false, true, false). + BorderForeground(borderSoft). + PaddingBottom(0). MarginBottom(1), body: lipgloss.NewStyle(). Foreground(text), @@ -71,26 +87,50 @@ func newTUITheme() tuiTheme { footer: lipgloss.NewStyle(). Foreground(muted). Border(lipgloss.NormalBorder(), true, false, false, false). - BorderForeground(border). - PaddingTop(1), + BorderForeground(borderSoft). + PaddingTop(1). + MarginTop(1), panel: lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(border). - PaddingLeft(1), - row: lipgloss.NewStyle(). + BorderForeground(borderSoft). + PaddingLeft(1). + MarginBottom(1), + panelAccent: lipgloss.NewStyle(). Border(lipgloss.ThickBorder(), false, false, false, true). - BorderForeground(panelAlt). - PaddingLeft(1), + BorderForeground(accent). + PaddingLeft(1). + MarginBottom(1), + row: lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, false, true). + BorderForeground(panelLift). + Padding(0, 1). + MarginBottom(1), rowSelected: lipgloss.NewStyle(). Border(lipgloss.ThickBorder(), false, false, false, true). BorderForeground(accent). Background(panel). - PaddingLeft(1), + Padding(0, 1). + MarginBottom(1), + rowNumber: lipgloss.NewStyle(). + Foreground(muted), + rowNumberOn: lipgloss.NewStyle(). + Foreground(accentSoft). + Bold(true), rowKey: lipgloss.NewStyle(). Foreground(accent). - Bold(true), + Bold(true). + Background(panelAlt). + Padding(0, 1), rowKeySelected: lipgloss.NewStyle(). Foreground(accentSoft). + Background(panelLift). + Bold(true). + Padding(0, 1), + rowTitle: lipgloss.NewStyle(). + Foreground(text). + Bold(true), + rowTitleOn: lipgloss.NewStyle(). + Foreground(text). Bold(true), rowHint: lipgloss.NewStyle(). Foreground(muted), @@ -111,7 +151,8 @@ func newTUITheme() tuiTheme { inputBox: lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(border). - Padding(0, 1), + Padding(0, 1). + Background(panelAlt), } } @@ -144,10 +185,11 @@ func (t tuiTheme) renderFrame(width int, title, subtitle, body, footer string) s placeWidth = frameWidth } - sections := []string{ - t.badge.Render("SAMBA CONFIGER"), - t.title.Width(contentWidth).Render(title), - } + headerLeft := lipgloss.JoinHorizontal(lipgloss.Top, t.badge.Render("SAMBA CONFIGER"), " ", t.chrome.Render("share editor")) + headerRule := t.headerBar.Render(strings.Repeat("─", max(0, contentWidth-lipgloss.Width(headerLeft)-1))) + header := lipgloss.JoinHorizontal(lipgloss.Center, headerLeft, " ", headerRule) + + sections := []string{header, t.title.Width(contentWidth).Render(title)} if subtitle != "" { sections = append(sections, t.subtitle.Width(contentWidth).Render(subtitle)) } @@ -172,7 +214,7 @@ func (t tuiTheme) renderSection(width int, title string, lines []string) string rendered = append(rendered, t.sectionTitle.Width(contentWidth).Render(title)) } for _, line := range lines { - rendered = append(rendered, t.panel.Width(contentWidth).Render(t.body.Width(contentWidth-2).Render(line))) + rendered = append(rendered, t.panel.Width(contentWidth).Render(t.body.Width(max(1, contentWidth-2)).Render(line))) } return strings.Join(rendered, "\n") } @@ -190,6 +232,13 @@ func (t tuiTheme) renderMessage(width int, kind, text string) string { ) } +func max(a, b int) int { + if a > b { + return a + } + return b +} + type menuOption struct { Key string Value string @@ -260,17 +309,29 @@ func (m menuModel) View() string { for i, option := range m.options { rowStyle := m.theme.row keyStyle := m.theme.rowKey + titleStyle := m.theme.rowTitle hintStyle := m.theme.rowHint - prefix := m.theme.muted.Render(fmt.Sprintf("%02d", i+1)) + numberStyle := m.theme.rowNumber + prefix := numberStyle.Render(fmt.Sprintf("%02d", i+1)) if i == m.cursor { rowStyle = m.theme.rowSelected keyStyle = m.theme.rowKeySelected + titleStyle = m.theme.rowTitleOn hintStyle = m.theme.rowHintActive - prefix = keyStyle.Render(">>") + numberStyle = m.theme.rowNumberOn + prefix = numberStyle.Render(">>") } - label := fmt.Sprintf("%s %s %s", prefix, keyStyle.Render("["+option.Key+"]"), option.Label) - rowLines := []string{label} + top := lipgloss.JoinHorizontal( + lipgloss.Center, + prefix, + " ", + keyStyle.Render(strings.ToUpper(option.Key)), + " ", + titleStyle.Render(option.Label), + ) + + rowLines := []string{top} if option.Description != "" { rowLines = append(rowLines, hintStyle.Render(option.Description)) } @@ -360,10 +421,8 @@ func (m textPromptModel) View() string { width := clampFrameWidth(m.width) contentWidth := innerWidth(width) - lines := []string{ - m.theme.inputLabel.Width(contentWidth).Render(m.label), - m.theme.inputBox.Width(contentWidth).Render(m.input.View()), - } + lines := []string{m.theme.inputLabel.Width(contentWidth).Render(m.label)} + lines = append(lines, m.theme.panelAccent.Width(contentWidth).Render(m.theme.inputBox.Width(max(8, contentWidth-2)).Render(m.input.View()))) if m.defaultValue != "" { lines = append(lines, m.theme.muted.Width(contentWidth).Render("Enter keeps: "+m.defaultValue)) }