diff options
Diffstat (limited to 'tools')
32 files changed, 1669 insertions, 0 deletions
diff --git a/tools/aoc2019/default.nix b/tools/aoc2019/default.nix new file mode 100644 index 000000000000..a53586eea9eb --- /dev/null +++ b/tools/aoc2019/default.nix @@ -0,0 +1,22 @@ +# Solutions for Advent of Code 2019, written in Emacs Lisp. +# +# For each day a new file is created as "solution-day$n.el". +{ pkgs, ... }: + +let + inherit (builtins) attrNames filter head listToAttrs match readDir; + dir = readDir ./.; + matchSolution = match "solution-(.*)\.el"; + isSolution = f: (matchSolution f) != null; + getDay = f: head (matchSolution f); + + solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir); + solutions = map (f: let day = getDay f; in { + name = day; + value = pkgs.writeElispBin { + name = "aoc2019"; + deps = p: with p; [ dash s ht ]; + src = ./. + ("/" + f); + }; + }) solutionFiles; +in listToAttrs solutions diff --git a/tools/aoc2019/solution-day1.el b/tools/aoc2019/solution-day1.el new file mode 100644 index 000000000000..d805c22ec870 --- /dev/null +++ b/tools/aoc2019/solution-day1.el @@ -0,0 +1,28 @@ +;; Advent of Code 2019 - Day 1 +(require 'dash) + +;; Puzzle 1: + +(defvar day-1/input + '(83285 96868 121640 51455 128067 128390 141809 52325 68310 140707 124520 149678 + 87961 52040 133133 52203 117483 85643 84414 86558 65402 122692 88565 61895 + 126271 128802 140363 109764 53600 114391 98973 124467 99574 69140 144856 + 56809 149944 138738 128823 82776 77557 51994 74322 64716 114506 124074 + 73096 97066 96731 149307 135626 121413 69575 98581 50570 60754 94843 72165 + 146504 53290 63491 50936 79644 119081 70218 85849 133228 114550 131943 + 67288 68499 80512 148872 99264 119723 68295 90348 146534 52661 99146 95993 + 130363 78956 126736 82065 77227 129950 97946 132345 107137 79623 148477 + 88928 118911 75277 97162 80664 149742 88983 74518)) + +(defun calculate-fuel (mass) + (- (/ mass 3) 2)) + +(message "Solution to day1/1: %d" (apply #'+ (-map #'calculate-fuel day-1/input))) + +;; Puzzle 2: +(defun calculate-recursive-fuel (mass) + (let ((fuel (calculate-fuel mass))) + (if (< fuel 0) 0 + (+ fuel (calculate-recursive-fuel fuel))))) + +(message "Solution to day1/2: %d" (apply #'+ (-map #'calculate-recursive-fuel day-1/input))) diff --git a/tools/aoc2019/solution-day2.el b/tools/aoc2019/solution-day2.el new file mode 100644 index 000000000000..6ecac1e2016c --- /dev/null +++ b/tools/aoc2019/solution-day2.el @@ -0,0 +1,53 @@ +;; -*- lexical-binding: t; -*- +;; Advent of Code 2019 - Day 2 +(require 'dash) +(require 'ht) + +(defvar day2/input + [1 0 0 3 1 1 2 3 1 3 4 3 1 5 0 3 2 1 9 19 1 19 5 23 1 13 23 27 1 27 6 31 + 2 31 6 35 2 6 35 39 1 39 5 43 1 13 43 47 1 6 47 51 2 13 51 55 1 10 55 + 59 1 59 5 63 1 10 63 67 1 67 5 71 1 71 10 75 1 9 75 79 2 13 79 83 1 9 + 83 87 2 87 13 91 1 10 91 95 1 95 9 99 1 13 99 103 2 103 13 107 1 107 10 + 111 2 10 111 115 1 115 9 119 2 119 6 123 1 5 123 127 1 5 127 131 1 10 + 131 135 1 135 6 139 1 10 139 143 1 143 6 147 2 147 13 151 1 5 151 155 1 + 155 5 159 1 159 2 163 1 163 9 0 99 2 14 0 0]) + +;; Puzzle 1 + +(defun day2/single-op (f state idx) + (let* ((a (aref state (aref state (+ 1 idx)))) + (b (aref state (aref state (+ 2 idx)))) + (p (aref state (+ 3 idx))) + (result (funcall f a b))) + (aset state p (funcall f a b)))) + +(defun day2/operate (state idx) + (pcase (aref state idx) + (99 (aref state 0)) + (1 (day2/single-op #'+ state idx) + (day2/operate state (+ 4 idx))) + (2 (day2/single-op #'* state idx) + (day2/operate state (+ 4 idx))) + (other (error "Unknown opcode: %s" other)))) + +(defun day2/program-with-inputs (noun verb) + (let* ((input (copy-tree day2/input t))) + (aset input 1 noun) + (aset input 2 verb) + (day2/operate input 0))) + +(message "Solution to day2/1: %s" (day2/program-with-inputs 12 2)) + +;; Puzzle 2 +(let* ((used (ht)) + (noun 0) + (verb 0) + (result (day2/program-with-inputs noun verb))) + (while (/= 19690720 result) + (setq noun (random 100)) + (setq verb (random 100)) + (unless (ht-get used (format "%d%d" noun verb)) + (ht-set used (format "%d%d" noun verb) t) + (setq result (day2/program-with-inputs noun verb)))) + + (message "Solution to day2/2: %s%s" noun verb)) diff --git a/tools/aoc2019/solution-day3.el b/tools/aoc2019/solution-day3.el new file mode 100644 index 000000000000..c0d2eb5ee657 --- /dev/null +++ b/tools/aoc2019/solution-day3.el @@ -0,0 +1,58 @@ +;; -*- lexical-binding: t; -*- +;; Advent of Code 2019 - Day 3 +;; +;; Note: Input was pre-processed with some Emacs shortcuts. +(require 'cl) +(require 'dash) +(require 'ht) +(require 's) + +(defvar day3/input/wire1 + "R1010,D422,L354,U494,L686,U894,R212,U777,L216,U9,L374,U77,R947,U385,L170,U916,R492,D553,L992,D890,L531,U360,R128,U653,L362,U522,R817,U198,L126,D629,L569,U300,L241,U145,R889,D196,L450,D576,L319,D147,R985,U889,L941,U837,L608,D77,L864,U911,L270,D869,R771,U132,L249,U603,L36,D328,L597,U992,L733,D370,L947,D595,L308,U536,L145,U318,R55,D773,R175,D505,R483,D13,R780,U778,R445,D107,R490,U245,L587,U502,R446,U639,R150,U35,L455,D522,R866,U858,R394,D975,R513,D378,R58,D646,L374,D675,R209,U228,R530,U543,L480,U677,L912,D164,L573,U587,L784,D626,L994,U250,L215,U985,R684,D79,L877,U811,L766,U617,L665,D246,L408,U800,L360,D272,L436,U138,R240,U735,L681,U68,L608,D59,R532,D808,L104,U968,R887,U819,R346,U698,L317,U582,R516,U55,L303,U607,L457,U479,L510,D366,L583,U519,R878,D195,R970,D267,R842,U784,R9,D946,R833,D238,L232,D94,L860,D47,L346,U951,R491,D745,R849,U273,R263,U392,L341,D808,R696,U326,R886,D296,L865,U833,R241,U644,R729,D216,R661,D712,L466,D699,L738,U5,L556,D693,R912,D13,R48,U63,L877,U628,L689,D929,R74,U924,R612,U153,R417,U425,L879,D378,R79,D248,L3,U519,R366,U281,R439,D823,R149,D668,R326,D342,L213,D735,R504,U265,L718,D842,L565,U105,L214,U963,R518,D681,R642,U170,L111,U6,R697,U572,R18,U331,L618,D255,R534,D322,L399,U595,L246,U651,L836,U757,R417,D795,R291,U759,L568,U965,R828,D570,R350,U317,R338,D173,L74,D833,L650,D844,L70,U913,R594,U407,R674,D684,L481,D564,L128,D277,R851,D274,L435,D582,R469,U729,R387,D818,R443,U504,R414,U8,L842,U845,R275,U986,R53,U660,R661,D225,R614,U159,R477") + +(defvar day3/input/wire2 + "L1010,D698,R442,U660,L719,U702,L456,D86,R938,D177,L835,D639,R166,D285,L694,U468,L569,D104,L234,D574,L669,U299,L124,D275,L179,D519,R617,U72,L985,D248,R257,D276,L759,D834,R490,U864,L406,U181,R911,U873,R261,D864,R260,U759,R648,U158,R308,D386,L835,D27,L745,U91,R840,U707,R275,U543,L663,U736,L617,D699,R924,U103,R225,U455,R708,U319,R569,U38,R315,D432,L179,D975,R519,D546,L295,U680,L685,U603,R262,D250,R7,U171,R261,U519,L832,U534,L471,U431,L474,U886,R10,D179,L79,D555,R452,U452,L832,U863,L367,U538,L237,D160,R441,U605,R942,U259,L811,D552,R646,D353,L225,D94,L35,D307,R752,U23,R698,U610,L379,D932,R698,D751,R178,D347,R325,D156,R471,D555,R558,D593,R773,U2,L955,U764,L735,U438,R364,D640,L757,U534,R919,U409,R361,U407,R336,D808,R877,D648,R610,U198,R340,U94,R795,D667,R811,U975,L965,D224,R565,D681,L64,U567,R621,U922,L665,U329,R242,U592,L727,D481,L339,U402,R213,D280,R656,U169,R976,D962,L294,D505,L251,D689,L497,U133,R230,D441,L90,D220,L896,D657,L500,U331,R502,U723,R762,D613,L447,D256,L226,U309,L935,U384,L740,D459,R309,D707,R952,D747,L304,D105,R977,D539,R941,D21,R291,U216,R132,D543,R515,U453,L854,D42,R982,U102,L469,D639,R559,D68,R302,U734,R980,D214,R107,D191,L730,D793,L63,U17,R807,U196,R412,D592,R330,D941,L87,D291,L44,D94,L272,D780,R968,U837,L712,D704,R163,U981,R537,U778,R220,D303,L196,D951,R163,D446,R11,D623,L72,D778,L158,U660,L189,D510,L247,D716,L89,U887,L115,U114,L36,U81,R927,U293,L265,U183,R331,D267,R745,D298,L561,D918,R299,U810,L322,U679,L739,D854,L581,U34,L862,D779,R23") + +;; Puzzle 1 + +(defun wire-from (raw) + (-map (lambda (s) + (cons (substring s 0 1) (string-to-number (substring s 1)))) + (s-split "," raw))) + +(defun day3/move (x y next) + (cl-flet ((steps (by op) + (-map op (reverse (number-sequence 1 by))))) + (pcase next + (`("L" . ,by) (steps by (lambda (n) (cons (- x n) y)))) + (`("R" . ,by) (steps by (lambda (n) (cons (+ x n) y)))) + (`("U" . ,by) (steps by (lambda (n) (cons x (+ y n))))) + (`("D" . ,by) (steps by (lambda (n) (cons x (- y n)))))))) + +(defun day3/wire-points (wire) + (let ((points (ht)) + (point-list (-reduce-from + (lambda (acc point) + (-let* (((x . y) (car acc)) + (next (day3/move x y point))) + (-concat next acc))) + '((0 . 0)) wire))) + (-map (lambda (p) (ht-set! points p t)) point-list) + (ht-remove! points '(0 . 0)) + points)) + +(defun day3/closest-intersection (wire1 wire2) + (let* ((wire1-points (day3/wire-points (wire-from wire1))) + (wire2-points (day3/wire-points (wire-from wire2))) + (crossed-points (-filter (lambda (p) (ht-contains? wire1-points p)) + (ht-keys wire2-points)))) + + (car (-sort #'< + (-map (-lambda ((x . y)) + (+ (abs x) (abs y))) + crossed-points))))) + +(message "Solution form day3/1: %d" + (day3/closest-intersection day3/input/wire1 + day3/input/wire2)) + diff --git a/tools/bin/__dispatch.sh b/tools/bin/__dispatch.sh new file mode 100755 index 000000000000..fb8a4d779185 --- /dev/null +++ b/tools/bin/__dispatch.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# This script dispatches invocations transparently to programs instantiated from +# Nix. +# +# To add a new tool, insert it into the case statement below by setting `attr` +# to the key in nixpkgs which represents the program you want to run. +set -ueo pipefail + +readonly REPO_ROOT=$(git rev-parse --show-toplevel) +readonly TARGET_TOOL=$(basename $0) + +case "${TARGET_TOOL}" in + terraform) + attr="third_party.terraform-gcp" + ;; + kontemplate) + attr="kontemplate" + ;; + blog_cli) + attr="tools.blog_cli" + ;; + stern) + attr="stern" + ;; + pass) + attr="tools.kms_pass" + ;; + aoc2019) + attr="tools.aoc2019.${1}" + ;; + *) + echo "The tool '${TARGET_TOOL}' is currently not installed in this repository." + exit 1 + ;; +esac + +result=$(nix-build --no-out-link --attr "${attr}" "${REPO_ROOT}") +PATH="${result}/bin:$PATH" + +exec "${TARGET_TOOL}" "${@}" diff --git a/tools/bin/aoc2019 b/tools/bin/aoc2019 new file mode 120000 index 000000000000..8390ec9c9652 --- /dev/null +++ b/tools/bin/aoc2019 @@ -0,0 +1 @@ +__dispatch.sh \ No newline at end of file diff --git a/tools/bin/blog_cli b/tools/bin/blog_cli new file mode 120000 index 000000000000..8390ec9c9652 --- /dev/null +++ b/tools/bin/blog_cli @@ -0,0 +1 @@ +__dispatch.sh \ No newline at end of file diff --git a/tools/bin/kontemplate b/tools/bin/kontemplate new file mode 120000 index 000000000000..8390ec9c9652 --- /dev/null +++ b/tools/bin/kontemplate @@ -0,0 +1 @@ +__dispatch.sh \ No newline at end of file diff --git a/tools/bin/pass b/tools/bin/pass new file mode 120000 index 000000000000..8390ec9c9652 --- /dev/null +++ b/tools/bin/pass @@ -0,0 +1 @@ +__dispatch.sh \ No newline at end of file diff --git a/tools/bin/stern b/tools/bin/stern new file mode 120000 index 000000000000..8390ec9c9652 --- /dev/null +++ b/tools/bin/stern @@ -0,0 +1 @@ +__dispatch.sh \ No newline at end of file diff --git a/tools/bin/terraform b/tools/bin/terraform new file mode 120000 index 000000000000..8390ec9c9652 --- /dev/null +++ b/tools/bin/terraform @@ -0,0 +1 @@ +__dispatch.sh \ No newline at end of file diff --git a/tools/blog_cli/README.md b/tools/blog_cli/README.md new file mode 100644 index 000000000000..7afa0fe9207a --- /dev/null +++ b/tools/blog_cli/README.md @@ -0,0 +1,41 @@ +tazblog CLI +=========== + +My blog stores its content in DNS, spread out over three types of `TXT` entries: + +* `TXT _posts.blog.tazj.in.`: A sorted list of posts, serialised as a JSON list of + strings (e.g. `["1486830338", "1476807384"]`) + +* `TXT _chunks.$postID.blog.tazj.in`: JSON chunks containing the blog post text + +* `TXT _meta.$postID.blog.tazj.in`: JSON blob with blog post metadata + +All JSON blobs are base64-encoded. + +This CLI tool helps to update those records. + +Each blog post data is a series of JSON-encoded structures which follow one of +these formats: + +``` +struct metadata { + chunks: int + title: string + date: date +} +``` + +Where `chunks` describes the number of chunks following this format: + +``` +struct chunk { + c: int + t: string +} +``` + +Writing a blog post to DNS means taking its text and metadata, chunking it up +and writing the chunks. + +Reading a blog post means retrieving all data, reading the metadata and then +assembling the chunks in order. diff --git a/tools/blog_cli/default.nix b/tools/blog_cli/default.nix new file mode 100644 index 000000000000..c22e4c949bc1 --- /dev/null +++ b/tools/blog_cli/default.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: + +pkgs.buildGo.program { + name = "blog_cli"; + srcs = [ ./main.go ]; + deps = with pkgs.third_party; [ + gopkgs."google.golang.org".api.dns.v1.gopkg + ]; +} // { meta.enableCI = true; } diff --git a/tools/blog_cli/main.go b/tools/blog_cli/main.go new file mode 100644 index 000000000000..db64f8378e40 --- /dev/null +++ b/tools/blog_cli/main.go @@ -0,0 +1,209 @@ +// The tazblog CLI implements updating my blog records in DNS, see the +// README in this folder for details. +// +// The post input format is a file with the title on one line, +// followed by the date on a line, followed by an empty line, followed +// by the post text. +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "time" + + "google.golang.org/api/dns/v1" +) + +var ( + project = flag.String("project", "tazjins-infrastructure", "Target GCP project") + zone = flag.String("zone", "blog-tazj-in", "Target Cloud DNS zone") + title = flag.String("title", "", "Title of the blog post") + date = flag.String("date", "", "Date the post was written on") + infile = flag.String("text", "", "Text file containing the blog post") + id = flag.String("id", "", "Post ID - will be generated if unset") +) + +// Number of runes to include in a single chunk. If any chunks exceed +// the limit of what can be encoded, the chunk size is reduced and we +// try again. +var chunkSize = 200 + +type day time.Time + +func (d day) MarshalJSON() ([]byte, error) { + j := (time.Time(d)).Format(`"2006-01-02"`) + return []byte(j), nil +} + +type metadata struct { + Chunks int `json:"c"` + Title string `json:"t"` + Date day `json:"d"` +} + +type chunk struct { + Chunk int + Text string +} + +type post struct { + ID string + Meta metadata + Chunks []string +} + +func (p *post) writeToDNS() error { + var additions []*dns.ResourceRecordSet + additions = append(additions, &dns.ResourceRecordSet{ + Name: fmt.Sprintf("_meta.%s.blog.tazj.in.", p.ID), + Type: "TXT", + Ttl: 1200, + Rrdatas: []string{ + encodeJSON(p.Meta), + }, + }) + + for i, c := range p.Chunks { + additions = append(additions, &dns.ResourceRecordSet{ + Name: fmt.Sprintf("_%v.%s.blog.tazj.in.", i, p.ID), + Type: "TXT", + Ttl: 1200, + Rrdatas: []string{c}, + }) + } + + ctx := context.Background() + dnsSvc, err := dns.NewService(ctx) + if err != nil { + return err + } + + change := dns.Change{ + Additions: additions, + } + + _, err = dnsSvc.Changes.Create(*project, *zone, &change).Do() + if err != nil { + return err + } + + return nil +} + +// Encode given value as JSON and base64-encode it. +func encodeJSON(v interface{}) string { + outer, err := json.Marshal(v) + if err != nil { + log.Fatalln("Failed to encode JSON", err) + } + + return base64.RawStdEncoding.EncodeToString(outer) +} + +// Encode a chunk and check whether it is too large +func encodeChunk(c chunk) (string, bool) { + tooLarge := false + s := base64.RawStdEncoding.EncodeToString([]byte(c.Text)) + + if len(s) >= 255 { + tooLarge = true + } + + return s, tooLarge +} + +func createPost(id, title, text string, date day) post { + runes := []rune(text) + n := 0 + tooLarge := false + + var chunks []string + + for chunkSize < len(runes) { + c, l := encodeChunk(chunk{ + Chunk: n, + Text: string(runes[0:chunkSize:chunkSize]), + }) + + tooLarge = tooLarge || l + chunks = append(chunks, c) + runes = runes[chunkSize:] + n++ + } + + if len(runes) > 0 { + c, l := encodeChunk(chunk{ + Chunk: n, + Text: string(runes), + }) + + tooLarge = tooLarge || l + chunks = append(chunks, c) + n++ + } + + if tooLarge { + log.Println("Too large at chunk size", chunkSize) + chunkSize -= 5 + return createPost(id, title, text, date) + } + + return post{ + ID: id, + Meta: metadata{ + Chunks: n, + Title: title, + Date: date, + }, + Chunks: chunks, + } +} + +func main() { + flag.Parse() + + if *title == "" { + log.Fatalln("Post title must be set (-title)") + } + + if *infile == "" { + log.Fatalln("Post text file must be set (-text)") + } + + if *id == "" { + log.Fatalln("Post ID must be set (-id)") + } + + var postDate day + if *date != "" { + t, err := time.Parse("2006-01-02", *date) + if err != nil { + log.Fatalln("Invalid post date", err) + } + + postDate = day(t) + } else { + postDate = day(time.Now()) + } + + t, err := ioutil.ReadFile(*infile) + if err != nil { + log.Fatalln("Failed to read post:", err) + } + + post := createPost(*id, *title, string(t), postDate) + + log.Println("Writing post to DNS ...") + err = post.writeToDNS() + + if err != nil { + log.Fatalln("Failed to write post:", err) + } + + log.Println("Successfully wrote entries") +} diff --git a/tools/emacs/.gitignore b/tools/emacs/.gitignore new file mode 100644 index 000000000000..7b666905f847 --- /dev/null +++ b/tools/emacs/.gitignore @@ -0,0 +1,11 @@ +.smex-items +*token* +auto-save-list/ +clones/ +elpa/ +irc.el +local.el +other/ +scripts/ +themes/ +*.elc diff --git a/tools/emacs/README.md b/tools/emacs/README.md new file mode 100644 index 000000000000..5c667333962e --- /dev/null +++ b/tools/emacs/README.md @@ -0,0 +1,7 @@ +tools/emacs +=========== + +This sub-folder builds my Emacs configuration, supplying packages from +Nix and configuration from this folder. + +I use Emacs for many things (including as my desktop environment). diff --git a/tools/emacs/init.el b/tools/emacs/init.el new file mode 100644 index 000000000000..66d38cd9fcde --- /dev/null +++ b/tools/emacs/init.el @@ -0,0 +1,168 @@ +;;; init.el --- Package bootstrapping. -*- lexical-binding: t; -*- + +;; Packages are installed via Nix configuration, this file only +;; initialises the newly loaded packages. + +(require 'use-package) +(require 'seq) + +(package-initialize) + +;; Add 'init' folder that contains other settings to load. +(add-to-list 'load-path (concat user-emacs-directory "init")) + +;; Initialise all packages installed via Nix. +;; +;; TODO: Generate this section in Nix for all packages that do not +;; require special configuration. + +;; +;; Packages providing generic functionality. +;; + +(use-package ace-window + :bind (("C-x o" . ace-window)) + :init + (setq aw-keys '(?f ?j ?d ?k ?s ?l ?a) + aw-scope 'frame)) + +(use-package auth-source-pass :init (auth-source-pass-enable)) + +(use-package avy + :bind (("M-j" . avy-goto-char) + ("M-p" . avy-pop-mark) + ("M-g g" . avy-goto-line))) + +(use-package browse-kill-ring) + +(use-package company + :hook ((prog-mode . company-mode)) + :bind (:map rust-mode-map ("<tab>" . company-indent-or-complete-common) + :map lisp-mode-map ("<tab>" . company-indent-or-complete-common)) + :init (setq company-tooltip-align-annotations t)) + +(use-package dash) +(use-package dash-functional) +(use-package edit-server :init (edit-server-start)) +(use-package gruber-darker-theme) +(use-package ht) +(use-package hydra) +(use-package idle-highlight-mode :hook ((prog-mode . idle-highlight-mode))) +(use-package paredit :hook ((lisp-mode . paredit-mode) + (emacs-lisp-mode . paredit-mode))) +(use-package multiple-cursors) +(use-package pinentry + :init + (setq epa-pinentry-mode 'loopback) + (pinentry-start)) + +(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode)) +(use-package rainbow-mode) +(use-package s) +(use-package smartparens :init (smartparens-global-mode)) +(use-package string-edit) +(use-package telephone-line) ;; configuration happens outside of use-package +(use-package undo-tree :init (global-undo-tree-mode)) +(use-package uuidgen) +(use-package which-key :init (which-key-mode t)) + +;; +;; Applications in emacs +;; + +(use-package magit + :bind ("C-c g" . magit-status) + :init (setq magit-repository-directories '(("/home/vincent/projects" . 2)))) + +(use-package password-store) +(use-package pg) +(use-package restclient) + +;; +;; Packages providing language-specific functionality +;; + +(use-package cargo + :hook ((rust-mode . cargo-minor-mode) + (cargo-process-mode . visual-line-mode)) + :bind (:map cargo-minor-mode-map ("C-c C-c C-l" . ignore))) + +(use-package dockerfile-mode) + +(use-package eglot + :init (defvar rust-eglot-initialized nil) + :hook ((rust-mode . (lambda () + (unless rust-eglot-initialized + (call-interactively #'eglot) + (setq rust-eglot-initialized t)))))) + +(use-package erlang + :hook ((erlang-mode . (lambda () + ;; Don't indent after '>' while I'm writing + (local-set-key ">" 'self-insert-command))))) + +(use-package go-mode) +(use-package haskell-mode) + +(use-package jq-mode + :init (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode))) + +(use-package kotlin-mode + :bind (:map kotlin-mode-map ("<tab>" . indent-relative))) + +(use-package markdown-mode + :init + (add-to-list 'auto-mode-alist '("\\.txt\\'" . markdown-mode)) + (add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode)) + (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode))) + +(use-package markdown-toc) + +(use-package nix-mode + :bind (:map nix-mode-map ("<tab>" . nix-indent-line))) + +(use-package nginx-mode) +(use-package rust-mode) +(use-package terraform-mode) +(use-package toml-mode) +(use-package web-mode) +(use-package yaml-mode) + +;; +;; EXWM / NixOS related packages +;; + +;; Configure a few basics before moving on to package-specific initialisation. +(setq custom-file (concat user-emacs-directory "init/custom.el")) +(load custom-file) + +(defvar home-dir (expand-file-name "~")) + +;; Seed RNG +(random t) + +(defun load-other-settings () + (mapc 'require '(nixos + mail-setup + look-and-feel + functions + settings + modes + bindings + term-setup + eshell-setup)) + (telephone-line-setup) + (ace-window-display-mode) + + (use-package sly + :init (setq inferior-lisp-program (concat (nix-store-path "sbcl") "/bin/sbcl")) + ;;(add-to-list 'company-backends 'sly-company) + )) + + +;; Some packages can only be initialised after the rest of the +;; settings has been applied: + +(add-hook 'after-init-hook 'load-other-settings) +(put 'narrow-to-region 'disabled nil) +(put 'upcase-region 'disabled nil) diff --git a/tools/emacs/init/bindings.el b/tools/emacs/init/bindings.el new file mode 100644 index 000000000000..f10869a5325f --- /dev/null +++ b/tools/emacs/init/bindings.el @@ -0,0 +1,54 @@ +;; Various keybindings, most of them taken from starter-kit-bindings + +;; Font size +(define-key global-map (kbd "C-+") 'text-scale-increase) +(define-key global-map (kbd "C--") 'text-scale-decrease) + +;; Use regex searches by default. +(global-set-key (kbd "\C-r") 'isearch-backward-regexp) +(global-set-key (kbd "M-%") 'query-replace-regexp) +(global-set-key (kbd "C-M-s") 'isearch-forward) +(global-set-key (kbd "C-M-r") 'isearch-backward) +(global-set-key (kbd "C-M-%") 'query-replace) + +;; Counsel stuff: +(global-set-key (kbd "C-c r g") 'counsel-rg) + +;; imenu instead of insert-file +(global-set-key (kbd "C-x i") 'imenu) + +;; Window switching. (C-x o goes to the next window) +(windmove-default-keybindings) ;; Shift+direction + +;; Start eshell or switch to it if it's active. +(global-set-key (kbd "C-x m") 'eshell) + +;; Start a new eshell even if one is active. +(global-set-key (kbd "C-x M") (lambda () (interactive) (eshell t))) + +(global-set-key (kbd "C-x p") 'ivy-browse-repositories) +(global-set-key (kbd "M-g M-g") 'goto-line-with-feedback) + +(global-set-key (kbd "C-c w") 'whitespace-cleanup) +(global-set-key (kbd "C-c a") 'align-regexp) + +;; Browse URLs (very useful for Gitlab's SSH output!) +(global-set-key (kbd "C-c b p") 'browse-url-at-point) +(global-set-key (kbd "C-c b b") 'browse-url) + +;; Goodness from @magnars +;; I don't need to kill emacs that easily +;; the mnemonic is C-x REALLY QUIT +(global-set-key (kbd "C-x r q") 'save-buffers-kill-terminal) +(global-set-key (kbd "C-x C-c") 'delete-frame) + +;; Open Fefes Blog +(global-set-key (kbd "C-c C-f") 'fefes-blog) + +;; Open a file in project: +(global-set-key (kbd "C-c f") 'project-find-file) + +;; Use swiper instead of isearch +(global-set-key "\C-s" 'swiper) + +(provide 'bindings) diff --git a/tools/emacs/init/custom.el b/tools/emacs/init/custom.el new file mode 100644 index 000000000000..4c92f0d32fc4 --- /dev/null +++ b/tools/emacs/init/custom.el @@ -0,0 +1,52 @@ +(custom-set-variables + ;; custom-set-variables was added by Custom. + ;; If you edit it by hand, you could mess it up, so be careful. + ;; Your init file should contain only one such instance. + ;; If there is more than one, they won't work right. + '(ac-auto-show-menu 0.8) + '(ac-delay 0.2) + '(aprila-nixops-path "/home/vincent/projects/langler/nixops") + '(aprila-release-author "Vincent Ambo <vincent@aprila.no>") + '(aprila-releases-path "/home/vincent/projects/langler/docs/releases") + '(avy-background t) + '(cargo-process--custom-path-to-bin "env CARGO_INCREMENTAL=1 cargo") + '(cargo-process--enable-rust-backtrace 1) + '(custom-enabled-themes (quote (gruber-darker))) + '(custom-safe-themes + (quote + ("d61fc0e6409f0c2a22e97162d7d151dee9e192a90fa623f8d6a071dbf49229c6" "3c83b3676d796422704082049fc38b6966bcad960f896669dfc21a7a37a748fa" "89336ca71dae5068c165d932418a368a394848c3b8881b2f96807405d8c6b5b6" default))) + '(elnode-send-file-program "/run/current-system/sw/bin/cat") + '(frame-brackground-mode (quote dark)) + '(global-auto-complete-mode t) + '(intero-debug nil) + '(intero-global-mode t nil (intero)) + '(intero-package-version "0.1.31") + '(kubernetes-commands-display-buffer-function (quote display-buffer)) + '(magit-log-show-gpg-status t) + '(ns-alternate-modifier (quote none)) + '(ns-command-modifier (quote control)) + '(ns-right-command-modifier (quote meta)) + '(require-final-newline (quote visit-save))) +(custom-set-faces + ;; custom-set-faces was added by Custom. + ;; If you edit it by hand, you could mess it up, so be careful. + ;; Your init file should contain only one such instance. + ;; If there is more than one, they won't work right. + '(default ((t (:foreground "#e4e4ef" :background "#181818")))) + '(rainbow-delimiters-depth-1-face ((t (:foreground "#2aa198")))) + '(rainbow-delimiters-depth-2-face ((t (:foreground "#b58900")))) + '(rainbow-delimiters-depth-3-face ((t (:foreground "#268bd2")))) + '(rainbow-delimiters-depth-4-face ((t (:foreground "#dc322f")))) + '(rainbow-delimiters-depth-5-face ((t (:foreground "#859900")))) + '(rainbow-delimiters-depth-6-face ((t (:foreground "#268bd2")))) + '(rainbow-delimiters-depth-7-face ((t (:foreground "#cb4b16")))) + '(rainbow-delimiters-depth-8-face ((t (:foreground "#d33682")))) + '(rainbow-delimiters-depth-9-face ((t (:foreground "#839496")))) + '(term-color-black ((t (:background "#282828" :foreground "#282828")))) + '(term-color-blue ((t (:background "#96a6c8" :foreground "#96a6c8")))) + '(term-color-cyan ((t (:background "#1fad83" :foreground "#1fad83")))) + '(term-color-green ((t (:background "#73c936" :foreground "#73c936")))) + '(term-color-magenta ((t (:background "#9e95c7" :foreground "#9e95c7")))) + '(term-color-red ((t (:background "#f43841" :foreground "#f43841")))) + '(term-color-white ((t (:background "#f5f5f5" :foreground "#f5f5f5")))) + '(term-color-yellow ((t (:background "#ffdd33" :foreground "#ffdd33"))))) diff --git a/tools/emacs/init/eshell-setup.el b/tools/emacs/init/eshell-setup.el new file mode 100644 index 000000000000..0b23c5a2d1bc --- /dev/null +++ b/tools/emacs/init/eshell-setup.el @@ -0,0 +1,68 @@ +;; EShell configuration + +(require 'eshell) + +;; Generic settings +;; Hide banner message ... +(setq eshell-banner-message "") + +;; Prompt configuration +(defun clean-pwd (path) + "Turns a path of the form /foo/bar/baz into /f/b/baz + (inspired by fish shell)" + (let* ((hpath (replace-regexp-in-string home-dir + "~" + path)) + (current-dir (split-string hpath "/")) + (cdir (last current-dir)) + (head (butlast current-dir))) + (concat (mapconcat (lambda (s) + (if (string= "" s) nil + (substring s 0 1))) + head + "/") + (if head "/" nil) + (car cdir)))) + +(defun vcprompt (&optional args) + "Call the external vcprompt command with optional arguments. + VCPrompt" + (replace-regexp-in-string + "\n" "" + (shell-command-to-string (concat "vcprompt" args)))) + +(defmacro with-face (str &rest properties) + `(propertize ,str 'face (list ,@properties))) + +(defun prompt-f () + "EShell prompt displaying VC info and such" + (concat + (with-face (concat (clean-pwd (eshell/pwd)) " ") :foreground "#96a6c8") + (if (= 0 (user-uid)) + (with-face "#" :foreground "#f43841") + (with-face "$" :foreground "#73c936")) + (with-face " " :foreground "#95a99f"))) + + +(setq eshell-prompt-function 'prompt-f) +(setq eshell-highlight-prompt nil) +(setq eshell-prompt-regexp "^.+? \\((\\(git\\|svn\\|hg\\|darcs\\|cvs\\|bzr\\):.+?) \\)?[$#] ") + +;; Ignore version control folders in autocompletion +(setq eshell-cmpl-cycle-completions nil + eshell-save-history-on-exit t + eshell-cmpl-dir-ignore "\\`\\(\\.\\.?\\|CVS\\|\\.svn\\|\\.git\\)/\\'") + +;; Load some EShell extensions +(eval-after-load 'esh-opt + '(progn + (require 'em-term) + (require 'em-cmpl) + ;; More visual commands! + (add-to-list 'eshell-visual-commands "ssh") + (add-to-list 'eshell-visual-commands "tail") + (add-to-list 'eshell-visual-commands "sl"))) + +(setq eshell-directory-name "~/.config/eshell/") + +(provide 'eshell-setup) diff --git a/tools/emacs/init/functions.el b/tools/emacs/init/functions.el new file mode 100644 index 000000000000..8b96a0e737df --- /dev/null +++ b/tools/emacs/init/functions.el @@ -0,0 +1,266 @@ +(require 's) +;; A few handy functions I use in init.el (or not, but they're nice to +;; have) + +(defun custom-download-theme (url filename) + "Downloads a theme through HTTP and places it in ~/.emacs.d/themes" + + ;; Ensure the directory exists + (unless (file-exists-p "~/.emacs.d/themes") + (make-directory "~/.emacs.d/themes")) + + ;; Adds the themes folder to the theme load path (if not already + ;; there) + (unless (member "~/.emacs.d/themes" custom-theme-load-path) + (add-to-list 'custom-theme-load-path "~/.emacs.d/themes")) + + ;; Download file if it doesn't exist. + + (let ((file + (concat "~/.emacs.d/themes/" filename))) + (unless (file-exists-p file) + (url-copy-file url file)))) + +(defun custom-download-script (url filename) + "Downloads an Elisp script, places it in ~/.emacs/other and then loads it" + + ;; Ensure the directory exists + (unless (file-exists-p "~/.emacs.d/other") + (make-directory "~/.emacs.d/other")) + + ;; Download file if it doesn't exist. + (let ((file + (concat "~/.emacs.d/other/" filename))) + (unless (file-exists-p file) + (url-copy-file url file)) + + (load file))) + +(defun keychain-password (account &optional keychain) + "Returns the password for the account, by default it's looked up in the Login.keychain but a + different keychain can be specified." + (let ((k (if keychain keychain "Login.keychain"))) + (replace-regexp-in-string + "\n" "" + (shell-command-to-string (concat "security find-generic-password -w -a " + account + " " + k))))) + +;; This clones a git repository to 'foldername in .emacs.d +;; if there isn't already a folder with that name +(defun custom-clone-git (url foldername) + "Clones a git repository to .emacs.d/foldername" + (let ((fullpath (concat "~/.emacs.d/" foldername))) + (unless (file-exists-p fullpath) + (async-shell-command (concat "git clone " url " " fullpath))))) + +(defun load-file-if-exists (filename) + (if (file-exists-p filename) + (load filename))) + +(defun goto-line-with-feedback () + "Show line numbers temporarily, while prompting for the line number input" + (interactive) + (unwind-protect + (progn + (setq-local display-line-numbers t) + (let ((target (read-number "Goto line: "))) + (avy-push-mark) + (goto-line target))) + (setq-local display-line-numbers nil))) + + +(defun untabify-buffer () + (interactive) + (untabify (point-min) (point-max))) + +(defun indent-buffer () + (interactive) + (indent-region (point-min) (point-max))) + +(defun cleanup-buffer () + "Perform a bunch of operations on the whitespace content of a buffer. +Including indent-buffer, which should not be called automatically on save." + (interactive) + (untabify-buffer) + (delete-trailing-whitespace) + (indent-buffer)) + +;; These come from the emacs starter kit + +(defun esk-add-watchwords () + (font-lock-add-keywords + nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)" + 1 font-lock-warning-face t)))) + +(defun esk-sudo-edit (&optional arg) + (interactive "p") + (if (or arg (not buffer-file-name)) + (find-file (concat "/sudo:root@localhost:" (read-file-name "File: "))) + (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name)))) + +;; Open Fefes blog +(defun fefes-blog () + (interactive) + (eww "https://blog.fefe.de/")) + +;; Open this machines NixOS config +(defun nix-config () + (interactive) + (find-file "/etc/nixos/configuration.nix")) + +;; Open the NixOS man page +(defun nixos-man () + (interactive) + (man "configuration.nix")) + +;; Open local emacs configuration +(defun emacs-config () + (interactive) + (dired "~/.emacs.d/")) + +;; Get the nix store path for a given derivation. +;; If the derivation has not been built before, this will trigger a build. +(defun nix-store-path (derivation) + (let ((expr (concat "with import <nixos> {}; " derivation))) + (s-chomp (shell-command-to-string (concat "nix-build -E '" expr "'"))))) + +(defun insert-nix-store-path () + (interactive) + (let ((derivation (read-string "Derivation name (in <nixos>): "))) + (insert (nix-store-path derivation)))) + +(defun toggle-force-newline () + "Buffer-local toggle for enforcing final newline on save." + (interactive) + (setq-local require-final-newline (not require-final-newline)) + (message "require-final-newline in buffer %s is now %s" + (buffer-name) + require-final-newline)) + +;; Helm includes a command to run external applications, which does +;; not seem to exist in ivy. This implementation uses some of the +;; logic from Helm to provide similar functionality using ivy. +(defun list-external-commands () + "Creates a list of all external commands available on $PATH + while filtering NixOS wrappers." + (cl-loop + for dir in (split-string (getenv "PATH") path-separator) + when (and (file-exists-p dir) (file-accessible-directory-p dir)) + for lsdir = (cl-loop for i in (directory-files dir t) + for bn = (file-name-nondirectory i) + when (and (not (s-contains? "-wrapped" i)) + (not (member bn completions)) + (not (file-directory-p i)) + (file-executable-p i)) + collect bn) + append lsdir into completions + finally return (sort completions 'string-lessp))) + +(defun run-external-command (cmd) + "Execute the specified command and notify the user when it + finishes." + (message "Starting %s..." cmd) + (set-process-sentinel + (start-process-shell-command cmd nil cmd) + (lambda (process event) + (when (string= event "finished\n") + (message "%s process finished." process))))) + +(defun ivy-run-external-command () + "Prompts the user with a list of all installed applications and + lets them select one to launch." + + (interactive) + (let ((external-commands-list (list-external-commands))) + (ivy-read "Command:" external-commands-list + :require-match t + :history 'external-commands-history + :action #'run-external-command))) + +(defun ivy-password-store (&optional password-store-dir) + "Custom version of password-store integration with ivy that + actually uses the GPG agent correctly." + + (interactive) + (ivy-read "Copy password of entry: " + (password-store-list (or password-store-dir (password-store-dir))) + :require-match t + :keymap ivy-pass-map + :action (lambda (entry) + (let ((password (auth-source-pass-get 'secret entry))) + (password-store-clear) + (kill-new password) + (setq password-store-kill-ring-pointer kill-ring-yank-pointer) + (message "Copied %s to the kill ring. Will clear in %s seconds." + entry (password-store-timeout)) + (setq password-store-timeout-timer + (run-at-time (password-store-timeout) + nil 'password-store-clear)))))) + +(defun ivy-browse-repositories () + "Select a git repository and open its associated magit buffer." + + (interactive) + (ivy-read "Repository: " + (magit-list-repos) + :require-match t + :sort t + :action #'magit-status)) + +(defun warmup-gpg-agent (arg &optional exit) + "Function used to warm up the GPG agent before use. This is + useful in cases where there is no easy way to make pinentry run + in the correct context (such as when sending email)." + (interactive) + (message "Warming up GPG agent") + (epg-sign-string (epg-make-context) "dummy") + nil) + +(defun bottom-right-window-p () + "Determines whether the last (i.e. bottom-right) window of the + active frame is showing the buffer in which this function is + executed." + (let* ((frame (selected-frame)) + (right-windows (window-at-side-list frame 'right)) + (bottom-windows (window-at-side-list frame 'bottom)) + (last-window (car (seq-intersection right-windows bottom-windows)))) + (eq (current-buffer) (window-buffer last-window)))) + +(defun inferior-erlang-nix-shell () + "Start an inferior Erlang process from the root of the current + project." + (interactive) + (inferior-erlang + (format "nix-shell --command erl %s" (cdr (project-current))))) + +(defun intero-fix-ghci-panic () + "Disable deferring of out of scope variable errors, which + triggers a bug in the interactive Emacs REPL printing a panic + under certain conditions." + + (interactive) + (let* ((root (intero-project-root)) + (package-name (intero-package-name)) + (backend-buffer (intero-buffer 'backend)) + (name (format "*intero:%s:%s:repl*" + (file-name-nondirectory root) + package-name)) + (setting ":set -fno-defer-out-of-scope-variables\n")) + (when (get-buffer name) + (with-current-buffer (get-buffer name) + (goto-char (point-max)) + (let ((process (get-buffer-process (current-buffer)))) + (when process (process-send-string process setting))))))) + +;; Brute-force fix: Ensure the setting is injected every time the REPL +;; is selected. +;; +;; Upstream issue: https://github.com/commercialhaskell/intero/issues/569 +(advice-add 'intero-repl :after (lambda (&rest r) (intero-fix-ghci-panic)) + '((name . intero-panic-fix))) +(advice-add 'intero-repl-load :after (lambda (&rest r) (intero-fix-ghci-panic)) + '((name . intero-panic-fix))) + +(provide 'functions) diff --git a/tools/emacs/init/look-and-feel.el b/tools/emacs/init/look-and-feel.el new file mode 100644 index 000000000000..3d480bd5f43e --- /dev/null +++ b/tools/emacs/init/look-and-feel.el @@ -0,0 +1,115 @@ +;;; -*- lexical-binding: t; -*- + +;; Hide those ugly tool bars: +(tool-bar-mode 0) +(scroll-bar-mode 0) +(menu-bar-mode 0) +(add-hook 'after-make-frame-functions + (lambda (frame) (scroll-bar-mode 0))) + +;; Don't do any annoying things: +(setq ring-bell-function 'ignore) +(setq initial-scratch-message "") + +;; Remember layout changes +(winner-mode 1) + +;; Usually emacs will run as a proper GUI application, in which case a few +;; extra settings are nice-to-have: +(when window-system + (setq frame-title-format '(buffer-file-name "%f" ("%b"))) + (mouse-wheel-mode t) + (blink-cursor-mode -1)) + +;; Configure editor fonts +(let ((font (format "Input Mono-%d" 12))) + (setq default-frame-alist `((font-backend . "xft") + (font . ,font))) + (set-frame-font font t t)) + +;; Display battery in mode-line's misc section on adho: +(when (equal "adho" (system-name)) + (setq battery-mode-line-format " %b%p%%") + (display-battery-mode)) + +;; Configure telephone-line +(defun telephone-misc-if-last-window () + "Renders the mode-line-misc-info string for display in the + mode-line if the currently active window is the last one in the + frame. + + The idea is to not display information like the current time, + load, battery levels in all buffers." + + (when (bottom-right-window-p) + (telephone-line-raw mode-line-misc-info t))) + +(defun telephone-line-setup () + (telephone-line-defsegment telephone-line-last-window-segment () + (telephone-misc-if-last-window)) + + ;; Display the current EXWM workspace index in the mode-line + (telephone-line-defsegment telephone-line-exwm-workspace-index () + (when (bottom-right-window-p) + (format "[%s]" exwm-workspace-current-index))) + + ;; Define a highlight font for ~ important ~ information in the last + ;; window. + (defface special-highlight '((t (:foreground "white" :background "#5f627f"))) "") + (add-to-list 'telephone-line-faces + '(highlight . (special-highlight . special-highlight))) + + (setq telephone-line-lhs + '((nil . (telephone-line-position-segment)) + (accent . (telephone-line-buffer-segment)))) + + (setq telephone-line-rhs + '((accent . (telephone-line-major-mode-segment)) + (nil . (telephone-line-last-window-segment + telephone-line-exwm-workspace-index)) + (highlight . (telephone-line-notmuch-counts)))) + + (setq telephone-line-primary-left-separator 'telephone-line-tan-left + telephone-line-primary-right-separator 'telephone-line-tan-right + telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left + telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right) + + (telephone-line-mode 1)) + +;; Auto refresh buffers +(global-auto-revert-mode 1) + +;; Use clipboard properly +(setq select-enable-clipboard t) + +;; Show in-progress chords in minibuffer +(setq echo-keystrokes 0.1) + +;; Show column numbers in all buffers +(column-number-mode t) + +;; Highlight currently active line +(global-hl-line-mode t) + +(defalias 'yes-or-no-p 'y-or-n-p) +(defalias 'auto-tail-revert-mode 'tail-mode) + +;; Style line numbers (shown with M-g g) +(setq linum-format + (lambda (line) + (propertize + (format (concat " %" + (number-to-string + (length (number-to-string + (line-number-at-pos (point-max))))) + "d ") + line) + 'face 'linum))) + +;; Display tabs as 2 spaces +(setq tab-width 2) + +;; Don't wrap around when moving between buffers +(setq windmove-wrap-around nil) + +(provide 'look-and-feel) diff --git a/tools/emacs/init/mail-setup.el b/tools/emacs/init/mail-setup.el new file mode 100644 index 000000000000..1700ccddd37d --- /dev/null +++ b/tools/emacs/init/mail-setup.el @@ -0,0 +1,98 @@ +(require 'notmuch) +(require 'counsel-notmuch) + +(global-set-key (kbd "C-c m") 'notmuch-hello) +(global-set-key (kbd "C-c C-m") 'counsel-notmuch) +(global-set-key (kbd "C-c C-e n") 'notmuch-mua-new-mail) + +(setq notmuch-cache-dir (format "%s/.cache/notmuch" (getenv "HOME"))) +(make-directory notmuch-cache-dir t) + +;; Cache addresses for completion: +(setq notmuch-address-save-filename (concat notmuch-cache-dir "/addresses")) + +;; Don't spam my home folder with drafts: +(setq notmuch-draft-folder "drafts") ;; relative to notmuch database + +;; Mark things as read when archiving them: +(setq notmuch-archive-tags '("-inbox" "-unread" "+archive")) + +;; Show me saved searches that I care about: +(setq notmuch-saved-searches + '((:name "inbox" :query "tag:inbox" :count-query "tag:inbox AND tag:unread" :key "i") + (:name "aprila-dev" :query "tag:aprila-dev" :count-query "tag:aprila-dev AND tag:unread" :key "d") + (:name "gitlab" :query "tag:gitlab" :key "g") + (:name "sent" :query "tag:sent" :key "t") + (:name "drafts" :query "tag:draft"))) +(setq notmuch-show-empty-saved-searches t) + +;; Mail sending configuration +(setq send-mail-function 'sendmail-send-it) ;; sendmail provided by MSMTP +(setq notmuch-always-prompt-for-sender t) +(setq notmuch-mua-user-agent-function + (lambda () (format "Emacs %s; notmuch.el %s" emacs-version notmuch-emacs-version))) +(setq mail-host-address (system-name)) +(setq notmuch-mua-cite-function #'message-cite-original-without-signature) + +;; Close mail buffers after sending mail +(setq message-kill-buffer-on-exit t) + +;; Ensure sender is correctly passed to msmtp +(setq mail-specify-envelope-from t + message-sendmail-envelope-from 'header + mail-envelope-from 'header) + +;; Store sent mail in the correct folder per account +(setq notmuch-maildir-use-notmuch-insert nil) +(setq notmuch-fcc-dirs '(("mail@tazj.in" . "tazjin/Sent") + ;; Not a mistake, Office365 apparently + ;; renames IMAP folders (!) to your local + ;; language instead of providing translations + ;; in the UI m( + ("vincent@aprila.no" . "aprila/Sende element"))) + +;; I don't use drafts but I instinctively hit C-x C-s constantly, lets +;; handle that gracefully. +(define-key notmuch-message-mode-map (kbd "C-x C-s") #'ignore) + +;; MSMTP decrypts passwords using pass, but pinentry doesn't work +;; correctly in that setup. This forces a warmup of the GPG agent +;; before sending the message. +;; +;; Note that the sending function is advised because the provided hook +;; for this seems to run at the wrong time. +(advice-add 'notmuch-mua-send-common :before 'warmup-gpg-agent) + +;; Define a telephone-line segment for displaying the count of unread, +;; important mails in the last window's mode-line: +(defvar *last-notmuch-count-redraw* 0) +(defvar *current-notmuch-count* nil) + +(defun update-display-notmuch-counts () + "Update and render the current state of the notmuch unread + count for display in the mode-line. + + The offlineimap-timer runs every 2 minutes, so it does not make + sense to refresh this much more often than that." + + (when (> (- (float-time) *last-notmuch-count-redraw*) 30) + (setq *last-notmuch-count-redraw* (float-time)) + (let* ((inbox-unread (notmuch-saved-search-count "tag:inbox and tag:unread")) + (devel-unread (notmuch-saved-search-count "tag:aprila-dev and tag:unread")) + (notmuch-count (format "I: %s; D: %s" inbox-unread devel-unread))) + (setq *current-notmuch-count* notmuch-count))) + + (when (and (bottom-right-window-p) + ;; Only render if the initial update is done and there + ;; are unread mails: + *current-notmuch-count* + (not (equal *current-notmuch-count* "I: 0; D: 0"))) + *current-notmuch-count*)) + +(telephone-line-defsegment telephone-line-notmuch-counts () + "This segment displays the count of unread notmuch messages in + the last window's mode-line (if unread messages are present)." + + (update-display-notmuch-counts)) + +(provide 'mail-setup) diff --git a/tools/emacs/init/modes.el b/tools/emacs/init/modes.el new file mode 100644 index 000000000000..19ed2a684349 --- /dev/null +++ b/tools/emacs/init/modes.el @@ -0,0 +1,36 @@ +;; Initializes modes I use. + +(add-hook 'prog-mode-hook 'esk-add-watchwords) + +;; Use auto-complete as completion at point +(defun set-auto-complete-as-completion-at-point-function () + (setq completion-at-point-functions '(auto-complete))) + +(add-hook 'auto-complete-mode-hook + 'set-auto-complete-as-completion-at-point-function) + +;; Enable rainbow-delimiters for all things programming +(add-hook 'prog-mode-hook 'rainbow-delimiters-mode) + +;; Enable Paredit & Company in Emacs Lisp mode +(add-hook 'emacs-lisp-mode-hook 'company-mode) + +;; Always highlight matching brackets +(show-paren-mode 1) + +;; Always auto-close parantheses and other pairs +;; (replaced by smartparens) +;; (electric-pair-mode) + +;; Keep track of recent files +(recentf-mode) + +;; Easily navigate sillycased words +(global-subword-mode 1) + +;; Transparently open compressed files +(auto-compression-mode t) + +;; Show available key chord completions + +(provide 'modes) diff --git a/tools/emacs/init/nixos.el b/tools/emacs/init/nixos.el new file mode 100644 index 000000000000..e384e9b77db8 --- /dev/null +++ b/tools/emacs/init/nixos.el @@ -0,0 +1,103 @@ +;; Configure additional settings if this is one of my NixOS machines +;; (i.e. if ExWM is required) +;; -*- lexical-binding: t; -*- + +(require 's) +(require 'f) +(require 'dash) + +(defun pulseaudio-ctl (cmd) + (shell-command (concat "pulseaudio-ctl " cmd)) + (message "Volume command: %s" cmd)) + +(defun volume-mute () (interactive) (pulseaudio-ctl "mute")) +(defun volume-up () (interactive) (pulseaudio-ctl "up")) +(defun volume-down () (interactive) (pulseaudio-ctl "down")) + +(defun brightness-up () + (interactive) + (shell-command "exec light -A 10") + (message "Brightness increased")) + +(defun brightness-down () + (interactive) + (shell-command "exec light -U 10") + (message "Brightness decreased")) + +(defun lock-screen () + (interactive) + (shell-command "screen-lock")) + +(defun generate-randr-config () + (-flatten `(,(-map (lambda (n) (list n "DP2")) (number-sequence 1 7)) + (0 "eDP1") + ,(-map (lambda (n) (list n "eDP1")) (number-sequence 8 9))))) + +(use-package exwm + :hook ((exwm-update-class . (lambda () + ;; Make class name the buffer name + (exwm-workspace-rename-buffer exwm-class-name)))) + :init + (progn + (require 'exwm-config) + + (fringe-mode 3) + + (setq exwm-workspace-number 10) + + ;; 's-r': Reset + (exwm-input-set-key (kbd "s-r") #'exwm-reset) + ;; 's-w': Switch workspace + (exwm-input-set-key (kbd "s-w") #'exwm-workspace-switch) + ;; 's-N': Switch to certain workspace + (dotimes (i 10) + (exwm-input-set-key (kbd (format "s-%d" i)) + `(lambda () + (interactive) + (exwm-workspace-switch-create ,i)))) + + ;; Launch applications with completion (dmenu style!) + (exwm-input-set-key (kbd "s-d") #'ivy-run-external-command) + (exwm-input-set-key (kbd "s-p") #'ivy-password-store) + (exwm-input-set-key (kbd "C-s-p") '(lambda () + (interactive) + (ivy-password-store "~/.aprila-secrets"))) + + ;; Add Alacritty selector to a key + (exwm-input-set-key (kbd "C-x t") #'counsel-switch-to-alacritty) + + ;; Toggle between line-mode / char-mode + (exwm-input-set-key (kbd "C-c C-t C-t") #'exwm-input-toggle-keyboard) + + ;; Volume keys + (exwm-input-set-key (kbd "<XF86AudioMute>") #'volume-mute) + (exwm-input-set-key (kbd "<XF86AudioRaiseVolume>") #'volume-up) + (exwm-input-set-key (kbd "<XF86AudioLowerVolume>") #'volume-down) + + ;; Brightness keys + (exwm-input-set-key (kbd "<XF86MonBrightnessDown>") #'brightness-down) + (exwm-input-set-key (kbd "<XF86MonBrightnessUp>") #'brightness-up) + (exwm-input-set-key (kbd "<XF86Display>") #'lock-screen) + + ;; Line-editing shortcuts + (exwm-input-set-simulation-keys + '(([?\C-d] . delete) + ([?\C-w] . ?\C-c))) + + ;; Enable EXWM + (exwm-enable) + + ;; Show time in the mode line + (display-time-mode) + + ;; Configure xrandr when running on laptop + (when (equal (shell-command-to-string "hostname") "adho\n") + (require 'exwm-randr) + (setq exwm-randr-workspace-output-plist (generate-randr-config)) + (exwm-randr-enable)) + + ;; Let buffers move seamlessly between workspaces + (setq exwm-workspace-show-all-buffers t) + (setq exwm-layout-show-all-buffers t))) + +(provide 'nixos) diff --git a/tools/emacs/init/settings.el b/tools/emacs/init/settings.el new file mode 100644 index 000000000000..2e4dedc0a535 --- /dev/null +++ b/tools/emacs/init/settings.el @@ -0,0 +1,65 @@ +(require 'prescient) +(require 'ivy-prescient) +(require 'uniquify) +(require 'ivy-pass) + +;; Make ivy go! +(ivy-mode 1) +(counsel-mode 1) + +(setq ivy-use-virtual-buffers t) +(setq enable-recursive-minibuffers t) + +;; Enable support for prescient in ivy & configure it +(ivy-prescient-mode) +(prescient-persist-mode) +(add-to-list 'ivy-prescient-excluded-commands 'counsel-rg) + +;; Move files to trash when deleting +(setq delete-by-moving-to-trash t) + +;; We don't live in the 80s, but we're also not a shitty web app. +(setq gc-cons-threshold 20000000) + +(setq uniquify-buffer-name-style 'forward) + +; Fix some defaults +(setq visible-bell nil + inhibit-startup-message t + color-theme-is-global t + sentence-end-double-space nil + shift-select-mode nil + uniquify-buffer-name-style 'forward + whitespace-style '(face trailing lines-tail tabs) + whitespace-line-column 80 + default-directory "~" + fill-column 80 + ediff-split-window-function 'split-window-horizontally) + +(add-to-list 'safe-local-variable-values '(lexical-binding . t)) +(add-to-list 'safe-local-variable-values '(whitespace-line-column . 80)) + +(set-default 'indent-tabs-mode nil) + +;; UTF-8 please +(setq locale-coding-system 'utf-8) ; pretty +(set-terminal-coding-system 'utf-8) ; pretty +(set-keyboard-coding-system 'utf-8) ; pretty +(set-selection-coding-system 'utf-8) ; please +(prefer-coding-system 'utf-8) ; with sugar on top + +;; Make emacs behave sanely (overwrite selected text) +(delete-selection-mode 1) + +;; Keep your temporary files in tmp, emacs! +(setq auto-save-file-name-transforms + `((".*" ,temporary-file-directory t))) +(setq backup-directory-alist + `((".*" . ,temporary-file-directory))) + +(remove-hook 'kill-buffer-query-functions 'server-kill-buffer-query-function) + +;; Show time in 24h format +(setq display-time-24hr-format t) + +(provide 'settings) diff --git a/tools/emacs/init/term-setup.el b/tools/emacs/init/term-setup.el new file mode 100644 index 000000000000..a2a71be9eeba --- /dev/null +++ b/tools/emacs/init/term-setup.el @@ -0,0 +1,37 @@ +;; Utilities for Alacritty buffers. + +(defun open-or-create-alacritty-buffer (buffer-name) + "Switch to the buffer with BUFFER-NAME or create a + new buffer running Alacritty." + (let ((buffer (get-buffer buffer-name))) + (if (not buffer) + (run-external-command "alacritty") + (switch-to-buffer buffer)))) + +(defun is-alacritty-buffer (buffer) + "Determine whether BUFFER runs Alacritty." + (and (equal 'exwm-mode (buffer-local-value 'major-mode buffer)) + (s-starts-with? "Alacritty" (buffer-name buffer)))) + +(defun counsel-switch-to-alacritty () + "Switch to a (multi-)term buffer or create one." + (interactive) + (let ((terms (-map #'buffer-name + (-filter #'is-alacritty-buffer (buffer-list))))) + (if terms + (ivy-read "Switch to Alacritty buffer: " + (cons "New terminal" terms) + :caller 'counsel-switch-to-alacritty + :require-match t + :action #'open-or-create-alacritty-buffer) + (run-external-command "alacritty")))) + +(defun alacritty-rename () + "Rename the current terminal buffer." + (interactive) + (let* ((buffer (get-buffer (buffer-name)))) + (if (is-alacritty-buffer buffer) + (rename-buffer (format "Alacritty<%s>" (read-string "New terminal name: "))) + (error "This function is only intended to rename Alacritty buffers.")))) + +(provide 'term-setup) diff --git a/tools/gotest/default.nix b/tools/gotest/default.nix new file mode 100644 index 000000000000..168d15748e1f --- /dev/null +++ b/tools/gotest/default.nix @@ -0,0 +1,27 @@ +# This file demonstrates how to make use of pkgs.buildGo. +# +# It introduces libraries and protobuf support, however gRPC support +# is not yet included. +# +# From the root of this repository this example can be built with +# `nix-build -A tools.gotest` +{ pkgs, ... }: + +let + inherit (pkgs) buildGo; + + somelib = buildGo.package { + name = "somelib"; + srcs = [ ./lib.go ]; + }; + + someproto = buildGo.proto { + name = "someproto"; + proto = ./test.proto; + }; + +in buildGo.program { + name = "gotest"; + srcs = [ ./main.go ]; + deps = [ somelib someproto ]; +} // { meta.enableCI = true; } diff --git a/tools/gotest/lib.go b/tools/gotest/lib.go new file mode 100644 index 000000000000..0aeebb2aea69 --- /dev/null +++ b/tools/gotest/lib.go @@ -0,0 +1,11 @@ +package somelib + +import "fmt" + +func Name() string { + return "edef" +} + +func Greet(s string) string { + return fmt.Sprintf("Hello %s", s) +} diff --git a/tools/gotest/main.go b/tools/gotest/main.go new file mode 100644 index 000000000000..99218c077617 --- /dev/null +++ b/tools/gotest/main.go @@ -0,0 +1,16 @@ +// This program just exists to import some libraries and demonstrate +// that the build works, it doesn't do anything useful. +package main + +import ( + "fmt" + "somelib" + "someproto" +) + +func main() { + p := someproto.Person{ + Name: somelib.Name(), + } + fmt.Println(somelib.Greet(fmt.Sprintf("%v", p))) +} diff --git a/tools/gotest/test.proto b/tools/gotest/test.proto new file mode 100644 index 000000000000..76af63072be3 --- /dev/null +++ b/tools/gotest/test.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; +package someproto; + +import "google/protobuf/timestamp.proto"; + +message Person { + string name = 1; + google.protobuf.Timestamp last_updated = 2; +} diff --git a/tools/kms_pass.nix b/tools/kms_pass.nix new file mode 100644 index 000000000000..14989b392dd1 --- /dev/null +++ b/tools/kms_pass.nix @@ -0,0 +1,60 @@ +# This tool mimics a subset of the interface of 'pass', but uses +# Google Cloud KMS for encryption. +# +# It is intended to be compatible with how 'kontemplate' invokes +# 'pass.' +# +# Only the 'show' and 'insert' commands are supported. + +{ pkgs, kms, ... }: + +let inherit (pkgs.third_party) google-cloud-sdk tree writeShellScriptBin; +in (writeShellScriptBin "pass" '' + set -eo pipefail + + CMD="$1" + readonly SECRET=$2 + readonly SECRET_PATH="$SECRETS_DIR/$SECRET" + + function secret_check { + if [[ -z $SECRET ]]; then + echo 'Secret must be specified' + exit 1 + fi + } + + if [[ -z $CMD ]]; then + CMD="ls" + fi + + case "$CMD" in + ls) + ${tree}/bin/tree $SECRETS_DIR + ;; + show) + secret_check + ${google-cloud-sdk}/bin/gcloud kms decrypt \ + --project ${kms.project} \ + --location ${kms.region} \ + --keyring ${kms.keyring} \ + --key ${kms.key} \ + --ciphertext-file $SECRET_PATH \ + --plaintext-file - + ;; + insert) + secret_check + ${google-cloud-sdk}/bin/gcloud kms encrypt \ + --project ${kms.project} \ + --location ${kms.region} \ + --keyring ${kms.keyring} \ + --key ${kms.key} \ + --ciphertext-file $SECRET_PATH \ + --plaintext-file - + echo "Inserted secret '$SECRET'" + ;; + *) + echo "Usage: pass show/insert <secret>" + exit 1 + ;; + esac +'') // { meta.enableCI = true; } |