From d376c7bb31e29cc3a4ac925be09df0d1b4564a3c Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 17 Jul 2024 11:39:18 -0700 Subject: [PATCH 01/39] Add bash function to set volume --- config/bash/functions.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/config/bash/functions.sh b/config/bash/functions.sh index 456a107..a612190 100644 --- a/config/bash/functions.sh +++ b/config/bash/functions.sh @@ -609,3 +609,17 @@ function bluetooth_reset() { echo "Bluetooth restarted!" } + +function set_volume() +{ + installed "amixer" || return 1 + + local volume + volume="$1" + + if [ "$volume" = "" ]; then + volume="25" + fi + + amixer -D pulse sset Master "${volume}%" +} From 7bccac35be84cc7ebf1d7ba319dd98a82fd81b60 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 17 Jul 2024 13:32:35 -0700 Subject: [PATCH 02/39] Add shell indicator to wezterm titles --- config/wezterm.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/config/wezterm.lua b/config/wezterm.lua index 3000918..2d23ed9 100644 --- a/config/wezterm.lua +++ b/config/wezterm.lua @@ -4,6 +4,36 @@ local act = wezterm.action local config = {} +function get_shell(tab_info) + local shell = '' + if tab_info.active_pane.domain_name == "local" then + shell = '(Git Bash) ' + elseif tab_info.active_pane.domain_name == "WSL:Ubuntu" then + shell = '(WSL) ' + end + return shell +end + +wezterm.on('format-window-title', function(tab, pane, tabs, panes, config) + local index = '' + if #tabs > 1 then + index = string.format('[%d/%d] ', tab.tab_index + 1, #tabs) + end + return index .. get_shell(tab) .. tab.active_pane.title +end) + +function tab_title(tab_info) + local title = tab_info.tab_title + if title and #title > 0 then + return title + end + return tab_info.active_pane.title +end + +wezterm.on('format-tab-title', function(tab, tabs, panes, config, hover, max_width) + return get_shell(tab) .. tab_title(tab) +end) + config.default_prog = {"bash"} config.default_domain = 'WSL:Ubuntu' From de061f45b7bf63956b70234e5158577767e37c04 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Fri, 19 Jul 2024 16:26:35 -0700 Subject: [PATCH 03/39] Add Neovim command for closing tabs --- config/nvim/lua/dot/globals.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config/nvim/lua/dot/globals.lua b/config/nvim/lua/dot/globals.lua index 1657c45..9c59da5 100644 --- a/config/nvim/lua/dot/globals.lua +++ b/config/nvim/lua/dot/globals.lua @@ -5,6 +5,11 @@ local VimPlug = require('dot.vim_plug') local M = {} +function M._move_to_column(opts) + local column = tonumber(opts.args) + Util.move_to_column(column) +end + function M._create_command(name, fn, opts) vim.api.nvim_create_user_command(name, fn, opts) end @@ -27,7 +32,8 @@ function M.init() M._create_commands({ { 'ReloadConfig', Util.reload_config, {} }, - { 'CloseTabsToRight', Util.close_tabs_to_right, {} } + { 'CloseTabsToRight', Util.close_tabs_to_right, {} }, + { 'MoveToColumn', M._move_to_column, { nargs = 1 } } }) end From a2b2fe0319cbcfbdad44af12a362664c0f1b4d8e Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 22 Jul 2024 00:19:05 -0700 Subject: [PATCH 04/39] Add xp todo --- todo.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/todo.md b/todo.md index a8538e5..99e943b 100644 --- a/todo.md +++ b/todo.md @@ -33,6 +33,10 @@ Improvements I'd like to make to my dotfiles. - [ ] Clean up Neovim Healthcheck (**Neovim Healtheck** section) +## XP Submodule + +Use `xp` as a submodule to DRY our Python. + ## Python CLI ### Git Sync Command From f9da930074d045fd94d47a683f1b451743df52f3 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Sat, 3 Aug 2024 15:39:46 -0700 Subject: [PATCH 05/39] Improve neovim tabline --- config/nvim/lua/dot/settings.lua | 81 ++++++++++++++++++++++++++++++++ config/nvim/lua/dot/util.lua | 34 ++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/config/nvim/lua/dot/settings.lua b/config/nvim/lua/dot/settings.lua index d835f7c..e24e715 100644 --- a/config/nvim/lua/dot/settings.lua +++ b/config/nvim/lua/dot/settings.lua @@ -58,10 +58,91 @@ function M._directories() vim.opt.directory = { dir, "." } end +function M._generate_tabline_tab_label(tab_index) + local max_filename_length = 48 + + local s = "" + + local buflist = vim.fn.tabpagebuflist(tab_index) + local winnr = vim.fn.tabpagewinnr(tab_index) + local bufname = vim.fn.fnamemodify(vim.fn.bufname(buflist[winnr]), ":t") + bufname = Util.truncate_center(bufname, max_filename_length) + + local is_modified = false + for _, i in ipairs(buflist) do + if Util.is_buffer_modified(i) then + Log.info("buffer " .. i .. " is modified") + is_modified = true + break + end + end + + -- Add an indicator if any buffer in the tab has unsaved changes + if is_modified then + Log.info("Adding modified indicator") + s = s .. "+" + else + s = s .. " " + end + + -- Add the tab index so it's easier to navigate to specific tabs + s = s .. "[" .. tab_index .. "]" + + -- Append the focused buffer's truncated file name or if it doesn't have + -- one just label it UNSAVED + if #bufname > 0 then + return s .. " " .. bufname + else + return s .. " UNSAVED" + end +end + +function M._generate_tabline_tab(i) + local s = "" + + -- Set whether or not the tab is selected + if i == vim.fn.tabpagenr() then + s = s .. '%#TabLineSel#' + else + s = s .. '%#TabLine#' + end + + -- Set the tab page number for navigation + s = s .. '%' .. i .. 'T' + + -- Set the tab label + s = s .. " " .. M._generate_tabline_tab_label(i) .. " " + + return s +end + +function M._generate_tabline() + local s = "" + + for i = 1, vim.fn.tabpagenr('$') do + s = s .. M._generate_tabline_tab(i) + end + + -- After the last tab fill with TabLineFill and reset tab page nr + s = s .. '%#TabLineFill#%T' + + -- Right-align the label to close the current tab page + if vim.fn.tabpagenr('$') > 1 then + s = s .. '%=%#TabLine#%999Xclose' + end + + return s +end + +function M._tab_line() + vim.o.tabline = "%!v:lua.require'dot.settings'._generate_tabline()" +end + function M.init() M._base() M._indentation() M._directories() + M._tab_line() end return M diff --git a/config/nvim/lua/dot/util.lua b/config/nvim/lua/dot/util.lua index 6c90c91..fe105e0 100644 --- a/config/nvim/lua/dot/util.lua +++ b/config/nvim/lua/dot/util.lua @@ -136,4 +136,38 @@ function M.copy_file_and_line() vim.fn.setreg('+', file_and_line) end +--[[ + Truncates a string from the center if it exceeds the specified maximum + length. + + The string is truncated by removing characters from the middle and replacing + them with two periods ('..') if the length of the input string exceeds the + specified maximum length. The resulting truncated string will have a total + length not exceeding the max_length parameter. + + Example: + truncate_center("VeryLongFileName.cpp", 12) => "VeryL..e.cpp" + + Parameters: + str (string): The input string to be truncated. + max_length (number): The maximum allowed length for the resulting string. + + Returns: + string: The possibly truncated string. +]] +function M.truncate_center(str, max_length) + local length = #str + if length <= max_length then + return str + else + local part_length = math.floor((max_length - 2) / 2) + return str:sub(1, part_length) .. ".." .. str:sub(length - part_length + 1, length) + end +end + +-- Returns whether or not a buffer contains unsaved changes +function M.is_buffer_modified(buffer_index) + return vim.fn.getbufvar(buffer_index, '&modified') == 1 +end + return M From a2247a40f816ec28fc9b333fb31186fe5a50b0eb Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Sat, 3 Aug 2024 15:40:23 -0700 Subject: [PATCH 06/39] CLI Windows support stuff --- cli/commands/provision.py | 6 +++--- cli/lib/provision/provisioner_dot.py | 2 +- todo.md | 21 ++++++++++++++++----- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cli/commands/provision.py b/cli/commands/provision.py index e33fb06..ce53e77 100644 --- a/cli/commands/provision.py +++ b/cli/commands/provision.py @@ -52,10 +52,10 @@ def add_provision_parser(subparsers: argparse._SubParsersAction) -> None: def cmd_provision(args: argparse.Namespace) -> None: - if os.getuid() == 0: - raise Exception("do not run as root") - if OperatingSystem.get().is_linux(): + if os.getuid() == 0: + raise Exception("do not run as root") + distro = DistroInformation.get() Log.info( "provisioning system", diff --git a/cli/lib/provision/provisioner_dot.py b/cli/lib/provision/provisioner_dot.py index f7d5a51..e84d885 100644 --- a/cli/lib/provision/provisioner_dot.py +++ b/cli/lib/provision/provisioner_dot.py @@ -26,7 +26,7 @@ def _generate_dot_cli_completion_script(self) -> str: cmd = [ "register-python-argcomplete", "--external-argcomplete-script", - os.path.join(Dir.dot(), "cli/dot.py"), + os.path.join(Dir.dot(), "cli", "dot.py"), "dot", ] diff --git a/todo.md b/todo.md index 99e943b..6509e81 100644 --- a/todo.md +++ b/todo.md @@ -205,17 +205,28 @@ python3 -m pip install typing_extensions Also needs to be installed as root if the script elevates sudo python3 -m pip install typing_extensions +### Windows Support + +- Don't need to implement full provisioning but at least get clean/link commands to work on Windows +- Might be nice to have a script to provision WezTerm on Windows +- Remove dot.sh and Makefile (Except maybe for bootstrapping) + +- Make sure the following is added to path before running `dot provision cli` + - `C:\Users\pewing\AppData\Roaming\Python\Python310\Scripts` + - Update version in the path as necessary + - Tools install via Pip aren't automatically added to PATH like they are on Linux + - TODO: Actually this is just broken altogether, the following fails when run directly in Git Bash: + - `register-python-argcomplete --external-argcomplete-script $HOME/dot/cli/dot.py dot` + - So it may just not play nicely with windows + - For now, maybe just copy it from Linux and update the paths? + + ## FZF Bash Integration `~/.fzf.bash` doesn't exist for me, maybe because I'm installing via apt. I'd like that so I can get fzf `ctrl+r` functionality so update the provision script to set that up correctly. -## Windows support in Python CLI - -- Don't need to implement full provisioning but at least get clean/link commands to work on Windows -- Might be nice to have a script to provision WezTerm on Windows - ## wezterm shell integration Automatically download wezterm.sh and source it in ~/.localrc or at least document this for WSL setup From e5f5be30568bdd32e337c0bcfd522845488ba750 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Fri, 9 Aug 2024 09:42:27 -0700 Subject: [PATCH 07/39] [Auto] Syncing local changes with remote --- bin/fzf_cached_wsl | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/bin/fzf_cached_wsl b/bin/fzf_cached_wsl index 9d8f40a..d98764d 100755 --- a/bin/fzf_cached_wsl +++ b/bin/fzf_cached_wsl @@ -85,7 +85,21 @@ def parse_args(): class FuzzyFileFinder: @staticmethod def find_files(directory, on_file): - ignore_patterns = [] + # TODO: It would be cool to support .fzfignore files in sub-directories + # but that gets a little more complicated because we'd need to layer + # them on top of each other while recursively descending and then + # remove them as we traverse back up. Definitely possible but just + # requires more effort than I want to put in right now. + + # TODO: I don't think these need to be separated, we can probably just + # have a single list of ignore patterns that match on either files or + # directories. There may be times where we want a pattern to only match + # against one or the other but I can't think of any specific examples + # right now. It is kind of nice in the .fzfignore file to see which + # patterns are expected to match against directories though. Anyways, + # leaving them separate for now but can revisit later. + file_ignore_patterns = [] + dir_ignore_patterns = [] ignore_file = os.path.join(directory, ".fzfignore") if os.path.isfile(ignore_file): ignore_file_content = None @@ -98,25 +112,37 @@ class FuzzyFileFinder: ) ) for line in lines: - if line.startswith("#"): + if line.lstrip().startswith("#") or line.strip() == "": continue - ignore_patterns.append(line) + elif line.lower().startswith("d "): + dir_ignore_patterns.append(line[2:]) + else: + file_ignore_patterns.append(line) def handle_dir( directory: FileWalker.Directory, ) -> Optional[FileWalker.DirectoryHandlerResult]: - if directory.get_name() == ".git": + name = directory.get_name() + if name == ".git": return FileWalker.DirectoryHandlerResult(skip=True) + for ignore_pattern in dir_ignore_patterns: + path_rel = directory.get_relative_path() + m = re.match(ignore_pattern, path_rel) + if m is not None: + # Keep this commented out for performance except when debugging + # Log.debug("ignoring directory", [("ignore_pattern", ignore_pattern), ("dir", path_rel)]) + return FileWalker.DirectoryHandlerResult(skip=True) def handle_file( file: FileWalker.File, ) -> Optional[FileWalker.FileHandlerResult]: name = file.get_name() - for ignore_pattern in ignore_patterns: - m = re.match(ignore_pattern, file.get_relative_path()) + for ignore_pattern in file_ignore_patterns: + path_rel = file.get_relative_path() + m = re.match(ignore_pattern, path_rel) if m is not None: # Keep this commented out for performance except when debugging - # Log.debug("ignoring file", [("ignore_pattern", ignore_pattern), ("file", file.get_relative_path())]) + # Log.debug("ignoring file", [("ignore_pattern", ignore_pattern), ("file", path_rel)]) return on_file(file.get_relative_path()) From ea5731b72f2e0b01bbfd6759fc35e25a23b35b76 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 14 Aug 2024 15:46:46 -0700 Subject: [PATCH 08/39] Add git-bash alias/function --- config/bash/aliases.sh | 5 +++++ config/bash/functions.sh | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/config/bash/aliases.sh b/config/bash/aliases.sh index 2a72e08..fd2ac7f 100644 --- a/config/bash/aliases.sh +++ b/config/bash/aliases.sh @@ -228,3 +228,8 @@ fi if _is_installed 'parsecd'; then set_alias '0' 'parsec' 'parsecd app_daemon=1' fi + +# Alias for WSL to open a new WezTerm window with a Git Bash shell +if _is_wsl; then + set_alias '0' 'git-bash' '_git_bash' +fi diff --git a/config/bash/functions.sh b/config/bash/functions.sh index a612190..498c5e3 100644 --- a/config/bash/functions.sh +++ b/config/bash/functions.sh @@ -623,3 +623,20 @@ function set_volume() amixer -D pulse sset Master "${volume}%" } + +function _git_bash() +{ + local wezterm_exe="/mnt/c/Program Files/WezTerm/wezterm-gui.exe" + local wezterm_args=( + "start" + "--domain" "local" + ) + + local bash_exe="C:\\Program Files\\Git\\bin\\bash.exe" + local bash_args=( + "-i" # Interactive + "-l" # Login shell + ) + + "$wezterm_exe" "${wezterm_args[@]}" -- "$bash_exe" "${bash_args[@]}" & +} From a6fef43b460b16a5b06ce44a9fe660c2d8e796f7 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 14 Aug 2024 15:53:10 -0700 Subject: [PATCH 09/39] Improve Neovim tabline --- config/nvim/lua/dot/settings.lua | 211 ++++++++++++++++++++++++------- 1 file changed, 168 insertions(+), 43 deletions(-) diff --git a/config/nvim/lua/dot/settings.lua b/config/nvim/lua/dot/settings.lua index e24e715..1b5d218 100644 --- a/config/nvim/lua/dot/settings.lua +++ b/config/nvim/lua/dot/settings.lua @@ -58,80 +58,205 @@ function M._directories() vim.opt.directory = { dir, "." } end -function M._generate_tabline_tab_label(tab_index) - local max_filename_length = 48 +function M._get_tab_info(i) + -- Check whether or not the tab is selected + local is_selected = false + if i == vim.fn.tabpagenr() then + is_selected = true + end - local s = "" + local buflist = vim.fn.tabpagebuflist(i) + local winnr = vim.fn.tabpagewinnr(i) - local buflist = vim.fn.tabpagebuflist(tab_index) - local winnr = vim.fn.tabpagewinnr(tab_index) - local bufname = vim.fn.fnamemodify(vim.fn.bufname(buflist[winnr]), ":t") - bufname = Util.truncate_center(bufname, max_filename_length) + -- The title is the name of the file currently open in the buffer focused + -- within the tab. If it doesn't have one because it's an unsaved buffer, + -- fall back to UNSAVED + local title = vim.fn.fnamemodify(vim.fn.bufname(buflist[winnr]), ":t") + if #title == 0 then + title = "UNSAVED" + end + -- Check if any buffers open in the tab are modified and need to be save local is_modified = false for _, i in ipairs(buflist) do if Util.is_buffer_modified(i) then - Log.info("buffer " .. i .. " is modified") + Log.debug("buffer " .. i .. " is modified") is_modified = true break end end - -- Add an indicator if any buffer in the tab has unsaved changes - if is_modified then - Log.info("Adding modified indicator") - s = s .. "+" - else - s = s .. " " - end + return { + title = title, + index = i, + is_modified = is_modified, + is_selected = is_selected + } +end - -- Add the tab index so it's easier to navigate to specific tabs - s = s .. "[" .. tab_index .. "]" +function M._format_tabline_tab(tab) + local s = "" - -- Append the focused buffer's truncated file name or if it doesn't have - -- one just label it UNSAVED - if #bufname > 0 then - return s .. " " .. bufname - else - return s .. " UNSAVED" - end + -- Format the modified indicator + s = s .. (tab["is_modified"] and "+" or " ") + + -- Format the tab index + s = s .. "[" .. tab["index"] .. "] " + + -- Format the tab title + s = s .. tab["title"] + + return s end -function M._generate_tabline_tab(i) +function M._format_tabline_no_truncation(tabs) local s = "" + for i, tab in pairs(tabs) do + -- Separate tabs with a space + if i > 1 then + s = s .. " " + end + -- Set the tab metadata + s = s .. (tab["is_selected"] and "%#TabLineSel#" or "%#TabLine#") + s = s .. "%" .. tab["index"] .. "T" - -- Set whether or not the tab is selected - if i == vim.fn.tabpagenr() then - s = s .. '%#TabLineSel#' - else - s = s .. '%#TabLine#' + s = s .. M._format_tabline_tab(tab) end + return s .. '%#TabLineFill#%T' +end + +-- The "Basic" truncation strategy is to hide all of the tabs that don't fit on +-- the tabline and put an indicator on the right of how many more tabs there +-- are. I Personallly like this better than squishing each tab down. I'd rather +-- the first N tabs be readable and just hide the ones that don't fit than make +-- them all less readable. +function M._format_tabline_basic_truncation(tabs, truncation) + -- TODO: We could merge this loop with the one below and do this in one + -- pass but I'm too lazy right now + local tab_contents = {} + for i, tab in pairs(tabs) do + local meta = (tab["is_selected"] and "%#TabLineSel#" or "%#TabLine#") + meta = meta .. "%" .. tab["index"] .. "T" + + local label = M._format_tabline_tab(tab) - -- Set the tab page number for navigation - s = s .. '%' .. i .. 'T' + tab_contents[i] = { + meta = meta, + label = label + } + end + + local suffix_base_len = #" ... (x more)" + local current_length = 0 + local truncate_index = 0 + for i, tab_content in pairs(tab_contents) do + -- First, check if we can even fit the truncation suffix. It could be + -- longer than the tab label if the file in the tab has a very short + -- name. If this won't fit, then we actually need to start truncating + -- at the previous tab + local remaining_tabs = (#tabs - i) + 1 + local suffix_len = suffix_base_len + math.floor(remaining_tabs / 10) + if (current_length + suffix_len) > truncation["max_length"] then + truncate_index = math.max(0, i - 1) + break + end + + -- Separate tabs with a space + local separator = "" + if i > 1 then + separator = " " + end - -- Set the tab label - s = s .. " " .. M._generate_tabline_tab_label(i) .. " " + local tab_len = #tab_content["label"] + #separator + + -- If appending this label would exceed the max length, start + -- truncating at this tab + if (current_length + tab_len) > truncation["max_length"] then + truncate_index = i + break + end + + current_length = current_length + tab_len + end + + -- Now that we know where to start truncating, format the tabline + local s = "" + for i, tab_content in pairs(tab_contents) do + if i >= truncate_index then + break + end + if i > 1 then + s = s .. " " + end + s = s .. tab_content["meta"] .. tab_content["label"] + end + + -- Align the truncation suffix to the right + s = s .. "%=" + + -- Fill any extra space and disassociate the following text from the last tab + s = s .. "%#TabLineFill#%T" + + -- Append the truncation suffix + local truncated_tabs = (#tabs - truncate_index) + 1 + s = s .. "... (" .. tostring(truncated_tabs) .. " more)" return s end -function M._generate_tabline() - local s = "" +function M._format_tabline(tabs, truncation) + if truncation["strategy"] == "Basic" then + return M._format_tabline_basic_truncation(tabs, truncation) + else + return M._format_tabline_no_truncation(tabs) + end +end +-- Tabline generation is done in two steps. The first step collects all of the +-- tab information and the second step formats the tabline. The reason is that +-- we need to be able to check the total length and truncate appropriately. +-- That's harder to do when the string contains characters that are actually +-- printed, like the `%#TabLine#` tokens. So instead, we need to gather all of +-- the desired tab labels, sum their lengths, choose a truncation strategy, and +-- then format everything using that strategy. +function M._generate_tabline() + -- Get all of the tab information + local tabs = {} for i = 1, vim.fn.tabpagenr('$') do - s = s .. M._generate_tabline_tab(i) + tabs[i] = M._get_tab_info(i) end - -- After the last tab fill with TabLineFill and reset tab page nr - s = s .. '%#TabLineFill#%T' + -- The length of the tab label prefix (I.E. "+[1] "), which can vary if the + -- tab index is multiple digits. Ignoring the case of >100 tabs; I never + -- use that many and there would be no good way to format that anyways + local prefix_length = #tabs >= 10 and 6 or 5 - -- Right-align the label to close the current tab page - if vim.fn.tabpagenr('$') > 1 then - s = s .. '%=%#TabLine#%999Xclose' + local total_length = 0 + for _, tab in ipairs(tabs) do + local title_len = #tab["title"] + local tab_len = title_len + prefix_length + total_length = total_length + tab_len end - return s + -- We separate tabs with a space so account for that as well + total_length = total_length + #tabs - 1 + + local truncation = { + strategy = "None", + total_length = total_length, + max_length = vim.o.columns, + amount = 0, + amount_per_tab = 0, + } + + -- Handle truncating if the tabline is too long + if total_length > truncation["max_length"] then + truncation["strategy"] = "Basic" + truncation["amount"] = truncation["total_length"] - truncation["max_length"] + truncation["amount_per_tab"] = truncation["amount"] - #tabs + end + + return M._format_tabline(tabs, truncation) end function M._tab_line() From 23bf5d2ae9b7d46d4b2603d478384fc4b03e0c9e Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Tue, 27 Aug 2024 09:48:53 -0700 Subject: [PATCH 10/39] Create default fd config file --- cli/commands/fd/__init__.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/cli/commands/fd/__init__.py b/cli/commands/fd/__init__.py index 8e31cc0..9d420e3 100644 --- a/cli/commands/fd/__init__.py +++ b/cli/commands/fd/__init__.py @@ -15,11 +15,16 @@ class Config: @staticmethod def load() -> dict[str, str]: - try: - with open(Config._path(), "r") as f: - return json.loads(f.read()) - except FileNotFoundError: - return {} + if not os.path.exists(Config._path()): + Config._init() + with open(Config._path(), "r") as f: + return json.loads(f.read()) + + @staticmethod + def _init() -> None: + os.makedirs(os.path.dirname(Config._path()), exist_ok=True) + with open(Config._path(), "w") as f: + f.write(json.dumps(Config._default(), indent=" ")) @staticmethod def _path(): @@ -27,6 +32,16 @@ def _path(): Config._PATH = f"{home()}/.config/fd/config.json" return Config._PATH + @staticmethod + def _default() -> dict[str, any]: + return { + "update": { + "git_search_paths": [ + "~/src" + ] + } + } + RegistryEntry = dict[str, str] RegistryEntries = dict[str, RegistryEntry] From 852abda709386fc85a1bc3b2a3aca68735d322c0 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Thu, 29 Aug 2024 11:33:29 -0700 Subject: [PATCH 11/39] Update logging to use dicts --- bin/fzf_cached_wsl | 16 ++++++++-------- cli/commands/git_sync.py | 2 +- cli/lib/common/alternatives.py | 2 +- cli/lib/common/apt.py | 4 ++-- cli/lib/common/archive.py | 4 ++-- cli/lib/common/git.py | 18 +++++++++--------- cli/lib/common/group.py | 6 +++--- cli/lib/common/links.py | 2 +- cli/lib/common/linter.py | 6 +++--- cli/lib/common/pip.py | 2 +- cli/lib/common/shell.py | 18 +++++++++--------- cli/lib/common/util.py | 16 ++++++++-------- cli/lib/provision/provisioner_dot.py | 2 +- cli/lib/provision/provisioner_flavours.py | 4 ++-- cli/lib/provision/provisioner_i3.py | 10 +++++----- cli/lib/provision/provisioner_kitty.py | 2 +- cli/lib/provision/provisioner_neovim.py | 4 ++-- cli/lib/provision/provisioner_treesitter.py | 8 ++++---- cli/lib/provision/provisioner_win32yank.py | 4 ++-- cli/lib/provision/symlink.py | 4 ++-- cli/lib/provision/system_provisioner.py | 2 +- 21 files changed, 68 insertions(+), 68 deletions(-) diff --git a/bin/fzf_cached_wsl b/bin/fzf_cached_wsl index d98764d..7371ab2 100755 --- a/bin/fzf_cached_wsl +++ b/bin/fzf_cached_wsl @@ -130,7 +130,7 @@ class FuzzyFileFinder: m = re.match(ignore_pattern, path_rel) if m is not None: # Keep this commented out for performance except when debugging - # Log.debug("ignoring directory", [("ignore_pattern", ignore_pattern), ("dir", path_rel)]) + # Log.debug("ignoring directory", {"ignore_pattern": ignore_pattern, "dir": path_rel}) return FileWalker.DirectoryHandlerResult(skip=True) def handle_file( @@ -142,7 +142,7 @@ class FuzzyFileFinder: m = re.match(ignore_pattern, path_rel) if m is not None: # Keep this commented out for performance except when debugging - # Log.debug("ignoring file", [("ignore_pattern", ignore_pattern), ("file", path_rel)]) + # Log.debug("ignoring file", {"ignore_pattern": ignore_pattern, "file": path_rel}) return on_file(file.get_relative_path()) @@ -164,7 +164,7 @@ class Cache: self._should_tidy = tidy self._cache_id = Cache._get_cache_id(directory) - Log.info("cache id determined", [("cache_id", self._cache_id)]) + Log.info("cache id determined", {"cache_id": self._cache_id}) self._existing_cache_files = self._get_existing_cache_files( self._tmp_directory, self._cache_id @@ -212,7 +212,7 @@ class Cache: def _tidy(self) -> None: for cache_file in self._existing_cache_files: - Log.info("removing old cache file", [("cache_file", cache_file)]) + Log.info("removing old cache file", {"cache_file": cache_file}) try: os.remove(cache_file) except FileNotFoundError: @@ -224,15 +224,15 @@ class Cache: # return None. def _load(self) -> set[str]: if not self._use_existing: - Log.info("skipping cache load", [("reason", "--no-cache flag specified")]) + Log.info("skipping cache load", {"reason": "--no-cache flag specified"}) return set() if len(self._existing_cache_files) == 0: - Log.info("skipping cache load", [("reason", "cache file does not exist")]) + Log.info("skipping cache load", {"reason": "cache file does not exist"}) return set() cache_file = self._existing_cache_files[0] - Log.info("loading cache from file", [("cache_file", cache_file)]) + Log.info("loading cache from file", {"cache_file": cache_file}) cache_file_content = None with open(cache_file, "r") as f: @@ -266,7 +266,7 @@ class Cache: def clean(tmp_dir: str): try: - Log.info("removing existing tmp directory", [("tmp_dir", tmp_dir)]) + Log.info("removing existing tmp directory", {"tmp_dir": tmp_dir}) Util.rmdir(tmp_dir) except FileNotFoundError: pass diff --git a/cli/commands/git_sync.py b/cli/commands/git_sync.py index 5e41734..15933ca 100644 --- a/cli/commands/git_sync.py +++ b/cli/commands/git_sync.py @@ -58,7 +58,7 @@ def find_common_commit(local_commits: list[GitCommit], remote_commits: list[GitC raise Exception("Failed to find a common commit between the local and remote repositories") common_commit = local_commits[local_commit_index] - Log.info("common commit found", [("hash", common_commit.hash), ("message", common_commit.message)]) + Log.info("common commit found", {"hash": common_commit.hash, "message": common_commit.message}) return local_commit_index, remote_commit_index diff --git a/cli/lib/common/alternatives.py b/cli/lib/common/alternatives.py index 6da04e9..9e9c575 100644 --- a/cli/lib/common/alternatives.py +++ b/cli/lib/common/alternatives.py @@ -26,7 +26,7 @@ def install( @staticmethod def set(name: str, path: str, sudo: bool, dry_run: bool): - Log.info("setting alternative", [("name", name), ("path", path)]) + Log.info("setting alternative", {"name": name, "path": path}) if dry_run: Log.info("skip setting alternative due to --dry-run") return diff --git a/cli/lib/common/apt.py b/cli/lib/common/apt.py index a8bbc70..0deed55 100644 --- a/cli/lib/common/apt.py +++ b/cli/lib/common/apt.py @@ -117,7 +117,7 @@ def upgrade(dry_run: bool) -> None: @staticmethod def install(packages: List[str], dry_run: bool) -> None: - Log.info("installing apt packages", [("packages", sorted(packages))]) + Log.info("installing apt packages", {"packages": sorted(packages)}) if dry_run: Log.info("skipping apt install due to --dry-run") return @@ -133,7 +133,7 @@ def install_deb_files(deb_files: List[str], dry_run: bool) -> None: [("packages", "[ " + ", ".join(deb_files) + " ]")], ) if dry_run: - Log.info("skipping install", [("reason", "dry run")]) + Log.info("skipping install", {"reason": "dry run"}) else: if subprocess.call(cmd) != 0: raise Exception("Failed to install packages") diff --git a/cli/lib/common/archive.py b/cli/lib/common/archive.py index 58d49fb..78a82eb 100644 --- a/cli/lib/common/archive.py +++ b/cli/lib/common/archive.py @@ -59,14 +59,14 @@ def _extract_tar( def _unzip( path: str, compression_type: CompressionType, dst_dir: str, dry_run: bool ) -> None: - Log.info("unzipping archive", [("path", path), ("dst_dir", dst_dir)]) + Log.info("unzipping archive", {"path": path, "dst_dir": dst_dir}) cmd = ["unzip", "-o", path, "-d", dst_dir] # Compression type is currently ignored for zip files if dry_run: - Log.info("skipping zip file extraction", [("reason", "dry run")]) + Log.info("skipping zip file extraction", {"reason": "dry run"}) else: sh(cmd) diff --git a/cli/lib/common/git.py b/cli/lib/common/git.py index af90ce8..3821eb4 100644 --- a/cli/lib/common/git.py +++ b/cli/lib/common/git.py @@ -134,10 +134,10 @@ def __init__(self, url, path): self._path = path def checkout(self, target: str, dry_run: bool) -> None: - Log.debug("checking out git target", [("target", target)]) + Log.debug("checking out git target", {"target": target}) if dry_run: - Log.debug("skipping git checkout", [("reason", "dry run")]) + Log.debug("skipping git checkout", {"reason": "dry run"}) return cmd = [ @@ -155,9 +155,9 @@ def checkout(self, target: str, dry_run: bool) -> None: class Git: @staticmethod def clone(url: str, path: str, dry_run: bool) -> GitRepository: - Log.debug("cloning git repository", [("url", url, "path", path)]) + Log.debug("cloning git repository", {"url": url, "path": path}) if dry_run: - Log.debug("skipping git clone", [("reason", "dry run")]) + Log.debug("skipping git clone", {"reason": "dry run"}) else: if subprocess.call(["git", "clone", url, path]) != 0: raise Exception("Failed to clone git repository") @@ -240,13 +240,13 @@ def commit(message: str) -> None: @staticmethod def push(remote: str, branch: str) -> None: - Log.debug("pushing to remote", [("remote", remote), ("branch", branch)]) + Log.debug("pushing to remote", {"remote": remote, "branch": branch}) cmd = ["git", "push", remote, branch] subprocess.check_call(cmd) @staticmethod def pull(remote: str, branch: str, rebase: bool = False) -> None: - Log.debug("pulling from remote", [("remote", remote), ("branch", branch)]) + Log.debug("pulling from remote", {"remote": remote, "branch": branch}) cmd = ["git", "pull"] if rebase: cmd.append("--rebase") @@ -255,15 +255,15 @@ def pull(remote: str, branch: str, rebase: bool = False) -> None: @staticmethod def create_branch(name: str) -> None: - Log.debug("creating a new branch", [("name", name)]) + Log.debug("creating a new branch", {"name": name}) subprocess.check_call(["git", "branch", name]) @staticmethod def checkout(target: str) -> None: - Log.debug("checking out target (branch/hash/tag)", [("target", target)]) + Log.debug("checking out target (branch/hash/tag)", {"target": target}) subprocess.check_call(["git", "checkout", target]) @staticmethod def cherry_pick(hash: str) -> None: - Log.debug("cherry-picking commit", [("hash", hash)]) + Log.debug("cherry-picking commit", {"hash": hash}) subprocess.check_call(["git", "cherry-pick", hash]) diff --git a/cli/lib/common/group.py b/cli/lib/common/group.py index 2ba26a4..8c83562 100644 --- a/cli/lib/common/group.py +++ b/cli/lib/common/group.py @@ -8,14 +8,14 @@ class Group: @staticmethod def add_user(group: str, user: str, dry_run: bool) -> None: - Log.info("adding user to group", [("user", user), ("group", group)]) + Log.info("adding user to group", {"user": user, "group": group}) if Group._is_user_in_group(group, user): - Log.info("skipping adding user", [("reason", "already a member")]) + Log.info("skipping adding user", {"reason": "already a member"}) return if dry_run: - Log.info("skipping adding user", [("reason", "dry run")]) + Log.info("skipping adding user", {"reason": "dry run"}) else: cmd = ["sudo", "usermod", "-aG", group, user] if subprocess.call(cmd) != 0: diff --git a/cli/lib/common/links.py b/cli/lib/common/links.py index 5bb2a37..ac937cc 100644 --- a/cli/lib/common/links.py +++ b/cli/lib/common/links.py @@ -65,7 +65,7 @@ def create(self) -> None: ) os.remove(self.dst) - Log.info(f"Creating symlink", [("source", self.src), ("target", self.dst)]) + Log.info(f"Creating symlink", {"source": self.src, "target": self.dst}) os.symlink(self.src, self.dst) def delete(self) -> None: diff --git a/cli/lib/common/linter.py b/cli/lib/common/linter.py index 982c69a..daf681f 100644 --- a/cli/lib/common/linter.py +++ b/cli/lib/common/linter.py @@ -93,7 +93,7 @@ def is_python_file(file_name: str) -> bool: @staticmethod def _remove_unused_imports(file: str, dry_run: bool) -> None: - Log.info("removing unused imports", [("file", file)]) + Log.info("removing unused imports", {"file": file}) if not dry_run: sh( ["autoflake", "--in-place", "--remove-all-unused-imports", file], @@ -102,13 +102,13 @@ def _remove_unused_imports(file: str, dry_run: bool) -> None: @staticmethod def _sort_imports(file: str, dry_run: bool) -> None: - Log.info("sorting imports", [("file", file)]) + Log.info("sorting imports", {"file": file}) if not dry_run: sh(["isort", file], check=True) @staticmethod def _format_file(file: str, dry_run: bool) -> None: - Log.info("formatting file", [("file", file)]) + Log.info("formatting file", {"file": file}) if not dry_run: sh(["black", file], check=True) diff --git a/cli/lib/common/pip.py b/cli/lib/common/pip.py index 7f38181..0ba62db 100644 --- a/cli/lib/common/pip.py +++ b/cli/lib/common/pip.py @@ -14,7 +14,7 @@ def install( sudo: bool = False, dry_run: bool = True, ) -> None: - Log.info("installing pip packages", [("packages", packages)]) + Log.info("installing pip packages", {"packages": packages}) if dry_run: Log.info("skipping pip install due to --dry-run") return diff --git a/cli/lib/common/shell.py b/cli/lib/common/shell.py index 8827113..48af7a3 100644 --- a/cli/lib/common/shell.py +++ b/cli/lib/common/shell.py @@ -12,10 +12,10 @@ class Shell: @staticmethod def mkdir(path: str, exist_ok: bool, sudo: bool, dry_run: bool) -> None: - Log.info("creating directory", [("path", path)]) + Log.info("creating directory", {"path": path}) if dry_run: - Log.info("skipping directory creation", [("reason", "dry run")]) + Log.info("skipping directory creation", {"reason": "dry run"}) return cmd = [] @@ -30,7 +30,7 @@ def mkdir(path: str, exist_ok: bool, sudo: bool, dry_run: bool) -> None: @staticmethod def rm(path: str, recursive: bool, force: bool, sudo: bool, dry_run: bool) -> None: - Log.info("removing file or directory", [("path", path)]) + Log.info("removing file or directory", {"path": path}) if dry_run: Log.info("skipping removal due to --dry-run") return @@ -47,7 +47,7 @@ def rm(path: str, recursive: bool, force: bool, sudo: bool, dry_run: bool) -> No @staticmethod def mv(src: str, dst: str, sudo: bool, dry_run: bool) -> None: - Log.info("moving file or directory", [("from", src), ("to", dst)]) + Log.info("moving file or directory", {"from": src, "to": dst}) if dry_run: Log.info("skipping directory move due to --dry-run") @@ -60,7 +60,7 @@ def mv(src: str, dst: str, sudo: bool, dry_run: bool) -> None: @staticmethod def ln(source: str, target: str, sudo: bool, dry_run: bool) -> None: - Log.info("creating symbolic link", [("source", source), ("target", target)]) + Log.info("creating symbolic link", {"source": source, "target": target}) if dry_run: Log.info("skipping symbolic link creation due to --dry-run") @@ -73,7 +73,7 @@ def ln(source: str, target: str, sudo: bool, dry_run: bool) -> None: @staticmethod def chmod(mod: str, file: str, sudo: bool, dry_run: bool) -> None: - Log.info("changing file permissions", [("file", file), ("permissions", mod)]) + Log.info("changing file permissions", {"file": file, "permissions": mod}) if dry_run: Log.info("skipping file permission update creation due to --dry-run") @@ -86,13 +86,13 @@ def chmod(mod: str, file: str, sudo: bool, dry_run: bool) -> None: @staticmethod def cd(path: str, dry_run: bool) -> None: - Log.info("changing directory", [("path", path)]) + Log.info("changing directory", {"path": path}) if dry_run: - Log.info("skipping directory change", [("reason", "dry run")]) + Log.info("skipping directory change", {"reason": "dry run"}) return os.chdir(path) @staticmethod def _exec(cmd: list[str]) -> int: - Log.debug("executing shell command", [("command", " ".join(cmd))]) + Log.debug("executing shell command", {"command": " ".join(cmd)}) return subprocess.call(cmd) diff --git a/cli/lib/common/util.py b/cli/lib/common/util.py index 758483b..bdccc86 100644 --- a/cli/lib/common/util.py +++ b/cli/lib/common/util.py @@ -20,7 +20,7 @@ def sh(cmd: list[str], check: bool = False) -> int: def mkdir_p(path: str, dry_run: bool) -> None: - Log.info("creating directory:", [("path", path)]) + Log.info("creating directory:", {"path": path}) if dry_run: Log.info("skipping directory creation due to --dry-run") @@ -29,7 +29,7 @@ def mkdir_p(path: str, dry_run: bool) -> None: def sudo_mkdir_p(path: str, dry_run: bool) -> None: - Log.info("creating directory:", [("path", path)]) + Log.info("creating directory:", {"path": path}) if dry_run: Log.info("skipping directory creation due to --dry-run") @@ -39,7 +39,7 @@ def sudo_mkdir_p(path: str, dry_run: bool) -> None: def sudo_rmdir(path: str, dry_run: bool) -> None: - Log.info("deleting directory:", [("path", path)]) + Log.info("deleting directory:", {"path": path}) if dry_run: Log.info("skipping directory deletion due to --dry-run") @@ -49,7 +49,7 @@ def sudo_rmdir(path: str, dry_run: bool) -> None: def sudo_mvdir(src: str, dst: str, dry_run: bool) -> None: - Log.info("moving directory:", [("from", src), ("to", dst)]) + Log.info("moving directory:", {"from": src, "to": dst}) if dry_run: Log.info("skipping directory move due to --dry-run") @@ -59,7 +59,7 @@ def sudo_mvdir(src: str, dst: str, dry_run: bool) -> None: def download_file(url: str, path: str, sudo: bool, force: bool, dry_run: bool) -> None: - Log.info("downloading file", [("url", url), ("path", path)]) + Log.info("downloading file", {"url": url, "path": path}) if os.path.isfile(path) and not force: Log.info( @@ -71,7 +71,7 @@ def download_file(url: str, path: str, sudo: bool, force: bool, dry_run: bool) - Shell.mkdir(os.path.dirname(path), True, sudo, dry_run) if dry_run: - Log.info("skipping download", [("path", path), ("reason", "dry run")]) + Log.info("skipping download", {"path": path, "reason": "dry run"}) else: urllib.request.urlretrieve(url, path) @@ -84,10 +84,10 @@ def write_file(path: str, content: str, sudo: bool, dry_run: bool) -> None: dry_run=dry_run, ) - Log.info("creating file", [("path", path), ("sudo", sudo)]) + Log.info("creating file", {"path": path, "sudo": sudo}) if dry_run: - Log.info("skipping file creation", [("reason", "dry run")]) + Log.info("skipping file creation", {"reason": "dry run"}) return # TODO: Handle sudo diff --git a/cli/lib/provision/provisioner_dot.py b/cli/lib/provision/provisioner_dot.py index e84d885..ed5d385 100644 --- a/cli/lib/provision/provisioner_dot.py +++ b/cli/lib/provision/provisioner_dot.py @@ -30,7 +30,7 @@ def _generate_dot_cli_completion_script(self) -> str: "dot", ] - Log.info("generating dot cli completion script", [("command", " ".join(cmd))]) + Log.info("generating dot cli completion script", {"command": " ".join(cmd)}) if self._args.dry_run: Log.info( diff --git a/cli/lib/provision/provisioner_flavours.py b/cli/lib/provision/provisioner_flavours.py index 34938ba..6fd09da 100644 --- a/cli/lib/provision/provisioner_flavours.py +++ b/cli/lib/provision/provisioner_flavours.py @@ -53,7 +53,7 @@ def provision(self) -> None: Log.info("deleting flavours release archive") Shell.rm(archive_path, False, False, False, self._args.dry_run) - Log.info("creating base install directory", [("path", base_install_dir)]) + Log.info("creating base install directory", {"path": base_install_dir}) Shell.mkdir(base_install_dir, True, True, self._args.dry_run) Log.info("deleting existing install directory if there is one") @@ -77,7 +77,7 @@ def provision(self) -> None: def _download_release_archive(self, version: str, path: str) -> None: if os.path.isfile(path): - Log.info("skipping download because file already exists", [("path", path)]) + Log.info("skipping download because file already exists", {"path": path}) return # Make sure the directory we are downloading to exists diff --git a/cli/lib/provision/provisioner_i3.py b/cli/lib/provision/provisioner_i3.py index 797e5df..dc8029a 100644 --- a/cli/lib/provision/provisioner_i3.py +++ b/cli/lib/provision/provisioner_i3.py @@ -26,18 +26,18 @@ def _i3_prepare_install_dir(install_dir: str, create: bool, dry_run: bool) -> No Shell.rm(install_dir, True, True, True, dry_run) if create: - Log.info("creating install directory", [("path", install_dir)]) + Log.info("creating install directory", {"path": install_dir}) Shell.mkdir(install_dir, True, True, dry_run) else: base_install_dir = os.path.dirname(install_dir) - Log.info("creating base install directory", [("path", base_install_dir)]) + Log.info("creating base install directory", {"path": base_install_dir}) Shell.mkdir(base_install_dir, True, True, dry_run) def _i3_bootstrap(dry_run: bool): Log.info("bootstrapping i3") if dry_run: - Log.info("skipping i3 bootstrap", [("reason", "dry run")]) + Log.info("skipping i3 bootstrap", {"reason": "dry run"}) return if subprocess.call(["meson", ".."]) != 0: raise Exception("meson returned non-zero exit code") @@ -46,7 +46,7 @@ def _i3_bootstrap(dry_run: bool): def _i3_build(dry_run: bool): Log.info("building i3") if dry_run: - Log.info("skipping i3 build", [("reason", "dry run")]) + Log.info("skipping i3 build", {"reason": "dry run"}) return if subprocess.call(["ninja"]) != 0: raise Exception("ninja returned non-zero exit code") @@ -59,7 +59,7 @@ def __init__(self, args: ProvisionerArgs) -> None: def provision(self) -> None: if not self._args.tags.has(Tags.x11): - Log.info("skipping i3 provisioner", [("reason", "x11 tag not present")]) + Log.info("skipping i3 provisioner", {"reason": "x11 tag not present"}) return latest_tag_name, latest_tag_version = I3Provisioner._get_latest_tag() diff --git a/cli/lib/provision/provisioner_kitty.py b/cli/lib/provision/provisioner_kitty.py index aabf918..3578eb9 100644 --- a/cli/lib/provision/provisioner_kitty.py +++ b/cli/lib/provision/provisioner_kitty.py @@ -64,7 +64,7 @@ def provision(self) -> None: Log.info("deleting kitty release archive") Shell.rm(archive_path, False, False, False, self._args.dry_run) - Log.info("creating base install directory", [("path", base_install_dir)]) + Log.info("creating base install directory", {"path": base_install_dir}) Shell.mkdir(base_install_dir, True, True, self._args.dry_run) Log.info("deleting existing install directory if there is one") diff --git a/cli/lib/provision/provisioner_neovim.py b/cli/lib/provision/provisioner_neovim.py index c1ea300..d1849ba 100644 --- a/cli/lib/provision/provisioner_neovim.py +++ b/cli/lib/provision/provisioner_neovim.py @@ -53,7 +53,7 @@ def provision(self) -> None: Log.info("deleting existing install directory if there is one") Shell.rm(install_dir, True, True, True, self._args.dry_run) - Log.info("creating install directory", [("path", install_dir)]) + Log.info("creating install directory", {"path": install_dir}) Shell.mkdir(install_dir, True, True, self._args.dry_run) Log.info("moving appimage to install location") @@ -87,7 +87,7 @@ def provision(self) -> None: def _download_release_appimage(self, version: str, path: str) -> None: if os.path.isfile(path): - Log.info("skipping download because file already exists", [("path", path)]) + Log.info("skipping download because file already exists", {"path": path}) return # Make sure the directory we are downloading to exists diff --git a/cli/lib/provision/provisioner_treesitter.py b/cli/lib/provision/provisioner_treesitter.py index c3138b8..eaf83e3 100644 --- a/cli/lib/provision/provisioner_treesitter.py +++ b/cli/lib/provision/provisioner_treesitter.py @@ -23,11 +23,11 @@ def prepare_install_dir(install_dir: str, create: bool, dry_run: bool) -> None: Shell.rm(install_dir, True, True, True, dry_run) if create: - Log.info("creating install directory", [("path", install_dir)]) + Log.info("creating install directory", {"path": install_dir}) Shell.mkdir(install_dir, True, True, dry_run) else: base_install_dir = os.path.dirname(install_dir) - Log.info("creating base install directory", [("path", base_install_dir)]) + Log.info("creating base install directory", {"path": base_install_dir}) Shell.mkdir(base_install_dir, True, True, dry_run) @@ -83,7 +83,7 @@ def provision(self) -> None: def _download_release_zip(self, version: str, path: str) -> None: if os.path.isfile(path): - Log.info("skipping download because file already exists", [("path", path)]) + Log.info("skipping download because file already exists", {"path": path}) return Log.info("downloading tree-sitter release archive") @@ -101,7 +101,7 @@ def _download_release_zip(self, version: str, path: str) -> None: @staticmethod def _unzip_executable(zip_path: str, dry_run: bool) -> None: - Log.info("unzipping zip file", [("path", zip_path)]) + Log.info("unzipping zip file", {"path": zip_path}) if dry_run: Log.info("skipping apt update due to --dry-run") else: diff --git a/cli/lib/provision/provisioner_win32yank.py b/cli/lib/provision/provisioner_win32yank.py index 168a45e..e7e5c8f 100644 --- a/cli/lib/provision/provisioner_win32yank.py +++ b/cli/lib/provision/provisioner_win32yank.py @@ -82,7 +82,7 @@ def _install(self, version: str) -> None: Log.info("extracting win32yank release archive") Archive.extract(self._archive_path, self._staging_dir, self._args.dry_run) - Log.info("creating win32yank install directory", [("path", self._install_dir)]) + Log.info("creating win32yank install directory", {"path": self._install_dir}) Shell.mkdir( path=self._install_dir, exist_ok=True, @@ -122,7 +122,7 @@ def _write_version_file(self, version: str) -> None: ) def _read_version_file(self) -> Union[str, None]: - Log.info("reading version file", [("path", self._version_file_path())]) + Log.info("reading version file", {"path": self._version_file_path()}) if not os.path.isfile(self._version_file_path()): return None with open(self._version_file_path(), "r") as f: diff --git a/cli/lib/provision/symlink.py b/cli/lib/provision/symlink.py index 9ebc59b..3ada8cf 100644 --- a/cli/lib/provision/symlink.py +++ b/cli/lib/provision/symlink.py @@ -7,10 +7,10 @@ class Symlink: @staticmethod def create(source: str, target: str, sudo: bool, dry_run: bool) -> None: - Log.info("creating symlink", [("source", source), ("target", target)]) + Log.info("creating symlink", {"source": source, "target": target}) if dry_run: - Log.info("skipping symlink creation", [("reason", "dry run")]) + Log.info("skipping symlink creation", {"reason": "dry run"}) return if not os.path.isfile(source): diff --git a/cli/lib/provision/system_provisioner.py b/cli/lib/provision/system_provisioner.py index 8d3a3f3..b117e6c 100644 --- a/cli/lib/provision/system_provisioner.py +++ b/cli/lib/provision/system_provisioner.py @@ -73,7 +73,7 @@ def provision(self) -> None: ) for component in component_provisioners: - Log.info("provisioning component", [("component", component)]) + Log.info("provisioning component", {"component": component}) component_provisioners[component].provision() @staticmethod From 7bc89843331163cff462c2614d1d1394da76a2d4 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 2 Oct 2024 22:08:54 -0700 Subject: [PATCH 12/39] [Auto] Syncing local changes with remote --- config/gitconfig | 1 + config/nvim/UltiSnips/tex.snippets | 44 ++++++++++++++++++++++++++++++ config/nvim/lua/dot/globals.lua | 7 ++++- 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 config/nvim/UltiSnips/tex.snippets diff --git a/config/gitconfig b/config/gitconfig index 61692f0..3b1ffd7 100644 --- a/config/gitconfig +++ b/config/gitconfig @@ -8,6 +8,7 @@ ss = status co = checkout cob = checkout -b + cot = checkout --track br = branch -v cont = rebase --continue rv = remote -v diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets new file mode 100644 index 0000000..7c7e75d --- /dev/null +++ b/config/nvim/UltiSnips/tex.snippets @@ -0,0 +1,44 @@ +snippet eq "Inline equation" +\( $1 \) +endsnippet + +snippet fn "Function i.e. f(x)" +$1\xPar{$2} +endsnippet + +snippet equation* "Equation without label" +\begin{equation*} + $1 +\end{equation*} +endsnippet + +snippet equation "Equation with label" +\begin{equation} + \label{eq:$1} + $2 +\end{equation*} + +Using (\ref{eq:$1})... +endsnippet + +snippet ( "Parenthesis" +\xPar{$1} +endsnippet + +snippet gathered "Centered multiline equation" +\begin{gathered} + $1 \\\\ + $2 +\end{gathered} +endsnippet + +snippet figure "Figure" +\begin{figure}[h] + \centering + \includegraphics[width=0.5\textwidth]{$3.png} + \caption{$1} + \label{fig:$2} +\end{figure} + +...as shown in Figure \ref{fig:$2}. +endsnippet diff --git a/config/nvim/lua/dot/globals.lua b/config/nvim/lua/dot/globals.lua index 9c59da5..5d29fca 100644 --- a/config/nvim/lua/dot/globals.lua +++ b/config/nvim/lua/dot/globals.lua @@ -10,6 +10,10 @@ function M._move_to_column(opts) Util.move_to_column(column) end +function M._reload_snippets(commands) + vim.cmd('call UltiSnips#RefreshSnippets()') +end + function M._create_command(name, fn, opts) vim.api.nvim_create_user_command(name, fn, opts) end @@ -33,7 +37,8 @@ function M.init() M._create_commands({ { 'ReloadConfig', Util.reload_config, {} }, { 'CloseTabsToRight', Util.close_tabs_to_right, {} }, - { 'MoveToColumn', M._move_to_column, { nargs = 1 } } + { 'MoveToColumn', M._move_to_column, { nargs = 1 } }, + { 'ReloadSnippets', M._reload_snippets, {} }, }) end From 225c55de3e34bab542f0a6ad9e682392b4b421af Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Thu, 3 Oct 2024 20:38:43 -0700 Subject: [PATCH 13/39] Updates --- config/nvim/UltiSnips/tex.snippets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets index 7c7e75d..48ac831 100644 --- a/config/nvim/UltiSnips/tex.snippets +++ b/config/nvim/UltiSnips/tex.snippets @@ -16,7 +16,7 @@ snippet equation "Equation with label" \begin{equation} \label{eq:$1} $2 -\end{equation*} +\end{equation} Using (\ref{eq:$1})... endsnippet From 6538663389f97ce5c98c0d2638f3d4e01d672a78 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Fri, 4 Oct 2024 23:31:18 -0700 Subject: [PATCH 14/39] Updates --- config/nvim/UltiSnips/tex.snippets | 51 ++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets index 48ac831..3f0a683 100644 --- a/config/nvim/UltiSnips/tex.snippets +++ b/config/nvim/UltiSnips/tex.snippets @@ -1,9 +1,21 @@ +snippet itemize "Itemization (i.e. bulleted list)" +\begin{itemize} + \item $1 +\end{itemize} +endsnippet + +snippet itemize "Enumeration (i.e. numbered list)" +\begin{enumerate} + \item $1 +\end{enumerate} +endsnippet + snippet eq "Inline equation" \( $1 \) endsnippet snippet fn "Function i.e. f(x)" -$1\xPar{$2} +\xFunc{$1}{$2} endsnippet snippet equation* "Equation without label" @@ -18,9 +30,13 @@ snippet equation "Equation with label" $2 \end{equation} -Using (\ref{eq:$1})... +Using \xRefEq{$1}... endsnippet +#snippet frac "Fraction" +#\frac{$1}{$2} +#endsnippet + snippet ( "Parenthesis" \xPar{$1} endsnippet @@ -42,3 +58,34 @@ snippet figure "Figure" ...as shown in Figure \ref{fig:$2}. endsnippet + +snippet graph "Graph figure using Tikz" +\begin{figure}[H] + \centering + \begin{tikzpicture} + \begin{axis}[ + axis lines = middle, + xmin = 0, xmax = 10, + ymin = 0, ymax = 5, + xlabel = $x$, ylabel = $y$, + domain=2:8, samples=100, + grid = none + ] + + % Draw a parabola + \addplot[ + arrows={Stealth[length=3mm,width=3mm]-Stealth[length=3mm,width=3mm]}, + domain=2:8, + samples=100, + smooth, + thick, + black, + ] {(0.2)*(x-5)^2 + 1}; + + \end{axis} + \end{tikzpicture} + \caption{#1} + \label{fig:#2} +\end{figure} +endsnippet + From 3479dc23b1c60c983a14ba26143e58b53d3097f9 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 7 Oct 2024 13:14:25 -0700 Subject: [PATCH 15/39] [Auto] Syncing local changes with remote --- config/nvim/UltiSnips/tex.snippets | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets index 3f0a683..40092ae 100644 --- a/config/nvim/UltiSnips/tex.snippets +++ b/config/nvim/UltiSnips/tex.snippets @@ -26,7 +26,7 @@ endsnippet snippet equation "Equation with label" \begin{equation} - \label{eq:$1} + \xLabelEq{$1} $2 \end{equation} @@ -53,7 +53,7 @@ snippet figure "Figure" \centering \includegraphics[width=0.5\textwidth]{$3.png} \caption{$1} - \label{fig:$2} + \xLabelFig{$2} \end{figure} ...as shown in Figure \ref{fig:$2}. @@ -85,7 +85,6 @@ snippet graph "Graph figure using Tikz" \end{axis} \end{tikzpicture} \caption{#1} - \label{fig:#2} + \xLabelFig{#2} \end{figure} endsnippet - From df3a8793171ac7158a76716c882ad20036f06ba6 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Sun, 13 Oct 2024 16:29:15 -0700 Subject: [PATCH 16/39] [Auto] Syncing local changes with remote --- config/nvim/UltiSnips/tex.snippets | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets index 40092ae..00a9b0e 100644 --- a/config/nvim/UltiSnips/tex.snippets +++ b/config/nvim/UltiSnips/tex.snippets @@ -4,7 +4,7 @@ snippet itemize "Itemization (i.e. bulleted list)" \end{itemize} endsnippet -snippet itemize "Enumeration (i.e. numbered list)" +snippet enumerate "Enumeration (i.e. numbered list)" \begin{enumerate} \item $1 \end{enumerate} @@ -48,6 +48,13 @@ snippet gathered "Centered multiline equation" \end{gathered} endsnippet +snippet cases "Centered multiline equation" +\begin{cases} + 1 & foo \\ + 0 & bar +\end{cases} +endsnippet + snippet figure "Figure" \begin{figure}[h] \centering From 334b0d71998408793f495e81ff7969c22bbe6225 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 14 Oct 2024 21:13:22 -0700 Subject: [PATCH 17/39] Updates --- cli/lib/common/log.py | 18 ++++++++---------- config/nvim/UltiSnips/tex.snippets | 9 ++++++++- todo.md | 11 +++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/cli/lib/common/log.py b/cli/lib/common/log.py index 27b97f1..6dbeba4 100644 --- a/cli/lib/common/log.py +++ b/cli/lib/common/log.py @@ -9,10 +9,8 @@ LogHandler = Union[logging.StreamHandler, logging.FileHandler] LogHandlers = list[LogHandler] -# TODO: Just make this a dictionary instead of a list of KVPs -# LogData = dict[str, Any] +LogData = dict[str, Any] LogDataKvp = tuple[str, Any] -LogData = list[LogDataKvp] # fmt: off _LOG_LEVEL_STRINGS = { @@ -91,27 +89,27 @@ def init( Log._logger = logger @staticmethod - def debug(msg: str, data: LogData = []) -> None: + def debug(msg: str, data: LogData = {}) -> None: Log._log(logging.DEBUG, msg, data) @staticmethod - def info(msg: str, data: LogData = []) -> None: + def info(msg: str, data: LogData = {}) -> None: Log._log(logging.INFO, msg, data) @staticmethod - def warn(msg: str, data: LogData = []) -> None: + def warn(msg: str, data: LogData = {}) -> None: Log._log(logging.WARNING, msg, data) @staticmethod - def error(msg: str, data: LogData = []) -> None: + def error(msg: str, data: LogData = {}) -> None: Log._log(logging.ERROR, msg, data) @staticmethod - def fatal(msg: str, data: LogData = []) -> None: + def fatal(msg: str, data: LogData = {}) -> None: Log._log(logging.FATAL, msg, data) @staticmethod - def _log(level: LogLevel, msg: str, data: LogData = []) -> None: + def _log(level: LogLevel, msg: str, data: LogData = {}) -> None: if Log._logger is None: return Log._logger.log(level, Log._format_msg(msg, data)) @@ -124,7 +122,7 @@ def _format_msg(msg: str, data: LogData) -> str: def _format_data(data: LogData) -> str: if len(data) == 0: return "" - return f" {{ {', '.join([Log._format_kvp(kvp) for kvp in data])} }}" + return f" {{ {', '.join([Log._format_kvp(kvp) for kvp in data.items()])} }}" @staticmethod def _format_kvp(kvp: LogDataKvp) -> str: diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets index 00a9b0e..0e73061 100644 --- a/config/nvim/UltiSnips/tex.snippets +++ b/config/nvim/UltiSnips/tex.snippets @@ -33,6 +33,13 @@ snippet equation "Equation with label" Using \xRefEq{$1}... endsnippet +snippet align* "Aligned multi-line equation without label" +\begin{align*} + x &= 1 \\\\ + y &= 2 +\end{align*} +endsnippet + #snippet frac "Fraction" #\frac{$1}{$2} #endsnippet @@ -50,7 +57,7 @@ endsnippet snippet cases "Centered multiline equation" \begin{cases} - 1 & foo \\ + 1 & foo \\\\ 0 & bar \end{cases} endsnippet diff --git a/todo.md b/todo.md index 6509e81..513f271 100644 --- a/todo.md +++ b/todo.md @@ -4,6 +4,7 @@ Improvements I'd like to make to my dotfiles. ## Table of Contents +- [High Priority](#high-priority) - [Misc](#misc) - [Python CLI](#python-cli) - [Bootstrapper](#bootstrapper) @@ -29,6 +30,16 @@ Improvements I'd like to make to my dotfiles. - [nvim-telescope/telescope.nvim](#nvim-telescope/telescope.nvim) - [neovim/nvim-lspconfig](#neovim/nvim-lspconfig) +## High Priority + +UltiSnips freezes sometimes in Neovim which is really annoying and was marked as won't fix because it's specific to Neovim: + +https://github.com/SirVer/ultisnips/issues/1381 + +We should switch to another snippet plugin, maybe `vim-vsnip` since I see that's what someone else did: + +https://github.com/Sangdol/vimrc/commit/b6c5cf06b761b17d5b39c39a2ae9ad584f48761a + ## Misc - [ ] Clean up Neovim Healthcheck (**Neovim Healtheck** section) From 4275a09f1d41c3448824b007059d56f3ee1ef976 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 18 Nov 2024 07:36:06 -0800 Subject: [PATCH 18/39] Add LaTeX snippets --- config/nvim/UltiSnips/tex.snippets | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets index 0e73061..e7dffe4 100644 --- a/config/nvim/UltiSnips/tex.snippets +++ b/config/nvim/UltiSnips/tex.snippets @@ -40,6 +40,15 @@ snippet align* "Aligned multi-line equation without label" \end{align*} endsnippet +snippet align "Aligned equation with label" +\begin{align} + \xLabelEq{$1} + $2 +\end{align} + +Using \xRefEq{$1}... +endsnippet + #snippet frac "Fraction" #\frac{$1}{$2} #endsnippet @@ -55,7 +64,7 @@ snippet gathered "Centered multiline equation" \end{gathered} endsnippet -snippet cases "Centered multiline equation" +snippet cases "Math Equation with Multiple Cases" \begin{cases} 1 & foo \\\\ 0 & bar From ca4623884b2ce5b5558a8ccbccdc829f703a72df Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 18 Nov 2024 07:43:55 -0800 Subject: [PATCH 19/39] Updates --- bin/fzf_cached_wsl | 14 +++++++---- config/nvim/UltiSnips/tex.snippets | 27 ++++++++++++++++++++- config/nvim/lua/dot/plugins.lua | 39 ++++++++++++++++++++++++++++++ todo.md | 22 +++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/bin/fzf_cached_wsl b/bin/fzf_cached_wsl index 7371ab2..aa9e81b 100755 --- a/bin/fzf_cached_wsl +++ b/bin/fzf_cached_wsl @@ -171,10 +171,10 @@ class Cache: ) Log.info( "scanned existing cache files", - [ - ("count", len(self._existing_cache_files)), - ("files", self._existing_cache_files), - ], + { + "count": len(self._existing_cache_files), + "files": self._existing_cache_files, + }, ) self._cached_files = set() @@ -290,7 +290,11 @@ def main(): Log.init("fzf_cached_wsl", log_level, stdout=False, file=log_file) Log.info( - "fzf_cached_wsl started", [("cache", args.cache), ("directory", args.directory)] + "fzf_cached_wsl started", + { + "cache": args.cache, + "directory": args.directory, + } ) cache = Cache(tmp_dir, args.directory, args.cache, args.tidy) diff --git a/config/nvim/UltiSnips/tex.snippets b/config/nvim/UltiSnips/tex.snippets index e7dffe4..b6a4b4e 100644 --- a/config/nvim/UltiSnips/tex.snippets +++ b/config/nvim/UltiSnips/tex.snippets @@ -18,6 +18,22 @@ snippet fn "Function i.e. f(x)" \xFunc{$1}{$2} endsnippet +snippet par "Wrap math term in parenthesis" +\xPar{$1} +endsnippet + +snippet pip "Wrap math term in pipes" +\xPip{$1} +endsnippet + +snippet brk "Wrap math term in square brackets" +\xBrk{$1} +endsnippet + +snippet brc "Wrap math term in curly braces" +\xBrc{$1} +endsnippet + snippet equation* "Equation without label" \begin{equation*} $1 @@ -49,8 +65,10 @@ snippet align "Aligned equation with label" Using \xRefEq{$1}... endsnippet +# TODO: Collides with the frac snippet in the default LaTeX snippets. Look into +# whether or not there's a way to always prioritize snippets from this file. #snippet frac "Fraction" -#\frac{$1}{$2} +# \frac{$1}{$2} #endsnippet snippet ( "Parenthesis" @@ -64,6 +82,13 @@ snippet gathered "Centered multiline equation" \end{gathered} endsnippet +snippet aligned "Aligned multiline equation" +\begin{aligned} + $1 \\\\ + $2 +\end{aligned} +endsnippet + snippet cases "Math Equation with Multiple Cases" \begin{cases} 1 & foo \\\\ diff --git a/config/nvim/lua/dot/plugins.lua b/config/nvim/lua/dot/plugins.lua index 2d8dc19..0a8e79f 100644 --- a/config/nvim/lua/dot/plugins.lua +++ b/config/nvim/lua/dot/plugins.lua @@ -7,6 +7,39 @@ local Notifications = require('dot.notifications') local Util = require('dot.util') local VimPlug = require('dot.vim_plug') +-- TODO: Copy/pasted from ChatGPT, find the right place for this and read +-- through it +-- Function to list snippets and allow FZF selection +function ShowSnippets() + local filetype = vim.bo.filetype -- Get the current filetype + local snippets = vim.fn["UltiSnips#SnippetsInCurrentScope"](1) -- Get available snippets + + -- If no snippets available, notify the user + if vim.tbl_isempty(snippets) then + print("No snippets available for this filetype.") + return + end + + -- Prepare snippets for FZF display + local fzf_snippets = {} + for trigger, info in pairs(snippets) do + local description = info.description or "" + table.insert(fzf_snippets, trigger .. " - " .. description) + end + + -- Use FZF to select a snippet + vim.fn["fzf#run"]({ + source = fzf_snippets, + sink = function(choice) + if choice then + local snippet_trigger = choice:match("^(%S+)") + vim.cmd("call UltiSnips#ExpandSnippetOrJump()") + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(snippet_trigger, true, false, true), 'm', true) + end + end + }) +end + local M = {} local plugins = { @@ -49,6 +82,11 @@ local plugins = { Map.nnoremap('ft', ':Tags') Map.nnoremap('fo', ':Files') + + -- TODO: Copy/pasted from ChatGPT, clean up to use the Map module + -- like above + -- Map the function to a keybinding (e.g., s) + vim.api.nvim_set_keymap("n", "fs", "lua ShowSnippets()", { noremap = true, silent = true }) end }, @@ -90,6 +128,7 @@ local plugins = { vim.g.UltiSnipsJumpBackwardTrigger = "" Map.nnoremaps('ue', ':UltiSnipsEdit') + end }, diff --git a/todo.md b/todo.md index 513f271..042812b 100644 --- a/todo.md +++ b/todo.md @@ -70,6 +70,28 @@ This is for repositories like my notes where I basically just always want to keep everything in sync and don't use branches. Optionally, accept a parameter for commit message. +#### Progress + +I started on this but it probably isn't bullet-proof yet. What it does: + +- If there are local changes that need to be committed + - Create a temporary branch and resolve/commit the changes in it + - This is a bit complicated and might be bug prone +- Fetch from all remotes +- Detect the most recent matching commit between the local and remote +- Get the number of commits the local repository is missing from remote and + vice versa +- Pull remote commits if there are any missing from local +- If there were local changes, cherry-pick them from the temp branch +- If remote is missing any local commits, push + +One thing I might want to change is to push the temporary branch to remote. I +just encountered an issue where I ran the sync command on my desktop PC and I +think it errored and I forgot to go back and resolve it. Now, working on my +laptop, I'm missing those changes. Had I at least pushed the temp branch, I +could have pulled it down and fixed it on my laptop but since I'm travelling +I'm just out of luck. + ### Implement More Provisioners Add a "proprietary" tag for: From 0f965ccaf68d0d83c5a6a698214ee891fe991d8f Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 18 Nov 2024 11:27:50 -0800 Subject: [PATCH 20/39] Format Python files --- cli/commands/fd/__init__.py | 12 ++++------ cli/commands/git_sync.py | 36 +++++++++++++++++++--------- cli/commands/provision.py | 2 +- cli/lib/common/file_walker.py | 2 +- cli/lib/common/git.py | 4 ++-- cli/lib/common/user.py | 4 ++-- cli/lib/common/util.py | 1 - cli/lib/provision/provisioner_dot.py | 1 - 8 files changed, 35 insertions(+), 27 deletions(-) diff --git a/cli/commands/fd/__init__.py b/cli/commands/fd/__init__.py index 9d420e3..556af65 100644 --- a/cli/commands/fd/__init__.py +++ b/cli/commands/fd/__init__.py @@ -34,13 +34,7 @@ def _path(): @staticmethod def _default() -> dict[str, any]: - return { - "update": { - "git_search_paths": [ - "~/src" - ] - } - } + return {"update": {"git_search_paths": ["~/src"]}} RegistryEntry = dict[str, str] @@ -111,7 +105,9 @@ def cmd_fd_choose(args: argparse.Namespace) -> None: selected_key = None cmd = ["fzf", "--query", args.query] try: - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, text=True) + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, text=True + ) selected_key = p.communicate(input=stdin)[0].strip() except FileNotFoundError as e: raise Exception("FZF not installed or not in PATH") from e diff --git a/cli/commands/git_sync.py b/cli/commands/git_sync.py index 15933ca..fa45ef6 100644 --- a/cli/commands/git_sync.py +++ b/cli/commands/git_sync.py @@ -1,12 +1,12 @@ #!/usr/bin/env python -import random -import string import argparse import json +import random +import string -from lib.common.log import Log from lib.common.git import Git, GitCommit +from lib.common.log import Log from lib.common.typing import StringOrNone # TODO: Initial rough implementation is does and a few main cases are tested @@ -17,26 +17,33 @@ # - Break up main function so this is more readable # - Add a '-m/--message' flag to allow users to specify a commit message + def add_git_sync_parser(subparsers: argparse._SubParsersAction) -> None: parser = subparsers.add_parser( "git-sync", # TODO: Add a more descriptive help message help="Simple sync to a remote git repository", ) - parser.add_argument("-d", "--dry-run", action="store_true", help="Dry run the command") - parser.add_argument("-v", "--verbose", action="store_true", help="Extra verbose output") + parser.add_argument( + "-d", "--dry-run", action="store_true", help="Dry run the command" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Extra verbose output" + ) parser.set_defaults(func=cmd_git_sync) def generate_temporary_branch_name(length: int = 8): characters = string.ascii_lowercase + string.digits - random_string = ''.join(random.choice(characters) for _ in range(length)) + random_string = "".join(random.choice(characters) for _ in range(length)) return f"temp-{random_string}" # Find the most recent matching commit between the local and remote # repositories. Returns the index into both arrays of that commit. -def find_common_commit(local_commits: list[GitCommit], remote_commits: list[GitCommit]) -> tuple[int, int]: +def find_common_commit( + local_commits: list[GitCommit], remote_commits: list[GitCommit] +) -> tuple[int, int]: remote_commits_dict = {} for i in range(len(remote_commits)): remote_commit = remote_commits[i] @@ -55,10 +62,15 @@ def find_common_commit(local_commits: list[GitCommit], remote_commits: list[GitC break if remote_commit_index is None: - raise Exception("Failed to find a common commit between the local and remote repositories") + raise Exception( + "Failed to find a common commit between the local and remote repositories" + ) common_commit = local_commits[local_commit_index] - Log.info("common commit found", {"hash": common_commit.hash, "message": common_commit.message}) + Log.info( + "common commit found", + {"hash": common_commit.hash, "message": common_commit.message}, + ) return local_commit_index, remote_commit_index @@ -140,7 +152,9 @@ def cmd_git_sync(args: argparse.Namespace) -> None: print_commits(remote_commits) # Find the most recent matching commit between the local and remote - local_commit_index, remote_commit_index = find_common_commit(local_commits, remote_commits) + local_commit_index, remote_commit_index = find_common_commit( + local_commits, remote_commits + ) print( local_commit_index, @@ -184,5 +198,5 @@ def cmd_git_sync(args: argparse.Namespace) -> None: def print_commits(commits: list[GitCommit]): - d = { "commits": [c.to_dict() for c in commits] } + d = {"commits": [c.to_dict() for c in commits]} print(json.dumps(d, indent=4)) diff --git a/cli/commands/provision.py b/cli/commands/provision.py index ce53e77..38cd5d7 100644 --- a/cli/commands/provision.py +++ b/cli/commands/provision.py @@ -4,8 +4,8 @@ import os from lib.common.distro_info import DistroInformation -from lib.common.os import OperatingSystem from lib.common.log import Log +from lib.common.os import OperatingSystem from lib.provision.provisioner import ProvisionerArgs from lib.provision.system_provisioner import SystemProvisioner from lib.provision.tag import Tags diff --git a/cli/lib/common/file_walker.py b/cli/lib/common/file_walker.py index 4d31c1b..b46940b 100644 --- a/cli/lib/common/file_walker.py +++ b/cli/lib/common/file_walker.py @@ -71,7 +71,7 @@ def get_directories(self) -> list["FileWalker.Directory"]: def get_nodes(self) -> list["FileWalker.Node"]: # TODO: How do I make this one line without angering mypy? - nodes: list["FileWalker.Node"] + nodes: list["FileWalker.Node"] = [] nodes += self._files nodes += self._directories return nodes diff --git a/cli/lib/common/git.py b/cli/lib/common/git.py index 3821eb4..e073e51 100644 --- a/cli/lib/common/git.py +++ b/cli/lib/common/git.py @@ -1,7 +1,6 @@ #!/usr/bin/env python import json -import re import subprocess from lib.common.log import Log @@ -34,7 +33,8 @@ def is_add_required(self) -> bool: def is_commit_required(self) -> bool: return ( - self.is_add_required() or (len(self.staged_modified) + len(self.staged_added)) > 0 + self.is_add_required() + or (len(self.staged_modified) + len(self.staged_added)) > 0 ) @staticmethod diff --git a/cli/lib/common/user.py b/cli/lib/common/user.py index 7430d81..e45fdf1 100644 --- a/cli/lib/common/user.py +++ b/cli/lib/common/user.py @@ -3,8 +3,8 @@ import os import subprocess -from lib.common.os import OperatingSystem from lib.common.log import Log +from lib.common.os import OperatingSystem # TODO: Created this without realizing group.py already exists, merge the two @@ -57,7 +57,7 @@ def get_groups(self) -> List[str]: ) def _get_groups_linux(self) -> List[str]: - import pwd, grp + import grp # Get the groups that the user is in groups = [g for g in grp.getgrall() if self._name in g.gr_mem] diff --git a/cli/lib/common/util.py b/cli/lib/common/util.py index bdccc86..1ceb1d8 100644 --- a/cli/lib/common/util.py +++ b/cli/lib/common/util.py @@ -4,7 +4,6 @@ import shutil import subprocess import urllib.request -from typing import List from lib.common.log import Log from lib.common.shell import Shell diff --git a/cli/lib/provision/provisioner_dot.py b/cli/lib/provision/provisioner_dot.py index ed5d385..5462ca7 100644 --- a/cli/lib/provision/provisioner_dot.py +++ b/cli/lib/provision/provisioner_dot.py @@ -5,7 +5,6 @@ from lib.common.dir import Dir from lib.common.log import Log -from lib.common.os import OperatingSystem from lib.common.util import write_file from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs From 50ffe9dc77542582ef4961233945dc9959a0fa7c Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 18 Nov 2024 11:32:33 -0800 Subject: [PATCH 21/39] Make invalid regexes easier to identify --- bin/fzf_cached_wsl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bin/fzf_cached_wsl b/bin/fzf_cached_wsl index aa9e81b..c43e9e2 100755 --- a/bin/fzf_cached_wsl +++ b/bin/fzf_cached_wsl @@ -139,11 +139,15 @@ class FuzzyFileFinder: name = file.get_name() for ignore_pattern in file_ignore_patterns: path_rel = file.get_relative_path() - m = re.match(ignore_pattern, path_rel) - if m is not None: - # Keep this commented out for performance except when debugging - # Log.debug("ignoring file", {"ignore_pattern": ignore_pattern, "file": path_rel}) - return + try: + m = re.match(ignore_pattern, path_rel) + if m is not None: + # Keep this commented out for performance except when debugging + # Log.debug("ignoring file", {"ignore_pattern": ignore_pattern, "file": path_rel}) + return + except: + print("ignore_pattern = {}, path_rel = {}".format(ignore_pattern, path_rel)) + raise on_file(file.get_relative_path()) From 4bdcb805f06b85631a35c104fb6e5072403210f6 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Sat, 21 Dec 2024 12:22:52 -0800 Subject: [PATCH 22/39] Improve git pending changes script --- bin/git_pending_changes.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bin/git_pending_changes.py b/bin/git_pending_changes.py index c54ca7d..711c8d3 100755 --- a/bin/git_pending_changes.py +++ b/bin/git_pending_changes.py @@ -35,16 +35,19 @@ def run_command(cmd: list[str]) -> list[str]: class GitRepoStatus: def __init__(self, dir: str) -> None: + self._dir = dir + # TODO: Differentiate between staged and unstaged and add other file # statuses self.modified_files: list[str] = [] self.untracked_files: list[str] = [] + self.deleted_files: list[str] = [] - lines = run_command(["git", "-C", dir, "status", "--short"]) + lines = run_command(["git", "-C", self._dir, "status", "--short"]) for line in lines: - file_status, file_path = line.split(" ") + file_status, file_path = line.split(" ", 1) file_status = file_status.strip().lower() file_path = file_path.strip().lower() @@ -56,9 +59,12 @@ def __init__(self, dir: str) -> None: self.untracked_files.append(file_path) else: raise Exception( - f"Unknown file status; dir = {dir}, file_status = {file_status}" + f"Unknown file status; dir = {self._dir}, file_status = {file_status}" ) + def print_full_status(self) -> None: + subprocess.call(["git", "-C", self._dir, "status"]) + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="TODO") @@ -92,8 +98,9 @@ def handle_git_repository(dir: str) -> None: if not has_uncommitted_changes(status): return - # TODO: Expand on this, like print a nice status summary - print(dir) + print("\n================================================================================") + print("Repository: " + dir + "\n") + status.print_full_status() def handle_dir( From 44ca2246ba385f7a9b9e6f4ac213c72f9cf855d9 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 15 Jan 2025 15:57:11 -0800 Subject: [PATCH 23/39] Updates --- config/bash/functions.sh | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/config/bash/functions.sh b/config/bash/functions.sh index 498c5e3..957dc73 100644 --- a/config/bash/functions.sh +++ b/config/bash/functions.sh @@ -288,6 +288,8 @@ function go_test_coverage() { fi } +# Converts a files binary contents to a nice hex representation and opens that +# in Neovim. function viewhex { installed "xxd" "nvim" || return 1 @@ -313,6 +315,50 @@ function viewhex { fi } +# This function reads a file and outputs a condensed text hex representation of +# its binary contents. For example, given the file hello.txt containing just +# the text: +# +# Hello world, I am here and my name is Paul! +# +# The function can be used as follows: +# +# tohex "hello.txt" "hello.hex.txt" +# +# As a result, hello.hex.txt will be created containing the following text: +# +# 48656c6c6f20776f726c642c204920616d206865726520616e64206d79206e616d65206973205061 +# 756c210a +# +# Each line will contain up to 80 characters representing 40 bytes of data. +# This can be used to copy a binary file across an SSH session without SCP. +function tohex() { + installed "hexdump" "od" || return 1 + + local input_file="$1" + local output_file="$2" + + # All of these work. The first two are effectively the same and don't add + # any whitespace between bytes. The third adds a space between each byte in + # the hex output. + #hexdump -ve '1/1 "%.2x"' "$input_file" > "$output_file" + hexdump -ve '40/1 "%.2x" 1 "\n"' "$input_file" > "$output_file" + #od -tx1 -An -v "$input_file" > "$output_file" +} + +# This function simply reverses the operation performed by tohex(). Given the +# text hex representation, it will output the original file. +# +# Example: tohex "hello.hex.txt" "hello.txt" +function fromhex() { + installed "xxd" || return 1 + + local input_file="$1" + local output_file="$2" + + xxd -p -r "$input_file" "$output_file" +} + function git_show_tool { installed "git" || return 1 @@ -479,6 +525,7 @@ function sdr() { } +# TODO: Allow specifying the output path function tar_directory() { local dir path name From c1816ab7e3ec69d3d06a85bf9f606988fa1f866b Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 5 Feb 2025 15:32:22 -0800 Subject: [PATCH 24/39] Fix neovim provisioner --- cli/commands/provision.py | 10 +++++----- cli/lib/common/alternatives.py | 7 ++++++- cli/lib/provision/provisioner_neovim.py | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cli/commands/provision.py b/cli/commands/provision.py index 38cd5d7..68954be 100644 --- a/cli/commands/provision.py +++ b/cli/commands/provision.py @@ -59,11 +59,11 @@ def cmd_provision(args: argparse.Namespace) -> None: distro = DistroInformation.get() Log.info( "provisioning system", - [ - ("distro.id", distro.id), - ("distro.release", distro.release), - ("distro.codename", distro.codename), - ], + { + "distro.id": distro.id, + "distro.release": distro.release, + "distro.codename": distro.codename, + }, ) tags = Tags.parse(args.tags) if isinstance(args.tags, str) else args.tags diff --git a/cli/lib/common/alternatives.py b/cli/lib/common/alternatives.py index 9e9c575..987a9d9 100644 --- a/cli/lib/common/alternatives.py +++ b/cli/lib/common/alternatives.py @@ -12,7 +12,12 @@ def install( ): Log.info( "Adding alternative", - [("link", link), ("name", name), ("path", path), ("priority", priority)], + { + "link": link, + "name": name, + "path": path, + "priority": priority, + }, ) if dry_run: Log.info("skip adding alternative due to --dry-run") diff --git a/cli/lib/provision/provisioner_neovim.py b/cli/lib/provision/provisioner_neovim.py index d1849ba..5d4f07b 100644 --- a/cli/lib/provision/provisioner_neovim.py +++ b/cli/lib/provision/provisioner_neovim.py @@ -40,7 +40,7 @@ def provision(self) -> None: return tmp_dir = f"{Dir.home()}/Downloads/neovim/{latest_version}" - appimage_filename = "nvim.appimage" + appimage_filename = "nvim-linux-x86_64.appimage" appimage_path = os.path.join(tmp_dir, appimage_filename) base_install_dir = "/opt/neovim" From 30fd26448d4213495110a58323779d3409cc986e Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 3 Mar 2025 16:36:54 -0800 Subject: [PATCH 25/39] Updates --- cli/lib/common/apt.py | 4 +++- cli/lib/common/pip.py | 2 +- cli/lib/provision/provisioner_dot.py | 2 +- cli/lib/provision/provisioner_flavours.py | 2 +- cli/lib/provision/provisioner_nodejs.py | 14 ++++++++++---- cli/lib/provision/provisioner_win32yank.py | 2 +- cli/lib/provision/symlink.py | 2 ++ todo.md | 7 +++++++ 8 files changed, 26 insertions(+), 9 deletions(-) diff --git a/cli/lib/common/apt.py b/cli/lib/common/apt.py index 0deed55..58214f0 100644 --- a/cli/lib/common/apt.py +++ b/cli/lib/common/apt.py @@ -130,7 +130,9 @@ def install_deb_files(deb_files: List[str], dry_run: bool) -> None: Log.info( "installing deb packages", - [("packages", "[ " + ", ".join(deb_files) + " ]")], + { + "packages": "[ " + ", ".join(deb_files) + " ]", + } ) if dry_run: Log.info("skipping install", {"reason": "dry run"}) diff --git a/cli/lib/common/pip.py b/cli/lib/common/pip.py index 0ba62db..127eaad 100644 --- a/cli/lib/common/pip.py +++ b/cli/lib/common/pip.py @@ -22,7 +22,7 @@ def install( cmd = [] if sudo: - cmd.append("sudo") + cmd += ["sudo", "-i"] cmd += ["python", "-m", "pip", "install"] diff --git a/cli/lib/provision/provisioner_dot.py b/cli/lib/provision/provisioner_dot.py index 5462ca7..2ba9e63 100644 --- a/cli/lib/provision/provisioner_dot.py +++ b/cli/lib/provision/provisioner_dot.py @@ -33,7 +33,7 @@ def _generate_dot_cli_completion_script(self) -> str: if self._args.dry_run: Log.info( - "skipping dot cli completion script generation", [("reason", "dry run")] + "skipping dot cli completion script generation", {"reason": "dry run"} ) return "" diff --git a/cli/lib/provision/provisioner_flavours.py b/cli/lib/provision/provisioner_flavours.py index 6fd09da..d649bfe 100644 --- a/cli/lib/provision/provisioner_flavours.py +++ b/cli/lib/provision/provisioner_flavours.py @@ -115,7 +115,7 @@ def _flavours_update(self) -> None: if p.wait() != 0: Log.warn( "Flavours update returned non-zero exit code", - [("exit_code", exit_code)], + {"exit_code": exit_code}, ) @staticmethod diff --git a/cli/lib/provision/provisioner_nodejs.py b/cli/lib/provision/provisioner_nodejs.py index 0c14f47..08ac1a3 100644 --- a/cli/lib/provision/provisioner_nodejs.py +++ b/cli/lib/provision/provisioner_nodejs.py @@ -28,14 +28,20 @@ def provision(self) -> None: # Get the currently installed nodejs version current_nodejs_version = NodeJSProvisioner._get_current_version() Log.info( - "identified current nodejs version", [("version", current_nodejs_version)] + "identified current nodejs version", + { + "version": current_nodejs_version, + } ) # Get latest nodejs version and convert to semver (I.E. "v22.2.0") latest_nodejs_release = Github.get_latest_release(org, repo) latest_nodejs_version = Semver.parse(latest_nodejs_release) Log.info( - "identified latest nodejs version", [("version", latest_nodejs_version)] + "identified latest nodejs version", + { + "version": latest_nodejs_version, + } ) # TODO: Make a utility function for this logic? @@ -69,7 +75,7 @@ def _install(self, version: str) -> None: # Extract node tarball Log.info( "extracting nodejs release archive", - [("archive", nodejs_archive_path), ("dst", staging_dir)], + { "archive": nodejs_archive_path, "dst": staging_dir }, ) Archive.extract(nodejs_archive_path, staging_dir, self._args.dry_run) @@ -85,7 +91,7 @@ def _install(self, version: str) -> None: nodejs_executables = ["corepack", "node", "npm", "npx"] Log.info( - "creating nodejs executable symlinks", [("executables", nodejs_executables)] + "creating nodejs executable symlinks", { "executables": nodejs_executables } ) # TODO: We should make a Symlink.create() method that handles deleting existing links and whatnot diff --git a/cli/lib/provision/provisioner_win32yank.py b/cli/lib/provision/provisioner_win32yank.py index e7e5c8f..795080a 100644 --- a/cli/lib/provision/provisioner_win32yank.py +++ b/cli/lib/provision/provisioner_win32yank.py @@ -24,7 +24,7 @@ def __init__(self, args: ProvisionerArgs) -> None: def provision(self) -> None: if not self._args.tags.has(Tags.wsl): Log.info( - "skipping win32yank provisioner", [("reason", "wsl tag not present")] + "skipping win32yank provisioner", {"reason": "wsl tag not present"} ) return diff --git a/cli/lib/provision/symlink.py b/cli/lib/provision/symlink.py index 3ada8cf..cf36e17 100644 --- a/cli/lib/provision/symlink.py +++ b/cli/lib/provision/symlink.py @@ -1,5 +1,7 @@ #!/usr/bin/env python +import os + from lib.common.log import Log from lib.common.shell import Shell diff --git a/todo.md b/todo.md index 042812b..e8659b2 100644 --- a/todo.md +++ b/todo.md @@ -92,6 +92,13 @@ laptop, I'm missing those changes. Had I at least pushed the temp branch, I could have pulled it down and fixed it on my laptop but since I'm travelling I'm just out of luck. +### Provisioner Output + +Better display which provisioners passed, failed, or didn't run because right +now if something fails half-way through it's very annoying to figure out where +to start again. GitHub's rate limiting seems pretty aggressive so re-running +the whole thing fails due to 403 errors. + ### Implement More Provisioners Add a "proprietary" tag for: From 0beb2e78fa2db8cde53f0aceb0ccd3f42965b56a Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Sun, 16 Mar 2025 12:35:25 -0700 Subject: [PATCH 26/39] Add nautilus path note to TODO --- todo.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/todo.md b/todo.md index e8659b2..983d967 100644 --- a/todo.md +++ b/todo.md @@ -43,6 +43,9 @@ https://github.com/Sangdol/vimrc/commit/b6c5cf06b761b17d5b39c39a2ae9ad584f48761a ## Misc - [ ] Clean up Neovim Healthcheck (**Neovim Healtheck** section) +- [ ] Change path address bar behavior in Nautilus? + - `dconf write /org/gnome/nautilus/preferences/always-use-location-entry true` + - Not sure if I actually like this, just need to remember the `Ctrl + l` hotkey ## XP Submodule From 4b951008b3d803a558ab6741d1c281a004dcb085 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 26 May 2025 22:46:01 -0700 Subject: [PATCH 27/39] Updates --- README.md | 2 ++ config/bash/aliases.sh | 3 ++- config/vsvimrc | 17 ++++++++++++++--- todo.md | 30 +++++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9dca55e..f312aa8 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ Some remaining items to tackle in regards to theming: ## Windows 10 +**TODO**: This doesn't exist anymore? Fix link... + For setup steps on Windows 10, see: [windows_setup.md](./windows_setup.md) diff --git a/config/bash/aliases.sh b/config/bash/aliases.sh index fd2ac7f..67b6747 100644 --- a/config/bash/aliases.sh +++ b/config/bash/aliases.sh @@ -214,7 +214,8 @@ if _is_installed 'docker'; then fi if _is_installed 'python'; then - set_alias '0' 'serve' 'python -m SimpleHTTPServer' + #set_alias '0' 'serve' 'python -m SimpleHTTPServer' + set_alias '0' 'serve' 'python -m http.server' fi # On Ubuntu, the system-supplied open file dialog can be very slow to open. diff --git a/config/vsvimrc b/config/vsvimrc index 2b0e74a..04335cb 100644 --- a/config/vsvimrc +++ b/config/vsvimrc @@ -82,11 +82,22 @@ nnoremap vs :vsc Edit.GoToSymbol nnoremap vr :vsc Edit.GoToRecentFile nnoremap vt :vsc Edit.GoToType -" Add gvim as an External Tool as described in this guide: +" To use this mapping, add gvim to Visual Studio as an External Tool +" +" The steps below were taken from the following guide: " https://vim.fandom.com/wiki/Integrate_gvim_with_Visual_Studio " -" Make sure it's at index 4 in the list (1-based) or change this to the -" corresponding index +" 1. In the Visual Studio top menu, navigate to `Tools > External Tools...` +" 2. Click `Add` to create a new external tool +" 3. Fill in the fields as follows: +" - Title: `&Vim` +" - Command: `C:\Program Files (x86)\Vim\vim82\gvim.exe` +" - Set this to the actual path to `gvim.exe` +" - Arguments: +" - `--servername gVimStudio --remote-silent +"execute 'normal! $(CurLine)G$(CurCol)|'" "$(ItemPath)" ` +" +" Ensure that the newly added `&Vim` external tool is the 4th item in the list +" or change this to the corresponding index nnoremap vv :vsc Tools.ExternalCommand4 " Map Visual Studio's native jump behavior which are decent equivalents to the diff --git a/todo.md b/todo.md index 983d967..1a542b6 100644 --- a/todo.md +++ b/todo.md @@ -295,7 +295,17 @@ I've started noodling on a hacky PoC for this in `bin/i3-util.sh` Now that we've split our configs let's not link `vi` and `vim` to Neovim. -## Visual Studio Key Bindings +## Visual Studio + +(The TODO here is to put all of this in a proper document) + +### Extensions + +- VsVim +- Struct Layout +- Smart Command Line Arguments VS2022 + +### Visual Studio Key Bindings Long term: Figure out a way to version control these properly @@ -320,6 +330,24 @@ Short term: Document how to set things up the way I like them - `Ctrl+U` Scroll up half a page - `Ctrl+V` Visual selection mode +### Disable CodeLens Annotations + +These are the things above functions that say: + +``` +2 references | Bob Smith, 123 days ago | 1 author, 3 changes +``` + +I find them really annoying because I always think there are extra whitespace lines that need to be removed + +To disable CodeLens: +1. Go to Tools > Options. +2. Expand the Text Editor section. +3. Select All Languages (or C# specifically if you only want to disable it for that). +4. Go to CodeLens. +5. Uncheck Enable CodeLens. +6. Click OK. + ## Neovim Healtheck - In nvim, run `:healthcheck` and go through the errors/warnings: From eb04202d1733c51241493a8a01542111446f7e3e Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Fri, 11 Jul 2025 15:55:15 -0700 Subject: [PATCH 28/39] Fix logging statement in CLI --- cli/lib/provision/provisioner_win32yank.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/lib/provision/provisioner_win32yank.py b/cli/lib/provision/provisioner_win32yank.py index 795080a..9f7e69e 100644 --- a/cli/lib/provision/provisioner_win32yank.py +++ b/cli/lib/provision/provisioner_win32yank.py @@ -112,7 +112,7 @@ def _install(self, version: str) -> None: def _write_version_file(self, version: str) -> None: Log.info( "writing version file", - [("path", self._version_file_path()), ("version", version)], + {"path": self._version_file_path(), "version": version}, ) write_file( path=self._version_file_path(), From 56ff6158f8ab585ab6e9193e5d2d60f8031f3b84 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Fri, 11 Jul 2025 16:42:04 -0700 Subject: [PATCH 29/39] Add override for WSL Wezterm domain name --- config/wezterm.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/config/wezterm.lua b/config/wezterm.lua index 2d23ed9..8839791 100644 --- a/config/wezterm.lua +++ b/config/wezterm.lua @@ -4,11 +4,23 @@ local act = wezterm.action local config = {} +function get_wsl_domain() + -- This should match the entry in the list output by `wsl --list` that + -- should be used as the default domain + local wsl_domain_name = os.getenv("WEZTERM_WSL_DOMAIN") + if not wsl_domain_name or wsl_domain_name == "" then + wsl_domain_name = "WSL:Ubuntu" + else + wsl_domain_name = "WSL:" .. wsl_domain_name + end + return wsl_domain_name +end + function get_shell(tab_info) local shell = '' if tab_info.active_pane.domain_name == "local" then shell = '(Git Bash) ' - elseif tab_info.active_pane.domain_name == "WSL:Ubuntu" then + elseif tab_info.active_pane.domain_name == get_wsl_domain() then shell = '(WSL) ' end return shell @@ -35,7 +47,7 @@ wezterm.on('format-tab-title', function(tab, tabs, panes, config, hover, max_wid end) config.default_prog = {"bash"} -config.default_domain = 'WSL:Ubuntu' +config.default_domain = get_wsl_domain() config.launch_menu = { { From f7ddcc56efc1010e5c86538e26dd2765c26c0292 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 30 Jul 2025 18:11:42 -0700 Subject: [PATCH 30/39] Add merge_pdfs bash function --- config/bash/functions.sh | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/config/bash/functions.sh b/config/bash/functions.sh index 957dc73..a97e293 100644 --- a/config/bash/functions.sh +++ b/config/bash/functions.sh @@ -687,3 +687,21 @@ function _git_bash() "$wezterm_exe" "${wezterm_args[@]}" -- "$bash_exe" "${bash_args[@]}" & } + +function merge_pdfs() +{ + installed "pdftk" || return 1 + + local input1="$1" + local input2="$2" + local output="$3" + + if [ -z "$input1" ] || [ -z "$input2" ] || [ -z "$output" ]; then + echo "Usage: merge_pdfs " + echo "" + echo "Example: merge_pdfs statement.pdf receipt.pdf statement_and_receipt.pdf" + return 1 + fi + + pdftk "$input1" "$input2" cat output "$output" +} From 0291ec72cf6b0f60612550d275dce87055b2f4b5 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Tue, 26 Aug 2025 22:13:04 -0700 Subject: [PATCH 31/39] Update TODO list --- todo.md | 53 ----------------------------------------------------- 1 file changed, 53 deletions(-) diff --git a/todo.md b/todo.md index 1a542b6..6753bc0 100644 --- a/todo.md +++ b/todo.md @@ -295,59 +295,6 @@ I've started noodling on a hacky PoC for this in `bin/i3-util.sh` Now that we've split our configs let's not link `vi` and `vim` to Neovim. -## Visual Studio - -(The TODO here is to put all of this in a proper document) - -### Extensions - -- VsVim -- Struct Layout -- Smart Command Line Arguments VS2022 - -### Visual Studio Key Bindings - -Long term: Figure out a way to version control these properly - -Short term: Document how to set things up the way I like them - -- Install `VsVim` extension -- Reset keyboard shortcuts to default - - Open the `Tools -> Options...` menu - - Navigate to `Environment -> Keyboard` in the left-hand side - - At the top, select `(Default)` from the first drop-down and click the - `Reset` button -- Select `VsVim` keyboard shortcut overrides - - Open the `Tools -> Options...` menu - - Navigate to `VsVim -> Keyboard` in the left-hand side - - For the following keyboard shortcuts, select `VsVim` in the `Handled by` - drop-down - - `Ctrl+]` (Go to definition) - - `Ctrl+D` Scroll down half a page - - `Ctrl+I` (Traverse down jumpstack) - - `Ctrl+O` (Traverse up jumpstack) - - `Ctrl+R` (Redo) - - `Ctrl+U` Scroll up half a page - - `Ctrl+V` Visual selection mode - -### Disable CodeLens Annotations - -These are the things above functions that say: - -``` -2 references | Bob Smith, 123 days ago | 1 author, 3 changes -``` - -I find them really annoying because I always think there are extra whitespace lines that need to be removed - -To disable CodeLens: -1. Go to Tools > Options. -2. Expand the Text Editor section. -3. Select All Languages (or C# specifically if you only want to disable it for that). -4. Go to CodeLens. -5. Uncheck Enable CodeLens. -6. Click OK. - ## Neovim Healtheck - In nvim, run `:healthcheck` and go through the errors/warnings: From dcf9c534c21f8cd181eac16d598f4c3514d728d2 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Thu, 20 Nov 2025 15:32:00 -0800 Subject: [PATCH 32/39] base16 flavours scp workaround --- config/bashrc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/bashrc b/config/bashrc index cc714df..0b3222a 100644 --- a/config/bashrc +++ b/config/bashrc @@ -57,6 +57,15 @@ fi export DOT_BASH_COMPLETION="1" source "$HOME/.bash_completion.d/dot.bash" +# TODO: This is a gross hack but it fixes an issue when using scp. Once I have +# more time to dig into the proper fix we should clean up. +SKIP_BASE16="0" +if [ -n "$SSH_CONNECTION" ] && [ -z "$SSH_TTY" ]; then + SKIP_BASE16="1" +fi + +if [ "$SKIP_BASE16" = "0" ]; then + # Base16 color scheme base16_shell_dir="$HOME/.config/base16-shell" [ ! -d "$base16_shell_dir" ] && \ @@ -78,4 +87,10 @@ BASE16_SHELL="$HOME/.config/base16-shell/" eval "base16_outrun-dark" # End flavours - bashrc + +fi + +# TODO: I think this was automatically appended and I didn't notice and +# accidentally committed it. We should put this somewhere that makes more +# sense. [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion From a09dbc3f4873eb800facf7ad8598dd7268ce54aa Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Thu, 20 Nov 2025 15:43:42 -0800 Subject: [PATCH 33/39] Proper fix for base16 scp issue --- config/bash/base16.sh | 20 ++++++++++++++++++++ config/bashrc | 32 ++++++-------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 config/bash/base16.sh diff --git a/config/bash/base16.sh b/config/bash/base16.sh new file mode 100644 index 0000000..74da5cd --- /dev/null +++ b/config/bash/base16.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# If the shell is non-interactive, don't do anything +[[ $- == *i* ]] || return + +# Base16 color scheme +base16_shell_dir="$HOME/.config/base16-shell" +[ ! -d "$base16_shell_dir" ] && \ + git clone "https://github.com/chriskempson/base16-shell.git" \ + "$base16_shell_dir" + +BASE16_SHELL_SET_BACKGROUND=true +if uname | grep -i 'linux' &>/dev/null; then + BASE16_SHELL_SET_BACKGROUND=false +fi + +BASE16_SHELL="$HOME/.config/base16-shell/" +[ -n "$PS1" ] && \ + [ -s "$BASE16_SHELL/profile_helper.sh" ] && \ + source "$BASE16_SHELL/profile_helper.sh" diff --git a/config/bashrc b/config/bashrc index 0b3222a..f61cc5e 100644 --- a/config/bashrc +++ b/config/bashrc @@ -57,38 +57,18 @@ fi export DOT_BASH_COMPLETION="1" source "$HOME/.bash_completion.d/dot.bash" -# TODO: This is a gross hack but it fixes an issue when using scp. Once I have -# more time to dig into the proper fix we should clean up. -SKIP_BASE16="0" -if [ -n "$SSH_CONNECTION" ] && [ -z "$SSH_TTY" ]; then - SKIP_BASE16="1" -fi - -if [ "$SKIP_BASE16" = "0" ]; then - -# Base16 color scheme -base16_shell_dir="$HOME/.config/base16-shell" -[ ! -d "$base16_shell_dir" ] && \ - git clone "https://github.com/chriskempson/base16-shell.git" \ - "$base16_shell_dir" - -BASE16_SHELL_SET_BACKGROUND=true -if uname | grep -i 'linux' &>/dev/null; then - BASE16_SHELL_SET_BACKGROUND=false -fi -BASE16_SHELL="$HOME/.config/base16-shell/" -[ -n "$PS1" ] && \ - [ -s "$BASE16_SHELL/profile_helper.sh" ] && \ - source "$BASE16_SHELL/profile_helper.sh" +# Initialize base16 color system +. "$DOTFILES/config/bash/base16.sh" +# TODO: Move this into base16.sh. Will need to adjust our flavours configs # Start flavours - bashrc # Base16 Outrun Dark -eval "base16_outrun-dark" -# End flavours - bashrc - +if command -v "base16_outrun-dark" >/dev/null 2>&1; then + eval "base16_outrun-dark" fi +# End flavours - bashrc # TODO: I think this was automatically appended and I didn't notice and # accidentally committed it. We should put this somewhere that makes more From 22d86b4b9285ce966247efb54730fa89a7d4f953 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Sun, 14 Dec 2025 22:37:13 -0800 Subject: [PATCH 34/39] Add jpg_to_png function --- config/bash/functions.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/config/bash/functions.sh b/config/bash/functions.sh index a97e293..07bc06f 100644 --- a/config/bash/functions.sh +++ b/config/bash/functions.sh @@ -401,6 +401,26 @@ function webp_to_jpg() { echo "Successfully converted $webp_file to $jpg_file" } +# Converts an image file from jpg to png format. +function jpg_to_png() { + installed "convert" || return 1 + + local jpg_file="$1" + local png_file="$2" + + if [[ -z "$jpg_file" || -z "$png_file" ]]; then + 1>&2 echo "Usage: jpg_to_png path_to_img.jpg path_to_img.png" + return 1 + fi + + if ! convert "$jpg_file" "$png_file"; then + 1>&2 echo "ERROR: failed to convert jpg file $jpg_file to png format" + return 1 + fi + + echo "Successfully converted $jpg_file to $png_file" +} + function docker_pss() { local tempfile tempfile="$(mktemp)" From 598a04122c127134a7137a7c6140ed6ceff0e284 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 15 Dec 2025 01:16:34 -0800 Subject: [PATCH 35/39] Provisioner script updates --- .gitignore | 1 + cli/commands/provision.py | 27 ++ cli/lib/common/version_cache.py | 289 ++++++++++++++++++++ cli/lib/provision/provisioner_flavours.py | 58 +++- cli/lib/provision/provisioner_i3.py | 47 +++- cli/lib/provision/provisioner_kitty.py | 52 +++- cli/lib/provision/provisioner_neovim.py | 54 +++- cli/lib/provision/provisioner_nodejs.py | 59 ++-- cli/lib/provision/provisioner_ripgrep.py | 41 ++- cli/lib/provision/provisioner_treesitter.py | 88 +++++- cli/lib/provision/provisioner_win32yank.py | 51 +++- config/bash/aliases.sh | 14 +- 12 files changed, 689 insertions(+), 92 deletions(-) create mode 100644 cli/lib/common/version_cache.py diff --git a/.gitignore b/.gitignore index 813bc79..c0563e0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ gitconfig_local *.zwc *.swp *.pyc +version_cache.json5 diff --git a/cli/commands/provision.py b/cli/commands/provision.py index 68954be..eb37a31 100644 --- a/cli/commands/provision.py +++ b/cli/commands/provision.py @@ -2,10 +2,13 @@ import argparse import os +from pathlib import Path +from lib.common.dir import Dir from lib.common.distro_info import DistroInformation from lib.common.log import Log from lib.common.os import OperatingSystem +from lib.common.version_cache import VersionCache from lib.provision.provisioner import ProvisionerArgs from lib.provision.system_provisioner import SystemProvisioner from lib.provision.tag import Tags @@ -43,6 +46,24 @@ def add_provision_parser(subparsers: argparse._SubParsersAction) -> None: default=Tags.default(), help="Comma delimited list of tags that influence provisioner behavior [x11|wsl]", ) + parser.add_argument( + "--no-version-cache", + dest="version_cache", + action="store_false", + default=True, + help="Disable the version cache when checking for latest versions", + ) + parser.add_argument( + "--version-cache-max-age-days", + type=int, + default=7, + metavar="DAYS", + help=( + "Maximum age (in days) for cached version entries. " + "If the cached entry is older than this, the script will attempt " + "to refresh it from the source (default: 7 days)." + ), + ) parser.add_argument( "components", nargs="*", @@ -68,6 +89,12 @@ def cmd_provision(args: argparse.Namespace) -> None: tags = Tags.parse(args.tags) if isinstance(args.tags, str) else args.tags + VersionCache.init( + args.version_cache, + Path(os.path.join(Dir.dot(), "version_cache.json5")), + args.version_cache_max_age_days, + ) + provisioner_args = ProvisionerArgs(args.dry_run, tags) provisioner = SystemProvisioner(provisioner_args, args.components) provisioner.provision() diff --git a/cli/lib/common/version_cache.py b/cli/lib/common/version_cache.py new file mode 100644 index 0000000..54b6090 --- /dev/null +++ b/cli/lib/common/version_cache.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python + +from __future__ import annotations + +import os +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, Optional + +from lib.common.log import Log + +# Prefer a real JSON5 parser if available. We'll fall back to strict JSON. +try: + import json5 as _json_reader # type: ignore +except Exception: + _json_reader = None + +import json as _json_fallback + + +class VersionCache: + """ + A small TTL-based cache for "latest version" lookups. + + File schema (JSON5-compatible): + { + versions: { + foo: { + version: "v1.2.3", + source: "github:owner/repo", + update_time: "2025-12-14T17:42:10Z", + last_attempt: "2025-12-14T17:42:10Z", + last_error: "optional string", + }, + ... + } + } + """ + + CACHE_PATH = Path("./version_cache.json5") + MAX_AGE_DAYS = 7 + ENABLED = True + + @staticmethod + def init( + enabled: bool, + cache_path: Path, + max_age_days: int, + ) -> None: + VersionCache.ENABLED = enabled + VersionCache.CACHE_PATH = cache_path + VersionCache.MAX_AGE_DAYS = max_age_days + + @staticmethod + def get_version( + name: str, + *, + use_stale_on_failed_attempt: bool = True, + failure_cooldown_seconds: Optional[int] = None, + ) -> Optional[Dict[str, Any]]: + """ + Returns the cached entry for `name`, or None if not present / not usable. + + Cache freshness is controlled by VersionCache.MAX_AGE_DAYS: + - MAX_AGE_DAYS > 0 : entry must be newer than that many days + - MAX_AGE_DAYS <= 0: freshness check disabled; always accept cached entry + + Negative caching behavior: + - If the entry is stale (or missing) and there's evidence of a more recent + failed attempt (last_attempt > update_time), then (optionally) return the + cached entry anyway and print a warning, instead of treating it as a miss. + - If failure_cooldown_seconds is set, the "failed attempt" shortcut only + applies when last_attempt is within that cooldown window from now. + """ + if not VersionCache.ENABLED: + return None + + data = VersionCache._load() + versions = data.get("versions", {}) + if not isinstance(versions, dict): + # Corrupt schema; treat as empty. + return None + + entry = versions.get(name) + if not isinstance(entry, dict): + return None + + # If TTL is disabled, return entry immediately + max_age_days = getattr(VersionCache, "MAX_AGE_DAYS", None) + if max_age_days is None or max_age_days <= 0: + return entry + + max_age_seconds = max_age_days * 24 * 60 * 60 + + now = VersionCache._utc_now() + update_dt = VersionCache._parse_iso_utc(entry.get("update_time")) + + if update_dt is not None: + age = (now - update_dt).total_seconds() + if age <= max_age_seconds: + return entry + + # Entry is stale (or missing update_time). Consider negative caching. + if use_stale_on_failed_attempt: + last_attempt_dt = VersionCache._parse_iso_utc(entry.get("last_attempt")) + if last_attempt_dt is not None: + # Failed attempt after last successful update? + failed_after_update = ( + update_dt is None or last_attempt_dt > update_dt + ) + + within_cooldown = True + if failure_cooldown_seconds is not None: + within_cooldown = ( + (now - last_attempt_dt).total_seconds() + <= failure_cooldown_seconds + ) + + if failed_after_update and within_cooldown: + last_err = entry.get("last_error") + msg = ( + f"[VersionCache] Using stale cached version for '{name}' " + f"because a recent fetch attempt failed." + ) + if last_err: + msg += f" Last error: {last_err}" + Log.warn(msg) + return entry + + return None + + @staticmethod + def update_version(name: str, version: str, source: Optional[str] = None) -> Dict[str, Any]: + """ + Upserts a successful version lookup. + - Sets update_time to now (UTC ISO8601, Z). + - Also sets last_attempt to now (since we just attempted). + - Clears last_error. + Returns the updated entry. + """ + data = VersionCache._load() + versions = data.setdefault("versions", {}) + if not isinstance(versions, dict): + data["versions"] = {} + versions = data["versions"] + + now_iso = VersionCache._utc_now_iso() + + entry = versions.get(name) + if not isinstance(entry, dict): + entry = {} + versions[name] = entry + + entry["version"] = version + if source is not None: + entry["source"] = source + entry["update_time"] = now_iso + entry["last_attempt"] = now_iso + entry.pop("last_error", None) + + VersionCache._save(data) + return entry + + @staticmethod + def add_failed_attempt(name: str, error: Optional[str] = None, source: Optional[str] = None) -> Dict[str, Any]: + """ + Records a failed attempt to refresh a version. + - Sets last_attempt to now (UTC ISO8601, Z). + - Stores last_error (if provided). + - Optionally updates source. + Does NOT modify update_time/version. + Returns the updated entry. + """ + data = VersionCache._load() + versions = data.setdefault("versions", {}) + if not isinstance(versions, dict): + data["versions"] = {} + versions = data["versions"] + + now_iso = VersionCache._utc_now_iso() + + entry = versions.get(name) + if not isinstance(entry, dict): + entry = {} + versions[name] = entry + + if source is not None: + entry["source"] = source + entry["last_attempt"] = now_iso + if error is not None: + entry["last_error"] = error + + VersionCache._save(data) + return entry + + # ----------------------------- + # Internal helpers + # ----------------------------- + + @staticmethod + def _utc_now() -> datetime: + return datetime.now(timezone.utc) + + @staticmethod + def _utc_now_iso() -> str: + # Use second precision, Z suffix. + return VersionCache._utc_now().replace(microsecond=0).isoformat().replace("+00:00", "Z") + + @staticmethod + def _parse_iso_utc(value: Any) -> Optional[datetime]: + if not isinstance(value, str) or not value: + return None + try: + s = value.strip() + # Accept "...Z" and "+00:00" + if s.endswith("Z"): + s = s[:-1] + "+00:00" + dt = datetime.fromisoformat(s) + if dt.tzinfo is None: + # Assume UTC if timezone omitted (best-effort) + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc) + except Exception: + return None + + @staticmethod + def _load() -> Dict[str, Any]: + path = VersionCache.CACHE_PATH + if not path.exists(): + return {"versions": {}} + + text = path.read_text(encoding="utf-8") + + # Try JSON5 if available; else fall back to strict JSON. + if _json_reader is not None: + try: + data = _json_reader.loads(text) + if isinstance(data, dict): + data.setdefault("versions", {}) + return data + except Exception: + pass + + # Strict JSON fallback (will fail if you truly use JSON5 features like comments/trailing commas) + try: + data = _json_fallback.loads(text) + if isinstance(data, dict): + data.setdefault("versions", {}) + return data + except Exception: + # If unreadable, don't clobber the file automatically; just act like empty. + Log.warn(f"[VersionCache] failed to parse cache file: {path}") + return {"versions": {}} + + return {"versions": {}} + + @staticmethod + def _save(data: Dict[str, Any]) -> None: + path = VersionCache.CACHE_PATH + path.parent.mkdir(parents=True, exist_ok=True) + + # Write JSON that is also valid JSON5 (quotes, etc.). Pretty for humans. + serialized = _json_fallback.dumps(data, indent=2, sort_keys=True) + "\n" + + # Atomic write: temp file in same directory, then replace. + tmp_fd = None + tmp_path = None + try: + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=str(path.parent), + delete=False, + prefix=path.name + ".tmp.", + ) as f: + tmp_fd = f.fileno() + tmp_path = f.name + f.write(serialized) + f.flush() + os.fsync(tmp_fd) + + os.replace(tmp_path, path) + finally: + if tmp_path and os.path.exists(tmp_path): + try: + os.remove(tmp_path) + except Exception: + pass diff --git a/cli/lib/provision/provisioner_flavours.py b/cli/lib/provision/provisioner_flavours.py index d649bfe..645e04c 100644 --- a/cli/lib/provision/provisioner_flavours.py +++ b/cli/lib/provision/provisioner_flavours.py @@ -2,7 +2,7 @@ import os import subprocess -from typing import Union +from typing import Tuple, Union from lib.common.archive import Archive from lib.common.dir import Dir @@ -10,6 +10,7 @@ from lib.common.log import Log from lib.common.semver import Semver from lib.common.shell import Shell +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs FLAVOURS_GITHUB_ORG = "Misterio77" @@ -21,31 +22,28 @@ def __init__(self, args: ProvisionerArgs) -> None: self._args = args def provision(self) -> None: - latest_version = Github.get_latest_release( - FLAVOURS_GITHUB_ORG, FLAVOURS_GITHUB_REPO - ) - latest_version = Semver.parse(latest_version) + target_release, target_version = FlavoursProvisioner._get_target_version() current_version = FlavoursProvisioner._get_current_version() if current_version is None: Log.info(f"Flavours is not installed") - elif current_version < latest_version: + elif current_version < target_version: Log.info( - f"Flavours {current_version} is installed but {latest_version} is available" + f"Flavours {current_version} is installed but {target_version} is available" ) else: - Log.info(f"Flavours {latest_version} is already installed, nothing to do") + Log.info(f"Flavours {target_version} is already installed, nothing to do") return - tmp_dir = f"{Dir.home()}/Downloads/flavours/{latest_version}" - archive_filename = f"flavours-{latest_version}-x86_64-linux.tar.gz" + tmp_dir = f"{Dir.home()}/Downloads/flavours/{target_release}" + archive_filename = f"flavours-{target_release}-x86_64-linux.tar.gz" archive_path = os.path.join(tmp_dir, archive_filename) base_install_dir = "/opt/flavours" - install_dir = f"/opt/flavours/{latest_version}" + install_dir = f"/opt/flavours/{target_release}" symlink_path = "/usr/local/bin/flavours" - self._download_release_archive(latest_version, archive_path) + self._download_release_archive(target_release, archive_path) Log.info("extracting flavours release archive") Archive.extract(archive_path, tmp_dir, self._args.dry_run) @@ -135,3 +133,39 @@ def _get_current_version() -> Union[str, None]: return Semver.parse(version_str) except FileNotFoundError as e: return None + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + # First check version cache to see if we have a cached version that is + # new enough + cached_version = VersionCache.get_version("flavours") + if cached_version is not None: + Log.info( + "using cached flavours version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_release = Github.get_latest_release( + FLAVOURS_GITHUB_ORG, FLAVOURS_GITHUB_REPO + ) + latest_version = Semver.parse(latest_release) + except Exception as e: + VersionCache.add_failed_attempt( + "flavours", + str(e), + source=f"github:{FLAVOURS_GITHUB_ORG}/{FLAVOURS_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "flavours", + latest_release, + f"github:{FLAVOURS_GITHUB_ORG}/{FLAVOURS_GITHUB_REPO}", + ) + + return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_i3.py b/cli/lib/provision/provisioner_i3.py index dc8029a..ab4efc7 100644 --- a/cli/lib/provision/provisioner_i3.py +++ b/cli/lib/provision/provisioner_i3.py @@ -11,6 +11,7 @@ from lib.common.log import Log from lib.common.semver import Semver from lib.common.shell import Shell +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs from lib.provision.symlink import Symlink from lib.provision.tag import Tags @@ -62,17 +63,16 @@ def provision(self) -> None: Log.info("skipping i3 provisioner", {"reason": "x11 tag not present"}) return - latest_tag_name, latest_tag_version = I3Provisioner._get_latest_tag() + target_tag_name, target_tag_version = I3Provisioner._get_target_version() current_version = I3Provisioner._get_current_version() - print(current_version) if current_version is None: Log.info(f"i3 is not installed") - elif current_version < latest_tag_version: + elif current_version < target_tag_version: Log.info( - f"i3 {current_version} is installed but {latest_tag_version} is available" + f"i3 {current_version} is installed but {target_tag_version} is available" ) else: - Log.info(f"i3 {latest_tag_version} is already installed, nothing to do") + Log.info(f"i3 {target_tag_version} is already installed, nothing to do") return # TODO: i3-gaps was merged into i3 as of release 4.22 but the version @@ -113,20 +113,20 @@ def provision(self) -> None: ) url = f"https://www.github.com/{I3_GITHUB_ORG}/{I3_GITHUB_REPO}" - staging_dir = f"/home/pewing/.tmp/i3/{latest_tag_name}" + staging_dir = f"/home/pewing/.tmp/i3/{target_tag_name}" build_dir = os.path.join(staging_dir, "build") cwd = os.getcwd() Shell.rm(staging_dir, True, True, False, self._args.dry_run) repo = Git.clone(url, staging_dir, self._args.dry_run) - repo.checkout(latest_tag_name, self._args.dry_run) + repo.checkout(target_tag_name, self._args.dry_run) Shell.mkdir(build_dir, True, False, self._args.dry_run) Shell.cd(build_dir, self._args.dry_run) _i3_bootstrap(self._args.dry_run) _i3_build(self._args.dry_run) Shell.cd(cwd, self._args.dry_run) - install_dir = os.path.join("/opt/i3", latest_tag_name) + install_dir = os.path.join("/opt/i3", target_tag_name) _i3_prepare_install_dir(install_dir, False, self._args.dry_run) Shell.mv(staging_dir, install_dir, True, self._args.dry_run) @@ -186,3 +186,34 @@ def _get_current_version() -> Union[str, None]: return Semver.parse(m.group(1)) except FileNotFoundError as e: return None + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + cached_version = VersionCache.get_version("i3") + if cached_version is not None: + Log.info( + "using cached i3 version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_tag_name, latest_tag_version = I3Provisioner._get_latest_tag() + except Exception as e: + VersionCache.add_failed_attempt( + "i3", + str(e), + source=f"github:{I3_GITHUB_ORG}/{I3_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "i3", + latest_tag_name, + f"github:{I3_GITHUB_ORG}/{I3_GITHUB_REPO}", + ) + + return latest_tag_name, latest_tag_version diff --git a/cli/lib/provision/provisioner_kitty.py b/cli/lib/provision/provisioner_kitty.py index 3578eb9..d63a5a4 100644 --- a/cli/lib/provision/provisioner_kitty.py +++ b/cli/lib/provision/provisioner_kitty.py @@ -3,7 +3,7 @@ import os import re import subprocess -from typing import Union +from typing import Tuple, Union from lib.common.archive import Archive from lib.common.dir import Dir @@ -11,6 +11,7 @@ from lib.common.log import Log from lib.common.semver import Semver from lib.common.shell import Shell +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs KITTY_GITHUB_ORG = "kovidgoyal" @@ -22,26 +23,25 @@ def __init__(self, args: ProvisionerArgs) -> None: self._args = args def provision(self) -> None: - latest_release = Github.get_latest_release(KITTY_GITHUB_ORG, KITTY_GITHUB_REPO) - latest_version = Semver.parse(latest_release) + target_release, target_version = KittyProvisioner._get_target_version() current_version = KittyProvisioner._get_current_version() if current_version is None: Log.info(f"Kitty is not installed") - elif current_version < latest_version: + elif current_version < target_version: Log.info( - f"Kitty {current_version} is installed but {latest_version} is available" + f"Kitty {current_version} is installed but {target_version} is available" ) else: - Log.info(f"Kitty {latest_version} is already installed, nothing to do") + Log.info(f"Kitty {target_version} is already installed, nothing to do") return - tmp_dir = f"{Dir.home()}/Downloads/kitty/{latest_version}" - archive_filename = f"kitty-{latest_release.replace('v', '')}-x86_64.txz" + tmp_dir = f"{Dir.home()}/Downloads/kitty/{target_version}" + archive_filename = f"kitty-{target_release.replace('v', '')}-x86_64.txz" archive_path = os.path.join(tmp_dir, archive_filename) base_install_dir = "/opt/kitty" - install_dir = f"/opt/kitty/{latest_version}" + install_dir = f"/opt/kitty/{target_version}" symlink_path_kitty = "/usr/local/bin/kitty" symlink_path_kitten = "/usr/local/bin/kitten" @@ -49,7 +49,7 @@ def provision(self) -> None: Github.download_release_artifact( KITTY_GITHUB_ORG, KITTY_GITHUB_REPO, - latest_release, + target_release, archive_filename, archive_path, True, @@ -109,3 +109,35 @@ def _get_current_version() -> Union[str, None]: return Semver.parse(m.group(1)) except FileNotFoundError as e: return None + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + cached_version = VersionCache.get_version("kitty") + if cached_version is not None: + Log.info( + "using cached kitty version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_release = Github.get_latest_release(KITTY_GITHUB_ORG, KITTY_GITHUB_REPO) + latest_version = Semver.parse(latest_release) + except Exception as e: + VersionCache.add_failed_attempt( + "kitty", + str(e), + source=f"github:{KITTY_GITHUB_ORG}/{KITTY_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "kitty", + latest_release, + f"github:{KITTY_GITHUB_ORG}/{KITTY_GITHUB_REPO}", + ) + + return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_neovim.py b/cli/lib/provision/provisioner_neovim.py index 5d4f07b..d48138b 100644 --- a/cli/lib/provision/provisioner_neovim.py +++ b/cli/lib/provision/provisioner_neovim.py @@ -3,7 +3,7 @@ import os import re import subprocess -from typing import Union +from typing import Tuple, Union from lib.common.alternatives import Alternatives from lib.common.dir import Dir @@ -12,6 +12,7 @@ from lib.common.pip import Pip from lib.common.semver import Semver from lib.common.shell import Shell +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs NEOVIM_GITHUB_ORG = "neovim" @@ -23,32 +24,29 @@ def __init__(self, args: ProvisionerArgs) -> None: self._args = args def provision(self) -> None: - latest_version = Github.get_latest_release( - NEOVIM_GITHUB_ORG, NEOVIM_GITHUB_REPO - ) - latest_version = Semver.parse(latest_version) + target_release, target_version = NeovimProvisioner._get_target_version() current_version = NeovimProvisioner._get_current_version() if current_version is None: Log.info(f"Neovim is not installed") - elif current_version < latest_version: + elif current_version < target_version: Log.info( - f"Neovim {current_version} is installed but {latest_version} is available" + f"Neovim {current_version} is installed but {target_version} is available" ) else: - Log.info(f"Neovim {latest_version} is already installed, nothing to do") + Log.info(f"Neovim {target_version} is already installed, nothing to do") return - tmp_dir = f"{Dir.home()}/Downloads/neovim/{latest_version}" + tmp_dir = f"{Dir.home()}/Downloads/neovim/{target_version}" appimage_filename = "nvim-linux-x86_64.appimage" appimage_path = os.path.join(tmp_dir, appimage_filename) base_install_dir = "/opt/neovim" - install_dir = f"/opt/neovim/{latest_version}" + install_dir = f"/opt/neovim/{target_version}" install_path = os.path.join(install_dir, appimage_filename) symlink_path = "/usr/local/bin/nvim" - self._download_release_appimage(latest_version, appimage_path) + self._download_release_appimage(target_version, appimage_path) Log.info("deleting existing install directory if there is one") Shell.rm(install_dir, True, True, True, self._args.dry_run) @@ -124,3 +122,37 @@ def _get_current_version() -> Union[str, None]: return Semver.parse(m.group(1)) except FileNotFoundError as e: return None + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + cached_version = VersionCache.get_version("neovim") + if cached_version is not None: + Log.info( + "using cached neovim version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_release = Github.get_latest_release( + NEOVIM_GITHUB_ORG, NEOVIM_GITHUB_REPO + ) + latest_version = Semver.parse(latest_release) + except Exception as e: + VersionCache.add_failed_attempt( + "neovim", + str(e), + source=f"github:{NEOVIM_GITHUB_ORG}/{NEOVIM_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "neovim", + latest_release, + f"github:{NEOVIM_GITHUB_ORG}/{NEOVIM_GITHUB_REPO}", + ) + + return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_nodejs.py b/cli/lib/provision/provisioner_nodejs.py index 08ac1a3..8b0cdfd 100644 --- a/cli/lib/provision/provisioner_nodejs.py +++ b/cli/lib/provision/provisioner_nodejs.py @@ -2,6 +2,7 @@ import os import subprocess +from typing import Tuple from lib.common.archive import Archive from lib.common.dir import Dir @@ -11,10 +12,11 @@ from lib.common.shell import Shell from lib.common.typing import StringOrNone from lib.common.util import download_file +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs -KITTY_GITHUB_ORG = "nodejs" -KITTY_GITHUB_REPO = "node" +NODEJS_GITHUB_ORG = "nodejs" +NODEJS_GITHUB_REPO = "node" class NodeJSProvisioner(IComponentProvisioner): @@ -22,9 +24,6 @@ def __init__(self, args: ProvisionerArgs) -> None: self._args = args def provision(self) -> None: - org = "nodejs" - repo = "node" - # Get the currently installed nodejs version current_nodejs_version = NodeJSProvisioner._get_current_version() Log.info( @@ -34,30 +33,22 @@ def provision(self) -> None: } ) - # Get latest nodejs version and convert to semver (I.E. "v22.2.0") - latest_nodejs_release = Github.get_latest_release(org, repo) - latest_nodejs_version = Semver.parse(latest_nodejs_release) - Log.info( - "identified latest nodejs version", - { - "version": latest_nodejs_version, - } - ) + target_release, target_version = NodeJSProvisioner._get_target_version() # TODO: Make a utility function for this logic? if current_nodejs_version is None: Log.info(f"nodejs is not installed") - elif current_nodejs_version < latest_nodejs_version: + elif current_nodejs_version < target_version: Log.info( - f"nodejs {current_nodejs_version} is installed but {latest_nodejs_version} is available" + f"nodejs {current_nodejs_version} is installed but {target_version} is available" ) else: Log.info( - f"nodejs {latest_nodejs_version} is already installed, nothing to do" + f"nodejs {target_version} is already installed, nothing to do" ) return - self._install(latest_nodejs_version) + self._install(target_version) def _install(self, version: str) -> None: staging_dir = Dir.staging("nodejs", str(version)) @@ -118,3 +109,35 @@ def _get_current_version() -> StringOrNone: return Semver.parse(version_str) except FileNotFoundError as e: return None + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + cached_version = VersionCache.get_version("nodejs") + if cached_version is not None: + Log.info( + "using cached nodejs version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_release = Github.get_latest_release(NODEJS_GITHUB_ORG, NODEJS_GITHUB_REPO) + latest_version = Semver.parse(latest_release) + except Exception as e: + VersionCache.add_failed_attempt( + "nodejs", + str(e), + source=f"github:{NODEJS_GITHUB_ORG}/{NODEJS_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "nodejs", + latest_release, + f"github:{NODEJS_GITHUB_ORG}/{NODEJS_GITHUB_REPO}", + ) + + return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_ripgrep.py b/cli/lib/provision/provisioner_ripgrep.py index 1007515..7d033eb 100644 --- a/cli/lib/provision/provisioner_ripgrep.py +++ b/cli/lib/provision/provisioner_ripgrep.py @@ -3,13 +3,14 @@ import os import re import subprocess -from typing import Union +from typing import Tuple, Union from lib.common.apt import Apt from lib.common.dir import Dir from lib.common.github import Github from lib.common.log import Log from lib.common.semver import Semver +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs RIPGREP_GITHUB_ORG = "BurntSushi" @@ -21,9 +22,7 @@ def __init__(self, args: ProvisionerArgs) -> None: self._args = args def provision(self) -> None: - latest_release = Github.get_latest_release( - RIPGREP_GITHUB_ORG, RIPGREP_GITHUB_REPO - ) + latest_release, _ = RipgrepProvisioner._get_target_version() # TODO: Standardize and share this behavior since it's the same in most provisioners # Maybe have like a `get_action` function on provisioners that returns one of three possible actions: @@ -76,3 +75,37 @@ def _get_current_version() -> Union[str, None]: return Semver.parse(m.group(1)) except FileNotFoundError as e: return None + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + cached_version = VersionCache.get_version("ripgrep") + if cached_version is not None: + Log.info( + "using cached ripgrep version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_release = Github.get_latest_release( + RIPGREP_GITHUB_ORG, RIPGREP_GITHUB_REPO + ) + latest_version = Semver.parse(latest_release) + except Exception as e: + VersionCache.add_failed_attempt( + "ripgrep", + str(e), + source=f"github:{RIPGREP_GITHUB_ORG}/{RIPGREP_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "ripgrep", + latest_release, + f"github:{RIPGREP_GITHUB_ORG}/{RIPGREP_GITHUB_REPO}", + ) + + return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_treesitter.py b/cli/lib/provision/provisioner_treesitter.py index eaf83e3..4d34b8e 100644 --- a/cli/lib/provision/provisioner_treesitter.py +++ b/cli/lib/provision/provisioner_treesitter.py @@ -3,13 +3,14 @@ import os import re import subprocess -from typing import Union +from typing import Tuple, Union from lib.common.dir import Dir from lib.common.github import Github from lib.common.log import Log from lib.common.semver import Semver from lib.common.shell import Shell +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs TREE_SITTER_GITHUB_ORG = "tree-sitter" @@ -36,23 +37,23 @@ def __init__(self, args: ProvisionerArgs) -> None: self._args = args def provision(self) -> None: - latest_version = TreeSitterProvisioner._get_latest_release() + _, target_version = TreeSitterProvisioner._get_target_version() current_version = TreeSitterProvisioner._get_current_version() if current_version is None: Log.info(f"tree-sitter is not installed") - elif current_version < latest_version: + elif current_version < target_version: Log.info( - f"tree-sitter {current_version} is installed but {latest_version} is available" + f"tree-sitter {current_version} is installed but {target_version} is available" ) else: Log.info( - f"tree-sitter {latest_version} is already installed, nothing to do" + f"tree-sitter {target_version} is already installed, nothing to do" ) return - staging_dir = Dir.staging("tree-sitter", str(latest_version)) - install_dir = Dir.install("tree-sitter", str(latest_version)) + staging_dir = Dir.staging("tree-sitter", str(target_version)) + install_dir = Dir.install("tree-sitter", str(target_version)) exe_name = "tree-sitter-linux-x64" exe_path_staging = os.path.join(staging_dir, exe_name) @@ -63,7 +64,7 @@ def provision(self) -> None: symlink_path = "/usr/local/bin/tree-sitter" - self._download_release_zip(str(latest_version), zip_path_staging) + self._download_release_zip(str(target_version), zip_path_staging) TreeSitterProvisioner._unzip_executable(zip_path_staging, self._args.dry_run) @@ -109,13 +110,33 @@ def _unzip_executable(zip_path: str, dry_run: bool) -> None: raise Exception("Failed to unzip tree-sitter executable") @staticmethod - def _get_latest_release() -> Semver: - releases = Github.get_releases(TREE_SITTER_GITHUB_ORG, TREE_SITTER_GITHUB_REPO) - tags = [r["tag_name"] for r in releases] - tags = filter(lambda t: "pre-release" not in t, tags) - tags = [Semver.parse(t) for t in tags] - tags = sorted(tags, reverse=True) - return tags[0] + def _get_latest_release() -> Tuple[str, Semver]: + releases = Github.get_releases( + TREE_SITTER_GITHUB_ORG, + TREE_SITTER_GITHUB_REPO, + ) + + # Collect (tag, semver) pairs, skipping pre-releases + parsed: list[Tuple[str, Semver]] = [] + for r in releases: + tag = r.get("tag_name") + if not tag: + continue + if "pre-release" in tag: + continue + + semver = Semver.parse(tag) + if semver is None: + continue + + parsed.append((tag, semver)) + + if not parsed: + raise RuntimeError("No valid semver releases found") + + # Sort by Semver descending + parsed.sort(key=lambda x: x[1], reverse=True) + return parsed[0] @staticmethod def _get_current_version() -> Union[str, None]: @@ -135,3 +156,40 @@ def _get_current_version() -> Union[str, None]: return Semver.parse(m.group(1)) except FileNotFoundError as e: return None + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + # TODO: v0.26+ doesn't work on Ubuntu 22.04 due to glibc version + # issues, so force v0.25 for now. We could do something like `if + # ubuntu_major_version < 24` but I didn't bother + Log.warn("forcing tree-sitter version 0.25.10 due to compatibility issues") + return "v0.25.10", Semver.parse("v0.25.10") + + cached_version = VersionCache.get_version("tree-sitter") + if cached_version is not None: + Log.info( + "using cached tree-sitter version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_release, latest_version = TreeSitterProvisioner._get_latest_release() + except Exception as e: + VersionCache.add_failed_attempt( + "tree-sitter", + str(e), + source=f"github:{TREE_SITTER_GITHUB_ORG}/{TREE_SITTER_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "tree-sitter", + latest_release, + f"github:{TREE_SITTER_GITHUB_ORG}/{TREE_SITTER_GITHUB_REPO}", + ) + + return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_win32yank.py b/cli/lib/provision/provisioner_win32yank.py index 9f7e69e..f11c548 100644 --- a/cli/lib/provision/provisioner_win32yank.py +++ b/cli/lib/provision/provisioner_win32yank.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import os -from typing import Union +from typing import Tuple, Union from lib.common.archive import Archive from lib.common.dir import Dir @@ -10,6 +10,7 @@ from lib.common.semver import Semver from lib.common.shell import Shell from lib.common.util import write_file +from lib.common.version_cache import VersionCache from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs from lib.provision.tag import Tags @@ -36,10 +37,7 @@ def provision(self) -> None: # construct the path to the version file. self._install_dir = f"/mnt/c/bin/" - latest_version = Github.get_latest_release( - WIN32YANK_GITHUB_ORG, WIN32YANK_GITHUB_REPO - ) - latest_version = Semver.parse(latest_version) + _, target_version = Win32YankProvisioner._get_target_version() # TODO: Gross but theres' no way to get the version from the # executable. It doesn't have a `--version` flag and the @@ -50,15 +48,15 @@ def provision(self) -> None: current_version = self._get_current_version() if current_version is None: Log.info(f"Win32Yank is not installed") - elif current_version < latest_version: + elif current_version < target_version: Log.info( - f"Win32Yank {current_version} is installed but {latest_version} is available" + f"Win32Yank {current_version} is installed but {target_version} is available" ) else: - Log.info(f"Win32Yank {latest_version} is already installed, nothing to do") + Log.info(f"Win32Yank {target_version} is already installed, nothing to do") return - self._install(latest_version) + self._install(target_version) def _install(self, version: str) -> None: self._staging_dir = Dir.staging("win32yank", str(version)) @@ -136,3 +134,38 @@ def _get_current_version(self) -> Union[str, None]: if version_str is None: return None return Semver.parse(version_str) + + @staticmethod + def _get_target_version() -> Tuple[str, Semver]: + cached_version = VersionCache.get_version("win32yank") + if cached_version is not None: + Log.info( + "using cached win32yank version", + { + "version": cached_version["version"], + "last_attempt": cached_version.get("last_attempt"), + }, + ) + return cached_version["version"], Semver.parse(cached_version["version"]) + + try: + latest_release = Github.get_latest_release( + WIN32YANK_GITHUB_ORG, WIN32YANK_GITHUB_REPO + ) + latest_version = Semver.parse(latest_release) + except Exception as e: + VersionCache.add_failed_attempt( + "win32yank", + str(e), + source=f"github:{WIN32YANK_GITHUB_ORG}/{WIN32YANK_GITHUB_REPO}", + ) + raise + + VersionCache.update_version( + "win32yank", + latest_release, + f"github:{WIN32YANK_GITHUB_ORG}/{WIN32YANK_GITHUB_REPO}", + ) + + return latest_release, latest_version + diff --git a/config/bash/aliases.sh b/config/bash/aliases.sh index 67b6747..ce5cf74 100644 --- a/config/bash/aliases.sh +++ b/config/bash/aliases.sh @@ -73,11 +73,15 @@ fi # Sorted, human-readable disk usage by depth if _is_installed 'du'; then - set_alias '0' 'du1' 'du -hd1 2>/dev/null | sort -hr' - set_alias '0' 'du2' 'du -hd2 2>/dev/null | sort -hr' - set_alias '0' 'du3' 'du -hd3 2>/dev/null | sort -hr' - set_alias '0' 'du4' 'du -hd4 2>/dev/null | sort -hr' - set_alias '0' 'du5' 'du -hd5 2>/dev/null | sort -hr' + set_alias '0' 'du1' 'du -hd1 2>/dev/null | sort -h' + set_alias '0' 'du2' 'du -hd2 2>/dev/null | sort -h' + set_alias '0' 'du3' 'du -hd3 2>/dev/null | sort -h' + set_alias '0' 'du4' 'du -hd4 2>/dev/null | sort -h' + set_alias '0' 'du5' 'du -hd5 2>/dev/null | sort -h' + set_alias '0' 'du6' 'du -hd6 2>/dev/null | sort -h' + set_alias '0' 'du7' 'du -hd7 2>/dev/null | sort -h' + set_alias '0' 'du8' 'du -hd8 2>/dev/null | sort -h' + set_alias '0' 'du9' 'du -hd9 2>/dev/null | sort -h' fi # Ranger aliases From ca50aaae2e267de3c67a609f5aec684212bef3e5 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Wed, 31 Dec 2025 16:40:09 -0800 Subject: [PATCH 36/39] Env file improvements --- config/env | 57 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/config/env b/config/env index 9781e8b..113a859 100644 --- a/config/env +++ b/config/env @@ -15,30 +15,53 @@ contains() { fi } -add_to_path() { +append_to_path() { p="$1" export PATH="${p}:${PATH}" } -try_add_to_path() { +prepend_to_path() { p="$1" - contains "$PATH" "$p" || add_to_path "$p" + export PATH="${PATH}:${p}" } -try_add_to_path "/usr/local/bin" -try_add_to_path "/usr/local/sbin" -try_add_to_path "$HOME/bin" -try_add_to_path "$HOME/.flatpak_aliases" -try_add_to_path "$HOME/.local/bin" -try_add_to_path "$DOTFILES/bin" -try_add_to_path "$HOME/.rvm/bin" -try_add_to_path "$HOME/.npm-global/bin" -try_add_to_path "/usr/local/go/bin" -try_add_to_path "$HOME/go/bin" -try_add_to_path "$HOME/go" -try_add_to_path "$HOME/.mix/escripts" -try_add_to_path "$HOME/box/bin" +try_append_to_path() { + p="$1" + contains "$PATH" "$p" || append_to_path "$p" +} + +try_prepend_to_path() { + p="$1" + contains "$PATH" "$p" || prepend_to_path "$p" +} + +try_append_to_path "/usr/local/bin" +try_append_to_path "/usr/local/sbin" +try_append_to_path "$HOME/bin" +try_append_to_path "$HOME/.flatpak_aliases" +try_append_to_path "$HOME/.local/bin" +try_append_to_path "$DOTFILES/bin" +try_append_to_path "$HOME/.rvm/bin" +try_append_to_path "$HOME/.npm-global/bin" +try_append_to_path "/usr/local/go/bin" +try_append_to_path "$HOME/go/bin" +try_append_to_path "$HOME/go" +try_append_to_path "$HOME/.mix/escripts" +try_append_to_path "$HOME/box/bin" + +# TODO: Make sure this matches the path we used. Also, do we need both this and +# the pyenv stuff below? Maybe stick to one? +try_prepend_to_path "$HOME/.venv/default/bin" if [ ! "$WSL_DISTRO_NAME" = "" ]; then - try_add_to_path "/mnt/c/bin" + try_append_to_path "/mnt/c/bin" +fi + +# TODO: Not sure if this is the right place for this or if improvements could +# be made. Fine for now but we can probably use try_prepend_to_path or whatever. +# If a pyenv installation exists, initialize it +if [ -d "$HOME/.pyenv" ]; then + export PYENV_ROOT="$HOME/.pyenv" + [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init - bash)" fi From c6ea145f1fea049781d066f02d7f64393fc0911c Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 5 Jan 2026 15:28:56 -0800 Subject: [PATCH 37/39] Nix Home Manager (#7) --- CLAUDE.md | 82 +++ GEMINI.md | 82 +++ Makefile | 7 + README.md | 160 +---- apply.sh | 667 ++++++++++++++++++ bin/cleanup_logs.sh | 5 +- bin/git_diff_with.sh | 110 +++ bin/run_vm_guest_additions.sh | 23 + bin/set-theme | 5 +- cli/README.md | 41 +- cli/commands/__init__.py | 4 - cli/commands/list.py | 19 - cli/commands/provision.py | 100 --- cli/lib/common/user.py | 2 +- cli/lib/provision/__init__.py | 1 - cli/lib/provision/provisioner.py | 27 - cli/lib/provision/provisioner_apt.py | 105 --- cli/lib/provision/provisioner_docker.py | 123 ---- cli/lib/provision/provisioner_dot.py | 47 -- cli/lib/provision/provisioner_flavours.py | 171 ----- cli/lib/provision/provisioner_i3.py | 219 ------ cli/lib/provision/provisioner_kitty.py | 143 ---- cli/lib/provision/provisioner_neovim.py | 158 ----- cli/lib/provision/provisioner_nodejs.py | 143 ---- cli/lib/provision/provisioner_pip.py | 30 - cli/lib/provision/provisioner_ripgrep.py | 111 --- cli/lib/provision/provisioner_treesitter.py | 195 ----- cli/lib/provision/provisioner_win32yank.py | 171 ----- cli/lib/provision/symlink.py | 36 - cli/lib/provision/system_provisioner.py | 81 --- cli/lib/provision/tag.py | 38 - config/bash/aliases.sh | 5 +- config/bash/core.sh | 2 +- config/bash/functions.sh | 34 + config/bashrc | 20 +- config/env | 31 +- .../custom/templates/bashrc.mustache | 4 +- config/i3 | 4 + config/nvim/lua/dot/globals.lua | 3 - config/nvim/lua/dot/plugins.lua | 89 +-- config/nvim/lua/dot/treesitter.lua | 47 +- config/nvim/lua/dot/vim_plug.lua | 34 - config/wslrc | 9 + config/xsession | 64 +- doc/nix_todos.md | 206 ++++++ doc/setup_ubuntu.md | 156 ++++ doc/setup_windows.md | 3 + doc/theme.md | 72 ++ doc/todo.md | 173 +++++ img/wallpaper.svg | 16 + mypy.ini | 11 - nix/flake.lock | 49 ++ nix/flake.nix | 52 ++ nix/home/features/development.nix | 59 ++ nix/home/lib/dotfiles-links.nix | 98 +++ nix/home/lib/python-environment.nix | 37 + nix/home/packages/cql-vim.nix | 13 + nix/home/packages/mesonic.nix | 13 + nix/home/packages/nvim-markdown.nix | 13 + nix/home/packages/wpr.nix | 33 + nix/home/roles/core.nix | 199 ++++++ nix/home/roles/desktop.nix | 118 ++++ nix/home/roles/gaming.nix | 12 + nix/home/roles/wsl.nix | 69 ++ nix/hosts.json | 28 + provision/manjaro.sh | 67 -- provision/ubuntu.sh | 398 ----------- provision/ubuntu_22.04.sh | 484 ------------- templates/set-bg.sh | 8 + todo.md | 431 ----------- 70 files changed, 2569 insertions(+), 3701 deletions(-) create mode 100644 CLAUDE.md create mode 100644 GEMINI.md create mode 100755 apply.sh create mode 100755 bin/git_diff_with.sh create mode 100755 bin/run_vm_guest_additions.sh delete mode 100644 cli/commands/list.py delete mode 100644 cli/commands/provision.py delete mode 100644 cli/lib/provision/__init__.py delete mode 100644 cli/lib/provision/provisioner.py delete mode 100644 cli/lib/provision/provisioner_apt.py delete mode 100644 cli/lib/provision/provisioner_docker.py delete mode 100644 cli/lib/provision/provisioner_dot.py delete mode 100644 cli/lib/provision/provisioner_flavours.py delete mode 100644 cli/lib/provision/provisioner_i3.py delete mode 100644 cli/lib/provision/provisioner_kitty.py delete mode 100644 cli/lib/provision/provisioner_neovim.py delete mode 100644 cli/lib/provision/provisioner_nodejs.py delete mode 100644 cli/lib/provision/provisioner_pip.py delete mode 100644 cli/lib/provision/provisioner_ripgrep.py delete mode 100644 cli/lib/provision/provisioner_treesitter.py delete mode 100644 cli/lib/provision/provisioner_win32yank.py delete mode 100644 cli/lib/provision/symlink.py delete mode 100644 cli/lib/provision/system_provisioner.py delete mode 100644 cli/lib/provision/tag.py delete mode 100644 config/nvim/lua/dot/vim_plug.lua create mode 100644 config/wslrc create mode 100644 doc/nix_todos.md create mode 100644 doc/setup_ubuntu.md create mode 100644 doc/setup_windows.md create mode 100644 doc/theme.md create mode 100644 doc/todo.md create mode 100755 img/wallpaper.svg create mode 100644 nix/flake.lock create mode 100644 nix/flake.nix create mode 100644 nix/home/features/development.nix create mode 100644 nix/home/lib/dotfiles-links.nix create mode 100644 nix/home/lib/python-environment.nix create mode 100644 nix/home/packages/cql-vim.nix create mode 100644 nix/home/packages/mesonic.nix create mode 100644 nix/home/packages/nvim-markdown.nix create mode 100644 nix/home/packages/wpr.nix create mode 100644 nix/home/roles/core.nix create mode 100644 nix/home/roles/desktop.nix create mode 100644 nix/home/roles/gaming.nix create mode 100644 nix/home/roles/wsl.nix create mode 100644 nix/hosts.json delete mode 100755 provision/manjaro.sh delete mode 100755 provision/ubuntu.sh delete mode 100755 provision/ubuntu_22.04.sh create mode 100644 templates/set-bg.sh delete mode 100644 todo.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ff8c405 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a dotfiles management system for Linux environments using Nix and Home Manager for declarative configuration. It supports multiple host types: desktops, servers, and WSL instances. + +## Common Commands + +### Fresh Install (Primary Method) +```bash +./apply.sh --nix-host +# Example: ./apply.sh --nix-host personal-desktop +``` +Available hosts are defined in `nix/hosts.json`: personal-desktop, work-desktop, personal-wsl, work-wsl, personal-server, work-server. + +### Apply Nix Configuration Changes +```bash +home-manager switch --flake ~/dot/nix# +``` + +### Python CLI Tool +```bash +dot provision [TAGS] # Run provisioners +dot link # Create symlinks (legacy) +dot clean # Remove symlinks +dot tidy # Format Python code (black, isort, autoflake) +``` + +### Type Checking and Linting +```bash +make mypy # Run mypy on all Python files +``` + +### Theming +```bash +set-theme # Apply base16 color scheme (e.g., outrun-dark) +flavours update all # Required first time setup +``` + +## Architecture + +### Nix Configuration (`nix/`) +- `flake.nix` - Entry point; generates Home Manager configurations from `hosts.json` +- `hosts.json` - Defines hosts and their roles (core, desktop, gaming, wsl) +- `home/roles/` - Host-type configurations (Ansible-style naming): + - `core.nix` - Base packages (neovim, git, fzf, tmux, Python tools) + - `desktop.nix` - GUI packages (i3, kitty, rofi, mpd, media tools) + - `gaming.nix` - Gaming packages + - `wsl.nix` - WSL-specific configuration +- `home/lib/` - Shared modules imported by roles: + - `dotfiles-links.nix` - Maps config files from `config/` to home directory + - `python-environment.nix` - Builds unified Python environment +- `home/features/` - Specific feature modules (e.g., docker) that can be shared across roles + +### Configuration Files (`config/`) +Application configs that get linked to home directory via Home Manager: +- `bash/` - Shell configuration +- `nvim/` - Neovim configuration +- `i3`, `sway` - Window manager configs +- `kitty.conf`, `alacritty/`, `wezterm.lua` - Terminal emulators +- `flavours/` - Base16 theming (schemes in `schemes/custom/`, templates in `templates/custom/`) + +### Python CLI (`cli/`) +- `dot.py` - Main entry point with argparse +- `commands/` - Command implementations (provision, link, tidy, lint, etc.) +- `lib/provision/` - Provisioners for apt, pip, nodejs, docker, neovim, etc. +- `lib/common/` - Shared utilities (shell, git, logging, file operations) + +### Utility Scripts (`bin/`) +Standalone scripts: `set-theme`, `i3-util.sh`, `startup.sh`, `fuzzy-fm`, etc. + +## Key Files + +- `apply.sh` - Bootstrap script for fresh installs (installs Nix, applies Home Manager) +- `links.json` - Legacy dotfile symlink mappings (now mostly handled by `dotfiles-links.nix`) +- `nix/hosts.json` - Host definitions with roles; drives the entire Nix configuration + +## Code Style + +Python code uses black, isort, and autoflake. Run `dot tidy` before committing Python changes. Type hints are encouraged; validate with `make mypy`. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..02b6aad --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,82 @@ +# GEMINI.md + +This file provides guidance to Gemini when working with code in this repository. + +## Project Overview + +This is a dotfiles management system for Linux environments using Nix and Home Manager for declarative configuration. It supports multiple host types: desktops, servers, and WSL instances. + +## Common Commands + +### Fresh Install (Primary Method) +```bash +./apply.sh --nix-host +# Example: ./apply.sh --nix-host personal-desktop +``` +Available hosts are defined in `nix/hosts.json`: personal-desktop, work-desktop, personal-wsl, work-wsl, personal-server, work-server. + +### Apply Nix Configuration Changes +```bash +home-manager switch --flake ~/dot/nix# +``` + +### Python CLI Tool +```bash +dot provision [TAGS] # Run provisioners +dot link # Create symlinks (legacy) +dot clean # Remove symlinks +dot tidy # Format Python code (black, isort, autoflake) +``` + +### Type Checking and Linting +```bash +make mypy # Run mypy on all Python files +``` + +### Theming +```bash +set-theme # Apply base16 color scheme (e.g., outrun-dark) +flavours update all # Required first time setup +``` + +## Architecture + +### Nix Configuration (`nix/`) +- `flake.nix` - Entry point; generates Home Manager configurations from `hosts.json` +- `hosts.json` - Defines hosts and their roles (core, desktop, gaming, wsl) +- `home/roles/` - Host-type configurations (Ansible-style naming): + - `core.nix` - Base packages (neovim, git, fzf, tmux, Python tools) + - `desktop.nix` - GUI packages (i3, kitty, rofi, mpd, media tools) + - `gaming.nix` - Gaming packages + - `wsl.nix` - WSL-specific configuration +- `home/lib/` - Shared modules imported by roles: + - `dotfiles-links.nix` - Maps config files from `config/` to home directory + - `python-environment.nix` - Builds unified Python environment +- `home/features/` - Specific feature modules (e.g., docker) that can be shared across roles + +### Configuration Files (`config/`) +Application configs that get linked to home directory via Home Manager: +- `bash/` - Shell configuration +- `nvim/` - Neovim configuration +- `i3`, `sway` - Window manager configs +- `kitty.conf`, `alacritty/`, `wezterm.lua` - Terminal emulators +- `flavours/` - Base16 theming (schemes in `schemes/custom/`, templates in `templates/custom/`) + +### Python CLI (`cli/`) +- `dot.py` - Main entry point with argparse +- `commands/` - Command implementations (provision, link, tidy, lint, etc.) +- `lib/provision/` - Provisioners for apt, pip, nodejs, docker, neovim, etc. +- `lib/common/` - Shared utilities (shell, git, logging, file operations) + +### Utility Scripts (`bin/`) +Standalone scripts: `set-theme`, `i3-util.sh`, `startup.sh`, `fuzzy-fm`, etc. + +## Key Files + +- `apply.sh` - Bootstrap script for fresh installs (installs Nix, applies Home Manager) +- `links.json` - Legacy dotfile symlink mappings (now mostly handled by `dotfiles-links.nix`) +- `nix/hosts.json` - Host definitions with roles; drives the entire Nix configuration + +## Code Style + +Python code uses black, isort, and autoflake. Run `dot tidy` before committing Python changes. Type hints are encouraged; validate with `make mypy`. diff --git a/Makefile b/Makefile index 1ba5f65..9824289 100644 --- a/Makefile +++ b/Makefile @@ -19,3 +19,10 @@ windows: mypy: find . -iname '*.py' | xargs mypy --config-file ./mypy.ini mypy --config-file ./mypy.ini ./bin/fzf_cached_wsl + +# Run nixfmt on all Nix files +.PHONY: nixfmt +nixfmt: + # Run this in a nix-shell so we can get the most up-to-date version of + # nixfmt since the --indent option was added fairly recently. + nix-shell -p nixfmt --run "find . -iname '*.nix' | xargs nixfmt --indent=4" diff --git a/README.md b/README.md index f312aa8..aeb2c69 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,18 @@ # Dotfiles -This repository contains my dotfiles! +This repository contains my dotfiles and uses Nix and Home Manager for +declarative configuration across my machines. -## Manual Setup +## Getting Started -A few things I haven't bothered to automate. +- **Ubuntu Setup Instructions:** + - [setup_ubuntu.md](./doc/setup_ubuntu.md) +- **Windows Setup Instructions:** + - [setup_windows.md](./doc/setup_windows.md) -### Git +## Theme -Create `~/.gitconfig_local` like: +For details on how theming is set up and how to modify a theme or change the +current theme, see: -``` -[user] - email = paul@foo.com - name = Paul Ewing -``` - -### Wallpaper Rotater - -If using `wpr`, create `~/.config/wpr/config.json` like: - -```json -{ - "WallpaperDir": "/home/username/Pictures/Wallpapers", - "DisplayCount": 1, - "Interval":120 -} -``` - -### Dual Boot Clock Fix - -If dual booting with Windows, set hardware clock to local time: - -```bash -timedatectl set-local-rtc 1 -``` - -Without this, clock time in Windows will be off. - -### Applications to Manually Install - -The following aren't in apt and need to be installed manually: - -- Chrome -- Beyond Compare -- Insync -- Discord -- RuneLite - -Alacritty is not yet in the default Ubuntu apt repositories: - -```bash -sudo add-apt-repository ppa:mmstick76/alacritty -sudo apt update -``` - -## Theming - -To make it easier to re-theme everything at once, I use -[base16](https://github.com/chriskempson/base16) and -[flavours](https://github.com/Misterio77/flavours). See: - -The tl;dr of `base16` is that it is a system for designing color schemes. -`base16` schemes consists of a palette of 16 colors - 8 shades and 8 accents. -Templates can then be created to render the base16 scheme into various config -formats for different applications. - -Due to some [turbulence](https://github.com/tinted-theming/home/issues/51) in -the `base16` project, I've added my most used schemes directly to my dotfiles -to avoid things breaking if repositories are ever moved or taken down. I've -also created my own templates rather than using the defaults. - -- [schemes](./config/flavours/schemes/custom) -- [templates](./config/flavours/templates/custom/templates) - -Using the `flavours` application, these templates are rendered directly into my -dotfiles based on the `flavours` config: - -- [flavours/config.toml](./config/flavours/config.toml) - -To apply a new color scheme, download and install -[flavours](https://github.com/Misterio77/flavours/releases/latest). - -The first time running, update sources. Even if using schemes/templates -committed to my dotfiles, this still appears to be necessary: - -```bash -flavours update all -``` - -**Note:** We should add flavours installation to the provision script. - -Once flavours is installed, set the theme using the -[set-theme](./bin/set-theme) script. This not only executes `flavours` but also -reloads config across various applications to smoothly transition themes. - -```bash -set-theme -``` - -The name should match the corresponding base16 scheme yaml file without the -extension. For example: - -```bash -flavours apply outrun-dark -``` - -The official lists of templates and schemes supported by flavours live here: - -- https://github.com/chriskempson/base16-schemes-source/blob/main/list.yaml -- https://github.com/chriskempson/base16-templates-source/blob/master/list.yaml - -Manual steps after changing themes: - -- Reload tmux config - - `:source-file ~/.tmux.conf` - - We should figure out how to automate this - -### TODO - -Some remaining items to tackle in regards to theming: -- Add templates for - - sway - -## Windows 10 - -**TODO**: This doesn't exist anymore? Fix link... - -For setup steps on Windows 10, see: - -[windows_setup.md](./windows_setup.md) - -### WSL - -Some useful things to add to `.localrc` in WSL. - -Remove the background highlighting of folders in ls: - -```bash -LS_COLORS=$LS_COLORS:'ow=1;34:' ; export LS_COLORS -``` - -WezTerm shell integration; this adds some useful features like having new tabs -open in the same directory as the previous: -``` -if [ "$TERM_PROGRAM" = "WezTerm" ] && [ -f "$HOME/wezterm.sh" ]; then - source "$HOME/wezterm.sh" -fi -``` - -For now just manually create and copy the `wezterm.sh` file from here: - -https://raw.githubusercontent.com/wez/wezterm/main/assets/shell-integration/wezterm.sh - -We could make this a bit nicer by automatically downloading it. +[theme.md](./doc/theme.md) diff --git a/apply.sh b/apply.sh new file mode 100755 index 0000000..1885083 --- /dev/null +++ b/apply.sh @@ -0,0 +1,667 @@ +#!/usr/bin/env bash + +set -euo pipefail + +#============================================================================== +# Utilities +#============================================================================== + +# Print a message to stderr. +# +# $*: The message to print. +yell() { >&2 echo "$*"; } + +# Print an error message to stderr and exit. +# +# $*: The error message to print. +die() { yell "ERROR: $*"; exit 1; } + +# Execute a command, and exit if it fails. +# +# $*: The command to execute. +try() { "$@" || die "Command failed: $*"; } + +# Checks if a command is installed and on the PATH. +# +# $1: The command name to check. +# Returns 0 if the command is installed, 1 otherwise. +is_cmd_installed() { + command -v "$1" >/dev/null 2>&1 || return 1 +} + +# Checks if the script is running in a WSL environment. +# +# Returns 0 if in WSL, 1 otherwise. +is_wsl() { + [ -n "${WSL_DISTRO_NAME-}" ] && return 0 || return 1 +} + +#============================================================================== +# Configuration +#============================================================================== +DOTFILES_DIR_DEFAULT="$HOME/dot" +STATE_FILE="$HOME/.local/state/dotfiles/apply.json" +APT_UPDATE_MAX_AGE_SECONDS=86400 # 24 hours + +# Prints the script usage information. +usage() { + cat </dev/null 2>&1; then + jq -r '.apt_last_update // empty' "$STATE_FILE" 2>/dev/null || echo "" + else + # Fallback: extract with grep/sed (works for simple JSON) + grep -o '"apt_last_update":[0-9]*' "$STATE_FILE" 2>/dev/null | sed 's/.*://' || echo "" + fi +} + +# Writes the current timestamp for apt update to the state file. +set_apt_last_update() { + local timestamp + timestamp="$(date +%s)" + + mkdir -p "$(dirname "$STATE_FILE")" + + if command -v jq >/dev/null 2>&1; then + # Use jq to update/create the JSON file + if [[ -f "$STATE_FILE" ]]; then + local tmp + tmp="$(mktemp)" + jq ".apt_last_update = $timestamp" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE" + else + echo "{\"apt_last_update\": $timestamp}" > "$STATE_FILE" + fi + else + # Simple fallback without jq, will clobber other fields + echo "{\"apt_last_update\": $timestamp}" > "$STATE_FILE" + fi +} + +# Checks if the apt update cache is stale. +# +# Returns 0 (true) if stale or missing, 1 (false) if fresh. +is_apt_update_stale() { + local last_update + last_update="$(get_apt_last_update)" + + if [[ -z "$last_update" ]]; then + return 0 # No record, consider stale + fi + + local now age + now="$(date +%s)" + age=$((now - last_update)) + + if [[ $age -ge $APT_UPDATE_MAX_AGE_SECONDS ]]; then + return 0 # Stale + else + return 1 # Fresh + fi +} + +# Runs apt-get update only if the cache is stale. +apt_update_if_stale() { + if is_apt_update_stale; then + local last_update age_hours + last_update="$(get_apt_last_update)" + if [[ -n "$last_update" ]]; then + age_hours=$(( ($(date +%s) - last_update) / 3600 )) + echo "[bootstrap] apt cache is stale (${age_hours}h old), running apt-get update..." + else + echo "[bootstrap] No apt update timestamp found, running apt-get update..." + fi + try sudo apt-get update -y + set_apt_last_update + else + local last_update age_hours + last_update="$(get_apt_last_update)" + age_hours=$(( ($(date +%s) - last_update) / 3600 )) + echo "[bootstrap] apt cache is fresh (${age_hours}h old), skipping apt-get update" + fi +} + +# Runs apt-get update unconditionally and updates the timestamp. +# Used when adding new repositories (e.g., Docker). +apt_update_always() { + echo "[bootstrap] Running apt-get update (forced)..." + try sudo apt-get update -y + set_apt_last_update +} + +#============================================================================== +# APT Upgrade Caching +#============================================================================== + +# Gets the last apt upgrade timestamp from the state file. +# +# Returns an empty string if not found. +get_apt_last_upgrade() { + if [[ ! -f "$STATE_FILE" ]]; then + echo "" + return + fi + if command -v jq >/dev/null 2>&1; then + jq -r '.apt_last_upgrade // empty' "$STATE_FILE" 2>/dev/null || echo "" + else + grep -o '"apt_last_upgrade":[0-9]*' "$STATE_FILE" 2>/dev/null | sed 's/.*://' || echo "" + fi +} + +# Writes the current timestamp for apt upgrade to the state file. +set_apt_last_upgrade() { + local timestamp + timestamp="$(date +%s)" + + mkdir -p "$(dirname "$STATE_FILE")" + + if command -v jq >/dev/null 2>&1; then + if [[ -f "$STATE_FILE" ]]; then + local tmp + tmp="$(mktemp)" + jq ".apt_last_upgrade = $timestamp" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE" + else + echo "{\"apt_last_upgrade\": $timestamp}" > "$STATE_FILE" + fi + else + # Without jq, we need to be careful not to clobber apt_last_update + # Best effort: just write upgrade timestamp (update will fix it next time with jq) + if [[ -f "$STATE_FILE" ]]; then + # Try to preserve existing content + local update_ts + update_ts="$(get_apt_last_update)" + if [[ -n "$update_ts" ]]; then + echo "{\"apt_last_update\": $update_ts, \"apt_last_upgrade\": $timestamp}" > "$STATE_FILE" + else + echo "{\"apt_last_upgrade\": $timestamp}" > "$STATE_FILE" + fi + else + echo "{\"apt_last_upgrade\": $timestamp}" > "$STATE_FILE" + fi + fi +} + +# Checks if the apt upgrade is stale. +is_apt_upgrade_stale() { + local last_upgrade + last_upgrade="$(get_apt_last_upgrade)" + + if [[ -z "$last_upgrade" ]]; then + return 0 # No record, consider stale + fi + + local now age + now="$(date +%s)" + age=$((now - last_upgrade)) + + if [[ $age -ge $APT_UPDATE_MAX_AGE_SECONDS ]]; then + return 0 # Stale + else + return 1 # Fresh + fi +} + +# Runs apt-get dist-upgrade only if the cache is stale. +apt_upgrade_if_stale() { + if is_apt_upgrade_stale; then + local last_upgrade age_hours + last_upgrade="$(get_apt_last_upgrade)" + if [[ -n "$last_upgrade" ]]; then + age_hours=$(( ($(date +%s) - last_upgrade) / 3600 )) + echo "[bootstrap] apt upgrade is stale (${age_hours}h old), running apt-get dist-upgrade..." + else + echo "[bootstrap] No apt upgrade timestamp found, running apt-get dist-upgrade..." + fi + try sudo apt-get dist-upgrade -y + set_apt_last_upgrade + else + local last_upgrade age_hours + last_upgrade="$(get_apt_last_upgrade)" + age_hours=$(( ($(date +%s) - last_upgrade) / 3600 )) + echo "[bootstrap] apt upgrade is fresh (${age_hours}h old), skipping apt-get dist-upgrade" + fi +} + +#============================================================================== +# APT Install Caching +#============================================================================== + +# Get the hash of installed apt packages from the state file. +# Assumes jq is available. +get_apt_pkgs_hash() { + if [[ ! -f "$STATE_FILE" ]]; then + echo "" + return + fi + jq -r '.apt_pkgs_hash // empty' "$STATE_FILE" 2>/dev/null || echo "" +} + +# Calculate and write the hash of installed apt packages to the state file. +# +# Assumes jq is available. +# $*: The list of packages to hash. +set_apt_pkgs_hash() { + local pkgs_hash + pkgs_hash=$(printf "%s\n" "$@" | sort | sha256sum | awk '{print $1}') + + mkdir -p "$(dirname "$STATE_FILE")" + + local tmp + tmp="$(mktemp)" + if [[ -f "$STATE_FILE" ]] && [[ -s "$STATE_FILE" ]]; then + jq ".apt_pkgs_hash = \"$pkgs_hash\"" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE" + else + echo "{\"apt_pkgs_hash\": \"$pkgs_hash\"}" > "$STATE_FILE" + fi +} + +#============================================================================== +# Host & Role Helpers +#============================================================================== + +# Lists the available Nix hosts from the hosts.json file. +nix_hosts() { + jq -r '.hosts | keys[]' "$DOTFILES_DIR/nix/hosts.json" +} + +# Checks if the current NIX_HOST has a specific role. +# +# $1: The role to check for. +# Returns 0 if the host has the role, 1 otherwise. +host_has_role() { + local role="$1" + if [ -z "$NIX_HOST" ] || [ ! -f "$DOTFILES_DIR/nix/hosts.json" ]; then + return 1 + fi + jq -e ".hosts[\"$NIX_HOST\"].roles | index(\"$role\")" \ + "$DOTFILES_DIR/nix/hosts.json" >/dev/null 2>&1 +} + +#============================================================================== +# Installation Functions +#============================================================================== + +# Installs Docker if it's not already installed and the 'core' role is active. +install_docker() { + # TODO: Eventually we're going to move this from core to its own role and + # when we do we'll need to update this condition + if ! host_has_role "core"; then + echo "[bootstrap] (install_docker) core role not enabled, skipping docker install" + return 0 + fi + + if command -v docker >/dev/null 2>&1; then + echo "[bootstrap] (install_docker) Docker is already installed, skipping docker install" + return 0 + fi + + # Add Docker's official GPG key: + try sudo install -m 0755 -d /etc/apt/keyrings + try sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc + try sudo chmod a+r /etc/apt/keyrings/docker.asc + + # Add the repository to Apt sources: + try sudo tee /etc/apt/sources.list.d/docker.sources </dev/null 2>&1 + try sudo usermod -aG docker $USER + + DOCKER_INSTALLED="1" +} + +# Sources the local, non-version-controlled rc file if it exists. +source_localrc() { + local localrc_path="$HOME/.localrc" + + if [ -f "$localrc_path" ]; then + echo "[bootstrap] (source_localrc) Sourcing localrc from path $localrc_path" + source "$HOME/.localrc" + else + echo "[bootstrap] (source_localrc) Localrc path '$localrc_path' doesn't exist; skipping." + fi +} + +# Validates that the NIX_HOST variable is set and is a valid host. +validate_nix_host() { + if [ -z "$NIX_HOST" ]; then + yell "[bootstrap] (validate_nix_host) Error: NIX_HOST is not set." + yell "Either specify the --nix-host argument or export this variable in your ~/.localrc file. Available hosts:" + yell "$(nix_hosts)" + yell "See $DOTFILES_DIR/nix/hosts.json for details." + exit 1 + fi + + if [ ! -f "$DOTFILES_DIR/nix/hosts.json" ]; then + die "hosts.json not found at $DOTFILES_DIR/nix/hosts.json" + fi + + if ! jq -e ".hosts[\"$NIX_HOST\"]" "$DOTFILES_DIR/nix/hosts.json" >/dev/null 2>&1; then + yell "NIX_HOST '$NIX_HOST' is not valid. Available hosts:" + yell "$(nix_hosts)" + yell "See $DOTFILES_DIR/nix/hosts.json for details." + exit 1 + fi +} + +# Installs minimal prerequisite packages using apt. +apt_bootstrap() { + if [[ ! "$DO_APT" -eq 1 ]]; then + echo "[bootstrap] (apt_bootstrap) Skipping apt steps because --no-apt was specified..." + return + fi + + echo "[bootstrap] Installing minimal prerequisites via apt..." + + # You can expand this later, but keep it small. + local pkgs=( + apt-file + ca-certificates + curl + git + jq + libfuse3-3 + locate + software-properties-common + xz-utils + ) + + # Add role-specific packages based on host configuration + if host_has_role "desktop"; then + pkgs+=( + i3 + i3status + kitty + ) + fi + + apt_update_if_stale + + if [[ "$DO_UPGRADE" -eq 1 ]]; then + apt_upgrade_if_stale + fi + + # Check if packages are already installed by comparing hashes + local current_pkgs_hash + current_pkgs_hash=$(printf "%s\n" "${pkgs[@]}" | sort | sha256sum | awk '{print $1}') + + local installed_pkgs_hash + installed_pkgs_hash=$(get_apt_pkgs_hash) + + if [[ "$current_pkgs_hash" == "$installed_pkgs_hash" ]]; then + echo "[bootstrap] apt packages are already up-to-date, skipping install." + else + echo "[bootstrap] New or changed apt packages detected, running install..." + try sudo apt-get install -y "${pkgs[@]}" + set_apt_pkgs_hash "${pkgs[@]}" + fi +} + +# Installs Nix if it's not already installed. +install_nix_if_needed() { + if is_cmd_installed nix; then + echo "[bootstrap] nix already installed." + return 0 + fi + + echo "[bootstrap] Installing Nix (single-user)..." + # Standard installer; can be customized later if needed. + try sh -c 'curl -L https://nixos.org/nix/install | sh -s -- --no-daemon' +} + +# Sources the Nix profile to make `nix` available in the current shell. +source_nix_profile() { + # Make nix available in the current shell, even right after install. + if is_cmd_installed nix; then + return 0 + fi + + # Common install locations for single-user Nix. + if [[ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]]; then + # shellcheck disable=SC1090 + source "$HOME/.nix-profile/etc/profile.d/nix.sh" + elif [[ -f "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" ]]; then + # Multi-user install path + # shellcheck disable=SC1091 + source "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" + fi + + is_cmd_installed nix || die "nix still not on PATH after sourcing profile." +} + +# Enables Nix experimental features (nix-command and flakes). +enable_nix_experimental() { + # Home Manager via flakes is the nicest iteration experience. + # This just ensures nix can use flakes/commands on fresh installs. + mkdir -p "$HOME/.config/nix" + local conf="$HOME/.config/nix/nix.conf" + + if [[ ! -f "$conf" ]] || ! grep -q "experimental-features" "$conf"; then + echo "[bootstrap] Enabling nix-command + flakes in $conf" + { + echo "experimental-features = nix-command flakes" + } >> "$conf" + fi +} + +# Applies the Home Manager configuration for the current host. +apply_home_manager() { + echo "[bootstrap] Applying Home Manager target: $NIX_HOST" + + # Assumes your repo contains a flake with homeConfigurations. + try nix --extra-experimental-features "nix-command flakes" \ + run "github:nix-community/home-manager" -- \ + switch -b hm-bak --flake "$DOTFILES_DIR/nix#$NIX_HOST" +} + +# Sets the default terminal and editor using update-alternatives. +set_default_terminal_and_editor() { + echo "[bootstrap] Setting system defaults via update-alternatives..." + + if host_has_role "desktop"; then + local nvim_path + kitty_path="$(command -v kitty || true)" + + if [[ -n "$kitty_path" ]]; then + try sudo update-alternatives --install /usr/bin/x-terminal-emulator x-terminal-emulator "$kitty_path" 50 + try sudo update-alternatives --set x-terminal-emulator "$kitty_path" + else + yell "[bootstrap] kitty not found on PATH; skipping terminal alternative" + fi + else + echo "[bootstrap] (set_default_terminal_and_editor) Desktop role not enabled, skipping setting default terminal emulator" + fi + + local nvim_path + nvim_path="$(command -v nvim || true)" + + if [[ -n "$nvim_path" ]]; then + try sudo update-alternatives --install /usr/bin/vi vi "$nvim_path" 60 + try sudo update-alternatives --set vi "$nvim_path" + try sudo update-alternatives --install /usr/bin/vim vim "$nvim_path" 60 + try sudo update-alternatives --set vim "$nvim_path" + try sudo update-alternatives --install /usr/bin/editor editor "$nvim_path" 60 + try sudo update-alternatives --set editor "$nvim_path" + else + yell "[bootstrap] nvim not found on PATH; skipping editor alternatives" + fi +} + +# Installs X11 and Wayland session files for graphical login managers. +install_session_desktop_files() { + echo "[bootstrap] Installing session desktop files..." + + local dot="$DOTFILES_DIR" + local xs_src="$dot/config/xsession.desktop" + local xs_dst="/usr/share/xsessions/xsession.desktop" + + local sway_src="$dot/config/sway-user.desktop" + local sway_dst="/usr/share/wayland-sessions/sway-user.desktop" + + + if ! host_has_role "desktop"; then + echo "[bootstrap] (install_session_desktop_files) Desktop role not enabled, skipping installing session desktop files" + return 0 + fi + + if [[ -f "$xs_src" ]]; then + try sudo install -m 0644 "$xs_src" "$xs_dst" + else + yell "[bootstrap] Missing $xs_src; skipping xsession.desktop" + fi + + if [[ -f "$sway_src" ]]; then + try sudo install -m 0644 "$sway_src" "$sway_dst" + else + yell "[bootstrap] Missing $sway_src; skipping sway-user.desktop" + fi +} + +#============================================================================== +# System Setup Caching +#============================================================================== + +# Checks if the one-time system setup tasks have been completed. +# +# Assumes jq is available. +# Returns 0 if setup is done, 1 otherwise. +is_system_setup_done() { + if [[ ! -f "$STATE_FILE" ]]; then + return 1 + fi + local setup_done + setup_done=$(jq -r '.system_setup_done // false' "$STATE_FILE") + if [[ "$setup_done" == "true" ]]; then + return 0 + else + return 1 + fi +} + +# Marks the one-time system setup tasks as completed in the state file. +# +# Assumes jq is available. +set_system_setup_done() { + mkdir -p "$(dirname "$STATE_FILE")" + local tmp + tmp="$(mktemp)" + if [[ -f "$STATE_FILE" ]] && [[ -s "$STATE_FILE" ]]; then + jq ".system_setup_done = true" "$STATE_FILE" > "$tmp" && mv "$tmp" "$STATE_FILE" + else + echo '{"system_setup_done": true}' > "$STATE_FILE" + fi +} + +# Runs the system-level setup tasks that require sudo, but only if they +# haven't been completed before. +run_sudo_setup_tasks_if_needed() { + if is_system_setup_done; then + echo "[bootstrap] System-level setup already completed, skipping." + return + fi + + echo "[bootstrap] Running system-level setup tasks..." + set_default_terminal_and_editor + install_session_desktop_files + set_system_setup_done + echo "[bootstrap] System-level setup tasks complete." +} + +#============================================================================== +# Main Execution +#============================================================================== +while [[ $# -gt 0 ]]; do + case "$1" in + --dir) DOTFILES_DIR="$2"; shift 2;; + --nix-host) NIX_HOST="$2"; shift 2;; + --no-upgrade) DO_UPGRADE=0; shift;; + --no-apt) DO_APT=0; shift;; + --reset-state) RESET_STATE=1; shift;; + -h|--help) usage; exit 0;; + *) die "Unknown argument: $1";; + esac +done + +main() { + if [[ "$RESET_STATE" -eq 1 ]]; then + echo "[bootstrap] Resetting state file at $STATE_FILE..." + rm -f "$STATE_FILE" + fi + + echo "[bootstrap] Starting on: $(lsb_release -ds 2>/dev/null || uname -a)" + if is_wsl; then + echo "[bootstrap] Detected WSL environment." + fi + + # We need jq for this script to run at all so if it's not installed, get it + if ! command -v jq >/dev/null 2>&1; then + echo "[bootstrap] jq not found, installing..." + apt_update_if_stale + try sudo apt-get install -y jq + fi + + source_localrc + validate_nix_host + apt_bootstrap + install_nix_if_needed + source_nix_profile + enable_nix_experimental + apply_home_manager + run_sudo_setup_tasks_if_needed + + install_docker + + echo "[bootstrap] Done." + + if [[ "$DOCKER_INSTALLED" -eq 1 ]]; then + echo "[bootstrap] Docker was just installed. Either reboot or log out and back in for the group change to take effect." + fi +} + +main "$@" diff --git a/bin/cleanup_logs.sh b/bin/cleanup_logs.sh index 3d3f431..f3088fb 100755 --- a/bin/cleanup_logs.sh +++ b/bin/cleanup_logs.sh @@ -2,6 +2,7 @@ # Delete any log files that haven't been touched in 14 days days="14" -logdir="$HOME/.logs" -[ -d "$logdir" ] && find "$logdir" -ctime +$days -type f -exec rm {} \; +DOTFILES_LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/logs" + +[ -d "$DOTFILES_LOG_DIR" ] && find "$DOTFILES_LOG_DIR" -ctime +$days -type f -exec rm {} \; \ No newline at end of file diff --git a/bin/git_diff_with.sh b/bin/git_diff_with.sh new file mode 100755 index 0000000..66f7069 --- /dev/null +++ b/bin/git_diff_with.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +function yell () { >&2 echo "$*"; } +function die () { yell "$*"; exit 1; } +function try () { "$@" || die "Command failed: $*"; } + +SCRIPT_PATH="$( realpath "$0" )" +SCRIPT_DIR="$( dirname "$SCRIPT_PATH" )" + +# TODO: This is only half implemented as a hacky test to see if it would be +# useful +# +# Diff the current git repository with a previous commit using Beyond Compare. +# Copies the repository to a temp directory and checks out the specified +# commit/branch/tag. Then runs Beyond compare to do a directory comparison +# between the current directory and the temp directory. +git_diff_with() +{ + # TODO: Document this parameter + local diff_target="$1" + + if [ -z "$diff_target" ]; then + # If diff_target isn't specified, default it to the previous commit hash + diff_target="$(git log -n 1 | grep -E '^commit ' | sed -e 's/^commit //' -e 's/ .*//')" + echo "Defaulting diff_target to $diff_target" + fi + + # TODO: We could make this robust and walk up the file tree but for now + # keep it simple + if [ ! -d "./.git" ]; then + yell "ERROR: Not at the root of a git repository" + return 1 + fi + + local tmp_dir_name tmp_dir + tmp_dir_name="$(basename $(pwd))_${diff_target}_diff" + tmp_dir="$HOME/tmp/${tmp_dir_name}" + + # TODO: DEBUG REMOVE + echo "tmp_dir = $tmp_dir" + + # TODO: Error handling below + + # Make sure the temporary directory doesn't already exist + # TODO: Maybe if it does just leave it and use it? If we're targeting a + # commit hash or tag it's unlikely to have changed. Could add a parameter + # to force re-checkout? + #rm -rf "$tmp_dir" + + if [ ! -d "$tmp_dir" ]; then + + local parent_dir="$(dirname "$tmp_dir")" + # Make sure the parent directory exists + if ! mkdir -p "$parent_dir"; then + yell "ERROR: Parent directory '$parent_dir' does not exist" + return 1 + fi + + if ! cp -r "$(pwd)" "$tmp_dir"; then + yell "ERROR: Failed to copy repository to the diff directory" + return 1 + fi + fi + + if ! cd "$tmp_dir"; then + yell "ERROR: Failed to change current working directory to the diff directory" + return 1 + fi + + # TODO: Find a cleaner way to always get back to the previous working + # directory. Maybe just execute all of this in a sub-shell? + + if ! git reset --hard; then + cd - + yell "ERROR: Git reset in diff directory failed" + return 1 + fi + + if ! git clean -fdx; then + cd - + yell "ERROR: Git clean in diff directory failed" + return 1 + fi + + if ! git checkout "$diff_target"; then + cd - + yell "ERROR: Git checkout in diff directory failed" + return 1 + fi + + cd - + + local bcompare_exe + # TODO: If WSL else... + #bcompare_exe="bcompare" + bcompare_exe="BCompare.exe" + + # TODO: Doesn't work on WSL because we need to canonicalize the path so + # that it's a valid Windows network path. + # e.g. /home/pewing/foo -> \\wsl.localhost\Ubuntu-24.04\home\pewing\foo + # I don't feel like trying to do that in bash, maybe we can make a Python + # utility for various path conversions + echo -e "Executing:\n\"$bcompare_exe\" \"$(pwd)\" \"$tmp_dir\"" + #"$bcompare_exe" "$(pwd)" "$tmp_dir" + "$bcompare_exe" \ + "\\\\WSL.LOCALHOST\\Ubuntu-24.04\\home\\pewing\\dot" \ + "\\\\WSL.LOCALHOST\\Ubuntu-24.04\\home\\pewing\\tmp\\${tmp_dir_name}" +} + +git_diff_with "$1" diff --git a/bin/run_vm_guest_additions.sh b/bin/run_vm_guest_additions.sh new file mode 100755 index 0000000..6dbc955 --- /dev/null +++ b/bin/run_vm_guest_additions.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +DOTFILES_LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/logs" +LOG_FILE="run_vm_guest_additions_$(date +"%Y%m%d_%H%M%S").log" +LOG_PATH="${DOTFILES_LOG_DIR}/${LOG_FILE}" + +log() { + echo "$*" >> "$LOG_PATH" +} + +# If spice-vdagent is installed, we're probably running in a Virtual Machine. +# Start it up because it enables clipboard sharing between host and guest. +if command -v spice-vdagent >/dev/null 2>&1; then + log "Found spice-vdagent at: $(command -v spice-vdagent)" + if pgrep -x spice-vdagent >/dev/null; then + log "spice-vdagent already running; skipping" + else + log "Starting spice-vdagent" + spice-vdagent >> "$LOG_PATH" 2>&1 + fi +else + log "spice-vdagent command not found; skipping" +fi diff --git a/bin/set-theme b/bin/set-theme index 03d711f..596a570 100755 --- a/bin/set-theme +++ b/bin/set-theme @@ -48,11 +48,8 @@ function get_background_opacity() { background_opacity="$(get_background_opacity "$theme")" sed -Ei "s/^background_opacity.*/background_opacity $background_opacity/" "$DOTFILES/config/kitty.conf" -# TODO: Either add flavours to provision script or install it here if it's -# missing if ! command -v flavours &>/dev/null; then - flavours_url="https://github.com/Misterio77/flavours/releases/latest" - die "Error: flavours is not installed; download and install it from: $flavours_url" + die "Error: flavours is not installed" fi # Run flavours to render the config templates into all dotfiles diff --git a/cli/README.md b/cli/README.md index bb00a33..4e6bd7b 100644 --- a/cli/README.md +++ b/cli/README.md @@ -4,45 +4,8 @@ This is a Python CLI tool for various dotfiles operations. ## Bash Auto-Completion (Requires Python 3.7+) -The `DotProvisioner` in the CLI automatically sets up Bash auto-completion so just run: - -```bash -dot provision -``` - -Or: - -```bash -dot provision dot -``` - -### Manual Setup - -Install the `argcomplete` python package: - -```bash -python -m pip install argcomplete -``` - -Generate the auto-completion script: - -```bash -export DOT_BASH_COMPLETION="1" -mkdir -p ~/.bash_completion.d -echo "$( - register-python-argcomplete --external-argcomplete-script $DOTFILES/cli/dot.py dot -)" &>~/.bash_completion.d/dot.bash -``` - -Add this to `.bashrc` or similar: - -```bash -export DOT_BASH_COMPLETION="1" -source "$HOME/.bash_completion.d/dot.bash" -``` - -Reload shell config and the auto-completion script should be sourced into the -shell automatically. +The CLI supports auto-completion via `argcomplete` and it should be configured +automatically by Nix home-manager. ## Code Formatting diff --git a/cli/commands/__init__.py b/cli/commands/__init__.py index 55fbe56..21a34ac 100644 --- a/cli/commands/__init__.py +++ b/cli/commands/__init__.py @@ -7,8 +7,6 @@ from .git_sync import add_git_sync_parser from .link import add_link_parser from .lint import add_lint_parser -from .list import add_list_parser -from .provision import add_provision_parser from .status import add_status_parser from .tidy import add_tidy_parser @@ -21,7 +19,5 @@ def add_command_parsers(parser: argparse.ArgumentParser) -> None: add_git_sync_parser(subparsers) add_link_parser(subparsers) add_lint_parser(subparsers) - add_list_parser(subparsers) - add_provision_parser(subparsers) add_status_parser(subparsers) add_tidy_parser(subparsers) diff --git a/cli/commands/list.py b/cli/commands/list.py deleted file mode 100644 index 9fb662f..0000000 --- a/cli/commands/list.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env python - -import argparse - -from lib.provision.system_provisioner import SystemProvisioner - - -def add_list_parser(subparsers: argparse._SubParsersAction) -> None: - parser = subparsers.add_parser("list", help="List the available provisioners") - parser.set_defaults(func=cmd_list) - - -def cmd_list(args: argparse.Namespace) -> None: - component_provisioners = SystemProvisioner.get_provisioner_list() - - # Purposefully use print instead of Log here so output is nicer - print("Component provisioners:") - for component_provisioner in component_provisioners: - print(f"- {component_provisioner}") diff --git a/cli/commands/provision.py b/cli/commands/provision.py deleted file mode 100644 index eb37a31..0000000 --- a/cli/commands/provision.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python - -import argparse -import os -from pathlib import Path - -from lib.common.dir import Dir -from lib.common.distro_info import DistroInformation -from lib.common.log import Log -from lib.common.os import OperatingSystem -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import ProvisionerArgs -from lib.provision.system_provisioner import SystemProvisioner -from lib.provision.tag import Tags - - -def add_provision_parser(subparsers: argparse._SubParsersAction) -> None: - parser = subparsers.add_parser( - "provision", help="Run provisioners to configure components" - ) - # TODO: Implement a confirmation - # parser.add_argument( - # "-y", - # "--yes", - # action="store_true", - # help="Don't ask for confirmation", - # ) - parser.add_argument( - "-d", - "--dry-run", - action="store_true", - help="Print provisioning actions without running them", - ) - parser.add_argument( - "-f", - "--force", - action="store_true", - help=( - "Provisioners may try to detect current state and skip " - "unnecessery steps; this forces those steps to be run" - ), - ) - parser.add_argument( - "-t", - "--tags", - default=Tags.default(), - help="Comma delimited list of tags that influence provisioner behavior [x11|wsl]", - ) - parser.add_argument( - "--no-version-cache", - dest="version_cache", - action="store_false", - default=True, - help="Disable the version cache when checking for latest versions", - ) - parser.add_argument( - "--version-cache-max-age-days", - type=int, - default=7, - metavar="DAYS", - help=( - "Maximum age (in days) for cached version entries. " - "If the cached entry is older than this, the script will attempt " - "to refresh it from the source (default: 7 days)." - ), - ) - parser.add_argument( - "components", - nargs="*", - help="The components to provision; if omitted, all components are provisioned", - ) - parser.set_defaults(func=cmd_provision) - - -def cmd_provision(args: argparse.Namespace) -> None: - if OperatingSystem.get().is_linux(): - if os.getuid() == 0: - raise Exception("do not run as root") - - distro = DistroInformation.get() - Log.info( - "provisioning system", - { - "distro.id": distro.id, - "distro.release": distro.release, - "distro.codename": distro.codename, - }, - ) - - tags = Tags.parse(args.tags) if isinstance(args.tags, str) else args.tags - - VersionCache.init( - args.version_cache, - Path(os.path.join(Dir.dot(), "version_cache.json5")), - args.version_cache_max_age_days, - ) - - provisioner_args = ProvisionerArgs(args.dry_run, tags) - provisioner = SystemProvisioner(provisioner_args, args.components) - provisioner.provision() diff --git a/cli/lib/common/user.py b/cli/lib/common/user.py index e45fdf1..b77ae91 100644 --- a/cli/lib/common/user.py +++ b/cli/lib/common/user.py @@ -68,7 +68,7 @@ def _get_groups_linux(self) -> List[str]: # Convert to platform-agnostic Group type return [Group(g.gr_name, g.gr_gid, g.gr_mem) for g in groups] - # TODO: Remove dry run stuff, make a wrapper in provision lib for that + # TODO: Remove dry run stuff # TODO: I actually don't know if this was even used? I think we used group.py def add_to_group(self, group: str, dry_run: bool) -> None: groups = set(self.get_groups()) diff --git a/cli/lib/provision/__init__.py b/cli/lib/provision/__init__.py deleted file mode 100644 index 4265cc3..0000000 --- a/cli/lib/provision/__init__.py +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env python diff --git a/cli/lib/provision/provisioner.py b/cli/lib/provision/provisioner.py deleted file mode 100644 index ae2038b..0000000 --- a/cli/lib/provision/provisioner.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -from abc import ABC, abstractmethod - -from lib.provision.tag import Tags - - -class ProvisionerArgs: - def __init__(self, dry_run: bool, tags: Tags) -> None: - self.dry_run: bool = dry_run - self.tags: Tags = tags - - -class IProvisioner(ABC): - @abstractmethod - def provision(self) -> None: - pass - - -class IComponentProvisioner(IProvisioner): - def __init__(self) -> None: - pass - - -class ISystemProvisioner(IProvisioner): - def __init__(self) -> None: - pass diff --git a/cli/lib/provision/provisioner_apt.py b/cli/lib/provision/provisioner_apt.py deleted file mode 100644 index bf37747..0000000 --- a/cli/lib/provision/provisioner_apt.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python - -from lib.common.apt import Apt -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs -from lib.provision.tag import Tags - -# fmt: off -APT_PACKAGES = { - "core": [ - "apt-utils", - "ca-certificates", - "curl", - "wget", - "gnupg", - "jq", - "software-properties-common", - "apt-file", - "libfuse2", # This is required to use AppImage - "locate", - "fzf", # TODO: Don't install fzf this way, shell integration broken - "net-tools", - "unzip", - "uchardet", - "dos2unix", - ], - "cli-tools": [ - "make", - "build-essential", - "cmake", - "meson", - "htop", - "iotop", - "git", - "vim", - "universal-ctags", # I think this has better c++11 support - "ranger", - "tmux", - "neofetch", - "id3v2", - "calcurse", - "rxvt-unicode", - "clang", - "clangd", - ], - "python-3": [ - "python3", - "python3-dev", - "python3-pip", - ], - "gui-tools": [ - "fonts-font-awesome", # Used for media buttons on polybar - "rofi", # Fuzzy application launcher - "dunst", # Desktop notifications - "feh", # Set wallpaper - "sxiv", # Image viewer - "nitrogen", # Set wallpaper - "pavucontrol", # Pulse Audio frontend - "compton", # Window compositor - "scrot", # Screen capture - "gucharmap", # Useful for debugging font issues - "keepassxc", # Credential manager - "remmina", # RDP session manager - "usb-creator-gtk", # Easily flash bootable USBs - "i3lock", # Lock screen - "meld", # Diff tool - "xclip", # Clipboard for X11 - "wl-clipboard", # Clipboard for Wayland - "xdotool", # X11 automation tool - "kitty", # Kitty terminal emulator - "kitty-terminfo", # Kitty TERMINFO - "webp", # Command line support for webp image files - ], - "media": [ - "inkscape", # Vector graphics editor - "mpv", # Minimal media player - "vlc", # General purpose FOSS media player - "easytag", # Edit ID3 Tags on MP3 files - "blueman", # Bluetooth device support - ], - "gaming": [ - "steam", - "steam-devices", - ], -} -# fmt: on - - -class AptProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - Apt.update(self._args.dry_run) - Apt.upgrade(self._args.dry_run) - - packages = ( - APT_PACKAGES["core"] + APT_PACKAGES["cli-tools"] + APT_PACKAGES["python-3"] - ) - - if self._args.tags.has(Tags.x11): - packages += APT_PACKAGES["gui-tools"] - packages += APT_PACKAGES["media"] - packages += APT_PACKAGES["gaming"] - - Apt.install(packages, self._args.dry_run) diff --git a/cli/lib/provision/provisioner_docker.py b/cli/lib/provision/provisioner_docker.py deleted file mode 100644 index 37c925f..0000000 --- a/cli/lib/provision/provisioner_docker.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import urllib - -from lib.common.apt import Apt -from lib.common.dir import Dir -from lib.common.distro_info import DistroInformation -from lib.common.group import Group -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.util import download_file, get_current_user -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - - -class DockerPackage: - def __init__(self, url, name, full_name, version): - self.url = url - self.name = name - self.full_name = full_name - self.version = version - self.semver = Semver.parse(version) - - -class DockerProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - distro_info = DistroInformation.get() - - base_url = f"https://download.docker.com/linux/ubuntu/dists/{distro_info.codename}/pool/stable/amd64" - - # TODO: Work in progress detecting latest available package versions - # and comparing against installed versions - installed_packages = Apt.get_installed_packages() - latest_packages = DockerProvisioner._get_latest_package_versions( - base_url, distro_info - ) - for p in latest_packages: - found = False - for i in installed_packages: - if i.name != p.name: - continue - found = True - print(f"{p.name}: {p.version} vs. {i.version}") - if not found: - raise Exception(f"{p.name} not installed") - return - - # TODO: Automatically choose latest version - packages = [ - "containerd.io_1.6.9-1_amd64.deb", - f"docker-ce_24.0.7-1~ubuntu.22.04~{distro_info.codename}_amd64.deb", - f"docker-ce-cli_24.0.7-1~ubuntu.22.04~{distro_info.codename}_amd64.deb", - f"docker-buildx-plugin_0.11.2-1~ubuntu.22.04~{distro_info.codename}_amd64.deb", - f"docker-compose-plugin_2.6.0~ubuntu-{distro_info.codename}_amd64.deb", - ] - - # TODO: Make this a common directory like Dir.tmp() - tmp_dir = os.path.join(Dir.home(), ".tmp") - Shell.mkdir(tmp_dir, True, False, self._args.dry_run) - - for package in packages: - download_file( - f"{base_url}/{package}", - os.path.join(tmp_dir, package), - False, - False, - self._args.dry_run, - ) - - Apt.install_deb_files(packages, self._args.dry_run) - - Group.add_user("docker", get_current_user().pw_name, self._args.dry_run) - - @staticmethod - def _get_latest_package_versions( - base_url: str, distro_info: DistroInformation - ) -> list[DockerPackage]: - version_regex_pattern = "[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+){0,1}" - package_regex_patterns = [ - f".*((containerd\.io)_({version_regex_pattern})_amd64.deb).*", - f".*((docker-ce)_({version_regex_pattern})~ubuntu.{distro_info.release}~{distro_info.codename}_amd64.deb).*", - f".*((docker-ce-cli)_({version_regex_pattern})~ubuntu.{distro_info.release}~{distro_info.codename}_amd64.deb).*", - f".*((docker-buildx-plugin)_({version_regex_pattern})~ubuntu.{distro_info.release}~{distro_info.codename}_amd64.deb).*", - f".*((docker-compose-plugin)_({version_regex_pattern})~ubuntu-{distro_info.codename}_amd64.deb).*", - ] - - def match(line): - for pattern in package_regex_patterns: - m = re.match(pattern, line) - if m is not None: - return m - return None - - response = urllib.request.urlopen(base_url).read().decode("utf-8") - - available_packages = {} - for line in response.split("\n"): - m = match(line) - if m is None: - continue - - full_name = m.group(1) - name = m.group(2) - version = m.group(3) - url = f"{base_url}/{full_name}" - - available_package = DockerPackage(url, name, full_name, version) - - if available_package.name not in available_packages: - available_packages[available_package.name] = [] - available_packages[name].append(available_package) - - latest_packages = [] - for package in available_packages: - sorted_packages = sorted( - available_packages[package], key=lambda p: p.semver, reverse=True - ) - latest_packages.append(sorted_packages[0]) - return latest_packages diff --git a/cli/lib/provision/provisioner_dot.py b/cli/lib/provision/provisioner_dot.py deleted file mode 100644 index 2ba9e63..0000000 --- a/cli/lib/provision/provisioner_dot.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -import os -import subprocess - -from lib.common.dir import Dir -from lib.common.log import Log -from lib.common.util import write_file -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - - -class DotProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - write_file( - os.path.join(Dir.home(), ".bash_completion.d", "dot.bash"), - self._generate_dot_cli_completion_script(), - sudo=False, - dry_run=self._args.dry_run, - ) - - def _generate_dot_cli_completion_script(self) -> str: - cmd = [ - "register-python-argcomplete", - "--external-argcomplete-script", - os.path.join(Dir.dot(), "cli", "dot.py"), - "dot", - ] - - Log.info("generating dot cli completion script", {"command": " ".join(cmd)}) - - if self._args.dry_run: - Log.info( - "skipping dot cli completion script generation", {"reason": "dry run"} - ) - return "" - - p = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True - ) - output, _ = p.communicate() - if p.returncode != 0: - print(p) - raise Exception("register-python-argcomplete script returned non-zero") - return output diff --git a/cli/lib/provision/provisioner_flavours.py b/cli/lib/provision/provisioner_flavours.py deleted file mode 100644 index 645e04c..0000000 --- a/cli/lib/provision/provisioner_flavours.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python - -import os -import subprocess -from typing import Tuple, Union - -from lib.common.archive import Archive -from lib.common.dir import Dir -from lib.common.github import Github -from lib.common.log import Log -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - -FLAVOURS_GITHUB_ORG = "Misterio77" -FLAVOURS_GITHUB_REPO = "flavours" - - -class FlavoursProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - target_release, target_version = FlavoursProvisioner._get_target_version() - - current_version = FlavoursProvisioner._get_current_version() - if current_version is None: - Log.info(f"Flavours is not installed") - elif current_version < target_version: - Log.info( - f"Flavours {current_version} is installed but {target_version} is available" - ) - else: - Log.info(f"Flavours {target_version} is already installed, nothing to do") - return - - tmp_dir = f"{Dir.home()}/Downloads/flavours/{target_release}" - archive_filename = f"flavours-{target_release}-x86_64-linux.tar.gz" - archive_path = os.path.join(tmp_dir, archive_filename) - - base_install_dir = "/opt/flavours" - install_dir = f"/opt/flavours/{target_release}" - symlink_path = "/usr/local/bin/flavours" - - self._download_release_archive(target_release, archive_path) - - Log.info("extracting flavours release archive") - Archive.extract(archive_path, tmp_dir, self._args.dry_run) - - Log.info("deleting flavours release archive") - Shell.rm(archive_path, False, False, False, self._args.dry_run) - - Log.info("creating base install directory", {"path": base_install_dir}) - Shell.mkdir(base_install_dir, True, True, self._args.dry_run) - - Log.info("deleting existing install directory if there is one") - Shell.rm(install_dir, True, True, True, self._args.dry_run) - - Log.info("moving temp directory to install location") - Shell.mv(tmp_dir, install_dir, True, self._args.dry_run) - - Log.info("deleting existing symlink if there is one") - Shell.rm(symlink_path, False, True, True, self._args.dry_run) - - Log.info("creating symlink to executable in install directory") - Shell.ln( - os.path.join(install_dir, "flavours"), - symlink_path, - True, - self._args.dry_run, - ) - - self._flavours_update() - - def _download_release_archive(self, version: str, path: str) -> None: - if os.path.isfile(path): - Log.info("skipping download because file already exists", {"path": path}) - return - - # Make sure the directory we are downloading to exists - Shell.mkdir(os.path.dirname(path), True, False, self._args.dry_run) - - Log.info("downloading flavours release archive") - Github.download_release_artifact( - FLAVOURS_GITHUB_ORG, - FLAVOURS_GITHUB_REPO, - version, - os.path.basename(path), - path, - True, - False, - False, - self._args.dry_run, - ) - - def _flavours_update(self) -> None: - Log.info("running flavours update") - - if self._args.dry_run: - Log.info("skipping flavours update due to --dry-run") - return - - p = subprocess.Popen( - ["flavours", "update", "all"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - # The shell provision script piped both stdout and stderr to /dev/null - # and didn't check exit code. Is that wise? - exit_code = p.wait() - if p.wait() != 0: - Log.warn( - "Flavours update returned non-zero exit code", - {"exit_code": exit_code}, - ) - - @staticmethod - def _get_current_version() -> Union[str, None]: - try: - p = subprocess.Popen( - ["flavours", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - stdout, _ = p.communicate() - if p.returncode != 0: - raise Exception("Flavours returned non-zero exit code") - - version_str = stdout.replace("flavours", "").strip() - return Semver.parse(version_str) - except FileNotFoundError as e: - return None - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - # First check version cache to see if we have a cached version that is - # new enough - cached_version = VersionCache.get_version("flavours") - if cached_version is not None: - Log.info( - "using cached flavours version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_release = Github.get_latest_release( - FLAVOURS_GITHUB_ORG, FLAVOURS_GITHUB_REPO - ) - latest_version = Semver.parse(latest_release) - except Exception as e: - VersionCache.add_failed_attempt( - "flavours", - str(e), - source=f"github:{FLAVOURS_GITHUB_ORG}/{FLAVOURS_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "flavours", - latest_release, - f"github:{FLAVOURS_GITHUB_ORG}/{FLAVOURS_GITHUB_REPO}", - ) - - return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_i3.py b/cli/lib/provision/provisioner_i3.py deleted file mode 100644 index ab4efc7..0000000 --- a/cli/lib/provision/provisioner_i3.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import subprocess -from typing import Tuple, Union - -from lib.common.apt import Apt -from lib.common.git import Git -from lib.common.github import Github -from lib.common.log import Log -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs -from lib.provision.symlink import Symlink -from lib.provision.tag import Tags - -I3_GITHUB_ORG = "i3" -I3_GITHUB_REPO = "i3" - - -# TODO: Move this somewhere so that all provisioners can share it since we've -# duplicated this logic in several places. -def _i3_prepare_install_dir(install_dir: str, create: bool, dry_run: bool) -> None: - Log.info("deleting existing install directory if there is one") - Shell.rm(install_dir, True, True, True, dry_run) - - if create: - Log.info("creating install directory", {"path": install_dir}) - Shell.mkdir(install_dir, True, True, dry_run) - else: - base_install_dir = os.path.dirname(install_dir) - Log.info("creating base install directory", {"path": base_install_dir}) - Shell.mkdir(base_install_dir, True, True, dry_run) - - -def _i3_bootstrap(dry_run: bool): - Log.info("bootstrapping i3") - if dry_run: - Log.info("skipping i3 bootstrap", {"reason": "dry run"}) - return - if subprocess.call(["meson", ".."]) != 0: - raise Exception("meson returned non-zero exit code") - - -def _i3_build(dry_run: bool): - Log.info("building i3") - if dry_run: - Log.info("skipping i3 build", {"reason": "dry run"}) - return - if subprocess.call(["ninja"]) != 0: - raise Exception("ninja returned non-zero exit code") - - -class I3Provisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - super().__init__() - self._args = args - - def provision(self) -> None: - if not self._args.tags.has(Tags.x11): - Log.info("skipping i3 provisioner", {"reason": "x11 tag not present"}) - return - - target_tag_name, target_tag_version = I3Provisioner._get_target_version() - current_version = I3Provisioner._get_current_version() - if current_version is None: - Log.info(f"i3 is not installed") - elif current_version < target_tag_version: - Log.info( - f"i3 {current_version} is installed but {target_tag_version} is available" - ) - else: - Log.info(f"i3 {target_tag_version} is already installed, nothing to do") - return - - # TODO: i3-gaps was merged into i3 as of release 4.22 but the version - # in Apt in Ubuntu 22.04 is still 4.20 so build it from source for now. - # Once we move to 24.04 we can probably just install the apt package - # and remove this whole provisioner. - - # Install pre-requisite packages needed to compile i3 from source - # TODO: I'm not sure if this is still accurate since the merge of - # i3-gaps into i3. For example, i3 builds with meson/ninja now and not - # automake so that is almost certainly not necessary. Since all of this - # will hopefully be deleted soon when we can just install from apt it's - # not worth the effort to clean up. - Apt.install( - [ - "libxcb1-dev", - "libxcb-keysyms1-dev", - "libpango1.0-dev", - "libxcb-util0-dev", - "libxcb-icccm4-dev", - "libyajl-dev", - "libstartup-notification0-dev", - "libxcb-randr0-dev", - "libev-dev", - "libxcb-cursor-dev", - "libxcb-xinerama0-dev", - "libxcb-xkb-dev", - "libxkbcommon-dev", - "libxkbcommon-x11-dev", - "autoconf", - "libxcb-xrm0", - "libxcb-xrm-dev", - "libxcb-shape0", - "libxcb-shape0-dev", - "automake", - ], - self._args.dry_run, - ) - - url = f"https://www.github.com/{I3_GITHUB_ORG}/{I3_GITHUB_REPO}" - staging_dir = f"/home/pewing/.tmp/i3/{target_tag_name}" - build_dir = os.path.join(staging_dir, "build") - cwd = os.getcwd() - - Shell.rm(staging_dir, True, True, False, self._args.dry_run) - repo = Git.clone(url, staging_dir, self._args.dry_run) - repo.checkout(target_tag_name, self._args.dry_run) - Shell.mkdir(build_dir, True, False, self._args.dry_run) - Shell.cd(build_dir, self._args.dry_run) - _i3_bootstrap(self._args.dry_run) - _i3_build(self._args.dry_run) - Shell.cd(cwd, self._args.dry_run) - - install_dir = os.path.join("/opt/i3", target_tag_name) - - _i3_prepare_install_dir(install_dir, False, self._args.dry_run) - Shell.mv(staging_dir, install_dir, True, self._args.dry_run) - - executables = [ - "build/i3", - "build/i3-config-wizard", - "build/i3-dump-log", - "build/i3-input", - "build/i3-msg", - "build/i3-nagbar", - "build/i3bar", - "i3-dmenu-desktop", - "i3-save-tree", - "i3-sensible-editor", - "i3-sensible-pager", - "i3-sensible-terminal", - ] - - Log.info("setting up i3wm symlinks") - for exe in executables: - source = os.path.join(install_dir, exe) - target = os.path.join("/usr/local/bin", os.path.basename(source)) - Symlink.create( - source=source, - target=target, - sudo=True, - dry_run=self._args.dry_run, - ) - - @staticmethod - def _get_latest_tag() -> Tuple[str, Semver]: - tags = Github.get_tags(I3_GITHUB_ORG, I3_GITHUB_REPO) - semver_tags = {} - for tag in tags: - tag_name = tag["name"] - m = re.match("[0-9]+\.[0-9]+(\.[0-9]){0,1}", tag_name) - if m is not None: - semver_tags[tag_name] = Semver.parse(tag_name) - return sorted(semver_tags.items(), reverse=True)[0] - - @staticmethod - def _get_current_version() -> Union[str, None]: - try: - p = subprocess.Popen( - ["i3", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - stdout, _ = p.communicate() - if p.returncode != 0: - raise Exception("i3 returned non-zero exit code") - m = re.match("i3 version ([0-9]+\.[0-9]+\.[0-9]+)", stdout) - if m is None: - return None - return Semver.parse(m.group(1)) - except FileNotFoundError as e: - return None - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - cached_version = VersionCache.get_version("i3") - if cached_version is not None: - Log.info( - "using cached i3 version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_tag_name, latest_tag_version = I3Provisioner._get_latest_tag() - except Exception as e: - VersionCache.add_failed_attempt( - "i3", - str(e), - source=f"github:{I3_GITHUB_ORG}/{I3_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "i3", - latest_tag_name, - f"github:{I3_GITHUB_ORG}/{I3_GITHUB_REPO}", - ) - - return latest_tag_name, latest_tag_version diff --git a/cli/lib/provision/provisioner_kitty.py b/cli/lib/provision/provisioner_kitty.py deleted file mode 100644 index d63a5a4..0000000 --- a/cli/lib/provision/provisioner_kitty.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import subprocess -from typing import Tuple, Union - -from lib.common.archive import Archive -from lib.common.dir import Dir -from lib.common.github import Github -from lib.common.log import Log -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - -KITTY_GITHUB_ORG = "kovidgoyal" -KITTY_GITHUB_REPO = "kitty" - - -class KittyProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - target_release, target_version = KittyProvisioner._get_target_version() - - current_version = KittyProvisioner._get_current_version() - if current_version is None: - Log.info(f"Kitty is not installed") - elif current_version < target_version: - Log.info( - f"Kitty {current_version} is installed but {target_version} is available" - ) - else: - Log.info(f"Kitty {target_version} is already installed, nothing to do") - return - - tmp_dir = f"{Dir.home()}/Downloads/kitty/{target_version}" - archive_filename = f"kitty-{target_release.replace('v', '')}-x86_64.txz" - archive_path = os.path.join(tmp_dir, archive_filename) - - base_install_dir = "/opt/kitty" - install_dir = f"/opt/kitty/{target_version}" - symlink_path_kitty = "/usr/local/bin/kitty" - symlink_path_kitten = "/usr/local/bin/kitten" - - Log.info("downloading kitty release archive") - Github.download_release_artifact( - KITTY_GITHUB_ORG, - KITTY_GITHUB_REPO, - target_release, - archive_filename, - archive_path, - True, - False, - False, - self._args.dry_run, - ) - - Log.info("extracting kitty release archive") - Archive.extract(archive_path, tmp_dir, self._args.dry_run) - - Log.info("deleting kitty release archive") - Shell.rm(archive_path, False, False, False, self._args.dry_run) - - Log.info("creating base install directory", {"path": base_install_dir}) - Shell.mkdir(base_install_dir, True, True, self._args.dry_run) - - Log.info("deleting existing install directory if there is one") - Shell.rm(install_dir, True, True, True, self._args.dry_run) - - Log.info("moving temp directory to install location") - Shell.mv(tmp_dir, install_dir, True, self._args.dry_run) - - Log.info("deleting existing symlinks") - Shell.rm(symlink_path_kitty, False, True, True, self._args.dry_run) - Shell.rm(symlink_path_kitten, False, True, True, self._args.dry_run) - - Log.info("creating symlinks to executables in install directory") - Shell.ln( - os.path.join(install_dir, "bin/kitty"), - symlink_path_kitty, - True, - self._args.dry_run, - ) - Shell.ln( - os.path.join(install_dir, "bin/kitten"), - symlink_path_kitten, - True, - self._args.dry_run, - ) - - @staticmethod - def _get_current_version() -> Union[str, None]: - try: - p = subprocess.Popen( - ["kitty", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - stdout, _ = p.communicate() - if p.returncode != 0: - raise Exception("Kitty returned non-zero exit code") - m = re.match("kitty ([0-9]+.[0-9]+.[0-9]+) created by Kovid Goyal", stdout) - if m is None: - return None - return Semver.parse(m.group(1)) - except FileNotFoundError as e: - return None - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - cached_version = VersionCache.get_version("kitty") - if cached_version is not None: - Log.info( - "using cached kitty version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_release = Github.get_latest_release(KITTY_GITHUB_ORG, KITTY_GITHUB_REPO) - latest_version = Semver.parse(latest_release) - except Exception as e: - VersionCache.add_failed_attempt( - "kitty", - str(e), - source=f"github:{KITTY_GITHUB_ORG}/{KITTY_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "kitty", - latest_release, - f"github:{KITTY_GITHUB_ORG}/{KITTY_GITHUB_REPO}", - ) - - return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_neovim.py b/cli/lib/provision/provisioner_neovim.py deleted file mode 100644 index d48138b..0000000 --- a/cli/lib/provision/provisioner_neovim.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import subprocess -from typing import Tuple, Union - -from lib.common.alternatives import Alternatives -from lib.common.dir import Dir -from lib.common.github import Github -from lib.common.log import Log -from lib.common.pip import Pip -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - -NEOVIM_GITHUB_ORG = "neovim" -NEOVIM_GITHUB_REPO = "neovim" - - -class NeovimProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - target_release, target_version = NeovimProvisioner._get_target_version() - - current_version = NeovimProvisioner._get_current_version() - if current_version is None: - Log.info(f"Neovim is not installed") - elif current_version < target_version: - Log.info( - f"Neovim {current_version} is installed but {target_version} is available" - ) - else: - Log.info(f"Neovim {target_version} is already installed, nothing to do") - return - - tmp_dir = f"{Dir.home()}/Downloads/neovim/{target_version}" - appimage_filename = "nvim-linux-x86_64.appimage" - appimage_path = os.path.join(tmp_dir, appimage_filename) - - base_install_dir = "/opt/neovim" - install_dir = f"/opt/neovim/{target_version}" - install_path = os.path.join(install_dir, appimage_filename) - symlink_path = "/usr/local/bin/nvim" - - self._download_release_appimage(target_version, appimage_path) - - Log.info("deleting existing install directory if there is one") - Shell.rm(install_dir, True, True, True, self._args.dry_run) - - Log.info("creating install directory", {"path": install_dir}) - Shell.mkdir(install_dir, True, True, self._args.dry_run) - - Log.info("moving appimage to install location") - Shell.mv(appimage_path, install_path, True, self._args.dry_run) - - Log.info("making appimage file executable") - Shell.chmod("+x", install_path, False, self._args.dry_run) - - Log.info("deleting existing symlink") - Shell.rm(symlink_path, False, True, True, self._args.dry_run) - - Log.info("creating symlinks to executables in install directory") - Shell.ln(install_path, symlink_path, True, self._args.dry_run) - - Log.info("installing pynvim python modules") - Pip.install(["pynvim"], True, True, self._args.dry_run) - - Log.info("updating alternatives to use nvim") - Alternatives.install( - "/usr/bin/vi", "vi", symlink_path, 60, True, self._args.dry_run - ) - Alternatives.set("vi", symlink_path, True, self._args.dry_run) - Alternatives.install( - "/usr/bin/vim", "vim", symlink_path, 60, True, self._args.dry_run - ) - Alternatives.set("vim", symlink_path, True, self._args.dry_run) - Alternatives.install( - "/usr/bin/editor", "editor", symlink_path, 60, True, self._args.dry_run - ) - Alternatives.set("editor", symlink_path, True, self._args.dry_run) - - def _download_release_appimage(self, version: str, path: str) -> None: - if os.path.isfile(path): - Log.info("skipping download because file already exists", {"path": path}) - return - - # Make sure the directory we are downloading to exists - Shell.mkdir(os.path.dirname(path), True, False, self._args.dry_run) - - Log.info("downloading neovim release appimage") - Github.download_release_artifact( - NEOVIM_GITHUB_ORG, - NEOVIM_GITHUB_REPO, - version, - os.path.basename(path), - path, - True, - False, - False, - self._args.dry_run, - ) - - @staticmethod - def _get_current_version() -> Union[str, None]: - try: - p = subprocess.Popen( - ["nvim", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - stdout, _ = p.communicate() - if p.returncode != 0: - raise Exception("Neovim returned non-zero exit code") - m = re.match("NVIM (v[0-9]+\.[0-9]+\.[0-9]+)", stdout) - if m is None: - return None - return Semver.parse(m.group(1)) - except FileNotFoundError as e: - return None - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - cached_version = VersionCache.get_version("neovim") - if cached_version is not None: - Log.info( - "using cached neovim version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_release = Github.get_latest_release( - NEOVIM_GITHUB_ORG, NEOVIM_GITHUB_REPO - ) - latest_version = Semver.parse(latest_release) - except Exception as e: - VersionCache.add_failed_attempt( - "neovim", - str(e), - source=f"github:{NEOVIM_GITHUB_ORG}/{NEOVIM_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "neovim", - latest_release, - f"github:{NEOVIM_GITHUB_ORG}/{NEOVIM_GITHUB_REPO}", - ) - - return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_nodejs.py b/cli/lib/provision/provisioner_nodejs.py deleted file mode 100644 index 8b0cdfd..0000000 --- a/cli/lib/provision/provisioner_nodejs.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python - -import os -import subprocess -from typing import Tuple - -from lib.common.archive import Archive -from lib.common.dir import Dir -from lib.common.github import Github -from lib.common.log import Log -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.typing import StringOrNone -from lib.common.util import download_file -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - -NODEJS_GITHUB_ORG = "nodejs" -NODEJS_GITHUB_REPO = "node" - - -class NodeJSProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - # Get the currently installed nodejs version - current_nodejs_version = NodeJSProvisioner._get_current_version() - Log.info( - "identified current nodejs version", - { - "version": current_nodejs_version, - } - ) - - target_release, target_version = NodeJSProvisioner._get_target_version() - - # TODO: Make a utility function for this logic? - if current_nodejs_version is None: - Log.info(f"nodejs is not installed") - elif current_nodejs_version < target_version: - Log.info( - f"nodejs {current_nodejs_version} is installed but {target_version} is available" - ) - else: - Log.info( - f"nodejs {target_version} is already installed, nothing to do" - ) - return - - self._install(target_version) - - def _install(self, version: str) -> None: - staging_dir = Dir.staging("nodejs", str(version)) - install_dir = Dir.install("nodejs", str(version)) - - nodejs_archive_name = f"node-{version}-linux-x64.tar.xz" - nodejs_archive_url = f"https://nodejs.org/dist/{version}/{nodejs_archive_name}" - nodejs_archive_path = f"{staging_dir}/{nodejs_archive_name}" - - # Download nodejs release tarball - download_file( - nodejs_archive_url, nodejs_archive_path, False, False, self._args.dry_run - ) - - # Extract node tarball - Log.info( - "extracting nodejs release archive", - { "archive": nodejs_archive_path, "dst": staging_dir }, - ) - Archive.extract(nodejs_archive_path, staging_dir, self._args.dry_run) - - # Move to install directory - Shell.mkdir(os.path.dirname(install_dir), True, True, self._args.dry_run) - Shell.mv( - os.path.join(staging_dir, f"node-{version}-linux-x64"), - install_dir, - True, - self._args.dry_run, - ) - - nodejs_executables = ["corepack", "node", "npm", "npx"] - - Log.info( - "creating nodejs executable symlinks", { "executables": nodejs_executables } - ) - - # TODO: We should make a Symlink.create() method that handles deleting existing links and whatnot - for exe in nodejs_executables: - symlink_src_path = os.path.join(install_dir, "bin", exe) - symlink_dst_path = os.path.join("/usr/local/bin", exe) - Shell.rm(symlink_dst_path, False, True, True, self._args.dry_run) - Shell.ln(symlink_src_path, symlink_dst_path, True, self._args.dry_run) - - @staticmethod - def _get_current_version() -> StringOrNone: - try: - p = subprocess.Popen( - ["node", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - stdout, _ = p.communicate() - if p.returncode != 0: - raise Exception("node returned non-zero exit code") - - version_str = stdout.strip() - return Semver.parse(version_str) - except FileNotFoundError as e: - return None - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - cached_version = VersionCache.get_version("nodejs") - if cached_version is not None: - Log.info( - "using cached nodejs version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_release = Github.get_latest_release(NODEJS_GITHUB_ORG, NODEJS_GITHUB_REPO) - latest_version = Semver.parse(latest_release) - except Exception as e: - VersionCache.add_failed_attempt( - "nodejs", - str(e), - source=f"github:{NODEJS_GITHUB_ORG}/{NODEJS_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "nodejs", - latest_release, - f"github:{NODEJS_GITHUB_ORG}/{NODEJS_GITHUB_REPO}", - ) - - return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_pip.py b/cli/lib/provision/provisioner_pip.py deleted file mode 100644 index 6e7ec53..0000000 --- a/cli/lib/provision/provisioner_pip.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python - -from lib.common.pip import Pip -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - -_PIP_PACKAGES = { - "core": [ - "black", - "mypy", - "isort", - "flake8", - "autoflake", - "ruff", - "argcomplete", - ], -} - - -class PipProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - packages = _PIP_PACKAGES["core"] - Pip.install( - packages=packages, - upgrade=True, - sudo=False, - dry_run=self._args.dry_run, - ) diff --git a/cli/lib/provision/provisioner_ripgrep.py b/cli/lib/provision/provisioner_ripgrep.py deleted file mode 100644 index 7d033eb..0000000 --- a/cli/lib/provision/provisioner_ripgrep.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import subprocess -from typing import Tuple, Union - -from lib.common.apt import Apt -from lib.common.dir import Dir -from lib.common.github import Github -from lib.common.log import Log -from lib.common.semver import Semver -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - -RIPGREP_GITHUB_ORG = "BurntSushi" -RIPGREP_GITHUB_REPO = "ripgrep" - - -class RipgrepProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - latest_release, _ = RipgrepProvisioner._get_target_version() - - # TODO: Standardize and share this behavior since it's the same in most provisioners - # Maybe have like a `get_action` function on provisioners that returns one of three possible actions: - # No-op, Update, Install, and a reason - current_version = RipgrepProvisioner._get_current_version() - if current_version is None: - Log.info(f"ripgrep is not installed") - elif current_version < Semver.parse(latest_release): - Log.info( - f"ripgrep {current_version} is installed but {latest_release} is available" - ) - else: - Log.info(f"ripgrep {latest_release} is already installed, nothing to do") - return - - staging_dir = Dir.staging("ripgrep", latest_release) - - deb_name = f"ripgrep_{latest_release}-1_amd64.deb" - deb_path = os.path.join(staging_dir, deb_name) - - Github.download_release_artifact( - RIPGREP_GITHUB_ORG, - RIPGREP_GITHUB_REPO, - latest_release, - deb_name, - deb_path, - True, - False, - False, - self._args.dry_run, - ) - - Apt.install_deb_files([deb_path], self._args.dry_run) - - @staticmethod - def _get_current_version() -> Union[str, None]: - try: - p = subprocess.Popen( - ["rg", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - stdout, _ = p.communicate() - if p.returncode != 0: - raise Exception("ripgrep returned non-zero exit code") - m = re.match("ripgrep ([0-9]+\.[0-9]+\.[0-9]+)", stdout) - if m is None: - return None - return Semver.parse(m.group(1)) - except FileNotFoundError as e: - return None - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - cached_version = VersionCache.get_version("ripgrep") - if cached_version is not None: - Log.info( - "using cached ripgrep version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_release = Github.get_latest_release( - RIPGREP_GITHUB_ORG, RIPGREP_GITHUB_REPO - ) - latest_version = Semver.parse(latest_release) - except Exception as e: - VersionCache.add_failed_attempt( - "ripgrep", - str(e), - source=f"github:{RIPGREP_GITHUB_ORG}/{RIPGREP_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "ripgrep", - latest_release, - f"github:{RIPGREP_GITHUB_ORG}/{RIPGREP_GITHUB_REPO}", - ) - - return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_treesitter.py b/cli/lib/provision/provisioner_treesitter.py deleted file mode 100644 index 4d34b8e..0000000 --- a/cli/lib/provision/provisioner_treesitter.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import subprocess -from typing import Tuple, Union - -from lib.common.dir import Dir -from lib.common.github import Github -from lib.common.log import Log -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs - -TREE_SITTER_GITHUB_ORG = "tree-sitter" -TREE_SITTER_GITHUB_REPO = "tree-sitter" - - -# TODO: Move this somewhere so that all provisioners can share it since we've -# duplicated this logic in several places -def prepare_install_dir(install_dir: str, create: bool, dry_run: bool) -> None: - Log.info("deleting existing install directory if there is one") - Shell.rm(install_dir, True, True, True, dry_run) - - if create: - Log.info("creating install directory", {"path": install_dir}) - Shell.mkdir(install_dir, True, True, dry_run) - else: - base_install_dir = os.path.dirname(install_dir) - Log.info("creating base install directory", {"path": base_install_dir}) - Shell.mkdir(base_install_dir, True, True, dry_run) - - -class TreeSitterProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - _, target_version = TreeSitterProvisioner._get_target_version() - - current_version = TreeSitterProvisioner._get_current_version() - if current_version is None: - Log.info(f"tree-sitter is not installed") - elif current_version < target_version: - Log.info( - f"tree-sitter {current_version} is installed but {target_version} is available" - ) - else: - Log.info( - f"tree-sitter {target_version} is already installed, nothing to do" - ) - return - - staging_dir = Dir.staging("tree-sitter", str(target_version)) - install_dir = Dir.install("tree-sitter", str(target_version)) - - exe_name = "tree-sitter-linux-x64" - exe_path_staging = os.path.join(staging_dir, exe_name) - exe_path_install = os.path.join(install_dir, exe_name) - - zip_name = f"{exe_name}.gz" - zip_path_staging = os.path.join(staging_dir, zip_name) - - symlink_path = "/usr/local/bin/tree-sitter" - - self._download_release_zip(str(target_version), zip_path_staging) - - TreeSitterProvisioner._unzip_executable(zip_path_staging, self._args.dry_run) - - prepare_install_dir(install_dir, True, self._args.dry_run) - - Log.info("moving executable to installation directory") - Shell.mv(exe_path_staging, exe_path_install, True, self._args.dry_run) - - Log.info("making file executable") - Shell.chmod("+x", exe_path_install, True, self._args.dry_run) - - Log.info("deleting existing symlink if there is one") - Shell.rm(symlink_path, False, True, True, self._args.dry_run) - - Log.info("creating symlink to executable in install directory") - Shell.ln(exe_path_install, symlink_path, True, self._args.dry_run) - - def _download_release_zip(self, version: str, path: str) -> None: - if os.path.isfile(path): - Log.info("skipping download because file already exists", {"path": path}) - return - - Log.info("downloading tree-sitter release archive") - Github.download_release_artifact( - TREE_SITTER_GITHUB_ORG, - TREE_SITTER_GITHUB_REPO, - version, - os.path.basename(path), - path, - True, - False, - False, - self._args.dry_run, - ) - - @staticmethod - def _unzip_executable(zip_path: str, dry_run: bool) -> None: - Log.info("unzipping zip file", {"path": zip_path}) - if dry_run: - Log.info("skipping apt update due to --dry-run") - else: - if subprocess.call(["gunzip", zip_path]) != 0: - raise Exception("Failed to unzip tree-sitter executable") - - @staticmethod - def _get_latest_release() -> Tuple[str, Semver]: - releases = Github.get_releases( - TREE_SITTER_GITHUB_ORG, - TREE_SITTER_GITHUB_REPO, - ) - - # Collect (tag, semver) pairs, skipping pre-releases - parsed: list[Tuple[str, Semver]] = [] - for r in releases: - tag = r.get("tag_name") - if not tag: - continue - if "pre-release" in tag: - continue - - semver = Semver.parse(tag) - if semver is None: - continue - - parsed.append((tag, semver)) - - if not parsed: - raise RuntimeError("No valid semver releases found") - - # Sort by Semver descending - parsed.sort(key=lambda x: x[1], reverse=True) - return parsed[0] - - @staticmethod - def _get_current_version() -> Union[str, None]: - try: - p = subprocess.Popen( - ["tree-sitter", "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - stdout, _ = p.communicate() - if p.returncode != 0: - raise Exception("tree-sitter returned non-zero exit code") - m = re.match("tree-sitter ([0-9]\.[0-9]+\.[0-9]+)", stdout) - if m is None: - return None - return Semver.parse(m.group(1)) - except FileNotFoundError as e: - return None - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - # TODO: v0.26+ doesn't work on Ubuntu 22.04 due to glibc version - # issues, so force v0.25 for now. We could do something like `if - # ubuntu_major_version < 24` but I didn't bother - Log.warn("forcing tree-sitter version 0.25.10 due to compatibility issues") - return "v0.25.10", Semver.parse("v0.25.10") - - cached_version = VersionCache.get_version("tree-sitter") - if cached_version is not None: - Log.info( - "using cached tree-sitter version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_release, latest_version = TreeSitterProvisioner._get_latest_release() - except Exception as e: - VersionCache.add_failed_attempt( - "tree-sitter", - str(e), - source=f"github:{TREE_SITTER_GITHUB_ORG}/{TREE_SITTER_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "tree-sitter", - latest_release, - f"github:{TREE_SITTER_GITHUB_ORG}/{TREE_SITTER_GITHUB_REPO}", - ) - - return latest_release, latest_version diff --git a/cli/lib/provision/provisioner_win32yank.py b/cli/lib/provision/provisioner_win32yank.py deleted file mode 100644 index f11c548..0000000 --- a/cli/lib/provision/provisioner_win32yank.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python - -import os -from typing import Tuple, Union - -from lib.common.archive import Archive -from lib.common.dir import Dir -from lib.common.github import Github -from lib.common.log import Log -from lib.common.semver import Semver -from lib.common.shell import Shell -from lib.common.util import write_file -from lib.common.version_cache import VersionCache -from lib.provision.provisioner import IComponentProvisioner, ProvisionerArgs -from lib.provision.tag import Tags - -WIN32YANK_GITHUB_ORG = "equalsraf" -WIN32YANK_GITHUB_REPO = "win32yank" - - -class Win32YankProvisioner(IComponentProvisioner): - def __init__(self, args: ProvisionerArgs) -> None: - self._args = args - - def provision(self) -> None: - if not self._args.tags.has(Tags.wsl): - Log.info( - "skipping win32yank provisioner", {"reason": "wsl tag not present"} - ) - return - - # There's an open issue as of implementing this on 2024-05-20 where - # running win32yank.exe from path within the WSL file system is very slow: - # https://github.com/equalsraf/win32yank/issues/22 - # To avoid that, make sure we install this on the Windows C: drive. Set - # this before calling _get_current_version() since we need it to - # construct the path to the version file. - self._install_dir = f"/mnt/c/bin/" - - _, target_version = Win32YankProvisioner._get_target_version() - - # TODO: Gross but theres' no way to get the version from the - # executable. It doesn't have a `--version` flag and the - # ProductInfo.VersionInfo metadata on the exe isn't populated. - # As a hack, just write the version installed to a text file next to - # the exe called win32yank_version.txt and check that. - - current_version = self._get_current_version() - if current_version is None: - Log.info(f"Win32Yank is not installed") - elif current_version < target_version: - Log.info( - f"Win32Yank {current_version} is installed but {target_version} is available" - ) - else: - Log.info(f"Win32Yank {target_version} is already installed, nothing to do") - return - - self._install(target_version) - - def _install(self, version: str) -> None: - self._staging_dir = Dir.staging("win32yank", str(version)) - - self._archive_name = f"win32yank-x64.zip" - self._archive_url = f"https://github.com/equalsraf/win32yank/releases/download/${version}/{self._archive_name}" - self._archive_path = f"{self._staging_dir}/{self._archive_name}" - - Github.download_release_artifact( - org=WIN32YANK_GITHUB_ORG, - repo=WIN32YANK_GITHUB_REPO, - release=version, - file=self._archive_name, - path=self._archive_path, - create_dir=True, - sudo=False, - force=False, - dry_run=self._args.dry_run, - ) - - Log.info("extracting win32yank release archive") - Archive.extract(self._archive_path, self._staging_dir, self._args.dry_run) - - Log.info("creating win32yank install directory", {"path": self._install_dir}) - Shell.mkdir( - path=self._install_dir, - exist_ok=True, - sudo=False, - dry_run=self._args.dry_run, - ) - - Log.info("moving win32yank.exe to install location") - Shell.mv( - src=os.path.join(self._staging_dir, "win32yank.exe"), - dst=os.path.join(self._install_dir, "win32yank.exe"), - sudo=False, - dry_run=self._args.dry_run, - ) - - Log.info("deleting win32yank staging directory") - Shell.rm( - path=self._staging_dir, - recursive=True, - force=True, - sudo=False, - dry_run=self._args.dry_run, - ) - - self._write_version_file(version) - - def _write_version_file(self, version: str) -> None: - Log.info( - "writing version file", - {"path": self._version_file_path(), "version": version}, - ) - write_file( - path=self._version_file_path(), - content=str(version), - sudo=False, - dry_run=self._args.dry_run, - ) - - def _read_version_file(self) -> Union[str, None]: - Log.info("reading version file", {"path": self._version_file_path()}) - if not os.path.isfile(self._version_file_path()): - return None - with open(self._version_file_path(), "r") as f: - return f.read() - - def _version_file_path(self) -> str: - return os.path.join(self._install_dir, "win32yank_version.txt") - - def _get_current_version(self) -> Union[str, None]: - version_str = self._read_version_file() - if version_str is None: - return None - return Semver.parse(version_str) - - @staticmethod - def _get_target_version() -> Tuple[str, Semver]: - cached_version = VersionCache.get_version("win32yank") - if cached_version is not None: - Log.info( - "using cached win32yank version", - { - "version": cached_version["version"], - "last_attempt": cached_version.get("last_attempt"), - }, - ) - return cached_version["version"], Semver.parse(cached_version["version"]) - - try: - latest_release = Github.get_latest_release( - WIN32YANK_GITHUB_ORG, WIN32YANK_GITHUB_REPO - ) - latest_version = Semver.parse(latest_release) - except Exception as e: - VersionCache.add_failed_attempt( - "win32yank", - str(e), - source=f"github:{WIN32YANK_GITHUB_ORG}/{WIN32YANK_GITHUB_REPO}", - ) - raise - - VersionCache.update_version( - "win32yank", - latest_release, - f"github:{WIN32YANK_GITHUB_ORG}/{WIN32YANK_GITHUB_REPO}", - ) - - return latest_release, latest_version - diff --git a/cli/lib/provision/symlink.py b/cli/lib/provision/symlink.py deleted file mode 100644 index cf36e17..0000000 --- a/cli/lib/provision/symlink.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python - -import os - -from lib.common.log import Log -from lib.common.shell import Shell - - -class Symlink: - @staticmethod - def create(source: str, target: str, sudo: bool, dry_run: bool) -> None: - Log.info("creating symlink", {"source": source, "target": target}) - - if dry_run: - Log.info("skipping symlink creation", {"reason": "dry run"}) - return - - if not os.path.isfile(source): - raise Exception(f"Symlink source doesn't exist: {source}") - - # Delete existing symlink target if there is one - Shell.rm( - path=target, - recursive=False, - force=True, - sudo=sudo, - dry_run=dry_run, - ) - - # Create the symlink - Shell.ln( - source=source, - target=target, - sudo=sudo, - dry_run=dry_run, - ) diff --git a/cli/lib/provision/system_provisioner.py b/cli/lib/provision/system_provisioner.py deleted file mode 100644 index b117e6c..0000000 --- a/cli/lib/provision/system_provisioner.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python - - -from lib.common.log import Log -from lib.provision.provisioner import ISystemProvisioner, ProvisionerArgs -from lib.provision.provisioner_apt import AptProvisioner -from lib.provision.provisioner_dot import DotProvisioner -from lib.provision.provisioner_flavours import FlavoursProvisioner -from lib.provision.provisioner_i3 import I3Provisioner -from lib.provision.provisioner_kitty import KittyProvisioner -from lib.provision.provisioner_neovim import NeovimProvisioner -from lib.provision.provisioner_nodejs import NodeJSProvisioner -from lib.provision.provisioner_pip import PipProvisioner -from lib.provision.provisioner_ripgrep import RipgrepProvisioner -from lib.provision.provisioner_treesitter import TreeSitterProvisioner -from lib.provision.provisioner_win32yank import Win32YankProvisioner - -# As of Python 3.7: -# > The insertion-order preservation nature of dict objects has been declared -# > to be an official part of the Python language spec. -# -# So, these will remain in order when iterating through the map which is -# important because some component provisioners may depend on others having -# already run, such as an apt update. We could enforce this more explicitly -# through a dependency system but that seems like overkill. -# -# TODO: I don't love how this is setup up. Right now this file shouldn't need -# to import all of the component provisioner types. Maybe we can make like a -# ComponentProvisionerRegistry and all of them can register themselves and then -# this class can just grab them from the registry. -# -# fmt: off -_COMPONENT_PROVISIONERS = { - "apt": AptProvisioner, - # TODO: Commenting out for now because this is WIP; currently raises an - # exception when Docker isn't already installed - #"docker": DockerProvisioner, - "kitty": KittyProvisioner, - "flavours": FlavoursProvisioner, - "neovim": NeovimProvisioner, - "tree-sitter": TreeSitterProvisioner, - "ripgrep": RipgrepProvisioner, - "i3": I3Provisioner, - "nodejs": NodeJSProvisioner, - "pip": PipProvisioner, - "dot": DotProvisioner, - "win32yank": Win32YankProvisioner, - # TODO: install_cava "$cache_dir" - # TODO: install_youtube-dl "$cache_dir" "$bin_dir" - # TODO: install_wpr "$cache_dir" "$bin_dir" - # TODO: install_mpd - # TODO: install_ncmpcpp -} -# fmt: on - - -def _all_components() -> list[str]: - return _COMPONENT_PROVISIONERS.keys() - - -class SystemProvisioner(ISystemProvisioner): - def __init__(self, args: ProvisionerArgs, components: list[str]) -> None: - self._args = args - self._components = components if len(components) > 0 else _all_components() - - def provision(self) -> None: - component_provisioners = {} - for component in self._components: - if component not in _COMPONENT_PROVISIONERS: - raise Exception(f"component not supported: {component}") - component_provisioners[component] = _COMPONENT_PROVISIONERS[component]( - self._args - ) - - for component in component_provisioners: - Log.info("provisioning component", {"component": component}) - component_provisioners[component].provision() - - @staticmethod - def get_provisioner_list() -> list[str]: - return _all_components() diff --git a/cli/lib/provision/tag.py b/cli/lib/provision/tag.py deleted file mode 100644 index 673dcba..0000000 --- a/cli/lib/provision/tag.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python - -from lib.common.os import OperatingSystem - - -class Tag: - def __init__(self, name: str) -> None: - self.name = name - - -class Tags: - x11 = Tag("x11") - wsl = Tag("wsl") - - def __init__(self, tags: list[Tag]) -> None: - self.tags = tags - - def has(self, tag: Tag) -> bool: - return any(t.name == tag.name for t in self.tags) - - @staticmethod - def default() -> "Tags": - tags = [] - - os = OperatingSystem.get() - - if os.is_wsl(): - tags.append(Tags.wsl) - - # TODO: Detect X11 - if not os.is_wsl(): - tags.append(Tags.x11) - - return Tags(tags) - - @staticmethod - def parse(tag_names: str) -> "Tags": - return Tags([Tag(tag_name) for tag_name in tag_names.split(",")]) diff --git a/config/bash/aliases.sh b/config/bash/aliases.sh index ce5cf74..482107f 100644 --- a/config/bash/aliases.sh +++ b/config/bash/aliases.sh @@ -34,8 +34,8 @@ function set_alias() { alias "$name=$command" } -# Dotfiles CLI -set_alias '1' 'dot' '$DOTFILES/cli/dot.py' +# Re-apply dotfiles +set_alias '0' 'df_apply' '$DOTFILES/apply.sh' # Edit/reload bash configs set_alias '0' 'aliases' '$EDITOR $DOTFILES/config/bash/aliases.sh' @@ -43,6 +43,7 @@ set_alias '0' 'functions' '$EDITOR $DOTFILES/config/bash/functions.sh' set_alias '0' 'reload_aliases' 'source $DOTFILES/config/bash/aliases.sh' set_alias '0' 'reload_functions' 'source $DOTFILES/config/bash/functions.sh' set_alias '0' 'localrc' '$EDITOR $HOME/.localrc' +set_alias '0' 'df_logs' '$EDITOR $HOME/.local/state/dotfiles/logs' # Basic aliases set_alias '0' 'cl' 'clear' diff --git a/config/bash/core.sh b/config/bash/core.sh index ecdec2c..f0b6e59 100644 --- a/config/bash/core.sh +++ b/config/bash/core.sh @@ -6,7 +6,7 @@ if [ -z "$DOT_CORE_SOURCED" ]; then DOT_CORE_SOURCED=1 function _is_wsl() { - [ -n "$WSL_DISTRO_NAME" ] && return 0 || return 1 + [ -n "${WSL_DISTRO_NAME-}" ] && return 0 || return 1 } # Using `command -v foo` on WSL is very slow. I profiled this on 2024/03/08 diff --git a/config/bash/functions.sh b/config/bash/functions.sh index 07bc06f..550c517 100644 --- a/config/bash/functions.sh +++ b/config/bash/functions.sh @@ -725,3 +725,37 @@ function merge_pdfs() pdftk "$input1" "$input2" cat output "$output" } + +str_contains() +{ + local string="$1" + local substring="$2" + if test "${string#*$substring}" != "$string" + then + return 0 # $substring is in $string + else + return 1 # $substring is not in $string + fi +} + +# Clone a Github repository to the specified path or a reasonable default if no +# path is provided +gh_clone() +{ + local org="$1" + local repo="$2" + local path="$3" + + local url="https://github.com/$org/$repo" + + if [ -z "$path" ]; then + path="$HOME/src/github/$org/$repo" + fi + + if [ -e "$path" ]; then + yell "ERROR: Target directory '$path' already exists" + return 1 + fi + + git clone "$url" "$path" +} diff --git a/config/bashrc b/config/bashrc index f61cc5e..c9ff3d7 100644 --- a/config/bashrc +++ b/config/bashrc @@ -15,14 +15,22 @@ src_if_exists() { } declare -a sources=( - "$HOME/.fzf.bash" "$HOME/.localrc" + + # No need to filter this out on non-WSL hosts because Nix won't link it + "$HOME/.wslrc" ) for i in "${sources[@]}"; do src_if_exists "$i" done +# Source fzf shell integrations if we have them +if command -v fzf-share >/dev/null; then + source "$(fzf-share)/key-bindings.bash" + source "$(fzf-share)/completion.bash" +fi + # Prompt black="\[\033[30m\]" red="\[\033[31m\]" @@ -36,6 +44,12 @@ none="\[\033[00m\]" PS1="${white}[${green}\u${white}@${blue}\H${white}:${yellow}\w${white}] " +# Bash auto-completion for dotfiles CLI +export DOT_BASH_COMPLETION="1" +if [ -f "$HOME/.config/bash/completions/dot" ]; then + source "$HOME/.config/bash/completions/dot" +fi + # Node.js configuration if [ -d "$HOME/.nvm" ]; then export NVM_DIR="$HOME/.nvm" @@ -53,10 +67,6 @@ if _is_installed "rust"; then [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" fi -# Bash auto-completion for dotfiles CLI -export DOT_BASH_COMPLETION="1" -source "$HOME/.bash_completion.d/dot.bash" - # Initialize base16 color system . "$DOTFILES/config/bash/base16.sh" diff --git a/config/env b/config/env index 113a859..3d33051 100644 --- a/config/env +++ b/config/env @@ -4,20 +4,16 @@ export DOTFILES="$HOME/dot" -contains() { - string="$1" - substring="$2" - if test "${string#*$substring}" != "$string" - then - return 0 # $substring is in $string - else - return 1 # $substring is not in $string - fi +is_in_path() { + case ":$PATH:" in + *":$1:"*) return 0 ;; + *) return 1 ;; + esac } append_to_path() { - p="$1" - export PATH="${p}:${PATH}" + PATH=$1${PATH:+":$PATH"} + export PATH } prepend_to_path() { @@ -26,13 +22,11 @@ prepend_to_path() { } try_append_to_path() { - p="$1" - contains "$PATH" "$p" || append_to_path "$p" + is_in_path "$1" || append_to_path "$1" } try_prepend_to_path() { - p="$1" - contains "$PATH" "$p" || prepend_to_path "$p" + is_in_path "$1" || prepend_to_path "$1" } try_append_to_path "/usr/local/bin" @@ -53,7 +47,7 @@ try_append_to_path "$HOME/box/bin" # the pyenv stuff below? Maybe stick to one? try_prepend_to_path "$HOME/.venv/default/bin" -if [ ! "$WSL_DISTRO_NAME" = "" ]; then +if [ -n "${WSL_DISTRO_NAME-}" ]; then try_append_to_path "/mnt/c/bin" fi @@ -65,3 +59,8 @@ if [ -d "$HOME/.pyenv" ]; then [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init - bash)" fi + +# Nix (single-user) - load Nix profile into PATH +if [ -r "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then + . "$HOME/.nix-profile/etc/profile.d/nix.sh" +fi diff --git a/config/flavours/templates/custom/templates/bashrc.mustache b/config/flavours/templates/custom/templates/bashrc.mustache index 0fed080..4924a87 100644 --- a/config/flavours/templates/custom/templates/bashrc.mustache +++ b/config/flavours/templates/custom/templates/bashrc.mustache @@ -1,5 +1,7 @@ # Base16 {{scheme-name}} -eval "base16_{{scheme-slug}}" +if command -v "base16_{{scheme-slug}}" >/dev/null 2>&1; then + eval "base16_{{scheme-slug}}" +fi diff --git a/config/i3 b/config/i3 index 6b3c88c..451f506 100644 --- a/config/i3 +++ b/config/i3 @@ -286,3 +286,7 @@ bar { binding_mode $base00 $base0A $base00 } } + +# If we're running in a Virtual Machine, start up any necessary software for +# integration between the host and guest. +exec_always "~/dot/bin/run_vm_guest_additions.sh" diff --git a/config/nvim/lua/dot/globals.lua b/config/nvim/lua/dot/globals.lua index 5d29fca..a0a6b33 100644 --- a/config/nvim/lua/dot/globals.lua +++ b/config/nvim/lua/dot/globals.lua @@ -1,7 +1,6 @@ local vim = vim local Util = require('dot.util') -local VimPlug = require('dot.vim_plug') local M = {} @@ -32,8 +31,6 @@ function M.init() _G.print_current_filetype = Util.print_current_filetype _G.reload_config = Util.reload_config - _G.install_vim_plug = VimPlug.install - M._create_commands({ { 'ReloadConfig', Util.reload_config, {} }, { 'CloseTabsToRight', Util.close_tabs_to_right, {} }, diff --git a/config/nvim/lua/dot/plugins.lua b/config/nvim/lua/dot/plugins.lua index 0a8e79f..7e54146 100644 --- a/config/nvim/lua/dot/plugins.lua +++ b/config/nvim/lua/dot/plugins.lua @@ -1,15 +1,15 @@ local vim = vim -local Plug = vim.fn["plug#"] local Log = require('dot.log') local Map = require('dot.map') local Notifications = require('dot.notifications') local Util = require('dot.util') -local VimPlug = require('dot.vim_plug') --- TODO: Copy/pasted from ChatGPT, find the right place for this and read --- through it -- Function to list snippets and allow FZF selection +-- Not currently that useful because it shows all snippets and the list is +-- massive and most of them I would never use, like all of the licensing +-- snippets. Maybe find a way to make this better. A simple solution could be +-- to make a regex filter to remove the ones we don't care about. function ShowSnippets() local filetype = vim.bo.filetype -- Get the current filetype local snippets = vim.fn["UltiSnips#SnippetsInCurrentScope"](1) -- Get available snippets @@ -47,9 +47,6 @@ local plugins = { is_enabled = function() return os.getenv("NVIM_COPILOT_ENABLED") == "1" end, - init = function() - Plug('github/copilot.vim') - end, configure = function() -- Don't use tab to accept Copilot suggestions which conflicts with -- snippets. Not using my Map.imap() function here because it @@ -72,10 +69,6 @@ local plugins = { }, fzf = { - init = function() - Plug('junegunn/fzf', { ['dir'] = '~/.fzf', ['do'] = './install --bin' }) - Plug('junegunn/fzf.vim') - end, configure = function() -- We should remove this eventually in favor of the below Map.nnoremap('o', ':Files') @@ -91,9 +84,6 @@ local plugins = { }, telescope = { - init = function() - Plug('nvim-telescope/telescope.nvim') - end, configure = function() Map.nnoremap('tf', ':Telescope find_files') Map.nnoremap('tt', ':Telescope tags') @@ -105,12 +95,6 @@ local plugins = { }, nvim_treesitter = { - init = function() - -- Sometimes the automatic :TSUpdate won't run successfully and then it - -- won't rerun. If cryptic treesitter errors show up after running - -- :PlugUpdate, try running :TSUpdate manually - Plug('nvim-treesitter/nvim-treesitter', { ['do'] = ':TSUpdate'}) - end, configure = function() local Treesitter = require('dot.treesitter') Treesitter.configure() @@ -118,10 +102,6 @@ local plugins = { }, ultisnips = { - init = function() - Plug('SirVer/ultisnips') - Plug('honza/vim-snippets') - end, configure = function() vim.g.UltiSnipsExpandTrigger = "" vim.g.UltiSnipsJumpForwardTrigger = "" @@ -133,9 +113,6 @@ local plugins = { }, nvim_markdown = { - init = function() - Plug('ixru/nvim-markdown') - end, configure = function() vim.g.vim_markdown_frontmatter = 1 @@ -146,9 +123,6 @@ local plugins = { }, clang_format = { - init = function() - Plug('rhysd/vim-clang-format') - end, configure = function() -- Format the current C/C++ file with clang-format (Uses vim-clang-format plugin) vim.g['clang_format#detect_style_file'] = 1 @@ -157,9 +131,6 @@ local plugins = { }, lsp_config = { - init = function() - Plug('neovim/nvim-lspconfig') - end, configure = function() local LspClangd = require('dot.lsp_clangd') local LspGopls = require('dot.lsp_gopls') @@ -172,54 +143,18 @@ local plugins = { Map.nnoremap('ls', ':LspStop') end }, - - other = { - init = function() - -- Colorschemes - Plug('chriskempson/base16-vim') - - Plug('rodjek/vim-puppet') - Plug('fatih/vim-go') - Plug('OrangeT/vim-csharp') - Plug('mattn/emmet-vim') - Plug('tpope/vim-vinegar') - Plug('nvie/vim-flake8') - Plug('tikhomirov/vim-glsl') - Plug('martinda/Jenkinsfile-vim-syntax') - Plug('aklt/plantuml-syntax') - Plug('elubow/cql-vim') - Plug('tpope/vim-fugitive') - Plug('igankevich/mesonic') - Plug('hrsh7th/nvim-cmp') - Plug('nvim-lua/popup.nvim') - Plug('nvim-lua/plenary.nvim') - Plug('glepnir/lspsaga.nvim') - Plug('hoob3rt/lualine.nvim') - end - }, } function M.init() - if not VimPlug.is_installed() then - Notifications.add("vim-plug is not installed; install it via `:lua install_vim_plug()`") - return - end - - -- Ever since switching from init.vim to init.lua, it doesn't seem like - -- Neovim autoloads the plug.vim file anymore. I'm not sure why and don't - -- have time right now to dig into it so just manually source it for now. - VimPlug.source() - - vim.call('plug#begin') - for plugin_name, plugin in pairs(plugins) do - M._init_plugin(plugin_name, plugin) - end - - vim.call('plug#end') - - for plugin_name, plugin in pairs(plugins) do - M._configure_plugin(plugin_name, plugin) + if plugin.is_enabled ~= nil and not plugin.is_enabled() then + Log.debug('skip configuring ' .. plugin_name .. ' plugin because it is disabled') + else + Log.debug('configuring ' .. plugin_name .. ' plugin') + if plugin.configure ~= nil then + plugin.configure() + end + end end end diff --git a/config/nvim/lua/dot/treesitter.lua b/config/nvim/lua/dot/treesitter.lua index 5fdf65a..4c0d771 100644 --- a/config/nvim/lua/dot/treesitter.lua +++ b/config/nvim/lua/dot/treesitter.lua @@ -34,49 +34,10 @@ function M.configure() -- downloads a ton of parsers I don't care about and that were occasionally -- causing errors. ensure_installed = { - "bash", - "c", - "c_sharp", - "cmake", - "commonlisp", - "cpp", - "css", - "csv", - "disassembly", - "dockerfile", - "elixir", - "erlang", - "gdscript", - "git_rebase", - "gitattributes", - "gitcommit", - "git_config", - "gitignore", - "go", - "godot_resource", - "gomod", - "gosum", - "gowork", - "groovy", - "hcl", - "html", - "ini", - "java", - "javascript", - "json", - "latex", - "lua", - "make", - "markdown", - "markdown_inline", - "meson", - "proto", - "puppet", - "python", - "toml", - "vim", - "xml", - "yaml", + -- Leave this empty because Nix will install the grammars into the + -- Nix store and we don't want the Treesitter plugin trying to + -- install them at runtime because it will fail due to missing + -- write permissions } }) end diff --git a/config/nvim/lua/dot/vim_plug.lua b/config/nvim/lua/dot/vim_plug.lua deleted file mode 100644 index 3937632..0000000 --- a/config/nvim/lua/dot/vim_plug.lua +++ /dev/null @@ -1,34 +0,0 @@ -local vim = vim - -local Log = require('dot.log') -local Util = require('dot.util') - -local M = { - path = Util.path_join(Util.data_dir(), 'site', 'autoload', 'plug.vim'), - url = 'https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim' -} - -function M.is_installed() - return Util.file_exists(M.path) -end - -function M.install() - local curl_command = Util.str_join(' ', '!curl', '-fLo', M.path, '--create-dirs', M.url) - Log.info('Installing vim-plug: ' .. curl_command) - vim.cmd(curl_command) -end - -function M.source() - local source_command = 'source ' .. M.path - Log.info('Sourcing vim-plug: ' .. source_command) - vim.cmd(source_command) -end - -function M.install_plugins() - -- TODO: Untested and currently unused - local autocmd_command = 'PlugInstall --sync' - Log.info('Installing plugins: ' .. autocmd_command) - vim.cmd(autocmd_command) -end - -return M diff --git a/config/wslrc b/config/wslrc new file mode 100644 index 0000000..23c3a2c --- /dev/null +++ b/config/wslrc @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Remove the background highlighting of folders in ls +LS_COLORS=$LS_COLORS:'ow=1;34:' +export LS_COLORS + +if [ "$TERM_PROGRAM" = "WezTerm" ] && [ -f "$HOME/wezterm.sh" ]; then + source "$HOME/wezterm.sh" +fi diff --git a/config/xsession b/config/xsession index 17bd6fe..4959fe3 100755 --- a/config/xsession +++ b/config/xsession @@ -8,9 +8,32 @@ # $ cd ~/dot/config # $ shellcheck --shell=dash -x ./env ./xsession -# Add user-defined fonts -xset +fp /home/paul/.fonts -xset fp rehash +DOTFILES_LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/logs" +XSESSION_LOG_FILE="xsession_$(date +"%Y%m%d_%H%M%S").log" +XSESSION_LOG_PATH="${DOTFILES_LOG_DIR}/${XSESSION_LOG_FILE}" + +log() { + echo "$*" >> "$XSESSION_LOG_PATH" +} + +# Initialize log directory and clean up old logs +mkdir -p "$DOTFILES_LOG_DIR" +if [ -x "$HOME/dot/bin/cleanup_logs.sh" ]; then + "$HOME/dot/bin/cleanup_logs.sh" +else + log "Warning: $HOME/dot/bin/cleanup_logs.sh not found or not executable" +fi + +# Log PATH both before and after we source the env file so we can debug issues +# if it's not modifying it correctly +log "Starting at $(date)" +log "PATH: $PATH" + +# Add user-defined fonts (only if directory exists) +if [ -d "$HOME/.fonts" ]; then + xset +fp "$HOME/.fonts" + xset fp rehash +fi # Disable bell xset -b @@ -47,13 +70,32 @@ ulimit -c unlimited # Set up environment variables # shellcheck source=./env -. "$HOME/dot/config/env" -echo "Path = $PATH" > "$HOME/test.txt" +ENV_CONFIG_PATH="$HOME/dot/config/env" +if [ -f "$ENV_CONFIG_PATH" ]; then + . "$ENV_CONFIG_PATH" + log "Sourced ${ENV_CONFIG_PATH} configuration file" + log "PATH: $PATH" +else + log "ERROR: ${ENV_CONFIG_PATH} not found" +fi + +# Wrap exec in a function so we can redirect its output cleanly +exec_i3() { + exec i3 -V -d all +} # Start i3 and log output -logdir="$HOME/.logs" -mkdir -p "$logdir" -"$HOME/dot/bin/cleanup_logs.sh" -i3logfile="$logdir/i3_$(date +"%Y%m%d_%H%M%S").log" -echo "Starting at $(date)" >> "$i3logfile" -exec /usr/local/bin/i3 -V -d all >> "$i3logfile" +log "Checking for i3: $(command -v i3 2>&1)" + +# Try to start i3 and log any errors +if command -v i3 >/dev/null 2>&1; then + log "Found i3 at: $(command -v i3)" + log "DISPLAY is: $DISPLAY" + exec_i3 >> "$XSESSION_LOG_PATH" 2>&1 +else + log "ERROR: i3 not found in PATH" + log "PATH was: $PATH" + # Try to show a notification + notify-send "X Session Failed" "i3 not found in PATH" 2>/dev/null || true + exit 1 +fi diff --git a/doc/nix_todos.md b/doc/nix_todos.md new file mode 100644 index 0000000..55ad370 --- /dev/null +++ b/doc/nix_todos.md @@ -0,0 +1,206 @@ +# Think we can delete this file now, go through and double check maybe then delete + +# Nix Migration TODOs + +This document tracks remaining work to achieve full parity between the old provisioning systems (shell scripts in `provision/` and Python scripts in `cli/lib/provision/`) and the new Nix/Home Manager configuration. + +**Important:** The primary entry point is `apply.sh`, which handles bootstrap/system-level tasks before invoking Home Manager. Many items that cannot or should not be managed by Nix are intentionally handled there. + +## Status Legend +- [ ] Not started +- [~] Partial / In progress +- [x] Complete + +--- + +## 1. Handled by apply.sh (Intentionally Not in Nix) + +These items are managed by `apply.sh` rather than Nix, typically because they require root access, have OpenGL/driver issues with Nix, or are needed before Nix is available. + +### 1.1 APT Bootstrap Packages + +Installed via apt before Nix is available or to avoid Nix packaging issues: + +| Package | Reason | Notes | +|---------|--------|-------| +| `apt-file` | Pre-Nix bootstrap | Ubuntu package search | +| `ca-certificates` | Pre-Nix bootstrap | Required for HTTPS/curl | +| `curl` | Pre-Nix bootstrap | Needed to install Nix itself | +| `git` | Pre-Nix bootstrap | Needed to clone dotfiles | +| `jq` | Pre-Nix bootstrap | Used by apply.sh to parse hosts.json | +| `libfuse` | Pre-Nix bootstrap | AppImage support | +| `locate` | Pre-Nix bootstrap | File search | +| `software-properties-common` | Pre-Nix bootstrap | Ubuntu apt tooling | +| `xz-utils` | Pre-Nix bootstrap | Compression utilities | + +### 1.2 Desktop Packages (OpenGL Issues) + +These are installed via apt to avoid OpenGL/graphics driver issues that are common with Nix on non-NixOS systems: + +| Package | Reason | Notes | +|---------|--------|-------| +| `i3` | OpenGL/driver issues | Window manager | +| `i3status` | Companion to i3 | Status bar | +| `kitty` | OpenGL/driver issues | GPU-accelerated terminal | + +### 1.3 System-Level Configuration + +These require root access and modify system directories: + +| Task | Location in apply.sh | Notes | +|------|---------------------|-------| +| `update-alternatives` for vi/vim/editor | `set_default_terminal_and_editor()` | Points to nvim | +| `update-alternatives` for x-terminal-emulator | `set_default_terminal_and_editor()` | Points to kitty (desktop only) | +| Install `xsession.desktop` | `install_session_desktop_files()` | To `/usr/share/xsessions/` | +| Install `sway-user.desktop` | `install_session_desktop_files()` | To `/usr/share/wayland-sessions/` | + +### 1.4 Docker Installation + +| Task | Location in apply.sh | Notes | +|------|---------------------|-------| +| Install Docker packages | `install_docker()` | docker-ce, containerd, buildx, compose | +| Add user to docker group | `install_docker()` | Requires logout/reboot to take effect | + +### 1.5 Nix Bootstrap + +| Task | Location in apply.sh | Notes | +|------|---------------------|-------| +| Install Nix | `install_nix_if_needed()` | Single-user installation | +| Enable flakes | `enable_nix_experimental()` | Writes to ~/.config/nix/nix.conf | +| Apply Home Manager | `apply_home_manager()` | Runs `home-manager switch` | + +--- + +## 2. Remaining TODOs + +### 2.1 Neovim Plugins (Nix Package Issues) + +| Plugin | Priority | Notes | +|--------|----------|-------| +| markdown.nvim | Low | Commented out in core.nix due to package issues | +| cql-vim | Low | Cassandra CQL support | +| mesonic | Low | Meson build system integration | + +--- + +## 3. Already Complete + +### 3.1 Packages in Nix + +| Category | Packages | +|----------|----------| +| Core utilities | wget, gnupg, jq, plocate, fzf, nettools, unzip, libuchardet, xz, dos2unix | +| CLI tools | gnumake, gcc, cmake, htop, iotop, universal-ctags, ranger, tmux, neofetch, id3v2, calcurse, vim, tree-sitter | +| Search/utils | ripgrep | +| Theming | flavours | +| C/C++ | clang-tools (clangd) | +| Desktop/GUI | font-awesome, rofi, dunst, feh, sxiv, nitrogen, pavucontrol, picom, scrot, gucharmap, keepassxc, remmina, i3lock, meld, xclip, wl-clipboard, xdotool, libwebp, arandr, rxvt-unicode, ventoy | +| Media | inkscape, mpv, vlc, easytag, blueman, mpd, ncmpcpp, cava, yt-dlp | +| Gaming | steam, steam-run, runelite | +| Development | go, gopls, delve, rustup, nodejs, npm, openjdk, dotnet-sdk, meson, ninja | +| Proprietary | bcompare | +| Custom | wpr (via custom derivation in `nix/home/packages/wpr.nix`) | + +### 3.2 Python Packages (via unified environment) + +| Package | Location | +|---------|----------| +| pip, pynvim, mpd2, black, mypy, isort, flake8, autoflake, argcomplete, json5 | core.nix | +| py3status | desktop.nix | +| ruff | core.nix (system package) | + +### 3.3 Neovim + +- Installed via `programs.neovim` with plugins +- treesitter with all grammars via `withAllGrammars` +- LSP config, telescope, copilot, etc. + +### 3.4 Dotfiles Links + +All links from `links.json` are implemented in `dotfiles-links.nix`. + +### 3.5 Activation Scripts + +| Task | Location | +|------|----------| +| dot CLI completion | core.nix `home.activation.dotArgcomplete` | +| MPD directories creation | desktop.nix `home.activation.createMpdDirs` | +| flavours update | core.nix `home.activation.flavoursUpdate` | +| win32yank installation | wsl.nix `home.activation.installWin32yank` | + +### 3.6 WSL-Specific + +| Task | Location | Notes | +|------|----------|-------| +| win32yank | wsl.nix activation script | Downloads to `/mnt/c/bin/` on Windows filesystem | +| BROWSER env var | wsl.nix | Set to `wslview` | + +--- + +## 4. Potentially Obsolete + +Items from old provisioners that are no longer needed: + +| Item | Reason | +|------|--------| +| Building i3-gaps from source | Gaps merged into mainline i3 as of 4.22 | +| `youtube-dl` | Replaced by `yt-dlp` (already in Nix) | +| `compton` | Replaced by `picom` (already in Nix) | +| Custom kitty installation | Now installed via apt in apply.sh | +| Custom neovim AppImage | Now installed via Nix | +| Custom flavours installation | Now installed via Nix | +| Custom ripgrep installation | Now installed via Nix | +| Custom nodejs installation | Now installed via Nix | +| `usb-creator-gtk` | Replaced by `ventoy` | + +--- + +## 5. Manjaro/Arch-Specific (Low Priority) + +The `provision/manjaro.sh` script includes packages not in the Ubuntu scripts. Consider adding if needed: + +| Package | Notes | +|---------|-------| +| `discord` | Chat application | +| `firefox` | Browser | +| `flatpak` | Universal package manager | +| `kicad` | PCB design software | +| `poppler` | PDF utilities | +| `transmission-gtk` | BitTorrent client | +| `sway` ecosystem | mako, swaybg, swayidle, swaylock, waybar | + +--- + +## 6. Future Considerations + +### Moving More to Nix + +Some items currently in apply.sh could potentially move to Nix in the future: + +1. **kitty/i3**: If OpenGL issues are resolved or using NixOS +2. **Docker**: Could use Podman from Nix as alternative +3. **System alternatives**: Could rely solely on Nix profile PATH ordering + +### NixOS Migration + +If migrating to NixOS, many apply.sh tasks would move to system configuration: +- Display manager desktop files +- Docker installation and group management +- System-wide alternatives + +### Version Pinning + +The old provisioners had version caching. Nix handles this via flake.lock: +- Commit flake.lock for reproducibility +- Use `nix flake update` to update nixpkgs +- Consider overlays for packages needing specific versions + +### Custom Packages + +Custom packages are stored in `nix/home/packages/`: +- `wpr.nix` - Personal wpr tool fetched from S3 + +To add new custom packages: +1. Create a `.nix` file in `nix/home/packages/` +2. Use `pkgs.callPackage ../packages/yourpkg.nix { }` in the relevant role +3. Add to the packages list diff --git a/doc/setup_ubuntu.md b/doc/setup_ubuntu.md new file mode 100644 index 0000000..e2782fe --- /dev/null +++ b/doc/setup_ubuntu.md @@ -0,0 +1,156 @@ +# Ubuntu Setup Instructions + +## New Machine Bootstrapping + +These are the typical steps to perform immediately after the inital Ubuntu +installation. + +Update the system and reboot: +``` +sudo apt update -y && sudo apt upgrade -y +shutdown -r now +``` + +Install Git: +``` +sudo apt install -y git +``` + +Set up an SSH key for Git. There are multiple ways to do this, such as: +- **Option 1:** Generate a new SSH key, log into GitHub in a browser, and add the new key +- **Option 2:** Transfer SSH key from another computer using a flash drive +- **Option 3:** Get SSH key from credential manager + +If generating a new SSH key: +``` +ssh-keygen -t ed25519 -C "git@pcewing.com" +``` + +If copying from a flash drive or credential manager, create the files and paste +them in: +``` +vi ~/.ssh/id_ed25519 +vi ~/.ssh/id_ed25519.pub +``` + +Add the the SSH key to the agent: +``` +chmod 600 ~/.ssh/id_ed25519* +eval "$(ssh-agent -s)" +ssh-add ~/.ssh/id_ed25519 +``` + +If the SSH key was generated, log into GitHub in a browser and add the key in +Settings. + +Clone the dotfiles repository: +``` +# Clone dotfiles +git clone git@github.com:pcewing/dotfiles.git ~/dot +``` + +If necessary, check out the desired branch: +``` +git checkout my-experimental-branch +``` + +Configure the host type of the machine so Nix knows how to provision it. For the list of valid host types, see [hosts.json](../nix/hosts.json). The easiest way is to export a `NIX_HOST` variable in `~/.localrc` as follows: +``` +echo "export NIX_HOST=\"personal-desktop\"" >> ~/.localrc +``` + +Apply the dotfiles configuration: +``` +cd ~/dot +./apply.sh +``` + +**Note:** The first time `apply.sh`, nix profile won't be sourced in the active shell. The easiest workaround is to just open a new shell. + +**TODO:** We should add a message to the end of the output instructing user to restart shell. We could write a file on the first run and check for its existence on subsequent runs. If it does not exist, prompt the user to restart the computer. Probably not a bad idea on the first bootstrap to make sure everything propogates. + +## Daily Operations + +After the initial setup, modifications made to dotfiles can be applied via the following alias: +``` +df_apply +``` + +## Manual Setup Steps + +### Git + +Create `~/.gitconfig_local` like: + +``` +[user] + email = paul@foo.com + name = Paul Ewing +``` + +### Desktop Wallpaper + +#### Basic Wallpaper Setup + +Put logic to apply a wallpaper in `~/set-bg.sh`. For example, download a +wallpaper to `~/Pictures/wallpaper.png` and set it via: +``` +feh --bg-scale "$HOME/Pictures/wallpaper.png" +``` + +You can also set the wallpaper using `nitrogen` and then in the shell script, run: +``` +nitrogen --restore & +``` + +#### Wallpaper Rotater + +If using `wpr`, create `~/.config/wpr/config.json` like: + +```json +{ + "WallpaperDir": "/home/username/Pictures/Wallpapers", + "DisplayCount": 1, + "Interval":120 +} +``` + +### Screen Layout + +When running a multi-monitor setup, set the screen layout by running `arandr`, +configuring the monitors as desired, and then saving the layout to +`~/.screenlayout/config.sh`. + +### Dual Boot Clock Fix + +If dual booting with Windows, set hardware clock to local time: + +```bash +timedatectl set-local-rtc 1 +``` + +Without this, clock time in Windows will be off. + +### Applications to Manually Install + +The following should be installed manually: + +- Chrome + - Reason: This is in Nix but when I tried using the Nix package, it just + crashes immediately and I didn't feel like debugging it. Most likely 3D + acceleration issues like i3wm and kitty had. +- Insync + - Download URL: https://www.insynchq.com/downloads/linux + - Setup: + - `insync start` + - Remember to set sync location to: `$HOME/box` + - Reason: Insync is available in Nix but there's a known bug with the tray + icon not rendering correctly. Given this already requires a fair amount + of manual setup to authenticate and map desired folders, installing it + manually is fine. +- Discord + - Reason: Discord stops working as soon as an upstream update is available + so it's easier to just install it via the official `.deb` and keep it + updated that way +- Visual Studio Code + - Probably could get this from Nix, I just didn't give it a proper go diff --git a/doc/setup_windows.md b/doc/setup_windows.md new file mode 100644 index 0000000..e16c065 --- /dev/null +++ b/doc/setup_windows.md @@ -0,0 +1,3 @@ +## Windows + +**TODO**: Add instructions here, like how the fake "symlinking" works, etc. diff --git a/doc/theme.md b/doc/theme.md new file mode 100644 index 0000000..9f3dfc4 --- /dev/null +++ b/doc/theme.md @@ -0,0 +1,72 @@ +# Theming + +**NOTE:** This is not quite accurate after switching to Nix and home-manager. +Now, dotfiles are effectively copied to their target location instead of +symlinked so when `set-theme` updates them, the changes won't take affect until +the next time Nix configuration is applied. + +To make it easier to re-theme everything at once, I use +[base16](https://github.com/chriskempson/base16) and +[flavours](https://github.com/Misterio77/flavours). See: + +The tl;dr of `base16` is that it is a system for designing color schemes. +`base16` schemes consists of a palette of 16 colors - 8 shades and 8 accents. +Templates can then be created to render the base16 scheme into various config +formats for different applications. + +Due to some [turbulence](https://github.com/tinted-theming/home/issues/51) in +the `base16` project, I've added my most used schemes directly to my dotfiles +to avoid things breaking if repositories are ever moved or taken down. I've +also created my own templates rather than using the defaults. + +- [schemes](./config/flavours/schemes/custom) +- [templates](./config/flavours/templates/custom/templates) + +Using the `flavours` application, these templates are rendered directly into my +dotfiles based on the `flavours` config: + +- [flavours/config.toml](./config/flavours/config.toml) + +To apply a new color scheme, download and install +[flavours](https://github.com/Misterio77/flavours/releases/latest). + +The first time running, update sources. Even if using schemes/templates +committed to my dotfiles, this still appears to be necessary: + +```bash +flavours update all +``` + +**Note:** We should add flavours installation to the provision script. + +Once flavours is installed, set the theme using the +[set-theme](./bin/set-theme) script. This not only executes `flavours` but also +reloads config across various applications to smoothly transition themes. + +```bash +set-theme +``` + +The name should match the corresponding base16 scheme yaml file without the +extension. For example: + +```bash +flavours apply outrun-dark +``` + +The official lists of templates and schemes supported by flavours live here: + +- https://github.com/chriskempson/base16-schemes-source/blob/main/list.yaml +- https://github.com/chriskempson/base16-templates-source/blob/master/list.yaml + +Manual steps after changing themes: + +- Reload tmux config + - `:source-file ~/.tmux.conf` + - We should figure out how to automate this + +### Incomplete + +Some remaining items to tackle in regards to theming: +- Add flavours templates for + - sway diff --git a/doc/todo.md b/doc/todo.md new file mode 100644 index 0000000..5d2510d --- /dev/null +++ b/doc/todo.md @@ -0,0 +1,173 @@ +# To-Do List + +Improvements I'd like to make to my dotfiles. + +## Table of Contents + +- [High Priority](#high-priority) + - [UltiSnips Freezing Issue](#ultisnips-freezing-issue) +- [Python CLI](#python-cli) + - [Bootstrapper](#bootstrapper) + - [Provisioner Groups or Tags](#provisioner-groups-or-tags) + - [Provisioner Command Logging](#provisioner-command-logging) + - [Check for Updates Feature](#check-for-updates-feature) + - [Code Cleanup](#code-cleanup) + - [Necessary Pip Packages](#necessary-pip-packages) +- [FZF Bash Integration](#fzf-bash-integration) +- [Windows support in Python CLI](#windows-support-in-python-cli) +- [wezterm shell integration](#wezterm-shell-integration) +- [I3WM "Virtual Desktops"](#i3wm-"virtual-desktops") +- [Python Tidy/Lint](#python-tidy/lint) +- [Don't symlink vim to neovim](#don't-symlink-vim-to-neovim) +- [Neovim](#neovim) + - [Neovim Healtheck](#neovim-healtheck) +- [Errors](#errors) + - [nvim-lua/completion-nvim](#nvim-lua/completion-nvim) + - [nvim-telescope/telescope.nvim](#nvim-telescope/telescope.nvim) +- [Warnings](#warnings) + - [glepnir/lspsaga.nvim](#glepnir/lspsaga.nvim) + - [nvim-treesitter/nvim-treesitter](#nvim-treesitter/nvim-treesitter) + - [Providers](#providers) + - [nvim-telescope/telescope.nvim](#nvim-telescope/telescope.nvim) + - [neovim/nvim-lspconfig](#neovim/nvim-lspconfig) + +**IMPORTANT NOTE:** A lot of the items in this file may be obsolete with the new Nix setup. Basically everything provisioner related is and I'm sure some other things are as well. We should go through and clean this up so that it's up-to-date. + +## High Priority + +### UltiSnips Freezing Issue + +**Note:** I'm not sure if this is still a problem after switching to Nix. Maybe wait and see if we still encounter this. + +UltiSnips freezes sometimes in Neovim which is really annoying and was marked as won't fix because it's specific to Neovim: + +https://github.com/SirVer/ultisnips/issues/1381 + +We should switch to another snippet plugin, maybe `vim-vsnip` since I see that's what someone else did: + +https://github.com/Sangdol/vimrc/commit/b6c5cf06b761b17d5b39c39a2ae9ad584f48761a + +## XP Submodule + +Use `xp` as a submodule to DRY our Python. + +## Python CLI + +### Git Sync Command + +Implement a simple `dot git-sync` command and a git alias to it like `git sync` +that does something like: + +``` +- Check if there are commits missing from upstream +- If there are, pull +- If there are merge conflicts, abort and print an error + - These should be handled manually +- If the merge is clean, continue on +- Add all local changes +- Commit local changes +- Push +``` + +This is for repositories like my notes where I basically just always want to +keep everything in sync and don't use branches. Optionally, accept a parameter +for commit message. + +#### Progress + +I started on this but it probably isn't bullet-proof yet. What it does: + +- If there are local changes that need to be committed + - Create a temporary branch and resolve/commit the changes in it + - This is a bit complicated and might be bug prone +- Fetch from all remotes +- Detect the most recent matching commit between the local and remote +- Get the number of commits the local repository is missing from remote and + vice versa +- Pull remote commits if there are any missing from local +- If there were local changes, cherry-pick them from the temp branch +- If remote is missing any local commits, push + +One thing I might want to change is to push the temporary branch to remote. I +just encountered an issue where I ran the sync command on my desktop PC and I +think it errored and I forgot to go back and resolve it. Now, working on my +laptop, I'm missing those changes. Had I at least pushed the temp branch, I +could have pulled it down and fixed it on my laptop but since I'm travelling +I'm just out of luck. + +### Code Cleanup + +- Remove the functions like `mkdir_p` in `util.py` and use the alternatives in + `shell.py` +- Use Python native facilities instead of the functions in `shell.py` + - These were used so that we could use `sudo` but now the tool just + elevates itself to root + +### Necessary Pip Packages + +python3 -m pip install typing_extensions + +Also needs to be installed as root if the script elevates +sudo python3 -m pip install typing_extensions + +# Windows Support + +- Get the `clean`/`link` commands to work on Windows +- One thing that would be handy is a command to diff the dotfiles against the + copied locations to see if we updated anything and forgot to backport it into + the repo + - Especially now that we're using Nix on Linux so those could get out of sync too + +## FZF Bash Integration + +**Note:** This statement is not longer accurate since we install fzf via Nix +now; however, I still don't think bash integration is set up so this TODO item +is still valid. + +`~/.fzf.bash` doesn't exist for me, maybe because I'm installing via apt. I'd +like that so I can get fzf `ctrl+r` functionality so update the provision +script to set that up correctly. + +## Python Tidy/Lint + +- [ ] Look into `ruff` since it may replace several other dependencies and also + claims to be much faster + +# Other TODOs (From 2025-01-03) + +- Add to setup doc: + - On multi-monitor setups, run arandr and set up a TODO.sh file + - Set up background wallpaper or whatever will manage it + - Maybe just `nitrogen --restore &` ? + - Can we put an svg in github repo and convert it to png or something? + - So it's text on disk and small in size but then we have a default wallpaper everywhere +- Move base16-shell installation out of bashrc maybe? + - Have nix do this? With current system, it will never update after first installation and it feels weird to have shell init scripts cloning git repositories +- Maybe we can merge some of the shell scripts i3 executes into a single shell script so they can all share the same logging and debugging facilities? +- bcompare in nix is Beyond Compare 4, is it possible to get 5? +- Are we forgetting to execute gtk stuff on i3 startup? Keyring, etc. + - Notice how long it takes `gnome-text-editor` to run; maybe it's a snap? + - Also look at some errors in terminal when running Firefox, Nitrogen, etc. + +- Rust App ideas + - Wallpaper setter? + - Cheat sheet viewer + - Tray icon with reminders about pending local git changes + +- .gitconfig_local + - Can we just make a new email solely for git that we can put in the public repo? + - Like, `git@pcewing.com`? + +## Old + +### I3WM "Virtual Desktops" + +**Note:** I don't realistically think this is worth the effort. Even if we ever +get it working, it will probably be time to switch to Wayland shortly after. + +10 workspaces isn't always enough. It would be nice to do something that +provides a similar workflow to virtual desktops on Windows. Like, 4 virtual +desktops that each have 10 workspaces. Maybe as an MVP, have a keyboard +shortcut that switches between the desktops and remaps keybindings accordingly. + +I've started noodling on a hacky PoC for this in `bin/i3-util.sh` diff --git a/img/wallpaper.svg b/img/wallpaper.svg new file mode 100755 index 0000000..5294e61 --- /dev/null +++ b/img/wallpaper.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/mypy.ini b/mypy.ini index c86bf6d..1854fdd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,14 +3,3 @@ warn_return_any = True warn_unused_configs = True disallow_untyped_defs = True - -# Per-module options: - -#[mypy-mycode.foo.*] -#disallow_untyped_defs = True - -#[mypy-mycode.bar] -#warn_return_any = False - -#[mypy-somelibrary] -#ignore_missing_imports = True diff --git a/nix/flake.lock b/nix/flake.lock new file mode 100644 index 0000000..e161f4c --- /dev/null +++ b/nix/flake.lock @@ -0,0 +1,49 @@ +{ + "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1726989464, + "narHash": "sha256-Vl+WVTJwutXkimwGprnEtXc/s/s8sMuXzqXaspIGlwM=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "2f23fa308a7c067e52dfcc30a0758f47043ec176", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-24.05", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1735563628, + "narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "home-manager": "home-manager", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/flake.nix b/nix/flake.nix new file mode 100644 index 0000000..0242aca --- /dev/null +++ b/nix/flake.nix @@ -0,0 +1,52 @@ +{ + description = "Paul's Home Manager configuration"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + + home-manager = { + url = "github:nix-community/home-manager/release-24.05"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + home-manager, + ... + }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + + # Load host definitions from JSON + hostsData = builtins.fromJSON (builtins.readFile ./hosts.json); + + # Convert role name to module path + roleToModule = role: ./home/roles/${role}.nix; + + # Generate a home-manager configuration for a single host + mkHostConfig = + hostName: hostConfig: + home-manager.lib.homeManagerConfiguration { + inherit pkgs; + modules = [ + { + home.username = hostConfig.username; + home.homeDirectory = "/home/${hostConfig.username}"; + home.stateVersion = "24.05"; + + imports = map roleToModule hostConfig.roles; + } + ]; + }; + + # Generate all homeConfigurations from the JSON + homeConfigurations = builtins.mapAttrs mkHostConfig hostsData.hosts; + in + { + inherit homeConfigurations; + }; +} diff --git a/nix/home/features/development.nix b/nix/home/features/development.nix new file mode 100644 index 0000000..0daedae --- /dev/null +++ b/nix/home/features/development.nix @@ -0,0 +1,59 @@ +# This module provides a set of common software development tools for various +# languages. It can be enabled in a role to provide a baseline development +# environment. +{ + config, + pkgs, + lib, + ... +}: + +{ + options.development.enable = lib.mkEnableOption "development tools"; + + config = lib.mkIf config.development.enable { + home.packages = with pkgs; [ + ######### + # Build Systems + ######### + meson + ninja + + ######### + # Golang + ######### + go + gopls + delve + + ######### + # Rust + ######### + rustup + + ######### + # NodeJS + ######### + nodejs + nodePackages.npm + + ######### + # Java + ######### + openjdk + + ######### + # .NET + ######### + dotnet-sdk + ]; + + # Ensure GOPATH is set + home.sessionVariables = { + GOPATH = "$HOME/go"; + }; + + # Add Go bin to PATH + home.sessionPath = [ "$HOME/go/bin" ]; + }; +} diff --git a/nix/home/lib/dotfiles-links.nix b/nix/home/lib/dotfiles-links.nix new file mode 100644 index 0000000..bda2509 --- /dev/null +++ b/nix/home/lib/dotfiles-links.nix @@ -0,0 +1,98 @@ +{ config, lib, ... }: + +let + dotConfigDir = ../../../config; + + # Helper to classify a link as XDG-compliant (in .config) or a regular + # home-relative dotfile. + link = + { dst, srcPath }: + if lib.hasPrefix ".config/" dst then + { + xdg = true; + key = lib.removePrefix ".config/" dst; + value = { + source = srcPath; + }; + } + else + { + xdg = false; + key = dst; + value = { + source = srcPath; + }; + }; + + # Helper to create a link item for the `items` list. + mk = + dstRel: srcRel: + link { + dst = dstRel; + srcPath = dotConfigDir + "/${srcRel}"; + }; + + # List of all files and directories to be linked into the home directory. + items = [ + (mk ".Xresources" "Xresources") + (mk ".bash_profile" "bash_profile") + (mk ".bashrc" "bashrc") + (mk ".config/dunst/dunstrc" "dunstrc") + (mk ".env" "env") + (mk ".gitconfig" "gitconfig") + (mk ".gvimrc" "gvimrc") + (mk ".config/i3/config" "i3") + (mk ".inputrc" "inputrc") + (mk ".config/mpd/mpd.conf" "mpd") + (mk ".config/ncmpcpp/bindings" "ncmpcpp/bindings") + (mk ".config/ncmpcpp/config" "ncmpcpp/config") + (mk ".config/picom/picom.conf" "picom.conf") + (mk ".profile" "profile") + (mk ".pulse/daemon.conf" "pulse/daemon.conf") + (mk ".config/py3status/config" "py3status.conf") + (mk ".config/ranger/rc.conf" "rangerrc") + (mk ".config/rofi/config.rasi" "rofi/config.rasi") + (mk ".config/rofi/base16.rasi" "rofi/base16.rasi") + (mk ".config/sway/config" "sway") + (mk ".swaysession" "swaysession") + (mk ".tmux.conf" "tmux.conf") + (mk ".vimrc" "vimrc") + (mk ".xinitrc" "xinitrc") + (mk ".xsession" "xsession") + + # directories + (mk ".config/nvim" "nvim") + (mk ".config/flavours" "flavours") + + # individual files under ~/.config + (mk ".config/kitty/kitty.conf" "kitty.conf") + (mk ".config/alacritty/alacritty.yml" "alacritty.yml") + (mk ".config/alacritty/base16.yml" "alacritty/base16.yml") + (mk ".config/alacritty/linux.yml" "alacritty/linux.yml") + (mk ".config/wezterm/wezterm.lua" "wezterm.lua") + ]; + + # Separate items into XDG and home-relative lists. + xdgPairs = builtins.filter (x: x.xdg) items; + homePairs = builtins.filter (x: !x.xdg) items; + + # Convert the lists to attribute sets suitable for home-manager options. + xdgAttr = lib.listToAttrs ( + map (x: { + name = x.key; + value = x.value; + }) xdgPairs + ); + homeAttr = lib.listToAttrs ( + map (x: { + name = x.key; + value = x.value; + }) homePairs + ); +in +{ + xdg.enable = true; + + home.file = homeAttr; + xdg.configFile = xdgAttr; +} diff --git a/nix/home/lib/python-environment.nix b/nix/home/lib/python-environment.nix new file mode 100644 index 0000000..feae220 --- /dev/null +++ b/nix/home/lib/python-environment.nix @@ -0,0 +1,37 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + # Collect all Python package functions from features + allPyPkgs = ps: lib.flatten (map (f: f ps) config.myPython.packageFns); + + # Build the unified Python environment + pythonEnv = pkgs.python3.withPackages allPyPkgs; +in +{ + options.myPython = { + packageFns = lib.mkOption { + type = lib.types.listOf (lib.types.functionTo (lib.types.listOf lib.types.package)); + default = [ ]; + description = "List of functions that take python packages and return packages to include"; + }; + + environment = lib.mkOption { + type = lib.types.package; + description = "The unified Python environment (read-only)"; + readOnly = true; + }; + }; + + config = { + # Expose the Python environment for other modules to reference + myPython.environment = pythonEnv; + + # Build a unified Python environment with all requested packages + home.packages = lib.mkIf (config.myPython.packageFns != [ ]) [ pythonEnv ]; + }; +} diff --git a/nix/home/packages/cql-vim.nix b/nix/home/packages/cql-vim.nix new file mode 100644 index 0000000..f424153 --- /dev/null +++ b/nix/home/packages/cql-vim.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: + +pkgs.vimUtils.buildVimPlugin { + pname = "cql-vim"; + version = "unstable-2024-01-03"; + src = pkgs.fetchFromGitHub { + owner = "elubow"; + repo = "cql-vim"; + rev = "6f61df5a633c3a91edea7bcb5d5772648df19d1a"; + sha256 = "sha256-GHiXIJpNJj/ysWywWziUuTy21LKjZKe7q3bUkJjeMV0="; + }; + meta.homepage = "https://github.com/elubow/cql-vim"; +} diff --git a/nix/home/packages/mesonic.nix b/nix/home/packages/mesonic.nix new file mode 100644 index 0000000..b58602a --- /dev/null +++ b/nix/home/packages/mesonic.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: + +pkgs.vimUtils.buildVimPlugin { + pname = "mesonic"; + version = "unstable-2024-01-03"; + src = pkgs.fetchFromGitHub { + owner = "igankevich"; + repo = "mesonic"; + rev = "d6780c3af29ebfc8c631399b2692b928da9bf7bd"; + sha256 = "sha256-XFrV7ZJtVqmUsad/94UZ/ZnPQOKyZ6mmsJlVLtKKAZQ="; + }; + meta.homepage = "https://github.com/igankevich/mesonic"; +} diff --git a/nix/home/packages/nvim-markdown.nix b/nix/home/packages/nvim-markdown.nix new file mode 100644 index 0000000..05a0513 --- /dev/null +++ b/nix/home/packages/nvim-markdown.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: + +pkgs.vimUtils.buildVimPlugin { + pname = "nvim-markdown"; + version = "unstable-2024-01-03"; + src = pkgs.fetchFromGitHub { + owner = "ixru"; + repo = "nvim-markdown"; + rev = "37850581fdaec153ce84af677d43bf8fce60813a"; + sha256 = "sha256-wjYTO9WqdDEbH4L3dsHqOoeQf0y/Uo6WX94w/D4EuGU="; + }; + meta.homepage = "https://github.com/ixru/nvim-markdown"; +} diff --git a/nix/home/packages/wpr.nix b/nix/home/packages/wpr.nix new file mode 100644 index 0000000..b4cbcd4 --- /dev/null +++ b/nix/home/packages/wpr.nix @@ -0,0 +1,33 @@ +# wpr - Custom X11 Wallpaper Rotator Tool +# https://s3-us-west-2.amazonaws.com/pcewing-wpr/releases/ +{ + lib, + stdenv, + fetchurl, + autoPatchelfHook, +}: + +stdenv.mkDerivation rec { + pname = "wpr"; + version = "0.1.0"; + + src = fetchurl { + url = "https://s3-us-west-2.amazonaws.com/pcewing-wpr/releases/${version}/wpr.${version}.linux-amd64.tar.gz"; + sha256 = "sha256-YuJBoD8JWn1+fBMYdihIRz7qxpM+T+WmC3ruL2TcHWM="; + }; + + nativeBuildInputs = [ autoPatchelfHook ]; + + sourceRoot = "."; + + installPhase = '' + mkdir -p $out/bin + cp wpr $out/bin/ + chmod +x $out/bin/wpr + ''; + + meta = with lib; { + description = "wpr x11 wallpaper rotator tool"; + platforms = platforms.linux; + }; +} diff --git a/nix/home/roles/core.nix b/nix/home/roles/core.nix new file mode 100644 index 0000000..3aec04d --- /dev/null +++ b/nix/home/roles/core.nix @@ -0,0 +1,199 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + wpr = pkgs.callPackage ../packages/wpr.nix { }; + nvim-markdown = pkgs.callPackage ../packages/nvim-markdown.nix { }; + cql-vim = pkgs.callPackage ../packages/cql-vim.nix { }; + mesonic = pkgs.callPackage ../packages/mesonic.nix { }; +in +{ + imports = [ + ../lib/dotfiles-links.nix + ../lib/python-environment.nix + ../features/development.nix + ]; + + # Enable the development feature + development.enable = true; + + # Declare Python packages needed by core + myPython.packageFns = [ + ( + ps: with ps; [ + pip + pynvim + mpd2 + black + mypy + isort + flake8 + autoflake + argcomplete + json5 + ] + ) + ]; + + home.sessionVariables = { + EDITOR = "nvim"; + VISUAL = "nvim"; + }; + + nixpkgs.config.allowUnfree = true; + + home.packages = with pkgs; [ + ################################# + # Core utilities + ################################# + cacert + wget + gnupg + jq + plocate + fzf + nettools + unzip + libuchardet + xz + dos2unix + + ############################### + # Basic command line utilities + ############################### + gnumake + gcc + cmake + htop + iotop + universal-ctags + ranger + tmux + neofetch + id3v2 + calcurse + vim + tree-sitter + + ################# + # Nix tooling + ################# + nixfmt-rfc-style + + ################# + # C/C++ tooling + ################# + clang-tools + + ################# + # Search / utils + ################# + ripgrep + + ################# + # Theming tool + ################# + flavours + + # Ruff is a Rust tool, top-level package + ruff + + ################# + # Custom packages + ################# + wpr + ]; + + # Provide a stable `dot` command that always uses the unified Python env + home.file.".local/bin/dot" = { + executable = true; + text = '' + #!/usr/bin/env bash + # Find python3 from the unified environment in PATH + exec python3 "$HOME/dot/cli/dot.py" "$@" + ''; + }; + + # Ensure ~/.local/bin is on PATH so `dot` is found + home.sessionPath = [ "$HOME/.local/bin" ]; + + programs.git.enable = true; + + programs.neovim = { + enable = true; + defaultEditor = true; + + plugins = with pkgs.vimPlugins; [ + fzf-vim + + telescope-nvim + plenary-nvim + popup-nvim + + nvim-treesitter.withAllGrammars + + ultisnips + vim-snippets + + # Custom nvim-markdown plugin + + vim-clang-format + nvim-lspconfig + + copilot-vim + + base16-vim + vim-puppet + vim-go + vim-csharp + emmet-vim + vim-vinegar + vim-flake8 + vim-glsl + Jenkinsfile-vim-syntax + plantuml-syntax + cql-vim + vim-fugitive + mesonic + + nvim-cmp + lspsaga-nvim + lualine-nvim + ]; + + extraPackages = with pkgs; [ + fzf + ripgrep + fd + clang-tools + nodejs # needed for copilot + some LSP tooling + ]; + }; + + programs.home-manager.enable = true; + + # Optional: generate a static completion file during activation + home.activation.dotArgcomplete = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if [ -x "$HOME/dot/cli/dot.py" ]; then + mkdir -p "$HOME/.config/bash/completions" + # Use the unified Python environment directly + ${config.myPython.environment}/bin/register-python-argcomplete \ + --external-argcomplete-script "$HOME/dot/cli/dot.py" dot \ + > "$HOME/.config/bash/completions/dot" + fi + ''; + + # Update flavours base16 schemes and templates on activation + home.activation.flavoursUpdate = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if command -v flavours >/dev/null 2>&1; then + echo "Updating flavours schemes and templates..." + # flavours update can be noisy and sometimes fails on first run, so we + # suppress errors and output + ${pkgs.flavours}/bin/flavours update all >/dev/null 2>&1 || true + fi + ''; +} diff --git a/nix/home/roles/desktop.nix b/nix/home/roles/desktop.nix new file mode 100644 index 0000000..c4a36f8 --- /dev/null +++ b/nix/home/roles/desktop.nix @@ -0,0 +1,118 @@ +{ pkgs, lib, ... }: + +let + wallpaperSvg = ../../../img/wallpaper.svg; + setBgTemplate = ../../../templates/set-bg.sh; +in +{ + imports = [ + ../lib/python-environment.nix + ]; + + # Declare Python packages needed by desktop + myPython.packageFns = [ + ( + ps: with ps; [ + py3status + # mpd2 is already included from core.nix + ] + ) + ]; + + home.sessionVariables = { + TERMINAL = "kitty"; + }; + + home.packages = with pkgs; [ + ######################## + # Desktop / GUI utilities + ######################## + font-awesome + rofi + dunst + feh + sxiv + nitrogen + pavucontrol + picom + scrot + gucharmap + keepassxc + remmina + i3lock + meld + xclip + wl-clipboard + xdotool + libwebp + arandr + librsvg # For SVG to PNG conversion + + # Even though kitty is our primary terminal emulator now, install urxvt as + # a backup because kitty's dependence on 3D acceleration has been + # problematic in the past + rxvt-unicode + + ######## + # Media + ######## + inkscape + mpv + vlc + easytag + blueman + + ######################### + # Music tooling (desktop-y) + ######################### + mpd + (ncmpcpp.override { + visualizerSupport = true; + }) + cava + + ######################### + # Video download + ######################### + yt-dlp + + ######################### + # Proprietary Software + ######################### + bcompare + + ######################### + # System Utilities + ######################### + ventoy + ]; + + # Install ~/set-bg.sh from templates/set-bg.sh only if it doesn't already + # exist. This allows per-machine customization without home-manager + # overwriting it. + home.activation.installSetBgScript = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + target="$HOME/set-bg.sh" + if [ ! -e "$target" ]; then + echo "Installing default $target from template..." + install -m 0755 ${setBgTemplate} "$target" + else + echo "$target already exists; leaving it untouched." + fi + ''; + + # Generate a PNG wallpaper from the source SVG. + home.activation.generateWallpaper = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + echo "Generating default wallpaper from SVG..." + mkdir -p "$HOME/Pictures" + ${pkgs.librsvg}/bin/rsvg-convert \ + -w 3840 -h 2160 \ + -o "$HOME/Pictures/default_wallpaper.png" \ + ${wallpaperSvg} + echo "Wallpaper generated at $HOME/Pictures/default_wallpaper.png" + ''; + + # Make sure mpd runtime directories exist or it will complain on the first startup + home.activation.createMpdDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + mkdir -p "$HOME/.mpd/playlists" "$HOME/.local/share/mpd" + ''; +} diff --git a/nix/home/roles/gaming.nix b/nix/home/roles/gaming.nix new file mode 100644 index 0000000..7ca9c33 --- /dev/null +++ b/nix/home/roles/gaming.nix @@ -0,0 +1,12 @@ +{ pkgs, ... }: +{ + # Allow proprietary packages for gaming (e.g., Steam) + nixpkgs.config.allowUnfree = true; + + home.packages = with pkgs; [ + steam + steam-run + + runelite + ]; +} diff --git a/nix/home/roles/wsl.nix b/nix/home/roles/wsl.nix new file mode 100644 index 0000000..fb5aff2 --- /dev/null +++ b/nix/home/roles/wsl.nix @@ -0,0 +1,69 @@ +{ pkgs, lib, ... }: + +let + win32yankVersion = "v0.1.1"; + win32yankUrl = "https://github.com/equalsraf/win32yank/releases/download/${win32yankVersion}/win32yank-x64.zip"; + win32yankInstallDir = "/mnt/c/bin"; + win32yankExe = "${win32yankInstallDir}/win32yank.exe"; + win32yankVersionFile = "${win32yankInstallDir}/win32yank_version.txt"; +in +{ + home.sessionVariables = { + BROWSER = "wslview"; + }; + + # Install win32yank to Windows filesystem for clipboard integration + # This needs to be on NTFS (not WSL filesystem) for performance reasons + home.activation.installWin32yank = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + WIN32YANK_VERSION="${win32yankVersion}" + WIN32YANK_URL="${win32yankUrl}" + WIN32YANK_DIR="${win32yankInstallDir}" + WIN32YANK_EXE="${win32yankExe}" + WIN32YANK_VERSION_FILE="${win32yankVersionFile}" + + install_win32yank() { + echo "Installing win32yank $WIN32YANK_VERSION..." + mkdir -p "$WIN32YANK_DIR" + TMP_DIR=$(mktemp -d) + ${pkgs.curl}/bin/curl -sL "$WIN32YANK_URL" -o "$TMP_DIR/win32yank.zip" + ${pkgs.unzip}/bin/unzip -o "$TMP_DIR/win32yank.zip" -d "$TMP_DIR" + cp "$TMP_DIR/win32yank.exe" "$WIN32YANK_EXE" + chmod +x "$WIN32YANK_EXE" + echo "$WIN32YANK_VERSION" > "$WIN32YANK_VERSION_FILE" + rm -rf "$TMP_DIR" + echo "win32yank $WIN32YANK_VERSION installed to $WIN32YANK_EXE" + } + + # Check if we need to install or update + if [ ! -f "$WIN32YANK_EXE" ]; then + echo "win32yank not found, installing..." + install_win32yank + elif [ ! -f "$WIN32YANK_VERSION_FILE" ]; then + echo "win32yank version file not found, reinstalling..." + install_win32yank + elif [ "$(cat "$WIN32YANK_VERSION_FILE")" != "$WIN32YANK_VERSION" ]; then + echo "win32yank version mismatch (have $(cat "$WIN32YANK_VERSION_FILE"), want $WIN32YANK_VERSION), updating..." + install_win32yank + else + echo "win32yank $WIN32YANK_VERSION already installed" + fi + ''; + + # Link the wslrc file. This is only active for WSL hosts. + home.file.".wslrc".source = ../../../config/wslrc; + + # Download wezterm shell integration script + home.activation.downloadWeztermShellIntegration = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + WEZTERM_SH_URL="https://raw.githubusercontent.com/wez/wezterm/main/assets/shell-integration/wezterm.sh" + WEZTERM_SH_DEST="$HOME/wezterm.sh" + + if [ ! -f "$WEZTERM_SH_DEST" ]; then + echo "Downloading wezterm.sh for shell integration..." + ${pkgs.curl}/bin/curl -sfL "$WEZTERM_SH_URL" -o "$WEZTERM_SH_DEST" + chmod +x "$WEZTERM_SH_DEST" + echo "wezterm.sh installed to $WEZTERM_SH_DEST" + else + echo "wezterm.sh already exists, skipping download." + fi + ''; +} diff --git a/nix/hosts.json b/nix/hosts.json new file mode 100644 index 0000000..26dcb6d --- /dev/null +++ b/nix/hosts.json @@ -0,0 +1,28 @@ +{ + "hosts": { + "personal-desktop": { + "username": "pewing", + "roles": ["core", "desktop", "gaming"] + }, + "work-desktop": { + "username": "pewing", + "roles": ["core", "desktop"] + }, + "personal-wsl": { + "username": "pewing", + "roles": ["core", "wsl"] + }, + "work-wsl": { + "username": "pewing", + "roles": ["core", "wsl"] + }, + "personal-server": { + "username": "pewing", + "roles": ["core"] + }, + "work-server": { + "username": "pewing", + "roles": ["core"] + } + } +} diff --git a/provision/manjaro.sh b/provision/manjaro.sh deleted file mode 100755 index 76771dc..0000000 --- a/provision/manjaro.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash - -# TODO: This script is very much WIP as I transition to Manjaro - -function pacman_install() { - package="$1" - sudo pacman -Syu --noconfirm $package -} - -function install_insync() { - # Instructions copied from: - # https://help.insynchq.com/en/articles/3417503-linux-installation-guide-unofficial - sudo pacman -S base-devel - git clone https://aur.archlinux.org/insync.git insync_install - cd insync_install - makepkg -si -} - -pacman_install base-devel -pacman_install ctags -pacman_install discord -pacman_install docker -pacman_install firefox -pacman_install flatpak -pacman_install htop -pacman_install i3-gaps -pacman_install i3status -pacman_install inkscape -pacman_install jdk-openjdk -pacman_install keepassxc -pacman_install kicad -pacman_install mlocate -pacman_install mpd -pacman_install mpv -pacman_install ncmpcpp -pacman_install poppler -pacman_install py3status -pacman_install python-pip -pacman_install ranger -pacman_install remmina -pacman_install rofi -pacman_install rxvt-unicode -pacman_install steam -pacman_install tmux -pacman_install transmission-gtk -pacman_install ttf-font-awesome -pacman_install xclip -pacman_install scrot -pacman_install feh - -# Sway is not quite ready for usage as a daily driver; however, this is the -# command to install it along with peripherals. -#pacman_install \ -# mako \ -# sway \ -# swaybg \ -# swayidle \ -# swaylock \ -# waybar \ -# wl-clipboard \ - -# Proprietary packages from the AUR -#pamac build nordvpn-bin -#pamac build bcompare - -# https://github.com/flathub/com.slack.Slack/issues/34 -#flatpak install com.slack.Slack diff --git a/provision/ubuntu.sh b/provision/ubuntu.sh deleted file mode 100755 index 230b8ff..0000000 --- a/provision/ubuntu.sh +++ /dev/null @@ -1,398 +0,0 @@ -#!/usr/bin/env bash - -function yell () { >&2 echo "$*"; } -function die () { yell "$*"; exit 1; } -function try () { "$@" || die "Command failed: $*"; } - -script_path="$( realpath "$0" )" -script_dir="$( dirname "$script_path" )" - -print_header() { - local header="$1" - - echo -e "\\n" - echo "$header" - echo "========================================" -} - -apt_update() { - echo "(Apt) Updating package lists... " - try sudo apt-get -y update -} - -apt_dist_upgrade() { - echo "(Apt) Upgrading packages... " - try sudo apt-get -y dist-upgrade -} - -apt_install() { - local packages="$@" - - echo "(Apt) Installing $packages... " - try sudo apt-get -y install $packages -} - -pip_install() { - local packages="$@" - - echo "(Pip) Installing $packages... " - try python3 -m pip install $packages -} - -function get_latest_github_release() { - local org="$1" - local repo="$2" - - local api_url="https://api.github.com/repos/$org/$repo/releases/latest" - echo "$( curl --silent "$api_url" | jq -r .tag_name )" -} - -###################################### -# Application Installation Functions # -###################################### - -configure_xsession() { - local src_path="$1" - local dst_path="$2" - - echo "Configuring xsession... " - - [ -f "$src_path" ] || die "File $src_path does not exist!" - - try sudo rm -f "$dst_path" - try sudo cp "$src_path" "$dst_path" - try sudo chmod 644 "$dst_path" -} - -configure_wayland_session() { - local src_path="$1" - local dst_path="$2" - - echo "Configuring wayland session... " - - [ -f "$src_path" ] || die "File $src_path does not exist!" - - try sudo rm -f "$dst_path" - try sudo cp "$src_path" "$dst_path" - try sudo chmod 644 "$dst_path" -} - -install_apt_packages() { - local p - - # Core utitilies - p="apt-utils" - p+=" ca-certificates" - p+=" curl" - p+=" wget" - p+=" gnupg" - p+=" jq" - p+=" software-properties-common" - p+=" apt-file" - - # Basic command line utitilies - p+=" make" - p+=" build-essential" - p+=" cmake" - p+=" meson" - p+=" htop" - p+=" iotop" - p+=" git" - p+=" vim" - #p+=" exuberant-ctags" - p+=" universal-ctags" # I think this has better c++11 support - p+=" ranger" - p+=" tmux" - p+=" neofetch" - p+=" id3v2" - p+=" calcurse" - - # Python - p+=" python python-dev" # Python 2.7 - p+=" python3 python3-dev python3-pip" # Python 3.x - - # General GUI Applications - p+=" fonts-font-awesome" # Used for media buttons on polybar - p+=" rofi" # Fuzzy application launcher - p+=" dunst" # Desktop notifications - p+=" feh" # Set wallpaper - p+=" sxiv" # Image viewer - p+=" nitrogen" # Set wallpaper - p+=" pavucontrol" # Pulse Audio frontend - p+=" compton" # Window compositor - p+=" scrot" # Screen capture - p+=" gucharmap" # Useful for debugging font issues - p+=" keepassxc" # Credential manager - p+=" remmina" # RDP session manager - p+=" usb-creator-gtk" # Easily flash bootable USBs - p+=" i3lock" # Lock screen - p+=" meld" # Diff tool - p+=" xclip" # Clipboard for X11 - p+=" wl-clipboard" # Clipboard for Wayland - p+=" xdotool" # X11 automation tool - p+=" kitty" # Kitty terminal emulator - p+=" kitty-terminfo" # Kitty TERMINFO - - # Media - p+=" inkscape" # Vector graphics editor - p+=" mpv" # Minimal media player - p+=" vlc" # General purpose FOSS media player - p+=" easytag" # Edit ID3 Tags on MP3 files - p+=" blueman" # Bluetooth device support - - # Gaming - p+=" steam" - p+=" steam-devices" - - apt_install "$p" -} - -install_neovim() { - print_header "Installing neovim" - - local nvim_path="/usr/local/bin/nvim" - if [ -f "$nvim_path" ]; then - echo "$nvim_path already exists, skipping installation..." - return - fi - - try sudo mkdir -p /opt/neovim - curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim.appimage - try sudo chmod u+x nvim.appimage - try sudo mv nvim.appimage /opt/neovim/nvim - try sudo ln -s /opt/neovim/nvim $nvim_path - - echo "Installing pynvim python modules..." - try sudo pip3 install --upgrade pynvim - - echo "Updating alternatives to use nvim..." - try sudo update-alternatives --install /usr/bin/vi vi "$nvim_path" 60 - try sudo update-alternatives --set vi "$nvim_path" - try sudo update-alternatives --install /usr/bin/vim vim "$nvim_path" 60 - try sudo update-alternatives --set vim "$nvim_path" - try sudo update-alternatives --install /usr/bin/editor editor "$nvim_path" 60 - try sudo update-alternatives --set editor "$nvim_path" -} - -install_cava() { - local cache_dir="$1" - - local version="$(get_latest_github_release "karlstav" "cava")" - - print_header "Installing cava ($version)" - - local cava_dir="$cache_dir/cava/$version" - local cava_exe="$cava_dir/cava" - if [ -f "$cava_exe" ]; then - echo "$cava_exe already exists, skipping installation..." - return - fi - - echo "Installing pre-requisites..." - apt_install "libfftw3-dev libasound2-dev libncursesw5-dev libpulse-dev libtool" - - echo "Cloning the cava repository" - try mkdir -p "$(dirname -- "$cava_dir")" - try git clone "https://github.com/karlstav/cava" "$cava_dir" - - local pwd; pwd="$(pwd)" - try cd "$cava_dir" - - echo "Building cava $version..." - try ./autogen.sh - try ./configure - try make - - echo "Installing cava $version..." - try sudo make install - - try cd "$pwd" -} - -install_i3gaps() { - local cache_dir="$1" - - local version="$(get_latest_github_release "airblader" "i3")" - - print_header "Installing i3-gaps ($version)" - - local i3gaps_dir="$cache_dir/i3gaps/$version" - local i3gaps_exe="$i3gaps_dir/build/i3" - if [ -f "$i3gaps_exe" ]; then - echo "$i3gaps_exe already exists, skipping installation..." - return - fi - - echo "Installing pre-requisites..." - apt_install "libxcb1-dev libxcb-keysyms1-dev libpango1.0-dev libxcb-util0-dev libxcb-icccm4-dev libyajl-dev libstartup-notification0-dev libxcb-randr0-dev libev-dev libxcb-cursor-dev libxcb-xinerama0-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev autoconf libxcb-xrm0 libxcb-xrm-dev libxcb-shape0 libxcb-shape0-dev automake" - - echo "Cloning the i3-gaps repository" - try mkdir -p "$(dirname -- "$i3gaps_dir")" - try git clone "https://www.github.com/Airblader/i3" "$i3gaps_dir" - - local pwd; pwd="$(pwd)" - try cd "$i3gaps_dir" - - echo "Checkout out version $version..." - try git checkout "$version" - - echo "Building i3-gaps $version..." - try mkdir -p build - try cd build - try meson .. - try ninja - - echo "Installing i3-gaps $version..." - try sudo meson install - - apt_install "i3status" - pip_install "py3status" - - try cd "$pwd" -} - -install_youtube-dl() { - local cache_dir="$1" - local bin_dir="$2" - - local version="$(get_latest_github_release "ytdl-org" "youtube-dl")" - - print_header "Installing youtube-dl ($version)" - - local ytdl_dir="$cache_dir/youtube-dl/$version" - local ytdl_exe="$ytdl_dir/youtube-dl" - if [ -f "$ytdl_exe" ]; then - echo "$ytdl_exe already exists, skipping installation..." - return - fi - - echo "Downloading youtube-dl $version..." - mkdir -p "$ytdl_dir" - try curl -L "https://yt-dl.org/downloads/$version/youtube-dl" -o "$ytdl_exe" - - echo "Installing youtube-dl $version..." - try chmod a+rx "$ytdl_exe" - try rm -f "$bin_dir/youtube-dl" - try ln -s "$ytdl_exe" "$bin_dir/youtube-dl" -} - -install_urxvt() { - print_header "Installing rxvt-unicode" - - if [ "$(command -v urxvt)" = "" ]; then - apt_install rxvt-unicode - else - echo "Skipping installation because urxvt is already installed..." - fi - - echo "Setting urxvt as the default terminal emulator..." - try sudo update-alternatives --set x-terminal-emulator "$(command -v urxvt)" -} - -install_wpr() { - local cache_dir="$1" - local bin_dir="$2" - - local version="0.1.0" - - print_header "Installing wpr..." - - local wpr_dir="$cache_dir/wpr/$version" - local wpr_exe="$wpr_dir/wpr" - if [ -f "$wpr_exe" ]; then - echo "$wpr_exe already exists, skipping installation..." - return - fi - - echo "Downloading wpr $version..." - mkdir -p "$wpr_dir" - local tarball_name="wpr.$version.linux-amd64.tar.gz" - local s3_url="https://s3-us-west-2.amazonaws.com" - local url="$s3_url/pcewing-wpr/releases/$version/$tarball_name" - try curl -L "$url" -o "$wpr_dir/$tarball_name" - - echo "Installing wpr $version..." - try tar --directory "$wpr_dir" -xvf "$wpr_dir/$tarball_name" - try chmod a+rx "$wpr_exe" - try rm -f "$bin_dir/wpr" - try ln -s "$wpr_exe" "$bin_dir/wpr" -} - -install_mpd() { - print_header "Installing mpd" - - if [ ! -z "$(command -v mpd)" ]; then - echo "mpd is already installed, skipping installation..." - return - fi - - apt_install "mpd" - - echo "Disabling the mpd service..." - try sudo systemctl stop mpd.service - try sudo systemctl stop mpd.socket - try sudo systemctl disable mpd.service - try sudo systemctl disable mpd.socket - - echo "Configuring mpd..." - mkdir -p "$HOME/.mpd" - mkdir -p "$HOME/.mpd/playlists" - mkdir -p "$HOME/.local/share/mpd" - - pip_install "python-mpd2" -} - -install_ncmpcpp() { - print_header "Installing ncmpcpp" - - if [ ! -z "$(command -v ncmpcpp)" ]; then - echo "ncmpcpp is already installed, skipping installation..." - return - fi - - apt_install "ncmpcpp" - - echo "Configuring ncmpcpp..." - mkdir -p "$HOME/.config/ncmpcpp" -} - -######## -# Main # -######## - -[[ -z "$DOTFILES" ]] && DOTFILES="$HOME/dot" - -source "$DOTFILES/config/bash/functions.sh" - -# For applications that are built from source, we will put them here -cache_dir="$HOME/.dotcache" && mkdir -p "$cache_dir" -bin_dir="$HOME/bin" && mkdir -p "$bin_dir" - -# Make sure apt is ready to use -apt_update -apt_dist_upgrade - -# Install everything via apt that is available in the default repositories -install_apt_packages - -# Set up a simple xsession desktop file that display managers will recognize. -# This will execute /etc/X11/Xsession which in turn executes the .xsession in -# the user's home directory. -configure_xsession \ - "$DOTFILES/config/xsession.desktop" \ - "/usr/share/xsessions/xsession.desktop" - -# Set up a wayland session for sway that the display manager will recognize -configure_wayland_session \ - "$DOTFILES/config/sway-user.desktop" \ - "/usr/share/wayland-sessions/sway-user.desktop" - -# Install everything else that needs special attention -install_urxvt -install_neovim -install_i3gaps "$cache_dir" -install_cava "$cache_dir" -install_youtube-dl "$cache_dir" "$bin_dir" -install_wpr "$cache_dir" "$bin_dir" -install_mpd -install_ncmpcpp diff --git a/provision/ubuntu_22.04.sh b/provision/ubuntu_22.04.sh deleted file mode 100755 index edd186a..0000000 --- a/provision/ubuntu_22.04.sh +++ /dev/null @@ -1,484 +0,0 @@ -#!/usr/bin/env bash - -function yell () { >&2 echo "$*"; } -function die () { yell "$*"; exit 1; } -function try () { "$@" || die "Command failed: $*"; } - -SCRIPT_PATH="$( realpath "$0" )" -SCRIPT_DIR="$( dirname "$SCRIPT_PATH" )" - -print_header() { - local header="$1" - - echo -e "\\n" - echo "$header" - echo "========================================" -} - -apt_update() { - echo "(Apt) Updating package lists... " - try sudo apt-get -y update -} - -apt_dist_upgrade() { - echo "(Apt) Upgrading packages... " - try sudo apt-get -y dist-upgrade -} - -apt_install() { - local packages="$@" - - echo "(Apt) Installing $packages... " - try sudo apt-get -y install $packages -} - -pip_install() { - local packages="$@" - - echo "(Pip) Installing $packages... " - try python3 -m pip install $packages -} - -function get_latest_github_release() { - local org="$1" - local repo="$2" - - local api_url="https://api.github.com/repos/$org/$repo/releases/latest" - echo "$( curl --silent "$api_url" | jq -r .tag_name )" -} - -###################################### -# Application Installation Functions # -###################################### - -configure_xsession() { - local src_path="$1" - local dst_path="$2" - - echo "Configuring xsession... " - - [ -f "$src_path" ] || die "File $src_path does not exist!" - - try sudo rm -f "$dst_path" - try sudo cp "$src_path" "$dst_path" - try sudo chmod 644 "$dst_path" -} - -configure_wayland_session() { - local src_path="$1" - local dst_path="$2" - - echo "Configuring wayland session... " - - [ -f "$src_path" ] || die "File $src_path does not exist!" - - try sudo rm -f "$dst_path" - try sudo cp "$src_path" "$dst_path" - try sudo chmod 644 "$dst_path" -} - -install_apt_packages() { - local p - - # Core utitilies - p="apt-utils" - p+=" ca-certificates" - p+=" curl" - p+=" wget" - p+=" gnupg" - p+=" jq" - p+=" software-properties-common" - p+=" apt-file" - p+=" libfuse2" # This is required to use AppImage - p+=" locate" - p+=" fzf" - p+=" net-tools" - p+=" unzip" - p+=" uchardet" # Useful for detecting text file encoding - - # Basic command line utitilies - p+=" make" - p+=" build-essential" - p+=" cmake" - p+=" meson" - p+=" htop" - p+=" iotop" - p+=" git" - p+=" vim" - #p+=" exuberant-ctags" - p+=" universal-ctags" # I think this has better c++11 support - p+=" ranger" - p+=" tmux" - p+=" neofetch" - p+=" id3v2" - p+=" calcurse" - p+=" rxvt-unicode" - p+=" clang" - p+=" clangd" - - # Python - p+=" python3 python3-dev python3-pip" # Python 3.x - - # General GUI Applications - p+=" fonts-font-awesome" # Used for media buttons on polybar - p+=" rofi" # Fuzzy application launcher - p+=" dunst" # Desktop notifications - p+=" feh" # Set wallpaper - p+=" sxiv" # Image viewer - p+=" nitrogen" # Set wallpaper - p+=" pavucontrol" # Pulse Audio frontend - p+=" compton" # Window compositor - p+=" scrot" # Screen capture - p+=" gucharmap" # Useful for debugging font issues - p+=" keepassxc" # Credential manager - p+=" remmina" # RDP session manager - p+=" usb-creator-gtk" # Easily flash bootable USBs - p+=" i3lock" # Lock screen - p+=" meld" # Diff tool - p+=" xclip" # Clipboard for X11 - p+=" wl-clipboard" # Clipboard for Wayland - p+=" xdotool" # X11 automation tool - p+=" kitty" # Kitty terminal emulator - p+=" kitty-terminfo" # Kitty TERMINFO - p+=" webp" # Command line support for webp image files - - # Media - p+=" inkscape" # Vector graphics editor - p+=" mpv" # Minimal media player - p+=" vlc" # General purpose FOSS media player - p+=" easytag" # Edit ID3 Tags on MP3 files - p+=" blueman" # Bluetooth device support - - # Gaming - p+=" steam" - p+=" steam-devices" - - apt_install "$p" -} - -install_neovim() { - print_header "Installing neovim" - - local nvim_path="/usr/local/bin/nvim" - if [ -f "$nvim_path" ]; then - echo "$nvim_path already exists, skipping installation..." - return - fi - - try sudo mkdir -p /opt/neovim - curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim.appimage - try sudo chmod u+x nvim.appimage - try sudo mv nvim.appimage /opt/neovim/nvim - try sudo ln -s /opt/neovim/nvim $nvim_path - - echo "Installing pynvim python modules..." - try sudo pip3 install --upgrade pynvim - - echo "Updating alternatives to use nvim..." - try sudo update-alternatives --install /usr/bin/vi vi "$nvim_path" 60 - try sudo update-alternatives --set vi "$nvim_path" - try sudo update-alternatives --install /usr/bin/vim vim "$nvim_path" 60 - try sudo update-alternatives --set vim "$nvim_path" - try sudo update-alternatives --install /usr/bin/editor editor "$nvim_path" 60 - try sudo update-alternatives --set editor "$nvim_path" -} - -install_cava() { - local cache_dir="$1" - - local version="$(get_latest_github_release "karlstav" "cava")" - - print_header "Installing cava ($version)" - - local cava_dir="$cache_dir/cava/$version" - local cava_exe="$cava_dir/cava" - if [ -f "$cava_exe" ]; then - echo "$cava_exe already exists, skipping installation..." - return - fi - - echo "Installing pre-requisites..." - apt_install "libfftw3-dev libasound2-dev libncursesw5-dev libpulse-dev libtool" - - echo "Cloning the cava repository" - try mkdir -p "$(dirname -- "$cava_dir")" - try git clone "https://github.com/karlstav/cava" "$cava_dir" - - local pwd; pwd="$(pwd)" - try cd "$cava_dir" - - echo "Building cava $version..." - try ./autogen.sh - try ./configure - try make - - echo "Installing cava $version..." - try sudo make install - - try cd "$pwd" -} - -install_i3gaps() { - local cache_dir="$1" - - local version="$(get_latest_github_release "airblader" "i3")" - - print_header "Installing i3-gaps ($version)" - - local i3gaps_dir="$cache_dir/i3gaps/$version" - local i3gaps_exe="$i3gaps_dir/build/i3" - if [ -f "$i3gaps_exe" ]; then - echo "$i3gaps_exe already exists, skipping installation..." - return - fi - - echo "Installing pre-requisites..." - apt_install "libxcb1-dev libxcb-keysyms1-dev libpango1.0-dev libxcb-util0-dev libxcb-icccm4-dev libyajl-dev libstartup-notification0-dev libxcb-randr0-dev libev-dev libxcb-cursor-dev libxcb-xinerama0-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev autoconf libxcb-xrm0 libxcb-xrm-dev libxcb-shape0 libxcb-shape0-dev automake" - - echo "Cloning the i3-gaps repository" - try mkdir -p "$(dirname -- "$i3gaps_dir")" - try git clone "https://www.github.com/Airblader/i3" "$i3gaps_dir" - - local pwd; pwd="$(pwd)" - try cd "$i3gaps_dir" - - echo "Checkout out version $version..." - try git checkout "$version" - - echo "Building i3-gaps $version..." - try mkdir -p build - try cd build - try meson .. - try ninja - - echo "Installing i3-gaps $version..." - try sudo meson install - - apt_install "i3status" - pip_install "py3status" - - try cd "$pwd" -} - -# TODO: youtube-dl was discontinued. Update this to install yt-dlp instead: -# https://www.linuxadictos.com/en/yt-dlp-fork-sucesor-del-descontinuado-youtube-dl-que-permite-descargar-videos-de-decenas-de-plataformas.html -install_youtube-dl() { - local cache_dir="$1" - local bin_dir="$2" - - local version="$(get_latest_github_release "ytdl-org" "youtube-dl")" - - print_header "Installing youtube-dl ($version)" - - local ytdl_dir="$cache_dir/youtube-dl/$version" - local ytdl_exe="$ytdl_dir/youtube-dl" - if [ -f "$ytdl_exe" ]; then - echo "$ytdl_exe already exists, skipping installation..." - return - fi - - echo "Downloading youtube-dl $version..." - mkdir -p "$ytdl_dir" - try curl -L "https://yt-dl.org/downloads/$version/youtube-dl" -o "$ytdl_exe" - - echo "Installing youtube-dl $version..." - try chmod a+rx "$ytdl_exe" - try rm -f "$bin_dir/youtube-dl" - try ln -s "$ytdl_exe" "$bin_dir/youtube-dl" -} - -install_kitty() { - local tmp_dir version tar_file cwd kitty_path - - cwd="$(pwd)" - - version="$(get_latest_github_release "kovidgoyal" "kitty")" - - if [ -z "$version" ]; then - die "Failed to determine latest kitty release" - fi - - tmp_dir="$HOME/Downloads/kitty/$version" - try mkdir -p "$tmp_dir" - try cd "$tmp_dir" - - # Make sure to strip the 'v' from the version out of the file name - tar_file="kitty-${version/v/}-x86_64.txz" - try wget "https://github.com/kovidgoyal/kitty/releases/download/$version/$tar_file" - - try tar -xJf "$tar_file" - try rm "$tar_file" - - try sudo mkdir -p "/opt/kitty" - try sudo rm -rf "/opt/kitty/$version" - try sudo mv "$tmp_dir" "/opt/kitty/$version" - - try sudo rm "/usr/local/bin/kitty" - try sudo ln -s "/opt/kitty/$version/bin/kitty" "/usr/local/bin/kitty" - try sudo rm "/usr/local/bin/kitten" - try sudo ln -s "/opt/kitty/$version/bin/kitten" "/usr/local/bin/kitten" - - kitty_path="$(command -v kitty)" - - echo "Setting the default terminal emulator to $default_terminal" - try sudo update-alternatives --install /usr/bin/x-terminal-emulator x-terminal-emulator "$kitty_path" 50 - try sudo update-alternatives --set x-terminal-emulator "$kitty_path" - - try cd "$cwd" -} - -install_wpr() { - local cache_dir="$1" - local bin_dir="$2" - - local version="0.1.0" - - print_header "Installing wpr..." - - local wpr_dir="$cache_dir/wpr/$version" - local wpr_exe="$wpr_dir/wpr" - if [ -f "$wpr_exe" ]; then - echo "$wpr_exe already exists, skipping installation..." - return - fi - - echo "Downloading wpr $version..." - mkdir -p "$wpr_dir" - local tarball_name="wpr.$version.linux-amd64.tar.gz" - local s3_url="https://s3-us-west-2.amazonaws.com" - local url="$s3_url/pcewing-wpr/releases/$version/$tarball_name" - try curl -L "$url" -o "$wpr_dir/$tarball_name" - - echo "Installing wpr $version..." - try tar --directory "$wpr_dir" -xvf "$wpr_dir/$tarball_name" - try chmod a+rx "$wpr_exe" - try rm -f "$bin_dir/wpr" - try ln -s "$wpr_exe" "$bin_dir/wpr" -} - -install_mpd() { - print_header "Installing mpd" - - if [ ! -z "$(command -v mpd)" ]; then - echo "mpd is already installed, skipping installation..." - return - fi - - apt_install "mpd" - - echo "Disabling the system mpd service..." - try sudo systemctl stop --now mpd.service - try sudo systemctl stop --now mpd.socket - try sudo systemctl disable mpd.service - try sudo systemctl disable mpd.socket - try sudo systemctl mask mpd.service - try sudo systemctl mask mpd.socket - - echo "Disabling the user mpd service..." - try systemctl stop --now --user mpd.service - try systemctl stop --now --user mpd.socket - try systemctl disable --user mpd.service - try systemctl disable --user mpd.socket - try systemctl mask --user mpd.service - try systemctl mask --user mpd.socket - - echo "Configuring mpd..." - mkdir -p "$HOME/.mpd" - mkdir -p "$HOME/.mpd/playlists" - mkdir -p "$HOME/.local/share/mpd" - - # TODO: This doesn't seem to be working? Or maybe it's because I updated - # Ubuntu and my pip packages disappeared? But this was missing and breaking - # my i3 status bar. - pip_install "python-mpd2" -} - -install_ncmpcpp() { - print_header "Installing ncmpcpp" - - if [ ! -z "$(command -v ncmpcpp)" ]; then - echo "ncmpcpp is already installed, skipping installation..." - return - fi - - apt_install "ncmpcpp" - - echo "Configuring ncmpcpp..." - mkdir -p "$HOME/.config/ncmpcpp" -} - -install_flavours() { - local tmp_dir version tar_file cwd - - cwd="$(pwd)" - - version="$(get_latest_github_release "Misterio77" "flavours")" - - if [ -z "$version" ]; then - die "Failed to determine latest flavours release" - fi - - tmp_dir="$HOME/Downloads/flavours/$version" - try mkdir -p "$tmp_dir" - try cd "$tmp_dir" - - tar_file="flavours-${version}-x86_64-linux.tar.gz" - try wget "https://github.com/Misterio77/flavours/releases/download/$version/$tar_file" - - try tar -xzf "$tar_file" - try rm "$tar_file" - - try sudo mkdir -p "/opt/flavours" - try sudo rm -rf "/opt/flavours/$version" - try sudo mv "$tmp_dir" "/opt/flavours/$version" - try sudo rm -f "/usr/local/bin/flavours" - try sudo ln -s "/opt/flavours/$version/flavours" "/usr/local/bin/flavours" - - flavours update all &>/dev/null - - try cd "$cwd" -} - -######## -# Main # -######## - -[[ -z "$DOTFILES" ]] && DOTFILES="$( realpath "$SCRIPT_DIR/.." )" - -source "$DOTFILES/config/bash/functions.sh" - -# For applications that are built from source, we will put them here -cache_dir="$HOME/.dotcache" && mkdir -p "$cache_dir" -bin_dir="$HOME/bin" && mkdir -p "$bin_dir" - -# Make sure apt is ready to use -apt_update -apt_dist_upgrade - -# Install everything via apt that is available in the default repositories -install_apt_packages - -# Set up a simple xsession desktop file that display managers will recognize. -# This will execute /etc/X11/Xsession which in turn executes the .xsession in -# the user's home directory. -configure_xsession \ - "$DOTFILES/config/xsession.desktop" \ - "/usr/share/xsessions/xsession.desktop" - -# Set up a wayland session for sway that the display manager will recognize -configure_wayland_session \ - "$DOTFILES/config/sway-user.desktop" \ - "/usr/share/wayland-sessions/sway-user.desktop" - -# Install everything else that needs special attention -install_kitty -install_neovim -install_i3gaps "$cache_dir" -#install_cava "$cache_dir" -install_youtube-dl "$cache_dir" "$bin_dir" -install_wpr "$cache_dir" "$bin_dir" -install_mpd -install_ncmpcpp -install_flavours - -# TODO: Install picom from source diff --git a/templates/set-bg.sh b/templates/set-bg.sh new file mode 100644 index 0000000..e3e54e9 --- /dev/null +++ b/templates/set-bg.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Set background wallpaper to the default +if command -v feh >/dev/null 2>&1; then + if [ -f "$HOME/Pictures/default_wallpaper.png" ]; then + feh --bg-scale "$HOME/Pictures/default_wallpaper.png" + fi +fi diff --git a/todo.md b/todo.md deleted file mode 100644 index 6753bc0..0000000 --- a/todo.md +++ /dev/null @@ -1,431 +0,0 @@ -# To-Do List - -Improvements I'd like to make to my dotfiles. - -## Table of Contents - -- [High Priority](#high-priority) -- [Misc](#misc) -- [Python CLI](#python-cli) - - [Bootstrapper](#bootstrapper) - - [Provisioner Groups or Tags](#provisioner-groups-or-tags) - - [Provisioner Command Logging](#provisioner-command-logging) - - [Check for Updates Feature](#check-for-updates-feature) - - [Code Cleanup](#code-cleanup) - - [Necessary Pip Packages](#necessary-pip-packages) -- [FZF Bash Integration](#fzf-bash-integration) -- [Windows support in Python CLI](#windows-support-in-python-cli) -- [wezterm shell integration](#wezterm-shell-integration) -- [I3WM "Virtual Desktops"](#i3wm-"virtual-desktops") -- [Python Tidy/Lint](#python-tidy/lint) -- [Don't symlink vim to neovim](#don't-symlink-vim-to-neovim) -- [Neovim Healtheck](#neovim-healtheck) -- [Errors](#errors) - - [nvim-lua/completion-nvim](#nvim-lua/completion-nvim) - - [nvim-telescope/telescope.nvim](#nvim-telescope/telescope.nvim) -- [Warnings](#warnings) - - [glepnir/lspsaga.nvim](#glepnir/lspsaga.nvim) - - [nvim-treesitter/nvim-treesitter](#nvim-treesitter/nvim-treesitter) - - [Providers](#providers) - - [nvim-telescope/telescope.nvim](#nvim-telescope/telescope.nvim) - - [neovim/nvim-lspconfig](#neovim/nvim-lspconfig) - -## High Priority - -UltiSnips freezes sometimes in Neovim which is really annoying and was marked as won't fix because it's specific to Neovim: - -https://github.com/SirVer/ultisnips/issues/1381 - -We should switch to another snippet plugin, maybe `vim-vsnip` since I see that's what someone else did: - -https://github.com/Sangdol/vimrc/commit/b6c5cf06b761b17d5b39c39a2ae9ad584f48761a - -## Misc - -- [ ] Clean up Neovim Healthcheck (**Neovim Healtheck** section) -- [ ] Change path address bar behavior in Nautilus? - - `dconf write /org/gnome/nautilus/preferences/always-use-location-entry true` - - Not sure if I actually like this, just need to remember the `Ctrl + l` hotkey - -## XP Submodule - -Use `xp` as a submodule to DRY our Python. - -## Python CLI - -### Git Sync Command - -Implement a simple `dot git-sync` command and a git alias to it like `git sync` -that does something like: - -``` -- Check if there are commits missing from upstream -- If there are, pull -- If there are merge conflicts, abort and print an error - - These should be handled manually -- If the merge is clean, continue on -- Add all local changes -- Commit local changes -- Push -``` - -This is for repositories like my notes where I basically just always want to -keep everything in sync and don't use branches. Optionally, accept a parameter -for commit message. - -#### Progress - -I started on this but it probably isn't bullet-proof yet. What it does: - -- If there are local changes that need to be committed - - Create a temporary branch and resolve/commit the changes in it - - This is a bit complicated and might be bug prone -- Fetch from all remotes -- Detect the most recent matching commit between the local and remote -- Get the number of commits the local repository is missing from remote and - vice versa -- Pull remote commits if there are any missing from local -- If there were local changes, cherry-pick them from the temp branch -- If remote is missing any local commits, push - -One thing I might want to change is to push the temporary branch to remote. I -just encountered an issue where I ran the sync command on my desktop PC and I -think it errored and I forgot to go back and resolve it. Now, working on my -laptop, I'm missing those changes. Had I at least pushed the temp branch, I -could have pulled it down and fixed it on my laptop but since I'm travelling -I'm just out of luck. - -### Provisioner Output - -Better display which provisioners passed, failed, or didn't run because right -now if something fails half-way through it's very annoying to figure out where -to start again. GitHub's rate limiting seems pretty aggressive so re-running -the whole thing fails due to 403 errors. - -### Implement More Provisioners - -Add a "proprietary" tag for: - -- Insync -- Beyond Compare 4 / 5 -- Parsec - -### Logging Noise - -We should change most of the logs to debug to reduce noise in the output. - -### Bootstrapper - -- Write a shell script to bootstrap so that `dot` can be run. - - [ ] Maybe put bootstrapper in a Gist so it's easier to grab on a new - system and have it set up git ssh keys, clone the dotfiles repo, etc? - -- Things it needs: - - Install Python and pip packages - - `python -m pip install --user --upgrade argcomplete` - -### Provisioner Groups or Tags - -It would be nice to have "groups" of provisioners so it's easy to -include/exclude a set of features. For example, a group for all of the X11 -applications so that they can be excluded when installing on a system without a -DE/WM. Or a group for WSL. - -Maybe it would be better to just have tags? So for example I could say: - -``` -dot provision --tags=wsl ... -``` - -And then the provisioners could just respect those tags. That's probably easier -actually. - -We could also default those tags intelligently by detecting whether or not -we're running in WSL, X11, etc. - -Maybe we could even just use tags for distro? Like rather than having separate -library folders for `jammy`, etc. In many cases the distro isn't going to -matter, especially when the only difference is version. If we ever want to -write a provisioner for an entirely different distro like Manjaro, we can -tackle that then but realistically, YAGNI. - -Instead of just tags, maybe we expand slightly to "attributes" which are -effectively key-value pairs instead of just values. So we could have attributes -like: - -``` -distro_family = "ubuntu", "centos" -distro_version = "20.04", 7.9 -wsl = true, false -window_manager = "i3", None -desktop_environment = "i3", None -graphical_environment = true, false -``` - -Then when running, these are all auto-detected but can be overriden via command -line options like so: - -``` -dot provision --attr "wsl=true" -``` - -I'll need to make sure `argparse` supports specifying options multiple times -but if not, we can just make the option `--attributes` and expect the value to -be a comma-delimited list. - -There are some attributes that we won't be able to default. Like say we want -one that dictates whether or not to install gaming software like `steam`. That -would just have to default to `False` and then if we want that installed we -could either do it manually afterwards by specifying the provisioner: - -``` -dot provision steam -``` - -Or add that attribute when provisioning everything: - -``` -dot provision --all --attr "gaming=true" -``` - -I think this is the approach I like the best so far. - -### Provisioner Command Logging - -When provisioners run external commands like `apt update` that generate a lot -of output, it makes the CLI output confusing. Maybe we can dump command output -to a log file so it's hidden during execution and then link to it in the case -of an exception/error? - -### Check for Updates Feature - -Dry-run is nice for seeing what will happen when I run the script but it would -be even better to have a "Check for updates" command. So I could see if there's -a new version of Neovim for example. - -Maybe like a `status` command? So it could be used like: - -``` -dot provision --status neovim -``` - -And would output something like: - -``` -Status: -- Neovim: Up-to-Date (0.9.5) <-- In green -``` - -Or: - -``` -Status: -- Neovim: Update available (0.9.4 -> 0.9.5) <-- In yellow -``` - -And could be run for multiple (Or `--all` provisioners): - -``` -$ dot provision --status --all - -Status: -- Neovim: Up-to-Date (0.9.5) <-- In green -- Flavours: Update available (v0.7.1 -> v0.7.2) <-- In yellow -``` - -### Code Cleanup - -- Remove the functions like `mkdir_p` in `util.py` and use the alternatives in - `shell.py` -- Use Python native facilities instead of the functions in `shell.py` - - These were used so that we could use `sudo` but now the tool just - elevates itself to root - -### Necessary Pip Packages - -python3 -m pip install typing_extensions - -Also needs to be installed as root if the script elevates -sudo python3 -m pip install typing_extensions - -### Windows Support - -- Don't need to implement full provisioning but at least get clean/link commands to work on Windows -- Might be nice to have a script to provision WezTerm on Windows -- Remove dot.sh and Makefile (Except maybe for bootstrapping) - -- Make sure the following is added to path before running `dot provision cli` - - `C:\Users\pewing\AppData\Roaming\Python\Python310\Scripts` - - Update version in the path as necessary - - Tools install via Pip aren't automatically added to PATH like they are on Linux - - TODO: Actually this is just broken altogether, the following fails when run directly in Git Bash: - - `register-python-argcomplete --external-argcomplete-script $HOME/dot/cli/dot.py dot` - - So it may just not play nicely with windows - - For now, maybe just copy it from Linux and update the paths? - - -## FZF Bash Integration - -`~/.fzf.bash` doesn't exist for me, maybe because I'm installing via apt. I'd -like that so I can get fzf `ctrl+r` functionality so update the provision -script to set that up correctly. - -## wezterm shell integration - -Automatically download wezterm.sh and source it in ~/.localrc or at least document this for WSL setup - -## I3WM "Virtual Desktops" - -10 workspaces isn't always enough. It would be nice to do something that -provides a similar workflow to virtual desktops on Windows. Like, 4 virtual -desktops that each have 10 workspaces. Maybe as an MVP, have a keyboard -shortcut that switches between the desktops and remaps keybindings accordingly. - -I've started noodling on a hacky PoC for this in `bin/i3-util.sh` - -## Python Tidy/Lint - -- [ ] Look into `ruff` since it may replace several other dependencies and also - claims to be much faster -- [ ] Set up a pre-commit hook to ensure files are always linted? - - [ ] Probably can't do this without significant work to fix all static - typing - -## Don't symlink vim to neovim - -Now that we've split our configs let's not link `vi` and `vim` to Neovim. - -## Neovim Healtheck - -- In nvim, run `:healthcheck` and go through the errors/warnings: - -### Errors - -#### nvim-lua/completion-nvim - -``` -completion: require("completion.health").check() - -- ERROR Failed to run healthcheck for "completion" plugin. Exception: - function health#check, line 25 - Vim(eval):E5108: Error executing lua [string "luaeval()"]:1: attempt to call field 'check' (a nil value) - stack traceback: - [string "luaeval()"]:1: in main chunk - -============================================================================== -completion_nvim: health#completion_nvim#check - -general ~ -- OK neovim version is supported - -completion source ~ -- OK all completion sources are valid - -snippet source ~ -- ERROR Your snippet source is not available! Possible values are: UltiSnips, Neosnippet, vim-vsnip, snippets.nvim -``` - -#### nvim-telescope/telescope.nvim - -``` -============================================================================== -telescope: require("telescope.health").check() - -Checking for required plugins ~ -- OK plenary installed. -- OK nvim-treesitter installed. - -Checking external dependencies ~ -- ERROR rg: not found. `live-grep` finder will not function without [BurntSushi/ripgrep](https://github.com/BurntSushi/ripgrep) installed. -- WARNING fd: not found. Install [sharkdp/fd](https://github.com/sharkdp/fd) for extended capabilities - -===== Installed extensions ===== ~ -``` - -### Warnings - -#### glepnir/lspsaga.nvim - -``` -============================================================================== -lspsaga: require("lspsaga.health").check() - -Lspsaga.nvim report ~ -- WARNING `tree-sitter` executable not found -- OK tree-sitter `markdown` parser found -- OK tree-sitter `markdown_inline` parser found -``` - -#### nvim-treesitter/nvim-treesitter - -``` -============================================================================== -nvim-treesitter: require("nvim-treesitter.health").check() - -Installation ~ -- WARNING `tree-sitter` executable not found (parser generator, only needed for :TSInstallFromGrammar, not required for :TSInstall) -- WARNING `node` executable not found (only needed for :TSInstallFromGrammar, not required for :TSInstall) -- OK `git` executable found. -- OK `cc` executable found. Selected from { vim.NIL, "cc", "gcc", "clang", "cl", "zig" } - Version: cc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 -- OK Neovim was compiled with tree-sitter runtime ABI version 14 (required >=13). Parsers must be compatible with runtime ABI. -``` - -#### Providers - -Is there a way we can say we intentionally don't want these providers to get -the warnings to go away? - -``` -============================================================================== -provider: health#provider#check - -Ruby provider (optional) ~ -- WARNING `ruby` and `gem` must be in $PATH. - - ADVICE: - - Install Ruby and verify that `ruby` and `gem` commands work. - -Node.js provider (optional) ~ -- WARNING `node` and `npm` (or `yarn`, `pnpm`) must be in $PATH. - - ADVICE: - - Install Node.js and verify that `node` and `npm` (or `yarn`, `pnpm`) commands work. - -Perl provider (optional) ~ -- WARNING "Neovim::Ext" cpan module is not installed - - ADVICE: - - See :help |provider-perl| for more information. - - You may disable this provider (and warning) by adding `let g:loaded_perl_provider = 0` to your init.vim -``` - -#### nvim-telescope/telescope.nvim - -``` -============================================================================== -telescope: require("telescope.health").check() - -Checking for required plugins ~ -- OK plenary installed. -- OK nvim-treesitter installed. - -Checking external dependencies ~ -- ERROR rg: not found. `live-grep` finder will not function without [BurntSushi/ripgrep](https://github.com/BurntSushi/ripgrep) installed. -- WARNING fd: not found. Install [sharkdp/fd](https://github.com/sharkdp/fd) for extended capabilities - -===== Installed extensions ===== ~ -``` - -#### neovim/nvim-lspconfig - -``` -============================================================================== -vim.lsp: require("vim.lsp.health").check() - -- LSP log level : TRACE -- WARNING Log level TRACE will cause degraded performance and high disk usage -- Log path: /home/pewing/.local/state/nvim/lsp.log -- Log size: 333 KB - -vim.lsp: Active Clients ~ -- No active clients -``` - From 9f05671cf91e817cd17e01bbf28b2f1810afe5a6 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 5 Jan 2026 13:35:12 -0800 Subject: [PATCH 38/39] AI recommended improvements --- apply.sh | 3 ++- bin/docker_clean.sh | 13 +++++-------- bin/fzf_cached_wsl | 3 ++- bin/set-theme | 3 +-- bin/startup.sh | 5 +++-- cli/lib/common/file_walker.py | 5 +++-- cli/lib/common/git.py | 6 ++++-- cli/lib/common/log.py | 24 +++++++++++++----------- config/bash/aliases.sh | 3 ++- config/bash/functions.sh | 16 ++++++++++------ config/bashrc | 5 +++-- config/env | 8 ++++---- config/nvim/lua/dot/lsp_clangd.lua | 3 ++- config/nvim/lua/dot/lsp_gopls.lua | 3 ++- config/nvim/lua/dot/util.lua | 8 ++++++-- config/vimrc | 20 ++++++++++++++++---- config/vsvimrc | 12 +----------- config/wezterm.lua | 7 ++++--- doc/setup_ubuntu.md | 4 ++-- nix/home/roles/gaming.nix | 4 ++-- nix/home/roles/wsl.nix | 2 ++ 21 files changed, 89 insertions(+), 68 deletions(-) diff --git a/apply.sh b/apply.sh index 1885083..07a2da6 100755 --- a/apply.sh +++ b/apply.sh @@ -505,7 +505,8 @@ set_default_terminal_and_editor() { echo "[bootstrap] Setting system defaults via update-alternatives..." if host_has_role "desktop"; then - local nvim_path + # BUG FIX: Removed incorrect variable declaration - was "nvim_path" but should be "kitty_path" + local kitty_path kitty_path="$(command -v kitty || true)" if [[ -n "$kitty_path" ]]; then diff --git a/bin/docker_clean.sh b/bin/docker_clean.sh index b251cae..22d69d5 100755 --- a/bin/docker_clean.sh +++ b/bin/docker_clean.sh @@ -1,20 +1,17 @@ #!/usr/bin/env bash -function destroy_containers() { - containers="$(docker ps -a \ - | grep -Ev '^CONTAINER' \ - | awk '{print $1}')" +# IMPROVEMENT: Use docker's built-in filtering with -q flag instead of grep/awk parsing. +# This is more reliable as it avoids issues with column alignment or format changes. +function destroy_containers() { + containers="$(docker ps -aq)" if [ -n "$containers" ]; then echo "$containers" | xargs docker rm -f fi } function destroy_images() { - images="$(docker images \ - | grep -Ev '^REPOSITORY' \ - | awk '{print $3}')" - + images="$(docker images -q)" if [ -n "$images" ]; then echo "$images" | xargs docker rmi fi diff --git a/bin/fzf_cached_wsl b/bin/fzf_cached_wsl index c43e9e2..d1359f5 100755 --- a/bin/fzf_cached_wsl +++ b/bin/fzf_cached_wsl @@ -145,7 +145,8 @@ class FuzzyFileFinder: # Keep this commented out for performance except when debugging # Log.debug("ignoring file", {"ignore_pattern": ignore_pattern, "file": path_rel}) return - except: + # IMPROVEMENT: Use specific exception type instead of bare except + except re.error: print("ignore_pattern = {}, path_rel = {}".format(ignore_pattern, path_rel)) raise diff --git a/bin/set-theme b/bin/set-theme index 596a570..38e84b1 100755 --- a/bin/set-theme +++ b/bin/set-theme @@ -4,8 +4,7 @@ function yell () { >&2 echo "$*"; } function die () { yell "$*"; exit 1; } function try () { "$@" || die "Command failed: $*"; } -script_path="$( realpath "$0" )" -script_dir="$( dirname "$script_path" )" +# IMPROVEMENT: Removed unused script_path and script_dir variables theme="$( flavours list | tr " " "\n" | rofi -i -dmenu )" if test -z "$theme"; then diff --git a/bin/startup.sh b/bin/startup.sh index 050d063..4ae45f4 100755 --- a/bin/startup.sh +++ b/bin/startup.sh @@ -30,7 +30,8 @@ function start_process() { # If it's not running just run it if [ "$running" = "0" ]; then - $procname $procargs >> "$logfile" 2>&1 & + # BUG FIX: Quoted $procargs to prevent word splitting issues with args containing spaces + $procname "$procargs" >> "$logfile" 2>&1 & return fi @@ -41,7 +42,7 @@ function start_process() { sleep 0.1 done - $procname $procargs >> "$logfile" 2>&1 & + $procname "$procargs" >> "$logfile" 2>&1 & fi } diff --git a/cli/lib/common/file_walker.py b/cli/lib/common/file_walker.py index b46940b..e92defb 100644 --- a/cli/lib/common/file_walker.py +++ b/cli/lib/common/file_walker.py @@ -113,8 +113,9 @@ def _walk(ctx: Context, directory: Directory) -> None: elif dir_entry.is_file(follow_symlinks=True): FileWalker._handle_file(ctx, FileWalker.File(ctx.root, path_rel)) elif dir_entry.is_symlink(): + # BUG FIX: Fixed typo "non-existant" -> "non-existent" Log.debug( - "encountered symlink directory entry with non-existant target" + "encountered symlink directory entry with non-existent target" ) else: Log.warn( @@ -140,7 +141,7 @@ def _handle_dir(ctx: Context, directory: Directory) -> None: if ctx.halt: return - # If handler requested to skip, do nothin + # If handler requested to skip, do nothing if result.skip: return diff --git a/cli/lib/common/git.py b/cli/lib/common/git.py index e073e51..c19b6b4 100644 --- a/cli/lib/common/git.py +++ b/cli/lib/common/git.py @@ -6,9 +6,10 @@ from lib.common.log import Log +# BUG FIX: Return type annotation was str but function returns list[str] def _execute_git_command( cmd: list[str], strip: bool = True, filter_empty: bool = True -) -> str: +) -> list[str]: p = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) stdout, _ = p.communicate() if p.returncode != 0: @@ -235,7 +236,8 @@ def add_all() -> None: @staticmethod def commit(message: str) -> None: - Log.debug("commiting staged changes") + # BUG FIX: Fixed typo "commiting" -> "committing" + Log.debug("committing staged changes") subprocess.check_call(["git", "commit", "--message", message]) @staticmethod diff --git a/cli/lib/common/log.py b/cli/lib/common/log.py index 6dbeba4..e2fc018 100644 --- a/cli/lib/common/log.py +++ b/cli/lib/common/log.py @@ -88,28 +88,30 @@ def init( Log._logger = logger + # IMPROVEMENT: Changed mutable default argument {} to None to avoid the + # well-known Python gotcha where mutable defaults are shared across calls @staticmethod - def debug(msg: str, data: LogData = {}) -> None: - Log._log(logging.DEBUG, msg, data) + def debug(msg: str, data: Optional[LogData] = None) -> None: + Log._log(logging.DEBUG, msg, data or {}) @staticmethod - def info(msg: str, data: LogData = {}) -> None: - Log._log(logging.INFO, msg, data) + def info(msg: str, data: Optional[LogData] = None) -> None: + Log._log(logging.INFO, msg, data or {}) @staticmethod - def warn(msg: str, data: LogData = {}) -> None: - Log._log(logging.WARNING, msg, data) + def warn(msg: str, data: Optional[LogData] = None) -> None: + Log._log(logging.WARNING, msg, data or {}) @staticmethod - def error(msg: str, data: LogData = {}) -> None: - Log._log(logging.ERROR, msg, data) + def error(msg: str, data: Optional[LogData] = None) -> None: + Log._log(logging.ERROR, msg, data or {}) @staticmethod - def fatal(msg: str, data: LogData = {}) -> None: - Log._log(logging.FATAL, msg, data) + def fatal(msg: str, data: Optional[LogData] = None) -> None: + Log._log(logging.FATAL, msg, data or {}) @staticmethod - def _log(level: LogLevel, msg: str, data: LogData = {}) -> None: + def _log(level: LogLevel, msg: str, data: LogData) -> None: if Log._logger is None: return Log._logger.log(level, Log._format_msg(msg, data)) diff --git a/config/bash/aliases.sh b/config/bash/aliases.sh index 482107f..4bc7e30 100644 --- a/config/bash/aliases.sh +++ b/config/bash/aliases.sh @@ -108,7 +108,8 @@ fi # Apt aliases if _is_installed 'apt'; then set_alias '0' 'apti' 'sudo apt install -y' - set_alias '0' 'apts' 'sudo apt search' + # IMPROVEMENT: Removed sudo from apt search - sudo is unnecessary for search operations + set_alias '0' 'apts' 'apt search' fi # Pacman aliases diff --git a/config/bash/functions.sh b/config/bash/functions.sh index 550c517..d33bfa5 100644 --- a/config/bash/functions.sh +++ b/config/bash/functions.sh @@ -35,7 +35,8 @@ function fm() { file_manager="$(xdg-mime query default inode/directory | sed -e 's/\.desktop//')" - if test -z "path"; then + # BUG FIX: Was missing $ before path variable, so condition was always false + if test -z "$path"; then path="." fi @@ -98,10 +99,11 @@ function nvimp() { } # Download the audio from a YouTube video as an MP3 file +# IMPROVEMENT: Changed from youtube-dl to yt-dlp, the actively maintained fork function yt_mp3() { - installed "youtube-dl" || return 1 + installed "yt-dlp" || return 1 - youtube-dl -x --audio-format "mp3" "$1" + yt-dlp -x --audio-format "mp3" "$1" } # WARNING: This shouldn't be called from an interactive shell as the passphrase @@ -284,7 +286,8 @@ function go_test_coverage() { if go test -coverprofile="$tempfile"; then go tool cover -html="$tempfile" else - 1>&1 echo -e "ERROR: Tests failed; to view coverage anyways run:\ngo tool cover -html=\"$tempfile\"" + # BUG FIX: Was using 1>&1 (stdout to stdout, no-op) instead of 1>&2 (stdout to stderr) + 1>&2 echo -e "ERROR: Tests failed; to view coverage anyways run:\ngo tool cover -html=\"$tempfile\"" fi } @@ -556,8 +559,9 @@ function tar_directory() { return 1 fi - path="$(realpath $dir)" - name="$(basename $path)" + # BUG FIX: Added quotes around $dir and $path to handle paths with spaces + path="$(realpath "$dir")" + name="$(basename "$path")" archive_path="/tmp/$name.tar.gz" diff --git a/config/bashrc b/config/bashrc index c9ff3d7..877b459 100644 --- a/config/bashrc +++ b/config/bashrc @@ -22,7 +22,7 @@ declare -a sources=( ) for i in "${sources[@]}"; do - src_if_exists "$i" + src_if_exists "$i" done # Source fzf shell integrations if we have them @@ -63,7 +63,8 @@ if _is_installed "go"; then fi # Rust configuration -if _is_installed "rust"; then +# BUG FIX: Changed "rust" to "rustc" - "rust" is not a command, rustc is the compiler +if _is_installed "rustc"; then [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" fi diff --git a/config/env b/config/env index 3d33051..f7ee632 100644 --- a/config/env +++ b/config/env @@ -11,14 +11,14 @@ is_in_path() { esac } +# BUG FIX: The original implementations had swapped logic - +# "append" was prepending and "prepend" was appending. append_to_path() { - PATH=$1${PATH:+":$PATH"} - export PATH + export PATH="${PATH:+$PATH:}$1" } prepend_to_path() { - p="$1" - export PATH="${PATH}:${p}" + export PATH="$1${PATH:+:$PATH}" } try_append_to_path() { diff --git a/config/nvim/lua/dot/lsp_clangd.lua b/config/nvim/lua/dot/lsp_clangd.lua index 4dc7637..abe0b66 100644 --- a/config/nvim/lua/dot/lsp_clangd.lua +++ b/config/nvim/lua/dot/lsp_clangd.lua @@ -35,7 +35,8 @@ function M.configure() -- server attaches to the current buffer local on_attach = function(client, buf) -- Enable completion triggered by - vim.api.nvim_buf_set_option(buf, 'omnifunc', 'v:lua.vim.lsp.omnifunc') + -- IMPROVEMENT: nvim_buf_set_option is deprecated in favor of vim.bo[buf] + vim.bo[buf].omnifunc = 'v:lua.vim.lsp.omnifunc' -- See `:help vim.lsp.*` for documentation on the below functions Map.nnoremapbs(buf, 'gD', 'lua vim.lsp.buf.declaration()') diff --git a/config/nvim/lua/dot/lsp_gopls.lua b/config/nvim/lua/dot/lsp_gopls.lua index ec90111..c86f6f6 100644 --- a/config/nvim/lua/dot/lsp_gopls.lua +++ b/config/nvim/lua/dot/lsp_gopls.lua @@ -19,7 +19,8 @@ function M.configure() -- language server attaches to the current buffer local on_attach = function(client, buf) -- Enable completion triggered by - vim.api.nvim_buf_set_option(buf, 'omnifunc', 'v:lua.vim.lsp.omnifunc') + -- IMPROVEMENT: nvim_buf_set_option is deprecated in favor of vim.bo[buf] + vim.bo[buf].omnifunc = 'v:lua.vim.lsp.omnifunc' -- See `:help vim.lsp.*` for documentation on the below functions Map.nnoremapbs(buf, 'ld', 'lua vim.lsp.buf.definition()') diff --git a/config/nvim/lua/dot/util.lua b/config/nvim/lua/dot/util.lua index fe105e0..6f8eba8 100644 --- a/config/nvim/lua/dot/util.lua +++ b/config/nvim/lua/dot/util.lua @@ -4,13 +4,17 @@ local Log = require('dot.log') local M = {} +-- IMPROVEMENT: vim.loop was renamed to vim.uv in Neovim 0.10+ +-- Use vim.uv if available, fall back to vim.loop for older versions +local uv = vim.uv or vim.loop + function M.is_windows() -- TODO: Confirm this is right; only checked it on Linux - return vim.loop.os_uname().sysname == "Windows" + return uv.os_uname().sysname == "Windows" end function M.is_linux() - return vim.loop.os_uname().sysname == "Linux" + return uv.os_uname().sysname == "Linux" end function M.path_sep() diff --git a/config/vimrc b/config/vimrc index 77147a3..dcd6b13 100644 --- a/config/vimrc +++ b/config/vimrc @@ -36,8 +36,12 @@ set softtabstop=4 set tabstop=4 set expandtab -" Don't use smart indent in markdown files -autocmd FileType markdown setlocal nosmartindent +" IMPROVEMENT: Wrapped autocmd in augroup to prevent duplicate autocmds on config reload +augroup dotfiles_markdown + autocmd! + " Don't use smart indent in markdown files + autocmd FileType markdown setlocal nosmartindent +augroup END " Set the Leader key. I leave the leader key as '\' and remap ' ' to it " instead of setting ' ' as the leader. This is so that showcmd is actually @@ -110,11 +114,15 @@ nnoremap r :source ~/.vimrc xnoremap n :normal " Install vim-plug automatically on Linux if it isn't already +" IMPROVEMENT: Wrapped VimEnter autocmd in augroup to prevent duplicate autocmds if has('unix') if empty(glob('~/.vim/autoload/plug.vim')) silent !curl -fLo ~/.vim/autoload/plug.vim --create-dirs \ \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim - autocmd VimEnter * PlugInstall --sync | source $MYVIMRC + augroup dotfiles_plug_install + autocmd! + autocmd VimEnter * PlugInstall --sync | source $MYVIMRC + augroup END endif endif @@ -161,7 +169,11 @@ call plug#end() " Format the current C/C++ file with clang-format (Uses vim-clang-format plugin) let g:clang_format#detect_style_file = 1 -autocmd FileType c,cpp vnoremap q :ClangFormat +" IMPROVEMENT: Wrapped autocmd in augroup to prevent duplicate autocmds on config reload +augroup dotfiles_clang_format + autocmd! + autocmd FileType c,cpp vnoremap q :ClangFormat +augroup END " FZF nnoremap o :Files diff --git a/config/vsvimrc b/config/vsvimrc index 04335cb..cf047ee 100644 --- a/config/vsvimrc +++ b/config/vsvimrc @@ -54,6 +54,7 @@ nnoremap 7gt nnoremap 8gt nnoremap 9gt nnoremap 10gt +" BUG FIX: Removed duplicate mapping block that was accidentally included twice nnoremap 1gt nnoremap 2gt nnoremap 3gt @@ -65,17 +66,6 @@ nnoremap 8gt nnoremap 9gt nnoremap 10gt -nnoremap 1gt -nnoremap 2gt -nnoremap 3gt -nnoremap 4gt -nnoremap 5gt -nnoremap 6gt -nnoremap 7gt -nnoremap 8gt -nnoremap 9gt -nnoremap 0gt - nnoremap vo :vsc Edit.GoToFile nnoremap vm :vsc Edit.GoToMember nnoremap vs :vsc Edit.GoToSymbol diff --git a/config/wezterm.lua b/config/wezterm.lua index 8839791..7005493 100644 --- a/config/wezterm.lua +++ b/config/wezterm.lua @@ -4,7 +4,8 @@ local act = wezterm.action local config = {} -function get_wsl_domain() +-- IMPROVEMENT: Added 'local' to helper functions to avoid polluting global namespace +local function get_wsl_domain() -- This should match the entry in the list output by `wsl --list` that -- should be used as the default domain local wsl_domain_name = os.getenv("WEZTERM_WSL_DOMAIN") @@ -16,7 +17,7 @@ function get_wsl_domain() return wsl_domain_name end -function get_shell(tab_info) +local function get_shell(tab_info) local shell = '' if tab_info.active_pane.domain_name == "local" then shell = '(Git Bash) ' @@ -34,7 +35,7 @@ wezterm.on('format-window-title', function(tab, pane, tabs, panes, config) return index .. get_shell(tab) .. tab.active_pane.title end) -function tab_title(tab_info) +local function tab_title(tab_info) local title = tab_info.tab_title if title and #title > 0 then return title diff --git a/doc/setup_ubuntu.md b/doc/setup_ubuntu.md index e2782fe..e8f9156 100644 --- a/doc/setup_ubuntu.md +++ b/doc/setup_ubuntu.md @@ -2,7 +2,7 @@ ## New Machine Bootstrapping -These are the typical steps to perform immediately after the inital Ubuntu +These are the typical steps to perform immediately after the initial Ubuntu installation. Update the system and reboot: @@ -67,7 +67,7 @@ cd ~/dot **Note:** The first time `apply.sh`, nix profile won't be sourced in the active shell. The easiest workaround is to just open a new shell. -**TODO:** We should add a message to the end of the output instructing user to restart shell. We could write a file on the first run and check for its existence on subsequent runs. If it does not exist, prompt the user to restart the computer. Probably not a bad idea on the first bootstrap to make sure everything propogates. +**TODO:** We should add a message to the end of the output instructing user to restart shell. We could write a file on the first run and check for its existence on subsequent runs. If it does not exist, prompt the user to restart the computer. Probably not a bad idea on the first bootstrap to make sure everything propagates. ## Daily Operations diff --git a/nix/home/roles/gaming.nix b/nix/home/roles/gaming.nix index 7ca9c33..521c7b6 100644 --- a/nix/home/roles/gaming.nix +++ b/nix/home/roles/gaming.nix @@ -1,7 +1,7 @@ { pkgs, ... }: { - # Allow proprietary packages for gaming (e.g., Steam) - nixpkgs.config.allowUnfree = true; + # IMPROVEMENT: Removed redundant nixpkgs.config.allowUnfree = true since it's + # already set in core.nix which is always imported alongside gaming.nix home.packages = with pkgs; [ steam diff --git a/nix/home/roles/wsl.nix b/nix/home/roles/wsl.nix index fb5aff2..a0f2546 100644 --- a/nix/home/roles/wsl.nix +++ b/nix/home/roles/wsl.nix @@ -14,7 +14,9 @@ in # Install win32yank to Windows filesystem for clipboard integration # This needs to be on NTFS (not WSL filesystem) for performance reasons + # IMPROVEMENT: Using run to respect verbose/dry-run flags from home-manager home.activation.installWin32yank = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + run() { echo "running: $*"; "$@"; } WIN32YANK_VERSION="${win32yankVersion}" WIN32YANK_URL="${win32yankUrl}" WIN32YANK_DIR="${win32yankInstallDir}" From de81313a5ff7dbce6095203c2c8d47c74bf527a3 Mon Sep 17 00:00:00 2001 From: Paul Ewing Date: Mon, 5 Jan 2026 22:37:16 -0800 Subject: [PATCH 39/39] Clean up suggestions in apply.sh --- apply.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/apply.sh b/apply.sh index 07a2da6..7969752 100755 --- a/apply.sh +++ b/apply.sh @@ -505,7 +505,6 @@ set_default_terminal_and_editor() { echo "[bootstrap] Setting system defaults via update-alternatives..." if host_has_role "desktop"; then - # BUG FIX: Removed incorrect variable declaration - was "nvim_path" but should be "kitty_path" local kitty_path kitty_path="$(command -v kitty || true)"