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/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 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 = { 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 b/profiles/linux-dev/workstation-v0/bin/sourceos index 16baa08..fbc4840 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 @@ -285,6 +286,24 @@ check_bin(){ return 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 + + 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(){ REQUIRED_MISSING=() OPTIONAL_MISSING=() @@ -310,6 +329,8 @@ status_collect(){ OPTIONAL_MISSING+=("xclip") fi + status_check_lampstand + if gnome_detect; then if ! check_bin gsettings; then WARNINGS+=("gsettings missing") @@ -451,6 +472,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 @@ -560,6 +582,13 @@ main(){ 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:-} 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[@]}" 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" 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 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."