commit e19a09f2df1753d6a40328fac8a7b609ceca945d Author: Ellis Rahhal Date: Thu Feb 13 10:35:17 2025 -0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2be92b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result diff --git a/README.md b/README.md new file mode 100644 index 0000000..8bdfe1c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# nixvim-config + +## Installation + +Add the following to the `inputs` section of your flake.nix: + +```nix + nixvim-config.url = "github:homefree/nixvim-config"; +``` + +And add the following module: + +```nix + @TODO +``` diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..8391d00 --- /dev/null +++ b/default.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./nixvim.nix + ]; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..92295d4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1738574474, + "narHash": "sha256-rvyfF49e/k6vkrRTV4ILrWd92W+nmBDfRYZgctOyolQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "fecfeb86328381268e29e998ddd3ebc70bbd7f7c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..0512372 --- /dev/null +++ b/flake.nix @@ -0,0 +1,35 @@ +{ + description = "Opinionated Nixvim config as a flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = { self, nixpkgs, ... } @ inputs: + # let + # system = "x86_64-linux"; + # in + { + packages.x86_64-linux = let + pkgs = import "${nixpkgs}" { + # system = "x86_64-linux"; + }; + + in with pkgs; { + nixvim-config = callPackage ./default.nix { + inherit builtins; + }; + }; + + nixosModules = rec { + homefree = import ./default.nix { + # inherit system; + }; + imports = [ ]; + default = homefree; + lan-client = import ./lan-client.nix { + # inherit system; + }; + }; + }; +} diff --git a/nixvim.nix b/nixvim.nix new file mode 100644 index 0000000..3cec966 --- /dev/null +++ b/nixvim.nix @@ -0,0 +1,920 @@ +{ pkgs, ... }: +{ + environment.systemPackages = with pkgs; [ + ripgrep + + ## To get rid of checkhealth warnings + chafa + fd + mercurial + ueberzugpp + viu + ]; + + environment.interactiveShellInit = '' + alias vi='nvim' + alias vim='nvim' + ''; + + programs.nixvim = { + enable = true; + + defaultEditor = true; + + ## ------------------------------------------------ + ## Options + ## ------------------------------------------------ + + globals = { + mapleader = " "; # global + maplocalleader = " "; # per buffer, e.g. can change behavior per filetype + ## To appropriately highlight codefences returned from denols + markdown_fenced_languages.__raw = '' + { + "ts=typescript" + } + ''; + }; + + opts = { + number = true; # Show line numbers + relativenumber = true; # Show relative line numbers + ruler = true; # displays line, column, and cursor position at bottom + wrap = false; # don't wrap lines + signcolumn = "yes"; # always show two column sign column on left + cursorline = true; # Highlight line cursor sits on + + undodir.__raw = "vim.fs.normalize('~/.local/share/nvim/undo/')"; + undofile = true; + + # ----------------------------------------------------- + # Backspace settings + # indent allow backspacing over autoindent + # eol allow backspacing over line breaks (join lines) + # start allow backspacing over the start of insert; CTRL-W and CTRL-U + # 0 same as ":set backspace=" (Vi compatible) + # 1 same as ":set backspace=indent,eol" + # 2 same as ":set backspace=indent,eol,start" + # ----------------------------------------------------- + + bs = "2"; + + # ----------------------------------------------------- + # Indentation settings + # ----------------------------------------------------- + + tabstop = 4; # number of spaces a tab counts for + shiftwidth = 4; # control how many columns text is indented with the reindent operations (<< and >>) and automatic C-style indentation. + expandtab = true; # Insert spaces when entering + softtabstop = 4; # Number of spaces that a counts for while performing editing operations, like inserting a or using . It "feels" like a tab though + ai = true; # auto indent + }; + + keymaps = [ + # ----------------------------------------------------- + # nvim-tree + # ----------------------------------------------------- + + ## Go to current buffer's file in nvim-tree + { + mode = [ "n" ]; + key = ",n"; + action = ":NvimTreeFindFile"; + } + ## Toggle nvim-tree visibility + { + mode = [ "n" ]; + key = ",m"; + action = ":NvimTreeToggle"; + } + + # ----------------------------------------------------- + # buffer manipulation + # ----------------------------------------------------- + + ## Next Buffer + { + key = ""; + action = ":bn"; + options = { noremap = true; }; + } + ## Previous Buffer + { + key = ""; + action = ":bp"; + options = { noremap = true; }; + } + ## Close Buffer + { + key = ""; + action = ":bd"; + options = { noremap = true; }; + } + ## Force Close Buffer + { + key = ""; + action = ":bd!"; + options = { noremap = true; }; + } + ## New Tab + { + key = "t"; + action = ":tabnew split"; + options = { noremap = true; }; + } + + # ----------------------------------------------------- + # Telescope + # ----------------------------------------------------- + + ## Lists files in your current working directory, respects .gitignore + { + mode = [ "n" ]; + key = "ff"; + action = "Telescope find_files"; + options = { noremap = true; }; + } + ## Finds files by filename + { + mode = [ "n" ]; + key = ""; + action = "Telescope find_files"; + # action = "FzfLua files"; + options = { noremap = true; }; + } + # Search for a string in your current working directory and get results live as you type, respects .gitignore. (Requires ripgrep) + { + mode = [ "n" ]; + key = "fg"; + action = "Telescope live_grep"; + # action = "FzfLua live_grep"; + options = { noremap = true; }; + } + # Search file contents + { + mode = [ "n" ]; + key = ""; + action = "Telescope live_grep"; + # action = "FzfLua live_grep"; + options = { noremap = true; }; + } + # Lists open buffers in current neovim instance + { + mode = [ "n" ]; + key = "db"; + action = "Telescope buffers"; + # action = "FzfLua buffers"; + options = { noremap = true; }; + } + # Lists available help tags and opens a new window with the relevant help info on + { + mode = [ "n" ]; + key = "fh"; + action = "Telescope help_tags"; + # action = "FzfLua helptags"; + options = { noremap = true; }; + } + # Lists manpage entries, opens them in a help window on + { + mode = [ "n" ]; + key = "fm"; + action = "Telescope man_pages"; + # action = "FzfLua manpages"; + options = { noremap = true; }; + } + # Lists previously open files + { + mode = [ "n" ]; + key = "fp"; + action = "Telescope oldfiles"; + # action = "FzfLua oldfiles"; + options = { noremap = true; }; + } + # Lists previously open files, Maps to ctrl-/ + { + mode = [ "n" ]; + key = ""; + action = "Telescope oldfiles"; + # action = "FzfLua oldfiles"; + options = { noremap = true; }; + } + # Lists spelling suggestions for the current word under the cursor, replaces word with selected suggestion on + { + mode = [ "n" ]; + key = "fs"; + action = "Telescope spell_suggest"; + # action = "FzfLua spell_suggest"; + options = { noremap = true; }; + } + # Lists LSP references for iword under the cursor + { + mode = [ "n" ]; + key = "fr"; + action = "Telescope lsp_references"; + # action = "FzfLua lsp_references"; + options = { noremap = true; }; + } + # Lists LSP incoming calls for word under the cursor + { + mode = [ "n" ]; + key = "fi"; + action = "Telescope lsp_incoming_calls"; + # action = "FzfLua lsp_incoming_calls"; + options = { noremap = true; }; + } + # Lists LSP outgoing calls for word under the cursor + { + mode = [ "n" ]; + key = "fo"; + action = "Telescope lsp_outgoing_calls"; + # action = "FzfLua lsp_outgoing_calls"; + options = { noremap = true; }; + } + # Dynamically Lists LSP for all workspace symbols + { + mode = [ "n" ]; + key = "fw"; + action = "Telescope lsp_dynamic_workspace_symbols"; + # action = "FzfLua lsp_workspace_symbols"; + options = { noremap = true; }; + } + # Goto the definition of the word under the cursor, if there's only one, otherwise show all options in Telescope + { + mode = [ "n" ]; + key = "fd"; + action = "Telescope lsp_definitions"; + options = { noremap = true; }; + } + # Got to previous error + { + mode = [ "n" ]; + key = "[d"; + action = "lua vim.diagnostic.goto_prev()"; + options = { noremap = true; silent = true; }; + } + { + mode = [ "n" ]; + key = ",k"; + action = "lua vim.diagnostic.goto_prev()"; + options = { noremap = true; silent = true; }; + } + # Got to next error + { + mode = [ "n" ]; + key = "]d"; + action = "lua vim.diagnostic.goto_next()"; + options = { noremap = true; silent = true; }; + } + { + mode = [ "n" ]; + key = ",j"; + action = "lua vim.diagnostic.goto_next()"; + options = { noremap = true; silent = true; }; + } + + ## Other Telescope options: + ## git_files search only files in git, respects .gitignore + ## oldfiles previously opened files + ## command_history + ## search_history + ## man_pages + ## resume lists the results including multi-selections of the previous + ## picker + + # ----------------------------------------------------- + # Diff + # ----------------------------------------------------- + + { + mode = [ "n" ]; + key = ",d"; + ## @TODO: This doesn't work + action = '' + function() + if next(require('diffview.lib').views) == nil then + vim.cmd('DiffviewOpen origin') + else + vim.cmd('DiffviewClose') + end + end + ''; + options = { noremap = true; }; + } + + # ----------------------------------------------------- + # Bufferline + # ----------------------------------------------------- + + { + mode = [ "n" ]; + key = ""; + action = ":BufferLineCyclePrev"; + options = { noremap = true; silent = true; }; + } + { + mode = [ "n" ]; + key = ""; + action = ":BufferLineCycleNex"; + options = { noremap = true; silent = true; }; + } + { + mode = [ "n" ]; + key = ""; + action = ":bdelete!"; + options = { noremap = true; silent = true; }; + } + ]; + + autoCmd = [ + ## Close nvim on last buffer closed, not leaving neovim-tree open + { + event = [ "BufEnter" ]; + pattern = [ "NvimTree_*" ]; + callback = { + __raw = '' + function() + local layout = vim.api.nvim_call_function("winlayout", {}) + if layout[1] == "leaf" and vim.api.nvim_buf_get_option(vim.api.nvim_win_get_buf(layout[2]), "filetype") == "NvimTree" and layout[3] == nil then vim.cmd("confirm quit") end + end + ''; + }; + } + ## Go to same line in file next time it is open + { + event = [ "BufReadPost" ]; + pattern = [ "*" ]; + callback = { + __raw = '' + function() + if vim.fn.line("'\"") > 1 and vim.fn.line("'\"") <= vim.fn.line("$") then + vim.api.nvim_exec("normal! g'\"",false) + end + end + ''; + }; + } + ## Highlight tabs and trailing whitespace + { + event = [ "BufEnter" ]; + pattern = [ "*" ]; + callback = { + __raw = '' + function() + vim.cmd([[ + if exists('w:extratabs') + call matchdelete(w:extratabs) + unlet w:extratabs + endif + if exists('w:trailingwhitespace') + call matchdelete(w:trailingwhitespace) + unlet w:trailingwhitespace + endif + highlight ExtraTabs ctermbg=red guibg=red + highlight TrailingWhitespace ctermbg=red guibg=red + if &ft != 'help' + let w:extratabs=matchadd('ExtraTabs', '\t\+') + let w:trailingwhitespace=matchadd('TrailingWhitespace', '\s\+$') + endif + ]]) + end + ''; + }; + } + ## Trim tailing whitespace on save + { + event = [ "BufWritePre" ]; + pattern = [ "*" ]; + callback = { + __raw = '' + function() + vim.cmd([[ + if &ft =~ 'javascript\|html\|jade\|json\|css\|less\|php\|python\|sh\|c\|cpp\|markdown\|yaml\|vim\|nix' + :%s/\s\+$//e + elseif expand('%:t') =~ '\.gltf$' || expand('%:t') =~ '\.glsl$' + :%s/\s\+$//e + endif + ]]) + end + ''; + }; + } + ]; + + ## ------------------------------------------------ + ## Theme + ## ------------------------------------------------ + + colorschemes.tokyonight.enable = true; + + # colorschemes.gruvbox.enable = true; + ## Or: + # extraPlugins = [ pkgs.vimPlugins.gruvbox ]; + # colorscheme = "gruvbox"; + + ## ------------------------------------------------ + ## Included Plugins + ## ------------------------------------------------ + + plugins.bufferline = { + enable = true; + # extraOptions = { + settings = { + options = { + tabpages = true; + sidebar_filetypes = { + NvimTree = true; + }; + diagnostics = "nvim_lsp"; + always_show_bufferline = true; + }; + highlights = { + buffer_selected = { + # fg = "#ffffff"; + bold = true; + }; + }; + }; + }; + + plugins.comment.enable = true; + + plugins.diffview = { + enable = true; + }; + + plugins.fugitive.enable = true; + + plugins.gitsigns.enable = true; + + plugins.lightline.enable = true; + + plugins.lualine.enable = true; + + plugins.nix.enable = true; + + plugins.noice.enable = true; + + plugins.notify = { + ## disable, very annoying as notifications block content and + ## are part of the buffer rotation + enable = false; + topDown = false; + }; + + plugins.nvim-autopairs.enable = true; + + plugins.nvim-tree = { + enable = true; + extraOptions = { + actions = { + remove_file = { + close_window = false; + }; + }; + ## Keep tree open if already open when opening a tab + tab = { + sync = { + open = true; + close = true; + }; + }; + view = { + width = 30; + }; + renderer = { + group_empty = true; + }; + git = { + enable = true; + ignore = false; + timeout = 500; + }; + }; + }; + + plugins.rainbow-delimiters.enable = true; + + plugins.sleuth.enable = true; + + plugins.telescope = { + enable = true; + extensions.ui-select.enable = true; + settings = { + defaults = { + mappings = { + i = { + # One instead of two esc taps to exit telescope + "" = { + __raw = "require('telescope.actions').close"; + }; + # Ctrl-space is used by Tmux, so remap to Ctrl-e + "" = { + __raw = "require('telescope.actions').to_fuzzy_refine"; + }; + # "" = { + # __raw = "require('trouble.sources.telescope').open"; + # }; + }; + n = { + # "" = { + # __raw = "require('trouble.sources.telescope').open"; + # }; + }; + }; + }; + }; + }; + + plugins.fzf-lua = { + enable = true; + # profile = "telescope"; + settings = { + oldfiles = { + # In Telescope, when I used fr, it would load old buffers. + # fzf lua does the same, but by default buffers visited in the current + # session are not included. I use fr all the time to switch + # back to buffers I was just in. If you missed this from Telescope, + # give it a try. + include_current_session = true; + }; + preview = { + vertical = "down:90%"; + horizontal = "right:90%"; + }; + previewers = { + builtin = { + # fzf-lua is very fast, but it really struggled to preview a couple files + # in a repo. Those files were very big JavaScript files (1MB, minified, all on a single line). + # It turns out it was Treesitter having trouble parsing the files. + # With this change, the previewer will not add syntax highlighting to files larger than 100KB + # (Yes, I know you shouldn't have 100KB minified files in source control.) + syntax_limit_b = 1024 * 100; # 100KB + }; + }; + grep = { + # One thing I missed from Telescope was the ability to live_grep and the + # run a filter on the filenames. + # Ex: Find all occurrences of "enable" but only in the "plugins" directory. + # With this change, I can sort of get the same behaviour in live_grep. + # ex: > enable --*/plugins/* + # I still find this a bit cumbersome. There's probably a better way of doing this. + rg_glob = true; # enable glob parsing + glob_flag = "--iglob"; # case insensitive globs + glob_separator = "%s%-%-"; # query separator pattern (lua): ' --' + }; + }; + keymaps = { + "" = { + action = "git_files"; + options = { + desc = "Fzf-Lua Git Files"; + silent = true; + }; + settings = { + previewers = { + cat = { + cmd = "${pkgs.coreutils-full}/bin/cat"; + }; + }; + # winopts = { + # height = 0.5; + # }; + }; + }; + # "" = "live_grep"; + # "" = "oldfiles"; + # "fd" = "lsp_definitions"; + # "fg" = "live_grep"; + # "fh" = "helptags"; + # "fi" = "lsp_incoming_calls"; + # "fm" = "manpages"; + # "fo" = "lsp_outgoing_calls"; + # "fp" = "oldfiles"; + # "fr" = "lsp_references"; + # "fs" = "spell_suggest"; + # "fw" = "lsp_workspace_symbols"; + # "db" = "buffers"; + # "ch" = "command_history"; + }; + }; + + plugins.treesitter.enable = false; + + plugins.tmux-navigator.enable = true; + + plugins.trouble.enable = true; + + # ## Needed for telescope, nvim-tree, trouble, diffview, bufferline, and other plugins + # ## Only on unstable at the moment + plugins.web-devicons.enable = true; + + ## ------------------------------------------------ + ## LSP / Completion + ## ------------------------------------------------ + + plugins.lsp = { + enable = true; + onAttach = '' + local active_clients = vim.lsp.get_active_clients() + if client.name == "denols" then + for _, client_ in pairs(active_clients) do + -- stop tsserver if denols is already active + if client_.name == "ts_ls" then + client_.stop() + end + end + elseif client.name == "ts_ls" then + for _, client_ in pairs(active_clients) do + -- prevent tsserver from starting if denols is already active + if client_.name == "denols" then + client.stop() + end + end + end + ''; + servers = { + ts_ls = { + enable = true; + rootDir = "require('lspconfig').util.root_pattern('package.json')"; + settings = { + single_file_support = false; + }; + }; + denols = { + enable = true; + rootDir = "require('lspconfig').util.root_pattern('deno.json', 'deno.jsonc')"; + }; + cssls.enable = true; + tailwindcss.enable = true; + html.enable = true; + phpactor.enable = true; + pyright.enable = true; + marksman.enable = true; + nil_ls.enable = true; + ## Using nil_ls + # nixd.enable = true; + dockerls.enable = true; # Docker + bashls.enable = true; # Bash + clangd.enable = true; # C/C++ + csharp_ls.enable = true; # C# + yamlls.enable = true; # YAML + ltex = { + enable = true; + settings = { + enabled = [ "astro" "html" "latex" "markdown" "text" "tex" "gitcommit" ]; + completionEnabled = true; + language = "en-US de-DE nl"; + # dictionary = { + # "nl-NL" = [ + # ":/home/liv/.local/share/nvim/ltex/nl-NL.txt" + # ]; + # "en-US" = [ + # ":/home/liv/.local/share/nvim/ltex/en-US.txt" + # ]; + # "de-DE" = [ + # ":/home/liv/.local/share/nvim/ltex/de-DE.txt" + # ]; + # }; + }; + }; + gopls = { # Golang + enable = true; + autostart = true; + }; + lua_ls = { # Lua + enable = true; + settings.telemetry.enable = false; + }; + # Rust + rust_analyzer = { + enable = true; + installRustc = true; + installCargo = true; + }; + }; + }; + + ## @TODO: Enable once stable + plugins.blink-cmp = { + enable = false; + }; + + plugins.cmp = { + enable = true; + autoEnableSources = true; + settings = { + enabled.__raw = '' + function() + -- local context = require("cmp.config.context") + -- local is_comment = context.in_treesitter_capture("comment") == true or context.in_syntax_group("Comment") + + buftype = vim.api.nvim_buf_get_option(0, "buftype") + if buftype == "prompt" then + -- don't show in Telescope + return false + end + + local col = vim.fn.col('.') - 1 + local line = vim.fn.getline('.') + local char_under_cursor = string.sub(line, col, col) + + if col == 0 or string.match(char_under_cursor, '%s') then + return false + end + return true + end + ''; + + sources = [ + { name = "nvim_lua"; } + { name = "nvim_lsp"; } + { name = "emoji"; } + { + name = "buffer"; # text within current buffer + option.get_bufnrs.__raw = "vim.api.nvim_list_bufs"; + keywordLength = 3; + } + # { name = "copilot"; } # enable/disable copilot + { + name = "path"; # file system paths + keywordLength = 3; + } + { + name = "luasnip"; # snippets + keywordLength = 3; + } + { name = "cmdline"; } + ]; + + formatting = { + fields = [ "kind" "abbr" "menu" ]; + format = '' + function(entry, vim_item) + local kind_icons = { + Text = "󰊄", + Method = "", + Function = "󰡱", + Constructor = "", + Field = "", + Variable = "󱀍", + Class = "", + Interface = "", + Module = "󰕳", + Property = "", + Unit = "", + Value = "", + Enum = "", + Keyword = "", + Snippet = "", + Color = "", + File = "", + Reference = "", + Folder = "", + EnumMember = "", + Constant = "", + Struct = "", + Event = "", + Operator = "", + TypeParameter = "", + } + vim_item.kind = string.format("%s", kind_icons[vim_item.kind]) + vim_item.menu = ({ + path = "[Path]", + nvim_lua = "[NVIM_LUA]", + nvim_lsp = "[LSP]", + luasnip = "[Snippet]", + buffer = "[Buffer]", + })[entry.source.name] + return vim_item + end + ''; + }; + + completion = { + completeopt = "menuone,noselect"; + }; + + autoEnableSources = true; + + experimental = { ghost_text = true; }; + + performance = { + debounce = 60; + fetchingTimeout = 200; + maxViewEntries = 30; + }; + + snippet = { + expand = '' + function(args) + require('luasnip').lsp_expand(args.body) + end + ''; + }; + + window = { + completion = { border = "solid"; }; + documentation = { border = "solid"; }; + }; + + mapping = { + "" = "cmp.mapping.select_next_item()"; + "" = "cmp.mapping.select_next_item()"; + "" = "cmp.mapping.select_prev_item()"; + "" = "cmp.mapping.select_prev_item()"; + "" = "cmp.mapping.abort()"; + "" = "cmp.mapping.scroll_docs(-4)"; + "" = "cmp.mapping.scroll_docs(4)"; + "" = "cmp.mapping.complete()"; + "" = "cmp.mapping.confirm({ select = true })"; + # "" = '' + # cmp.mapping(function(fallback) + # -- local context = require("cmp.config.context") + # -- local is_comment = context.in_treesitter_capture("comment") == true or context.in_syntax_group("Comment") + # + # local col = vim.fn.col('.') - 1 + # local line = vim.fn.getline('.') + # local char_under_cursor = string.sub(line, col, col) + # + # if col == 0 or string.match(char_under_cursor, '%s') then + # fallback() + # elseif cmp.visible() then + # cmp.confirm({ select = true }) + # else + # fallback() + # end + # end, { "i", "s" }) + # ''; + "" = "cmp.mapping.confirm({ behavior = cmp.ConfirmBehavior.Replace, select = true })"; + "" = '' + cmp.mapping(function() + if luasnip.expand_or_locally_jumpable() then + luasnip.expand_or_jump() + end + end, { 'i', 's' }) + ''; + "" = '' + cmp.mapping(function() + if luasnip.locally_jumpable(-1) then + luasnip.jump(-1) + end + end, { 'i', 's' }) + ''; + }; + }; + }; + + plugins.nvim-lightbulb = { + enable = true; + }; + # config = '' + # lua << EOF + # require('nvim-lightbulb').setup({ + # float = { + # -- "true" causes "invalid buffer id" error + # enabled = false, + # }, + # autocmd = { + # enabled = true, + # }, + # }) + # EOF + # ''; + plugins.lsp-signature = { + enable = true; + }; + # config = '' + # lua << EOF + # require("lsp_signature").setup() + # EOF + # ''; + + ## ------------------------------------------------ + ## Extra Plugins + ## ------------------------------------------------ + + extraPlugins = with pkgs.vimPlugins; [ + vim-dirdiff + { + plugin = vim-signify; + config = '' + let g:signify_vcs_cmds = { 'git': 'git diff --no-color --no-ext-diff -U0 master -- %f' } + let g:signify_priority = 1 + highlight SignColumn ctermbg=237 + ''; + } + vim-surround + + ## focus-nvim only in unstable + # (pkgs.vimUtils.buildVimPlugin { + # name = "focus-nvim"; + # src = pkgs.fetchFromGitHub { + # owner = "nvim-focus"; + # repo = "focus.nvim"; + # rev = "3841a38df972534567e85840d7ead20d3a26faa6"; + # sha256 = "sha256-mgHk4u0ab2uSUNE+7DU22IO/xS5uop9iATfFRk6l6hs="; + # }; + # }) + ]; + }; +} diff --git a/openconnect-pulse-launcher.py b/openconnect-pulse-launcher.py new file mode 100755 index 0000000..1d830e9 --- /dev/null +++ b/openconnect-pulse-launcher.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python + +import getopt +import inspect +import logging +import os +import psutil +import signal +import shutil +import subprocess +import sys +import time +import urllib + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.support.ui import WebDriverWait +from xdg_base_dirs import xdg_config_home + +script_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + +class OpenconnectPulseLauncher: + + def signal_handler(self, _sig, _frame): + subprocess.run(['sudo', 'route', 'del', 'default', 'gw', self.vpn_gateway_ip]) + while 'openconnect' in (i.name() for i in psutil.process_iter()): + subprocess.run(['sudo', 'pkill', '-SIGINT', 'openconnect']) + ps = subprocess.Popen( + ['getent', 'hosts', self.hostname], + stdout=subprocess.PIPE, + ) + output = subprocess.check_output( + ['awk', '{print $1}'], + stdin=ps.stdout + ) + ps.wait() + vpn_ip = output.decode().rstrip() + # This is normally deleted when the VPN is killed, but sometimes is left behind as there are two entries + subprocess.run(['sudo', 'route', 'del', vpn_ip]) + sys.exit(0) + + def __init__(self): + self.is_root = os.geteuid() == 0 + self.chrome_profile_dir = os.path.join(xdg_config_home(), 'chromedriver', 'pulsevpn') + if not os.path.exists(self.chrome_profile_dir): + os.makedirs(self.chrome_profile_dir) + + self.vpn_gateway_ip = None + + signal.signal(signal.SIGINT, self.signal_handler) + + def is_dsid_valid(self, dsid): + # Expiry is set to Session + return dsid is not None and 'value' in dsid + + def connect(self, vpn_url, chromedriver_path, chromium_path, debug=False, script=None): + self.hostname = urllib.parse.urlparse(vpn_url).hostname + + dsid = None + returncode = 0 + while True: + if self.is_dsid_valid(dsid) and returncode != 2: + logging.info('Launching openconnect.') + + ## Run in background + + ## openconnect is built to already point to a pre-packaged vpnc-script, so no need to specify + # p = subprocess.run(['sudo', 'openconnect', '-b', '-C', dsid['value'], '--protocol=pulse', vpn_url, '-s', '${pkgs.unstable.vpnc-scripts}/bin/vpnc-script']) + + ## --no-dtls addresses VPN dying with "ESP detected dead peer", and also "ESP receive error: Message too long" error + ## See: https://gitlab.com/openconnect/openconnect/-/issues/647 + ## Downside: lots of console spam + ## Also, seems to die often with this error: + ## Short packet received (2 bytes) + ## Unrecoverable I/O error; exiting. + # p = subprocess.run(['sudo', 'openconnect', '--no-dtls', '-b', '-C', dsid['value'], '--protocol=pulse', vpn_url]) + command_line = ['sudo', 'openconnect'] + if debug: + command_line.extend(['-vvvv']) + if script is not None: + command_line.extend(['-s', script]) + command_line.extend(['-b', '-C', dsid['value'], '--protocol=pulse', vpn_url]) + if debug: + print('Command line:') + print(' {}'.format(' '.join(command_line))) + print('') + p = subprocess.run(command_line) + + returncode = p.returncode + + ## Get tun0 IP and set as default GW (vpnc-script doesn't do this for some reason) + ## Probably due to something like this: + ## https://github.com/dlenski/openconnect/issues/125#issuecomment-426032102 + ## There is an error on the command line when openconnect is run: + ## Error: argument "via" is wrong: use nexthop syntax to specify multiple via + + ## sleep to make sure tun0 is available + time.sleep(3) + ps = subprocess.Popen( + ['ifconfig', 'tun0'], + stdout=subprocess.PIPE + ) + output = subprocess.check_output( + ['awk', '-F', ' *|:', '/inet /{print $3}'], + stdin=ps.stdout + ) + ps.wait() + self.vpn_gateway_ip = output.decode().rstrip() + print('VPN IP: '+self.vpn_gateway_ip) + p = subprocess.run(['sudo', 'route', 'add', 'default', 'gw', self.vpn_gateway_ip]) + + # Wait for ctrl-c + signal.pause() + else: + returncode = 0 + service = Service(executable_path=chromedriver_path) + options = webdriver.ChromeOptions() + options.binary_location = chromium_path + options.add_argument('--window-size=800,900') + # options.add_argument('--remote-debugging-pipe') + # options.add_argument('--remote-debugging-port=9222') + options.add_argument('user-data-dir=' + self.chrome_profile_dir) + + logging.info('Starting browser.') + driver = webdriver.Chrome(service=service, options=options) + + driver.get(vpn_url) + dsid = WebDriverWait(driver, float('inf')).until(lambda driver: driver.get_cookie('DSID')) + driver.quit() + logging.info('DSID cookie: %s', dsid) + +def main(argv): + script_name = os.path.basename(__file__) + chromedriver_path = shutil.which('chromedriver') + chromium_path = shutil.which('chromium') or shutil.which('google-chrome') + help_message = '{} '.format(script_name) + + try: + opts, args = getopt.getopt(argv, 'hds:c:', ['help', 'debug', 'script=', 'chromedriver-path']) + except getopt.GetoptError: + print(help_message) + sys.exit(2) + if len(args) != 1: + print(help_message) + sys.exit(2) + debug = False + script = None + for o, a in opts: + if o in ('-h', '--help'): + print(help_message) + sys.exit() + elif o in ('-d', '--debug'): + debug = True + elif o in ('-s', '--script'): + if len(a): + script = a + elif o in ('-c', '--chromedriver-path'): + if len(a): + chromedriver_path = a + vpn_url = args[0] + + launcher = OpenconnectPulseLauncher() + launcher.connect(vpn_url, chromedriver_path=chromedriver_path, chromium_path=chromium_path, debug=debug, script=script) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..06d1bc0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +attrs==23.2.0 +certifi==2024.2.2 +h11==0.14.0 +idna==3.6 +outcome==1.3.0.post0 +psutil==5.9.8 +PySocks==1.7.1 +selenium==4.19.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +trio==0.25.0 +trio-websocket==0.11.1 +typing_extensions==4.11.0 +urllib3==2.2.1 +wsproto==1.2.0 +xdg-base-dirs==6.0.1