first commit

This commit is contained in:
Ellis Rahhal 2025-02-13 10:35:17 -08:00
commit e19a09f2df
8 changed files with 1186 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
result

15
README.md Normal file
View file

@ -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
```

6
default.nix Normal file
View file

@ -0,0 +1,6 @@
{ ... }:
{
imports = [
./nixvim.nix
];
}

27
flake.lock generated Normal file
View file

@ -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
}

35
flake.nix Normal file
View file

@ -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;
};
};
};
}

920
nixvim.nix Normal file
View file

@ -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 <Tab>
softtabstop = 4; # Number of spaces that a <Tab> counts for while performing editing operations, like inserting a <Tab> or using <BS>. 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<CR>";
}
## Toggle nvim-tree visibility
{
mode = [ "n" ];
key = ",m";
action = ":NvimTreeToggle<CR>";
}
# -----------------------------------------------------
# buffer manipulation
# -----------------------------------------------------
## Next Buffer
{
key = "<Tab>";
action = ":bn<CR>";
options = { noremap = true; };
}
## Previous Buffer
{
key = "<S-Tab>";
action = ":bp<CR>";
options = { noremap = true; };
}
## Close Buffer
{
key = "<leader><Tab>";
action = ":bd<CR>";
options = { noremap = true; };
}
## Force Close Buffer
{
key = "<leader><S-Tab>";
action = ":bd!<CR>";
options = { noremap = true; };
}
## New Tab
{
key = "<leader>t";
action = ":tabnew split<CR>";
options = { noremap = true; };
}
# -----------------------------------------------------
# Telescope
# -----------------------------------------------------
## Lists files in your current working directory, respects .gitignore
{
mode = [ "n" ];
key = "<leader>ff";
action = "<cmd>Telescope find_files<cr>";
options = { noremap = true; };
}
## Finds files by filename
{
mode = [ "n" ];
key = "<c-p>";
action = "<cmd>Telescope find_files<cr>";
# action = "<cmd>FzfLua files<cr>";
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 = "<leader>fg";
action = "<cmd>Telescope live_grep<cr>";
# action = "<cmd>FzfLua live_grep<cr>";
options = { noremap = true; };
}
# Search file contents
{
mode = [ "n" ];
key = "<c-s>";
action = "<cmd>Telescope live_grep<cr>";
# action = "<cmd>FzfLua live_grep<cr>";
options = { noremap = true; };
}
# Lists open buffers in current neovim instance
{
mode = [ "n" ];
key = "<leader>db";
action = "<cmd>Telescope buffers<cr>";
# action = "<cmd>FzfLua buffers<cr>";
options = { noremap = true; };
}
# Lists available help tags and opens a new window with the relevant help info on <cr>
{
mode = [ "n" ];
key = "<leader>fh";
action = "<cmd>Telescope help_tags<cr>";
# action = "<cmd>FzfLua helptags<cr>";
options = { noremap = true; };
}
# Lists manpage entries, opens them in a help window on <cr>
{
mode = [ "n" ];
key = "<leader>fm";
action = "<cmd>Telescope man_pages<cr>";
# action = "<cmd>FzfLua manpages<cr>";
options = { noremap = true; };
}
# Lists previously open files
{
mode = [ "n" ];
key = "<leader>fp";
action = "<cmd>Telescope oldfiles<cr>";
# action = "<cmd>FzfLua oldfiles<cr>";
options = { noremap = true; };
}
# Lists previously open files, Maps to ctrl-/
{
mode = [ "n" ];
key = "<c-_>";
action = "<cmd>Telescope oldfiles<cr>";
# action = "<cmd>FzfLua oldfiles<cr>";
options = { noremap = true; };
}
# Lists spelling suggestions for the current word under the cursor, replaces word with selected suggestion on <cr>
{
mode = [ "n" ];
key = "<leader>fs";
action = "<cmd>Telescope spell_suggest<cr>";
# action = "<cmd>FzfLua spell_suggest<cr>";
options = { noremap = true; };
}
# Lists LSP references for iword under the cursor
{
mode = [ "n" ];
key = "<leader>fr";
action = "<cmd>Telescope lsp_references<cr>";
# action = "<cmd>FzfLua lsp_references<cr>";
options = { noremap = true; };
}
# Lists LSP incoming calls for word under the cursor
{
mode = [ "n" ];
key = "<leader>fi";
action = "<cmd>Telescope lsp_incoming_calls<cr>";
# action = "<cmd>FzfLua lsp_incoming_calls<cr>";
options = { noremap = true; };
}
# Lists LSP outgoing calls for word under the cursor
{
mode = [ "n" ];
key = "<leader>fo";
action = "<cmd>Telescope lsp_outgoing_calls<cr>";
# action = "<cmd>FzfLua lsp_outgoing_calls<cr>";
options = { noremap = true; };
}
# Dynamically Lists LSP for all workspace symbols
{
mode = [ "n" ];
key = "<leader>fw";
action = "<cmd>Telescope lsp_dynamic_workspace_symbols<cr>";
# action = "<cmd>FzfLua lsp_workspace_symbols<cr>";
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 = "<leader>fd";
action = "<cmd>Telescope lsp_definitions<cr>";
options = { noremap = true; };
}
# Got to previous error
{
mode = [ "n" ];
key = "[d";
action = "<cmd>lua vim.diagnostic.goto_prev()<CR>";
options = { noremap = true; silent = true; };
}
{
mode = [ "n" ];
key = ",k";
action = "<cmd>lua vim.diagnostic.goto_prev()<CR>";
options = { noremap = true; silent = true; };
}
# Got to next error
{
mode = [ "n" ];
key = "]d";
action = "<cmd>lua vim.diagnostic.goto_next()<CR>";
options = { noremap = true; silent = true; };
}
{
mode = [ "n" ];
key = ",j";
action = "<cmd>lua vim.diagnostic.goto_next()<CR>";
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 = "<A-h>";
action = ":BufferLineCyclePrev<CR>";
options = { noremap = true; silent = true; };
}
{
mode = [ "n" ];
key = "<A-l>";
action = ":BufferLineCycleNex<CR>";
options = { noremap = true; silent = true; };
}
{
mode = [ "n" ];
key = "<A-c>";
action = ":bdelete!<CR>";
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
"<esc>" = {
__raw = "require('telescope.actions').close";
};
# Ctrl-space is used by Tmux, so remap to Ctrl-e
"<c-e>" = {
__raw = "require('telescope.actions').to_fuzzy_refine";
};
# "<c-o>" = {
# __raw = "require('trouble.sources.telescope').open";
# };
};
n = {
# "<c-o>" = {
# __raw = "require('trouble.sources.telescope').open";
# };
};
};
};
};
};
plugins.fzf-lua = {
enable = true;
# profile = "telescope";
settings = {
oldfiles = {
# In Telescope, when I used <leader>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 <leader>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 = {
"<C-p>" = {
action = "git_files";
options = {
desc = "Fzf-Lua Git Files";
silent = true;
};
settings = {
previewers = {
cat = {
cmd = "${pkgs.coreutils-full}/bin/cat";
};
};
# winopts = {
# height = 0.5;
# };
};
};
# "<C-s>" = "live_grep";
# "<C-_>" = "oldfiles";
# "<leader>fd" = "lsp_definitions";
# "<leader>fg" = "live_grep";
# "<leader>fh" = "helptags";
# "<leader>fi" = "lsp_incoming_calls";
# "<leader>fm" = "manpages";
# "<leader>fo" = "lsp_outgoing_calls";
# "<leader>fp" = "oldfiles";
# "<leader>fr" = "lsp_references";
# "<leader>fs" = "spell_suggest";
# "<leader>fw" = "lsp_workspace_symbols";
# "<leader>db" = "buffers";
# "<leader>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 = {
"<C-j>" = "cmp.mapping.select_next_item()";
"<C-n>" = "cmp.mapping.select_next_item()";
"<C-k>" = "cmp.mapping.select_prev_item()";
"<C-p>" = "cmp.mapping.select_prev_item()";
"<C-e>" = "cmp.mapping.abort()";
"<C-b>" = "cmp.mapping.scroll_docs(-4)";
"<C-f>" = "cmp.mapping.scroll_docs(4)";
"<C-Space>" = "cmp.mapping.complete()";
"<Tab>" = "cmp.mapping.confirm({ select = true })";
# "<Tab>" = ''
# 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" })
# '';
"<S-Tab>" = "cmp.mapping.confirm({ behavior = cmp.ConfirmBehavior.Replace, select = true })";
"<C-l>" = ''
cmp.mapping(function()
if luasnip.expand_or_locally_jumpable() then
luasnip.expand_or_jump()
end
end, { 'i', 's' })
'';
"<C-h>" = ''
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=";
# };
# })
];
};
}

166
openconnect-pulse-launcher.py Executable file
View file

@ -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 = '{} <vpn_url>'.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:])

16
requirements.txt Normal file
View file

@ -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