more UI and change default local user permissions

This commit is contained in:
2026-03-19 22:22:24 +00:00
parent a997b184f5
commit a79666f5a6
4 changed files with 198 additions and 37 deletions

48
app.go
View File

@@ -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
}

View File

@@ -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)
}
}

Binary file not shown.

129
tui.go
View File

@@ -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))
}