diff --git a/.gitignore b/.gitignore index 6dd907d..6bbd624 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ lazy-lock.json tmp/ +config.json +config.json.backup.* diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..4a65c73 --- /dev/null +++ b/config.example.json @@ -0,0 +1,23 @@ +{ + "paths": { + "obsidianVault": "~/Documents/obsidian-vault/", + "srcDirectory": "~/.local/src/", + "scriptsDirectory": "~/scripts/", + "wallpaperScript": "~/scripts/pywal/wallpapermenu.sh" + }, + "editor": { + "tabSize": 4, + "scrollOffset": 8, + "theme": "wave" + }, + "ai": { + "model": "claude-sonnet-4-5", + "openCodeModel": "anthropic/claude-sonnet-4-5" + }, + "lsp": { + "servers": ["ts_ls", "eslint", "jsonls", "html", "cssls", "tailwindcss"] + }, + "treesitter": { + "languages": ["lua", "vim", "bash", "javascript", "typescript", "tsx", "json", "yaml", "html", "css"] + } +} diff --git a/lua/core/config.lua b/lua/core/config.lua new file mode 100644 index 0000000..cf254fb --- /dev/null +++ b/lua/core/config.lua @@ -0,0 +1,156 @@ +-- Config loader module +-- Loads user configuration from config.json with defaults and validation + +local M = {} + +-- Default configuration values +M.defaults = { + paths = { + obsidianVault = "~/Documents/obsidian-vault/", + srcDirectory = "~/.local/src/", + scriptsDirectory = "~/scripts/", + wallpaperScript = "~/scripts/pywal/wallpapermenu.sh", + }, + editor = { + tabSize = 4, + scrollOffset = 8, + theme = "wave", + }, + ai = { + model = "claude-sonnet-4-5", + openCodeModel = "anthropic/claude-sonnet-4-5", + }, + lsp = { + servers = { "ts_ls", "eslint", "jsonls", "html", "cssls", "tailwindcss" }, + }, + treesitter = { + languages = { "lua", "vim", "bash", "javascript", "typescript", "tsx", "json", "yaml", "html", "css" }, + }, +} + +-- Cache for loaded config +local config_cache = nil + +-- Deep merge two tables (b overrides a) +local function deep_merge(a, b) + local result = vim.deepcopy(a) + for k, v in pairs(b) do + if type(v) == "table" and type(result[k]) == "table" then + result[k] = deep_merge(result[k], v) + else + result[k] = v + end + end + return result +end + +-- Expand ~ to home directory in paths +local function expand_path(path) + if type(path) ~= "string" then + return path + end + return path:gsub("^~", vim.fn.expand("$HOME")) +end + +-- Recursively expand paths in a table +local function expand_paths(tbl) + -- Check if this is an array (sequential numeric keys) + if #tbl > 0 then + local result = {} + for i, v in ipairs(tbl) do + result[i] = v + end + return result + end + + local result = {} + for k, v in pairs(tbl) do + if type(v) == "table" then + result[k] = expand_paths(v) + elseif type(v) == "string" and type(k) == "string" and (k:lower():match("path") or k:lower():match("vault") or k:lower():match("directory") or k:lower():match("script")) then + result[k] = expand_path(v) + else + result[k] = v + end + end + return result +end + +-- Load configuration from config.json +function M.load() + if config_cache then + return config_cache + end + + local config_path = vim.fn.stdpath("config") .. "/config.json" + local user_config = {} + + -- Check if config file exists + if vim.fn.filereadable(config_path) == 1 then + local file = io.open(config_path, "r") + if file then + local content = file:read("*a") + file:close() + + -- Try to parse JSON + local ok, parsed = pcall(vim.json.decode, content) + if ok and type(parsed) == "table" then + user_config = parsed + else + vim.notify("config.json: Parse error, using defaults", vim.log.levels.WARN) + end + end + end + + -- Merge defaults with user config + local merged = deep_merge(M.defaults, user_config) + + -- Expand paths + config_cache = expand_paths(merged) + + return config_cache +end + +-- Get a config value by dot-notation path (e.g., "paths.obsidianVault") +function M.get(path) + local config = M.load() + local keys = vim.split(path, ".", { plain = true }) + local value = config + + for _, key in ipairs(keys) do + if type(value) ~= "table" then + return nil + end + value = value[key] + end + + return value +end + +-- Convenience getters for common sections +function M.paths() + return M.load().paths +end + +function M.editor() + return M.load().editor +end + +function M.ai() + return M.load().ai +end + +function M.lsp() + return M.load().lsp +end + +function M.treesitter() + return M.load().treesitter +end + +-- Reset cache (useful for testing or config reload) +function M.reset() + config_cache = nil +end + +return M diff --git a/lua/core/options.lua b/lua/core/options.lua index 583de74..74b2701 100644 --- a/lua/core/options.lua +++ b/lua/core/options.lua @@ -1,4 +1,7 @@ -- Basic Settings +local config = require("core.config") +local editor = config.editor() + vim.opt.number = true vim.opt.relativenumber = true vim.opt.mouse = 'a' @@ -6,15 +9,15 @@ vim.opt.ignorecase = true vim.opt.smartcase = true vim.opt.hlsearch = false vim.opt.wrap = false -vim.opt.tabstop = 4 -vim.opt.shiftwidth = 4 +vim.opt.tabstop = editor.tabSize +vim.opt.shiftwidth = editor.tabSize vim.opt.expandtab = true vim.opt.termguicolors = true vim.opt.cursorline = true vim.opt.signcolumn = 'yes' vim.opt.clipboard = "unnamedplus" -vim.opt.scrolloff = 8 -vim.opt.sidescrolloff = 8 +vim.opt.scrolloff = editor.scrollOffset +vim.opt.sidescrolloff = editor.scrollOffset -- Leader key vim.g.mapleader = ' ' diff --git a/lua/plugins/ai.lua b/lua/plugins/ai.lua index a8f68ba..4d63cc6 100644 --- a/lua/plugins/ai.lua +++ b/lua/plugins/ai.lua @@ -1,3 +1,6 @@ +local config = require("core.config") +local ai = config.ai() + return { -- AI agent (99) { @@ -52,10 +55,10 @@ return { local result = handle:read("*a") handle:close() if result:match("^[24]%d%d") then - return "anthropic/claude-sonnet-4-5" + return ai.openCodeModel end end - return "claude-sonnet-4-5" + return ai.model end)(), logger = { diff --git a/lua/plugins/colorscheme.lua b/lua/plugins/colorscheme.lua index 00e15dd..a44386c 100644 --- a/lua/plugins/colorscheme.lua +++ b/lua/plugins/colorscheme.lua @@ -1,3 +1,7 @@ +local config = require("core.config") +local editor = config.editor() +local paths = config.paths() + return { -- Kanagawa colorscheme { @@ -24,9 +28,9 @@ return { overrides = function(colors) return {} end, - theme = "wave", + theme = editor.theme, background = { - dark = "wave", + dark = editor.theme, light = "lotus" }, }) @@ -93,7 +97,7 @@ return { if selection.value == "_wallpaper_picker" then -- launch wallpaper picker, then reload pywal vim.fn.jobstart( - { "bash", "-c", "~/scripts/pywal/wallpapermenu.sh && sleep 1" }, + { "bash", "-c", paths.wallpaperScript .. " && sleep 1" }, { on_exit = function() vim.schedule(function() diff --git a/lua/plugins/lsp.lua b/lua/plugins/lsp.lua index df99cdd..5c9ce8e 100644 --- a/lua/plugins/lsp.lua +++ b/lua/plugins/lsp.lua @@ -1,3 +1,5 @@ +local config = require("core.config") + return { -- Mason (LSP installer) { @@ -12,7 +14,7 @@ return { dependencies = { "williamboman/mason.nvim" }, config = function() require("mason-lspconfig").setup({ - ensure_installed = { "ts_ls", "eslint", "jsonls", "html", "cssls", "tailwindcss" }, + ensure_installed = config.get("lsp.servers"), }) end, }, diff --git a/lua/plugins/notes.lua b/lua/plugins/notes.lua index 703e49e..3d884db 100644 --- a/lua/plugins/notes.lua +++ b/lua/plugins/notes.lua @@ -1,3 +1,5 @@ +local config = require("core.config") + return { -- Obsidian integration { @@ -14,7 +16,7 @@ return { workspaces = { { name = "vault", - path = "/mnt/work/obsidian-vault/", + path = config.get("paths.obsidianVault"), }, }, completion = { diff --git a/lua/plugins/treesitter.lua b/lua/plugins/treesitter.lua index 7ac2272..6a9750f 100644 --- a/lua/plugins/treesitter.lua +++ b/lua/plugins/treesitter.lua @@ -1,3 +1,5 @@ +local config = require("core.config") + return { -- Syntax highlighting { @@ -5,10 +7,7 @@ return { build = ":TSUpdate", config = function() require("nvim-treesitter.configs").setup({ - ensure_installed = { - "lua", "vim", "bash", "javascript", "typescript", "tsx", "json", "yaml", "html", "css", "prisma", - "graphql" - }, + ensure_installed = config.get("treesitter.languages"), highlight = { enable = true }, indent = { enable = true }, }) diff --git a/lua/plugins/ui.lua b/lua/plugins/ui.lua index 7d9ca80..f3a138f 100644 --- a/lua/plugins/ui.lua +++ b/lua/plugins/ui.lua @@ -1,3 +1,6 @@ +local config = require("core.config") +local paths = config.paths() + return { -- Status line { @@ -31,8 +34,8 @@ return { dashboard.button("e", " New file", ":ene startinsert "), dashboard.button("f", "󰍉 Find file", ":lua require('fzf-lua').files() "), dashboard.button("t", " Browse cwd", ":NvimTreeOpen"), - dashboard.button("r", " Browse src", ":e ~/.local/src/"), - dashboard.button("s", "󰯂 Browse scripts", ":e ~/scripts/"), + dashboard.button("r", " Browse src", ":e " .. paths.srcDirectory .. ""), + dashboard.button("s", "󰯂 Browse scripts", ":e " .. paths.scriptsDirectory .. ""), dashboard.button("c", " Config", ":e ~/.config/nvim/"), dashboard.button("m", " Mappings", ":e ~/.config/nvim/lua/core/keymaps.lua"), dashboard.button("p", " Plugins", ":PlugInstall"), diff --git a/setup.sh b/setup.sh index 7221b83..e098e58 100755 --- a/setup.sh +++ b/setup.sh @@ -1,43 +1,206 @@ #!/bin/bash set -e # exit on error +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper function for colored output +print_green() { echo -e "${GREEN}$1${NC}"; } +print_yellow() { echo -e "${YELLOW}$1${NC}"; } +print_blue() { echo -e "${BLUE}$1${NC}"; } + +# Helper function for prompts with defaults +prompt_with_default() { + local prompt="$1" + local default="$2" + local result + read -p "$(echo -e "${BLUE}$prompt${NC} [${GREEN}$default${NC}]: ")" result + echo "${result:-$default}" +} + # This script sets up dotfiles and installs opencode: # - Backs up and symlinks .tmux.conf from dotfiles/ to ~/ # - Backs up and symlinks starship.toml from dotfiles/ to ~/.config/ # - Installs opencode CLI if not already present +# - Generates config.json with interactive prompts + +NVIM_CONFIG_DIR="$HOME/.config/nvim" + +# ============================================ +# Section 1: Interactive Config Generation +# ============================================ +print_green "╔════════════════════════════════════════════╗" +print_green "║ Neovim Configuration Setup Wizard ║" +print_green "╚════════════════════════════════════════════╝" +echo "" + +# Check if config.json already exists +if [ -f "$NVIM_CONFIG_DIR/config.json" ]; then + print_yellow "Existing config.json found." + read -p "$(echo -e "${YELLOW}Overwrite? (y/N): ${NC}")" overwrite + if [[ ! "$overwrite" =~ ^[Yy]$ ]]; then + print_blue "Keeping existing config.json" + SKIP_CONFIG=true + else + # Backup existing config + cp "$NVIM_CONFIG_DIR/config.json" "$NVIM_CONFIG_DIR/config.json.backup.$(date +%Y%m%d%H%M%S)" + print_green "Backed up existing config.json" + SKIP_CONFIG=false + fi +else + SKIP_CONFIG=false +fi + +if [ "$SKIP_CONFIG" = false ]; then + echo "" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_blue " Section 1: Paths" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + OBSIDIAN_VAULT=$(prompt_with_default "Obsidian vault path" "~/Documents/obsidian-vault/") + SRC_DIR=$(prompt_with_default "Source code directory" "~/.local/src/") + SCRIPTS_DIR=$(prompt_with_default "Scripts directory" "~/scripts/") + WALLPAPER_SCRIPT=$(prompt_with_default "Wallpaper script path" "~/scripts/pywal/wallpapermenu.sh") + + echo "" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_blue " Section 2: Editor Settings" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + TAB_SIZE=$(prompt_with_default "Tab size" "4") + SCROLL_OFFSET=$(prompt_with_default "Scroll offset" "8") + echo "" + print_yellow "Theme variants: wave (default), dragon (dark), lotus (light)" + THEME=$(prompt_with_default "Kanagawa theme variant" "wave") + + echo "" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_blue " Section 3: Languages" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + print_yellow "Enter comma-separated lists" + echo "" + LSP_SERVERS=$(prompt_with_default "LSP servers" "ts_ls, eslint, jsonls, html, cssls, tailwindcss") + TS_LANGUAGES=$(prompt_with_default "Treesitter languages" "lua, vim, bash, javascript, typescript, tsx, json, yaml, html, css") + + echo "" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + print_blue " Section 4: AI Settings" + print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + print_yellow "Claude models: claude-sonnet-4-5, claude-opus-4, haiku" + AI_MODEL=$(prompt_with_default "Claude Code model" "claude-sonnet-4-5") + OPENCODE_MODEL=$(prompt_with_default "OpenCode model (with provider prefix)" "anthropic/claude-sonnet-4-5") + + # Convert comma-separated strings to JSON arrays + format_json_array() { + echo "$1" | sed 's/,/\n/g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | \ + awk 'BEGIN{printf "["} NR>1{printf ", "} {printf "\"%s\"", $0} END{printf "]"}' + } + + LSP_SERVERS_JSON=$(format_json_array "$LSP_SERVERS") + TS_LANGUAGES_JSON=$(format_json_array "$TS_LANGUAGES") + + # Generate config.json + cat > "$NVIM_CONFIG_DIR/config.json" << EOF +{ + "paths": { + "obsidianVault": "$OBSIDIAN_VAULT", + "srcDirectory": "$SRC_DIR", + "scriptsDirectory": "$SCRIPTS_DIR", + "wallpaperScript": "$WALLPAPER_SCRIPT" + }, + "editor": { + "tabSize": $TAB_SIZE, + "scrollOffset": $SCROLL_OFFSET, + "theme": "$THEME" + }, + "ai": { + "model": "$AI_MODEL", + "openCodeModel": "$OPENCODE_MODEL" + }, + "lsp": { + "servers": $LSP_SERVERS_JSON + }, + "treesitter": { + "languages": $TS_LANGUAGES_JSON + } +} +EOF + + echo "" + print_green "✓ config.json generated successfully!" + echo "" +fi + +# ============================================ +# Section 2: Dotfile Symlinks +# ============================================ +print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +print_blue " Setting up dotfile symlinks..." +print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" # Check and backup/symlink .tmux.conf TMUX_TARGET="$HOME/.config/nvim/dotfiles/.tmux.conf" if [ -L ~/.tmux.conf ] && [ "$(readlink ~/.tmux.conf)" = "$TMUX_TARGET" ]; then - echo ".tmux.conf already linked correctly" + print_green "✓ .tmux.conf already linked correctly" elif [ -f ~/.tmux.conf ] || [ -L ~/.tmux.conf ]; then cp ~/.tmux.conf ~/.tmux.conf.backup.$(date +%Y%m%d%H%M%S) ln -sf "$TMUX_TARGET" ~/.tmux.conf - echo ".tmux.conf backed up and linked" + print_green "✓ .tmux.conf backed up and linked" elif [ -f "$TMUX_TARGET" ]; then ln -sf "$TMUX_TARGET" ~/.tmux.conf - echo ".tmux.conf linked" + print_green "✓ .tmux.conf linked" fi # Check and backup/symlink starship.toml STARSHIP_TARGET="$HOME/.config/nvim/dotfiles/starship.toml" if [ -L ~/.config/starship.toml ] && [ "$(readlink ~/.config/starship.toml)" = "$STARSHIP_TARGET" ]; then - echo "starship.toml already linked correctly" + print_green "✓ starship.toml already linked correctly" elif [ -f ~/.config/starship.toml ] || [ -L ~/.config/starship.toml ]; then cp ~/.config/starship.toml ~/.config/starship.toml.backup.$(date +%Y%m%d%H%M%S) ln -sf "$STARSHIP_TARGET" ~/.config/starship.toml - echo "starship.toml backed up and linked" + print_green "✓ starship.toml backed up and linked" elif [ -f "$STARSHIP_TARGET" ]; then mkdir -p ~/.config ln -sf "$STARSHIP_TARGET" ~/.config/starship.toml - echo "starship.toml linked" + print_green "✓ starship.toml linked" fi -# Check if opencode is installed, if not, install opencode +# ============================================ +# Section 3: OpenCode Installation +# ============================================ +echo "" +print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +print_blue " Checking OpenCode installation..." +print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + if ! command -v opencode &> /dev/null; then + print_yellow "Installing OpenCode..." curl -fsSL https://opencode.ai/install | bash + print_green "✓ OpenCode installed" +else + print_green "✓ OpenCode already installed" fi +# ============================================ +# Section 4: Shell Aliases +# ============================================ +echo "" +print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +print_blue " Setting up shell aliases..." +print_blue "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + # Detect shell config file SHELL_RC="" if [ -f ~/.zshrc ]; then @@ -50,7 +213,7 @@ fi if [ -n "$SHELL_RC" ]; then # Create a temporary file with all our aliases and functions TEMP_ALIASES=$(mktemp) - + cat > "$TEMP_ALIASES" << 'EOF' # Git shortcuts @@ -162,13 +325,25 @@ EOF echo "" >> "$SHELL_RC" echo "# Added by nvim dotfiles setup" >> "$SHELL_RC" cat "$TEMP_ALIASES" >> "$SHELL_RC" - echo "Shell aliases and functions added to $SHELL_RC" + print_green "✓ Shell aliases and functions added to $SHELL_RC" else - echo "Shell aliases already exist in $SHELL_RC, skipping..." + print_green "✓ Shell aliases already exist in $SHELL_RC" fi - + # Clean up rm "$TEMP_ALIASES" else - echo "No .bashrc or .zshrc found, skipping shell config setup" + print_yellow "⚠ No .bashrc or .zshrc found, skipping shell config setup" fi + +# ============================================ +# Done! +# ============================================ +echo "" +print_green "╔════════════════════════════════════════════╗" +print_green "║ Setup Complete! ║" +print_green "╚════════════════════════════════════════════╝" +echo "" +print_blue "Run 'nvim' to start using your configuration." +print_blue "Your config is stored in: $NVIM_CONFIG_DIR/config.json" +echo ""