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 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"; + }; +} 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 "$@" 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..766f050 --- /dev/null +++ b/profiles/linux-dev/workstation-v0/bin/sourceos-search.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Thin Lampstand-backed local search helper for SourceOS workstation-v0. +# Keeps the launcher as an action bus and delegates file search to Lampstand. + +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[@]}" 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."