commit - 07461bbc2e0322a12f588a41d54805de6a87aae4
commit + 6feee7d6ad31b0b2185ed57342a88d3e570bb981
blob - 013f5b458bb09b1c1a60cf170fb3db0e8b02e326
blob + f681cda2dec4d8571d7993a81d215c31c35f77eb
Binary files hosts/impish/overlays/config/magdalena/config.json.nix and hosts/impish/overlays/config/magdalena/config.json.nix differ
blob - beb932be04cc8905c5dad7002ad382e10f4ab557
blob + 3b32bbbdb771a9eadec88ce3d169ddf74a59b07e
--- justfile
+++ justfile
nix flake check --no-update-lock-file
deploy host:
- @backup git:nix
+ @sursum git:nix
NH_ELEVATION_STRATEGY=none nh os switch . --hostname {{ host }} --target-host root@{{ host }} --build-host root@{{ host }}
home:
- -@backup git:nix
+ -@sursum git:nix
nh home switch .
diff host:
- -@backup git:nix
+ -@sursum git:nix
NH_ELEVATION_STRATEGY=none nh os switch -n -d always . \
--hostname {{ host }} \
--target-host root@{{ host }} \
blob - f9f473e401e40749c33df98ed8639622a8105461 (mode 644)
blob + /dev/null
--- modules/mixins/dotfiles/bin/backup
+++ /dev/null
-#!/usr/bin/env ruby
-# vim: ft=ruby
-# frozen_string_literal: true
-
-require "English"
-require "optparse"
-require "open3"
-require "timeout"
-require "yaml"
-
-module Backup
- Result = Struct.new(:status, :out, :err) do
- def success? = status&.success?
- def exitcode = status&.exitstatus || 127
- end
-
- Backend = Struct.new(:list, :run)
-
- @failures = []
- @current_pid = nil
-
- def self.ts = Time.now.strftime("%Y-%m-%dT%H:%M:%S%z")
- def self.log(msg) = puts("[#{ts}]: #{msg}")
- def self.err(msg) = warn("[#{ts}]: #{msg}")
-
- def self.fatal(msg)
- err(msg)
- exit(1)
- end
-
- def self.record_failure(msg)
- @failures << msg
- err("ERROR: #{msg}")
- end
-
- def self.check(msg, result)
- return true if result.success?
-
- detail = result.err.to_s.strip
- record_failure(detail.empty? ? msg : "#{msg}: #{detail}")
- false
- end
-
- HOME = ENV.fetch("HOME") { fatal("HOME not set") }
- PLAKAR_CONFIG_DIR = "#{HOME}/.config/plakar".freeze
-
- GIT_DIRS = {
- "bandcamp" => "#{HOME}/misc/music/backlog/_todo",
- "mpd" => "#{HOME}/.config/mpd",
- "nix" => "#{HOME}/src/sr.ht/nix",
- "notes" => "#{HOME}/notes",
- "pass" => "#{HOME}/.password-store",
- "releases" => "#{HOME}/src/mtmn.name/releases"
- }.freeze
-
- TARSNAP_DIRS = {
- "mpd" => "#{HOME}/.config/mpd",
- "notes" => "#{HOME}/notes",
- "password-store" => "#{HOME}/.password-store",
- "releases" => "#{HOME}/src/mtmn.name/releases"
- }.freeze
-
- RCLONE_REMOTE = "backup"
-
- def self.run_cmd(verbose, *args)
- if verbose
- $stdout.flush
- $stderr.flush
- @current_pid = Process.spawn(*args, in: :close)
- _, status = Process.wait2(@current_pid)
- @current_pid = nil
- Result.new(status, "", status.nil? ? "command not found: #{args.first}" : "")
- else
- out, err, status = Open3.capture3(*args)
- Result.new(status, out, err)
- end
- rescue Errno::ENOENT => e
- @current_pid = nil
- Result.new(nil, "", e.message)
- end
-
- def self.git(dir, *args, verbose:)
- run_cmd(verbose, "git", "--git-dir=#{dir}/.git", "--work-tree=#{dir}", *args)
- end
-
- def self.commit_changes(dir, verbose:)
- r = git(dir, "diff", "--cached", "--name-only", verbose: false)
- return false unless check("Failed to list staged changes in #{dir}", r)
-
- files = r.out.lines.map(&:strip).reject(&:empty?).join(" ")
- r2 = git(dir, "commit", "-m", files, verbose: verbose)
- return false unless check("Failed to commit changes in #{dir}", r2)
-
- log("Committed changes in #{dir}")
- true
- end
-
- def self.git_remote(dir, action, verb)
- log("#{verb.capitalize} changes from #{dir}")
- r = git(dir, action, verbose: false)
- if r.success?
- log("Done #{verb}ing #{dir}")
- true
- else
- record_failure("#{verb.capitalize} failed for #{dir} (exit #{r.exitcode}): #{r.err.strip}")
- false
- end
- end
-
- def self.push_changes(dir) = git_remote(dir, "push", "push")
- def self.fetch_changes(dir) = git_remote(dir, "pull", "fetch")
-
- def self.sync_repo(verbose:, push:, fetch:, name:, dir:)
- log("git/#{name}")
- r = git(dir, "add", "-A", verbose: verbose)
- return unless check("Failed to stage changes in #{dir}", r)
-
- diff = git(dir, "diff", "--cached", "--quiet", verbose: false)
- committed = if diff.success?
- log("Nothing to commit in #{dir}")
- true
- else
- commit_changes(dir, verbose: verbose)
- end
-
- return unless committed
- return if fetch && !fetch_changes(dir)
-
- push_changes(dir) if push && committed
- end
-
- def self.tarsnap_archive(verbose:, name:, dir:)
- stamp = Time.now.strftime("%Y-%m-%d_%H:%M:%S")
- log("Archiving tarsnap/#{name} from #{dir}")
- r = run_cmd(verbose, "tarsnap", "-c", "-f", "#{name}-#{stamp}", "-C", dir, ".")
- return unless check("tarsnap failed for #{name}", r)
-
- log("Done tarsnap/#{name}")
- end
-
- def self.rclone_backup(verbose:, name:, dir:)
- remote_path = "#{RCLONE_REMOTE}:#{name}"
- log("Syncing rclone/#{name} from #{dir} to #{remote_path}")
- args = ["rclone", "copy", dir, remote_path]
- if verbose
- args.concat(["--progress", "--verbose"])
- else
- args.concat(["--stats-one-line"])
- end
- args.concat([
- "--transfers", "4",
- "--checkers", "8",
- "--checksum"
- ])
- r = run_cmd(verbose, *args)
- return unless check("rclone failed for #{name}", r)
-
- log("Done rclone/#{name}")
- end
-
- def self.parse_plakar_show(text)
- records = {}
- name = nil
- text.each_line do |line|
- stripped = line.strip
- next if stripped.empty?
-
- if line.start_with?(" ")
- key, val = stripped.split(":", 2)
- records[name][key] = val&.strip if name
- else
- name = stripped.delete_suffix(":")
- records[name] = {}
- end
- end
-
- records
- end
-
- def self.plakar_query(subcommand)
- r = run_cmd(false, "plakar", subcommand, "show")
- return {} unless r.success?
-
- parse_plakar_show(r.out)
- end
-
- def self.load_plakar_yml(name)
- path = File.join(PLAKAR_CONFIG_DIR, "#{name}.yml")
- return nil unless File.file?(path)
-
- YAML.load_file(path)[name.to_s] || {}
- rescue => e
- fatal("Failed to parse #{path}: #{e.message}")
- end
-
- def self.plakar_stores
- @plakar_stores ||= (load_plakar_yml("stores") || plakar_query("store")).freeze
- end
-
- def self.plakar_sources
- @plakar_sources ||= (load_plakar_yml("sources") || plakar_query("source")).freeze
- end
-
- def self.plakar_stores_hash = plakar_stores
- def self.plakar_stores_list = plakar_stores.keys
- def self.plakar_sources_list = plakar_sources.keys
- def self.plakar_source_dirs = plakar_sources.transform_values { |cfg| cfg["location"] }.freeze
-
- def self.plakar_ignore_file(store)
- path = File.join(PLAKAR_CONFIG_DIR, "#{store}.ignore")
- File.file?(path) ? path : nil
- end
-
- def self.plakar_source(verbose:, store:, source:)
- ignore = plakar_ignore_file(store)
- args = ["plakar", "at", "@#{store}", "backup"]
- args.push("-ignore-file", ignore) if ignore
- args.push("@#{source}")
- log("Syncing plakar/#{store}/#{source}")
- r = run_cmd(verbose, *args)
- return unless check("plakar @#{store} failed for #{source}", r)
-
- log("Done plakar/#{store}/#{source}")
- end
-
- def self.plakar_store(verbose:, store:, exclude: [])
- fatal("Unknown plakar store: #{store}") unless plakar_stores_list.include?(store)
- (plakar_sources_list - exclude).each { |src| plakar_source(verbose: verbose, store: store, source: src) }
- end
-
- def self.backup_plakar(verbose:, exclude: [])
- plakar_stores_list.each { |store| plakar_store(verbose: verbose, store: store, exclude: exclude) }
- end
-
- def self.list_flat(name, dirs)
- puts(name)
- dirs.keys.sort.each { |k| puts(" #{k}") }
- end
-
- def self.list_plakar
- puts("plakar")
- puts(" sources")
- plakar_sources_list.sort.each { |s| puts(" #{s}") }
- plakar_stores_list.sort.each do |store|
- loc = plakar_stores_hash.dig(store, "location") || "?"
- puts(" #{store} (#{loc})")
- end
- end
-
- def self.list_rclone
- puts("rclone")
- puts(" sources")
- plakar_source_dirs.keys.sort.each { |s| puts(" #{s}") }
- puts(" #{RCLONE_REMOTE}")
- end
-
- def self.git_run(verbose:, push:, fetch:, parts:, exclude:)
- if parts.empty?
- GIT_DIRS.except(*exclude).each { |n, d| sync_repo(verbose: verbose, push: push, fetch: fetch, name: n, dir: d) }
- else
- name = parts.first
- return if exclude.include?(name)
- sync_repo(
- verbose: verbose,
- push: push,
- fetch: fetch,
- name: name,
- dir: GIT_DIRS.fetch(name) { fatal("Unknown git target: #{name}") }
- )
- end
- end
-
- def self.tarsnap_run(verbose:, parts:, exclude:)
- if parts.empty?
- TARSNAP_DIRS.except(*exclude).each { |n, d| tarsnap_archive(verbose: verbose, name: n, dir: d) }
- else
- name = parts.first
- return if exclude.include?(name)
- tarsnap_archive(
- verbose: verbose,
- name: name,
- dir: TARSNAP_DIRS.fetch(name) { fatal("Unknown tarsnap target: #{name}") }
- )
- end
- end
-
- def self.rclone_run(verbose:, parts:, exclude:)
- if parts.empty?
- plakar_source_dirs.except(*exclude).each { |n, d| rclone_backup(verbose: verbose, name: n, dir: d) }
- else
- name = parts.first
- return if exclude.include?(name)
- rclone_backup(
- verbose: verbose,
- name: name,
- dir: plakar_source_dirs.fetch(name) { fatal("Unknown rclone target: #{name}") }
- )
- end
- end
-
- def self.plakar_run(verbose:, parts:, exclude:)
- if parts.empty?
- backup_plakar(verbose: verbose, exclude: exclude)
- elsif parts.size == 1
- plakar_store(verbose: verbose, store: parts.first, exclude: exclude)
- else
- store = parts[0]
- selected = parts[1].split(",") - exclude
- selected.each do |source|
- plakar_source(verbose: verbose, store: store, source: source)
- end
- end
- end
-
- BACKENDS = {
- "git" => Backend.new(
- list: -> { list_flat("git", GIT_DIRS) },
- run: ->(verbose:, push:, fetch:, exclude:, parts:) { git_run(verbose: verbose, push: push, fetch: fetch, exclude: exclude, parts: parts) }
- ),
- "tarsnap" => Backend.new(
- list: -> { list_flat("tarsnap", TARSNAP_DIRS) },
- run: ->(verbose:, push:, fetch:, exclude:, parts:) { tarsnap_run(verbose: verbose, exclude: exclude, parts: parts) }
- ),
- "plakar" => Backend.new(
- list: -> { list_plakar },
- run: ->(verbose:, push:, fetch:, exclude:, parts:) { plakar_run(verbose: verbose, exclude: exclude, parts: parts) }
- ),
- "rclone" => Backend.new(
- list: -> { list_rclone },
- run: ->(verbose:, push:, fetch:, exclude:, parts:) { rclone_run(verbose: verbose, exclude: exclude, parts: parts) }
- )
- }.freeze
-
- def self.list_targets
- BACKENDS.each_value { |b| b.list.call }
- end
-
- def self.backup_target(verbose:, push:, fetch:, exclude:, target:)
- parts = target.split(":")
- bname = parts.shift
- backend = BACKENDS[bname] || fatal("Unknown target: #{target}")
- backend.run.call(verbose: verbose, push: push, fetch: fetch, exclude: exclude, parts: parts)
- end
-
- def self.main(args)
- Signal.trap("INT") do
- warn("[#{Time.now.strftime("%Y-%m-%dT%H:%M:%S%z")}]: Interrupted")
- if @current_pid
- begin
- Process.kill("TERM", @current_pid)
- begin
- Timeout.timeout(60) { Process.wait(@current_pid) }
- rescue Timeout::Error
- begin
- Process.kill("KILL", @current_pid)
- rescue
- Errno::ESRCH
- end
- end
- rescue Errno::ESRCH
- end
- end
- exit(130)
- end
-
- options = {verbose: true, push: false, fetch: false, list: false, exclude: []}
- parser = OptionParser.new do |o|
- o.banner = "Usage: backup [options] [target]\n" \
- " target: git, plakar, tarsnap, rclone, <backend>:<name>\n" \
- " plakar sources may be comma-separated: plakar:<store>:<src1,src2,...>\n" \
- " -e may exclude sources: backup plakar:<store> -e <src1,src2,...>"
- o.on("-p", "--push", "Fetch and push changes after commit") { options[:push] = true }
- o.on("-f", "--fetch", "Run git pull for git targets before backup") { options[:fetch] = true }
- o.on("-q", "--quiet", "Suppress command output") { options[:verbose] = false }
- o.on("-l", "--list", "List all configured backup targets") { options[:list] = true }
- o.on("-e", "--exclude LIST", "Comma-separated list of sources/targets to skip") { |v| options[:exclude].concat(v.split(",").map(&:strip)) }
- o.on("-h", "--help") do
- puts(o)
- exit(0)
- end
- end
-
- parser.parse!(args)
- target = args.shift
- unless options[:list] || target
- list_targets
- exit(0)
- end
-
- options[:fetch] = true if options[:push]
-
- list_targets if options[:list]
- if target
- backup_target(verbose: options[:verbose], push: options[:push], fetch: options[:fetch], exclude: options[:exclude], target: target)
- end
-
- fatal("#{@failures.size} backup target(s) failed") unless @failures.empty?
- end
-end
-
-Backup.main(ARGV)
blob - e3d5063e4cfc82737d397f7678b74fe4bfa4017c
blob + 6f0dfddcdbe41ffd8105e2c99aca3f39d1fd688b
--- modules/mixins/dotfiles/bin/tuped
+++ modules/mixins/dotfiles/bin/tuped
(print usage)
(os/exit 0))
(def opts (parse-args argv))
- (unless (have-spiped) (die "spiped not found on PATH (install net/spiped)"))
+ (unless (have-spiped) (die "spiped not found"))
(unless (opts :key) (die "--key is required"))
(when (zero? (opts :local-port)) (die "--local-port is required"))
(when (zero? (opts :remote-port)) (die "--remote-port is required"))
blob - 418d45818ead0246b5592bfb08545518c6c1f72d
blob + 1c0c36295391d53d53f269f7236ad97bd49cc2f4
--- modules/mixins/dotfiles/config/nvim/fnl/init.fnl
+++ modules/mixins/dotfiles/config/nvim/fnl/init.fnl
(gh :jpalardy/vim-slime)
(gh :gpanders/nvim-parinfer)
(gh :L3MON4D3/LuaSnip)
+ (gh :janet-lang/janet.vim)
{:src (gh :neovim/nvim-lspconfig) :version :master}
{:src (gh :saghen/blink.cmp) :version (vim.version.range "^1")}]
{:load true})
blob - 16b6538f40b07b3db232d246fd8717463ee5b686
blob + 4fcc19d5c21f4103b3bf86df9b3d954ee6c9def5
--- modules/mixins/dotfiles/config/shell/rc/command_overrides
+++ modules/mixins/dotfiles/config/shell/rc/command_overrides
alias dmesg='dmesg -T --color=always | less -R +G'
alias scc='scc --no-cocomo'
+alias backup='sursum'