feat: add rofi-project-opener for rofi-based project launching
Adds package and HM module for quickly opening projects in terminals. Uses XDG config file instead of environment variables for better shell-independence and proper NixOS PATH sourcing.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# CLI/Terminal-related Home Manager modules
|
# CLI/Terminal-related Home Manager modules
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
./rofi-project-opener.nix
|
||||||
./stt-ptt.nix
|
./stt-ptt.nix
|
||||||
./zellij-ps.nix
|
./zellij-ps.nix
|
||||||
];
|
];
|
||||||
|
|||||||
90
modules/home-manager/cli/rofi-project-opener.nix
Normal file
90
modules/home-manager/cli/rofi-project-opener.nix
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
with lib; let
|
||||||
|
cfg = config.cli.rofi-project-opener;
|
||||||
|
|
||||||
|
# Convert list of paths to colon-separated string
|
||||||
|
projectDirsStr = concatStringsSep ":" cfg.projectDirs;
|
||||||
|
in {
|
||||||
|
options.cli.rofi-project-opener = {
|
||||||
|
enable = mkEnableOption "Rofi-based project directory launcher";
|
||||||
|
|
||||||
|
projectDirs = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = ["~/dev" "~/projects"];
|
||||||
|
description = ''
|
||||||
|
List of base directories to scan for project subdirectories.
|
||||||
|
Each directory will be scanned for immediate subdirectories (non-hidden).
|
||||||
|
Projects are displayed as "base_dir/project_name" in rofi.
|
||||||
|
|
||||||
|
Supports ~ for home directory expansion.
|
||||||
|
'';
|
||||||
|
example = literalExpression ''["~/dev" "~/projects" "~/code"]'';
|
||||||
|
};
|
||||||
|
|
||||||
|
terminal = mkOption {
|
||||||
|
type = types.either types.str types.package;
|
||||||
|
default = "kitty";
|
||||||
|
description = "Terminal emulator to use for launching opencode. Can be a string or package.";
|
||||||
|
example = literalExpression "pkgs.alacritty";
|
||||||
|
};
|
||||||
|
|
||||||
|
terminalCommand = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = ''
|
||||||
|
Custom command to pass to the terminal.
|
||||||
|
|
||||||
|
Use %s as a placeholder for the project path.
|
||||||
|
If empty, defaults to opening a shell in the project directory and running opencode.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- "" (empty) - Uses default: cd to project, run opencode
|
||||||
|
- "-e zsh -c 'cd %s && opencode'" - Custom shell with explicit path
|
||||||
|
- "--hold -e nvim" - Open editor directly (no %s = no cd)
|
||||||
|
'';
|
||||||
|
example = literalExpression ''"-e zsh -c 'cd %s && opencode'"'';
|
||||||
|
};
|
||||||
|
|
||||||
|
rofiPrompt = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "Select project";
|
||||||
|
description = "Prompt text displayed in rofi.";
|
||||||
|
example = "Open project:";
|
||||||
|
};
|
||||||
|
|
||||||
|
rofiArgs = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = ["-dmenu" "-i"];
|
||||||
|
description = ''
|
||||||
|
Arguments to pass to rofi.
|
||||||
|
|
||||||
|
Common options:
|
||||||
|
- "-dmenu" - Enable dmenu mode (required)
|
||||||
|
- "-i" - Case-insensitive matching
|
||||||
|
- "-theme <theme>" - Use specific rofi theme
|
||||||
|
- "-width <percentage>" - Window width
|
||||||
|
- "-lines <number>" - Number of visible lines
|
||||||
|
'';
|
||||||
|
example = literalExpression ''["-dmenu" "-i" "-theme gruvbox"]'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
home.packages = [pkgs.rofi-project-opener];
|
||||||
|
|
||||||
|
# Write config file (shell-independent)
|
||||||
|
xdg.configFile."rofi-project-opener/config".text = ''
|
||||||
|
# rofi-project-opener configuration
|
||||||
|
PROJECT_DIRS="${projectDirsStr}"
|
||||||
|
TERMINAL="${if isDerivation cfg.terminal then "${cfg.terminal}/bin/${cfg.terminal.pname or (builtins.baseNameOf (toString cfg.terminal))}" else cfg.terminal}"
|
||||||
|
${optionalString (cfg.terminalCommand != "") ''TERMINAL_CMD="${cfg.terminalCommand}"''}
|
||||||
|
ROFI_PROMPT="${cfg.rofiPrompt}"
|
||||||
|
ROFI_ARGS="${escapeShellArgs cfg.rofiArgs}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
mem0 = pkgs.callPackage ./mem0 {};
|
mem0 = pkgs.callPackage ./mem0 {};
|
||||||
msty-studio = pkgs.callPackage ./msty-studio {};
|
msty-studio = pkgs.callPackage ./msty-studio {};
|
||||||
pomodoro-timer = pkgs.callPackage ./pomodoro-timer {};
|
pomodoro-timer = pkgs.callPackage ./pomodoro-timer {};
|
||||||
|
rofi-project-opener = pkgs.callPackage ./rofi-project-opener {};
|
||||||
stt-ptt = pkgs.callPackage ./stt-ptt {};
|
stt-ptt = pkgs.callPackage ./stt-ptt {};
|
||||||
tuxedo-backlight = pkgs.callPackage ./tuxedo-backlight {};
|
tuxedo-backlight = pkgs.callPackage ./tuxedo-backlight {};
|
||||||
zellij-ps = pkgs.callPackage ./zellij-ps {};
|
zellij-ps = pkgs.callPackage ./zellij-ps {};
|
||||||
|
|||||||
165
pkgs/rofi-project-opener/default.nix
Normal file
165
pkgs/rofi-project-opener/default.nix
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
writeShellScriptBin,
|
||||||
|
rofi,
|
||||||
|
libnotify,
|
||||||
|
coreutils,
|
||||||
|
gnugrep,
|
||||||
|
gnused,
|
||||||
|
}:
|
||||||
|
writeShellScriptBin "rofi-project-opener" ''
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# rofi-project-opener - Rofi-based project directory launcher
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Core utilities
|
||||||
|
NOTIFY="${libnotify}/bin/notify-send"
|
||||||
|
ROFI="${rofi}/bin/rofi"
|
||||||
|
MKDIR="${coreutils}/bin/mkdir"
|
||||||
|
RM="${coreutils}/bin/rm"
|
||||||
|
SORT="${coreutils}/bin/sort"
|
||||||
|
CUT="${coreutils}/bin/cut"
|
||||||
|
DIRNAME="${coreutils}/bin/dirname"
|
||||||
|
BASENAME="${coreutils}/bin/basename"
|
||||||
|
GREP="${gnugrep}/bin/grep"
|
||||||
|
SED="${gnused}/bin/sed"
|
||||||
|
|
||||||
|
# Configuration from config file or environment variables
|
||||||
|
CONFIG_FILE="''${XDG_CONFIG_HOME:-$HOME/.config}/rofi-project-opener/config"
|
||||||
|
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback to environment variables or defaults
|
||||||
|
PROJECT_DIRS="''${PROJECT_DIRS:-$HOME}"
|
||||||
|
TERMINAL="''${TERMINAL:-kitty}"
|
||||||
|
TERMINAL_CMD="''${TERMINAL_CMD:-}"
|
||||||
|
ROFI_PROMPT="''${ROFI_PROMPT:-Select project}"
|
||||||
|
ROFI_ARGS="''${ROFI_ARGS:--dmenu -i}"
|
||||||
|
|
||||||
|
# Temporary file for project list
|
||||||
|
PROJECTS_LIST="''${XDG_RUNTIME_DIR:-/tmp}/rofi-project-opener.$$"
|
||||||
|
|
||||||
|
# Cleanup on exit
|
||||||
|
cleanup() {
|
||||||
|
[[ -f "$PROJECTS_LIST" ]] && "$RM" -f "$PROJECTS_LIST"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Ensure runtime directory exists
|
||||||
|
"$MKDIR" -p "$("$DIRNAME" "$PROJECTS_LIST")"
|
||||||
|
|
||||||
|
# Show error notification
|
||||||
|
show_error() {
|
||||||
|
local message="$1"
|
||||||
|
echo "[rofi-project-opener] Error: $message" >&2
|
||||||
|
"$NOTIFY" -u critical -a "rofi-project-opener" "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build list of projects
|
||||||
|
build_project_list() {
|
||||||
|
> "$PROJECTS_LIST"
|
||||||
|
|
||||||
|
# Split PROJECT_DIRS by colon and iterate
|
||||||
|
IFS=':' read -ra DIR_ARRAY <<< "$PROJECT_DIRS"
|
||||||
|
for base_dir in "''${DIR_ARRAY[@]}"; do
|
||||||
|
# Expand ~ to $HOME
|
||||||
|
base_dir="''${base_dir/#\~/$HOME}"
|
||||||
|
# Expand $HOME variable (allows both ~ and $HOME in config)
|
||||||
|
base_dir="$(eval echo "$base_dir")"
|
||||||
|
|
||||||
|
# Skip if directory doesn't exist
|
||||||
|
if [[ ! -d "$base_dir" ]]; then
|
||||||
|
echo "[rofi-project-opener] Warning: Directory does not exist: $base_dir" >&2
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find 1st level subdirectories (non-hidden)
|
||||||
|
for project in "$base_dir"/*/; do
|
||||||
|
if [[ -d "$project" ]]; then
|
||||||
|
# Get directory name without trailing slash
|
||||||
|
project_path="''${project%/}"
|
||||||
|
project_name="$("$BASENAME" "$project_path")"
|
||||||
|
|
||||||
|
# Skip hidden directories
|
||||||
|
if [[ "$project_name" == .* ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
base_display="$("$BASENAME" "$base_dir")"
|
||||||
|
echo "$base_display/$project_name|$project_path" >> "$PROJECTS_LIST"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if we found any projects
|
||||||
|
[[ -s "$PROJECTS_LIST" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build project list
|
||||||
|
if ! build_project_list; then
|
||||||
|
show_error "No projects found in PROJECT_DIRS: $PROJECT_DIRS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deduplicate and sort
|
||||||
|
"$SORT" -t '|' -k1 -u "$PROJECTS_LIST" -o "$PROJECTS_LIST"
|
||||||
|
|
||||||
|
# Display in rofi and get selection
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
selection=$("$CUT" -d '|' -f1 "$PROJECTS_LIST" | "$ROFI" $ROFI_ARGS -p "$ROFI_PROMPT") || true
|
||||||
|
|
||||||
|
# Exit if cancelled
|
||||||
|
if [[ -z "$selection" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the full path from selection
|
||||||
|
project_path=$("$GREP" "^$selection|" "$PROJECTS_LIST" | "$CUT" -d '|' -f2)
|
||||||
|
|
||||||
|
# Exit if path not found (shouldn't happen)
|
||||||
|
if [[ -z "$project_path" ]]; then
|
||||||
|
show_error "Could not find project path for: $selection"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify directory still exists
|
||||||
|
if [[ ! -d "$project_path" ]]; then
|
||||||
|
show_error "Project directory no longer exists: $project_path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build terminal command
|
||||||
|
if [[ -n "$TERMINAL_CMD" ]]; then
|
||||||
|
# Check if %s placeholder is present
|
||||||
|
if [[ "$TERMINAL_CMD" == *"%s"* ]]; then
|
||||||
|
# Replace %s with project path and use as-is
|
||||||
|
final_cmd="''${TERMINAL_CMD//%s/$project_path}"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
exec "$TERMINAL" $final_cmd
|
||||||
|
else
|
||||||
|
# Treat as command to run: wrap in shell with cd (--hold keeps terminal open)
|
||||||
|
# Source /etc/profile (base PATH) and HM session vars (sessionPath) for NixOS
|
||||||
|
exec "$TERMINAL" --hold -e bash --login -c "
|
||||||
|
unset __HM_SESS_VARS_SOURCED
|
||||||
|
for f in ~/.nix-profile/etc/profile.d/hm-session-vars.sh /etc/profiles/per-user/\$USER/etc/profile.d/hm-session-vars.sh; do
|
||||||
|
[ -f \"\$f\" ] && . \"\$f\"
|
||||||
|
done
|
||||||
|
cd '$project_path' && exec $TERMINAL_CMD
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Default: open terminal, cd to project, run opencode
|
||||||
|
# Source /etc/profile (base PATH) and HM session vars (sessionPath) for NixOS
|
||||||
|
exec "$TERMINAL" --hold -e bash --login -c "
|
||||||
|
unset __HM_SESS_VARS_SOURCED
|
||||||
|
for f in ~/.nix-profile/etc/profile.d/hm-session-vars.sh /etc/profiles/per-user/\$USER/etc/profile.d/hm-session-vars.sh; do
|
||||||
|
[ -f \"\$f\" ] && . \"\$f\"
|
||||||
|
done
|
||||||
|
cd '$project_path' && opencode
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
''
|
||||||
Reference in New Issue
Block a user