feat: complete session preferences and PAM fixture gates
This commit is contained in:
@@ -1,7 +1,3 @@
|
||||
// auth — faillock integration.
|
||||
//
|
||||
// Reads /etc/security/faillock.conf for deny count, executes
|
||||
// faillock --user to get tally, supports --reset via admin bypass.
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
// auth — PAM authentication with CGO bindings.
|
||||
//
|
||||
// Uses runtime.LockOSThread to prevent goroutine migration during
|
||||
// PAM transactions, as PAM modules rely on thread-local storage.
|
||||
//
|
||||
// Mirrors vendor/src/auth/pam.rs and vendor/src/auth/mod.rs.
|
||||
// Keep each PAM transaction on one OS thread. PAM modules can use thread-local state.
|
||||
package auth
|
||||
|
||||
/*
|
||||
@@ -12,11 +7,11 @@ package auth
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Match the //export-generated signature.
|
||||
// Match the signature that cgo generates for //export.
|
||||
extern int latchd_pam_conv(int, struct pam_message **,
|
||||
struct pam_response **, void *);
|
||||
|
||||
// Store a uintptr_t handle in C heap so the Go GC never sees a Go pointer.
|
||||
// Store the numeric handle in C memory. Do not pass a Go pointer to PAM.
|
||||
static void *storeHandle(uintptr_t h) {
|
||||
uintptr_t *p = malloc(sizeof(uintptr_t));
|
||||
if (p) *p = h;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// auth — UTMPX user accounting (glibc only).
|
||||
//
|
||||
// Mirrors vendor/src/auth/utmpx.rs.
|
||||
package auth
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// Helper: assign seconds + microseconds to utmpx's ut_tv field.
|
||||
// The C compiler handles the type conversion, so this works whether
|
||||
// ut_tv is struct timeval (long tv_sec/tv_usec) or the compat struct
|
||||
// (__uint32_t tv_sec / __int32_t tv_usec).
|
||||
// Let the C compiler convert values for either native or compatibility ut_tv fields.
|
||||
|
||||
#include <stdint.h>
|
||||
static inline void latchd_set_utmpx_time(struct utmpx *ut, int64_t sec, int32_t usec) {
|
||||
|
||||
@@ -168,7 +168,6 @@ func LoadVariables(filename string) (map[string]string, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", filename, err)
|
||||
}
|
||||
// Parse a flat TOML table of string values.
|
||||
vars := make(map[string]string)
|
||||
var table map[string]any
|
||||
if err := toml.Unmarshal(raw, &table); err != nil {
|
||||
@@ -180,8 +179,6 @@ func LoadVariables(filename string) (map[string]string, error) {
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
// ── variable substitution ───────────────────────────────────────────────
|
||||
|
||||
func resolveVars(in string, vars map[string]string) string {
|
||||
for k, v := range vars {
|
||||
in = strings.ReplaceAll(in, "$"+k, v)
|
||||
@@ -189,8 +186,6 @@ func resolveVars(in string, vars map[string]string) string {
|
||||
return in
|
||||
}
|
||||
|
||||
// ── embedded default config ─────────────────────────────────────────────
|
||||
|
||||
const defaultTOML = `
|
||||
tty = 2
|
||||
main_log_path = "/var/log/latchd.log"
|
||||
|
||||
@@ -18,8 +18,6 @@ import (
|
||||
"latchd/tui/bg"
|
||||
)
|
||||
|
||||
// ── pixelated user avatar ───────────────────────────────────────────────
|
||||
|
||||
var avatarArt = []string{
|
||||
` ▄▄▄▄▄ `,
|
||||
` ▄███████▄ `,
|
||||
@@ -31,8 +29,6 @@ var avatarArt = []string{
|
||||
` ██ ██ `,
|
||||
}
|
||||
|
||||
// ── messages ─────────────────────────────────────────────────────────────
|
||||
|
||||
type tickMsg time.Time
|
||||
|
||||
type authResult struct {
|
||||
@@ -57,8 +53,6 @@ const (
|
||||
modeAdminPass
|
||||
)
|
||||
|
||||
// ── result (sent back to main on successful login) ──────────────────────
|
||||
|
||||
type Result struct {
|
||||
Username string
|
||||
Password string
|
||||
@@ -67,8 +61,6 @@ type Result struct {
|
||||
Preview bool
|
||||
}
|
||||
|
||||
// ── model ───────────────────────────────────────────────────────────────
|
||||
|
||||
type Model struct {
|
||||
cfg config.Config
|
||||
preview bool
|
||||
@@ -137,8 +129,6 @@ func (m *Model) Init() tea.Cmd {
|
||||
return tea.Batch(tick(), tea.EnterAltScreen)
|
||||
}
|
||||
|
||||
// ── update ──────────────────────────────────────────────────────────────
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
@@ -222,8 +212,6 @@ func (m *Model) handleBypass(msg adminBypassResult) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ── keys ────────────────────────────────────────────────────────────────
|
||||
|
||||
func (m *Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
k := msg.String()
|
||||
|
||||
@@ -392,8 +380,6 @@ func (m *Model) selectedEnv() session.PostLoginEnv {
|
||||
return m.envs[m.selIdx]
|
||||
}
|
||||
|
||||
// ── commands ────────────────────────────────────────────────────────────
|
||||
|
||||
func (m *Model) loginCmd() tea.Cmd {
|
||||
if m.preview {
|
||||
return func() tea.Msg { time.Sleep(800 * time.Millisecond); return authResult{ok: true} }
|
||||
@@ -422,14 +408,10 @@ func (m *Model) adminBypassCmd() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// ── queries ────────────────────────────────────────────────────────────
|
||||
|
||||
func (m *Model) Result() *Result { return m.result }
|
||||
func (m *Model) Quitting() bool { return m.quitting }
|
||||
func (m *Model) SetAutomata(a *bg.Automata) { m.automata = a }
|
||||
|
||||
// ── view ───────────────────────────────────────────────────────────────
|
||||
|
||||
var (
|
||||
accent = lipgloss.Color("208") // orange
|
||||
subtle = lipgloss.Color("8") // grey
|
||||
@@ -527,7 +509,6 @@ func overlayCenter(w, h int, content string) string {
|
||||
func (m *Model) renderLayout() string {
|
||||
clock := m.renderClock()
|
||||
|
||||
// top bar: power hints on left, clock on right
|
||||
var topParts []string
|
||||
topParts = append(topParts, m.renderPowerHints())
|
||||
if clock != "" {
|
||||
@@ -535,24 +516,19 @@ func (m *Model) renderLayout() string {
|
||||
}
|
||||
top := strings.Join(topParts, " ")
|
||||
|
||||
// avatar + form side by side
|
||||
avatar := m.renderAvatar()
|
||||
form := m.renderForm()
|
||||
mid := lipgloss.JoinHorizontal(lipgloss.Top, avatar, " ", form)
|
||||
|
||||
// DE switcher with specifier label
|
||||
sw := m.renderSwitcher()
|
||||
|
||||
// caps lock
|
||||
caps := m.renderCapsLock()
|
||||
|
||||
// admin unlock
|
||||
var admin string
|
||||
if m.mode == modeAdminUser || m.mode == modeAdminPass {
|
||||
admin = m.renderAdminForm()
|
||||
}
|
||||
|
||||
// status
|
||||
st := m.renderStatus()
|
||||
|
||||
var parts []string
|
||||
@@ -655,7 +631,6 @@ func (m *Model) renderSwitcher() string {
|
||||
|
||||
var parts []string
|
||||
|
||||
// session specifier (like "x11", "wayland")
|
||||
spec := swDot.Render(m.envs[m.selIdx].Kind + " ")
|
||||
|
||||
if cfg.ShowMovers && m.selIdx > 0 {
|
||||
@@ -679,7 +654,6 @@ func (m *Model) renderSwitcher() string {
|
||||
|
||||
swLine := lipgloss.JoinHorizontal(lipgloss.Center, parts...)
|
||||
|
||||
// Dot indicators
|
||||
var dots []string
|
||||
for i := 0; i < len(m.envs); i++ {
|
||||
if i == m.selIdx {
|
||||
@@ -747,8 +721,6 @@ func (m *Model) renderStatus() string {
|
||||
return st.Align(lipgloss.Center).Width(70).Render(m.status)
|
||||
}
|
||||
|
||||
// ── caps lock detection ────────────────────────────────────────────────
|
||||
|
||||
const kdgkbled = 0x4B64
|
||||
|
||||
func capsLockOn() bool {
|
||||
@@ -762,8 +734,6 @@ func capsLockOn() bool {
|
||||
return leds&0x04 != 0
|
||||
}
|
||||
|
||||
// ── field editing ──────────────────────────────────────────────────────
|
||||
|
||||
func handleField(k string, buf *string, cur, scroll *int) {
|
||||
switch {
|
||||
case k == "backspace" || k == "ctrl+h":
|
||||
@@ -846,8 +816,6 @@ func abs(n int) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// ── tick ────────────────────────────────────────────────────────────────
|
||||
|
||||
func tick() tea.Cmd {
|
||||
return tea.Tick(time.Second/30, func(t time.Time) tea.Msg {
|
||||
return tickMsg(t)
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// latchd — TUI display/login manager for GNU/Linux and BSD.
|
||||
//
|
||||
// Supports TTY, X11, and Wayland sessions with PAM authentication.
|
||||
// Go rewrite of Lemurs (Rust source under vendor/).
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -193,8 +189,6 @@ func setupLog(path string) {
|
||||
log.SetFlags(log.LstdFlags)
|
||||
}
|
||||
|
||||
// ── chvt ────────────────────────────────────────────────────────────────
|
||||
|
||||
const vtA = 0x5606
|
||||
const vtW = 0x5607
|
||||
const kbt = 0x4B33
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// session — environment variable setup for latchd sessions.
|
||||
//
|
||||
// Mirrors vendor/src/post_login/env_variables.rs.
|
||||
package session
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// session — process execution and session spawning.
|
||||
//
|
||||
// Mirrors vendor/src/post_login/mod.rs and wait_with_log.rs.
|
||||
package session
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// session — environment scanning from xsessions, wayland-sessions,
|
||||
// and custom script directories. Includes desktop entry parser.
|
||||
//
|
||||
// Mirrors vendor/src/post_login/mod.rs:get_envs and parse_desktop_entry.
|
||||
package session
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
package session
|
||||
|
||||
// Wayland session spawning is handled in exec.go/spawnWayland.
|
||||
// This file exists for future Wayland-specific compositor logic.
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// session — X11 server setup.
|
||||
//
|
||||
// Mirrors vendor/src/post_login/x.rs:setup_x.
|
||||
package session
|
||||
|
||||
import (
|
||||
@@ -47,7 +44,7 @@ func setupX(uid, gid uint32, groups []uint32, cfg *config.Config) (*exec.Cmd, er
|
||||
dd = "0" + dd
|
||||
}
|
||||
|
||||
// Ignore SIGUSR1 so Xorg sends it when ready.
|
||||
// Xorg sends SIGUSR1 only if the parent ignores it during startup.
|
||||
signal.Ignore(syscall.SIGUSR1)
|
||||
|
||||
cmd := exec.Command(cfg.SystemShell, "-c",
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
// bg — cellular automata background for the TUI.
|
||||
//
|
||||
// Conway's Game of Life rendered at 30 Hz using Unicode half-block
|
||||
// characters (▀) to double the effective vertical resolution.
|
||||
// The grid is double-buffered for zero-allocation ticking, and
|
||||
// render output is built in a pre-allocated strings.Builder so the
|
||||
|
||||
// The renderer packs two cell rows into each Unicode half-block row.
|
||||
// Two buffers let Tick update the grid without an allocation per frame.
|
||||
package bg
|
||||
|
||||
import (
|
||||
@@ -12,7 +7,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Bright orange (accent) for alive cells, dark grey for dead.
|
||||
const (
|
||||
fgAlive = "\x1b[38;2;255;127;0m"
|
||||
fgDead = "\x1b[38;2;30;30;30m"
|
||||
@@ -92,8 +86,6 @@ func (a *Automata) count(g []uint8, x, y, w, h int) uint8 {
|
||||
return c
|
||||
}
|
||||
|
||||
// Frame returns the current generation as a single string suitable for
|
||||
// writing to the alternate screen.
|
||||
func (a *Automata) Frame() string {
|
||||
a.buf.Reset()
|
||||
src := a.a
|
||||
|
||||
Reference in New Issue
Block a user