From c6c6f82ecaaf8533b4c677f9560debd8247936bf Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:38:38 -0400 Subject: [PATCH 01/11] workstation-v0: add Lampstand-backed search helper --- .../workstation-v0/bin/sourceos-search.sh | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 profiles/linux-dev/workstation-v0/bin/sourceos-search.sh diff --git a/profiles/linux-dev/workstation-v0/bin/sourceos-search.sh b/profiles/linux-dev/workstation-v0/bin/sourceos-search.sh new file mode 100644 index 0000000..d4b4b06 --- /dev/null +++ b/profiles/linux-dev/workstation-v0/bin/sourceos-search.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +err(){ printf "ERROR: %s\n" "$*" >&2; } +info(){ printf "INFO: %s\n" "$*" >&2; } +warn(){ printf "WARN: %s\n" "$*" >&2; } + +have(){ command -v "$1" >/dev/null 2>&1; } + +cache_dir(){ echo "${XDG_CACHE_HOME:-$HOME/.cache}/sourceos"; } + +open_file(){ + local p=$1 + if have xdg-open; then xdg-open "$p" >/dev/null 2>&1 || true + elif have open; then open "$p" >/dev/null 2>&1 || true + else warn "no opener found; wrote: $p" + fi +} + +run_lampstand(){ + if have lampstand; then lampstand "$@"; return; fi + if have python3 && python3 -c 'import lampstand.cli' >/dev/null 2>&1; then + python3 -m lampstand.cli "$@"; return + fi + err "Lampstand is not available on PATH and the Python module is not importable." + return 127 +} + +SEARCH_LIMIT=20 +SEARCH_SNIPPET=0 +SEARCH_PROMPT=0 +SEARCH_OPEN=0 +SEARCH_WRITE_PATH="" +QUERY_PARTS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --limit) shift; [[ $# -gt 0 ]] || { err "--limit requires a value"; exit 2; }; SEARCH_LIMIT="$1"; shift ;; + --snippet) SEARCH_SNIPPET=1; shift ;; + --prompt) SEARCH_PROMPT=1; shift ;; + --open) SEARCH_OPEN=1; shift ;; + --write) shift; [[ $# -gt 0 ]] || { err "--write requires a path"; exit 2; }; SEARCH_WRITE_PATH="$1"; shift ;; + -h|--help) + cat <<'EOF' +Usage: sourceos-search.sh [--limit N] [--snippet] [--prompt] [--open|--write ] [query...] +EOF + exit 0 ;; + *) QUERY_PARTS+=("$1"); shift ;; + esac +done + +if [[ ${#QUERY_PARTS[@]} -eq 0 && "$SEARCH_PROMPT" == "1" ]]; then + if have fuzzel; then q="$(printf '\n' | fuzzel --dmenu --prompt 'search> ' || true)" + elif have wofi; then q="$(printf '\n' | wofi --dmenu --prompt 'search> ' || true)" + elif have rofi; then q="$(printf '\n' | rofi -dmenu -p 'search> ' || true)" + elif have fzf; then q="$(printf '\n' | fzf --print-query --prompt='search> ' | head -n1 || true)" + else err "no prompt-capable launcher found"; exit 2 + fi + [[ -n "$q" ]] && QUERY_PARTS+=("$q") +fi + +query="${QUERY_PARTS[*]:-}" +[[ -n "$query" ]] || { err "search requires a query or --prompt"; exit 2; } + +args=(query "$query" --limit "$SEARCH_LIMIT") +[[ "$SEARCH_SNIPPET" == "1" ]] && args+=(--snippet) + +if [[ "$SEARCH_OPEN" == "1" || -n "$SEARCH_WRITE_PATH" ]]; then + path="$SEARCH_WRITE_PATH" + if [[ -z "$path" ]]; then mkdir -p "$(cache_dir)"; path="$(cache_dir)/search.txt"; fi + out="$(run_lampstand "${args[@]}")" + mkdir -p "$(dirname "$path")" + printf '%s\n' "$out" > "$path" + info "wrote: $path" + [[ "$SEARCH_OPEN" == "1" ]] && open_file "$path" + exit 0 +fi + +run_lampstand "${args[@]}" From 91399c25449f9306b9551ae6d743d79208ff11d3 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:39:50 -0400 Subject: [PATCH 02/11] workstation-v0: add Lampstand provisioning helper --- .../workstation-v0/bin/install-lampstand.sh | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 profiles/linux-dev/workstation-v0/bin/install-lampstand.sh diff --git a/profiles/linux-dev/workstation-v0/bin/install-lampstand.sh b/profiles/linux-dev/workstation-v0/bin/install-lampstand.sh new file mode 100644 index 0000000..aaab0a1 --- /dev/null +++ b/profiles/linux-dev/workstation-v0/bin/install-lampstand.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install Lampstand for workstation-v0 and optionally enable a user service. +# Strategy: +# - prefer a local checkout at ~/dev/lampstand (or SOURCEOS_LAMPSTAND_SRC) +# - otherwise install from the public git URL via pipx +# - write a user systemd unit for lampstandd if the daemon entrypoint exists + +info(){ printf "INFO: %s\n" "$*" >&2; } +warn(){ printf "WARN: %s\n" "$*" >&2; } +err(){ printf "ERROR: %s\n" "$*" >&2; } + +have(){ command -v "$1" >/dev/null 2>&1; } + +lampstand_src(){ + local local_src="${SOURCEOS_LAMPSTAND_SRC:-$HOME/dev/lampstand}" + if [[ -f "$local_src/pyproject.toml" ]]; then + printf '%s\n' "$local_src" + return + fi + printf '%s\n' 'git+https://github.com/SocioProphet/lampstand.git' +} + +ensure_pipx(){ + if have pipx; then + return 0 + fi + if have brew; then + info "Installing pipx via brew (best-effort)" + brew install pipx || true + fi + have pipx +} + +install_lampstand(){ + local src + src="$(lampstand_src)" + info "Installing Lampstand via pipx from: $src" + pipx install --force "$src" +} + +write_user_service(){ + local bin="$HOME/.local/bin/lampstandd" + if [[ ! -x "$bin" ]]; then + warn "lampstandd not found at $bin; skipping user service" + return 0 + fi + + local svc_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" + local svc="$svc_dir/sourceos-lampstand.service" + mkdir -p "$svc_dir" + + cat > "$svc" <<'EOF' +[Unit] +Description=SourceOS Lampstand search daemon +After=graphical-session.target +PartOf=graphical-session.target + +[Service] +Type=simple +ExecStart=%h/.local/bin/lampstandd --root %h --rpc unixjson +Restart=on-failure +RestartSec=2 + +[Install] +WantedBy=default.target +EOF + + info "wrote user service: $svc" + + if have systemctl; then + systemctl --user daemon-reload || true + systemctl --user enable sourceos-lampstand.service || true + systemctl --user restart sourceos-lampstand.service || true + fi +} + +main(){ + if ! ensure_pipx; then + warn "pipx is not available; skipping Lampstand install" + exit 0 + fi + + install_lampstand || { + warn "Lampstand install failed (non-fatal)" + exit 0 + } + + write_user_service || warn "Lampstand user service setup failed (non-fatal)" +} + +main "$@" From 164fab20c7b3b20ba46bf9e2ee0f481d6cae6762 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:43:12 -0400 Subject: [PATCH 03/11] packages(search): add Lampstand Nix package expression --- packages/search/lampstand.nix | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/search/lampstand.nix diff --git a/packages/search/lampstand.nix b/packages/search/lampstand.nix new file mode 100644 index 0000000..5266efa --- /dev/null +++ b/packages/search/lampstand.nix @@ -0,0 +1,26 @@ +{ lib, python3Packages, lampstand-src }: + +python3Packages.buildPythonApplication { + pname = "lampstand"; + version = "0.0.0"; + src = lampstand-src; + + pyproject = true; + build-system = with python3Packages; [ + setuptools + wheel + ]; + + pythonImportsCheck = [ + "lampstand.cli" + "lampstand.lampstandd" + ]; + + doCheck = false; + + meta = { + description = "SourceOS desktop file indexing and search service"; + license = lib.licenses.mit; + mainProgram = "lampstand"; + }; +} From 8ea1b4e8af5bcc18fe59bc01b0d4ea664c68dea2 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:44:34 -0400 Subject: [PATCH 04/11] ci(workstation): add Lampstand installer smoke workflow --- .github/workflows/workstation-lampstand.yml | 61 +++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/workstation-lampstand.yml diff --git a/.github/workflows/workstation-lampstand.yml b/.github/workflows/workstation-lampstand.yml new file mode 100644 index 0000000..f86cd3e --- /dev/null +++ b/.github/workflows/workstation-lampstand.yml @@ -0,0 +1,61 @@ +name: workstation-lampstand + +on: + pull_request: + paths: + - 'profiles/linux-dev/workstation-v0/bin/install-lampstand.sh' + - 'profiles/linux-dev/workstation-v0/bin/sourceos-search.sh' + - 'profiles/linux-dev/workstation-v0/manifest.yaml' + - '.github/workflows/workstation-lampstand.yml' + push: + branches: + - main + paths: + - 'profiles/linux-dev/workstation-v0/bin/install-lampstand.sh' + - 'profiles/linux-dev/workstation-v0/bin/sourceos-search.sh' + - 'profiles/linux-dev/workstation-v0/manifest.yaml' + - '.github/workflows/workstation-lampstand.yml' + +jobs: + lampstand-installer-smoke: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Syntax check installer + run: | + set -euo pipefail + bash -n profiles/linux-dev/workstation-v0/bin/install-lampstand.sh + bash -n profiles/linux-dev/workstation-v0/bin/sourceos-search.sh + + - name: Smoke: installer writes user service with stub pipx + run: | + set -euo pipefail + tmp_home=$(mktemp -d) + tmp_bin=$(mktemp -d) + tmp_src=$(mktemp -d) + printf '[project]\nname = "lampstand"\n' > "$tmp_src/pyproject.toml" + + cat > "$tmp_bin/pipx" <<'EOF' + #!/usr/bin/env bash + set -euo pipefail + test "${1:-}" = install + mkdir -p "$HOME/.local/bin" + cat > "$HOME/.local/bin/lampstandd" <<'EOS' + #!/usr/bin/env bash + exit 0 + EOS + chmod +x "$HOME/.local/bin/lampstandd" + EOF + chmod +x "$tmp_bin/pipx" + + HOME="$tmp_home" \ + XDG_CONFIG_HOME="$tmp_home/.config" \ + SOURCEOS_LAMPSTAND_SRC="$tmp_src" \ + PATH="$tmp_bin:$PATH" \ + bash profiles/linux-dev/workstation-v0/bin/install-lampstand.sh + + svc="$tmp_home/.config/systemd/user/sourceos-lampstand.service" + test -f "$svc" + grep -F 'lampstandd --root %h --rpc unixjson' "$svc" >/dev/null From e7002971bc3b99e054f99b4a93d14b05f7c2dd33 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:45:50 -0400 Subject: [PATCH 05/11] workstation-v0: declare Lampstand search backend and pipx --- profiles/linux-dev/workstation-v0/manifest.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/profiles/linux-dev/workstation-v0/manifest.yaml b/profiles/linux-dev/workstation-v0/manifest.yaml index 38e732e..6a1a89b 100644 --- a/profiles/linux-dev/workstation-v0/manifest.yaml +++ b/profiles/linux-dev/workstation-v0/manifest.yaml @@ -62,6 +62,7 @@ spec: - kondo - glow - hyperfine + - pipx - rclone - minio-mc - rsync @@ -90,3 +91,10 @@ spec: - rofi trigger: "space" command: "sourceos palette" + search: + file_backend: lampstand + invariant: no_redundant_file_search + authority: sourceos-search + notes: + - "Lampstand is the Linux-native file search backend." + - "The launcher remains an action bus and must not perform a second file-search pass." From c5c0ca763b7ba57bcc4a568750371ab5f9f6f816 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:47:18 -0400 Subject: [PATCH 06/11] workstation-v0: wire Lampstand provisioning into installer --- profiles/linux-dev/workstation-v0/install.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/profiles/linux-dev/workstation-v0/install.sh b/profiles/linux-dev/workstation-v0/install.sh index deaee4e..e29c2b3 100644 --- a/profiles/linux-dev/workstation-v0/install.sh +++ b/profiles/linux-dev/workstation-v0/install.sh @@ -75,6 +75,16 @@ install_sourceos_cli(){ fi } +install_lampstand_backend(){ + local script="$PROFILE_DIR/bin/install-lampstand.sh" + if [[ -x "$script" ]]; then + info "Installing Lampstand backend (best-effort)" + "$script" || warn "Lampstand install failed (non-fatal)" + else + warn "Lampstand installer not found: $script" + fi +} + patch_shell_rc_if_enabled(){ if ! autopatch_enabled; then return 0 @@ -188,6 +198,7 @@ main(){ install_user install_shell_spine install_sourceos_cli + install_lampstand_backend patch_shell_rc_if_enabled apply_gnome_baseline apply_gnome_extensions From 6866a882a7dc36b38dc368a6fa67d142cad3267d Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:48:32 -0400 Subject: [PATCH 07/11] workstation-v0: wire Lampstand search into sourceos helper --- .../linux-dev/workstation-v0/bin/sourceos | 212 +++++------------- 1 file changed, 59 insertions(+), 153 deletions(-) diff --git a/profiles/linux-dev/workstation-v0/bin/sourceos b/profiles/linux-dev/workstation-v0/bin/sourceos index 16baa08..504d4df 100644 --- a/profiles/linux-dev/workstation-v0/bin/sourceos +++ b/profiles/linux-dev/workstation-v0/bin/sourceos @@ -49,6 +49,7 @@ Usage: sourceos fix fish [apply|dry-run|revert] [--json] [--open|--write ] sourceos doctor [--json|--open|--write ] sourceos status [--json|--open|--write ] + sourceos search [--limit N] [--snippet] [--prompt] [--open|--write ] [query...] sourceos profile apply sourceos profile path @@ -259,17 +260,9 @@ run_fix_helper(){ exit "$rc" } -run_fix_all(){ - run_fix_helper all "$1" "$2" "$3" "$4" -} - -run_fix_shell(){ - run_fix_helper shell "$1" "$2" "$3" "$4" -} - -run_fix_fish(){ - run_fix_helper fish "$1" "$2" "$3" "$4" -} +run_fix_all(){ run_fix_helper all "$1" "$2" "$3" "$4"; } +run_fix_shell(){ run_fix_helper shell "$1" "$2" "$3" "$4"; } +run_fix_fish(){ run_fix_helper fish "$1" "$2" "$3" "$4"; } gnome_detect(){ [[ "${XDG_CURRENT_DESKTOP:-}" == *GNOME* ]] && return 0 @@ -277,12 +270,19 @@ gnome_detect(){ return 1 } -check_bin(){ - local bin=$1 - if have "$bin"; then - return 0 +check_bin(){ command -v "$1" >/dev/null 2>&1; } + +status_check_lampstand(){ + local search_helper="$PROFILE_DIR/bin/sourceos-search.sh" + + if [[ ! -f "$search_helper" ]]; then + WARNINGS+=("missing sourceos search helper: $search_helper") fi - return 1 + + if check_bin lampstand; then return 0; fi + if have python3 && python3 -c 'import lampstand.cli' >/dev/null 2>&1; then return 0; fi + + WARNINGS+=("lampstand backend unavailable") } status_collect(){ @@ -301,27 +301,19 @@ status_collect(){ ) for b in "${required[@]}"; do - if ! check_bin "$b"; then - REQUIRED_MISSING+=("$b") - fi + if ! check_bin "$b"; then REQUIRED_MISSING+=("$b"); fi done - if ! check_bin xclip; then - OPTIONAL_MISSING+=("xclip") - fi + if ! check_bin xclip; then OPTIONAL_MISSING+=("xclip"); fi + + status_check_lampstand if gnome_detect; then - if ! check_bin gsettings; then - WARNINGS+=("gsettings missing") - fi + if ! check_bin gsettings; then WARNINGS+=("gsettings missing"); fi if check_bin gnome-extensions; then - if ! gnome-extensions list 2>/dev/null | grep -qx 'dash-to-dock@micxgx.gmail.com'; then - WARNINGS+=("missing gnome extension: dash-to-dock@micxgx.gmail.com") - fi - if ! gnome-extensions list 2>/dev/null | grep -qx 'appindicatorsupport@rgcjonas.gmail.com'; then - WARNINGS+=("missing gnome extension: appindicatorsupport@rgcjonas.gmail.com") - fi + if ! gnome-extensions list 2>/dev/null | grep -qx 'dash-to-dock@micxgx.gmail.com'; then WARNINGS+=("missing gnome extension: dash-to-dock@micxgx.gmail.com"); fi + if ! gnome-extensions list 2>/dev/null | grep -qx 'appindicatorsupport@rgcjonas.gmail.com'; then WARNINGS+=("missing gnome extension: appindicatorsupport@rgcjonas.gmail.com"); fi else WARNINGS+=("gnome-extensions missing") fi @@ -332,12 +324,9 @@ status_json(){ status_collect local ok=true - if (( ${#REQUIRED_MISSING[@]} > 0 )); then - ok=false - fi + if (( ${#REQUIRED_MISSING[@]} > 0 )); then ok=false; fi local profile="linux-dev/workstation-v0" - local gnome=false if gnome_detect; then gnome=true; fi @@ -347,32 +336,21 @@ status_json(){ printf ',"gnome":%s' "$gnome" printf ',"required_missing":[' - for ((i=0;i<${#REQUIRED_MISSING[@]};i++)); do - [[ $i -gt 0 ]] && printf ',' - printf '"%s"' "$(json_escape "${REQUIRED_MISSING[$i]}")" - done + for ((i=0;i<${#REQUIRED_MISSING[@]};i++)); do [[ $i -gt 0 ]] && printf ','; printf '"%s"' "$(json_escape "${REQUIRED_MISSING[$i]}")"; done printf ']' printf ',"optional_missing":[' - for ((i=0;i<${#OPTIONAL_MISSING[@]};i++)); do - [[ $i -gt 0 ]] && printf ',' - printf '"%s"' "$(json_escape "${OPTIONAL_MISSING[$i]}")" - done + for ((i=0;i<${#OPTIONAL_MISSING[@]};i++)); do [[ $i -gt 0 ]] && printf ','; printf '"%s"' "$(json_escape "${OPTIONAL_MISSING[$i]}")"; done printf ']' printf ',"warnings":[' - for ((i=0;i<${#WARNINGS[@]};i++)); do - [[ $i -gt 0 ]] && printf ',' - printf '"%s"' "$(json_escape "${WARNINGS[$i]}")" - done + for ((i=0;i<${#WARNINGS[@]};i++)); do [[ $i -gt 0 ]] && printf ','; printf '"%s"' "$(json_escape "${WARNINGS[$i]}")"; done printf ']' printf '}' printf '\n' - if [[ "$ok" == "true" ]]; then - return 0 - fi + if [[ "$ok" == "true" ]]; then return 0; fi return 2 } @@ -387,15 +365,11 @@ status_human(){ return 2 fi - if (( ${#OPTIONAL_MISSING[@]} > 0 )); then - warn "missing optional: ${OPTIONAL_MISSING[*]}" - fi + if (( ${#OPTIONAL_MISSING[@]} > 0 )); then warn "missing optional: ${OPTIONAL_MISSING[*]}"; fi if (( ${#WARNINGS[@]} > 0 )); then warn "warnings:" - for w in "${WARNINGS[@]}"; do - warn "- $w" - done + for w in "${WARNINGS[@]}"; do warn "- $w"; done fi return 0 @@ -420,9 +394,7 @@ status_write(){ local path=$1 [[ -n "$path" ]] || { err "--write requires a path"; exit 2; } - local dir - dir="$(dirname "$path")" - mkdir -p "$dir" + mkdir -p "$(dirname "$path")" set +e status_json >"$path" 2>/dev/null @@ -434,7 +406,6 @@ status_write(){ } palette_menu(){ - # Print menu entries as: labelcommand cat <<'EOF' Palette: Audit doctor (open JSON report) sourceos doctor --json --open Palette: Audit fix all (open report) sourceos fix all dry-run --open @@ -451,6 +422,7 @@ Palette: Fix fish config (apply) sourceos fix fish apply Palette: Revert fish config patch sourceos fix fish revert Status (open report) sourceos status --open Doctor (open text report) sourceos doctor --open +Search files (Lampstand) sourceos search --prompt --snippet --open Apply profile sourceos profile apply Open sesh sesh Open tmux tmux @@ -462,24 +434,10 @@ EOF } palette_select(){ - # Prefer Wayland-first fuzzel, then wofi, then rofi, then terminal fzf. - if have fuzzel; then - palette_menu | fuzzel --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}' - return - fi - if have wofi; then - palette_menu | wofi --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}' - return - fi - if have rofi; then - palette_menu | rofi -dmenu -p 'sourceos> ' | awk -F '\t' '{print $2}' - return - fi - if have fzf; then - palette_menu | fzf --with-nth=1 --delimiter=$'\t' --prompt='sourceos> ' | awk -F '\t' '{print $2}' - return - fi - + if have fuzzel; then palette_menu | fuzzel --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}'; return; fi + if have wofi; then palette_menu | wofi --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}'; return; fi + if have rofi; then palette_menu | rofi -dmenu -p 'sourceos> ' | awk -F '\t' '{print $2}'; return; fi + if have fzf; then palette_menu | fzf --with-nth=1 --delimiter=$'\t' --prompt='sourceos> ' | awk -F '\t' '{print $2}'; return; fi err "no launcher found (fuzzel/wofi/rofi/fzf)" return 2 } @@ -494,95 +452,43 @@ run_palette(){ main(){ local cmd=${1:-} case "$cmd" in - -h|--help|help|"") - usage - ;; - - palette) - run_palette - ;; - + -h|--help|help|"") usage ;; + palette) run_palette ;; fix) shift || true case "${1:-}" in - all) - shift || true - parse_fix_args "$@" - run_fix_all "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" - ;; - shell) - shift || true - parse_fix_args "$@" - run_fix_shell "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" - ;; - fish) - shift || true - parse_fix_args "$@" - run_fix_fish "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" - ;; - *) - err "unknown fix target: ${1:-}" - usage - exit 2 - ;; + all) shift || true; parse_fix_args "$@"; run_fix_all "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" ;; + shell) shift || true; parse_fix_args "$@"; run_fix_shell "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" ;; + fish) shift || true; parse_fix_args "$@"; run_fix_fish "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" ;; + *) err "unknown fix target: ${1:-}"; usage; exit 2 ;; esac ;; - - doctor) - shift || true - parse_doctor_args "$@" - run_doctor - ;; - + doctor) shift || true; parse_doctor_args "$@"; run_doctor ;; status) shift || true case "${1:-}" in - --json) - shift || true - status_json - ;; - --open) - shift || true - status_open - ;; - --write) - shift || true - status_write "${1:-}" - ;; - "") - status_human - ;; - *) - err "unknown status flag: ${1:-}" - usage - exit 2 - ;; + --json) shift || true; status_json ;; + --open) shift || true; status_open ;; + --write) shift || true; status_write "${1:-}" ;; + "") status_human ;; + *) err "unknown status flag: ${1:-}"; usage; exit 2 ;; esac ;; - + search) + shift || true + local search_helper="$PROFILE_DIR/bin/sourceos-search.sh" + [[ -f "$search_helper" ]] || { err "search helper missing: $search_helper"; exit 2; } + exec bash "$search_helper" "$@" + ;; profile) shift || true - local sub=${1:-} - case "$sub" in - apply) - run_apply - ;; - path) - profile_path - ;; - *) - err "unknown profile subcommand: $sub" - usage - exit 2 - ;; + case "${1:-}" in + apply) run_apply ;; + path) profile_path ;; + *) err "unknown profile subcommand: ${1:-}"; usage; exit 2 ;; esac ;; - - *) - err "unknown command: $cmd" - usage - exit 2 - ;; + *) err "unknown command: $cmd"; usage; exit 2 ;; esac } From 768b883b89dbe337ab0797443e52f4279daf0c6a Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:51:55 -0400 Subject: [PATCH 08/11] workstation-v0: add Lampstand doctor validation --- profiles/linux-dev/workstation-v0/doctor.sh | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/profiles/linux-dev/workstation-v0/doctor.sh b/profiles/linux-dev/workstation-v0/doctor.sh index e7e2c84..ad8fbb1 100644 --- a/profiles/linux-dev/workstation-v0/doctor.sh +++ b/profiles/linux-dev/workstation-v0/doctor.sh @@ -218,6 +218,33 @@ check_fusuma_lane(){ fi } +check_lampstand_lane(){ + local search_helper="$(cd "$(dirname "$0")" && pwd)/bin/sourceos-search.sh" + + if [[ -f "$search_helper" ]]; then + info "ok: sourceos search helper" + record_result ok sourceos-search-helper "$search_helper" + else + warn "sourceos search helper missing: $search_helper" + record_result warn sourceos-search-helper "missing" + fi + + if have lampstand; then + info "ok: lampstand" + record_result ok lampstand "binary present" + return + fi + + if have python3 && python3 -c 'import lampstand.cli' >/dev/null 2>&1; then + info "ok: lampstand python module" + record_result ok lampstand "python module importable" + return + fi + + warn "lampstand missing (search surface exists but backend is unavailable)" + record_result warn lampstand "missing backend" +} + check_gsettings_equals(){ local schema=$1 local key=$2 @@ -310,6 +337,7 @@ main(){ check rclone check mc check rsync + check_lampstand_lane if gnome_detect; then record_result info gnome "detected" From 3754a357c64d32d13b3c1ca0bd93f3dc0760543c Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:52:46 -0400 Subject: [PATCH 09/11] flake: expose Lampstand package for workstation-v0 --- flake.nix | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 0122873..5603031 100644 --- a/flake.nix +++ b/flake.nix @@ -3,9 +3,13 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + lampstand-src = { + url = "github:SocioProphet/lampstand"; + flake = false; + }; }; - outputs = { self, nixpkgs }: + outputs = { self, nixpkgs, lampstand-src }: let lib = nixpkgs.lib; systems = [ "x86_64-linux" "aarch64-linux" ]; @@ -19,6 +23,9 @@ meshd = pkgs.callPackage ./packages/mesh/meshd.nix { }; meshd-linkd = pkgs.callPackage ./packages/mesh/meshd-linkd.nix { }; meshd-exitd = pkgs.callPackage ./packages/mesh/meshd-exitd.nix { }; + lampstand = pkgs.callPackage ./packages/search/lampstand.nix { + inherit lampstand-src; + }; default = meshd; }); @@ -74,7 +81,7 @@ }; workstation-v0 = pkgs.mkShell { - packages = presentPkgs; + packages = presentPkgs ++ [ self.packages.${system}.lampstand ]; shellHook = '' echo "SourceOS Workstation v0 dev shell" echo "See docs/workstation/README.md" @@ -151,6 +158,7 @@ meshd-package = self.packages.${system}.meshd; meshd-linkd-package = self.packages.${system}.meshd-linkd; meshd-exitd-package = self.packages.${system}.meshd-exitd; + lampstand-package = self.packages.${system}.lampstand; }); sourceos = { From bbe2e8ec56c5fe52ee0889415287f5157c270977 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:53:50 -0400 Subject: [PATCH 10/11] docs(workstation): document Lampstand search and provisioning lane --- docs/workstation/README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/workstation/README.md b/docs/workstation/README.md index 7ac5691..4cf4afa 100644 --- a/docs/workstation/README.md +++ b/docs/workstation/README.md @@ -21,18 +21,25 @@ Workstation scripts are guarded by the `workstation-scripts` GitHub Actions work - bash -n - a small `sourceos status --json` smoke parse +The Lampstand provisioning/search lane is guarded by the `workstation-lampstand` workflow: +- installer syntax checks +- `sourceos-search.sh` syntax checks +- stubbed `pipx` smoke proving the installer writes the user service + It triggers on PRs and main pushes touching: - `profiles/linux-dev/workstation-v0/**` - `docs/workstation/**` -Workflow file: +Workflow files: - `.github/workflows/workstation-scripts.yml` +- `.github/workflows/workstation-lampstand.yml` ## Workstation v0 goals - CLI-first developer experience with keyboard-first navigation. - GNOME baseline customization via GSettings and pinned extensions (no GNOME core forks). - Open-source launcher palette (Wayland-first): `sourceos palette` uses fuzzel (primary) with wofi/rofi fallbacks. +- Lampstand-backed local search surface via `sourceos search`, with the launcher treated as an action bus rather than a second filesystem index. - Primary keyboard remap lane: `input-remapper` on Fedora/GNOME. - Compatibility remap lanes: `xremap` and `kinto` (explicit compatibility path, not default). - Touchpad gesture lane: `fusuma`. @@ -45,6 +52,16 @@ From the repo: ./profiles/linux-dev/workstation-v0/install.sh +The profile installer provisions Lampstand best-effort through: + + ./profiles/linux-dev/workstation-v0/bin/install-lampstand.sh + +Lampstand install behavior: +- `pipx` is the user-space installer. +- local checkout is preferred from `$SOURCEOS_LAMPSTAND_SRC` or `~/dev/lampstand` when present. +- otherwise the installer falls back to `git+https://github.com/SocioProphet/lampstand.git`. +- when `lampstandd` is available, the installer writes `sourceos-lampstand.service` under the user systemd directory. + ## Validate ./profiles/linux-dev/workstation-v0/doctor.sh @@ -53,6 +70,7 @@ Or via the installed helper: sourceos doctor sourceos status --json + sourceos search 'report OR invoice' --snippet ## Nix-first support @@ -60,7 +78,12 @@ This repository is Nix-native. A dev shell is provided: nix develop .#workstation-v0 +Lampstand is also exposed as a Nix package: + + nix build .#lampstand + Notes: +- The workstation devShell includes the repo-local Lampstand package in addition to best-effort nixpkgs CLI tools. - The workstation devShell is best-effort; missing nixpkgs attrs will be reported on entry. - The profile installers still support non-Nix systems using dnf/rpm-ostree and brew where applicable. @@ -69,6 +92,8 @@ Notes: - Workstation v0 avoids non-open launchers. - Launcher install is best-effort via distro packages (Fedora: fuzzel) and does not silently enable third-party repos. - Kinto is treated as an explicit compatibility lane rather than the default Wayland-first path. +- File search should resolve through Lampstand when available; the launcher must not run a redundant second file-search pass. +- Lampstand is installed in user space and exposed through a user service, not as a mandatory host-system package. ## Related docs From c049fc6a29c1c0dc7a0d2ddd84ec87c9644d9877 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:59:33 -0400 Subject: [PATCH 11/11] workstation-v0: minimize sourceos helper diff for Lampstand search --- .../linux-dev/workstation-v0/bin/sourceos | 203 ++++++++++++++---- 1 file changed, 163 insertions(+), 40 deletions(-) diff --git a/profiles/linux-dev/workstation-v0/bin/sourceos b/profiles/linux-dev/workstation-v0/bin/sourceos index 504d4df..fbc4840 100644 --- a/profiles/linux-dev/workstation-v0/bin/sourceos +++ b/profiles/linux-dev/workstation-v0/bin/sourceos @@ -260,9 +260,17 @@ run_fix_helper(){ exit "$rc" } -run_fix_all(){ run_fix_helper all "$1" "$2" "$3" "$4"; } -run_fix_shell(){ run_fix_helper shell "$1" "$2" "$3" "$4"; } -run_fix_fish(){ run_fix_helper fish "$1" "$2" "$3" "$4"; } +run_fix_all(){ + run_fix_helper all "$1" "$2" "$3" "$4" +} + +run_fix_shell(){ + run_fix_helper shell "$1" "$2" "$3" "$4" +} + +run_fix_fish(){ + run_fix_helper fish "$1" "$2" "$3" "$4" +} gnome_detect(){ [[ "${XDG_CURRENT_DESKTOP:-}" == *GNOME* ]] && return 0 @@ -270,7 +278,13 @@ gnome_detect(){ return 1 } -check_bin(){ command -v "$1" >/dev/null 2>&1; } +check_bin(){ + local bin=$1 + if have "$bin"; then + return 0 + fi + return 1 +} status_check_lampstand(){ local search_helper="$PROFILE_DIR/bin/sourceos-search.sh" @@ -279,8 +293,13 @@ status_check_lampstand(){ WARNINGS+=("missing sourceos search helper: $search_helper") fi - if check_bin lampstand; then return 0; fi - if have python3 && python3 -c 'import lampstand.cli' >/dev/null 2>&1; then return 0; fi + if check_bin lampstand; then + return 0 + fi + + if have python3 && python3 -c 'import lampstand.cli' >/dev/null 2>&1; then + return 0 + fi WARNINGS+=("lampstand backend unavailable") } @@ -301,19 +320,29 @@ status_collect(){ ) for b in "${required[@]}"; do - if ! check_bin "$b"; then REQUIRED_MISSING+=("$b"); fi + if ! check_bin "$b"; then + REQUIRED_MISSING+=("$b") + fi done - if ! check_bin xclip; then OPTIONAL_MISSING+=("xclip"); fi + if ! check_bin xclip; then + OPTIONAL_MISSING+=("xclip") + fi status_check_lampstand if gnome_detect; then - if ! check_bin gsettings; then WARNINGS+=("gsettings missing"); fi + if ! check_bin gsettings; then + WARNINGS+=("gsettings missing") + fi if check_bin gnome-extensions; then - if ! gnome-extensions list 2>/dev/null | grep -qx 'dash-to-dock@micxgx.gmail.com'; then WARNINGS+=("missing gnome extension: dash-to-dock@micxgx.gmail.com"); fi - if ! gnome-extensions list 2>/dev/null | grep -qx 'appindicatorsupport@rgcjonas.gmail.com'; then WARNINGS+=("missing gnome extension: appindicatorsupport@rgcjonas.gmail.com"); fi + if ! gnome-extensions list 2>/dev/null | grep -qx 'dash-to-dock@micxgx.gmail.com'; then + WARNINGS+=("missing gnome extension: dash-to-dock@micxgx.gmail.com") + fi + if ! gnome-extensions list 2>/dev/null | grep -qx 'appindicatorsupport@rgcjonas.gmail.com'; then + WARNINGS+=("missing gnome extension: appindicatorsupport@rgcjonas.gmail.com") + fi else WARNINGS+=("gnome-extensions missing") fi @@ -324,9 +353,12 @@ status_json(){ status_collect local ok=true - if (( ${#REQUIRED_MISSING[@]} > 0 )); then ok=false; fi + if (( ${#REQUIRED_MISSING[@]} > 0 )); then + ok=false + fi local profile="linux-dev/workstation-v0" + local gnome=false if gnome_detect; then gnome=true; fi @@ -336,21 +368,32 @@ status_json(){ printf ',"gnome":%s' "$gnome" printf ',"required_missing":[' - for ((i=0;i<${#REQUIRED_MISSING[@]};i++)); do [[ $i -gt 0 ]] && printf ','; printf '"%s"' "$(json_escape "${REQUIRED_MISSING[$i]}")"; done + for ((i=0;i<${#REQUIRED_MISSING[@]};i++)); do + [[ $i -gt 0 ]] && printf ',' + printf '"%s"' "$(json_escape "${REQUIRED_MISSING[$i]}")" + done printf ']' printf ',"optional_missing":[' - for ((i=0;i<${#OPTIONAL_MISSING[@]};i++)); do [[ $i -gt 0 ]] && printf ','; printf '"%s"' "$(json_escape "${OPTIONAL_MISSING[$i]}")"; done + for ((i=0;i<${#OPTIONAL_MISSING[@]};i++)); do + [[ $i -gt 0 ]] && printf ',' + printf '"%s"' "$(json_escape "${OPTIONAL_MISSING[$i]}")" + done printf ']' printf ',"warnings":[' - for ((i=0;i<${#WARNINGS[@]};i++)); do [[ $i -gt 0 ]] && printf ','; printf '"%s"' "$(json_escape "${WARNINGS[$i]}")"; done + for ((i=0;i<${#WARNINGS[@]};i++)); do + [[ $i -gt 0 ]] && printf ',' + printf '"%s"' "$(json_escape "${WARNINGS[$i]}")" + done printf ']' printf '}' printf '\n' - if [[ "$ok" == "true" ]]; then return 0; fi + if [[ "$ok" == "true" ]]; then + return 0 + fi return 2 } @@ -365,11 +408,15 @@ status_human(){ return 2 fi - if (( ${#OPTIONAL_MISSING[@]} > 0 )); then warn "missing optional: ${OPTIONAL_MISSING[*]}"; fi + if (( ${#OPTIONAL_MISSING[@]} > 0 )); then + warn "missing optional: ${OPTIONAL_MISSING[*]}" + fi if (( ${#WARNINGS[@]} > 0 )); then warn "warnings:" - for w in "${WARNINGS[@]}"; do warn "- $w"; done + for w in "${WARNINGS[@]}"; do + warn "- $w" + done fi return 0 @@ -394,7 +441,9 @@ status_write(){ local path=$1 [[ -n "$path" ]] || { err "--write requires a path"; exit 2; } - mkdir -p "$(dirname "$path")" + local dir + dir="$(dirname "$path")" + mkdir -p "$dir" set +e status_json >"$path" 2>/dev/null @@ -406,6 +455,7 @@ status_write(){ } palette_menu(){ + # Print menu entries as: labelcommand cat <<'EOF' Palette: Audit doctor (open JSON report) sourceos doctor --json --open Palette: Audit fix all (open report) sourceos fix all dry-run --open @@ -434,10 +484,24 @@ EOF } palette_select(){ - if have fuzzel; then palette_menu | fuzzel --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}'; return; fi - if have wofi; then palette_menu | wofi --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}'; return; fi - if have rofi; then palette_menu | rofi -dmenu -p 'sourceos> ' | awk -F '\t' '{print $2}'; return; fi - if have fzf; then palette_menu | fzf --with-nth=1 --delimiter=$'\t' --prompt='sourceos> ' | awk -F '\t' '{print $2}'; return; fi + # Prefer Wayland-first fuzzel, then wofi, then rofi, then terminal fzf. + if have fuzzel; then + palette_menu | fuzzel --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}' + return + fi + if have wofi; then + palette_menu | wofi --dmenu --prompt 'sourceos> ' | awk -F '\t' '{print $2}' + return + fi + if have rofi; then + palette_menu | rofi -dmenu -p 'sourceos> ' | awk -F '\t' '{print $2}' + return + fi + if have fzf; then + palette_menu | fzf --with-nth=1 --delimiter=$'\t' --prompt='sourceos> ' | awk -F '\t' '{print $2}' + return + fi + err "no launcher found (fuzzel/wofi/rofi/fzf)" return 2 } @@ -452,43 +516,102 @@ run_palette(){ main(){ local cmd=${1:-} case "$cmd" in - -h|--help|help|"") usage ;; - palette) run_palette ;; + -h|--help|help|"") + usage + ;; + + palette) + run_palette + ;; + fix) shift || true case "${1:-}" in - all) shift || true; parse_fix_args "$@"; run_fix_all "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" ;; - shell) shift || true; parse_fix_args "$@"; run_fix_shell "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" ;; - fish) shift || true; parse_fix_args "$@"; run_fix_fish "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" ;; - *) err "unknown fix target: ${1:-}"; usage; exit 2 ;; + all) + shift || true + parse_fix_args "$@" + run_fix_all "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" + ;; + shell) + shift || true + parse_fix_args "$@" + run_fix_shell "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" + ;; + fish) + shift || true + parse_fix_args "$@" + run_fix_fish "$FIX_MODE" "$FIX_JSON" "$FIX_OPEN" "$FIX_WRITE_PATH" + ;; + *) + err "unknown fix target: ${1:-}" + usage + exit 2 + ;; esac ;; - doctor) shift || true; parse_doctor_args "$@"; run_doctor ;; + + doctor) + shift || true + parse_doctor_args "$@" + run_doctor + ;; + status) shift || true case "${1:-}" in - --json) shift || true; status_json ;; - --open) shift || true; status_open ;; - --write) shift || true; status_write "${1:-}" ;; - "") status_human ;; - *) err "unknown status flag: ${1:-}"; usage; exit 2 ;; + --json) + shift || true + status_json + ;; + --open) + shift || true + status_open + ;; + --write) + shift || true + status_write "${1:-}" + ;; + "") + status_human + ;; + *) + err "unknown status flag: ${1:-}" + usage + exit 2 + ;; esac ;; + search) shift || true local search_helper="$PROFILE_DIR/bin/sourceos-search.sh" [[ -f "$search_helper" ]] || { err "search helper missing: $search_helper"; exit 2; } exec bash "$search_helper" "$@" ;; + profile) shift || true - case "${1:-}" in - apply) run_apply ;; - path) profile_path ;; - *) err "unknown profile subcommand: ${1:-}"; usage; exit 2 ;; + local sub=${1:-} + case "$sub" in + apply) + run_apply + ;; + path) + profile_path + ;; + *) + err "unknown profile subcommand: $sub" + usage + exit 2 + ;; esac ;; - *) err "unknown command: $cmd"; usage; exit 2 ;; + + *) + err "unknown command: $cmd" + usage + exit 2 + ;; esac }