feat: complete session preferences and PAM fixture gates

This commit is contained in:
2026-09-26 02:44:29 +05:30
parent 83345a1842
commit ee6186ed0b
34 changed files with 801 additions and 327 deletions

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
// auth — UTMPX user accounting (glibc only).
//
// Mirrors vendor/src/auth/utmpx.rs.
package auth
/*

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,3 @@
// session — environment variable setup for latchd sessions.
//
// Mirrors vendor/src/post_login/env_variables.rs.
package session
import (

View File

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

View File

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

View File

@@ -1,4 +1 @@
package session
// Wayland session spawning is handled in exec.go/spawnWayland.
// This file exists for future Wayland-specific compositor logic.

View File

@@ -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",

View File

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