diff options
526 files changed, 55334 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..030a71e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +[*] +insert_final_newline=true +trim_trailing_whitespace=true +indent_size=2 +indent_style=space + +[*.{yaml,nix,sh}] +indent_size=2 +indent_style=space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ec117c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/Session.vim +history.txt diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..015bfac --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,20 @@ +keys: + - &ivi age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0 + - &serber age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v + - &pump age1tzsvgxaxwvh4874d977fk0z7ghm4mqpm0c80vhxft87dv46p5uesq7mk42 +creation_rules: + - path_regex: secrets/[^/]+\.?(yaml|json|env|ini)?$ + key_groups: + - age: + - *ivi + - *serber + - *pump + - path_regex: secrets/lemptop/[^/]+\.?(yaml|json|env|ini)?$ + key_groups: + - age: + - *ivi + - path_regex: secrets/serber/[^/]+\.?(yaml|json|env|ini)?$ + key_groups: + - age: + - *serber + - *ivi diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0bcc090 --- /dev/null +++ b/flake.lock @@ -0,0 +1,711 @@ +{ + "nodes": { + "blobs": { + "flake": false, + "locked": { + "lastModified": 1604995301, + "narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=", + "owner": "simple-nixos-mailserver", + "repo": "blobs", + "rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265", + "type": "gitlab" + }, + "original": { + "owner": "simple-nixos-mailserver", + "repo": "blobs", + "type": "gitlab" + } + }, + "deploy-rs": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs", + "utils": "utils" + }, + "locked": { + "lastModified": 1727447169, + "narHash": "sha256-3KyjMPUKHkiWhwR91J1YchF6zb6gvckCAY1jOE+ne0U=", + "owner": "serokell", + "repo": "deploy-rs", + "rev": "aa07eb05537d4cd025e2310397a6adcedfe72c76", + "type": "github" + }, + "original": { + "owner": "serokell", + "repo": "deploy-rs", + "type": "github" + } + }, + "dns": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733919067, + "narHash": "sha256-ZsL5pKwEDhcZhVJh+3IwgHus7kSW/N8qOlBscwB6BCI=", + "owner": "kirelagin", + "repo": "dns.nix", + "rev": "a23f43f9762aa96d3e35c8eeefa7610bd0cdf456", + "type": "github" + }, + "original": { + "owner": "kirelagin", + "repo": "dns.nix", + "type": "github" + } + }, + "drduh-yubikey-guide": { + "inputs": { + "drduhConfig": "drduhConfig", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1733703227, + "narHash": "sha256-5njR8Ha2FvELuRtcEKoQuQ8BKqSiZHDA3RJGYrPRDfg=", + "owner": "drduh", + "repo": "YubiKey-Guide", + "rev": "166f838a437304872b12a38ad6f1066b7a2e65e5", + "type": "github" + }, + "original": { + "owner": "drduh", + "repo": "YubiKey-Guide", + "type": "github" + } + }, + "drduhConfig": { + "flake": false, + "locked": { + "lastModified": 1719781410, + "narHash": "sha256-cmtAG7UQX7mVNoHHpVIqasfkjnO7VtBMcz8MJ7frO0k=", + "owner": "drduh", + "repo": "config", + "rev": "4eca229664d056737f1a097cdbdb10e5f247b0bc", + "type": "github" + }, + "original": { + "owner": "drduh", + "repo": "config", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_3": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_4": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_5": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "neovim-nightly-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "neovim-nightly-overlay", + "hercules-ci-effects", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "id": "flake-parts", + "type": "indirect" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1614513358, + "narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5466c5bbece17adaab2d82fae80b46e807611bf3", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "ghostty": { + "inputs": { + "flake-compat": "flake-compat_2", + "nixpkgs-stable": "nixpkgs-stable", + "nixpkgs-unstable": "nixpkgs-unstable", + "zig": "zig" + }, + "locked": { + "lastModified": 1735658428, + "narHash": "sha256-vg5GRc6H1SjVpGbbtq9HLTAilztC+vq3dRmzlIYWVxo=", + "owner": "ghostty-org", + "repo": "ghostty", + "rev": "eaa872216b577d68c09bfa13758abdedaf4fa80e", + "type": "github" + }, + "original": { + "owner": "ghostty-org", + "repo": "ghostty", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat_4", + "gitignore": "gitignore", + "nixpkgs": [ + "neovim-nightly-overlay", + "nixpkgs" + ], + "nixpkgs-stable": [ + "neovim-nightly-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1734797603, + "narHash": "sha256-ulZN7ps8nBV31SE+dwkDvKIzvN6hroRY8sYOT0w+E28=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "f0f0dc4920a903c3e08f5bdb9246bb572fcae498", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "neovim-nightly-overlay", + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "hercules-ci-effects": { + "inputs": { + "flake-parts": "flake-parts_2", + "nixpkgs": [ + "neovim-nightly-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733333617, + "narHash": "sha256-nMMQXREGvLOLvUa0ByhYFdaL0Jov0t1wzLbKjr05P2w=", + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "rev": "56f8ea8d502c87cf62444bec4ee04512e8ea24ea", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "hercules-ci-effects", + "type": "github" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1735381016, + "narHash": "sha256-CyCZFhMUkuYbSD6bxB/r43EdmDE7hYeZZPTCv0GudO4=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "10e99c43cdf4a0713b4e81d90691d22c6a58bdf2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "neovim-nightly-overlay": { + "inputs": { + "flake-compat": "flake-compat_3", + "flake-parts": "flake-parts", + "git-hooks": "git-hooks", + "hercules-ci-effects": "hercules-ci-effects", + "neovim-src": "neovim-src", + "nixpkgs": "nixpkgs_3", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1735605816, + "narHash": "sha256-n7c2VcDQN2C6vUV8/AfaZpHYWgvlbFcHErqf//hPwC8=", + "owner": "nix-community", + "repo": "neovim-nightly-overlay", + "rev": "bcd445a62280469b07354f1263bc1f136e64506a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "neovim-nightly-overlay", + "type": "github" + } + }, + "neovim-src": { + "flake": false, + "locked": { + "lastModified": 1735598207, + "narHash": "sha256-T/UtvF0WSkXy4Lk0ZzCsqjp6At0SRmwTsRXDxJcFzMA=", + "owner": "neovim", + "repo": "neovim", + "rev": "e9c077d197a80a2ecd858821b18d0be3e3eb6d0b", + "type": "github" + }, + "original": { + "owner": "neovim", + "repo": "neovim", + "type": "github" + } + }, + "nix-darwin": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1737063096, + "narHash": "sha256-Qv40syutOYQZOBjS76Swk4VeUz08KDrsc9IDSdXmyqs=", + "path": "/Users/ivi/nix-darwin", + "type": "path" + }, + "original": { + "path": "/Users/ivi/nix-darwin", + "type": "path" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1702272962, + "narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-24_11": { + "locked": { + "lastModified": 1734083684, + "narHash": "sha256-5fNndbndxSx5d+C/D0p/VF32xDiJCJzyOqorOYW4JEo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "314e12ba369ccdb9b352a4db26ff419f7c49fa84", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-24.11", + "type": "indirect" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1733423277, + "narHash": "sha256-TxabjxEgkNbCGFRHgM/b9yZWlBj60gUOUnRT/wbVQR8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "e36963a147267afc055f7cf65225958633e536bf", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "release-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1733229606, + "narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1721226092, + "narHash": "sha256-UBvzVpo5sXSi2S/Av+t+Q+C2mhMIw/LBEZR+d6NMjws=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c716603a63aca44f39bef1986c13402167450e0a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1735523292, + "narHash": "sha256-opBsbR/nrGxiiF6XzlVluiHYb6yN/hEwv+lBWTy9xoM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6d97d419e5a9b36e6293887a89a078cf85f5a61b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1735471104, + "narHash": "sha256-0q9NGQySwDQc7RhAV2ukfnu7Gxa5/ybJ2ANT8DQrQrs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "88195a94f390381c6afcdaa933c2f6ff93959cb4", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_5": { + "locked": { + "lastModified": 1732014248, + "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "nixpkgs_6": { + "locked": { + "lastModified": 1731763621, + "narHash": "sha256-ddcX4lQL0X05AYkrkV2LMFgGdRvgap7Ho8kgon3iWZk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c69a9bffbecde46b4b939465422ddc59493d3e4d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "deploy-rs": "deploy-rs", + "dns": "dns", + "drduh-yubikey-guide": "drduh-yubikey-guide", + "ghostty": "ghostty", + "home-manager": "home-manager", + "neovim-nightly-overlay": "neovim-nightly-overlay", + "nix-darwin": "nix-darwin", + "nixpkgs": "nixpkgs_4", + "simple-nixos-mailserver": "simple-nixos-mailserver", + "sops-nix": "sops-nix" + } + }, + "simple-nixos-mailserver": { + "inputs": { + "blobs": "blobs", + "flake-compat": "flake-compat_5", + "nixpkgs": "nixpkgs_5", + "nixpkgs-24_11": "nixpkgs-24_11" + }, + "locked": { + "lastModified": 1735230346, + "narHash": "sha256-zgR8NTiNDPVNrfaiOlB9yHSmCqFDo7Ks2IavaJ2dZo4=", + "owner": "simple-nixos-mailserver", + "repo": "nixos-mailserver", + "rev": "dc0569066e79ae96184541da6fa28f35a33fbf7b", + "type": "gitlab" + }, + "original": { + "owner": "simple-nixos-mailserver", + "repo": "nixos-mailserver", + "type": "gitlab" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": "nixpkgs_6" + }, + "locked": { + "lastModified": 1735468296, + "narHash": "sha256-ZjUjbvS06jf4fElOF4ve8EHjbpbRVHHypStoY8HGzk8=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "bcb8b65aa596866eb7e5c3e1a6cccbf5d1560b27", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "neovim-nightly-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1735135567, + "narHash": "sha256-8T3K5amndEavxnludPyfj3Z1IkcFdRpR23q+T0BVeZE=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "9e09d30a644c57257715902efbb3adc56c79cf28", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "zig": { + "inputs": { + "flake-compat": [ + "ghostty" + ], + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "ghostty", + "nixpkgs-stable" + ] + }, + "locked": { + "lastModified": 1717848532, + "narHash": "sha256-d+xIUvSTreHl8pAmU1fnmkfDTGQYCn2Rb/zOwByxS2M=", + "owner": "mitchellh", + "repo": "zig-overlay", + "rev": "02fc5cc555fc14fda40c42d7c3250efa43812b43", + "type": "github" + }, + "original": { + "owner": "mitchellh", + "repo": "zig-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3a7eab6 --- /dev/null +++ b/flake.nix @@ -0,0 +1,217 @@ +{ + description = "Nixos, home-manager, and deploy-rs configuration"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + sops-nix.url = "github:Mic92/sops-nix"; + home-manager = { + url = "github:nix-community/home-manager"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + deploy-rs.url = "github:serokell/deploy-rs"; + dns = { + url = "github:kirelagin/dns.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver"; + neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay"; + drduh-yubikey-guide.url = "github:drduh/YubiKey-Guide"; + ghostty = { + url = "github:ghostty-org/ghostty"; + }; + nix-darwin = { + url = "path:/Users/ivi/nix-darwin"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + + outputs = inputs @ { + self, + nixpkgs, + home-manager, + sops-nix, + deploy-rs, + ghostty, + ... + }: let + withLibs = + nixpkgs.lib.foldl + (acc: inputLib: acc.extend (_: _: inputLib)) + nixpkgs.lib; + + lib = + (withLibs [ + inputs.nix-darwin.lib + home-manager.lib + ]) + .extend + (import ./lib inputs); + + nixosSystems = with lib; { + lemptop = { + system = "x86_64-linux"; + modules = + [ + ./machines/lemptop.nix + { + environment.systemPackages = [ + ghostty.packages.x86_64-linux.default + ]; + } + ] + ++ modulesIn ./profiles/core + ++ modulesIn ./profiles/graphical + ++ modulesIn ./profiles/station + ++ modulesIn ./profiles/email + ++ [ + (import ./profiles/netboot/system.nix nixosConfigurations.pump) + ]; + opts = { + isStation = true; + syncthing = { + enable = true; + id = "TGRWV6Z-5CJ4KRI-4VDTIUE-UA5LQYS-3ARZGNK-KL7HGXP-352PB5Q-ADTV6Q2"; + }; + }; + }; + + pump = { + system = "x86_64-linux"; + modules = + [ + ./machines/pump-netboot.nix + ./profiles/core/configuration.nix + ./profiles/core/syncthing.nix + ./profiles/core/secrets.nix + ./profiles/core/hm.nix + ] + ++ modulesIn ./profiles/homeserver; + opts = { + isServer = true; + ipv4 = ["192.168.2.13"]; + ipv6 = ["2a02:a46b:ee73:1:c240:4bcb:9fc3:71ab"]; + tailnet = { + ipv4 = "100.90.145.95"; + ipv6 = "fd7a:115c:a1e0::e2da:915f"; + nodeKey = "nodekey:dcd737aab30c21eb4f44a40193f3b16a8535ffe2fb5008904b39bb54e2da915e"; + }; + syncthing = { + enable = false; + # id = "7USTCMT-QZTLGPL-5FCRKJW-BZUGMOS-H7D2TTK-F4COYPG-5D7VUO2-QFME2AS"; + }; + }; + }; + + serber = { + system = "x86_64-linux"; + modules = + [ + ./machines/serber.nix + ] + ++ modulesIn ./profiles/core + ++ modulesIn ./profiles/server; + opts = { + isServer = true; + ipv4 = ["65.109.143.65"]; + ipv6 = ["2a01:4f9:c012:ccc2::1"]; + }; + }; + + gpg = { + system = "aarch64-linux"; + modules = + [ + (import ./machines/gpg.nix inputs.drduh-yubikey-guide) + ./profiles/core/configuration.nix + ./profiles/core/hm.nix + ./profiles/core/meta.nix + ./profiles/core/neovim.nix + ] + ++ modulesIn ./profiles/graphical; + opts = {}; + }; + + vm-aarch64 = { + system = "aarch64-linux"; + modules = + [ + ./machines/vm-aarch64.nix + { + environment.systemPackages = [ + ghostty.packages.aarch64-linux.default + ]; + } + ] + ++ modulesIn ./profiles/core + ++ modulesIn ./profiles/graphical; + opts = { + isStation = true; + syncthing = { + enable = true; + id = "LDZVZ6H-KO3BKC6-FMLZOND-MKXI4DF-SNT27OT-Q5KMN2M-A2DYFNQ-3BWUYA6"; + }; + }; + }; + + bellerophone = { + opts = { + syncthing = { + enable = true; + id = "75U7B2F-SZOJRY2-UKAADJD-NI3R5SJ-K4J35IN-D2NJJFJ-JG5TCJA-AUERDAA"; + }; + }; + }; + }; + + darwinSystems = with lib; { + work = { + system = "aarch64-darwin"; + modules = + [ + ./machines/work.nix + ] + ++ modulesIn ./profiles/core; + opts = { + isDarwin = true; + syncthing = { + enable = true; + id = "GR5MHK2-HDCFX4I-Y7JYKDN-EFTQFG6-24CXSHB-M5C6R3G-2GWX5ED-VEPAQA7"; + }; + }; + }; + }; + + mkSystems = lib.mkSystemsFor (nixosSystems // darwinSystems); + in + with lib; { + inherit lib; + + nixosConfigurations = mkSystems nixosSystems; + + darwinConfigurations = mkSystems darwinSystems; + + deploy.nodes = { + pump = { + hostname = "192.168.2.13"; # hostname + "." + my.domain; + sshUser = "root"; + profiles.system.path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.pump; + }; + }; + + devShells.x86_64-linux.hetzner = let + pkgs = import nixpkgs {system = "x86_64-linux";}; + in + with pkgs; + mkShell { + name = "deploy"; + buildInputs = [ + pkgs.bashInteractive + deploy-rs.packages."${system}".default + ]; + shellHook = '' + export HCLOUD_TOKEN="$(pass show personal/hetzner-token)" + ''; + }; + }; +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..b4408e2 --- /dev/null +++ b/justfile @@ -0,0 +1,81 @@ +SSH_OPTIONS := "-o PubkeyAuthentication=no -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" +NIXNAME := "vm-aarch64" + +@vm-bootstrap0 diskname ip: + #!/usr/bin/env bash + ssh {{SSH_OPTIONS}} -p22 root@{{ip}} " + parted /dev/{{diskname}} -- mklabel gpt + parted /dev/{{diskname}} -- mkpart primary 512MB -8GB + parted /dev/{{diskname}} -- mkpart primary linux-swap -8GB 100\% + parted /dev/{{diskname}} -- mkpart ESP fat32 1MB 512MB + parted /dev/{{diskname}} -- set 3 esp on + sleep 1 + mkfs.ext4 -L nixos /dev/{{diskname}}p1 + mkswap -L swap /dev/{{diskname}}p2 + mkfs.fat -F 32 -n boot /dev/{{diskname}}p3 + sleep 1 + mount /dev/disk/by-label/nixos /mnt + mkdir -p /mnt/boot + mount /dev/disk/by-label/boot /mnt/boot + nixos-generate-config --root /mnt + sed --in-place '/system\.stateVersion = .*/a \ + nix.package = pkgs.nixVersions.latest;\n \ + nix.extraOptions = \"experimental-features = nix-command flakes configurable-impure-env\";\n \ + services.openssh.enable = true;\n \ + services.openssh.settings.PasswordAuthentication = true;\n \ + services.openssh.settings.PermitRootLogin = \"yes\";\n \ + users.users.root.initialPassword = \"root\";\n \ + ' /mnt/etc/nixos/configuration.nix + nixos-install --no-root-passwd && reboot + " + +@vm-secrets ip: + # GPG keyring + rsync -av -e 'ssh {{SSH_OPTIONS}}' \ + --exclude='.#*' \ + --exclude='S.*' \ + $HOME/.gnupg/ root@{{ip}}:~/.gnupg + # SSH keys + rsync -av -e 'ssh {{SSH_OPTIONS}}' \ + --exclude='environment' \ + --exclude='ssh_auth_sock' \ + $HOME/.ssh/ root@{{ip}}:~/.ssh + # Sops keys + rsync -avr -e 'ssh {{SSH_OPTIONS}}' --relative ~/./.config/sops root@{{ip}}:~ + +# copy the Nix configurations into the VM. +@vm-copy ip: + rsync -av -e 'ssh {{SSH_OPTIONS}} -p22' \ + --exclude='.git/' \ + --rsync-path="sudo rsync" \ + ./ root@{{ip}}:/nix-config + +# run the nixos-rebuild switch command. This does NOT copy files so you +# have to run vm/copy before. +@vm-switch ip: (vm-copy ip) (vm-secrets ip) + ssh {{SSH_OPTIONS}} -p22 root@{{ip}} " \ + sudo NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nixos-rebuild switch --impure --flake \"/nix-config#{{NIXNAME}}\" \ + " + +# after bootstrap0, run this to finalize. After this, do everything else +# in the VM unless secrets change. +@vm-bootstrap ip: (vm-switch ip) + ssh {{SSH_OPTIONS}} -p22 root@{{ip}} " \ + sudo reboot; \ + " + +@symlinks: + #!/usr/bin/env bash + set -x + ln -sf /nix-config/mut/DefaultKeyBinding.dict ~/Library/KeyBindings/DefaultKeyBinding.dict + ! [ -d ~/.config/aerospace ] && ln -sf /nix-config/mut/aerospace ~/.config/aerospace + ! [ -d ~/.config/ghostty ] && ln -sf /nix-config/mut/ghostty ~/.config/ghostty + ! [ -d ~/.config/nvim ] && ln -sf /nix-config/mut/neovim ~/.config/nvim + ! [ -d ~/.config/k9s ] && ln -sf /nix-config/mut/k9s ~/.config/k9s + ! [ -d ~/.config/carapace ] && ln -sf /nix-config/mut/carapace ~/.config/carapace + ! [ -d ~/.config/git ] && ln -sf /nix-config/mut/git ~/.config/git + + ! [ -d ~/.config/nushell ] && ln -sf /nix-config/mut/nushell ~/.config/nushell + ! [ -d ~/.config/vis ] && ln -sf /nix-config/mut/vis ~/.config/vis + rm -rf "$HOME/Library/Application Support/nushell"; ln -sf /nix-config/mut/nushell "$HOME/Library/Application Support/nushell" + true diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..bca0a8e --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,76 @@ +inputs: lib: prev: with lib; rec { + modulesAttrsIn = dir: pipe dir [ + builtins.readDir + (mapAttrsToList (name: type: + if type == "regular" && hasSuffix ".nix" name && name != "default.nix" then + [ { name = removeSuffix ".nix" name; value = dir + "/${name}"; } ] + else if type == "directory" && pathExists (dir + "/${name}/default.nix") then + [ { inherit name; value = dir + "/${name}"; } ] + else + [] + )) + concatLists + listToAttrs + ]; + + modulesIn = dir: attrValues (modulesAttrsIn dir); + + # Collects the inputs of a flake recursively (with possible duplicates). + collectFlakeInputs = input: + [ input ] ++ concatMap collectFlakeInputs (builtins.attrValues (input.inputs or {})); + + my = import ./my.nix inputs.self lib; + + mkMachines = import ./machine.nix lib; + + # Gets module from ./machines/ and uses the lib to define which other modules + # the machine needs. + mkSystem = machines: name: systemInputs @ { + system, + modules, + opts, + ... + }: + let + machine = machines.${name}; + systemClosure = + (if hasInfix "darwin" system then + darwinSystem + else + nixosSystem); + home-manager = + (if hasInfix "darwin" system then + [inputs.home-manager.darwinModules.default] + else + [inputs.home-manager.nixosModules.default]); + in + systemClosure { + inherit lib system; + specialArgs = { + inherit (inputs) self; + inherit machines machine inputs; + }; + modules = + modules + ++ + home-manager + ++ [ + ({pkgs, ...}: { + nixpkgs.overlays = with lib; [ + (composeManyExtensions [ + (import ../overlays/vimPlugins.nix {inherit pkgs;}) + (import ../overlays/openpomodoro-cli.nix {inherit pkgs lib;}) + inputs.neovim-nightly-overlay.overlays.default + ]) + ]; + }) + ]; + }; + + mkSystemsFor = allSystems: systems: + let + machines = mkMachines (mapAttrs (name: value: value.opts) allSystems); + in + (mapAttrs (mkSystem machines) systems); + +} diff --git a/lib/machine.nix b/lib/machine.nix new file mode 100644 index 0000000..10e766f --- /dev/null +++ b/lib/machine.nix @@ -0,0 +1,98 @@ +lib: systemOptions: with lib; let + modules = [ + { + options.machines = mkOption { + description = "Machine options"; + default = {}; + type = with types; attrsOf (submodule ({ name, config, ... }: { + freeformType = attrs; + options = { + modules = mkOption { + description = "Final list of modules to import"; + type = listOf str; + default = []; + }; + profiles = mkOption { + description = "List of profiles to use"; + type = listOf str; + default = []; + }; + hostname = mkOption { + description = "The machine's hostname"; + type = str; + readOnly = true; + default = name; + }; + ipv4 = mkOption { + description = "The machines public IPv4 addresses"; + type = listOf str; + default = []; + }; + ipv6 = mkOption { + description = "The machines public IPv6 addresses"; + type = listOf str; + default = []; + }; + isStation = mkOption { + description = "The machine is a desktop station"; + type = bool; + default = false; + }; + isServer = mkOption { + description = "The machine is a server"; + type = bool; + default = false; + }; + isFake = mkOption { + description = "The machine is a fake machine"; + type = bool; + default = false; + }; + isDarwin = mkOption { + description = "The machine is a fake machine"; + type = bool; + default = false; + }; + tailnet = mkOption { + default = {}; + type = with types; attrsOf (submodule ({ name, config, ... }: { + options = { + ipv4 = mkOption { + description = "The machine's tailnet IPv4 address"; + type = str; + default = null; + }; + ipv6 = mkOption { + description = "The machine's tailnet IPv6 address"; + type = str; + default = null; + }; + nodeKey = mkOption { + description = "The machine's tailnet public key"; + type = str; + default = null; + }; + }; + })); + }; + syncthing = mkOption { + default = {}; + type = with types; submodule { + freeformType = attrs; + options = { + id = mkOption { + description = "The machine's syncting public id"; + type = str; + default = ""; + }; + enable = mkEnableOption "Add to syncthing cluster"; + }; + }; + }; + }; + })); + }; + config.machines = systemOptions; + } + ]; +in (evalModules { inherit modules; }).config.machines diff --git a/lib/my.nix b/lib/my.nix new file mode 100644 index 0000000..04cfc1c --- /dev/null +++ b/lib/my.nix @@ -0,0 +1,110 @@ +self: lib: with lib; let + modules = [ + { + config = { + _module.freeformType = with types; attrs; + + username = "ivi"; + githubUsername = "ivi-vink"; + realName = "Mike Vink"; + domain = "vinkies.net"; + email = "ivi@vinkies.net"; + sshKeys = [ + "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOF/b1fDKIKsjEIC5nfshd3PkzqnD672Miyo/3fyrxMfAAAABHNzaDo= ivi@vm-aarch64" + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDqsfYS7sOLfLWvGTmxT2QYGkbXJ5kREFl42n3jtte5sLps76KECgKqEjA4OLhNZ51lKFBDzcn1QOUl3RN4+qHsBtkr+02a7hhf1bBLeb1sx6+FVXdsarln5lUF/NMcpj6stUi8mqY4aQ21jQKxZsGip9fI8fx3HtXYCVhIarRbshQlwDqTplJBLDtrnmWTprxVnz1xSZRr3euXsIh1FFQZI6klPPBa6qFJtWWtGNBCRr8Sruo6I4on7QjNyW/s1OgiNAR0N2IO9wCdjlXrjNnFEAaMrpDpZde7eULbiFP2pHYVVy/InwNhhePYkeBh/4BzlaUZVv6gXsX7wOC5OyWaXbbMzWEopbnqeXXLwNyOZ88YpN/c+kZk2/1CHl+xmlVGAr9TnZ9VST5Y4ZAEqq8OKoP3ZcchAWxWjzTgPogSfiIAP/n5xrgB+8uRZb/gkN+I7RTQKGrS2Ex7gfkj39beDeevQj3XVQ1U2kp3n+jUBHItCCpZyHISgTYW2Ct6lrziJpD0kPlAOrN3BGQtkStHYK+4EE1PrrwWGkG7Ue+tlETe8FTg+AMv1VjLV9b3pHZJCrao5/cY2MxkfGzf4HTfeueqSLSsrYuiogHAPvvzfvOV5un+dWX8HyeBjmKTBwDBFuhdca/wzk0ArHSgEYUmh2NXj/G4gaSF3EX5ZSxmMQ== ${my.email}" + ]; + + # machines = { + # wsl = { + # isFake = true; + # profiles = [ + # "core" + # ]; + # }; + # vm-aarch64 = { + # isStation = true; + # profiles = [ + # "core" + # "graphical" + # ]; + # syncthing = { + # enable = true; + # id = "LDZVZ6H-KO3BKC6-FMLZOND-MKXI4DF-SNT27OT-Q5KMN2M-A2DYFNQ-3BWUYA6"; + # }; + # }; + # persephone = { + # isFake = true; + # tailnet = { + # ipv4 = "100.72.127.82"; + # ipv6 = "fd7a:115c:a1e0::9c08:7f52"; + # nodeKey = "nodekey:2ffbb54277ba6c29337807b74f69438eba4d3802bffbe9c7df4093139c087f51"; + # }; + # }; + # bellerophone = { + # isFake = true; + # tailnet = { + # ipv4 = "100.123.235.65"; + # ipv6 = "fd7a:115c:a1e0::bafb:eb41"; + # nodeKey = "nodekey:e2a9f948a1252a4b1f1932bb99e73981fa0b7173825b54ba968f9cc0bafbeb40"; + # }; + # syncthing = { + # enable = true; + # id = "75U7B2F-SZOJRY2-UKAADJD-NI3R5SJ-K4J35IN-D2NJJFJ-JG5TCJA-AUERDAA"; + # }; + # }; + # serber = { + # isServer = true; + # profiles = [ + # "core" + # "server" + # ]; + # ipv4 = [ "65.109.143.65" ]; + # ipv6 = [ "2a01:4f9:c012:ccc2::1" ]; + # }; + # work = { + # isDarwin = true; + # profiles = [ + # "core" + # ]; + # syncthing = { + # enable = true; + # id = "GR5MHK2-HDCFX4I-Y7JYKDN-EFTQFG6-24CXSHB-M5C6R3G-2GWX5ED-VEPAQA7"; + # }; + # }; + # lemptop = { + # isStation = true; + # profiles = [ + # "core" + # "graphical" + # "station" + # "email" + # "netboot" + # ]; + # syncthing = { + # enable = true; + # id = "TGRWV6Z-5CJ4KRI-4VDTIUE-UA5LQYS-3ARZGNK-KL7HGXP-352PB5Q-ADTV6Q2"; + # }; + # }; + # pump = { + # isServer = true; + # profiles = [ + # "core" + # "homeserver" + # ]; + # ipv4 = [ "192.168.2.13" ]; + # ipv6 = [ "2a02:a46b:ee73:1:c240:4bcb:9fc3:71ab" ]; + # tailnet = { + # ipv4 = "100.90.145.95"; + # ipv6 = "fd7a:115c:a1e0::e2da:915f"; + # nodeKey = "nodekey:dcd737aab30c21eb4f44a40193f3b16a8535ffe2fb5008904b39bb54e2da915e"; + # }; + # syncthing = { + # enable = true; + # id = "7USTCMT-QZTLGPL-5FCRKJW-BZUGMOS-H7D2TTK-F4COYPG-5D7VUO2-QFME2AS"; + # }; + # }; + # }; + }; + } + ]; +in (evalModules { inherit modules; }).config diff --git a/machines/gpg.nix b/machines/gpg.nix new file mode 100644 index 0000000..5bde3fc --- /dev/null +++ b/machines/gpg.nix @@ -0,0 +1,287 @@ +self: { lib, modulesPath, ... }: with lib; { + imports = [ + "${modulesPath}/profiles/all-hardware.nix" + "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" + ( + { + lib, + pkgs, + config, + ... + }: let + gpgAgentConf = pkgs.runCommand "gpg-agent.conf" {} '' + cat <<'CONFIG' > $out + # https://github.com/drduh/config/blob/master/gpg-agent.conf + # https://www.gnupg.org/documentation/manuals/gnupg/Agent-Options.html + pinentry-program /usr/bin/pinentry-curses + enable-ssh-support + ttyname $GPG_TTY + default-cache-ttl 60 + max-cache-ttl 120 + CONFIG + ''; + gpgConf = pkgs.runCommand "gpg.conf" {} '' + cat <<'CONFIG' > $out + # https://github.com/drduh/config/blob/master/gpg.conf + # https://www.gnupg.org/documentation/manuals/gnupg/GPG-Options.html + # 'gpg --version' to get capabilities + # Use AES256, 192, or 128 as cipher + personal-cipher-preferences AES256 AES192 AES + # Use SHA512, 384, or 256 as digest + personal-digest-preferences SHA512 SHA384 SHA256 + # Use ZLIB, BZIP2, ZIP, or no compression + personal-compress-preferences ZLIB BZIP2 ZIP Uncompressed + # Default preferences for new keys + default-preference-list SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed + # SHA512 as digest to sign keys + cert-digest-algo SHA512 + # SHA512 as digest for symmetric ops + s2k-digest-algo SHA512 + # AES256 as cipher for symmetric ops + s2k-cipher-algo AES256 + # UTF-8 support for compatibility + charset utf-8 + # No comments in messages + no-comments + # No version in output + no-emit-version + # Disable banner + no-greeting + # Long key id format + keyid-format 0xlong + # Display UID validity + list-options show-uid-validity + verify-options show-uid-validity + # Display all keys and their fingerprints + with-fingerprint + # Display key origins and updates + #with-key-origin + # Cross-certify subkeys are present and valid + require-cross-certification + # Disable caching of passphrase for symmetrical ops + no-symkey-cache + # Output ASCII instead of binary + armor + # Enable smartcard + use-agent + # Disable recipient key ID in messages (breaks Mailvelope) + throw-keyids + # Default key ID to use (helpful with throw-keyids) + #default-key 0xFF00000000000001 + #trusted-key 0xFF00000000000001 + # Group recipient keys (preferred ID last) + #group keygroup = 0xFF00000000000003 0xFF00000000000002 0xFF00000000000001 + # Keyserver URL + #keyserver hkps://keys.openpgp.org + #keyserver hkps://keys.mailvelope.com + #keyserver hkps://keyserver.ubuntu.com:443 + #keyserver hkps://pgpkeys.eu + #keyserver hkps://pgp.circl.lu + #keyserver hkp://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion + # Keyserver proxy + #keyserver-options http-proxy=http://127.0.0.1:8118 + #keyserver-options http-proxy=socks5-hostname://127.0.0.1:9050 + # Enable key retrieval using WKD and DANE + #auto-key-locate wkd,dane,local + #auto-key-retrieve + # Trust delegation mechanism + #trust-model tofu+pgp + # Show expired subkeys + #list-options show-unusable-subkeys + # Verbose output + #verbose + CONFIG + ''; + + dicewareAddress = "localhost"; + dicewarePort = 8080; + viewYubikeyGuide = pkgs.writeShellScriptBin "view-yubikey-guide" '' + viewer="${pkgs.glow}/bin/glow -p" + exec $viewer "${self}/README.md" + ''; + yubikeyGuide = pkgs.symlinkJoin { + name = "yubikey-guide"; + paths = [viewYubikeyGuide]; + }; + dicewareScript = pkgs.writeShellScriptBin "diceware-webapp" '' + viewer="$(type -P xdg-open || true)" + if [ -z "$viewer" ]; then + viewer="chromium" + fi + exec $viewer "http://"${lib.escapeShellArg dicewareAddress}":${toString dicewarePort}/index.html" + ''; + dicewarePage = pkgs.stdenv.mkDerivation { + name = "diceware-page"; + src = pkgs.fetchFromGitHub { + owner = "grempe"; + repo = "diceware"; + rev = "9ef886a2a9699f73ae414e35755fd2edd69983c8"; + sha256 = "44rpK8svPoKx/e/5aj0DpEfDbKuNjroKT4XUBpiOw2g="; + }; + patches = [ + # Include changes published on https://secure.research.vt.edu/diceware/ + (self + /diceware-vt.patch) + ]; + buildPhase = '' + cp -a . $out + ''; + }; + in { + isoImage = { + isoName = mkForce "yubikeyLive.iso"; + # As of writing, zstd-based iso is 1542M, takes ~2mins to + # compress. If you prefer a smaller image and are happy to + # wait, delete the line below, it will default to a + # slower-but-smaller xz (1375M in 8mins as of writing). + squashfsCompression = "zstd"; + + appendToMenuLabel = " YubiKey Live ${self.lastModifiedDate}"; + makeEfiBootable = true; # EFI booting + makeUsbBootable = true; # USB booting + }; + + swapDevices = []; + + boot = { + tmp.cleanOnBoot = true; + kernel.sysctl = {"kernel.unprivileged_bpf_disabled" = 1;}; + }; + + services = { + pcscd.enable = true; + udev.packages = [pkgs.yubikey-personalization]; + # Automatically log in at the virtual consoles. + getty.autologinUser = mkForce my.username; + displayManager = { + autoLogin = { + enable = true; + user = my.username; + }; + }; + # Host the `https://secure.research.vt.edu/diceware/` website offline + nginx = { + enable = true; + virtualHosts."diceware.local" = { + listen = [ + { addr = dicewareAddress; port = dicewarePort; } + ]; + root = "${dicewarePage}"; + }; + }; + }; + + programs = { + ssh.startAgent = false; + gnupg = { + dirmngr.enable = true; + agent = { + enable = true; + enableSSHSupport = true; + }; + }; + }; + + security = { + pam.services.lightdm.text = '' + auth sufficient pam_succeed_if.so user ingroup wheel + ''; + sudo = { + enable = true; + wheelNeedsPassword = false; + }; + }; + + environment.systemPackages = with pkgs; [ + # Tools for backing up keys + paperkey + pgpdump + parted + cryptsetup + + # Yubico's official tools + yubikey-manager + yubikey-manager-qt + yubikey-personalization + yubikey-personalization-gui + yubico-piv-tool + yubioath-flutter + + # Testing + ent + + # Password generation tools + diceware + pwgen + rng-tools + + # Might be useful beyond the scope of the guide + cfssl + pcsctools + tmux + htop + + # This guide itself (run `view-yubikey-guide` on the terminal + # to open it in a non-graphical environment). + yubikeyGuide + dicewareScript + + # PDF and Markdown viewer + zathura + glow + ]; + + # Disable networking so the system is air-gapped + # Comment all of these lines out if you'll need internet access + boot.initrd.network.enable = false; + networking = { + resolvconf.enable = false; + dhcpcd.enable = false; + dhcpcd.allowInterfaces = []; + interfaces = {}; + firewall.enable = true; + useDHCP = false; + useNetworkd = false; + wireless.enable = false; + networkmanager.enable = lib.mkForce false; + }; + + # Unset history so it's never stored Set GNUPGHOME to an + # ephemeral location and configure GPG with the guide + + environment.interactiveShellInit = '' + unset HISTFILE + export GNUPGHOME="/run/user/$(id -u)/gnupg" + if [ ! -d "$GNUPGHOME" ]; then + echo "Creating \$GNUPGHOME…" + install --verbose -m=0700 --directory="$GNUPGHOME" + fi + [ ! -f "$GNUPGHOME/gpg.conf" ] && cp --verbose "${gpgConf}" "$GNUPGHOME/gpg.conf" + [ ! -f "$GNUPGHOME/gpg-agent.conf" ] && cp --verbose ${gpgAgentConf} "$GNUPGHOME/gpg-agent.conf" + echo "\$GNUPGHOME is \"$GNUPGHOME\"" + ''; + + hm.xsession.initExtra = '' + ${pkgs.xorg.xset}/bin/xset r rate 230 30 + [ -z "$(lsusb | grep microdox)" ] && ${pkgs.xorg.setxkbmap}/bin/setxkbmap -option "ctrl:swapcaps" + dwm + ''; + + # Copy the contents of contrib to the home directory, add a + # shortcut to the guide on the desktop, and link to the whole + # repo in the documents folder. + system.activationScripts.yubikeyGuide = let + homeDir = "/home/${my.username}/"; + desktopDir = homeDir + "Desktop/"; + documentsDir = homeDir + "Documents/"; + in '' + mkdir -p ${desktopDir} ${documentsDir} + chown ${my.username} ${homeDir} ${desktopDir} ${documentsDir} + + cp -R ${self}/contrib/* ${homeDir} + ln -sfT ${self} ${documentsDir}/YubiKey-Guide + ''; + system.stateVersion = "24.05"; + } + ) + ]; +} diff --git a/machines/lemptop.nix b/machines/lemptop.nix new file mode 100644 index 0000000..bd24dd7 --- /dev/null +++ b/machines/lemptop.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, modulesPath, ... }: +with lib; +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + # networking.nameservers = ["192.168.2.13"]; + hm.xsession.initExtra = '' + ${pkgs.xorg.xset}/bin/xset r rate 230 30 + [ -z "$(lsusb | grep microdox)" ] && ${pkgs.xorg.setxkbmap}/bin/setxkbmap -option "ctrl:swapcaps" + wal -R + dwm + ''; + + sops.age.keyFile = "${config.hm.xdg.configHome}/sops/age/keys.txt"; + services.tailscale.enable = true; + networking.firewall = { + trustedInterfaces = [ "tailscale0" ]; + allowedUDPPorts = [ config.services.tailscale.port ]; + }; + services.syncthing = { + cert = builtins.toFile "syncthing-cert" '' + -----BEGIN CERTIFICATE----- + MIICHDCCAaKgAwIBAgIIFZKAkMwT4FgwCgYIKoZIzj0EAwIwSjESMBAGA1UEChMJ + U3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdlbmVyYXRlZDESMBAG + A1UEAxMJc3luY3RoaW5nMB4XDTI0MDIxMTAwMDAwMFoXDTQ0MDIwNjAwMDAwMFow + SjESMBAGA1UEChMJU3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdl + bmVyYXRlZDESMBAGA1UEAxMJc3luY3RoaW5nMHYwEAYHKoZIzj0CAQYFK4EEACID + YgAE3vRYSYSQ0ZRPG97Bo9m+0LMVGGiJ3/2I+QBaWHe+pDMh3nB7cOV04z9s2q7z + MNjIsWYBPVUxIKFdIMfFN4svH2YpDt1Ps4AdfdPVUv/EsCIoyrtAc13Y64GJSKtF + GFKao1UwUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG + AQUFBwMCMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJc3luY3RoaW5nMAoGCCqG + SM49BAMCA2gAMGUCMQDgWiqyibzhjXcbVVj0ZR8uITLTrZrrpUT13iiL674JK7uV + DRY28bmdBaZXrOPvOgICMDq8lNeqdQ/jq5CCLe+KJZdtJ/E4XWtls3av09XP+DXK + BtFKP2jvlC7HHtZMKManKQ== + -----END CERTIFICATE----- + ''; + }; + my.shell = pkgs.nushell; + environment.shells = [pkgs.bashInteractive pkgs.zsh pkgs.nushell]; + environment.pathsToLink = [ "/share/zsh" ]; + programs.zsh.enable = true; + + documentation.dev.enable = true; + networking.hostName = "lemptop"; + networking.networkmanager.enable = true; + + programs.slock.enable = true; + services.xserver.enable = true; + services.xserver.displayManager.startx.enable = true; + + services.pcscd.enable = true; + security.pam.services = { + login.u2fAuth = true; + sudo.u2fAuth = true; + }; + services.udev.packages = [ pkgs.yubikey-personalization ]; + services.udev.extraRules = '' + # Yubico Yubikey II + ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0010|0110|0111|0114|0116|0401|0403|0405|0407|0410", \ + ENV{ID_SECURITY_TOKEN}="1" + + KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0113|0114|0115|0116|0120|0200|0402|0403|0406|0407|0410", TAG+="uaccess" + ''; + + virtualisation.docker.enable = true; + programs.nix-ld.enable = true; + + hardware.pulseaudio.enable = false; + security.rtkit.enable = true; + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + wireplumber.enable = true; + }; + hardware.bluetooth.enable = true; + services.blueman.enable = true; + hardware.keyboard.qmk.enable = true; + hardware.system76.enableAll = true; + services.xserver.videoDrivers = [ "i915" ]; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + # boot.kernelPackages = pkgs.linuxPackages_latest; + boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" "usb_storage" "sd_mod" "sdhci_pci" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.kernelParams = [ "i915.force_probe=46a8" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/08ed8d2d-38be-4019-9a84-dbded2cd0649"; + fsType = "ext4"; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/655D-8467"; + fsType = "vfat"; + }; + + swapDevices = [ ]; + + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.wlp0s20f3.useDHCP = lib.mkDefault true; + + system.stateVersion = "23.05"; + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/machines/pump-netboot.nix b/machines/pump-netboot.nix new file mode 100644 index 0000000..5125440 --- /dev/null +++ b/machines/pump-netboot.nix @@ -0,0 +1,64 @@ +{ config, pkgs, lib, modulesPath, ... }: with lib; { + imports = [ + (modulesPath + "/installer/netboot/netboot-minimal.nix") + ]; + services.getty.autologinUser = lib.mkForce "root"; + users.users.root.openssh.authorizedKeys.keys = my.sshKeys; + + services.openssh.enable = true; + sops.age.keyFile = "${config.my.home}/sops/age/keys.txt"; + services.syncthing = { + cert = builtins.toFile "syncthing-cert" '' + -----BEGIN CERTIFICATE----- + MIICGzCCAaKgAwIBAgIIRGieK4FEhD0wCgYIKoZIzj0EAwIwSjESMBAGA1UEChMJ + U3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdlbmVyYXRlZDESMBAG + A1UEAxMJc3luY3RoaW5nMB4XDTI0MDIxMTAwMDAwMFoXDTQ0MDIwNjAwMDAwMFow + SjESMBAGA1UEChMJU3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdl + bmVyYXRlZDESMBAGA1UEAxMJc3luY3RoaW5nMHYwEAYHKoZIzj0CAQYFK4EEACID + YgAEH/4taBY2lcNBXZCxNOklTahIlhN+ypYMOqw7LNlKZVdv7JzRR67akp/F99mF + PA+IB1CQoPOTXUjnhm84Tob/8MoUA1jM5uspclxXG95eMw2J7E7svBEGJA2RsEQE + dsU3o1UwUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG + AQUFBwMCMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJc3luY3RoaW5nMAoGCCqG + SM49BAMCA2cAMGQCMCP0Ro0ZjGfQf9R3x3neKZzrJxkD11ZK9NBNTaeWAKbrhkjp + qqW9uTONfIOXZmgtrQIwf6Ykr934UA5I6Rk8qNV8d082n3FNMw1NgK9GmUv2XMZ5 + eOpDAYJrhLx5jb7d3L4/ + -----END CERTIFICATE----- + ''; + }; + + networking.hostName = "pump"; + networking.domain = "vinkies.net"; + + boot.supportedFilesystems = [ "zfs" ]; + boot.zfs.forceImportRoot = false; + networking.hostId = "7da046cb"; + + boot.initrd.availableKernelModules = [ "e1000e" ]; + boot.initrd.network = { + enable = true; + ssh = { + enable = true; # Use a different port than your usual SSH port! + port = 2222; + hostKeys = [ + (/. + "${config.my.home}" + "/.ssh/initrd/key") + ]; + authorizedKeys = my.sshKeys; + }; + postCommands = '' + echo "zfs load-key -a; killall zfs" >> /root/.profile + ''; + }; + + fileSystems."/data" = + { device = "zpool/data"; + fsType = "zfs"; + neededForBoot = true; + }; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + system.stateVersion = "24.05"; + nix.extraOptions = mkForce '' + experimental-features = nix-command flakes + ''; + nix.package = mkForce pkgs.nixVersions.stable; +} diff --git a/machines/serber.nix b/machines/serber.nix new file mode 100644 index 0000000..6a0f045 --- /dev/null +++ b/machines/serber.nix @@ -0,0 +1,63 @@ +{ modulesPath, lib, ... }: { + imports = [ (modulesPath + "/profiles/qemu-guest.nix") ]; + + sops.age.sshKeyPaths = ["/etc/ssh/ssh_host_ed25519_key"]; + + services.syncthing.enable = false; + + environment.etc."resolv.conf".source = lib.mkForce "/run/systemd/resolve/resolv.conf"; + services.resolved = { + enable = true; + dnssec = "true"; + domains = [ "~." ]; + dnsovertls = "true"; + }; + + # This file was populated at runtime with the networking + # details gathered from the active system. + networking = { + nameservers = [ + "1.1.1.1" + "1.0.0.1" + "2606:4700:4700::1111" + "2606:4700:4700::1001" + ]; + defaultGateway = "172.31.1.1"; + defaultGateway6 = { + address = "fe80::1"; + interface = "eth0"; + }; + dhcpcd.enable = false; + usePredictableInterfaceNames = lib.mkForce false; + interfaces = { + eth0 = { + ipv4.addresses = [ + { address="65.109.143.65"; prefixLength=32; } + ]; + ipv6.addresses = [ + { address="2a01:4f9:c012:ccc2::1"; prefixLength=64; } + { address="fe80::9400:3ff:fe46:c7bc"; prefixLength=64; } + ]; + ipv4.routes = [ { address = "172.31.1.1"; prefixLength = 32; } ]; + ipv6.routes = [ { address = "fe80::1"; prefixLength = 128; } ]; + }; + }; + }; + services.udev.extraRules = '' + ATTR{address}=="96:00:03:46:c7:bc", NAME="eth0" + + ''; + + boot.loader.grub.device = "/dev/sda"; + boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ]; + boot.initrd.kernelModules = [ "nvme" ]; + fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; + + boot.tmp.cleanOnBoot = true; + zramSwap.enable = true; + networking.hostName = "serber"; + networking.domain = ""; + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keys = [''sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIPZHOBNQdo5oBnQ8f147QtelhLmYItiruoNfoHF89qrJAAAABHNzaDo='' ]; + system.stateVersion = "23.11"; +} diff --git a/machines/vm-aarch64.nix b/machines/vm-aarch64.nix new file mode 100644 index 0000000..42c9d07 --- /dev/null +++ b/machines/vm-aarch64.nix @@ -0,0 +1,138 @@ +# https://github.com/mitchellh/nixos-config/blob/main/machines/vm-aarch64-prl.nix +{ + self, + config, + pkgs, + lib, + ... +}: +with lib; { + imports = [ + (self + "/profiles/vmware-guest.nix") + ]; + system.stateVersion = "24.05"; + virtualisation.vmware.guest.enable = true; + virtualisation.docker.enable = false; + virtualisation.docker.rootless = { + enable = true; + setSocketVariable = true; + daemon.settings = { + hosts = ["unix:///run/user/${toString config.my.uid}/docker.sock" "tcp://127.0.0.1:2376"]; + }; + }; + systemd.user.services.docker.serviceConfig.Environment = "DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS=\"-p 0.0.0.0:2376:2376/tcp\""; + systemd.user.services.docker.serviceConfig.ExecStart = let + cfg = config.virtualisation.docker.rootless; + in + mkForce "${cfg.package}/bin/dockerd-rootless --config-file=${(pkgs.formats.json {}).generate "daemon.json" cfg.daemon.settings}"; + networking.hostName = "vm-aarch64"; + programs.nix-ld.enable = true; + + hm.xsession.initExtra = '' + ${pkgs.xorg.xset}/bin/xset r rate 230 30 + ${pkgs.open-vm-tools}/bin/vmware-user-suid-wrapper + wal -R + dwm + ''; + hm.services.ssh-agent.enable = true; + environment.variables = { + WEBKIT_DISABLE_COMPOSITING_MODE = 1; + }; + environment.systemPackages = with pkgs; [ + kubernetes-helm + (azure-cli.withExtensions [azure-cli.extensions.aks-preview azure-cli.extensions.account]) + awscli2 + (google-cloud-sdk.withExtraComponents (with google-cloud-sdk.components; [ + gke-gcloud-auth-plugin + ])) + k9s + kubectl + krew + kubelogin + just + (ffmpeg.override { + withXcb = true; + }) + mpv + ]; + + services.pcscd.enable = true; + sops.age.keyFile = "${config.hm.xdg.configHome}/sops/age/keys.txt"; + my.shell = pkgs.nushell; + + environment.shells = [pkgs.bashInteractive pkgs.zsh pkgs.nushell]; + environment.pathsToLink = ["/share/zsh"]; + programs.zsh.enable = true; + + services.openssh.enable = true; + services.openssh.settings.PasswordAuthentication = true; + services.openssh.settings.PermitRootLogin = "yes"; + + # Setup qemu so we can run x86_64 binaries + boot.binfmt.emulatedSystems = ["x86_64-linux"]; + + # Disable the default module and import our override. We have + # customizations to make this work on aarch64. + disabledModules = ["virtualisation/vmware-guest.nix"]; + + # Interface is this on M1 + networking.interfaces.ens160.useDHCP = true; + + # Lots of stuff that uses aarch64 that claims doesn't work, but actually works. + nixpkgs.config.allowUnfree = true; + nixpkgs.config.allowUnsupportedSystem = true; + + # This works through our custom module imported above + # virtualisation.vmware.guest.enable = true; + + # Share our host filesystem + # fileSystems."/host" = { + # fsType = "fuse./run/current-system/sw/bin/vmhgfs-fuse"; + # device = ".host:/"; + # options = [ + # "umask=22" + # "uid=1000" + # "gid=1000" + # "allow_other" + # "auto_unmount" + # "defaults" + # ]; + # }; + + # Use the systemd-boot EFI boot loader. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + # VMware, Parallels both only support this being 0 otherwise you see + # "error switching console mode" on boot. + boot.loader.systemd-boot.consoleMode = "0"; + + # Hardware + boot.initrd.availableKernelModules = ["xhci_pci" "nvme" "sr_mod"]; + boot.initrd.kernelModules = []; + boot.kernelModules = []; + boot.extraModulePackages = []; + + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/boot"; + fsType = "vfat"; + options = ["fmask=0022" "dmask=0022"]; + }; + + swapDevices = []; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.ens160.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + nix.settings.trusted-users = [my.username]; +} diff --git a/machines/work.nix b/machines/work.nix new file mode 100644 index 0000000..b1d9127 --- /dev/null +++ b/machines/work.nix @@ -0,0 +1,134 @@ +{ self, config, pkgs, lib, ... }: with lib; { + options = { + virtualisation = mkSinkUndeclaredOptions {}; + programs = { + virt-manager = mkSinkUndeclaredOptions {}; + steam = mkSinkUndeclaredOptions {}; + }; + hardware = mkSinkUndeclaredOptions {}; + services = { + resolved = mkSinkUndeclaredOptions {}; + }; + security = { + sudo.wheelNeedsPassword = mkSinkUndeclaredOptions {}; + }; + systemd = mkSinkUndeclaredOptions {}; + users.users = mkOption { + type = types.attrsOf (types.submodule ({...}: { + options = { + extraGroups = mkSinkUndeclaredOptions {}; + isNormalUser = mkSinkUndeclaredOptions {}; + }; + config = { + home = "/Users/${my.username}"; + }; + })); + }; + }; + config = { + fonts = { + packages = with pkgs; [ + nerd-fonts.fira-code + nerd-fonts.jetbrains-mono + ]; + }; + users.users.root.home = mkForce "/var/root"; + # List packages installed in system profile. To search by name, run: + # $ nix-env -qaP | grep wget + environment.systemPackages = + [ + pkgs.nushell + pkgs.zsh + pkgs.bashInteractive + pkgs.just + pkgs.git + ]; + hm = { + # services.ssh-agent.enable = true; + programs.git.enable = mkForce false; + home = { + sessionPath = [ + "/opt/homebrew/bin" + ]; + # file.".config/aerospace".source = config.lib.meta.mkMutableSymlink /mut/aerospace; + # file."Library/KeyBindings/DefaultKeyBinding.dict".source = config.lib.meta.mkMutableSymlink /mut/DefaultKeyBinding.dict; + file."gpg-agent.conf" = { + text = '' + pinentry-program /opt/homebrew/bin/pinentry-mac + enable-ssh-support + ttyname $GPG_TTY + default-cache-ttl 60 + max-cache-ttl 120 + ''; + target = ".gnupg/gpg-agent.conf"; + }; + }; + }; + + networking.hostName = "work"; + sops.age.keyFile = "${config.hm.xdg.configHome}/sops/age/keys.txt"; + homebrew = { + enable = true; + brews = [ + "pinentry-mac" + ]; + casks = [ + "docker" + "intellij-idea-ce" + "visual-studio-code" + "zed" + ]; + masApps = { + tailscale = 1475387142; + slack = 803453959; + }; + }; + services.openssh.enable = false; + services.syncthing = { + cert = builtins.toFile "syncthing-cert" '' + -----BEGIN CERTIFICATE----- + MIICHDCCAaKgAwIBAgIICf/IfhEqojIwCgYIKoZIzj0EAwIwSjESMBAGA1UEChMJ + U3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdlbmVyYXRlZDESMBAG + A1UEAxMJc3luY3RoaW5nMB4XDTI0MDIwOTAwMDAwMFoXDTQ0MDIwNDAwMDAwMFow + SjESMBAGA1UEChMJU3luY3RoaW5nMSAwHgYDVQQLExdBdXRvbWF0aWNhbGx5IEdl + bmVyYXRlZDESMBAGA1UEAxMJc3luY3RoaW5nMHYwEAYHKoZIzj0CAQYFK4EEACID + YgAEB3N4kE5gTlpCt8W/ocQQbDZMvIzmNghcl0tsc+EVPXCTnpinIB48jOxGNkPr + rm0o3EEPrI8O+cJqSydeyeSVMKYCjNswP6LiYNWaWua+SXjz25FurJxV21LXYMhc + 1egPo1UwUzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG + AQUFBwMCMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJc3luY3RoaW5nMAoGCCqG + SM49BAMCA2gAMGUCMEOYa4HZKLy4WimWlAIpXU/joYvpIPS3dJP50VQIkKFj/eL8 + p8+rG7+7P03W7J4E6AIxANp5CxwCtTlh1a1+8Kdvfc7ZvFuMwPlM3d8EFk9y9aRZ + jurkqKKyl7EUOk0ufvUaQQ== + -----END CERTIFICATE----- + ''; + }; + # Auto upgrade nix package and the daemon service. + services.nix-daemon.enable = true; + # nix.package = pkgs.nix; + + # Necessary for using flakes on this system. + nix.settings.experimental-features = "nix-command flakes"; + + nix.extraOptions = ''extra-platforms = x86_64-darwin aarch64-darwin ''; + + nix.linux-builder.enable = true; + nix.settings.trusted-users = [ "@admin" "@ivi" ]; + + # Set Git commit hash for darwin-version. + system.configurationRevision = self.rev or self.dirtyRev or null; + + # Used for backwards compatibility, please read the changelog before changing. + # $ darwin-rebuild changelog + system.stateVersion = 4; + + # The platform the configuration will be used on. + nixpkgs.hostPlatform = "aarch64-darwin"; + my.shell = pkgs.nushell; + + environment.shells = [pkgs.bashInteractive pkgs.zsh pkgs.nushell]; + environment.pathsToLink = [ "/share/zsh" ]; + environment.variables = { + SLACK_NO_AUTO_UPDATES = "1"; + }; + }; +} diff --git a/mut/DefaultKeyBinding.dict b/mut/DefaultKeyBinding.dict new file mode 100644 index 0000000..8d39f47 --- /dev/null +++ b/mut/DefaultKeyBinding.dict @@ -0,0 +1,69 @@ +{ +/* Keybindings for emacs emulation. Compiled by Jacob Rus. + * + * This is a pretty good set, especially considering that many emacs bindings + * such as C-o, C-a, C-e, C-k, C-y, C-v, C-f, C-b, C-p, C-n, C-t, and + * perhaps a few more, are already built into the system. + * + * BEWARE: + * This file uses the Option key as a meta key. This has the side-effect + * of overriding Mac OS keybindings for the option key, which generally + * make common symbols and non-english letters. + */ + + /* Ctrl shortcuts */ + "^l" = "centerSelectionInVisibleArea:"; /* C-l Recenter */ + // "^/" = "undo:"; /* C-/ Undo */ + // "^_" = "undo:"; /* C-_ Undo */ + "^ " = "setMark:"; /* C-Spc Set mark */ + "^\@" = "setMark:"; /* C-@ Set mark */ + "^w" = "deleteToMark:"; /* C-w Delete to mark */ + + + /* Incremental search. */ +/* Uncomment these lines If Incremental Search IM is installed */ +/* "^s" = "ISIM_incrementalSearch:"; /* C-s Incremental search */ +/* "^r" = "ISIM_reverseIncrementalSearch:"; /* C-r Reverse incremental search */ +/* "^g" = "abort:"; /* C-g Abort */ + + + /* Meta shortcuts */ + "~f" = "moveWordForward:"; /* M-f Move forward word */ + "~b" = "moveWordBackward:"; /* M-b Move backward word */ + "~<" = "moveToBeginningOfDocument:"; /* M-< Move to beginning of document */ + "~>" = "moveToEndOfDocument:"; /* M-> Move to end of document */ + "~v" = "pageUp:"; /* M-v Page Up */ + "~/" = "complete:"; /* M-/ Complete */ + "~c" = ( "capitalizeWord:", /* M-c Capitalize */ + "moveForward:", + "moveForward:"); + "~u" = ( "uppercaseWord:", /* M-u Uppercase */ + "moveForward:", + "moveForward:"); + "~l" = ( "lowercaseWord:", /* M-l Lowercase */ + "moveForward:", + "moveForward:"); + "~d" = "deleteWordForward:"; /* M-d Delete word forward */ + "^w" = "deleteWordBackward:"; /* C-w Delete word backward */ + "^~h" = "deleteWordBackward:"; /* M-C-h Delete word backward */ + "~\U007F" = "deleteWordBackward:"; /* M-Bksp Delete word backward */ + "~t" = "transposeWords:"; /* M-t Transpose words */ + // "~\@" = ( "setMark:", /* M-@ Mark word */ + // "moveWordForward:", + // "swapWithMark"); + // "~h" = ( "setMark:", /* M-h Mark paragraph */ + // "moveToEndOfParagraph:", + // "swapWithMark"); + + /* C-x shortcuts */ + // "^x" = { + // "u" = "undo:"; /* C-x u Undo */ + // "k" = "performClose:"; /* C-x k Close */ + // "^f" = "openDocument:"; /* C-x C-f Open (find file) */ + // "^x" = "swapWithMark:"; /* C-x C-x Swap with mark */ + // "^m" = "selectToMark:"; /* C-x C-m Select to mark*/ + // "^s" = "saveDocument:"; /* C-x C-s Save */ + // "^w" = "saveDocumentAs:"; /* C-x C-w Save as */ + // }; + +} diff --git a/mut/aerospace/aerospace.toml b/mut/aerospace/aerospace.toml new file mode 100644 index 0000000..aacd535 --- /dev/null +++ b/mut/aerospace/aerospace.toml @@ -0,0 +1,183 @@ +# Place a copy of this config to ~/.aerospace.toml +# After that, you can edit ~/.aerospace.toml to your liking + +# You can use it to add commands that run after login to macOS user session. +# 'start-at-login' needs to be 'true' for 'after-login-command' to work +# Available commands: https://nikitabobko.github.io/AeroSpace/commands +after-login-command = [] + +# You can use it to add commands that run after AeroSpace startup. +# 'after-startup-command' is run after 'after-login-command' +# Available commands : https://nikitabobko.github.io/AeroSpace/commands +after-startup-command = [] + +# Start AeroSpace at login +start-at-login = true + +# Normalizations. See: https://nikitabobko.github.io/AeroSpace/guide#normalization +enable-normalization-flatten-containers = true +enable-normalization-opposite-orientation-for-nested-containers = true + +# See: https://nikitabobko.github.io/AeroSpace/guide#layouts +# The 'accordion-padding' specifies the size of accordion padding +# You can set 0 to disable the padding feature +accordion-padding = 100 + +# Possible values: tiles|accordion +default-root-container-layout = 'accordion' + +# Possible values: horizontal|vertical|auto +# 'auto' means: wide monitor (anything wider than high) gets horizontal orientation, +# tall monitor (anything higher than wide) gets vertical orientation +default-root-container-orientation = 'auto' + +# Mouse follows focus when focused monitor changes +# Drop it from your config, if you don't like this behavior +# See https://nikitabobko.github.io/AeroSpace/guide#on-focus-changed-callbacks +# See https://nikitabobko.github.io/AeroSpace/commands#move-mouse +# Fallback value (if you omit the key): on-focused-monitor-changed = [] +on-focused-monitor-changed = ['move-mouse monitor-lazy-center'] + +# You can effectively turn off macOS "Hide application" (cmd-h) feature by toggling this flag +# Useful if you don't use this macOS feature, but accidentally hit cmd-h or cmd-alt-h key +# Also see: https://nikitabobko.github.io/AeroSpace/goodies#disable-hide-app +automatically-unhide-macos-hidden-apps = false + +# Possible values: (qwerty|dvorak) +# See https://nikitabobko.github.io/AeroSpace/guide#key-mapping +[key-mapping] + preset = 'qwerty' + +# Gaps between windows (inner-*) and between monitor edges (outer-*). +# Possible values: +# - Constant: gaps.outer.top = 8 +# - Per monitor: gaps.outer.top = [{ monitor.main = 16 }, { monitor."some-pattern" = 32 }, 24] +# In this example, 24 is a default value when there is no match. +# Monitor pattern is the same as for 'workspace-to-monitor-force-assignment'. +# See: +# https://nikitabobko.github.io/AeroSpace/guide#assign-workspaces-to-monitors +[gaps] + inner.horizontal = 0 + inner.vertical = 0 + outer.left = 0 + outer.bottom = 0 + outer.top = 0 + outer.right = 0 + +# 'main' binding mode declaration +# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes +# 'main' binding mode must be always presented +# Fallback value (if you omit the key): mode.main.binding = {} +[mode.main.binding] + + # All possible keys: + # - Letters. a, b, c, ..., z + # - Numbers. 0, 1, 2, ..., 9 + # - Keypad numbers. keypad0, keypad1, keypad2, ..., keypad9 + # - F-keys. f1, f2, ..., f20 + # - Special keys. minus, equal, period, comma, slash, backslash, quote, semicolon, + # backtick, leftSquareBracket, rightSquareBracket, space, enter, esc, + # backspace, tab + # - Keypad special. keypadClear, keypadDecimalMark, keypadDivide, keypadEnter, keypadEqual, + # keypadMinus, keypadMultiply, keypadPlus + # - Arrows. left, down, up, right + + # All possible modifiers: cmd, alt, ctrl, shift + + # All possible commands: https://nikitabobko.github.io/AeroSpace/commands + + # See: https://nikitabobko.github.io/AeroSpace/commands#exec-and-forget + # You can uncomment the following lines to open up terminal with alt + enter shortcut + # (like in i3) + cmd-shift-d = 'exec-and-forget source /etc/profile; PATH="$HOME/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" passmenu' + cmd-f = 'fullscreen' + cmd-d = 'exec-and-forget source /etc/profile; PATH="$HOME/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" dmenu' + cmd-enter = '''exec-and-forget osascript -e ' + tell application "System Events" + if exists application process "Ghostty" then + tell application process "Ghostty" + click menu item "New Window" of menu "File" of menu bar 1 + end tell + else + tell application "Ghostty" to activate + end if + end tell' + ''' + + cmd-w = '''exec-and-forget osascript -e ' + tell application "System Events" + if exists application process "Google Chrome" then + tell application process "Google Chrome" + click menu item "New Window" of menu "File" of menu bar 1 + end tell + else + tell application "Google Chrome" to activate + end if + end tell' + ''' + + # See: https://nikitabobko.github.io/AeroSpace/commands#layout + cmd-slash = 'layout tiles horizontal vertical' + cmd-comma = 'layout accordion horizontal vertical' + + # See: https://nikitabobko.github.io/AeroSpace/commands#focus + cmd-j = 'focus right' + cmd-k = 'focus left' + + # See: https://nikitabobko.github.io/AeroSpace/commands#move + cmd-shift-j = 'move right' + cmd-shift-k = 'move left' + + # See: https://nikitabobko.github.io/AeroSpace/commands#resize + cmd-minus = 'resize smart -50' + cmd-equal = 'resize smart +50' + + # See: https://nikitabobko.github.io/AeroSpace/commands#workspace + cmd-1 = 'workspace 1' + cmd-2 = 'workspace 2' + cmd-3 = 'workspace 3' + cmd-4 = 'workspace 4' + cmd-5 = 'workspace 5' + cmd-6 = 'workspace 6' + cmd-7 = 'workspace 7' + cmd-8 = 'workspace 8' + cmd-9 = 'workspace 9' + + # See: https://nikitabobko.github.io/AeroSpace/commands#move-node-to-workspace + cmd-shift-1 = 'move-node-to-workspace 1' + cmd-shift-2 = 'move-node-to-workspace 2' + cmd-shift-3 = 'move-node-to-workspace 3' + cmd-shift-4 = 'move-node-to-workspace 4' + cmd-shift-5 = 'move-node-to-workspace 5' + cmd-shift-6 = 'move-node-to-workspace 6' + cmd-shift-7 = 'move-node-to-workspace 7' + cmd-shift-8 = 'move-node-to-workspace 8' + cmd-shift-9 = 'move-node-to-workspace 9' + + # See: https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth + cmd-semicolon = 'workspace-back-and-forth' + # See: https://nikitabobko.github.io/AeroSpace/commands#move-workspace-to-monitor + # cmd-shift-tab = 'move-workspace-to-monitor --wrap-around next' + + # See: https://nikitabobko.github.io/AeroSpace/commands#mode + cmd-shift-semicolon = 'mode service' + +# 'service' binding mode declaration. +# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes +[mode.service.binding] + esc = ['reload-config', 'mode main'] + r = ['flatten-workspace-tree', 'mode main'] # reset layout + f = ['layout floating tiling', 'mode main'] # Toggle between floating and tiling layout + backspace = ['close-all-windows-but-current', 'mode main'] + + # sticky is not yet supported https://github.com/nikitabobko/AeroSpace/issues/2 + #s = ['layout sticky tiling', 'mode main'] + + alt-shift-h = ['join-with left', 'mode main'] + alt-shift-j = ['join-with down', 'mode main'] + alt-shift-k = ['join-with up', 'mode main'] + alt-shift-l = ['join-with right', 'mode main'] + + down = 'volume down' + up = 'volume up' + shift-down = ['volume set 0', 'mode main'] diff --git a/mut/bin/checkout b/mut/bin/checkout new file mode 100755 index 0000000..9619085 --- /dev/null +++ b/mut/bin/checkout @@ -0,0 +1,69 @@ +#!/bin/sh +error () { + echo "$1" + exit 1 +} + +. <(pass show work/env) +DEST_DIR="" +case "${@}" in + az|"az "*) + shift + LIST_PROJECTS="/_apis/projects?api-version=7.1-preview.4" + AUTH_HEADER="Authorization: Basic $(echo -n ":$GIT_PASS" | base64)" + LIST_REPOSITORIES="/_apis/git/repositories?api-version=7.1-preview.1" + GIT_DIR="$HOME/projects/" + if [ ! -d $GIT_DIR ]; then + mkdir -p $GIT_DIR + fi + MAX_REPOS=20 + + echo "curl -s -H \"$AUTH_HEADER\" $WORK_AZDO_GIT_ORG_URL$LIST_PROJECTS" + PROJECT=$(curl -s -H "$AUTH_HEADER" $WORK_AZDO_GIT_ORG_URL$LIST_PROJECTS \ + | jq ' + .value[].name + ' \ + | xargs -I{} bash -c " + curl -s -H '$AUTH_HEADER' $WORK_AZDO_GIT_ORG_URL/{}$LIST_REPOSITORIES \ + | jq ' + .value[].name + ' \ + | awk '{ gsub(/\"/, \"\", \$1); printf \"{}/_git/%s\\n\", \$1 }' + " \ + | fzf) + + DEST_DIR="$GIT_DIR/$(echo $PROJECT | cut -d '/' -f3)" + if [ ! -d $DEST_DIR ] + then + git clone --bare $WORK_AZDO_GIT_ORG_URL/$PROJECT $DEST_DIR + fi + ;; + gh|"gh "*) + shift + repo=$(gh repo list --json owner,name -q '.[] | "\(.owner.login)/\(.name)"' | fzf --print-query -1) + GIT_DIR="$HOME/projects" + if [ ! -d $GIT_DIR ]; then + mkdir -p $GIT_DIR + fi + + if [[ "$(echo "$repo" | wc -l)" -ne 1 ]]; then + echo "Fetching my repo" + repo="$(echo "$repo" | tail -n1)" + fi + + DEST_DIR="$GIT_DIR/$(echo $repo | cut -d '/' -f2)" + if [ ! -d $DEST_DIR ] + then + gh repo clone $repo $DEST_DIR -- --bare + fi + ;; + *) + error "Don't know how to fetch this" + ;; +esac + +if ! [[ -z "$DEST_DIR" ]]; then + cd $DEST_DIR + git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + $EDITOR "$DEST_DIR" +fi diff --git a/mut/bin/choose b/mut/bin/choose new file mode 100644 index 0000000..b6e5633 --- /dev/null +++ b/mut/bin/choose @@ -0,0 +1,3 @@ +#!/bin/bash +fifo_name="/tmp/choose" + diff --git a/mut/bin/compile b/mut/bin/compile new file mode 100755 index 0000000..7208b36 --- /dev/null +++ b/mut/bin/compile @@ -0,0 +1,40 @@ +#!/bin/sh +echo " Compiliiing ${@}" + +error () { + echo "$1" + exit 1 +} + +case "${@}" in + racket*) + shift + echo " \-> racket -l errortrace -t ${@}" + racket -l errortrace -t ${@} + ;; + ansible-lint*) + shift + echo " \-> ansible-lint --profile production --write=all -qq --nocolor" + ansible-lint --profile production --write=all -qq --nocolor ${@} + ;; + ansible-playbook*) + shift + echo " \-> ansible-playbook -e@<(pass)" + ansible-playbook -b -e "{\"ansible_become_pass\":\"$PASSWORD\"}" ${@} + ;; + awx*) + echo " \-> awx" + shift + awx "$@" | filter-ansi + ;; + helm\ lint*) + shift + shift + echo " \-> helm lint --set cluster=debug-cluster --strict --quiet --with-subcharts ${@}" + helm lint --set cluster=debug-cluster --strict --quiet --with-subcharts ${@} | sed -u -E -e "s@$(basename ${PWD})/|error during tpl function execution for \".*\"@@g" + ;; + *) + echo " \-> ${@}" + ${@} + ;; +esac diff --git a/mut/bin/desktop-open-pipe b/mut/bin/desktop-open-pipe new file mode 100755 index 0000000..338929d --- /dev/null +++ b/mut/bin/desktop-open-pipe @@ -0,0 +1,7 @@ +#!/usr/bin/env nu +echo listening for open commands +loop { + let line = nc -l 127.0.0.1 1994 + echo $line | save --append /tmp/debuglogs + try { ^open $line } +} diff --git a/mut/bin/dmenu b/mut/bin/dmenu new file mode 100755 index 0000000..2442c2c --- /dev/null +++ b/mut/bin/dmenu @@ -0,0 +1,2 @@ +#!/bin/sh +/Applications/dmenu-mac.app/Contents/MacOS/dmenu-mac "$@" diff --git a/mut/bin/filter-ansi b/mut/bin/filter-ansi new file mode 100755 index 0000000..a45092e --- /dev/null +++ b/mut/bin/filter-ansi @@ -0,0 +1,2 @@ +#!/bin/sh +cat -u - | sed -u -E -e 's/\x1b\[[0-9;]*[mGKHF]|\r//g' diff --git a/mut/bin/get-sshables b/mut/bin/get-sshables new file mode 100755 index 0000000..fa88f7c --- /dev/null +++ b/mut/bin/get-sshables @@ -0,0 +1,7 @@ +#!/bin/sh +set -euxo pipefail +[[ -d ~/sshables ]] || mkdir -p ~/sshables + +for cluster in $(kubectl config get-clusters | tail -n +2); do + [[ -f ~/sshables/$cluster ]] || { echo $cluster; kubectl --context $cluster get nodes -oname > ~/sshables/$cluster; } +done diff --git a/mut/bin/hs b/mut/bin/hs new file mode 120000 index 0000000..c55f613 --- /dev/null +++ b/mut/bin/hs @@ -0,0 +1 @@ +/Applications/Hammerspoon.app/Contents/Frameworks/hs/hs
\ No newline at end of file diff --git a/mut/bin/kubeconfig-merge b/mut/bin/kubeconfig-merge new file mode 100755 index 0000000..2567b60 --- /dev/null +++ b/mut/bin/kubeconfig-merge @@ -0,0 +1,3 @@ +#!/bin/sh +cp $HOME/.kube/config /tmp/.kube_config +KUBECONFIG=$1:/tmp/.kube_config kubectl config view --flatten > $HOME/.kube/config diff --git a/mut/bin/lfub b/mut/bin/lfub new file mode 100755 index 0000000..50bae0d --- /dev/null +++ b/mut/bin/lfub @@ -0,0 +1,28 @@ +#!/bin/sh + +# This is a wrapper script for lb that allows it to create image previews with +# ueberzug. This works in concert with the lf configuration file and the +# lf-cleaner script. +set -e + +cleanup() { + exec 3>&- + rm "$FIFO_UEBERZUG" +} + +if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ]; then + lf "$@" +else + [ ! -d "$HOME/.cache/lf" ] && mkdir -p "$HOME/.cache/lf" + export FIFO_UEBERZUG="$HOME/.cache/lf/ueberzug-$$" + mkfifo "$FIFO_UEBERZUG" + ueberzug layer -s <"$FIFO_UEBERZUG" -p json & + exec 3>"$FIFO_UEBERZUG" + sys="$(uname)" + if ! [ "$sys" = "Darwin" ]; then + trap cleanup HUP INT QUIT TERM PWR EXIT + else + trap cleanup HUP INT QUIT TERM EXIT + fi + lf "$@" 3>&- +fi diff --git a/mut/bin/linkhandler b/mut/bin/linkhandler new file mode 100755 index 0000000..4dc1cf3 --- /dev/null +++ b/mut/bin/linkhandler @@ -0,0 +1,37 @@ +#!/bin/sh + +# Feed script a url or file location. +# If an image, it will view in sxiv, +# if a video or gif, it will view in mpv +# if a music file or pdf, it will download, +# otherwise it opens link in browser. + +if command -v pbpaste >/dev/null; +then + paste=pbpaste +else + paste="xclip -o" +fi + +if [ -z $BROWSER ]; then + BROWSER=open +fi + +if [ -z "$1" ]; then + url="$($paste)" +else + url="$1" +fi + +case "$url" in + *mkv|*webm|*mp4|*youtube.com/watch*|*youtube.com/playlist*|*youtube.com/shorts*|*youtu.be*|*hooktube.com*|*bitchute.com*|*videos.lukesmith.xyz*|*odysee.com*) + nohup mpv -quiet "$url" >/dev/null 2>&1 ;; + *png|*jpg|*jpe|*jpeg|*gif) + curl -sL "$url" > "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" && sxiv -a "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" ;; + *pdf|*cbz|*cbr) + curl -sL "$url" > "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" && zathura "/tmp/$(echo "$url" | sed "s/.*\///;s/%20/ /g")" ;; + *mp3|*flac|*opus|*mp3?source*) + qndl "$url" 'curl -LO' ;; + *) + [ -f "$url" ] && nohup "$TERMINAL" -e "$EDITOR" "$url" >/dev/null 2>&1 || nohup "$BROWSER" "$url" +esac diff --git a/mut/bin/macos b/mut/bin/macos new file mode 100755 index 0000000..ba8e43c --- /dev/null +++ b/mut/bin/macos @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +args="" +if [ -n "$1" ]; then + args="$(printf " %q" "${@}")" +fi +TERM=xterm-256color ssh -i ~/.ssh/macos mike@192.168.122.75 $args diff --git a/mut/bin/macosfs b/mut/bin/macosfs new file mode 100755 index 0000000..02820d2 --- /dev/null +++ b/mut/bin/macosfs @@ -0,0 +1,2 @@ +#!/bin/sh +TERM=xterm-256color nix-shell -p sshfs --run "sshfs -o IdentityFile=$HOME/.ssh/macos $1 mike@192.168.122.75:$2" diff --git a/mut/bin/mailsync b/mut/bin/mailsync new file mode 100755 index 0000000..426e5b7 --- /dev/null +++ b/mut/bin/mailsync @@ -0,0 +1,112 @@ +#!/bin/sh + +# - Syncs mail for all accounts, or a single account given as an argument. +# - Displays a notification showing the number of new mails. +# - Displays a notification for each new mail with its subject displayed. +# - Runs notmuch to index new mail. +# - This script can be set up as a cron job for automated mail syncing. + +# There are many arbitrary and ugly features in this script because it is +# inherently difficult to pass environmental variables to cronjobs and other +# issues. It also should at least be compatible with Linux (and maybe BSD) with +# Xorg and MacOS as well. + +# Run only if not already running in other instance +pgrep mbsync >/dev/null && { echo "mbsync is already running."; exit ;} + +# First, we have to get the right variables for the mbsync file, the pass +# archive, notmuch and the GPG home. This is done by searching common profile +# files for variable assignments. This is ugly, but there are few options that +# will work on the maximum number of machines. +eval "$(grep -h -- \ + "^\s*\(export \)\?\(MBSYNCRC\|MPOPRC\|PASSWORD_STORE_DIR\|PASSWORD_STORE_GPG_OPTS\|NOTMUCH_CONFIG\|GNUPGHOME\|MAILSYNC_MUTE\|XDG_CONFIG_HOME\|XDG_DATA_HOME\)=" \ + "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.zprofile" "$HOME/.config/zsh/.zprofile" "$HOME/.zshenv" \ + "$HOME/.config/zsh/.zshenv" "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/zsh/.zshrc" \ + "$HOME/.pam_environment" 2>/dev/null)" + +export GPG_TTY="$(tty)" + +[ -n "$MBSYNCRC" ] && alias mbsync="mbsync -c $MBSYNCRC" || MBSYNCRC="$HOME/.mbsyncrc" +[ -n "$MPOPRC" ] || MPOPRC="$HOME/.config/mpop/config" + +lastrun="${XDG_CONFIG_HOME:-$HOME/.config}/neomutt/.mailsynclastrun" + +# Settings are different for MacOS (Darwin) systems. +case "$(uname)" in + Darwin) notify() { osascript -e "display notification \"$2\" with title \"$1\"" ;} ;; + *) + case "$(readlink -f /sbin/init)" in + *systemd*|*openrc*) export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus ;; + esac + # remember if a display server is running since `ps` doesn't always contain a display + pgrepoutput="$(pgrep -ax X\(\|org\|wayland\))" + displays="$(echo "$pgrepoutput" | grep -wo "[0-9]*:[0-9]\+" | sort -u)" + [ -z "$displays" ] && [ -d /tmp/.X11-unix ] && displays=$(cd /tmp/.X11-unix && for x in X*; do echo ":${x#X}"; done) + + notify() { [ -n "$pgrepoutput" ] && for x in ${displays:-:0}; do + export DISPLAY="$x" + notify-send --app-name="mutt-wizard" "$1" "$2" + done ;} + ;; +esac + +# Check account for new mail. Notify if there is new content. +syncandnotify() { + case "$1" in + imap) mbsync -q "$2" ;; + pop) mpop -q "$2" ;; + esac + new=$(find\ + "$HOME/.local/share/mail/${2%%-*}/"[Ii][Nn][Bb][Oo][Xx]/new/ \ + "$HOME/.local/share/mail/${2%%-*}/"[Ii][Nn][Bb][Oo][Xx]/cur/ \ + -type f -newer "$lastrun" 2> /dev/null) + newcount=$(echo "$new" | sed '/^\s*$/d' | wc -l) + case 1 in + $((newcount > 5)) ) + echo "$newcount new mail for $2." + [ -z "$MAILSYNC_MUTE" ] && notify "New Mail!" "📬 $newcount new mail(s) in \`$2\` account." + ;; + $((newcount > 0)) ) + echo "$newcount new mail for $2." + [ -z "$MAILSYNC_MUTE" ] && + for file in $new; do + # Extract and decode subject and sender from mail. + subject="$(sed -n "/^Subject:/ s|Subject: *|| p" "$file" | + perl -CS -MEncode -ne 'print decode("MIME-Header", $_)')" + from="$(sed -n "/^From:/ s|From: *|| p" "$file" | + perl -CS -MEncode -ne 'print decode("MIME-Header", $_)')" + from="${from% *}" ; from="${from%\"}" ; from="${from#\"}" + notify "📧$from:" "$subject" + done + ;; + *) echo "No new mail for $2." ;; +esac +} + +allgroups="$(grep -hs "Group" "$MBSYNCRC" "$MPOPRC" | sort -u)" + +# Get accounts to sync. All if no argument. Prefix with `error` if non-existent. +IFS=' +' +if [ -z "$1" ]; then + tosync="$allgroups" +else + tosync="$(for arg in "$@"; do for grp in $allgroups; do + [ "$arg" = "${grp##* }" ] && echo "$grp" && break + done || echo "error $arg"; done)" +fi + +for grp in $tosync; do + case $grp in + Group*) syncandnotify imap "${grp##* }" & ;; + account*) syncandnotify pop "${grp##* }" & ;; + error*) echo "ERROR: Account ${channelt##* } not found." ;; + esac +done + +wait + +notmuch-hook + +#Create a touch file that indicates the time of the last run of mailsync +touch "$lastrun" diff --git a/mut/bin/maimpick b/mut/bin/maimpick new file mode 100755 index 0000000..f2a5f2b --- /dev/null +++ b/mut/bin/maimpick @@ -0,0 +1,22 @@ +#!/bin/sh + +# This is bound to Shift+PrintScreen by default, requires maim. It lets you +# choose the kind of screenshot to take, including copying the image or even +# highlighting an area to copy. scrotcucks on suicidewatch right now. + +# variables +output="$(date '+%y%m%d-%H%M-%S').png" +clip() { + xclip -f -t image/png | xclip -sel c -t image/png +} + +case "$(printf "a selected area\\ncurrent window\\nfull screen\\na selected area (save)\\ncurrent window (save)\\nfull screen (save)" | dmenu -l 6 -i -p "Screenshot which area?")" in + "a selected area") maim -u -s | clip ;; + "current window") + echo "$(xdotool getactivewindow)" + maim -q -d 0.2 -i "$(xdotool getactivewindow)" | clip ;; + "full screen") maim -q -d 0.2 | clip ;; + "a selected area (save)") maim -u -s pic-selected-"${output}" ;; + "current window (save)") maim -q -d 0.2 -i "$(xdotool getactivewindow)" pic-window-"${output}" ;; + "full screen (save)") maim -q -d 0.2 pic-full-"${output}" ;; +esac diff --git a/mut/bin/news b/mut/bin/news new file mode 100755 index 0000000..7e302b9 --- /dev/null +++ b/mut/bin/news @@ -0,0 +1,5 @@ +#!/bin/sh +cat <(cat ~/.config/newsboat/urls) <(for url in $(env | grep NEWSBOAT_URL_); do + printf '%s\n' ${url#NEWSBOAT_URL_*=} +done) > ~/.newsboat-urls +newsboat -u ~/.newsboat-urls diff --git a/mut/bin/nixup b/mut/bin/nixup new file mode 100755 index 0000000..537f3bb --- /dev/null +++ b/mut/bin/nixup @@ -0,0 +1,94 @@ +#!/bin/sh +case "${@}" in + bootstrap-store) + [[ -d ${HOME}/nix ]] || { + docker create --name nix-data-${USER} nixos/nix sh >/dev/null 2>&1 + sudo docker cp nix-data-${USER}:/nix ~ + docker rm nix-data-${USER} + } + docker create -v ${HOME}/nix:/nix --name nix-data-${USER} nixos/nix sh + ;; + nuke) + docker rm nix-data-${USER} + docker rm nixos-${USER} + ;; + "") + if ! docker image ls | grep nixos-${USER}; then + cat > /tmp/docker-build-${USER} <<EOF +FROM alpine + +# Enable HTTPS support in wget and set nsswitch.conf to make resolution work within containers +RUN apk add --no-cache --update openssl \ + && echo hosts: files dns > /etc/nsswitch.conf + +# Download Nix and install it into the system. +ARG NIX_VERSION=2.3.14 +RUN wget https://nixos.org/releases/nix/nix-\${NIX_VERSION}/nix-\${NIX_VERSION}-\$(uname -m)-linux.tar.xz \ + && tar xf nix-\${NIX_VERSION}-\$(uname -m)-linux.tar.xz \ + && addgroup -g 30000 -S nixbld \ + && for i in \$(seq 1 30); do adduser -S -D -h /var/empty -g "Nix build user \$i" -u \$((30000 + i)) -G nixbld nixbld\$i ; done \ + && mkdir -m 0755 /etc/nix \ + && echo 'sandbox = false' > /etc/nix/nix.conf \ + && mkdir -m 0755 /nix && USER=root sh nix-\${NIX_VERSION}-\$(uname -m)-linux/install \ + && ln -s /nix/var/nix/profiles/default/etc/profile.d/nix.sh /etc/profile.d/ \ + && rm -r /nix-\${NIX_VERSION}-\$(uname -m)-linux* \ + && /nix/var/nix/profiles/default/bin/nix-collect-garbage --delete-old \ + && /nix/var/nix/profiles/default/bin/nix-store --optimise \ + && /nix/var/nix/profiles/default/bin/nix-store --verify --check-contents + +# Somehow this file is missing? +RUN mkdir -p /etc/bash && touch /etc/bash/bashrc + +ONBUILD ENV \ + ENV=/etc/profile \ + USER=root \ + PATH=/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin \ + GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt \ + NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +ENV \ + ENV=/etc/profile \ + USER=root \ + PATH=/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin \ + GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt \ + NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + NIX_PATH=/nix/var/nix/profiles/per-user/root/channels + +# Add your user the alpine way +RUN apk add --no-cache --update shadow \ + && groupadd -g $(getent group docker | cut -d: -f3) docker \ + && groupadd -g $(id -g) ${USER} \ + && useradd -g $(id -g) --groups wheel,docker -u $(id -u) ${USER} \ + && rm -rf /var/cache/apk/* +EOF + docker build . -t nixos-${USER} -f /tmp/docker-build-${USER} + fi + docker run --volumes-from=nix-data-${USER} --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /etc/kube:/etc/kube \ + -v /etc/ssl/certs/ca-bundle.crt:/etc/ssl/certs/ca-bundle.crt \ + -v /etc/ssl/certs/ca-bundle.crt:/etc/ssl/certs/ca-certificates.crt \ + -e GIT_SSL_CAINFO=/etc/ssl/certs/ca-bundle.crt \ + -e NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt \ + -e SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt \ + -e no_proxy=$no_proxy \ + -e http_proxy=$http_proxy \ + -e https_proxy=$http_proxy \ + -e SHELL=bash \ + -e USER=${USER} \ + -u $(id -u):$(id -g) \ + --group-add wheel \ + --group-add docker \ + -v ${HOME}:${HOME} \ + -w ${HOME} \ + --name nixos-${USER} \ + --network host \ + nixos-${USER} bash --login + ;; + clear) + docker run --rm --volumes-from=nix-data-${USER} nixos/nix nix-collect-garbage -d + ;; + list) + docker run --rm --volumes-from nix-data-${USER} nixos/nix ls -la /nix + ;; +esac diff --git a/mut/bin/notmuch-hook b/mut/bin/notmuch-hook new file mode 100755 index 0000000..8203558 --- /dev/null +++ b/mut/bin/notmuch-hook @@ -0,0 +1,23 @@ +notmuch new --quiet +notmuch tag -new +unread +jobs -- 'tag:new and (from:jobs-listings* or from:jobs-noreply*)' +notmuch tag -new +unread +dev -- 'tag:new and (from:/.*github.com/ or thread:{from:/.*github.com/})' + +# New needs to be removed, otherwise it will re add inbox unread +notmuch tag -new +inbox +unread -- tag:new + +# Gmail + mbsync = a lot of duplicates due to the archive +notmuch tag -new -inbox +archive -- 'folder:/Archive/ -folder:/Inbox/ -folder:/\[Gmail\]/ -folder:/FarDrafts/ -folder:/Important/ -folder:/Sent/' +notmuch tag --remove-all +sent -- folder:/Sent/ +notmuch tag --remove-all +drafts -- folder:/Drafts/ + +# Tag messages with files that were moved to trash in neomutt +notmuch tag --remove-all +trash -- folder:/Trash/ + +# Same but with messages with files that were moved to spam +notmuch tag --remove-all +spam -- folder:/Spam/ or folder:/Junk/ +# Remove files of messages that were tagged but still have files left behind in the mailbox, should be fine since gmail already keeps a duplicate in the Archive so the message will not be deleted only one file of the message +# TODO(): make this work with non gmail emails too +# notmuch search --output=files -- 'folder:/Inbox/ -tag:inbox' | grep Inbox | xargs >/dev/null 2>&1 rm + +# update dwmblocks mail module +pkill -RTMIN+12 dwmblocks diff --git a/mut/bin/oath b/mut/bin/oath new file mode 100755 index 0000000..0173a2d --- /dev/null +++ b/mut/bin/oath @@ -0,0 +1,2 @@ +#!/bin/sh +nix-shell -p yubikey-manager --run 'ykman oath accounts code --single Pionative:mike@pionative.com' | xclip -f | xclip -sel c -f diff --git a/mut/bin/openfile b/mut/bin/openfile new file mode 100755 index 0000000..0f60b10 --- /dev/null +++ b/mut/bin/openfile @@ -0,0 +1,10 @@ +#!/bin/sh + +# Helps open a file with xdg-open from mutt in a external program without weird side effects. +tempdir="${XDG_CACHE_HOME:-$HOME/.cache}/mutt-wizard/files" +file="$tempdir/${1##*/}" +[ "$(uname)" = "Darwin" ] && opener="open" || opener="setsid -f xdg-open" +mkdir -p "$tempdir" +cp -f "$1" "$file" +$opener "$file" >/dev/null 2>&1 +find "${tempdir:?}" -mtime +1 -type f -delete diff --git a/mut/bin/pass-ansible-vault-client b/mut/bin/pass-ansible-vault-client new file mode 100755 index 0000000..8d1f9a2 --- /dev/null +++ b/mut/bin/pass-ansible-vault-client @@ -0,0 +1,17 @@ +#!/bin/sh +VAULT_ID="" +while [[ $# -gt 0 ]]; do + case $1 in + --vault-id) + VAULT_ID=$2 + shift + shift + ;; + --vault-id=*) + VAULT_ID="${1#*=}" + shift + ;; + esac +done + +pass show work/ansible-vault/$VAULT_ID diff --git a/mut/bin/passmenu b/mut/bin/passmenu new file mode 100755 index 0000000..d323930 --- /dev/null +++ b/mut/bin/passmenu @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +shopt -s nullglob globstar + +dmenu=dmenu +copy() { + xclip -f | xclip -f -sel c +} +if [ "$(uname)" = "Darwin" ]; then + copy() { + pbcopy + } +fi + +( + export PASSWORD_STORE_DIR="$HOME/sync/password-store" + prefix="$PASSWORD_STORE_DIR" + echo "prefix: $prefix" + password_files=( "$prefix"/**/*.gpg ) + password_files=( "${password_files[@]#"$prefix"/}" ) + password_files=( "${password_files[@]%.gpg}" ) + echo "password_files: ${password_files[*]}" + + password="$(printf '%s\n' "${password_files[@]}" | "$dmenu" "$@")" + echo "password: $password" + + [[ -n $password ]] || exit + + pass show "$password" | head -n1 | copy +) >/tmp/debug 2>&1 + diff --git a/mut/bin/pnsh-nvim b/mut/bin/pnsh-nvim new file mode 100755 index 0000000..833a67d --- /dev/null +++ b/mut/bin/pnsh-nvim @@ -0,0 +1,42 @@ +#!/usr/bin/env nu +let desktop_open_pipe = $"($env.HOME)/.cache/desktop-open.pipe" +if not ($desktop_open_pipe | path exists) { + mkfifo $desktop_open_pipe + bash -c 'nohup desktop-open-pipe &' +} + +let args = ( +"--init" + +" --entrypoint=/usr/bin/nu" + +" --env=TERM=xterm" + +$" --env=TERMINFO" + +$" --env=EDITOR=vis" + +$" --volume=($env.TERMINFO):($env.TERMINFO)" + +" --env=_ZO_DATA_DIR=/hostfs/.local/share/zoxide" + +" --volume=/etc/profiles/per-user/ivi/etc/profile.d:/etc/profiles/per-user/ivi/etc/profile.d" + +" --env=SHELL=/usr/bin/nu" + +" --env=DISPLAY" + +" --env=XDG_RUNTIME_DIR" + +" --volume=/tmp/.X11-unix:/tmp/.X11-unix" + +$" --volume=($env.HOME)/.ssh/known_hosts:($env.HOME)/.ssh/known_hosts" + +" --volume=/run/pcscd/pcscd.comm:/run/pcscd/pcscd.comm" + +$" --hostname=(hostname)" + +" --env=STARSHIP_CONFIG=/hostfs/.config/starship.toml" + +" --env=HOME" + +$" --volume=($env.HOME):($env.HOME)" + +$" --workdir=($env | default $env.HOME PWD | get PWD)" + +# " --volume=/nix/store:/nix/store" + +$" --volume=/nix-config:/nix-config" + +$" --volume=($env.HOME)/.ssh:/root/.ssh" + +$" --volume=($env | default "/var/run" XDG_RUNTIME_DIR | get XDG_RUNTIME_DIR)/docker.sock:/var/run/docker.sock" + +" --net=host" +) + +( +^pnsh + --pnsh-host-bindfs-disabled + --pnsh-docker-extra-args=$"($args)" + --with-docker + --docker-image=pionativedev.azurecr.io/pionative/pnsh-vis + --docker-tag=latest +) diff --git a/mut/bin/recordwin b/mut/bin/recordwin new file mode 100755 index 0000000..bb48104 --- /dev/null +++ b/mut/bin/recordwin @@ -0,0 +1,9 @@ +#!/bin/sh +if pidof ffmpeg; then + notify-send ffmpeg "killing current recording" + pkill --signal=TERM ffmpeg +else + notify-send ffmpeg "Start recording" + ffmpeg -f x11grab $(xdotool getwindowfocus getwindowgeometry | tr '\n' ' ' | gawk '{print "-video_size " $8 " -i +"$4 }') -y ~/recording.webm + notify-send ffmpeg "saved recording to ~/recording.webm" +fi diff --git a/mut/bin/rotdir b/mut/bin/rotdir new file mode 100755 index 0000000..d171f29 --- /dev/null +++ b/mut/bin/rotdir @@ -0,0 +1,12 @@ +#!/bin/sh + +# When I open an image from the file manager in nsxiv (the image viewer), I want +# to be able to press the next/previous keys to key through the rest of the +# images in the same directory. This script "rotates" the content of a +# directory based on the first chosen file, so that if I open the 15th image, +# if I press next, it will go to the 16th etc. Autistic, I know, but this is +# one of the reasons that nsxiv is great for being able to read standard input. + +[ -z "$1" ] && echo "usage: rotdir regex 2>&1" && exit 1 +base="$(basename "$1")" +ls "$PWD" | awk -v BASE="$base" 'BEGIN { lines = ""; m = 0; } { if ($0 == BASE) { m = 1; } } { if (!m) { if (lines) { lines = lines"\n"; } lines = lines""$0; } else { print $0; } } END { print lines; }' diff --git a/mut/bin/sb-battery b/mut/bin/sb-battery new file mode 100755 index 0000000..93cbe08 --- /dev/null +++ b/mut/bin/sb-battery @@ -0,0 +1,37 @@ +#!/bin/sh + +# Prints all batteries, their percentage remaining and an emoji corresponding +# to charge status (🔌 for plugged up, 🔋 for discharging on battery, etc.). + +case $BLOCK_BUTTON in + 3) notify-send "🔋 Battery module" "🔋: discharging +🛑: not charging +♻: stagnant charge +🔌: charging +⚡: charged +❗: battery very low! +- Scroll to change adjust xbacklight." ;; + 4) xbacklight -inc 10 ;; + 5) xbacklight -dec 10 ;; + 6) "$TERMINAL" -e "$EDITOR" "$0" ;; +esac + +# Loop through all attached batteries and format the info +for battery in /sys/class/power_supply/BAT?*; do + # If non-first battery, print a space separator. + [ -n "${capacity+x}" ] && printf " " + # Sets up the status and capacity + case "$(cat "$battery/status" 2>&1)" in + "Full") status="⚡" ;; + "Discharging") status="🔋" ;; + "Charging") status="🔌" ;; + "Not charging") status="🛑" ;; + "Unknown") status="♻️" ;; + *) exit 1 ;; + esac + capacity="$(cat "$battery/capacity" 2>&1)" + # Will make a warn variable if discharging and low + [ "$status" = "🔋" ] && [ "$capacity" -le 25 ] && warn="❗" + # Prints the info + printf "%s%s%d%%" "$status" "$warn" "$capacity"; unset warn +done && printf "\\n" diff --git a/mut/bin/sb-clock b/mut/bin/sb-clock new file mode 100755 index 0000000..c079a58 --- /dev/null +++ b/mut/bin/sb-clock @@ -0,0 +1,30 @@ +#!/bin/sh + +clock=$(date '+%I') + +case "$clock" in + "00") icon="🕛" ;; + "01") icon="🕐" ;; + "02") icon="🕑" ;; + "03") icon="🕒" ;; + "04") icon="🕓" ;; + "05") icon="🕔" ;; + "06") icon="🕕" ;; + "07") icon="🕖" ;; + "08") icon="🕗" ;; + "09") icon="🕘" ;; + "10") icon="🕙" ;; + "11") icon="🕚" ;; + "12") icon="🕛" ;; +esac + +case $BLOCK_BUTTON in + 1) notify-send "This Month" "$(cal --color=always | sed "s/..7m/<b><span color=\"cyan\">/;s|..0m|</span></b>|")" && notify-send -t 100000 "$(khal list now 14d -f "{calendar-color} {start-time} {title} {status} {description} +" | sed "s/..7m/<b><span color=\"cyan\">/;s|..0m|</span></b>|")" ;; + 2) setsid -f "$TERMINAL" -e khal interactive ;; + 3) notify-send "📅 Time/date module" "\- Left click to show upcoming appointments for the next three days via \`calcurse -d3\` and show the month via \`cal\` +- Middle click opens calcurse if installed" ;; + 6) "$TERMINAL" -e "$EDITOR" "$0" ;; +esac + +date "+%Y %b %d (%a) $icon%I:%M%p" diff --git a/mut/bin/sb-internet b/mut/bin/sb-internet new file mode 100755 index 0000000..94b7da2 --- /dev/null +++ b/mut/bin/sb-internet @@ -0,0 +1,26 @@ +#!/bin/sh + +# Show wifi 📶 and percent strength or 📡 if none. +# Show 🌐 if connected to ethernet or ❎ if none. +# Show 🔒 if a vpn connection is active + +case $BLOCK_BUTTON in + 1) "$TERMINAL" -e nmtui; pkill -RTMIN+4 dwmblocks ;; + 3) notify-send "🌐 Internet module" "\- Click to connect +❌: wifi disabled +📡: no wifi connection +📶: wifi connection with quality +❎: no ethernet +🌐: ethernet working +🔒: vpn is active +" ;; + 6) "$TERMINAL" -e "$EDITOR" "$0" ;; +esac + +if grep -xq 'up' /sys/class/net/w*/operstate 2>/dev/null ; then + wifiicon="$(awk '/^\s*w/ { print "📶", int($3 * 100 / 70) "% " }' /proc/net/wireless)" +elif grep -xq 'down' /sys/class/net/w*/operstate 2>/dev/null ; then + grep -xq '0x1003' /sys/class/net/w*/flags && wifiicon="📡 " || wifiicon="❌ " +fi + +printf "%s%s%s\n" "$wifiicon" "$(sed "s/down/❎/;s/up/🌐/" /sys/class/net/e*/operstate 2>/dev/null)" "$(sed "s/.*/🔒/" /sys/class/net/tun*/operstate 2>/dev/null)" diff --git a/mut/bin/sb-mailbox b/mut/bin/sb-mailbox new file mode 100755 index 0000000..8fd2d5b --- /dev/null +++ b/mut/bin/sb-mailbox @@ -0,0 +1,22 @@ +#!/bin/sh + +# Displays number of unread mail and an loading icon if updating. +# When clicked, brings up `neomutt`. + +case $BLOCK_BUTTON in + 1) setsid -f "$TERMINAL" -e neomutt ;; + 2) setsid -f mailsync >/dev/null ;; + 3) notify-send "📬 Mail module" "\- Shows unread mail +- Shows 🔃 if syncing mail +- Left click opens neomutt +- Middle click syncs mail" ;; + 6) "$TERMINAL" -e "$EDITOR" "$0" ;; +esac + +# NOTE(): can't figure out why this one doesn't work, emails don't end up in new folder always +# unread="$(find "${XDG_DATA_HOME:-$HOME/.local/share}"/mail/*/[Ii][Nn][Bb][Oo][Xx]/new/* -type f | wc -l 2>/dev/null)" +unread="$(notmuch search tag:inbox and tag:unread | wc -l 2>/dev/null)" + +pidof mbsync >/dev/null 2>&1 && icon="🔃" + +[ "$unread" = "0" ] && [ "$icon" = "" ] || echo "📬$unread$icon" diff --git a/mut/bin/sb-music b/mut/bin/sb-music new file mode 100755 index 0000000..d164b4b --- /dev/null +++ b/mut/bin/sb-music @@ -0,0 +1,19 @@ +#!/bin/sh + +filter() { sed "/^volume:/d;s/\\&/&/g;s/\\[paused\\].*/⏸/g;/\\[playing\\].*/d;/^ERROR/Q" | paste -sd ' ' -;} + +pidof -x sb-mpdup >/dev/null 2>&1 || sb-mpdup >/dev/null 2>&1 & + +case $BLOCK_BUTTON in + 1) mpc status | filter ; setsid -f "$TERMINAL" -e ncmpcpp ;; # right click, pause/unpause + 2) mpc toggle | filter ;; # right click, pause/unpause + 3) mpc status | filter ; notify-send "🎵 Music module" "\- Shows mpd song playing. +- ⏸ when paused. +- Left click opens ncmpcpp. +- Middle click pauses. +- Scroll changes track.";; # right click, pause/unpause + 4) mpc prev | filter ;; # scroll up, previous + 5) mpc next | filter ;; # scroll down, next + 6) mpc status | filter ; "$TERMINAL" -e "$EDITOR" "$0" ;; + *) mpc status | filter ;; +esac diff --git a/mut/bin/sb-nettraf b/mut/bin/sb-nettraf new file mode 100755 index 0000000..178f677 --- /dev/null +++ b/mut/bin/sb-nettraf @@ -0,0 +1,29 @@ +#!/bin/sh + +# Module showing network traffic. Shows how much data has been received (RX) or +# transmitted (TX) since the previous time this script ran. So if run every +# second, gives network traffic per second. + +case $BLOCK_BUTTON in + 1) setsid -f "$TERMINAL" -e bmon ;; + 3) notify-send "🌐 Network traffic module" "🔻: Traffic received +🔺: Traffic transmitted" ;; + 6) "$TERMINAL" -e "$EDITOR" "$0" ;; +esac + +update() { + sum=0 + for arg; do + read -r i < "$arg" + sum=$(( sum + i )) + done + cache=/tmp/${1##*/} + [ -f "$cache" ] && read -r old < "$cache" || old=0 + printf %d\\n "$sum" > "$cache" + printf %d\\n $(( sum - old )) +} + +rx=$(update /sys/class/net/[ew]*/statistics/rx_bytes) +tx=$(update /sys/class/net/[ew]*/statistics/tx_bytes) + +printf "🔻%4sB 🔺%4sB\\n" $(numfmt --to=iec $rx $tx) diff --git a/mut/bin/sb-news b/mut/bin/sb-news new file mode 100755 index 0000000..fe701db --- /dev/null +++ b/mut/bin/sb-news @@ -0,0 +1,17 @@ +#!/bin/sh + +# Displays number of unread news items and an loading icon if updating. +# When clicked, brings up `newsboat`. + +case $BLOCK_BUTTON in + 1) setsid "$TERMINAL" -e newsboat ;; + 2) setsid -f newsup >/dev/null exit ;; + 3) notify-send "📰 News module" "\- Shows unread news items +- Shows 🔃 if updating with \`newsup\` +- Left click opens newsboat +- Middle click syncs RSS feeds +<b>Note:</b> Only one instance of newsboat (including updates) may be running at a time." ;; + 6) "$TERMINAL" -e "$EDITOR" "$0" ;; +esac + + cat /tmp/newsupdate 2>/dev/null || echo "$(newsboat -x print-unread | awk '{ if($1>0) print "📰" $1}')$(cat "${XDG_CONFIG_HOME:-$HOME/.config}"/newsboat/.update 2>/dev/null)" diff --git a/mut/bin/sb-pomodoro b/mut/bin/sb-pomodoro new file mode 100755 index 0000000..1eb222a --- /dev/null +++ b/mut/bin/sb-pomodoro @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Displays current pomodoro status +# When clicked, brings up `newsboat`. + +case $BLOCK_BUTTON in + 1) setsid -f pomodoro start >/dev/null ;; + 2) setsid -f pomodoro finish >/dev/null ;; + 3) notify-send "🍅 Pomodoro module" "\- Shows current pomodoro status +- Shows ⏱ if a Pomodoro is running +- Left click starts a new Pomodoro +- Middle click finishes a Pomodoro early +- Shift click opens ~/.pomodoro in editor" ;; + 6) "$TERMINAL" -e "$EDITOR" "$HOME/.pomodoro" ;; +esac + +if ! status=$(pomodoro status); then + exit 0 +fi + +if [ -z "$status" ]; then + daily_completed=$(grep -c "^$(date --iso-8601)" ~/.pomodoro/history) + daily_total="$(grep daily_goal ~/.pomodoro/settings | cut -f2 -d'=')" + + msg="$daily_completed/$daily_total🍅" + if [ "$daily_completed" -ne "0" ]; then + seconds_since_last_started="$(( $(date +%s -u) - $(tail -n1 ~/.pomodoro/history | cut -f1 -d' ' | xargs -I{} date -d"{}" +%s -u) ))" + seconds_since_last="$(( "$seconds_since_last_started" - 60*$(tail -n1 ~/.pomodoro/history | cut -f2 -d' ' | cut -f2 -d'=')))" + if [ "$seconds_since_last" -lt 0 ]; then + seconds_since_last=0 + fi + msg="$(date -d@"$seconds_since_last" +"%H:%M" -u)min ago $msg" + fi + echo "$msg" + exit 0 +fi + +echo "$status" diff --git a/mut/bin/sb-volume b/mut/bin/sb-volume new file mode 100755 index 0000000..e66dea7 --- /dev/null +++ b/mut/bin/sb-volume @@ -0,0 +1,39 @@ +#!/bin/sh + +# Prints the current volume or 🔇 if muted. + +case $BLOCK_BUTTON in + 1) setsid -w -f "$TERMINAL" -e pulsemixer; pkill -RTMIN+10 "${STATUSBAR:-dwmblocks}" ;; + 2) wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle ;; + 4) wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%+ ;; + 5) wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%- ;; + 3) notify-send "📢 Volume module" "\- Shows volume 🔊, 🔇 if muted. +- Middle click to mute. +- Scroll to change." ;; + 6) setsid -f "$TERMINAL" -e "$EDITOR" "$0" ;; +esac + +vol="$(wpctl get-volume @DEFAULT_AUDIO_SINK@)" + +# If muted, print 🔇 and exit. +[ "$vol" != "${vol%\[MUTED\]}" ] && echo 🔇 && exit + +vol="${vol#Volume: }" + +split() { + # For ommiting the . without calling and external program. + IFS=$2 + set -- $1 + printf '%s' "$@" +} + +vol="$(printf "%.0f" "$(split "$vol" ".")")" + +case 1 in + $((vol >= 70)) ) icon="🔊" ;; + $((vol >= 30)) ) icon="🔉" ;; + $((vol >= 1)) ) icon="🔈" ;; + * ) echo 🔇 && exit ;; +esac + +echo "$icon$vol%" diff --git a/mut/bin/setbg b/mut/bin/setbg new file mode 100755 index 0000000..7f5977d --- /dev/null +++ b/mut/bin/setbg @@ -0,0 +1,19 @@ +#!/bin/sh +reload=0 +while getopts "r" opt; do + case "$opt" in + h|\?) exit 0 ;; + r) reload=1 ;; + esac +done +if [ $reload -eq 1 ]; then + # (cat ~/.cache/wal/sequences &) + wal -R +else + if [ -z "$1" ]; then + sxiv -tob ~/Wallpapers | xargs wal -i + else + wal -i "$1" + fi + kill -HUP "$(pidof dwm)" +fi diff --git a/mut/bin/showkey b/mut/bin/showkey Binary files differnew file mode 100755 index 0000000..9bb9a3b --- /dev/null +++ b/mut/bin/showkey diff --git a/mut/bin/spectrwmbar b/mut/bin/spectrwmbar new file mode 100755 index 0000000..a106b01 --- /dev/null +++ b/mut/bin/spectrwmbar @@ -0,0 +1,59 @@ +#!/usr/bin/env sh +# script for spectrwm status bar + +trap 'update' 5 + +fgcolors=("+@fg=1;" "+@fg=2;" "+@fg=3;" "+@fg=4;" "+@fg=5;" "+@fg=6;" "+@fg=7;" "+@fg=8;") +nfgcolors=${#fgcolors[@]} + +SLEEP_SEC=5m + +repeat() { + i=0; while [ $i -lt $1 ] + do + echo -ne "$TOKEN" + i=$(( i + 1 )) + done +} + +cpu() { + read cpu a b c previdle rest < /proc/stat + prevtotal=$((a+b+c+previdle)) + sleep 0.5 + read cpu a b c idle rest < /proc/stat + total=$((a+b+c+idle)) + cpu=$((100*( (total-prevtotal) - (idle-previdle) ) / (total-prevtotal) )) + echo -e "CPU: $cpu%" +} + +battery() { + BATTERY="$(cat /sys/class/power_supply/BAT0/capacity)" + + BAR_LEFT=$BATTERY + BATTERY_BAR="" + BLOCK=$(( 100 / nfgcolors )) + TOKEN=$(printf '\u2588') + + BAT_COL=$(( $nfgcolors -1 )) + #loops forever outputting a line every SLEEP_SEC secs + while [ $(( BAR_LEFT - BLOCK )) -gt 0 ] + do + BATTERY_BAR="${fgcolors[$BAT_COL]}$(repeat $BLOCK)${BATTERY_BAR}" + BAR_LEFT=$(( BAR_LEFT - BLOCK )) + BAT_COL=$(( BAT_COL - 1)) + done + + BATTERY_BAR="BATTERY: ${fgcolors[$BAT_COL]}$(repeat $BAR_LEFT)${BATTERY_BAR}" + echo $BATTERY_BAR +} + +update() { + echo "$(cpu) $(battery)" + wait +} + +while :; do + update + sleep $SLEEP_SEC & + wait +done diff --git a/mut/bin/surf-open.sh b/mut/bin/surf-open.sh new file mode 100755 index 0000000..c22edc2 --- /dev/null +++ b/mut/bin/surf-open.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# +# See the LICENSE file for copyright and license details. +# + +xidfile="$HOME/tmp/tabbed-surf.xid" +uri="" + +if [ "$#" -gt 0 ]; +then + uri="$1" +fi + +runtabbed() { + tabbed -dn tabbed-surf -r 2 surf -e '' "$uri" >"$xidfile" \ + 2>/dev/null & +} + +if [ ! -r "$xidfile" ]; +then + runtabbed +else + xid=$(cat "$xidfile") + xprop -id "$xid" >/dev/null 2>&1 + if [ $? -gt 0 ]; + then + runtabbed + else + surf -e "$xid" "$uri" >/dev/null 2>&1 & + fi +fi + diff --git a/mut/bin/sysact b/mut/bin/sysact new file mode 100755 index 0000000..4bb92dc --- /dev/null +++ b/mut/bin/sysact @@ -0,0 +1,21 @@ +#!/bin/sh + +# A dmenu wrapper script for system functions. +export WM="dwm" +ctl='systemctl' + +wmpid(){ # This function is needed if there are multiple instances of the window manager. + echo "$(pidof dwm)" +} + +case "$(printf "🔒 lock\n🚪 leave $WM\n♻️ renew $WM\n🐻 hibernate\n🔃 reboot\n🖥️shutdown\n💤 sleep\n📺 display off" | dmenu -i -p 'Action: ')" in + '🔒 lock') slock ;; + "🚪 leave $WM") kill -TERM "$(wmpid)" ;; + "♻️ renew $WM") kill -HUP "$(wmpid)" ;; + '🐻 hibernate') slock $ctl hibernate -i ;; + '💤 sleep') slock $ctl suspend -i ;; + '🔃 reboot') $ctl reboot -i ;; + '🖥️shutdown') $ctl poweroff -i ;; + '📺 display off') xset dpms force off ;; + *) exit 1 ;; +esac diff --git a/mut/bin/terragrunt b/mut/bin/terragrunt new file mode 100755 index 0000000..d0b47f7 --- /dev/null +++ b/mut/bin/terragrunt @@ -0,0 +1,76 @@ +#!/bin/sh +TERRAGRUNT_ARGS=() +while [[ $# -gt 0 ]]; do + case $1 in + -full) + FULL=1 + shift + ;; + -p|--path) + path="$2" + shift + shift + ;; + -p=*|--path=*) + path="${1#*=}" + shift + ;; + *|-*) + TERRAGRUNT_ARGS+=("$1") + shift + esac +done + +TTY="" +case ${TERRAGRUNT_ARGS[0]} in + plan) + TERRAGRUNT_ARGS+=(-no-color -compact-warnings) + ;; + apply|destroy) + TTY="-t" + for arg in $TERRAGRUNT_ARGS; do + if [[ $arg -eq "gruntplan" ]]; then + TTY="" + fi + done + TERRAGRUNT_ARGS+=(-no-color -compact-warnings) + ;; + init) + TERRAGRUNT_ARGS+=(-no-color -compact-warnings) + ;; +esac + +VARIABLES="" +REPO="${PWD}" +for var in $(pass show work/env) +do + case $var in + TERRAGRUNT_EXTRA_MOUNTS*) + TERRAGRUNT_EXTRA_MOUNTS="$TERRAGRUNT_EXTRA_MOUNTS ${var#*=}" + ;; + *) + VARIABLES="$VARIABLES$(printf ' -e %s' "$var")" + ;; + esac +done + +for var in $(printenv) +do + case $var in + TF_*) + VARIABLES="$VARIABLES$(printf ' -e %s' $var)" + ;; + esac +done + +WORKDIR="$REPO/$path" + +docker run --rm -i $TTY \ + $VARIABLES \ + -v $HOME/.terragrunt-cache:/tmp \ + -v $HOME/.azure:/root/.azure \ + -v $HOME/.netrc:/root/.netrc \ + $TERRAGRUNT_EXTRA_MOUNTS \ + -v ${REPO}:${REPO} \ + -w ${WORKDIR} \ + $TERRAGRUNT_CONTAINER terragrunt ${TERRAGRUNT_ARGS[@]} | filter-ansi diff --git a/mut/bin/transadd b/mut/bin/transadd new file mode 100755 index 0000000..a598fad --- /dev/null +++ b/mut/bin/transadd @@ -0,0 +1,9 @@ +#!/bin/sh + +# Mimeapp script for adding torrent to transmission-daemon, but will also start the daemon first if not running. + +# transmission-daemon sometimes fails to take remote requests in its first moments, hence the sleep. + +pidof transmission-daemon >/dev/null || (transmission-daemon && notify-send "Starting transmission daemon..." && sleep 3 && pkill -RTMIN+7 "${STATUSBAR:-dwmblocks}") + +transmission-remote -a "$@" && notify-send "🔽 Torrent added." diff --git a/mut/bin/vremote b/mut/bin/vremote new file mode 100755 index 0000000..4d0d417 --- /dev/null +++ b/mut/bin/vremote @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +if [ -z "$PATH" ]; then + PATH="/etc/profiles/per-user/$USER/bin:/run/current-system/sw/bin:/usr/bin:/bin" +fi +server_pipe="$XDG_CACHE_HOME/nvim/server.pipe" +if ! [ -e "$server_pipe" ]; then + nohup nvim --listen "$server_pipe" --headless >/dev/null 2>&1 & +fi + +( + file_names=() + if [ -n "$1" ]; then + for file_name in "${@}"; do + if [[ "${file_name:0:1}" == / || "${file_name:0:2}" == ~[/a-zA-Z0-9] ]] + then + file_names+=("$file_name") + else + file_names+=("${PWD}/$file_name") + fi + done + echo "got file_names: ${file_names[*]}" + fi + + if ! nvim \ + --headless \ + --server ~/.cache/nvim/server.pipe \ + --remote-expr 'luaeval("vim.json.encode(vim.iter(vim.api.nvim_list_uis()):map(function(v) return v.chan end):totable())")' \ + | jq -er '.[]' + then + nvim --server "$server_pipe" --remote "${file_names[@]}" >/dev/tty + exec nvim --server "$server_pipe" --remote-ui >/dev/tty + else + if ! command -v osascript >/dev/null 2>&1; then + notify-send "already existing ui +starting new nvim instance" + else + osascript -e 'display notification "already existing ui..." with title "vremote"' + fi + exec nvim "${file_names[@]}" >/dev/tty </dev/tty + fi +) > ~/vremote_logs 2>&1 diff --git a/mut/bin/window b/mut/bin/window new file mode 100755 index 0000000..bd40a4c --- /dev/null +++ b/mut/bin/window @@ -0,0 +1,11 @@ +#!/bin/sh +PIPE=/tmp/window-fifo +STDIN="$(cat -)" +rm $PIPE +mkfifo $PIPE +echo "$STDIN" | tee $PIPE >/dev/null & +if command -v st >/dev/null; then + st -e sh -c "<$PIPE ${*}" & +else + tmux splitw sh -c "<$PIPE ${*}" & +fi diff --git a/mut/bin/xdg-open b/mut/bin/xdg-open new file mode 100755 index 0000000..f401d16 --- /dev/null +++ b/mut/bin/xdg-open @@ -0,0 +1,5 @@ +#!/bin/bash +case "$(file --mime-type $1 | awk '{print $2}')" in + text/*|application/json) exec "$EDITOR" $1 ;; + *) nu --commands "^echo $1 | nc 127.0.0.1 1994 | echo" ;; +esac diff --git a/mut/bin/zshcmd b/mut/bin/zshcmd new file mode 100755 index 0000000..1a2fc88 --- /dev/null +++ b/mut/bin/zshcmd @@ -0,0 +1,10 @@ +#!/usr/bin/env zsh +if [ -f ~/.zshrc ]; then + source ~/.zshrc &>/dev/null +fi + +# Enable alias expansion +setopt aliases + +# Run your commands that use aliases +eval ${@} diff --git a/mut/carapace/specs/bluegreen.yaml b/mut/carapace/specs/bluegreen.yaml new file mode 100644 index 0000000..6b3d4f1 --- /dev/null +++ b/mut/carapace/specs/bluegreen.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://carapace.sh/schemas/command.json +name: bluegreen +description: Bluegreen +parsing: disabled +completion: + positionalany: ["$carapace.bridge.Click([bluegreen])"] + diff --git a/mut/carapace/specs/pioctl.yaml b/mut/carapace/specs/pioctl.yaml new file mode 100644 index 0000000..0092804 --- /dev/null +++ b/mut/carapace/specs/pioctl.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://carapace.sh/schemas/command.json +name: pioctl +description: Pioctl +parsing: disabled +completion: + positionalany: ["$carapace.bridge.Click([pioctl])"] + diff --git a/mut/carapace/specs/pistarchio.yaml b/mut/carapace/specs/pistarchio.yaml new file mode 100644 index 0000000..cc9738c --- /dev/null +++ b/mut/carapace/specs/pistarchio.yaml @@ -0,0 +1,5 @@ +name: pistarchio +description: pistarchio +parsing: disabled +completion: + positionalany: ["$carapace.bridge.Carapace([pistarchio])"] diff --git a/mut/carapace/specs/upctl.yaml b/mut/carapace/specs/upctl.yaml new file mode 100644 index 0000000..7574e24 --- /dev/null +++ b/mut/carapace/specs/upctl.yaml @@ -0,0 +1,5 @@ +name: upctl +description: upctl +parsing: disabled +completion: + positionalany: ["$carapace.bridge.Cobra([upctl])"] diff --git a/mut/dmenu-mac/.github/workflows/main.yml b/mut/dmenu-mac/.github/workflows/main.yml new file mode 100644 index 0000000..2164189 --- /dev/null +++ b/mut/dmenu-mac/.github/workflows/main.yml @@ -0,0 +1,40 @@ +name: Build + +on: [push] + +jobs: + build: + runs-on: macos-12 + + steps: + - uses: actions/checkout@v1 + + - name: Lint + run: swiftlint --strict + + - name: Build + run: xcodebuild + -scheme dmenu-mac + -archivePath dmenu-mac.xcarchive archive + + - name: Package + run: xcodebuild + -exportArchive + -archivePath dmenu-mac.xcarchive + -exportOptionsPlist mac-application-archive.plist + -exportPath . + + - name: Compress + run: zip -r dmenu-mac.zip dmenu-mac.app + + - uses: actions/upload-artifact@v1 + with: + name: dmenu-mac.zip + path: dmenu-mac.zip + + - name: Release + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_USER: "user" + run: hub release edit ${GITHUB_REF//refs\/tags\//} -a dmenu-mac.zip -m '' diff --git a/mut/dmenu-mac/.gitignore b/mut/dmenu-mac/.gitignore new file mode 100644 index 0000000..a8a030c --- /dev/null +++ b/mut/dmenu-mac/.gitignore @@ -0,0 +1,5 @@ +xcuserdata/ +/*.app +/*.xcarchive +/*.zip +build diff --git a/mut/dmenu-mac/.swiftlint.yml b/mut/dmenu-mac/.swiftlint.yml new file mode 100644 index 0000000..c68a148 --- /dev/null +++ b/mut/dmenu-mac/.swiftlint.yml @@ -0,0 +1,4 @@ +identifier_name: + excluded: # excluded via string array + - id + - i diff --git a/mut/dmenu-mac/LICENSE b/mut/dmenu-mac/LICENSE new file mode 100644 index 0000000..9cecc1d --- /dev/null +++ b/mut/dmenu-mac/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/mut/dmenu-mac/README.md b/mut/dmenu-mac/README.md new file mode 100644 index 0000000..2a92104 --- /dev/null +++ b/mut/dmenu-mac/README.md @@ -0,0 +1,59 @@ + +# dmenu-mac + +[](https://github.com/oNaiPs/dmenu-mac) + + + +dmenu inspired application launcher. + + + +## Who is it for +Anyone that needs a quick and intuitive keyboard-only application launcher that does not rely on spotlight indexing. + +## Why +If you are like me and have a shit-ton of files on your computer, and spotlight keeps your CPU running like crazy. + +1. [Disable spotlight](https://www.google.com/search?q=disable+spotlight+completely) completely and its global shortcut (recommended but not necessary) +3. Download and run dmenu-mac + +## How to use +1. Open the app, use cmd-Space to bring it to front. +2. Optionally, you can change the binding by clicking the ... on the right of the menu. +3. Type the application you want to open, hit enter to run the one selected. + +### Pipes +You can make dmenu-mac part of your scripting toolbox, use it to prompt the user for options: +``` +echo "Yes\nNo" | dmenu-mac -p "Are you sure?" +Yes +``` + +## Installation + +dmenu-mac can be installed with [brew](https://brew.sh/) running: + +``` +brew install dmenu-mac +``` + +Optionally, you can download it [here](https://github.com/oNaiPs/dmenu-mac/releases). + +NOTE: the releases are not signed yet, use it at your own risk. I'll take care of that as soon as we can assess the number of people interested in the project. + +*Mac OS X 10.12 or greater required. + +## Features + +- Uses fuzzy search +- Configurable global hotkey +- Multi-display support +- Not dependant on spotlight indexing + +# Pull requests +Any improvement/bugfix is welcome. + +# Authors + +[@onaips](https://twitter.com/onaips) diff --git a/mut/dmenu-mac/demo.gif b/mut/dmenu-mac/demo.gif Binary files differnew file mode 100644 index 0000000..6eae60d --- /dev/null +++ b/mut/dmenu-mac/demo.gif diff --git a/mut/dmenu-mac/dmenu-mac.entitlements b/mut/dmenu-mac/dmenu-mac.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/mut/dmenu-mac/dmenu-mac.entitlements @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>com.apple.security.app-sandbox</key> + <true/> +</dict> +</plist> diff --git a/mut/dmenu-mac/dmenu-mac.xcodeproj/project.pbxproj b/mut/dmenu-mac/dmenu-mac.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e4f5210 --- /dev/null +++ b/mut/dmenu-mac/dmenu-mac.xcodeproj/project.pbxproj @@ -0,0 +1,651 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 7F25568F269CF945002324AC /* CommandArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F25568E269CF945002324AC /* CommandArguments.swift */; }; + 7F255692269CF988002324AC /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 7F255691269CF988002324AC /* ArgumentParser */; }; + 7F255694269D01FF002324AC /* InputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F255693269D01FF002324AC /* InputField.swift */; }; + 7F2CCA1E268B10F700C14B29 /* FileWatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7F2CCA1D268B10F700C14B29 /* FileWatcher */; }; + 7F2CCA23268B195F00C14B29 /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2CCA22268B195F00C14B29 /* Notification+Name.swift */; }; + 7F2CCA2E268FE37900C14B29 /* ListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2CCA2D268FE37900C14B29 /* ListProvider.swift */; }; + 7F2CCA30268FE38C00C14B29 /* AppListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2CCA2F268FE38C00C14B29 /* AppListProvider.swift */; }; + 7F2CCA32268FEDF600C14B29 /* PipeListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2CCA31268FEDF600C14B29 /* PipeListProvider.swift */; }; + 7F2CCA3F268FFDB500C14B29 /* ReadStdin.h.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F2CCA3E268FFDB500C14B29 /* ReadStdin.h.m */; }; + 7F3278F01C71C9CE00AFB227 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F3278E81C71C9CE00AFB227 /* AppDelegate.swift */; }; + 7F3278F11C71C9CE00AFB227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7F3278E91C71C9CE00AFB227 /* Assets.xcassets */; }; + 7F3278F51C71C9CE00AFB227 /* SearchWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F3278ED1C71C9CE00AFB227 /* SearchWindow.swift */; }; + 7F3278F71C71C9CE00AFB227 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F3278EF1C71C9CE00AFB227 /* SearchViewController.swift */; }; + 7F3279001C71D39800AFB227 /* VerticalAlignedTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F3278FF1C71D39800AFB227 /* VerticalAlignedTextFieldCell.swift */; }; + 7F3279091C71DF2300AFB227 /* DDHotKeyCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F3279031C71DF2300AFB227 /* DDHotKeyCenter.m */; }; + 7F32790A1C71DF2300AFB227 /* DDHotKeyTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F3279051C71DF2300AFB227 /* DDHotKeyTextField.m */; }; + 7F32790B1C71DF2300AFB227 /* DDHotKeyUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F3279071C71DF2300AFB227 /* DDHotKeyUtilities.m */; }; + 7F32790E1C71DF3E00AFB227 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F32790D1C71DF3E00AFB227 /* Carbon.framework */; }; + 7F5D0E261CACCD0100268983 /* ResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F5D0E241CACCD0100268983 /* ResultsView.swift */; }; + 7F5D0E2C1CACCDD400268983 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F5D0E281CACCDD400268983 /* Main.storyboard */; }; + 7F6511F529A617F8007D3EB3 /* Preferences in Frameworks */ = {isa = PBXBuildFile; productRef = 7F6511F429A617F8007D3EB3 /* Preferences */; }; + 7F6511F729A619DE007D3EB3 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F6511F629A619DE007D3EB3 /* GeneralSettingsViewController.swift */; }; + 7F6511FB29A61A4E007D3EB3 /* GeneralSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F6511F929A61A4E007D3EB3 /* GeneralSettingsViewController.xib */; }; + 7F6511FE29A61C32007D3EB3 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 7F6511FD29A61C32007D3EB3 /* KeyboardShortcuts */; }; + 7F90D2D226FDE2400003EC7E /* dmenu-mac in Resources */ = {isa = PBXBuildFile; fileRef = 7F90D2D126FDE2400003EC7E /* dmenu-mac */; }; + 7FA29F6C29A55B0B00B27359 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 7FA29F6B29A55B0B00B27359 /* LaunchAtLogin */; }; + 7FDA366E241FDA5E00B51212 /* Fuse in Frameworks */ = {isa = PBXBuildFile; productRef = 7FDA366D241FDA5E00B51212 /* Fuse */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 7F25568E269CF945002324AC /* CommandArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CommandArguments.swift; path = src/CommandArguments.swift; sourceTree = "<group>"; }; + 7F255693269D01FF002324AC /* InputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InputField.swift; path = src/InputField.swift; sourceTree = "<group>"; }; + 7F2CCA22268B195F00C14B29 /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Notification+Name.swift"; path = "src/Notification+Name.swift"; sourceTree = "<group>"; }; + 7F2CCA2D268FE37900C14B29 /* ListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ListProvider.swift; path = src/ListProvider.swift; sourceTree = "<group>"; }; + 7F2CCA2F268FE38C00C14B29 /* AppListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppListProvider.swift; path = src/AppListProvider.swift; sourceTree = "<group>"; }; + 7F2CCA31268FEDF600C14B29 /* PipeListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PipeListProvider.swift; path = src/PipeListProvider.swift; sourceTree = "<group>"; }; + 7F2CCA3D268FFDB500C14B29 /* ReadStdin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ReadStdin.h; path = src/ReadStdin.h; sourceTree = "<group>"; }; + 7F2CCA3E268FFDB500C14B29 /* ReadStdin.h.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ReadStdin.h.m; path = src/ReadStdin.h.m; sourceTree = "<group>"; }; + 7F3278E81C71C9CE00AFB227 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = src/AppDelegate.swift; sourceTree = "<group>"; }; + 7F3278E91C71C9CE00AFB227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = src/Assets.xcassets; sourceTree = "<group>"; }; + 7F3278EC1C71C9CE00AFB227 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = src/Info.plist; sourceTree = "<group>"; }; + 7F3278ED1C71C9CE00AFB227 /* SearchWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchWindow.swift; path = src/SearchWindow.swift; sourceTree = "<group>"; }; + 7F3278EF1C71C9CE00AFB227 /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchViewController.swift; path = src/SearchViewController.swift; sourceTree = "<group>"; }; + 7F3278FF1C71D39800AFB227 /* VerticalAlignedTextFieldCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VerticalAlignedTextFieldCell.swift; path = src/VerticalAlignedTextFieldCell.swift; sourceTree = "<group>"; }; + 7F3279021C71DF2300AFB227 /* DDHotKeyCenter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DDHotKeyCenter.h; path = "src/3rd-party/DDHotKeyCenter.h"; sourceTree = "<group>"; }; + 7F3279031C71DF2300AFB227 /* DDHotKeyCenter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDHotKeyCenter.m; path = "src/3rd-party/DDHotKeyCenter.m"; sourceTree = "<group>"; }; + 7F3279041C71DF2300AFB227 /* DDHotKeyTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DDHotKeyTextField.h; path = "src/3rd-party/DDHotKeyTextField.h"; sourceTree = "<group>"; }; + 7F3279051C71DF2300AFB227 /* DDHotKeyTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDHotKeyTextField.m; path = "src/3rd-party/DDHotKeyTextField.m"; sourceTree = "<group>"; }; + 7F3279061C71DF2300AFB227 /* DDHotKeyUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DDHotKeyUtilities.h; path = "src/3rd-party/DDHotKeyUtilities.h"; sourceTree = "<group>"; }; + 7F3279071C71DF2300AFB227 /* DDHotKeyUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDHotKeyUtilities.m; path = "src/3rd-party/DDHotKeyUtilities.m"; sourceTree = "<group>"; }; + 7F32790D1C71DF3E00AFB227 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; + 7F3279111C71E02300AFB227 /* BridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BridgingHeader.h; path = src/BridgingHeader.h; sourceTree = "<group>"; }; + 7F5D0E241CACCD0100268983 /* ResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ResultsView.swift; path = src/ResultsView.swift; sourceTree = "<group>"; }; + 7F5D0E291CACCDD400268983 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = src/Base.lproj/Main.storyboard; sourceTree = "<group>"; }; + 7F6511F629A619DE007D3EB3 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GeneralSettingsViewController.swift; path = src/GeneralSettingsViewController.swift; sourceTree = "<group>"; }; + 7F6511FA29A61A4E007D3EB3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = src/Base.lproj/GeneralSettingsViewController.xib; sourceTree = "<group>"; }; + 7F7982A5241FF0FD0079AFD2 /* dmenu-mac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "dmenu-mac.entitlements"; sourceTree = "<group>"; }; + 7F90D2D126FDE2400003EC7E /* dmenu-mac */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "dmenu-mac"; sourceTree = "<group>"; }; + 7FE8A6481C717481000A2C4C /* dmenu-mac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dmenu-mac.app"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7FE8A6451C717481000A2C4C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F2CCA1E268B10F700C14B29 /* FileWatcher in Frameworks */, + 7F6511FE29A61C32007D3EB3 /* KeyboardShortcuts in Frameworks */, + 7F6511F529A617F8007D3EB3 /* Preferences in Frameworks */, + 7F32790E1C71DF3E00AFB227 /* Carbon.framework in Frameworks */, + 7F255692269CF988002324AC /* ArgumentParser in Frameworks */, + 7FA29F6C29A55B0B00B27359 /* LaunchAtLogin in Frameworks */, + 7FDA366E241FDA5E00B51212 /* Fuse in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7F3278E71C71C96100AFB227 /* dmenu-mac */ = { + isa = PBXGroup; + children = ( + 7F3279011C71DF1900AFB227 /* 3rd-party */, + 7F6511F829A619FD007D3EB3 /* src */, + 7F3278E91C71C9CE00AFB227 /* Assets.xcassets */, + 7F3278FA1C71CA0200AFB227 /* Base.lproj */, + 7F3279111C71E02300AFB227 /* BridgingHeader.h */, + 7F7982A5241FF0FD0079AFD2 /* dmenu-mac.entitlements */, + 7F3278EC1C71C9CE00AFB227 /* Info.plist */, + 7F90D2D026FDE2240003EC7E /* scripts */, + ); + name = "dmenu-mac"; + sourceTree = "<group>"; + }; + 7F3278FA1C71CA0200AFB227 /* Base.lproj */ = { + isa = PBXGroup; + children = ( + 7F5D0E281CACCDD400268983 /* Main.storyboard */, + 7F6511F929A61A4E007D3EB3 /* GeneralSettingsViewController.xib */, + ); + name = Base.lproj; + sourceTree = "<group>"; + }; + 7F3279011C71DF1900AFB227 /* 3rd-party */ = { + isa = PBXGroup; + children = ( + 7F3279021C71DF2300AFB227 /* DDHotKeyCenter.h */, + 7F3279031C71DF2300AFB227 /* DDHotKeyCenter.m */, + 7F3279041C71DF2300AFB227 /* DDHotKeyTextField.h */, + 7F3279051C71DF2300AFB227 /* DDHotKeyTextField.m */, + 7F3279061C71DF2300AFB227 /* DDHotKeyUtilities.h */, + 7F3279071C71DF2300AFB227 /* DDHotKeyUtilities.m */, + ); + name = "3rd-party"; + sourceTree = "<group>"; + }; + 7F32790F1C71DF4600AFB227 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7F32790D1C71DF3E00AFB227 /* Carbon.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + 7F6511F829A619FD007D3EB3 /* src */ = { + isa = PBXGroup; + children = ( + 7F3278E81C71C9CE00AFB227 /* AppDelegate.swift */, + 7F2CCA2F268FE38C00C14B29 /* AppListProvider.swift */, + 7F25568E269CF945002324AC /* CommandArguments.swift */, + 7F6511F629A619DE007D3EB3 /* GeneralSettingsViewController.swift */, + 7F255693269D01FF002324AC /* InputField.swift */, + 7F2CCA2D268FE37900C14B29 /* ListProvider.swift */, + 7F2CCA22268B195F00C14B29 /* Notification+Name.swift */, + 7F2CCA31268FEDF600C14B29 /* PipeListProvider.swift */, + 7F2CCA3D268FFDB500C14B29 /* ReadStdin.h */, + 7F2CCA3E268FFDB500C14B29 /* ReadStdin.h.m */, + 7F5D0E241CACCD0100268983 /* ResultsView.swift */, + 7F3278EF1C71C9CE00AFB227 /* SearchViewController.swift */, + 7F3278ED1C71C9CE00AFB227 /* SearchWindow.swift */, + 7F3278FF1C71D39800AFB227 /* VerticalAlignedTextFieldCell.swift */, + ); + name = src; + sourceTree = "<group>"; + }; + 7F90D2D026FDE2240003EC7E /* scripts */ = { + isa = PBXGroup; + children = ( + 7F90D2D126FDE2400003EC7E /* dmenu-mac */, + ); + path = scripts; + sourceTree = "<group>"; + }; + 7FE8A63F1C717481000A2C4C = { + isa = PBXGroup; + children = ( + 7F3278E71C71C96100AFB227 /* dmenu-mac */, + 7F32790F1C71DF4600AFB227 /* Frameworks */, + 7FE8A6491C717481000A2C4C /* Products */, + ); + sourceTree = "<group>"; + }; + 7FE8A6491C717481000A2C4C /* Products */ = { + isa = PBXGroup; + children = ( + 7FE8A6481C717481000A2C4C /* dmenu-mac.app */, + ); + name = Products; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7FE8A6471C717481000A2C4C /* dmenu-mac */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7FE8A6571C717481000A2C4C /* Build configuration list for PBXNativeTarget "dmenu-mac" */; + buildPhases = ( + 7F2CCA26268F996200C14B29 /* ShellScript */, + 7FE8A6441C717481000A2C4C /* Sources */, + 7FE8A6451C717481000A2C4C /* Frameworks */, + 7FE8A6461C717481000A2C4C /* Resources */, + 7FA29F6D29A6139B00B27359 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "dmenu-mac"; + packageProductDependencies = ( + 7FDA366D241FDA5E00B51212 /* Fuse */, + 7F2CCA1D268B10F700C14B29 /* FileWatcher */, + 7F255691269CF988002324AC /* ArgumentParser */, + 7FA29F6B29A55B0B00B27359 /* LaunchAtLogin */, + 7F6511F429A617F8007D3EB3 /* Preferences */, + 7F6511FD29A61C32007D3EB3 /* KeyboardShortcuts */, + ); + productName = "dmenu-mac"; + productReference = 7FE8A6481C717481000A2C4C /* dmenu-mac.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7FE8A6401C717481000A2C4C /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = ""; + LastSwiftUpdateCheck = 0720; + LastUpgradeCheck = 1250; + ORGANIZATIONNAME = "Jose Pereira"; + TargetAttributes = { + 7FE8A6471C717481000A2C4C = { + CreatedOnToolsVersion = 7.2.1; + LastSwiftMigration = 1020; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 7FE8A6431C717481000A2C4C /* Build configuration list for PBXProject "dmenu-mac" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7FE8A63F1C717481000A2C4C; + packageReferences = ( + 7FDA366C241FDA5E00B51212 /* XCRemoteSwiftPackageReference "fuse-swift" */, + 7F2CCA1C268B10F700C14B29 /* XCRemoteSwiftPackageReference "FileWatcher" */, + 7F255690269CF988002324AC /* XCRemoteSwiftPackageReference "swift-argument-parser" */, + 7FA29F6A29A55B0B00B27359 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */, + 7F6511F329A617F7007D3EB3 /* XCRemoteSwiftPackageReference "Preferences" */, + 7F6511FC29A61C32007D3EB3 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, + ); + productRefGroup = 7FE8A6491C717481000A2C4C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7FE8A6471C717481000A2C4C /* dmenu-mac */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7FE8A6461C717481000A2C4C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F6511FB29A61A4E007D3EB3 /* GeneralSettingsViewController.xib in Resources */, + 7F90D2D226FDE2400003EC7E /* dmenu-mac in Resources */, + 7F5D0E2C1CACCDD400268983 /* Main.storyboard in Resources */, + 7F3278F11C71C9CE00AFB227 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 7F2CCA26268F996200C14B29 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; + 7FA29F6D29A6139B00B27359 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${BUILT_PRODUCTS_DIR}/LaunchAtLogin_LaunchAtLogin.bundle/Contents/Resources/copy-helper-swiftpm.sh\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7FE8A6441C717481000A2C4C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F3279001C71D39800AFB227 /* VerticalAlignedTextFieldCell.swift in Sources */, + 7F2CCA30268FE38C00C14B29 /* AppListProvider.swift in Sources */, + 7F5D0E261CACCD0100268983 /* ResultsView.swift in Sources */, + 7F3278F71C71C9CE00AFB227 /* SearchViewController.swift in Sources */, + 7F32790A1C71DF2300AFB227 /* DDHotKeyTextField.m in Sources */, + 7F3278F01C71C9CE00AFB227 /* AppDelegate.swift in Sources */, + 7F25568F269CF945002324AC /* CommandArguments.swift in Sources */, + 7F2CCA2E268FE37900C14B29 /* ListProvider.swift in Sources */, + 7F6511F729A619DE007D3EB3 /* GeneralSettingsViewController.swift in Sources */, + 7F3279091C71DF2300AFB227 /* DDHotKeyCenter.m in Sources */, + 7F2CCA32268FEDF600C14B29 /* PipeListProvider.swift in Sources */, + 7F2CCA3F268FFDB500C14B29 /* ReadStdin.h.m in Sources */, + 7F32790B1C71DF2300AFB227 /* DDHotKeyUtilities.m in Sources */, + 7F2CCA23268B195F00C14B29 /* Notification+Name.swift in Sources */, + 7F3278F51C71C9CE00AFB227 /* SearchWindow.swift in Sources */, + 7F255694269D01FF002324AC /* InputField.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 7F5D0E281CACCDD400268983 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7F5D0E291CACCDD400268983 /* Base */, + ); + name = Main.storyboard; + sourceTree = "<group>"; + }; + 7F6511F929A61A4E007D3EB3 /* GeneralSettingsViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 7F6511FA29A61A4E007D3EB3 /* Base */, + ); + name = GeneralSettingsViewController.xib; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7FE8A6551C717481000A2C4C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_OPTIMIZATION = time; + CLANG_ANALYZER_GCD_PERFORMANCE = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_USE_OPTIMIZATION_PROFILE = NO; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_HEADERS_RUN_UNIFDEF = YES; + COPY_PHASE_STRIP = YES; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = YES; + GCC_FAST_MATH = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SHORT_ENUMS = YES; + GCC_UNROLL_LOOPS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LLVM_LTO = YES_THIN; + MACH_O_TYPE = mh_execute; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; + SWIFT_DISABLE_SAFETY_CHECKS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 7FE8A6561C717481000A2C4C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_OPTIMIZATION = time; + CLANG_ANALYZER_GCD_PERFORMANCE = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; + CLANG_USE_OPTIMIZATION_PROFILE = NO; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_HEADERS_RUN_UNIFDEF = YES; + COPY_PHASE_STRIP = YES; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = YES; + GCC_FAST_MATH = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = fast; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_SHORT_ENUMS = YES; + GCC_UNROLL_LOOPS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LLVM_LTO = YES_THIN; + MACH_O_TYPE = mh_execute; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + SDKROOT = macosx; + STRINGS_FILE_OUTPUT_ENCODING = "UTF-8"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_DISABLE_SAFETY_CHECKS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 7FE8A6581C717481000A2C4C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = "dmenu-mac.entitlements"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = src/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.onaips.dmenu-macos"; + PRODUCT_NAME = "dmenu-mac"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = src/BridgingHeader.h; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 7FE8A6591C717481000A2C4C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = "dmenu-mac.entitlements"; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = src/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.onaips.dmenu-macos"; + PRODUCT_NAME = "dmenu-mac"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = src/BridgingHeader.h; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7FE8A6431C717481000A2C4C /* Build configuration list for PBXProject "dmenu-mac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7FE8A6551C717481000A2C4C /* Debug */, + 7FE8A6561C717481000A2C4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7FE8A6571C717481000A2C4C /* Build configuration list for PBXNativeTarget "dmenu-mac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7FE8A6581C717481000A2C4C /* Debug */, + 7FE8A6591C717481000A2C4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 7F255690269CF988002324AC /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.4.3; + }; + }; + 7F2CCA1C268B10F700C14B29 /* XCRemoteSwiftPackageReference "FileWatcher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/eonist/FileWatcher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.2.3; + }; + }; + 7F6511F329A617F7007D3EB3 /* XCRemoteSwiftPackageReference "Preferences" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Preferences"; + requirement = { + branch = main; + kind = branch; + }; + }; + 7F6511FC29A61C32007D3EB3 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; + requirement = { + branch = main; + kind = branch; + }; + }; + 7FA29F6A29A55B0B00B27359 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin"; + requirement = { + branch = main; + kind = branch; + }; + }; + 7FDA366C241FDA5E00B51212 /* XCRemoteSwiftPackageReference "fuse-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/krisk/fuse-swift"; + requirement = { + branch = master; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 7F255691269CF988002324AC /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = 7F255690269CF988002324AC /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; + 7F2CCA1D268B10F700C14B29 /* FileWatcher */ = { + isa = XCSwiftPackageProductDependency; + package = 7F2CCA1C268B10F700C14B29 /* XCRemoteSwiftPackageReference "FileWatcher" */; + productName = FileWatcher; + }; + 7F6511F429A617F8007D3EB3 /* Preferences */ = { + isa = XCSwiftPackageProductDependency; + package = 7F6511F329A617F7007D3EB3 /* XCRemoteSwiftPackageReference "Preferences" */; + productName = Preferences; + }; + 7F6511FD29A61C32007D3EB3 /* KeyboardShortcuts */ = { + isa = XCSwiftPackageProductDependency; + package = 7F6511FC29A61C32007D3EB3 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; + productName = KeyboardShortcuts; + }; + 7FA29F6B29A55B0B00B27359 /* LaunchAtLogin */ = { + isa = XCSwiftPackageProductDependency; + package = 7FA29F6A29A55B0B00B27359 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */; + productName = LaunchAtLogin; + }; + 7FDA366D241FDA5E00B51212 /* Fuse */ = { + isa = XCSwiftPackageProductDependency; + package = 7FDA366C241FDA5E00B51212 /* XCRemoteSwiftPackageReference "fuse-swift" */; + productName = Fuse; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 7FE8A6401C717481000A2C4C /* Project object */; +} diff --git a/mut/dmenu-mac/dmenu-mac.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mut/dmenu-mac/dmenu-mac.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/mut/dmenu-mac/dmenu-mac.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:"> + </FileRef> +</Workspace> diff --git a/mut/dmenu-mac/dmenu-mac.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mut/dmenu-mac/dmenu-mac.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/mut/dmenu-mac/dmenu-mac.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/mut/dmenu-mac/dmenu-mac.xcodeproj/xcshareddata/xcschemes/dmenu-mac.xcscheme b/mut/dmenu-mac/dmenu-mac.xcodeproj/xcshareddata/xcschemes/dmenu-mac.xcscheme new file mode 100644 index 0000000..4f37b46 --- /dev/null +++ b/mut/dmenu-mac/dmenu-mac.xcodeproj/xcshareddata/xcschemes/dmenu-mac.xcscheme @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1250" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "7FE8A6471C717481000A2C4C" + BuildableName = "dmenu-mac.app" + BlueprintName = "dmenu-mac" + ReferencedContainer = "container:dmenu-mac.xcodeproj"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "7FE8A6471C717481000A2C4C" + BuildableName = "dmenu-mac.app" + BlueprintName = "dmenu-mac" + ReferencedContainer = "container:dmenu-mac.xcodeproj"> + </BuildableReference> + </MacroExpansion> + <Testables> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "NO" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "7FE8A6471C717481000A2C4C" + BuildableName = "dmenu-mac.app" + BlueprintName = "dmenu-mac" + ReferencedContainer = "container:dmenu-mac.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <BuildableProductRunnable + runnableDebuggingMode = "0"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "7FE8A6471C717481000A2C4C" + BuildableName = "dmenu-mac.app" + BlueprintName = "dmenu-mac" + ReferencedContainer = "container:dmenu-mac.xcodeproj"> + </BuildableReference> + </BuildableProductRunnable> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/mut/dmenu-mac/mac-application-archive.plist b/mut/dmenu-mac/mac-application-archive.plist new file mode 100644 index 0000000..07fbd0b --- /dev/null +++ b/mut/dmenu-mac/mac-application-archive.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>method</key> + <string>mac-application</string> +</dict> +</plist>
\ No newline at end of file diff --git a/mut/dmenu-mac/scripts/dmenu-mac b/mut/dmenu-mac/scripts/dmenu-mac new file mode 100755 index 0000000..111b364 --- /dev/null +++ b/mut/dmenu-mac/scripts/dmenu-mac @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# +function realpath() { python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0"; } +CONTENTS="$(dirname "$(dirname "$(realpath "$0")")")" +DMENU_MAC="$CONTENTS/MacOS/dmenu-mac" +"$DMENU_MAC" "$@" diff --git a/mut/dmenu-mac/src/3rd-party/DDHotKeyCenter.h b/mut/dmenu-mac/src/3rd-party/DDHotKeyCenter.h new file mode 100644 index 0000000..6f79bbb --- /dev/null +++ b/mut/dmenu-mac/src/3rd-party/DDHotKeyCenter.h @@ -0,0 +1,93 @@ +/* + DDHotKey -- DDHotKeyCenter.h + + Copyright (c) Dave DeLong <http://www.davedelong.com> + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import <Cocoa/Cocoa.h> + +//a convenient typedef for the required signature of a hotkey block callback +typedef void (^DDHotKeyTask)(NSEvent*); + +@interface DDHotKey : NSObject + +// creates a new hotkey but does not register it ++ (instancetype)hotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task; + +@property (nonatomic, assign, readonly) id target; +@property (nonatomic, readonly) SEL action; +@property (nonatomic, strong, readonly) id object; +@property (nonatomic, copy, readonly) DDHotKeyTask task; + +@property (nonatomic, readonly) unsigned short keyCode; +@property (nonatomic, readonly) NSUInteger modifierFlags; + +@end + +#pragma mark - + +@interface DDHotKeyCenter : NSObject + ++ (instancetype)sharedHotKeyCenter; + +/** + Register a hotkey. + */ +- (DDHotKey *)registerHotKey:(DDHotKey *)hotKey; + +/** + Register a target/action hotkey. + The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask; + Returns the hotkey registered. If registration failed, returns nil. + */ +- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags target:(id)target action:(SEL)action object:(id)object; + +/** + Register a block callback hotkey. + The modifierFlags must be a bitwise OR of NSCommandKeyMask, NSAlternateKeyMask, NSControlKeyMask, or NSShiftKeyMask; + Returns the hotkey registered. If registration failed, returns nil. + */ +- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task; + +/** + See if a hotkey exists with the specified keycode and modifier flags. + NOTE: this will only check among hotkeys you have explicitly registered with DDHotKeyCenter. This does not check all globally registered hotkeys. + */ +- (BOOL)hasRegisteredHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags; + +/** + Unregister a specific hotkey + */ +- (void)unregisterHotKey:(DDHotKey *)hotKey; + +/** + Unregister all hotkeys + */ +- (void)unregisterAllHotKeys; + +/** + Unregister all hotkeys with a specific target + */ +- (void)unregisterHotKeysWithTarget:(id)target; + +/** + Unregister all hotkeys with a specific target and action + */ +- (void)unregisterHotKeysWithTarget:(id)target action:(SEL)action; + +/** + Unregister a hotkey with a specific keycode and modifier flags + */ +- (void)unregisterHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags; + +/** + Returns a set of currently registered hotkeys + **/ +- (NSSet *)registeredHotKeys; + +@end + diff --git a/mut/dmenu-mac/src/3rd-party/DDHotKeyCenter.m b/mut/dmenu-mac/src/3rd-party/DDHotKeyCenter.m new file mode 100644 index 0000000..fd0b313 --- /dev/null +++ b/mut/dmenu-mac/src/3rd-party/DDHotKeyCenter.m @@ -0,0 +1,284 @@ +/* + DDHotKey -- DDHotKeyCenter.m + + Copyright (c) Dave DeLong <http://www.davedelong.com> + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import <Carbon/Carbon.h> +#import <objc/runtime.h> + +#import "DDHotKeyCenter.h" +#import "DDHotKeyUtilities.h" + +#pragma mark Private Global Declarations + +OSStatus dd_hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData); + +#pragma mark DDHotKey + +@interface DDHotKey () + +@property (nonatomic, retain) NSValue *hotKeyRef; +@property (nonatomic) UInt32 hotKeyID; + + +@property (nonatomic, assign, setter = _setTarget:) id target; +@property (nonatomic, setter = _setAction:) SEL action; +@property (nonatomic, strong, setter = _setObject:) id object; +@property (nonatomic, copy, setter = _setTask:) DDHotKeyTask task; + +@property (nonatomic, setter = _setKeyCode:) unsigned short keyCode; +@property (nonatomic, setter = _setModifierFlags:) NSUInteger modifierFlags; + +@end + +@implementation DDHotKey + ++ (instancetype)hotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task { + DDHotKey *newHotKey = [[self alloc] init]; + [newHotKey _setTask:task]; + [newHotKey _setKeyCode:keyCode]; + [newHotKey _setModifierFlags:flags]; + return newHotKey; +} + +- (void) dealloc { + [[DDHotKeyCenter sharedHotKeyCenter] unregisterHotKey:self]; +} + +- (NSUInteger)hash { + return [self keyCode] ^ [self modifierFlags]; +} + +- (BOOL)isEqual:(id)object { + BOOL equal = NO; + if ([object isKindOfClass:[DDHotKey class]]) { + equal = ([object keyCode] == [self keyCode]); + equal &= ([object modifierFlags] == [self modifierFlags]); + } + return equal; +} + +- (NSString *)description { + NSMutableArray *bits = [NSMutableArray array]; + if ((_modifierFlags & NSEventModifierFlagControl) > 0) { [bits addObject:@"NSControlKeyMask"]; } + if ((_modifierFlags & NSEventModifierFlagCommand) > 0) { [bits addObject:@"NSCommandKeyMask"]; } + if ((_modifierFlags & NSEventModifierFlagShift) > 0) { [bits addObject:@"NSShiftKeyMask"]; } + if ((_modifierFlags & NSEventModifierFlagOption) > 0) { [bits addObject:@"NSAlternateKeyMask"]; } + + NSString *flags = [NSString stringWithFormat:@"(%@)", [bits componentsJoinedByString:@" | "]]; + NSString *invokes = @"(block)"; + if ([self target] != nil && [self action] != nil) { + invokes = [NSString stringWithFormat:@"[%@ %@]", [self target], NSStringFromSelector([self action])]; + } + return [NSString stringWithFormat:@"%@\n\t(key: %hu\n\tflags: %@\n\tinvokes: %@)", [super description], [self keyCode], flags, invokes]; +} + +- (void)invokeWithEvent:(NSEvent *)event { + if (_target != nil && _action != nil && [_target respondsToSelector:_action]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [_target performSelector:_action withObject:event withObject:_object]; +#pragma clang diagnostic pop + } else if (_task != nil) { + _task(event); + } +} + +@end + +#pragma mark DDHotKeyCenter + +static DDHotKeyCenter *sharedHotKeyCenter = nil; + +@implementation DDHotKeyCenter { + NSMutableSet *_registeredHotKeys; + UInt32 _nextHotKeyID; +} + ++ (instancetype)sharedHotKeyCenter { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedHotKeyCenter = [super allocWithZone:nil]; + sharedHotKeyCenter = [sharedHotKeyCenter init]; + + EventTypeSpec eventSpec; + eventSpec.eventClass = kEventClassKeyboard; + eventSpec.eventKind = kEventHotKeyReleased; + InstallApplicationEventHandler(&dd_hotKeyHandler, 1, &eventSpec, NULL, NULL); + }); + return sharedHotKeyCenter; +} + ++ (id)allocWithZone:(NSZone *)zone { + return sharedHotKeyCenter; +} + +- (id)init { + if (self != sharedHotKeyCenter) { return sharedHotKeyCenter; } + + self = [super init]; + if (self) { + _registeredHotKeys = [[NSMutableSet alloc] init]; + _nextHotKeyID = 1; + } + return self; +} + +- (NSSet *)hotKeysMatching:(BOOL(^)(DDHotKey *hotkey))matcher { + NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + return matcher(evaluatedObject); + }]; + return [_registeredHotKeys filteredSetUsingPredicate:predicate]; +} + +- (BOOL)hasRegisteredHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags { + return [self hotKeysMatching:^BOOL(DDHotKey *hotkey) { + return hotkey.keyCode == keyCode && hotkey.modifierFlags == flags; + }].count > 0; +} + +- (DDHotKey *)_registerHotKey:(DDHotKey *)hotKey { + if ([_registeredHotKeys containsObject:hotKey]) { + return hotKey; + } + + EventHotKeyID keyID; + keyID.signature = 'htk1'; + keyID.id = _nextHotKeyID; + + EventHotKeyRef carbonHotKey; + UInt32 flags = DDCarbonModifierFlagsFromCocoaModifiers([hotKey modifierFlags]); + OSStatus err = RegisterEventHotKey([hotKey keyCode], flags, keyID, GetEventDispatcherTarget(), 0, &carbonHotKey); + + //error registering hot key + if (err != 0) { return nil; } + + NSValue *refValue = [NSValue valueWithPointer:carbonHotKey]; + [hotKey setHotKeyRef:refValue]; + [hotKey setHotKeyID:_nextHotKeyID]; + + _nextHotKeyID++; + [_registeredHotKeys addObject:hotKey]; + + return hotKey; +} + +- (DDHotKey *)registerHotKey:(DDHotKey *)hotKey { + return [self _registerHotKey:hotKey]; +} + +- (void)unregisterHotKey:(DDHotKey *)hotKey { + NSValue *hotKeyRef = [hotKey hotKeyRef]; + if (hotKeyRef) { + EventHotKeyRef carbonHotKey = (EventHotKeyRef)[hotKeyRef pointerValue]; + UnregisterEventHotKey(carbonHotKey); + [hotKey setHotKeyRef:nil]; + } + + [_registeredHotKeys removeObject:hotKey]; +} + +- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags task:(DDHotKeyTask)task { + //we can't add a new hotkey if something already has this combo + if ([self hasRegisteredHotKeyWithKeyCode:keyCode modifierFlags:flags]) { return nil; } + + DDHotKey *newHotKey = [[DDHotKey alloc] init]; + [newHotKey _setTask:task]; + [newHotKey _setKeyCode:keyCode]; + [newHotKey _setModifierFlags:flags]; + + return [self _registerHotKey:newHotKey]; +} + +- (DDHotKey *)registerHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags target:(id)target action:(SEL)action object:(id)object { + //we can't add a new hotkey if something already has this combo + if ([self hasRegisteredHotKeyWithKeyCode:keyCode modifierFlags:flags]) { return nil; } + + //build the hotkey object: + DDHotKey *newHotKey = [[DDHotKey alloc] init]; + [newHotKey _setTarget:target]; + [newHotKey _setAction:action]; + [newHotKey _setObject:object]; + [newHotKey _setKeyCode:keyCode]; + [newHotKey _setModifierFlags:flags]; + return [self _registerHotKey:newHotKey]; +} + +- (void)unregisterHotKeysMatching:(BOOL(^)(DDHotKey *hotkey))matcher { + //explicitly unregister the hotkey, since relying on the unregistration in -dealloc can be problematic + @autoreleasepool { + NSSet *matches = [self hotKeysMatching:matcher]; + for (DDHotKey *hotKey in matches) { + [self unregisterHotKey:hotKey]; + } + } +} + +- (void)unregisterHotKeysWithTarget:(id)target { + [self unregisterHotKeysMatching:^BOOL(DDHotKey *hotkey) { + return hotkey.target == target; + }]; +} + +- (void)unregisterHotKeysWithTarget:(id)target action:(SEL)action { + [self unregisterHotKeysMatching:^BOOL(DDHotKey *hotkey) { + return hotkey.target == target && sel_isEqual(hotkey.action, action); + }]; +} + +- (void)unregisterHotKeyWithKeyCode:(unsigned short)keyCode modifierFlags:(NSUInteger)flags { + [self unregisterHotKeysMatching:^BOOL(DDHotKey *hotkey) { + return hotkey.keyCode == keyCode && hotkey.modifierFlags == flags; + }]; +} + +- (void)unregisterAllHotKeys { + NSSet *keys = [_registeredHotKeys copy]; + for (DDHotKey *key in keys) { + [self unregisterHotKey:key]; + } +} + +- (NSSet *)registeredHotKeys { + return [self hotKeysMatching:^BOOL(DDHotKey *hotkey) { + return hotkey.hotKeyRef != NULL; + }]; +} + +@end + +OSStatus dd_hotKeyHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *userData) { + @autoreleasepool { + EventHotKeyID hotKeyID; + GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID); + + UInt32 keyID = hotKeyID.id; + + NSSet *matchingHotKeys = [[DDHotKeyCenter sharedHotKeyCenter] hotKeysMatching:^BOOL(DDHotKey *hotkey) { + return hotkey.hotKeyID == keyID; + }]; + if ([matchingHotKeys count] > 1) { NSLog(@"ERROR!"); } + DDHotKey *matchingHotKey = [matchingHotKeys anyObject]; + + NSEvent *event = [NSEvent eventWithEventRef:theEvent]; + NSEvent *keyEvent = [NSEvent keyEventWithType:NSEventTypeKeyUp + location:[event locationInWindow] + modifierFlags:[event modifierFlags] + timestamp:[event timestamp] + windowNumber:-1 + context:nil + characters:@"" + charactersIgnoringModifiers:@"" + isARepeat:NO + keyCode:[matchingHotKey keyCode]]; + + [matchingHotKey invokeWithEvent:keyEvent]; + } + + return noErr; +} diff --git a/mut/dmenu-mac/src/3rd-party/DDHotKeyTextField.h b/mut/dmenu-mac/src/3rd-party/DDHotKeyTextField.h new file mode 100644 index 0000000..c399d62 --- /dev/null +++ b/mut/dmenu-mac/src/3rd-party/DDHotKeyTextField.h @@ -0,0 +1,20 @@ +/* + DDHotKey -- DDHotKeyTextField.h + + Copyright (c) Dave DeLong <http://www.davedelong.com> + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import <Foundation/Foundation.h> +#import "DDHotKeyCenter.h" + +@interface DDHotKeyTextField : NSTextField + +@property (nonatomic, strong) DDHotKey *hotKey; + +@end + +@interface DDHotKeyTextFieldCell : NSTextFieldCell @end
\ No newline at end of file diff --git a/mut/dmenu-mac/src/3rd-party/DDHotKeyTextField.m b/mut/dmenu-mac/src/3rd-party/DDHotKeyTextField.m new file mode 100644 index 0000000..ccabd10 --- /dev/null +++ b/mut/dmenu-mac/src/3rd-party/DDHotKeyTextField.m @@ -0,0 +1,138 @@ +/* + DDHotKey -- DDHotKeyTextField.m + + Copyright (c) Dave DeLong <http://www.davedelong.com> + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import <Carbon/Carbon.h> + +#import "DDHotKeyTextField.h" +#import "DDHotKeyUtilities.h" + +@interface DDHotKeyTextFieldEditor : NSTextView + +@property (nonatomic, weak) DDHotKeyTextField *hotKeyField; + +@end + +static DDHotKeyTextFieldEditor *DDFieldEditor(void); +static DDHotKeyTextFieldEditor *DDFieldEditor(void) { + static DDHotKeyTextFieldEditor *editor; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + editor = [[DDHotKeyTextFieldEditor alloc] initWithFrame:NSMakeRect(0, 0, 100, 32)]; + [editor setFieldEditor:YES]; + }); + return editor; +} + +@implementation DDHotKeyTextFieldCell + +- (NSTextView *)fieldEditorForView:(NSView *)view { + if ([view isKindOfClass:[DDHotKeyTextField class]]) { + DDHotKeyTextFieldEditor *editor = DDFieldEditor(); + editor.insertionPointColor = editor.backgroundColor; + editor.hotKeyField = (DDHotKeyTextField *)view; + return editor; + } + return nil; +} + +@end + +@implementation DDHotKeyTextField + ++ (Class)cellClass { + return [DDHotKeyTextFieldCell class]; +} + +- (void)setHotKey:(DDHotKey *)hotKey { + if (_hotKey != hotKey) { + _hotKey = hotKey; + [super setStringValue:[DDStringFromKeyCode(hotKey.keyCode, hotKey.modifierFlags) uppercaseString]]; + } +} + +- (void)setStringValue:(NSString *)aString { + NSLog(@"-[DDHotKeyTextField setStringValue:] is not what you want. Use -[DDHotKeyTextField setHotKey:] instead."); + [super setStringValue:aString]; +} + +- (NSString *)stringValue { + NSLog(@"-[DDHotKeyTextField stringValue] is not what you want. Use -[DDHotKeyTextField hotKey] instead."); + return [super stringValue]; +} + +@end + +@implementation DDHotKeyTextFieldEditor { + BOOL _hasSeenKeyDown; + id _globalMonitor; + DDHotKey *_originalHotKey; +} + +- (void)setHotKeyField:(DDHotKeyTextField *)hotKeyField { + _hotKeyField = hotKeyField; + _originalHotKey = _hotKeyField.hotKey; +} + +- (void)processHotkeyEvent:(NSEvent *)event { + NSUInteger flags = event.modifierFlags; + BOOL hasModifier = (flags & (NSEventModifierFlagCommand | NSEventModifierFlagOption | NSEventModifierFlagControl | NSEventModifierFlagShift | NSEventModifierFlagFunction)) > 0; + + if (event.type == NSEventTypeKeyDown) { + _hasSeenKeyDown = YES; + unichar character = [event.charactersIgnoringModifiers characterAtIndex:0]; + + + if (hasModifier == NO && ([[NSCharacterSet newlineCharacterSet] characterIsMember:character] || event.keyCode == kVK_Escape)) { + if (event.keyCode == kVK_Escape) { + self.hotKeyField.hotKey = _originalHotKey; + + NSString *str = DDStringFromKeyCode(_originalHotKey.keyCode, _originalHotKey.modifierFlags); + self.textStorage.mutableString.string = [str uppercaseString]; + } + [self.hotKeyField sendAction:self.hotKeyField.action to:self.hotKeyField.target]; + [self.window makeFirstResponder:nil]; + return; + } + } + + if ((event.type == NSEventTypeKeyDown || (event.type == NSEventTypeFlagsChanged && _hasSeenKeyDown == NO)) && hasModifier) { + self.hotKeyField.hotKey = [DDHotKey hotKeyWithKeyCode:event.keyCode modifierFlags:flags task:_originalHotKey.task]; + NSString *str = DDStringFromKeyCode(event.keyCode, flags); + [self.textStorage.mutableString setString:[str uppercaseString]]; + [self.hotKeyField sendAction:self.hotKeyField.action to:self.hotKeyField.target]; + } +} + +- (BOOL)becomeFirstResponder { + BOOL ok = [super becomeFirstResponder]; + if (ok) { + _hasSeenKeyDown = NO; + _globalMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSEventMaskKeyDown | NSEventMaskFlagsChanged) handler:^NSEvent*(NSEvent *event){ + [self processHotkeyEvent:event]; + return nil; + }]; + } + return ok; +} + +- (BOOL)resignFirstResponder { + BOOL ok = [super resignFirstResponder]; + if (ok) { + self.hotKeyField = nil; + if (_globalMonitor) { + [NSEvent removeMonitor:_globalMonitor]; + _globalMonitor = nil; + } + } + + return ok; +} + +@end diff --git a/mut/dmenu-mac/src/3rd-party/DDHotKeyUtilities.h b/mut/dmenu-mac/src/3rd-party/DDHotKeyUtilities.h new file mode 100644 index 0000000..54b25a4 --- /dev/null +++ b/mut/dmenu-mac/src/3rd-party/DDHotKeyUtilities.h @@ -0,0 +1,14 @@ +/* + DDHotKey -- DDHotKeyUtilities.h + + Copyright (c) Dave DeLong <http://www.davedelong.com> + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import <Foundation/Foundation.h> + +extern NSString *DDStringFromKeyCode(unsigned short keyCode, NSUInteger modifiers); +extern UInt32 DDCarbonModifierFlagsFromCocoaModifiers(NSUInteger flags); diff --git a/mut/dmenu-mac/src/3rd-party/DDHotKeyUtilities.m b/mut/dmenu-mac/src/3rd-party/DDHotKeyUtilities.m new file mode 100644 index 0000000..f1a51cd --- /dev/null +++ b/mut/dmenu-mac/src/3rd-party/DDHotKeyUtilities.m @@ -0,0 +1,145 @@ +/* + DDHotKey -- DDHotKeyUtilities.m + + Copyright (c) Dave DeLong <http://www.davedelong.com> + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + The software is provided "as is", without warranty of any kind, including all implied warranties of merchantability and fitness. In no event shall the author(s) or copyright holder(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software. + */ + +#import "DDHotKeyUtilities.h" +#import <Carbon/Carbon.h> +#import <AppKit/AppKit.h> + +static NSDictionary *_DDKeyCodeToCharacterMap(void); +static NSDictionary *_DDKeyCodeToCharacterMap(void) { + static NSDictionary *keyCodeMap = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keyCodeMap = @{ + @(kVK_Return) : @"↩", + @(kVK_Tab) : @"⇥", + @(kVK_Space) : @"⎵", + @(kVK_Delete) : @"⌫", + @(kVK_Escape) : @"⎋", + @(kVK_Command) : @"⌘", + @(kVK_Shift) : @"⇧", + @(kVK_CapsLock) : @"⇪", + @(kVK_Option) : @"⌥", + @(kVK_Control) : @"⌃", + @(kVK_RightShift) : @"⇧", + @(kVK_RightOption) : @"⌥", + @(kVK_RightControl) : @"⌃", + @(kVK_VolumeUp) : @"🔊", + @(kVK_VolumeDown) : @"🔈", + @(kVK_Mute) : @"🔇", + @(kVK_Function) : @"\u2318", + @(kVK_F1) : @"F1", + @(kVK_F2) : @"F2", + @(kVK_F3) : @"F3", + @(kVK_F4) : @"F4", + @(kVK_F5) : @"F5", + @(kVK_F6) : @"F6", + @(kVK_F7) : @"F7", + @(kVK_F8) : @"F8", + @(kVK_F9) : @"F9", + @(kVK_F10) : @"F10", + @(kVK_F11) : @"F11", + @(kVK_F12) : @"F12", + @(kVK_F13) : @"F13", + @(kVK_F14) : @"F14", + @(kVK_F15) : @"F15", + @(kVK_F16) : @"F16", + @(kVK_F17) : @"F17", + @(kVK_F18) : @"F18", + @(kVK_F19) : @"F19", + @(kVK_F20) : @"F20", + // @(kVK_Help) : @"", + @(kVK_ForwardDelete) : @"⌦", + @(kVK_Home) : @"↖", + @(kVK_End) : @"↘", + @(kVK_PageUp) : @"⇞", + @(kVK_PageDown) : @"⇟", + @(kVK_LeftArrow) : @"←", + @(kVK_RightArrow) : @"→", + @(kVK_DownArrow) : @"↓", + @(kVK_UpArrow) : @"↑", + }; + }); + return keyCodeMap; +} + +NSString *DDStringFromKeyCode(unsigned short keyCode, NSUInteger modifiers) { + NSMutableString *final = [NSMutableString stringWithString:@""]; + NSDictionary *characterMap = _DDKeyCodeToCharacterMap(); + + if (modifiers & NSEventModifierFlagControl) { + [final appendString:[characterMap objectForKey:@(kVK_Control)]]; + } + if (modifiers & NSEventModifierFlagOption) { + [final appendString:[characterMap objectForKey:@(kVK_Option)]]; + } + if (modifiers & NSEventModifierFlagShift) { + [final appendString:[characterMap objectForKey:@(kVK_Shift)]]; + } + if (modifiers & NSEventModifierFlagCommand) { + [final appendString:[characterMap objectForKey:@(kVK_Command)]]; + } + + if (keyCode == kVK_Control || keyCode == kVK_Option || keyCode == kVK_Shift || keyCode == kVK_Command) { + return final; + } + + NSString *mapped = [characterMap objectForKey:@(keyCode)]; + if (mapped != nil) { + [final appendString:mapped]; + } else { + + TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource(); + CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); + + // Fix crash using non-unicode layouts, such as Chinese or Japanese. + if (!uchr) { + CFRelease(currentKeyboard); + currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); + } + + const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr); + + if (keyboardLayout) { + UInt32 deadKeyState = 0; + UniCharCount maxStringLength = 255; + UniCharCount actualStringLength = 0; + UniChar unicodeString[maxStringLength]; + + UInt32 keyModifiers = DDCarbonModifierFlagsFromCocoaModifiers(modifiers); + + OSStatus status = UCKeyTranslate(keyboardLayout, + keyCode, kUCKeyActionDown, keyModifiers, + LMGetKbdType(), 0, + &deadKeyState, + maxStringLength, + &actualStringLength, unicodeString); + + if (actualStringLength > 0 && status == noErr) { + NSString *characterString = [NSString stringWithCharacters:unicodeString length:(NSUInteger)actualStringLength]; + + [final appendString:characterString]; + } + } + } + + return final; +} + +UInt32 DDCarbonModifierFlagsFromCocoaModifiers(NSUInteger flags) { + UInt32 newFlags = 0; + if ((flags & NSEventModifierFlagControl) > 0) { newFlags |= controlKey; } + if ((flags & NSEventModifierFlagCommand) > 0) { newFlags |= cmdKey; } + if ((flags & NSEventModifierFlagShift) > 0) { newFlags |= shiftKey; } + if ((flags & NSEventModifierFlagOption) > 0) { newFlags |= optionKey; } + if ((flags & NSEventModifierFlagCapsLock) > 0) { newFlags |= alphaLock; } + return newFlags; +} diff --git a/mut/dmenu-mac/src/AppDelegate.swift b/mut/dmenu-mac/src/AppDelegate.swift new file mode 100644 index 0000000..5c115b3 --- /dev/null +++ b/mut/dmenu-mac/src/AppDelegate.swift @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa +import Carbon +import LaunchAtLogin +import Preferences +import KeyboardShortcuts + +extension Settings.PaneIdentifier { + static let general = Self("general") +} + +// import legacy settings if they existed +let kLegacyKc = "kDefaultsGlobalShortcutKeycode" +let kLegacyMf = "kDefaultsGlobalShortcutModifiedFlags" + +extension KeyboardShortcuts.Name { + static let activateSearch = Self("activateSearchGlobalShortcut", default: .init( + (UserDefaults.standard.object(forKey: kLegacyKc) != nil) ? + KeyboardShortcuts.Key(rawValue: UserDefaults.standard.integer(forKey: kLegacyKc)): + .space, + modifiers: (UserDefaults.standard.object(forKey: kLegacyKc) != nil) ? + NSEvent.ModifierFlags(rawValue: UInt(UserDefaults.standard.integer(forKey: kLegacyMf))) : + [.command])) +} + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { + @IBOutlet var controllerWindow: NSWindowController? + + private var statusItem: NSStatusItem! + private var startAtLaunch: NSMenuItem! + + func applicationDidFinishLaunching(_ aNotification: Notification) { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + + if let button = statusItem.button { + button.title = "d" + } + setupMenus() + } + + func applicationWillTerminate(_ aNotification: Notification) { + } + + func setupMenus() { + let menu = NSMenu() + + let open = NSMenuItem(title: "Open", action: #selector(resumeApp), keyEquivalent: "") + menu.addItem(open) + + let settings = NSMenuItem(title: "Settings", action: #selector(openSettings), keyEquivalent: "") + menu.addItem(settings) + + menu.addItem(NSMenuItem.separator()) + startAtLaunch = NSMenuItem(title: "Launch at Login", action: #selector(toggleLaunchAtLogin), keyEquivalent: "") + startAtLaunch.state = LaunchAtLogin.isEnabled ? .on : .off + menu.addItem(startAtLaunch) + + menu.addItem(NSMenuItem.separator()) + + menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")) + + statusItem.menu = menu + } + + @objc func resumeApp() { + let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: Bundle.main) + // swiftlint:disable force_cast + let mainPageController = storyboard.instantiateController( + withIdentifier: "SearchViewController") as! SearchViewController + // swiftlint:enable force_cast + mainPageController.resumeApp() + } + + @objc func openSettings() { + settingsWindowController.show() + } + + @objc func toggleLaunchAtLogin() { + let enabled = !LaunchAtLogin.isEnabled + LaunchAtLogin.isEnabled = enabled + startAtLaunch.state = enabled ? .on : .off + } + + private lazy var settings: [SettingsPane] = [ + GeneralSettingsViewController() + ] + + private lazy var settingsWindowController = SettingsWindowController( + preferencePanes: settings, + style: .segmentedControl, + animated: true, + hidesToolbarForSingleItem: true + ) +} diff --git a/mut/dmenu-mac/src/AppListProvider.swift b/mut/dmenu-mac/src/AppListProvider.swift new file mode 100644 index 0000000..7aa3ad8 --- /dev/null +++ b/mut/dmenu-mac/src/AppListProvider.swift @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Foundation +import FileWatcher +import Fuse + +/** + * Provide a list of launcheable apps for the OS + */ +class AppListProvider: ListProvider { + + var appDirDict = [String: Bool]() + + var appList = [URL]() + + init() { + let applicationDir = NSSearchPathForDirectoriesInDomains( + .applicationDirectory, .localDomainMask, true)[0] + + // Catalina moved default applications under a different mask. + let systemApplicationDir = NSSearchPathForDirectoriesInDomains( + .applicationDirectory, .systemDomainMask, true)[0] + + // appName to dir recursivity key/valye dict + appDirDict[applicationDir] = true + appDirDict[systemApplicationDir] = true + appDirDict["/System/Library/CoreServices/"] = false + + initFileWatch(Array(appDirDict.keys)) + updateAppList() + } + + func initFileWatch(_ dirs: [String]) { + let filewatcher = FileWatcher(dirs) + filewatcher.callback = {_ in + self.updateAppList() + } + filewatcher.start() + } + + func updateAppList() { + var newAppList = [URL]() + appDirDict.keys.forEach { path in + let urlPath = URL(fileURLWithPath: path, isDirectory: true) + let list = getAppList(urlPath, recursive: appDirDict[path]!) + newAppList.append(contentsOf: list) + } + appList = newAppList + } + + func getAppList(_ appDir: URL, recursive: Bool = true) -> [URL] { + var list = [URL]() + let fileManager = FileManager.default + + do { + let subs = try fileManager.contentsOfDirectory(atPath: appDir.path) + + for sub in subs { + let dir = appDir.appendingPathComponent(sub) + + if dir.pathExtension == "app" { + list.append(dir) + } else if dir.hasDirectoryPath && recursive { + list.append(contentsOf: self.getAppList(dir)) + } + } + } catch { + NSLog("Error on getAppList: %@", error.localizedDescription) + } + return list + } + + func get() -> [ListItem] { + return appList.map({ListItem(name: $0.deletingPathExtension().lastPathComponent, data: $0)}) + } + + func doAction(item: ListItem) { + guard let app: URL = item.data as? URL else { + NSLog("Cannot do action on item \(item.name)") + return + } + DispatchQueue.main.async { + NSWorkspace.shared.open(app) + } + } +} diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/Contents.json b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..b32bc8b --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x", + "filename" : "icon_16@1x.png" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x", + "filename" : "icon_16@2x.png" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x", + "filename" : "icon_32@1x.png" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x", + "filename" : "icon_32@2x.png" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x", + "filename" : "icon_128@1x.png" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x", + "filename" : "icon_128@2x.png" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x", + "filename" : "icon_256@1x.png" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x", + "filename" : "icon_256@2x.png" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x", + "filename" : "icon_512@1x.png" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x", + "filename" : "icon_512@2x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +}
\ No newline at end of file diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png Binary files differnew file mode 100644 index 0000000..57a791f --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_128@1x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png Binary files differnew file mode 100644 index 0000000..d508b78 --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_128@2x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png Binary files differnew file mode 100644 index 0000000..b765709 --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_16@1x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png Binary files differnew file mode 100644 index 0000000..b6c8724 --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_16@2x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png Binary files differnew file mode 100644 index 0000000..d508b78 --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_256@1x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png Binary files differnew file mode 100644 index 0000000..106eada --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_256@2x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png Binary files differnew file mode 100644 index 0000000..b6c8724 --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_32@1x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png Binary files differnew file mode 100644 index 0000000..7e80008 --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_32@2x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png Binary files differnew file mode 100644 index 0000000..106eada --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_512@1x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png Binary files differnew file mode 100644 index 0000000..a38e6bd --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/AppIcon.appiconset/icon_512@2x.png diff --git a/mut/dmenu-mac/src/Assets.xcassets/Contents.json b/mut/dmenu-mac/src/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/mut/dmenu-mac/src/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mut/dmenu-mac/src/Base.lproj/GeneralSettingsViewController.xib b/mut/dmenu-mac/src/Base.lproj/GeneralSettingsViewController.xib new file mode 100644 index 0000000..1b30b58 --- /dev/null +++ b/mut/dmenu-mac/src/Base.lproj/GeneralSettingsViewController.xib @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/> + <capability name="NSView safe area layout guides" minToolsVersion="12.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <objects> + <customObject id="-2" userLabel="File's Owner" customClass="GeneralSettingsViewController" customModule="dmenu_mac" customModuleProvider="target"> + <connections> + <outlet property="customView" destination="4nh-zO-SVY" id="voW-MV-1d4"/> + <outlet property="view" destination="c22-O7-iKe" id="ZQe-UH-LCX"/> + </connections> + </customObject> + <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> + <customObject id="-3" userLabel="Application" customClass="NSObject"/> + <customView wantsLayer="YES" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c22-O7-iKe"> + <rect key="frame" x="0.0" y="0.0" width="510" height="65"/> + <subviews> + <customView autoresizesSubviews="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4nh-zO-SVY"> + <rect key="frame" x="210" y="20" width="300" height="25"/> + <constraints> + <constraint firstAttribute="height" constant="25" id="Bgf-ZC-F3a"/> + <constraint firstAttribute="width" constant="300" id="DUS-S2-npV"/> + </constraints> + <shadow key="shadow"> + <color key="color" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </shadow> + <viewLayoutGuide key="safeArea" id="yaK-ue-AZE"/> + <viewLayoutGuide key="layoutMargins" id="Le8-6r-xay"/> + </customView> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Vt6-jC-8Jo"> + <rect key="frame" x="18" y="25" width="186" height="16"/> + <textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Global Shortcut" id="uKy-ok-dDi"> + <font key="font" usesAppearanceFont="YES"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + </subviews> + <constraints> + <constraint firstAttribute="trailing" secondItem="4nh-zO-SVY" secondAttribute="trailing" id="ANe-tV-l64"/> + <constraint firstItem="4nh-zO-SVY" firstAttribute="leading" secondItem="Vt6-jC-8Jo" secondAttribute="trailing" constant="8" symbolic="YES" id="Czy-5O-KIG"/> + <constraint firstItem="4nh-zO-SVY" firstAttribute="top" secondItem="c22-O7-iKe" secondAttribute="top" constant="20" symbolic="YES" id="El7-4y-U6x"/> + <constraint firstItem="Vt6-jC-8Jo" firstAttribute="centerY" secondItem="4nh-zO-SVY" secondAttribute="centerY" id="Vbd-sJ-pbv"/> + <constraint firstItem="4nh-zO-SVY" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="bEk-HF-Wfw"/> + <constraint firstItem="Vt6-jC-8Jo" firstAttribute="centerY" secondItem="c22-O7-iKe" secondAttribute="centerY" id="y8M-U2-sKU"/> + <constraint firstItem="Vt6-jC-8Jo" firstAttribute="leading" secondItem="c22-O7-iKe" secondAttribute="leading" constant="20" symbolic="YES" id="yXa-2Y-xkG"/> + </constraints> + <point key="canvasLocation" x="71" y="294.5"/> + </customView> + </objects> +</document> diff --git a/mut/dmenu-mac/src/Base.lproj/Main.storyboard b/mut/dmenu-mac/src/Base.lproj/Main.storyboard new file mode 100644 index 0000000..6fc3e41 --- /dev/null +++ b/mut/dmenu-mac/src/Base.lproj/Main.storyboard @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS"> + <dependencies> + <deployment identifier="macosx"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Application--> + <scene sceneID="zQX-TL-VWj"> + <objects> + <application id="gxZ-1U-eOM" sceneMemberID="viewController"> + <menu key="mainMenu" title="Main Menu" systemMenu="main" id="dDP-cD-PHO"/> + <connections> + <outlet property="delegate" destination="xgv-xq-kjR" id="r8A-cU-Ure"/> + </connections> + </application> + <customObject id="xgv-xq-kjR" customClass="AppDelegate" customModule="dmenu_mac" customModuleProvider="target"/> + <customObject id="RwB-ew-8eE" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="75" y="0.0"/> + </scene> + <!--Window Controller--> + <scene sceneID="R2V-B0-nI4"> + <objects> + <windowController storyboardIdentifier="SearchWindowController" id="B8D-0N-5wS" sceneMemberID="viewController"> + <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" animationBehavior="default" id="IQv-IB-iLA" customClass="SearchWindow" customModule="dmenu_mac" customModuleProvider="target"> + <rect key="contentRect" x="196" y="0.0" width="485" height="25"/> + <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/> + <connections> + <outlet property="delegate" destination="B8D-0N-5wS" id="WoP-cW-Qfs"/> + </connections> + </window> + <connections> + <segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/> + </connections> + </windowController> + <customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="74.5" y="249.5"/> + </scene> + <!--Search View Controller--> + <scene sceneID="hIz-AP-VOD"> + <objects> + <viewController storyboardIdentifier="SearchViewController" id="XfG-lQ-9wD" customClass="SearchViewController" customModule="dmenu_mac" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" wantsLayer="YES" id="m2S-Jp-Qdl"> + <rect key="frame" x="0.0" y="0.0" width="629" height="31"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <textField focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Fjb-7T-x1e" customClass="InputField" customModule="dmenu_mac" customModuleProvider="target"> + <rect key="frame" x="10" y="0.0" width="150" height="31"/> + <constraints> + <constraint firstAttribute="width" constant="150" id="gz9-SZ-TTB"/> + <constraint firstAttribute="height" constant="31" id="oAG-TV-bUz"/> + </constraints> + <textFieldCell key="cell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" focusRingType="none" alignment="left" placeholderString="Search" drawsBackground="YES" id="pMg-YR-zKP" customClass="VerticalAlignedTextFieldCell" customModule="dmenu_mac" customModuleProvider="target"> + <font key="font" metaFont="system"/> + <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" white="1" alpha="0.0" colorSpace="deviceWhite"/> + </textFieldCell> + </textField> + <scrollView fixedFrame="YES" borderType="none" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="QVh-If-BUC"> + <rect key="frame" x="174" y="0.0" width="416" height="31"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <clipView key="contentView" autoresizesSubviews="NO" drawsBackground="NO" copiesOnScroll="NO" id="zRh-Jt-rH1"> + <rect key="frame" x="0.0" y="0.0" width="416" height="31"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <view id="5lG-jR-0ih" customClass="ResultsView" customModule="dmenu_mac" customModuleProvider="target"> + <rect key="frame" x="0.0" y="-3674" width="418" height="31"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <connections> + <outlet property="scrollView" destination="QVh-If-BUC" id="PXN-Pz-YRx"/> + </connections> + </view> + </subviews> + <color key="backgroundColor" red="0.11764705882352941" green="0.11764705882352941" blue="0.11764705882352941" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/> + </clipView> + <scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="uQ2-nO-u0R"> + <rect key="frame" x="-100" y="-100" width="181" height="16"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="Czm-aK-GgP"> + <rect key="frame" x="-100" y="-100" width="16" height="94"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + </scrollView> + </subviews> + <constraints> + <constraint firstAttribute="bottom" secondItem="Fjb-7T-x1e" secondAttribute="bottom" id="J9X-bX-kJ0"/> + <constraint firstItem="Fjb-7T-x1e" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="x2n-mW-u0G"/> + <constraint firstItem="Fjb-7T-x1e" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="10" id="yFY-Az-NlG"/> + </constraints> + </view> + <connections> + <outlet property="resultsText" destination="5lG-jR-0ih" id="bRp-pM-yOg"/> + <outlet property="searchText" destination="Fjb-7T-x1e" id="dUT-7x-0zh"/> + </connections> + </viewController> + <customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="203.5" y="481.5"/> + </scene> + </scenes> +</document> diff --git a/mut/dmenu-mac/src/BridgingHeader.h b/mut/dmenu-mac/src/BridgingHeader.h new file mode 100644 index 0000000..82e9a72 --- /dev/null +++ b/mut/dmenu-mac/src/BridgingHeader.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef BridgingHeader_h +#define BridgingHeader_h + +#import "DDHotKeyCenter.h" +#import "DDHotKeyTextField.h" +#import "ReadStdin.h" + +#endif /* BridgingHeader_h */ diff --git a/mut/dmenu-mac/src/CommandArguments.swift b/mut/dmenu-mac/src/CommandArguments.swift new file mode 100644 index 0000000..7031ce9 --- /dev/null +++ b/mut/dmenu-mac/src/CommandArguments.swift @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import ArgumentParser + +struct DmenuMac: ParsableArguments { + @Option(name: .shortAndLong, help: "Show a prompt instead of the search input.") + var prompt: String? +} diff --git a/mut/dmenu-mac/src/GeneralSettingsViewController.swift b/mut/dmenu-mac/src/GeneralSettingsViewController.swift new file mode 100644 index 0000000..83d73e1 --- /dev/null +++ b/mut/dmenu-mac/src/GeneralSettingsViewController.swift @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa +import Preferences +import KeyboardShortcuts + +final class GeneralSettingsViewController: NSViewController, SettingsPane { + let preferencePaneIdentifier = Settings.PaneIdentifier.general + let preferencePaneTitle = "General" + let toolbarItemIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "General settings")! + + @IBOutlet weak var customView: NSView? + + override var nibName: NSNib.Name? { "GeneralSettingsViewController" } + + override func loadView() { + super.loadView() + + let recorder = KeyboardShortcuts.RecorderCocoa(for: .activateSearch) + recorder.frame = CGRect(x: 0, y: 0, width: 150, height: 25) + customView?.addSubview(recorder) + + } +} diff --git a/mut/dmenu-mac/src/Info.plist b/mut/dmenu-mac/src/Info.plist new file mode 100644 index 0000000..e55981b --- /dev/null +++ b/mut/dmenu-mac/src/Info.plist @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIconFile</key> + <string></string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>0.7.2</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>0.7.2</string> + <key>LSApplicationCategoryType</key> + <string>public.app-category.utilities</string> + <key>LSMinimumSystemVersion</key> + <string>$(MACOSX_DEPLOYMENT_TARGET)</string> + <key>LSUIElement</key> + <true/> + <key>NSHumanReadableCopyright</key> + <string>Copyright © 2016 Jose Pereira. All rights reserved.</string> + <key>NSMainStoryboardFile</key> + <string>Main</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist> diff --git a/mut/dmenu-mac/src/InputField.swift b/mut/dmenu-mac/src/InputField.swift new file mode 100644 index 0000000..31fb8dd --- /dev/null +++ b/mut/dmenu-mac/src/InputField.swift @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Foundation + +class InputField: NSTextField { + override func becomeFirstResponder() -> Bool { + let responderStatus = super.becomeFirstResponder() + + if let fieldEditor = self.window?.fieldEditor(true, for: self) as? NSTextView { + fieldEditor.selectedTextAttributes = [ + // Make selection transparent + NSAttributedString.Key.backgroundColor: NSColor.clear + ] + // Make blinking cursos transparent + fieldEditor.insertionPointColor = NSColor.clear + } + + return responderStatus + } +} diff --git a/mut/dmenu-mac/src/ListProvider.swift b/mut/dmenu-mac/src/ListProvider.swift new file mode 100644 index 0000000..e895414 --- /dev/null +++ b/mut/dmenu-mac/src/ListProvider.swift @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Foundation + +protocol ListProvider { + // Returns list of items + func get() -> [ListItem] + + // Performs action on a selected item + func doAction(item: ListItem) +} + +struct ListItem { + var name: String + var data: Any? +} diff --git a/mut/dmenu-mac/src/Notification+Name.swift b/mut/dmenu-mac/src/Notification+Name.swift new file mode 100644 index 0000000..0abaffb --- /dev/null +++ b/mut/dmenu-mac/src/Notification+Name.swift @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Foundation + +extension Notification.Name { + static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification") +} diff --git a/mut/dmenu-mac/src/PipeListProvider.swift b/mut/dmenu-mac/src/PipeListProvider.swift new file mode 100644 index 0000000..32a678a --- /dev/null +++ b/mut/dmenu-mac/src/PipeListProvider.swift @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Foundation + +/** + * Provide a list from a terminal pipe. When action is performed, quit app since we act like a prompt + */ +class PipeListProvider: ListProvider { + var choices = [String]() + + init(str: String) { + choices = str.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: "\n") + } + + func get() -> [ListItem] { + return choices.map({ListItem(name: $0, data: nil)}) + } + + func doAction(item: ListItem) { + print(item.name) + NSApplication.shared.terminate(self) + } +} diff --git a/mut/dmenu-mac/src/ReadStdin.h b/mut/dmenu-mac/src/ReadStdin.h new file mode 100644 index 0000000..94b6744 --- /dev/null +++ b/mut/dmenu-mac/src/ReadStdin.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface ReadStdin : NSObject + ++(NSString *)read; +@end + +NS_ASSUME_NONNULL_END diff --git a/mut/dmenu-mac/src/ReadStdin.h.m b/mut/dmenu-mac/src/ReadStdin.h.m new file mode 100644 index 0000000..08b4e19 --- /dev/null +++ b/mut/dmenu-mac/src/ReadStdin.h.m @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#import "ReadStdin.h" + +#import <poll.h> + +@implementation ReadStdin + ++(NSString *)read { + char buf[BUFSIZ]; + + // prevent fgets from being blocked + int flags; + flags = fcntl(STDIN_FILENO, F_GETFL, 0); + flags |= O_NONBLOCK; + fcntl(STDIN_FILENO, F_SETFL, flags); + + NSMutableString *str = [NSMutableString string]; + while (fgets(buf, sizeof(BUFSIZ), stdin) != 0) { + [str appendString:[NSString stringWithUTF8String:buf]]; + } + + return str; +} + +@end diff --git a/mut/dmenu-mac/src/ResultsView.swift b/mut/dmenu-mac/src/ResultsView.swift new file mode 100644 index 0000000..075e87a --- /dev/null +++ b/mut/dmenu-mac/src/ResultsView.swift @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +class ResultsView: NSView { + @IBOutlet fileprivate var scrollView: NSScrollView! + + let rectFillPadding: CGFloat = 5 + var resultsList: [ListItem] = [] + + var dirtyWidth: Bool = false + var selectedRect = NSRect() + + var selectedIndexValue: Int = 0 + var selectedIndex: Int { + get { + return selectedIndexValue + } + set { + if newValue < 0 || newValue >= resultsList.count { + return + } + + selectedIndexValue = newValue + needsDisplay = true + } + } + + var list: [ListItem] { + get { + return resultsList + } + set { + selectedIndexValue = 0 + resultsList = newValue + needsDisplay = true + } + } + + func selectedItem() -> ListItem? { + if selectedIndexValue < 0 || selectedIndexValue >= resultsList.count { + return nil + } else { + return resultsList[selectedIndexValue] + } + } + + func clear() { + resultsList.removeAll() + needsDisplay = true + } + + override func draw(_ dirtyRect: NSRect) { + var textX = CGFloat(rectFillPadding) + let drawList = list.count > 0 ? list : [ListItem(name: "No results", data: nil)] + + for i in 0 ..< drawList.count { + let item = (drawList[i].name) as NSString + let size = item.size(withAttributes: [NSAttributedString.Key: Any]()) + let textY = (frame.height - size.height) / 2 + + if selectedIndexValue == i { + selectedRect = NSRect( + x: textX - rectFillPadding, + y: textY - rectFillPadding, + width: size.width + rectFillPadding * 2, + height: size.height + rectFillPadding * 2) + NSColor.selectedTextBackgroundColor.setFill() + __NSRectFill(selectedRect) + } + + item.draw(in: NSRect( + x: textX, + y: textY, + width: size.width, + height: size.height), withAttributes: [ + NSAttributedString.Key.foregroundColor: NSColor.textColor + ]) + + textX += 10 + size.width + } + if dirtyWidth { + frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: textX, height: frame.height) + dirtyWidth = false + scrollView.contentView.scrollToVisible(selectedRect) + } + } + + func updateWidth() { + dirtyWidth = true + } +} diff --git a/mut/dmenu-mac/src/SearchViewController.swift b/mut/dmenu-mac/src/SearchViewController.swift new file mode 100644 index 0000000..592937d --- /dev/null +++ b/mut/dmenu-mac/src/SearchViewController.swift @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Carbon +import Cocoa +import Fuse +import KeyboardShortcuts + +class SearchViewController: NSViewController, NSTextFieldDelegate, NSWindowDelegate { + + @IBOutlet fileprivate var searchText: InputField! + @IBOutlet fileprivate var resultsText: ResultsView! + var hotkey: DDHotKey? + var listProvider: ListProvider? + var promptValue = "" + + override func viewDidLoad() { + super.viewDidLoad() + searchText.delegate = self + + KeyboardShortcuts.onKeyUp(for: .activateSearch) { [self] in + resumeApp() + } + + DistributedNotificationCenter.default.addObserver( + self, + selector: #selector(interfaceModeChanged), + name: .AppleInterfaceThemeChangedNotification, + object: nil + ) + + let stdinStr = ReadStdin.read() + if stdinStr.count > 0 { + listProvider = PipeListProvider(str: stdinStr) + } else { + listProvider = AppListProvider() + } + + let options = DmenuMac.parseOrExit() + if options.prompt != nil { + promptValue = options.prompt! + } + + clearFields() + resumeApp() + } + + @objc func interfaceModeChanged(sender: NSNotification) { + updateColors() + } + + func updateColors() { + guard let window = NSApp.windows.first else { return } + + window.isOpaque = false + window.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.6) + searchText.textColor = NSColor.textColor + } + + @objc func resumeApp() { + NSApplication.shared.activate(ignoringOtherApps: true) + view.window?.orderFrontRegardless() + + if let controller = view.window as? SearchWindow { + controller.updatePosition() + } + + updateColors() + } + + func controlTextDidChange(_ obj: Notification) { + if searchText.stringValue == "" { + clearFields() + return + } + + // Get provider list, filter using fuzzy search, apply + var scoreDict = [Int: Double]() + + let fuse = Fuse(threshold: 0.4) + let pattern = fuse.createPattern(from: searchText.stringValue) + + let list = listProvider?.get() ?? [] + + for (idx, item) in list.enumerated() { + guard let result = fuse.search(pattern, in: item.name) else { + continue + } + scoreDict[idx] = result.score + } + + let sortedScoreDict = scoreDict.sorted(by: {$0.1 < $1.1}).map({list[$0.0]}) + if !sortedScoreDict.isEmpty { + self.resultsText.list = sortedScoreDict + } else { + self.resultsText.clear() + } + + self.resultsText.updateWidth() + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + let movingLeft: Bool = + commandSelector == #selector(moveLeft(_:)) || + commandSelector == #selector(insertBacktab(_:)) + let movingRight: Bool = + commandSelector == #selector(moveRight(_:)) || + commandSelector == #selector(insertTab(_:)) + + if movingLeft { + resultsText.selectedIndex = resultsText.selectedIndex == 0 ? + resultsText.list.count - 1 : resultsText.selectedIndex - 1 + resultsText.updateWidth() + return true + } else if movingRight { + resultsText.selectedIndex = (resultsText.selectedIndex + 1) % resultsText.list.count + resultsText.updateWidth() + return true + } else if commandSelector == #selector(insertNewline(_:)) { + // open current selected app + if let item = resultsText.selectedItem() { + listProvider?.doAction(item: item) + closeApp() + } + + return true + } else if commandSelector == #selector(cancelOperation(_:)) { + closeApp() + return true + } + + return false + } + + func clearFields() { + self.searchText.stringValue = promptValue + self.resultsText.list = listProvider?.get().sorted(by: {$0.name < $1.name}) ?? [] + } + + func closeApp() { + clearFields() + if promptValue == "" { + NSApplication.shared.hide(nil) + } + } +} diff --git a/mut/dmenu-mac/src/SearchWindow.swift b/mut/dmenu-mac/src/SearchWindow.swift new file mode 100644 index 0000000..ca0657e --- /dev/null +++ b/mut/dmenu-mac/src/SearchWindow.swift @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +class SearchWindow: NSWindow { + + override func awakeFromNib() { + self.hasShadow = false + self.collectionBehavior = NSWindow.CollectionBehavior.canJoinAllSpaces + updatePosition() + } + + /** + * Updates search window position. + */ + func updatePosition() { + guard let screen = NSScreen.main else { return } + + let frame = NSRect( + x: screen.frame.minX, + y: screen.frame.minY + screen.frame.height - self.frame.height, + width: screen.frame.width, + height: self.frame.height) + + setFrame(frame, display: false) + } + + override var canBecomeKey: Bool { + return true + } + + override var canBecomeMain: Bool { + return true + } +} diff --git a/mut/dmenu-mac/src/SettingsViewController.swift b/mut/dmenu-mac/src/SettingsViewController.swift new file mode 100644 index 0000000..12cd449 --- /dev/null +++ b/mut/dmenu-mac/src/SettingsViewController.swift @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +public protocol SettingsViewControllerDelegate: AnyObject { + func onSettingsCanceled() + func onSettingsApplied() +} + +class SettingsViewController: NSViewController { + + @IBOutlet var hotkeyTextField: DDHotKeyTextField! + weak var delegate: SettingsViewControllerDelegate? + + override func viewDidLoad() { + let keycode = UserDefaults.standard + .integer(forKey: kDefaultsGlobalShortcutKeycode) + let modifierFlags = UserDefaults.standard + .integer(forKey: kDefaultsGlobalShortcutModifiedFlags) + + hotkeyTextField.hotKey = DDHotKey( + keyCode: UInt16(keycode), + modifierFlags: UInt(modifierFlags), + task: nil) + } + + @IBAction func applySettings(_ sender: AnyObject) { + UserDefaults.standard.set( + Int(hotkeyTextField.hotKey.keyCode), forKey: kDefaultsGlobalShortcutKeycode) + UserDefaults.standard.set( + Int(hotkeyTextField.hotKey.modifierFlags), forKey: kDefaultsGlobalShortcutModifiedFlags) + + delegate?.onSettingsApplied() + } + + @IBAction func cancelSettings(_ sender: AnyObject) { + delegate?.onSettingsCanceled() + } +} diff --git a/mut/dmenu-mac/src/VerticalAlignedTextFieldCell.swift b/mut/dmenu-mac/src/VerticalAlignedTextFieldCell.swift new file mode 100644 index 0000000..a40a4ec --- /dev/null +++ b/mut/dmenu-mac/src/VerticalAlignedTextFieldCell.swift @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016 Jose Pereira <onaips@gmail.com>. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +class VerticalAlignedTextFieldCell: NSTextFieldCell { + var editingOrSelecting: Bool = false + + override func drawingRect(forBounds theRect: NSRect) -> NSRect { + var newRect = super.drawingRect(forBounds: theRect) + if !editingOrSelecting { + let textSize = self.cellSize(forBounds: theRect) + let heightDelta = newRect.size.height - textSize.height + if heightDelta > 0 { + newRect.size.height -= heightDelta + newRect.origin.y += (heightDelta / 2) + } + } + return newRect + } + + override func select(withFrame aRect: NSRect, in controlView: NSView, + editor textObj: NSText, delegate anObject: Any?, + start selStart: Int, length selLength: Int) { + let aRect = self.drawingRect(forBounds: aRect) + + editingOrSelecting = true + super.select(withFrame: aRect, + in: controlView, + editor: textObj, + delegate: anObject, + start: selStart, + length: selLength) + + editingOrSelecting = false + } + + override func edit(withFrame aRect: NSRect, in controlView: NSView, + editor textObj: NSText, delegate anObject: Any?, + event theEvent: NSEvent?) { + let aRect = self.drawingRect(forBounds: aRect) + editingOrSelecting = true + self.edit(withFrame: aRect, + in: controlView, + editor: textObj, + delegate: anObject, + event: theEvent) + editingOrSelecting = false + } +} diff --git a/mut/dwm/FUNDING.yml b/mut/dwm/FUNDING.yml new file mode 100644 index 0000000..c7c9a22 --- /dev/null +++ b/mut/dwm/FUNDING.yml @@ -0,0 +1,2 @@ +custom: ["https://lukesmith.xyz/donate.html"] +github: lukesmithxyz diff --git a/mut/dwm/LICENSE b/mut/dwm/LICENSE new file mode 100644 index 0000000..1e1b5a4 --- /dev/null +++ b/mut/dwm/LICENSE @@ -0,0 +1,38 @@ +MIT/X Consortium License + +© 2006-2019 Anselm R Garbe <anselm@garbe.ca> +© 2006-2009 Jukka Salmi <jukka at salmi dot ch> +© 2006-2007 Sander van Dijk <a dot h dot vandijk at gmail dot com> +© 2007-2011 Peter Hartlich <sgkkr at hartlich dot com> +© 2007-2009 Szabolcs Nagy <nszabolcs at gmail dot com> +© 2007-2009 Christof Musik <christof at sendfax dot de> +© 2007-2009 Premysl Hruby <dfenze at gmail dot com> +© 2007-2008 Enno Gottox Boland <gottox at s01 dot de> +© 2008 Martin Hurton <martin dot hurton at gmail dot com> +© 2008 Neale Pickett <neale dot woozle dot org> +© 2009 Mate Nagy <mnagy at port70 dot net> +© 2010-2016 Hiltjo Posthuma <hiltjo@codemadness.org> +© 2010-2012 Connor Lane Smith <cls@lubutu.com> +© 2011 Christoph Lohmann <20h@r-36.net> +© 2015-2016 Quentin Rameau <quinq@fifth.space> +© 2015-2016 Eric Pruitt <eric.pruitt@gmail.com> +© 2016-2017 Markus Teich <markus.teich@stusta.mhn.de> +© 2019-2020 Luke Smith <luke@lukesmith.xyz> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/mut/dwm/Makefile b/mut/dwm/Makefile new file mode 100644 index 0000000..fe9df31 --- /dev/null +++ b/mut/dwm/Makefile @@ -0,0 +1,52 @@ +# dwm - dynamic window manager +# See LICENSE file for copyright and license details. + +include config.mk + +SRC = drw.c dwm.c util.c +OBJ = ${SRC:.c=.o} + +all: options dwm + +options: + @echo dwm build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +.c.o: + ${CC} -c ${CFLAGS} $< + +${OBJ}: config.h config.mk + +dwm: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + +clean: + rm -f dwm ${OBJ} dwm-${VERSION}.tar.gz *.orig *.rej + +dist: clean + mkdir -p dwm-${VERSION} + cp -R LICENSE Makefile README config.mk\ + dwm.1 drw.h util.h ${SRC} transient.c dwm-${VERSION} + tar -cf dwm-${VERSION}.tar dwm-${VERSION} + gzip dwm-${VERSION}.tar + rm -rf dwm-${VERSION} + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp -f dwm ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/dwm + mkdir -p ${DESTDIR}${MANPREFIX}/man1 + sed "s/VERSION/${VERSION}/g" < dwm.1 > ${DESTDIR}${MANPREFIX}/man1/dwm.1 + chmod 644 ${DESTDIR}${MANPREFIX}/man1/dwm.1 + mkdir -p ${DESTDIR}${PREFIX}/share/dwm + cp -f larbs.mom ${DESTDIR}${PREFIX}/share/dwm + chmod 644 ${DESTDIR}${PREFIX}/share/dwm/larbs.mom + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/dwm\ + ${DESTDIR}${PREFIX}/share/dwm/larbs.mom\ + ${DESTDIR}${MANPREFIX}/man1/dwm.1 + +.PHONY: all options clean dist install uninstall diff --git a/mut/dwm/PKGBUILD b/mut/dwm/PKGBUILD new file mode 100644 index 0000000..1903c72 --- /dev/null +++ b/mut/dwm/PKGBUILD @@ -0,0 +1,44 @@ +_pkgname=dwm +pkgname=$_pkgname-larbs-git +pkgver=6.2.r1888.0ac09e0 +pkgrel=1 +pkgdesc="Luke's build of dwm" +url=https://github.com/LukeSmithxyz/dwm +arch=(i686 x86_64) +license=(MIT) +makedepends=(git) +depends=(freetype2 libx11 libxft) +optdepends=( + 'dmenu: program launcher' + 'st: terminal emulator') +provides=($_pkgname) +conflicts=($_pkgname) +source=(git+https://github.com/LukeSmithxyz/dwm) +sha256sums=('SKIP') + +pkgver() { + cd "$_pkgname" + echo "$(awk '/^VERSION =/ {print $3}' config.mk)".r"$(git rev-list --count HEAD)"."$(git rev-parse --short HEAD)" +} + +prepare() { + cd "$_pkgname" + echo "CPPFLAGS+=${CPPFLAGS}" >> config.mk + echo "CFLAGS+=${CFLAGS}" >> config.mk + echo "LDFLAGS+=${LDFLAGS}" >> config.mk + # to use a custom config.h, place it in the package directory + if [[ -f ${SRCDEST}/config.h ]]; then + cp "${SRCDEST}/config.h" . + fi +} + +build() { + cd "$_pkgname" + make X11INC=/usr/include/X11 X11LIB=/usr/lib/X11 +} + +package() { + cd "$_pkgname" + make PREFIX=/usr DESTDIR="$pkgdir" install + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/mut/dwm/README.md b/mut/dwm/README.md new file mode 100644 index 0000000..1c15d73 --- /dev/null +++ b/mut/dwm/README.md @@ -0,0 +1,36 @@ +# Luke's build of dwm + +## FAQ + +> What are the bindings? + +This is suckless, mmmbud, the source code is the documentation! Check out [config.h](config.h). + +Okay, okay, actually I keep a readme in `larbs.mom` for my whole system, including the binds here. +Press <kbd>super+F1</kbd> to view it in dwm (zathura is required for that binding). +I haven't kept `man dwm`/`dwm.1` updated though. PRs welcome on that, lol. + +## Patches and features + +- [Clickable statusbar](https://dwm.suckless.org/patches/statuscmd/) with my build of [dwmblocks](https://github.com/lukesmithxyz/dwmblocks). +- Reads [xresources](https://dwm.suckless.org/patches/xresources/) colors/variables (i.e. works with `pywal`, etc.). +- scratchpad: Accessible with <kbd>mod+shift+enter</kbd>. +- New layouts: bstack, fibonacci, deck, centered master and more. All bound to keys <kbd>super+(shift+)t/y/u/i</kbd>. +- True fullscreen (<kbd>super+f</kbd>) and prevents focus shifting. +- Windows can be made sticky (<kbd>super+s</kbd>). +- [hide vacant tags](https://dwm.suckless.org/patches/hide_vacant_tags/) hides tags with no windows. +- [stacker](https://dwm.suckless.org/patches/stacker/): Move windows up the stack manually (<kbd>super-K/J</kbd>). +- [shiftview](https://dwm.suckless.org/patches/nextprev/): Cycle through tags (<kbd>super+g/;</kbd>). +- [vanitygaps](https://dwm.suckless.org/patches/vanitygaps/): Gaps allowed across all layouts. +- [swallow patch](https://dwm.suckless.org/patches/swallow/): if a program run from a terminal would make it inoperable, it temporarily takes its place to save space. + + +## Installation for newbs + +```bash +git clone https://github.com/LukeSmithxyz/dwm.git +cd dwm +sudo make install +``` + +There is also a `PKGBUILD` usable on distributions with pacman. Run `makepkg -si` instead of `sudo make install`. diff --git a/mut/dwm/config.h b/mut/dwm/config.h new file mode 100644 index 0000000..d082b8c --- /dev/null +++ b/mut/dwm/config.h @@ -0,0 +1,337 @@ +/* See LICENSE file for copyright and license details. */ + +/* Constants */ +#define TERMINAL "ghostty" +#define TERMCLASS "Ghostty" +#define BROWSER "surf-open.sh" + +/* appearance */ +static unsigned int borderpx = 3; /* border pixel of windows */ +static unsigned int snap = 32; /* snap pixel */ +static unsigned int gappih = 20; /* horiz inner gap between windows */ +static unsigned int gappiv = 10; /* vert inner gap between windows */ +static unsigned int gappoh = 10; /* horiz outer gap between windows and screen edge */ +static unsigned int gappov = 30; /* vert outer gap between windows and screen edge */ +static int swallowfloating = 0; /* 1 means swallow floating windows by default */ +static int smartgaps = 0; /* 1 means no outer gap when there is only one window */ +static int showbar = 1; /* 0 means no bar */ +static int topbar = 1; /* 0 means bottom bar */ +static char *fonts[] = { "monospace:size=10", "NotoColorEmoji:pixelsize=10:antialias=true:autohint=true" }; +static char normbgcolor[] = "#222222"; +static char normbordercolor[] = "#444444"; +static char normfgcolor[] = "#bbbbbb"; +static char selfgcolor[] = "#eeeeee"; +static char selbordercolor[] = "#17293d"; +static char selbgcolor[] = "#005577"; +static char *colors[][3] = { + /* fg bg border */ + [SchemeNorm] = { normfgcolor, normbgcolor, normbordercolor }, + [SchemeSel] = { selfgcolor, selbgcolor, selbordercolor }, +}; + +typedef struct { + const char *name; + const void *cmd; +} Sp; +const char *spcmd1[] = {TERMINAL, "-n", "spterm", "-g", "120x34", NULL }; +const char *spcmd2[] = {TERMINAL, "-n", "spcalc", "-f", "monospace:size=16", "-g", "50x20", "-e", "bc", "-lq", NULL }; +static Sp scratchpads[] = { + /* name cmd */ + {"spterm", spcmd1}, + {"spcalc", spcmd2}, +}; + +/* tagging */ +static const char *tags[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9" }; + +static const Rule rules[] = { + /* xprop(1): + * WM_CLASS(STRING) = instance, class + * WM_NAME(STRING) = title + */ + /* class instance title tags mask isfloating isterminal noswallow monitor */ + { "Wfica", NULL, NULL, 0, 0, 0, 0, -1 }, + { "Gimp", NULL, NULL, 1 << 8, 0, 0, 0, -1 }, + { TERMCLASS, NULL, NULL, 0, 0, 1, 0, -1 }, + { NULL, NULL, "Event Tester", 0, 0, 0, 1, -1 }, + { TERMCLASS, "floatterm", NULL, 0, 1, 1, 0, -1 }, + { TERMCLASS, "bg", NULL, 1 << 7, 0, 1, 0, -1 }, + { TERMCLASS, "spterm", NULL, SPTAG(0), 1, 1, 0, -1 }, + { TERMCLASS, "spcalc", NULL, SPTAG(1), 1, 1, 0, -1 }, +}; + +/* layout(s) */ +static float mfact = 0.55; /* factor of master area size [0.05..0.95] */ +static int nmaster = 1; /* number of clients in master area */ +static int resizehints = 0; /* 1 means respect size hints in tiled resizals */ +static const int lockfullscreen = 1; /* 1 will force focus on the fullscreen window */ +#define FORCE_VSPLIT 1 /* nrowgrid layout: force two clients to always split vertically */ +#include "vanitygaps.c" +static const Layout layouts[] = { + /* symbol arrange function */ + { "[]=", tile }, /* Default: Master on left, slaves on right */ + { "TTT", bstack }, /* Master on top, slaves on bottom */ + + { "[@]", spiral }, /* Fibonacci spiral */ + { "[\\]", dwindle }, /* Decreasing in size right and leftward */ + + { "[D]", deck }, /* Master on left, slaves in monocle-like mode on right */ + { "[M]", monocle }, /* All windows on top of eachother */ + + { "|M|", centeredmaster }, /* Master in middle, slaves on sides */ + { ">M>", centeredfloatingmaster }, /* Same but master floats */ + + { "><>", NULL }, /* no layout function means floating behavior */ + { NULL, NULL }, +}; + +/* key definitions */ +#define MODKEY Mod4Mask +#define TAGKEYS(KEY,TAG) \ + { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ + { MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \ + { MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \ + { MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} }, +#define STACKKEYS(MOD,ACTION) \ + { MOD, XK_j, ACTION##stack, {.i = INC(+1) } }, \ + { MOD, XK_k, ACTION##stack, {.i = INC(-1) } }, \ + { MOD, XK_v, ACTION##stack, {.i = 0 } }, \ + /* { MOD, XK_grave, ACTION##stack, {.i = PREVSEL } }, \ */ + /* { MOD, XK_a, ACTION##stack, {.i = 1 } }, \ */ + /* { MOD, XK_z, ACTION##stack, {.i = 2 } }, \ */ + /* { MOD, XK_x, ACTION##stack, {.i = -1 } }, */ + +/* helper for spawning shell commands in the pre dwm-5.0 fashion */ +#define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } + +/* commands */ +static const char *termcmd[] = { TERMINAL, NULL }; + +/* + * Xresources preferences to load at startup + */ +ResourcePref resources[] = { + { "color0", STRING, &normbordercolor }, + { "color8", STRING, &selbordercolor }, + { "color0", STRING, &normbgcolor }, + { "color4", STRING, &normfgcolor }, + { "color0", STRING, &selfgcolor }, + { "color4", STRING, &selbgcolor }, + { "borderpx", INTEGER, &borderpx }, + { "snap", INTEGER, &snap }, + { "showbar", INTEGER, &showbar }, + { "topbar", INTEGER, &topbar }, + { "nmaster", INTEGER, &nmaster }, + { "resizehints", INTEGER, &resizehints }, + { "mfact", FLOAT, &mfact }, + { "gappih", INTEGER, &gappih }, + { "gappiv", INTEGER, &gappiv }, + { "gappoh", INTEGER, &gappoh }, + { "gappov", INTEGER, &gappov }, + { "swallowfloating", INTEGER, &swallowfloating }, + { "smartgaps", INTEGER, &smartgaps }, +}; + +#include <X11/XF86keysym.h> +#include "shiftview.c" + +static const Key keys[] = { + /* modifier key function argument */ + STACKKEYS(MODKEY, focus) + STACKKEYS(MODKEY|ShiftMask, push) + /* { MODKEY|ShiftMask, XK_Escape, spawn, SHCMD("") }, */ + { MODKEY, XK_grave, spawn, {.v = (const char*[]){ "dmenuunicode", NULL } } }, + /* { MODKEY|ShiftMask, XK_grave, togglescratch, SHCMD("") }, */ + TAGKEYS( XK_1, 0) + TAGKEYS( XK_2, 1) + TAGKEYS( XK_3, 2) + TAGKEYS( XK_4, 3) + TAGKEYS( XK_5, 4) + TAGKEYS( XK_6, 5) + TAGKEYS( XK_7, 6) + TAGKEYS( XK_8, 7) + TAGKEYS( XK_9, 8) + { MODKEY, XK_0, view, {.ui = ~0 } }, + { MODKEY|ShiftMask, XK_0, tag, {.ui = ~0 } }, + { MODKEY, XK_minus, spawn, SHCMD("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-; kill -44 $(pidof dwmblocks)") }, + { MODKEY|ShiftMask, XK_minus, spawn, SHCMD("wpctl set-volume @DEFAULT_AUDIO_SINK@ 15%-; kill -44 $(pidof dwmblocks)") }, + { MODKEY, XK_equal, spawn, SHCMD("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+; kill -44 $(pidof dwmblocks)") }, + { MODKEY|ShiftMask, XK_equal, spawn, SHCMD("wpctl set-volume @DEFAULT_AUDIO_SINK@ 15%+; kill -44 $(pidof dwmblocks)") }, + { MODKEY, XK_BackSpace, spawn, {.v = (const char*[]){ "sysact", NULL } } }, + { MODKEY|ShiftMask, XK_BackSpace, spawn, {.v = (const char*[]){ "sysact", NULL } } }, + + { MODKEY, XK_Tab, view, {0} }, + /* { MODKEY|ShiftMask, XK_Tab, spawn, SHCMD("") }, */ + { MODKEY, XK_q, killclient, {0} }, + { MODKEY|ShiftMask, XK_q, spawn, {.v = (const char*[]){ "sysact", NULL } } }, + { MODKEY, XK_w, spawn, {.v = (const char*[]){ BROWSER, "https://google.com", NULL } } }, + { MODKEY|ShiftMask, XK_w, spawn, {.v = (const char*[]){ TERMINAL, "-e", "sudo", "nmtui", NULL } } }, + { MODKEY, XK_e, spawn, SHCMD(TERMINAL " -e neomutt ; pkill -RTMIN+12 dwmblocks; rmdir ~/.abook 2>/dev/null") }, + { MODKEY|ShiftMask, XK_e, spawn, SHCMD(TERMINAL " -e abook -C ~/.config/abook/abookrc --datafile ~/.config/abook/addressbook") }, + { MODKEY, XK_r, spawn, {.v = (const char*[]){ TERMINAL, "-e", "lfub", NULL } } }, + { MODKEY|ShiftMask, XK_r, spawn, {.v = (const char*[]){ TERMINAL, "-e", "htop", NULL } } }, + { MODKEY, XK_t, setlayout, {.v = &layouts[0]} }, /* tile */ + { MODKEY|ShiftMask, XK_t, setlayout, {.v = &layouts[1]} }, /* bstack */ + { MODKEY, XK_y, setlayout, {.v = &layouts[2]} }, /* spiral */ + { MODKEY|ShiftMask, XK_y, setlayout, {.v = &layouts[3]} }, /* dwindle */ + { MODKEY, XK_u, setlayout, {.v = &layouts[4]} }, /* deck */ + { MODKEY|ShiftMask, XK_u, setlayout, {.v = &layouts[5]} }, /* monocle */ + { MODKEY, XK_i, setlayout, {.v = &layouts[6]} }, /* centeredmaster */ + { MODKEY|ShiftMask, XK_i, setlayout, {.v = &layouts[7]} }, /* centeredfloatingmaster */ + { MODKEY, XK_o, incnmaster, {.i = +1 } }, + { MODKEY|ShiftMask, XK_o, incnmaster, {.i = -1 } }, + { MODKEY, XK_p, spawn, {.v = (const char*[]){ "mpc", "toggle", NULL } } }, + { MODKEY|ShiftMask, XK_p, spawn, SHCMD("mpc pause; pauseallmpv") }, + { MODKEY, XK_bracketleft, spawn, {.v = (const char*[]){ "mpc", "seek", "-10", NULL } } }, + { MODKEY|ShiftMask, XK_bracketleft, spawn, {.v = (const char*[]){ "mpc", "seek", "-60", NULL } } }, + { MODKEY, XK_bracketright, spawn, {.v = (const char*[]){ "mpc", "seek", "+10", NULL } } }, + { MODKEY|ShiftMask, XK_bracketright, spawn, {.v = (const char*[]){ "mpc", "seek", "+60", NULL } } }, + { MODKEY, XK_backslash, view, {0} }, + /* { MODKEY|ShiftMask, XK_backslash, spawn, SHCMD("") }, */ + + { MODKEY, XK_a, togglegaps, {0} }, + { MODKEY|ShiftMask, XK_a, defaultgaps, {0} }, + { MODKEY, XK_s, togglesticky, {0} }, + /* { MODKEY|ShiftMask, XK_s, spawn, SHCMD("") }, */ + { MODKEY, XK_d, spawn, {.v = (const char*[]){ "dmenu_run", NULL } } }, + { MODKEY|ShiftMask, XK_d, spawn, {.v = (const char*[]){ "passmenu", NULL } } }, + { MODKEY, XK_f, togglefullscr, {0} }, + { MODKEY|ShiftMask, XK_f, setlayout, {.v = &layouts[8]} }, + { MODKEY, XK_g, shiftview, { .i = -1 } }, + { MODKEY|ShiftMask, XK_g, shifttag, { .i = -1 } }, + { MODKEY, XK_h, setmfact, {.f = -0.05} }, + /* J and K are automatically bound above in STACKEYS */ + { MODKEY, XK_l, setmfact, {.f = +0.05} }, + { MODKEY, XK_semicolon, shiftview, { .i = 1 } }, + { MODKEY|ShiftMask, XK_semicolon, shifttag, { .i = 1 } }, + { MODKEY, XK_apostrophe, togglescratch, {.ui = 1} }, + /* { MODKEY|ShiftMask, XK_apostrophe, spawn, SHCMD("") }, */ + { MODKEY|ShiftMask, XK_apostrophe, togglesmartgaps, {0} }, + { MODKEY, XK_Return, spawn, {.v = termcmd } }, + { MODKEY|ShiftMask, XK_Return, togglescratch, {.ui = 0} }, + + { MODKEY, XK_z, incrgaps, {.i = +3 } }, + /* { MODKEY|ShiftMask, XK_z, spawn, SHCMD("") }, */ + { MODKEY, XK_x, incrgaps, {.i = -3 } }, + /* { MODKEY|ShiftMask, XK_x, spawn, SHCMD("") }, */ + { MODKEY, XK_c, spawn, {.v = (const char*[]){ TERMINAL, "-e", "profanity", NULL } } }, + { MODKEY|ShiftMask, XK_c, spawn, {.v = (const char*[]){ TERMINAL, "-e", "tiny", NULL } } }, + /* V is automatically bound above in STACKKEYS */ + { MODKEY, XK_b, togglebar, {0} }, + /* { MODKEY|ShiftMask, XK_b, spawn, SHCMD("") }, */ + { MODKEY, XK_n, spawn, SHCMD(TERMINAL " -e vremote") }, + { MODKEY|ShiftMask, XK_n, spawn, SHCMD(TERMINAL " -e newsboat ; pkill -RTMIN+6 dwmblocks") }, + { MODKEY, XK_m, spawn, {.v = (const char*[]){ TERMINAL, "-e", "ncmpcpp", NULL } } }, + { MODKEY|ShiftMask, XK_m, spawn, SHCMD("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle; kill -44 $(pidof dwmblocks)") }, + { MODKEY, XK_comma, spawn, {.v = (const char*[]){ "mpc", "prev", NULL } } }, + { MODKEY|ShiftMask, XK_comma, spawn, {.v = (const char*[]){ "mpc", "seek", "0%", NULL } } }, + { MODKEY, XK_period, spawn, {.v = (const char*[]){ "mpc", "next", NULL } } }, + { MODKEY|ShiftMask, XK_period, spawn, {.v = (const char*[]){ "mpc", "repeat", NULL } } }, + + { MODKEY, XK_Left, focusmon, {.i = -1 } }, + { MODKEY|ShiftMask, XK_Left, tagmon, {.i = -1 } }, + { MODKEY, XK_Right, focusmon, {.i = +1 } }, + { MODKEY|ShiftMask, XK_Right, tagmon, {.i = +1 } }, + + { MODKEY, XK_Page_Up, shiftview, { .i = -1 } }, + { MODKEY|ShiftMask, XK_Page_Up, shifttag, { .i = -1 } }, + { MODKEY, XK_Page_Down, shiftview, { .i = +1 } }, + { MODKEY|ShiftMask, XK_Page_Down, shifttag, { .i = +1 } }, + { MODKEY, XK_Insert, spawn, SHCMD("xdotool type $(grep -v '^#' ~/.local/share/larbs/snippets | dmenu -i -l 50 | cut -d' ' -f1)") }, + + { MODKEY, XK_F1, spawn, SHCMD("groff -mom /usr/local/share/dwm/larbs.mom -Tpdf | zathura -") }, + { MODKEY, XK_F2, spawn, {.v = (const char*[]){ "tutorialvids", NULL } } }, + { MODKEY, XK_F3, spawn, {.v = (const char*[]){ "displayselect", NULL } } }, + { MODKEY, XK_F4, spawn, SHCMD(TERMINAL " -e pulsemixer; kill -44 $(pidof dwmblocks)") }, + { MODKEY, XK_F5, xrdb, {.v = NULL } }, + { MODKEY, XK_F6, spawn, {.v = (const char*[]){ "torwrap", NULL } } }, + { MODKEY, XK_F7, spawn, {.v = (const char*[]){ "td-toggle", NULL } } }, + { MODKEY, XK_F8, spawn, {.v = (const char*[]){ "mailsync", NULL } } }, + { MODKEY, XK_F9, spawn, {.v = (const char*[]){ "mounter", NULL } } }, + { MODKEY, XK_F10, spawn, {.v = (const char*[]){ "unmounter", NULL } } }, + { MODKEY, XK_F11, spawn, SHCMD("mpv --untimed --no-cache --no-osc --no-input-default-bindings --profile=low-latency --input-conf=/dev/null --title=webcam $(ls /dev/video[0,2,4,6,8] | tail -n 1)") }, + { MODKEY, XK_F12, spawn, SHCMD("remaps") }, + { MODKEY, XK_space, zoom, {0} }, + { MODKEY|ShiftMask, XK_space, togglefloating, {0} }, + + { 0, XK_Print, spawn, SHCMD("maim -q -d 0.2 -i \"$(xdotool getactivewindow)\" | xclip -f -t image/png | xclip -sel c -t image/png ") }, + { ShiftMask, XK_Print, spawn, {.v = (const char*[]){ "maimpick", NULL } } }, + { MODKEY, XK_Print, spawn, {.v = (const char*[]){ "dmenurecord", NULL } } }, + { MODKEY|ShiftMask, XK_Print, spawn, {.v = (const char*[]){ "dmenurecord", "kill", NULL } } }, + { MODKEY, XK_Delete, spawn, {.v = (const char*[]){ "dmenurecord", "kill", NULL } } }, + { MODKEY, XK_Scroll_Lock, spawn, SHCMD("killall screenkey || screenkey &") }, + + { 0, XF86XK_AudioMute, spawn, SHCMD("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle; kill -44 $(pidof dwmblocks)") }, + { 0, XF86XK_AudioRaiseVolume, spawn, SHCMD("wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%+; kill -44 $(pidof dwmblocks)") }, + { 0, XF86XK_AudioLowerVolume, spawn, SHCMD("wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%-; kill -44 $(pidof dwmblocks)") }, + { 0, XF86XK_AudioPrev, spawn, {.v = (const char*[]){ "mpc", "prev", NULL } } }, + { 0, XF86XK_AudioNext, spawn, {.v = (const char*[]){ "mpc", "next", NULL } } }, + { 0, XF86XK_AudioPause, spawn, {.v = (const char*[]){ "mpc", "pause", NULL } } }, + { 0, XF86XK_AudioPlay, spawn, {.v = (const char*[]){ "mpc", "play", NULL } } }, + { 0, XF86XK_AudioStop, spawn, {.v = (const char*[]){ "mpc", "stop", NULL } } }, + { 0, XF86XK_AudioRewind, spawn, {.v = (const char*[]){ "mpc", "seek", "-10", NULL } } }, + { 0, XF86XK_AudioForward, spawn, {.v = (const char*[]){ "mpc", "seek", "+10", NULL } } }, + { 0, XF86XK_AudioMedia, spawn, {.v = (const char*[]){ TERMINAL, "-e", "ncmpcpp", NULL } } }, + { 0, XF86XK_AudioMicMute, spawn, SHCMD("pactl set-source-mute @DEFAULT_SOURCE@ toggle") }, + /* { 0, XF86XK_PowerOff, spawn, {.v = (const char*[]){ "sysact", NULL } } }, */ + { 0, XF86XK_Calculator, spawn, {.v = (const char*[]){ TERMINAL, "-e", "bc", "-l", NULL } } }, + { 0, XF86XK_Sleep, spawn, {.v = (const char*[]){ "sudo", "-A", "zzz", NULL } } }, + { 0, XF86XK_WWW, spawn, {.v = (const char*[]){ BROWSER, NULL } } }, + { 0, XF86XK_DOS, spawn, {.v = termcmd } }, + { 0, XF86XK_ScreenSaver, spawn, SHCMD("slock & xset dpms force off; mpc pause; pauseallmpv") }, + { 0, XF86XK_TaskPane, spawn, {.v = (const char*[]){ TERMINAL, "-e", "htop", NULL } } }, + { 0, XF86XK_Mail, spawn, SHCMD(TERMINAL " -e neomutt ; pkill -RTMIN+12 dwmblocks") }, + { 0, XF86XK_MyComputer, spawn, {.v = (const char*[]){ TERMINAL, "-e", "lfub", "/", NULL } } }, + /* { 0, XF86XK_Battery, spawn, SHCMD("") }, */ + { 0, XF86XK_Launch1, spawn, {.v = (const char*[]){ "xset", "dpms", "force", "off", NULL } } }, + { 0, XF86XK_TouchpadToggle, spawn, SHCMD("(synclient | grep 'TouchpadOff.*1' && synclient TouchpadOff=0) || synclient TouchpadOff=1") }, + { 0, XF86XK_TouchpadOff, spawn, {.v = (const char*[]){ "synclient", "TouchpadOff=1", NULL } } }, + { 0, XF86XK_TouchpadOn, spawn, {.v = (const char*[]){ "synclient", "TouchpadOff=0", NULL } } }, + { 0, XF86XK_MonBrightnessUp, spawn, {.v = (const char*[]){ "xbacklight", "-inc", "15", NULL } } }, + { 0, XF86XK_MonBrightnessDown, spawn, {.v = (const char*[]){ "xbacklight", "-dec", "15", NULL } } }, + + /* { MODKEY|Mod4Mask, XK_h, incrgaps, {.i = +1 } }, */ + /* { MODKEY|Mod4Mask, XK_l, incrgaps, {.i = -1 } }, */ + /* { MODKEY|Mod4Mask|ShiftMask, XK_h, incrogaps, {.i = +1 } }, */ + /* { MODKEY|Mod4Mask|ShiftMask, XK_l, incrogaps, {.i = -1 } }, */ + /* { MODKEY|Mod4Mask|ControlMask, XK_h, incrigaps, {.i = +1 } }, */ + /* { MODKEY|Mod4Mask|ControlMask, XK_l, incrigaps, {.i = -1 } }, */ + /* { MODKEY|Mod4Mask|ShiftMask, XK_0, defaultgaps, {0} }, */ + /* { MODKEY, XK_y, incrihgaps, {.i = +1 } }, */ + /* { MODKEY, XK_o, incrihgaps, {.i = -1 } }, */ + /* { MODKEY|ControlMask, XK_y, incrivgaps, {.i = +1 } }, */ + /* { MODKEY|ControlMask, XK_o, incrivgaps, {.i = -1 } }, */ + /* { MODKEY|Mod4Mask, XK_y, incrohgaps, {.i = +1 } }, */ + /* { MODKEY|Mod4Mask, XK_o, incrohgaps, {.i = -1 } }, */ + /* { MODKEY|ShiftMask, XK_y, incrovgaps, {.i = +1 } }, */ + /* { MODKEY|ShiftMask, XK_o, incrovgaps, {.i = -1 } }, */ + +}; + +/* button definitions */ +/* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */ +static const Button buttons[] = { + /* click event mask button function argument */ +#ifndef __OpenBSD__ + { ClkWinTitle, 0, Button2, zoom, {0} }, + { ClkStatusText, 0, Button1, sigdwmblocks, {.i = 1} }, + { ClkStatusText, 0, Button2, sigdwmblocks, {.i = 2} }, + { ClkStatusText, 0, Button3, sigdwmblocks, {.i = 3} }, + { ClkStatusText, 0, Button4, sigdwmblocks, {.i = 4} }, + { ClkStatusText, 0, Button5, sigdwmblocks, {.i = 5} }, + { ClkStatusText, ShiftMask, Button1, sigdwmblocks, {.i = 6} }, +#endif + { ClkStatusText, ShiftMask, Button3, spawn, SHCMD(TERMINAL " -e nvim ~/.local/src/dwmblocks/config.h") }, + { ClkClientWin, MODKEY, Button1, movemouse, {0} }, + { ClkClientWin, MODKEY, Button2, defaultgaps, {0} }, + { ClkClientWin, MODKEY, Button3, resizemouse, {0} }, + { ClkClientWin, MODKEY, Button4, incrgaps, {.i = +1} }, + { ClkClientWin, MODKEY, Button5, incrgaps, {.i = -1} }, + { ClkTagBar, 0, Button1, view, {0} }, + { ClkTagBar, 0, Button3, toggleview, {0} }, + { ClkTagBar, MODKEY, Button1, tag, {0} }, + { ClkTagBar, MODKEY, Button3, toggletag, {0} }, + { ClkTagBar, 0, Button4, shiftview, {.i = -1} }, + { ClkTagBar, 0, Button5, shiftview, {.i = 1} }, + { ClkRootWin, 0, Button2, togglebar, {0} }, +}; diff --git a/mut/dwm/config.mk b/mut/dwm/config.mk new file mode 100644 index 0000000..0d70060 --- /dev/null +++ b/mut/dwm/config.mk @@ -0,0 +1,39 @@ +# dwm version +VERSION = 6.4 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# Xinerama, comment if you don't want it +XINERAMALIBS = -lXinerama +XINERAMAFLAGS = -DXINERAMA + +# freetype +FREETYPELIBS = -lfontconfig -lXft +FREETYPEINC = /usr/include/freetype2 +# OpenBSD (uncomment) +#MANPREFIX = ${PREFIX}/man +#FREETYPEINC = ${X11INC}/freetype2 + +# includes and libs +INCS = -I${X11INC} -I${FREETYPEINC} +LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS} -lX11-xcb -lxcb -lxcb-res + +# flags +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS} +#CFLAGS = -g -std=c99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS} +CFLAGS = -std=c99 -pedantic -Wall -Wno-deprecated-declarations -Os ${INCS} ${CPPFLAGS} +LDFLAGS = ${LIBS} + +# Solaris +#CFLAGS = -fast ${INCS} -DVERSION=\"${VERSION}\" +#LDFLAGS = ${LIBS} + +# compiler and linker +CC = cc diff --git a/mut/dwm/drw.c b/mut/dwm/drw.c new file mode 100644 index 0000000..975ca63 --- /dev/null +++ b/mut/dwm/drw.c @@ -0,0 +1,453 @@ +/* See LICENSE file for copyright and license details. */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <X11/Xlib.h> +#include <X11/Xft/Xft.h> + +#include "drw.h" +#include "util.h" + +#define UTF_INVALID 0xFFFD +#define UTF_SIZ 4 + +static const unsigned char utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +static const unsigned char utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; +static const long utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; +static const long utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + +static long +utf8decodebyte(const char c, size_t *i) +{ + for (*i = 0; *i < (UTF_SIZ + 1); ++(*i)) + if (((unsigned char)c & utfmask[*i]) == utfbyte[*i]) + return (unsigned char)c & ~utfmask[*i]; + return 0; +} + +static size_t +utf8validate(long *u, size_t i) +{ + if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) + *u = UTF_INVALID; + for (i = 1; *u > utfmax[i]; ++i) + ; + return i; +} + +static size_t +utf8decode(const char *c, long *u, size_t clen) +{ + size_t i, j, len, type; + long udecoded; + + *u = UTF_INVALID; + if (!clen) + return 0; + udecoded = utf8decodebyte(c[0], &len); + if (!BETWEEN(len, 1, UTF_SIZ)) + return 1; + for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { + udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); + if (type) + return j; + } + if (j < len) + return 0; + *u = udecoded; + utf8validate(u, len); + + return len; +} + +Drw * +drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h) +{ + Drw *drw = ecalloc(1, sizeof(Drw)); + + drw->dpy = dpy; + drw->screen = screen; + drw->root = root; + drw->w = w; + drw->h = h; + drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen)); + drw->gc = XCreateGC(dpy, root, 0, NULL); + XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter); + + return drw; +} + +void +drw_resize(Drw *drw, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + drw->w = w; + drw->h = h; + if (drw->drawable) + XFreePixmap(drw->dpy, drw->drawable); + drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen)); +} + +void +drw_free(Drw *drw) +{ + XFreePixmap(drw->dpy, drw->drawable); + XFreeGC(drw->dpy, drw->gc); + drw_fontset_free(drw->fonts); + free(drw); +} + +/* This function is an implementation detail. Library users should use + * drw_fontset_create instead. + */ +static Fnt * +xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern) +{ + Fnt *font; + XftFont *xfont = NULL; + FcPattern *pattern = NULL; + + if (fontname) { + /* Using the pattern found at font->xfont->pattern does not yield the + * same substitution results as using the pattern returned by + * FcNameParse; using the latter results in the desired fallback + * behaviour whereas the former just results in missing-character + * rectangles being drawn, at least with some fonts. */ + if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) { + fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname); + return NULL; + } + if (!(pattern = FcNameParse((FcChar8 *) fontname))) { + fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname); + XftFontClose(drw->dpy, xfont); + return NULL; + } + } else if (fontpattern) { + if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) { + fprintf(stderr, "error, cannot load font from pattern.\n"); + return NULL; + } + } else { + die("no font specified."); + } + + font = ecalloc(1, sizeof(Fnt)); + font->xfont = xfont; + font->pattern = pattern; + font->h = xfont->ascent + xfont->descent; + font->dpy = drw->dpy; + + return font; +} + +static void +xfont_free(Fnt *font) +{ + if (!font) + return; + if (font->pattern) + FcPatternDestroy(font->pattern); + XftFontClose(font->dpy, font->xfont); + free(font); +} + +Fnt* +drw_fontset_create(Drw* drw, char *fonts[], size_t fontcount) +{ + Fnt *cur, *ret = NULL; + size_t i; + + if (!drw || !fonts) + return NULL; + + for (i = 1; i <= fontcount; i++) { + if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) { + cur->next = ret; + ret = cur; + } + } + return (drw->fonts = ret); +} + +void +drw_fontset_free(Fnt *font) +{ + if (font) { + drw_fontset_free(font->next); + xfont_free(font); + } +} + +void +drw_clr_create(Drw *drw, Clr *dest, const char *clrname) +{ + if (!drw || !dest || !clrname) + return; + + if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen), + clrname, dest)) + die("error, cannot allocate color '%s'", clrname); + + dest->pixel |= 0xff << 24; +} + +/* Wrapper to create color schemes. The caller has to call free(3) on the + * returned color scheme when done using it. */ +Clr * +drw_scm_create(Drw *drw, char *clrnames[], size_t clrcount) +{ + size_t i; + Clr *ret; + + /* need at least two colors for a scheme */ + if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(XftColor)))) + return NULL; + + for (i = 0; i < clrcount; i++) + drw_clr_create(drw, &ret[i], clrnames[i]); + return ret; +} + +void +drw_setfontset(Drw *drw, Fnt *set) +{ + if (drw) + drw->fonts = set; +} + +void +drw_setscheme(Drw *drw, Clr *scm) +{ + if (drw) + drw->scheme = scm; +} + +void +drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert) +{ + if (!drw || !drw->scheme) + return; + XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel); + if (filled) + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + else + XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1); +} + +int +drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert) +{ + int i, ty, ellipsis_x = 0; + unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len; + XftDraw *d = NULL; + Fnt *usedfont, *curfont, *nextfont; + int utf8strlen, utf8charlen, render = x || y || w || h; + long utf8codepoint = 0; + const char *utf8str; + FcCharSet *fccharset; + FcPattern *fcpattern; + FcPattern *match; + XftResult result; + int charexists = 0, overflow = 0; + /* keep track of a couple codepoints for which we have no match. */ + enum { nomatches_len = 64 }; + static struct { long codepoint[nomatches_len]; unsigned int idx; } nomatches; + static unsigned int ellipsis_width = 0; + + if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts) + return 0; + + if (!render) { + w = invert ? invert : ~invert; + } else { + XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel); + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + d = XftDrawCreate(drw->dpy, drw->drawable, + DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen)); + x += lpad; + w -= lpad; + } + + usedfont = drw->fonts; + if (!ellipsis_width && render) + ellipsis_width = drw_fontset_getwidth(drw, "..."); + while (1) { + ew = ellipsis_len = utf8strlen = 0; + utf8str = text; + nextfont = NULL; + while (*text) { + utf8charlen = utf8decode(text, &utf8codepoint, UTF_SIZ); + for (curfont = drw->fonts; curfont; curfont = curfont->next) { + charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint); + if (charexists) { + drw_font_getexts(curfont, text, utf8charlen, &tmpw, NULL); + if (ew + ellipsis_width <= w) { + /* keep track where the ellipsis still fits */ + ellipsis_x = x + ew; + ellipsis_w = w - ew; + ellipsis_len = utf8strlen; + } + + if (ew + tmpw > w) { + overflow = 1; + /* called from drw_fontset_getwidth_clamp(): + * it wants the width AFTER the overflow + */ + if (!render) + x += tmpw; + else + utf8strlen = ellipsis_len; + } else if (curfont == usedfont) { + utf8strlen += utf8charlen; + text += utf8charlen; + ew += tmpw; + } else { + nextfont = curfont; + } + break; + } + } + + if (overflow || !charexists || nextfont) + break; + else + charexists = 0; + } + + if (utf8strlen) { + if (render) { + ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent; + XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg], + usedfont->xfont, x, ty, (XftChar8 *)utf8str, utf8strlen); + } + x += ew; + w -= ew; + } + if (render && overflow) + drw_text(drw, ellipsis_x, y, ellipsis_w, h, 0, "...", invert); + + if (!*text || overflow) { + break; + } else if (nextfont) { + charexists = 0; + usedfont = nextfont; + } else { + /* Regardless of whether or not a fallback font is found, the + * character must be drawn. */ + charexists = 1; + + for (i = 0; i < nomatches_len; ++i) { + /* avoid calling XftFontMatch if we know we won't find a match */ + if (utf8codepoint == nomatches.codepoint[i]) + goto no_match; + } + + fccharset = FcCharSetCreate(); + FcCharSetAddChar(fccharset, utf8codepoint); + + if (!drw->fonts->pattern) { + /* Refer to the comment in xfont_create for more information. */ + die("the first font in the cache must be loaded from a font string."); + } + + fcpattern = FcPatternDuplicate(drw->fonts->pattern); + FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue); + FcPatternAddBool(fcpattern, FC_COLOR, FcFalse); + + FcConfigSubstitute(NULL, fcpattern, FcMatchPattern); + FcDefaultSubstitute(fcpattern); + match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result); + + FcCharSetDestroy(fccharset); + FcPatternDestroy(fcpattern); + + if (match) { + usedfont = xfont_create(drw, NULL, match); + if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) { + for (curfont = drw->fonts; curfont->next; curfont = curfont->next) + ; /* NOP */ + curfont->next = usedfont; + } else { + xfont_free(usedfont); + nomatches.codepoint[++nomatches.idx % nomatches_len] = utf8codepoint; +no_match: + usedfont = drw->fonts; + } + } + } + } + if (d) + XftDrawDestroy(d); + + return x + (render ? w : 0); +} + +void +drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y); + XSync(drw->dpy, False); +} + +unsigned int +drw_fontset_getwidth(Drw *drw, const char *text) +{ + if (!drw || !drw->fonts || !text) + return 0; + return drw_text(drw, 0, 0, 0, 0, 0, text, 0); +} + +unsigned int +drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n) +{ + unsigned int tmp = 0; + if (drw && drw->fonts && text && n) + tmp = drw_text(drw, 0, 0, 0, 0, 0, text, n); + return MIN(n, tmp); +} + +void +drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h) +{ + XGlyphInfo ext; + + if (!font || !text) + return; + + XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext); + if (w) + *w = ext.xOff; + if (h) + *h = font->h; +} + +Cur * +drw_cur_create(Drw *drw, int shape) +{ + Cur *cur; + + if (!drw || !(cur = ecalloc(1, sizeof(Cur)))) + return NULL; + + cur->cursor = XCreateFontCursor(drw->dpy, shape); + + return cur; +} + +void +drw_cur_free(Drw *drw, Cur *cursor) +{ + if (!cursor) + return; + + XFreeCursor(drw->dpy, cursor->cursor); + free(cursor); +} diff --git a/mut/dwm/drw.h b/mut/dwm/drw.h new file mode 100644 index 0000000..000fb09 --- /dev/null +++ b/mut/dwm/drw.h @@ -0,0 +1,58 @@ +/* See LICENSE file for copyright and license details. */ + +typedef struct { + Cursor cursor; +} Cur; + +typedef struct Fnt { + Display *dpy; + unsigned int h; + XftFont *xfont; + FcPattern *pattern; + struct Fnt *next; +} Fnt; + +enum { ColFg, ColBg, ColBorder }; /* Clr scheme index */ +typedef XftColor Clr; + +typedef struct { + unsigned int w, h; + Display *dpy; + int screen; + Window root; + Drawable drawable; + GC gc; + Clr *scheme; + Fnt *fonts; +} Drw; + +/* Drawable abstraction */ +Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h); +void drw_resize(Drw *drw, unsigned int w, unsigned int h); +void drw_free(Drw *drw); + +/* Fnt abstraction */ +Fnt *drw_fontset_create(Drw* drw, char *fonts[], size_t fontcount); +void drw_fontset_free(Fnt* set); +unsigned int drw_fontset_getwidth(Drw *drw, const char *text); +unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n); +void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h); + +/* Colorscheme abstraction */ +void drw_clr_create(Drw *drw, Clr *dest, const char *clrname); +Clr *drw_scm_create(Drw *drw, char *clrnames[], size_t clrcount); + +/* Cursor abstraction */ +Cur *drw_cur_create(Drw *drw, int shape); +void drw_cur_free(Drw *drw, Cur *cursor); + +/* Drawing context manipulation */ +void drw_setfontset(Drw *drw, Fnt *set); +void drw_setscheme(Drw *drw, Clr *scm); + +/* Drawing functions */ +void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert); +int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert); + +/* Map functions */ +void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h); diff --git a/mut/dwm/dwm.1 b/mut/dwm/dwm.1 new file mode 100644 index 0000000..62fff83 --- /dev/null +++ b/mut/dwm/dwm.1 @@ -0,0 +1,213 @@ +.TH DWM 1 dwm\-VERSION +.SH NAME +dwm \- dynamic window manager (Luke Smith <https://lukesmith.xyz>'s build) +.SH SYNOPSIS +.B dwm +.RB [ \-v ] +.SH DESCRIPTION +dwm is a dynamic window manager for X. +.P +dwm "orders" windows based on recency and primacy, while dwm layouts may +change, the most recent "master" window is shown in the most prominent +position. There are bindings for cycling through and promoting windows to the +master position. +.P +Windows are grouped by tags. Each window can be tagged with one or multiple +tags. Selecting certain tags displays all windows with these tags. +.P +Each screen contains a small status bar which displays all available tags, the +layout, the title of the focused window, and the text read from the root window +name property, if the screen is focused. A floating window is indicated with an +empty square and a maximised floating window is indicated with a filled square +before the windows title. The selected tags are indicated with a different +color. The tags of the focused window are indicated with a filled square in the +top left corner. The tags which are applied to one or more windows are +indicated with an empty square in the top left corner. +.P +dwm draws a small border around windows to indicate the focus state. +.SH OPTIONS +.TP +.B \-v +prints version information to stderr, then exits. +.SH USAGE +.SS Status bar +.TP +.B X root window name +is read and displayed in the status text area. It can be set with the +.BR xsetroot (1) +command. +.TP +.B Left click +click on a tag label to display all windows with that tag, click on the layout +label toggles between tiled and floating layout. +.TP +.B Right click +click on a tag label adds/removes all windows with that tag to/from the view. +.TP +.B Super\-Left click +click on a tag label applies that tag to the focused window. +.TP +.B Super\-Right click +click on a tag label adds/removes that tag to/from the focused window. +.SS Keyboard commands +.TP +.B Super\-Return +Start terminal, +.BR st(1). +.TP +.B Super\-d +Spawn +.BR dmenu(1) +for launching other programs. +.TP +.B Super\-grave +Spawn +.BR dmenuunicode(1) +for selecting emoji. +.TP +.B Super\-minus/plus, Super\-Shift\-minus/plus +Decrease/increase volume by 5 and 15 respectively. +.TP +.B Super\-b +Toggles bar on and off. +.TP +.B Super\-q +Close focused window. +.TP +.B Super\-t/T +Sets tiled/bstack layouts. +.TP +.B Super\-f +Toggle fullscreen window. +.TP +.B Super\-F +Toggle floating layout. +.TP +.B Super\-y/Y +Sets Fibonacci spiral/dwinde layouts. +.TP +.B Super\-u/U +Sets centered master layout. +.TP +.B Super\-i/I +Sets centered master or floating master layouts. +.TP +.B Super\-space +Zooms/cycles focused window to/from master area. +.TP +.B Super\-j/k +Focus next/previous window. +.TP +.B Super\-Shift\-j/k +Move selected window down/up in stack. +.TP +.B Super\-o/O +Increase/decrease number of windows in master area. +.TP +.B Super\-l +Increase master area size. +.TP +.B Super\-h +Decrease master area size. +.TP +.B Super\-Shift\-space +Toggle focused window between tiled and floating state. +.TP +.B Super\-Tab +Toggles to the previously selected tags. +.TP +.B Super\-g +Moves to the previous tag. +.TP +.B Super\-Shift\-g +Moves selected window to the previous tag. +.TP +.B Super\-; +Moves to the next tag. +.TP +.B Super\-Shift\-; +Moves selected window to the next tag. +.TP +.B Super\-PageUp +Moves to the previous tag. +.TP +.B Super\-Shift\-PageUp +Moves selected window to the previous tag. +.TP +.B Super\-Pagedown +Moves to the next tag. +.TP +.B Super\-Shift\-PageDown +Moves selected window to the next tag. +.TP +.B Super\-a +Toggle gaps. +.TP +.B Super\-z +Increase gaps between windows. +.TP +.B Super\-x +Decrease gaps between windows. +.TP +.B Super\-Shift\-[1..n] +Apply nth tag to focused window. +.TP +.B Super\-Shift\-0 +Apply all tags to focused window. +.TP +.B Super\-Control\-Shift\-[1..n] +Add/remove nth tag to/from focused window. +.TP +.B Super\-[1..n] +View all windows with nth tag. +.TP +.B Super\-0 +View all windows with any tag. +.TP +.B Super\-Control\-[1..n] +Add/remove all windows with nth tag to/from the view. +.TP +.B Super\-Shift\-q +Quit dwm. +.TP +.B Mod1\-Control\-Shift\-q +Menu to refresh/quit/reboot/shutdown. +.SS Mouse commands +.TP +.B Super\-Left click +Move focused window while dragging. Tiled windows will be toggled to the floating state. +.TP +.B Super\-Middle click +Toggles focused window between floating and tiled state. +.TP +.B Super\-Right click +Resize focused window while dragging. Tiled windows will be toggled to the floating state. +.SH CUSTOMIZATION +dwm is customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.SH SIGNALS +.TP +.B SIGHUP - 1 +Restart the dwm process. +.TP +.B SIGTERM - 15 +Cleanly terminate the dwm process. +.SH SEE ALSO +.BR dmenu (1), +.BR st (1) +.SH ISSUES +Java applications which use the XToolkit/XAWT backend may draw grey windows +only. The XToolkit/XAWT backend breaks ICCCM-compliance in recent JDK 1.5 and early +JDK 1.6 versions, because it assumes a reparenting window manager. Possible workarounds +are using JDK 1.4 (which doesn't contain the XToolkit/XAWT backend) or setting the +environment variable +.BR AWT_TOOLKIT=MToolkit +(to use the older Motif backend instead) or running +.B xprop -root -f _NET_WM_NAME 32a -set _NET_WM_NAME LG3D +or +.B wmname LG3D +(to pretend that a non-reparenting window manager is running that the +XToolkit/XAWT backend can recognize) or when using OpenJDK setting the environment variable +.BR _JAVA_AWT_WM_NONREPARENTING=1 . +.SH BUGS +Send all bug reports with a patch to hackers@suckless.org. diff --git a/mut/dwm/dwm.c b/mut/dwm/dwm.c new file mode 100644 index 0000000..9d143a2 --- /dev/null +++ b/mut/dwm/dwm.c @@ -0,0 +1,2685 @@ +/* See LICENSE file for copyright and license details. + * + * dynamic window manager is designed like any other X client as well. It is + * driven through handling X events. In contrast to other X clients, a window + * manager selects for SubstructureRedirectMask on the root window, to receive + * events about window (dis-)appearance. Only one X connection at a time is + * allowed to select for this event mask. + * + * The event handlers of dwm are organized in an array which is accessed + * whenever a new event has been fetched. This allows event dispatching + * in O(1) time. + * + * Each child of the root window is called a client, except windows which have + * set the override_redirect flag. Clients are organized in a linked client + * list on each monitor, the focus history is remembered through a stack list + * on each monitor. Each client contains a bit array to indicate the tags of a + * client. + * + * Keys and tagging rules are organized as arrays and defined in config.h. + * + * To understand everything else, start reading main(). + */ +#include <errno.h> +#include <locale.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> +#include <X11/Xatom.h> +#include <X11/Xlib.h> +#include <X11/Xproto.h> +#include <X11/Xresource.h> +#include <X11/Xutil.h> +#include <X11/Xresource.h> +#ifdef XINERAMA +#include <X11/extensions/Xinerama.h> +#endif /* XINERAMA */ +#include <X11/Xft/Xft.h> +#include <X11/Xlib-xcb.h> +#include <xcb/res.h> + +#include "drw.h" +#include "util.h" + +/* macros */ +#define BUTTONMASK (ButtonPressMask|ButtonReleaseMask) +#define CLEANMASK(mask) (mask & ~(numlockmask|LockMask) & (ShiftMask|ControlMask|Mod1Mask|Mod2Mask|Mod3Mask|Mod4Mask|Mod5Mask)) +#define GETINC(X) ((X) - 2000) +#define INC(X) ((X) + 2000) +#define INTERSECT(x,y,w,h,m) (MAX(0, MIN((x)+(w),(m)->wx+(m)->ww) - MAX((x),(m)->wx)) \ + * MAX(0, MIN((y)+(h),(m)->wy+(m)->wh) - MAX((y),(m)->wy))) +#define ISINC(X) ((X) > 1000 && (X) < 3000) +#define ISVISIBLE(C) ((C->tags & C->mon->tagset[C->mon->seltags]) || C->issticky) +#define PREVSEL 3000 +#define LENGTH(X) (sizeof X / sizeof X[0]) +#define MOUSEMASK (BUTTONMASK|PointerMotionMask) +#define MOD(N,M) ((N)%(M) < 0 ? (N)%(M) + (M) : (N)%(M)) +#define WIDTH(X) ((X)->w + 2 * (X)->bw) +#define HEIGHT(X) ((X)->h + 2 * (X)->bw) +#define NUMTAGS (LENGTH(tags) + LENGTH(scratchpads)) +#define TAGMASK ((1 << NUMTAGS) - 1) +#define SPTAG(i) ((1 << LENGTH(tags)) << (i)) +#define SPTAGMASK (((1 << LENGTH(scratchpads))-1) << LENGTH(tags)) +#define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad) +#define TRUNC(X,A,B) (MAX((A), MIN((X), (B)))) + +/* enums */ +enum { CurNormal, CurResize, CurMove, CurLast }; /* cursor */ +enum { SchemeNorm, SchemeSel }; /* color schemes */ +enum { NetSupported, NetWMName, NetWMState, NetWMCheck, + NetWMFullscreen, NetActiveWindow, NetWMWindowType, + NetWMWindowTypeDialog, NetClientList, NetClientInfo, NetLast }; /* EWMH atoms */ +enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */ +enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, + ClkClientWin, ClkRootWin, ClkLast }; /* clicks */ + +typedef union { + int i; + unsigned int ui; + float f; + const void *v; +} Arg; + +typedef struct { + unsigned int click; + unsigned int mask; + unsigned int button; + void (*func)(const Arg *arg); + const Arg arg; +} Button; + +typedef struct Monitor Monitor; +typedef struct Client Client; +struct Client { + char name[256]; + float mina, maxa; + int x, y, w, h; + int oldx, oldy, oldw, oldh; + int basew, baseh, incw, inch, maxw, maxh, minw, minh, hintsvalid; + int bw, oldbw; + unsigned int tags; + int isfixed, isfloating, isurgent, neverfocus, oldstate, isfullscreen, isterminal, noswallow, issticky; + pid_t pid; + Client *next; + Client *snext; + Client *swallowing; + Monitor *mon; + Window win; +}; + +typedef struct { + unsigned int mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Key; + +typedef struct { + const char *symbol; + void (*arrange)(Monitor *); +} Layout; + +struct Monitor { + char ltsymbol[16]; + float mfact; + int nmaster; + int num; + int by; /* bar geometry */ + int mx, my, mw, mh; /* screen size */ + int wx, wy, ww, wh; /* window area */ + int gappih; /* horizontal gap between windows */ + int gappiv; /* vertical gap between windows */ + int gappoh; /* horizontal outer gaps */ + int gappov; /* vertical outer gaps */ + unsigned int seltags; + unsigned int sellt; + unsigned int tagset[2]; + int showbar; + int topbar; + Client *clients; + Client *sel; + Client *stack; + Monitor *next; + Window barwin; + const Layout *lt[2]; +}; + +typedef struct { + const char *class; + const char *instance; + const char *title; + unsigned int tags; + int isfloating; + int isterminal; + int noswallow; + int monitor; +} Rule; + +/* Xresources preferences */ +enum resource_type { + STRING = 0, + INTEGER = 1, + FLOAT = 2 +}; + +typedef struct { + char *name; + enum resource_type type; + void *dst; +} ResourcePref; + +/* function declarations */ +static void applyrules(Client *c); +static int applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact); +static void arrange(Monitor *m); +static void arrangemon(Monitor *m); +static void attach(Client *c); +static void attachstack(Client *c); +static void buttonpress(XEvent *e); +static void checkotherwm(void); +static void cleanup(void); +static void cleanupmon(Monitor *mon); +static void clientmessage(XEvent *e); +static void configure(Client *c); +static void configurenotify(XEvent *e); +static void configurerequest(XEvent *e); +static void copyvalidchars(char *text, char *rawtext); +static Monitor *createmon(void); +static void destroynotify(XEvent *e); +static void detach(Client *c); +static void detachstack(Client *c); +static Monitor *dirtomon(int dir); +static void drawbar(Monitor *m); +static void drawbars(void); +static void enternotify(XEvent *e); +static void expose(XEvent *e); +static void focus(Client *c); +static void focusin(XEvent *e); +static void focusmon(const Arg *arg); +static void focusstack(const Arg *arg); +static Atom getatomprop(Client *c, Atom prop); +static int getrootptr(int *x, int *y); +static long getstate(Window w); +static int gettextprop(Window w, Atom atom, char *text, unsigned int size); +static void grabbuttons(Client *c, int focused); +static void grabkeys(void); +static void incnmaster(const Arg *arg); +static void keypress(XEvent *e); +static void killclient(const Arg *arg); +static void manage(Window w, XWindowAttributes *wa); +static void mappingnotify(XEvent *e); +static void maprequest(XEvent *e); +static void monocle(Monitor *m); +static void motionnotify(XEvent *e); +static void movemouse(const Arg *arg); +static Client *nexttiled(Client *c); +static void pop(Client *c); +static void propertynotify(XEvent *e); +static void pushstack(const Arg *arg); +static void quit(const Arg *arg); +static Monitor *recttomon(int x, int y, int w, int h); +static void resize(Client *c, int x, int y, int w, int h, int interact); +static void resizeclient(Client *c, int x, int y, int w, int h); +static void resizemouse(const Arg *arg); +static void restack(Monitor *m); +static void run(void); +static void runAutostart(void); +static void scan(void); +static int sendevent(Client *c, Atom proto); +static void sendmon(Client *c, Monitor *m); +static void setclientstate(Client *c, long state); +static void setclienttagprop(Client *c); +static void setfocus(Client *c); +static void setfullscreen(Client *c, int fullscreen); +static void setlayout(const Arg *arg); +static void setmfact(const Arg *arg); +static void setup(void); +static void seturgent(Client *c, int urg); +static void showhide(Client *c); +static void sigchld(int unused); +#ifndef __OpenBSD__ +static int getdwmblockspid(); +static void sigdwmblocks(const Arg *arg); +#endif +static void sighup(int unused); +static void sigterm(int unused); +static void spawn(const Arg *arg); +static int stackpos(const Arg *arg); +static void tag(const Arg *arg); +static void tagmon(const Arg *arg); +static void togglebar(const Arg *arg); +static void togglefloating(const Arg *arg); +static void togglescratch(const Arg *arg); +static void togglesticky(const Arg *arg); +static void togglefullscr(const Arg *arg); +static void toggletag(const Arg *arg); +static void toggleview(const Arg *arg); +static void unfocus(Client *c, int setfocus); +static void unmanage(Client *c, int destroyed); +static void unmapnotify(XEvent *e); +static void updatebarpos(Monitor *m); +static void updatebars(void); +static void updateclientlist(void); +static int updategeom(void); +static void updatenumlockmask(void); +static void updatesizehints(Client *c); +static void updatestatus(void); +static void updatetitle(Client *c); +static void updatewindowtype(Client *c); +static void updatewmhints(Client *c); +static void view(const Arg *arg); +static Client *wintoclient(Window w); +static Monitor *wintomon(Window w); +static int xerror(Display *dpy, XErrorEvent *ee); +static int xerrordummy(Display *dpy, XErrorEvent *ee); +static int xerrorstart(Display *dpy, XErrorEvent *ee); +static void zoom(const Arg *arg); +static void xrdb(const Arg *arg); +static void load_xresources(void); +static void resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst); + +static pid_t getparentprocess(pid_t p); +static int isdescprocess(pid_t p, pid_t c); +static Client *swallowingclient(Window w); +static Client *termforwin(const Client *c); +static pid_t winpid(Window w); + + +/* variables */ +static const char broken[] = "broken"; +static char stext[256]; +static char rawstext[256]; +static int dwmblockssig; +pid_t dwmblockspid = 0; +static int screen; +static int sw, sh; /* X display screen geometry width, height */ +static int bh; /* bar height */ +static int lrpad; /* sum of left and right padding for text */ +static int (*xerrorxlib)(Display *, XErrorEvent *); +static unsigned int numlockmask = 0; +static void (*handler[LASTEvent]) (XEvent *) = { + [ButtonPress] = buttonpress, + [ClientMessage] = clientmessage, + [ConfigureRequest] = configurerequest, + [ConfigureNotify] = configurenotify, + [DestroyNotify] = destroynotify, + [EnterNotify] = enternotify, + [Expose] = expose, + [FocusIn] = focusin, + [KeyPress] = keypress, + [MappingNotify] = mappingnotify, + [MapRequest] = maprequest, + [MotionNotify] = motionnotify, + [PropertyNotify] = propertynotify, + [UnmapNotify] = unmapnotify +}; +static Atom wmatom[WMLast], netatom[NetLast]; +static int restart = 0; +static int running = 1; +static Cur *cursor[CurLast]; +static Clr **scheme; +static Display *dpy; +static Drw *drw; +static Monitor *mons, *selmon; +static Window root, wmcheckwin; + +static xcb_connection_t *xcon; + +/* configuration, allows nested code to access above variables */ +#include "config.h" + +/* compile-time check if all tags fit into an unsigned int bit array. */ +struct NumTags { char limitexceeded[LENGTH(tags) > 31 ? -1 : 1]; }; + +/* function implementations */ +void +applyrules(Client *c) +{ + const char *class, *instance; + unsigned int i; + const Rule *r; + Monitor *m; + XClassHint ch = { NULL, NULL }; + + /* rule matching */ + c->isfloating = 0; + c->tags = 0; + XGetClassHint(dpy, c->win, &ch); + class = ch.res_class ? ch.res_class : broken; + instance = ch.res_name ? ch.res_name : broken; + + for (i = 0; i < LENGTH(rules); i++) { + r = &rules[i]; + if ((!r->title || strstr(c->name, r->title)) + && (!r->class || strstr(class, r->class)) + && (!r->instance || strstr(instance, r->instance))) + { + c->isterminal = r->isterminal; + c->isfloating = r->isfloating; + c->noswallow = r->noswallow; + c->tags |= r->tags; + if ((r->tags & SPTAGMASK) && r->isfloating) { + c->x = c->mon->wx + (c->mon->ww / 2 - WIDTH(c) / 2); + c->y = c->mon->wy + (c->mon->wh / 2 - HEIGHT(c) / 2); + } + + for (m = mons; m && m->num != r->monitor; m = m->next); + if (m) + c->mon = m; + } + } + if (ch.res_class) + XFree(ch.res_class); + if (ch.res_name) + XFree(ch.res_name); + c->tags = c->tags & TAGMASK ? c->tags & TAGMASK : (c->mon->tagset[c->mon->seltags] & ~SPTAGMASK); +} + +int +applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact) +{ + int baseismin; + Monitor *m = c->mon; + + /* set minimum possible */ + *w = MAX(1, *w); + *h = MAX(1, *h); + if (interact) { + if (*x > sw) + *x = sw - WIDTH(c); + if (*y > sh) + *y = sh - HEIGHT(c); + if (*x + *w + 2 * c->bw < 0) + *x = 0; + if (*y + *h + 2 * c->bw < 0) + *y = 0; + } else { + if (*x >= m->wx + m->ww) + *x = m->wx + m->ww - WIDTH(c); + if (*y >= m->wy + m->wh) + *y = m->wy + m->wh - HEIGHT(c); + if (*x + *w + 2 * c->bw <= m->wx) + *x = m->wx; + if (*y + *h + 2 * c->bw <= m->wy) + *y = m->wy; + } + if (*h < bh) + *h = bh; + if (*w < bh) + *w = bh; + if (resizehints || c->isfloating || !c->mon->lt[c->mon->sellt]->arrange) { + if (!c->hintsvalid) + updatesizehints(c); + /* see last two sentences in ICCCM 4.1.2.3 */ + baseismin = c->basew == c->minw && c->baseh == c->minh; + if (!baseismin) { /* temporarily remove base dimensions */ + *w -= c->basew; + *h -= c->baseh; + } + /* adjust for aspect limits */ + if (c->mina > 0 && c->maxa > 0) { + if (c->maxa < (float)*w / *h) + *w = *h * c->maxa + 0.5; + else if (c->mina < (float)*h / *w) + *h = *w * c->mina + 0.5; + } + if (baseismin) { /* increment calculation requires this */ + *w -= c->basew; + *h -= c->baseh; + } + /* adjust for increment value */ + if (c->incw) + *w -= *w % c->incw; + if (c->inch) + *h -= *h % c->inch; + /* restore base dimensions */ + *w = MAX(*w + c->basew, c->minw); + *h = MAX(*h + c->baseh, c->minh); + if (c->maxw) + *w = MIN(*w, c->maxw); + if (c->maxh) + *h = MIN(*h, c->maxh); + } + return *x != c->x || *y != c->y || *w != c->w || *h != c->h; +} + +void +arrange(Monitor *m) +{ + if (m) + showhide(m->stack); + else for (m = mons; m; m = m->next) + showhide(m->stack); + if (m) { + arrangemon(m); + restack(m); + } else for (m = mons; m; m = m->next) + arrangemon(m); +} + +void +arrangemon(Monitor *m) +{ + strncpy(m->ltsymbol, m->lt[m->sellt]->symbol, sizeof m->ltsymbol); + if (m->lt[m->sellt]->arrange) + m->lt[m->sellt]->arrange(m); +} + +void +attach(Client *c) +{ + c->next = c->mon->clients; + c->mon->clients = c; +} + +void +attachstack(Client *c) +{ + c->snext = c->mon->stack; + c->mon->stack = c; +} + +void +swallow(Client *p, Client *c) +{ + if (c->noswallow || c->isterminal) + return; + if (!swallowfloating && c->isfloating) + return; + + detach(c); + detachstack(c); + + setclientstate(c, WithdrawnState); + XUnmapWindow(dpy, p->win); + + p->swallowing = c; + c->mon = p->mon; + + Window w = p->win; + p->win = c->win; + c->win = w; + updatetitle(p); + + XWindowChanges wc; + wc.border_width = p->bw; + XConfigureWindow(dpy, p->win, CWBorderWidth, &wc); + XMoveResizeWindow(dpy, p->win, p->x, p->y, p->w, p->h); + XSetWindowBorder(dpy, p->win, scheme[SchemeNorm][ColBorder].pixel); + + arrange(p->mon); + configure(p); + updateclientlist(); +} + +void +unswallow(Client *c) +{ + c->win = c->swallowing->win; + + free(c->swallowing); + c->swallowing = NULL; + + /* unfullscreen the client */ + setfullscreen(c, 0); + updatetitle(c); + arrange(c->mon); + XMapWindow(dpy, c->win); + + XWindowChanges wc; + wc.border_width = c->bw; + XConfigureWindow(dpy, c->win, CWBorderWidth, &wc); + XMoveResizeWindow(dpy, c->win, c->x, c->y, c->w, c->h); + XSetWindowBorder(dpy, c->win, scheme[SchemeNorm][ColBorder].pixel); + + setclientstate(c, NormalState); + focus(NULL); + arrange(c->mon); +} + +void +buttonpress(XEvent *e) +{ + unsigned int i, x, click, occ = 0; + Arg arg = {0}; + Client *c; + Monitor *m; + XButtonPressedEvent *ev = &e->xbutton; + + click = ClkRootWin; + /* focus monitor if necessary */ + if ((m = wintomon(ev->window)) && m != selmon) { + unfocus(selmon->sel, 1); + selmon = m; + focus(NULL); + } + if (ev->window == selmon->barwin) { + i = x = 0; + for (c = m->clients; c; c = c->next) + occ |= c->tags == 255 ? 0 : c->tags; + do { + /* do not reserve space for vacant tags */ + if (!(occ & 1 << i || m->tagset[m->seltags] & 1 << i)) + continue; + x += TEXTW(tags[i]); + } while (ev->x >= x && ++i < LENGTH(tags)); + if (i < LENGTH(tags)) { + click = ClkTagBar; + arg.ui = 1 << i; + } else if (ev->x < x + TEXTW(selmon->ltsymbol)) + click = ClkLtSymbol; + else if (ev->x > (x = selmon->ww - (int)TEXTW(stext) + lrpad)) { + click = ClkStatusText; + + char *text = rawstext; + int i = -1; + char ch; + dwmblockssig = 0; + while (text[++i]) { + if ((unsigned char)text[i] < ' ') { + ch = text[i]; + text[i] = '\0'; + x += TEXTW(text) - lrpad; + text[i] = ch; + text += i+1; + i = -1; + if (x >= ev->x) break; + dwmblockssig = ch; + } + } + } else + click = ClkWinTitle; + } else if ((c = wintoclient(ev->window))) { + focus(c); + restack(selmon); + XAllowEvents(dpy, ReplayPointer, CurrentTime); + click = ClkClientWin; + } + for (i = 0; i < LENGTH(buttons); i++) + if (click == buttons[i].click && buttons[i].func && buttons[i].button == ev->button + && CLEANMASK(buttons[i].mask) == CLEANMASK(ev->state)) + buttons[i].func(click == ClkTagBar && buttons[i].arg.i == 0 ? &arg : &buttons[i].arg); +} + +void +checkotherwm(void) +{ + xerrorxlib = XSetErrorHandler(xerrorstart); + /* this causes an error if some other window manager is running */ + XSelectInput(dpy, DefaultRootWindow(dpy), SubstructureRedirectMask); + XSync(dpy, False); + XSetErrorHandler(xerror); + XSync(dpy, False); +} + +void +cleanup(void) +{ + Arg a = {.ui = ~0}; + Layout foo = { "", NULL }; + Monitor *m; + size_t i; + + view(&a); + selmon->lt[selmon->sellt] = &foo; + for (m = mons; m; m = m->next) + while (m->stack) + unmanage(m->stack, 0); + XUngrabKey(dpy, AnyKey, AnyModifier, root); + while (mons) + cleanupmon(mons); + for (i = 0; i < CurLast; i++) + drw_cur_free(drw, cursor[i]); + for (i = 0; i < LENGTH(colors); i++) + free(scheme[i]); + free(scheme); + XDestroyWindow(dpy, wmcheckwin); + drw_free(drw); + XSync(dpy, False); + XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); +} + +void +cleanupmon(Monitor *mon) +{ + Monitor *m; + + if (mon == mons) + mons = mons->next; + else { + for (m = mons; m && m->next != mon; m = m->next); + m->next = mon->next; + } + XUnmapWindow(dpy, mon->barwin); + XDestroyWindow(dpy, mon->barwin); + free(mon); +} + +void +clientmessage(XEvent *e) +{ + XClientMessageEvent *cme = &e->xclient; + Client *c = wintoclient(cme->window); + + if (!c) + return; + if (cme->message_type == netatom[NetWMState]) { + if (cme->data.l[1] == netatom[NetWMFullscreen] + || cme->data.l[2] == netatom[NetWMFullscreen]) + setfullscreen(c, (cme->data.l[0] == 1 /* _NET_WM_STATE_ADD */ + || (cme->data.l[0] == 2 /* _NET_WM_STATE_TOGGLE */ && !c->isfullscreen))); + } else if (cme->message_type == netatom[NetActiveWindow]) { + if (c != selmon->sel && !c->isurgent) + seturgent(c, 1); + } +} + +void +configure(Client *c) +{ + XConfigureEvent ce; + + ce.type = ConfigureNotify; + ce.display = dpy; + ce.event = c->win; + ce.window = c->win; + ce.x = c->x; + ce.y = c->y; + ce.width = c->w; + ce.height = c->h; + ce.border_width = c->bw; + ce.above = None; + ce.override_redirect = False; + XSendEvent(dpy, c->win, False, StructureNotifyMask, (XEvent *)&ce); +} + +void +configurenotify(XEvent *e) +{ + Monitor *m; + Client *c; + XConfigureEvent *ev = &e->xconfigure; + int dirty; + + /* TODO: updategeom handling sucks, needs to be simplified */ + if (ev->window == root) { + dirty = (sw != ev->width || sh != ev->height); + sw = ev->width; + sh = ev->height; + if (updategeom() || dirty) { + drw_resize(drw, sw, bh); + updatebars(); + for (m = mons; m; m = m->next) { + for (c = m->clients; c; c = c->next) + if (c->isfullscreen) + resizeclient(c, m->mx, m->my, m->mw, m->mh); + XMoveResizeWindow(dpy, m->barwin, m->wx, m->by, m->ww, bh); + } + focus(NULL); + arrange(NULL); + } + } +} + +void +configurerequest(XEvent *e) +{ + Client *c; + Monitor *m; + XConfigureRequestEvent *ev = &e->xconfigurerequest; + XWindowChanges wc; + + if ((c = wintoclient(ev->window))) { + if (ev->value_mask & CWBorderWidth) + c->bw = ev->border_width; + else if (c->isfloating || !selmon->lt[selmon->sellt]->arrange) { + m = c->mon; + if (ev->value_mask & CWX) { + c->oldx = c->x; + c->x = m->mx + ev->x; + } + if (ev->value_mask & CWY) { + c->oldy = c->y; + c->y = m->my + ev->y; + } + if (ev->value_mask & CWWidth) { + c->oldw = c->w; + c->w = ev->width; + } + if (ev->value_mask & CWHeight) { + c->oldh = c->h; + c->h = ev->height; + } + if ((c->x + c->w) > m->mx + m->mw && c->isfloating) + c->x = m->mx + (m->mw / 2 - WIDTH(c) / 2); /* center in x direction */ + if ((c->y + c->h) > m->my + m->mh && c->isfloating) + c->y = m->my + (m->mh / 2 - HEIGHT(c) / 2); /* center in y direction */ + if ((ev->value_mask & (CWX|CWY)) && !(ev->value_mask & (CWWidth|CWHeight))) + configure(c); + if (ISVISIBLE(c)) + XMoveResizeWindow(dpy, c->win, c->x, c->y, c->w, c->h); + } else + configure(c); + } else { + wc.x = ev->x; + wc.y = ev->y; + wc.width = ev->width; + wc.height = ev->height; + wc.border_width = ev->border_width; + wc.sibling = ev->above; + wc.stack_mode = ev->detail; + XConfigureWindow(dpy, ev->window, ev->value_mask, &wc); + } + XSync(dpy, False); +} + +void +copyvalidchars(char *text, char *rawtext) +{ + int i = -1, j = 0; + + while(rawtext[++i]) { + if ((unsigned char)rawtext[i] >= ' ') { + text[j++] = rawtext[i]; + } + } + text[j] = '\0'; +} + +Monitor * +createmon(void) +{ + Monitor *m; + + m = ecalloc(1, sizeof(Monitor)); + m->tagset[0] = m->tagset[1] = 1; + m->mfact = mfact; + m->nmaster = nmaster; + m->showbar = showbar; + m->topbar = topbar; + m->gappih = gappih; + m->gappiv = gappiv; + m->gappoh = gappoh; + m->gappov = gappov; + m->lt[0] = &layouts[0]; + m->lt[1] = &layouts[1 % LENGTH(layouts)]; + strncpy(m->ltsymbol, layouts[0].symbol, sizeof m->ltsymbol); + return m; +} + +void +destroynotify(XEvent *e) +{ + Client *c; + XDestroyWindowEvent *ev = &e->xdestroywindow; + + if ((c = wintoclient(ev->window))) + unmanage(c, 1); + + else if ((c = swallowingclient(ev->window))) + unmanage(c->swallowing, 1); +} + +void +detach(Client *c) +{ + Client **tc; + + for (tc = &c->mon->clients; *tc && *tc != c; tc = &(*tc)->next); + *tc = c->next; +} + +void +detachstack(Client *c) +{ + Client **tc, *t; + + for (tc = &c->mon->stack; *tc && *tc != c; tc = &(*tc)->snext); + *tc = c->snext; + + if (c == c->mon->sel) { + for (t = c->mon->stack; t && !ISVISIBLE(t); t = t->snext); + c->mon->sel = t; + } +} + +Monitor * +dirtomon(int dir) +{ + Monitor *m = NULL; + + if (dir > 0) { + if (!(m = selmon->next)) + m = mons; + } else if (selmon == mons) + for (m = mons; m->next; m = m->next); + else + for (m = mons; m->next != selmon; m = m->next); + return m; +} + +void +drawbar(Monitor *m) +{ + int x, w, tw = 0; + int boxs = drw->fonts->h / 9; + int boxw = drw->fonts->h / 6 + 2; + unsigned int i, occ = 0, urg = 0; + Client *c; + + if(!m->showbar) + return; + + /* draw status first so it can be overdrawn by tags later */ + if (m == selmon) { /* status is only drawn on selected monitor */ + drw_setscheme(drw, scheme[SchemeNorm]); + tw = TEXTW(stext) - lrpad + 2; /* 2px right padding */ + drw_text(drw, m->ww - tw, 0, tw, bh, 0, stext, 0); + } + + for (c = m->clients; c; c = c->next) { + occ |= c->tags == 255 ? 0 : c->tags; + if (c->isurgent) + urg |= c->tags; + } + x = 0; + for (i = 0; i < LENGTH(tags); i++) { + /* do not draw vacant tags */ + if (!(occ & 1 << i || m->tagset[m->seltags] & 1 << i)) + continue; + + w = TEXTW(tags[i]); + drw_setscheme(drw, scheme[m->tagset[m->seltags] & 1 << i ? SchemeSel : SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, tags[i], urg & 1 << i); + x += w; + } + w = TEXTW(m->ltsymbol); + drw_setscheme(drw, scheme[SchemeNorm]); + x = drw_text(drw, x, 0, w, bh, lrpad / 2, m->ltsymbol, 0); + + if ((w = m->ww - tw - x) > bh) { + if (m->sel) { + drw_setscheme(drw, scheme[m == selmon ? SchemeSel : SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, m->sel->name, 0); + if (m->sel->isfloating) + drw_rect(drw, x + boxs, boxs, boxw, boxw, m->sel->isfixed, 0); + } else { + drw_setscheme(drw, scheme[SchemeNorm]); + drw_rect(drw, x, 0, w, bh, 1, 1); + } + } + drw_map(drw, m->barwin, 0, 0, m->ww, bh); +} + +void +drawbars(void) +{ + Monitor *m; + + for (m = mons; m; m = m->next) + drawbar(m); +} + +void +enternotify(XEvent *e) +{ + Client *c; + Monitor *m; + XCrossingEvent *ev = &e->xcrossing; + + if ((ev->mode != NotifyNormal || ev->detail == NotifyInferior) && ev->window != root) + return; + c = wintoclient(ev->window); + m = c ? c->mon : wintomon(ev->window); + if (m != selmon) { + unfocus(selmon->sel, 1); + selmon = m; + } else if (!c || c == selmon->sel) + return; + focus(c); +} + +void +expose(XEvent *e) +{ + Monitor *m; + XExposeEvent *ev = &e->xexpose; + + if (ev->count == 0 && (m = wintomon(ev->window))) + drawbar(m); +} + +void +focus(Client *c) +{ + if (!c || !ISVISIBLE(c)) { + for (c = selmon->stack; c && (!ISVISIBLE(c) || (c->issticky && !selmon->sel->issticky)); c = c->snext); + + if (!c) /* No windows found; check for available stickies */ + for (c = selmon->stack; c && !ISVISIBLE(c); c = c->snext); + } + + if (selmon->sel && selmon->sel != c) + unfocus(selmon->sel, 0); + if (c) { + if (c->mon != selmon) + selmon = c->mon; + if (c->isurgent) + seturgent(c, 0); + detachstack(c); + attachstack(c); + grabbuttons(c, 1); + XSetWindowBorder(dpy, c->win, scheme[SchemeSel][ColBorder].pixel); + setfocus(c); + } else { + XSetInputFocus(dpy, root, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); + } + selmon->sel = c; + drawbars(); +} + +/* there are some broken focus acquiring clients needing extra handling */ +void +focusin(XEvent *e) +{ + XFocusChangeEvent *ev = &e->xfocus; + + if (selmon->sel && ev->window != selmon->sel->win) + setfocus(selmon->sel); +} + +void +focusmon(const Arg *arg) +{ + Monitor *m; + + if (!mons->next) + return; + if ((m = dirtomon(arg->i)) == selmon) + return; + unfocus(selmon->sel, 0); + selmon = m; + focus(NULL); +} + +void +focusstack(const Arg *arg) +{ + int i = stackpos(arg); + Client *c, *p; + + if(i < 0 || !selmon->sel || (selmon->sel->isfullscreen && lockfullscreen)) + return; + + for(p = NULL, c = selmon->clients; c && (i || !ISVISIBLE(c)); + i -= ISVISIBLE(c) ? 1 : 0, p = c, c = c->next); + focus(c ? c : p); + restack(selmon); +} + +Atom +getatomprop(Client *c, Atom prop) +{ + int di; + unsigned long dl; + unsigned char *p = NULL; + Atom da, atom = None; + + if (XGetWindowProperty(dpy, c->win, prop, 0L, sizeof atom, False, XA_ATOM, + &da, &di, &dl, &dl, &p) == Success && p) { + atom = *(Atom *)p; + XFree(p); + } + return atom; +} + +#ifndef __OpenBSD__ +int +getdwmblockspid() +{ + char buf[16]; + FILE *fp = popen("pidof -s dwmblocks", "r"); + fgets(buf, sizeof(buf), fp); + pid_t pid = strtoul(buf, NULL, 10); + pclose(fp); + dwmblockspid = pid; + return pid != 0 ? 0 : -1; +} +#endif + +int +getrootptr(int *x, int *y) +{ + int di; + unsigned int dui; + Window dummy; + + return XQueryPointer(dpy, root, &dummy, &dummy, x, y, &di, &di, &dui); +} + +long +getstate(Window w) +{ + int format; + long result = -1; + unsigned char *p = NULL; + unsigned long n, extra; + Atom real; + + if (XGetWindowProperty(dpy, w, wmatom[WMState], 0L, 2L, False, wmatom[WMState], + &real, &format, &n, &extra, (unsigned char **)&p) != Success) + return -1; + if (n != 0) + result = *p; + XFree(p); + return result; +} + +int +gettextprop(Window w, Atom atom, char *text, unsigned int size) +{ + char **list = NULL; + int n; + XTextProperty name; + + if (!text || size == 0) + return 0; + text[0] = '\0'; + if (!XGetTextProperty(dpy, w, &name, atom) || !name.nitems) + return 0; + if (name.encoding == XA_STRING) { + strncpy(text, (char *)name.value, size - 1); + } else if (XmbTextPropertyToTextList(dpy, &name, &list, &n) >= Success && n > 0 && *list) { + strncpy(text, *list, size - 1); + XFreeStringList(list); + } + text[size - 1] = '\0'; + XFree(name.value); + return 1; +} + +void +grabbuttons(Client *c, int focused) +{ + updatenumlockmask(); + { + unsigned int i, j; + unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; + XUngrabButton(dpy, AnyButton, AnyModifier, c->win); + if (!focused) + XGrabButton(dpy, AnyButton, AnyModifier, c->win, False, + BUTTONMASK, GrabModeSync, GrabModeSync, None, None); + for (i = 0; i < LENGTH(buttons); i++) + if (buttons[i].click == ClkClientWin) + for (j = 0; j < LENGTH(modifiers); j++) + XGrabButton(dpy, buttons[i].button, + buttons[i].mask | modifiers[j], + c->win, False, BUTTONMASK, + GrabModeAsync, GrabModeSync, None, None); + } +} + +void +grabkeys(void) +{ + updatenumlockmask(); + { + unsigned int i, j, k; + unsigned int modifiers[] = { 0, LockMask, numlockmask, numlockmask|LockMask }; + int start, end, skip; + KeySym *syms; + + XUngrabKey(dpy, AnyKey, AnyModifier, root); + XDisplayKeycodes(dpy, &start, &end); + syms = XGetKeyboardMapping(dpy, start, end - start + 1, &skip); + if (!syms) + return; + for (k = start; k <= end; k++) + for (i = 0; i < LENGTH(keys); i++) + /* skip modifier codes, we do that ourselves */ + if (keys[i].keysym == syms[(k - start) * skip]) + for (j = 0; j < LENGTH(modifiers); j++) + XGrabKey(dpy, k, + keys[i].mod | modifiers[j], + root, True, + GrabModeAsync, GrabModeAsync); + XFree(syms); + } +} + +void +incnmaster(const Arg *arg) +{ + selmon->nmaster = MAX(selmon->nmaster + arg->i, 0); + arrange(selmon); +} + +#ifdef XINERAMA +static int +isuniquegeom(XineramaScreenInfo *unique, size_t n, XineramaScreenInfo *info) +{ + while (n--) + if (unique[n].x_org == info->x_org && unique[n].y_org == info->y_org + && unique[n].width == info->width && unique[n].height == info->height) + return 0; + return 1; +} +#endif /* XINERAMA */ + +void +keypress(XEvent *e) +{ + unsigned int i; + KeySym keysym; + XKeyEvent *ev; + + ev = &e->xkey; + keysym = XKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0); + for (i = 0; i < LENGTH(keys); i++) + if (keysym == keys[i].keysym + && CLEANMASK(keys[i].mod) == CLEANMASK(ev->state) + && keys[i].func) + keys[i].func(&(keys[i].arg)); +} + +void +killclient(const Arg *arg) +{ + if (!selmon->sel) + return; + if (!sendevent(selmon->sel, wmatom[WMDelete])) { + XGrabServer(dpy); + XSetErrorHandler(xerrordummy); + XSetCloseDownMode(dpy, DestroyAll); + XKillClient(dpy, selmon->sel->win); + XSync(dpy, False); + XSetErrorHandler(xerror); + XUngrabServer(dpy); + } +} + +void +manage(Window w, XWindowAttributes *wa) +{ + Client *c, *t = NULL, *term = NULL; + Window trans = None; + XWindowChanges wc; + + c = ecalloc(1, sizeof(Client)); + c->win = w; + c->pid = winpid(w); + /* geometry */ + c->x = c->oldx = wa->x; + c->y = c->oldy = wa->y; + c->w = c->oldw = wa->width; + c->h = c->oldh = wa->height; + c->oldbw = wa->border_width; + + updatetitle(c); + if (XGetTransientForHint(dpy, w, &trans) && (t = wintoclient(trans))) { + c->mon = t->mon; + c->tags = t->tags; + } else { + c->mon = selmon; + applyrules(c); + term = termforwin(c); + } + + if (c->x + WIDTH(c) > c->mon->wx + c->mon->ww) + c->x = c->mon->wx + c->mon->ww - WIDTH(c); + if (c->y + HEIGHT(c) > c->mon->wy + c->mon->wh) + c->y = c->mon->wy + c->mon->wh - HEIGHT(c); + c->x = MAX(c->x, c->mon->wx); + c->y = MAX(c->y, c->mon->wy); + c->bw = borderpx; + + wc.border_width = c->bw; + XConfigureWindow(dpy, w, CWBorderWidth, &wc); + XSetWindowBorder(dpy, w, scheme[SchemeNorm][ColBorder].pixel); + configure(c); /* propagates border_width, if size doesn't change */ + updatewindowtype(c); + updatesizehints(c); + updatewmhints(c); + { + int format; + unsigned long *data, n, extra; + Monitor *m; + Atom atom; + if (XGetWindowProperty(dpy, c->win, netatom[NetClientInfo], 0L, 2L, False, XA_CARDINAL, + &atom, &format, &n, &extra, (unsigned char **)&data) == Success && n == 2) { + c->tags = *data; + for (m = mons; m; m = m->next) { + if (m->num == *(data+1)) { + c->mon = m; + break; + } + } + } + if (n > 0) + XFree(data); + } + setclienttagprop(c); + + c->x = c->mon->mx + (c->mon->mw - WIDTH(c)) / 2; + c->y = c->mon->my + (c->mon->mh - HEIGHT(c)) / 2; + XSelectInput(dpy, w, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask); + grabbuttons(c, 0); + if (!c->isfloating) + c->isfloating = c->oldstate = trans != None || c->isfixed; + if (c->isfloating) + XRaiseWindow(dpy, c->win); + attach(c); + attachstack(c); + XChangeProperty(dpy, root, netatom[NetClientList], XA_WINDOW, 32, PropModeAppend, + (unsigned char *) &(c->win), 1); + XMoveResizeWindow(dpy, c->win, c->x + 2 * sw, c->y, c->w, c->h); /* some windows require this */ + setclientstate(c, NormalState); + if(selmon->sel && selmon->sel->isfullscreen && !c->isfloating) + setfullscreen(selmon->sel, 0); + if (c->mon == selmon) + unfocus(selmon->sel, 0); + c->mon->sel = c; + XMapWindow(dpy, c->win); + if (term) + swallow(term, c); + arrange(c->mon); + focus(NULL); +} + +void +mappingnotify(XEvent *e) +{ + XMappingEvent *ev = &e->xmapping; + + XRefreshKeyboardMapping(ev); + if (ev->request == MappingKeyboard) + grabkeys(); +} + +void +maprequest(XEvent *e) +{ + static XWindowAttributes wa; + XMapRequestEvent *ev = &e->xmaprequest; + + if (!XGetWindowAttributes(dpy, ev->window, &wa) || wa.override_redirect) + return; + if (!wintoclient(ev->window)) + manage(ev->window, &wa); +} + +void +monocle(Monitor *m) +{ + unsigned int n; + int oh, ov, ih, iv; + Client *c; + + getgaps(m, &oh, &ov, &ih, &iv, &n); + + if (n > 0) /* override layout symbol */ + snprintf(m->ltsymbol, sizeof m->ltsymbol, "[%d]", n); + for (c = nexttiled(m->clients); c; c = nexttiled(c->next)) + resize(c, m->wx + ov, m->wy + oh, m->ww - 2 * c->bw - 2 * ov, m->wh - 2 * c->bw - 2 * oh, 0); +} + +void +motionnotify(XEvent *e) +{ + static Monitor *mon = NULL; + Monitor *m; + XMotionEvent *ev = &e->xmotion; + + if (ev->window != root) + return; + if ((m = recttomon(ev->x_root, ev->y_root, 1, 1)) != mon && mon) { + unfocus(selmon->sel, 1); + selmon = m; + focus(NULL); + } + mon = m; +} + +void +movemouse(const Arg *arg) +{ + int x, y, ocx, ocy, nx, ny; + Client *c; + Monitor *m; + XEvent ev; + Time lasttime = 0; + + if (!(c = selmon->sel)) + return; + if (c->isfullscreen) /* no support moving fullscreen windows by mouse */ + return; + restack(selmon); + ocx = c->x; + ocy = c->y; + if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync, + None, cursor[CurMove]->cursor, CurrentTime) != GrabSuccess) + return; + if (!getrootptr(&x, &y)) + return; + do { + XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev); + switch(ev.type) { + case ConfigureRequest: + case Expose: + case MapRequest: + handler[ev.type](&ev); + break; + case MotionNotify: + if ((ev.xmotion.time - lasttime) <= (1000 / 60)) + continue; + lasttime = ev.xmotion.time; + + nx = ocx + (ev.xmotion.x - x); + ny = ocy + (ev.xmotion.y - y); + if (abs(selmon->wx - nx) < snap) + nx = selmon->wx; + else if (abs((selmon->wx + selmon->ww) - (nx + WIDTH(c))) < snap) + nx = selmon->wx + selmon->ww - WIDTH(c); + if (abs(selmon->wy - ny) < snap) + ny = selmon->wy; + else if (abs((selmon->wy + selmon->wh) - (ny + HEIGHT(c))) < snap) + ny = selmon->wy + selmon->wh - HEIGHT(c); + if (!c->isfloating && selmon->lt[selmon->sellt]->arrange + && (abs(nx - c->x) > snap || abs(ny - c->y) > snap)) + togglefloating(NULL); + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) + resize(c, nx, ny, c->w, c->h, 1); + break; + } + } while (ev.type != ButtonRelease); + XUngrabPointer(dpy, CurrentTime); + if ((m = recttomon(c->x, c->y, c->w, c->h)) != selmon) { + sendmon(c, m); + selmon = m; + focus(NULL); + } +} + +Client * +nexttiled(Client *c) +{ + for (; c && (c->isfloating || !ISVISIBLE(c)); c = c->next); + return c; +} + +void +pop(Client *c) +{ + detach(c); + attach(c); + focus(c); + arrange(c->mon); +} + +void +pushstack(const Arg *arg) { + int i = stackpos(arg); + Client *sel = selmon->sel, *c, *p; + + if(i < 0 || !sel) + return; + else if(i == 0) { + detach(sel); + attach(sel); + } + else { + for(p = NULL, c = selmon->clients; c; p = c, c = c->next) + if(!(i -= (ISVISIBLE(c) && c != sel))) + break; + c = c ? c : p; + detach(sel); + sel->next = c->next; + c->next = sel; + } + arrange(selmon); +} + +void +propertynotify(XEvent *e) +{ + Client *c; + Window trans; + XPropertyEvent *ev = &e->xproperty; + + if ((ev->window == root) && (ev->atom == XA_WM_NAME)) { + updatestatus(); + } else if (ev->state == PropertyDelete) { + return; /* ignore */ + } else if ((c = wintoclient(ev->window))) { + switch(ev->atom) { + default: break; + case XA_WM_TRANSIENT_FOR: + if (!c->isfloating && (XGetTransientForHint(dpy, c->win, &trans)) && + (c->isfloating = (wintoclient(trans)) != NULL)) + arrange(c->mon); + break; + case XA_WM_NORMAL_HINTS: + updatesizehints(c); + break; + case XA_WM_HINTS: + updatewmhints(c); + drawbars(); + break; + } + if (ev->atom == XA_WM_NAME || ev->atom == netatom[NetWMName]) { + updatetitle(c); + if (c == c->mon->sel) + drawbar(c->mon); + } + if (ev->atom == netatom[NetWMWindowType]) + updatewindowtype(c); + } +} + +void +quit(const Arg *arg) +{ + if(arg->i) restart = 1; + running = 0; +} + +Monitor * +recttomon(int x, int y, int w, int h) +{ + Monitor *m, *r = selmon; + int a, area = 0; + + for (m = mons; m; m = m->next) + if ((a = INTERSECT(x, y, w, h, m)) > area) { + area = a; + r = m; + } + return r; +} + +void +resize(Client *c, int x, int y, int w, int h, int interact) +{ + if (applysizehints(c, &x, &y, &w, &h, interact)) + resizeclient(c, x, y, w, h); +} + +void +resizeclient(Client *c, int x, int y, int w, int h) +{ + XWindowChanges wc; + + c->oldx = c->x; c->x = wc.x = x; + c->oldy = c->y; c->y = wc.y = y; + c->oldw = c->w; c->w = wc.width = w; + c->oldh = c->h; c->h = wc.height = h; + wc.border_width = c->bw; + XConfigureWindow(dpy, c->win, CWX|CWY|CWWidth|CWHeight|CWBorderWidth, &wc); + configure(c); + XSync(dpy, False); +} + +void +resizemouse(const Arg *arg) +{ + int ocx, ocy, nw, nh; + Client *c; + Monitor *m; + XEvent ev; + Time lasttime = 0; + + if (!(c = selmon->sel)) + return; + if (c->isfullscreen) /* no support resizing fullscreen windows by mouse */ + return; + restack(selmon); + ocx = c->x; + ocy = c->y; + if (XGrabPointer(dpy, root, False, MOUSEMASK, GrabModeAsync, GrabModeAsync, + None, cursor[CurResize]->cursor, CurrentTime) != GrabSuccess) + return; + XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1); + do { + XMaskEvent(dpy, MOUSEMASK|ExposureMask|SubstructureRedirectMask, &ev); + switch(ev.type) { + case ConfigureRequest: + case Expose: + case MapRequest: + handler[ev.type](&ev); + break; + case MotionNotify: + if ((ev.xmotion.time - lasttime) <= (1000 / 60)) + continue; + lasttime = ev.xmotion.time; + + nw = MAX(ev.xmotion.x - ocx - 2 * c->bw + 1, 1); + nh = MAX(ev.xmotion.y - ocy - 2 * c->bw + 1, 1); + if (c->mon->wx + nw >= selmon->wx && c->mon->wx + nw <= selmon->wx + selmon->ww + && c->mon->wy + nh >= selmon->wy && c->mon->wy + nh <= selmon->wy + selmon->wh) + { + if (!c->isfloating && selmon->lt[selmon->sellt]->arrange + && (abs(nw - c->w) > snap || abs(nh - c->h) > snap)) + togglefloating(NULL); + } + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) + resize(c, c->x, c->y, nw, nh, 1); + break; + } + } while (ev.type != ButtonRelease); + XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w + c->bw - 1, c->h + c->bw - 1); + XUngrabPointer(dpy, CurrentTime); + while (XCheckMaskEvent(dpy, EnterWindowMask, &ev)); + if ((m = recttomon(c->x, c->y, c->w, c->h)) != selmon) { + sendmon(c, m); + selmon = m; + focus(NULL); + } +} + +void +restack(Monitor *m) +{ + Client *c; + XEvent ev; + XWindowChanges wc; + + drawbar(m); + if (!m->sel) + return; + if (m->sel->isfloating || !m->lt[m->sellt]->arrange) + XRaiseWindow(dpy, m->sel->win); + if (m->lt[m->sellt]->arrange) { + wc.stack_mode = Below; + wc.sibling = m->barwin; + for (c = m->stack; c; c = c->snext) + if (!c->isfloating && ISVISIBLE(c)) { + XConfigureWindow(dpy, c->win, CWSibling|CWStackMode, &wc); + wc.sibling = c->win; + } + } + XSync(dpy, False); + while (XCheckMaskEvent(dpy, EnterWindowMask, &ev)); +} + +void +run(void) +{ + XEvent ev; + /* main event loop */ + XSync(dpy, False); + while (running && !XNextEvent(dpy, &ev)) + if (handler[ev.type]) + handler[ev.type](&ev); /* call handler */ +} + +void +runAutostart(void) { + system("killall -q dwmblocks; dwmblocks &"); +} + +void +scan(void) +{ + unsigned int i, num; + Window d1, d2, *wins = NULL; + XWindowAttributes wa; + + if (XQueryTree(dpy, root, &d1, &d2, &wins, &num)) { + for (i = 0; i < num; i++) { + if (!XGetWindowAttributes(dpy, wins[i], &wa) + || wa.override_redirect || XGetTransientForHint(dpy, wins[i], &d1)) + continue; + if (wa.map_state == IsViewable || getstate(wins[i]) == IconicState) + manage(wins[i], &wa); + } + for (i = 0; i < num; i++) { /* now the transients */ + if (!XGetWindowAttributes(dpy, wins[i], &wa)) + continue; + if (XGetTransientForHint(dpy, wins[i], &d1) + && (wa.map_state == IsViewable || getstate(wins[i]) == IconicState)) + manage(wins[i], &wa); + } + if (wins) + XFree(wins); + } +} + +void +sendmon(Client *c, Monitor *m) +{ + if (c->mon == m) + return; + unfocus(c, 1); + detach(c); + detachstack(c); + c->mon = m; + c->tags = m->tagset[m->seltags]; /* assign tags of target monitor */ + attach(c); + attachstack(c); + setclienttagprop(c); + focus(NULL); + arrange(NULL); +} + +void +setclientstate(Client *c, long state) +{ + long data[] = { state, None }; + + XChangeProperty(dpy, c->win, wmatom[WMState], wmatom[WMState], 32, + PropModeReplace, (unsigned char *)data, 2); +} + +int +sendevent(Client *c, Atom proto) +{ + int n; + Atom *protocols; + int exists = 0; + XEvent ev; + + if (XGetWMProtocols(dpy, c->win, &protocols, &n)) { + while (!exists && n--) + exists = protocols[n] == proto; + XFree(protocols); + } + if (exists) { + ev.type = ClientMessage; + ev.xclient.window = c->win; + ev.xclient.message_type = wmatom[WMProtocols]; + ev.xclient.format = 32; + ev.xclient.data.l[0] = proto; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(dpy, c->win, False, NoEventMask, &ev); + } + return exists; +} + +void +setfocus(Client *c) +{ + if (!c->neverfocus) { + XSetInputFocus(dpy, c->win, RevertToPointerRoot, CurrentTime); + XChangeProperty(dpy, root, netatom[NetActiveWindow], + XA_WINDOW, 32, PropModeReplace, + (unsigned char *) &(c->win), 1); + } + sendevent(c, wmatom[WMTakeFocus]); +} + +void +setfullscreen(Client *c, int fullscreen) +{ + if (fullscreen && !c->isfullscreen) { + XChangeProperty(dpy, c->win, netatom[NetWMState], XA_ATOM, 32, + PropModeReplace, (unsigned char*)&netatom[NetWMFullscreen], 1); + c->isfullscreen = 1; + c->oldstate = c->isfloating; + c->oldbw = c->bw; + c->bw = 0; + c->isfloating = 1; + resizeclient(c, c->mon->mx, c->mon->my, c->mon->mw, c->mon->mh); + XRaiseWindow(dpy, c->win); + } else if (!fullscreen && c->isfullscreen){ + XChangeProperty(dpy, c->win, netatom[NetWMState], XA_ATOM, 32, + PropModeReplace, (unsigned char*)0, 0); + c->isfullscreen = 0; + c->isfloating = c->oldstate; + c->bw = c->oldbw; + c->x = c->oldx; + c->y = c->oldy; + c->w = c->oldw; + c->h = c->oldh; + resizeclient(c, c->x, c->y, c->w, c->h); + arrange(c->mon); + } +} + +int +stackpos(const Arg *arg) { + int n, i; + Client *c, *l; + + if(!selmon->clients) + return -1; + + if(arg->i == PREVSEL) { + for(l = selmon->stack; l && (!ISVISIBLE(l) || l == selmon->sel); l = l->snext); + if(!l) + return -1; + for(i = 0, c = selmon->clients; c != l; i += ISVISIBLE(c) ? 1 : 0, c = c->next); + return i; + } + else if(ISINC(arg->i)) { + if(!selmon->sel) + return -1; + for(i = 0, c = selmon->clients; c != selmon->sel; i += ISVISIBLE(c) ? 1 : 0, c = c->next); + for(n = i; c; n += ISVISIBLE(c) ? 1 : 0, c = c->next); + return MOD(i + GETINC(arg->i), n); + } + else if(arg->i < 0) { + for(i = 0, c = selmon->clients; c; i += ISVISIBLE(c) ? 1 : 0, c = c->next); + return MAX(i + arg->i, 0); + } + else + return arg->i; +} + +void +setlayout(const Arg *arg) +{ + if (!arg || !arg->v || arg->v != selmon->lt[selmon->sellt]) + selmon->sellt ^= 1; + if (arg && arg->v) + selmon->lt[selmon->sellt] = (Layout *)arg->v; + strncpy(selmon->ltsymbol, selmon->lt[selmon->sellt]->symbol, sizeof selmon->ltsymbol); + if (selmon->sel) + arrange(selmon); + else + drawbar(selmon); +} + +/* arg > 1.0 will set mfact absolutely */ +void +setmfact(const Arg *arg) +{ + float f; + + if (!arg || !selmon->lt[selmon->sellt]->arrange) + return; + f = arg->f < 1.0 ? arg->f + selmon->mfact : arg->f - 1.0; + if (f < 0.05 || f > 0.95) + return; + selmon->mfact = f; + arrange(selmon); +} + +void +setup(void) +{ + int i; + XSetWindowAttributes wa; + Atom utf8string; + + /* clean up any zombies immediately */ + sigchld(0); + + signal(SIGHUP, sighup); + signal(SIGTERM, sigterm); + + /* init screen */ + screen = DefaultScreen(dpy); + sw = DisplayWidth(dpy, screen); + sh = DisplayHeight(dpy, screen); + root = RootWindow(dpy, screen); + drw = drw_create(dpy, screen, root, sw, sh); + if (!drw_fontset_create(drw, fonts, LENGTH(fonts))) + die("no fonts could be loaded."); + lrpad = drw->fonts->h; + bh = drw->fonts->h + 2; + updategeom(); + /* init atoms */ + utf8string = XInternAtom(dpy, "UTF8_STRING", False); + wmatom[WMProtocols] = XInternAtom(dpy, "WM_PROTOCOLS", False); + wmatom[WMDelete] = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + wmatom[WMState] = XInternAtom(dpy, "WM_STATE", False); + wmatom[WMTakeFocus] = XInternAtom(dpy, "WM_TAKE_FOCUS", False); + netatom[NetActiveWindow] = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); + netatom[NetSupported] = XInternAtom(dpy, "_NET_SUPPORTED", False); + netatom[NetWMName] = XInternAtom(dpy, "_NET_WM_NAME", False); + netatom[NetWMState] = XInternAtom(dpy, "_NET_WM_STATE", False); + netatom[NetWMCheck] = XInternAtom(dpy, "_NET_SUPPORTING_WM_CHECK", False); + netatom[NetWMFullscreen] = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); + netatom[NetWMWindowType] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); + netatom[NetWMWindowTypeDialog] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); + netatom[NetClientList] = XInternAtom(dpy, "_NET_CLIENT_LIST", False); + netatom[NetClientInfo] = XInternAtom(dpy, "_NET_CLIENT_INFO", False); + /* init cursors */ + cursor[CurNormal] = drw_cur_create(drw, XC_left_ptr); + cursor[CurResize] = drw_cur_create(drw, XC_sizing); + cursor[CurMove] = drw_cur_create(drw, XC_fleur); + /* init appearance */ + scheme = ecalloc(LENGTH(colors), sizeof(Clr *)); + for (i = 0; i < LENGTH(colors); i++) + scheme[i] = drw_scm_create(drw, colors[i], 3); + /* init bars */ + updatebars(); + updatestatus(); + /* supporting window for NetWMCheck */ + wmcheckwin = XCreateSimpleWindow(dpy, root, 0, 0, 1, 1, 0, 0, 0); + XChangeProperty(dpy, wmcheckwin, netatom[NetWMCheck], XA_WINDOW, 32, + PropModeReplace, (unsigned char *) &wmcheckwin, 1); + XChangeProperty(dpy, wmcheckwin, netatom[NetWMName], utf8string, 8, + PropModeReplace, (unsigned char *) "dwm", 3); + XChangeProperty(dpy, root, netatom[NetWMCheck], XA_WINDOW, 32, + PropModeReplace, (unsigned char *) &wmcheckwin, 1); + /* EWMH support per view */ + XChangeProperty(dpy, root, netatom[NetSupported], XA_ATOM, 32, + PropModeReplace, (unsigned char *) netatom, NetLast); + XDeleteProperty(dpy, root, netatom[NetClientList]); + XDeleteProperty(dpy, root, netatom[NetClientInfo]); + /* select events */ + wa.cursor = cursor[CurNormal]->cursor; + wa.event_mask = SubstructureRedirectMask|SubstructureNotifyMask + |ButtonPressMask|PointerMotionMask|EnterWindowMask + |LeaveWindowMask|StructureNotifyMask|PropertyChangeMask; + XChangeWindowAttributes(dpy, root, CWEventMask|CWCursor, &wa); + XSelectInput(dpy, root, wa.event_mask); + grabkeys(); + focus(NULL); +} + +void +seturgent(Client *c, int urg) +{ + XWMHints *wmh; + + c->isurgent = urg; + if (!(wmh = XGetWMHints(dpy, c->win))) + return; + wmh->flags = urg ? (wmh->flags | XUrgencyHint) : (wmh->flags & ~XUrgencyHint); + XSetWMHints(dpy, c->win, wmh); + XFree(wmh); +} + +void +showhide(Client *c) +{ + if (!c) + return; + if (ISVISIBLE(c)) { + if ((c->tags & SPTAGMASK) && c->isfloating) { + c->x = c->mon->wx + (c->mon->ww / 2 - WIDTH(c) / 2); + c->y = c->mon->wy + (c->mon->wh / 2 - HEIGHT(c) / 2); + } + /* show clients top down */ + XMoveWindow(dpy, c->win, c->x, c->y); + if ((!c->mon->lt[c->mon->sellt]->arrange || c->isfloating) && !c->isfullscreen) + resize(c, c->x, c->y, c->w, c->h, 0); + showhide(c->snext); + } else { + /* hide clients bottom up */ + showhide(c->snext); + XMoveWindow(dpy, c->win, WIDTH(c) * -2, c->y); + } +} + +void +sighup(int unused) +{ + Arg a = {.i = 1}; + quit(&a); +} + +void +sigterm(int unused) +{ + Arg a = {.i = 0}; + quit(&a); +} + +#ifndef __OpenBSD__ +void +sigdwmblocks(const Arg *arg) +{ + union sigval sv; + sv.sival_int = 0 | (dwmblockssig << 8) | arg->i; + if (!dwmblockspid) + if (getdwmblockspid() == -1) + return; + + if (sigqueue(dwmblockspid, SIGUSR1, sv) == -1) { + if (errno == ESRCH) { + if (!getdwmblockspid()) + sigqueue(dwmblockspid, SIGUSR1, sv); + } + } +} +#endif + +void +sigchld(int unused) +{ + if (signal(SIGCHLD, sigchld) == SIG_ERR) + die("can't install SIGCHLD handler:"); + while (0 < waitpid(-1, NULL, WNOHANG)); +} + +void +spawn(const Arg *arg) +{ + if (fork() == 0) { + if (dpy) + close(ConnectionNumber(dpy)); + setsid(); + execvp(((char **)arg->v)[0], (char **)arg->v); + die("dwm: execvp '%s' failed:", ((char **)arg->v)[0]); + } +} + +void +setclienttagprop(Client *c) +{ + long data[] = { (long) c->tags, (long) c->mon->num }; + XChangeProperty(dpy, c->win, netatom[NetClientInfo], XA_CARDINAL, 32, + PropModeReplace, (unsigned char *) data, 2); +} + +void +tag(const Arg *arg) +{ + Client *c; + if (selmon->sel && arg->ui & TAGMASK) { + c = selmon->sel; + selmon->sel->tags = arg->ui & TAGMASK; + setclienttagprop(c); + focus(NULL); + arrange(selmon); + } +} + +void +tagmon(const Arg *arg) +{ + if (!selmon->sel || !mons->next) + return; + sendmon(selmon->sel, dirtomon(arg->i)); +} + +void +togglebar(const Arg *arg) +{ + selmon->showbar = !selmon->showbar; + updatebarpos(selmon); + XMoveResizeWindow(dpy, selmon->barwin, selmon->wx, selmon->by, selmon->ww, bh); + arrange(selmon); +} + +void +togglefloating(const Arg *arg) +{ + if (!selmon->sel) + return; + if (selmon->sel->isfullscreen) /* no support for fullscreen windows */ + return; + selmon->sel->isfloating = !selmon->sel->isfloating || selmon->sel->isfixed; + if (selmon->sel->isfloating) + resize(selmon->sel, selmon->sel->x, selmon->sel->y, + selmon->sel->w, selmon->sel->h, 0); + arrange(selmon); +} + +void +togglefullscr(const Arg *arg) +{ + if(selmon->sel) + setfullscreen(selmon->sel, !selmon->sel->isfullscreen); +} + +void +togglesticky(const Arg *arg) +{ + if (!selmon->sel) + return; + selmon->sel->issticky = !selmon->sel->issticky; + arrange(selmon); +} + +void +togglescratch(const Arg *arg) +{ + Client *c; + unsigned int found = 0; + unsigned int scratchtag = SPTAG(arg->ui); + Arg sparg = {.v = scratchpads[arg->ui].cmd}; + + for (c = selmon->clients; c && !(found = c->tags & scratchtag); c = c->next); + if (found) { + unsigned int newtagset = selmon->tagset[selmon->seltags] ^ scratchtag; + if (newtagset) { + selmon->tagset[selmon->seltags] = newtagset; + focus(NULL); + arrange(selmon); + } + if (ISVISIBLE(c)) { + focus(c); + restack(selmon); + } + } else { + selmon->tagset[selmon->seltags] |= scratchtag; + spawn(&sparg); + } +} + +void +toggletag(const Arg *arg) +{ + unsigned int newtags; + + if (!selmon->sel) + return; + newtags = selmon->sel->tags ^ (arg->ui & TAGMASK); + if (newtags) { + selmon->sel->tags = newtags; + setclienttagprop(selmon->sel); + focus(NULL); + arrange(selmon); + } +} + +void +toggleview(const Arg *arg) +{ + unsigned int newtagset = selmon->tagset[selmon->seltags] ^ (arg->ui & TAGMASK); + + if (newtagset) { + selmon->tagset[selmon->seltags] = newtagset; + focus(NULL); + arrange(selmon); + } +} + +void +unfocus(Client *c, int setfocus) +{ + if (!c) + return; + grabbuttons(c, 0); + XSetWindowBorder(dpy, c->win, scheme[SchemeNorm][ColBorder].pixel); + if (setfocus) { + XSetInputFocus(dpy, root, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); + } +} + +void +unmanage(Client *c, int destroyed) +{ + Monitor *m = c->mon; + XWindowChanges wc; + + if (c->swallowing) { + unswallow(c); + return; + } + + Client *s = swallowingclient(c->win); + if (s) { + free(s->swallowing); + s->swallowing = NULL; + arrange(m); + focus(NULL); + return; + } + + detach(c); + detachstack(c); + if (!destroyed) { + wc.border_width = c->oldbw; + XGrabServer(dpy); /* avoid race conditions */ + XSetErrorHandler(xerrordummy); + XSelectInput(dpy, c->win, NoEventMask); + XConfigureWindow(dpy, c->win, CWBorderWidth, &wc); /* restore border */ + XUngrabButton(dpy, AnyButton, AnyModifier, c->win); + setclientstate(c, WithdrawnState); + XSync(dpy, False); + XSetErrorHandler(xerror); + XUngrabServer(dpy); + } + free(c); + + if (!s) { + arrange(m); + focus(NULL); + updateclientlist(); + } +} + +void +unmapnotify(XEvent *e) +{ + Client *c; + XUnmapEvent *ev = &e->xunmap; + + if ((c = wintoclient(ev->window))) { + if (ev->send_event) + setclientstate(c, WithdrawnState); + else + unmanage(c, 0); + } +} + +void +updatebars(void) +{ + Monitor *m; + XSetWindowAttributes wa = { + .override_redirect = True, + .background_pixmap = ParentRelative, + .event_mask = ButtonPressMask|ExposureMask + }; + XClassHint ch = {"dwm", "dwm"}; + for (m = mons; m; m = m->next) { + if (m->barwin) + continue; + m->barwin = XCreateWindow(dpy, root, m->wx, m->by, m->ww, bh, 0, DefaultDepth(dpy, screen), + CopyFromParent, DefaultVisual(dpy, screen), + CWOverrideRedirect|CWBackPixmap|CWEventMask, &wa); + XDefineCursor(dpy, m->barwin, cursor[CurNormal]->cursor); + XMapRaised(dpy, m->barwin); + XSetClassHint(dpy, m->barwin, &ch); + } +} + +void +updatebarpos(Monitor *m) +{ + m->wy = m->my; + m->wh = m->mh; + if (m->showbar) { + m->wh -= bh; + m->by = m->topbar ? m->wy : m->wy + m->wh; + m->wy = m->topbar ? m->wy + bh : m->wy; + } else + m->by = -bh; +} + +void +updateclientlist() +{ + Client *c; + Monitor *m; + + XDeleteProperty(dpy, root, netatom[NetClientList]); + for (m = mons; m; m = m->next) + for (c = m->clients; c; c = c->next) + XChangeProperty(dpy, root, netatom[NetClientList], + XA_WINDOW, 32, PropModeAppend, + (unsigned char *) &(c->win), 1); +} + +int +updategeom(void) +{ + int dirty = 0; + +#ifdef XINERAMA + if (XineramaIsActive(dpy)) { + int i, j, n, nn; + Client *c; + Monitor *m; + XineramaScreenInfo *info = XineramaQueryScreens(dpy, &nn); + XineramaScreenInfo *unique = NULL; + + for (n = 0, m = mons; m; m = m->next, n++); + /* only consider unique geometries as separate screens */ + unique = ecalloc(nn, sizeof(XineramaScreenInfo)); + for (i = 0, j = 0; i < nn; i++) + if (isuniquegeom(unique, j, &info[i])) + memcpy(&unique[j++], &info[i], sizeof(XineramaScreenInfo)); + XFree(info); + nn = j; + + /* new monitors if nn > n */ + for (i = n; i < nn; i++) { + for (m = mons; m && m->next; m = m->next); + if (m) + m->next = createmon(); + else + mons = createmon(); + } + for (i = 0, m = mons; i < nn && m; m = m->next, i++) + if (i >= n + || unique[i].x_org != m->mx || unique[i].y_org != m->my + || unique[i].width != m->mw || unique[i].height != m->mh) + { + dirty = 1; + m->num = i; + m->mx = m->wx = unique[i].x_org; + m->my = m->wy = unique[i].y_org; + m->mw = m->ww = unique[i].width; + m->mh = m->wh = unique[i].height; + updatebarpos(m); + } + /* removed monitors if n > nn */ + for (i = nn; i < n; i++) { + for (m = mons; m && m->next; m = m->next); + while ((c = m->clients)) { + dirty = 1; + m->clients = c->next; + detachstack(c); + c->mon = mons; + attach(c); + attachstack(c); + } + if (m == selmon) + selmon = mons; + cleanupmon(m); + } + free(unique); + } else +#endif /* XINERAMA */ + { /* default monitor setup */ + if (!mons) + mons = createmon(); + if (mons->mw != sw || mons->mh != sh) { + dirty = 1; + mons->mw = mons->ww = sw; + mons->mh = mons->wh = sh; + updatebarpos(mons); + } + } + if (dirty) { + selmon = mons; + selmon = wintomon(root); + } + return dirty; +} + +void +updatenumlockmask(void) +{ + unsigned int i, j; + XModifierKeymap *modmap; + + numlockmask = 0; + modmap = XGetModifierMapping(dpy); + for (i = 0; i < 8; i++) + for (j = 0; j < modmap->max_keypermod; j++) + if (modmap->modifiermap[i * modmap->max_keypermod + j] + == XKeysymToKeycode(dpy, XK_Num_Lock)) + numlockmask = (1 << i); + XFreeModifiermap(modmap); +} + +void +updatesizehints(Client *c) +{ + long msize; + XSizeHints size; + + if (!XGetWMNormalHints(dpy, c->win, &size, &msize)) + /* size is uninitialized, ensure that size.flags aren't used */ + size.flags = PSize; + if (size.flags & PBaseSize) { + c->basew = size.base_width; + c->baseh = size.base_height; + } else if (size.flags & PMinSize) { + c->basew = size.min_width; + c->baseh = size.min_height; + } else + c->basew = c->baseh = 0; + if (size.flags & PResizeInc) { + c->incw = size.width_inc; + c->inch = size.height_inc; + } else + c->incw = c->inch = 0; + if (size.flags & PMaxSize) { + c->maxw = size.max_width; + c->maxh = size.max_height; + } else + c->maxw = c->maxh = 0; + if (size.flags & PMinSize) { + c->minw = size.min_width; + c->minh = size.min_height; + } else if (size.flags & PBaseSize) { + c->minw = size.base_width; + c->minh = size.base_height; + } else + c->minw = c->minh = 0; + if (size.flags & PAspect) { + c->mina = (float)size.min_aspect.y / size.min_aspect.x; + c->maxa = (float)size.max_aspect.x / size.max_aspect.y; + } else + c->maxa = c->mina = 0.0; + c->isfixed = (c->maxw && c->maxh && c->maxw == c->minw && c->maxh == c->minh); +} + +void +updatestatus(void) +{ + if (!gettextprop(root, XA_WM_NAME, rawstext, sizeof(rawstext))) + strcpy(stext, "dwm-"VERSION); + else + copyvalidchars(stext, rawstext); + drawbar(selmon); +} + +void +updatetitle(Client *c) +{ + if (!gettextprop(c->win, netatom[NetWMName], c->name, sizeof c->name)) + gettextprop(c->win, XA_WM_NAME, c->name, sizeof c->name); + if (c->name[0] == '\0') /* hack to mark broken clients */ + strcpy(c->name, broken); +} + +void +updatewindowtype(Client *c) +{ + Atom state = getatomprop(c, netatom[NetWMState]); + Atom wtype = getatomprop(c, netatom[NetWMWindowType]); + + if (state == netatom[NetWMFullscreen]) + setfullscreen(c, 1); + if (wtype == netatom[NetWMWindowTypeDialog]) + c->isfloating = 1; +} + +void +updatewmhints(Client *c) +{ + XWMHints *wmh; + + if ((wmh = XGetWMHints(dpy, c->win))) { + if (c == selmon->sel && wmh->flags & XUrgencyHint) { + wmh->flags &= ~XUrgencyHint; + XSetWMHints(dpy, c->win, wmh); + } else + c->isurgent = (wmh->flags & XUrgencyHint) ? 1 : 0; + if (wmh->flags & InputHint) + c->neverfocus = !wmh->input; + else + c->neverfocus = 0; + XFree(wmh); + } +} + +void +view(const Arg *arg) +{ + if ((arg->ui & TAGMASK) == selmon->tagset[selmon->seltags]) + return; + selmon->seltags ^= 1; /* toggle sel tagset */ + if (arg->ui & TAGMASK) + selmon->tagset[selmon->seltags] = arg->ui & TAGMASK; + focus(NULL); + arrange(selmon); +} + +pid_t +winpid(Window w) +{ + pid_t result = 0; + + xcb_res_client_id_spec_t spec = {0}; + spec.client = w; + spec.mask = XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID; + + xcb_generic_error_t *e = NULL; + xcb_res_query_client_ids_cookie_t c = xcb_res_query_client_ids(xcon, 1, &spec); + xcb_res_query_client_ids_reply_t *r = xcb_res_query_client_ids_reply(xcon, c, &e); + + if (!r) + return (pid_t)0; + + xcb_res_client_id_value_iterator_t i = xcb_res_query_client_ids_ids_iterator(r); + for (; i.rem; xcb_res_client_id_value_next(&i)) { + spec = i.data->spec; + if (spec.mask & XCB_RES_CLIENT_ID_MASK_LOCAL_CLIENT_PID) { + uint32_t *t = xcb_res_client_id_value_value(i.data); + result = *t; + break; + } + } + + free(r); + + if (result == (pid_t)-1) + result = 0; + return result; +} + +pid_t +getparentprocess(pid_t p) +{ + unsigned int v = 0; + +#if defined(__linux__) + FILE *f; + char buf[256]; + snprintf(buf, sizeof(buf) - 1, "/proc/%u/stat", (unsigned)p); + + if (!(f = fopen(buf, "r"))) + return (pid_t)0; + + if (fscanf(f, "%*u %*s %*c %u", (unsigned *)&v) != 1) + v = (pid_t)0; + fclose(f); +#elif defined(__FreeBSD__) + struct kinfo_proc *proc = kinfo_getproc(p); + if (!proc) + return (pid_t)0; + + v = proc->ki_ppid; + free(proc); +#endif + return (pid_t)v; +} + +int +isdescprocess(pid_t p, pid_t c) +{ + while (p != c && c != 0) + c = getparentprocess(c); + + return (int)c; +} + +Client * +termforwin(const Client *w) +{ + Client *c; + Monitor *m; + + if (!w->pid || w->isterminal) + return NULL; + + for (m = mons; m; m = m->next) { + for (c = m->clients; c; c = c->next) { + if (c->isterminal && !c->swallowing && c->pid && isdescprocess(c->pid, w->pid)) + return c; + } + } + + return NULL; +} + +Client * +swallowingclient(Window w) +{ + Client *c; + Monitor *m; + + for (m = mons; m; m = m->next) { + for (c = m->clients; c; c = c->next) { + if (c->swallowing && c->swallowing->win == w) + return c; + } + } + + return NULL; +} + +Client * +wintoclient(Window w) +{ + Client *c; + Monitor *m; + + for (m = mons; m; m = m->next) + for (c = m->clients; c; c = c->next) + if (c->win == w) + return c; + return NULL; +} + +Monitor * +wintomon(Window w) +{ + int x, y; + Client *c; + Monitor *m; + + if (w == root && getrootptr(&x, &y)) + return recttomon(x, y, 1, 1); + for (m = mons; m; m = m->next) + if (w == m->barwin) + return m; + if ((c = wintoclient(w))) + return c->mon; + return selmon; +} + +/* There's no way to check accesses to destroyed windows, thus those cases are + * ignored (especially on UnmapNotify's). Other types of errors call Xlibs + * default error handler, which may call exit. */ +int +xerror(Display *dpy, XErrorEvent *ee) +{ + if (ee->error_code == BadWindow + || (ee->request_code == X_SetInputFocus && ee->error_code == BadMatch) + || (ee->request_code == X_PolyText8 && ee->error_code == BadDrawable) + || (ee->request_code == X_PolyFillRectangle && ee->error_code == BadDrawable) + || (ee->request_code == X_PolySegment && ee->error_code == BadDrawable) + || (ee->request_code == X_ConfigureWindow && ee->error_code == BadMatch) + || (ee->request_code == X_GrabButton && ee->error_code == BadAccess) + || (ee->request_code == X_GrabKey && ee->error_code == BadAccess) + || (ee->request_code == X_CopyArea && ee->error_code == BadDrawable)) + return 0; + fprintf(stderr, "dwm: fatal error: request code=%d, error code=%d\n", + ee->request_code, ee->error_code); + return xerrorxlib(dpy, ee); /* may call exit */ +} + +int +xerrordummy(Display *dpy, XErrorEvent *ee) +{ + return 0; +} + +/* Startup Error handler to check if another window manager + * is already running. */ +int +xerrorstart(Display *dpy, XErrorEvent *ee) +{ + die("dwm: another window manager is already running"); + return -1; +} + +void +zoom(const Arg *arg) +{ + Client *c = selmon->sel; + + if (!selmon->lt[selmon->sellt]->arrange || !c || c->isfloating) + return; + if (c == nexttiled(selmon->clients) && !(c = nexttiled(c->next))) + return; + pop(c); +} + +void +xrdb(const Arg *arg) +{ + load_xresources(); + + for (int i = 0; i < LENGTH(colors); i++) + scheme[i] = drw_scm_create(drw, colors[i], 3); + + focus(NULL); + arrange(NULL); +} + +void +resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst) +{ + char *sdst = NULL; + int *idst = NULL; + float *fdst = NULL; + + sdst = dst; + idst = dst; + fdst = dst; + + char fullname[256]; + char *type; + XrmValue ret; + + snprintf(fullname, sizeof(fullname), "%s.%s", "dwm", name); + fullname[sizeof(fullname) - 1] = '\0'; + + XrmGetResource(db, fullname, "*", &type, &ret); + if (!(ret.addr == NULL || strncmp("String", type, 64))) + { + switch (rtype) { + case STRING: + strcpy(sdst, ret.addr); + break; + case INTEGER: + *idst = strtoul(ret.addr, NULL, 10); + break; + case FLOAT: + *fdst = strtof(ret.addr, NULL); + break; + } + } +} + +void +load_xresources(void) +{ + Display *display; + char *resm; + XrmDatabase db; + ResourcePref *p; + + display = XOpenDisplay(NULL); + resm = XResourceManagerString(display); + if (!resm) + return; + + db = XrmGetStringDatabase(resm); + for (p = resources; p < resources + LENGTH(resources); p++) + resource_load(db, p->name, p->type, p->dst); + XCloseDisplay(display); +} + +int +main(int argc, char *argv[]) +{ + if (argc == 2 && !strcmp("-v", argv[1])) + die("dwm-"VERSION); + else if (argc != 1) + die("usage: dwm [-v]"); + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fputs("warning: no locale support\n", stderr); + if (!(dpy = XOpenDisplay(NULL))) + die("dwm: cannot open display"); + if (!(xcon = XGetXCBConnection(dpy))) + die("dwm: cannot get xcb connection\n"); + checkotherwm(); + XrmInitialize(); + load_xresources(); + setup(); +#ifdef __OpenBSD__ + if (pledge("stdio rpath proc exec", NULL) == -1) + die("pledge"); +#endif /* __OpenBSD__ */ + scan(); + runAutostart(); + run(); + if(restart) execvp(argv[0], argv); + cleanup(); + XCloseDisplay(dpy); + return EXIT_SUCCESS; +} diff --git a/mut/dwm/larbs.mom b/mut/dwm/larbs.mom new file mode 100644 index 0000000..999ce1c --- /dev/null +++ b/mut/dwm/larbs.mom @@ -0,0 +1,362 @@ +.de LI +.LIST +.SHIFT_LIST 10p +.. +.PARA_SPACE 1m +.TITLE "\s+(10A Friendly Guide to LARBS!\s0" +.AUTHOR "\s+5Luke Smith\s0" +.DOCTYPE DEFAULT +.COPYSTYLE FINAL +.PRINTSTYLE TYPESET +.PT_SIZE 12 +.START +Use vim keys (\f(CWh/j/k/l\fP) to navigate this document. +Pressing \f(CWs\fP will fit it to window width (\f(CWa\fP to revert). +\f(CWK\fP and \f(CWJ\fP zoom in and out. +\f(CWSuper+f\fP to toggle fullscreen. +\f(CWq\fP to quit. +\f(CW/\fP to search for text. +(These are general binds set for \fBzathura\fP, the pdf reader.) +.LI +.ITEM +\f(CWMod+F1\fP will show this document at any time. +.ITEM +By \f(CWMod\fP, I mean the Super Key, usually known as "the Windows Key." +.LIST OFF +.PP +FAQs are at the end of this document. +.HEADING 1 "Welcome!" +.HEADING 2 "Basic goals and principles of this system:" +.LI +.ITEM +\fBNaturalness\fP \(en +Remove the border between mind and matter: +everything important should be as few keypresses as possible away from you, +and you shouldn't have to think about what you're doing. +Immersion. +.ITEM +\fBEconomy\fP \(en +Programs should be simple and light on system resources and highly extensible. +Because of this, many are terminal or small ncurses programs that have all the magic inside of them. +.ITEM +\fBKeyboard/vim-centrality\fP \(en +All terminal programs (and other programs) use vim keys when possible. +Your hands never need leave the home row or thereabout. +.ITEM +\fBDecentralization\fP \(en +This system is a web of small, modifiable and replaceable programs that users can easily customize. +.LIST OFF +.HEADING 2 "General keyboard changes" +.LI +.ITEM +Capslock is a useless key in high quality space. +It's now remapped. +If you press it alone, it will function as escape, making vimcraft much more natural, +but you can also hold it down and it will act as another Windows/super/mod key. +.ITEM +The menu button (usually between the right Alt and Ctrl) is an alternative Super/Mod button. +This is to make one-handing on laptops easier. +.LIST OFF +.PP +If you'd like to change any of these keyboard changes, you need only open and change the \f(CWremaps\fP script. +All custom scripts in LARBS are located in \f(CW~/.local/bin/\fP. +Actually, this should go without saying, but \fIeverything\fP here can easily be changed. +Below in this document, there is information about where to change programs/components. +.PP +Additionally, while this isn't a part of the desktop environment, the default editing mode in the shell is using vi bindings. +If you want to learn more of this, run \f(CWMod+F2\fP and type and select the option for "vi mode in shell". +This setting can be changed if you don't like it by deleting or commenting out the contents of \f(CW~/.config/shell/inputrc\fP. +.HEADING 2 "The Status Bar" +.PP +To the left, you'll see the numbers of your current workspace/tag(s). +On the right, you'll see various system status notifiers, the date, volume, even music and local weather if possible, etc. +Each module on the right of the statusbar is a script located in \f(CW~/.local/bin/statusbar/\fP. +You can see what they do and modify them from there. +I'm sure you can figure it out. +You can also right click on the module to see what it does. +.PP +The program dwmblocks is what is run to generate the statusbar from those scripts. +You can edit its config/source code in \f(CW~/.local/src/dwmblocks/\fP to tell it what scripts/commands you want it to display. +.HEADING 3 "HiDPI and 4K Displays" +.PP +If you have a screen with a very high dots-per-inch, the interface, particularly the status bar at the top may be very small. To change this, you can run \f(CWxrandr --dpi 96\fP, replacing 96 with a higher number, then you can refresh the window manager in the menu at \f(CWsuper+backspace\fP. To make this change persistent after reboot, edit \f(CW~/.xprofile\fP and you will see that same command which you can change to have the dots-per-inch value you want. +.HEADING 2 "Deeper Tutorials" +.PP +Press \f(CWmod+F2\fP at any time to get a menu of programs to watch videos about streaming directly from YouTube. +You can also check the config files for programs which detail a lot of the specific bindings. +.HEADING 1 "Key Bindings" +.PP +The window manager dwm abstractly orders all of your windows into a stack from most important to least based on when you last manipulated it. +dwm is an easy to use window manager, but you should understand that it makes use of that stack layout. +If you're not familiar, I recommend you press \f(CWMod+F2\fP and select the "dwm" option to watch my brief tutorial (note that the bindings I discuss in the video are the default dwm binds, which are different (inferior) to those here). +.PP +Notice also the case sensitivity of the shortcuts\c +.FOOTNOTE +To type capital letters, hold down the \f(CWShift\fP key\(emthat might sound like an obvious and condescending thing to tell you, but there have literally been multiple people (Boomers) who have emailed me asking how to type a capital letter since caps lock isn't enabled. +.FOOTNOTE OFF + , Be sure you play around with these. Be flexible with the basic commands and the system will grow on you quick. +.LI +.ITEM +\f(CWMod+Enter\fP \(en Spawn terminal (the default terminal is \f(CWst\fP; run \f(CWman st\fP for more.) +.ITEM +\f(CWMod+q\fP \(en Close window +.ITEM +\f(CWMod+d\fP \(en dmenu (For running commands or programs without shortcuts) +.ITEM +\f(CWMod+j/k\fP \(en Cycle thru windows by their stack order +.ITEM +\f(CWMod+Space\fP \(en Make selected window the master (or switch master with 2nd) +.ITEM +\f(CWMod+h/l\fP \(en Change width of master window +.ITEM +\f(CWMod+z/x\fP \(en Increase/decrease gaps (may also hold \f(CWMod\fP and scroll mouse) +.ITEM +\f(CWMod+a\fP \(en Toggle gaps +.ITEM +\f(CWMod+A\fP \(en Gaps return to default values (may also hold \f(CWMod\fP and middle click) +.ITEM +\f(CWMod+Shift+Space\fP \(en Make a window float (move and resize with \f(CWMod+\fPleft/right click). +.ITEM +\f(CWMod+s\fP \(en Make/unmake a window "sticky" (follows you from tag to tag) +.ITEM +\f(CWMod+b\fP \(en Toggle statusbar (may also middle click on desktop) +.ITEM +\f(CWMod+v\fP \(en Jump to master window +.LIST OFF +.HEADING 2 "Window layouts" +.LI +.ITEM +\f(CWMod+t\fP \(en Tiling mode (active by default) +.ITEM +\f(CWMod+T\fP \(en Bottom stack mode (just like tiling, but master is on top) +.ITEM +\f(CWMod+f\fP \(en Fullscreen mode +.ITEM +\f(CWMod+F\fP \(en Floating (AKA normie) mode +.ITEM +\f(CWMod+y\fP \(en Fibonacci spiral mode +.ITEM +\f(CWMod+Y\fP \(en Dwindle mode (similar to Fibonacci) +.ITEM +\f(CWMod+u\fP \(en Master on left, other windows in monocle mode +.ITEM +\f(CWMod+U\fP \(en Monocle mode (all windows fullscreen and cycle through) +.ITEM +\f(CWMod+i\fP \(en Center the master window +.ITEM +\f(CWMod+I\fP \(en Center and float the master window +.ITEM +\f(CWMod+o/O\fP \(en Increase/decrease the number of master windows +.LIST OFF +.HEADING 2 "Basic Programs" +.LI +.ITEM +\f(CWMod+r\fP \(en lf (file browser/manager) +.ITEM +\f(CWMod+R\fP \(en htop (task manager, system monitor that R*dditors use to look cool) +.ITEM +\f(CWMod+e\fP \(en neomutt (email) \(en Must be first configured by running \f(CWmw add\fP. +.ITEM +\f(CWMod+E\fP \(en abook (contacts, addressbook, emails) +.ITEM +\f(CWMod+m\fP \(en ncmpcpp (music player) +.ITEM +\f(CWMod+w\fP \(en Web browser (LibreWolf by default) +.ITEM +\f(CWMod+W\fP \(en nmtui (for connecting to wireless internet) +.ITEM +\f(CWMod+n\fP \(en vimwiki (for notes) +.ITEM +\f(CWMod+N\fP \(en newsboat (RSS feed reader) +.ITEM +\f(CWMod+F4\fP \(en pulsemixer (audio system control) +.ITEM +\f(CWMod+Shift+Enter\fP \(en Show/hide dropdown terminal +.ITEM +\f(CWMod+'\fP \(en Show/hide dropdown calculator +.ITEM +\f(CWMod+D\fP \(en passmenu (password manager) +.LIST OFF +.HEADING 2 "Tags/Workspaces" +.PP +There are nine tags, active tags are highlighted in the top left. +.LI +.ITEM +\f(CWMod+(Number)\fP \(en Go to that number tag +.ITEM +\f(CWMod+Shift+(Number)\fP \(en Send window to that tag +.ITEM +\f(CWMod+Tab\fP \(en Go to previous tag (may also use \f(CW\\\fP for Tab) +.ITEM +\f(CWMod+g\fP \(en Go to left tag (hold shift to send window there) +.ITEM +\f(CWMod+;\fP \(en Go to right tag (hold shift to send window there) +.ITEM +\f(CWMod+Left/Right\fP \(en Go to another display +.ITEM +\f(CWMod+Shift+Left/+Right\fP \(en Move window to another display +.LIST OFF +.HEADING 2 "System" +.LI +.ITEM +\f(CWMod+BackSpace\fP \(enChoose to lock screen, logout, shutdown, reboot, etc. +.ITEM +\f(CWMod+F1\fP \(en Show this document +.ITEM +\f(CWMod+F2\fP \(en Watch tutorial videos on a subject +.ITEM +\f(CWMod+F3\fP \(en Select screen/display to use +.ITEM +\f(CWMod+F4\fP \(en pulsemixer (audio control) +.ITEM +\f(CWMod+F6\fP \(en Transmission torrent client (not installed by default) +.ITEM +\f(CWMod+F7\fP \(en Toggle on/off transmission client via dmenu +.ITEM +\f(CWMod+F8\fP \(en Check mail, if mutt-wizard is configured. (Run \f(CWmw add\fP to set up.) +.ITEM +\f(CWMod+F9\fP \(en Mount a USB drive/hard drive or Android +.ITEM +\f(CWMod+F10\fP \(en Unmount a non-essential drive or Android +.ITEM +\f(CWMod+F11\fP \(en View webcam +.ITEM +\f(CWMod+F12\fP \(en Rerun keyboard mapping scripts if new keyboard is attached +.ITEM +\f(CWMod+`\fP \(en Select an emoji to copy to clipboard +.ITEM +\f(CWMod+Insert\fP \(en Pastes text you have saved in a file at ~/.local/share/larbs/snippets +.LIST OFF +.HEADING 2 "Audio" +.PP +I use ncmpcpp as a music player, which is a front end for mpd. +.LI +.ITEM +\f(CWMod+m\fP \(en ncmpcpp, the music player +.ITEM +\f(CWMod+.\fP \(en Next track +.ITEM +\f(CWMod+,\fP \(en Previous track +.ITEM +\f(CWMod+<\fP \(en Restart track +.ITEM +\f(CWMod+>\fP \(en Toggle playlist looping +.ITEM +\f(CWMod+p\fP \(en Toggle pause +.ITEM +\f(CWMod+p\fP \(en Force pause music player daemon and all mpv videos +.ITEM +\f(CWMod+M\fP \(en Mute all audio +.ITEM +\f(CWMod+-\fP \(en Decrease volume (holding shift increases amount) +.ITEM +\f(CWMod++\fP \(en Increase volume (holding shift increases amount) +.ITEM +\f(CWMod+[\fP \(en Back 10 seconds (holding shift moves by one minute) +.ITEM +\f(CWMod+]\fP \(en Forward 10 seconds (holding shift moves by one minute) +.ITEM +\f(CWMod+F4\fP \(en pulsemixer (general audio/volume sink/source control) +.LIST OFF +.HEADING 2 "Recording" +.PP +I use maim and ffmpeg to make different recordings of the desktop and audio. +All of these recording shortcuts will output into \f(CW~\fP, and will not overwrite +previous recordings as their names are based on their exact times. +.LI +.ITEM +\f(CWPrintScreen\fP \(en Take a screenshot +.ITEM +\f(CWShift+PrintScreen\fP \(en Select area to screenshot +.ITEM +\f(CWMod+PrintScreen\fP \(en Opens dmenu menu to select kind of audio/video recording +.ITEM +\f(CWMod+Delete\fP \(en Kills any recording started in the above way. +.ITEM +\f(CWMod+Shift+c\fP \(en Toggles a webcam in the bottom right for screencasting. +.ITEM +\f(CWMod+ScrollLock\fP \(en Toggle screenkey (if installed) to show keypresses +.LIST OFF +.HEADING 2 "Other buttons" +.PP +I've mapped those extra buttons that some keyboards have (play and pause +buttons, screen brightness, email, web browsing buttons, etc.) to what you +would expect. +.HEADING 1 "Configuration" +.PP +Dotfiles/settings files are located in \f(CW~/.config/\fP. +.PP +Suckless programs, dwm (the window manager), st (the terminal) and dmenu among others do not have traditional config files, but have their source code location in \f(CW~/.local/src/\fP (press \f(CWrr\fP to jump to that directory). +There you can modify their \f(CWconfig.h\fP files or other source code, then \f(CWsudo make install\fP to reinstall. +.PP +vim is set to automatically recompile and install these programs whenever you save changes to any \f(CWconfig.h\fP file +(compilation will be nearly instantaneous). +You'll have to restart the program to see its effects obviously. +.HEADING 1 "Frequently Asked Questions (FAQs)" +.HEADING 2 "My keyboard isn't working as expected!" +.PP +As mentioned above, LARBS makes some keyboard changes with the \f(CWremaps\fP script. +These settings may override your preferred settings, so you should open this file and comment out troublesome lines if you have issues. +.HEADING 2 "My audio isn't working!" +.PP +On fresh install, the Linux audio system often mutes outputs. +You may also need to set your preferred default output sink which you can do by the command line, or by selecting one with \f(CWpulsemixer\fP (\f(CWMod+F4\fP). +.HEADING 2 "How do I copy and paste?" +.PP +Copying and pasting is always program-specific on any system. +In most graphical programs, copy and paste will be the same as they are on Windows: \f(CWctrl-c\fP and \f(CWctrl-v\fP. +In the Linux terminal, those binds have other more important purposes, so you can run \f(CWman st\fP to see how to copy and paste in my terminal build. +.PP +Additionally, I've set vim to use the clipboard as the default buffer, which means when you yank or delete something in vim, it will be in your system clipboard as well, so you can \f(CWctrl-v\fP it into your browser instance, etc. You can also paste material copied from other programs into vim with the typical vim bindings. +.HEADING 2 "How do I change the background/wallpaper?" +.PP +The system will always read the file \f(CW~/.local/share/bg\fP as the wallpaper. +The script \f(CWsetbg\fP, if run on an image will set it as the persistent background. +When using the file manager, you can simply hover over an image name and type \f(CWb\fP and this will run \f(CWsetbg\fP. +.HEADING 2 "How I change the colorscheme?" +.PP +LARBS no longer deploys Xresources by default, but check \f(CW~/.config/x11/xresources\fP for a list of color schemes you can activate or add your own. When you save the file, vim will automatically update the colorscheme. If you'd like these colors activated by default on login, there is a line in \f(CW~/.config/x11/xprofile\fP you can uncomment to allow that. +.PP +Or, if you want to use \f(CWwal\fP to automatically generate colorschemes from your wallpapers, just install it and \f(CWsetbg\fP will automatically detect and run it on startup and wallpaper change. +.HEADING 2 "How do I set up my email?" +.PP +LARBS comes with mutt-wizard, which gives the ability to receive and send all your email and keep an offline store of it all in your terminal, without the need for browser. +You can add email accounts by running \f(CWmw -a your@email.com\fP. +See \f(CWman mw\fP for all the information you need about mutt-wizard. +.PP +Once you have successfully added your email address(es), you can open your mail with \f(CWneomutt\fP which is also bound to \f(CWMod+e\fP. +You can sync your mail by pressing \f(CWMod+F8\fP and you can set a cronjob to sync mail every 10 minutes by running \f(CWmw -t 10\fP. +.PP +You may also want to install \f(CWpam-gnupg-git\fP, which can be set up to automatically unlock your GPG key on login, which will allow you avoid having put in a password to sync and send, all while keeping your password safe and encrypted on your machine. +.HEADING 2 "How do I set up my music?" +.PP +By default, mpd, the music daemon assumes that \f(CW~/Music\fP is your music directory. +This can be changed in \f(CW~/.config/mpd/mpd.conf\fP. +When you add music to your music folder, you may have to run \f(CWmpc up\fP in the terminal to update the database. +mpd is controlled by ncmpcpp, which is accessible by \f(CWMod+m\fP. +.HEADING 2 "How do I update LARBS?" +.PP +LARBS is deployed as a git repository in your home directory. +You can use it as such to fetch, diff and merge changes from the remote repository. +If you don't want to do that or don't know how to use git, you can actually just rerun the script (as root) and reinstall LARBS and it will automatically update an existing install if you select the same username. +This will overwrite the original config files though, including changes you made for them, but this is an easier brute force approach that will also install any new dependencies. +.HEADING 1 "Important Links" +.PP +You can follow links via the keyboard in this pdf reader by pressing \f(CWf\fP followed by the number that appears on the desired link. +.LI +.ITEM +.PDF_WWW_LINK "mailto:luke@lukesmith.xyz" "luke@lukesmith.xyz" +\(en For questions! +.ITEM +.PDF_WWW_LINK "http://lukesmith.xyz" "https://lukesmith.xyz" +\(en For stalking! +.ITEM +.PDF_WWW_LINK "https://lukesmith.xyz/donate" "https://lukesmith.xyz/donate" +\(en To incentivize more development of LARBS! +.ITEM +.PDF_WWW_LINK "https://github.com/LukeSmithxyz" "My Github Page" +\(en For the code behind it! +.ITEM +.PDF_WWW_LINK "http://lukesmith.xyz/rss.xml" "RSS" +\(en For updates! +.LIST OFF diff --git a/mut/dwm/shiftview.c b/mut/dwm/shiftview.c new file mode 100644 index 0000000..bb43969 --- /dev/null +++ b/mut/dwm/shiftview.c @@ -0,0 +1,65 @@ +/** Function to shift the current view to the left/right + * + * @param: "arg->i" stores the number of tags to shift right (positive value) + * or left (negative value) + */ +void +shiftview(const Arg *arg) +{ + Arg shifted; + Client *c; + unsigned int tagmask = 0; + + for (c = selmon->clients; c; c = c->next) + if (!(c->tags & SPTAGMASK)) + tagmask = tagmask | c->tags; + + shifted.ui = selmon->tagset[selmon->seltags] & ~SPTAGMASK; + if (arg->i > 0) /* left circular shift */ + do { + shifted.ui = (shifted.ui << arg->i) + | (shifted.ui >> (LENGTH(tags) - arg->i)); + shifted.ui &= ~SPTAGMASK; + } while (tagmask && !(shifted.ui & tagmask)); + else /* right circular shift */ + do { + shifted.ui = (shifted.ui >> (- arg->i) + | shifted.ui << (LENGTH(tags) + arg->i)); + shifted.ui &= ~SPTAGMASK; + } while (tagmask && !(shifted.ui & tagmask)); + + view(&shifted); +} + +void +shifttag(const Arg *arg) +{ + Arg a; + Client *c; + unsigned visible = 0; + int i = arg->i; + int count = 0; + int nextseltags, curseltags = selmon->tagset[selmon->seltags]; + + do { + if(i > 0) // left circular shift + nextseltags = (curseltags << i) | (curseltags >> (LENGTH(tags) - i)); + + else // right circular shift + nextseltags = (curseltags >> - i) | (curseltags << (LENGTH(tags) + i)); + + // Check if tag is visible + for (c = selmon->clients; c && !visible; c = c->next) + if (nextseltags & c->tags) { + visible = 1; + break; + } + i += arg->i; + } while (!visible && ++count < 10); + + if (count < 10) { + a.i = nextseltags; + tag(&a); + } +} + diff --git a/mut/dwm/transient.c b/mut/dwm/transient.c new file mode 100644 index 0000000..040adb5 --- /dev/null +++ b/mut/dwm/transient.c @@ -0,0 +1,42 @@ +/* cc transient.c -o transient -lX11 */ + +#include <stdlib.h> +#include <unistd.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> + +int main(void) { + Display *d; + Window r, f, t = None; + XSizeHints h; + XEvent e; + + d = XOpenDisplay(NULL); + if (!d) + exit(1); + r = DefaultRootWindow(d); + + f = XCreateSimpleWindow(d, r, 100, 100, 400, 400, 0, 0, 0); + h.min_width = h.max_width = h.min_height = h.max_height = 400; + h.flags = PMinSize | PMaxSize; + XSetWMNormalHints(d, f, &h); + XStoreName(d, f, "floating"); + XMapWindow(d, f); + + XSelectInput(d, f, ExposureMask); + while (1) { + XNextEvent(d, &e); + + if (t == None) { + sleep(5); + t = XCreateSimpleWindow(d, r, 50, 50, 100, 100, 0, 0, 0); + XSetTransientForHint(d, t, f); + XStoreName(d, t, "transient"); + XMapWindow(d, t); + XSelectInput(d, t, ExposureMask); + } + } + + XCloseDisplay(d); + exit(0); +} diff --git a/mut/dwm/util.c b/mut/dwm/util.c new file mode 100644 index 0000000..96b82c9 --- /dev/null +++ b/mut/dwm/util.c @@ -0,0 +1,36 @@ +/* See LICENSE file for copyright and license details. */ +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "util.h" + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(1); +} + +void * +ecalloc(size_t nmemb, size_t size) +{ + void *p; + + if (!(p = calloc(nmemb, size))) + die("calloc:"); + return p; +} diff --git a/mut/dwm/util.h b/mut/dwm/util.h new file mode 100644 index 0000000..f633b51 --- /dev/null +++ b/mut/dwm/util.h @@ -0,0 +1,8 @@ +/* See LICENSE file for copyright and license details. */ + +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) + +void die(const char *fmt, ...); +void *ecalloc(size_t nmemb, size_t size); diff --git a/mut/dwm/vanitygaps.c b/mut/dwm/vanitygaps.c new file mode 100644 index 0000000..4c98e69 --- /dev/null +++ b/mut/dwm/vanitygaps.c @@ -0,0 +1,550 @@ +/* Key binding functions */ +static void defaultgaps(const Arg *arg); +static void incrgaps(const Arg *arg); +/* static void incrigaps(const Arg *arg); */ +/* static void incrogaps(const Arg *arg); */ +/* static void incrohgaps(const Arg *arg); */ +/* static void incrovgaps(const Arg *arg); */ +/* static void incrihgaps(const Arg *arg); */ +/* static void incrivgaps(const Arg *arg); */ +static void togglegaps(const Arg *arg); +static void togglesmartgaps(const Arg *arg); + +/* Layouts */ +static void bstack(Monitor *m); +static void centeredmaster(Monitor *m); +static void centeredfloatingmaster(Monitor *m); +static void deck(Monitor *m); +static void dwindle(Monitor *m); +static void fibonacci(Monitor *m, int s); +static void spiral(Monitor *m); +static void tile(Monitor *); + +/* Internals */ +static void getgaps(Monitor *m, int *oh, int *ov, int *ih, int *iv, unsigned int *nc); +static void setgaps(int oh, int ov, int ih, int iv); + +/* Settings */ +static int enablegaps = 1; + +static void +setgaps(int oh, int ov, int ih, int iv) +{ + if (oh < 0) oh = 0; + if (ov < 0) ov = 0; + if (ih < 0) ih = 0; + if (iv < 0) iv = 0; + + selmon->gappoh = oh; + selmon->gappov = ov; + selmon->gappih = ih; + selmon->gappiv = iv; + arrange(selmon); +} + +static void +togglegaps(const Arg *arg) +{ + enablegaps = !enablegaps; + arrange(NULL); +} + +static void +togglesmartgaps(const Arg *arg) +{ + smartgaps = !smartgaps; + arrange(NULL); +} + +static void +defaultgaps(const Arg *arg) +{ + setgaps(gappoh, gappov, gappih, gappiv); +} + +static void +incrgaps(const Arg *arg) +{ + setgaps( + selmon->gappoh + arg->i, + selmon->gappov + arg->i, + selmon->gappih + arg->i, + selmon->gappiv + arg->i + ); +} + +/* static void */ +/* incrigaps(const Arg *arg) */ +/* { */ +/* setgaps( */ +/* selmon->gappoh, */ +/* selmon->gappov, */ +/* selmon->gappih + arg->i, */ +/* selmon->gappiv + arg->i */ +/* ); */ +/* } */ + +/* static void */ +/* incrogaps(const Arg *arg) */ +/* { */ +/* setgaps( */ +/* selmon->gappoh + arg->i, */ +/* selmon->gappov + arg->i, */ +/* selmon->gappih, */ +/* selmon->gappiv */ +/* ); */ +/* } */ + +/* static void */ +/* incrohgaps(const Arg *arg) */ +/* { */ +/* setgaps( */ +/* selmon->gappoh + arg->i, */ +/* selmon->gappov, */ +/* selmon->gappih, */ +/* selmon->gappiv */ +/* ); */ +/* } */ + +/* static void */ +/* incrovgaps(const Arg *arg) */ +/* { */ +/* setgaps( */ +/* selmon->gappoh, */ +/* selmon->gappov + arg->i, */ +/* selmon->gappih, */ +/* selmon->gappiv */ +/* ); */ +/* } */ + +/* static void */ +/* incrihgaps(const Arg *arg) */ +/* { */ +/* setgaps( */ +/* selmon->gappoh, */ +/* selmon->gappov, */ +/* selmon->gappih + arg->i, */ +/* selmon->gappiv */ +/* ); */ +/* } */ + +/* static void */ +/* incrivgaps(const Arg *arg) */ +/* { */ +/* setgaps( */ +/* selmon->gappoh, */ +/* selmon->gappov, */ +/* selmon->gappih, */ +/* selmon->gappiv + arg->i */ +/* ); */ +/* } */ + +static void +getgaps(Monitor *m, int *oh, int *ov, int *ih, int *iv, unsigned int *nc) +{ + unsigned int n, oe, ie; + oe = ie = enablegaps; + Client *c; + + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++); + if (smartgaps && n == 1) { + oe = 0; // outer gaps disabled when only one client + } + + *oh = m->gappoh*oe; // outer horizontal gap + *ov = m->gappov*oe; // outer vertical gap + *ih = m->gappih*ie; // inner horizontal gap + *iv = m->gappiv*ie; // inner vertical gap + *nc = n; // number of clients +} + +void +getfacts(Monitor *m, int msize, int ssize, float *mf, float *sf, int *mr, int *sr) +{ + unsigned int n; + float mfacts, sfacts; + int mtotal = 0, stotal = 0; + Client *c; + + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++); + mfacts = MIN(n, m->nmaster); + sfacts = n - m->nmaster; + + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++) + if (n < m->nmaster) + mtotal += msize / mfacts; + else + stotal += ssize / sfacts; + + *mf = mfacts; // total factor of master area + *sf = sfacts; // total factor of stack area + *mr = msize - mtotal; // the remainder (rest) of pixels after an even master split + *sr = ssize - stotal; // the remainder (rest) of pixels after an even stack split +} + +/*** + * Layouts + */ + +/* + * Bottomstack layout + gaps + * https://dwm.suckless.org/patches/bottomstack/ + */ + +static void +bstack(Monitor *m) +{ + unsigned int i, n; + int mx = 0, my = 0, mh = 0, mw = 0; + int sx = 0, sy = 0, sh = 0, sw = 0; + float mfacts, sfacts; + int mrest, srest; + Client *c; + + int oh, ov, ih, iv; + getgaps(m, &oh, &ov, &ih, &iv, &n); + + if (n == 0) + return; + + sx = mx = m->wx + ov; + sy = my = m->wy + oh; + sh = mh = m->wh - 2*oh; + mw = m->ww - 2*ov - iv * (MIN(n, m->nmaster) - 1); + sw = m->ww - 2*ov - iv * (n - m->nmaster - 1); + + if (m->nmaster && n > m->nmaster) { + sh = (mh - ih) * (1 - m->mfact); + mh = (mh - ih) * m->mfact; + sx = mx; + sy = my + mh + ih; + } + + getfacts(m, mw, sw, &mfacts, &sfacts, &mrest, &srest); + + for (i = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) { + if (i < m->nmaster) { + resize(c, mx, my, (mw / mfacts) + (i < mrest ? 1 : 0) - (2*c->bw), mh - (2*c->bw), 0); + mx += WIDTH(c) + iv; + } else { + resize(c, sx, sy, (sw / sfacts) + ((i - m->nmaster) < srest ? 1 : 0) - (2*c->bw), sh - (2*c->bw), 0); + sx += WIDTH(c) + iv; + } + } +} + +/* + * Centred master layout + gaps + * https://dwm.suckless.org/patches/centeredmaster/ + */ + +void +centeredmaster(Monitor *m) +{ + unsigned int i, n; + int mx = 0, my = 0, mh = 0, mw = 0; + int lx = 0, ly = 0, lw = 0, lh = 0; + int rx = 0, ry = 0, rw = 0, rh = 0; + float mfacts = 0, lfacts = 0, rfacts = 0; + int mtotal = 0, ltotal = 0, rtotal = 0; + int mrest = 0, lrest = 0, rrest = 0; + Client *c; + + int oh, ov, ih, iv; + getgaps(m, &oh, &ov, &ih, &iv, &n); + + if (n == 0) + return; + + /* initialize areas */ + mx = m->wx + ov; + my = m->wy + oh; + mh = m->wh - 2*oh - ih * ((!m->nmaster ? n : MIN(n, m->nmaster)) - 1); + mw = m->ww - 2*ov; + lh = m->wh - 2*oh - ih * (((n - m->nmaster) / 2) - 1); + rh = m->wh - 2*oh - ih * (((n - m->nmaster) / 2) - ((n - m->nmaster) % 2 ? 0 : 1)); + + if (m->nmaster && n > m->nmaster) { + /* go mfact box in the center if more than nmaster clients */ + if (n - m->nmaster > 1) { + /* ||<-S->|<---M--->|<-S->|| */ + mw = (m->ww - 2*ov - 2*iv) * m->mfact; + lw = (m->ww - mw - 2*ov - 2*iv) / 2; + mx += lw + iv; + } else { + /* ||<---M--->|<-S->|| */ + mw = (mw - iv) * m->mfact; + lw = m->ww - mw - iv - 2*ov; + } + rw = lw; + lx = m->wx + ov; + ly = m->wy + oh; + rx = mx + mw + iv; + ry = m->wy + oh; + } + + /* calculate facts */ + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++) { + if (!m->nmaster || n < m->nmaster) + mfacts += 1; + else if ((n - m->nmaster) % 2) + lfacts += 1; // total factor of left hand stack area + else + rfacts += 1; // total factor of right hand stack area + } + + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++) + if (!m->nmaster || n < m->nmaster) + mtotal += mh / mfacts; + else if ((n - m->nmaster) % 2) + ltotal += lh / lfacts; + else + rtotal += rh / rfacts; + + mrest = mh - mtotal; + lrest = lh - ltotal; + rrest = rh - rtotal; + + for (i = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) { + if (!m->nmaster || i < m->nmaster) { + /* nmaster clients are stacked vertically, in the center of the screen */ + resize(c, mx, my, mw - (2*c->bw), (mh / mfacts) + (i < mrest ? 1 : 0) - (2*c->bw), 0); + my += HEIGHT(c) + ih; + } else { + /* stack clients are stacked vertically */ + if ((i - m->nmaster) % 2 ) { + resize(c, lx, ly, lw - (2*c->bw), (lh / lfacts) + ((i - 2*m->nmaster) < 2*lrest ? 1 : 0) - (2*c->bw), 0); + ly += HEIGHT(c) + ih; + } else { + resize(c, rx, ry, rw - (2*c->bw), (rh / rfacts) + ((i - 2*m->nmaster) < 2*rrest ? 1 : 0) - (2*c->bw), 0); + ry += HEIGHT(c) + ih; + } + } + } +} + +void +centeredfloatingmaster(Monitor *m) +{ + unsigned int i, n; + float mfacts, sfacts; + int mrest, srest; + int mx = 0, my = 0, mh = 0, mw = 0; + int sx = 0, sy = 0, sh = 0, sw = 0; + Client *c; + + float mivf = 1.0; // master inner vertical gap factor + int oh, ov, ih, iv; + getgaps(m, &oh, &ov, &ih, &iv, &n); + + if (n == 0) + return; + + sx = mx = m->wx + ov; + sy = my = m->wy + oh; + sh = mh = m->wh - 2*oh; + mw = m->ww - 2*ov - iv*(n - 1); + sw = m->ww - 2*ov - iv*(n - m->nmaster - 1); + + if (m->nmaster && n > m->nmaster) { + mivf = 0.8; + /* go mfact box in the center if more than nmaster clients */ + if (m->ww > m->wh) { + mw = m->ww * m->mfact - iv*mivf*(MIN(n, m->nmaster) - 1); + mh = m->wh * 0.9 - 2*oh; + } else { + mw = m->ww * 0.9 - iv*mivf*(MIN(n, m->nmaster) - 1); + mh = m->wh * m->mfact; + } + mx = m->wx + (m->ww - mw) / 2; + my = m->wy + (m->wh - mh) / 2; + + sx = m->wx + ov; + sy = m->wy + oh; + sh = m->wh - 2*oh; + } + + getfacts(m, mw, sw, &mfacts, &sfacts, &mrest, &srest); + + for (i = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) + if (i < m->nmaster) { + /* nmaster clients are stacked horizontally, in the center of the screen */ + resize(c, mx, my, (mw / mfacts) + (i < mrest ? 1 : 0) - (2*c->bw), mh - (2*c->bw), 0); + mx += WIDTH(c) + iv*mivf; + } else { + /* stack clients are stacked horizontally */ + resize(c, sx, sy, (sw / sfacts) + ((i - m->nmaster) < srest ? 1 : 0) - (2*c->bw), sh - (2*c->bw), 0); + sx += WIDTH(c) + iv; + } +} + +/* + * Deck layout + gaps + * https://dwm.suckless.org/patches/deck/ + */ + +static void +deck(Monitor *m) +{ + unsigned int i, n; + int mx = 0, my = 0, mh = 0, mw = 0; + int sx = 0, sy = 0, sh = 0, sw = 0; + float mfacts, sfacts; + int mrest, srest; + Client *c; + + int oh, ov, ih, iv; + getgaps(m, &oh, &ov, &ih, &iv, &n); + + if (n == 0) + return; + + sx = mx = m->wx + ov; + sy = my = m->wy + oh; + sh = mh = m->wh - 2*oh - ih * (MIN(n, m->nmaster) - 1); + sw = mw = m->ww - 2*ov; + + if (m->nmaster && n > m->nmaster) { + sw = (mw - iv) * (1 - m->mfact); + mw = (mw - iv) * m->mfact; + sx = mx + mw + iv; + sh = m->wh - 2*oh; + } + + getfacts(m, mh, sh, &mfacts, &sfacts, &mrest, &srest); + + if (n - m->nmaster > 0) /* override layout symbol */ + snprintf(m->ltsymbol, sizeof m->ltsymbol, "D %d", n - m->nmaster); + + for (i = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) + if (i < m->nmaster) { + resize(c, mx, my, mw - (2*c->bw), (mh / mfacts) + (i < mrest ? 1 : 0) - (2*c->bw), 0); + my += HEIGHT(c) + ih; + } else { + resize(c, sx, sy, sw - (2*c->bw), sh - (2*c->bw), 0); + } +} + +/* + * Fibonacci layout + gaps + * https://dwm.suckless.org/patches/fibonacci/ + */ + +static void +fibonacci(Monitor *m, int s) +{ + unsigned int i, n; + int nx, ny, nw, nh; + int oh, ov, ih, iv; + Client *c; + + getgaps(m, &oh, &ov, &ih, &iv, &n); + + if (n == 0) + return; + + nx = m->wx + ov; + ny = oh; + nw = m->ww - 2*ov; + nh = m->wh - 2*oh; + + for (i = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next)) { + if ((i % 2 && nh / 2 > 2*c->bw) + || (!(i % 2) && nw / 2 > 2*c->bw)) { + if (i < n - 1) { + if (i % 2) + nh = (nh - ih) / 2; + else + nw = (nw - iv) / 2; + + if ((i % 4) == 2 && !s) + nx += nw + iv; + else if ((i % 4) == 3 && !s) + ny += nh + ih; + } + if ((i % 4) == 0) { + if (s) + ny += nh + ih; + else + ny -= nh + ih; + } + else if ((i % 4) == 1) + nx += nw + iv; + else if ((i % 4) == 2) + ny += nh + ih; + else if ((i % 4) == 3) { + if (s) + nx += nw + iv; + else + nx -= nw + iv; + } + if (i == 0) { + if (n != 1) + nw = (m->ww - 2*ov - iv) * m->mfact; + ny = m->wy + oh; + } + else if (i == 1) + nw = m->ww - nw - iv - 2*ov; + i++; + } + + resize(c, nx, ny, nw - (2*c->bw), nh - (2*c->bw), False); + } +} + +static void +dwindle(Monitor *m) +{ + fibonacci(m, 1); +} + +static void +spiral(Monitor *m) +{ + fibonacci(m, 0); +} + +/* + * Default tile layout + gaps + */ + +static void +tile(Monitor *m) +{ + unsigned int i, n; + int mx = 0, my = 0, mh = 0, mw = 0; + int sx = 0, sy = 0, sh = 0, sw = 0; + float mfacts, sfacts; + int mrest, srest; + Client *c; + + + int oh, ov, ih, iv; + getgaps(m, &oh, &ov, &ih, &iv, &n); + + if (n == 0) + return; + + sx = mx = m->wx + ov; + sy = my = m->wy + oh; + mh = m->wh - 2*oh - ih * (MIN(n, m->nmaster) - 1); + sh = m->wh - 2*oh - ih * (n - m->nmaster - 1); + sw = mw = m->ww - 2*ov; + + if (m->nmaster && n > m->nmaster) { + sw = (mw - iv) * (1 - m->mfact); + mw = (mw - iv) * m->mfact; + sx = mx + mw + iv; + } + + getfacts(m, mh, sh, &mfacts, &sfacts, &mrest, &srest); + + for (i = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) + if (i < m->nmaster) { + resize(c, mx, my, mw - (2*c->bw), (mh / mfacts) + (i < mrest ? 1 : 0) - (2*c->bw), 0); + my += HEIGHT(c) + ih; + } else { + resize(c, sx, sy, sw - (2*c->bw), (sh / sfacts) + ((i - m->nmaster) < srest ? 1 : 0) - (2*c->bw), 0); + sy += HEIGHT(c) + ih; + } +} diff --git a/mut/dwmblocks/.gitignore b/mut/dwmblocks/.gitignore new file mode 100644 index 0000000..c4bb970 --- /dev/null +++ b/mut/dwmblocks/.gitignore @@ -0,0 +1,53 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex +dwmblocks + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/mut/dwmblocks/FUNDING.yml b/mut/dwmblocks/FUNDING.yml new file mode 100644 index 0000000..f8e6076 --- /dev/null +++ b/mut/dwmblocks/FUNDING.yml @@ -0,0 +1,3 @@ +github: lukesmithxyz +custom: ["https://lukesmith.xyz/donate", "https://paypal.me/lukemsmith", "https://lukesmith.xyz/crypto"] +patreon: lukesmith diff --git a/mut/dwmblocks/LICENSE b/mut/dwmblocks/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/mut/dwmblocks/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/mut/dwmblocks/Makefile b/mut/dwmblocks/Makefile new file mode 100644 index 0000000..5cfbb5a --- /dev/null +++ b/mut/dwmblocks/Makefile @@ -0,0 +1,19 @@ +.POSIX: + +PREFIX = /usr/local +CC = gcc + +dwmblocks: dwmblocks.o + $(CC) dwmblocks.o -lX11 -o dwmblocks +dwmblocks.o: dwmblocks.c config.h + $(CC) -c dwmblocks.c +clean: + rm -f *.o *.gch dwmblocks +install: dwmblocks + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f dwmblocks $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/dwmblocks +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/dwmblocks + +.PHONY: clean install uninstall diff --git a/mut/dwmblocks/README.md b/mut/dwmblocks/README.md new file mode 100644 index 0000000..7d21e30 --- /dev/null +++ b/mut/dwmblocks/README.md @@ -0,0 +1,44 @@ +# dwmblocks + +Modular status bar for dwm written in c. + +# Modifying blocks + +The statusbar is made from text output from commandline programs. Blocks are +added and removed by editing the config.h file. + +# Luke's build + +I have dwmblocks read my preexisting scripts +[here in my dotfiles repo](https://github.com/LukeSmithxyz/voidrice/tree/master/.local/bin/statusbar). +So if you want my build out of the box, download those and put them in your +`$PATH`. I do this to avoid redundancy in LARBS, both i3 and dwm use the same +statusbar scripts. + +# Signaling changes + +Most statusbars constantly rerun every script every several seconds to update. +This is an option here, but a superior choice is giving your module a signal +that you can signal to it to update on a relevant event, rather than having it +rerun idly. + +For example, the audio module has the update signal 10 by default. Thus, +running `pkill -RTMIN+10 dwmblocks` will update it. + +You can also run `kill -44 $(pidof dwmblocks)` which will have the same effect, +but is faster. Just add 34 to your typical signal number. + +My volume module *never* updates on its own, instead I have this command run +along side my volume shortcuts in dwm to only update it when relevant. + +Note that all modules must have different signal numbers. + +# Clickable modules + +Like i3blocks, this build allows you to build in additional actions into your +scripts in response to click events. See the above linked scripts for examples +of this using the `$BLOCK_BUTTON` variable. + +For this feature to work, you need the appropriate patch in dwm as well. See +[here](https://dwm.suckless.org/patches/statuscmd/). +Credit for those patches goes to Daniel Bylinka (daniel.bylinka@gmail.com). diff --git a/mut/dwmblocks/config.h b/mut/dwmblocks/config.h new file mode 100644 index 0000000..a18259d --- /dev/null +++ b/mut/dwmblocks/config.h @@ -0,0 +1,36 @@ +//Modify this file to change what commands output to your statusbar, and recompile using the make command. +static const Block blocks[] = { + /*Icon*/ /*Command*/ /*Update Interval*/ /*Update Signal*/ + /* {"⌨", "sb-kbselect", 0, 30}, */ + /* {"", "cat /tmp/recordingicon 2>/dev/null", 0, 9}, */ + /* {"", "sb-tasks", 10, 26}, */ + /* {"", "sb-pomodoro", 60, 26}, */ + /* {"", "sb-music", 0, 11}, */ + /* {"", "sb-pacpackages", 0, 8}, */ + /* {"", "sb-news", 0, 6}, */ + /* {"", "sb-price xmr Monero 🔒 24", 9000, 24}, */ + /* {"", "sb-price eth Ethereum 🍸 23", 9000, 23}, */ + /* {"", "sb-price btc Bitcoin 💰 21", 9000, 21}, */ + /* {"", "sb-torrent", 20, 7}, */ + /* {"", "sb-memory", 10, 14}, */ + /* {"", "sb-cpu", 10, 18}, */ + /* {"", "sb-moonphase", 18000, 17}, */ + /* {"", "sb-doppler", 0, 13}, */ + /* {"", "sb-forecast", 18000, 5}, */ + /* {"", "sb-mailbox", 180, 12}, */ + {"", "sb-nettraf", 1, 16}, + {"", "sb-volume", 0, 10}, + /* {"", "sb-battery", 5, 3}, */ + {"", "sb-clock", 60, 1}, + {"", "sb-internet", 5, 4}, + /* {"", "sb-iplocate", 0, 27}, */ + /* {"", "sb-help-icon", 0, 15}, */ +}; + +//Sets delimiter between status commands. NULL character ('\0') means no delimiter. +static char *delim = " "; + +// Have dwmblocks automatically recompile and run when you edit this file in +// vim with the following line in your vimrc/init.vim: + +// autocmd BufWritePost ~/.local/src/dwmblocks/config.h !cd ~/.local/src/dwmblocks/; sudo make install && { killall -q dwmblocks;setsid dwmblocks & } diff --git a/mut/dwmblocks/dwmblocks.c b/mut/dwmblocks/dwmblocks.c new file mode 100644 index 0000000..0969ed7 --- /dev/null +++ b/mut/dwmblocks/dwmblocks.c @@ -0,0 +1,294 @@ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <time.h> +#include <signal.h> +#include <errno.h> +#include <X11/Xlib.h> +#define LENGTH(X) (sizeof(X) / sizeof (X[0])) +#define CMDLENGTH 50 + +typedef struct { + char* icon; + char* command; + unsigned int interval; + unsigned int signal; +} Block; +void sighandler(int num); +void buttonhandler(int sig, siginfo_t *si, void *ucontext); +void replace(char *str, char old, char new); +void remove_all(char *str, char to_remove); +void getcmds(int time); +#ifndef __OpenBSD__ +void getsigcmds(int signal); +void setupsignals(); +void sighandler(int signum); +#endif +int getstatus(char *str, char *last); +void setroot(); +void statusloop(); +void termhandler(int signum); + + +#include "config.h" + +static Display *dpy; +static int screen; +static Window root; +static char statusbar[LENGTH(blocks)][CMDLENGTH] = {0}; +static char statusstr[2][256]; +static int statusContinue = 1; +static void (*writestatus) () = setroot; + +void replace(char *str, char old, char new) +{ + for(char * c = str; *c; c++) + if(*c == old) + *c = new; +} + +// the previous function looked nice but unfortunately it didnt work if to_remove was in any position other than the last character +// theres probably still a better way of doing this +void remove_all(char *str, char to_remove) { + char *read = str; + char *write = str; + while (*read) { + if (*read != to_remove) { + *write++ = *read; + } + ++read; + } + *write = '\0'; +} + +int gcd(int a, int b) +{ + int temp; + while (b > 0){ + temp = a % b; + + a = b; + b = temp; + } + return a; +} + + +//opens process *cmd and stores output in *output +void getcmd(const Block *block, char *output) +{ + if (block->signal) + { + output[0] = block->signal; + output++; + } + char *cmd = block->command; + FILE *cmdf = popen(cmd,"r"); + if (!cmdf){ + //printf("failed to run: %s, %d\n", block->command, errno); + return; + } + char tmpstr[CMDLENGTH] = ""; + // TODO decide whether its better to use the last value till next time or just keep trying while the error was the interrupt + // this keeps trying to read if it got nothing and the error was an interrupt + // could also just read to a separate buffer and not move the data over if interrupted + // this way will take longer trying to complete 1 thing but will get it done + // the other way will move on to keep going with everything and the part that failed to read will be wrong till its updated again + // either way you have to save the data to a temp buffer because when it fails it writes nothing and then then it gets displayed before this finishes + char * s; + int e; + do { + errno = 0; + s = fgets(tmpstr, CMDLENGTH-(strlen(delim)+1), cmdf); + e = errno; + } while (!s && e == EINTR); + pclose(cmdf); + int i = strlen(block->icon); + strcpy(output, block->icon); + strcpy(output+i, tmpstr); + remove_all(output, '\n'); + i = strlen(output); + if ((i > 0 && block != &blocks[LENGTH(blocks) - 1])){ + strcat(output, delim); + } + i+=strlen(delim); + output[i++] = '\0'; +} + +void getcmds(int time) +{ + const Block* current; + for(int i = 0; i < LENGTH(blocks); i++) + { + current = blocks + i; + if ((current->interval != 0 && time % current->interval == 0) || time == -1){ + getcmd(current,statusbar[i]); + } + } +} + +#ifndef __OpenBSD__ +void getsigcmds(int signal) +{ + const Block *current; + for (int i = 0; i < LENGTH(blocks); i++) + { + current = blocks + i; + if (current->signal == signal){ + getcmd(current,statusbar[i]); + } + } +} + +void setupsignals() +{ + struct sigaction sa; + + for(int i = SIGRTMIN; i <= SIGRTMAX; i++) + signal(i, SIG_IGN); + + for(int i = 0; i < LENGTH(blocks); i++) + { + if (blocks[i].signal > 0) + { + signal(SIGRTMIN+blocks[i].signal, sighandler); + sigaddset(&sa.sa_mask, SIGRTMIN+blocks[i].signal); + } + } + sa.sa_sigaction = buttonhandler; + sa.sa_flags = SA_SIGINFO; + sigaction(SIGUSR1, &sa, NULL); + struct sigaction sigchld_action = { + .sa_handler = SIG_DFL, + .sa_flags = SA_NOCLDWAIT + }; + sigaction(SIGCHLD, &sigchld_action, NULL); + +} +#endif + +int getstatus(char *str, char *last) +{ + strcpy(last, str); + str[0] = '\0'; + for(int i = 0; i < LENGTH(blocks); i++) { + strcat(str, statusbar[i]); + if (i == LENGTH(blocks) - 1) + strcat(str, " "); + } + str[strlen(str)-1] = '\0'; + return strcmp(str, last);//0 if they are the same +} + +void setroot() +{ + if (!getstatus(statusstr[0], statusstr[1]))//Only set root if text has changed. + return; + Display *d = XOpenDisplay(NULL); + if (d) { + dpy = d; + } + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + XStoreName(dpy, root, statusstr[0]); + XCloseDisplay(dpy); +} + +void pstdout() +{ + if (!getstatus(statusstr[0], statusstr[1]))//Only write out if text has changed. + return; + printf("%s\n",statusstr[0]); + fflush(stdout); +} + + +void statusloop() +{ +#ifndef __OpenBSD__ + setupsignals(); +#endif + // first figure out the default wait interval by finding the + // greatest common denominator of the intervals + unsigned int interval = -1; + for(int i = 0; i < LENGTH(blocks); i++){ + if(blocks[i].interval){ + interval = gcd(blocks[i].interval, interval); + } + } + unsigned int i = 0; + int interrupted = 0; + const struct timespec sleeptime = {interval, 0}; + struct timespec tosleep = sleeptime; + getcmds(-1); + while(statusContinue) + { + // sleep for tosleep (should be a sleeptime of interval seconds) and put what was left if interrupted back into tosleep + interrupted = nanosleep(&tosleep, &tosleep); + // if interrupted then just go sleep again for the remaining time + if(interrupted == -1){ + continue; + } + // if not interrupted then do the calling and writing + getcmds(i); + writestatus(); + // then increment since its actually been a second (plus the time it took the commands to run) + i += interval; + // set the time to sleep back to the sleeptime of 1s + tosleep = sleeptime; + } +} + +#ifndef __OpenBSD__ +void sighandler(int signum) +{ + getsigcmds(signum-SIGRTMIN); + writestatus(); +} + +void buttonhandler(int sig, siginfo_t *si, void *ucontext) +{ + char button[2] = {'0' + si->si_value.sival_int & 0xff, '\0'}; + pid_t process_id = getpid(); + sig = si->si_value.sival_int >> 8; + if (fork() == 0) + { + const Block *current; + for (int i = 0; i < LENGTH(blocks); i++) + { + current = blocks + i; + if (current->signal == sig) + break; + } + char shcmd[1024]; + sprintf(shcmd,"%s && kill -%d %d",current->command, current->signal+34,process_id); + char *command[] = { "/bin/sh", "-c", shcmd, NULL }; + setenv("BLOCK_BUTTON", button, 1); + setsid(); + execvp(command[0], command); + exit(EXIT_SUCCESS); + } +} + +#endif + +void termhandler(int signum) +{ + statusContinue = 0; + exit(0); +} + +int main(int argc, char** argv) +{ + for(int i = 0; i < argc; i++) + { + if (!strcmp("-d",argv[i])) + delim = argv[++i]; + else if(!strcmp("-p",argv[i])) + writestatus = pstdout; + } + signal(SIGTERM, termhandler); + signal(SIGINT, termhandler); + statusloop(); +} diff --git a/mut/dwmblocks/patches/dwmblocks-statuscmd-fork.diff b/mut/dwmblocks/patches/dwmblocks-statuscmd-fork.diff new file mode 100644 index 0000000..1ae7d7a --- /dev/null +++ b/mut/dwmblocks/patches/dwmblocks-statuscmd-fork.diff @@ -0,0 +1,77 @@ +diff --git a/dwmblocks.c b/dwmblocks.c +index 7d7a564..e2c5dd0 100644 +--- a/dwmblocks.c ++++ b/dwmblocks.c +@@ -34,8 +34,6 @@ static int screen; + static Window root; + static char statusbar[LENGTH(blocks)][CMDLENGTH] = {0}; + static char statusstr[2][256]; +-static char exportstring[CMDLENGTH + 22] = "export BLOCK_BUTTON=-;"; +-static int button = 0; + static int statusContinue = 1; + static void (*writestatus) () = setroot; + +@@ -55,21 +53,8 @@ void getcmd(const Block *block, char *output) + output[0] = block->signal; + output++; + } +- char* cmd; +- FILE *cmdf; +- if (button) +- { +- cmd = strcat(exportstring, block->command); +- cmd[20] = '0' + button; +- button = 0; +- cmdf = popen(cmd,"r"); +- cmd[22] = '\0'; +- } +- else +- { +- cmd = block->command; +- cmdf = popen(cmd,"r"); +- } ++ char *cmd = block->command; ++ FILE *cmdf = popen(cmd,"r"); + if (!cmdf) + return; + fgets(output, CMDLENGTH, cmdf); +@@ -117,6 +102,7 @@ void setupsignals() + sa.sa_sigaction = buttonhandler; + sa.sa_flags = SA_SIGINFO; + sigaction(SIGUSR1, &sa, NULL); ++ signal(SIGCHLD, SIG_IGN); + + } + #endif +@@ -179,9 +165,29 @@ void sighandler(int signum) + + void buttonhandler(int sig, siginfo_t *si, void *ucontext) + { +- button = si->si_value.sival_int & 0xff; +- getsigcmds(si->si_value.sival_int >> 8); ++ int button = si->si_value.sival_int & 0xff; ++ sig = si->si_value.sival_int >> 8; ++ getsigcmds(sig); + writestatus(); ++ if (fork() == 0) ++ { ++ static char exportstring[CMDLENGTH + 22] = "export BLOCK_BUTTON=-;"; ++ const Block *current; ++ int i; ++ for (i = 0; i < LENGTH(blocks); i++) ++ { ++ current = blocks + i; ++ if (current->signal == sig) ++ break; ++ } ++ char *cmd = strcat(exportstring, blocks[i].command); ++ cmd[20] = '0' + button; ++ char *command[] = { "/bin/sh", "-c", cmd, NULL }; ++ setsid(); ++ execvp(command[0], command); ++ exit(EXIT_SUCCESS); ++ cmd[22] = '\0'; ++ } + } + + #endif diff --git a/mut/dwmblocks/patches/dwmblocks-statuscmd-signal.diff b/mut/dwmblocks/patches/dwmblocks-statuscmd-signal.diff new file mode 100644 index 0000000..c2092e7 --- /dev/null +++ b/mut/dwmblocks/patches/dwmblocks-statuscmd-signal.diff @@ -0,0 +1,93 @@ +diff --git a/dwmblocks.c b/dwmblocks.c +index 88bdfb0..7bd14df 100644 +--- a/dwmblocks.c ++++ b/dwmblocks.c +@@ -14,6 +14,7 @@ typedef struct { + unsigned int signal; + } Block; + void sighandler(int num); ++void buttonhandler(int sig, siginfo_t *si, void *ucontext); + void replace(char *str, char old, char new); + void getcmds(int time); + #ifndef __OpenBSD__ +@@ -34,6 +35,8 @@ static int screen; + static Window root; + static char statusbar[LENGTH(blocks)][CMDLENGTH] = {0}; + static char statusstr[2][256]; ++static char exportstring[CMDLENGTH + 16] = "export BUTTON=-;"; ++static int button = 0; + static int statusContinue = 1; + static void (*writestatus) () = setroot; + +@@ -48,16 +51,34 @@ void replace(char *str, char old, char new) + //opens process *cmd and stores output in *output + void getcmd(const Block *block, char *output) + { ++ if (block->signal) ++ { ++ output[0] = block->signal; ++ output++; ++ } + strcpy(output, block->icon); +- char *cmd = block->command; +- FILE *cmdf = popen(cmd,"r"); ++ char* cmd; ++ FILE *cmdf; ++ if (button) ++ { ++ cmd = strcat(exportstring, block->command); ++ cmd[14] = '0' + button; ++ button = 0; ++ cmdf = popen(cmd,"r"); ++ cmd[16] = '\0'; ++ } ++ else ++ { ++ cmd = block->command; ++ cmdf = popen(cmd,"r"); ++ } + if (!cmdf) + return; + char c; + int i = strlen(block->icon); + fgets(output+i, CMDLENGTH-i, cmdf); + i = strlen(output); +- if (delim != '\0' && --i) ++ if (delim != '\0' && i) + output[i++] = delim; + output[i++] = '\0'; + pclose(cmdf); +@@ -88,11 +106,18 @@ void getsigcmds(int signal) + + void setupsignals() + { ++ struct sigaction sa; + for(int i = 0; i < LENGTH(blocks); i++) + { + if (blocks[i].signal > 0) ++ { + signal(SIGRTMIN+blocks[i].signal, sighandler); ++ sigaddset(&sa.sa_mask, SIGRTMIN+blocks[i].signal); ++ } + } ++ sa.sa_sigaction = buttonhandler; ++ sa.sa_flags = SA_SIGINFO; ++ sigaction(SIGUSR1, &sa, NULL); + + } + #endif +@@ -152,6 +177,14 @@ void sighandler(int signum) + getsigcmds(signum-SIGRTMIN); + writestatus(); + } ++ ++void buttonhandler(int sig, siginfo_t *si, void *ucontext) ++{ ++ button = si->si_value.sival_int & 0xff; ++ getsigcmds(si->si_value.sival_int >> 8); ++ writestatus(); ++} ++ + #endif + + void termhandler(int signum) diff --git a/mut/emacs/init.el b/mut/emacs/init.el new file mode 100644 index 0000000..52de580 --- /dev/null +++ b/mut/emacs/init.el @@ -0,0 +1,181 @@ +;; do autoload stuff here +(package-initialize) +(require 'package) +(add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/") t) + +(defun seq-keep (function sequence) + "Apply FUNCTION to SEQUENCE and return the list of all the non-nil results." + (delq nil (seq-map function sequence))) + +(defvar rc/package-contents-refreshed nil) + +(defun rc/package-refresh-contents-once () + (when (not rc/package-contents-refreshed) + (setq rc/package-contents-refreshed t) + (package-refresh-contents))) + +(defun rc/require-one-package (package) + (when (not (package-installed-p package)) + (rc/package-refresh-contents-once) + (package-install package))) + +(defun rc/require (&rest packages) + (dolist (package packages) + (rc/require-one-package package))) + +(defun rc/require-theme (theme) + (let ((theme-package (->> theme + (symbol-name) + (funcall (-flip #'concat) "-theme") + (intern)))) + (rc/require theme-package) + (load-theme theme t))) + + +(rc/require 'dash) +(require 'dash) +(rc/require 'dash-functional) +(require 'dash-functional) + +(defun rc/get-default-font () + (cond + ((eq system-type 'windows-nt) "Consolas-13") + (t "mono-13"))) +(add-to-list 'default-frame-alist `(font . ,(rc/get-default-font))) +(rc/require 'ansi-color) + +(rc/require 'ido 'ido-completing-read+ 'smex 'corfu) +(ido-mode t) +(ido-everywhere t) +(ido-ubiquitous-mode t) +(global-corfu-mode) + +(global-set-key (kbd "M-x") 'smex) +(global-set-key (kbd "M-X") 'smex-major-mode-commands) +;; This is your old M-x. p +(global-set-key (kbd "C-c C-c M-x") 'execute-extended-command) + +(tool-bar-mode 0) +(menu-bar-mode 0) +(scroll-bar-mode 0) +(column-number-mode 1) +(show-paren-mode 1) + +(setq-default inhibit-splash-screen t + make-backup-files nil + tab-width 4 + indent-tabs-mode nil + compilation-scroll-output t + visible-bell (equal system-type 'windows-nt)) + +(setq-default c-basic-offset 4 + c-default-style '((java-mode . "java") + (awk-mode . "awk") + (other . "bsd"))) +(setq split-width-threshold 9999) + +(defun rc/duplicate-line () + "Duplicate current line" + (interactive) + (let ((column (- (point) (point-at-bol))) + (line (let ((s (thing-at-point 'line t))) + (if s (string-remove-suffix "\n" s) "")))) + (move-end-of-line 1) + (newline) + (insert line) + (move-beginning-of-line 1) + (forward-char column))) + +(global-set-key (kbd "M-J") 'text-scale-decrease) +(global-set-key (kbd "M-K") 'text-scale-increase) + +(global-set-key (kbd "M-c") 'rc/duplicate-line) +(global-set-key (kbd "C-c p") 'find-file-at-point) +(global-display-line-numbers-mode) +(setq next-line-add-newlines t) +(setq display-line-numbers-type 'relative) + +(rc/require 'direnv 'editorconfig 'multiple-cursors) +(editorconfig-mode 1) +(electric-pair-mode) +(global-set-key (kbd "C-S-c C-S-c") 'mc/edit-lines) +(global-set-key (kbd "C->") 'mc/mark-next-like-this) +(global-set-key (kbd "C-<") 'mc/mark-previous-like-this) +(global-set-key (kbd "C-c C-<") 'mc/mark-all-like-this) +(global-set-key (kbd "C-.") 'mc/mark-all-in-region) + +(rc/require 'cl-lib 'magit) +(setq magit-auto-revert-mode nil) +(global-set-key (kbd "C-c m s") 'magit-status) +(global-set-key (kbd "C-c m l") 'magit-log) + +(require 'dired-x) +(setq dired-omit-files + (concat dired-omit-files "\\|^\\..+$")) +(setq-default dired-dwim-target t) +(setq dired-listing-switches "-alh") + +;; stolen from: https://emacs.stackexchange.com/questions/24698/ansi-escape-sequences-in-compilation-mode +(rc/require 'ansi-color) +(defun endless/colorize-compilation () + "Colorize from `compilation-filter-start' to `point'." + (let ((inhibit-read-only t)) + (ansi-color-apply-on-region + compilation-filter-start (point)))) +(add-hook 'compilation-filter-hook + #'endless/colorize-compilation) + +(setq TeX-auto-save t) +(setq TeX-parse-self t) +(setq-default TeX-master nil) + +(setq completion-auto-select 'second-tab) +(setq completions-format 'one-column) +(setq completions-max-height 20) +(define-key completion-in-region-mode-map (kbd "M-p") #'minibuffer-previous-completion) +(define-key completion-in-region-mode-map (kbd "M-n") #'minibuffer-next-completion) +;; (rc/require 'consult 'vertico 'orderless) +;; (setq completion-in-region-function #'completion--) + + +(rc/require + 'nix-mode + 'go-mode + 'auctex + 'yaml-pro + 'rust-mode) + + +(require 'lsp-mode) +(add-hook 'rust-mode-hook #'lsp-deferred) +(add-hook 'go-mode-hook #'lsp-deferred) +(defun lsp-go-install-save-hooks () + (add-hook 'before-save-hook #'lsp-format-buffer t t) + (add-hook 'before-save-hook #'lsp-organize-imports t t)) +(add-hook 'go-mode-hook #'lsp-go-install-save-hooks) +(lsp-register-custom-settings + '(("gopls.hints.assignVariableTypes" t t) + ("gopls.hints.compositeLiteralFields" t t) + ("gopls.hints.compositeLiteralTypes" t t) + ("gopls.hints.constantValues" t t) + ("gopls.hints.functionTypeParameters" t t) + ("gopls.hints.parameterNames" t t) + ("gopls.hints.rangeVariableTypes" t t))) + +(rc/require-theme 'gruber-darker) +(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. + '(custom-enabled-themes '(gruber-darker)) + '(custom-safe-themes + '("ba4ab079778624e2eadbdc5d9345e6ada531dc3febeb24d257e6d31d5ed02577" "a9dc7790550dcdb88a23d9f81cc0333490529a20e160a8599a6ceaf1104192b5" "5f128efd37c6a87cd4ad8e8b7f2afaba425425524a68133ac0efd87291d05874" "5b9a45080feaedc7820894ebbfe4f8251e13b66654ac4394cb416fef9fdca789" "9013233028d9798f901e5e8efb31841c24c12444d3b6e92580080505d56fd392" "6adeb971e4d5fe32bee0d5b1302bc0dfd70d4b42bad61e1c346599a6dc9569b5" "8d3ef5ff6273f2a552152c7febc40eabca26bae05bd12bc85062e2dc224cde9a" "75b2a02e1e0313742f548d43003fcdc45106553af7283fb5fad74359e07fe0e2" "b9761a2e568bee658e0ff723dd620d844172943eb5ec4053e2b199c59e0bcc22" "9d29a302302cce971d988eb51bd17c1d2be6cd68305710446f658958c0640f68" "f053f92735d6d238461da8512b9c071a5ce3b9d972501f7a5e6682a90bf29725" "dc8285f7f4d86c0aebf1ea4b448842a6868553eded6f71d1de52f3dcbc960039" "38c0c668d8ac3841cb9608522ca116067177c92feeabc6f002a27249976d7434" "162201cf5b5899938cfaec99c8cb35a2f1bf0775fc9ccbf5e63130a1ea217213" "ff24d14f5f7d355f47d53fd016565ed128bf3af30eb7ce8cae307ee4fe7f3fd0" "da75eceab6bea9298e04ce5b4b07349f8c02da305734f7c0c8c6af7b5eaa9738" "e3daa8f18440301f3e54f2093fe15f4fe951986a8628e98dcd781efbec7a46f2" "631c52620e2953e744f2b56d102eae503017047fb43d65ce028e88ef5846ea3b" "88f7ee5594021c60a4a6a1c275614103de8c1435d6d08cc58882f920e0cec65e" "dfb1c8b5bfa040b042b4ef660d0aab48ef2e89ee719a1f24a4629a0c5ed769e8" "e13beeb34b932f309fb2c360a04a460821ca99fe58f69e65557d6c1b10ba18c7" default)) + '(package-selected-packages + '(doom-themes corfu yaml-pro smex rust-mode nix-mode multiple-cursors magit lsp-ui ido-completing-read+ gruber-darker-theme go-mode editorconfig direnv dash-functional auctex))) +(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. + diff --git a/mut/ghostty/config b/mut/ghostty/config new file mode 100644 index 0000000..ea81f22 --- /dev/null +++ b/mut/ghostty/config @@ -0,0 +1,18 @@ +keybind = alt+k=scroll_page_fractional:-0.4 +keybind = alt+j=scroll_page_fractional:+0.4 +# keybind = alt+b=scroll_page_up +# keybind = alt+f=scroll_page_down +keybind = alt+c=copy_to_clipboard +keybind = alt+v=paste_from_clipboard +keybind = alt+shift+k=increase_font_size:1 +keybind = alt+shift+j=decrease_font_size:1 + +keybind = ctrl+zero=unbind +keybind = ctrl+enter=unbind +theme=GruvboxLuke +window-decoration=false +confirm-close-surface=false +macos-option-as-alt=true + +clipboard-read = allow +clipboard-write = allow diff --git a/mut/ghostty/themes/GruvboxLuke b/mut/ghostty/themes/GruvboxLuke new file mode 100644 index 0000000..3f09286 --- /dev/null +++ b/mut/ghostty/themes/GruvboxLuke @@ -0,0 +1,23 @@ +palette = 0=#262626 +palette = 1=#d75f5f +palette = 2=#afaf00 +palette = 3=#ffaf00 +palette = 4=#83adad +palette = 5=#d485ad +palette = 6=#85ad85 +palette = 7=#dab997 +palette = 8=#8a8a8a +palette = 9=#d75f5f +palette = 10=#afaf00 +palette = 11=#ffaf00 +palette = 12=#83adad +palette = 13=#d485ad +palette = 14=#85ad85 +palette = 15=#ebdbb2 + +background = #262626 +foreground = #dab997 +cursor-color = #dab997 + +selection-background = #665c54 +selection-foreground = #ebdbb2 diff --git a/mut/git/config b/mut/git/config new file mode 100644 index 0000000..9d9b277 --- /dev/null +++ b/mut/git/config @@ -0,0 +1,25 @@ +[commit] + gpgsign = false + +[gpg] + format = "ssh" + +[merge] + tool = "fugitive" + +[mergetool "fugitive"] + cmd = "vim -f -c \"Gdiff\" \"$MERGED\"" + +[user] + email = "ivi@vinkies.net" + name = "Mike Vink" + signingKey = "/Users/ivi/.ssh/id_ed25519_sk.pub" + +[worktree] + guessRemote = true + +[includeIf "hasconfig:remote.*.url:git@github.com:**/**"] + path = "/nix-config/mut/git/github" + +[includeIf "hasconfig:remote.*.url:https://github.com/**/**"] + path = "/nix-config/mut/git/github" diff --git a/mut/git/github b/mut/git/github new file mode 100644 index 0000000..421194d --- /dev/null +++ b/mut/git/github @@ -0,0 +1,2 @@ +[user] + email = "59492084+ivi-vink@users.noreply.github.com" diff --git a/mut/git/ignore b/mut/git/ignore new file mode 100644 index 0000000..1ea4fc3 --- /dev/null +++ b/mut/git/ignore @@ -0,0 +1,6 @@ +/.direnv/ +/.envrc +/.env +.vimsession.vim +tfplan +plan diff --git a/mut/k9s/aliases.yaml b/mut/k9s/aliases.yaml new file mode 100644 index 0000000..ee4d9ec --- /dev/null +++ b/mut/k9s/aliases.yaml @@ -0,0 +1,9 @@ +aliases: + dp: deployments + sec: v1/secrets + jo: jobs + cr: clusterroles + crb: clusterrolebindings + ro: roles + rb: rolebindings + np: networkpolicies diff --git a/mut/k9s/config.yaml b/mut/k9s/config.yaml new file mode 100644 index 0000000..de5ae68 --- /dev/null +++ b/mut/k9s/config.yaml @@ -0,0 +1,42 @@ +k9s: + portForwardAddress: 0.0.0.0 + liveViewAutoRefresh: false + screenDumpDir: /home/ivi/.local/state/k9s/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: true + logoless: true + crumbsless: false + reactive: true + noIcons: false + skin: gruvbox + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 diff --git a/mut/k9s/skins/gruvbox.yaml b/mut/k9s/skins/gruvbox.yaml new file mode 100644 index 0000000..d3bd8e4 --- /dev/null +++ b/mut/k9s/skins/gruvbox.yaml @@ -0,0 +1,105 @@ +# ----------------------------------------------------------------------------- +# K9s Gruvbox Dark Skin +# Author: [@indiebrain](https://github.com/indiebrain) +# ----------------------------------------------------------------------------- + +# Styles... +foreground: &foreground "#ebdbb2" +background: &background "#272727" +current_line: ¤t_line "#ebdbb2" +selection: &selection "#3c3735" +comment: &comment "#bdad93" +cyan: &cyan "#689d69" +green: &green "#989719" +orange: &orange "#d79920" +magenta: &magenta "#b16185" +blue: &blue "#448488" +red: &red "#cc231c" + +k9s: + body: + fgColor: *foreground + bgColor: *background + logoColor: *blue + prompt: + fgColor: *foreground + bgColor: *background + suggestColor: *orange + info: + fgColor: *magenta + sectionColor: *foreground + help: + fgColor: *foreground + bgColor: *background + keyColor: *magenta + numKeyColor: *blue + sectionColor: *green + dialog: + fgColor: *foreground + bgColor: *background + buttonFgColor: *foreground + buttonBgColor: *magenta + buttonFocusFgColor: white + buttonFocusBgColor: *cyan + labelFgColor: *orange + fieldFgColor: *foreground + frame: + border: + fgColor: *selection + focusColor: *current_line + menu: + fgColor: *foreground + keyColor: *magenta + numKeyColor: *magenta + crumbs: + fgColor: *foreground + bgColor: *comment + activeColor: *blue + status: + newColor: *cyan + modifyColor: *blue + addColor: *green + errorColor: *red + highlightColor: *orange + killColor: *comment + completedColor: *comment + title: + fgColor: *foreground + bgColor: *background + highlightColor: *orange + counterColor: *blue + filterColor: *magenta + views: + charts: + bgColor: background + defaultDialColors: + - *blue + - *red + defaultChartColors: + - *blue + - *red + table: + fgColor: *foreground + bgColor: *background + cursorFgColor: "#fff" + cursorBgColor: *current_line + header: + fgColor: *foreground + bgColor: *background + sorterColor: *selection + xray: + fgColor: *foreground + bgColor: *background + cursorColor: *current_line + graphicColor: *blue + showIcons: false + yaml: + keyColor: *magenta + colonColor: *blue + valueColor: *foreground + logs: + fgColor: *foreground + bgColor: *background + indicator: + fgColor: *foreground + bgColor: *background diff --git a/mut/lf/cleaner b/mut/lf/cleaner new file mode 100755 index 0000000..9513d38 --- /dev/null +++ b/mut/lf/cleaner @@ -0,0 +1,6 @@ +#!/bin/sh +if [ -n "$FIFO_UEBERZUG" ]; then + printf '{"action": "remove", "identifier": "PREVIEW"}\n' > "$FIFO_UEBERZUG" +else + exec kitten icat --clear --stdin no --transfer-mode file < /dev/null > /dev/tty +fi diff --git a/mut/lf/lfrc b/mut/lf/lfrc new file mode 100644 index 0000000..7047e2d --- /dev/null +++ b/mut/lf/lfrc @@ -0,0 +1,175 @@ +# Luke's lf settings + + +# Note on Image Previews +# For those wanting image previews, like this system, there are four steps to +# set it up. These are done automatically for LARBS users, but I will state +# them here for others doing it manually. +# +# 1. ueberzug must be installed. +# 2. The scope file (~/.config/lf/scope for me), must have a command similar to +# mine to generate ueberzug images. +# 3. A `set cleaner` line as below is a cleaner script. +# 4. lf should be started through a wrapper script (~/.local/bin/lfub for me) +# that creates the environment for ueberzug. This command can be be aliased +# in your shellrc (`alias lf="lfub") or if set to a binding, should be +# called directly instead of normal lf. + +# Basic vars +set shellopts '-eu' +set ifs "\n" +set scrolloff 10 +set icons +set period 1 +set hiddenfiles ".*:*.aux:*.log:*.bbl:*.bcf:*.blg:*.run.xml" +set cleaner '~/.config/lf/cleaner' +set previewer '~/.config/lf/scope' +set autoquit true +set relativenumber + +cmd z %{{ + echo "$1" > ~/lflogs + result="$(zoxide query --exclude "${PWD}" -- "$1")" + lf -remote "send ${id} cd '${result}'" +}} +map z push :z<space> + +# cmds/functions +cmd open ${{ + case $(file --mime-type "$(readlink -f $f)" -b) in + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) localc $fx ;; + image/vnd.djvu|application/pdf|application/octet-stream|application/postscript) setsid -f zathura $fx >/dev/null 2>&1 ;; + text/*|application/json|inode/x-empty|application/x-subrip) $EDITOR $fx;; + image/x-xcf) setsid -f gimp $f >/dev/null 2>&1 ;; + image/svg+xml) display -- $f ;; + image/*) rotdir $f | grep -i "\.\(png\|jpg\|jpeg\|gif\|webp\|avif\|tif\|ico\)\(_large\)*$" | + setsid -f nsxiv -aio 2>/dev/null | while read -r file; do + [ -z "$file" ] && continue + lf -remote "send select \"$file\"" + lf -remote "send toggle" + done & + ;; + audio/*|video/x-ms-asf) mpv --audio-display=no $f ;; + video/*) setsid -f mpv $f -quiet >/dev/null 2>&1 ;; + application/pdf|application/vnd.djvu|application/epub*) setsid -f zathura $fx >/dev/null 2>&1 ;; + application/pgp-encrypted) $EDITOR $fx ;; + application/vnd.openxmlformats-officedocument.wordprocessingml.document|application/vnd.oasis.opendocument.text|application/vnd.openxmlformats-officedocument.spreadsheetml.sheet|application/octet-stream|application/vnd.oasis.opendocument.spreadsheet|application/vnd.oasis.opendocument.spreadsheet-template|application/vnd.openxmlformats-officedocument.presentationml.presentation|application/vnd.oasis.opendocument.presentation-template|application/vnd.oasis.opendocument.presentation|application/vnd.ms-powerpoint|application/vnd.oasis.opendocument.graphics|application/vnd.oasis.opendocument.graphics-template|application/vnd.oasis.opendocument.formula|application/vnd.oasis.opendocument.database) setsid -f libreoffice $fx >/dev/null 2>&1 ;; + *) for f in $fx; do setsid -f $OPENER $f >/dev/null 2>&1; done;; + esac +}} + +cmd mkdir $mkdir -p "$@" + +cmd extract ${{ + clear; tput cup $(($(tput lines)/3)); tput bold + set -f + printf "%s\n\t" "$fx" + printf "extract?[y/N]" + read ans + [ $ans = "y" ] && { + case $fx in + *.tar.bz2) tar xjf $fx ;; + *.tar.gz) tar xzf $fx ;; + *.bz2) bunzip2 $fx ;; + *.rar) unrar e $fx ;; + *.gz) gunzip $fx ;; + *.tar) tar xf $fx ;; + *.tbz2) tar xjf $fx ;; + *.tgz) tar xzf $fx ;; + *.zip) unzip $fx ;; + *.Z) uncompress $fx ;; + *.7z) 7z x $fx ;; + *.tar.xz) tar xf $fx ;; + esac + } +}} + +cmd delete ${{ + clear; tput cup $(($(tput lines)/3)); tput bold + set -f + printf "%s\n\t" "$fx" + printf "delete?[y/N]" + read ans + [ $ans = "y" ] && rm -rf -- $fx +}} + +cmd moveto ${{ + clear; tput cup $(($(tput lines)/3)); tput bold + set -f + clear; echo "Move to where?" + dest="$(sed -e 's/\s*#.*//' -e '/^$/d' -e 's/^\S*\s*//' ${XDG_CONFIG_HOME:-$HOME/.config}/shell/bm-dirs | fzf | sed 's|~|$HOME|')" && + for x in $fx; do + eval mv -iv \"$x\" \"$dest\" + done && + notify-send "🚚 File(s) moved." "File(s) moved to $dest." +}} + +cmd copyto ${{ + clear; tput cup $(($(tput lines)/3)); tput bold + set -f + clear; echo "Copy to where?" + dest="$(sed -e 's/\s*#.*//' -e '/^$/d' -e 's/^\S*\s*//' ${XDG_CONFIG_HOME:-$HOME/.config}/shell/bm-dirs | fzf | sed 's|~|$HOME|')" && + for x in $fx; do + eval cp -ivr \"$x\" \"$dest\" + done && + notify-send "📋 File(s) copied." "File(s) copies to $dest." +}} + +cmd setbg "$1" + +cmd bulkrename ${{ + tmpfile_old="$(mktemp)" + tmpfile_new="$(mktemp)" + + [ -n "$fs" ] && fs=$(basename -a $fs) || fs=$(ls) + + echo "$fs" > "$tmpfile_old" + echo "$fs" > "$tmpfile_new" + $EDITOR "$tmpfile_new" + + [ "$(wc -l < "$tmpfile_old")" -eq "$(wc -l < "$tmpfile_new")" ] || { rm -f "$tmpfile_old" "$tmpfile_new"; exit 1; } + + paste "$tmpfile_old" "$tmpfile_new" | while IFS="$(printf '\t')" read -r src dst + do + [ "$src" = "$dst" ] || [ -e "$dst" ] || mv -- "$src" "$dst" + done + + rm -f "$tmpfile_old" "$tmpfile_new" + lf -remote "send $id unselect" +}} + +# Bindings +map . $$EDITOR ${PWD} +map <c-f> $lf -remote "send $id select \"$(fzf)\"" +map J $lf -remote "send $id cd $(sed -e 's/\s*#.*//' -e '/^$/d' -e 's/^\S*\s*//' ${XDG_CONFIG_HOME:-$HOME/.config}/shell/bm-dirs | fzf)" +map gh +map g top +map D delete +map E extract +map C copyto +map M moveto +map <c-n> push :mkdir<space>""<left> +map <c-r> reload +map <c-s> set hidden! +map <enter> shell +map x $$f +map X !$f +map o &mimeopen "$f" +map O $mimeopen --ask "$f" + +map A :rename; cmd-end # at the very end +map c push A<c-u> # new rename +map I :rename; cmd-home # at the very beginning +map i :rename # before extension +map a :rename; cmd-right # after extension +map B bulkrename +map b $setbg $f +map <backspace2> clear + +map <c-e> down +map <c-y> up +map V push :!nvim<space> + +map W $setsid -f $TERMINAL >/dev/null 2>&1 + +map Y $printf "%s" "$fx" | xclip -selection clipboard diff --git a/mut/lf/scope b/mut/lf/scope new file mode 100755 index 0000000..57c0ed9 --- /dev/null +++ b/mut/lf/scope @@ -0,0 +1,59 @@ +#!/bin/sh + +# File preview handler for lf. + +set -C -f +IFS="$(printf '%b_' '\n')"; IFS="${IFS%_}" + +image() { + if [ -f "$1" ] && command -V kitten >/dev/null 2>&1; then + kitten icat --transfer-mode file --stdin no --place "${2}x${3}@${4}x${5}" "$1" < /dev/null > /dev/tty + elif [ -f "$1" ] && [ -n "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ] && command -V ueberzug >/dev/null 2>&1; then + printf '{"action": "add", "identifier": "PREVIEW", "x": "%s", "y": "%s", "width": "%s", "height": "%s", "scaler": "contain", "path": "%s"}\n' "$4" "$5" "$(($2-1))" "$(($3-1))" "$1" > "$FIFO_UEBERZUG" + else + mediainfo "$6" + fi +} + +# Note that the cache file name is a function of file information, meaning if +# an image appears in multiple places across the machine, it will not have to +# be regenerated once seen. + +case "$(file --dereference --brief --mime-type -- "$1")" in + image/avif) CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/lf/thumb.$(stat --printf '%n\0%i\0%F\0%s\0%W\0%Y' -- "$(readlink -f "$1")" | sha256sum | cut -d' ' -f1)" + [ ! -f "$CACHE" ] && convert "$1" "$CACHE.jpg" + image "$CACHE.jpg" "$2" "$3" "$4" "$5" "$1" ;; + image/vnd.djvu) + CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/lf/thumb.$(stat --printf '%n\0%i\0%F\0%s\0%W\0%Y' -- "$(readlink -f "$1")" | sha256sum | cut -d' ' -f1)" + [ ! -f "$CACHE" ] && djvused "$1" -e 'select 1; save-page-with /dev/stdout' | convert -density 200 - "$CACHE.jpg" > /dev/null 2>&1 + image "$CACHE.jpg" "$2" "$3" "$4" "$5" "$1" ;; +image/svg+xml) + CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/lf/thumb.$(stat --printf '%n\0%i\0%F\0%s\0%W\0%Y' -- "$(readlink -f "$1")" | sha256sum | cut -d' ' -f1)" + [ ! -f "$CACHE" ] && inkscape --convert-dpi-method=none -o "$CACHE.png" --export-overwrite -D --export-png-color-mode=RGBA_16 "$1" + image "$CACHE.png" "$2" "$3" "$4" "$5" "$1" + ;; + image/*) image "$1" "$2" "$3" "$4" "$5" "$1" ;; + text/html) open "$1" ;; + text/troff) man ./ "$1" | col -b ;; + text/* | */xml | application/json | application/x-ndjson) bat -p --theme ansi --terminal-width "$(($4-2))" -f "$1" ;; + audio/* | application/octet-stream) mediainfo "$1" || exit 1 ;; + video/* ) + CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/lf/thumb.$(stat --printf '%n\0%i\0%F\0%s\0%W\0%Y' -- "$(readlink -f "$1")" | sha256sum | cut -d' ' -f1)" + [ ! -f "$CACHE" ] && ffmpegthumbnailer -i "$1" -o "$CACHE" -s 0 + image "$CACHE" "$2" "$3" "$4" "$5" "$1" + ;; + */pdf) + CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/lf/thumb.$(stat --printf '%n\0%i\0%F\0%s\0%W\0%Y' -- "$(readlink -f "$1")" | sha256sum | cut -d' ' -f1)" + [ ! -f "$CACHE.jpg" ] && pdftoppm -jpeg -f 1 -singlefile "$1" "$CACHE" + image "$CACHE.jpg" "$2" "$3" "$4" "$5" "$1" + ;; + */epub+zip|*/mobi*) + CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/lf/thumb.$(stat --printf '%n\0%i\0%F\0%s\0%W\0%Y' -- "$(readlink -f "$1")" | sha256sum | cut -d' ' -f1)" + [ ! -f "$CACHE.jpg" ] && gnome-epub-thumbnailer "$1" "$CACHE.jpg" + image "$CACHE.jpg" "$2" "$3" "$4" "$5" "$1" + ;; + application/*zip) atool --list -- "$1" ;; + *opendocument*) odt2txt "$1" ;; + application/pgp-encrypted) gpg -d -- "$1" ;; +esac +exit 1 diff --git a/mut/neovim/README.md b/mut/neovim/README.md new file mode 100644 index 0000000..3a441bd --- /dev/null +++ b/mut/neovim/README.md @@ -0,0 +1,2 @@ +# mike_neovim +installs my neovim config on mac or linux diff --git a/mut/neovim/after/queries/nu/highlights.scm b/mut/neovim/after/queries/nu/highlights.scm new file mode 100644 index 0000000..55b04ed --- /dev/null +++ b/mut/neovim/after/queries/nu/highlights.scm @@ -0,0 +1,313 @@ +;;; --- +;;; keywords +[ + "def" + "alias" + "export-env" + "export" + "extern" + "module" + + "let" + "let-env" + "mut" + "const" + + "hide-env" + + "source" + "source-env" + + "overlay" + "register" + + "loop" + "while" + "error" + + "do" + "if" + "else" + "try" + "catch" + "match" + + "break" + "continue" + "return" + +] @keyword + +(hide_mod "hide" @keyword) +(decl_use "use" @keyword) + +(ctrl_for + "for" @keyword + "in" @keyword +) +(overlay_list "list" @keyword.storage.modifier) +(overlay_hide "hide" @keyword.storage.modifier) +(overlay_new "new" @keyword.storage.modifier) +(overlay_use + "use" @keyword.storage.modifier + "as" @keyword +) +(ctrl_error "make" @keyword.storage.modifier) + +;;; --- +;;; literals +(val_number) @number +(val_duration unit: _ @variable.parameter) +(val_filesize unit: _ @variable.parameter) +(val_binary + [ + "0b" + "0o" + "0x" + ] @number + "[" @punctuation.bracket + digit: [ + "," @punctuation.delimiter + (hex_digit) @number + ] + "]" @punctuation.bracket +) @number +(val_bool) @constant.builtin +(val_nothing) @constant.builtin +(val_string) @variable.parameter +arg_str: (val_string) @variable.parameter +file_path: (val_string) @variable.parameter +(val_date) @number +(inter_escape_sequence) @constant.character.escape +(escape_sequence) @constant.character.escape +(val_interpolated [ + "$\"" + "$\'" + "\"" + "\'" +] @string) +(unescaped_interpolated_content) @string +(escaped_interpolated_content) @string +(expr_interpolated ["(" ")"] @variable.parameter) + +;;; --- +;;; operators +(expr_binary [ + "+" + "-" + "*" + "/" + "mod" + "//" + "++" + "**" + "==" + "!=" + "<" + "<=" + ">" + ">=" + "=~" + "!~" + "and" + "or" + "xor" + "bit-or" + "bit-xor" + "bit-and" + "bit-shl" + "bit-shr" + "in" + "not-in" + "starts-with" + "ends-with" +] @operator ) + +(where_command [ + "+" + "-" + "*" + "/" + "mod" + "//" + "++" + "**" + "==" + "!=" + "<" + "<=" + ">" + ">=" + "=~" + "!~" + "and" + "or" + "xor" + "bit-or" + "bit-xor" + "bit-and" + "bit-shl" + "bit-shr" + "in" + "not-in" + "starts-with" + "ends-with" +] @operator) + +(assignment [ + "=" + "+=" + "-=" + "*=" + "/=" + "++=" +] @operator) + +(expr_unary ["not" "-"] @operator) + +(val_range [ + ".." + "..=" + "..<" +] @operator) + +["=>" "=" "|"] @operator + +[ + "o>" "out>" + "e>" "err>" + "e+o>" "err+out>" + "o+e>" "out+err>" +] @operator + +;;; --- +;;; punctuation +[ + "," + ";" +] @punctuation.special + +(param_long_flag ["--"] @punctuation.delimiter) +(long_flag ["--"] @punctuation.delimiter) +(long_flag_equals_value ["--"] @punctuation.delimiter) +(short_flag ["-"] @punctuation.delimiter) +(long_flag_equals_value ["="] @punctuation.special) +(param_short_flag ["-"] @punctuation.delimiter) +(param_rest "..." @punctuation.delimiter) +(param_type [":"] @punctuation.special) +(param_value ["="] @punctuation.special) +(param_cmd ["@"] @punctuation.special) +(param_opt ["?"] @punctuation.special) +(returns "->" @punctuation.special) + +[ + "(" ")" + "{" "}" + "[" "]" +] @punctuation.bracket + +(val_record + (record_entry ":" @punctuation.delimiter)) +key: (identifier) @property + +;;; --- +;;; identifiers +(param_rest + name: (_) @variable.parameter) +(param_opt + name: (_) @variable.parameter) +(parameter + param_name: (_) @variable.parameter) +(param_cmd + (cmd_identifier) @string) + +(param_long_flag (long_flag_identifier) @attribute) +(param_short_flag (param_short_flag_identifier) @attribute) + +(short_flag (short_flag_identifier) @attribute) +(long_flag_identifier) @attribute + +(scope_pattern [(wild_card) @function]) + +(cmd_identifier) @function +; generated with Nu 0.93.0 +; > help commands +; | filter { $in.command_type == builtin and $in.category != core } +; | each {$'"($in.name | split row " " | $in.0)"'} +; | uniq +; | str join ' ' +(command + head: [ + (cmd_identifier) @function.builtin + (#any-of? @function.builtin + "all" "ansi" "any" "append" "ast" "bits" "bytes" "cal" "cd" "char" "clear" + "collect" "columns" "compact" "complete" "config" "cp" "date" "debug" + "decode" "default" "detect" "dfr" "drop" "du" "each" "encode" "enumerate" + "every" "exec" "exit" "explain" "explore" "export-env" "fill" "filter" + "find" "first" "flatten" "fmt" "format" "from" "generate" "get" "glob" + "grid" "group" "group-by" "hash" "headers" "histogram" "history" "http" + "input" "insert" "inspect" "interleave" "into" "is-empty" "is-not-empty" + "is-terminal" "items" "join" "keybindings" "kill" "last" "length" + "let-env" "lines" "load-env" "ls" "math" "merge" "metadata" "mkdir" + "mktemp" "move" "mv" "nu-check" "nu-highlight" "open" "panic" "par-each" + "parse" "path" "plugin" "port" "prepend" "print" "ps" "query" "random" + "range" "reduce" "reject" "rename" "reverse" "rm" "roll" "rotate" + "run-external" "save" "schema" "select" "seq" "shuffle" "skip" "sleep" + "sort" "sort-by" "split" "split-by" "start" "stor" "str" "sys" "table" + "take" "tee" "term" "timeit" "to" "touch" "transpose" "tutor" "ulimit" + "uname" "uniq" "uniq-by" "update" "upsert" "url" "values" "view" "watch" + "where" "which" "whoami" "window" "with-env" "wrap" "zip" + ) + ]) + +(command + "^" @punctuation.delimiter + head: (_) @function +) + +"where" @function.builtin + +(path + ["." "?"] @punctuation.delimiter +) @variable.parameter + +(stmt_let (identifier) @variable) + +(val_variable + "$" @punctuation.special + [ + (identifier) @variable + "in" @special + "nu" @namespace + "env" @constant + ] +) @none + +(record_entry + ":" @punctuation.special) + +;;; --- +;;; types +(flat_type) @type +(list_type + "list" @type.enum + ["<" ">"] @punctuation.bracket +) +(collection_type + ["record" "table"] @type.enum + "<" @punctuation.bracket + key: (_) @variable.parameter + ["," ":"] @punctuation.special + ">" @punctuation.bracket +) + +(shebang) @keyword.directive +(comment) @comment +( + (comment) @comment.documentation + (decl_def) +) +( + (parameter) + (comment) @comment.documentation +) diff --git a/mut/neovim/after/queries/nu/indents.scm b/mut/neovim/after/queries/nu/indents.scm new file mode 100644 index 0000000..488772a --- /dev/null +++ b/mut/neovim/after/queries/nu/indents.scm @@ -0,0 +1,25 @@ +[ + (expr_parenthesized) + (parameter_bracks) + + (val_record) + (val_list) + (val_closure) + (val_table) + + (block) +] @indent.begin + +[ + "}" + "]" + ")" +] @indent.end + +[ + "}" + "]" + ")" +] @indent.branch + +(comment) @indent.auto diff --git a/mut/neovim/after/queries/nu/injections.scm b/mut/neovim/after/queries/nu/injections.scm new file mode 100644 index 0000000..30804d6 --- /dev/null +++ b/mut/neovim/after/queries/nu/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment"))
\ No newline at end of file diff --git a/mut/neovim/compiler/ansible-lint.vim b/mut/neovim/compiler/ansible-lint.vim new file mode 100644 index 0000000..9427092 --- /dev/null +++ b/mut/neovim/compiler/ansible-lint.vim @@ -0,0 +1,11 @@ +if exists('current_compiler') + finish +endif +let current_compiler = 'go-test' + +if exists(':CompilerSet') != 2 + command -nargs=* CompilerSet setlocal <args> +endif + +CompilerSet makeprg=compile\ ansible-lint +CompilerSet errorformat=%Z%f:%l\ %m,%Z%f:%l,%E%\\%%(%\\S%\\)%\\@=%m,%C%\\%%(%\\S%\\)%\\@=%m,%-G diff --git a/mut/neovim/compiler/go-test.vim b/mut/neovim/compiler/go-test.vim new file mode 100644 index 0000000..61442e5 --- /dev/null +++ b/mut/neovim/compiler/go-test.vim @@ -0,0 +1,14 @@ +if exists('current_compiler') + finish +endif +let current_compiler = 'go-test' + +if exists(':CompilerSet') != 2 + command -nargs=* CompilerSet setlocal <args> +endif + +" %f>%l:%c:%t:%n:%m +CompilerSet makeprg=go\ test +CompilerSet errorformat=%.%#:\ %m\ %f:%l,%.%#:\ %m\ at\ %f:%l%.%#, + +" vim: sw=2 sts=2 et diff --git a/mut/neovim/compiler/helm.vim b/mut/neovim/compiler/helm.vim new file mode 100644 index 0000000..7e4b21d --- /dev/null +++ b/mut/neovim/compiler/helm.vim @@ -0,0 +1,14 @@ +if exists('current_compiler') + finish +endif +let current_compiler = 'go-test' + +if exists(':CompilerSet') != 2 + command -nargs=* CompilerSet setlocal <args> +endif + +CompilerSet makeprg=compile\ helm\ lint +CompilerSet errorformat=\[%t%.%#\]%.%#\ template:\ %f:%l:%c:\ %m, + \\[%t%.%#\]\ %f:\ %m, + +" vim: sw=2 sts=2 et diff --git a/mut/neovim/compiler/nix.lua b/mut/neovim/compiler/nix.lua new file mode 100644 index 0000000..d012641 --- /dev/null +++ b/mut/neovim/compiler/nix.lua @@ -0,0 +1 @@ +vim.opt.errorformat = [[%.%#at %f:%l:%c%.%#,%mat %f:%l:%c,%.%#error:%.%#'%f': %m,]] diff --git a/mut/neovim/compiler/racket.vim b/mut/neovim/compiler/racket.vim new file mode 100644 index 0000000..1f98a41 --- /dev/null +++ b/mut/neovim/compiler/racket.vim @@ -0,0 +1,26 @@ +if exists('current_compiler') + finish +endif +let current_compiler = 'go-test' + +if exists(':CompilerSet') != 2 + command -nargs=* CompilerSet setlocal <args> +endif + +" The errorformat can also use vim's regular expression syntax (albeit in a rather awkward way) which gives us a solution to the problem. We can use a non-capturing group and a zero-width assertion to require the presence of these signaling phrases without consuming them. This then allows the %m to pick them up. As plain regular expression syntax this zero-width assertion looks like: +" +" \%(undefined reference\|multiple definition\)\@= +" +" But in order to use it in efm we need to replace \ by %\ and % by %% + + +CompilerSet makeprg=compile\ racket +CompilerSet errorformat=\%Z%*\\S%.%#, + \%C\ \ \ %f:%l:%c, + \%C\ \ \ %f:%l:%c:\ %m, + \%C\ \ %.%#%\\%%(module%\\spath:%\\\|at\:%\\\|in\:%\\\|expected\:%\\\|given\:%\\)%\\@=%m, + \%C\ %.%#, + \%E%\\%%(%\\w%\\)%\\@=%f:%*\\d:%*\\d:\ %m, + \%E%*\\f:%*\\d:%*\\d:\ %m, + \%E%\\%%(%\\S%\\+:%\\\|%\.%\\+--%\\)%\\@=%m, +" vim: sw=2 sts=2 et diff --git a/mut/neovim/compiler/rust.lua b/mut/neovim/compiler/rust.lua new file mode 100644 index 0000000..272ae18 --- /dev/null +++ b/mut/neovim/compiler/rust.lua @@ -0,0 +1 @@ +vim.opt.errorformat=[[%-G,%-Gerror: aborting %.%#,%-Gerror: Could not compile %.%#,%Eerror: %m,%Eerror[E%n]: %m,%Wwarning: %m,%Inote: %m,%C %#--> %f:%l:%c,%E left:%m,%C right:%m %f:%l:%c,%Z]] diff --git a/mut/neovim/compiler/terragrunt.vim b/mut/neovim/compiler/terragrunt.vim new file mode 100644 index 0000000..54f94ef --- /dev/null +++ b/mut/neovim/compiler/terragrunt.vim @@ -0,0 +1,21 @@ +if exists('current_compiler') + finish +endif +let current_compiler = 'go-test' + +if exists(':CompilerSet') != 2 + command -nargs=* CompilerSet setlocal <args> +endif + +" The errorformat can also use vim's regular expression syntax (albeit in a rather awkward way) which gives us a solution to the problem. We can use a non-capturing group and a zero-width assertion to require the presence of these signaling phrases without consuming them. This then allows the %m to pick them up. As plain regular expression syntax this zero-width assertion looks like: +" +" \%(undefined reference\|multiple definition\)\@= +" +" But in order to use it in efm we need to replace \ by %\ and % by %% + + +CompilerSet makeprg=terragrunt +CompilerSet errorformat=%.%#level=%t%.%#msg=%f:%l%\\,%c-%*\\d:\ %m, + \%Z%m, + \%E-\ %m\ (at\ %f:%l\\,%c-%*\\d), +" vim: sw=2 sts=2 et diff --git a/mut/neovim/init.lua b/mut/neovim/init.lua new file mode 100644 index 0000000..7ab99c8 --- /dev/null +++ b/mut/neovim/init.lua @@ -0,0 +1 @@ +return require("my") diff --git a/mut/neovim/lua/my/events.lua b/mut/neovim/lua/my/events.lua new file mode 100644 index 0000000..933ba19 --- /dev/null +++ b/mut/neovim/lua/my/events.lua @@ -0,0 +1,120 @@ +local lsp = require("my.lsp") +local oil = require("oil") +local lint = require("lint") +local event = vim.api.nvim_create_autocmd +local command = vim.api.nvim_create_user_command + +vim.api.nvim_create_augroup("my", {clear= true}) +vim.api.nvim_create_augroup("conf#events", {clear= true}) + +event( + "User", + {group= "conf#events", + pattern= { "ZoxideDirChanged" }, + callback= function() + vim.schedule(function() + oil.open(vim.fn.getcwd()) + end) + end}) + +event( + "BufReadPost", + {group= "conf#events", + pattern= { "*" }, + callback=function() + local pattern = "'\\s\\+$'" + vim.cmd("syn match TrailingWhitespace " .. pattern) + vim.cmd("hi link TrailingWhitespace IncSearch") + end}) + +event( + "BufWritePost", + {group= "conf#events", + pattern={ "*" }, + callback=function() + lint.try_lint() + vim.schedule(function() vim.diagnostic.setloclist({open= false}) end) + end}) + +local session_file = vim.fn.expand("~/.vimsession.vim") +event( + "VimLeave", + {group= "conf#events", + pattern= { "*" }, + callback=function() + vim.cmd("mksession! " .. session_file) + end}) + +event( + "LspAttach", + {group = "conf#events", + pattern = { "*" }, + callback = function(ev) + lsp.attach({ + client = vim.lsp.get_client_by_id(ev.data.client_id), + buf = ev.buf, + }) + end}) + +event( + "LspAttach", + {group = "conf#events", + pattern = { "*" }, + callback = function(ev) + lsp.attach({ + client = vim.lsp.get_client_by_id(ev.data.client_id), + buf = ev.buf, + }) + end}) + +-- filetypes + +event( + "FileType", { + group="conf#events", + pattern={ "go", "gomod", "gowork", "gotmpl" }, + callback=function(ev) + vim.lsp.start({ + name="gopls", + cmd={ "gopls" }, + root_dir=vim.fs.root(ev.buf, {"go.work", "go.mod", ".git"}) + }) + end, + }) + +event( + "FileType", { + group="conf#events", + pattern={ "python" }, + callback=function(ev) + vim.lsp.start({ + name="basedpyright", + cmd={ "basedpyright-langserver", "--stdio" }, + settings={ + basedpyright = { + analysis = { + autoSearchPaths = true, + diagnosticMode = "openFilesOnly", + useLibraryCodeForTypes = true, + autoImportCompletions = true, + inlayHints = { + variableTypes = true, + callArgumentNames = true, + functionReturnTypes = true, + genericTypes = true, + }, + }, + }, + }, + root_dir=vim.fs.root(ev.buf, { + 'pyproject.toml', + 'setup.py', + 'setup.cfg', + 'requirements.txt', + 'Pipfile', + 'pyrightconfig.json', + '.git', + }) + }) + end, + }) diff --git a/mut/neovim/lua/my/init.lua b/mut/neovim/lua/my/init.lua new file mode 100644 index 0000000..76bf1f0 --- /dev/null +++ b/mut/neovim/lua/my/init.lua @@ -0,0 +1,550 @@ +require("my.settings") +_G.P = function(...) + vim.iter({...}):map(vim.inspect):each(print) +end +_G.ternary = function ( cond , T , F ) + if cond then return T else return F end +end +vim.cmd "colorscheme kanagawa-wave" + +vim.cmd "filetype plugin on" +vim.cmd "filetype indent on" +vim.cmd "highlight WinSeparator guibg=None" +vim.cmd "packadd cfilter" + +vim.api.nvim_set_hl(0, "VirtualTextWarning", {link= "Grey"}) +vim.api.nvim_set_hl(0, "VirtualTextError", {link= "DiffDelete"}) +vim.api.nvim_set_hl(0, "VirtualTextInfo", {link= "DiffChange"}) +vim.api.nvim_set_hl(0, "VirtualTextHint", {link= "DiffAdd"}) +vim.diagnostic.config({virtual_text = false, virtual_lines = { highlight_whole_line = false, only_current_line = true } }) + +local map = vim.keymap.set +local unmap = vim.keymap.del +function i_grep(word, file) + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes( + ":silent grep " + .. ternary(not (word == ""), word .. " ", "") + .. file:gsub("oil://", "") + .. "<c-f>B<left>i<space>", + true, false, true + ), + "n", false + ) +end + +function cope() + require("quicker").refresh() + vim.cmd(":botright copen " .. math.floor(vim.o.lines / 2.1)) +end + +map("n", "gb", ":GBrowse<CR>") +map("n", "g<cr>", ":G<cr>") +map("n", "ge", function() vim.diagnostic.open_float() end) +map("n", "-", ":Oil<cr>") +map("n", "<leader>qf", cope) +map("n", "<leader>q<BS>", ":cclose<cr>") +map("n", "<leader>ll", ":lopen<cr>") +map("n", "<leader>l<BS>", ":lclose<cr>") +map("n", "<M-h>", cope) +map("n", "<C-n>", ":cnext<cr>") +map("n", "<C-p>", ":cprev<cr>") +map("n", "<C-a>", ":Rerun<CR>") +map("n", "<C-s>", function() + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes( + ":Sh<up><c-f>", + true, false, true + ), + "n", false + ) + vim.schedule(function() + vim.cmd("let v:searchforward = 0") + map("n","/","/Sh.*",{buffer=true}) + map("n","?","?Sh.*",{buffer=true}) + end) +end) +map("n", "<C-x>", function() + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes( + ":Compile<up><c-f>", + true, false, true + ), + "n", false + ) + vim.schedule(function() + vim.cmd("let v:searchforward = 0") + map("n","/","/Compile.*",{buffer=true}) + map("n","?","?Compile.*",{buffer=true}) + end) +end) +map("n", "[q",":cprevious<cr>") +map("n", "]q",":cnext<cr>") +map("n", "[x",":lprevious<cr>") +map("n", "]x",":lnext<cr>") +map("n", "[g",":GV<cr>") +map("n", "]g",":GV?<cr>") +map("n", "]G",":GV!<cr>") +map("n", "<leader>:", function() i_grep("<c-r><c-w>", vim.fn.bufname("%")) end) +map("v", "<leader>:", ":Vgrep!<cr>") +map("n", "<leader>;", function() i_grep("", vim.fn.fnamemodify(vim.fn.bufname("%"), ":h")) end) +map("v", "<leader>;", ":Vgrep<cr>") +map("n", "<leader>'", ":Find ") +map("n", "<leader>x<cr>", function() vim.cmd "b #" end) + +require("nvim_comment").setup() + +local oil_actions = require("oil.actions") +map("n", "_", oil_actions.open_cwd.callback) + +local fzf = require("fzf-lua") +local action = (require "fzf-lua.actions") +fzf.setup {"max-perf"} +fzf.register_ui_select() +map("n", "<leader>xp", fzf.files) +map("n", "<leader>xa", fzf.args) +map("n", "<leader>x;", fzf.quickfix) +map("n", "<leader>xb", function() + fzf.buffers({ + actions={default={fn=action.buf_edit_or_qf}} + }) +end) + +local obsidian = require("obsidian") +obsidian.setup { workspaces = { + { name = "notes", path = ternary(vim.fn.isdirectory(vim.fn.expand("~/Sync/my/notes")) == 1, "~/Sync/my/notes", "~/sync/my/notes")} +}} + + +vim.api.nvim_create_user_command( + "Vgrep", +function(cmd) + local buf, lrow, lcol = unpack(vim.fn.getpos("'<")) + local buf, rrow, rcol = unpack(vim.fn.getpos("'>")) + -- (local [line & rest] (vim.api.nvim_buf_get_text 0 (- <row 1) (- <col 1) (- >row 1) >col {})) + local firstline = + vim.iter(vim.api.nvim_buf_get_text(0, lrow-1, lcol-1, rrow-1, rcol, {})) + :next() + if cmd.bang then + i_grep(firstline, vim.fn.bufname("%")) + else + i_grep(firstline, vim.fn.fnamemodify(vim.fn.bufname("%"), ":h")) + end +end, + {range= 1, bang=true} +) + +vim.api.nvim_create_user_command( + "NixEdit", +function(cmd) + local f = io.popen("nix eval --raw /nix-config#nixosConfigurations." .. vim.fn.hostname() .. ".pkgs." .. cmd.args) + vim.cmd("e " .. f:read()) +end, + {nargs=1} +) + + +local last_job_state = nil +local last_job_thunk = nil +local last_job_lines = "" +function qf(inputs, opts) + local id, title = inputs.id, inputs.title + local prettify = function(line) + local l = line:gsub("%c+%[[0-9:;<=>?]*[!\"#$%%&'()*+,-./]*[@A-Z%[%]^_`a-z{|}~]*;?[A-Z]?", "") + return l + end + local in_qf = function() + return vim.opt_local.buftype:get() == "quickfix" + end + local is_at_last_line = function() + local row, _ = vim.api.nvim_win_get_cursor(0) + local last_line = vim.api.nvim_buf_line_count(0) + return row == last_line + end + return function(lines) + lines = vim.iter(lines):map(prettify):totable() + vim.schedule(function() + local what = { + id=id, + title=title, + lines=lines, + efm=opts.efm, + } + vim.fn.setqflist( + {}, "a", what + ) + if (not in_qf()) or (is_at_last_line() and in_qf()) then + vim.cmd ":cbottom" + end + end) + end +end + +function qfjob(cmd, stdin, opts) + last_job_lines = "" + local opts = opts or {} + opts.filter = opts.filter or (function(line) + return line + end) + + local title = table.concat(cmd, " ") + vim.fn.setqflist({}, " ", {title=title}) + local append_lines = qf(vim.fn.getqflist({id=0,title=1}), opts) + last_job_state = vim.system( + cmd, { + stdin=stdin, + stdout=function(err,data) + if data then + if not opts.buffer then + append_lines(vim.iter(data:gmatch("[^\n]+")):map(opts.filter)) + else + last_job_lines = last_job_lines .. data + end + end + end, + stderr=function(err,data) + if data then + if not opts.buffer then + append_lines(vim.iter(data:gmatch("[^\n]+")):map(opts.filter)) + else + last_job_lines = last_job_lines .. data + end + end + end, + }, + function(job) + vim.schedule(function() + if opts.buffer then + append_lines(vim.iter(last_job_lines:gmatch("[^\n]+")):map(opts.filter)) + end + + local winnr = vim.fn.winnr() + if not (job.code == 0) then + cope() + if not (winnr == vim.fn.winnr()) then + vim.notify([["]] .. title .. [[" failed!]]) + vim.cmd "wincmd p | cbot" + end + else + if opts.open then + cope() + end + vim.notify([["]] .. title .. [[" succeeded!]]) + end + end) + end) +end + +vim.api.nvim_create_user_command( + "Find", + function(cmd) + local bufs = vim.iter(vim.api.nvim_list_bufs()) + :fold({}, function(acc, b) + acc[vim.api.nvim_buf_get_name(b)] = vim.api.nvim_buf_get_mark(b, [["]]) + return acc + end) + qfjob({ "fdfind", "--absolute-path", "--type", "f", "-E", vim.fn.expand("%:."), cmd.args }, nil, {efm = "%f:%l:%c:%m", open = true, filter = function(line) + local pos = bufs[line] or {} + local lnum, col = (pos[1] or "1"), (pos[2] or "0") + return line .. ":" .. lnum .. ":" .. col .. ":" .. "hello" + end}) + end, + {nargs="*", bang=true, complete="file"}) + +function opts_for_args(args) + local opts = { + go = { + test = function(cmd) + return {buffer = true, efm=require('my.packages.go').efm()} + end + } + } + local arg_opts = vim.iter(args) + :fold(opts, function(acc, v) + if type(acc) == "table" and acc[v] then + return acc[v] + end + return acc + end) + if type(arg_opts) == "function" then + return arg_opts() + elseif type(arg_opts) == "table" then + return (arg_opts[1] or function() return {} end)() + end +end + +vim.api.nvim_create_user_command( + "Sh", + function(cmd) + local thunk = function() qfjob({ "nu", "--commands", cmd.args }, nil, opts_for_args(cmd.fargs)) end + last_job_thunk = thunk + thunk() + end, + {nargs="*", bang=true, complete="shellcmd"}) + +vim.api.nvim_create_user_command( + "Rerun", + function(cmd) + if not last_job_state then + vim.notify "nothing to rerun" + else + if not last_job_state:is_closing() then + vim.notify "Last job not finished" + else + last_job_thunk() + end + end + end, {bang=true}) + +vim.api.nvim_create_user_command( + "Stop", + function() + if last_job_state then + last_job_state:kill() + vim.notify "killed job" + else + vim.notify "nothing to do" + end + end, {bang=true}) + +local browse_git_remote = function(fugitive_data) + local path = fugitive_data.path + if not path then + local bufname = vim.fn.bufname("%") + if vim.startswith(bufname,"oil://") then + local d = "oil://" .. vim.fs.dirname(fugitive_data.git_dir) .. "/" + path = bufname:sub(d:len()+1, bufname:len()) + end + end + assert(path) + + local home, org, project, repo = "", "" + if vim.startswith(fugitive_data.remote, "git@") then + home, repo = fugitive_data.remote:match("git@([^:]+):(.*)%.git") + if not (home and repo) then + home, org, project, repo = fugitive_data.remote:match("git@([^:]+):.*/(.*)/(.*)/(.*)") + end + end + assert((home and org and project and repo) or (home and repo)) + + local homes = { + ["ssh.dev.azure.com"] = "dev.azure.com", + } + if homes[home] then + home = homes[home] + end + + local urls = { + ["bitbucket.org"] = { + ["tree"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/src/" .. fugitive_data.commit .. "/" .. path + end, + ["blob"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/src/" .. fugitive_data.commit .. "/" .. path + end, + ["commit"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/commits/" .. fugitive_data.commit + end, + ["ref"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/commits/" .. fugitive_data.commit + end, + }, + ["dev.azure.com"] = { + ["tree"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. org .. "/" .. project .. "/_git/" .. repo .. "?version=GB" .. fugitive_data.commit .. "&path=/" .. path + end, + ["blob"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. org .. "/" .. project.. "/_git/" .. repo .. "?version=GB" .. fugitive_data.commit .. "&path=/" .. path + end, + ["commit"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. org .. "/" .. project.. "/_git/" .. repo .. "/commit/" .. fugitive_data.commit + end, + ["ref"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. org .. "/" .. project.. "/_git/" .. repo .. "/commit/" .. fugitive_data.commit + end, + }, + ["gitlab.com"] = { + ["tree"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/-/tree/" .. fugitive_data.commit .. "/" .. path + end, + ["blob"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/-/blob/" .. fugitive_data.commit .. "/" .. path + end, + ["commit"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/-/commit/" .. fugitive_data.commit + end, + ["ref"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/-/commit/" .. fugitive_data.commit + end, + }, + ["github.com"] = { + ["tree"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/tree/" .. fugitive_data.commit .. "/" .. path + end, + ["blob"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/blob/" .. fugitive_data.commit .. "/" .. path + end, + ["commit"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/commit/" .. fugitive_data.commit + end, + ["ref"] = function(home, org, project, repo) + return "https://" .. home .. "/" .. repo .. "/commit/" .. fugitive_data.commit + end, + }, + } + + return urls[home][fugitive_data.type](home, org, project, repo) +end +vim.g.fugitive_browse_handlers = { browse_git_remote } + +-- require("lsp_signature").setup() +require("nvim-treesitter.configs").setup({highlight = {enable = true}}) +require("gitsigns").setup({ + current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` + current_line_blame_opts = { + virt_text = true, + virt_text_pos = 'right_align', -- 'eol' | 'overlay' | 'right_align' + delay = 1000, + ignore_whitespace = false, + virt_text_priority = 100, + use_focus = true, + }, +}) + +vim.opt.clipboard:append({"unnamedplus"}) + +local osc52 = require("vim.ui.clipboard.osc52") + +function paste() + return { + vim.fn.split(vim.fn.getreg(""), "\n"), + vim.fn.getregtype("") + } +end +-- function xclip(lines) +-- vim.system({ "nu", "--commands", "xclip -f -sel c | xclip"}, {stdin=lines, text=true}, nil) +-- end +function pbcopy(lines) + vim.system({ "nu", "--commands", "pbcopy"}, {stdin=lines, text=true}, nil) +end + +-- Unix, Linux variants +local fh, err = assert(io.popen("which pbcopy 2>/dev/null", "r")) +if fh:read() then + vim.g.clipboard = { + name = 'pbcopy Clipboard', + copy = { + ['+'] = pbcopy, + ['*'] = pbcopy, + }, + paste = { + ['+'] = paste, + ['*'] = paste, + }, + } +else + vim.g.clipboard = { + name = 'OSC 52', + copy = { + ['+'] = require('vim.ui.clipboard.osc52').copy('+'), + ['*'] = require('vim.ui.clipboard.osc52').copy('*'), + }, + paste = { + ['+'] = require('vim.ui.clipboard.osc52').paste('+'), + ['*'] = require('vim.ui.clipboard.osc52').paste('*'), + }, + } +end +require("my.events") +require("my.packages") + +-- require('render-markdown').setup ({ +-- opts = { +-- file_types = { "markdown", "Avante" }, +-- }, +-- ft = { "markdown", "Avante" },}) +-- require('avante_lib').load() +-- -- require('copilot').setup {} +-- require('avante').setup ({ +-- provider = "openai", +-- openai = { +-- model = "gpt-4o", +-- }, +-- behaviour = { +-- auto_suggestions = false, +-- auto_set_highlight_group = true, +-- auto_set_keymaps = true, +-- auto_apply_diff_after_generation = false, +-- support_paste_from_clipboard = false, +-- }, +-- mappings = { +-- --- @class AvanteConflictMappings +-- diff = { +-- ours = "co", +-- theirs = "ct", +-- all_theirs = "ca", +-- both = "cb", +-- cursor = "cc", +-- next = "]x", +-- prev = "[x", +-- }, +-- suggestion = { +-- accept = "<M-l>", +-- next = "<M-]>", +-- prev = "<M-[>", +-- dismiss = "<C-]>", +-- }, +-- jump = { +-- next = "]]", +-- prev = "[[", +-- }, +-- submit = { +-- normal = "<CR>", +-- insert = "<C-s>", +-- }, +-- sidebar = { +-- apply_all = "A", +-- apply_cursor = "a", +-- switch_windows = "<Tab>", +-- reverse_switch_windows = "<S-Tab>", +-- }, +-- }, +-- }) + +require("quicker").setup({ + keys = { + { + ">", + function() + require("quicker").expand({ before = 2, after = 2, add_to_existing = true }) + end, + desc = "Expand quickfix context", + }, + { + "<", + function() + require("quicker").collapse() + end, + desc = "Collapse quickfix context", + }, + }, +}) +-- (local +-- draw +-- (fn [toggle] +-- (if +-- toggle +-- (do +-- (vim.cmd "set virtualedit=all") +-- (vim.keymap.set :v "<leader>;" "<esc>:VBox<CR>") +-- (vim.keymap.set "n" "J" "<C-v>j:VBox<CR>") +-- (vim.keymap.set "n" "K" "<C-v>k:VBox<CR>") +-- (vim.keymap.set "n" "L" "<C-v>l:VBox<CR>") +-- (vim.keymap.set "n" "H" "<C-v>h:VBox<CR>")) +-- (do +-- (vim.cmd "set virtualedit=") +-- (vim.keymap.del :v "<leader>;") +-- (vim.keymap.del "n" "J") +-- (vim.keymap.del "n" "K") +-- (vim.keymap.del "n" "L") +-- (vim.keymap.del "n" "H"))))) diff --git a/mut/neovim/lua/my/lsp.lua b/mut/neovim/lua/my/lsp.lua new file mode 100644 index 0000000..2fad5aa --- /dev/null +++ b/mut/neovim/lua/my/lsp.lua @@ -0,0 +1,46 @@ +function set_buf_opt(buf, name, value) + return function() vim.api.nvim_buf_set_option(buf, name, value) end +end + +function buf_map(mode, key, fn) + return function() vim.keymap.set(mode, key, fn, {silent= true, noremap = true, buffer = 0}) end +end + +function lsp_action(action) + return vim.lsp.buf[action] +end + +local capability_map = { + -- completionProvider = (set_buf_opt :omnifunc "v:lua.vim.lsp.omnifunc"), + -- hoverProvider = set_buf_opt("keywordprg", ":LspHover"), + renameProvider = buf_map("n", "<leader>gr", lsp_action("rename")), + signatureHelpProvider = buf_map("n", "<leader>gs", lsp_action("signature_help")), + definitionProvider = buf_map("n", "<leader>gd", lsp_action("definition")), + declaration = buf_map("n", "<leader>gD", lsp_action("declaration")), + implementationProvider = buf_map("n", "<leader>gi", lsp_action("implementation")), + referencesProvider = buf_map("n", "<leader>gg", lsp_action("references")), + documentSymbolProvider = buf_map("n", "<leader>gds", lsp_action("workspace_symbol")), + codeActionProvider = buf_map("n", "<leader>ga", lsp_action("code_action")), + codeLensProvider = buf_map("n", "<leader>gl", vim.lsp.codelens.run), + inlayHintProvider = function() + vim.lsp.inlay_hint.enable(true) + buf_map("n", "<leader>gh", function() vim.lsp.inlay_hint.enable(0, not vim.lsp.inlay_hint.is_enabled(0)) end) + end, + documentFormattingProvider = function() + set_buf_opt("formatexpr", "v:lua.vim.lsp.format()") + buf_map("n", "<leader>gq", function() vim.lsp.buf.format({async= true}) end) + end, +} + +local M = {} + +M.attach = function (ev) + vim.iter(ev.client.server_capabilities) + :each(function(c) + local fn = capability_map[c] + if fn then fn() end + end) + ev.client.capabilities = require('blink.cmp').get_lsp_capabilities(ev.client.capabilities) +end + +return M diff --git a/mut/neovim/lua/my/packages/blink.lua b/mut/neovim/lua/my/packages/blink.lua new file mode 100644 index 0000000..31726dc --- /dev/null +++ b/mut/neovim/lua/my/packages/blink.lua @@ -0,0 +1,57 @@ +local blink = require('blink.cmp') +blink.setup { + fuzzy = { prebuilt_binaries = { force_version = "v0.10.0" } }, + -- 'default' for mappings similar to built-in completion + -- 'super-tab' for mappings similar to vscode (tab to accept, arrow keys to navigate) + -- 'enter' for mappings similar to 'super-tab' but with 'enter' to accept + -- See the full "keymap" documentation for information on defining your own keymap. + keymap = { preset = 'default' }, + + appearance = { + -- Sets the fallback highlight groups to nvim-cmp's highlight groups + -- Useful for when your theme doesn't support blink.cmp + -- Will be removed in a future release + use_nvim_cmp_as_default = true, + -- Set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' + -- Adjusts spacing to ensure icons are aligned + nerd_font_variant = 'mono' + }, + + -- Default list of enabled providers defined so that you can extend it + -- elsewhere in your config, without redefining it, due to `opts_extend` + sources = { + default = { 'lsp', 'path', 'snippets', 'buffer' }, + }, + + snippets = { preset = 'luasnip' }, +} + +local map = vim.keymap.set +local unmap = vim.keymap.del +local event = vim.api.nvim_create_autocmd +map("n", "<leader>xf", function() + event({"CmdwinEnter"}, { + once = true, + callback = function() + map("i","<c-j>", function() + blink.hide() + vim.schedule(function() + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("<cr>", true, false, true), + "c", false) + end) + end,{buffer=true}) + map("i","<bs>","<c-o>vT/d",{buffer=true}) + end + }) + event({"CmdwinLeave"}, { + once = true, + callback = function() + unmap("i","<c-j>",{buffer=true}) + unmap("i","<bs>",{buffer=true}) + end + }) + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes(":edit <c-r>=expand('%:p:h')<cr><c-f>A/", true, false, true), + "c", false) +end) diff --git a/mut/neovim/lua/my/packages/dap.lua b/mut/neovim/lua/my/packages/dap.lua new file mode 100644 index 0000000..c9240f9 --- /dev/null +++ b/mut/neovim/lua/my/packages/dap.lua @@ -0,0 +1,125 @@ +local dap = require("dap") +local adapters = dap.adapters +local configurations = dap.configurations + +local dapui = require("dapui") +local dappy = require("dap-python") + +adapters.delve = { + type="server", + port="${port}", + executable={ + command="dlv", + args= { "dap", "-l", "127.0.0.1:${port}" } + } +} + +configurations.go = { + {type = "delve", + name = "Debug", + request = "launch", + env = {CGO_CFLAGS="-Wno-error=cpp"}, + program = "${file}"}, + {type = "delve", + name = "DebugTest", + request = "launch", + mode = "test", + env = {CGO_CFLAGS="-Wno-error=cpp"}, + program = "${file}"}, + {type = "delve", + name = "DebugTerraform", + request = "launch", + program = "${file}", + env = {CGO_CFLAGS="-Wno-error=cpp"}, + args = { "-debug" }}, + {type = "delve", + name = "DebugTerraformAcc", + request = "launch", + program = "${file}", + mode = "test", + env = {CGO_CFLAGS="-Wno-error=cpp", TF_ACC=1}}, + {type = "delve", + name = "DebugTestSuite", + request = "launch", + mode = "test", + env = {CGO_CFLAGS="-Wno-error=cpp"}, + program = "${fileDirname}"}, +} + +dapui.setup { + expand_lines=false, + layouts={ + {position="bottom", size=10, elements={ {id="repl", size=0.5}, {id="console", size=0.5} }}, + {position="left", size=40, elements={ {id="breakpoints",size=0.25}, {id="stacks", size=0.25}, {id="watches", size=0.25}, {id="scopes", size=0.25} }}, + {position="bottom", size=25, elements={ {id="repl", size=0.35}, {id="watches", size=0.65} }}, + } +} + +dappy.setup() + +-- (local run_table +-- {:python +-- (fn [fname] +-- { +-- :name (.. "Launch " fname) +-- :program fname +-- :console "externalTerminal" +-- :request "launch" +-- :type "python" +-- :cwd :/Users/ivi/Programming +-- :waitOnAbnormalExit true +-- :waitOnNormalExit true})}) +-- (vim.keymap.set +-- :n +-- "s;" +-- (fn [] +-- (local fname (vim.fn.fnamemodify (vim.fn.bufname "%") ":p")) +-- (local get_config (. run_table (vim.opt_local.ft:get))) +-- +-- (set dap.defaults.fallback.external_terminal +-- {:command :/Applications/Alacritty.app/Contents/MacOS/alacritty +-- :args [:-T :dap :--working-directory (vim.fn.getcwd) :-e]}) +-- +-- (if get_config +-- (dap.run (get_config fname))))) + + +vim.keymap.set("n", "si", + function() + dapui.toggle {layout=1, reset=true} + dapui.toggle {layout=2, reset=true} + end, {silent=true}) + +vim.keymap.set("n", "s<enter>", function() dapui.toggle {layout=3, reset=true} end, {silent=true}) +-- ;; "breakpoints", +-- ;; "repl", +-- ;; "scopes", +-- ;; "stacks", +-- ;; "watches", +-- ;; "hover", +-- ;; "console",) +vim.keymap.set("n", "sfw", + function() + dapui.float_element("watches", {width=vim.api.nvim_win_get_width(0), height=30, enter=true}) + end, {silent=true}) + +vim.keymap.set("n", "sfs", + function() + dapui.float_element("scopes", {width=vim.api.nvim_win_get_width(0), height=30, enter=true}) + end, {silent=true}) + +vim.keymap.set("n", "sq", dap.terminate, {silent=true}) +vim.keymap.set("n", "sc", dap.continue, {silent=true}) +vim.keymap.set("n", "sr", dap.run_to_cursor, {silent=true}) +vim.keymap.set("n", "sn", dap.step_over, {silent=true}) +vim.keymap.set("n", "ss", dap.step_into, {silent=true}) +vim.keymap.set("n", "so", dap.step_out, {silent=true}) +vim.keymap.set("n", "sb", dap.toggle_breakpoint, {silent=true}) +vim.keymap.set("n", "sB", dap.set_breakpoint, {silent=true}) +vim.keymap.set("n", "slp", + function() + dap.set_breakpoint(nil, nil, vim.fn.input("Log point message: ")) + end, {silent=true}) + +vim.keymap.set("n", "st", dap.repl.toggle, {silent=true}) +vim.keymap.set("n", "sl", dap.run_last, {silent=true}) diff --git a/mut/neovim/lua/my/packages/go.lua b/mut/neovim/lua/my/packages/go.lua new file mode 100644 index 0000000..4abf384 --- /dev/null +++ b/mut/neovim/lua/my/packages/go.lua @@ -0,0 +1,75 @@ +local M = {} +local go = require("go") +local gotest = require("go.gotest") +go.setup { + test_efm = false, -- errorfomat for quickfix, default mix mode, set to true will be efm only + luasnip = true, + + goimports = false, + fillstruct = false, + gofmt = false, + max_line_len = nil, + tag_transform = false, + test_dir = false, + comment_placeholder = " ", + icons = false, + verbose = false, + log_path = vim.fn.expand("~/tmp/gonvim.log"), + lsp_cfg = false, + lsp_gofumpt = false, + lsp_on_attach = nil, + lsp_keymaps = false, + lsp_codelens = false, + diagnostic = false, + lsp_inlay_hints = {enable= false}, + gopls_remote_auto = false, + gocoverage_sign = "█", + sign_priority = 7, + dap_debug = false, + dap_debug_gui = false, + dap_debug_keymap = false, + dap_vt = false, + textobjects = false, + gopls_cmd = nil, + build_tags = "", + test_runner = "go", + run_in_floaterm = false, + iferr_vertical_shift = 4, +} + +local efm = function() + local indent = [[%\\%( %\\)]] + local efm = [[%-G=== RUN %.%#]] + efm = efm .. [[,%-G]] .. indent .. [[%#--- PASS: %.%#]] + efm = efm .. [[,%G--- FAIL: %\\%(Example%\\)%\\@= (%.%#)]] + efm = efm .. [[,%G]] .. indent .. [[%#--- FAIL: (%.%#)]] + efm = efm .. [[,%A]] .. indent .. [[%\\+%[%^:]%\\+: %f:%l: %m]] + efm = efm .. [[,%+Gpanic: test timed out after %.%\\+]] + efm = efm .. ',%+Afatal error: %.%# [recovered]' + efm = efm .. [[,%+Afatal error: %.%#]] + efm = efm .. [[,%+Apanic: %.%#]] + -- + -- -- exit + efm = efm .. ',%-Cexit status %[0-9]%\\+' + efm = efm .. ',exit status %[0-9]%\\+' + -- -- failed lines + efm = efm .. ',%-CFAIL%\\t%.%#' + efm = efm .. ',FAIL%\\t%.%#' + -- compiling error + + efm = efm .. ',%A%f:%l:%c: %m' + efm = efm .. ',%A%f:%l: %m' + efm = efm .. ',%f:%l +0x%[0-9A-Fa-f]%\\+' -- pannic with adress + efm = efm .. ',%-G%\\t%\\f%\\+:%\\d%\\+ +0x%[0-9A-Fa-f]%\\+' -- test failure, address invalid inside + -- multi-line + efm = efm .. ',%+G%\\t%m' + -- efm = efm .. ',%-C%.%#' -- ignore rest of unmatched lines + -- efm = efm .. ',%-G%.%#' + + efm = string.gsub(efm, ' ', [[\ ]]) + -- log(efm) + return efm +end +gotest.efm = efm +M.efm = efm +return M diff --git a/mut/neovim/lua/my/packages/init.lua b/mut/neovim/lua/my/packages/init.lua new file mode 100644 index 0000000..9c3a65f --- /dev/null +++ b/mut/neovim/lua/my/packages/init.lua @@ -0,0 +1,6 @@ +require("my.packages.oil") +require("my.packages.blink") +require("my.packages.lint") +require("my.packages.luasnip") +require("my.packages.dap") +require("my.packages.go") diff --git a/mut/neovim/lua/my/packages/lint.lua b/mut/neovim/lua/my/packages/lint.lua new file mode 100644 index 0000000..815f9b3 --- /dev/null +++ b/mut/neovim/lua/my/packages/lint.lua @@ -0,0 +1,26 @@ +local lint = require("lint") +local conform = require("conform") + +function is_executable(program) + return vim.fn.executable(program) == 1 +end + +lint.linters_by_ft = { + markdown=ternary(is_executable("vale"), { "vale" }, {}), + python=ternary(is_executable("ruff"), { "ruff" }, {}), + sh={ "shellcheck" }, +} + +conform.setup { + formatters_by_ft={ + python= { "ruff_format", "isort" }, + go= { "goimports" }, + nix= { "alejandra" }, + terraform= { "terraform_fmt" }, + hcl= { "terraform_fmt" }, + }, + format_on_save={ + timeout_ms= 500, + lsp_fallback= false + } +} diff --git a/mut/neovim/lua/my/packages/lualine.fnl b/mut/neovim/lua/my/packages/lualine.fnl new file mode 100644 index 0000000..4f57425 --- /dev/null +++ b/mut/neovim/lua/my/packages/lualine.fnl @@ -0,0 +1,17 @@ +(local lualine (require :lualine)) +(local clients #(do + (local bn (vim.fn.fnamemodify (vim.fn.bufname :%) ::p)) + (local m (bn:match ".*clients/([a-z]+)/.*")) + (if (not= nil m) + m + ""))) +(lualine.setup + {:extensions [:quickfix :fugitive :oil :fzf :nvim-dap-ui] + :sections + {:lualine_c ["%=" {1 clients :color :WarningMsg}]} + :winbar + {:lualine_a [:filename]} + :inactive_winbar + {:lualine_a [:filename]} + :tabline + {:lualine_a [:tabs]}}) diff --git a/mut/neovim/lua/my/packages/luasnip.lua b/mut/neovim/lua/my/packages/luasnip.lua new file mode 100644 index 0000000..338b343 --- /dev/null +++ b/mut/neovim/lua/my/packages/luasnip.lua @@ -0,0 +1,44 @@ +local ls = require("luasnip") +local s = ls.snippet +local sn = ls.snippet_node +local isn = ls.indent_snippet_node +local t = ls.text_node +local i = ls.insert_node +local f = ls.function_node +local c = ls.choice_node +local d = ls.dynamic_node +local r = ls.restore_node +local events = require("luasnip.util.events") +local ai = require("luasnip.nodes.absolute_indexer") +local extras = require("luasnip.extras") +local l = extras.lambda +local rep = extras.rep +local p = extras.partial +local m = extras.match +local n = extras.nonempty +local dl = extras.dynamic_lambda +local fmt = require("luasnip.extras.fmt").fmt +local fmta = require("luasnip.extras.fmt").fmta +local conds = require("luasnip.extras.expand_conditions") +local postfix = require("luasnip.extras.postfix").postfix +local types = require("luasnip.util.types") +local parse = require("luasnip.util.parser").parse_snippet +local ms = ls.multi_snippet +local k = require("luasnip.nodes.key_indexer").new_key + +vim.keymap.set( { "i", }, "<C-K>", ls.expand, {silent= true}) +vim.keymap.set( { "i", "s" }, "<C-L>", function() if ls.expand_or_jumpable() then ls.expand_or_jump(1) end end, {silent=true}) +vim.keymap.set( { "i", "s" }, "<C-J>", function() ls.jump(-1) end, {silent=true}) +vim.keymap.set( { "i", "s" }, "<C-E>", function() + if ls.choice_active() then + ls.change_choice(1) + end +end, {silent=true}) + +ls.add_snippets( + "go", { + s("echo", { t("fmt.Println("), i(1), t(")"), i(2) }), + s("echof", { t("fmt.Printf(\"%v\\n\", "), i(1), t(")"), i(2) }), + s("log", { t("fmt.Println("), i(1), t(")"), i(2) }), + s("logf", { t("fmt.Printf(\"%v\\n\", "), i(1), t(")"), i(2) }), + }) diff --git a/mut/neovim/lua/my/packages/oil.lua b/mut/neovim/lua/my/packages/oil.lua new file mode 100644 index 0000000..fa66001 --- /dev/null +++ b/mut/neovim/lua/my/packages/oil.lua @@ -0,0 +1,75 @@ +local oil=require("oil") +local fzf=require("fzf-lua") +local map = vim.keymap.set +local unmap = vim.keymap.del + +oil.setup({ + default_file_explorer = true, + skip_confirm_for_simple_edits = true, + + columns = {"size","permissions"}, + view_options = { + show_hidden = false, + is_hidden_file = function(name, bufnr) + return vim.startswith(name, ".") + end, + is_always_hidden = function(name, bufnr) return false end, + sort = { {"type" ,"asc"}, {"name","asc"} } + }, + + + keymaps = { + ["g?"] = "actions.show_help", + ["<CR>"] = "actions.select", + ["<C-s>"] = function() + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes( + ":Sh<up><c-f>", + true, false, true + ), + "n", false + ) + vim.schedule(function() + vim.cmd("let v:searchforward = 0") + map("n","/","/Sh.*",{buffer=true}) + map("n","?","?Sh.*",{buffer=true}) + end) + end, + [ "<C-h>" ] = "actions.select_split", + [ "<C-t>" ] = "actions.select_tab", + [ "<C-p>" ] = fzf.files, + [ "<C-c>" ] = "actions.close", + [ "<C-l>" ] = "actions.refresh", + [ "." ] = "actions.open_cmdline", + [ "gx" ] = { + callback = function() + local file, dir = oil.get_cursor_entry(), oil.get_current_dir() + if dir and file then + vim.cmd("argadd " .. dir .. file.name) + vim.cmd "args" + end + end + }, + [ "gX" ] = { + callback = function() + local file, dir = oil.get_cursor_entry(), oil.get_current_dir() + if dir and file then + vim.cmd("argdel " .. dir .. file.name) + vim.cmd "args" + end + end + }, + [ "gc" ] = { + callback = function() + vim.cmd("argdel *") + vim.cmd("args") + end + }, + [ "-" ] = "actions.parent", + [ "_" ] = "actions.open_cwd", + [ "cd" ] = "actions.cd", + [ "~" ] = "actions.tcd", + [ "gs" ] = "actions.change_sort", + [ "g." ] = "actions.toggle_hidden" + } +}) diff --git a/mut/neovim/lua/my/settings.lua b/mut/neovim/lua/my/settings.lua new file mode 100644 index 0000000..f08bf16 --- /dev/null +++ b/mut/neovim/lua/my/settings.lua @@ -0,0 +1,68 @@ +vim.g.codeium_enabled = false +vim.g.loaded_2html_plugin = false +vim.g.loaded_fzf = false +vim.g.loaded_health = false +vim.g.loaded_matchit = false +vim.g.loaded_matchparen = nil +vim.g.loaded_netrwPlugin = false +vim.g.loaded_rplugin = false +vim.g.loaded_shada = false +vim.g.loaded_tohtml = false +vim.g.loaded_tutor = false + +vim.g.zoxide_use_select = true +vim.g.zoxide_hook = "pwd" +vim.g.mapleader = " " +vim.g.maplocalleader = " " +vim.g.dirvish_mode = ":sort | sort ,^.*[^/]$, r" + +vim.opt.grepprg = "rg --vimgrep" +vim.opt.grepformat = "%f:%l:%c:%m" +vim.opt.shortmess:append("c") +vim.opt.diffopt:append("vertical") +vim.opt.isfname:append("@-@") +vim.opt.wmw = 10 +vim.opt.inccommand = "split" +vim.opt.signcolumn = "yes" +vim.opt.smd = false +vim.opt.scrolloff = 8 +vim.opt.termguicolors = true +vim.opt.incsearch = true +vim.opt.undofile = true +vim.opt.undodir = vim.fn.expand("~/.local/share/nvim/undo") +vim.opt.backup = false +vim.opt.backupcopy = "yes" +vim.opt.swapfile = false +vim.opt.wrap = false +vim.opt.splitbelow = true +vim.opt.magic = true +vim.opt.showbreak = "+++" +-- vim.opt.; listchars {:eol ""} +vim.opt.list = true +vim.opt.autoread = true +vim.opt.autoindent = true +vim.opt.smartindent = true +vim.opt.expandtab = true +vim.opt.tabstop = 4 +vim.opt.softtabstop = 4 +vim.opt.shiftwidth = 4 +vim.opt.hidden = true +vim.opt.number = true +vim.opt.relativenumber = true +vim.opt.exrc = true +vim.opt.secure = true +-- vim.opt.; completeopt "menu,longest,preview" +vim.opt.wmnu = true +vim.opt.wop = "pum" +-- vim.opt.; wildmode "list:longest" +vim.opt.complete = ".,w,k,kspell,b" +vim.opt.foldopen = "block,hor,jump,mark,percent,quickfix,search,tag" +vim.opt.laststatus = 3 +-- vim.opt.; winbar "%=%m %f" +vim.opt.winbar = "" +vim.opt.hlsearch = false +vim.opt.showtabline = 1 +vim.opt.cmdheight = 1 + +vim.opt.shellpipe = "out+err>" +vim.opt.shell = "sh" diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.cargo/config.toml b/mut/neovim/pack/plugins/start/blink.cmp/.cargo/config.toml new file mode 100644 index 0000000..88bd7e3 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.cargo/config.toml @@ -0,0 +1,27 @@ +[target.x86_64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] + +[target.aarch64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] + +[target.x86_64-unknown-linux-musl] +rustflags = ["-C", "target-feature=-crt-static"] + +[target.aarch64-unknown-linux-musl] +rustflags = ["-C", "target-feature=-crt-static"] + +[target.aarch64-linux-android] +rustflags = [ + "-C", + "linker=aarch64-linux-android-clang", + "-C", + "link-args=-rdynamic", + "-C", + "default-linker-libraries", +] diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/bug_report.yml b/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..b08076b --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,42 @@ +name: Bug Report +description: File a bug report +labels: ["bug"] +body: + - type: checkboxes + id: checklist + attributes: + label: Make sure you have done the following + options: + - label: Updated to the latest version of `blink.cmp` + required: true + - label: Searched for existing issues and documentation (try `<C-k>` on https://cmp.saghen.dev) + required: true + - type: textarea + id: bug-description + attributes: + label: Bug Description + description: If the issue may be related to your configuration, please include a [repro.lua](https://github.com/Saghen/blink.cmp/blob/main/repro.lua) + validations: { required: true } + - type: textarea + id: user-config + attributes: + label: Relevant configuration + description: Copypaste the part of the config relevant to the bug. Do not paste the entire default config. + render: lua + placeholder: | + sources = { + default = { 'lsp', 'path', 'snippets', 'buffer' }, + }, + validations: { required: false } + - type: input + id: version-info + attributes: + label: "`neovim` version" + placeholder: "output of `nvim --version`" + validations: { required: true } + - type: input + id: branch-or-tag + attributes: + label: "`blink.cmp` version" + placeholder: "examples: main, d2b411c or v0.9.2" + validations: { required: true } diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/config.yml b/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/feature_request.yml b/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..7ecff7c --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,10 @@ +name: Feature request +description: Suggest an idea +labels: ["feature"] +body: + - type: textarea + id: feature-description + attributes: + label: Feature Description + description: A clear and concise description of what the feature is. + validations: { required: true } diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.github/workflows/nix.yaml b/mut/neovim/pack/plugins/start/blink.cmp/.github/workflows/nix.yaml new file mode 100644 index 0000000..54086f1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.github/workflows/nix.yaml @@ -0,0 +1,38 @@ +name: Test Nix + +on: + push: + pull_request: + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + name: Test Nix Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + with: + extra-conf: | + accept-flake-config = true + + - uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Check the flake + run: nix flake check + + - name: Build devshell + run: nix develop --command "rustc" + + - name: Build the library + run: nix build .#blink-fuzzy-lib + + - name: Build the plugin in nix + run: nix build .#blink-cmp + + - name: Build the library (outside nix) + run: nix run .#build-plugin diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.github/workflows/release.yaml b/mut/neovim/pack/plugins/start/blink.cmp/.github/workflows/release.yaml new file mode 100644 index 0000000..b2d643d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.github/workflows/release.yaml @@ -0,0 +1,130 @@ +name: Release + +on: + push: + tags: + - "v*" + +jobs: + build: + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.os }} + permissions: + contents: read + strategy: + matrix: + include: + ## Linux builds + # Glibc 2.31 + - os: ubuntu-20.04 + target: x86_64-unknown-linux-gnu + artifact_name: target/x86_64-unknown-linux-gnu/release/libblink_cmp_fuzzy.so + - os: ubuntu-20.04 + target: aarch64-unknown-linux-gnu + artifact_name: target/aarch64-unknown-linux-gnu/release/libblink_cmp_fuzzy.so + # Musl 1.2.3 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + artifact_name: target/x86_64-unknown-linux-musl/release/libblink_cmp_fuzzy.so + - os: ubuntu-latest + target: aarch64-unknown-linux-musl + artifact_name: target/aarch64-unknown-linux-musl/release/libblink_cmp_fuzzy.so + # Android(Termux) + - os: ubuntu-latest + target: aarch64-linux-android + artifact_name: target/aarch64-linux-android/release/libblink_cmp_fuzzy.so + + ## macOS builds + - os: macos-latest + target: x86_64-apple-darwin + artifact_name: target/x86_64-apple-darwin/release/libblink_cmp_fuzzy.dylib + - os: macos-latest + target: aarch64-apple-darwin + artifact_name: target/aarch64-apple-darwin/release/libblink_cmp_fuzzy.dylib + + ## Windows builds + - os: windows-latest + target: x86_64-pc-windows-msvc + artifact_name: target/x86_64-pc-windows-msvc/release/blink_cmp_fuzzy.dll + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + run: | + rustup toolchain install nightly + rustup default nightly + rustup target add ${{ matrix.target }} + + - name: Build for Linux + if: contains(matrix.os, 'ubuntu') + run: | + cargo install cross --git https://github.com/cross-rs/cross + cross build --release --target ${{ matrix.target }} + mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.so" + + - name: Build for macOS + if: contains(matrix.os, 'macos') + run: | + # Ventura (https://en.wikipedia.org/wiki/MacOS_version_history#Releases) + MACOSX_DEPLOYMENT_TARGET="13" cargo build --release --target ${{ matrix.target }} + mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.dylib" + + - name: Build for Windows + if: contains(matrix.os, 'windows') + run: | + cargo build --release --target ${{ matrix.target }} + mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.dll" + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.target }} + path: ${{ matrix.target }}.* + + release: + name: Release + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + + - name: Generate checksums + run: | + for file in ./**/*; do + sha256sum "$file" > "${file}.sha256" + done + + - name: Upload Release Assets + uses: softprops/action-gh-release@v2 + with: + name: ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + token: ${{ github.token }} + files: ./**/* + draft: false + prerelease: false + generate_release_notes: true + + deploy-docs: + name: Deploy docs + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + + - name: Build + run: npm ci && npm run build:release + working-directory: docs + + - name: Deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy docs/.vitepress/dist --project-name=blink-cmp diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.gitignore b/mut/neovim/pack/plugins/start/blink.cmp/.gitignore new file mode 100644 index 0000000..ab9ecd3 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.gitignore @@ -0,0 +1,9 @@ +target/ +.archive.lua +_*.lua +.lazy.lua +dual/ +result +.direnv +.devenv +.repro/ diff --git a/mut/neovim/pack/plugins/start/blink.cmp/.stylua.toml b/mut/neovim/pack/plugins/start/blink.cmp/.stylua.toml new file mode 100644 index 0000000..6d75cf6 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/.stylua.toml @@ -0,0 +1,7 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferSingle" +call_parentheses = "Always" +collapse_simple_statement = "Always" diff --git a/mut/neovim/pack/plugins/start/blink.cmp/CHANGELOG.md b/mut/neovim/pack/plugins/start/blink.cmp/CHANGELOG.md new file mode 100644 index 0000000..e85bb7d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/CHANGELOG.md @@ -0,0 +1,804 @@ +## [0.10.0](https://github.com/Saghen/blink.cmp/compare/v0.9.3...v0.10.0) (2025-01-08) + +### BREAKING CHANGES + +* mini.snippets and snippets presets (#877) +* support `preselect` with `auto_insert`, set as default + +### Features + +* add `get_selected_item` public function ([9e1e7e6](https://github.com/Saghen/blink.cmp/commit/9e1e7e604e3419fa0777a2b747ded74d35013c06)) +* mini.snippets and snippets presets ([#877](https://github.com/Saghen/blink.cmp/issues/877)) ([854ab87](https://github.com/Saghen/blink.cmp/commit/854ab87aefdac2b757d97595f98673d64f1878bc)) +* set default capabilities on 0.11 ([#897](https://github.com/Saghen/blink.cmp/issues/897)) ([af1febb](https://github.com/Saghen/blink.cmp/commit/af1febb17f9ddc87cf73e69d3f61218cdc18ed85)) +* support `preselect` with `auto_insert`, set as default ([8126d0e](https://github.com/Saghen/blink.cmp/commit/8126d0e6a2a0e62d3872d718c3d50313f9f7fe3a)), closes [#668](https://github.com/Saghen/blink.cmp/issues/668) + +### Bug Fixes + +* `get_char_at_cursor` attempting to get char on empty line ([7d6bf9a](https://github.com/Saghen/blink.cmp/commit/7d6bf9adea67a200067effe5ef589515e71230c8)), closes [#926](https://github.com/Saghen/blink.cmp/issues/926) +* `within_query_bounds` including 1 position after bounds ([36ba8eb](https://github.com/Saghen/blink.cmp/commit/36ba8eb9c166c21d6d2a8b5f88f9c55d1966b383)), closes [#890](https://github.com/Saghen/blink.cmp/issues/890) [#875](https://github.com/Saghen/blink.cmp/issues/875) +* assert vim.lsp.config fn exists before calling ([#927](https://github.com/Saghen/blink.cmp/issues/927)) ([47efef8](https://github.com/Saghen/blink.cmp/commit/47efef83802b26bd2ff7193b24af4c7f747dc145)) +* buildVimPlugin ([#933](https://github.com/Saghen/blink.cmp/issues/933)) ([3f5dcbc](https://github.com/Saghen/blink.cmp/commit/3f5dcbc1c28edd2ab31b9bac27cc63de4e56b87c)) +* clear context on ignored cursor moved when not on keyword ([0f8de3a](https://github.com/Saghen/blink.cmp/commit/0f8de3abd560f38415d71fc6ee9885c2bf53b814)), closes [#937](https://github.com/Saghen/blink.cmp/issues/937) +* ignore cursor moved when cursor equal before vs after ([17eea33](https://github.com/Saghen/blink.cmp/commit/17eea330a5d111f3cd67f59bb3832cc78f55db14)) +* **signature:** use `char_under_cursor` in `on_char_added` handler ([#935](https://github.com/Saghen/blink.cmp/issues/935)) ([275d407](https://github.com/Saghen/blink.cmp/commit/275d40713191e6c0012783ecf762a4faa138098b)), closes [#909](https://github.com/Saghen/blink.cmp/issues/909) + +## [0.9.3](https://github.com/Saghen/blink.cmp/compare/v0.9.2...v0.9.3) (2025-01-06) + +### Features + +* add plaintex, tex and context brackets ([9ffdb7b](https://github.com/Saghen/blink.cmp/commit/9ffdb7b71d0ee9abcccb61d3b8fb60defc4d47ff)) +* **path:** replace `/` in front of cursor on directory ([d2b411c](https://github.com/Saghen/blink.cmp/commit/d2b411ca2ec894ccab9d7dc0bd506e44920983ef)) + +### Bug Fixes + +* add .repro to gitignore ([0d1e3c3](https://github.com/Saghen/blink.cmp/commit/0d1e3c34b172bf93380f8675ec962c301f2b5aaa)) +* cmdline completion new text not including prefix ([bc480aa](https://github.com/Saghen/blink.cmp/commit/bc480aa927ef4afbf5431f566e8aea7458e9f8df)), closes [#883](https://github.com/Saghen/blink.cmp/issues/883) +* ignore buffer local treesitter option ([d704244](https://github.com/Saghen/blink.cmp/commit/d704244327c1bc1fdd9c0218fe4fff04ca78d3c0)), closes [#913](https://github.com/Saghen/blink.cmp/issues/913) +* ignore non-key char in cmdline completion ([cc0e632](https://github.com/Saghen/blink.cmp/commit/cc0e6329e7603b5749c7fe98a76e39ed17bab860)), closes [#893](https://github.com/Saghen/blink.cmp/issues/893) +* **nix:** use native gcc on macos ([3ab6832](https://github.com/Saghen/blink.cmp/commit/3ab6832b2fc3e49aad9c984089cfc0c5ec788531)), closes [#652](https://github.com/Saghen/blink.cmp/issues/652) +* **nix:** use nix gcc and provide libiconv ([#916](https://github.com/Saghen/blink.cmp/issues/916)) ([5d2d601](https://github.com/Saghen/blink.cmp/commit/5d2d6010d9a5376f9073c1182887e547e3c0ec17)) + +## [0.9.2](https://github.com/Saghen/blink.cmp/compare/v0.9.1...v0.9.2) (2025-01-03) + +### Bug Fixes + +* unicode range when checking if char is keyword ([100d3c8](https://github.com/Saghen/blink.cmp/commit/100d3c8bfc8059c2fd2347d00ab70ee91c7ff3ca)), closes [#878](https://github.com/Saghen/blink.cmp/issues/878) + +## [0.9.1](https://github.com/Saghen/blink.cmp/compare/v0.9.0...v0.9.1) (2025-01-03) + +### Features + +* ignore global min_keyword_length for manual trigger ([56f5d31](https://github.com/Saghen/blink.cmp/commit/56f5d314f772617b506d92e46b8e946535edc04e)), closes [#643](https://github.com/Saghen/blink.cmp/issues/643) +* **nix:** add formatter ([#867](https://github.com/Saghen/blink.cmp/issues/867)) ([a0274b1](https://github.com/Saghen/blink.cmp/commit/a0274b10f04ea625b602f6383e3cb2fc38dcfd71)), closes [#736](https://github.com/Saghen/blink.cmp/issues/736) +* normalize search paths ([8a64275](https://github.com/Saghen/blink.cmp/commit/8a64275948cead4de55cd78c7dc74b2c6465605e)), closes [#835](https://github.com/Saghen/blink.cmp/issues/835) +* smarter edit/fuzzy range guessing ([768bcc0](https://github.com/Saghen/blink.cmp/commit/768bcc08282919168cd9bdf29aa8fcbf968fc457)), closes [#46](https://github.com/Saghen/blink.cmp/issues/46) +* sort cmdline items starting with special characters last ([ae3bf0d](https://github.com/Saghen/blink.cmp/commit/ae3bf0d51902df20121378da2ee6893bcc92fa63)), closes [#818](https://github.com/Saghen/blink.cmp/issues/818) +* support custom/customlist cmdline completions directly ([7e7deaa](https://github.com/Saghen/blink.cmp/commit/7e7deaa8bfa578d147e2d1f04a3373fac2afd58f)), closes [#849](https://github.com/Saghen/blink.cmp/issues/849) + +### Bug Fixes + +* column alignment off by 1 when bounds length == 0 ([0d162bd](https://github.com/Saghen/blink.cmp/commit/0d162bd1b0bbd80a1b5a2dc23d98249d4f8c28f6)) +* get full unicode char at cursor position ([e831cab](https://github.com/Saghen/blink.cmp/commit/e831cab7a4c31da02c72044190e9afc1a9ed584c)), closes [#864](https://github.com/Saghen/blink.cmp/issues/864) +* hyphen not being considered a keyword ([8ca8ca4](https://github.com/Saghen/blink.cmp/commit/8ca8ca444e0801411e077cdee655e5efa3f77b36)), closes [#866](https://github.com/Saghen/blink.cmp/issues/866) +* ignore non custom/customlist completion types ([f7857fc](https://github.com/Saghen/blink.cmp/commit/f7857fcb98e52899eb06f07ecb972a430d0de6e0)), closes [#849](https://github.com/Saghen/blink.cmp/issues/849) +* keyword range not being respected for fuzzy matching ([4cc4e37](https://github.com/Saghen/blink.cmp/commit/4cc4e37dd39eec683a9e1a82e71cd1791bda7761)) +* path provider not respecting trailing_slash=false ([#862](https://github.com/Saghen/blink.cmp/issues/862)) ([0ff2ed5](https://github.com/Saghen/blink.cmp/commit/0ff2ed566e753844825cd8d2483933861cea55ff)) +* set undolevels to force undo point ([4c63b4e](https://github.com/Saghen/blink.cmp/commit/4c63b4e29738268950911bb0c70ffaaba26b53d7)), closes [#852](https://github.com/Saghen/blink.cmp/issues/852) +* use tmp file for downloading to prevent crash on mac on update ([84e065b](https://github.com/Saghen/blink.cmp/commit/84e065bef1504076a0cc3f75f9867b9bce6f328b)), closes [#68](https://github.com/Saghen/blink.cmp/issues/68) +* window direction sorting on Windows ([#846](https://github.com/Saghen/blink.cmp/issues/846)) ([00ad008](https://github.com/Saghen/blink.cmp/commit/00ad008cbea4d0d2b5880e7c7386caa9fc4e5e2b)) + +### Performance Improvements + +* use faster 0.11 vim.validate ([#868](https://github.com/Saghen/blink.cmp/issues/868)) ([a8957ba](https://github.com/Saghen/blink.cmp/commit/a8957bab8faad4436e7ad62244c39335b95450a4)) + +## [0.9.0](https://github.com/Saghen/blink.cmp/compare/v0.8.2...v0.9.0) (2024-12-31) + +### BREAKING CHANGES + +* rename `BlinkCmpCompletionMenu*` autocmds to `BlinkCmpMenu*` +* set default documentation max_width to 80 +* rename `align_to_component` to `align_to`, add `cursor` option + +### Features + +* add back support for showing when moving onto trigger character ([cf9cc6e](https://github.com/Saghen/blink.cmp/commit/cf9cc6e43edd2718294ef9801a223c463f50a4ce)), closes [#780](https://github.com/Saghen/blink.cmp/issues/780) [#745](https://github.com/Saghen/blink.cmp/issues/745) +* add callback option to cmp.show ([33b82e5](https://github.com/Saghen/blink.cmp/commit/33b82e5832757319c485ab45c0db4ace554e3183)), closes [#806](https://github.com/Saghen/blink.cmp/issues/806) +* add callback to hide/cancel, rework show callback ([73a5f4e](https://github.com/Saghen/blink.cmp/commit/73a5f4e387ade764a833d290dbb5da77b0d84b4c)), closes [#806](https://github.com/Saghen/blink.cmp/issues/806) +* add type annotation for keymap function params ([#829](https://github.com/Saghen/blink.cmp/issues/829)) ([3d7e773](https://github.com/Saghen/blink.cmp/commit/3d7e773d3e8a02720b23f58ffee631a0c1e2e1d1)) +* escape filenames in cmdline ([e53db6a](https://github.com/Saghen/blink.cmp/commit/e53db6a53f85b1c0d56eed66811bfbac520abd6c)), closes [#751](https://github.com/Saghen/blink.cmp/issues/751) +* **nix:** use Cargo.lock instead of hash ([#773](https://github.com/Saghen/blink.cmp/issues/773)) ([d9513ee](https://github.com/Saghen/blink.cmp/commit/d9513ee9f8b111a46e262be2b36172ca335051a2)) +* **nix:** use filesets ([#772](https://github.com/Saghen/blink.cmp/issues/772)) ([e524347](https://github.com/Saghen/blink.cmp/commit/e524347697b6664870536dfcdd17e3ab56177b99)) +* rename `align_to_component` to `align_to`, add `cursor` option ([9387c75](https://github.com/Saghen/blink.cmp/commit/9387c75af7f8ec1495f4ed5a35cd29f054647dfc)), closes [#344](https://github.com/Saghen/blink.cmp/issues/344) +* rename `BlinkCmpCompletionMenu*` autocmds to `BlinkCmpMenu*` ([fa4312c](https://github.com/Saghen/blink.cmp/commit/fa4312c11f9ab102333f5a18f1a30af5ae636c04)) +* run callback for cmp.show, even if menu is open ([a1476d3](https://github.com/Saghen/blink.cmp/commit/a1476d3596f032be3f2d77630c8eee3951d3f74c)) +* set default documentation max_width to 80 ([1a61625](https://github.com/Saghen/blink.cmp/commit/1a61625ad2a25c4e1ffffacb4bd0826c244af88f)) +* support `@` mode for cmdline ([4c2744d](https://github.com/Saghen/blink.cmp/commit/4c2744d99a13c687e4995fe0a050f40c15dbb2d9)), closes [#696](https://github.com/Saghen/blink.cmp/issues/696) +* support configuring clipboard register for snippets ([8f51a4e](https://github.com/Saghen/blink.cmp/commit/8f51a4ec23773cc96ec6b3ca336a5d70eebb2fb2)), closes [#800](https://github.com/Saghen/blink.cmp/issues/800) +* support unsafe no lock for fuzzy matcher ([6f8da35](https://github.com/Saghen/blink.cmp/commit/6f8da35fc8f1f8046d25b88b7708178cb4126abe)), closes [#817](https://github.com/Saghen/blink.cmp/issues/817) +* support windows drives for path source ([98fded2](https://github.com/Saghen/blink.cmp/commit/98fded25d772a749cbf26e569e735ca7a3fb9d12)), closes [#612](https://github.com/Saghen/blink.cmp/issues/612) +* use filter text on non-prefixed test in cmdline ([8c194b6](https://github.com/Saghen/blink.cmp/commit/8c194b6fa34b174b2ab30ff1d005c6b1b03ba523)) + +### Bug Fixes + +* **accept/brackets:** respect `item.kind` when moving cursor ([#779](https://github.com/Saghen/blink.cmp/issues/779)) ([c54dfbf](https://github.com/Saghen/blink.cmp/commit/c54dfbfdfabac3b5a66ba90c89fab86d7651d106)) +* add missing regex file for path source ([1118d07](https://github.com/Saghen/blink.cmp/commit/1118d07c1b720873fe3498a662a265ae8a9a7ee4)), closes [#834](https://github.com/Saghen/blink.cmp/issues/834) +* alignment double offset on align_to ([24d6868](https://github.com/Saghen/blink.cmp/commit/24d6868d0a18bb02cbee7fc5cc2a09fa309e3eb7)) +* apply non-snippet detection to non-snippet kinds ([434ea2b](https://github.com/Saghen/blink.cmp/commit/434ea2b05c2bae0cff6249893c8324fa3a56d865)), closes [#790](https://github.com/Saghen/blink.cmp/issues/790) +* avoid namespace collision with vim.api.keyset.keymap ([63718e9](https://github.com/Saghen/blink.cmp/commit/63718e93d46f07b869f033fd13b78597ebbde72b)), closes [#767](https://github.com/Saghen/blink.cmp/issues/767) +* check enabled before showing trigger and on mapping ([e670720](https://github.com/Saghen/blink.cmp/commit/e6707202772be974ae2d54239b806707bb72ccdb)), closes [#716](https://github.com/Saghen/blink.cmp/issues/716) +* clamp text edit end character to start character, if lines equal ([6891bcb](https://github.com/Saghen/blink.cmp/commit/6891bcb06b6f21de68278991f29e53452b822d48)), closes [#634](https://github.com/Saghen/blink.cmp/issues/634) +* create target/release dir, if it doesn't exist ([4020c23](https://github.com/Saghen/blink.cmp/commit/4020c2353b906950cb80be2fc1fabee8a9a9c291)), closes [#819](https://github.com/Saghen/blink.cmp/issues/819) +* documentation losing syntax highlighting on doc reopen ([#768](https://github.com/Saghen/blink.cmp/issues/768)) ([ef59763](https://github.com/Saghen/blink.cmp/commit/ef59763c8a58fb1dedfb2d58a2ebd0fbe247f96c)), closes [#703](https://github.com/Saghen/blink.cmp/issues/703) +* don't prevent show() when ghost-text is visible ([#796](https://github.com/Saghen/blink.cmp/issues/796)) ([59d6b4f](https://github.com/Saghen/blink.cmp/commit/59d6b4fbe94cfc350b1392772a61cbcf942619c7)) +* filter help tags by arg prefix ([21da714](https://github.com/Saghen/blink.cmp/commit/21da71413bf749f21d2174c1cd7e8efa40809a93)), closes [#818](https://github.com/Saghen/blink.cmp/issues/818) +* flatten leaving empty tables ([#799](https://github.com/Saghen/blink.cmp/issues/799)) ([021216d](https://github.com/Saghen/blink.cmp/commit/021216da4683db5627d4b321dbde075aa771b5e7)) +* getcmdcompltype returning empty string ([eb9e651](https://github.com/Saghen/blink.cmp/commit/eb9e651bca40bbfb4de2a77a293a1e18bb373ee8)), closes [#696](https://github.com/Saghen/blink.cmp/issues/696) +* remove redundant is enabled check ([f4add54](https://github.com/Saghen/blink.cmp/commit/f4add54f999962e6385d42bad341366b85184217)) +* return incomplete on err/nil from lsp ([1ef9bb9](https://github.com/Saghen/blink.cmp/commit/1ef9bb97740e7b55401e213da5dd6b04b77e56ff)), closes [#719](https://github.com/Saghen/blink.cmp/issues/719) +* set default details to empty array ([0350fee](https://github.com/Saghen/blink.cmp/commit/0350feedfa8adb07b6750f6d9150c26e13eae0d2)) +* trigger context initial_kind resetting ([3ef27bc](https://github.com/Saghen/blink.cmp/commit/3ef27bcd7ff2367c6053421d4a8981bedc33d53e)), closes [#803](https://github.com/Saghen/blink.cmp/issues/803) +* use correct regex for filenames ([8df826f](https://github.com/Saghen/blink.cmp/commit/8df826f168f102d0fbea92cbd85995ce66a821c7)), closes [#761](https://github.com/Saghen/blink.cmp/issues/761) +* use existing arg prefix for help filtering in cmdline ([c593e83](https://github.com/Saghen/blink.cmp/commit/c593e8385d9f8f82a6e108fbabcd1f64fce72684)) +* wait for all LSPs to respond before showing ([86a13ae](https://github.com/Saghen/blink.cmp/commit/86a13aeb104d6ea782557518ee1a350712df7bd7)), closes [#691](https://github.com/Saghen/blink.cmp/issues/691) + +## [0.8.2](https://github.com/Saghen/blink.cmp/compare/v0.8.1...v0.8.2) (2024-12-23) + +### Features + +* improve auto_show flexibility ([#697](https://github.com/Saghen/blink.cmp/issues/697)) ([a937edd](https://github.com/Saghen/blink.cmp/commit/a937edde979a8ff140779fa0d425af566bc73cb7)) +* improve error messages for pre built binaries ([c36b60c](https://github.com/Saghen/blink.cmp/commit/c36b60c22f7357d741c9166e4d509b745cc8b441)) +* sort cmdline completions case insensitive ([b68e924](https://github.com/Saghen/blink.cmp/commit/b68e92426af46d60f08a4d2f58ed1e44d4e56087)), closes [#715](https://github.com/Saghen/blink.cmp/issues/715) +* support dynamic selection mode ([c1017f0](https://github.com/Saghen/blink.cmp/commit/c1017f0a827736e3397f9b60dfe8e8ebb4a0ae72)) + +### Bug Fixes + +* add git to nix build dependencies and shell ([ed1d4f5](https://github.com/Saghen/blink.cmp/commit/ed1d4f573f8988353d6e437f5e70ee334ea099fe)) +* add java to blocked filetypes for semantic token auto_brackets ([#729](https://github.com/Saghen/blink.cmp/issues/729)) ([140ed36](https://github.com/Saghen/blink.cmp/commit/140ed3633419965e8f2228af0d5fbaa4c1956f78)) +* add missing git.lua for downloader ([f7bef25](https://github.com/Saghen/blink.cmp/commit/f7bef25052820d4d7604a296c739ba9d885117f8)) +* auto_show function logic ([#707](https://github.com/Saghen/blink.cmp/issues/707)) ([4ef6d1e](https://github.com/Saghen/blink.cmp/commit/4ef6d1ee29e8ae9138a47bba9374b7c0c97452b6)), closes [#697](https://github.com/Saghen/blink.cmp/issues/697) +* check version sha of locally built, better detection ([3ffd31d](https://github.com/Saghen/blink.cmp/commit/3ffd31d0c52a51d064f4761d5c0bfad64129c1e9)), closes [#68](https://github.com/Saghen/blink.cmp/issues/68) +* doc scrollbar render ([#724](https://github.com/Saghen/blink.cmp/issues/724)) ([8f71ccb](https://github.com/Saghen/blink.cmp/commit/8f71ccbe668860a4ebcaed3928d80d2119559ad9)) +* inherit package.cpath in worker thread ([#726](https://github.com/Saghen/blink.cmp/issues/726)) ([b6c7762](https://github.com/Saghen/blink.cmp/commit/b6c7762407b6c4521b46244f35fab05cfd1c6863)), closes [#725](https://github.com/Saghen/blink.cmp/issues/725) +* **notifications:** add title to notifications ([#722](https://github.com/Saghen/blink.cmp/issues/722)) ([f93af0f](https://github.com/Saghen/blink.cmp/commit/f93af0f486ada13e8c34f42c911788b9232b811f)) +* prebuilt binary error message always firing ([cab0e8e](https://github.com/Saghen/blink.cmp/commit/cab0e8e169a2c595018f9fdb981e056094bd5aeb)) + +## [0.8.1](https://github.com/Saghen/blink.cmp/compare/v0.8.0...v0.8.1) (2024-12-21) + +### Features + +* **path:** sort directories first, then by name lowercase ([400de65](https://github.com/Saghen/blink.cmp/commit/400de65da795b5939ace36978de3d1edeb84b0de)) + +### Bug Fixes + +* checkhealth after checksum changes ([d8ffbe9](https://github.com/Saghen/blink.cmp/commit/d8ffbe95190a776c6a28c86650efcbc23c5f6521)), closes [#669](https://github.com/Saghen/blink.cmp/issues/669) +* duplicate cursor moved event firing ([e360828](https://github.com/Saghen/blink.cmp/commit/e360828a188dc30658067eac63feded08857c076)) +* get global mapping for fallback in cmdline mode ([92da013](https://github.com/Saghen/blink.cmp/commit/92da0133b240e60100fcb04b32fcd7270f765d94)), closes [#674](https://github.com/Saghen/blink.cmp/issues/674) +* internal types for config not using strict config ([bdece4e](https://github.com/Saghen/blink.cmp/commit/bdece4e90e70baee956e2351220527a619d25052)) +* **path:** no items when file fails stat ([4218120](https://github.com/Saghen/blink.cmp/commit/421812086661bba3aa318030eee12719fc5da072)), closes [#688](https://github.com/Saghen/blink.cmp/issues/688) +* type signature for enabled indicating ctx could be passed ([3cb7208](https://github.com/Saghen/blink.cmp/commit/3cb7208546b4e1f0c5e492cbcfccd083a1c89351)), closes [#695](https://github.com/Saghen/blink.cmp/issues/695) +* use context.get_line() when getting preview undo text edit ([0f92fb8](https://github.com/Saghen/blink.cmp/commit/0f92fb8dcff634e880a60e266f041dfe175b82bf)), closes [#702](https://github.com/Saghen/blink.cmp/issues/702) +* wrong key upstreamed by cmdline_events ([4757317](https://github.com/Saghen/blink.cmp/commit/475731741bbd8266767d48ad46b63f715577ac8e)), closes [#700](https://github.com/Saghen/blink.cmp/issues/700) + +## [0.8.0](https://github.com/Saghen/blink.cmp/compare/v0.7.6...v0.8.0) (2024-12-20) + +> [!IMPORTANT] +> `sources.completion.enabled_providers` has been moved to `sources.default` + +### Highlights + +* Cmdline completions! ([#323](https://github.com/Saghen/blink.cmp/issues/323)) +* Sorting now respects LSP hints more directly and doesn't sort alphabetically or by kind by default +* Sources v2 ([#465](https://github.com/Saghen/blink.cmp/issues/465)), adds support for async sources, timeouts, smarter fallbacks, adding sources at runtime and more! + +### Features + +* `extra_curl_args` option for prebuilt binaries download ([4c2e9e7](https://github.com/Saghen/blink.cmp/commit/4c2e9e74905502e3662fbd4af7b0d1b680971a04)), closes [#481](https://github.com/Saghen/blink.cmp/issues/481) +* add [ to show_on_x_blocked_trigger_characters ([#632](https://github.com/Saghen/blink.cmp/issues/632)) ([046a2af](https://github.com/Saghen/blink.cmp/commit/046a2af7580ba90cda9ffebbab3f1fe68ca1fa59)) +* add `{` to `show_on_x_blocked_trigger_characters` ([712bd30](https://github.com/Saghen/blink.cmp/commit/712bd301fc2158e6443144ff9c8ce01b8bf5a77b)), closes [#597](https://github.com/Saghen/blink.cmp/issues/597) +* add global transform_items and min_keyword_length ([e07cb27](https://github.com/Saghen/blink.cmp/commit/e07cb2756d5cc339dfa4bf4d9bc91b3779dbb743)), closes [#502](https://github.com/Saghen/blink.cmp/issues/502) [#504](https://github.com/Saghen/blink.cmp/issues/504) +* allow providers customize documentation rendering ([#650](https://github.com/Saghen/blink.cmp/issues/650)) ([bc94c75](https://github.com/Saghen/blink.cmp/commit/bc94c7508379b4828206759562162ce10af82b68)) +* cmdline completions ([#323](https://github.com/Saghen/blink.cmp/issues/323)) ([414d615](https://github.com/Saghen/blink.cmp/commit/414d615afcd9268522160dca5855fef8132f6e9e)) +* **cmdline:** allow configuring separate cmdline preset ([#532](https://github.com/Saghen/blink.cmp/issues/532)) ([13b3e57](https://github.com/Saghen/blink.cmp/commit/13b3e572e863bafe8fad3a97473271a2c9c700ce)) +* **config:** add partial types for each config option ([#549](https://github.com/Saghen/blink.cmp/issues/549)) ([c3bba64](https://github.com/Saghen/blink.cmp/commit/c3bba64d6c32adf4156e9d1273b14494838a3058)), closes [#427](https://github.com/Saghen/blink.cmp/issues/427) +* **config:** allow plugins to disable blink for some buffers ([#556](https://github.com/Saghen/blink.cmp/issues/556)) ([c8e86a3](https://github.com/Saghen/blink.cmp/commit/c8e86a3ed1eff07e2c1108779a720d3b4c6b86a7)) +* demote snippets from LSP explicitly ([b7c84ac](https://github.com/Saghen/blink.cmp/commit/b7c84ac4e17f3e160a626a9a89b609e02151a135)) +* disable keymaps when no cmdline sources are defined ([88ec601](https://github.com/Saghen/blink.cmp/commit/88ec6010ddbb249257d22265b8a96842f10c7142)) +* enable auto-brackets by default ([4d099ee](https://github.com/Saghen/blink.cmp/commit/4d099eeb72cfbd6496376fbb3265a1887a7c85fe)) +* enable treesiter highlight in menu per source ([#526](https://github.com/Saghen/blink.cmp/issues/526)) ([f99b03c](https://github.com/Saghen/blink.cmp/commit/f99b03c756b32680eea28e89f95e3c6987cc6c80)), closes [#438](https://github.com/Saghen/blink.cmp/issues/438) +* ensure nvim 0.10+ on startup ([30a4a52](https://github.com/Saghen/blink.cmp/commit/30a4a52d2362e3a272ae1cf28552852ae09b38a9)) +* expose `cmp.is_visible()` api ([2c826d9](https://github.com/Saghen/blink.cmp/commit/2c826d9167c7236f6079790bc35bcb024021e683)), closes [#535](https://github.com/Saghen/blink.cmp/issues/535) +* filter out LSP text items by default ([814392a](https://github.com/Saghen/blink.cmp/commit/814392a7164336fe5fbd6d4b97a69dce9eb6e4ef)) +* honor extended luasnip filetypes and cache each ([#625](https://github.com/Saghen/blink.cmp/issues/625)) ([c3ef922](https://github.com/Saghen/blink.cmp/commit/c3ef9223a69ededed611b3ef617bac5651b87833)) +* ignore when source defining trigger character returns no items ([684950d](https://github.com/Saghen/blink.cmp/commit/684950d3c4027e10a46f4bd478182839760b8fde)), closes [#597](https://github.com/Saghen/blink.cmp/issues/597) +* include ghost text in is_visible ([1006662](https://github.com/Saghen/blink.cmp/commit/1006662ad53c92adf9ae6f2d05cee38f613d08ff)) +* increase max length of buffer entry to 512 characters ([4ab0860](https://github.com/Saghen/blink.cmp/commit/4ab0860d361234e714d3beac2828d215f3f481e1)), closes [#478](https://github.com/Saghen/blink.cmp/issues/478) +* merge resolved item with item ([7a83acf](https://github.com/Saghen/blink.cmp/commit/7a83acf5b3cba829b07a05009866548c8e948ac0)), closes [#553](https://github.com/Saghen/blink.cmp/issues/553) +* reset whole luasnip cache on snippets added ([bff6c0f](https://github.com/Saghen/blink.cmp/commit/bff6c0f06bdc1114c5816b0f6b19ad6a7e15a638)) +* resolve help tags ourselves in cmdline ([02051bf](https://github.com/Saghen/blink.cmp/commit/02051bf2d9c8f116680659f091b510598a4aea38)), closes [#631](https://github.com/Saghen/blink.cmp/issues/631) +* rework cmdline source ([8f718cc](https://github.com/Saghen/blink.cmp/commit/8f718cc0d845348fd19c964aa6a82b06ea49c210)) +* rework download logic with checksums ([#629](https://github.com/Saghen/blink.cmp/issues/629)) ([53d22cb](https://github.com/Saghen/blink.cmp/commit/53d22cbac470b5ed8bfa2c3c195b82e03b501629)) +* set cursor position for additional text edits ([f0ab5e5](https://github.com/Saghen/blink.cmp/commit/f0ab5e504b160d4bc60f52a02e8d2453052420d3)), closes [#223](https://github.com/Saghen/blink.cmp/issues/223) +* set path to fallback to buffer by default ([c9594d5](https://github.com/Saghen/blink.cmp/commit/c9594d5682ca421ee1bcb4284329f2d7dde71b50)) +* sort on score and sort_text only by default, disable frecency and proximity on no keyword ([76230d5](https://github.com/Saghen/blink.cmp/commit/76230d5a4a02cd1db8dec33b6eed0b4bc2dcbc53)), closes [#570](https://github.com/Saghen/blink.cmp/issues/570) +* sources v2 ([#465](https://github.com/Saghen/blink.cmp/issues/465)) ([533608f](https://github.com/Saghen/blink.cmp/commit/533608f56b912aba98250a3c1501ee687d7cf5eb)), closes [#386](https://github.com/Saghen/blink.cmp/issues/386) [#219](https://github.com/Saghen/blink.cmp/issues/219) [#328](https://github.com/Saghen/blink.cmp/issues/328) [#331](https://github.com/Saghen/blink.cmp/issues/331) [#312](https://github.com/Saghen/blink.cmp/issues/312) [#454](https://github.com/Saghen/blink.cmp/issues/454) [#444](https://github.com/Saghen/blink.cmp/issues/444) [#372](https://github.com/Saghen/blink.cmp/issues/372) [#475](https://github.com/Saghen/blink.cmp/issues/475) +* support callback on `cmp.accept()` ([be3e9cf](https://github.com/Saghen/blink.cmp/commit/be3e9cf435588b3ff4de7abcb04ec90c812f1871)) +* support configuring prefetch_on_insert, disable by default ([9d4286f](https://github.com/Saghen/blink.cmp/commit/9d4286f9a410af788ee8406ec45e268aa4b23c9f)) +* **trigger:** prefetch on InsertEnter ([#507](https://github.com/Saghen/blink.cmp/issues/507)) ([7e98665](https://github.com/Saghen/blink.cmp/commit/7e9866529768065e0e191e436fc60220bef5185e)) +* use block icon for tailwind items ([#544](https://github.com/Saghen/blink.cmp/issues/544)) ([1502c75](https://github.com/Saghen/blink.cmp/commit/1502c754b9c241eecab1393d74a4eb6ccdfe0e64)) +* use number[] for ui_cmdline_pos ([80a5198](https://github.com/Saghen/blink.cmp/commit/80a5198a357ddcee97d94ac2be9a3590cd5a63f5)) +* validate config doesn't have erroneous fields ([834163e](https://github.com/Saghen/blink.cmp/commit/834163eebdfdb1ca2a4a54b1e8d4c8d2c8184c12)), closes [#501](https://github.com/Saghen/blink.cmp/issues/501) +* **window:** add `filetype` configuration ([#499](https://github.com/Saghen/blink.cmp/issues/499)) ([eb6213b](https://github.com/Saghen/blink.cmp/commit/eb6213b974e604f9ef8560e6c2379d757e81954d)) + +### Bug Fixes + +* **accept:** schecule `fuzzy.access` using uv.new_work ([#522](https://github.com/Saghen/blink.cmp/issues/522)) ([f66f19c](https://github.com/Saghen/blink.cmp/commit/f66f19c864e68ee5e2fb452648b7f6995ddadaa3)) +* account for cmdheight in cmdline_position (thanks [@lnrds](https://github.com/lnrds)!) ([6b67d16](https://github.com/Saghen/blink.cmp/commit/6b67d16036b780f49e44d3f5de207d3c7301f3e4)), closes [#538](https://github.com/Saghen/blink.cmp/issues/538) +* add '=' to cmdline trigger characters ([fb03ca7](https://github.com/Saghen/blink.cmp/commit/fb03ca7dd41fc5c234bf5ec089568f4eae584efb)), closes [#541](https://github.com/Saghen/blink.cmp/issues/541) +* add back, skip undo point for snippet kinds ([1563079](https://github.com/Saghen/blink.cmp/commit/15630796fc8c3c45c345d2fe73de6b3a1dc9bb11)) +* add gcc to flake.nix ([380bccf](https://github.com/Saghen/blink.cmp/commit/380bccf6eb1e3135fbab986f54aabd9147ff5977)), closes [#581](https://github.com/Saghen/blink.cmp/issues/581) +* add icon gap on ellipsis, remove references to renderer ([793b6ac](https://github.com/Saghen/blink.cmp/commit/793b6ac94efe754d31299b7de2e953244fe0d4ab)) +* add mode to context type ([f1afb8c](https://github.com/Saghen/blink.cmp/commit/f1afb8c77686ba6f5159dcb7591bf21efcc5f410)) +* allow 'none' preset for keymaps in validation ([bf1fd6a](https://github.com/Saghen/blink.cmp/commit/bf1fd6a690882a9bf5e07ded70fb3bba5d8a5bdf)) +* always get latest keyword ([13853d5](https://github.com/Saghen/blink.cmp/commit/13853d5c9cf827fc051fa7adebe701cce2ecd22f)), closes [#539](https://github.com/Saghen/blink.cmp/issues/539) +* check raw key for space in cmdline_events ([7be970e](https://github.com/Saghen/blink.cmp/commit/7be970e278334482710e1f37936c8480b522a751)) +* check that scrollbar is not nil ([790369b](https://github.com/Saghen/blink.cmp/commit/790369bb9998d1f9a01f67378e407622b492cf69)), closes [#525](https://github.com/Saghen/blink.cmp/issues/525) +* clear LuaSnip cache on snippet updates ([#664](https://github.com/Saghen/blink.cmp/issues/664)) ([b1b58e7](https://github.com/Saghen/blink.cmp/commit/b1b58e7b9895f43e64891346f76238d697aaadb9)) +* cmdline event suppression and scrollbar rendering ([e3b3fde](https://github.com/Saghen/blink.cmp/commit/e3b3fdedbc14afe7361228f7d2c8ce84cee272a6)), closes [#523](https://github.com/Saghen/blink.cmp/issues/523) +* cmdline events firing cursor moved when changed ([97989c8](https://github.com/Saghen/blink.cmp/commit/97989c8ee257239566c4d08264b080703ccc923b)), closes [#520](https://github.com/Saghen/blink.cmp/issues/520) +* cmdline including current arg prefix ([49bff2b](https://github.com/Saghen/blink.cmp/commit/49bff2bf23f15ae31a245e9ffd1b79a9f95bed61)), closes [#609](https://github.com/Saghen/blink.cmp/issues/609) +* **cmdline:** not delete buf when hide scrollbar cause it seems not necessary ([#591](https://github.com/Saghen/blink.cmp/issues/591)) ([0046d0c](https://github.com/Saghen/blink.cmp/commit/0046d0cc3e9bdd2dc36c2ec7a79aee32e76afa73)) +* completion auto_insert replace incorrect range ([#621](https://github.com/Saghen/blink.cmp/issues/621)) ([5926869](https://github.com/Saghen/blink.cmp/commit/59268691492bc1abfb0ed91a1cb3ac9fcc01650c)), closes [#460](https://github.com/Saghen/blink.cmp/issues/460) +* **completion:** disable in prompt buffers ([#574](https://github.com/Saghen/blink.cmp/issues/574)) ([1097d4e](https://github.com/Saghen/blink.cmp/commit/1097d4e24909c5b1a15b1ac6907ec26f78f5d22c)) +* consider functions as snippet commands ([d065c87](https://github.com/Saghen/blink.cmp/commit/d065c87b59a301065f863134d3a8271bdff6f630)) +* disable ghost text in command mode ([ad17735](https://github.com/Saghen/blink.cmp/commit/ad17735a6ddb4255cad6f0af574150761baf5ee4)), closes [#524](https://github.com/Saghen/blink.cmp/issues/524) +* don't block trigger characters in command mode ([0a729ae](https://github.com/Saghen/blink.cmp/commit/0a729ae1c4ab48695fb327161768720a82ed698f)), closes [#541](https://github.com/Saghen/blink.cmp/issues/541) +* don't create undo point when kind equals snippet ([343e89d](https://github.com/Saghen/blink.cmp/commit/343e89d39deb14b5cc6de844ce069ae3d98d7403)) +* don't duplicate `.` when completing hidden files in path source ([#557](https://github.com/Saghen/blink.cmp/issues/557)) ([714e2b5](https://github.com/Saghen/blink.cmp/commit/714e2b5f3fdcabd6ad31f98c71f930b260644c72)) +* don't show when moving on trigger character, hide on no items after trigger ([7a04612](https://github.com/Saghen/blink.cmp/commit/7a046122de512db8194dae130d691526b5031456)), closes [#545](https://github.com/Saghen/blink.cmp/issues/545) +* duplicate snippets in luasnip when autosnippets are enabled ([12ffc10](https://github.com/Saghen/blink.cmp/commit/12ffc10c6283ac148a89d72b5540d819fc80e2ff)) +* fire cursor moved when jumping between tab stops in a snippet ([1e4808e](https://github.com/Saghen/blink.cmp/commit/1e4808e3429bc060fa538728115edcaebbfc5c35)), closes [#545](https://github.com/Saghen/blink.cmp/issues/545) +* **fuzzy:** initialize db only once ([7868d47](https://github.com/Saghen/blink.cmp/commit/7868d477018f73bff6ca60757c1171223084bd12)) +* **ghost_text:** correctly disable on cmdline ([54d1a98](https://github.com/Saghen/blink.cmp/commit/54d1a980595e056e7be45a10d1cc8c34159f6d74)) +* ignore snippets that only contain text ([284dd37](https://github.com/Saghen/blink.cmp/commit/284dd37f9bbc632f8281d6361e877db5b45e6ff0)), closes [#624](https://github.com/Saghen/blink.cmp/issues/624) +* ignore sort_text if either are nil ([3ba583c](https://github.com/Saghen/blink.cmp/commit/3ba583cedb321291f3145b6e85039ed315b06b17)), closes [#595](https://github.com/Saghen/blink.cmp/issues/595) +* include space for cmdline events ([38b9c4f](https://github.com/Saghen/blink.cmp/commit/38b9c4f36a815fd3d9094e6d5c236a83dbb68ff9)) +* incorrect bounds when removing word under cursor in buffer sources ([d682165](https://github.com/Saghen/blink.cmp/commit/d6821651b145c730ca59faee638947a067243b24)), closes [#560](https://github.com/Saghen/blink.cmp/issues/560) +* **keymap:** incorrect merging strategy ([f88bd66](https://github.com/Saghen/blink.cmp/commit/f88bd66d88e9248276996c0f5b5c2b7fa5aa851f)), closes [#599](https://github.com/Saghen/blink.cmp/issues/599) +* **keymap:** normalize mapping capitalization ([#599](https://github.com/Saghen/blink.cmp/issues/599)) ([596a7ab](https://github.com/Saghen/blink.cmp/commit/596a7ab89cca7cdcddc0422e8f5a449042b7ff80)) +* **luasnip:** add global_snippets with ft="all" ([#546](https://github.com/Saghen/blink.cmp/issues/546)) ([9f1fb75](https://github.com/Saghen/blink.cmp/commit/9f1fb75b3ec282253ce6392360a584d0234904d0)) +* on_key for cmdline events ([89479f3](https://github.com/Saghen/blink.cmp/commit/89479f3f4c9096330a321a6cc438f5bc3f1e596b)), closes [#534](https://github.com/Saghen/blink.cmp/issues/534) +* prefetch first item when selection == 'manual' | 'auto_insert' ([a8222cf](https://github.com/Saghen/blink.cmp/commit/a8222cf1ccbf24818ae926f94779267659809ab0)), closes [#627](https://github.com/Saghen/blink.cmp/issues/627) +* **provider:** add missing validations ([#516](https://github.com/Saghen/blink.cmp/issues/516)) ([1eda2b9](https://github.com/Saghen/blink.cmp/commit/1eda2b989213b54a66589b44236bfcb427c9a5fe)) +* **provider:** restore path completion source ([#506](https://github.com/Saghen/blink.cmp/issues/506)) ([b2d13ba](https://github.com/Saghen/blink.cmp/commit/b2d13ba7a0aa6f53d3b0db2cd5ede7827ec72f5b)), closes [#465](https://github.com/Saghen/blink.cmp/issues/465) +* re-enable scrollbar on menu ([d48bb17](https://github.com/Saghen/blink.cmp/commit/d48bb176ae3a8d2f3fa4240f9098b94f1f0947ca)), closes [#519](https://github.com/Saghen/blink.cmp/issues/519) +* remove vim.notify on snippet only containing text ([59ef8a4](https://github.com/Saghen/blink.cmp/commit/59ef8a45eeafef35d8196473d86acbe515116027)) +* respect opts.index when checking if cmp.accept can be run ([ea12c51](https://github.com/Saghen/blink.cmp/commit/ea12c516ef43f14683903064bad7612d6e6a6a02)), closes [#633](https://github.com/Saghen/blink.cmp/issues/633) +* revert enabled logic or ([cfd1b7f](https://github.com/Saghen/blink.cmp/commit/cfd1b7f1b24ed77049d978c0a8813097a6e3acc7)), closes [#574](https://github.com/Saghen/blink.cmp/issues/574) [#577](https://github.com/Saghen/blink.cmp/issues/577) +* run callback when LSP client returns nil ([f9b72e3](https://github.com/Saghen/blink.cmp/commit/f9b72e3c1a1b61984b9128fb3e024fdf8a3d07fa)), closes [#543](https://github.com/Saghen/blink.cmp/issues/543) +* schedule get_bufnrs for buffer source ([342c5ed](https://github.com/Saghen/blink.cmp/commit/342c5ed6336d2850c59937747daccb4e880319e0)) +* signature help window documentation rendering ([264aea4](https://github.com/Saghen/blink.cmp/commit/264aea42fb2de42a377ae573141cfb61ab849f47)) +* sort by sortText/label again ([30705ab](https://github.com/Saghen/blink.cmp/commit/30705aba472b5c67b3a34d84f40d36add75b4c44)), closes [#444](https://github.com/Saghen/blink.cmp/issues/444) +* **sources:** set default item kind to `Property` ([#505](https://github.com/Saghen/blink.cmp/issues/505)) ([08ff824](https://github.com/Saghen/blink.cmp/commit/08ff824de4b76d314f7871e0345f7990b3faccb4)) +* **tailwind:** color rendering ([#601](https://github.com/Saghen/blink.cmp/issues/601)) ([02528e8](https://github.com/Saghen/blink.cmp/commit/02528e8ccbe4d0cef5e1df52eda419c5ed557ad3)) +* uncomment event emitter autocmd ([e1cf25f](https://github.com/Saghen/blink.cmp/commit/e1cf25fea50593993777865b3cca1db556a4a90b)) +* use luasnip get_snippet_filetypes, remove global_snippets option ([c0b5ae9](https://github.com/Saghen/blink.cmp/commit/c0b5ae940d7516eb07ca499f5a46445f216c46d3)), closes [#603](https://github.com/Saghen/blink.cmp/issues/603) +* use transform_items on resolve ([85176f7](https://github.com/Saghen/blink.cmp/commit/85176f7e3264b8ac3b571db12191416a4dce0303)), closes [#614](https://github.com/Saghen/blink.cmp/issues/614) + +## [0.7.5](https://github.com/Saghen/blink.cmp/compare/v0.7.4...v0.7.5) (2024-12-10) + +### Features + +* use `enabled` function instead of blocked_filetypes ([a6636c1](https://github.com/Saghen/blink.cmp/commit/a6636c1c38704c1581750b29abb0addabd198b89)), closes [#440](https://github.com/Saghen/blink.cmp/issues/440) + +### Bug Fixes + +* **fallback:** make fallback work with buffer-local mappings ([#483](https://github.com/Saghen/blink.cmp/issues/483)) ([8b553f6](https://github.com/Saghen/blink.cmp/commit/8b553f65419d051fe84eeeda3e2071e104c4f272)) + +## [0.7.4](https://github.com/Saghen/blink.cmp/compare/v0.7.3...v0.7.4) (2024-12-09) + +### Features + +* support non-latin characters for keyword and buffer source ([51d5f59](https://github.com/Saghen/blink.cmp/commit/51d5f598adf7f1cd1bb188011bb761c1856083a9)), closes [#130](https://github.com/Saghen/blink.cmp/issues/130) [#388](https://github.com/Saghen/blink.cmp/issues/388) + +### Bug Fixes + +* check response.err instead of response.error ([#473](https://github.com/Saghen/blink.cmp/issues/473)) ([e720477](https://github.com/Saghen/blink.cmp/commit/e7204774a6e99c5e222c930565353c757d2d0ec1)) +* completion.trigger.show_in_snippet ([#452](https://github.com/Saghen/blink.cmp/issues/452)) ([a42afb6](https://github.com/Saghen/blink.cmp/commit/a42afb61ad455816aef6baa1992f8de45e9a5eb1)), closes [#443](https://github.com/Saghen/blink.cmp/issues/443) +* documentation window auto show once and for all ([624676e](https://github.com/Saghen/blink.cmp/commit/624676efda13aa78a042aba29ee13e109821fa76)), closes [#430](https://github.com/Saghen/blink.cmp/issues/430) +* fill in cargoHash ([aa70277](https://github.com/Saghen/blink.cmp/commit/aa70277f537c942f7e477fd135531fffc37d81f3)) +* **highlight:** fix invalid highlight for doc separator ([#449](https://github.com/Saghen/blink.cmp/issues/449)) ([283a6af](https://github.com/Saghen/blink.cmp/commit/283a6afee44e0aea9b17074d49779558354d3520)) +* luasnip resolve documentation ([85f318b](https://github.com/Saghen/blink.cmp/commit/85f318b6db5b48d825d4ef575b405a8d41233753)), closes [#437](https://github.com/Saghen/blink.cmp/issues/437) +* make buffer events options required ([d0b0e16](https://github.com/Saghen/blink.cmp/commit/d0b0e16671733432986953bf4ddff268eb5b2d7c)) +* **render:** not render two separator for doc window ([#451](https://github.com/Saghen/blink.cmp/issues/451)) ([fc12fa9](https://github.com/Saghen/blink.cmp/commit/fc12fa99d4e1274d331c2004e777981193f7d6f8)) +* revert luasnip source to use current cursor position ([5cfff34](https://github.com/Saghen/blink.cmp/commit/5cfff3433a2afc3f4e29eb4e3caa8f80953f0cfb)) + +## [0.7.3](https://github.com/Saghen/blink.cmp/compare/v0.7.2...v0.7.3) (2024-12-03) + +### Bug Fixes + +* revert to original logic for updating menu position ([99129b6](https://github.com/Saghen/blink.cmp/commit/99129b67759c1b78198e527eae9cc91121cded29)), closes [#436](https://github.com/Saghen/blink.cmp/issues/436) + +## [0.7.2](https://github.com/Saghen/blink.cmp/compare/v0.7.1...v0.7.2) (2024-12-03) + +> [!IMPORTANT] +> A native `luasnip` source has been added, please see the [README](https://github.com/Saghen/blink.cmp#luasnip) for the configuration + +### Features + +* add `auto_show` property for menu ([29fe017](https://github.com/Saghen/blink.cmp/commit/29fe017624030fa53ee053626762fa385a9adb19)), closes [#402](https://github.com/Saghen/blink.cmp/issues/402) +* clamp text edit range to bounds ([7ceff61](https://github.com/Saghen/blink.cmp/commit/7ceff61595aae682b421a68e208719b1523c7b44)), closes [#257](https://github.com/Saghen/blink.cmp/issues/257) +* expose reload function ([f4e53f2](https://github.com/Saghen/blink.cmp/commit/f4e53f2ac7a3d8c3ef47be0dffa97dca637bf696)), closes [#428](https://github.com/Saghen/blink.cmp/issues/428) +* native luasnip source ([08b59ed](https://github.com/Saghen/blink.cmp/commit/08b59edc59950be279f8c72a20bd7897e9f0d021)), closes [#378](https://github.com/Saghen/blink.cmp/issues/378) [#401](https://github.com/Saghen/blink.cmp/issues/401) [#432](https://github.com/Saghen/blink.cmp/issues/432) + +### Bug Fixes + +* avoid removing words for current line on out of focus buffers ([2cbb02d](https://github.com/Saghen/blink.cmp/commit/2cbb02da58ab40f2bfd3dd85f80cba76d6279987)), closes [#433](https://github.com/Saghen/blink.cmp/issues/433) +* documentation not updating after manually opened ([8c1fdc9](https://github.com/Saghen/blink.cmp/commit/8c1fdc901cfead1cd88ed3e652d45ca7d75a3d3f)), closes [#430](https://github.com/Saghen/blink.cmp/issues/430) +* handle nil line ([#429](https://github.com/Saghen/blink.cmp/issues/429)) ([38b3ad6](https://github.com/Saghen/blink.cmp/commit/38b3ad6d4af9d392d3e5e0dabcb14e7d8e348314)) + +## [0.7.1](https://github.com/Saghen/blink.cmp/compare/v0.7.0...v0.7.1) (2024-12-02) + +### Bug Fixes + +* arguments on curl ([f992b72](https://github.com/Saghen/blink.cmp/commit/f992b72017cac77d4f4e22dc05016e5d79adff68)) +* drop retry from curl ([6e9fb62](https://github.com/Saghen/blink.cmp/commit/6e9fb6254bb49eaf014a48049ff511bbfd6a66a3)), closes [#425](https://github.com/Saghen/blink.cmp/issues/425) + +## [0.7.0](https://github.com/Saghen/blink.cmp/compare/v0.6.2...v0.7.0) (2024-12-02) + +> [!IMPORTANT] +> Most of the configuration has been reworked, please see the README for the new schema + +* Includes an enormous refactor in preparation for sources v2, commandline completions, and the v1 release [#389](https://github.com/Saghen/blink.cmp/issues/389) +* Enable experimental Treesitter highlighting on the labels via `completion.menu.draw.treesitter = true` + +### BREAKING CHANGES + +* nuke the debt ([#389](https://github.com/Saghen/blink.cmp/issues/389)) ([1187172](https://github.com/Saghen/blink.cmp/commit/11871727278381febd05d1ee1a17f98fb2e32b26)), closes [#323](https://github.com/Saghen/blink.cmp/issues/323) + +### Features + +* add show_on_keyword and show_on_trigger_character trigger options ([69a69dd](https://github.com/Saghen/blink.cmp/commit/69a69dd7c66f2290dea849846402266b2303782c)), closes [#402](https://github.com/Saghen/blink.cmp/issues/402) +* allow completing buffer words with unicode ([#392](https://github.com/Saghen/blink.cmp/issues/392)) ([e1d3e9d](https://github.com/Saghen/blink.cmp/commit/e1d3e9d4a64466b521940b3ccb67c6fd534b0032)) +* call execute after accepting, but before applying semantic brackets ([073449a](https://github.com/Saghen/blink.cmp/commit/073449a872d49d0c61cb1cf020232d609b2b3d8c)) +* default to empty table for setup ([#412](https://github.com/Saghen/blink.cmp/issues/412)) ([4559ec5](https://github.com/Saghen/blink.cmp/commit/4559ec5cfb91ed8080e2f8df7d4784e12aa27f18)) +* error on download failure ([6054da2](https://github.com/Saghen/blink.cmp/commit/6054da23af87117afd1de59bb77df90037e84675)) +* nuke the debt ([#389](https://github.com/Saghen/blink.cmp/issues/389)) ([1187172](https://github.com/Saghen/blink.cmp/commit/11871727278381febd05d1ee1a17f98fb2e32b26)), closes [#323](https://github.com/Saghen/blink.cmp/issues/323) +* prebuilt binary retry, disable progress, and docs ([bc67391](https://github.com/Saghen/blink.cmp/commit/bc67391de57ce3e42302b13cccf9dd41207c0860)), closes [#68](https://github.com/Saghen/blink.cmp/issues/68) +* **render:** support `source_id` and `source_name` in menu render ([#400](https://github.com/Saghen/blink.cmp/issues/400)) ([d5f62f9](https://github.com/Saghen/blink.cmp/commit/d5f62f981cde0660944626aaeaab8541c9516346)) +* support accepting and drawing by index ([4b1a793](https://github.com/Saghen/blink.cmp/commit/4b1a79305d9acb22171062053a6c942383fefa72)), closes [#382](https://github.com/Saghen/blink.cmp/issues/382) +* support get_bufnrs for the buffer source ([#411](https://github.com/Saghen/blink.cmp/issues/411)) ([4c65dbd](https://github.com/Saghen/blink.cmp/commit/4c65dbde1709bed2cb87483b0ce4eb522098bebc)) +* treesitter highlighter ([#404](https://github.com/Saghen/blink.cmp/issues/404)) ([08a0777](https://github.com/Saghen/blink.cmp/commit/08a07776838e205c697a3d05bcf43104a2adacf5)) +* use sort_text over label for sorting ([0386120](https://github.com/Saghen/blink.cmp/commit/0386120c3bbe32a6746b73a8e38ec954c58575c9)), closes [#365](https://github.com/Saghen/blink.cmp/issues/365) + +### Bug Fixes + +* accept grabbing wrong config ([3dcf98d](https://github.com/Saghen/blink.cmp/commit/3dcf98d8a5c1c720d5a3d789ac14a9741dbe70eb)) +* allow border to be a table ([52f6387](https://github.com/Saghen/blink.cmp/commit/52f63878c0affef88023cd2a00a103644cb7ccfa)), closes [#398](https://github.com/Saghen/blink.cmp/issues/398) +* auto_insert scheduling and module reference ([1b3cd31](https://github.com/Saghen/blink.cmp/commit/1b3cd31e26066308f97075fee7744cd8694cd75e)) +* autocmd called in fast event ([9428983](https://github.com/Saghen/blink.cmp/commit/94289832dc7c148862fdf9326e173df265abe8ad)), closes [#396](https://github.com/Saghen/blink.cmp/issues/396) +* buffer events suppression, auto_insert selection ([96ceb56](https://github.com/Saghen/blink.cmp/commit/96ceb56f7b6e0abeacb01aa2b04abef33121d38b)), closes [#415](https://github.com/Saghen/blink.cmp/issues/415) +* convert additional text edits to utf-8 ([49981f2](https://github.com/Saghen/blink.cmp/commit/49981f2bc8c04967cf868574913f092392a267fe)), closes [#397](https://github.com/Saghen/blink.cmp/issues/397) +* cycling list skipping one item ([07b2ee1](https://github.com/Saghen/blink.cmp/commit/07b2ee14eaae6908f0da44bfa918177d167b12de)) +* deduplicate mode changes, dont hide on select mode ([04ff262](https://github.com/Saghen/blink.cmp/commit/04ff262f3590cd9b63dab03e2cecc759d4abdf69)), closes [#393](https://github.com/Saghen/blink.cmp/issues/393) +* default snippet active function not returning ([59add2d](https://github.com/Saghen/blink.cmp/commit/59add2d602d9a13003ed3430232b3689872ea9ac)), closes [#399](https://github.com/Saghen/blink.cmp/issues/399) +* don't set window properties when nil ([cb815af](https://github.com/Saghen/blink.cmp/commit/cb815afca7c32af7feeb3a90d5b450620d4bef2b)), closes [#407](https://github.com/Saghen/blink.cmp/issues/407) +* ensure failed curl doesn't update the version ([933052b](https://github.com/Saghen/blink.cmp/commit/933052b8e9b585c24c493fdc34a66519d4889c1b)), closes [#68](https://github.com/Saghen/blink.cmp/issues/68) +* ensure menu selection index is within bounds ([bb5407d](https://github.com/Saghen/blink.cmp/commit/bb5407d27e93dc71f8572571ab04b3fc02fc8259)), closes [#416](https://github.com/Saghen/blink.cmp/issues/416) +* filter text always being nil ([33f7d8d](https://github.com/Saghen/blink.cmp/commit/33f7d8df8119673b7eca3d7a04ed28b805cae296)), closes [#365](https://github.com/Saghen/blink.cmp/issues/365) +* incorrect context start_col 1 char after beginning of line ([e88da6a](https://github.com/Saghen/blink.cmp/commit/e88da6a123c857ec2da92ff488c3f82cfba718ef)), closes [#405](https://github.com/Saghen/blink.cmp/issues/405) +* invalid configuration and readme after refactor ([56f7cb6](https://github.com/Saghen/blink.cmp/commit/56f7cb679ef9e5c09351bfa67b081c68ad27349f)), closes [#394](https://github.com/Saghen/blink.cmp/issues/394) +* keyword range "full" when covering end of line ([160b687](https://github.com/Saghen/blink.cmp/commit/160b6875095977d49e16c4e33add4b0e6b0c8668)), closes [#268](https://github.com/Saghen/blink.cmp/issues/268) +* misc typing issues ([b94172c](https://github.com/Saghen/blink.cmp/commit/b94172c8b28f6030c0df3f846eec4a129a25c5bb)) +* only affect initial show for show_on_keyword and show_on_trigger_character ([ea61b1d](https://github.com/Saghen/blink.cmp/commit/ea61b1dc9ed2c4a092ab1365657bc4220b1b5488)), closes [#402](https://github.com/Saghen/blink.cmp/issues/402) +* signature window highlight ns ([0b9a128](https://github.com/Saghen/blink.cmp/commit/0b9a1282eb4f9e44de66fd689d4e301bb987abf5)) +* signature window setup ([cab7576](https://github.com/Saghen/blink.cmp/commit/cab7576350c12de902dc18a85d17f4733f1f9938)) +* super-tab preset keymap name ([f569aeb](https://github.com/Saghen/blink.cmp/commit/f569aeb9e684a2b18514077501e98b0f9ef873bd)) +* user autocmd called in fast event not being wrapped ([e9baeea](https://github.com/Saghen/blink.cmp/commit/e9baeeac1d05d8cbbbee560380853baeb8b316f3)) + +### Documentation + +* add note about reworked config ([180be7b](https://github.com/Saghen/blink.cmp/commit/180be7ba574033baa30fa8af0db4f59db7353584)) + +## [0.6.2](https://github.com/Saghen/blink.cmp/compare/v0.6.1...v0.6.2) (2024-11-26) + +### Features + +* add `cancel` command for use with `auto_insert` ([c58b3a8](https://github.com/Saghen/blink.cmp/commit/c58b3a8ec2cd71b422fbd4b1607e924996dfdebb)), closes [#215](https://github.com/Saghen/blink.cmp/issues/215) +* remove rust from blocked auto brackets filetypes ([8500a62](https://github.com/Saghen/blink.cmp/commit/8500a62e6f07a823b373df91b00c997734b3c664)), closes [#359](https://github.com/Saghen/blink.cmp/issues/359) + +### Bug Fixes + +* mark all config properties as optional ([e328bde](https://github.com/Saghen/blink.cmp/commit/e328bdedc4d12d01ff5c68bee8ea6ae6f33f42f7)), closes [#370](https://github.com/Saghen/blink.cmp/issues/370) +* path source not handling hidden files correctly ([22c5c0d](https://github.com/Saghen/blink.cmp/commit/22c5c0d2c96d5ab86cd23f8df76f005505138a5d)), closes [#369](https://github.com/Saghen/blink.cmp/issues/369) +* use offset encoding of first client ([0a2abab](https://github.com/Saghen/blink.cmp/commit/0a2ababaa450f50afeb4653c3d40b34344aa80d6)), closes [#380](https://github.com/Saghen/blink.cmp/issues/380) + +## [0.6.1](https://github.com/Saghen/blink.cmp/compare/v0.6.0...v0.6.1) (2024-11-24) + +### Features + +* add prebuilt binaries for android ([#362](https://github.com/Saghen/blink.cmp/issues/362)) ([11a50fe](https://github.com/Saghen/blink.cmp/commit/11a50fe006a4482ab5acb5bcd77efa4fb9f944f8)) + +## [0.6.0](https://github.com/Saghen/blink.cmp/compare/v0.5.1...v0.6.0) (2024-11-24) + +### BREAKING CHANGES + +* matched character highlighting, draw rework (#245) +* set default nerd_font_variant to mono + +### Features + +* add `execute` function for sources ([653b262](https://github.com/Saghen/blink.cmp/commit/653b2629e1dab0c6d0084d90f30a600d601812a1)) +* add get_filetype option for snippet source ([#352](https://github.com/Saghen/blink.cmp/issues/352)) ([7c3ad2b](https://github.com/Saghen/blink.cmp/commit/7c3ad2b1fcd0250df69162ad71439cfe547f9608)), closes [#292](https://github.com/Saghen/blink.cmp/issues/292) +* add scrollbar to autocomplete menu ([#259](https://github.com/Saghen/blink.cmp/issues/259)) ([4c2a36c](https://github.com/Saghen/blink.cmp/commit/4c2a36ce8efb2f02d12600b43b3de32898d07433)) +* add snippet indicator back to label on render ([6f5ae79](https://github.com/Saghen/blink.cmp/commit/6f5ae79218334e5d1ca783e22847bbc6b4daef16)) +* allow disabling keymap by passing an empty table ([e384594](https://github.com/Saghen/blink.cmp/commit/e384594deee2f7be225cb89dbcb72d9b6482fde8)) +* avoid taking up space when scrollbar is hidden ([77f037c](https://github.com/Saghen/blink.cmp/commit/77f037cae07358368f3b7548ba39cffceb49349e)) +* extract word from completion item for auto-insert preview ([#341](https://github.com/Saghen/blink.cmp/issues/341)) ([285f6f4](https://github.com/Saghen/blink.cmp/commit/285f6f498c8ba3ac0788edb1db2f8d2d3cb20fad)) +* matched character highlighting, draw rework ([#245](https://github.com/Saghen/blink.cmp/issues/245)) ([683c47a](https://github.com/Saghen/blink.cmp/commit/683c47ac8c6e538122dc0fe50187b78f8995a549)) +* option to disable treesitter highlighting ([1c14f8e](https://github.com/Saghen/blink.cmp/commit/1c14f8e8817015634c593eb3832a73e4993c561e)) +* position documentation based on desired size, not max size ([973f06a](https://github.com/Saghen/blink.cmp/commit/973f06a164835b74247f46b3c5b2ae895a1acb1b)) +* set default nerd_font_variant to mono ([d3e1c92](https://github.com/Saghen/blink.cmp/commit/d3e1c92e68b74f3d05f6ab7dfff2af8f83769149)) +* support editRange, use textEditText when editRange is defined ([db3d1ad](https://github.com/Saghen/blink.cmp/commit/db3d1ad8d6420ce29d548991468cc0107fe9d04b)), closes [#310](https://github.com/Saghen/blink.cmp/issues/310) +* temporarily disable markdown combining ([24b4d35](https://github.com/Saghen/blink.cmp/commit/24b4d350b469595ff39ce48a45ee12b59578aae6)) +* use filter_text when available ([12b4f11](https://github.com/Saghen/blink.cmp/commit/12b4f116648d87551a07def740a0375446105bbc)) +* validate provider names in enabled_providers ([e9c9b41](https://github.com/Saghen/blink.cmp/commit/e9c9b41ea0f8ae36b7c19c970bf313f1ca93bd1b)) + +### Bug Fixes + +* add ctx.icon_gap in kind_icon component ([ccf02f5](https://github.com/Saghen/blink.cmp/commit/ccf02f5e39e3ed7b4e65dbe667a3329313540eba)) +* applying preview text_edit ([#296](https://github.com/Saghen/blink.cmp/issues/296)) ([8372a6b](https://github.com/Saghen/blink.cmp/commit/8372a6bfce9499f3bb8a91a23db8fe1d83f2d625)) +* check if source is in `enabled_providers` before calling source:enabled ([#266](https://github.com/Saghen/blink.cmp/issues/266)) ([338d2a6](https://github.com/Saghen/blink.cmp/commit/338d2a6e81b9e0f9e66b691c36c9959a2705085a)) +* clear last_char on trigger hide ([1ce30c9](https://github.com/Saghen/blink.cmp/commit/1ce30c9d1aa539f05e99b9ecea0dcc35d4cc33fe)), closes [#228](https://github.com/Saghen/blink.cmp/issues/228) +* completion label details containing newline characters ([#265](https://github.com/Saghen/blink.cmp/issues/265)) ([1628800](https://github.com/Saghen/blink.cmp/commit/1628800e1747ecc767368cab45916177c723da82)) +* consider the border when calculating the position of the autocom… ([#325](https://github.com/Saghen/blink.cmp/issues/325)) ([41178d3](https://github.com/Saghen/blink.cmp/commit/41178d39670ce8db5e93a0028a7f23729559a326)) +* consider the border when calculating the width of the documentat… ([#326](https://github.com/Saghen/blink.cmp/issues/326)) ([130eb51](https://github.com/Saghen/blink.cmp/commit/130eb512e2849c021d73bd269b77cc3b0ecf8b74)) +* convert to utf-8 encoding on text edits ([2e37993](https://github.com/Saghen/blink.cmp/commit/2e379931090f3737b844598a18382241197aaa2a)), closes [#188](https://github.com/Saghen/blink.cmp/issues/188) [#200](https://github.com/Saghen/blink.cmp/issues/200) +* default highlight groups ([#317](https://github.com/Saghen/blink.cmp/issues/317)) ([69a987b](https://github.com/Saghen/blink.cmp/commit/69a987b96cf754a12b6d7dafce1d2d49ade591f2)) +* default to item when assigning defaults, only use known defaults ([fb9f374](https://github.com/Saghen/blink.cmp/commit/fb9f3744cbc4c8b0c6792ed1c072009864a1bd6d)), closes [#151](https://github.com/Saghen/blink.cmp/issues/151) +* documentation misplacement due to screenpos returning 0,0 ([cb0baa4](https://github.com/Saghen/blink.cmp/commit/cb0baa4403fe5cf6d5dc3af483176780e44ba071)) +* download mechanism works with GIT_DIR and GIT_WORK_TREE set ([#275](https://github.com/Saghen/blink.cmp/issues/275)) ([8c9930c](https://github.com/Saghen/blink.cmp/commit/8c9930c94e17ca0ab9956986b175cd91f4ac3a59)) +* drop unnecessary filetype configuration ([bec27d9](https://github.com/Saghen/blink.cmp/commit/bec27d9196fe3c0020b56e49533a8f08cc8ea45f)), closes [#295](https://github.com/Saghen/blink.cmp/issues/295) +* drop vim print ([c3447cc](https://github.com/Saghen/blink.cmp/commit/c3447cc2bd4afec7050230b49a3e889c43084400)) +* get the cursor position relative to the window instead of the sc… ([#327](https://github.com/Saghen/blink.cmp/issues/327)) ([5479abf](https://github.com/Saghen/blink.cmp/commit/5479abfbfb47bf4d23220a6e5a3eb11f23e57214)) +* **ghost-text:** flickering using autocmds ([#255](https://github.com/Saghen/blink.cmp/issues/255)) ([a94bbaf](https://github.com/Saghen/blink.cmp/commit/a94bbaf9f2c6329f4593233f069b3dea21b4cedc)) +* handle gap for empty text ([#301](https://github.com/Saghen/blink.cmp/issues/301)) ([371ad28](https://github.com/Saghen/blink.cmp/commit/371ad288544423531121c1abf0d519dda791e9f1)) +* handle not being in a git repository, fix error on flakes ([#281](https://github.com/Saghen/blink.cmp/issues/281)) ([d2a216d](https://github.com/Saghen/blink.cmp/commit/d2a216de72a6b3a741c214b66e70897ff6f16dc2)) +* ignore empty doc lines and detail lines ([aeaa2e7](https://github.com/Saghen/blink.cmp/commit/aeaa2e78dad7885e99b5a00a70b9c57c5a5302aa)), closes [#247](https://github.com/Saghen/blink.cmp/issues/247) +* join newlines in `label_description` ([#333](https://github.com/Saghen/blink.cmp/issues/333)) ([8ba2069](https://github.com/Saghen/blink.cmp/commit/8ba2069a57cf6580dea6a50bf71e5b3b2924b284)) +* make ghost-text extmark with pcall ([#287](https://github.com/Saghen/blink.cmp/issues/287)) ([a2f6cfb](https://github.com/Saghen/blink.cmp/commit/a2f6cfb2902e1410f5cdbf386b9af337754f1a07)) +* offset encoding conversion on nvim 0.11.0 ([#308](https://github.com/Saghen/blink.cmp/issues/308)) ([9822c6b](https://github.com/Saghen/blink.cmp/commit/9822c6b40ad91a14e2c75696db30999ae5cf1fc5)), closes [#307](https://github.com/Saghen/blink.cmp/issues/307) +* offset encoding for text edits ([c2a56e4](https://github.com/Saghen/blink.cmp/commit/c2a56e473ff5952211f7c890de0b831e8df3976d)) +* only undo if not snippet ([f4dcebf](https://github.com/Saghen/blink.cmp/commit/f4dcebfd720810b14eb2ad62102028c104bf2205)), closes [#244](https://github.com/Saghen/blink.cmp/issues/244) +* override typing and module ([f1647f7](https://github.com/Saghen/blink.cmp/commit/f1647f7fd97ac7129e1cb8a1ed242ae326f25d6e)) +* padded window ([#315](https://github.com/Saghen/blink.cmp/issues/315)) ([7a37c64](https://github.com/Saghen/blink.cmp/commit/7a37c643412f19b04a03ed4c71e94da175efcfb8)) +* prevent index out of bounds in get_code_block_range ([#271](https://github.com/Saghen/blink.cmp/issues/271)) ([e6c735b](https://github.com/Saghen/blink.cmp/commit/e6c735be455c90df4aa7c11cfe7542f111234de6)) +* remove offset from label detail highlight ([5262586](https://github.com/Saghen/blink.cmp/commit/52625866f5b9a9358313308276dcf110cf1a42ea)) +* reset documentation scroll on new item ([cd3aa32](https://github.com/Saghen/blink.cmp/commit/cd3aa32276308d0c1bddf7a14cd13a8776eb5575)), closes [#239](https://github.com/Saghen/blink.cmp/issues/239) +* scrollbar gutter not updating on window resize ([c8cf209](https://github.com/Saghen/blink.cmp/commit/c8cf209dc843c5a42945bb95a4b8598bcab8c6f8)) +* **scrollbar:** use cursorline to determine thumb position ([#267](https://github.com/Saghen/blink.cmp/issues/267)) ([28fcf95](https://github.com/Saghen/blink.cmp/commit/28fcf952d14a022cd64f89ff32b3442c6101b873)) +* signature help now highlights the right parameter ([#297](https://github.com/Saghen/blink.cmp/issues/297)) ([3fe4c75](https://github.com/Saghen/blink.cmp/commit/3fe4c75c69f208462c4e8957005f6ccb72b1da25)) +* **snippets:** fix nullpointer exception ([#355](https://github.com/Saghen/blink.cmp/issues/355)) ([3ac471b](https://github.com/Saghen/blink.cmp/commit/3ac471bbfe614adb77fc8179dd4adaa0d1576542)) +* tailwind colors ([#306](https://github.com/Saghen/blink.cmp/issues/306)) ([8e3af0e](https://github.com/Saghen/blink.cmp/commit/8e3af0ec0079b599fb57f97653b2f20f98e2a5bb)) +* **types:** allow resolving empty response from blink.cmd.Source ([#254](https://github.com/Saghen/blink.cmp/issues/254)) ([46a5f0b](https://github.com/Saghen/blink.cmp/commit/46a5f0b9fd8e6753d118853d384ae85bfdb70c30)) +* use pmenu scrollbar highlights ([5632376](https://github.com/Saghen/blink.cmp/commit/5632376d4f51d777013d5f48414a15f02be854af)) + +## [0.5.1](https://github.com/Saghen/blink.cmp/compare/v0.5.0...v0.5.1) (2024-11-03) + +### BREAKING CHANGES + +* set max_width to 80 for documentation + +### Features + +* 'enter' keymap ([4ec5cea](https://github.com/Saghen/blink.cmp/commit/4ec5cea4858eee31919cc2a5bc1850846073c5ec)) +* add label details to all draw functions ([f9c58ab](https://github.com/Saghen/blink.cmp/commit/f9c58ab26a427883965394959276fd347574b11e)), closes [#97](https://github.com/Saghen/blink.cmp/issues/97) +* add winblend option for windows ([#237](https://github.com/Saghen/blink.cmp/issues/237)) ([ca94ee0](https://github.com/Saghen/blink.cmp/commit/ca94ee0b1ec848bac6426811f12f6da39e48d02a)) +* align completion window ([#235](https://github.com/Saghen/blink.cmp/issues/235)) ([0c13fbd](https://github.com/Saghen/blink.cmp/commit/0c13fbd3d7bed1d4bab08d3831c95ee3dfb7277f)), closes [#221](https://github.com/Saghen/blink.cmp/issues/221) +* allow merging of keymap preset with custom keymap ([#233](https://github.com/Saghen/blink.cmp/issues/233)) ([6b46164](https://github.com/Saghen/blink.cmp/commit/6b46164eac2feb6dd49e6e8c434cb276f50c8132)) +* better extraction of detail from doc ([b0815e4](https://github.com/Saghen/blink.cmp/commit/b0815e461623d9a9ea06fb632167ca25656abcf5)) +* only offset window when using preset draw ([75cadbc](https://github.com/Saghen/blink.cmp/commit/75cadbcd2657ed01326ca2b0e5e4d78a77127ca3)) +* rework window positioning ([a67adaf](https://github.com/Saghen/blink.cmp/commit/a67adaf623f9c6e1803a693044608b73e02e8da3)), closes [#45](https://github.com/Saghen/blink.cmp/issues/45) [#194](https://github.com/Saghen/blink.cmp/issues/194) +* set max_width to 80 for documentation ([dc1de2b](https://github.com/Saghen/blink.cmp/commit/dc1de2bf962c67e8ba8647710817bbce04f92bdb)) +* TailwindCSS highlight support ([#143](https://github.com/Saghen/blink.cmp/issues/143)) ([b2bbef5](https://github.com/Saghen/blink.cmp/commit/b2bbef52f24799f0e79a3adf6038366b26e2451b)) + +### Bug Fixes + +* add "enter" keymap to types ([3ca68ef](https://github.com/Saghen/blink.cmp/commit/3ca68ef008e383a28c760de2d5ee65b35efbb5c5)) +* allow to be lazy loaded on InsertEnter ([#243](https://github.com/Saghen/blink.cmp/issues/243)) ([9d50661](https://github.com/Saghen/blink.cmp/commit/9d5066134b339c5e4aa6cec3daa086d3b0671892)) +* alpine linux detection ([a078c87](https://github.com/Saghen/blink.cmp/commit/a078c877ac17a912a51aba9d9e0068a0f1ed509b)) +* check LSP methods before requesting ([193423c](https://github.com/Saghen/blink.cmp/commit/193423ca584e4e1a9639d6c480a6b952db566c21)), closes [#220](https://github.com/Saghen/blink.cmp/issues/220) +* documentation width ([9bdd828](https://github.com/Saghen/blink.cmp/commit/9bdd828e474e69badb64a305179930cf66acf649)) +* **documentation:** better docs ([#234](https://github.com/Saghen/blink.cmp/issues/234)) ([a253b35](https://github.com/Saghen/blink.cmp/commit/a253b356092b8f64ac66200c249afe5978c3fc39)) +* enable show_in_snippet by default ([76d11a6](https://github.com/Saghen/blink.cmp/commit/76d11a617075dc53e89e1c9b9ce5c62435abdfba)) +* ensure treesitter does not run on windows ([2ac2f43](https://github.com/Saghen/blink.cmp/commit/2ac2f43513cdf63313192271427cc55608f0bedb)), closes [#193](https://github.com/Saghen/blink.cmp/issues/193) +* lazily call fuzzy access ([aeb6195](https://github.com/Saghen/blink.cmp/commit/aeb6195ba870c61e4e0f2d4e8ef1bcc80464af9b)) +* make all of source provider config optional ([055b943](https://github.com/Saghen/blink.cmp/commit/055b9435358f68ae26de75d9294749bd69c22ccc)) +* only check enabled fallback sources ([#232](https://github.com/Saghen/blink.cmp/issues/232)) ([ecb3520](https://github.com/Saghen/blink.cmp/commit/ecb3520c899eee9dbe738620f3c327b8089fe1f8)) +* window direction and autocomplete closing on position update ([4b3fd8f](https://github.com/Saghen/blink.cmp/commit/4b3fd8f5ce6ece4f84d6c6ddfd0a42f43b889574)), closes [#240](https://github.com/Saghen/blink.cmp/issues/240) + +## [0.5.0](https://github.com/Saghen/blink.cmp/compare/v0.4.1...v0.5.0) (2024-10-30) + +> [!IMPORTANT] +> The **keymap** configuration has been reworked, please see the README for the new schema + +You may now use `nvim-cmp` sources within `blink.cmp` using @stefanboca's compatibility layer: https://github.com/Saghen/blink.compat + +### BREAKING CHANGES + +* rework keymap config + +### Features + +* `enabled` function for sources ([c104663](https://github.com/Saghen/blink.cmp/commit/c104663e92c15dd59ee3b299249361cd095206f4)), closes [#208](https://github.com/Saghen/blink.cmp/issues/208) +* accept error handling, expose autocomplete.select ([9cd1236](https://github.com/Saghen/blink.cmp/commit/9cd123657fce6e563a7d24b438f61b012ca1559f)) +* cache resolve tasks ([83a8303](https://github.com/Saghen/blink.cmp/commit/83a8303e2744d01249f465f219f0dc5a41104a9e)) +* glibc 2.17 and musl prebuilt binaries ([c593835](https://github.com/Saghen/blink.cmp/commit/c593835fe1b0297dfbcabe46edcd1edb9d317b94)), closes [#160](https://github.com/Saghen/blink.cmp/issues/160) +* ignore _*.lua files ([f6eccaf](https://github.com/Saghen/blink.cmp/commit/f6eccaf3f2ef8939ea661ee8384e299a9428999c)) +* lsp capabilities ([e0e08cb](https://github.com/Saghen/blink.cmp/commit/e0e08cbfea667ff21b9e6e5acb0389ddd6d2de41)) +* output preview with ghost text, including for snippets ([#186](https://github.com/Saghen/blink.cmp/issues/186)) ([6d25187](https://github.com/Saghen/blink.cmp/commit/6d2518745db83da0b15f60e22c15c205fb1ed56f)) +* **perf:** call score_offset func once per source ([bd90e00](https://github.com/Saghen/blink.cmp/commit/bd90e007f33c60a3a11bb99ff2e8bfd897fe27b3)) +* prefetch resolve on select ([52ec2c9](https://github.com/Saghen/blink.cmp/commit/52ec2c985cb0ef9459b73bb8b08801f35f092f6d)) +* resolve item before accept ([3927128](https://github.com/Saghen/blink.cmp/commit/3927128e712806c22c20487ef0a1ed885bfec292)) +* rework keymap config ([3fd92f0](https://github.com/Saghen/blink.cmp/commit/3fd92f0bbceb31a3cd32b1d7a9d2a62071c85d91)) +* show completion window after accept if on trigger character ([28e0b5a](https://github.com/Saghen/blink.cmp/commit/28e0b5a873c6f4e687260384595a05c55a888ccf)), closes [#198](https://github.com/Saghen/blink.cmp/issues/198) +* support disabling accept on trigger character, block parenthesis ([125d4f1](https://github.com/Saghen/blink.cmp/commit/125d4f1288b3b309d219848559adfca3cc61f8b5)), closes [#212](https://github.com/Saghen/blink.cmp/issues/212) +* switch default keymap to select_and_accept ([f0f2672](https://github.com/Saghen/blink.cmp/commit/f0f26728c3e5c65cf2d27a1b24e4e3fbd26773fb)) +* use treesitter for signature help hl ([0271d79](https://github.com/Saghen/blink.cmp/commit/0271d7957324df68bd352fc7aef60606c96c88ca)) + +### Bug Fixes + +* add back cursor move after accept, but use current line ([ceeeb53](https://github.com/Saghen/blink.cmp/commit/ceeeb538b091c43aa6fb6fd6020531a37cef2191)) +* always return item in resolve ([6f0fc86](https://github.com/Saghen/blink.cmp/commit/6f0fc86f8fbb94ae23770c01dc2e3cf9e1886e99)), closes [#211](https://github.com/Saghen/blink.cmp/issues/211) +* documentation auto show no longer working ([#202](https://github.com/Saghen/blink.cmp/issues/202)) ([6290abd](https://github.com/Saghen/blink.cmp/commit/6290abd24b14723ba4827c28367a805bcc4773de)) +* dont move cursor after accepting ([cab91c5](https://github.com/Saghen/blink.cmp/commit/cab91c5f56eb15394d4cabddcd62eee6963129ec)) +* fallback show_documentation when window open ([bc311b7](https://github.com/Saghen/blink.cmp/commit/bc311b756ca89652bfb18b07a99ff52f424d63a2)) +* handle failed lsp resolve request gracefully ([4c40bf2](https://github.com/Saghen/blink.cmp/commit/4c40bf25f2371d6b3df6f130e154ebac0b9c3422)) +* ignore nil item for resolve prefetching ([b7d1233](https://github.com/Saghen/blink.cmp/commit/b7d1233d826a0406538955b4ef2448dc0e72c536)), closes [#209](https://github.com/Saghen/blink.cmp/issues/209) +* invalid insertTextMode capabilities ([4de7b7e](https://github.com/Saghen/blink.cmp/commit/4de7b7e64100cfdbfc564c475a1713ba2498ba25)) +* prevent treesitter from running on windows ([9b9be31](https://github.com/Saghen/blink.cmp/commit/9b9be318773dcce04f5017574fbe5ed638429852)) +* schedule non-expr fallback keymaps ([#196](https://github.com/Saghen/blink.cmp/issues/196)) ([1a55fd1](https://github.com/Saghen/blink.cmp/commit/1a55fd1e03193e10cb8bc866cc2bc47c9473061c)) +* sending erroneous fields to LSP on resolve ([e82c1b7](https://github.com/Saghen/blink.cmp/commit/e82c1b73607c4905582028e81bc40b10ce9eb8ea)) +* set default keymap to use accept ([7d265b4](https://github.com/Saghen/blink.cmp/commit/7d265b4a19f2c198eda06baf031cb0e41cc3095c)) +* snippet reload function ([407f2d5](https://github.com/Saghen/blink.cmp/commit/407f2d526fd07b651a8a7330df2c4fd05b32a014)) +* snippet resolve ([5d9fa1c](https://github.com/Saghen/blink.cmp/commit/5d9fa1c36cc9e43a9d7cd65ddcc417128a9d41c3)) + +## [0.4.1](https://github.com/Saghen/blink.cmp/compare/v0.4.0...v0.4.1) (2024-10-24) + +### Bug Fixes + +* check semantic token type ([0b493ff](https://github.com/Saghen/blink.cmp/commit/0b493ff3ce7fd8d318e7e1024fbadfe2ec3a624a)) +* exclude prefix including one char ([70438ac](https://github.com/Saghen/blink.cmp/commit/70438ac5016d3ab609f422d1ef084870cb9ceb29)) + +## [0.4.0](https://github.com/Saghen/blink.cmp/compare/v0.3.1...v0.4.0) (2024-10-24) + +> [!IMPORTANT] +> The sources configuration has been reworked, please see the README for the new schema + +### BREAKING CHANGES + +* rework sources config structure and available options +* rework sources system and configuration + +### Features + +* add extra space to ... on normal nerd font ([9b9647b](https://github.com/Saghen/blink.cmp/commit/9b9647bc23f52270ce43e579dda9b9eb5d00b7b8)) +* add nix build-plugin command, update devShell to not require ([32069be](https://github.com/Saghen/blink.cmp/commit/32069be108dda4cf2b0a7316a0be366398187003)) +* auto_show on autocomplete window ([#98](https://github.com/Saghen/blink.cmp/issues/98)) ([82e03b1](https://github.com/Saghen/blink.cmp/commit/82e03b1207b14845d8f29041e2f244693708425b)) +* **config:** add ignored filetypes option ([#108](https://github.com/Saghen/blink.cmp/issues/108)) ([b56a2b1](https://github.com/Saghen/blink.cmp/commit/b56a2b18804a9e4cce6450b886c33fb6b0a58e39)) +* custom documentation highlighting ([90d6394](https://github.com/Saghen/blink.cmp/commit/90d63948368982800ce886e93fc9c7d1c36cf74c)), closes [#113](https://github.com/Saghen/blink.cmp/issues/113) +* default to not showing in snippet ([49b033a](https://github.com/Saghen/blink.cmp/commit/49b033a11830652790603fe9b2a7e6275e626730)), closes [#131](https://github.com/Saghen/blink.cmp/issues/131) +* expose reload function for sources ([ff1f5fa](https://github.com/Saghen/blink.cmp/commit/ff1f5fa525312676f1aaba9b601bd0e13c52644b)), closes [#28](https://github.com/Saghen/blink.cmp/issues/28) +* expose typo resistance, update frizbee ([63b7b22](https://github.com/Saghen/blink.cmp/commit/63b7b2219f4595973225a13e6e664fc836ee305c)) +* **fuzzy:** lazy get lua properties ([5cc63f0](https://github.com/Saghen/blink.cmp/commit/5cc63f0f298ad31cab37d7b2a478e61a3601a768)) +* ignore empty fallback_for table ([9c9e0cc](https://github.com/Saghen/blink.cmp/commit/9c9e0cc78b0933c66916eaccc6a974608d2426de)), closes [#122](https://github.com/Saghen/blink.cmp/issues/122) +* ignore some characters at prefix of keyword ([569156f](https://github.com/Saghen/blink.cmp/commit/569156f432e67a4bd4dccee4fb9beafbf15a1d30)), closes [#135](https://github.com/Saghen/blink.cmp/issues/135) +* mark buffer completion items as plain text ([0f5f484](https://github.com/Saghen/blink.cmp/commit/0f5f484583b19484599b9dfb524180902d10e5b3)), closes [#148](https://github.com/Saghen/blink.cmp/issues/148) +* more robust preview for auto_insert mode ([6e15864](https://github.com/Saghen/blink.cmp/commit/6e158647bbc0628fe46c0175acf32d954c0c172f)), closes [#117](https://github.com/Saghen/blink.cmp/issues/117) +* notify user on json parsing error for snippets ([c5146a5](https://github.com/Saghen/blink.cmp/commit/c5146a5c23db48824fc7720f1265f3e75fc1c634)), closes [#132](https://github.com/Saghen/blink.cmp/issues/132) +* place cursor at first tab stop on snippet preview ([d3e8701](https://github.com/Saghen/blink.cmp/commit/d3e87015e891022a8fe36f43c60806c21f231cea)) +* re-enable typo resistance by default ([b35a559](https://github.com/Saghen/blink.cmp/commit/b35a559abea64a1b77abf69e8252ab5cd063868a)) +* reduce build-plugin command to minimal dependencies, add command to docs ([cfaf9fc](https://github.com/Saghen/blink.cmp/commit/cfaf9fc89a4b0ad36bba7a2605ae9b2f15efbf52)) +* remove snippet deduplication ([e296d8f](https://github.com/Saghen/blink.cmp/commit/e296d8ffcea78c400210d09c669359af89802618)), closes [#146](https://github.com/Saghen/blink.cmp/issues/146) +* rework sources config structure and available options ([e3a811b](https://github.com/Saghen/blink.cmp/commit/e3a811bc9bc9cf55de8bd7a7bcee84236e015dc2)), closes [#144](https://github.com/Saghen/blink.cmp/issues/144) +* rework sources system and configuration ([7fea65c](https://github.com/Saghen/blink.cmp/commit/7fea65c4e4c83d3b194a4d9e4d813204fd9f0ded)), closes [#144](https://github.com/Saghen/blink.cmp/issues/144) +* select_and_accept keymap ([6394508](https://github.com/Saghen/blink.cmp/commit/6394508b0c3f1f1f95e02c8057ccfc4b746dbd75)), closes [#118](https://github.com/Saghen/blink.cmp/issues/118) +* support detail only in doc window ([#147](https://github.com/Saghen/blink.cmp/issues/147)) ([57abdb8](https://github.com/Saghen/blink.cmp/commit/57abdb838cfcc624a87dba2b10f665c30e604b4e)) +* support LSP item defaults ([ffc4282](https://github.com/Saghen/blink.cmp/commit/ffc428208f292fa00cb7cced09d35de6e815ab55)) +* support LSPs with only full semantic tokens and cleanup ([0626cb5](https://github.com/Saghen/blink.cmp/commit/0626cb5446fd8d4ccfae53a93b648534ea1c7bf3)) +* support using suffix for fuzzy matching ([815f4df](https://github.com/Saghen/blink.cmp/commit/815f4dffa58d89d95c6f6e12133b0c1103fa0bdd)), closes [#88](https://github.com/Saghen/blink.cmp/issues/88) +* switch to mlua ([#105](https://github.com/Saghen/blink.cmp/issues/105)) ([873680d](https://github.com/Saghen/blink.cmp/commit/873680d16459d6747609804b08612f6a11f04591)) +* use textEditText as fallback for textEdit ([abcb2a0](https://github.com/Saghen/blink.cmp/commit/abcb2a0dab207e03d382acf62074f639a3572e20)) +* **windows:** add support for individual border character highlights ([#175](https://github.com/Saghen/blink.cmp/issues/175)) ([3c1a502](https://github.com/Saghen/blink.cmp/commit/3c1a5020ab9a993e8bc8c5c05d29588747f00b78)) + +### Bug Fixes + +* add back undo text edit for accept ([f62046a](https://github.com/Saghen/blink.cmp/commit/f62046a775605f597f2b370672d05ee0c9123142)) +* add missing select_and_accept keymap to config ([d2140dc](https://github.com/Saghen/blink.cmp/commit/d2140dc7615991ea88fa1fd75dd4fccb53a73e25)) +* always hide window on accept ([7f5a3d9](https://github.com/Saghen/blink.cmp/commit/7f5a3d9a820125e7da0a1816efaddb84d47a7f18)) +* auto insert breaking on single line text edit ([78ac56e](https://github.com/Saghen/blink.cmp/commit/78ac56e96144ed7475bb6d11981d3c8154bfd366)), closes [#169](https://github.com/Saghen/blink.cmp/issues/169) +* check if item contains brackets before defering to semantic token ([e5f543d](https://github.com/Saghen/blink.cmp/commit/e5f543da2a0ce91c8720b67f0ea6cfa941dc26d6)) +* **config:** set correct type def for blink.cmp.WindowBorderChar ([516190b](https://github.com/Saghen/blink.cmp/commit/516190bcdafa387d417cfb235cbcd7385e902089)) +* don't show completions when trigger context is nil ([5b39d83](https://github.com/Saghen/blink.cmp/commit/5b39d83ac4fed46c57d8db987ea56cb1c0e68b0e)) +* drop prints ([89259f9](https://github.com/Saghen/blink.cmp/commit/89259f936e413e0a324b2ea369eb8ccefc05a14f)), closes [#179](https://github.com/Saghen/blink.cmp/issues/179) +* drop prints ([67fa41f](https://github.com/Saghen/blink.cmp/commit/67fa41f0f0501beb64d4acc8678f8788331a470e)) +* frizbee not matching on capital letters ([722b41b](https://github.com/Saghen/blink.cmp/commit/722b41b0a7028581004888623f3f79c1d9eab8b8)), closes [#162](https://github.com/Saghen/blink.cmp/issues/162) [#173](https://github.com/Saghen/blink.cmp/issues/173) +* fuzzy get query returning extra characters ([b2380a0](https://github.com/Saghen/blink.cmp/commit/b2380a0301e4385e4964cd57e790d6ce169b2b71)), closes [#170](https://github.com/Saghen/blink.cmp/issues/170) [#172](https://github.com/Saghen/blink.cmp/issues/172) [#176](https://github.com/Saghen/blink.cmp/issues/176) +* fuzzy panic on too many items ([1e6dcbf](https://github.com/Saghen/blink.cmp/commit/1e6dcbffbe224fa10ef9fab490ad07dbd9dd19b0)) +* handle treesitter get_parser failure ([fe68c28](https://github.com/Saghen/blink.cmp/commit/fe68c288268f01d1e3b7e692abac2e1fb2093d78)), closes [#171](https://github.com/Saghen/blink.cmp/issues/171) +* item defaults not being applied ([42f8efb](https://github.com/Saghen/blink.cmp/commit/42f8efb43bed968050fcb8feb5fad4b6b27a9b05)), closes [#158](https://github.com/Saghen/blink.cmp/issues/158) +* missing access function ([d11f271](https://github.com/Saghen/blink.cmp/commit/d11f271ddd980e05545627093924e8359c5416b3)) +* only show autocomplete window on select if auto_show is disabled ([fa64556](https://github.com/Saghen/blink.cmp/commit/fa6455635b8f12504e2e892fd8ce8926e679cf68)) +* re-enable memory check for now ([6b24f48](https://github.com/Saghen/blink.cmp/commit/6b24f484d56eeb9b48cb4ce6b58481cdfc50a3bd)) +* remove debug prints ([9846c2d](https://github.com/Saghen/blink.cmp/commit/9846c2d2bfdeaa3088c9c0143030524402fffdf9)) +* select always triggering when auto_show enabled ([db635f2](https://github.com/Saghen/blink.cmp/commit/db635f201f5ac5f48e7e33cc268f23e7646fd946)) +* select_and_accept not working with auto_insert ([65eb336](https://github.com/Saghen/blink.cmp/commit/65eb336f6c33964becacfbfc850f18d6e3cd5581)), closes [#118](https://github.com/Saghen/blink.cmp/issues/118) +* signature window no longer overlaps cursor ([#149](https://github.com/Saghen/blink.cmp/issues/149)) ([7d6b50b](https://github.com/Saghen/blink.cmp/commit/7d6b50b140eadb51d1bf59b93a02293333a59519)) +* single char non-matching keyword ([3cb084c](https://github.com/Saghen/blink.cmp/commit/3cb084cc4e3cc989895d0eabf0ccc690c15d19ed)), closes [#141](https://github.com/Saghen/blink.cmp/issues/141) +* skip treesitter hl on nil lang ([cb9397c](https://github.com/Saghen/blink.cmp/commit/cb9397c89104ab09d85ba5ce1b40852635f9b142)) +* temporary workaround for insertReplaceEdit ([c218faf](https://github.com/Saghen/blink.cmp/commit/c218fafbf275725532f3cf2eaebdf863b958d48e)), closes [#178](https://github.com/Saghen/blink.cmp/issues/178) +* typo in signature.win ([#125](https://github.com/Saghen/blink.cmp/issues/125)) ([69ad25f](https://github.com/Saghen/blink.cmp/commit/69ad25f38e1eb833b7aa5a0efb2d6c485e191149)) +* use treesitter.language.get_lang when choosing parser ([213fd94](https://github.com/Saghen/blink.cmp/commit/213fd94de2ab83ff409e1fd240625959bf61624e)), closes [#133](https://github.com/Saghen/blink.cmp/issues/133) +* window positioning with folds ([819b978](https://github.com/Saghen/blink.cmp/commit/819b978328b244fc124cfcd74661b2a7f4259f4f)), closes [#95](https://github.com/Saghen/blink.cmp/issues/95) + +## [0.3.1](https://github.com/Saghen/blink.cmp/compare/v0.3.0...v0.3.1) (2024-10-14) + +### Bug Fixes + +* **ci:** use correct file ext for windows ([af68874](https://github.com/Saghen/blink.cmp/commit/af68874f1b2e628e0c72ec27f5225d0c6b2d6820)) + +## [0.3.0](https://github.com/Saghen/blink.cmp/compare/v0.2.1...v0.3.0) (2024-10-14) + +### BREAKING CHANGES + +* implement auto-insert option (#65) +* autocompletion window components alignment (#51) +* disable auto_show documentation by default, use <C-space> to toggle + +### Features + +* add back min_width to autocomplete ([9a008c9](https://github.com/Saghen/blink.cmp/commit/9a008c942f180a23671f598ed9680770b254a599)) +* add basic event trigger API ([#31](https://github.com/Saghen/blink.cmp/issues/31)) ([127f518](https://github.com/Saghen/blink.cmp/commit/127f51827cc038aab402abc6bacf9862dd2d72ad)) +* add detail to documentation window ([#33](https://github.com/Saghen/blink.cmp/issues/33)) ([588e4d4](https://github.com/Saghen/blink.cmp/commit/588e4d4a7e42bae0e26c82ebc1ea3c68fa4e7cf0)) +* add health.lua and basic healthchecks ([#101](https://github.com/Saghen/blink.cmp/issues/101)) ([a12617d](https://github.com/Saghen/blink.cmp/commit/a12617d1eb69484d2656ccc40c40e8254b1ea3ec)) +* add minimal render style ([#85](https://github.com/Saghen/blink.cmp/issues/85)) ([b4bbad1](https://github.com/Saghen/blink.cmp/commit/b4bbad181b0e1b9cdf1025b790cf720d707a8c26)) +* added a preselect option to the cmp menu ([#24](https://github.com/Saghen/blink.cmp/issues/24)) ([1749e32](https://github.com/Saghen/blink.cmp/commit/1749e32c524dc1815fe4abbad0b33439316c4596)) +* apply keymap on InsertEnter ([340370d](https://github.com/Saghen/blink.cmp/commit/340370d526996b99ff75c1858294f15502af0179)), closes [#37](https://github.com/Saghen/blink.cmp/issues/37) +* **ci:** support windows pre-built binaries ([#100](https://github.com/Saghen/blink.cmp/issues/100)) ([b378d50](https://github.com/Saghen/blink.cmp/commit/b378d5022743e56dc450ab1b6a75ab03de36f86b)) +* disable auto_show documentation by default, use <C-space> to toggle ([84361bd](https://github.com/Saghen/blink.cmp/commit/84361bdbd9e9ab2a7c06b0f458c2829cef46348d)) +* don't search forward when guessing text edit ([a7e1acc](https://github.com/Saghen/blink.cmp/commit/a7e1acc1ed9b0ad004af124bcb6c7d71a7eb5378)), closes [#58](https://github.com/Saghen/blink.cmp/issues/58) +* drop source groups in favor of fallback_for ([#83](https://github.com/Saghen/blink.cmp/issues/83)) ([1f0c0f3](https://github.com/Saghen/blink.cmp/commit/1f0c0f349488f5138757abec2d327ac6c143a4f0)) +* expose source provider config to sources ([deba523](https://github.com/Saghen/blink.cmp/commit/deba523406f45eb0a227c33d57a8d75a79abb4cf)) +* ignore repeated call at cursor position in trigger ([4883420](https://github.com/Saghen/blink.cmp/commit/48834207c143f5e84d7a71cd250b2049ec0a6d8c)) +* implement auto-insert option ([#65](https://github.com/Saghen/blink.cmp/issues/65)) ([1df7d33](https://github.com/Saghen/blink.cmp/commit/1df7d33e930c042dc91287a02a97e1ccf8a92d5d)) +* make fuzzy secondary min_score more lenient ([b330b61](https://github.com/Saghen/blink.cmp/commit/b330b61ffac753be3f1257eda92e1596c0ab3174)) +* stylize markdown in documentation window ([05229dd](https://github.com/Saghen/blink.cmp/commit/05229ddc2fd1695c979e2807aa96842978dd4779)) +* use faster shallow_copy for context ([98575f0](https://github.com/Saghen/blink.cmp/commit/98575f054db18bc763100b8d14a9eae0417209d5)) + +### Bug Fixes + +* accept replacing first char in line ([655d2ee](https://github.com/Saghen/blink.cmp/commit/655d2ee2673950451a491294fd3ce7e17cfb0a24)), closes [#38](https://github.com/Saghen/blink.cmp/issues/38) +* add union to utils ([88f71b1](https://github.com/Saghen/blink.cmp/commit/88f71b16ecd650775516bd2b30ab808283b7242c)) +* autocomplete positioning on first char in line ([7afb06c](https://github.com/Saghen/blink.cmp/commit/7afb06ca9962e3670b5ed01e7301709a53917edd)) +* autocompletion window components alignment ([#51](https://github.com/Saghen/blink.cmp/issues/51)) ([a4f5f8e](https://github.com/Saghen/blink.cmp/commit/a4f5f8eef9182515050d94d54f4c2bb97767987b)) +* binary symlink in flake only working on Linux ([#93](https://github.com/Saghen/blink.cmp/issues/93)) ([fc5feb8](https://github.com/Saghen/blink.cmp/commit/fc5feb887f3f379fff0756b2be2a35c8aa841a44)) +* check if LSP supports resolve provider ([957a57a](https://github.com/Saghen/blink.cmp/commit/957a57a9d3d90c1a9974b9af66f4a9a1f80fdb5f)), closes [#48](https://github.com/Saghen/blink.cmp/issues/48) +* close completion if the accepted item matches the current word ([2f1b85b](https://github.com/Saghen/blink.cmp/commit/2f1b85bc4f15e2f3660550ef92161a93482f2fd8)), closes [#41](https://github.com/Saghen/blink.cmp/issues/41) +* close completion window on ctrl+c in insert mode ([#63](https://github.com/Saghen/blink.cmp/issues/63)) ([e695c79](https://github.com/Saghen/blink.cmp/commit/e695c798b2d53a429f2f3ba1551a21ae2c4dc11a)) +* **config:** make blink lua config fields optional ([#18](https://github.com/Saghen/blink.cmp/issues/18)) ([9c73b0d](https://github.com/Saghen/blink.cmp/commit/9c73b0dc8c158c7162092258177ff8a03aa2919b)) +* context not clearing on trigger character, path regexes ([15cb871](https://github.com/Saghen/blink.cmp/commit/15cb871d1f8c52050a0fcd07d31115f3a63cf20c)), closes [#16](https://github.com/Saghen/blink.cmp/issues/16) +* correctly handle non-blink keymaps with string rhs ([#78](https://github.com/Saghen/blink.cmp/issues/78)) ([1ad59aa](https://github.com/Saghen/blink.cmp/commit/1ad59aa6ab142c19508ee6ed222b73a3ffd13521)) +* disable blink.cmp remaps when telescope prompt is open ([#104](https://github.com/Saghen/blink.cmp/issues/104)) ([7f2f74f](https://github.com/Saghen/blink.cmp/commit/7f2f74fe037ccad1a573e3d42f114d0d23b954d8)), closes [#102](https://github.com/Saghen/blink.cmp/issues/102) +* documentation manual trigger not updating on scroll ([cd15078](https://github.com/Saghen/blink.cmp/commit/cd15078763946522dddbb818803e99b2b321e742)) +* documentation of snippet cannot be shown when description is list ([#92](https://github.com/Saghen/blink.cmp/issues/92)) ([f99bf6b](https://github.com/Saghen/blink.cmp/commit/f99bf6bdabadc2b47fd7355ae2af912a30b9c3cc)) +* don't initialize first_fill with 1 ([#87](https://github.com/Saghen/blink.cmp/issues/87)) ([526f786](https://github.com/Saghen/blink.cmp/commit/526f786a8658f99dff36013b4e31d1f7e6b0a56b)) +* double send on append on trigger character ([ebbce90](https://github.com/Saghen/blink.cmp/commit/ebbce90400ea1ed3e14fdec88fdef59c0185ad46)), closes [#25](https://github.com/Saghen/blink.cmp/issues/25) +* enable kind auto brackets for TS/JS ([808f628](https://github.com/Saghen/blink.cmp/commit/808f628713ae78665511be42b2c054c92208a00e)) +* expand vars in snippets for insertText ([ce337cb](https://github.com/Saghen/blink.cmp/commit/ce337cb95f2172070c0e9333e3439eb20ae4c72a)), closes [#27](https://github.com/Saghen/blink.cmp/issues/27) +* **ffi:** handle cargo library naming conventions for windows binaries ([#74](https://github.com/Saghen/blink.cmp/issues/74)) ([e9493c6](https://github.com/Saghen/blink.cmp/commit/e9493c6aa4942da7e3a62c118195ea07df815dc2)) +* guess text edit once and for all ([fc348da](https://github.com/Saghen/blink.cmp/commit/fc348dac16f190042d20aee62ea61b66c7c1380a)) +* handle empty table in additionalTextEdits ([#99](https://github.com/Saghen/blink.cmp/issues/99)) ([65e9605](https://github.com/Saghen/blink.cmp/commit/65e9605924ff774fb3612441a1d18737b5c9f58a)) +* handle newlines in autocomplete suggestions ([#110](https://github.com/Saghen/blink.cmp/issues/110)) ([c39227a](https://github.com/Saghen/blink.cmp/commit/c39227adfaf66939b6a319bb1ed43d9ade5bbd9b)) +* passthrough bind on show/hide when shown/hidden ([a5145ae](https://github.com/Saghen/blink.cmp/commit/a5145ae69ef2d4193574a09fcd50bea20481f516)), closes [#49](https://github.com/Saghen/blink.cmp/issues/49) +* re-enable preselect by default ([64673ea](https://github.com/Saghen/blink.cmp/commit/64673ea454f46664ac6f6545f5d3577fd27421e9)) +* replace keycodes on callback alternate mappings ([df5c0de](https://github.com/Saghen/blink.cmp/commit/df5c0de57b443545d4fe04cff9cd97ca3d20bbbf)), closes [#47](https://github.com/Saghen/blink.cmp/issues/47) +* respect autocomplete min_width ([#86](https://github.com/Saghen/blink.cmp/issues/86)) ([c15aefe](https://github.com/Saghen/blink.cmp/commit/c15aefeea77345b21ed79cb9defc322ae19f7eda)) +* signature window failing when trigger context empty ([6a21d7c](https://github.com/Saghen/blink.cmp/commit/6a21d7c12d7186313e0dea2c04d0dd63b6534115)) +* snippet keymaps not applying in insert ([a89ae20](https://github.com/Saghen/blink.cmp/commit/a89ae200840de7661eb92d8ae202b279ecc56da9)), closes [#70](https://github.com/Saghen/blink.cmp/issues/70) +* snippet source markdown generation ([a6cf72a](https://github.com/Saghen/blink.cmp/commit/a6cf72ae58362c126f91993326b5c8b43366eb7f)) +* snippets source expanding vars ([5ffd608](https://github.com/Saghen/blink.cmp/commit/5ffd608dc4cd4df8fcfe43b366c5960f05056e45)) +* strip blink fields from lsp items for resolve ([ab99b02](https://github.com/Saghen/blink.cmp/commit/ab99b02f4b5c378c7c79e5d24954d00450e78f1b)) +* union_keys not using pairs ([8c2cb2e](https://github.com/Saghen/blink.cmp/commit/8c2cb2efb63411499f6746ae7be34e2b0a581bad)) +* update ffi.lua ([27903be](https://github.com/Saghen/blink.cmp/commit/27903bef41bc745c4d5419e86ca5bf09ed538f2b)) +* use correct prev/next keymap ([#53](https://github.com/Saghen/blink.cmp/issues/53)) ([f456c2a](https://github.com/Saghen/blink.cmp/commit/f456c2aa0994f709f9aec991ed2b4b705f787e48)), closes [/github.com/Saghen/blink.cmp/pull/23#issuecomment-2399876619](https://github.com/Saghen//github.com/Saghen/blink.cmp/pull/23/issues/issuecomment-2399876619) +* use empty separator for joining snippet description ([28f3a31](https://github.com/Saghen/blink.cmp/commit/28f3a316de01fc4a14c67689b0547428499e933d)) +* use internal CompletionItemKind table ([4daf96d](https://github.com/Saghen/blink.cmp/commit/4daf96d76e06d6c248587f860ddb5717ced9bbd3)), closes [#17](https://github.com/Saghen/blink.cmp/issues/17) + +## [0.2.1](https://github.com/Saghen/blink.cmp/compare/v0.2.0...v0.2.1) (2024-10-08) + +### Features + +* cycle completions ([#12](https://github.com/Saghen/blink.cmp/issues/12)) ([d20e34d](https://github.com/Saghen/blink.cmp/commit/d20e34d8c87925bd27dff12961588459f649cd92)) + +### Bug Fixes + +* autocomplete window positioning with borders ([ba62bda](https://github.com/Saghen/blink.cmp/commit/ba62bda5af9b5f2a8accb102eb4791fab94e2a90)), closes [#29](https://github.com/Saghen/blink.cmp/issues/29) +* check server capabilities ([#5](https://github.com/Saghen/blink.cmp/issues/5)) ([8d2615d](https://github.com/Saghen/blink.cmp/commit/8d2615d00a9892647a6d0e0e564b781a4e6afabe)) +* keymaps not replacing keycodes ([5dd7d66](https://github.com/Saghen/blink.cmp/commit/5dd7d667228e3a98d01146db7c4461f42644d0c1)) +* keymaps replacing buffer local bindings ([506ea74](https://github.com/Saghen/blink.cmp/commit/506ea74e53a825cc6efd40a46c4129576409e440)), closes [#39](https://github.com/Saghen/blink.cmp/issues/39) +* use buffer-local keymaps ([ecb3510](https://github.com/Saghen/blink.cmp/commit/ecb3510ef2132956fb2df3dcc927e0f84d1a1c1d)), closes [#20](https://github.com/Saghen/blink.cmp/issues/20) +* use correct prev/next keymap for k and j ([#23](https://github.com/Saghen/blink.cmp/issues/23)) ([43e7532](https://github.com/Saghen/blink.cmp/commit/43e753228fe4a722e29d4953cad74a61728183cb)) + +## [0.2.0](https://github.com/Saghen/blink.cmp/compare/v0.1.0...v0.2.0) (2024-10-07) + +### Features + +* blink cmp specific winhighlights and highlights ([a034865](https://github.com/Saghen/blink.cmp/commit/a034865d585800503a61995a850ecb622a3d36cc)) +* check for brackets in front of item ([2c6ee0d](https://github.com/Saghen/blink.cmp/commit/2c6ee0d5fa32e286255a4ca119ff74713676bf60)) +* custom drawing support ([3e55028](https://github.com/Saghen/blink.cmp/commit/3e550286534e68cff42f96747e58db0610f7b4b5)) +* customizable undo point ([876707f](https://github.com/Saghen/blink.cmp/commit/876707f214e7ca0875e05eae45b876396b6c33fb)) +* introduce customizable winhighlight for autocomplete and documentation windows ([1a9cb7a](https://github.com/Saghen/blink.cmp/commit/1a9cb7ac70a912689ab09b96c6f9e75c888faed6)) +* support keyword_length on sources ([77080a5](https://github.com/Saghen/blink.cmp/commit/77080a529f88064e0ff04ef69b08fbe7445bcd0d)) + +### Bug Fixes + +* autocomplete window placement ([4e9d7ca](https://github.com/Saghen/blink.cmp/commit/4e9d7ca62c83c3ff2d64e925aab4dac10266f33b)) +* frecency access scoring ([e736972](https://github.com/Saghen/blink.cmp/commit/e73697265ff9091c9cca3db060be60d8e3962c5e)) +* misc ([5f4db7a](https://github.com/Saghen/blink.cmp/commit/5f4db7a1507dcca3b0f4d4fbeaef1f42262aea8f)) +* path completions ([6a5cf05](https://github.com/Saghen/blink.cmp/commit/6a5cf05c704a42cfbfa3009d3ac8e727637567b8)) +* respect min/max width for autocomplete window rendering ([0843884](https://github.com/Saghen/blink.cmp/commit/08438846b8016a9457c3234f4066655dc62b97a0)) +* signature trigger config ([cf9e4aa](https://github.com/Saghen/blink.cmp/commit/cf9e4aaf778f56d2dda8f43c30cb68762aecc425)) +* signature window showing up after context deleted ([857b336](https://github.com/Saghen/blink.cmp/commit/857b336ccdc5a389564e6e2b58571bc07c5cce32)) +* window placement with border ([d6a81d3](https://github.com/Saghen/blink.cmp/commit/d6a81d320f8880e219a3f937ecea1f78aca680e3)) + +## [0.1.0](https://github.com/Saghen/blink.cmp/compare/1b282880e699be37c3719308d6660a68d9081b14...v0.1.0) (2024-10-05) + +### Features + +* .local/state db location and misc ([bf76a01](https://github.com/Saghen/blink.cmp/commit/bf76a01482f6a3f7e019d0050df73ccf8ad93cf6)) +* accept and auto brackets config ([fd32689](https://github.com/Saghen/blink.cmp/commit/fd32689fbd07b953a54b61cc871f714c9dd004d5)) +* add back to repo ([24422f2](https://github.com/Saghen/blink.cmp/commit/24422f2341acf6ebdf7c9bc798cd81fbf5029d03)) +* add documentation keymaps ([e248579](https://github.com/Saghen/blink.cmp/commit/e248579b5cfe939048b613de7a7cdfcb884cd078)) +* auto brackets support ([7203d51](https://github.com/Saghen/blink.cmp/commit/7203d5195970f300ee5529a8427060ee1db9ae41)) +* basic snippet expansion and text edit support ([451dd9e](https://github.com/Saghen/blink.cmp/commit/451dd9eeaa37f6c5598bf7293b8da2bfdfe9162e)) +* better documentation window positioning ([a7ee523](https://github.com/Saghen/blink.cmp/commit/a7ee523978ba653b6eb4d9bac2af1d70f6e89f7b)) +* complete rework ([1efdc8a](https://github.com/Saghen/blink.cmp/commit/1efdc8a0ff38d3f1ff89acd9b04aa14844f50e42)) +* consolidate context table ([ad9ba28](https://github.com/Saghen/blink.cmp/commit/ad9ba28d0a8c1fda91e24e33d29b8013dd4c760a)) +* drop performance logging ([2974bc0](https://github.com/Saghen/blink.cmp/commit/2974bc0569b2d611ce399a733753c90f6ab61a9d)) +* dynamic cmp and doc window width ([6b78c89](https://github.com/Saghen/blink.cmp/commit/6b78c89276f8a520b4b802c8893f30e0ee7a5c82)) +* enable path source by default ([e7362c0](https://github.com/Saghen/blink.cmp/commit/e7362c0786ae889b738c9f1f34a312d834005d37)) +* hack around LSPs returning filtered items ([b58a382](https://github.com/Saghen/blink.cmp/commit/b58a382640f2ddfe0b07ce70439e935e49e39e36)) +* handle no items in source provider ([82106a4](https://github.com/Saghen/blink.cmp/commit/82106a482e899c27d3fa830aa7f65c020848fc68)) +* immediate fuzzy on keystroke ([1d3d54f](https://github.com/Saghen/blink.cmp/commit/1d3d54f20f2412e33975db88a60c6f2c148e7903)) +* implement snippets without deps ([37dbee4](https://github.com/Saghen/blink.cmp/commit/37dbee453dc2655a0a0e74d9b95ee00c08a8cf32)) +* init flake ([87e0416](https://github.com/Saghen/blink.cmp/commit/87e041699169d4f837c5f430e26756f0c2f76623)) +* initial ([1b28288](https://github.com/Saghen/blink.cmp/commit/1b282880e699be37c3719308d6660a68d9081b14)) +* initial configuration support ([b101fc1](https://github.com/Saghen/blink.cmp/commit/b101fc117c4f161f78c1783391f8719c619d15a5)) +* keymaps in config ([d6bad7b](https://github.com/Saghen/blink.cmp/commit/d6bad7bca485ffe6c254daf1e3d2df581b37eebc)) +* lock position to context start ([6ee55d4](https://github.com/Saghen/blink.cmp/commit/6ee55d4e2d938b138246e6ab11adbb320b19f7e7)) +* maintain window on immediate new context while deleting ([4d1b785](https://github.com/Saghen/blink.cmp/commit/4d1b7854c2d4c373cfdc027074aa305927c5414a)) +* min score on fuzzy results, avoid trimming valid items ([14a014d](https://github.com/Saghen/blink.cmp/commit/14a014dce49e658f5eed32853c9241d7869bc5dd)) +* misc ([e8372ab](https://github.com/Saghen/blink.cmp/commit/e8372abf86861a8fde4612257a4f9586626ad05f)) +* multi-repo setup based on mini.nvim ([15e808b](https://github.com/Saghen/blink.cmp/commit/15e808b70704e5f909305c487cb7fbfd5a95fc46)) +* nerd font variant and misc cleanup ([6571c96](https://github.com/Saghen/blink.cmp/commit/6571c96b3aede5ae4f37b3d4999a4ac374593910)) +* nvim cmp as default highlight ([b93a5e3](https://github.com/Saghen/blink.cmp/commit/b93a5e3476b42fc8f79bdeb6fc0f5e0ca8b4bc68)) +* pre-built binary download support, misc refactors ([b1004ab](https://github.com/Saghen/blink.cmp/commit/b1004ab8c23656a5cb3d20b67bc3e5485f818ade)) +* put context via wrapper ([f5d4dae](https://github.com/Saghen/blink.cmp/commit/f5d4dae67c31c2239805187f6351a4dc99259e26)) +* reenable auto_show for documentation ([f1f7de4](https://github.com/Saghen/blink.cmp/commit/f1f7de496fa653518dea34bfe0446d0babac7d4e)) +* rework path source ([5787816](https://github.com/Saghen/blink.cmp/commit/5787816e5e28d1c61803552008545abc851505eb)) +* rework sources again ([7568de9](https://github.com/Saghen/blink.cmp/commit/7568de938a49a26cebf39369e39211f1c959cd9c)) +* rework sources system ([3ee91b5](https://github.com/Saghen/blink.cmp/commit/3ee91b50e7dfc0340b723ae06c4a06f9c8e1e437)) +* show on insert on trigger character ([a9ff243](https://github.com/Saghen/blink.cmp/commit/a9ff243cf0271904708b8a6ef6bf3150238cbc2d)) +* signature help and misc ([fbfdf29](https://github.com/Saghen/blink.cmp/commit/fbfdf2906ea145f4faaf94865eeb40bb30dd8db2)) +* smarter caching, misc fixes ([3f1c8bd](https://github.com/Saghen/blink.cmp/commit/3f1c8bd81b9499345fa50e3707fa127a58160062)) +* smarter fuzzy, drop logging ([6b09eaa](https://github.com/Saghen/blink.cmp/commit/6b09eaa8f47d9bba971a0fd1e8a9e93263bb69e1)) +* sort _ items last ([210f21f](https://github.com/Saghen/blink.cmp/commit/210f21fe73c150253b3dd1529852522ee47b23d3)) +* source should_show, windowing config/fixes, misc ([3d1c168](https://github.com/Saghen/blink.cmp/commit/3d1c1688c6888df50069d50b07e9641f53394ce0)) +* update flake to reflect merge with mono-repo ([aa80347](https://github.com/Saghen/blink.cmp/commit/aa80347f93fb95df2ca98be2c03116a27c554e04)) +* use naersk to simplify build, remove unused inputs ([5579688](https://github.com/Saghen/blink.cmp/commit/55796882a7354f9dbbea5997285e1cd4e92df905)) +* use remote fzrs for build ([04d5647](https://github.com/Saghen/blink.cmp/commit/04d5647009d74e6c14050d094e9d66cd1ace0b5a)) +* WIP sources rework ([ad347a1](https://github.com/Saghen/blink.cmp/commit/ad347a165d2f7e3030b0fe44261e4624d9826134)) + +### Bug Fixes + +* a lot ([8a599ba](https://github.com/Saghen/blink.cmp/commit/8a599ba6725cc0892f6d0155fbbb4bd51a02c9d5)) +* accept auto brackets ([3927e23](https://github.com/Saghen/blink.cmp/commit/3927e23926ef05fc729b065c771de6ee293a587f)) +* add version to pkg ([8983597](https://github.com/Saghen/blink.cmp/commit/89835978d6f6d820abb398ed01a32fcb69a10232)) +* avoid immediately showing on context change ([632e6ac](https://github.com/Saghen/blink.cmp/commit/632e6ac9f3ca7ad2f78fd5f5c100e9437fccf845)) +* avoid setting filetype for preview for now ([32ef1b9](https://github.com/Saghen/blink.cmp/commit/32ef1b9e79a85e9cccee274171c07034ac1a3fc3)) +* buffer response context ([4650a35](https://github.com/Saghen/blink.cmp/commit/4650a35d058aba78c8c59b0aad44f0a13a08a287)) +* cancel signature help request on hide ([b1fdee5](https://github.com/Saghen/blink.cmp/commit/b1fdee5277aba73791a1c991a51df7ac940d4321)) +* documentation delays ([01d5fd0](https://github.com/Saghen/blink.cmp/commit/01d5fd0fc3863e0cd2c9eb53739a369ed1ca4a4e)) +* keymap and simplify ([0924c8a](https://github.com/Saghen/blink.cmp/commit/0924c8a9d64121677f4ed165d4fe65b3ccb8a3ff)) +* keymaps ([863bad7](https://github.com/Saghen/blink.cmp/commit/863bad7d66d616b6498c7d9ba59249138517689a)) +* lazy.nvim loading ([9115fc2](https://github.com/Saghen/blink.cmp/commit/9115fc2e1dfa64d9ed974d11bc3562e8fdf67449)) +* maintain autocomplete pos when scrolling/resizing ([a720117](https://github.com/Saghen/blink.cmp/commit/a720117e49c47e9c45981b419427f5d636118338)) +* plugin paths ([ae4aeae](https://github.com/Saghen/blink.cmp/commit/ae4aeae0a32fde09ce64b2bb9b056ef2f1f50ad2)) +* proximity and frecency bonus ([7bb4000](https://github.com/Saghen/blink.cmp/commit/7bb40005fcfc713d04db1e33461170bc012344ed)) +* reference correct signature window ([30855cd](https://github.com/Saghen/blink.cmp/commit/30855cde1e9b76c351133411fad15c0f13d1dcd8)) +* remove debug prints ([013dc02](https://github.com/Saghen/blink.cmp/commit/013dc0276677741f7f8c1b436303355b975a0e73)) +* remove references to removed inputs ([d69b4d1](https://github.com/Saghen/blink.cmp/commit/d69b4d1c6866455d443bb9ca9dac45b1235d0757)) +* set pname instead of name ([addf204](https://github.com/Saghen/blink.cmp/commit/addf204b58014cd8a87b9af57e38406b896128ce)) +* snippets items ([d8a593d](https://github.com/Saghen/blink.cmp/commit/d8a593db311d83e5d8cf8db0a5ada01e92e88b16)) +* sources trigger character blocklist ([69d3854](https://github.com/Saghen/blink.cmp/commit/69d38546f166fff074d3e5458b0625653d7e2e91)) +* trigger, docs, so much stuff ([fae11d1](https://github.com/Saghen/blink.cmp/commit/fae11d16bb4efac3b74f84040b1f50776e4d55cb)) +* update package build dir to cmp/fuzzy ([13203e3](https://github.com/Saghen/blink.cmp/commit/13203e3cb0a9196635243d0b47c33a9cb7c1326c)) diff --git a/mut/neovim/pack/plugins/start/blink.cmp/Cargo.lock b/mut/neovim/pack/plugins/start/blink.cmp/Cargo.lock new file mode 100644 index 0000000..2165ecc --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/Cargo.lock @@ -0,0 +1,906 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "blink-cmp-fuzzy" +version = "0.1.0" +dependencies = [ + "frizbee", + "heed", + "lazy_static", + "mlua", + "regex", + "serde", +] + +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "doxygen-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" +dependencies = [ + "phf", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "frizbee" +version = "0.1.0" +source = "git+https://github.com/saghen/frizbee#7c2aa4661a43c6f8565c2200a07428bdf675ce1b" +dependencies = [ + "memchr", + "smith_waterman_macro", +] + +[[package]] +name = "heed" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd54745cfacb7b97dee45e8fdb91814b62bccddb481debb7de0f9ee6b7bf5b43" +dependencies = [ + "bitflags", + "byteorder", + "heed-traits", + "heed-types", + "libc", + "lmdb-master-sys", + "once_cell", + "page_size", + "serde", + "synchronoise", + "url", +] + +[[package]] +name = "heed-traits" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" + +[[package]] +name = "heed-types" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" +dependencies = [ + "bincode", + "byteorder", + "heed-traits", + "serde", + "serde_json", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lmdb-master-sys" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "472c3760e2a8d0f61f322fb36788021bb36d573c502b50fa3e2bcaac3ec326c9" +dependencies = [ + "cc", + "doxygen-rs", + "libc", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mlua" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea43c3ffac2d0798bd7128815212dd78c98316b299b7a902dabef13dc7b6b8d" +dependencies = [ + "bstr", + "either", + "mlua-sys", + "mlua_derive", + "num-traits", + "parking_lot", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a11d485edf0f3f04a508615d36c7d50d299cf61a7ee6d3e2530651e0a31771" +dependencies = [ + "cc", + "cfg-if", + "pkg-config", +] + +[[package]] +name = "mlua_derive" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870d71c172fcf491c6b5fb4c04160619a2ee3e5a42a1402269c66bcbf1dd4deb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "serde_json" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "smith_waterman_macro" +version = "0.1.0" +source = "git+https://github.com/saghen/frizbee#7c2aa4661a43c6f8565c2200a07428bdf675ce1b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synchronoise" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" +dependencies = [ + "crossbeam-queue", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] diff --git a/mut/neovim/pack/plugins/start/blink.cmp/Cargo.toml b/mut/neovim/pack/plugins/start/blink.cmp/Cargo.toml new file mode 100644 index 0000000..55aa6ba --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "blink-cmp-fuzzy" +version = "0.1.0" +edition = "2021" + +[lib] +path = "lua/blink/cmp/fuzzy/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +regex = "1.11.1" +lazy_static = "1.5.0" +frizbee = { git = "https://github.com/saghen/frizbee" } +serde = { version = "1.0.216", features = ["derive"] } +heed = "0.21.0" +mlua = { version = "0.10.2", features = ["module", "luajit"] } diff --git a/mut/neovim/pack/plugins/start/blink.cmp/LICENSE b/mut/neovim/pack/plugins/start/blink.cmp/LICENSE new file mode 100644 index 0000000..81d22ed --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Liam Dyer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mut/neovim/pack/plugins/start/blink.cmp/README.md b/mut/neovim/pack/plugins/start/blink.cmp/README.md new file mode 100644 index 0000000..be1f6d0 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/README.md @@ -0,0 +1,40 @@ +> [!WARNING] +> This plugin is _beta_ quality. Expect breaking changes and many bugs + +# Blink Completion (blink.cmp) + +**blink.cmp** is a completion plugin with support for LSPs and external sources that updates on every keystroke with minimal overhead (0.5-4ms async). It use a [custom SIMD fuzzy searcher](https://github.com/saghen/frizbee) to easily handle >20k items. It provides extensibility via hooks into the trigger, sources and rendering pipeline. Plenty of work has been put into making each stage of the pipeline as intelligent as possible, such as frecency and proximity bonus on fuzzy matching, and this work is on-going. + +<https://github.com/user-attachments/assets/9849e57a-3c2c-49a8-959c-dbb7fef78c80> + +## Features + +- Works out of the box with no additional configuration +- Updates on every keystroke (0.5-4ms async, single core) +- [Typo resistant fuzzy](https://github.com/saghen/frizbee) with frecency and proximity bonus +- Extensive LSP support ([tracker](./docs/development/lsp-tracker.md)) +- Native `vim.snippet` support (including `friendly-snippets`) +- External sources support ([compatibility layer for `nvim-cmp` sources](https://github.com/saghen/blink.compat)) +- Auto-bracket support based on semantic tokens +- Signature help (experimental, opt-in) +- Command line completion +- [Comparison with nvim-cmp](https://cmp.saghen.dev/#compared-to-nvim-cmp) + +## Installation + +Head over to the [documentation website](https://cmp.saghen.dev/installation) for installation instructions and configuration options. + +## Special Thanks + +- [@hrsh7th](https://github.com/hrsh7th/) nvim-cmp used as inspiration and cmp-path/cmp-cmdline implementations modified for path/cmdline sources +- [@garymjr](https://github.com/garymjr) nvim-snippets implementation modified for snippets source +- [@redxtech](https://github.com/redxtech) Help with design and testing +- [@aaditya-sahay](https://github.com/aaditya-sahay) Help with rust, design and testing + +### Contributors + +- [@stefanboca](https://github.com/stefanboca) Author of [blink.compat](https://github.com/saghen/blink.compat) +- [@lopi-py](https://github.com/lopi-py) Contributes to the windowing code +- [@scottmckendry](https://github.com/scottmckendry) Contributes to the CI and prebuilt binaries +- [@balssh](https://github.com/Balssh) + [@konradmalik](https://github.com/konradmalik) Manages nix flake, nixpkg and nixvim +- [@abeldekat](https://github.com/abeldekat) Implemented mini.snippets source diff --git a/mut/neovim/pack/plugins/start/blink.cmp/build.rs b/mut/neovim/pack/plugins/start/blink.cmp/build.rs new file mode 100644 index 0000000..1744be2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/build.rs @@ -0,0 +1,15 @@ +fn main() { + // delete existing version file created by downloader + let _ = std::fs::remove_file("target/release/version"); + + // get current sha from git + let output = std::process::Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .unwrap(); + let sha = String::from_utf8(output.stdout).unwrap(); + + // write to version + std::fs::create_dir_all("target/release").unwrap(); + std::fs::write("target/release/version", sha.trim()).unwrap(); +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/.gitignore b/mut/neovim/pack/plugins/start/blink.cmp/docs/.gitignore new file mode 100644 index 0000000..5506568 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules +.vitepress/cache +.vitepress/dist diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/.prettierrc b/mut/neovim/pack/plugins/start/blink.cmp/docs/.prettierrc new file mode 100644 index 0000000..31ba22d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 120 +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/config.mts b/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/config.mts new file mode 100644 index 0000000..d7fadcc --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/config.mts @@ -0,0 +1,68 @@ +import { defineConfig } from 'vitepress' +import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' +import taskLists from 'markdown-it-task-lists' +import { execSync } from 'node:child_process' + +const isMain = process.env.IS_RELEASE !== 'true' +const version = execSync('git describe --tags --abbrev=0', { encoding: 'utf-8' }).trim() + +const siteUrl = isMain ? 'https://main.cmp.saghen.dev' : 'https://cmp.saghen.dev' +const otherSiteUrl = isMain ? 'https://cmp.saghen.dev' : 'https://main.cmp.saghen.dev' + +const title = isMain ? 'Main' : version +const otherTitle = isMain ? version : 'Main' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: 'Blink Completion (blink.cmp)', + description: 'Performant, batteries-included completion plugin for Neovim', + sitemap: { hostname: siteUrl }, + head: [['link', { rel: 'icon', href: '/favicon.png' }]], + themeConfig: { + nav: [{ text: `Version: ${title}`, items: [{ text: otherTitle, link: otherSiteUrl }] }], + sidebar: [ + { text: 'Introduction', link: '/' }, + { text: 'Installation', link: '/installation' }, + { text: 'Recipes', link: '/recipes' }, + { + text: 'Configuration', + items: [ + { text: 'General', link: '/configuration/general' }, + { text: 'Appearance', link: '/configuration/appearance' }, + { text: 'Completion', link: '/configuration/completion' }, + { text: 'Fuzzy', link: '/configuration/fuzzy' }, + { text: 'Keymap', link: '/configuration/keymap' }, + { text: 'Signature', link: '/configuration/signature' }, + { text: 'Sources', link: '/configuration/sources' }, + { text: 'Snippets', link: '/configuration/snippets' }, + { text: 'Reference', link: '/configuration/reference' }, + ], + }, + { + text: 'Development', + items: [ + { text: 'Architecture', link: '/development/architecture' }, + { text: 'Writing Sources', link: '/development/writing-sources' }, + { text: 'LSP Tracker', link: '/development/lsp-tracker' }, + ], + }, + ], + + socialLinks: [{ icon: 'github', link: 'https://github.com/saghen/blink.cmp' }], + + search: { + provider: 'local', + }, + }, + + markdown: { + theme: { + light: 'catppuccin-latte', + dark: 'catppuccin-mocha', + }, + config(md) { + md.use(tabsMarkdownPlugin) + md.use(taskLists) + }, + }, +}) diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/theme/index.ts b/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..f13e9f9 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/theme/index.ts @@ -0,0 +1,14 @@ +// https://vitepress.dev/guide/custom-theme +import DefaultTheme from 'vitepress/theme' +import type { Theme } from 'vitepress' +import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' + +import '@catppuccin/vitepress/theme/mocha/blue.css' +import './style.css' + +export default { + extends: DefaultTheme, + enhanceApp({ app }) { + enhanceAppWithTabs(app) + }, +} satisfies Theme diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/theme/style.css b/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/theme/style.css new file mode 100644 index 0000000..05ce570 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/.vitepress/theme/style.css @@ -0,0 +1,14 @@ +/* Wrap text in code blocks */ +code { + white-space: pre-wrap !important; + word-break: break-word !important; +} + +.content-container { + max-width: 800px !important; +} + +.VPBadge > a { + text-decoration: none !important; + color: inherit !important; +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/appearance.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/appearance.md new file mode 100644 index 0000000..9d13913 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/appearance.md @@ -0,0 +1,29 @@ +# Appearance <Badge type="info"><a href="./reference#appearance">Go to default configuration</a></Badge> + +If you're looking for how to change the appearance of the completion menu, check out the [menu draw configuration](./completion#completion-menu-draw). + +## Highlight groups + +| Group | Default | Description | +| ----- | ------- | ----------- | +| `BlinkCmpMenu` | Pmenu | The completion menu window | +| `BlinkCmpMenuBorder` | Pmenu | The completion menu window border | +| `BlinkCmpMenuSelection` | PmenuSel | The completion menu window selected item | +| `BlinkCmpScrollBarThumb` | PmenuThumb | The scrollbar thumb | +| `BlinkCmpScrollBarGutter` | PmenuSbar | The scrollbar gutter | +| `BlinkCmpLabel` | Pmenu | Label of the completion item | +| `BlinkCmpLabelDeprecated` | NonText | Deprecated label of the completion item | +| `BlinkCmpLabelMatch` | Pmenu | (Currently unused) Label of the completion item when it matches the query | +| `BlinkCmpLabelDetail` | NonText | Label description of the completion item | +| `BlinkCmpLabelDescription` | NonText | Label description of the completion item | +| `BlinkCmpKind` | Special | Kind icon/text of the completion item | +| `BlinkCmpKind<kind>` | Special | Kind icon/text of the completion item | +| `BlinkCmpSource` | NonText | Source of the completion item | +| `BlinkCmpGhostText` | NonText | Preview item with ghost text | +| `BlinkCmpDoc` | NormalFloat | The documentation window | +| `BlinkCmpDocBorder` | NormalFloat | The documentation window border | +| `BlinkCmpDocSeparator` | NormalFloat | The documentation separator between doc and detail | +| `BlinkCmpDocCursorLine` | Visual | The documentation window cursor line | +| `BlinkCmpSignatureHelp` | NormalFloat | The signature help window | +| `BlinkCmpSignatureHelpBorder` | NormalFloat | The signature help window border | +| `BlinkCmpSignatureHelpActiveParameter` | LspSignatureActiveParameter | Active parameter of the signature help | diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/completion.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/completion.md new file mode 100644 index 0000000..be8ec1a --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/completion.md @@ -0,0 +1,170 @@ +# Completion + +Blink cmp has *a lot* of configuration options, the following document tries to highlight the ones you'll likely care the most about for each section. For all options, click on the "Go to default configuration" button next to each header. + +## Keyword <Badge type="info"><a href="./reference#completion-keyword">Go to default configuration</a></Badge> + +Controls what the plugin considers to be a keyword, used for fuzzy matching and triggering completions. Most notably, the `range` option controls whether the keyword should match against the text before *and* after the cursor, or just before the cursor. + +:::tabs +== Prefix +<img src="https://github.com/user-attachments/assets/6398e470-58c7-4624-989a-bffe26c7f443" /> +== Full +<img src="https://github.com/user-attachments/assets/3e082492-6a5d-4dba-b4ba-6a1bfca50351" /> +::: + +## Trigger <Badge type="info"><a href="./reference#completion-trigger">Go to default configuration</a></Badge> + +Controls when to request completion items from the sources and show the completion menu. The following options are available, excluding their `show_on` prefix: + +:::tabs +== Keyword +Shows after typing a keyword, typically an alphanumeric character, `-` or `_` + +```lua +completion.trigger.show_on_keyword = true +``` + +<video src="https://github.com/user-attachments/assets/5e8f8f9f-bc6a-4d21-9cce-2e291b6a7de8" muted autoplay loop /> +== Trigger Character + +Shows after typing a trigger character, defined by the sources. For example for Lua or Rust, the LSP will define `.` as a trigger character. + +```lua +completion.trigger.show_on_trigger_character = true +``` + +<video src="https://github.com/user-attachments/assets/b4ee0069-2de8-44e7-b3ca-51b10bc4cb4a" muted autoplay loop /> +== Insert on Trigger Character + +Shows after entering insert mode on top of a trigger character. + +```lua +completion.trigger.show_on_insert_on_trigger_character = true +``` + +<video src="https://github.com/user-attachments/assets/9e7aa3c2-4756-4a5e-a0e8-303d3ae0fda9" muted autoplay loop /> +== Accept on Trigger Character + +Shows after accepting a completion item, where the cursor ends up on top of a trigger character. + +```lua +completion.trigger.show_on_accept_on_trigger_character = true +``` + +TODO: Find a case where this actually fires : ) +::: + +## List <Badge type="info"><a href="./reference#completion-list">Go to default configuration</a></Badge> + +Manages the completion list and its behavior when selecting items. The most commonly changed option is `selection.preselect/auto_insert`, which controls whether the list will automatically select the first item in the list, and whether a "preview" will be inserted on selection. + +:::tabs +== Preselect, Auto Insert (default) +```lua +completion.list.selection = { preselect = true, auto_insert = true } +``` +Selects the first item automatically, and inserts a preview of the item on selection. The `cancel` keymap (default `<C-e>`) will close the menu and undo the preview. + +<video src="https://github.com/user-attachments/assets/ef295526-8332-4ad0-9a2a-e2f6484081b2" muted autoplay loop /> + +== Preselect +```lua +completion.list.selection = { preselect = true, auto_insert = false } +``` +Selects the first item automatically + +<img src="https://github.com/user-attachments/assets/69079ced-43f1-437e-8a45-3cb13f841d61" /> +== Manual +```lua +completion.list.selection = { preselect = false, auto_insert = false } +``` + +No item will be selected by default. You may use the `select_and_accept` keymap command to select the first item and accept it when there's no selection. The `accept` keymap command, on the other hand, will only trigger if an item is selected. + +<video src="https://github.com/user-attachments/assets/09cd9b4b-18b3-456b-bb0a-074ae54e9d77" muted autoplay loop /> +== Manual, Auto Insert +```lua +completion.list.selection = { preselect = false, auto_insert = true } +``` + +Selecting an item will insert a "preview" of the item automatically. You may use the `select_and_accept` keymap command to select the first item and accept it when there's no selection. The `accept` keymap command will only trigger if an item is selected. The `cancel` keymap (default `<C-e>`) will close the menu and undo the preview. + +<video src="https://github.com/user-attachments/assets/4658b61d-1b95-404a-b6b5-3a4afbfb8112" muted autoplay loop /> +::: + +To control the selection behavior per mode, pass a function to `selection.preselect/auto_insert`: + +```lua +completion.list.selection = { + preselect = true, + auto_insert = true, + + -- or a function + preselect = function(ctx) + return ctx.mode ~= 'cmdline' and not require('blink.cmp').snippet_active({ direction = 1 }) + end, + -- auto_insert = function(ctx) return ctx.mode ~= 'cmdline' end, +} +``` + + +## Accept <Badge type="info"><a href="./reference#completion-accept">Go to default configuration</a></Badge> + +Manages the behavior when accepting an item in the completion menu. + +### Auto Brackets + +> [!NOTE] +> Some LSPs may add auto brackets themselves. You may be able to configure this behavior in your LSP client configuration + +LSPs provide a `kind` field for completion items, indicating whether the item is a function, method, variable, etc. The plugin will automatically add brackets for functions/methods and place the cursor inside the brackets. For items not marked as such, the plugin will asynchronously resolve the semantic tokens from the LSP and add brackets if marked as a function. A default list of brackets have been included in the default configuration, but you may add more in the configuration (contributions welcome!). + +If brackets are showing when you don't expect them, try disabling `kind_resolution` or `semantic_token_resolution` for that filetype (`echo &filetype`). If that fixes the issue, please open a PR setting this as the default! + +## Menu <Badge type="info"><a href="./reference#completion-menu">Go to default configuration</a></Badge> + +Manages the appearance of the completion menu. You may prevent the menu from automatically showing by setting `completion.menu.auto_show = false` and manually showing it with the `show` keymap command. + +### Menu Draw <Badge type="info"><a href="./reference#completion-menu-draw">Go to default configuration</a></Badge> + +blink.cmp uses a grid-based layout to render the completion menu. The components, defined in `draw.components[string]`, define `text` and `highlight` functions which are called for each completion item. The `highlight` function will be called only when the item appears on screen, so expensive operations such as Treesitter highlighting may be performed (contributions welcome!, [for example](https://www.reddit.com/r/neovim/comments/1ca4gm2/colorful_cmp_menu_powered_by_treesitter/)). The components may define their min and max width, where `ellipsis = true` (enabled by default), will draw the `…` character when the text is truncated. Setting `width.fill = true` will fill the remaining space, effectively making subsequent components right aligned, with respect to their column. + +Columns effectively allow you to vertically align a set of components. Each column, defined as an array in `draw.columns`, will be rendered for all of the completion items, where the longest rendered row will determine the width of the column. You may define `gap = number` in your column to insert a gap between components. + +For a setup similar to nvim-cmp, use the following config: + +```lua +completion.menu.draw.columns = { { "label", "label_description", gap = 1 }, { "kind_icon", "kind" } }, +``` + +### Treesitter + +You may use treesitter to highlight the label text for the given list of sources. This feature is experimental, contributions welcome! + +```lua +completion.menu.draw.treesitter = { 'lsp' } +``` + +## Documentation <Badge type="info"><a href="./reference#completion-documentation">Go to default configuration</a></Badge> + +By default, the documentation window will only show when triggered by the `show_documentation` keymap command. However, you may add the following configuration to show the documentation whenever an item is selected. + +```lua +completion.documentation = { + auto_show = true, + auto_show_delay_ms = 500, +} +``` + +If you're noticing high CPU usage or stuttering when opening the documentation, you may try setting `completion.documentation.treesitter_highlighting = false`. + +## Ghost Text <Badge type="info"><a href="./reference#completion-ghost-text">Go to default configuration</a></Badge> + +Ghost text shows a preview of the currently selected item as virtual text inline. You may want to try setting `completion.menu.auto_show = false` and enabling ghost text, or you may use both in parallel. + +```lua +completion.ghost_text.enabled = true +``` + +<img src="https://github.com/user-attachments/assets/1d30ef90-3ba4-43ca-a1a6-faa70f830e17" /> diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/fuzzy.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/fuzzy.md new file mode 100644 index 0000000..7ef1a5b --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/fuzzy.md @@ -0,0 +1,30 @@ +# Fuzzy + +Blink uses a SIMD fuzzy matcher called [frizbee](https://github.com/saghen/frizbee) which achieves ~6x the performance of fzf while ignoring typos. Check out the repo for more information! + +## Installation + +### Prebuilt binaries (default on a release tag) + +By default, Blink will download a prebuilt binary from the latest release, when you're on a release tag (via `version = '*'` on `lazy.nvim` for example). If you're not on a release tag, you may force a specific version via `fuzzy.prebuilt_binaries.force_version`. See [the latest release](https://github.com/saghen/blink.cmp/releases/latest) for supported systems. See `prebuilt_binaries` section of the [reference configuration](./reference.md#prebuilt-binaries) for more options. + +You may instead install the prebuilt binaries manually by downloading the appropriate binary from the [latest release](https://github.com/saghen/blink.cmp/releases/latest) and placing it at `$data/lazy/blink.cmp/target/release/libblink_cmp_fuzzy.$ext`. Get the `$data` path via `:echo stdpath('data')`. Use `.so` for linux, `.dylib` for mac, and `.dll` for windows. If you're unsure whether you want `-musl` or `-gnu` for linux, you very likely want `-gnu`. + +```sh +# Linux +~/.local/share/nvim/lazy/blink.cmp/target/release/libblink_cmp_fuzzy.so + +# Mac +~/.local/share/nvim/lazy/blink.cmp/target/release/libblink_cmp_fuzzy.dylib + +# Windows +~/Appdata/Local/nvim/lazy/blink.cmp/target/release/libblink_cmp_fuzzy.dll +``` + +### Build from source (recommended for `main`) + +When on `main`, it's highly recommended to build from source via `cargo build --release` (via `build = '...'` on `lazy.nvim` for example). This requires a nightly rust toolchain, which will be automatically downloaded when using `rustup`. + +## Configuration + +See the [fuzzy section of the reference configuration](./reference.md#fuzzy) diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/general.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/general.md new file mode 100644 index 0000000..5dbe2d7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/general.md @@ -0,0 +1,66 @@ +# General + +Blink cmp has *a lot* of configuration options, the following code block highlights some changes you're most likely to care about. For more information, check out the additional pages. + +For more common configurations, see the [recipes](../recipes.md). + +> [!IMPORTANT] Do not copy this entire configuration! It contains only non-default options + +```lua +{ + -- Disable for some filetypes + enabled = function() + return not vim.tbl_contains({ "lua", "markdown" }, vim.bo.filetype) + and vim.bo.buftype ~= "prompt" + and vim.b.completion ~= false + end, + + completion = { + -- 'prefix' will fuzzy match on the text before the cursor + -- 'full' will fuzzy match on the text before *and* after the cursor + -- example: 'foo_|_bar' will match 'foo_' for 'prefix' and 'foo__bar' for 'full' + keyword = { range = 'full' }, + + -- Disable auto brackets + -- NOTE: some LSPs may add auto brackets themselves anyway + accept = { auto_brackets = { enabled = false }, }, + + -- Don't select by default, auto insert on selection + list = { selection = { preselect = false, auto_insert = true } }, + -- or set either per mode via a function + list = { selection = { preselect = function(ctx) return ctx.mode ~= 'cmdline' end } }, + + menu = { + -- Don't automatically show the completion menu + auto_show = false, + + -- nvim-cmp style menu + draw = { + columns = { + { "label", "label_description", gap = 1 }, + { "kind_icon", "kind" } + }, + } + }, + + -- Show documentation when selecting a completion item + documentation = { auto_show = true, auto_show_delay_ms = 500 }, + + -- Display a preview of the selected item on the current line + ghost_text = { enabled = true }, + }, + + sources = { + -- Remove 'buffer' if you don't want text completions, by default it's only enabled when LSP returns no items + default = { 'lsp', 'path', 'snippets', 'buffer' }, + -- Disable cmdline completions + cmdline = {}, + }, + + -- Use a preset for snippets, check the snippets documentation for more information + snippets = { preset = 'default' | 'luasnip' | 'mini_snippets' }, + + -- Experimental signature help support + signature = { enabled = true } +} +``` diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/keymap.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/keymap.md new file mode 100644 index 0000000..d82c5b8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/keymap.md @@ -0,0 +1,145 @@ +# Keymap + +Blink uses a special schema for defining keymaps since it needs to handle falling back to other mappings. However, there's nothing stopping you from using `require('blink.cmp')` and implementing these keymaps yourself. + +Your custom key mappings are merged with a `preset` and any conflicting keys will overwrite the preset mappings. The `fallback` command will run the next non blink keymap. + +## Example + +Each keymap may be a list of commands and/or functions, where commands map directly to `require('blink.cmp')[command]()`. If the command/function returns `false` or `nil`, the next command/function will be run. + +```lua +keymap = { + -- set to 'none' to disable the 'default' preset + preset = 'default', + + ['<Up>'] = { 'select_prev', 'fallback' }, + ['<Down>'] = { 'select_next', 'fallback' }, + + -- disable a keymap from the preset + ['<C-e>'] = {}, + + -- show with a list of providers + ['<C-space>'] = { function(cmp) cmp.show({ providers = { 'snippets' } }) end }, + + -- control whether the next command will be run when using a function + ['<C-n>'] = { + function(cmp) + if some_condition then return end -- runs the next command + return true -- doesn't run the next command + end, + 'select_next' + }, + + -- optionally, separate cmdline keymaps + -- cmdline = {} +} +``` + +## Commands + +- `show`: Shows the completion menu + - Optionally use `function(cmp) cmp.show({ providers = { 'snippets' } }) end` to show with a specific list of providers +- `hide`: Hides the completion menu +- `cancel`: Reverts `completion.list.selection.auto_insert` and hides the completion menu +- `accept`: Accepts the currently selected item + - Optionally pass an index to select a specific item in the list: `function(cmp) cmp.accept({ index = 1 }) end` + - Optionally pass a `callback` to run after the item is accepted: `function(cmp) cmp.accept({ callback = function() vim.api.nvim_feedkeys('\n', 'n', true) end }) end` +- `select_and_accept`: Accepts the currently selected item, or the first item if none are selected +- `select_prev`: Selects the previous item, cycling to the bottom of the list if at the top, if `completion.list.cycle.from_top == true` + - Optionally control the `auto_insert` property of `completion.list.selection`: `function(cmp) cmp.select_prev({ auto_insert = false }) end` +- `select_next`: Selects the next item, cycling to the top of the list if at the bottom, if `completion.list.cycle.from_bottom == true` + - Optionally control the `auto_insert` property of `completion.list.selection`: `function(cmp) cmp.select_next({ auto_insert = false }) end` +- `show_documentation`: Shows the documentation for the currently selected item +- `hide_documentation`: Hides the documentation +- `scroll_documentation_up`: Scrolls the documentation up by 4 lines + - Optionally use `function(cmp) cmp.scroll_documentation_up(4) end` to scroll by a specific number of lines +- `scroll_documentation_down`: Scrolls the documentation down by 4 lines + - Optionally use `function(cmp) cmp.scroll_documentation_down(4) end` to scroll by a specific number of lines +- `snippet_forward`: Jumps to the next snippet placeholder +- `snippet_backward`: Jumps to the previous snippet placeholder +- `fallback`: Runs the next non-blink keymap, or runs the built-in neovim binding + +## Cmdline + +You may set a separate keymap for cmdline by defining `keymap.cmdline`, with an identical structure to `keymap`. + +```lua +keymap = { + preset = 'default', + ... + cmdline = { + preset = 'enter', + ... + } +} +``` + +## Presets + +Set the preset to `none` to disable the presets + +### `default` + +```lua +['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, +['<C-e>'] = { 'hide' }, +['<C-y>'] = { 'select_and_accept' }, + +['<C-p>'] = { 'select_prev', 'fallback' }, +['<C-n>'] = { 'select_next', 'fallback' }, + +['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, +['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, + +['<Tab>'] = { 'snippet_forward', 'fallback' }, +['<S-Tab>'] = { 'snippet_backward', 'fallback' }, +``` + +### `super-tab` + +You may want to set `completion.trigger.show_in_snippet = false` or use `completion.list.selection.preselect = function(ctx) return not require('blink.cmp').snippet_active({ direction = 1 }) end`. See more info in: https://cmp.saghen.dev/configuration/completion.html#list + +```lua +['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, +['<C-e>'] = { 'hide', 'fallback' }, + +['<Tab>'] = { + function(cmp) + if cmp.snippet_active() then return cmp.accept() + else return cmp.select_and_accept() end + end, + 'snippet_forward', + 'fallback' +}, +['<S-Tab>'] = { 'snippet_backward', 'fallback' }, + +['<Up>'] = { 'select_prev', 'fallback' }, +['<Down>'] = { 'select_next', 'fallback' }, +['<C-p>'] = { 'select_prev', 'fallback' }, +['<C-n>'] = { 'select_next', 'fallback' }, + +['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, +['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, +``` + +### `enter` + +You may want to set `completion.list.selection.preselect = false`. See more info in: https://cmp.saghen.dev/configuration/completion.html#list + +```lua +['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, +['<C-e>'] = { 'hide', 'fallback' }, +['<CR>'] = { 'accept', 'fallback' }, + +['<Tab>'] = { 'snippet_forward', 'fallback' }, +['<S-Tab>'] = { 'snippet_backward', 'fallback' }, + +['<Up>'] = { 'select_prev', 'fallback' }, +['<Down>'] = { 'select_next', 'fallback' }, +['<C-p>'] = { 'select_prev', 'fallback' }, +['<C-n>'] = { 'select_next', 'fallback' }, + +['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, +['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, +``` diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/reference.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/reference.md new file mode 100644 index 0000000..cc916af --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/reference.md @@ -0,0 +1,547 @@ +# Reference + +> [!IMPORTANT] +> Do not copy the default configuration! Only include options you want to change in your configuration +```lua +-- Enables keymaps, completions and signature help when true +enabled = function() return vim.bo.buftype ~= "prompt" and vim.b.completion ~= false end, + +-- See the "keymap" page for more information +keymap = 'default', +``` + +## Snippets + +```lua +snippets = { + -- Function to use when expanding LSP provided snippets + expand = function(snippet) vim.snippet.expand(snippet) end, + -- Function to use when checking if a snippet is active + active = function(filter) return vim.snippet.active(filter) end, + -- Function to use when jumping between tab stops in a snippet, where direction can be negative or positive + jump = function(direction) vim.snippet.jump(direction) end, +} +``` + +## Completion + +### Completion Keyword + +```lua +completion.keyword = { + -- 'prefix' will fuzzy match on the text before the cursor + -- 'full' will fuzzy match on the text before *and* after the cursor + -- example: 'foo_|_bar' will match 'foo_' for 'prefix' and 'foo__bar' for 'full' + range = 'prefix', +} +``` + +### Completion Trigger + +```lua +completion.trigger = { + -- When true, will prefetch the completion items when entering insert mode + prefetch_on_insert = false, + + -- When false, will not show the completion window automatically when in a snippet + show_in_snippet = true, + + -- When true, will show the completion window after typing any of alphanumerics, `-` or `_` + show_on_keyword = true, + + -- When true, will show the completion window after typing a trigger character + show_on_trigger_character = true, + + -- LSPs can indicate when to show the completion window via trigger characters + -- however, some LSPs (i.e. tsserver) return characters that would essentially + -- always show the window. We block these by default. + show_on_blocked_trigger_characters = function() + if vim.api.nvim_get_mode().mode == 'c' then return {} end + + -- you can also block per filetype, for example: + -- if vim.bo.filetype == 'markdown' then + -- return { ' ', '\n', '\t', '.', '/', '(', '[' } + -- end + + return { ' ', '\n', '\t' } + end, + + -- When both this and show_on_trigger_character are true, will show the completion window + -- when the cursor comes after a trigger character after accepting an item + show_on_accept_on_trigger_character = true, + + -- When both this and show_on_trigger_character are true, will show the completion window + -- when the cursor comes after a trigger character when entering insert mode + show_on_insert_on_trigger_character = true, + + -- List of trigger characters (on top of `show_on_blocked_trigger_characters`) that won't trigger + -- the completion window when the cursor comes after a trigger character when + -- entering insert mode/accepting an item + show_on_x_blocked_trigger_characters = { "'", '"', '(' }, + -- or a function, similar to show_on_blocked_trigger_character +} +``` + +### Completion List + +```lua +completion.list = { + -- Maximum number of items to display + max_items = 200, + + selection = { + -- When `true`, will automatically select the first item in the completion list + preselect = true, + -- preselect = function(ctx) return ctx.mode ~= 'cmdline' end, + + -- When `true`, inserts the completion item automatically when selecting it + -- You may want to bind a key to the `cancel` command (default <C-e>) when using this option, + -- which will both undo the selection and hide the completion menu + auto_insert = true, + -- auto_insert = function(ctx) return ctx.mode ~= 'cmdline' end + }, + + cycle = { + -- When `true`, calling `select_next` at the *bottom* of the completion list + -- will select the *first* completion item. + from_bottom = true, + -- When `true`, calling `select_prev` at the *top* of the completion list + -- will select the *last* completion item. + from_top = true, + }, +}, +``` + +### Completion Accept + +```lua +completion.accept = { + -- Create an undo point when accepting a completion item + create_undo_point = true, + -- Experimental auto-brackets support + auto_brackets = { + -- Whether to auto-insert brackets for functions + enabled = true, + -- Default brackets to use for unknown languages + default_brackets = { '(', ')' }, + -- Overrides the default blocked filetypes + override_brackets_for_filetypes = {}, + -- Synchronously use the kind of the item to determine if brackets should be added + kind_resolution = { + enabled = true, + blocked_filetypes = { 'typescriptreact', 'javascriptreact', 'vue' }, + }, + -- Asynchronously use semantic token to determine if brackets should be added + semantic_token_resolution = { + enabled = true, + blocked_filetypes = { 'java' }, + -- How long to wait for semantic tokens to return before assuming no brackets should be added + timeout_ms = 400, + }, + }, +}, +``` + +### Completion Menu + +```lua +completion.menu = { + enabled = true, + min_width = 15, + max_height = 10, + border = 'none', + winblend = 0, + winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None', + -- Keep the cursor X lines away from the top/bottom of the window + scrolloff = 2, + -- Note that the gutter will be disabled when border ~= 'none' + scrollbar = true, + -- Which directions to show the window, + -- falling back to the next direction when there's not enough space + direction_priority = { 's', 'n' }, + + -- Whether to automatically show the window when new completion items are available + auto_show = true, + + -- Screen coordinates of the command line + cmdline_position = function() + if vim.g.ui_cmdline_pos ~= nil then + local pos = vim.g.ui_cmdline_pos -- (1, 0)-indexed + return { pos[1] - 1, pos[2] } + end + local height = (vim.o.cmdheight == 0) and 1 or vim.o.cmdheight + return { vim.o.lines - height, 0 } + end, +} +``` + +### Completion Menu Draw + +```lua +-- Controls how the completion items are rendered on the popup window +completion.menu.draw = { + -- Aligns the keyword you've typed to a component in the menu + align_to = 'label', -- or 'none' to disable, or 'cursor' to align to the cursor + -- Left and right padding, optionally { left, right } for different padding on each side + padding = 1, + -- Gap between columns + gap = 1, + -- Use treesitter to highlight the label text for the given list of sources + treesitter = {}, + -- treesitter = { 'lsp' } + + -- Components to render, grouped by column + columns = { { 'kind_icon' }, { 'label', 'label_description', gap = 1 } }, + + -- Definitions for possible components to render. Each defines: + -- ellipsis: whether to add an ellipsis when truncating the text + -- width: control the min, max and fill behavior of the component + -- text function: will be called for each item + -- highlight function: will be called only when the line appears on screen + components = { + kind_icon = { + ellipsis = false, + text = function(ctx) return ctx.kind_icon .. ctx.icon_gap end, + highlight = function(ctx) + return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or 'BlinkCmpKind' .. ctx.kind + end, + }, + + kind = { + ellipsis = false, + width = { fill = true }, + text = function(ctx) return ctx.kind end, + highlight = function(ctx) + return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or 'BlinkCmpKind' .. ctx.kind + end, + }, + + label = { + width = { fill = true, max = 60 }, + text = function(ctx) return ctx.label .. ctx.label_detail end, + highlight = function(ctx) + -- label and label details + local highlights = { + { 0, #ctx.label, group = ctx.deprecated and 'BlinkCmpLabelDeprecated' or 'BlinkCmpLabel' }, + } + if ctx.label_detail then + table.insert(highlights, { #ctx.label, #ctx.label + #ctx.label_detail, group = 'BlinkCmpLabelDetail' }) + end + + -- characters matched on the label by the fuzzy matcher + for _, idx in ipairs(ctx.label_matched_indices) do + table.insert(highlights, { idx, idx + 1, group = 'BlinkCmpLabelMatch' }) + end + + return highlights + end, + }, + + label_description = { + width = { max = 30 }, + text = function(ctx) return ctx.label_description end, + highlight = 'BlinkCmpLabelDescription', + }, + + source_name = { + width = { max = 30 }, + text = function(ctx) return ctx.source_name end, + highlight = 'BlinkCmpSource', + }, + }, +}, +``` + +### Completion Documentation + +```lua +completion.documentation = { + -- Controls whether the documentation window will automatically show when selecting a completion item + auto_show = false, + -- Delay before showing the documentation window + auto_show_delay_ms = 500, + -- Delay before updating the documentation window when selecting a new item, + -- while an existing item is still visible + update_delay_ms = 50, + -- Whether to use treesitter highlighting, disable if you run into performance issues + treesitter_highlighting = true, + window = { + min_width = 10, + max_width = 80, + max_height = 20, + border = 'padded', + winblend = 0, + winhighlight = 'Normal:BlinkCmpDoc,FloatBorder:BlinkCmpDocBorder,EndOfBuffer:BlinkCmpDoc', + -- Note that the gutter will be disabled when border ~= 'none' + scrollbar = true, + -- Which directions to show the documentation window, + -- for each of the possible menu window directions, + -- falling back to the next direction when there's not enough space + direction_priority = { + menu_north = { 'e', 'w', 'n', 's' }, + menu_south = { 'e', 'w', 's', 'n' }, + }, + }, +} +``` + +### Completion Ghost Text + +```lua +-- Displays a preview of the selected item on the current line +completion.ghost_text = { + enabled = false, +}, +``` + +## Signature + +```lua +-- Experimental signature help support +signature = { + enabled = false, + trigger = { + blocked_trigger_characters = {}, + blocked_retrigger_characters = {}, + -- When true, will show the signature help window when the cursor comes after a trigger character when entering insert mode + show_on_insert_on_trigger_character = true, + }, + window = { + min_width = 1, + max_width = 100, + max_height = 10, + border = 'padded', + winblend = 0, + winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder', + scrollbar = false, -- Note that the gutter will be disabled when border ~= 'none' + -- Which directions to show the window, + -- falling back to the next direction when there's not enough space, + -- or another window is in the way + direction_priority = { 'n', 's' }, + -- Disable if you run into performance issues + treesitter_highlighting = true, + }, +} +``` + +## Fuzzy + +```lua +fuzzy = { + -- When enabled, allows for a number of typos relative to the length of the query + -- Disabling this matches the behavior of fzf + use_typo_resistance = true, + -- Frecency tracks the most recently/frequently used items and boosts the score of the item + use_frecency = true, + -- Proximity bonus boosts the score of items matching nearby words + use_proximity = true, + -- UNSAFE!! When enabled, disables the lock and fsync when writing to the frecency database. This should only be used on unsupported platforms (i.e. alpine termux) + use_unsafe_no_lock = false, + -- Controls which sorts to use and in which order, falling back to the next sort if the first one returns nil + -- You may pass a function instead of a string to customize the sorting + sorts = { 'score', 'sort_text' }, + + prebuilt_binaries = { + -- Whether or not to automatically download a prebuilt binary from github. If this is set to `false` + -- you will need to manually build the fuzzy binary dependencies by running `cargo build --release` + download = true, + -- Ignores mismatched version between the built binary and the current git sha, when building locally + ignore_version_mismatch = false, + -- When downloading a prebuilt binary, force the downloader to resolve this version. If this is unset + -- then the downloader will attempt to infer the version from the checked out git tag (if any). + -- + -- Beware that if the fuzzy matcher changes while tracking main then this may result in blink breaking. + force_version = nil, + -- When downloading a prebuilt binary, force the downloader to use this system triple. If this is unset + -- then the downloader will attempt to infer the system triple from `jit.os` and `jit.arch`. + -- Check the latest release for all available system triples + -- + -- Beware that if the fuzzy matcher changes while tracking main then this may result in blink breaking. + force_system_triple = nil, + -- Extra arguments that will be passed to curl like { 'curl', ..extra_curl_args, ..built_in_args } + extra_curl_args = {} + }, +} +``` + +## Sources + +```lua +sources = { + -- Static list of providers to enable, or a function to dynamically enable/disable providers based on the context + default = { 'lsp', 'path', 'snippets', 'buffer' }, + + -- You may also define providers per filetype + per_filetype = { + -- lua = { 'lsp', 'path' }, + }, + + -- By default, we choose providers for the cmdline based on the current cmdtype + -- You may disable cmdline completions by replacing this with an empty table + cmdline = function() + local type = vim.fn.getcmdtype() + -- Search forward and backward + if type == '/' or type == '?' then return { 'buffer' } end + -- Commands + if type == ':' or type == '@' then return { 'cmdline' } end + return {} + end, + + -- Function to use when transforming the items before they're returned for all providers + -- The default will lower the score for snippets to sort them lower in the list + transform_items = function(_, items) return items end, + + -- Minimum number of characters in the keyword to trigger all providers + -- May also be `function(ctx: blink.cmp.Context): number` + min_keyword_length = 0, +} +``` + +### Providers + +```lua +-- Please see https://github.com/Saghen/blink.compat for using `nvim-cmp` sources +sources.providers = { + lsp = { + name = 'LSP', + module = 'blink.cmp.sources.lsp', + fallbacks = { 'buffer' }, + -- Filter text items from the LSP provider, since we have the buffer provider for that + transform_items = function(_, items) + for _, item in ipairs(items) do + if item.kind == require('blink.cmp.types').CompletionItemKind.Snippet then + item.score_offset = item.score_offset - 3 + end + end + + return vim.tbl_filter( + function(item) return item.kind ~= require('blink.cmp.types').CompletionItemKind.Text end, + items + ) + end, + + --- NOTE: All of these options may be functions to get dynamic behavior + --- See the type definitions for more information + enabled = true, -- Whether or not to enable the provider + async = false, -- Whether we should wait for the provider to return before showing the completions + timeout_ms = 2000, -- How long to wait for the provider to return before showing completions and treating it as asynchronous + transform_items = nil, -- Function to transform the items before they're returned + should_show_items = true, -- Whether or not to show the items + max_items = nil, -- Maximum number of items to display in the menu + min_keyword_length = 0, -- Minimum number of characters in the keyword to trigger the provider + -- If this provider returns 0 items, it will fallback to these providers. + -- If multiple providers falback to the same provider, all of the providers must return 0 items for it to fallback + fallbacks = {}, + score_offset = 0, -- Boost/penalize the score of the items + override = nil, -- Override the source's functions + }, + + path = { + name = 'Path', + module = 'blink.cmp.sources.path', + score_offset = 3, + fallbacks = { 'buffer' }, + opts = { + trailing_slash = true, + label_trailing_slash = true, + get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end, + show_hidden_files_by_default = false, + } + }, + + snippets = { + name = 'Snippets', + module = 'blink.cmp.sources.snippets', + + -- For `snippets.preset == 'default'` + opts = { + friendly_snippets = true, + search_paths = { vim.fn.stdpath('config') .. '/snippets' }, + global_snippets = { 'all' }, + extended_filetypes = {}, + ignored_filetypes = {}, + get_filetype = function(context) + return vim.bo.filetype + end + -- Set to '+' to use the system clipboard, or '"' to use the unnamed register + clipboard_register = nil, + } + + -- For `snippets.preset == 'luasnip'` + opts = { + -- Whether to use show_condition for filtering snippets + use_show_condition = true, + -- Whether to show autosnippets in the completion list + show_autosnippets = true, + } + + -- For `snippets.preset == 'mini_snippets'` + opts = { + -- Whether to use a cache for completion items + use_items_cache = true, + } + }, + + buffer = { + name = 'Buffer', + module = 'blink.cmp.sources.buffer', + opts = { + -- default to all visible buffers + get_bufnrs = function() + return vim + .iter(vim.api.nvim_list_wins()) + :map(function(win) return vim.api.nvim_win_get_buf(win) end) + :filter(function(buf) return vim.bo[buf].buftype ~= 'nofile' end) + :totable() + end, + } + }, +} +``` + +## Appearance + +```lua +appearance = { + highlight_ns = vim.api.nvim_create_namespace('blink_cmp'), + -- Sets the fallback highlight groups to nvim-cmp's highlight groups + -- Useful for when your theme doesn't support blink.cmp + -- Will be removed in a future release + use_nvim_cmp_as_default = false, + -- Set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' + -- Adjusts spacing to ensure icons are aligned + nerd_font_variant = 'mono', + kind_icons = { + Text = '', + Method = '', + Function = '', + Constructor = '', + + Field = '', + Variable = '', + Property = '', + + Class = '', + Interface = '', + Struct = '', + Module = '', + + Unit = '', + Value = '', + Enum = '', + EnumMember = '', + + Keyword = '', + Constant = '', + + Snippet = '', + Color = '', + File = '', + Reference = '', + Folder = '', + Event = '', + Operator = '', + TypeParameter = '', + }, +} +``` diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/signature.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/signature.md new file mode 100644 index 0000000..4fc4d8f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/signature.md @@ -0,0 +1,13 @@ +# Signature <Badge type="info"><a href="./reference#signature">Go to default configuration</a></Badge> + +> [!IMPORTANT] +> This feature is *experimental*, contributions welcome! + +Blink supports signature help, automatically triggered when typing trigger characters, defined by the LSP, such as `(` for `lua`. The menu will be updated when pressing a retrigger character, such as `,`. Due to it being experimental, this feature is opt-in. + +```lua +signature = { enabled = true } +``` + +<img src="https://github.com/user-attachments/assets/9ab576c8-2a04-465f-88c0-9c130fef146c" /> + diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/snippets.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/snippets.md new file mode 100644 index 0000000..918bae0 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/snippets.md @@ -0,0 +1,116 @@ +# Snippets <Badge type="info"><a href="./reference#snippets">Go to default configuration</a></Badge> + +Blink uses the `vim.snippet` API by default for expanding and navigating snippets. The built-in `snippets` source will load [friendly-snippets](https://github.com/rafamadriz/friendly-snippets), if available, and load any snippets found at `~/.config/nvim/snippets/`. For use with Luasnip, see the [Luasnip section](#luasnip). + +## Custom snippets + +By default, the `snippets` source will check `~/.config/nvim/snippets` for your custom snippets, but you may add additional folders via `sources.providers.snippets.opts.search_paths`. Currently, only VSCode style snippets are supported, but you may look into [Luasnip](https://github.com/L3MON4D3/LuaSnip) if you'd like more advanced functionality. + +[Chris Grieser](https://github.com/chrisgrieser) has made a great introduction to writing custom snippets [in the nvim-scissors repo](https://github.com/chrisgrieser/nvim-scissors?tab=readme-ov-file#cookbook--faq). Here's an example, using the linux/mac path for the neovim configuration: + +```jsonc +// ~/.config/nvim/snippets/package.json +{ + "name": "personal-snippets", + "contributes": { + "snippets": [ + { "language": "lua", "path": "./lua.json" } + ] + } +} +``` + +```jsonc +// ~/.config/nvim/snippets/lua.json +{ + "foo": { + "prefix": "foo", + "body": [ + "local ${1:foo} = ${2:bar}", + "return ${3:baz}" + ], + } +} +``` + +## Luasnip + +```lua +{ + 'saghen/blink.cmp', + version = '*', + -- !Important! Make sure you're using the latest release of LuaSnip + -- `main` does not work at the moment + dependencies = { 'L3MON4D3/LuaSnip', version = 'v2.*' }, + opts = { + snippets = { preset = 'luasnip' }, + -- ensure you have the `snippets` source (enabled by default) + sources = { + default = { 'lsp', 'path', 'snippets', 'buffer' }, + }, + } +} +``` + +## `mini.snippets` + +```lua +{ + 'saghen/blink.cmp', + dependencies = 'echasnovski/mini.snippets', + opts = { + snippets = { preset = 'mini_snippets' }, + -- ensure you have the `snippets` source (enabled by default) + sources = { + default = { 'lsp', 'path', 'snippets', 'buffer' }, + }, + } +} +``` + +## Disable all snippets + +```lua +sources.transform_items = function(_, items) + return vim.tbl_filter(function(item) + return item.kind ~= require('blink.cmp.types').CompletionItemKind.Snippet + end, items) +end +``` + +When setting up your capabilities with `lspconfig`, add the following: + +```lua +capabilities = require('blink.cmp').get_lsp_capabilities({ + textDocument = { completion = { completionItem = { snippetSupport = false } } }, +}) +``` + +Some LSPs may ignore the `snippetSupport` field, in which case, you need to set LSP specific options while setting them up. Some examples: + +```lua +-- If you're using `opts = { ['rust-analyzer'] = { } }` in your lspconfig configuration, simply put these options in there instead + +-- For `rust-analyzer` +lspconfig['rust-analyzer'].setup({ + completion = { + capable = { + snippets = 'add_parenthesis' + } + } +}) + +-- For `lua_ls` +lspconfig.lua_ls.setup({ + settings = { + Lua = { + completion = { + callSnippet = 'Disable', + keywordSnippet = 'Disable', + } + } + } +}) +``` + +Please open a PR if you know of any other LSPs that require special configuration! diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/sources.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/sources.md new file mode 100644 index 0000000..c19a2a8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/configuration/sources.md @@ -0,0 +1,65 @@ +# Sources <Badge type="info"><a href="./reference#sources">Go to default configuration</a></Badge> + +> [!NOTE] +> Check out the [recipes](../recipes.md) for some common configurations + +Blink provides a sources interface, modelled after LSPs, for getting completion items, trigger characters, documentation and signature help. The `lsp`, `path`, `snippets`, `luasnip` and `buffer` sources are built-in. You may add additional [community sources](#community-sources) as well. Check out [writing sources](../development/writing-sources.md) to learn how to write your own! + +## Providers + +Sources are configured via the `sources.providers` table, where each `id` (`key`) must have a `name` and `module` field. The `id` (`key`) may be used in the `sources.default/per_filetype/cmdline` to enable the source. + +```lua +sources = { + default = { 'lsp' }, + providers = { + lsp = { + name = 'LSP', + module = 'blink.cmp.sources.lsp', + } + } +} +``` + +### Provider options + +All of the fields shown below apply to all sources. The `opts` field is passed to the source directly, and will vary by source. + +```lua +sources.providers.lsp = { + name = 'LSP', + module = 'blink.cmp.sources.lsp', + opts = {} -- Passed to the source directly, varies by source + + --- NOTE: All of these options may be functions to get dynamic behavior + --- See the type definitions for more information + enabled = true, -- Whether or not to enable the provider + async = false, -- Whether we should wait for the provider to return before showing the completions + timeout_ms = 2000, -- How long to wait for the provider to return before showing completions and treating it as asynchronous + transform_items = nil, -- Function to transform the items before they're returned + should_show_items = true, -- Whether or not to show the items + max_items = nil, -- Maximum number of items to display in the menu + min_keyword_length = 0, -- Minimum number of characters in the keyword to trigger the provider + -- If this provider returns 0 items, it will fallback to these providers. + -- If multiple providers falback to the same provider, all of the providers must return 0 items for it to fallback + fallbacks = {}, + score_offset = 0, -- Boost/penalize the score of the items + override = nil, -- Override the source's functions +} +``` + +## Using `nvim-cmp` sources + +Blink can use `nvim-cmp` sources through a compatibility layer developed by [stefanboca](https://github.com/stefanboca): [blink.compat](https://github.com/Saghen/blink.compat). Please open any issues with `blink.compat` in that repo + +## Community sources + +- [lazydev](https://github.com/folke/lazydev.nvim) +- [vim-dadbod-completion](https://github.com/kristijanhusak/vim-dadbod-completion) +- [blink-ripgrep](https://github.com/mikavilpas/blink-ripgrep.nvim) +- [blink-cmp-ripgrep](https://github.com/niuiic/blink-cmp-rg.nvim) +- [blink-cmp-ctags](https://github.com/netmute/blink-cmp-ctags) +- [blink-cmp-copilot](https://github.com/giuxtaposition/blink-cmp-copilot) +- [minuet-ai.nvim](https://github.com/milanglacier/minuet-ai.nvim) +- [blink-emoji.nvim](https://github.com/moyiz/blink-emoji.nvim) +- [blink-cmp-dictionary](https://github.com/Kaiser-Yang/blink-cmp-dictionary) diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/development/architecture.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/development/architecture.md new file mode 100644 index 0000000..9904897 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/development/architecture.md @@ -0,0 +1,9 @@ +# Architecture + +The plugin use a 4 stage pipeline: trigger -> sources -> fuzzy -> render +1. **Trigger:** Controls when to request completion items from the sources and provides a context downstream with the current query (i.e. `hello.wo|`, the query would be `wo`) and the treesitter object under the cursor (i.e. for intelligently enabling/disabling sources). It respects trigger characters passed by the LSP (or any other source) and includes it in the context for sending to the LSP. +2. **Sources:** Provides a common interface for and merges the results of completion, trigger character, resolution of additional information and cancellation. Some sources are builtin: `LSP`, `buffer`, `path`, `snippets` +3. **Fuzzy:** Rust <-> Lua FFI which performs both filtering and sorting of the items + - **Filtering:** The fuzzy matching uses smith-waterman, same as FZF, but implemented in SIMD for ~6x the performance of FZF (TODO: add benchmarks). Due to the SIMD's performance, the prefiltering phase on FZF was dropped to allow for typos. Similar to fzy/fzf, additional points are given to prefix matches, characters with capitals (to promote camelCase/PascalCase first char matching) and matches after delimiters (to promote snake_case first char matching) + - **Sorting:** Combines fuzzy matching score with frecency and proximity bonus. Each completion item may also include a `score_offset` which will be added to this score to demote certain sources. The `snippets` source takes advantage of this to avoid taking precedence over the LSP source. The parameters here still need to be tuned, so please let me know if you find some magical parameters! +4. **Windows:** Responsible for placing the menu, documentation and function parameters windows. All of the rendering can be overridden following a syntax similar to incline.nvim. It uses the neovim window decoration provider to provide next to no overhead from highlighting. diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/development/lsp-tracker.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/development/lsp-tracker.md new file mode 100644 index 0000000..5f69417 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/development/lsp-tracker.md @@ -0,0 +1,69 @@ +# LSP Support Tracker + +## Completion Items + +- [x] `completionItem/resolve` + +### Client Capabilities + +- [ ] `dynamicRegistration` +- [x] `CompletionItem` + - [x] `snippetSupport` + - [ ] `commitCharacterSupport` + - [x] `documentationFormat` + - [x] `deprecatedSupport` + - [ ] `preselectSupport` + - [x] `tagSupport` + - [ ] `insertReplaceSupport` + - [x] `resolveSupport` + - [x] `insertTextModeSupport` + - [x] `labelDetailsSupport` +- [x] `completionItemKind` +- [x] `contextSupport` + +### Server Capabilities + +- [x] `triggerCharacters` +- [ ] `allCommitCharacters` +- [x] `resolveProvider` +- [x] `CompletionItem` + - [x] `labelDetailsSupport` + +### Request Params + +- [x] `CompletionContext` + - [x] `triggerKind` + - [x] `triggerCharacter` + +### List + +- [x] `isIncomplete` +- [x] `itemDefaults` + - [x] `commitCharacters` + - [x] `editRange` + - [x] `insertTextFormat` + - [x] `insertTextMode` + - [x] `data` +- [x] `items` + +### Item + +- [x] `label` +- [x] `labelDetails` +- [x] `kind` +- [x] `tags` +- [x] `detail` +- [x] `documentation` <- both string and markup content +- [x] `deprecated` +- [ ] `preselect` +- [x] `sortText` +- [x] `filterText` +- [x] `insertText` +- [x] `insertTextFormat` <- regular or snippet +- [ ] `insertTextMode` <- asIs only, not sure we'll support adjustIndentation +- [x] `textEdit` +- [x] `textEditText` +- [x] `additionalTextEdits` <- known issue where applying the main text edit will cause this to be wrong if the additional text edit comes after since the indices will be offset +- [ ] `commitCharacters` +- [ ] `command` +- [x] `data` <- Don't think there's anything special to do here diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/development/writing-sources.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/development/writing-sources.md new file mode 100644 index 0000000..81048e9 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/development/writing-sources.md @@ -0,0 +1,3 @@ +# Writing Sources + +TODO diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/index.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/index.md new file mode 100644 index 0000000..5f9b6e4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/index.md @@ -0,0 +1,47 @@ +# Blink Completion (blink.cmp) + +> [!IMPORTANT] +> This plugin is *beta* quality. Expect breaking changes and many bugs + +**blink.cmp** is a completion plugin with support for LSPs and external sources that updates on every keystroke with minimal overhead (0.5-4ms async). It use a [custom SIMD fuzzy searcher](https://github.com/saghen/frizbee) to easily handle >20k items. It provides extensibility via hooks into the trigger, sources and rendering pipeline. Plenty of work has been put into making each stage of the pipeline as intelligent as possible, such as frecency and proximity bonus on fuzzy matching, and this work is on-going. + +<video controls autoplay muted src="https://github.com/user-attachments/assets/9849e57a-3c2c-49a8-959c-dbb7fef78c80"></video> + +## Features + +- Works out of the box with no additional configuration +- Updates on every keystroke (0.5-4ms async, single core) +- [Typo resistant fuzzy](https://github.com/saghen/frizbee) with frecency and proximity bonus +- Extensive LSP support ([tracker](./development/lsp-tracker.md)) +- Native `vim.snippet` support (including `friendly-snippets`) +- External sources support ([compatibility layer for `nvim-cmp` sources](https://github.com/Saghen/blink.compat)) +- Auto-bracket support based on semantic tokens +- Signature help (experimental, opt-in) + +## Special Thanks + +- [@hrsh7th](https://github.com/hrsh7th/) nvim-cmp used as inspiration and cmp-path/cmp-cmdline implementations modified for path/cmdline sources +- [@garymjr](https://github.com/garymjr) nvim-snippets implementation modified for snippets source +- [@redxtech](https://github.com/redxtech) Help with design and testing +- [@aaditya-sahay](https://github.com/aaditya-sahay) Help with rust, design and testing + +### Contributors + +- [@stefanboca](https://github.com/stefanboca) Author of [blink.compat](https://github.com/saghen/blink.compat) +- [@lopi-py](https://github.com/lopi-py) Contributes to the windowing code +- [@scottmckendry](https://github.com/scottmckendry) Contributes to the CI and prebuilt binaries +- [@balssh](https://github.com/Balssh) + [@konradmalik](https://github.com/konradmalik) Manages nix flake, nixpkg and nixvim + +## Compared to nvim-cmp + +- Avoids the complexity of nvim-cmp's configuration by providing sensible defaults +- Updates on every keystroke with 0.5-4ms of overhead, versus nvim-cmp's default debounce of 60ms with 2-50ms hitches from processing + - Setting nvim-cmp's debounce to 0ms leads to visible stuttering. If you'd like to stick with nvim-cmp, try [yioneko's fork](https://github.com/yioneko/nvim-cmp) or the more recent [magazine.nvim](https://github.com/iguanacucumber/magazine.nvim) +- Boosts completion item score via frecency _and_ proximity bonus. nvim-cmp boosts score via proximity bonus and optionally by recency +- Typo-resistant fuzzy matching unlike nvim-cmp's fzf-style fuzzy matching +- Core sources (buffer, snippets, path, lsp) are built-in versus nvim-cmp's exclusively external sources +- Built-in auto bracket and signature help support + +### Planned missing features + +- Significantly more testing and documentation diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/installation.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/installation.md new file mode 100644 index 0000000..3a82c6f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/installation.md @@ -0,0 +1,135 @@ +# Installation + +> [!IMPORTANT] +> Blink uses a prebuilt binary for the fuzzy matcher which will be downloaded automatically when on a tag. +> You may build from source with rust nightly. See the [fuzzy documentation](./configuration/fuzzy.md) for more information. + +## Requirements + +- Neovim 0.10+ +- Using prebuilt binaries: + - curl + - git +- Building from source: + - Rust nightly or [rustup](https://rustup.rs/) + +## `lazy.nvim` + +```lua +{ + 'saghen/blink.cmp', + -- optional: provides snippets for the snippet source + dependencies = 'rafamadriz/friendly-snippets', + + -- use a release tag to download pre-built binaries + version = '*', + -- AND/OR build from source, requires nightly: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust + -- build = 'cargo build --release', + -- If you use nix, you can build from source using latest nightly rust with: + -- build = 'nix run .#build-plugin', + + ---@module 'blink.cmp' + ---@type blink.cmp.Config + opts = { + -- 'default' for mappings similar to built-in completion + -- 'super-tab' for mappings similar to vscode (tab to accept, arrow keys to navigate) + -- 'enter' for mappings similar to 'super-tab' but with 'enter' to accept + -- See the full "keymap" documentation for information on defining your own keymap. + keymap = { preset = 'default' }, + + appearance = { + -- Sets the fallback highlight groups to nvim-cmp's highlight groups + -- Useful for when your theme doesn't support blink.cmp + -- Will be removed in a future release + use_nvim_cmp_as_default = true, + -- Set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' + -- Adjusts spacing to ensure icons are aligned + nerd_font_variant = 'mono' + }, + + -- Default list of enabled providers defined so that you can extend it + -- elsewhere in your config, without redefining it, due to `opts_extend` + sources = { + default = { 'lsp', 'path', 'snippets', 'buffer' }, + }, + }, + opts_extend = { "sources.default" } +} +``` + +> [!IMPORTANT] +> On Neovim 0.11+ and Blink.cmp 0.10+, you may skip this step + +Setting capabilities for `nvim-lspconfig`: + +```lua +-- LSP servers and clients communicate which features they support through "capabilities". +-- By default, Neovim supports a subset of the LSP specification. +-- With blink.cmp, Neovim has *more* capabilities which are communicated to the LSP servers. +-- Explanation from TJ: https://youtu.be/m8C0Cq9Uv9o?t=1275 +-- +-- This can vary by config, but in general for nvim-lspconfig: + +{ + 'neovim/nvim-lspconfig', + dependencies = { 'saghen/blink.cmp' }, + + -- example using `opts` for defining servers + opts = { + servers = { + lua_ls = {} + } + }, + config = function(_, opts) + local lspconfig = require('lspconfig') + for server, config in pairs(opts.servers) do + -- passing config.capabilities to blink.cmp merges with the capabilities in your + -- `opts[server].capabilities, if you've defined it + config.capabilities = require('blink.cmp').get_lsp_capabilities(config.capabilities) + lspconfig[server].setup(config) + end + end + + -- example calling setup directly for each LSP + config = function() + local capabilities = require('blink.cmp').get_lsp_capabilities() + local lspconfig = require('lspconfig') + + lspconfig['lua-ls'].setup({ capabilities = capabilities }) + end +} +``` + +## `mini.deps` + +The following section includes only the installation and, optionally, building of the fuzzy matcher. Check the [lazy.nvim](#lazy.nvim) section for recommended configuration options and setting up `nvim-lspconfig`. + +```lua +-- use a release tag to download pre-built binaries +MiniDeps.add({ + source = "saghen/blink.cmp", + depends = { + "rafamadriz/friendly-snippets", + }, + checkout = "some.version", -- check releases for latest tag +}) + +-- OR build from source, requires nightly: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust +local function build_blink(params) + vim.notify('Building blink.cmp', vim.log.levels.INFO) + local obj = vim.system({ 'cargo', 'build', '--release' }, { cwd = params.path }):wait() + if obj.code == 0 then + vim.notify('Building blink.cmp done', vim.log.levels.INFO) + else + vim.notify('Building blink.cmp failed', vim.log.levels.ERROR) + end +end + +MiniDeps.add({ + source = 'Saghen/blink.cmp', + hooks = { + post_install = build_blink, + post_checkout = build_blink, + }, +}) +``` diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/package-lock.json b/mut/neovim/pack/plugins/start/blink.cmp/docs/package-lock.json new file mode 100644 index 0000000..9f52631 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/package-lock.json @@ -0,0 +1,2525 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@catppuccin/vitepress": "^0.1.0", + "@types/node": "^22.10.5", + "markdown-it-task-lists": "^2.1.1", + "vitepress": "^1.5.0", + "vitepress-plugin-tabs": "^0.5.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.18.0.tgz", + "integrity": "sha512-DLIrAukjsSrdMNNDx1ZTks72o4RH/1kOn8Wx5zZm8nnqFexG+JzY4SANnCNEjnFQPJTTvC+KpgiNW/CP2lumng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.18.0.tgz", + "integrity": "sha512-0VpGG2uQW+h2aejxbG8VbnMCQ9ary9/ot7OASXi6OjE0SRkYQ/+pkW+q09+IScif3pmsVVYggmlMPtAsmYWHng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.18.0.tgz", + "integrity": "sha512-X1WMSC+1ve2qlMsemyTF5bIjwipOT+m99Ng1Tyl36ZjQKTa54oajBKE0BrmM8LD8jGdtukAgkUhFoYOaRbMcmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.18.0.tgz", + "integrity": "sha512-FAJRNANUOSs/FgYOJ/Njqp+YTe4TMz2GkeZtfsw1TMiA5mVNRS/nnMpxas9771aJz7KTEWvK9GwqPs0K6RMYWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.18.0.tgz", + "integrity": "sha512-I2dc94Oiwic3SEbrRp8kvTZtYpJjGtg5y5XnqubgnA15AgX59YIY8frKsFG8SOH1n2rIhUClcuDkxYQNXJLg+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.18.0.tgz", + "integrity": "sha512-x6XKIQgKFTgK/bMasXhghoEjHhmgoP61pFPb9+TaUJ32aKOGc65b12usiGJ9A84yS73UDkXS452NjyP50Knh/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.18.0.tgz", + "integrity": "sha512-qI3LcFsVgtvpsBGR7aNSJYxhsR+Zl46+958ODzg8aCxIcdxiK7QEVLMJMZAR57jGqW0Lg/vrjtuLFDMfSE53qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.18.0.tgz", + "integrity": "sha512-bGvJg7HnGGm+XWYMDruZXWgMDPVt4yCbBqq8DM6EoaMBK71SYC4WMfIdJaw+ABqttjBhe6aKNRkWf/bbvYOGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.18.0.tgz", + "integrity": "sha512-lBssglINIeGIR+8KyzH05NAgAmn1BCrm5D2T6pMtr/8kbTHvvrm1Zvcltc5dKUQEFyyx3J5+MhNc7kfi8LdjVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.18.0.tgz", + "integrity": "sha512-uSnkm0cdAuFwdMp4pGT5vHVQ84T6AYpTZ3I0b3k/M3wg4zXDhl3aCiY8NzokEyRLezz/kHLEEcgb/tTTobOYVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.18.0.tgz", + "integrity": "sha512-1XFjW0C3pV0dS/9zXbV44cKI+QM4ZIz9cpatXpsjRlq6SUCpLID3DZHsXyE6sTb8IhyPaUjk78GEJT8/3hviqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.18.0.tgz", + "integrity": "sha512-0uodeNdAHz1YbzJh6C5xeQ4T6x5WGiUxUq3GOaT/R4njh5t78dq+Rb187elr7KtnjUmETVVuCvmEYaThfTHzNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.18.0.tgz", + "integrity": "sha512-tZCqDrqJ2YE2I5ukCQrYN8oiF6u3JIdCxrtKq+eniuLkjkO78TKRnXrVcKZTmfFJyyDK8q47SfDcHzAA3nHi6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@catppuccin/vitepress": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@catppuccin/vitepress/-/vitepress-0.1.0.tgz", + "integrity": "sha512-HJilKL8dzidGWwDBNcGVzDmjHocwhaxQ0tleRf0/Aab7MDCHK63jzoYHifXro6mH+AtK120WHfsHiJl54sSaww==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "^5.0.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.16.tgz", + "integrity": "sha512-mnQ0Ih8CDIgOqbi0qz01AJNOeFVuGFRimelg3JmJtD0y5EpZVw+enPPcpcxJKipsRZ/oqhcP3AhYkF1kM7yomg==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.3.tgz", + "integrity": "sha512-VRcf4GYUIkxIchGM9DrapRcxtgojg4IWKUtX5EtW+4PJiGzF2xQqZSv27PJt+WLc18KT3CNLpNWow9JYV5n+Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.24.3", + "@shikijs/engine-oniguruma": "1.24.3", + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.3.tgz", + "integrity": "sha512-De8tNLvYjeK6V0Gb47jIH2M+OKkw+lWnSV1j3HVDFMlNIglmVcTMG2fASc29W0zuFbfEEwKjO8Fe4KYSO6Ce3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1", + "oniguruma-to-es": "0.8.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.3.tgz", + "integrity": "sha512-iNnx950gs/5Nk+zrp1LuF+S+L7SKEhn8k9eXgFYPGhVshKppsYwRmW8tpmAMvILIMSDfrgqZ0w+3xWVQB//1Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1" + } + }, + "node_modules/@shikijs/transformers": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.24.3.tgz", + "integrity": "sha512-Zdu+pVZwQkUy/KWIVJFQlSqZGvPySU6oYZ2TsBKp3Ay5m1XzykPSeiaVvAh6LtyaXYDLeCdA3LjZfHyTXFjGTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "shiki": "1.24.3" + } + }, + "node_modules/@shikijs/types": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.3.tgz", + "integrity": "sha512-FPMrJ69MNxhRtldRk69CghvaGlbbN3pKRuvko0zvbfa2dXp4pAngByToqS5OY5jvN8D7LKR4RJE8UvzlCOuViw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.1.tgz", + "integrity": "sha512-79QfK1393x9Ho60QFyLti+QfdJzRQCVLFb97kOIV7Eo9vQU/roINgk7m24uv0a7AUvN//RDH36FLjjK48v0s9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", + "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.6.8.tgz", + "integrity": "sha512-ma6dY/sZR36zALVsV1W7eC57c6IJPXsy8SNgZn1PLVWU4z4dPn5TIBmnF4stmdJ4sQcixqKaQ8pwjbMPzEZwiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.6.8" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.8.tgz", + "integrity": "sha512-JhJ8M3sPU+v0P2iZBF2DkdmR9L0dnT5RXJabJqX6o8KtFs3tebdvfoXV2Dm3BFuqeECuMJIfF1aCzSt+WQ4wrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.6.8", + "birpc": "^0.2.19", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.8.tgz", + "integrity": "sha512-9MBPO5Z3X1nYGFqTJyohl6Gmf/J7UNN1oicHdyzBVZP4jnhZ4c20MgtaHDIzWmHDHCMYVS5bwKxT3jxh7gOOKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz", + "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "11.3.0", + "@vueuse/shared": "11.3.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.3.0.tgz", + "integrity": "sha512-5fzRl0apQWrDezmobchoiGTkGw238VWESxZHazfhP3RM7pDSiyXy18QbfYkILoYNTd23HPAfQTJpkUc5QbkwTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "11.3.0", + "@vueuse/shared": "11.3.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz", + "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz", + "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.18.0.tgz", + "integrity": "sha512-/tfpK2A4FpS0o+S78o3YSdlqXr0MavJIDlFK3XZrlXLy7vaRXJvW5jYg3v5e/wCaF8y0IpMjkYLhoV6QqfpOgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-abtesting": "5.18.0", + "@algolia/client-analytics": "5.18.0", + "@algolia/client-common": "5.18.0", + "@algolia/client-insights": "5.18.0", + "@algolia/client-personalization": "5.18.0", + "@algolia/client-query-suggestions": "5.18.0", + "@algolia/client-search": "5.18.0", + "@algolia/ingestion": "1.18.0", + "@algolia/monitoring": "1.18.0", + "@algolia/recommend": "5.18.0", + "@algolia/requester-browser-xhr": "5.18.0", + "@algolia/requester-fetch": "5.18.0", + "@algolia/requester-node-http": "5.18.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.2.tgz", + "integrity": "sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz", + "integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true, + "license": "ISC" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", + "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.1.tgz", + "integrity": "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.8.0.tgz", + "integrity": "sha512-rY+/a6b+uCgoYIL9itjY0x99UUDHXmGaw7Jjk5ZvM/3cxDJifyxFr/Zm4tTmF6Tre18gAakJo7AzhKUeMNLgHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.0.2", + "regex-recursion": "^5.0.0" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.25.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.3.tgz", + "integrity": "sha512-dzQmIFtM970z+fP9ziQ3yG4e3ULIbwZzJ734vaMVUTaKQ2+Ru1Ou/gjshOYVHCcd1rpAelC6ngjvjDXph98unQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.0.2.tgz", + "integrity": "sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.0.0.tgz", + "integrity": "sha512-UwyOqeobrCCqTXPcsSqH4gDhOjD5cI/b8kjngWgSZbxYh5yVjAwTjO5+hAuPRNiuR70+5RlWSs+U9PVcVcW9Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.24.3.tgz", + "integrity": "sha512-eMeX/ehE2IDKVs71kB4zVcDHjutNcOtm+yIRuR4sA6ThBbdFI0DffGJiyoKCodj0xRGxIoWC3pk/Anmm5mzHmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.24.3", + "@shikijs/engine-javascript": "1.24.3", + "@shikijs/engine-oniguruma": "1.24.3", + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.5.0.tgz", + "integrity": "sha512-q4Q/G2zjvynvizdB3/bupdYkCJe2umSAMv9Ju4d92E6/NXJ59z70xB0q5p/4lpRyAwflDsbwy1mLV9Q5+nlB+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "^3.6.2", + "@docsearch/js": "^3.6.2", + "@iconify-json/simple-icons": "^1.2.10", + "@shikijs/core": "^1.22.2", + "@shikijs/transformers": "^1.22.2", + "@shikijs/types": "^1.22.2", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/devtools-api": "^7.5.4", + "@vue/shared": "^3.5.12", + "@vueuse/core": "^11.1.0", + "@vueuse/integrations": "^11.1.0", + "focus-trap": "^7.6.0", + "mark.js": "8.11.1", + "minisearch": "^7.1.0", + "shiki": "^1.22.2", + "vite": "^5.4.10", + "vue": "^3.5.12" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vitepress-plugin-tabs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/vitepress-plugin-tabs/-/vitepress-plugin-tabs-0.5.0.tgz", + "integrity": "sha512-SIhFWwGsUkTByfc2b279ray/E0Jt8vDTsM1LiHxmCOBAEMmvzIBZSuYYT1DpdDTiS3SuJieBheJkYnwCq/yD9A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vitepress": "^1.0.0-rc.27", + "vue": "^3.3.8" + } + }, + "node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/package.json b/mut/neovim/pack/plugins/start/blink.cmp/docs/package.json new file mode 100644 index 0000000..b9e67d5 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/package.json @@ -0,0 +1,15 @@ +{ + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "build:release": "IS_RELEASE=true vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "@catppuccin/vitepress": "^0.1.0", + "@types/node": "^22.10.5", + "markdown-it-task-lists": "^2.1.1", + "vitepress": "^1.5.0", + "vitepress-plugin-tabs": "^0.5.0" + } +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/public/favicon.png b/mut/neovim/pack/plugins/start/blink.cmp/docs/public/favicon.png Binary files differnew file mode 100644 index 0000000..d40a396 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/public/favicon.png diff --git a/mut/neovim/pack/plugins/start/blink.cmp/docs/recipes.md b/mut/neovim/pack/plugins/start/blink.cmp/docs/recipes.md new file mode 100644 index 0000000..9b5cf3d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/docs/recipes.md @@ -0,0 +1,241 @@ +# Recipes + +[[toc]] + +## General + +### Disable per filetype + +```lua +enabled = function() + return not vim.tbl_contains({ "lua", "markdown" }, vim.bo.filetype) + and vim.bo.buftype ~= "prompt" + and vim.b.completion ~= false +end, +``` + +### Border + +```lua +completion = { + menu = { border = 'single' }, + documentation = { window = { border = 'single' } }, +}, +signature = { window = { border = 'single' } }, +``` + +### Change selection type per mode + +```lua +completion = { + list = { + selection = { + preselect = function(ctx) return ctx.mode ~= 'cmdline' end, + auto_insert = function(ctx) return ctx.mode ~= 'cmdline' end + } + } +} +``` + +### Don't show completion menu automatically in cmdline mode + +```lua +completion = { + menu = { auto_show = function(ctx) return ctx.mode ~= 'cmdline' end } +} +``` + +### Don't show completion menu automatically when searching + +```lua +completion = { + menu = { + auto_show = function(ctx) + return ctx.mode ~= "cmdline" or not vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) + end, + }, +} +``` + +### Select Nth item from the list + +Here's an example configuration that allows you to select the nth item from the list, based on [#382](https://github.com/Saghen/blink.cmp/issues/382): + +```lua +keymap = { + preset = 'default', + ['<A-1>'] = { function(cmp) cmp.accept({ index = 1 }) end }, + ['<A-2>'] = { function(cmp) cmp.accept({ index = 2 }) end }, + ['<A-3>'] = { function(cmp) cmp.accept({ index = 3 }) end }, + ['<A-4>'] = { function(cmp) cmp.accept({ index = 4 }) end }, + ['<A-5>'] = { function(cmp) cmp.accept({ index = 5 }) end }, + ['<A-6>'] = { function(cmp) cmp.accept({ index = 6 }) end }, + ['<A-7>'] = { function(cmp) cmp.accept({ index = 7 }) end }, + ['<A-8>'] = { function(cmp) cmp.accept({ index = 8 }) end }, + ['<A-9>'] = { function(cmp) cmp.accept({ index = 9 }) end }, + ['<A-0>'] = { function(cmp) cmp.accept({ index = 10 }) end }, +}, +completion = { + menu = { + draw = { + columns = { { 'item_idx' }, { 'kind_icon' }, { 'label', 'label_description', gap = 1 } }, + components = { + item_idx = { + text = function(ctx) return ctx.idx == 10 and '0' or ctx.idx >= 10 and ' ' or tostring(ctx.idx) end, + highlight = 'BlinkCmpItemIdx' -- optional, only if you want to change its color + } + } + } + } +} +``` + +### `mini.icons` + +[Original discussion](https://github.com/Saghen/blink.cmp/discussions/458) + +```lua +completion = { + menu = { + draw = { + components = { + kind_icon = { + ellipsis = false, + text = function(ctx) + local kind_icon, _, _ = require('mini.icons').get('lsp', ctx.kind) + return kind_icon + end, + -- Optionally, you may also use the highlights from mini.icons + highlight = function(ctx) + local _, hl, _ = require('mini.icons').get('lsp', ctx.kind) + return hl + end, + } + } + } + } +} +``` + +### Hide Copilot on suggestion + +```lua +vim.api.nvim_create_autocmd('User', { + pattern = 'BlinkCmpMenuOpen', + callback = function() + require("copilot.suggestion").dismiss() + vim.b.copilot_suggestion_hidden = true + end, +}) + +vim.api.nvim_create_autocmd('User', { + pattern = 'BlinkCmpMenuClose', + callback = function() + vim.b.copilot_suggestion_hidden = false + end, +}) +``` + +### Show on newline, tab and space + +Note that you may want to add the override to other sources as well, since if the LSP doesnt return any items, we won't show the menu if it was triggered by any of these three characters. + +```lua +-- by default, blink.cmp will block newline, tab and space trigger characters, disable that behavior +completion.trigger.blocked_trigger_characters = {} + +-- add newline, tab and space to LSP source trigger characters +sources.providers.lsp.override.get_trigger_characters = function(self) + local trigger_characters = self:get_trigger_characters() + vim.list_extend(trigger_characters, { '\n', '\t', ' ' }) + return trigger_characters +end +``` + +## Sources + +### Dynamically picking providers by treesitter node/filetype + +```lua +sources.default = function(ctx) + local success, node = pcall(vim.treesitter.get_node) + if vim.bo.filetype == 'lua' then + return { 'lsp', 'path' } + elseif success and node and vim.tbl_contains({ 'comment', 'line_comment', 'block_comment' }, node:type()) then + return { 'buffer' } + else + return { 'lsp', 'path', 'snippets', 'buffer' } + end +end +``` + +### Hide snippets after trigger character + +> [!NOTE] +> Untested, might not work well, please open a PR if you find a better solution! + +Trigger characters are defined by the sources. For example, for Lua, the trigger characters are `.`, `"`, `'`. + +```lua +sources.providers.snippets.should_show_items = function(ctx) + return ctx.trigger.initial_kind ~= 'trigger_character' +end +``` + +### Disable all snippets + +See the [relevant section in the snippets documentation](./configuration/snippets.md#disable-all-snippets) + +### Set minimum keyword length by filetype + +```lua +sources.min_keyword_length = function() + return vim.bo.filetype == 'markdown' and 2 or 0 +end +``` + +## For writers + +When writing prose, you may want significantly different behavior than typical LSP completions. If you find any interesting configurations, please open a PR adding it here! + +### Keep first letter capitalization on buffer source + +```lua +sources = { + providers = { + buffer = { + -- keep case of first char + transform_items = function (a, items) + local keyword = a.get_keyword() + local correct, case + if keyword:match('^%l') then + correct = '^%u%l+$' + case = string.lower + elseif keyword:match('^%u') then + correct = '^%l+$' + case = string.upper + else + return items + end + + -- avoid duplicates from the corrections + local seen = {} + local out = {} + for _, item in ipairs(items) do + local raw = item.insertText + if raw:match(correct) then + local text = case(raw:sub(1,1)) .. raw:sub(2) + item.insertText = text + item.label = text + end + if not seen[item.insertText] then + seen[item.insertText] = true + table.insert(out, item) + end + end + return out + end + } + } +} +``` diff --git a/mut/neovim/pack/plugins/start/blink.cmp/flake.lock b/mut/neovim/pack/plugins/start/blink.cmp/flake.lock new file mode 100644 index 0000000..7667448 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/flake.lock @@ -0,0 +1,97 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1734676450, + "narHash": "sha256-iwcxhTVe4h5TqW0HsNiOQP27eMBmbBshF+q2UjEy5aU=", + "owner": "nix-community", + "repo": "fenix", + "rev": "46e19fa0eb3260b2c3ee5b2cf89e73343c1296ab", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1736166416, + "narHash": "sha256-U47xeACNBpkSO6IcCm0XvahsVXpJXzjPIQG7TZlOToU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "b30f97d8c32d804d2d832ee837d0f1ca0695faa5", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1733096140, + "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1734622712, + "narHash": "sha256-2Oc2LbFypF1EG3zTVIVcuT5XFJ7R3oAwu2tS8B0qQ0I=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "fe027d79d22f2a7645da4143f5cc0f5f56239b97", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/flake.nix b/mut/neovim/pack/plugins/start/blink.cmp/flake.nix new file mode 100644 index 0000000..23e5799 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/flake.nix @@ -0,0 +1,117 @@ +{ + description = "Set of simple, performant neovim plugins"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + fenix.url = "github:nix-community/fenix"; + fenix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = inputs@{ flake-parts, nixpkgs, ... }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = + [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; + + perSystem = { self, config, self', inputs', pkgs, system, lib, ... }: { + # use fenix overlay + _module.args.pkgs = import nixpkgs { + inherit system; + overlays = [ inputs.fenix.overlays.default ]; + }; + + # define the packages provided by this flake + packages = let + fs = lib.fileset; + # nix source files (*.nix) + nixFs = fs.fileFilter (file: file.hasExt == "nix") ./.; + # rust source files + rustFs = fs.unions [ + # Cargo.* + (fs.fileFilter (file: lib.hasPrefix "Cargo" file.name) ./.) + # *.rs + (fs.fileFilter (file: file.hasExt "rs") ./.) + # additional files + ./.cargo + ./rust-toolchain.toml + ]; + # nvim source files + # all that are not nix, nor rust, nor other ignored files + nvimFs = + fs.difference ./. (fs.unions [ nixFs rustFs ./docs ./repro.lua ]); + version = "0.10.0"; + in { + blink-fuzzy-lib = let + inherit (inputs'.fenix.packages.minimal) toolchain; + rustPlatform = pkgs.makeRustPlatform { + cargo = toolchain; + rustc = toolchain; + }; + in rustPlatform.buildRustPackage { + pname = "blink-fuzzy-lib"; + inherit version; + src = fs.toSource { + root = ./.; + fileset = rustFs; + }; + cargoLock = { + lockFile = ./Cargo.lock; + allowBuiltinFetchGit = true; + }; + + nativeBuildInputs = with pkgs; [ git ]; + }; + + blink-cmp = pkgs.vimUtils.buildVimPlugin { + pname = "blink-cmp"; + inherit version; + src = fs.toSource { + root = ./.; + fileset = nvimFs; + }; + preInstall = '' + mkdir -p target/release + ln -s ${self'.packages.blink-fuzzy-lib}/lib/libblink_cmp_fuzzy.* target/release/ + ''; + }; + + default = self'.packages.blink-cmp; + }; + + # builds the native module of the plugin + apps.build-plugin = { + type = "app"; + program = let + buildScript = pkgs.writeShellApplication { + name = "build-plugin"; + runtimeInputs = with pkgs; [ fenix.minimal.toolchain gcc ]; + text = '' + export LIBRARY_PATH="${lib.makeLibraryPath [ pkgs.libiconv ]}"; + cargo build --release + ''; + }; + in (lib.getExe buildScript); + }; + + # define the default dev environment + devShells.default = pkgs.mkShell { + name = "blink"; + inputsFrom = [ + self'.packages.blink-fuzzy-lib + self'.packages.blink-cmp + self'.apps.build-plugin + ]; + packages = with pkgs; [ rust-analyzer-nightly ]; + }; + + formatter = pkgs.nixfmt-classic; + }; + }; + + nixConfig = { + extra-substituters = [ "https://nix-community.cachix.org" ]; + extra-trusted-public-keys = [ + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs" + ]; + }; +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink-cmp.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink-cmp.lua new file mode 100644 index 0000000..3009bed --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink-cmp.lua @@ -0,0 +1,2 @@ +return require('blink.cmp') + diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/init.lua new file mode 100644 index 0000000..c4041d3 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/init.lua @@ -0,0 +1,110 @@ +local text_edits_lib = require('blink.cmp.lib.text_edits') +local brackets_lib = require('blink.cmp.completion.brackets') + +--- Applies a completion item to the current buffer +--- @param ctx blink.cmp.Context +--- @param item blink.cmp.CompletionItem +--- @param callback fun() +local function accept(ctx, item, callback) + local sources = require('blink.cmp.sources.lib') + require('blink.cmp.completion.trigger').hide() + + -- Start the resolve immediately since text changes can invalidate the item + -- with some LSPs (i.e. rust-analyzer) causing them to return the item as-is + -- without i.e. auto-imports + sources + .resolve(ctx, item) + :map(function(item) + item = vim.deepcopy(item) + + -- Get additional text edits, converted to utf-8 + local all_text_edits = vim.deepcopy(item.additionalTextEdits or {}) + all_text_edits = vim.tbl_map( + function(text_edit) return text_edits_lib.to_utf_8(text_edit, text_edits_lib.offset_encoding_from_item(item)) end, + all_text_edits + ) + + -- TODO: it's not obvious that this is converting to utf-8 + item.textEdit = text_edits_lib.get_from_item(item) + + -- Create an undo point, if it's not a snippet, since the snippet engine should handle undo + if + ctx.mode == 'default' + and require('blink.cmp.config').completion.accept.create_undo_point + and item.insertTextFormat ~= vim.lsp.protocol.InsertTextFormat.Snippet + -- HACK: We check the kind here because the Luasnip source returns PlainText and handles + -- expansion itself. Otherwise, Luasnip will fail to enter select mode + -- https://github.com/Saghen/blink.cmp/commit/284dd37f9bbc632f8281d6361e877db5b45e6ff0#r150498482 + and item.kind ~= require('blink.cmp.types').CompletionItemKind.Snippet + then + -- setting the undolevels forces neovim to create an undo point + vim.o.undolevels = vim.o.undolevels + end + + -- Ignore snippets that only contain text + -- FIXME: doesn't handle escaped snippet placeholders "\\$1" should output "$1", not "\$1" + if + item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet + and item.kind ~= require('blink.cmp.types').CompletionItemKind.Snippet + then + local parsed_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(item.textEdit.newText) + if + parsed_snippet ~= nil + and #parsed_snippet.data.children == 1 + and parsed_snippet.data.children[1].type == vim.lsp._snippet_grammar.NodeType.Text + then + item.insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText + end + end + + -- Add brackets to the text edit if needed + local brackets_status, text_edit_with_brackets, offset = brackets_lib.add_brackets(ctx, vim.bo.filetype, item) + item.textEdit = text_edit_with_brackets + + -- Snippet + if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then + assert(ctx.mode == 'default', 'Snippets are only supported in default mode') + + -- We want to handle offset_encoding and the text edit api can do this for us + -- so we empty the newText and apply + local temp_text_edit = vim.deepcopy(item.textEdit) + temp_text_edit.newText = '' + table.insert(all_text_edits, temp_text_edit) + text_edits_lib.apply(all_text_edits) + + -- Expand the snippet + require('blink.cmp.config').snippets.expand(item.textEdit.newText) + + -- OR Normal: Apply the text edit and move the cursor + else + table.insert(all_text_edits, item.textEdit) + text_edits_lib.apply(all_text_edits) + -- TODO: should move the cursor only by the offset since text edit handles everything else? + ctx.set_cursor({ ctx.get_cursor()[1], item.textEdit.range.start.character + #item.textEdit.newText + offset }) + end + + -- Let the source execute the item itself + sources.execute(ctx, item):map(function() + -- Check semantic tokens for brackets, if needed, and apply additional text edits + if brackets_status == 'check_semantic_token' then + -- TODO: since we apply the additional text edits after, auto imported functions will not + -- get auto brackets. If we apply them before, we have to modify the textEdit to compensate + brackets_lib.add_brackets_via_semantic_token(vim.bo.filetype, item, function() + require('blink.cmp.completion.trigger').show_if_on_trigger_character({ is_accept = true }) + require('blink.cmp.signature.trigger').show_if_on_trigger_character() + callback() + end) + else + require('blink.cmp.completion.trigger').show_if_on_trigger_character({ is_accept = true }) + require('blink.cmp.signature.trigger').show_if_on_trigger_character() + callback() + end + + -- Notify the rust module that the item was accessed + require('blink.cmp.fuzzy').access(item) + end) + end) + :catch(function(err) vim.notify(err, vim.log.levels.ERROR, { title = 'blink.cmp' }) end) +end + +return accept diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/prefix.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/prefix.lua new file mode 100644 index 0000000..3c51715 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/prefix.lua @@ -0,0 +1,58 @@ +local PAIRS_AND_INVALID_CHARS = {} +string.gsub('\'"=$()[]<>{} \t\n\r', '.', function(char) PAIRS_AND_INVALID_CHARS[string.byte(char)] = true end) + +local CLOSING_PAIR = { + [string.byte('<')] = string.byte('>'), + [string.byte('[')] = string.byte(']'), + [string.byte('(')] = string.byte(')'), + [string.byte('{')] = string.byte('}'), + [string.byte('"')] = string.byte('"'), + [string.byte("'")] = string.byte("'"), +} + +local ALPHANUMERIC = {} +string.gsub( + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + '.', + function(char) ALPHANUMERIC[string.byte(char)] = true end +) + +--- Gets the prefix of the given text, stopping at brackets and quotes +--- @param text string +--- @return string +local function get_prefix_before_brackets_and_quotes(text) + local closing_pairs_stack = {} + local word = '' + + local add = function(char) + word = word .. string.char(char) + + -- if we've seen the opening pair, and we've just received the closing pair, + -- remove it from the closing pairs stack + if closing_pairs_stack[#closing_pairs_stack] == char then + table.remove(closing_pairs_stack, #closing_pairs_stack) + -- if the character is an opening pair, add it to the closing pairs stack + elseif CLOSING_PAIR[char] ~= nil then + table.insert(closing_pairs_stack, CLOSING_PAIR[char]) + end + end + + local has_alphanumeric = false + for i = 1, #text do + local char = string.byte(text, i) + if PAIRS_AND_INVALID_CHARS[char] == nil then + add(char) + has_alphanumeric = has_alphanumeric or ALPHANUMERIC[char] + elseif not has_alphanumeric or #closing_pairs_stack ~= 0 then + add(char) + -- if we had an alphanumeric, and the closing pairs stack *just* emptied, + -- because the current character is a closing pair, we exit + if has_alphanumeric and #closing_pairs_stack == 0 then break end + else + break + end + end + return word +end + +return get_prefix_before_brackets_and_quotes diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/preview.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/preview.lua new file mode 100644 index 0000000..88b46e2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/accept/preview.lua @@ -0,0 +1,35 @@ +--- @param item blink.cmp.CompletionItem +--- @return lsp.TextEdit undo_text_edit, integer[]? undo_cursor_pos The text edit to apply and the original cursor +--- position to move to when undoing the preview, +local function preview(item) + local text_edits_lib = require('blink.cmp.lib.text_edits') + local text_edit = text_edits_lib.get_from_item(item) + + if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then + local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(text_edit.newText) + local snippet = expanded_snippet and tostring(expanded_snippet) or text_edit.newText + local get_prefix_before_brackets_and_quotes = require('blink.cmp.completion.accept.prefix') + text_edit.newText = get_prefix_before_brackets_and_quotes(snippet) + end + + local undo_text_edit = text_edits_lib.get_undo_text_edit(text_edit) + local cursor_pos = { + text_edit.range.start.line + 1, + text_edit.range.start.character + #text_edit.newText, + } + + text_edits_lib.apply({ text_edit }) + + local original_cursor = vim.api.nvim_win_get_cursor(0) + local cursor_moved = false + + -- TODO: remove when text_edits_lib.apply begins setting cursor position + if vim.api.nvim_get_mode().mode ~= 'c' then + vim.api.nvim_win_set_cursor(0, cursor_pos) + cursor_moved = true + end + + return undo_text_edit, cursor_moved and original_cursor or nil +end + +return preview diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/config.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/config.lua new file mode 100644 index 0000000..9201a08 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/config.lua @@ -0,0 +1,37 @@ +return { + -- stylua: ignore + blocked_filetypes = { + 'sql', 'ruby', 'perl', 'lisp', 'scheme', 'clojure', + 'prolog', 'vb', 'elixir', 'smalltalk', 'applescript' + }, + per_filetype = { + -- languages with a space + haskell = { ' ', '' }, + fsharp = { ' ', '' }, + ocaml = { ' ', '' }, + erlang = { ' ', '' }, + tcl = { ' ', '' }, + nix = { ' ', '' }, + helm = { ' ', '' }, + + shell = { ' ', '' }, + sh = { ' ', '' }, + bash = { ' ', '' }, + fish = { ' ', '' }, + zsh = { ' ', '' }, + powershell = { ' ', '' }, + + make = { ' ', '' }, + + -- languages with square brackets + wl = { '[', ']' }, + wolfram = { '[', ']' }, + mma = { '[', ']' }, + mathematica = { '[', ']' }, + context = { '[', ']' }, + + -- languages with curly brackets + tex = { '{', '}' }, + plaintex = { '{', '}' }, + }, +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/init.lua new file mode 100644 index 0000000..511b42b --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/init.lua @@ -0,0 +1,6 @@ +local brackets = {} + +brackets.add_brackets = require('blink.cmp.completion.brackets.kind') +brackets.add_brackets_via_semantic_token = require('blink.cmp.completion.brackets.semantic') + +return brackets diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/kind.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/kind.lua new file mode 100644 index 0000000..f09f180 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/kind.lua @@ -0,0 +1,52 @@ +local utils = require('blink.cmp.completion.brackets.utils') + +--- @param ctx blink.cmp.Context +--- @param filetype string +--- @param item blink.cmp.CompletionItem +--- @return 'added' | 'check_semantic_token' | 'skipped', lsp.TextEdit | lsp.InsertReplaceEdit, number +local function add_brackets(ctx, filetype, item) + local text_edit = item.textEdit + assert(text_edit ~= nil, 'Got nil text edit while adding brackets via kind') + local brackets_for_filetype = utils.get_for_filetype(filetype, item) + + -- skip if we're not in default mode + if ctx.mode ~= 'default' then return 'skipped', text_edit, 0 end + + -- if there's already the correct brackets in front, skip but indicate the cursor should move in front of the bracket + -- TODO: what if the brackets_for_filetype[1] == '' or ' ' (haskell/ocaml)? + -- TODO: should this check semantic tokens and still move the cursor in that case? + if utils.has_brackets_in_front(text_edit, brackets_for_filetype[1]) then + local offset = utils.can_have_brackets(item, brackets_for_filetype) and #brackets_for_filetype[1] or 0 + return 'skipped', text_edit, offset + end + + -- if the item already contains the brackets, conservatively skip adding brackets + -- todo: won't work for snippets when the brackets_for_filetype is { '{', '}' } + -- I've never seen a language like that though + if brackets_for_filetype[1] ~= ' ' and text_edit.newText:match('[\\' .. brackets_for_filetype[1] .. ']') ~= nil then + return 'skipped', text_edit, 0 + end + + -- check if configuration incidates we should skip + if not utils.should_run_resolution(filetype, 'kind') then return 'check_semantic_token', text_edit, 0 end + -- cannot have brackets, skip + if not utils.can_have_brackets(item, brackets_for_filetype) then return 'check_semantic_token', text_edit, 0 end + + text_edit = vim.deepcopy(text_edit) + -- For snippets, we add the cursor position between the brackets as the last placeholder + if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then + local placeholders = utils.snippets_extract_placeholders(text_edit.newText) + local last_placeholder_index = math.max(0, unpack(placeholders)) + text_edit.newText = text_edit.newText + .. brackets_for_filetype[1] + .. '$' + .. tostring(last_placeholder_index + 1) + .. brackets_for_filetype[2] + -- Otherwise, we add as usual + else + text_edit.newText = text_edit.newText .. brackets_for_filetype[1] .. brackets_for_filetype[2] + end + return 'added', text_edit, -#brackets_for_filetype[2] +end + +return add_brackets diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/semantic.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/semantic.lua new file mode 100644 index 0000000..c64afbd --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/semantic.lua @@ -0,0 +1,109 @@ +local config = require('blink.cmp.config').completion.accept.auto_brackets +local utils = require('blink.cmp.completion.brackets.utils') + +local semantic = {} + +--- Asynchronously use semantic tokens to determine if brackets should be added +--- @param filetype string +--- @param item blink.cmp.CompletionItem +--- @param callback fun() +function semantic.add_brackets_via_semantic_token(filetype, item, callback) + if not utils.should_run_resolution(filetype, 'semantic_token') then return callback() end + + local text_edit = item.textEdit + assert(text_edit ~= nil, 'Got nil text edit while adding brackets via semantic tokens') + local client = vim.lsp.get_client_by_id(item.client_id) + if client == nil then return callback() end + + local capabilities = client.server_capabilities.semanticTokensProvider + if not capabilities or not capabilities.legend or (not capabilities.range and not capabilities.full) then + return callback() + end + + local token_types = client.server_capabilities.semanticTokensProvider.legend.tokenTypes + local params = { + textDocument = vim.lsp.util.make_text_document_params(), + range = capabilities.range and { + start = { line = text_edit.range.start.line, character = text_edit.range.start.character }, + ['end'] = { line = text_edit.range.start.line + 1, character = 0 }, + } or nil, + } + + local cursor_before_call = vim.api.nvim_win_get_cursor(0) + + local start_time = vim.uv.hrtime() + client.request( + capabilities.range and 'textDocument/semanticTokens/range' or 'textDocument/semanticTokens/full', + params, + function(err, result) + if err ~= nil or result == nil or #result.data == 0 then return callback() end + + -- cancel if it's been too long, or if the cursor moved + local ms_since_call = (vim.uv.hrtime() - start_time) / 1000000 + local cursor_after_call = vim.api.nvim_win_get_cursor(0) + if + ms_since_call > config.semantic_token_resolution.timeout_ms + or cursor_before_call[1] ~= cursor_after_call[1] + or cursor_before_call[2] ~= cursor_after_call[2] + then + return callback() + end + + for _, token in ipairs(semantic.process_semantic_token_data(result.data, token_types)) do + if + cursor_after_call[1] == token.line + and cursor_after_call[2] >= token.start_col + and cursor_after_call[2] <= token.end_col + and (token.type == 'function' or token.type == 'method') + then + -- add the brackets + local brackets_for_filetype = utils.get_for_filetype(filetype, item) + local line = vim.api.nvim_get_current_line() + local start_col = text_edit.range.start.character + #text_edit.newText + local new_line = line:sub(1, start_col) + .. brackets_for_filetype[1] + .. brackets_for_filetype[2] + .. line:sub(start_col + 1) + vim.api.nvim_set_current_line(new_line) + vim.api.nvim_win_set_cursor(0, { cursor_after_call[1], start_col + #brackets_for_filetype[1] }) + callback() + return + end + end + + callback() + end + ) +end + +function semantic.process_semantic_token_data(data, token_types) + local tokens = {} + local idx = 0 + local token_line = 0 + local token_start_col = 0 + + while (idx + 1) * 5 <= #data do + local delta_token_line = data[idx * 5 + 1] + local delta_token_start_col = data[idx * 5 + 2] + local delta_token_length = data[idx * 5 + 3] + local type = token_types[data[idx * 5 + 4] + 1] + + if delta_token_line > 0 then token_start_col = 0 end + token_line = token_line + delta_token_line + token_start_col = token_start_col + delta_token_start_col + + table.insert(tokens, { + line = token_line + 1, + start_col = token_start_col, + end_col = token_start_col + delta_token_length, + type = type, + }) + + token_start_col = token_start_col + delta_token_length + idx = idx + 1 + end + + return tokens +end + +return semantic.add_brackets_via_semantic_token diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/utils.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/utils.lua new file mode 100644 index 0000000..cf84f11 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/brackets/utils.lua @@ -0,0 +1,61 @@ +local config = require('blink.cmp.config').completion.accept.auto_brackets +local CompletionItemKind = require('blink.cmp.types').CompletionItemKind +local brackets = require('blink.cmp.completion.brackets.config') +local utils = {} + +--- @param snippet string +function utils.snippets_extract_placeholders(snippet) + local placeholders = {} + local pattern = [=[(\$\{(\d+)(:([^}\\]|\\.)*?)?\})]=] + + for _, number, _, _ in snippet:gmatch(pattern) do + table.insert(placeholders, tonumber(number)) + end + + return placeholders +end + +--- @param filetype string +--- @param item blink.cmp.CompletionItem +--- @return string[] +function utils.get_for_filetype(filetype, item) + local default = config.default_brackets + local per_filetype = config.override_brackets_for_filetypes[filetype] or brackets.per_filetype[filetype] + + if type(per_filetype) == 'function' then return per_filetype(item) or default end + return per_filetype or default +end + +--- @param filetype string +--- @param resolution_method 'kind' | 'semantic_token' +--- @return boolean +function utils.should_run_resolution(filetype, resolution_method) + -- resolution method specific + if not config[resolution_method .. '_resolution'].enabled then return false end + local resolution_blocked_filetypes = config[resolution_method .. '_resolution'].blocked_filetypes + if vim.tbl_contains(resolution_blocked_filetypes, filetype) then return false end + + -- global + if not config.enabled then return false end + if vim.tbl_contains(config.force_allow_filetypes, filetype) then return true end + return not vim.tbl_contains(config.blocked_filetypes, filetype) + and not vim.tbl_contains(brackets.blocked_filetypes, filetype) +end + +--- @param text_edit lsp.TextEdit | lsp.InsertReplaceEdit +--- @param bracket string +--- @return boolean +function utils.has_brackets_in_front(text_edit, bracket) + local line = vim.api.nvim_get_current_line() + local col = text_edit.range['end'].character + 1 + return line:sub(col, col) == bracket +end + +--- @param item blink.cmp.CompletionItem +--- @param _ string[] +-- TODO: for edge cases, we should probably also take brackets themselves into consideration +function utils.can_have_brackets(item, _) + return item.kind == CompletionItemKind.Function or item.kind == CompletionItemKind.Method +end + +return utils diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/init.lua new file mode 100644 index 0000000..49c6fc2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/init.lua @@ -0,0 +1,88 @@ +local config = require('blink.cmp.config') +local completion = {} + +function completion.setup() + -- trigger controls when to show the window and the current context for caching + local trigger = require('blink.cmp.completion.trigger') + trigger.activate() + + -- sources fetch completion items and documentation + local sources = require('blink.cmp.sources.lib') + + -- manages the completion list state: + -- fuzzy matching items + -- when to show/hide the windows + -- selection + -- accepting and previewing items + local list = require('blink.cmp.completion.list') + + -- trigger -> sources: request completion items from the sources on show + trigger.show_emitter:on(function(event) sources.request_completions(event.context) end) + trigger.hide_emitter:on(function() + sources.cancel_completions() + list.hide() + end) + + -- sources -> list + sources.completions_emitter:on(function(event) + -- schedule for later to avoid adding 0.5-4ms to insertion latency + vim.schedule(function() + -- since this was performed asynchronously, we check if the context has changed + if trigger.context == nil or event.context.id ~= trigger.context.id then return end + -- don't show the list if prefetching results + if event.context.trigger.kind == 'prefetch' then return end + + -- don't show if all the sources that defined the trigger character returned no items + if event.context.trigger.character ~= nil then + local triggering_source_returned_items = false + for _, source in pairs(event.context.providers) do + local trigger_characters = sources.get_provider_by_id(source):get_trigger_characters() + if + event.items[source] + and #event.items[source] > 0 + and vim.tbl_contains(trigger_characters, trigger.context.trigger.character) + then + triggering_source_returned_items = true + break + end + end + + if not triggering_source_returned_items then return list.hide() end + end + + list.show(event.context, event.items) + end) + end) + + --- list -> windows: ghost text and completion menu + -- setup completion menu + if config.completion.menu.enabled then + list.show_emitter:on( + function(event) require('blink.cmp.completion.windows.menu').open_with_items(event.context, event.items) end + ) + list.hide_emitter:on(function() require('blink.cmp.completion.windows.menu').close() end) + list.select_emitter:on(function(event) + require('blink.cmp.completion.windows.menu').set_selected_item_idx(event.idx) + require('blink.cmp.completion.windows.documentation').auto_show_item(event.context, event.item) + end) + end + + -- setup ghost text + if config.completion.ghost_text.enabled then + list.select_emitter:on( + function(event) require('blink.cmp.completion.windows.ghost_text').show_preview(event.item) end + ) + list.hide_emitter:on(function() require('blink.cmp.completion.windows.ghost_text').clear_preview() end) + end + + -- run 'resolve' on the item ahead of time to avoid delays + -- when accepting the item or showing documentation + list.select_emitter:on(function(event) + -- when selection.preselect == false, we still want to prefetch the first item + local item = event.item or list.items[1] + if item == nil then return end + require('blink.cmp.completion.prefetch')(event.context, event.item) + end) +end + +return completion diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/list.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/list.lua new file mode 100644 index 0000000..61fa2c3 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/list.lua @@ -0,0 +1,250 @@ +--- Manages most of the state for the completion list such that downstream consumers can be mostly stateless +--- @class (exact) blink.cmp.CompletionList +--- @field config blink.cmp.CompletionListConfig +--- @field show_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListShowEvent> +--- @field hide_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListHideEvent> +--- @field select_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListSelectEvent> +--- @field accept_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListAcceptEvent> +--- +--- @field context? blink.cmp.Context +--- @field items blink.cmp.CompletionItem[] +--- @field selected_item_idx? number +--- @field preview_undo? { text_edit: lsp.TextEdit, cursor: integer[]?} +--- +--- @field show fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>) +--- @field fuzzy fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>): blink.cmp.CompletionItem[] +--- @field hide fun() +--- +--- @field get_selected_item fun(): blink.cmp.CompletionItem? +--- @field get_selection_mode fun(context: blink.cmp.Context): { preselect: boolean, auto_insert: boolean } +--- @field get_item_idx_in_list fun(item?: blink.cmp.CompletionItem): number? +--- @field select fun(idx?: number, opts?: { auto_insert?: boolean, undo_preview?: boolean, is_explicit_selection?: boolean }) +--- @field select_next fun(opts?: blink.cmp.CompletionListSelectOpts) +--- @field select_prev fun(opts?: blink.cmp.CompletionListSelectOpts) +--- +--- @field undo_preview fun() +--- @field apply_preview fun(item: blink.cmp.CompletionItem) +--- @field accept fun(opts?: blink.cmp.CompletionListAcceptOpts): boolean Applies the currently selected item, returning true if it succeeded + +--- @class blink.cmp.CompletionListSelectOpts +--- @field auto_insert? boolean When `true`, inserts the completion item automatically when selecting it + +--- @class blink.cmp.CompletionListSelectAndAcceptOpts +--- @field callback? fun() Called after the item is accepted + +--- @class blink.cmp.CompletionListAcceptOpts : blink.cmp.CompletionListSelectAndAcceptOpts +--- @field index? number The index of the item to accept, if not provided, the currently selected item will be accepted + +--- @class blink.cmp.CompletionListShowEvent +--- @field items blink.cmp.CompletionItem[] +--- @field context blink.cmp.Context + +--- @class blink.cmp.CompletionListHideEvent +--- @field context blink.cmp.Context + +--- @class blink.cmp.CompletionListSelectEvent +--- @field idx? number +--- @field item? blink.cmp.CompletionItem +--- @field items blink.cmp.CompletionItem[] +--- @field context blink.cmp.Context + +--- @class blink.cmp.CompletionListAcceptEvent +--- @field item blink.cmp.CompletionItem +--- @field context blink.cmp.Context + +--- @type blink.cmp.CompletionList +--- @diagnostic disable-next-line: missing-fields +local list = { + select_emitter = require('blink.cmp.lib.event_emitter').new('select', 'BlinkCmpListSelect'), + accept_emitter = require('blink.cmp.lib.event_emitter').new('accept', 'BlinkCmpAccept'), + show_emitter = require('blink.cmp.lib.event_emitter').new('show', 'BlinkCmpShow'), + hide_emitter = require('blink.cmp.lib.event_emitter').new('hide', 'BlinkCmpHide'), + config = require('blink.cmp.config').completion.list, + context = nil, + items = {}, + is_explicitly_selected = false, + preview_undo = nil, +} + +---------- State ---------- + +function list.show(context, items_by_source) + -- reset state for new context + local is_new_context = not list.context or list.context.id ~= context.id + if is_new_context then + list.preview_undo = nil + list.is_explicitly_selected = false + end + + -- if the keyword changed, the list is no longer explicitly selected + local bounds_equal = list.context ~= nil + and list.context.bounds.start_col == context.bounds.start_col + and list.context.bounds.length == context.bounds.length + if not bounds_equal then list.is_explicitly_selected = false end + + local previous_selected_item = list.get_selected_item() + + -- update the context/list and emit + list.context = context + list.items = list.fuzzy(context, items_by_source) + + if #list.items == 0 then + list.hide_emitter:emit({ context = context }) + else + list.show_emitter:emit({ items = list.items, context = context }) + end + + -- maintain the selection if the user selected an item + local previous_item_idx = list.get_item_idx_in_list(previous_selected_item) + if list.is_explicitly_selected and previous_item_idx ~= nil and previous_item_idx <= 10 then + list.select(previous_item_idx, { auto_insert = false, undo_preview = false }) + + -- otherwise, use the default selection + else + list.select( + list.get_selection_mode(list.context).preselect and 1 or nil, + { auto_insert = false, undo_preview = false, is_explicit_selection = false } + ) + end +end + +function list.fuzzy(context, items_by_source) + local fuzzy = require('blink.cmp.fuzzy') + local filtered_items = fuzzy.fuzzy( + context.get_line(), + context.get_cursor()[2], + items_by_source, + require('blink.cmp.config').completion.keyword.range + ) + + -- apply the per source max_items + filtered_items = require('blink.cmp.sources.lib').apply_max_items_for_completions(context, filtered_items) + + -- apply the global max_items + return require('blink.cmp.lib.utils').slice(filtered_items, 1, list.config.max_items) +end + +function list.hide() list.hide_emitter:emit({ context = list.context }) end + +---------- Selection ---------- + +function list.get_selected_item() return list.items[list.selected_item_idx] end + +function list.get_selection_mode(context) + assert(context ~= nil, 'Context must be set before getting selection mode') + + local preselect = list.config.selection.preselect + if type(preselect) == 'function' then preselect = preselect(context) end + --- @cast preselect boolean + + local auto_insert = list.config.selection.auto_insert + if type(auto_insert) == 'function' then auto_insert = auto_insert(context) end + --- @cast auto_insert boolean + + return { preselect = preselect, auto_insert = auto_insert } +end + +function list.get_item_idx_in_list(item) + if item == nil then return end + return require('blink.cmp.lib.utils').find_idx(list.items, function(i) return i.label == item.label end) +end + +function list.select(idx, opts) + opts = opts or {} + local item = list.items[idx] + + local auto_insert = opts.auto_insert + if auto_insert == nil then auto_insert = list.get_selection_mode(list.context).auto_insert end + + require('blink.cmp.completion.trigger').suppress_events_for_callback(function() + if opts.undo_preview ~= false then list.undo_preview() end + if auto_insert and item ~= nil then list.apply_preview(item) end + end) + + --- @diagnostic disable-next-line: assign-type-mismatch + list.is_explicitly_selected = opts.is_explicit_selection == nil and true or opts.is_explicit_selection + list.selected_item_idx = idx + list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context }) +end + +function list.select_next(opts) + if #list.items == 0 or list.context == nil then return end + + -- haven't selected anything yet, select the first item + if list.selected_item_idx == nil then return list.select(1, opts) end + + -- end of the list + if list.selected_item_idx == #list.items then + -- cycling around has been disabled, ignore + if not list.config.cycle.from_bottom then return end + + -- preselect is not enabled, we go back to no selection + if not list.get_selection_mode(list.context).preselect then return list.select(nil, opts) end + + -- otherwise, we cycle around + return list.select(1, opts) + end + + -- typical case, select the next item + list.select(list.selected_item_idx + 1, opts) +end + +function list.select_prev(opts) + if #list.items == 0 or list.context == nil then return end + + -- haven't selected anything yet, select the last item + if list.selected_item_idx == nil then return list.select(#list.items, opts) end + + -- start of the list + if list.selected_item_idx == 1 then + -- cycling around has been disabled, ignore + if not list.config.cycle.from_top then return end + + -- auto_insert is enabled, we go back to no selection + if list.get_selection_mode(list.context).auto_insert then return list.select(nil, opts) end + + -- otherwise, we cycle around + return list.select(#list.items, opts) + end + + -- typical case, select the previous item + list.select(list.selected_item_idx - 1, opts) +end + +---------- Preview ---------- + +function list.undo_preview() + if list.preview_undo == nil then return end + + require('blink.cmp.lib.text_edits').apply({ list.preview_undo.text_edit }) + if list.preview_undo.cursor then + require('blink.cmp.completion.trigger.context').set_cursor(list.preview_undo.cursor) + end + list.preview_undo = nil +end + +function list.apply_preview(item) + -- undo the previous preview if it exists + list.undo_preview() + -- apply the new preview + local undo_text_edit, undo_cursor = require('blink.cmp.completion.accept.preview')(item) + list.preview_undo = { text_edit = undo_text_edit, cursor = undo_cursor } +end + +---------- Accept ---------- + +function list.accept(opts) + opts = opts or {} + local item = list.items[opts.index or list.selected_item_idx] + if item == nil then return false end + + list.undo_preview() + local accept = require('blink.cmp.completion.accept') + accept(list.context, item, function() + list.accept_emitter:emit({ item = item, context = list.context }) + if opts.callback then opts.callback() end + end) + return true +end + +return list diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/prefetch.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/prefetch.lua new file mode 100644 index 0000000..c722a30 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/prefetch.lua @@ -0,0 +1,29 @@ +-- Run `resolve` on the item ahead of time to avoid delays +-- when accepting the item or showing documentation + +local last_context_id = nil +local last_request = nil +local timer = vim.uv.new_timer() + +--- @param context blink.cmp.Context +--- @param item blink.cmp.CompletionItem +local function prefetch_resolve(context, item) + if not item then return end + + local resolve = vim.schedule_wrap(function() + if last_request ~= nil then last_request:cancel() end + last_request = require('blink.cmp.sources.lib').resolve(context, item) + end) + + -- immediately resolve if the context has changed + if last_context_id ~= context.id then + last_context_id = context.id + resolve() + end + + -- otherwise, wait for the debounce period + timer:stop() + timer:start(50, 0, resolve) +end + +return prefetch_resolve diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/context.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/context.lua new file mode 100644 index 0000000..6eb367d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/context.lua @@ -0,0 +1,118 @@ +-- TODO: remove the end_col field from ContextBounds + +--- @class blink.cmp.ContextBounds +--- @field line string +--- @field line_number number +--- @field start_col number +--- @field length number + +--- @class blink.cmp.Context +--- @field mode blink.cmp.Mode +--- @field id number +--- @field bufnr number +--- @field cursor number[] +--- @field line string +--- @field bounds blink.cmp.ContextBounds +--- @field trigger blink.cmp.ContextTrigger +--- @field providers string[] +--- +--- @field new fun(opts: blink.cmp.ContextOpts): blink.cmp.Context +--- @field get_keyword fun(): string +--- @field within_query_bounds fun(self: blink.cmp.Context, cursor: number[]): boolean +--- +--- @field get_mode fun(): blink.cmp.Mode +--- @field get_cursor fun(): number[] +--- @field set_cursor fun(cursor: number[]) +--- @field get_line fun(num?: number): string +--- @field get_bounds fun(range: blink.cmp.CompletionKeywordRange): blink.cmp.ContextBounds + +--- @class blink.cmp.ContextTrigger +--- @field initial_kind blink.cmp.CompletionTriggerKind The trigger kind when the context was first created +--- @field initial_character? string The trigger character when initial_kind == 'trigger_character' +--- @field kind blink.cmp.CompletionTriggerKind The current trigger kind +--- @field character? string The trigger character when kind == 'trigger_character' + +--- @class blink.cmp.ContextOpts +--- @field id number +--- @field providers string[] +--- @field initial_trigger_kind blink.cmp.CompletionTriggerKind +--- @field initial_trigger_character? string +--- @field trigger_kind blink.cmp.CompletionTriggerKind +--- @field trigger_character? string + +--- @type blink.cmp.Context +--- @diagnostic disable-next-line: missing-fields +local context = {} + +function context.new(opts) + local cursor = context.get_cursor() + local line = context.get_line() + + return setmetatable({ + mode = context.get_mode(), + id = opts.id, + bufnr = vim.api.nvim_get_current_buf(), + cursor = cursor, + line = line, + bounds = context.get_bounds('full'), + trigger = { + initial_kind = opts.initial_trigger_kind, + initial_character = opts.initial_trigger_character, + kind = opts.trigger_kind, + character = opts.trigger_character, + }, + providers = opts.providers, + }, { __index = context }) +end + +function context.get_keyword() + local keyword = require('blink.cmp.config').completion.keyword + local range = context.get_bounds(keyword.range) + return string.sub(context.get_line(), range.start_col, range.start_col + range.length - 1) +end + +--- @param cursor number[] +--- @return boolean +function context:within_query_bounds(cursor) + local row, col = cursor[1], cursor[2] + local bounds = self.bounds + return row == bounds.line_number and col >= bounds.start_col and col < (bounds.start_col + bounds.length) +end + +function context.get_mode() return vim.api.nvim_get_mode().mode == 'c' and 'cmdline' or 'default' end + +function context.get_cursor() + return context.get_mode() == 'cmdline' and { 1, vim.fn.getcmdpos() - 1 } or vim.api.nvim_win_get_cursor(0) +end + +function context.set_cursor(cursor) + local mode = context.get_mode() + if mode == 'default' then return vim.api.nvim_win_set_cursor(0, cursor) end + + assert(mode == 'cmdline', 'Unsupported mode for setting cursor: ' .. mode) + assert(cursor[1] == 1, 'Cursor must be on the first line in cmdline mode') + vim.fn.setcmdpos(cursor[2]) +end + +function context.get_line(num) + if context.get_mode() == 'cmdline' then + assert( + num == nil or num == 0, + 'Cannot get line number ' .. tostring(num) .. ' in cmdline mode. Only 0 is supported' + ) + return vim.fn.getcmdline() + end + + if num == nil then num = context.get_cursor()[1] - 1 end + return vim.api.nvim_buf_get_lines(0, num, num + 1, false)[1] +end + +--- Gets characters around the cursor and returns the range, 0-indexed +function context.get_bounds(range) + local line = context.get_line() + local cursor = context.get_cursor() + local start_col, end_col = require('blink.cmp.fuzzy').get_keyword_range(line, cursor[2], range) + return { line_number = cursor[1], start_col = start_col + 1, length = end_col - start_col } +end + +return context diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/init.lua new file mode 100644 index 0000000..1bd330f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/init.lua @@ -0,0 +1,241 @@ +--- @alias blink.cmp.CompletionTriggerKind 'manual' | 'prefetch' | 'keyword' | 'trigger_character' +--- +-- Handles hiding and showing the completion window. When a user types a trigger character +-- (provided by the sources) or anything matching the `keyword_regex`, we create a new `context`. +-- This can be used downstream to determine if we should make new requests to the sources or not. +--- @class blink.cmp.CompletionTrigger +--- @field buffer_events blink.cmp.BufferEvents +--- @field cmdline_events blink.cmp.CmdlineEvents +--- @field current_context_id number +--- @field context? blink.cmp.Context +--- @field show_emitter blink.cmp.EventEmitter<{ context: blink.cmp.Context }> +--- @field hide_emitter blink.cmp.EventEmitter<{}> +--- +--- @field activate fun() +--- @field is_keyword_character fun(char: string): boolean +--- @field is_trigger_character fun(char: string, is_show_on_x?: boolean): boolean +--- @field suppress_events_for_callback fun(cb: fun()) +--- @field show_if_on_trigger_character fun(opts?: { is_accept?: boolean }) +--- @field show fun(opts?: { trigger_kind: blink.cmp.CompletionTriggerKind, trigger_character?: string, force?: boolean, send_upstream?: boolean, providers?: string[] }): blink.cmp.Context? +--- @field hide fun() +--- @field within_query_bounds fun(cursor: number[]): boolean +--- @field get_bounds fun(regex: vim.regex, line: string, cursor: number[]): blink.cmp.ContextBounds + +local config = require('blink.cmp.config').completion.trigger +local context = require('blink.cmp.completion.trigger.context') +local utils = require('blink.cmp.completion.trigger.utils') + +--- @type blink.cmp.CompletionTrigger +--- @diagnostic disable-next-line: missing-fields +local trigger = { + current_context_id = -1, + show_emitter = require('blink.cmp.lib.event_emitter').new('show'), + hide_emitter = require('blink.cmp.lib.event_emitter').new('hide'), +} + +function trigger.activate() + trigger.buffer_events = require('blink.cmp.lib.buffer_events').new({ + -- TODO: should this ignore trigger.kind == 'prefetch'? + has_context = function() return trigger.context ~= nil end, + show_in_snippet = config.show_in_snippet, + }) + trigger.cmdline_events = require('blink.cmp.lib.cmdline_events').new() + + local function on_char_added(char, is_ignored) + -- we were told to ignore the text changed event, so we update the context + -- but don't send an on_show event upstream + if is_ignored then + if trigger.context ~= nil then trigger.show({ send_upstream = false, trigger_kind = 'keyword' }) end + + -- character forces a trigger according to the sources, create a fresh context + elseif trigger.is_trigger_character(char) and (config.show_on_trigger_character or trigger.context ~= nil) then + trigger.context = nil + trigger.show({ trigger_kind = 'trigger_character', trigger_character = char }) + + -- character is part of a keyword + elseif trigger.is_keyword_character(char) and (config.show_on_keyword or trigger.context ~= nil) then + trigger.show({ trigger_kind = 'keyword' }) + + -- nothing matches so hide + else + trigger.hide() + end + end + + local function on_cursor_moved(event, is_ignored) + local cursor = context.get_cursor() + local cursor_col = cursor[2] + + local char_under_cursor = utils.get_char_at_cursor() + local is_keyword = trigger.is_keyword_character(char_under_cursor) + + -- we were told to ignore the cursor moved event, so we update the context + -- but don't send an on_show event upstream + if is_ignored and event == 'CursorMoved' then + if trigger.context ~= nil then + -- TODO: If we `auto_insert` with the `path` source, we may end up on a trigger character + -- i.e. `downloads/`. If we naively update the context, we'll show the menu with the + -- existing context. So we clear the context if we're not on a keyword character. + -- Is there a better solution here? + if not is_keyword then trigger.context = nil end + + trigger.show({ send_upstream = false, trigger_kind = 'keyword' }) + end + return + end + + local is_on_trigger_for_show = trigger.is_trigger_character(char_under_cursor) + + -- TODO: doesn't handle `a` where the cursor moves immediately after + -- Reproducable with `example.|a` and pressing `a`, should not show the menu + local insert_enter_on_trigger_character = config.show_on_trigger_character + and config.show_on_insert_on_trigger_character + and event == 'InsertEnter' + and trigger.is_trigger_character(char_under_cursor, true) + + -- check if we're still within the bounds of the query used for the context + if trigger.context ~= nil and trigger.context:within_query_bounds(cursor) then + trigger.show({ trigger_kind = 'keyword' }) + + -- check if we've entered insert mode on a trigger character + -- or if we've moved onto a trigger character while open + elseif + insert_enter_on_trigger_character + or (is_on_trigger_for_show and trigger.context ~= nil and trigger.context.trigger.kind ~= 'prefetch') + then + trigger.context = nil + trigger.show({ trigger_kind = 'trigger_character', trigger_character = char_under_cursor }) + + -- show if we currently have a context, and we've moved outside of it's bounds by 1 char + elseif is_keyword and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then + trigger.context = nil + trigger.show({ trigger_kind = 'keyword' }) + + -- prefetch completions without opening window on InsertEnter + elseif event == 'InsertEnter' and config.prefetch_on_insert then + trigger.show({ trigger_kind = 'prefetch' }) + + -- otherwise hide + else + trigger.hide() + end + end + + trigger.buffer_events:listen({ + on_char_added = on_char_added, + on_cursor_moved = on_cursor_moved, + on_insert_leave = function() trigger.hide() end, + }) + trigger.cmdline_events:listen({ + on_char_added = on_char_added, + on_cursor_moved = on_cursor_moved, + on_leave = function() trigger.hide() end, + }) +end + +function trigger.is_keyword_character(char) + -- special case for hyphen, since we don't consider a lone hyphen to be a keyword + if char == '-' then return true end + + local keyword_start_col, keyword_end_col = require('blink.cmp.fuzzy').get_keyword_range(char, #char, 'prefix') + return keyword_start_col ~= keyword_end_col +end + +function trigger.is_trigger_character(char, is_show_on_x) + local sources = require('blink.cmp.sources.lib') + local is_trigger = vim.tbl_contains(sources.get_trigger_characters(context.get_mode()), char) + + local show_on_blocked_trigger_characters = type(config.show_on_blocked_trigger_characters) == 'function' + and config.show_on_blocked_trigger_characters() + or config.show_on_blocked_trigger_characters + --- @cast show_on_blocked_trigger_characters string[] + local show_on_x_blocked_trigger_characters = type(config.show_on_x_blocked_trigger_characters) == 'function' + and config.show_on_x_blocked_trigger_characters() + or config.show_on_x_blocked_trigger_characters + --- @cast show_on_x_blocked_trigger_characters string[] + + local is_blocked = vim.tbl_contains(show_on_blocked_trigger_characters, char) + or (is_show_on_x and vim.tbl_contains(show_on_x_blocked_trigger_characters, char)) + + return is_trigger and not is_blocked +end + +--- Suppresses on_hide and on_show events for the duration of the callback +function trigger.suppress_events_for_callback(cb) + local mode = vim.api.nvim_get_mode().mode == 'c' and 'cmdline' or 'default' + + local events = mode == 'default' and trigger.buffer_events or trigger.cmdline_events + if not events then return cb() end + + events:suppress_events_for_callback(cb) +end + +function trigger.show_if_on_trigger_character(opts) + if + (opts and opts.is_accept) + and (not config.show_on_trigger_character or not config.show_on_accept_on_trigger_character) + then + return + end + + local cursor_col = context.get_cursor()[2] + local char_under_cursor = context.get_line():sub(cursor_col, cursor_col) + + if trigger.is_trigger_character(char_under_cursor, true) then + trigger.show({ trigger_kind = 'trigger_character', trigger_character = char_under_cursor }) + end +end + +function trigger.show(opts) + if not require('blink.cmp.config').enabled() then return trigger.hide() end + + opts = opts or {} + + -- already triggered at this position, ignore + local mode = context.get_mode() + local cursor = context.get_cursor() + if + not opts.force + and trigger.context ~= nil + and trigger.context.mode == mode + and cursor[1] == trigger.context.cursor[1] + and cursor[2] == trigger.context.cursor[2] + then + return + end + + -- update the context id to indicate a new context, and not an update to an existing context + if trigger.context == nil or opts.providers ~= nil then + trigger.current_context_id = trigger.current_context_id + 1 + end + + local providers = opts.providers + or (trigger.context and trigger.context.providers) + or require('blink.cmp.sources.lib').get_enabled_provider_ids(context.get_mode()) + + local initial_trigger_kind = trigger.context and trigger.context.trigger.initial_kind or opts.trigger_kind + -- if we prefetched, don't keep that as the initial trigger kind + if initial_trigger_kind == 'prefetch' then initial_trigger_kind = opts.trigger_kind end + -- if we're manually triggering, set it as the initial trigger kind + if opts.trigger_kind == 'manual' then initial_trigger_kind = 'manual' end + + trigger.context = context.new({ + id = trigger.current_context_id, + providers = providers, + initial_trigger_kind = initial_trigger_kind, + initial_trigger_character = trigger.context and trigger.context.trigger.initial_character or opts.trigger_character, + trigger_kind = opts.trigger_kind, + trigger_character = opts.trigger_character, + }) + + if opts.send_upstream ~= false then trigger.show_emitter:emit({ context = trigger.context }) end + return trigger.context +end + +function trigger.hide() + if not trigger.context then return end + trigger.context = nil + trigger.hide_emitter:emit() +end + +return trigger diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/utils.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/utils.lua new file mode 100644 index 0000000..b2878c2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/trigger/utils.lua @@ -0,0 +1,30 @@ +local context = require('blink.cmp.completion.trigger.context') +local utils = {} + +--- Gets the full Unicode character at cursor position +--- @return string +function utils.get_char_at_cursor() + local line = context.get_line() + if line == '' then return '' end + local cursor_col = context.get_cursor()[2] + + -- Find the start of the UTF-8 character + local start_col = cursor_col + while start_col > 1 do + local char = string.byte(line:sub(start_col, start_col)) + if char < 0x80 or char > 0xBF then break end + start_col = start_col - 1 + end + + -- Find the end of the UTF-8 character + local end_col = cursor_col + while end_col < #line do + local char = string.byte(line:sub(end_col + 1, end_col + 1)) + if char < 0x80 or char > 0xBF then break end + end_col = end_col + 1 + end + + return line:sub(start_col, end_col) +end + +return utils diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/documentation.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/documentation.lua new file mode 100644 index 0000000..69a6dd8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/documentation.lua @@ -0,0 +1,228 @@ +--- @class blink.cmp.CompletionDocumentationWindow +--- @field win blink.cmp.Window +--- @field last_context_id? number +--- @field auto_show_timer uv_timer_t +--- @field shown_item? blink.cmp.CompletionItem +--- +--- @field auto_show_item fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem) +--- @field show_item fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem) +--- @field update_position fun() +--- @field scroll_up fun(amount: number) +--- @field scroll_down fun(amount: number) +--- @field close fun() + +local config = require('blink.cmp.config').completion.documentation +local win_config = config.window + +local sources = require('blink.cmp.sources.lib') +local menu = require('blink.cmp.completion.windows.menu') + +--- @type blink.cmp.CompletionDocumentationWindow +--- @diagnostic disable-next-line: missing-fields +local docs = { + win = require('blink.cmp.lib.window').new({ + min_width = win_config.min_width, + max_width = win_config.max_width, + max_height = win_config.max_height, + border = win_config.border, + winblend = win_config.winblend, + winhighlight = win_config.winhighlight, + scrollbar = win_config.scrollbar, + wrap = true, + filetype = 'blink-cmp-documentation', + scrolloff = 0, + }), + last_context_id = nil, + auto_show_timer = vim.uv.new_timer(), +} + +menu.position_update_emitter:on(function() docs.update_position() end) +menu.close_emitter:on(function() docs.close() end) + +function docs.auto_show_item(context, item) + docs.auto_show_timer:stop() + if docs.win:is_open() then + docs.auto_show_timer:start(config.update_delay_ms, 0, function() + vim.schedule(function() docs.show_item(context, item) end) + end) + elseif config.auto_show then + docs.auto_show_timer:start(config.auto_show_delay_ms, 0, function() + vim.schedule(function() docs.show_item(context, item) end) + end) + end +end + +function docs.show_item(context, item) + docs.auto_show_timer:stop() + if item == nil or not menu.win:is_open() then return docs.win:close() end + + -- TODO: cancellation + -- TODO: only resolve if documentation does not exist + sources + .resolve(context, item) + ---@param item blink.cmp.CompletionItem + :map(function(item) + if item.documentation == nil and item.detail == nil then + docs.close() + return + end + + if docs.shown_item ~= item then + --- @type blink.cmp.RenderDetailAndDocumentationOpts + local default_render_opts = { + bufnr = docs.win:get_buf(), + detail = item.detail, + documentation = item.documentation, + max_width = docs.win.config.max_width, + use_treesitter_highlighting = config and config.treesitter_highlighting, + } + local render = require('blink.cmp.lib.window.docs').render_detail_and_documentation + + if item.documentation and item.documentation.render ~= nil then + -- let the provider render the documentation and optionally override + -- the default rendering + item.documentation.render({ + item = item, + window = docs.win, + default_implementation = function(opts) render(vim.tbl_extend('force', default_render_opts, opts)) end, + }) + else + render(default_render_opts) + end + end + docs.shown_item = item + + if menu.win:get_win() then + docs.win:open() + docs.win:set_cursor({ 1, 0 }) -- reset scroll + docs.update_position() + end + end) + :catch(function(err) vim.notify(err, vim.log.levels.ERROR, { title = 'blink.cmp' }) end) +end + +-- TODO: compensate for wrapped lines +function docs.scroll_up(amount) + local winnr = docs.win:get_win() + if winnr == nil then return end + + local top_line = math.max(1, vim.fn.line('w0', winnr)) + local desired_line = math.max(1, top_line - amount) + + docs.win:set_cursor({ desired_line, 0 }) +end + +-- TODO: compensate for wrapped lines +function docs.scroll_down(amount) + local winnr = docs.win:get_win() + if winnr == nil then return end + + local line_count = vim.api.nvim_buf_line_count(docs.win:get_buf()) + local bottom_line = math.max(1, vim.fn.line('w$', winnr)) + local desired_line = math.min(line_count, bottom_line + amount) + + docs.win:set_cursor({ desired_line, 0 }) +end + +function docs.update_position() + if not docs.win:is_open() or not menu.win:is_open() then return end + + docs.win:update_size() + + local menu_winnr = menu.win:get_win() + if not menu_winnr then return end + local menu_win_config = vim.api.nvim_win_get_config(menu_winnr) + local menu_win_height = menu.win:get_height() + local menu_border_size = menu.win:get_border_size() + + local cursor_win_row = vim.fn.winline() + + -- decide direction priority based on the menu window's position + local menu_win_is_up = menu_win_config.row - cursor_win_row < 0 + local direction_priority = menu_win_is_up and win_config.direction_priority.menu_north + or win_config.direction_priority.menu_south + + -- remove the direction priority of the signature window if it's open + local signature = require('blink.cmp.signature.window') + if signature.win and signature.win:is_open() then + direction_priority = vim.tbl_filter( + function(dir) return dir ~= (menu_win_is_up and 's' or 'n') end, + direction_priority + ) + end + + -- decide direction, width and height of window + local win_width = docs.win:get_width() + local win_height = docs.win:get_height() + local pos = docs.win:get_direction_with_window_constraints(menu.win, direction_priority, { + width = math.min(win_width, win_config.desired_min_width), + height = math.min(win_height, win_config.desired_min_height), + }) + + -- couldn't find anywhere to place the window + if not pos then + docs.win:close() + return + end + + -- set width and height based on available space + docs.win:set_height(pos.height) + docs.win:set_width(pos.width) + + -- set position based on provided direction + + local height = docs.win:get_height() + local width = docs.win:get_width() + + local function set_config(opts) + docs.win:set_win_config({ relative = 'win', win = menu_winnr, row = opts.row, col = opts.col }) + end + if pos.direction == 'n' then + if menu_win_is_up then + set_config({ row = -height - menu_border_size.top, col = -menu_border_size.left }) + else + set_config({ row = -1 - height - menu_border_size.top, col = -menu_border_size.left }) + end + elseif pos.direction == 's' then + if menu_win_is_up then + set_config({ + row = 1 + menu_win_height - menu_border_size.top, + col = -menu_border_size.left, + }) + else + set_config({ + row = menu_win_height - menu_border_size.top, + col = -menu_border_size.left, + }) + end + elseif pos.direction == 'e' then + if menu_win_is_up and menu_win_height < height then + set_config({ + row = menu_win_height - menu_border_size.top - height, + col = menu_win_config.width + menu_border_size.right, + }) + else + set_config({ + row = -menu_border_size.top, + col = menu_win_config.width + menu_border_size.right, + }) + end + elseif pos.direction == 'w' then + if menu_win_is_up and menu_win_height < height then + set_config({ + row = menu_win_height - menu_border_size.top - height, + col = -width - menu_border_size.left, + }) + else + set_config({ row = -menu_border_size.top, col = -width - menu_border_size.left }) + end + end +end + +function docs.close() + docs.win:close() + docs.auto_show_timer:stop() + docs.shown_item = nil +end + +return docs diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/ghost_text.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/ghost_text.lua new file mode 100644 index 0000000..2869e95 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/ghost_text.lua @@ -0,0 +1,100 @@ +local config = require('blink.cmp.config').completion.ghost_text +local highlight_ns = require('blink.cmp.config').appearance.highlight_ns +local text_edits_lib = require('blink.cmp.lib.text_edits') +local snippets_utils = require('blink.cmp.sources.snippets.utils') + +--- @class blink.cmp.windows.GhostText +--- @field win integer? +--- @field selected_item blink.cmp.CompletionItem? +--- @field extmark_id integer? +--- +--- @field is_open fun(): boolean +--- @field show_preview fun(item: blink.cmp.CompletionItem) +--- @field clear_preview fun() +--- @field draw_preview fun(bufnr: number) + +--- @type blink.cmp.windows.GhostText +--- @diagnostic disable-next-line: missing-fields +local ghost_text = { + win = nil, + selected_item = nil, + extmark_id = nil, +} + +--- @param textEdit lsp.TextEdit +local function get_still_untyped_text(textEdit) + local type_text_length = textEdit.range['end'].character - textEdit.range.start.character + return textEdit.newText:sub(type_text_length + 1) +end + +-- immediately re-draw the preview when the cursor moves/text changes +vim.api.nvim_create_autocmd({ 'CursorMovedI', 'TextChangedI' }, { + callback = function() + if config.enabled and ghost_text.win then ghost_text.draw_preview(vim.api.nvim_win_get_buf(ghost_text.win)) end + end, +}) + +function ghost_text.is_open() return ghost_text.extmark_id ~= nil end + +--- @param selected_item? blink.cmp.CompletionItem +function ghost_text.show_preview(selected_item) + -- nothing to show, clear the preview + if not selected_item then + ghost_text.clear_preview() + return + end + + -- doesn't work in command mode + -- TODO: integrate with noice.nvim? + if vim.api.nvim_get_mode().mode == 'c' then return end + + -- update state and redraw + local changed = ghost_text.selected_item ~= selected_item + ghost_text.selected_item = selected_item + ghost_text.win = vim.api.nvim_get_current_win() + if changed then ghost_text.draw_preview(vim.api.nvim_win_get_buf(ghost_text.win)) end +end + +function ghost_text.clear_preview() + ghost_text.selected_item = nil + ghost_text.win = nil + if ghost_text.extmark_id ~= nil then + vim.api.nvim_buf_del_extmark(0, highlight_ns, ghost_text.extmark_id) + ghost_text.extmark_id = nil + end +end + +function ghost_text.draw_preview(bufnr) + if not ghost_text.selected_item then return end + + local text_edit = text_edits_lib.get_from_item(ghost_text.selected_item) + + if ghost_text.selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then + local expanded_snippet = snippets_utils.safe_parse(text_edit.newText) + text_edit.newText = expanded_snippet and tostring(expanded_snippet) or text_edit.newText + end + + local display_lines = vim.split(get_still_untyped_text(text_edit), '\n', { plain = true }) or {} + + local virt_lines = {} + if #display_lines > 1 then + for i = 2, #display_lines do + virt_lines[i - 1] = { { display_lines[i], 'BlinkCmpGhostText' } } + end + end + + local cursor_pos = { + text_edit.range.start.line, + text_edit.range['end'].character, + } + + ghost_text.extmark_id = vim.api.nvim_buf_set_extmark(bufnr, highlight_ns, cursor_pos[1], cursor_pos[2], { + id = ghost_text.extmark_id, + virt_text_pos = 'inline', + virt_text = { { display_lines[1], 'BlinkCmpGhostText' } }, + virt_lines = virt_lines, + hl_mode = 'combine', + }) +end + +return ghost_text diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/menu.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/menu.lua new file mode 100644 index 0000000..a749ddd --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/menu.lua @@ -0,0 +1,136 @@ +--- @class blink.cmp.CompletionMenu +--- @field win blink.cmp.Window +--- @field items blink.cmp.CompletionItem[] +--- @field renderer blink.cmp.Renderer +--- @field selected_item_idx? number +--- @field context blink.cmp.Context? +--- @field open_emitter blink.cmp.EventEmitter<{}> +--- @field close_emitter blink.cmp.EventEmitter<{}> +--- @field position_update_emitter blink.cmp.EventEmitter<{}> +--- +--- @field open_with_items fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]) +--- @field open fun() +--- @field close fun() +--- @field set_selected_item_idx fun(idx?: number) +--- @field update_position fun() +--- @field redraw_if_needed fun() + +local config = require('blink.cmp.config').completion.menu + +--- @type blink.cmp.CompletionMenu +--- @diagnostic disable-next-line: missing-fields +local menu = { + win = require('blink.cmp.lib.window').new({ + min_width = config.min_width, + max_height = config.max_height, + border = config.border, + winblend = config.winblend, + winhighlight = config.winhighlight, + cursorline = false, + scrolloff = config.scrolloff, + scrollbar = config.scrollbar, + filetype = 'blink-cmp-menu', + }), + items = {}, + context = nil, + auto_show = config.auto_show, + open_emitter = require('blink.cmp.lib.event_emitter').new('completion_menu_open', 'BlinkCmpMenuOpen'), + close_emitter = require('blink.cmp.lib.event_emitter').new('completion_menu_close', 'BlinkCmpMenuClose'), + position_update_emitter = require('blink.cmp.lib.event_emitter').new( + 'completion_menu_position_update', + 'BlinkCmpMenuPositionUpdate' + ), +} + +vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, { + callback = function() menu.update_position() end, +}) + +function menu.open_with_items(context, items) + menu.context = context + menu.items = items + menu.selected_item_idx = menu.selected_item_idx ~= nil and math.min(menu.selected_item_idx, #items) or nil + + if not menu.renderer then menu.renderer = require('blink.cmp.completion.windows.render').new(config.draw) end + menu.renderer:draw(context, menu.win:get_buf(), items) + + local auto_show = menu.auto_show + if type(auto_show) == 'function' then auto_show = auto_show(context, items) end + if auto_show then + menu.open() + menu.update_position() + end +end + +function menu.open() + if menu.win:is_open() then return end + + menu.win:open() + if menu.selected_item_idx ~= nil then + vim.api.nvim_win_set_cursor(menu.win:get_win(), { menu.selected_item_idx, 0 }) + end + + menu.open_emitter:emit() +end + +function menu.close() + menu.auto_show = config.auto_show + if not menu.win:is_open() then return end + + menu.win:close() + menu.close_emitter:emit() +end + +function menu.set_selected_item_idx(idx) + menu.win:set_option_value('cursorline', idx ~= nil) + menu.selected_item_idx = idx + if menu.win:is_open() then menu.win:set_cursor({ idx or 1, 0 }) end +end + +--- TODO: Don't switch directions if the context is the same +function menu.update_position() + local context = menu.context + if context == nil then return end + + local win = menu.win + if not win:is_open() then return end + + win:update_size() + + local border_size = win:get_border_size() + local pos = win:get_vertical_direction_and_height(config.direction_priority) + + -- couldn't find anywhere to place the window + if not pos then + win:close() + return + end + + local alignment_start_col = menu.renderer:get_alignment_start_col() + + -- place the window at the start col of the current text we're fuzzy matching against + -- so the window doesnt move around as we type + local row = pos.direction == 's' and 1 or -pos.height - border_size.vertical + + if vim.api.nvim_get_mode().mode == 'c' then + local cmdline_position = config.cmdline_position() + win:set_win_config({ + relative = 'editor', + row = cmdline_position[1] + row, + col = math.max(cmdline_position[2] + context.bounds.start_col - alignment_start_col, 0), + }) + else + local cursor_col = context.get_cursor()[2] + + local col = context.bounds.start_col - alignment_start_col - cursor_col - 1 - border_size.left + if config.draw.align_to == 'cursor' then col = 0 end + + win:set_win_config({ relative = 'cursor', row = row, col = col }) + end + + win:set_height(pos.height) + + menu.position_update_emitter:emit() +end + +return menu diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/column.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/column.lua new file mode 100644 index 0000000..b9a75d0 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/column.lua @@ -0,0 +1,120 @@ +--- @class blink.cmp.DrawColumn +--- @field components blink.cmp.DrawComponent[] +--- @field gap number +--- @field lines string[][] +--- @field width number +--- @field ctxs blink.cmp.DrawItemContext[] +--- +--- @field new fun(components: blink.cmp.DrawComponent[], gap: number): blink.cmp.DrawColumn +--- @field render fun(self: blink.cmp.DrawColumn, ctxs: blink.cmp.DrawItemContext[]) +--- @field get_line_text fun(self: blink.cmp.DrawColumn, line_idx: number): string +--- @field get_line_highlights fun(self: blink.cmp.DrawColumn, line_idx: number): blink.cmp.DrawHighlight[] + +local text_lib = require('blink.cmp.completion.windows.render.text') + +--- @type blink.cmp.DrawColumn +--- @diagnostic disable-next-line: missing-fields +local column = {} + +function column.new(components, gap) + local self = setmetatable({}, { __index = column }) + self.components = components + self.gap = gap + self.lines = {} + self.width = 0 + self.ctxs = {} + return self +end + +function column:render(ctxs) + --- render text and get the max widths of each component + --- @type string[][] + local lines = {} + local max_component_widths = {} + for _, ctx in ipairs(ctxs) do + --- @type string[] + local line = {} + for component_idx, component in ipairs(self.components) do + local text = text_lib.apply_component_width(component.text(ctx) or '', component) + table.insert(line, text) + max_component_widths[component_idx] = + math.max(max_component_widths[component_idx] or 0, vim.api.nvim_strwidth(text)) + end + table.insert(lines, line) + end + + --- get the total width of the column + local column_width = 0 + for _, max_component_width in ipairs(max_component_widths) do + if max_component_width > 0 then column_width = column_width + max_component_width + self.gap end + end + column_width = math.max(column_width - self.gap, 0) + + --- find the component that will fill the empty space + local fill_idx = -1 + for component_idx, component in ipairs(self.components) do + if component.width and component.width.fill then + fill_idx = component_idx + break + end + end + if fill_idx == -1 then fill_idx = #self.components end + + --- and add extra spaces until we reach the column width + for _, line in ipairs(lines) do + local line_width = 0 + for _, component_text in ipairs(line) do + if #component_text > 0 then line_width = line_width + vim.api.nvim_strwidth(component_text) + self.gap end + end + line_width = line_width - self.gap + local remaining_width = column_width - line_width + line[fill_idx] = text_lib.pad(line[fill_idx], vim.api.nvim_strwidth(line[fill_idx]) + remaining_width) + end + + -- store results for later + self.width = column_width + self.lines = lines + self.ctxs = ctxs +end + +function column:get_line_text(line_idx) + local text = '' + local line = self.lines[line_idx] + for _, component in ipairs(line) do + if #component > 0 then text = text .. component .. string.rep(' ', self.gap) end + end + return text:sub(1, -self.gap - 1) +end + +function column:get_line_highlights(line_idx) + local ctx = self.ctxs[line_idx] + local offset = 0 + local highlights = {} + + for component_idx, component in ipairs(self.components) do + local text = self.lines[line_idx][component_idx] + if #text > 0 then + local column_highlights = type(component.highlight) == 'function' and component.highlight(ctx, text) + or component.highlight + + if type(column_highlights) == 'string' then + table.insert(highlights, { offset, offset + #text, group = column_highlights }) + elseif type(column_highlights) == 'table' then + for _, highlight in ipairs(column_highlights) do + table.insert(highlights, { + offset + highlight[1], + offset + highlight[2], + group = highlight.group, + params = highlight.params, + }) + end + end + + offset = offset + #text + self.gap + end + end + + return highlights +end + +return column diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/context.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/context.lua new file mode 100644 index 0000000..301825f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/context.lua @@ -0,0 +1,85 @@ +--- @class blink.cmp.DrawItemContext +--- @field self blink.cmp.Draw +--- @field item blink.cmp.CompletionItem +--- @field idx number +--- @field label string +--- @field label_detail string +--- @field label_description string +--- @field label_matched_indices number[] +--- @field kind string +--- @field kind_icon string +--- @field icon_gap string +--- @field deprecated boolean +--- @field source_id string +--- @field source_name string + +local draw_context = {} + +--- @param context blink.cmp.Context +--- @param draw blink.cmp.Draw +--- @param items blink.cmp.CompletionItem[] +--- @return blink.cmp.DrawItemContext[] +function draw_context.get_from_items(context, draw, items) + local matched_indices = require('blink.cmp.fuzzy').fuzzy_matched_indices( + context.get_line(), + context.get_cursor()[2], + vim.tbl_map(function(item) return item.label end, items), + require('blink.cmp.config').completion.keyword.range + ) + + local ctxs = {} + for idx, item in ipairs(items) do + ctxs[idx] = draw_context.new(draw, idx, item, matched_indices[idx]) + end + return ctxs +end + +local config = require('blink.cmp.config').appearance +local kinds = require('blink.cmp.types').CompletionItemKind + +--- @param draw blink.cmp.Draw +--- @param item_idx number +--- @param item blink.cmp.CompletionItem +--- @param matched_indices number[] +--- @return blink.cmp.DrawItemContext +function draw_context.new(draw, item_idx, item, matched_indices) + local kind = kinds[item.kind] or 'Unknown' + local kind_icon = require('blink.cmp.completion.windows.render.tailwind').get_kind_icon(item) + or config.kind_icons[kind] + or config.kind_icons.Field + local icon_spacing = config.nerd_font_variant == 'mono' and '' or ' ' + + -- Some LSPs can return labels with newlines + -- Escape them to avoid errors in nvim_buf_set_lines when rendering the completion menu + local newline_char = '↲' .. icon_spacing + + local label = item.label:gsub('\n', newline_char) .. (kind == 'Snippet' and '~' or '') + if config.nerd_font_variant == 'normal' then label = label:gsub('…', '… ') end + + local label_detail = (item.labelDetails and item.labelDetails.detail or ''):gsub('\n', newline_char) + if config.nerd_font_variant == 'normal' then label_detail = label_detail:gsub('…', '… ') end + + local label_description = (item.labelDetails and item.labelDetails.description or ''):gsub('\n', newline_char) + if config.nerd_font_variant == 'normal' then label_description = label_description:gsub('…', '… ') end + + local source_id = item.source_id + local source_name = item.source_name + + return { + self = draw, + item = item, + idx = item_idx, + label = label, + label_detail = label_detail, + label_description = label_description, + label_matched_indices = matched_indices, + kind = kind, + kind_icon = kind_icon, + icon_gap = config.nerd_font_variant == 'mono' and '' or ' ', + deprecated = item.deprecated or (item.tags and vim.tbl_contains(item.tags, 1)) or false, + source_id = source_id, + source_name = source_name, + } +end + +return draw_context diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/init.lua new file mode 100644 index 0000000..1422ec2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/init.lua @@ -0,0 +1,146 @@ +--- @class blink.cmp.Renderer +--- @field def blink.cmp.Draw +--- @field padding number[] +--- @field gap number +--- @field columns blink.cmp.DrawColumn[] +--- +--- @field new fun(draw: blink.cmp.Draw): blink.cmp.Renderer +--- @field draw fun(self: blink.cmp.Renderer, context: blink.cmp.Context, bufnr: number, items: blink.cmp.CompletionItem[]) +--- @field get_component_column_location fun(self: blink.cmp.Renderer, component_name: string): { column_idx: number, component_idx: number } +--- @field get_component_start_col fun(self: blink.cmp.Renderer, component_name: string): number +--- @field get_alignment_start_col fun(self: blink.cmp.Renderer): number + +local ns = vim.api.nvim_create_namespace('blink_cmp_renderer') + +--- @type blink.cmp.Renderer +--- @diagnostic disable-next-line: missing-fields +local renderer = {} + +function renderer.new(draw) + --- Convert the component names in the columns to the component definitions + --- @type blink.cmp.DrawComponent[][] + local columns_definitions = vim.tbl_map(function(column) + local components = {} + for _, component_name in ipairs(column) do + local component = draw.components[component_name] + assert(component ~= nil, 'No component definition found for component: "' .. component_name .. '"') + table.insert(components, draw.components[component_name]) + end + + return { + components = components, + gap = column.gap or 0, + } + end, draw.columns) + + local padding = type(draw.padding) == 'number' and { draw.padding, draw.padding } or draw.padding + --- @cast padding number[] + + local self = setmetatable({}, { __index = renderer }) + self.padding = padding + self.gap = draw.gap + self.def = draw + self.columns = vim.tbl_map( + function(column_definition) + return require('blink.cmp.completion.windows.render.column').new( + column_definition.components, + column_definition.gap + ) + end, + columns_definitions + ) + return self +end + +function renderer:draw(context, bufnr, items) + -- gather contexts + local draw_contexts = require('blink.cmp.completion.windows.render.context').get_from_items(context, self.def, items) + + -- render the columns + for _, column in ipairs(self.columns) do + column:render(draw_contexts) + end + + -- apply to the buffer + local lines = {} + for idx, _ in ipairs(draw_contexts) do + local line = '' + if self.padding[1] > 0 then line = string.rep(' ', self.padding[1]) end + + for _, column in ipairs(self.columns) do + local text = column:get_line_text(idx) + if #text > 0 then line = line .. text .. string.rep(' ', self.gap) end + end + line = line:sub(1, -self.gap - 1) + + if self.padding[2] > 0 then line = line .. string.rep(' ', self.padding[2]) end + + table.insert(lines, line) + end + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + vim.api.nvim_set_option_value('modified', false, { buf = bufnr }) + + -- Setting highlights is slow and we update on every keystroke so we instead use a decoration provider + -- which will only render highlights of the visible lines. This also avoids having to do virtual scroll + -- like nvim-cmp does, which breaks on UIs like neovide + vim.api.nvim_set_decoration_provider(ns, { + on_win = function(_, _, win_bufnr) return bufnr == win_bufnr end, + on_line = function(_, _, _, line) + local offset = self.padding[1] + for _, column in ipairs(self.columns) do + local text = column:get_line_text(line + 1) + if #text > 0 then + local highlights = column:get_line_highlights(line + 1) + for _, highlight in ipairs(highlights) do + local col = offset + highlight[1] + local end_col = offset + highlight[2] + vim.api.nvim_buf_set_extmark(bufnr, ns, line, col, { + end_col = end_col, + hl_group = highlight.group, + hl_mode = 'combine', + hl_eol = true, + ephemeral = true, + }) + end + offset = offset + #text + self.gap + end + end + end, + }) +end + +function renderer:get_component_column_location(component_name) + for column_idx, column in ipairs(self.def.columns) do + for component_idx, other_component_name in ipairs(column) do + if other_component_name == component_name then return { column_idx, component_idx } end + end + end + error('No component found with name: ' .. component_name) +end + +function renderer:get_component_start_col(component_name) + local column_idx, component_idx = unpack(self:get_component_column_location(component_name)) + + -- add previous columns + local start_col = self.padding[1] + for i = 1, column_idx - 1 do + start_col = start_col + self.columns[i].width + self.gap + end + + -- add previous components + local line = self.columns[column_idx].lines[1] + if not line then return start_col end + for i = 1, component_idx - 1 do + start_col = start_col + #line[i] + end + + return start_col +end + +function renderer:get_alignment_start_col() + local component_name = self.def.align_to + if component_name == nil or component_name == 'none' or component_name == 'cursor' then return 0 end + return self:get_component_start_col(component_name) +end + +return renderer diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/tailwind.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/tailwind.lua new file mode 100644 index 0000000..0bff6b2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/tailwind.lua @@ -0,0 +1,31 @@ +local tailwind = {} + +local kinds = require('blink.cmp.types').CompletionItemKind + +--- @param item blink.cmp.CompletionItem +--- @return string|nil +function tailwind.get_hex_color(item) + local doc = item.documentation + if item.kind ~= kinds.Color or not doc then return end + local content = type(doc) == 'string' and doc or doc.value + if content and content:match('^#%x%x%x%x%x%x$') then return content end +end + +--- @param item blink.cmp.CompletionItem +--- @return string? +function tailwind.get_kind_icon(item) + if tailwind.get_hex_color(item) then return '██' end +end + +--- @param ctx blink.cmp.DrawItemContext +--- @return string|nil +function tailwind.get_hl(ctx) + local hex_color = tailwind.get_hex_color(ctx.item) + if not hex_color then return end + + local hl_name = 'HexColor' .. hex_color:sub(2) + if #vim.api.nvim_get_hl(0, { name = hl_name }) == 0 then vim.api.nvim_set_hl(0, hl_name, { fg = hex_color }) end + return hl_name +end + +return tailwind diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/text.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/text.lua new file mode 100644 index 0000000..b614edc --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/text.lua @@ -0,0 +1,72 @@ +local config = require('blink.cmp.config') +local text_lib = {} + +--- Applies the component width settings to the text +--- @param text string +--- @param component blink.cmp.DrawComponent +--- @return string text +function text_lib.apply_component_width(text, component) + local width = component.width or {} + if width.fixed ~= nil then return text_lib.set_width(text, width.fixed, component) end + if width.min ~= nil then text = text_lib.pad(text, width.min) end + if width.max ~= nil then text = text_lib.truncate(text, width.max, component.ellipsis) end + return text +end + +--- Sets the text width to the given width +--- @param text string +--- @param width number +--- @param component blink.cmp.DrawComponent +--- @return string text +function text_lib.set_width(text, width, component) + local length = vim.api.nvim_strwidth(text) + if length > width then + return text_lib.truncate(text, width, component.ellipsis) + elseif length < width then + return text_lib.pad(text, width) + else + return text + end +end + +--- Truncates the text to the given width +--- @param text string +--- @param target_width number +--- @param ellipsis? boolean +--- @return string truncated_text +function text_lib.truncate(text, target_width, ellipsis) + local ellipsis_str = ellipsis ~= false and '…' or '' + if ellipsis ~= false and config.nerd_font_variant == 'normal' then ellipsis_str = ellipsis_str .. ' ' end + + local text_width = vim.api.nvim_strwidth(text) + local ellipsis_width = vim.api.nvim_strwidth(ellipsis_str) + if text_width > target_width then + return vim.fn.strcharpart(text, 0, target_width - ellipsis_width) .. ellipsis_str + end + return text +end + +--- Pads the text to the given width +--- @param text string +--- @param target_width number +--- @return string padded_text The amount of padding added to the left and the padded text +function text_lib.pad(text, target_width) + local text_width = vim.api.nvim_strwidth(text) + if text_width >= target_width then return text end + return text .. string.rep(' ', target_width - text_width) + + -- if alignment == 'left' then + -- return 0, text .. string.rep(' ', target_width - text_width) + -- elseif alignment == 'center' then + -- local extra_space = target_width - text_width + -- local half_width_start = math.floor(extra_space / 2) + -- local half_width_end = math.ceil(extra_space / 2) + -- return half_width_start, string.rep(' ', half_width_start) .. text .. string.rep(' ', half_width_end) + -- elseif alignment == 'right' then + -- return target_width - text_width, string.rep(' ', target_width - text_width) .. text + -- else + -- error('Invalid alignment: ' .. alignment) + -- end +end + +return text_lib diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/treesitter.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/treesitter.lua new file mode 100644 index 0000000..901c46a --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/treesitter.lua @@ -0,0 +1,70 @@ +local treesitter = {} + +---@type table<string, blink.cmp.DrawHighlight[]> +local cache = {} +local cache_size = 0 +local MAX_CACHE_SIZE = 1000 + +--- @param ctx blink.cmp.DrawItemContext +--- @param opts? {offset?: number} +function treesitter.highlight(ctx, opts) + local ret = cache[ctx.label] + if not ret then + -- cleanup cache if it's too big + cache_size = cache_size + 1 + if cache_size > MAX_CACHE_SIZE then + cache = {} + cache_size = 0 + end + ret = treesitter._highlight(ctx) + cache[ctx.label] = ret + end + + -- offset highlights if needed + if opts and opts.offset then + ret = vim.deepcopy(ret) + for _, hl in ipairs(ret) do + hl[1] = hl[1] + opts.offset + hl[2] = hl[2] + opts.offset + end + end + return ret +end + +--- @param ctx blink.cmp.DrawItemContext +function treesitter._highlight(ctx) + local ret = {} ---@type blink.cmp.DrawHighlight[] + + local source = ctx.label + local lang = vim.treesitter.language.get_lang(vim.bo.filetype) + if not lang then return ret end + + local ok, parser = pcall(vim.treesitter.get_string_parser, source, lang) + if not ok then return ret end + + parser:parse(true) + + parser:for_each_tree(function(tstree, tree) + if not tstree then return end + local query = vim.treesitter.query.get(tree:lang(), 'highlights') + -- Some injected languages may not have highlight queries. + if not query then return end + + for capture, node in query:iter_captures(tstree:root(), source) do + local _, start_col, _, end_col = node:range() + + ---@type string + local name = query.captures[capture] + if name ~= 'spell' then + ret[#ret + 1] = { + start_col, + end_col, + group = '@' .. name .. '.' .. lang, + } + end + end + end) + return ret +end + +return treesitter diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/types.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/types.lua new file mode 100644 index 0000000..186b3dc --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/completion/windows/render/types.lua @@ -0,0 +1,24 @@ +--- @class blink.cmp.Draw +--- @field align_to? string | 'none' | 'cursor' Align the window to the component with the given name, or to the cursor +--- @field padding? number | number[] Padding on the left and right of the grid +--- @field gap? number Gap between columns +--- @field columns? { [number]: string, gap?: number }[] Components to render, grouped by column +--- @field components? table<string, blink.cmp.DrawComponent> Component definitions +--- @field treesitter? string[] Use treesitter to highlight the label text of completions from these sources +--- +--- @class blink.cmp.DrawHighlight +--- @field [number] number Start and end index of the highlight +--- @field group? string Highlight group +--- @field params? table Additional parameters passed as the `params` field of the highlight +--- +--- @class blink.cmp.DrawWidth +--- @field fixed? number Fixed width +--- @field fill? boolean Fill the remaining space +--- @field min? number Minimum width +--- @field max? number Maximum width +--- +--- @class blink.cmp.DrawComponent +--- @field width? blink.cmp.DrawWidth +--- @field ellipsis? boolean Whether to add an ellipsis when truncating the text +--- @field text? fun(ctx: blink.cmp.DrawItemContext): string? Renders the text of the component +--- @field highlight? string | fun(ctx: blink.cmp.DrawItemContext, text: string): string | blink.cmp.DrawHighlight[] Renders the highlights of the component diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/appearance.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/appearance.lua new file mode 100644 index 0000000..08a97ba --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/appearance.lua @@ -0,0 +1,58 @@ +--- @class (exact) blink.cmp.AppearanceConfig +--- @field highlight_ns number +--- @field use_nvim_cmp_as_default boolean Sets the fallback highlight groups to nvim-cmp's highlight groups. Useful for when your theme doesn't support blink.cmp, will be removed in a future release. +--- @field nerd_font_variant 'mono' | 'normal' Set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font'. Adjusts spacing to ensure icons are aligned +--- @field kind_icons table<string, string> + +local validate = require('blink.cmp.config.utils').validate +local appearance = { + --- @type blink.cmp.AppearanceConfig + default = { + highlight_ns = vim.api.nvim_create_namespace('blink_cmp'), + use_nvim_cmp_as_default = false, + nerd_font_variant = 'mono', + kind_icons = { + Text = '', + Method = '', + Function = '', + Constructor = '', + + Field = '', + Variable = '', + Property = '', + + Class = '', + Interface = '', + Struct = '', + Module = '', + + Unit = '', + Value = '', + Enum = '', + EnumMember = '', + + Keyword = '', + Constant = '', + + Snippet = '', + Color = '', + File = '', + Reference = '', + Folder = '', + Event = '', + Operator = '', + TypeParameter = '', + }, + }, +} + +function appearance.validate(config) + validate('appearance', { + highlight_ns = { config.highlight_ns, 'number' }, + use_nvim_cmp_as_default = { config.use_nvim_cmp_as_default, 'boolean' }, + nerd_font_variant = { config.nerd_font_variant, 'string' }, + kind_icons = { config.kind_icons, 'table' }, + }, config) +end + +return appearance diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/accept.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/accept.lua new file mode 100644 index 0000000..a81aa55 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/accept.lua @@ -0,0 +1,72 @@ +--- @class (exact) blink.cmp.CompletionAcceptConfig +--- @field create_undo_point boolean Create an undo point when accepting a completion item +--- @field auto_brackets blink.cmp.AutoBracketsConfig + +--- @class (exact) blink.cmp.AutoBracketsConfig +--- @field enabled boolean Whether to auto-insert brackets for functions +--- @field default_brackets string[] Default brackets to use for unknown languages +--- @field override_brackets_for_filetypes table<string, string[] | fun(item: blink.cmp.CompletionItem): string[]> +--- @field force_allow_filetypes string[] Overrides the default blocked filetypes +--- @field blocked_filetypes string[] +--- @field kind_resolution blink.cmp.AutoBracketResolutionConfig Synchronously use the kind of the item to determine if brackets should be added +--- @field semantic_token_resolution blink.cmp.AutoBracketSemanticTokenResolutionConfig Asynchronously use semantic token to determine if brackets should be added + +--- @class (exact) blink.cmp.AutoBracketResolutionConfig +--- @field enabled boolean +--- @field blocked_filetypes string[] + +--- @class (exact) blink.cmp.AutoBracketSemanticTokenResolutionConfig +--- @field enabled boolean +--- @field blocked_filetypes string[] +--- @field timeout_ms number How long to wait for semantic tokens to return before assuming no brackets should be added + +local validate = require('blink.cmp.config.utils').validate +local accept = { + --- @type blink.cmp.CompletionAcceptConfig + default = { + create_undo_point = true, + auto_brackets = { + enabled = true, + default_brackets = { '(', ')' }, + override_brackets_for_filetypes = {}, + force_allow_filetypes = {}, + blocked_filetypes = {}, + kind_resolution = { + enabled = true, + blocked_filetypes = { 'typescriptreact', 'javascriptreact', 'vue', 'rust' }, + }, + semantic_token_resolution = { + enabled = true, + blocked_filetypes = { 'java' }, + timeout_ms = 400, + }, + }, + }, +} + +function accept.validate(config) + validate('completion.accept', { + create_undo_point = { config.create_undo_point, 'boolean' }, + auto_brackets = { config.auto_brackets, 'table' }, + }, config) + validate('completion.accept.auto_brackets', { + enabled = { config.auto_brackets.enabled, 'boolean' }, + default_brackets = { config.auto_brackets.default_brackets, 'table' }, + override_brackets_for_filetypes = { config.auto_brackets.override_brackets_for_filetypes, 'table' }, + force_allow_filetypes = { config.auto_brackets.force_allow_filetypes, 'table' }, + blocked_filetypes = { config.auto_brackets.blocked_filetypes, 'table' }, + kind_resolution = { config.auto_brackets.kind_resolution, 'table' }, + semantic_token_resolution = { config.auto_brackets.semantic_token_resolution, 'table' }, + }, config.auto_brackets) + validate('completion.accept.auto_brackets.kind_resolution', { + enabled = { config.auto_brackets.kind_resolution.enabled, 'boolean' }, + blocked_filetypes = { config.auto_brackets.kind_resolution.blocked_filetypes, 'table' }, + }, config.auto_brackets.kind_resolution) + validate('completion.accept.auto_brackets.semantic_token_resolution', { + enabled = { config.auto_brackets.semantic_token_resolution.enabled, 'boolean' }, + blocked_filetypes = { config.auto_brackets.semantic_token_resolution.blocked_filetypes, 'table' }, + timeout_ms = { config.auto_brackets.semantic_token_resolution.timeout_ms, 'number' }, + }, config.auto_brackets.semantic_token_resolution) +end + +return accept diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/documentation.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/documentation.lua new file mode 100644 index 0000000..71784d1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/documentation.lua @@ -0,0 +1,98 @@ +--- @class (exact) blink.cmp.CompletionDocumentationConfig +--- @field auto_show boolean Controls whether the documentation window will automatically show when selecting a completion item +--- @field auto_show_delay_ms number Delay before showing the documentation window +--- @field update_delay_ms number Delay before updating the documentation window when selecting a new item, while an existing item is still visible +--- @field treesitter_highlighting boolean Whether to use treesitter highlighting, disable if you run into performance issues +--- @field window blink.cmp.CompletionDocumentationWindowConfig + +--- @class (exact) blink.cmp.CompletionDocumentationWindowConfig +--- @field min_width number +--- @field max_width number +--- @field max_height number +--- @field desired_min_width number +--- @field desired_min_height number +--- @field border blink.cmp.WindowBorder +--- @field winblend number +--- @field winhighlight string +--- @field scrollbar boolean Note that the gutter will be disabled when border ~= 'none' +--- @field direction_priority blink.cmp.CompletionDocumentationDirectionPriorityConfig Which directions to show the window, for each of the possible menu window directions, falling back to the next direction when there's not enough space + +--- @class (exact) blink.cmp.CompletionDocumentationDirectionPriorityConfig +--- @field menu_north ("n" | "s" | "e" | "w")[] +--- @field menu_south ("n" | "s" | "e" | "w")[] + +local validate = require('blink.cmp.config.utils').validate +local documentation = { + --- @type blink.cmp.CompletionDocumentationConfig + default = { + auto_show = false, + auto_show_delay_ms = 500, + update_delay_ms = 50, + treesitter_highlighting = true, + window = { + min_width = 10, + max_width = 80, + max_height = 20, + desired_min_width = 50, + desired_min_height = 10, + border = 'padded', + winblend = 0, + winhighlight = 'Normal:BlinkCmpDoc,FloatBorder:BlinkCmpDocBorder,EndOfBuffer:BlinkCmpDoc', + scrollbar = true, + direction_priority = { + menu_north = { 'e', 'w', 'n', 's' }, + menu_south = { 'e', 'w', 's', 'n' }, + }, + }, + }, +} + +function documentation.validate(config) + validate('completion.documentation', { + auto_show = { config.auto_show, 'boolean' }, + auto_show_delay_ms = { config.auto_show_delay_ms, 'number' }, + update_delay_ms = { config.update_delay_ms, 'number' }, + treesitter_highlighting = { config.treesitter_highlighting, 'boolean' }, + window = { config.window, 'table' }, + }, config) + + validate('completion.documentation.window', { + min_width = { config.window.min_width, 'number' }, + max_width = { config.window.max_width, 'number' }, + max_height = { config.window.max_height, 'number' }, + desired_min_width = { config.window.desired_min_width, 'number' }, + desired_min_height = { config.window.desired_min_height, 'number' }, + border = { config.window.border, { 'string', 'table' } }, + winblend = { config.window.winblend, 'number' }, + winhighlight = { config.window.winhighlight, 'string' }, + scrollbar = { config.window.scrollbar, 'boolean' }, + direction_priority = { config.window.direction_priority, 'table' }, + }, config.window) + + validate('completion.documentation.window.direction_priority', { + menu_north = { + config.window.direction_priority.menu_north, + function(directions) + if type(directions) ~= 'table' or #directions == 0 then return false end + for _, direction in ipairs(directions) do + if not vim.tbl_contains({ 'n', 's', 'e', 'w' }, direction) then return false end + end + return true + end, + 'one of: "n", "s", "e", "w"', + }, + menu_south = { + config.window.direction_priority.menu_south, + function(directions) + if type(directions) ~= 'table' or #directions == 0 then return false end + for _, direction in ipairs(directions) do + if not vim.tbl_contains({ 'n', 's', 'e', 'w' }, direction) then return false end + end + return true + end, + 'one of: "n", "s", "e", "w"', + }, + }, config.window.direction_priority) +end + +return documentation diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/ghost_text.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/ghost_text.lua new file mode 100644 index 0000000..47fb2cd --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/ghost_text.lua @@ -0,0 +1,19 @@ +--- Displays a preview of the selected item on the current line +--- @class (exact) blink.cmp.CompletionGhostTextConfig +--- @field enabled boolean + +local validate = require('blink.cmp.config.utils').validate +local ghost_text = { + --- @type blink.cmp.CompletionGhostTextConfig + default = { + enabled = false, + }, +} + +function ghost_text.validate(config) + validate('completion.ghost_text', { + enabled = { config.enabled, 'boolean' }, + }, config) +end + +return ghost_text diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/init.lua new file mode 100644 index 0000000..24407ff --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/init.lua @@ -0,0 +1,42 @@ +--- @class (exact) blink.cmp.CompletionConfig +--- @field keyword blink.cmp.CompletionKeywordConfig +--- @field trigger blink.cmp.CompletionTriggerConfig +--- @field list blink.cmp.CompletionListConfig +--- @field accept blink.cmp.CompletionAcceptConfig +--- @field menu blink.cmp.CompletionMenuConfig +--- @field documentation blink.cmp.CompletionDocumentationConfig +--- @field ghost_text blink.cmp.CompletionGhostTextConfig + +local validate = require('blink.cmp.config.utils').validate +local completion = { + default = { + keyword = require('blink.cmp.config.completion.keyword').default, + trigger = require('blink.cmp.config.completion.trigger').default, + list = require('blink.cmp.config.completion.list').default, + accept = require('blink.cmp.config.completion.accept').default, + menu = require('blink.cmp.config.completion.menu').default, + documentation = require('blink.cmp.config.completion.documentation').default, + ghost_text = require('blink.cmp.config.completion.ghost_text').default, + }, +} + +function completion.validate(config) + validate('completion', { + keyword = { config.keyword, 'table' }, + trigger = { config.trigger, 'table' }, + list = { config.list, 'table' }, + accept = { config.accept, 'table' }, + menu = { config.menu, 'table' }, + documentation = { config.documentation, 'table' }, + ghost_text = { config.ghost_text, 'table' }, + }, config) + require('blink.cmp.config.completion.keyword').validate(config.keyword) + require('blink.cmp.config.completion.trigger').validate(config.trigger) + require('blink.cmp.config.completion.list').validate(config.list) + require('blink.cmp.config.completion.accept').validate(config.accept) + require('blink.cmp.config.completion.menu').validate(config.menu) + require('blink.cmp.config.completion.documentation').validate(config.documentation) + require('blink.cmp.config.completion.ghost_text').validate(config.ghost_text) +end + +return completion diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/keyword.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/keyword.lua new file mode 100644 index 0000000..a922ac4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/keyword.lua @@ -0,0 +1,27 @@ +--- @class (exact) blink.cmp.CompletionKeywordConfig +--- 'prefix' will fuzzy match on the text before the cursor +--- 'full' will fuzzy match on the text before *and* after the cursor +--- example: 'foo_|_bar' will match 'foo_' for 'prefix' and 'foo__bar' for 'full' +--- @field range blink.cmp.CompletionKeywordRange +--- +--- @alias blink.cmp.CompletionKeywordRange +--- | 'prefix' Fuzzy match on the text before the cursor (example: 'foo_|bar' will match 'foo_') +--- | 'full' Fuzzy match on the text before *and* after the cursor (example: 'foo_|_bar' will match 'foo__bar') + +local validate = require('blink.cmp.config.utils').validate +local keyword = { + --- @type blink.cmp.CompletionKeywordConfig + default = { range = 'prefix' }, +} + +function keyword.validate(config) + validate('completion.keyword', { + range = { + config.range, + function(range) return vim.tbl_contains({ 'prefix', 'full' }, range) end, + 'one of: prefix, full', + }, + }, config) +end + +return keyword diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/list.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/list.lua new file mode 100644 index 0000000..c0c5690 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/list.lua @@ -0,0 +1,54 @@ +--- @class (exact) blink.cmp.CompletionListConfig +--- @field max_items number Maximum number of items to display +--- @field selection blink.cmp.CompletionListSelectionConfig +--- @field cycle blink.cmp.CompletionListCycleConfig + +--- @class (exact) blink.cmp.CompletionListSelectionConfig +--- @field preselect boolean | fun(ctx: blink.cmp.Context): boolean When `true`, will automatically select the first item in the completion list +--- @field auto_insert boolean | fun(ctx: blink.cmp.Context): boolean When `true`, inserts the completion item automatically when selecting it. You may want to bind a key to the `cancel` command (default <C-e>) when using this option, which will both undo the selection and hide the completion menu + +--- @class (exact) blink.cmp.CompletionListCycleConfig +--- @field from_bottom boolean When `true`, calling `select_next` at the *bottom* of the completion list will select the *first* completion item. +--- @field from_top boolean When `true`, calling `select_prev` at the *top* of the completion list will select the *last* completion item. + +local validate = require('blink.cmp.config.utils').validate +local list = { + --- @type blink.cmp.CompletionListConfig + default = { + max_items = 200, + selection = { + preselect = true, + auto_insert = true, + }, + cycle = { + from_bottom = true, + from_top = true, + }, + }, +} + +function list.validate(config) + if type(config.selection) == 'function' then + error( + '`completion.list.selection` has been replaced with `completion.list.selection.preselect` and `completion.list.selection.auto_insert`. See the docs for more info: https://cmp.saghen.dev/configuration/completion.html#list' + ) + end + + validate('completion.list', { + max_items = { config.max_items, 'number' }, + selection = { config.selection, 'table' }, + cycle = { config.cycle, 'table' }, + }, config) + + validate('completion.list.selection', { + preselect = { config.selection.preselect, { 'boolean', 'function' } }, + auto_insert = { config.selection.auto_insert, { 'boolean', 'function' } }, + }, config.selection) + + validate('completion.list.cycle', { + from_bottom = { config.cycle.from_bottom, 'boolean' }, + from_top = { config.cycle.from_top, 'boolean' }, + }, config.cycle) +end + +return list diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/menu.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/menu.lua new file mode 100644 index 0000000..c807566 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/menu.lua @@ -0,0 +1,223 @@ +local validate = require('blink.cmp.config.utils').validate + +--- @class (exact) blink.cmp.CompletionMenuConfig +--- @field enabled boolean +--- @field min_width number +--- @field max_height number +--- @field border blink.cmp.WindowBorder +--- @field winblend number +--- @field winhighlight string +--- @field scrolloff number Keep the cursor X lines away from the top/bottom of the window +--- @field scrollbar boolean Note that the gutter will be disabled when border ~= 'none' +--- @field direction_priority ("n" | "s")[] Which directions to show the window, falling back to the next direction when there's not enough space +--- @field order blink.cmp.CompletionMenuOrderConfig TODO: implement +--- @field auto_show boolean | fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean Whether to automatically show the window when new completion items are available +--- @field cmdline_position fun(): number[] Screen coordinates (0-indexed) of the command line +--- @field draw blink.cmp.Draw Controls how the completion items are rendered on the popup window + +--- @class (exact) blink.cmp.CompletionMenuOrderConfig +--- @field n 'top_down' | 'bottom_up' +--- @field s 'top_down' | 'bottom_up' + +local window = { + --- @type blink.cmp.CompletionMenuConfig + default = { + enabled = true, + min_width = 15, + max_height = 10, + border = 'none', + winblend = 0, + winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None', + -- keep the cursor X lines away from the top/bottom of the window + scrolloff = 2, + -- note that the gutter will be disabled when border ~= 'none' + scrollbar = true, + -- which directions to show the window, + -- falling back to the next direction when there's not enough space + direction_priority = { 's', 'n' }, + -- which direction previous/next items show up + -- TODO: implement + order = { n = 'bottom_up', s = 'top_down' }, + + -- Whether to automatically show the window when new completion items are available + auto_show = true, + + -- Screen coordinates of the command line + cmdline_position = function() + if vim.g.ui_cmdline_pos ~= nil then + local pos = vim.g.ui_cmdline_pos -- (1, 0)-indexed + return { pos[1] - 1, pos[2] } + end + local height = (vim.o.cmdheight == 0) and 1 or vim.o.cmdheight + return { vim.o.lines - height, 0 } + end, + + -- Controls how the completion items are rendered on the popup window + draw = { + -- Aligns the keyword you've typed to a component in the menu + align_to = 'label', -- or 'none' to disable + -- Left and right padding, optionally { left, right } for different padding on each side + padding = 1, + -- Gap between columns + gap = 1, + treesitter = {}, -- Use treesitter to highlight the label text of completions from these sources + -- Components to render, grouped by column + columns = { { 'kind_icon' }, { 'label', 'label_description', gap = 1 } }, + -- Definitions for possible components to render. Each component defines: + -- ellipsis: whether to add an ellipsis when truncating the text + -- width: control the min, max and fill behavior of the component + -- text function: will be called for each item + -- highlight function: will be called only when the line appears on screen + components = { + kind_icon = { + ellipsis = false, + text = function(ctx) return ctx.kind_icon .. ctx.icon_gap end, + highlight = function(ctx) + return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or ('BlinkCmpKind' .. ctx.kind) + end, + }, + + kind = { + ellipsis = false, + width = { fill = true }, + text = function(ctx) return ctx.kind end, + highlight = function(ctx) + return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or ('BlinkCmpKind' .. ctx.kind) + end, + }, + + label = { + width = { fill = true, max = 60 }, + text = function(ctx) return ctx.label .. ctx.label_detail end, + highlight = function(ctx) + -- label and label details + local label = ctx.label + local highlights = { + { 0, #label, group = ctx.deprecated and 'BlinkCmpLabelDeprecated' or 'BlinkCmpLabel' }, + } + if ctx.label_detail then + table.insert(highlights, { #label, #label + #ctx.label_detail, group = 'BlinkCmpLabelDetail' }) + end + + if vim.list_contains(ctx.self.treesitter, ctx.source_id) then + -- add treesitter highlights + vim.list_extend(highlights, require('blink.cmp.completion.windows.render.treesitter').highlight(ctx)) + end + + -- characters matched on the label by the fuzzy matcher + for _, idx in ipairs(ctx.label_matched_indices) do + table.insert(highlights, { idx, idx + 1, group = 'BlinkCmpLabelMatch' }) + end + + return highlights + end, + }, + + label_description = { + width = { max = 30 }, + text = function(ctx) return ctx.label_description end, + highlight = 'BlinkCmpLabelDescription', + }, + + source_name = { + width = { max = 30 }, + -- source_name or source_id are supported + text = function(ctx) return ctx.source_name end, + highlight = 'BlinkCmpSource', + }, + }, + }, + }, +} + +function window.validate(config) + validate('completion.menu', { + enabled = { config.enabled, 'boolean' }, + min_width = { config.min_width, 'number' }, + max_height = { config.max_height, 'number' }, + border = { config.border, { 'string', 'table' } }, + winblend = { config.winblend, 'number' }, + winhighlight = { config.winhighlight, 'string' }, + scrolloff = { config.scrolloff, 'number' }, + scrollbar = { config.scrollbar, 'boolean' }, + direction_priority = { + config.direction_priority, + function(direction_priority) + for _, direction in ipairs(direction_priority) do + if not vim.tbl_contains({ 'n', 's' }, direction) then return false end + end + return true + end, + 'one of: "n", "s"', + }, + order = { config.order, 'table' }, + auto_show = { config.auto_show, { 'boolean', 'function' } }, + cmdline_position = { config.cmdline_position, 'function' }, + draw = { config.draw, 'table' }, + }, config) + validate('completion.menu.order', { + n = { config.order.n, { 'string', 'nil' } }, + s = { config.order.s, { 'string', 'nil' } }, + }, config.order) + + validate('completion.menu.draw', { + align_to = { + config.draw.align_to, + function(align) + if align == 'none' or align == 'cursor' then return true end + for _, column in ipairs(config.draw.columns) do + for _, component in ipairs(column) do + if component == align then return true end + end + end + return false + end, + '"none" or one of the components defined in the "columns"', + }, + + padding = { + config.draw.padding, + function(padding) + if type(padding) == 'number' then return true end + if type(padding) ~= 'table' or #padding ~= 2 then return false end + if type(padding[1]) == 'number' and type(padding[2]) == 'number' then return true end + return false + end, + 'a number or a tuple of 2 numbers (i.e. [1, 2])', + }, + gap = { config.draw.gap, 'number' }, + + treesitter = { config.draw.treesitter, 'table' }, + + columns = { + config.draw.columns, + function(columns) + local available_components = vim.tbl_keys(config.draw.components) + + if type(columns) ~= 'table' or #columns == 0 then return false end + for _, column in ipairs(columns) do + if #column == 0 then return false end + for _, component in ipairs(column) do + if not vim.tbl_contains(available_components, component) then return false end + end + if column.gap ~= nil and type(column.gap) ~= 'number' then return false end + end + return true + end, + 'a table of tables, where each table contains a list of components and an optional gap. List of available components: ' + .. table.concat(vim.tbl_keys(config.draw.components), ', '), + }, + components = { config.draw.components, 'table' }, + }, config.draw) + + for component, definition in pairs(config.draw.components) do + validate('completion.menu.draw.components.' .. component, { + ellipsis = { definition.ellipsis, 'boolean', true }, + width = { definition.width, 'table', true }, + text = { definition.text, 'function' }, + highlight = { definition.highlight, { 'string', 'function' }, true }, + }, config.draw.components[component]) + end +end + +return window diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/trigger.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/trigger.lua new file mode 100644 index 0000000..94d0af8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/completion/trigger.lua @@ -0,0 +1,42 @@ +--- @class (exact) blink.cmp.CompletionTriggerConfig +--- @field prefetch_on_insert boolean When true, will prefetch the completion items when entering insert mode. WARN: buggy, not recommended unless you'd like to help develop prefetching +--- @field show_in_snippet boolean When false, will not show the completion window when in a snippet +--- @field show_on_keyword boolean When true, will show the completion window after typing any of alphanumerics, `-` or `_` +--- @field show_on_trigger_character boolean When true, will show the completion window after typing a trigger character +--- @field show_on_blocked_trigger_characters string[] | (fun(): string[]) LSPs can indicate when to show the completion window via trigger characters. However, some LSPs (i.e. tsserver) return characters that would essentially always show the window. We block these by default. +--- @field show_on_accept_on_trigger_character boolean When both this and show_on_trigger_character are true, will show the completion window when the cursor comes after a trigger character after accepting an item +--- @field show_on_insert_on_trigger_character boolean When both this and show_on_trigger_character are true, will show the completion window when the cursor comes after a trigger character when entering insert mode +--- @field show_on_x_blocked_trigger_characters string[] | (fun(): string[]) List of trigger characters (on top of `show_on_blocked_trigger_characters`) that won't trigger the completion window when the cursor comes after a trigger character when entering insert mode/accepting an item + +local validate = require('blink.cmp.config.utils').validate +local trigger = { + --- @type blink.cmp.CompletionTriggerConfig + default = { + prefetch_on_insert = false, + show_in_snippet = true, + show_on_keyword = true, + show_on_trigger_character = true, + show_on_blocked_trigger_characters = function() + if vim.api.nvim_get_mode().mode == 'c' then return {} end + return { ' ', '\n', '\t' } + end, + show_on_accept_on_trigger_character = true, + show_on_insert_on_trigger_character = true, + show_on_x_blocked_trigger_characters = { "'", '"', '(', '{', '[' }, + }, +} + +function trigger.validate(config) + validate('completion.trigger', { + prefetch_on_insert = { config.prefetch_on_insert, 'boolean' }, + show_in_snippet = { config.show_in_snippet, 'boolean' }, + show_on_keyword = { config.show_on_keyword, 'boolean' }, + show_on_trigger_character = { config.show_on_trigger_character, 'boolean' }, + show_on_blocked_trigger_characters = { config.show_on_blocked_trigger_characters, { 'function', 'table' } }, + show_on_accept_on_trigger_character = { config.show_on_accept_on_trigger_character, 'boolean' }, + show_on_insert_on_trigger_character = { config.show_on_insert_on_trigger_character, 'boolean' }, + show_on_x_blocked_trigger_characters = { config.show_on_x_blocked_trigger_characters, { 'function', 'table' } }, + }, config) +end + +return trigger diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/fuzzy.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/fuzzy.lua new file mode 100644 index 0000000..aa50e0b --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/fuzzy.lua @@ -0,0 +1,66 @@ +--- @class (exact) blink.cmp.FuzzyConfig +--- @field use_typo_resistance boolean When enabled, allows for a number of typos relative to the length of the query. Disabling this matches the behavior of fzf +--- @field use_frecency boolean Tracks the most recently/frequently used items and boosts the score of the item +--- @field use_proximity boolean Boosts the score of items matching nearby words +--- @field use_unsafe_no_lock boolean UNSAFE!! When enabled, disables the lock and fsync when writing to the frecency database. This should only be used on unsupported platforms (i.e. alpine termux) +--- @field sorts ("label" | "sort_text" | "kind" | "score" | blink.cmp.SortFunction)[] Controls which sorts to use and in which order, these three are currently the only allowed options +--- @field prebuilt_binaries blink.cmp.PrebuiltBinariesConfig + +--- @class (exact) blink.cmp.PrebuiltBinariesConfig +--- @field download boolean Whenther or not to automatically download a prebuilt binary from github. If this is set to `false` you will need to manually build the fuzzy binary dependencies by running `cargo build --release` +--- @field ignore_version_mismatch boolean Ignores mismatched version between the built binary and the current git sha, when building locally +--- @field force_version? string When downloading a prebuilt binary, force the downloader to resolve this version. If this is unset then the downloader will attempt to infer the version from the checked out git tag (if any). WARN: Beware that `main` may be incompatible with the version you select +--- @field force_system_triple? string When downloading a prebuilt binary, force the downloader to use this system triple. If this is unset then the downloader will attempt to infer the system triple from `jit.os` and `jit.arch`. Check the latest release for all available system triples. WARN: Beware that `main` may be incompatible with the version you select +--- @field extra_curl_args string[] Extra arguments that will be passed to curl like { 'curl', ..extra_curl_args, ..built_in_args } + +--- @alias blink.cmp.SortFunction fun(a: blink.cmp.CompletionItem, b: blink.cmp.CompletionItem): boolean | nil + +local validate = require('blink.cmp.config.utils').validate +local fuzzy = { + --- @type blink.cmp.FuzzyConfig + default = { + use_typo_resistance = true, + use_frecency = true, + use_proximity = true, + use_unsafe_no_lock = false, + sorts = { 'score', 'sort_text' }, + prebuilt_binaries = { + download = true, + ignore_version_mismatch = false, + force_version = nil, + force_system_triple = nil, + extra_curl_args = {}, + }, + }, +} + +function fuzzy.validate(config) + validate('fuzzy', { + use_typo_resistance = { config.use_typo_resistance, 'boolean' }, + use_frecency = { config.use_frecency, 'boolean' }, + use_proximity = { config.use_proximity, 'boolean' }, + use_unsafe_no_lock = { config.use_unsafe_no_lock, 'boolean' }, + sorts = { + config.sorts, + function(sorts) + for _, sort in ipairs(sorts) do + if not vim.tbl_contains({ 'label', 'sort_text', 'kind', 'score' }, sort) and type(sort) ~= 'function' then + return false + end + end + return true + end, + 'one of: "label", "sort_text", "kind", "score" or a function', + }, + prebuilt_binaries = { config.prebuilt_binaries, 'table' }, + }, config) + validate('fuzzy.prebuilt_binaries', { + download = { config.prebuilt_binaries.download, 'boolean' }, + ignore_version_mismatch = { config.prebuilt_binaries.ignore_version_mismatch, 'boolean' }, + force_version = { config.prebuilt_binaries.force_version, { 'string', 'nil' } }, + force_system_triple = { config.prebuilt_binaries.force_system_triple, { 'string', 'nil' } }, + extra_curl_args = { config.prebuilt_binaries.extra_curl_args, { 'table' } }, + }, config.prebuilt_binaries) +end + +return fuzzy diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/init.lua new file mode 100644 index 0000000..ac5678c --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/init.lua @@ -0,0 +1,57 @@ +--- @class (exact) blink.cmp.ConfigStrict +--- @field enabled fun(): boolean +--- @field keymap blink.cmp.KeymapConfig +--- @field completion blink.cmp.CompletionConfig +--- @field fuzzy blink.cmp.FuzzyConfig +--- @field sources blink.cmp.SourceConfig +--- @field signature blink.cmp.SignatureConfig +--- @field snippets blink.cmp.SnippetsConfig +--- @field appearance blink.cmp.AppearanceConfig + +local validate = require('blink.cmp.config.utils').validate +--- @type blink.cmp.ConfigStrict +local config = { + enabled = function() return vim.bo.buftype ~= 'prompt' and vim.b.completion ~= false end, + keymap = require('blink.cmp.config.keymap').default, + completion = require('blink.cmp.config.completion').default, + fuzzy = require('blink.cmp.config.fuzzy').default, + sources = require('blink.cmp.config.sources').default, + signature = require('blink.cmp.config.signature').default, + snippets = require('blink.cmp.config.snippets').default, + appearance = require('blink.cmp.config.appearance').default, +} + +--- @type blink.cmp.ConfigStrict +--- @diagnostic disable-next-line: missing-fields +local M = {} + +--- @param cfg blink.cmp.ConfigStrict +function M.validate(cfg) + validate('config', { + enabled = { cfg.enabled, 'function' }, + keymap = { cfg.keymap, 'table' }, + completion = { cfg.completion, 'table' }, + fuzzy = { cfg.fuzzy, 'table' }, + sources = { cfg.sources, 'table' }, + signature = { cfg.signature, 'table' }, + snippets = { cfg.snippets, 'table' }, + appearance = { cfg.appearance, 'table' }, + }, cfg) + require('blink.cmp.config.keymap').validate(cfg.keymap) + require('blink.cmp.config.completion').validate(cfg.completion) + require('blink.cmp.config.fuzzy').validate(cfg.fuzzy) + require('blink.cmp.config.sources').validate(cfg.sources) + require('blink.cmp.config.signature').validate(cfg.signature) + require('blink.cmp.config.snippets').validate(cfg.snippets) + require('blink.cmp.config.appearance').validate(cfg.appearance) +end + +--- @param user_config blink.cmp.Config +function M.merge_with(user_config) + config = vim.tbl_deep_extend('force', config, user_config) + M.validate(config) +end + +return setmetatable(M, { + __index = function(_, k) return config[k] end, +}) diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/keymap.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/keymap.lua new file mode 100644 index 0000000..86d74eb --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/keymap.lua @@ -0,0 +1,174 @@ +--- @alias blink.cmp.KeymapCommand +--- | 'fallback' Fallback to the built-in behavior +--- | 'show' Show the completion window +--- | 'hide' Hide the completion window +--- | 'cancel' Cancel the current completion, undoing the preview from auto_insert +--- | 'accept' Accept the current completion item +--- | 'select_and_accept' Select the first completion item, if there's no selection, and accept +--- | 'select_prev' Select the previous completion item +--- | 'select_next' Select the next completion item +--- | 'show_documentation' Show the documentation window +--- | 'hide_documentation' Hide the documentation window +--- | 'scroll_documentation_up' Scroll the documentation window up +--- | 'scroll_documentation_down' Scroll the documentation window down +--- | 'snippet_forward' Move the cursor forward to the next snippet placeholder +--- | 'snippet_backward' Move the cursor backward to the previous snippet placeholder +--- | (fun(cmp: blink.cmp.API): boolean?) Custom function where returning true will prevent the next command from running + +--- @alias blink.cmp.KeymapPreset +--- | 'none' No keymaps +--- Mappings similar to the built-in completion: +--- ```lua +--- { +--- ['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, +--- ['<C-e>'] = { 'cancel', 'fallback' }, +--- ['<C-y>'] = { 'select_and_accept' }, +--- +--- ['<C-p>'] = { 'select_prev', 'fallback' }, +--- ['<C-n>'] = { 'select_next', 'fallback' }, +--- +--- ['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, +--- ['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, +--- +--- ['<Tab>'] = { 'snippet_forward', 'fallback' }, +--- ['<S-Tab>'] = { 'snippet_backward', 'fallback' }, +--- } +--- ``` +--- | 'default' +--- Mappings similar to VSCode. +--- You may want to set `completion.trigger.show_in_snippet = false` or use `completion.list.selection.preselect = function(ctx) return not require('blink.cmp').snippet_active({ direction = 1 }) end` when using this mapping: +--- ```lua +--- { +--- ['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, +--- ['<C-e>'] = { 'cancel', 'fallback' }, +--- +--- ['<Tab>'] = { +--- function(cmp) +--- if cmp.snippet_active() then return cmp.accept() +--- else return cmp.select_and_accept() end +--- end, +--- 'snippet_forward', +--- 'fallback' +--- }, +--- ['<S-Tab>'] = { 'snippet_backward', 'fallback' }, +--- +--- ['<Up>'] = { 'select_prev', 'fallback' }, +--- ['<Down>'] = { 'select_next', 'fallback' }, +--- ['<C-p>'] = { 'select_prev', 'fallback' }, +--- ['<C-n>'] = { 'select_next', 'fallback' }, +--- +--- ['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, +--- ['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, +--- } +--- ``` +--- | 'super-tab' +--- Similar to 'super-tab' but with `enter` to accept +--- You may want to set `completion.list.selection.preselect = false` when using this keymap: +--- ```lua +--- { +--- ['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, +--- ['<C-e>'] = { 'cancel', 'fallback' }, +--- ['<CR>'] = { 'accept', 'fallback' }, +--- +--- ['<Tab>'] = { 'snippet_forward', 'fallback' }, +--- ['<S-Tab>'] = { 'snippet_backward', 'fallback' }, +--- +--- ['<Up>'] = { 'select_prev', 'fallback' }, +--- ['<Down>'] = { 'select_next', 'fallback' }, +--- ['<C-p>'] = { 'select_prev', 'fallback' }, +--- ['<C-n>'] = { 'select_next', 'fallback' }, +--- +--- ['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, +--- ['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, +--- } +--- ``` +--- | 'enter' + +--- When specifying 'preset' in the keymap table, the custom key mappings are merged with the preset, and any conflicting keys will overwrite the preset mappings. +--- The "fallback" command will run the next non blink keymap. +--- +--- Example: +--- +--- ```lua +--- keymap = { +--- preset = 'default', +--- ['<Up>'] = { 'select_prev', 'fallback' }, +--- ['<Down>'] = { 'select_next', 'fallback' }, +--- +--- -- disable a keymap from the preset +--- ['<C-e>'] = {}, +--- +--- -- optionally, define different keymaps for cmdline +--- cmdline = { +--- preset = 'cmdline', +--- } +--- } +--- ``` +--- +--- When defining your own keymaps without a preset, no keybinds will be assigned automatically. +--- @class (exact) blink.cmp.BaseKeymapConfig +--- @field preset? blink.cmp.KeymapPreset +--- @field [string] blink.cmp.KeymapCommand[] Table of keys => commands[] + +--- @class (exact) blink.cmp.KeymapConfig : blink.cmp.BaseKeymapConfig +--- @field cmdline? blink.cmp.BaseKeymapConfig Optionally, define a separate keymap for cmdline + +local keymap = { + --- @type blink.cmp.KeymapConfig + default = { + preset = 'default', + }, +} + +--- @param config blink.cmp.KeymapConfig +function keymap.validate(config) + local commands = { + 'fallback', + 'show', + 'hide', + 'cancel', + 'accept', + 'select_and_accept', + 'select_prev', + 'select_next', + 'show_documentation', + 'hide_documentation', + 'scroll_documentation_up', + 'scroll_documentation_down', + 'snippet_forward', + 'snippet_backward', + } + local presets = { 'default', 'super-tab', 'enter', 'none' } + + local validation_schema = {} + for key, value in pairs(config) do + -- nested cmdline keymap + if key == 'cmdline' then + keymap.validate(value) + + -- preset + elseif key == 'preset' then + validation_schema[key] = { + value, + function(preset) return vim.tbl_contains(presets, preset) end, + 'one of: ' .. table.concat(presets, ', '), + } + + -- key + else + validation_schema[key] = { + value, + function(key_commands) + for _, command in ipairs(key_commands) do + if type(command) ~= 'function' and not vim.tbl_contains(commands, command) then return false end + end + return true + end, + 'commands must be one of: ' .. table.concat(commands, ', '), + } + end + end + require('blink.cmp.config.utils')._validate(validation_schema) +end + +return keymap diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/shared.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/shared.lua new file mode 100644 index 0000000..1ab7a60 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/shared.lua @@ -0,0 +1,2 @@ +--- @alias blink.cmp.WindowBorderChar string | table +--- @alias blink.cmp.WindowBorder 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | 'padded' | 'none' | blink.cmp.WindowBorderChar[] diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/signature.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/signature.lua new file mode 100644 index 0000000..987c7ca --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/signature.lua @@ -0,0 +1,72 @@ +--- @class (exact) blink.cmp.SignatureConfig +--- @field enabled boolean +--- @field trigger blink.cmp.SignatureTriggerConfig +--- @field window blink.cmp.SignatureWindowConfig + +--- @class (exact) blink.cmp.SignatureTriggerConfig +--- @field blocked_trigger_characters string[] +--- @field blocked_retrigger_characters string[] +--- @field show_on_insert_on_trigger_character boolean When true, will show the signature help window when the cursor comes after a trigger character when entering insert mode + +--- @class (exact) blink.cmp.SignatureWindowConfig +--- @field min_width number +--- @field max_width number +--- @field max_height number +--- @field border blink.cmp.WindowBorder +--- @field winblend number +--- @field winhighlight string +--- @field scrollbar boolean Note that the gutter will be disabled when border ~= 'none' +--- @field direction_priority ("n" | "s")[] Which directions to show the window, falling back to the next direction when there's not enough space, or another window is in the way. +--- @field treesitter_highlighting boolean Disable if you run into performance issues + +local validate = require('blink.cmp.config.utils').validate +local signature = { + --- @type blink.cmp.SignatureConfig + default = { + enabled = false, + trigger = { + enabled = true, + blocked_trigger_characters = {}, + blocked_retrigger_characters = {}, + show_on_insert_on_trigger_character = true, + }, + window = { + min_width = 1, + max_width = 100, + max_height = 10, + border = 'padded', + winblend = 0, + winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder', + scrollbar = false, + direction_priority = { 'n', 's' }, + treesitter_highlighting = true, + }, + }, +} + +function signature.validate(config) + validate('signature', { + enabled = { config.enabled, 'boolean' }, + trigger = { config.trigger, 'table' }, + window = { config.window, 'table' }, + }, config) + validate('signature.trigger', { + enabled = { config.trigger.enabled, 'boolean' }, + blocked_trigger_characters = { config.trigger.blocked_trigger_characters, 'table' }, + blocked_retrigger_characters = { config.trigger.blocked_retrigger_characters, 'table' }, + show_on_insert_on_trigger_character = { config.trigger.show_on_insert_on_trigger_character, 'boolean' }, + }, config.trigger) + validate('signature.window', { + min_width = { config.window.min_width, 'number' }, + max_width = { config.window.max_width, 'number' }, + max_height = { config.window.max_height, 'number' }, + border = { config.window.border, { 'string', 'table' } }, + winblend = { config.window.winblend, 'number' }, + winhighlight = { config.window.winhighlight, 'string' }, + scrollbar = { config.window.scrollbar, 'boolean' }, + direction_priority = { config.window.direction_priority, 'table' }, + treesitter_highlighting = { config.window.treesitter_highlighting, 'boolean' }, + }, config.window) +end + +return signature diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/snippets.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/snippets.lua new file mode 100644 index 0000000..cd804e9 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/snippets.lua @@ -0,0 +1,65 @@ +--- @class (exact) blink.cmp.SnippetsConfig +--- @field preset 'default' | 'luasnip' | 'mini_snippets' +--- @field expand fun(snippet: string) Function to use when expanding LSP provided snippets +--- @field active fun(filter?: { direction?: number }): boolean Function to use when checking if a snippet is active +--- @field jump fun(direction: number) Function to use when jumping between tab stops in a snippet, where direction can be negative or positive + +--- @param handlers table<'default' | 'luasnip' | 'mini_snippets', fun(...): any> +local function by_preset(handlers) + return function(...) + local preset = require('blink.cmp.config').snippets.preset + return handlers[preset](...) + end +end + +local validate = require('blink.cmp.config.utils').validate +local snippets = { + --- @type blink.cmp.SnippetsConfig + default = { + preset = 'default', + -- NOTE: we wrap `vim.snippet` calls to reduce startup by 1-2ms + expand = by_preset({ + default = function(snippet) vim.snippet.expand(snippet) end, + luasnip = function(snippet) require('luasnip').lsp_expand(snippet) end, + mini_snippets = function(snippet) + if not _G.MiniSnippets then error('mini.snippets has not been setup') end + local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert + insert(snippet) + end, + }), + active = by_preset({ + default = function(filter) return vim.snippet.active(filter) end, + luasnip = function(filter) + if filter and filter.direction then return require('luasnip').jumpable(filter.direction) end + return require('luasnip').in_snippet() + end, + mini_snippets = function() + if not _G.MiniSnippets then error('mini.snippets has not been setup') end + return MiniSnippets.session.get(false) ~= nil + end, + }), + jump = by_preset({ + default = function(direction) vim.snippet.jump(direction) end, + luasnip = function(direction) require('luasnip').jump(direction) end, + mini_snippets = function(direction) + if not _G.MiniSnippets then error('mini.snippets has not been setup') end + MiniSnippets.session.jump(direction == -1 and 'prev' or 'next') + end, + }), + }, +} + +function snippets.validate(config) + validate('snippets', { + preset = { + config.preset, + function(preset) return vim.tbl_contains({ 'default', 'luasnip', 'mini_snippets' }, preset) end, + 'one of: "default", "luasnip", "mini_snippets"', + }, + expand = { config.expand, 'function' }, + active = { config.active, 'function' }, + jump = { config.jump, 'function' }, + }, config) +end + +return snippets diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/sources.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/sources.lua new file mode 100644 index 0000000..ab06b5f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/sources.lua @@ -0,0 +1,149 @@ +--- @class blink.cmp.SourceConfig +--- Static list of providers to enable, or a function to dynamically enable/disable providers based on the context +--- +--- Example dynamically picking providers based on the filetype and treesitter node: +--- ```lua +--- function(ctx) +--- local node = vim.treesitter.get_node() +--- if vim.bo.filetype == 'lua' then +--- return { 'lsp', 'path' } +--- elseif node and vim.tbl_contains({ 'comment', 'line_comment', 'block_comment' }), node:type()) +--- return { 'buffer' } +--- else +--- return { 'lsp', 'path', 'snippets', 'buffer' } +--- end +--- end +--- ``` +--- @field default string[] | fun(): string[] +--- @field per_filetype table<string, string[] | fun(): string[]> +--- @field cmdline string[] | fun(): string[] +--- +--- @field transform_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] Function to transform the items before they're returned +--- @field min_keyword_length number | fun(ctx: blink.cmp.Context): number Minimum number of characters in the keyword to trigger +--- +--- @field providers table<string, blink.cmp.SourceProviderConfig> + +--- @class blink.cmp.SourceProviderConfig +--- @field name string +--- @field module string +--- @field enabled? boolean | fun(): boolean Whether or not to enable the provider +--- @field opts? table +--- @field async? boolean | fun(ctx: blink.cmp.Context): boolean Whether blink should wait for the source to return before showing the completions +--- @field timeout_ms? number | fun(ctx: blink.cmp.Context): number How long to wait for the provider to return before showing completions and treating it as asynchronous +--- @field transform_items? fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] Function to transform the items before they're returned +--- @field should_show_items? boolean | fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean Whether or not to show the items +--- @field max_items? number | fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): number Maximum number of items to display in the menu +--- @field min_keyword_length? number | fun(ctx: blink.cmp.Context): number Minimum number of characters in the keyword to trigger the provider +--- @field fallbacks? string[] | fun(ctx: blink.cmp.Context, enabled_sources: string[]): string[] If this provider returns 0 items, it will fallback to these providers +--- @field score_offset? number | fun(ctx: blink.cmp.Context, enabled_sources: string[]): number Boost/penalize the score of the items +--- @field deduplicate? blink.cmp.DeduplicateConfig TODO: implement +--- @field override? blink.cmp.SourceOverride Override the source's functions + +local validate = require('blink.cmp.config.utils').validate +local sources = { + --- @type blink.cmp.SourceConfig + default = { + default = { 'lsp', 'path', 'snippets', 'buffer' }, + per_filetype = {}, + cmdline = function() + local type = vim.fn.getcmdtype() + -- Search forward and backward + if type == '/' or type == '?' then return { 'buffer' } end + -- Commands + if type == ':' or type == '@' then return { 'cmdline' } end + return {} + end, + + transform_items = function(_, items) return items end, + min_keyword_length = 0, + + providers = { + lsp = { + name = 'LSP', + module = 'blink.cmp.sources.lsp', + fallbacks = { 'buffer' }, + transform_items = function(_, items) + -- demote snippets + for _, item in ipairs(items) do + if item.kind == require('blink.cmp.types').CompletionItemKind.Snippet then + item.score_offset = item.score_offset - 3 + end + end + + -- filter out text items, since we have the buffer source + return vim.tbl_filter( + function(item) return item.kind ~= require('blink.cmp.types').CompletionItemKind.Text end, + items + ) + end, + }, + path = { + name = 'Path', + module = 'blink.cmp.sources.path', + score_offset = 3, + fallbacks = { 'buffer' }, + }, + snippets = { + name = 'Snippets', + module = 'blink.cmp.sources.snippets', + score_offset = -3, + }, + buffer = { + name = 'Buffer', + module = 'blink.cmp.sources.buffer', + score_offset = -3, + }, + cmdline = { + name = 'cmdline', + module = 'blink.cmp.sources.cmdline', + }, + }, + }, +} + +function sources.validate(config) + assert( + config.completion == nil, + '`sources.completion.enabled_providers` has been replaced with `sources.default`. !!Note!! Be sure to update `opts_extend` as well if you have it set' + ) + + validate('sources', { + default = { config.default, { 'function', 'table' } }, + per_filetype = { config.per_filetype, 'table' }, + cmdline = { config.cmdline, { 'function', 'table' } }, + + transform_items = { config.transform_items, 'function' }, + min_keyword_length = { config.min_keyword_length, { 'number', 'function' } }, + + providers = { config.providers, 'table' }, + }, config) + for id, provider in pairs(config.providers) do + sources.validate_provider(id, provider) + end +end + +function sources.validate_provider(id, provider) + assert( + provider.fallback_for == nil, + '`fallback_for` has been replaced with `fallbacks` which work in the opposite direction. For example, fallback_for = { "lsp" } on "buffer" would now be "fallbacks" = { "buffer" } on "lsp"' + ) + + validate('sources.providers.' .. id, { + name = { provider.name, 'string' }, + module = { provider.module, 'string' }, + enabled = { provider.enabled, { 'boolean', 'function' }, true }, + opts = { provider.opts, 'table', true }, + async = { provider.async, { 'boolean', 'function' }, true }, + timeout_ms = { provider.timeout_ms, { 'number', 'function' }, true }, + transform_items = { provider.transform_items, 'function', true }, + should_show_items = { provider.should_show_items, { 'boolean', 'function' }, true }, + max_items = { provider.max_items, { 'number', 'function' }, true }, + min_keyword_length = { provider.min_keyword_length, { 'number', 'function' }, true }, + fallbacks = { provider.fallback_for, { 'table', 'function' }, true }, + score_offset = { provider.score_offset, { 'number', 'function' }, true }, + deduplicate = { provider.deduplicate, 'table', true }, + override = { provider.override, 'table', true }, + }, provider) +end + +return sources diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/types_partial.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/types_partial.lua new file mode 100644 index 0000000..1f525d1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/types_partial.lua @@ -0,0 +1,78 @@ +--- @class (exact) blink.cmp.Config : blink.cmp.ConfigStrict +--- @field enabled? fun(): boolean +--- @field keymap? blink.cmp.KeymapConfig +--- @field completion? blink.cmp.CompletionConfigPartial +--- @field fuzzy? blink.cmp.FuzzyConfigPartial +--- @field sources? blink.cmp.SourceConfigPartial +--- @field signature? blink.cmp.SignatureConfigPartial +--- @field snippets? blink.cmp.SnippetsConfigPartial +--- @field appearance? blink.cmp.AppearanceConfigPartial + +--- @class (exact) blink.cmp.CompletionConfigPartial : blink.cmp.CompletionConfig +--- @field keyword? blink.cmp.CompletionKeywordConfigPartial +--- @field trigger? blink.cmp.CompletionTriggerConfigPartial +--- @field list? blink.cmp.CompletionListConfigPartial +--- @field accept? blink.cmp.CompletionAcceptConfigPartial +--- @field menu? blink.cmp.CompletionMenuConfigPartial +--- @field documentation? blink.cmp.CompletionDocumentationConfigPartial +--- @field ghost_text? blink.cmp.CompletionGhostTextConfigPartial + +--- @class (exact) blink.cmp.CompletionKeywordConfigPartial : blink.cmp.CompletionKeywordConfig, {} + +--- @class (exact) blink.cmp.CompletionTriggerConfigPartial : blink.cmp.CompletionTriggerConfig, {} + +--- @class (exact) blink.cmp.CompletionListConfigPartial : blink.cmp.CompletionListConfig, {} +--- @field selection? blink.cmp.CompletionListSelectionConfigPartial +--- @field cycle? blink.cmp.CompletionListCycleConfigPartial + +--- @class (exact) blink.cmp.CompletionListSelectionConfigPartial : blink.cmp.CompletionListSelectionConfig, {} + +--- @class (exact) blink.cmp.CompletionListCycleConfigPartial : blink.cmp.CompletionListCycleConfig, {} + +--- @class (exact) blink.cmp.CompletionAcceptConfigPartial : blink.cmp.CompletionAcceptConfig, {} +--- @field auto_brackets? blink.cmp.AutoBracketsConfigPartial + +--- @class (exact) blink.cmp.AutoBracketsConfigPartial : blink.cmp.AutoBracketsConfig, {} +--- @field kind_resolution? blink.cmp.AutoBracketResolutionConfigPartial Synchronously use the kind of the item to determine if brackets should be added +--- @field semantic_token_resolution? blink.cmp.AutoBracketSemanticTokenResolutionConfigPartial Asynchronously use semantic token to determine if brackets should be added + +--- @class (exact) blink.cmp.AutoBracketResolutionConfigPartial : blink.cmp.AutoBracketResolutionConfig, {} + +--- @class (exact) blink.cmp.AutoBracketSemanticTokenResolutionConfigPartial : blink.cmp.AutoBracketSemanticTokenResolutionConfig, {} + +--- @class (exact) blink.cmp.CompletionMenuConfigPartial : blink.cmp.CompletionMenuConfig, {} +--- @field order? blink.cmp.CompletionMenuOrderConfigPartial TODO: implement + +--- @class (exact) blink.cmp.CompletionMenuOrderConfigPartial : blink.cmp.CompletionMenuOrderConfig, {} + +--- @class (exact) blink.cmp.CompletionDocumentationConfigPartial : blink.cmp.CompletionDocumentationConfig, {} +--- @field window? blink.cmp.CompletionDocumentationWindowConfigPartial + +--- @class (exact) blink.cmp.CompletionDocumentationWindowConfigPartial : blink.cmp.CompletionDocumentationWindowConfig, {} +--- @field direction_priority? blink.cmp.CompletionDocumentationDirectionPriorityConfigPartial Which directions to show the window, for each of the possible menu window directions, falling back to the next direction when there's not enough space + +--- @class (exact) blink.cmp.CompletionDocumentationDirectionPriorityConfigPartial : blink.cmp.CompletionDocumentationDirectionPriorityConfig, {} + +--- @class (exact) blink.cmp.CompletionGhostTextConfigPartial : blink.cmp.CompletionGhostTextConfig, {} + +--- @class (exact) blink.cmp.FuzzyConfigPartial : blink.cmp.FuzzyConfig, {} +--- @field prebuilt_binaries? blink.cmp.PrebuiltBinariesConfigPartial + +--- @class (exact) blink.cmp.PrebuiltBinariesConfigPartial : blink.cmp.PrebuiltBinariesConfig, {} + +--- @class blink.cmp.SourceConfigPartial : blink.cmp.SourceConfig, {} +--- @field providers? table<string, blink.cmp.SourceProviderConfigPartial> + +--- @class blink.cmp.SourceProviderConfigPartial : blink.cmp.SourceProviderConfig, {} + +--- @class (exact) blink.cmp.SignatureConfigPartial : blink.cmp.SignatureConfig, {} +--- @field trigger? blink.cmp.SignatureTriggerConfigPartial +--- @field window? blink.cmp.SignatureWindowConfigPartial + +--- @class (exact) blink.cmp.SignatureTriggerConfigPartial : blink.cmp.SignatureTriggerConfig, {} + +--- @class (exact) blink.cmp.SignatureWindowConfigPartial : blink.cmp.SignatureWindowConfig, {} + +--- @class (exact) blink.cmp.SnippetsConfigPartial : blink.cmp.SnippetsConfig, {} + +--- @class (exact) blink.cmp.AppearanceConfigPartial : blink.cmp.AppearanceConfig, {} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/utils.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/utils.lua new file mode 100644 index 0000000..4cbc052 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/config/utils.lua @@ -0,0 +1,36 @@ +local utils = {} + +-- Code taken from @MariaSolOs in a indent-blankline.nvim PR: +-- https://github.com/lukas-reineke/indent-blankline.nvim/pull/934/files#diff-09ebcaa8c75cd1e92d25640e377ab261cfecaf8351c9689173fd36c2d0c23d94R16 +-- Saves a whopping 20% compared to vim.validate (0.8ms -> 0.64ms) + +--- Use the faster validate version if available +--- @param spec table<string, {[1]:any, [2]:function|string, [3]:string|true|nil}> +--- NOTE: We disable some Lua diagnostics here since lua_ls isn't smart enough to +--- realize that we're using an overloaded function. +function utils._validate(spec) + if vim.fn.has('nvim-0.11') == 0 then return vim.validate(spec) end + for key, key_spec in pairs(spec) do + local message = type(key_spec[3]) == 'string' and key_spec[3] or nil --[[@as string?]] + local optional = type(key_spec[3]) == 'boolean' and key_spec[3] or nil --[[@as boolean?]] + ---@diagnostic disable-next-line:param-type-mismatch, redundant-parameter + vim.validate(key, key_spec[1], key_spec[2], optional, message) + end +end + +--- @param path string The path to the field being validated +--- @param tbl table The table to validate +--- @param source table The original table that we're validating against +--- @see vim.validate +function utils.validate(path, tbl, source) + -- validate + local _, err = pcall(utils._validate, tbl) + if err then error(path .. '.' .. err) end + + -- check for erroneous fields + for k, _ in pairs(source) do + if tbl[k] == nil then error(path .. '.' .. k .. ': unexpected field found in configuration') end + end +end + +return utils diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/files.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/files.lua new file mode 100644 index 0000000..28bbac5 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/files.lua @@ -0,0 +1,181 @@ +local async = require('blink.cmp.lib.async') +local utils = require('blink.cmp.lib.utils') +local system = require('blink.cmp.fuzzy.download.system') + +local function get_lib_extension() + if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then return '.dylib' end + if jit.os:lower() == 'windows' then return '.dll' end + return '.so' +end + +local current_file_dir = debug.getinfo(1).source:match('@?(.*/)') +local current_file_dir_parts = vim.split(current_file_dir, '/') +local root_dir = table.concat(utils.slice(current_file_dir_parts, 1, #current_file_dir_parts - 6), '/') +local lib_folder = root_dir .. '/target/release' +local lib_filename = 'libblink_cmp_fuzzy' .. get_lib_extension() +local lib_path = lib_folder .. '/' .. lib_filename +local checksum_filename = lib_filename .. '.sha256' +local checksum_path = lib_path .. '.sha256' +local version_path = lib_folder .. '/version' + +local files = { + get_lib_extension = get_lib_extension, + root_dir = root_dir, + lib_folder = lib_folder, + lib_filename = lib_filename, + lib_path = lib_path, + checksum_path = checksum_path, + checksum_filename = checksum_filename, + version_path = version_path, +} + +--- Checksums --- + +function files.get_checksum() + return files.read_file(files.checksum_path):map(function(checksum) return vim.split(checksum, ' ')[1] end) +end + +function files.get_checksum_for_file(path) + return async.task.new(function(resolve, reject) + local os = system.get_info() + local args + if os == 'linux' then + args = { 'sha256sum', path } + elseif os == 'mac' or os == 'osx' then + args = { 'shasum', '-a', '256', path } + elseif os == 'windows' then + args = { 'certutil', '-hashfile', path, 'SHA256' } + end + + vim.system(args, {}, function(out) + if out.code ~= 0 then return reject('Failed to calculate checksum of pre-built binary: ' .. out.stderr) end + + local stdout = out.stdout or '' + if os == 'windows' then stdout = vim.split(stdout, '\r\n')[2] end + -- We get an output like 'sha256sum filename' on most systems, so we grab just the checksum + return resolve(vim.split(stdout, ' ')[1]) + end) + end) +end + +function files.verify_checksum() + return async.task + .await_all({ files.get_checksum(), files.get_checksum_for_file(files.lib_path) }) + :map(function(checksums) + assert(#checksums == 2, 'Expected 2 checksums, got ' .. #checksums) + assert(checksums[1] and checksums[2], 'Expected checksums to be non-nil') + assert( + checksums[1] == checksums[2], + 'Checksum of pre-built binary does not match. Expected "' .. checksums[1] .. '", got "' .. checksums[2] .. '"' + ) + end) +end + +--- Prebuilt binary --- + +function files.get_version() + return files + .read_file(files.version_path) + :map(function(version) + if #version == 40 then + return { sha = version } + else + return { tag = version } + end + end) + :catch(function() return {} end) +end + +--- @param version string +--- @return blink.cmp.Task +function files.set_version(version) + return files + .create_dir(files.root_dir .. '/target') + :map(function() return files.create_dir(files.lib_folder) end) + :map(function() return files.write_file(files.version_path, version) end) +end + +--- Filesystem helpers --- + +--- @param path string +--- @return blink.cmp.Task +function files.read_file(path) + return async.task.new(function(resolve, reject) + vim.uv.fs_open(path, 'r', 438, function(open_err, fd) + if open_err or fd == nil then return reject(open_err or 'Unknown error') end + vim.uv.fs_read(fd, 1024, 0, function(read_err, data) + vim.uv.fs_close(fd, function() end) + if read_err or data == nil then return reject(read_err or 'Unknown error') end + return resolve(data) + end) + end) + end) +end + +--- @param path string +--- @param data string +--- @return blink.cmp.Task +function files.write_file(path, data) + return async.task.new(function(resolve, reject) + vim.uv.fs_open(path, 'w', 438, function(open_err, fd) + if open_err or fd == nil then return reject(open_err or 'Unknown error') end + vim.uv.fs_write(fd, data, 0, function(write_err) + vim.uv.fs_close(fd, function() end) + if write_err then return reject(write_err) end + return resolve() + end) + end) + end) +end + +--- @param path string +--- @return blink.cmp.Task +function files.exists(path) + return async.task.new(function(resolve) + vim.uv.fs_stat(path, function(err) resolve(not err) end) + end) +end + +--- @param path string +--- @return blink.cmp.Task +function files.stat(path) + return async.task.new(function(resolve, reject) + vim.uv.fs_stat(path, function(err, stat) + if err then return reject(err) end + resolve(stat) + end) + end) +end + +--- @param path string +--- @return blink.cmp.Task +function files.create_dir(path) + return files + .stat(path) + :map(function(stat) return stat.type == 'directory' end) + :catch(function() return false end) + :map(function(exists) + if exists then return end + + return async.task.new(function(resolve, reject) + vim.uv.fs_mkdir(path, 511, function(err) + if err then return reject(err) end + resolve() + end) + end) + end) +end + +--- Renames a file +--- @param old_path string +--- @param new_path string +function files.rename(old_path, new_path) + return async.task.new(function(resolve, reject) + vim.uv.fs_rename(old_path, new_path, function(err) + if err then return reject(err) end + resolve() + end) + end) +end + +return files diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/git.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/git.lua new file mode 100644 index 0000000..63a7646 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/git.lua @@ -0,0 +1,70 @@ +local async = require('blink.cmp.lib.async') +local files = require('blink.cmp.fuzzy.download.files') +local git = {} + +function git.get_version() + return async.task.await_all({ git.get_tag(), git.get_sha() }):map( + function(results) + return { + tag = results[1], + sha = results[2], + } + end + ) +end + +function git.get_tag() + return async.task.new(function(resolve, reject) + -- If repo_dir is nil, no git reposiory is found, similar to `out.code == 128` + local repo_dir = vim.fs.root(files.root_dir, '.git') + if not repo_dir then resolve() end + + vim.system({ + 'git', + '--git-dir', + vim.fs.joinpath(repo_dir, '.git'), + '--work-tree', + repo_dir, + 'describe', + '--tags', + '--exact-match', + }, { cwd = files.root_dir }, function(out) + if out.code == 128 then return resolve() end + if out.code ~= 0 then + return reject('While getting git tag, git exited with code ' .. out.code .. ': ' .. out.stderr) + end + + local lines = vim.split(out.stdout, '\n') + if not lines[1] then return reject('Expected atleast 1 line of output from git describe') end + return resolve(lines[1]) + end) + end) +end + +function git.get_sha() + return async.task.new(function(resolve, reject) + -- If repo_dir is nil, no git reposiory is found, similar to `out.code == 128` + local repo_dir = vim.fs.root(files.root_dir, '.git') + if not repo_dir then resolve() end + + vim.system({ + 'git', + '--git-dir', + vim.fs.joinpath(repo_dir, '.git'), + '--work-tree', + repo_dir, + 'rev-parse', + 'HEAD', + }, { cwd = files.root_dir }, function(out) + if out.code == 128 then return resolve() end + if out.code ~= 0 then + return reject('While getting git sha, git exited with code ' .. out.code .. ': ' .. out.stderr) + end + + local sha = vim.split(out.stdout, '\n')[1] + return resolve(sha) + end) + end) +end + +return git diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/init.lua new file mode 100644 index 0000000..085becd --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/init.lua @@ -0,0 +1,170 @@ +local download_config = require('blink.cmp.config').fuzzy.prebuilt_binaries +local async = require('blink.cmp.lib.async') +local git = require('blink.cmp.fuzzy.download.git') +local files = require('blink.cmp.fuzzy.download.files') +local system = require('blink.cmp.fuzzy.download.system') + +local download = {} + +--- @param callback fun(err: string | nil) +function download.ensure_downloaded(callback) + callback = vim.schedule_wrap(callback) + + if not download_config.download then return callback() end + + async.task + .await_all({ git.get_version(), files.get_version() }) + :map(function(results) + return { + git = results[1], + current = results[2], + } + end) + :map(function(version) + local target_git_tag = download_config.force_version or version.git.tag + + -- not built locally, not on a git tag, error + assert( + version.current.sha ~= nil or target_git_tag ~= nil, + "\nDetected an out of date or missing fuzzy matching library. Can't download from github due to not being on a git tag and no `fuzzy.prebuilt_binaries.force_version` is set." + .. '\nEither run `cargo build --release` via your package manager, switch to a git tag, or set `fuzzy.prebuilt_binaries.force_version` in config. ' + .. 'See the docs for more info.' + ) + + -- built locally, ignore + if + not download_config.force_version + and ( + version.current.sha == version.git.sha + or version.current.sha ~= nil and download_config.ignore_version_mismatch + ) + then + return + end + + -- built locally but outdated and not on a git tag, error + if + not download_config.force_version + and version.current.sha ~= nil + and version.current.sha ~= version.git.sha + then + assert( + target_git_tag or download_config.ignore_version_mismatch, + "\nFound an outdated version of the fuzzy matching library, but can't download from github due to not being on a git tag. " + .. '\n!! FOR DEVELOPERS !!, set `fuzzy.prebuilt_binaries.ignore_version_mismatch = true` in config. ' + .. '\n!! FOR USERS !!, either run `cargo build --release` via your package manager, switch to a git tag, or set `fuzzy.prebuilt_binaries.force_version` in config. ' + .. 'See the docs for more info.' + ) + if not download_config.ignore_version_mismatch then + vim.schedule( + function() + vim.notify( + '[blink.cmp]: Found an outdated version of the fuzzy matching library built locally', + vim.log.levels.INFO, + { title = 'blink.cmp' } + ) + end + ) + end + end + + -- already downloaded and the correct version, just verify the checksum, and re-download if checksum fails + if version.current.tag ~= nil and version.current.tag == target_git_tag then + return files.verify_checksum():catch(function(err) + vim.schedule(function() + vim.notify(err, vim.log.levels.WARN, { title = 'blink.cmp' }) + vim.notify( + '[blink.cmp]: Pre-built binary failed checksum verification, re-downloading', + vim.log.levels.WARN, + { title = 'blink.cmp' } + ) + end) + return download.download(target_git_tag) + end) + end + + -- unknown state + if not target_git_tag then error('Unknown error while getting pre-built binary. Consider re-installing') end + + -- download as per usual + vim.schedule( + function() vim.notify('[blink.cmp]: Downloading pre-built binary', vim.log.levels.INFO, { title = 'blink.cmp' }) end + ) + return download.download(target_git_tag) + end) + :map(function() callback() end) + :catch(function(err) callback(err) end) +end + +function download.download(version) + -- NOTE: we set the version to 'v0.0.0' to avoid a failure causing the pre-built binary being marked as locally built + return files + .set_version('v0.0.0') + :map(function() return download.from_github(version) end) + :map(function() return files.verify_checksum() end) + :map(function() return files.set_version(version) end) +end + +--- @param tag string +--- @return blink.cmp.Task +function download.from_github(tag) + return system.get_triple():map(function(system_triple) + if not system_triple then + return error( + 'Your system is not supported by pre-built binaries. You must run cargo build --release via your package manager with rust nightly. See the README for more info.' + ) + end + + local base_url = 'https://github.com/saghen/blink.cmp/releases/download/' .. tag .. '/' + local library_url = base_url .. system_triple .. files.get_lib_extension() + local checksum_url = base_url .. system_triple .. files.get_lib_extension() .. '.sha256' + + return async + .task + .await_all({ + download.download_file(library_url, files.lib_filename .. '.tmp'), + download.download_file(checksum_url, files.checksum_filename), + }) + -- Mac caches the library in the kernel, so updating in place causes a crash + -- We instead write to a temporary file and rename it, as mentioned in: + -- https://developer.apple.com/documentation/security/updating-mac-software + :map( + function() + return files.rename( + files.lib_folder .. '/' .. files.lib_filename .. '.tmp', + files.lib_folder .. '/' .. files.lib_filename + ) + end + ) + end) +end + +--- @param url string +--- @param filename string +--- @return blink.cmp.Task +function download.download_file(url, filename) + return async.task.new(function(resolve, reject) + local args = { 'curl' } + vim.list_extend(args, download_config.extra_curl_args) + vim.list_extend(args, { + '--fail', -- Fail on 4xx/5xx + '--location', -- Follow redirects + '--silent', -- Don't show progress + '--show-error', -- Show errors, even though we're using --silent + '--create-dirs', + '--output', + files.lib_folder .. '/' .. filename, + url, + }) + + vim.system(args, {}, function(out) + if out.code ~= 0 then + reject('Failed to download ' .. filename .. 'for pre-built binaries: ' .. out.stderr) + else + resolve() + end + end) + end) +end + +return download diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/system.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/system.lua new file mode 100644 index 0000000..7b83a0f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/download/system.lua @@ -0,0 +1,74 @@ +local download_config = require('blink.cmp.config').fuzzy.prebuilt_binaries +local async = require('blink.cmp.lib.async') +local system = {} + +system.triples = { + mac = { + arm = 'aarch64-apple-darwin', + x64 = 'x86_64-apple-darwin', + }, + windows = { + x64 = 'x86_64-pc-windows-msvc', + }, + linux = { + android = 'aarch64-linux-android', + arm = function(libc) return 'aarch64-unknown-linux-' .. libc end, + x64 = function(libc) return 'x86_64-unknown-linux-' .. libc end, + }, +} + +--- Gets the operating system and architecture of the current system +--- @return string, string +function system.get_info() + local os = jit.os:lower() + if os == 'osx' then os = 'mac' end + local arch = jit.arch:lower():match('arm') and 'arm' or jit.arch:lower():match('x64') and 'x64' or nil + return os, arch +end + +--- Gets the system triple for the current system +--- I.e. `x86_64-unknown-linux-gnu` or `aarch64-apple-darwin` +--- @return blink.cmp.Task +function system.get_triple() + return async.task.new(function(resolve) + if download_config.force_system_triple then return resolve(download_config.force_system_triple) end + + local os, arch = system.get_info() + local triples = system.triples[os] + + if os == 'linux' then + if vim.fn.has('android') == 1 then return resolve(triples.android) end + + vim.uv.fs_stat('/etc/alpine-release', function(err, is_alpine) + local libc = (not err and is_alpine) and 'musl' or 'gnu' + local triple = triples[arch] + return resolve(triple and type(triple) == 'function' and triple(libc) or triple) + end) + else + return resolve(triples[arch]) + end + end) +end + +--- Same as `system.get_triple` but synchronous +--- @see system.get_triple +--- @return string | nil +function system.get_triple_sync() + if download_config.force_system_triple then return download_config.force_system_triple end + + local os, arch = system.get_info() + local triples = system.triples[os] + + if os == 'linux' then + if vim.fn.has('android') == 1 then return triples.android end + + local success, is_alpine = pcall(vim.uv.fs_stat, '/etc/alpine-release') + local libc = (success and is_alpine) and 'musl' or 'gnu' + local triple = triples[arch] + return triple and type(triple) == 'function' and triple(libc) or triple + else + return triples[arch] + end +end + +return system diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/frecency.rs b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/frecency.rs new file mode 100644 index 0000000..a672598 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/frecency.rs @@ -0,0 +1,153 @@ +use crate::lsp_item::LspItem; +use heed::{types::*, EnvFlags}; +use heed::{Database, Env, EnvOpenOptions}; +use mlua::Result as LuaResult; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Clone, Serialize, Deserialize)] +struct CompletionItemKey { + label: String, + kind: u32, + source_id: String, +} + +impl From<&LspItem> for CompletionItemKey { + fn from(item: &LspItem) -> Self { + Self { + label: item.label.clone(), + kind: item.kind, + source_id: item.source_id.clone(), + } + } +} + +#[derive(Debug)] +pub struct FrecencyTracker { + env: Env, + db: Database<SerdeBincode<CompletionItemKey>, SerdeBincode<Vec<u64>>>, + access_thresholds: Vec<(f64, u64)>, +} + +impl FrecencyTracker { + pub fn new(db_path: &str, use_unsafe_no_lock: bool) -> LuaResult<Self> { + fs::create_dir_all(db_path).map_err(|err| { + mlua::Error::RuntimeError( + "Failed to create frecency database directory: ".to_string() + &err.to_string(), + ) + })?; + let env = unsafe { + let mut opts = EnvOpenOptions::new(); + if use_unsafe_no_lock { + opts.flags(EnvFlags::NO_LOCK | EnvFlags::NO_SYNC | EnvFlags::NO_META_SYNC); + } + opts.open(db_path).map_err(|err| { + mlua::Error::RuntimeError( + "Failed to open frecency database: ".to_string() + &err.to_string(), + ) + })? + }; + env.clear_stale_readers().map_err(|err| { + mlua::Error::RuntimeError( + "Failed to clear stale readers for frecency database: ".to_string() + + &err.to_string(), + ) + })?; + + // we will open the default unnamed database + let mut wtxn = env.write_txn().map_err(|err| { + mlua::Error::RuntimeError( + "Failed to open write transaction for frecency database: ".to_string() + + &err.to_string(), + ) + })?; + let db = env.create_database(&mut wtxn, None).map_err(|err| { + mlua::Error::RuntimeError( + "Failed to create frecency database: ".to_string() + &err.to_string(), + ) + })?; + + let access_thresholds = [ + (1., 1000 * 60 * 2), // 2 minutes + (0.2, 1000 * 60 * 60), // 1 hour + (0.1, 1000 * 60 * 60 * 24), // 1 day + (0.05, 1000 * 60 * 60 * 24 * 7), // 1 week + ] + .to_vec(); + + Ok(FrecencyTracker { + env: env.clone(), + db, + access_thresholds, + }) + } + + fn get_accesses(&self, item: &LspItem) -> LuaResult<Option<Vec<u64>>> { + let rtxn = self.env.read_txn().map_err(|err| { + mlua::Error::RuntimeError( + "Failed to start read transaction for frecency database: ".to_string() + + &err.to_string(), + ) + })?; + self.db + .get(&rtxn, &CompletionItemKey::from(item)) + .map_err(|err| { + mlua::Error::RuntimeError( + "Failed to read from frecency database: ".to_string() + &err.to_string(), + ) + }) + } + + fn get_now(&self) -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } + + pub fn access(&mut self, item: &LspItem) -> LuaResult<()> { + let mut wtxn = self.env.write_txn().map_err(|err| { + mlua::Error::RuntimeError( + "Failed to start write transaction for frecency database: ".to_string() + + &err.to_string(), + ) + })?; + + let mut accesses = self.get_accesses(item)?.unwrap_or_default(); + accesses.push(self.get_now()); + + self.db + .put(&mut wtxn, &CompletionItemKey::from(item), &accesses) + .map_err(|err| { + mlua::Error::RuntimeError( + "Failed to write to frecency database: ".to_string() + &err.to_string(), + ) + })?; + + wtxn.commit().map_err(|err| { + mlua::Error::RuntimeError( + "Failed to commit write transaction for frecency database: ".to_string() + + &err.to_string(), + ) + })?; + + Ok(()) + } + + pub fn get_score(&self, item: &LspItem) -> i64 { + let accesses = self.get_accesses(item).unwrap_or(None).unwrap_or_default(); + let now = self.get_now(); + let mut score = 0.0; + 'outer: for access in &accesses { + let duration_since = now - access; + for (rank, threshold_duration_since) in &self.access_thresholds { + if duration_since < *threshold_duration_since { + score += rank; + continue 'outer; + } + } + } + score.min(4.) as i64 + } +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/fuzzy.rs b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/fuzzy.rs new file mode 100644 index 0000000..b09bb99 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/fuzzy.rs @@ -0,0 +1,183 @@ +// TODO: refactor this heresy + +use crate::frecency::FrecencyTracker; +use crate::keyword; +use crate::lsp_item::LspItem; +use mlua::prelude::*; +use mlua::FromLua; +use mlua::Lua; +use std::collections::HashMap; +use std::collections::HashSet; + +#[derive(Clone, Hash)] +pub struct FuzzyOptions { + match_suffix: bool, + use_typo_resistance: bool, + use_frecency: bool, + use_proximity: bool, + nearby_words: Option<Vec<String>>, + min_score: u16, +} + +impl FromLua for FuzzyOptions { + fn from_lua(value: LuaValue, _lua: &'_ Lua) -> LuaResult<Self> { + if let Some(tab) = value.as_table() { + let match_suffix: bool = tab.get("match_suffix").unwrap_or_default(); + let use_typo_resistance: bool = tab.get("use_typo_resistance").unwrap_or_default(); + let use_frecency: bool = tab.get("use_frecency").unwrap_or_default(); + let use_proximity: bool = tab.get("use_proximity").unwrap_or_default(); + let nearby_words: Option<Vec<String>> = tab.get("nearby_words").ok(); + let min_score: u16 = tab.get("min_score").unwrap_or_default(); + + Ok(FuzzyOptions { + match_suffix, + use_typo_resistance, + use_frecency, + use_proximity, + nearby_words, + min_score, + }) + } else { + Err(mlua::Error::FromLuaConversionError { + from: "LuaValue", + to: "FuzzyOptions".to_string(), + message: None, + }) + } + } +} + +fn group_by_needle( + line: &str, + cursor_col: usize, + haystack: &[String], + match_suffix: bool, +) -> HashMap<String, Vec<(usize, String)>> { + let mut items_by_needle: HashMap<String, Vec<(usize, String)>> = HashMap::new(); + for (idx, item_text) in haystack.iter().enumerate() { + let needle = keyword::guess_keyword_from_item(item_text, line, cursor_col, match_suffix); + let entry = items_by_needle.entry(needle).or_default(); + entry.push((idx, item_text.to_string())); + } + items_by_needle +} + +pub fn fuzzy( + line: &str, + cursor_col: usize, + haystack: &[LspItem], + frecency: &FrecencyTracker, + opts: FuzzyOptions, +) -> (Vec<i32>, Vec<u32>) { + let haystack_labels = haystack + .iter() + .map(|s| s.filter_text.clone().unwrap_or(s.label.clone())) + .collect::<Vec<_>>(); + let options = frizbee::Options { + prefilter: !opts.use_typo_resistance, + min_score: opts.min_score, + stable_sort: false, + ..Default::default() + }; + + // Items may have different fuzzy matching ranges, so we split them up by needle + let mut matches = group_by_needle(line, cursor_col, &haystack_labels, opts.match_suffix) + .into_iter() + // Match on each needle and combine + .flat_map(|(needle, haystack)| { + let mut matches = frizbee::match_list( + &needle, + &haystack + .iter() + .map(|(_, str)| str.as_str()) + .collect::<Vec<_>>(), + options, + ); + for mtch in matches.iter_mut() { + mtch.index_in_haystack = haystack[mtch.index_in_haystack].0; + } + matches + }) + .collect::<Vec<_>>(); + + matches.sort_by_key(|mtch| mtch.index_in_haystack); + for (idx, mtch) in matches.iter_mut().enumerate() { + mtch.index = idx; + } + + // Get the score for each match, adding score_offset, frecency and proximity bonus + let nearby_words: HashSet<String> = HashSet::from_iter(opts.nearby_words.unwrap_or_default()); + let match_scores = matches + .iter() + .map(|mtch| { + let frecency_score = if opts.use_frecency { + frecency.get_score(&haystack[mtch.index_in_haystack]) as i32 + } else { + 0 + }; + let nearby_words_score = if opts.use_proximity { + nearby_words + .get(&haystack_labels[mtch.index_in_haystack]) + .map(|_| 2) + .unwrap_or(0) + } else { + 0 + }; + let score_offset = haystack[mtch.index_in_haystack].score_offset; + + (mtch.score as i32) + frecency_score + nearby_words_score + score_offset + }) + .collect::<Vec<_>>(); + + // Find the highest score and filter out matches that are unreasonably lower than it + if opts.use_typo_resistance { + let max_score = matches.iter().map(|mtch| mtch.score).max().unwrap_or(0); + let secondary_min_score = max_score.max(16) - 16; + matches = matches + .into_iter() + .filter(|mtch| mtch.score >= secondary_min_score) + .collect::<Vec<_>>(); + } + + // Return scores and indices + ( + matches + .iter() + .map(|mtch| match_scores[mtch.index]) + .collect::<Vec<_>>(), + matches + .iter() + .map(|mtch| mtch.index_in_haystack as u32) + .collect::<Vec<_>>(), + ) +} + +pub fn fuzzy_matched_indices( + line: &str, + cursor_col: usize, + haystack: &[String], + match_suffix: bool, +) -> Vec<Vec<usize>> { + let mut matches = group_by_needle(line, cursor_col, haystack, match_suffix) + .into_iter() + .flat_map(|(needle, haystack)| { + frizbee::match_list_for_matched_indices( + &needle, + &haystack + .iter() + .map(|(_, str)| str.as_str()) + .collect::<Vec<_>>(), + ) + .into_iter() + .enumerate() + .map(|(idx, matched_indices)| (haystack[idx].0, matched_indices)) + .collect::<Vec<_>>() + }) + .collect::<Vec<_>>(); + matches.sort_by_key(|mtch| mtch.0); + + matches + .into_iter() + .map(|(_, matched_indices)| matched_indices) + .collect::<Vec<_>>() +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/init.lua new file mode 100644 index 0000000..ad4db03 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/init.lua @@ -0,0 +1,118 @@ +local config = require('blink.cmp.config') + +--- @class blink.cmp.Fuzzy +local fuzzy = { + rust = require('blink.cmp.fuzzy.rust'), + haystacks_by_provider_cache = {}, + has_init_db = false, +} + +function fuzzy.init_db() + if fuzzy.has_init_db then return end + + fuzzy.rust.init_db(vim.fn.stdpath('data') .. '/blink/cmp/fuzzy.db', config.use_unsafe_no_lock) + + vim.api.nvim_create_autocmd('VimLeavePre', { + callback = fuzzy.rust.destroy_db, + }) + + fuzzy.has_init_db = true +end + +---@param item blink.cmp.CompletionItem +function fuzzy.access(item) + fuzzy.init_db() + + -- writing to the db takes ~10ms, so schedule writes in another thread + vim.uv + .new_work(function(itm, cpath) + package.cpath = cpath + require('blink.cmp.fuzzy.rust').access(vim.mpack.decode(itm)) + end, function() end) + :queue(vim.mpack.encode(item), package.cpath) +end + +---@param lines string +function fuzzy.get_words(lines) return fuzzy.rust.get_words(lines) end + +--- @param line string +--- @param cursor_col number +--- @param haystack string[] +--- @param range blink.cmp.CompletionKeywordRange +function fuzzy.fuzzy_matched_indices(line, cursor_col, haystack, range) + return fuzzy.rust.fuzzy_matched_indices(line, cursor_col, haystack, range == 'full') +end + +--- @param line string +--- @param cursor_col number +--- @param haystacks_by_provider table<string, blink.cmp.CompletionItem[]> +--- @param range blink.cmp.CompletionKeywordRange +--- @return blink.cmp.CompletionItem[] +function fuzzy.fuzzy(line, cursor_col, haystacks_by_provider, range) + fuzzy.init_db() + + for provider_id, haystack in pairs(haystacks_by_provider) do + -- set the provider items once since Lua <-> Rust takes the majority of the time + if fuzzy.haystacks_by_provider_cache[provider_id] ~= haystack then + fuzzy.haystacks_by_provider_cache[provider_id] = haystack + fuzzy.rust.set_provider_items(provider_id, haystack) + end + end + + -- get the nearby words + local cursor_row = vim.api.nvim_win_get_cursor(0)[1] + local start_row = math.max(0, cursor_row - 30) + local end_row = math.min(cursor_row + 30, vim.api.nvim_buf_line_count(0)) + local nearby_text = table.concat(vim.api.nvim_buf_get_lines(0, start_row, end_row, false), '\n') + local nearby_words = #nearby_text < 10000 and fuzzy.rust.get_words(nearby_text) or {} + + local keyword_start_col, keyword_end_col = + require('blink.cmp.fuzzy').get_keyword_range(line, cursor_col, config.completion.keyword.range) + local keyword_length = keyword_end_col - keyword_start_col + + local filtered_items = {} + for provider_id, haystack in pairs(haystacks_by_provider) do + -- perform fuzzy search + local scores, matched_indices = fuzzy.rust.fuzzy(line, cursor_col, provider_id, { + -- each matching char is worth 7 points (+ 1 for matching capitalization) + -- and it receives a bonus for capitalization, delimiter and prefix + -- so this should generally be good + -- TODO: make this configurable + -- TODO: instead of a min score, set X number of allowed typos + min_score = config.fuzzy.use_typo_resistance and (6 * keyword_length) or 0, + use_typo_resistance = config.fuzzy.use_typo_resistance, + use_frecency = config.fuzzy.use_frecency and keyword_length > 0, + use_proximity = config.fuzzy.use_proximity and keyword_length > 0, + sorts = config.fuzzy.sorts, + nearby_words = nearby_words, + match_suffix = range == 'full', + }) + + for idx, item_index in ipairs(matched_indices) do + local item = haystack[item_index + 1] + item.score = scores[idx] + table.insert(filtered_items, item) + end + end + + return require('blink.cmp.fuzzy.sort').sort(filtered_items, config.fuzzy.sorts) +end + +--- @param line string +--- @param col number +--- @param range? blink.cmp.CompletionKeywordRange +--- @return number, number +function fuzzy.get_keyword_range(line, col, range) + return require('blink.cmp.fuzzy.rust').get_keyword_range(line, col, range == 'full') +end + +--- @param item blink.cmp.CompletionItem +--- @param line string +--- @param col number +--- @param range blink.cmp.CompletionKeywordRange +--- @return number, number +function fuzzy.guess_edit_range(item, line, col, range) + return require('blink.cmp.fuzzy.rust').guess_edit_range(item, line, col, range == 'full') +end + +return fuzzy diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/keyword.rs b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/keyword.rs new file mode 100644 index 0000000..13d5020 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/keyword.rs @@ -0,0 +1,84 @@ +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + static ref BACKWARD_REGEX: Regex = Regex::new(r"[\p{L}0-9_][\p{L}0-9_\\-]*$").unwrap(); + static ref FORWARD_REGEX: Regex = Regex::new(r"^[\p{L}0-9_\\-]+").unwrap(); +} + +/// Given a line and cursor position, returns the start and end indices of the keyword +pub fn get_keyword_range(line: &str, col: usize, match_suffix: bool) -> (usize, usize) { + let before_match_start = BACKWARD_REGEX + .find(&line[0..col.min(line.len())]) + .map(|m| m.start()); + if !match_suffix { + return (before_match_start.unwrap_or(col), col); + } + + let after_match_end = FORWARD_REGEX + .find(&line[col.min(line.len())..]) + .map(|m| m.end() + col); + ( + before_match_start.unwrap_or(col), + after_match_end.unwrap_or(col), + ) +} + +/// Given a string, guesses the start and end indices in the line for the specific item +/// 1. Get the keyword range (alphanumeric, underscore, hyphen) on the line and end of the item +/// text +/// 2. Check if the suffix of the item text matches the suffix of the line text, if so, include the +/// suffix in the range +/// +/// Example: +/// line: example/str/trim +/// item: str/trim +/// matches on: str/trim +/// +/// line: example/trim +/// item: str/trim +/// matches on: trim +/// +/// TODO: +/// line: ' +/// item: 'tabline' +/// matches on: ' +pub fn guess_keyword_range_from_item( + item_text: &str, + line: &str, + cursor_col: usize, + match_suffix: bool, +) -> (usize, usize) { + let line_range = get_keyword_range(line, cursor_col, match_suffix); + let text_range = get_keyword_range(item_text, item_text.len(), false); + + let line_prefix = line.chars().take(line_range.0).collect::<String>(); + let text_prefix = item_text.chars().take(text_range.0).collect::<String>(); + if line_prefix.ends_with(&text_prefix) { + return (line_range.0 - text_prefix.len(), line_range.1); + } + + line_range +} + +pub fn guess_keyword_from_item( + item_text: &str, + line: &str, + cursor_col: usize, + match_suffix: bool, +) -> String { + let (start, end) = guess_keyword_range_from_item(item_text, line, cursor_col, match_suffix); + line[start..end].to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_keyword_range_unicode() { + let line = "'вest'"; + let col = line.len() - 1; + assert_eq!(get_keyword_range(line, col, false), (1, line.len() - 1)); + } +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/lib.rs b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/lib.rs new file mode 100644 index 0000000..99b74ad --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/lib.rs @@ -0,0 +1,156 @@ +use crate::frecency::FrecencyTracker; +use crate::fuzzy::FuzzyOptions; +use crate::lsp_item::LspItem; +use lazy_static::lazy_static; +use mlua::prelude::*; +use regex::Regex; +use std::collections::{HashMap, HashSet}; +use std::sync::RwLock; + +mod frecency; +mod fuzzy; +mod keyword; +mod lsp_item; + +lazy_static! { + static ref REGEX: Regex = Regex::new(r"\p{L}[\p{L}0-9_\\-]{2,}").unwrap(); + static ref FRECENCY: RwLock<Option<FrecencyTracker>> = RwLock::new(None); + static ref HAYSTACKS_BY_PROVIDER: RwLock<HashMap<String, Vec<LspItem>>> = + RwLock::new(HashMap::new()); +} + +pub fn init_db(_: &Lua, (db_path, use_unsafe_no_lock): (String, bool)) -> LuaResult<bool> { + let mut frecency = FRECENCY.write().map_err(|_| { + mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) + })?; + if frecency.is_some() { + return Ok(false); + } + *frecency = Some(FrecencyTracker::new(&db_path, use_unsafe_no_lock)?); + Ok(true) +} + +pub fn destroy_db(_: &Lua, _: ()) -> LuaResult<bool> { + let frecency = FRECENCY.write().map_err(|_| { + mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) + })?; + drop(frecency); + + let mut frecency = FRECENCY.write().map_err(|_| { + mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) + })?; + *frecency = None; + + Ok(true) +} + +pub fn access(_: &Lua, item: LspItem) -> LuaResult<bool> { + let mut frecency_handle = FRECENCY.write().map_err(|_| { + mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) + })?; + let frecency = frecency_handle.as_mut().ok_or_else(|| { + mlua::Error::RuntimeError("Attempted to use frencecy before initialization".to_string()) + })?; + frecency.access(&item)?; + Ok(true) +} + +pub fn set_provider_items( + _: &Lua, + (provider_id, items): (String, Vec<LspItem>), +) -> LuaResult<bool> { + let mut items_by_provider = HAYSTACKS_BY_PROVIDER.write().map_err(|_| { + mlua::Error::RuntimeError("Failed to acquire lock for items by provider".to_string()) + })?; + items_by_provider.insert(provider_id, items); + Ok(true) +} + +pub fn fuzzy( + _lua: &Lua, + (line, cursor_col, provider_id, opts): (String, usize, String, FuzzyOptions), +) -> LuaResult<(Vec<i32>, Vec<u32>)> { + let mut frecency_handle = FRECENCY.write().map_err(|_| { + mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) + })?; + let frecency = frecency_handle.as_mut().ok_or_else(|| { + mlua::Error::RuntimeError("Attempted to use frencecy before initialization".to_string()) + })?; + + let haystacks_by_provider = HAYSTACKS_BY_PROVIDER.read().map_err(|_| { + mlua::Error::RuntimeError("Failed to acquire lock for items by provider".to_string()) + })?; + let haystack = haystacks_by_provider.get(&provider_id).ok_or_else(|| { + mlua::Error::RuntimeError(format!( + "Attempted to fuzzy match for provider {} before setting the provider's items", + provider_id + )) + })?; + + Ok(fuzzy::fuzzy(&line, cursor_col, haystack, frecency, opts)) +} + +pub fn fuzzy_matched_indices( + _lua: &Lua, + (line, cursor_col, haystack, match_suffix): (String, usize, Vec<String>, bool), +) -> LuaResult<Vec<Vec<usize>>> { + Ok(fuzzy::fuzzy_matched_indices( + &line, + cursor_col, + &haystack, + match_suffix, + )) +} + +pub fn get_keyword_range( + _lua: &Lua, + (line, col, match_suffix): (String, usize, bool), +) -> LuaResult<(usize, usize)> { + Ok(keyword::get_keyword_range(&line, col, match_suffix)) +} + +pub fn guess_edit_range( + _lua: &Lua, + (item, line, cursor_col, match_suffix): (LspItem, String, usize, bool), +) -> LuaResult<(usize, usize)> { + // TODO: take the max range from insert_text and filter_text + Ok(keyword::guess_keyword_range_from_item( + item.insert_text.as_ref().unwrap_or(&item.label), + &line, + cursor_col, + match_suffix, + )) +} + +pub fn get_words(_: &Lua, text: String) -> LuaResult<Vec<String>> { + Ok(REGEX + .find_iter(&text) + .map(|m| m.as_str().to_string()) + .filter(|s| s.len() < 512) + .collect::<HashSet<String>>() + .into_iter() + .collect()) +} + +// NOTE: skip_memory_check greatly improves performance +// https://github.com/mlua-rs/mlua/issues/318 +#[mlua::lua_module(skip_memory_check)] +fn blink_cmp_fuzzy(lua: &Lua) -> LuaResult<LuaTable> { + let exports = lua.create_table()?; + exports.set("init_db", lua.create_function(init_db)?)?; + exports.set("destroy_db", lua.create_function(destroy_db)?)?; + exports.set("access", lua.create_function(access)?)?; + exports.set( + "set_provider_items", + lua.create_function(set_provider_items)?, + )?; + exports.set("fuzzy", lua.create_function(fuzzy)?)?; + exports.set( + "fuzzy_matched_indices", + lua.create_function(fuzzy_matched_indices)?, + )?; + exports.set("get_keyword_range", lua.create_function(get_keyword_range)?)?; + exports.set("guess_edit_range", lua.create_function(guess_edit_range)?)?; + exports.set("get_words", lua.create_function(get_words)?)?; + Ok(exports) +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/lsp_item.rs b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/lsp_item.rs new file mode 100644 index 0000000..a24669e --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/lsp_item.rs @@ -0,0 +1,46 @@ +use mlua::prelude::*; + +#[derive(Debug)] +pub struct LspItem { + pub label: String, + pub filter_text: Option<String>, + pub sort_text: Option<String>, + pub insert_text: Option<String>, + pub kind: u32, + pub score_offset: i32, + pub source_id: String, +} + +impl FromLua for LspItem { + fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> { + if let Some(tab) = value.as_table() { + let label = tab.get("label").unwrap_or_default(); + let filter_text = tab.get("filterText").ok(); + let sort_text = tab.get("sortText").ok(); + let insert_text = tab + .get::<LuaTable>("textEdit") + .and_then(|text_edit| text_edit.get("newText")) + .ok() + .or_else(|| tab.get("insertText").ok()); + let kind = tab.get("kind").unwrap_or_default(); + let score_offset = tab.get("score_offset").unwrap_or(0); + let source_id = tab.get("source_id").unwrap_or_default(); + + Ok(LspItem { + label, + filter_text, + sort_text, + insert_text, + kind, + score_offset, + source_id, + }) + } else { + Err(mlua::Error::FromLuaConversionError { + from: "LuaValue", + to: "LspItem".to_string(), + message: None, + }) + } + } +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/rust.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/rust.lua new file mode 100644 index 0000000..e2374cf --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/rust.lua @@ -0,0 +1,20 @@ +--- @return string +local function get_lib_extension() + if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then return '.dylib' end + if jit.os:lower() == 'windows' then return '.dll' end + return '.so' +end + +-- search for the lib in the /target/release directory with and without the lib prefix +-- since MSVC doesn't include the prefix +package.cpath = package.cpath + .. ';' + .. debug.getinfo(1).source:match('@?(.*/)') + .. '../../../../target/release/lib?' + .. get_lib_extension() + .. ';' + .. debug.getinfo(1).source:match('@?(.*/)') + .. '../../../../target/release/?' + .. get_lib_extension() + +return require('blink_cmp_fuzzy') diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/sort.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/sort.lua new file mode 100644 index 0000000..ec32ac3 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/fuzzy/sort.lua @@ -0,0 +1,48 @@ +local sort = {} + +--- @param list blink.cmp.CompletionItem[] +--- @param funcs ("label" | "sort_text" | "kind" | "score" | blink.cmp.SortFunction)[] +--- @return blink.cmp.CompletionItem[] +function sort.sort(list, funcs) + local sorting_funcs = vim.tbl_map( + function(name_or_func) return type(name_or_func) == 'string' and sort[name_or_func] or name_or_func end, + funcs + ) + table.sort(list, function(a, b) + for _, sorting_func in ipairs(sorting_funcs) do + local result = sorting_func(a, b) + if result ~= nil then return result end + end + end) + return list +end + +function sort.score(a, b) + if a.score == b.score then return end + return a.score > b.score +end + +function sort.kind(a, b) + if a.kind == b.kind then return end + return a.kind < b.kind +end + +function sort.sort_text(a, b) + if a.sortText == b.sortText or a.sortText == nil or b.sortText == nil then return end + return a.sortText < b.sortText +end + +function sort.label(a, b) + local _, entry1_under = a.label:find('^_+') + local _, entry2_under = b.label:find('^_+') + entry1_under = entry1_under or 0 + entry2_under = entry2_under or 0 + if entry1_under > entry2_under then + return false + elseif entry1_under < entry2_under then + return true + end + return a.label < b.label +end + +return sort diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/health.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/health.lua new file mode 100644 index 0000000..d8ff4a1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/health.lua @@ -0,0 +1,35 @@ +local health = {} + +function health.check() + vim.health.start('blink.cmp healthcheck') + + local required_executables = { 'curl', 'git' } + for _, executable in ipairs(required_executables) do + if vim.fn.executable(executable) == 0 then + vim.health.error(executable .. ' is not installed') + else + vim.health.ok(executable .. ' is installed') + end + end + + -- check if os is supported + local download_system = require('blink.cmp.fuzzy.download.system') + local system_triple = download_system.get_triple_sync() + if system_triple then + vim.health.ok('Your system is supported by pre-built binaries (' .. system_triple .. ')') + else + vim.health.warn( + 'Your system is not supported by pre-built binaries. You must run cargo build --release via your package manager with rust nightly. See the README for more info.' + ) + end + + local download_files = require('blink.cmp.fuzzy.download.files') + local lib_path_without_prefix = string.gsub(download_files.lib_path, 'libblink_cmp_fuzzy', 'blink_cmp_fuzzy') + if vim.uv.fs_stat(download_files.lib_path) or vim.uv.fs_stat(lib_path_without_prefix) then + vim.health.ok('blink_cmp_fuzzy lib is downloaded/built') + else + vim.health.warn('blink_cmp_fuzzy lib is not downloaded/built') + end +end + +return health diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/highlights.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/highlights.lua new file mode 100644 index 0000000..97db66f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/highlights.lua @@ -0,0 +1,47 @@ +local highlights = {} + +function highlights.setup() + local use_nvim_cmp = require('blink.cmp.config').appearance.use_nvim_cmp_as_default + + --- @param hl_group string Highlight group name, e.g. 'ErrorMsg' + --- @param opts vim.api.keyset.highlight Highlight definition map + --- @return nil + local set_hl = function(hl_group, opts) + opts.default = true -- Prevents overriding existing definitions + vim.api.nvim_set_hl(0, hl_group, opts) + end + + if use_nvim_cmp then + set_hl('BlinkCmpLabel', { link = 'CmpItemAbbr' }) + set_hl('BlinkCmpLabelMatch', { link = 'CmpItemAbbrMatch' }) + end + + set_hl('BlinkCmpLabelDeprecated', { link = use_nvim_cmp and 'CmpItemAbbrDeprecated' or 'NonText' }) + set_hl('BlinkCmpLabelDetail', { link = use_nvim_cmp and 'CmpItemMenu' or 'NonText' }) + set_hl('BlinkCmpLabelDescription', { link = use_nvim_cmp and 'CmpItemMenu' or 'NonText' }) + set_hl('BlinkCmpKind', { link = use_nvim_cmp and 'CmpItemKind' or 'Special' }) + set_hl('BlinkCmpSource', { link = use_nvim_cmp and 'CmpItemMenu' or 'NonText' }) + for _, kind in ipairs(require('blink.cmp.types').CompletionItemKind) do + set_hl('BlinkCmpKind' .. kind, { link = use_nvim_cmp and 'CmpItemKind' .. kind or 'BlinkCmpKind' }) + end + + set_hl('BlinkCmpScrollBarThumb', { link = 'PmenuThumb' }) + set_hl('BlinkCmpScrollBarGutter', { link = 'PmenuSbar' }) + + set_hl('BlinkCmpGhostText', { link = use_nvim_cmp and 'CmpGhostText' or 'NonText' }) + + set_hl('BlinkCmpMenu', { link = 'Pmenu' }) + set_hl('BlinkCmpMenuBorder', { link = 'Pmenu' }) + set_hl('BlinkCmpMenuSelection', { link = 'PmenuSel' }) + + set_hl('BlinkCmpDoc', { link = 'NormalFloat' }) + set_hl('BlinkCmpDocBorder', { link = 'NormalFloat' }) + set_hl('BlinkCmpDocSeparator', { link = 'NormalFloat' }) + set_hl('BlinkCmpDocCursorLine', { link = 'Visual' }) + + set_hl('BlinkCmpSignatureHelp', { link = 'NormalFloat' }) + set_hl('BlinkCmpSignatureHelpBorder', { link = 'NormalFloat' }) + set_hl('BlinkCmpSignatureHelpActiveParameter', { link = 'LspSignatureActiveParameter' }) +end + +return highlights diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/init.lua new file mode 100644 index 0000000..deb9116 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/init.lua @@ -0,0 +1,235 @@ +--- @class blink.cmp.API +local cmp = {} + +local has_setup = false +--- Initializes blink.cmp with the given configuration and initiates the download +--- for the fuzzy matcher's prebuilt binaries, if necessary +--- @param opts? blink.cmp.Config +function cmp.setup(opts) + if has_setup then return end + has_setup = true + + opts = opts or {} + + if vim.fn.has('nvim-0.10') == 0 then + vim.notify('blink.cmp requires nvim 0.10 and newer', vim.log.levels.ERROR, { title = 'blink.cmp' }) + return + end + + local config = require('blink.cmp.config') + config.merge_with(opts) + + require('blink.cmp.fuzzy.download').ensure_downloaded(function(err) + if err then vim.notify(err, vim.log.levels.ERROR) end + + -- setup highlights, keymap, completion and signature help + require('blink.cmp.highlights').setup() + require('blink.cmp.keymap').setup() + require('blink.cmp.completion').setup() + if config.signature.enabled then require('blink.cmp.signature').setup() end + end) +end + +------- Public API ------- + +--- Checks if the completion menu is currently visible +--- @return boolean +function cmp.is_visible() + return require('blink.cmp.completion.windows.menu').win:is_open() + or require('blink.cmp.completion.windows.ghost_text').is_open() +end + +--- Show the completion window +--- @params opts? { providers?: string[], callback?: fun() } +function cmp.show(opts) + opts = opts or {} + + -- TODO: when passed a list of providers, we should check if we're already showing the menu + -- with that list of providers + if require('blink.cmp.completion.windows.menu').win:is_open() and not (opts and opts.providers) then return end + + vim.schedule(function() + require('blink.cmp.completion.windows.menu').auto_show = true + + -- HACK: because blink is event based, we don't have an easy way to know when the "show" + -- event completes. So we wait for the list to trigger the show event and check if we're + -- still in the same context + local context + if opts.callback then + vim.api.nvim_create_autocmd('User', { + pattern = 'BlinkCmpShow', + callback = function(event) + if context ~= nil and event.data.context.id == context.id then opts.callback() end + end, + once = true, + }) + end + + context = require('blink.cmp.completion.trigger').show({ + force = true, + providers = opts and opts.providers, + trigger_kind = 'manual', + }) + end) + return true +end + +--- Hide the completion window +--- @params opts? { callback?: fun() } +function cmp.hide(opts) + if not cmp.is_visible() then return end + + vim.schedule(function() + require('blink.cmp.completion.trigger').hide() + if opts and opts.callback then opts.callback() end + end) + return true +end + +--- Cancel the current completion, undoing the preview from auto_insert +--- @params opts? { callback?: fun() } +function cmp.cancel(opts) + if not cmp.is_visible() then return end + vim.schedule(function() + require('blink.cmp.completion.list').undo_preview() + require('blink.cmp.completion.trigger').hide() + if opts and opts.callback then opts.callback() end + end) + return true +end + +--- Accept the current completion item +--- @param opts? blink.cmp.CompletionListAcceptOpts +function cmp.accept(opts) + opts = opts or {} + if not cmp.is_visible() then return end + + local completion_list = require('blink.cmp.completion.list') + local item = opts.index ~= nil and completion_list.items[opts.index] or completion_list.get_selected_item() + if item == nil then return end + + vim.schedule(function() completion_list.accept(opts) end) + return true +end + +--- Select the first completion item, if there's no selection, and accept +--- @param opts? blink.cmp.CompletionListSelectAndAcceptOpts +function cmp.select_and_accept(opts) + if not cmp.is_visible() then return end + + local completion_list = require('blink.cmp.completion.list') + vim.schedule( + function() + completion_list.accept({ + index = completion_list.selected_item_idx or 1, + callback = opts and opts.callback, + }) + end + ) + return true +end + +--- Select the previous completion item +--- @param opts? blink.cmp.CompletionListSelectOpts +function cmp.select_prev(opts) + if not cmp.is_visible() then return end + vim.schedule(function() require('blink.cmp.completion.list').select_prev(opts) end) + return true +end + +--- Select the next completion item +--- @param opts? blink.cmp.CompletionListSelectOpts +function cmp.select_next(opts) + if not cmp.is_visible() then return end + vim.schedule(function() require('blink.cmp.completion.list').select_next(opts) end) + return true +end + +--- Gets the currently selected completion item +function cmp.get_selected_item() return require('blink.cmp.completion.list').get_selected_item() end + +--- Show the documentation window +function cmp.show_documentation() + local menu = require('blink.cmp.completion.windows.menu') + local documentation = require('blink.cmp.completion.windows.documentation') + if documentation.win:is_open() or not menu.win:is_open() then return end + + local context = require('blink.cmp.completion.list').context + local item = require('blink.cmp.completion.list').get_selected_item() + if not item or not context then return end + + vim.schedule(function() documentation.show_item(context, item) end) + return true +end + +--- Hide the documentation window +function cmp.hide_documentation() + local documentation = require('blink.cmp.completion.windows.documentation') + if not documentation.win:is_open() then return end + + vim.schedule(function() documentation.close() end) + return true +end + +--- Scroll the documentation window up +--- @param count? number +function cmp.scroll_documentation_up(count) + local documentation = require('blink.cmp.completion.windows.documentation') + if not documentation.win:is_open() then return end + + vim.schedule(function() documentation.scroll_up(count or 4) end) + return true +end + +--- Scroll the documentation window down +--- @param count? number +function cmp.scroll_documentation_down(count) + local documentation = require('blink.cmp.completion.windows.documentation') + if not documentation.win:is_open() then return end + + vim.schedule(function() documentation.scroll_down(count or 4) end) + return true +end + +--- Check if a snippet is active, optionally filtering by direction +--- @param filter? { direction?: number } +function cmp.snippet_active(filter) return require('blink.cmp.config').snippets.active(filter) end + +--- Move the cursor forward to the next snippet placeholder +function cmp.snippet_forward() + local snippets = require('blink.cmp.config').snippets + if not snippets.active({ direction = 1 }) then return end + vim.schedule(function() snippets.jump(1) end) + return true +end + +--- Move the cursor backward to the previous snippet placeholder +function cmp.snippet_backward() + local snippets = require('blink.cmp.config').snippets + if not snippets.active({ direction = -1 }) then return end + vim.schedule(function() snippets.jump(-1) end) + return true +end + +--- Tells the sources to reload a specific provider or all providers (when nil) +--- @param provider? string +function cmp.reload(provider) require('blink.cmp.sources.lib').reload(provider) end + +--- Gets the capabilities to pass to the LSP client +--- @param override? lsp.ClientCapabilities Overrides blink.cmp's default capabilities +--- @param include_nvim_defaults? boolean Whether to include nvim's default capabilities +function cmp.get_lsp_capabilities(override, include_nvim_defaults) + return require('blink.cmp.sources.lib').get_lsp_capabilities(override, include_nvim_defaults) +end + +--- Add a new source provider at runtime +--- @param id string +--- @param provider_config blink.cmp.SourceProviderConfig +function cmp.add_provider(id, provider_config) + local config = require('blink.cmp.config') + assert(config.sources.providers[id] == nil, 'Provider with id ' .. id .. ' already exists') + require('blink.cmp.config.sources').validate_provider(id, provider_config) + config.sources.providers[id] = provider_config +end + +return cmp diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/apply.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/apply.lua new file mode 100644 index 0000000..b10bd0f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/apply.lua @@ -0,0 +1,132 @@ +local apply = {} + +local snippet_commands = { 'snippet_forward', 'snippet_backward' } + +--- Applies the keymaps to the current buffer +--- @param keys_to_commands table<string, blink.cmp.KeymapCommand[]> +function apply.keymap_to_current_buffer(keys_to_commands) + -- skip if we've already applied the keymaps + for _, mapping in ipairs(vim.api.nvim_buf_get_keymap(0, 'i')) do + if mapping.desc == 'blink.cmp' then return end + end + + -- insert mode: uses both snippet and insert commands + for key, commands in pairs(keys_to_commands) do + if #commands == 0 then goto continue end + + local fallback = require('blink.cmp.keymap.fallback').wrap('i', key) + apply.set('i', key, function() + if not require('blink.cmp.config').enabled() then return fallback() end + + for _, command in ipairs(commands) do + -- special case for fallback + if command == 'fallback' then + return fallback() + + -- run user defined functions + elseif type(command) == 'function' then + if command(require('blink.cmp')) then return end + + -- otherwise, run the built-in command + elseif require('blink.cmp')[command]() then + return + end + end + end) + + ::continue:: + end + + -- snippet mode: uses only snippet commands + for key, commands in pairs(keys_to_commands) do + local has_snippet_command = false + for _, command in ipairs(commands) do + if vim.tbl_contains(snippet_commands, command) or type(command) == 'function' then has_snippet_command = true end + end + if not has_snippet_command or #commands == 0 then goto continue end + + local fallback = require('blink.cmp.keymap.fallback').wrap('s', key) + apply.set('s', key, function() + if not require('blink.cmp.config').enabled() then return fallback() end + + for _, command in ipairs(keys_to_commands[key] or {}) do + -- special case for fallback + if command == 'fallback' then + return fallback() + + -- run user defined functions + elseif type(command) == 'function' then + if command(require('blink.cmp')) then return end + + -- only run snippet commands + elseif vim.tbl_contains(snippet_commands, command) then + local did_run = require('blink.cmp')[command]() + if did_run then return end + end + end + end) + + ::continue:: + end +end + +function apply.cmdline_keymaps(keys_to_commands) + -- cmdline mode: uses only insert commands + for key, commands in pairs(keys_to_commands) do + local has_insert_command = false + for _, command in ipairs(commands) do + has_insert_command = has_insert_command or not vim.tbl_contains(snippet_commands, command) + end + if not has_insert_command or #commands == 0 then goto continue end + + local fallback = require('blink.cmp.keymap.fallback').wrap('c', key) + apply.set('c', key, function() + for _, command in ipairs(commands) do + -- special case for fallback + if command == 'fallback' then + return fallback() + + -- run user defined functions + elseif type(command) == 'function' then + if command(require('blink.cmp')) then return end + + -- otherwise, run the built-in command + elseif not vim.tbl_contains(snippet_commands, command) then + local did_run = require('blink.cmp')[command]() + if did_run then return end + end + end + end) + + ::continue:: + end +end + +--- @param mode string +--- @param key string +--- @param callback fun(): string | nil +function apply.set(mode, key, callback) + if mode == 'c' then + vim.api.nvim_set_keymap(mode, key, '', { + callback = callback, + expr = true, + -- silent must be false for fallback to work + -- otherwise, you get very weird behavior + silent = false, + noremap = true, + replace_keycodes = false, + desc = 'blink.cmp', + }) + else + vim.api.nvim_buf_set_keymap(0, mode, key, '', { + callback = callback, + expr = true, + silent = true, + noremap = true, + replace_keycodes = false, + desc = 'blink.cmp', + }) + end +end + +return apply diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/fallback.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/fallback.lua new file mode 100644 index 0000000..a73d69e --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/fallback.lua @@ -0,0 +1,91 @@ +local fallback = {} + +--- Add missing types. Remove when fixed upstream +---@class blink.cmp.Fallback : vim.api.keyset.keymap +---@field lhs string +---@field mode string +---@field rhs? string +---@field lhsraw? string +---@field buffer? number + +--- Gets the non blink.cmp global keymap for the given mode and key +--- @param mode string +--- @param key string +--- @return blink.cmp.Fallback | nil +function fallback.get_non_blink_global_mapping_for_key(mode, key) + local normalized_key = vim.api.nvim_replace_termcodes(key, true, true, true) + + -- get global mappings + local mappings = vim.api.nvim_get_keymap(mode) + + for _, mapping in ipairs(mappings) do + --- @cast mapping blink.cmp.Fallback + local mapping_key = vim.api.nvim_replace_termcodes(mapping.lhs, true, true, true) + if mapping_key == normalized_key and mapping.desc ~= 'blink.cmp' then return mapping end + end +end + +--- Gets the non blink.cmp buffer keymap for the given mode and key +--- @param mode string +--- @param key string +--- @return blink.cmp.Fallback? +function fallback.get_non_blink_buffer_mapping_for_key(mode, key) + local normalized_key = vim.api.nvim_replace_termcodes(key, true, true, true) + + local buffer_mappings = vim.api.nvim_buf_get_keymap(0, mode) + + for _, mapping in ipairs(buffer_mappings) do + --- @cast mapping blink.cmp.Fallback + local mapping_key = vim.api.nvim_replace_termcodes(mapping.lhs, true, true, true) + if mapping_key == normalized_key and mapping.desc ~= 'blink.cmp' then return mapping end + end +end + +--- Returns a function that will run the first non blink.cmp keymap for the given mode and key +--- @param mode string +--- @param key string +--- @return fun(): string? +function fallback.wrap(mode, key) + -- In default mode, there can't be multiple mappings on a single key for buffer local mappings + -- In cmdline mode, there can't be multiple mappings on a single key for global mappings + local buffer_mapping = mode ~= 'c' and fallback.get_non_blink_buffer_mapping_for_key(mode, key) + or fallback.get_non_blink_global_mapping_for_key(mode, key) + return function() + local mapping = buffer_mapping or fallback.get_non_blink_global_mapping_for_key(mode, key) + if mapping then return fallback.run_non_blink_keymap(mapping, key) end + return vim.api.nvim_replace_termcodes(key, true, true, true) + end +end + +--- Runs the first non blink.cmp keymap for the given mode and key +--- @param mapping blink.cmp.Fallback +--- @param key string +--- @return string | nil +function fallback.run_non_blink_keymap(mapping, key) + -- TODO: there's likely many edge cases here. the nvim-cmp version is lacking documentation + -- and is quite complex. we should look to see if we can simplify their logic + -- https://github.com/hrsh7th/nvim-cmp/blob/ae644feb7b67bf1ce4260c231d1d4300b19c6f30/lua/cmp/utils/keymap.lua + if type(mapping.callback) == 'function' then + -- with expr = true, which we use, we can't modify the buffer without scheduling + -- so if the keymap does not use expr, we must schedule it + if mapping.expr ~= 1 then + vim.schedule(mapping.callback) + return + end + + local expr = mapping.callback() + if type(expr) == 'string' and mapping.replace_keycodes == 1 then + expr = vim.api.nvim_replace_termcodes(expr, true, true, true) + end + return expr + elseif mapping.rhs then + local rhs = vim.api.nvim_replace_termcodes(mapping.rhs, true, true, true) + if mapping.expr == 1 then rhs = vim.api.nvim_eval(rhs) end + return rhs + end + + -- pass the key along as usual + return vim.api.nvim_replace_termcodes(key, true, true, true) +end + +return fallback diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/init.lua new file mode 100644 index 0000000..a5e7009 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/init.lua @@ -0,0 +1,74 @@ +local keymap = {} + +--- Lowercases all keys in the mappings table +--- @param existing_mappings table<string, blink.cmp.KeymapCommand[]> +--- @param new_mappings table<string, blink.cmp.KeymapCommand[]> +--- @return table<string, blink.cmp.KeymapCommand[]> +function keymap.merge_mappings(existing_mappings, new_mappings) + local merged_mappings = vim.deepcopy(existing_mappings) + for new_key, new_mapping in pairs(new_mappings) do + -- normalize the keys and replace, since naively merging would not handle <C-a> == <c-a> + for existing_key, _ in pairs(existing_mappings) do + if + vim.api.nvim_replace_termcodes(existing_key, true, true, true) + == vim.api.nvim_replace_termcodes(new_key, true, true, true) + then + merged_mappings[existing_key] = new_mapping + goto continue + end + end + + -- key wasn't found, add it as per usual + merged_mappings[new_key] = new_mapping + + ::continue:: + end + return merged_mappings +end + +---@param keymap_config blink.cmp.BaseKeymapConfig +function keymap.get_mappings(keymap_config) + local mappings = vim.deepcopy(keymap_config) + + -- Handle preset + if mappings.preset then + local preset_keymap = require('blink.cmp.keymap.presets').get(mappings.preset) + + -- Remove 'preset' key from opts to prevent it from being treated as a keymap + mappings.preset = nil + + -- Merge the preset keymap with the user-defined keymaps + -- User-defined keymaps overwrite the preset keymaps + mappings = keymap.merge_mappings(preset_keymap, mappings) + end + return mappings +end + +function keymap.setup() + local config = require('blink.cmp.config') + local mappings = keymap.get_mappings(config.keymap) + -- We set on the buffer directly to avoid buffer-local keymaps (such as from autopairs) + -- from overriding our mappings. We also use InsertEnter to avoid conflicts with keymaps + -- applied on other autocmds, such as LspAttach used by nvim-lspconfig and most configs + vim.api.nvim_create_autocmd('InsertEnter', { + callback = function() + if not require('blink.cmp.config').enabled() then return end + require('blink.cmp.keymap.apply').keymap_to_current_buffer(mappings) + end, + }) + + -- This is not called when the plugin loads since it first checks if the binary is + -- installed. As a result, when lazy-loaded on InsertEnter, the event may be missed + if vim.api.nvim_get_mode().mode == 'i' and require('blink.cmp.config').enabled() then + require('blink.cmp.keymap.apply').keymap_to_current_buffer(mappings) + end + + -- Apply cmdline keymaps since they're global, if any sources are defined + local cmdline_sources = require('blink.cmp.config').sources.cmdline + if type(cmdline_sources) ~= 'table' or #cmdline_sources > 0 then + local cmdline_mappings = keymap.get_mappings(config.keymap.cmdline or config.keymap) + require('blink.cmp.keymap.apply').cmdline_keymaps(cmdline_mappings) + end +end + +return keymap diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/presets.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/presets.lua new file mode 100644 index 0000000..1e7de16 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/keymap/presets.lua @@ -0,0 +1,72 @@ +local presets = { + none = {}, + + default = { + ['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, + ['<C-e>'] = { 'cancel', 'fallback' }, + ['<C-y>'] = { 'select_and_accept' }, + + ['<C-p>'] = { 'select_prev', 'fallback' }, + ['<C-n>'] = { 'select_next', 'fallback' }, + + ['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, + ['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, + + ['<Tab>'] = { 'snippet_forward', 'fallback' }, + ['<S-Tab>'] = { 'snippet_backward', 'fallback' }, + }, + + ['super-tab'] = { + ['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, + ['<C-e>'] = { 'cancel', 'fallback' }, + + ['<Tab>'] = { + function(cmp) + if cmp.snippet_active() then + return cmp.accept() + else + return cmp.select_and_accept() + end + end, + 'snippet_forward', + 'fallback', + }, + ['<S-Tab>'] = { 'snippet_backward', 'fallback' }, + + ['<Up>'] = { 'select_prev', 'fallback' }, + ['<Down>'] = { 'select_next', 'fallback' }, + ['<C-p>'] = { 'select_prev', 'fallback' }, + ['<C-n>'] = { 'select_next', 'fallback' }, + + ['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, + ['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, + }, + + enter = { + ['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' }, + ['<C-e>'] = { 'cancel', 'fallback' }, + ['<CR>'] = { 'accept', 'fallback' }, + + ['<Tab>'] = { 'snippet_forward', 'fallback' }, + ['<S-Tab>'] = { 'snippet_backward', 'fallback' }, + + ['<Up>'] = { 'select_prev', 'fallback' }, + ['<Down>'] = { 'select_next', 'fallback' }, + ['<C-p>'] = { 'select_prev', 'fallback' }, + ['<C-n>'] = { 'select_next', 'fallback' }, + + ['<C-b>'] = { 'scroll_documentation_up', 'fallback' }, + ['<C-f>'] = { 'scroll_documentation_down', 'fallback' }, + }, +} + +--- Gets the preset keymap for the given preset name +--- @param name string +--- @return table<string, blink.cmp.KeymapCommand[]> +function presets.get(name) + local preset = presets[name] + if preset == nil then error('Invalid blink.cmp keymap preset: ' .. name) end + return preset +end + +return presets diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/async.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/async.lua new file mode 100644 index 0000000..b9c39ac --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/async.lua @@ -0,0 +1,217 @@ +--- Allows chaining of async operations without callback hell +--- +--- @class blink.cmp.Task +--- @field status blink.cmp.TaskStatus +--- @field result any | nil +--- @field error any | nil +--- @field new fun(fn: fun(resolve: fun(result: any), reject: fun(err: any))): blink.cmp.Task +--- +--- @field cancel fun(self: blink.cmp.Task) +--- @field map fun(self: blink.cmp.Task, fn: fun(result: any): blink.cmp.Task | any): blink.cmp.Task +--- @field catch fun(self: blink.cmp.Task, fn: fun(err: any): blink.cmp.Task | any): blink.cmp.Task +--- +--- @field on_completion fun(self: blink.cmp.Task, cb: fun(result: any)) +--- @field on_failure fun(self: blink.cmp.Task, cb: fun(err: any)) +--- @field on_cancel fun(self: blink.cmp.Task, cb: fun()) +--- @field _completion_cbs function[] +--- @field _failure_cbs function[] +--- @field _cancel_cbs function[] +--- @field _cancel? fun() +local task = { + __task = true, +} + +---@enum blink.cmp.TaskStatus +local STATUS = { + RUNNING = 1, + COMPLETED = 2, + FAILED = 3, + CANCELLED = 4, +} + +function task.new(fn) + local self = setmetatable({}, { __index = task }) + self.status = STATUS.RUNNING + self._completion_cbs = {} + self._failure_cbs = {} + self._cancel_cbs = {} + self.result = nil + self.error = nil + + local resolve = function(result) + if self.status ~= STATUS.RUNNING then return end + + self.status = STATUS.COMPLETED + self.result = result + + for _, cb in ipairs(self._completion_cbs) do + cb(result) + end + end + + local reject = function(err) + if self.status ~= STATUS.RUNNING then return end + + self.status = STATUS.FAILED + self.error = err + + for _, cb in ipairs(self._failure_cbs) do + cb(err) + end + end + + local success, cancel_fn_or_err = pcall(function() return fn(resolve, reject) end) + + if not success then + reject(cancel_fn_or_err) + elseif type(cancel_fn_or_err) == 'function' then + self._cancel = cancel_fn_or_err + end + + return self +end + +function task:cancel() + if self.status ~= STATUS.RUNNING then return end + self.status = STATUS.CANCELLED + + if self._cancel ~= nil then self._cancel() end + for _, cb in ipairs(self._cancel_cbs) do + cb() + end +end + +--- mappings + +function task:map(fn) + local chained_task + chained_task = task.new(function(resolve, reject) + self:on_completion(function(result) + local success, mapped_result = pcall(fn, result) + if not success then + reject(mapped_result) + return + end + + if type(mapped_result) == 'table' and mapped_result.__task then + mapped_result:on_completion(resolve) + mapped_result:on_failure(reject) + mapped_result:on_cancel(function() chained_task:cancel() end) + return + end + resolve(mapped_result) + end) + self:on_failure(reject) + self:on_cancel(function() chained_task:cancel() end) + return function() chained_task:cancel() end + end) + return chained_task +end + +function task:catch(fn) + local chained_task + chained_task = task.new(function(resolve, reject) + self:on_completion(resolve) + self:on_failure(function(err) + local success, mapped_err = pcall(fn, err) + if not success then + reject(mapped_err) + return + end + + if type(mapped_err) == 'table' and mapped_err.__task then + mapped_err:on_completion(resolve) + mapped_err:on_failure(reject) + mapped_err:on_cancel(function() chained_task:cancel() end) + return + end + resolve(mapped_err) + end) + self:on_cancel(function() chained_task:cancel() end) + return function() chained_task:cancel() end + end) + return chained_task +end + +--- events + +function task:on_completion(cb) + if self.status == STATUS.COMPLETED then + cb(self.result) + elseif self.status == STATUS.RUNNING then + table.insert(self._completion_cbs, cb) + end + return self +end + +function task:on_failure(cb) + if self.status == STATUS.FAILED then + cb(self.error) + elseif self.status == STATUS.RUNNING then + table.insert(self._failure_cbs, cb) + end + return self +end + +function task:on_cancel(cb) + if self.status == STATUS.CANCELLED then + cb() + elseif self.status == STATUS.RUNNING then + table.insert(self._cancel_cbs, cb) + end + return self +end + +--- utils + +function task.await_all(tasks) + if #tasks == 0 then + return task.new(function(resolve) resolve({}) end) + end + + local all_task + all_task = task.new(function(resolve, reject) + local results = {} + local has_resolved = {} + + local function resolve_if_completed() + -- we can't check #results directly because a table like + -- { [2] = { ... } } has a length of 2 + for i = 1, #tasks do + if has_resolved[i] == nil then return end + end + resolve(results) + end + + for idx, task in ipairs(tasks) do + task:on_completion(function(result) + results[idx] = result + has_resolved[idx] = true + resolve_if_completed() + end) + task:on_failure(function(err) + reject(err) + for _, task in ipairs(tasks) do + task:cancel() + end + end) + task:on_cancel(function() + for _, sub_task in ipairs(tasks) do + sub_task:cancel() + end + if all_task == nil then + vim.schedule(function() all_task:cancel() end) + else + all_task:cancel() + end + end) + end + end) + return all_task +end + +function task.empty() + return task.new(function(resolve) resolve() end) +end + +return { task = task, STATUS = STATUS } diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/buffer_events.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/buffer_events.lua new file mode 100644 index 0000000..dcca8b8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/buffer_events.lua @@ -0,0 +1,148 @@ +--- Exposes three events (cursor moved, char added, insert leave) for triggers to use. +--- Notably, when "char added" is fired, the "cursor moved" event will not be fired. +--- Unlike in regular neovim, ctrl + c and buffer switching will trigger "insert leave" + +--- @class blink.cmp.BufferEvents +--- @field has_context fun(): boolean +--- @field show_in_snippet boolean +--- @field ignore_next_text_changed boolean +--- @field ignore_next_cursor_moved boolean +--- +--- @field new fun(opts: blink.cmp.BufferEventsOptions): blink.cmp.BufferEvents +--- @field listen fun(self: blink.cmp.BufferEvents, opts: blink.cmp.BufferEventsListener) +--- @field suppress_events_for_callback fun(self: blink.cmp.BufferEvents, cb: fun()) + +--- @class blink.cmp.BufferEventsOptions +--- @field has_context fun(): boolean +--- @field show_in_snippet boolean + +--- @class blink.cmp.BufferEventsListener +--- @field on_char_added fun(char: string, is_ignored: boolean) +--- @field on_cursor_moved fun(event: 'CursorMoved' | 'InsertEnter', is_ignored: boolean) +--- @field on_insert_leave fun() + +--- @type blink.cmp.BufferEvents +--- @diagnostic disable-next-line: missing-fields +local buffer_events = {} + +function buffer_events.new(opts) + return setmetatable({ + has_context = opts.has_context, + show_in_snippet = opts.show_in_snippet, + ignore_next_text_changed = false, + ignore_next_cursor_moved = false, + }, { __index = buffer_events }) +end + +--- Normalizes the autocmds + ctrl+c into a common api and handles ignored events +function buffer_events:listen(opts) + local snippet = require('blink.cmp.config').snippets + + local last_char = '' + vim.api.nvim_create_autocmd('InsertCharPre', { + callback = function() + if snippet.active() and not self.show_in_snippet and not self.has_context() then return end + last_char = vim.v.char + end, + }) + + vim.api.nvim_create_autocmd('TextChangedI', { + callback = function() + if not require('blink.cmp.config').enabled() then return end + if snippet.active() and not self.show_in_snippet and not self.has_context() then return end + + local is_ignored = self.ignore_next_text_changed + self.ignore_next_text_changed = false + + -- no characters added so let cursormoved handle it + if last_char == '' then return end + + opts.on_char_added(last_char, is_ignored) + + last_char = '' + end, + }) + + vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI', 'InsertEnter' }, { + callback = function(ev) + -- only fire a CursorMoved event (notable not CursorMovedI) + -- when jumping between tab stops in a snippet while showing the menu + if + ev.event == 'CursorMoved' + and (vim.api.nvim_get_mode().mode ~= 'v' or not self.has_context() or not snippet.active()) + then + return + end + + local is_cursor_moved = ev.event == 'CursorMoved' or ev.event == 'CursorMovedI' + + local is_ignored = is_cursor_moved and self.ignore_next_cursor_moved + if is_cursor_moved then self.ignore_next_cursor_moved = false end + + -- characters added so let textchanged handle it + if last_char ~= '' then return end + + if not require('blink.cmp.config').enabled() then return end + if not self.show_in_snippet and not self.has_context() and snippet.active() then return end + + opts.on_cursor_moved(is_cursor_moved and 'CursorMoved' or ev.event, is_ignored) + end, + }) + + -- definitely leaving the context + vim.api.nvim_create_autocmd({ 'ModeChanged', 'BufLeave' }, { + callback = function() + last_char = '' + -- HACK: when using vim.snippet.expand, the mode switches from insert -> normal -> visual -> select + -- so we schedule to ignore the intermediary modes + -- TODO: deduplicate requests + vim.schedule(function() + if not vim.tbl_contains({ 'i', 's' }, vim.api.nvim_get_mode().mode) then opts.on_insert_leave() end + end) + end, + }) + + -- ctrl+c doesn't trigger InsertLeave so handle it separately + local ctrl_c = vim.api.nvim_replace_termcodes('<C-c>', true, true, true) + vim.on_key(function(key) + if key == ctrl_c then + vim.schedule(function() + local mode = vim.api.nvim_get_mode().mode + if mode ~= 'i' then + last_char = '' + opts.on_insert_leave() + end + end) + end + end) +end + +--- Suppresses autocmd events for the duration of the callback +--- HACK: there's likely edge cases with this since we can't know for sure +--- if the autocmds will fire for cursor_moved afaik +function buffer_events:suppress_events_for_callback(cb) + local cursor_before = vim.api.nvim_win_get_cursor(0) + local changed_tick_before = vim.api.nvim_buf_get_changedtick(0) + + cb() + + local cursor_after = vim.api.nvim_win_get_cursor(0) + local changed_tick_after = vim.api.nvim_buf_get_changedtick(0) + + local is_insert_mode = vim.api.nvim_get_mode().mode == 'i' + + self.ignore_next_text_changed = changed_tick_after ~= changed_tick_before and is_insert_mode + + -- HACK: the cursor may move from position (1, 1) to (1, 0) and back to (1, 1) during the callback + -- This will trigger a CursorMovedI event, but we can't detect it simply by checking the cursor position + -- since they're equal before vs after the callback. So instead, we always mark the cursor as ignored in + -- insert mode, but if the cursor was equal, we undo the ignore after a small delay, which practically guarantees + -- that the CursorMovedI event will fire + -- TODO: It could make sense to override the nvim_win_set_cursor function and mark as ignored if it's called + -- on the current buffer + local cursor_moved = cursor_after[1] ~= cursor_before[1] or cursor_after[2] ~= cursor_before[2] + self.ignore_next_cursor_moved = is_insert_mode + if not cursor_moved then vim.defer_fn(function() self.ignore_next_cursor_moved = false end, 10) end +end + +return buffer_events diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/cmdline_events.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/cmdline_events.lua new file mode 100644 index 0000000..6f23ed8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/cmdline_events.lua @@ -0,0 +1,104 @@ +--- @class blink.cmp.CmdlineEvents +--- @field has_context fun(): boolean +--- @field ignore_next_text_changed boolean +--- @field ignore_next_cursor_moved boolean +--- +--- @field new fun(): blink.cmp.CmdlineEvents +--- @field listen fun(self: blink.cmp.CmdlineEvents, opts: blink.cmp.CmdlineEventsListener) +--- @field suppress_events_for_callback fun(self: blink.cmp.CmdlineEvents, cb: fun()) + +--- @class blink.cmp.CmdlineEventsListener +--- @field on_char_added fun(char: string, is_ignored: boolean) +--- @field on_cursor_moved fun(event: 'CursorMoved' | 'InsertEnter', is_ignored: boolean) +--- @field on_leave fun() + +--- @type blink.cmp.CmdlineEvents +--- @diagnostic disable-next-line: missing-fields +local cmdline_events = {} + +function cmdline_events.new() + return setmetatable({ + ignore_next_text_changed = false, + ignore_next_cursor_moved = false, + }, { __index = cmdline_events }) +end + +function cmdline_events:listen(opts) + -- TextChanged + local on_changed = function(key) opts.on_char_added(key, false) end + + -- We handle backspace as a special case, because the text will have changed + -- but we still want to fire the CursorMoved event, and not the TextChanged event + local did_backspace = false + local is_change_queued = false + vim.on_key(function(raw_key, escaped_key) + if vim.api.nvim_get_mode().mode ~= 'c' then return end + + -- ignore if it's a special key + -- FIXME: odd behavior when escaped_key has multiple keycodes, i.e. by pressing <C-p> and then "t" + local key = vim.fn.keytrans(escaped_key) + if key == '<BS>' and not is_change_queued then did_backspace = true end + if key:sub(1, 1) == '<' and key:sub(#key, #key) == '>' and raw_key ~= ' ' then return end + if key == '' then return end + + if not is_change_queued then + is_change_queued = true + did_backspace = false + vim.schedule(function() + on_changed(raw_key) + is_change_queued = false + end) + end + end) + + -- CursorMoved + local previous_cmdline = '' + vim.api.nvim_create_autocmd('CmdlineEnter', { + callback = function() previous_cmdline = '' end, + }) + + -- TODO: switch to CursorMovedC when nvim 0.11 is released + -- HACK: check every 16ms (60 times/second) to see if the cursor moved + -- for neovim < 0.11 + local timer = vim.uv.new_timer() + local previous_cursor + local callback + callback = vim.schedule_wrap(function() + timer:start(16, 0, callback) + if vim.api.nvim_get_mode().mode ~= 'c' then return end + + local cmdline_equal = vim.fn.getcmdline() == previous_cmdline + local cursor_equal = vim.fn.getcmdpos() == previous_cursor + + previous_cmdline = vim.fn.getcmdline() + previous_cursor = vim.fn.getcmdpos() + + if cursor_equal or (not cmdline_equal and not did_backspace) then return end + did_backspace = false + + local is_ignored = self.ignore_next_cursor_moved + self.ignore_next_cursor_moved = false + + opts.on_cursor_moved('CursorMoved', is_ignored) + end) + timer:start(16, 0, callback) + + vim.api.nvim_create_autocmd('CmdlineLeave', { + callback = function() opts.on_leave() end, + }) +end + +--- Suppresses autocmd events for the duration of the callback +--- HACK: there's likely edge cases with this +function cmdline_events:suppress_events_for_callback(cb) + local cursor_before = vim.fn.getcmdpos() + + cb() + + if not vim.api.nvim_get_mode().mode == 'c' then return end + + local cursor_after = vim.fn.getcmdpos() + self.ignore_next_cursor_moved = self.ignore_next_cursor_moved or cursor_after ~= cursor_before +end + +return cmdline_events diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/event_emitter.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/event_emitter.lua new file mode 100644 index 0000000..d3939cb --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/event_emitter.lua @@ -0,0 +1,37 @@ +--- @class blink.cmp.EventEmitter<T> : { event: string, autocmd?: string, listeners: table<fun(data: T)>, new: ( fun(event: string, autocmd: string): blink.cmp.EventEmitter ), on: ( fun(self: blink.cmp.EventEmitter, callback: fun(data: T)) ), off: ( fun(self: blink.cmp.EventEmitter, callback: fun(data: T)) ), emit: ( fun(self: blink.cmp.EventEmitter, data?: table) ) }; +--- TODO: is there a better syntax for this? + +local event_emitter = {} + +--- @param event string +--- @param autocmd? string +function event_emitter.new(event, autocmd) + local self = setmetatable({}, { __index = event_emitter }) + self.event = event + self.autocmd = autocmd + self.listeners = {} + return self +end + +function event_emitter:on(callback) table.insert(self.listeners, callback) end + +function event_emitter:off(callback) + for idx, cb in ipairs(self.listeners) do + if cb == callback then table.remove(self.listeners, idx) end + end +end + +function event_emitter:emit(data) + data = data or {} + data.event = self.event + for _, callback in ipairs(self.listeners) do + callback(data) + end + if self.autocmd then + require('blink.cmp.lib.utils').schedule_if_needed( + function() vim.api.nvim_exec_autocmds('User', { pattern = self.autocmd, modeline = false, data = data }) end + ) + end +end + +return event_emitter diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/text_edits.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/text_edits.lua new file mode 100644 index 0000000..2ce76fe --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/text_edits.lua @@ -0,0 +1,193 @@ +local config = require('blink.cmp.config') +local context = require('blink.cmp.completion.trigger.context') + +local text_edits = {} + +--- Applies one or more text edits to the current buffer, assuming utf-8 encoding +--- @param edits lsp.TextEdit[] +function text_edits.apply(edits) + local mode = context.get_mode() + if mode == 'default' then return vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), 'utf-8') end + + assert(mode == 'cmdline', 'Unsupported mode for text edits: ' .. mode) + assert(#edits == 1, 'Cmdline mode only supports one text edit. Contributions welcome!') + + local edit = edits[1] + local line = context.get_line() + local edited_line = line:sub(1, edit.range.start.character) + .. edit.newText + .. line:sub(edit.range['end'].character + 1) + -- FIXME: for some reason, we have to set the cursor here, instead of later, + -- because this will override the cursor position set later + vim.fn.setcmdline(edited_line, edit.range.start.character + #edit.newText + 1) +end + +------- Undo ------- + +--- Gets the reverse of the text edit, must be called before applying +--- @param text_edit lsp.TextEdit +--- @return lsp.TextEdit +function text_edits.get_undo_text_edit(text_edit) + return { + range = text_edits.get_undo_range(text_edit), + newText = text_edits.get_text_to_replace(text_edit), + } +end + +--- Gets the range for undoing an applied text edit +--- @param text_edit lsp.TextEdit +function text_edits.get_undo_range(text_edit) + text_edit = vim.deepcopy(text_edit) + local lines = vim.split(text_edit.newText, '\n') + local last_line_len = lines[#lines] and #lines[#lines] or 0 + + local range = text_edit.range + range['end'].line = range.start.line + #lines - 1 + range['end'].character = #lines > 1 and last_line_len or range.start.character + last_line_len + + return range +end + +--- Gets the text the text edit will replace +--- @param text_edit lsp.TextEdit +--- @return string +function text_edits.get_text_to_replace(text_edit) + local lines = {} + for line = text_edit.range.start.line, text_edit.range['end'].line do + local line_text = context.get_line() + local is_start_line = line == text_edit.range.start.line + local is_end_line = line == text_edit.range['end'].line + + if is_start_line and is_end_line then + table.insert(lines, line_text:sub(text_edit.range.start.character + 1, text_edit.range['end'].character)) + elseif is_start_line then + table.insert(lines, line_text:sub(text_edit.range.start.character + 1)) + elseif is_end_line then + table.insert(lines, line_text:sub(1, text_edit.range['end'].character)) + else + table.insert(lines, line_text) + end + end + return table.concat(lines, '\n') +end + +------- Get ------- + +--- Grabbed from vim.lsp.utils. Converts an offset_encoding to byte offset +--- @param position lsp.Position +--- @param offset_encoding? 'utf-8'|'utf-16'|'utf-32' +--- @return number +local function get_line_byte_from_position(position, offset_encoding) + local bufnr = vim.api.nvim_get_current_buf() + local col = position.character + + -- When on the first character, we can ignore the difference between byte and character + if col == 0 then return 0 end + + local line = vim.api.nvim_buf_get_lines(bufnr, position.line, position.line + 1, false)[1] or '' + if vim.fn.has('nvim-0.11.0') == 1 then + col = vim.str_byteindex(line, offset_encoding or 'utf-16', col, false) or 0 + else + col = vim.lsp.util._str_byteindex_enc(line, col, offset_encoding or 'utf-16') + end + return math.min(col, #line) +end + +--- Gets the text edit from an item, handling insert/replace ranges and converts +--- offset encodings (utf-16 | utf-32) to utf-8 +--- @param item blink.cmp.CompletionItem +--- @return lsp.TextEdit +function text_edits.get_from_item(item) + local text_edit = vim.deepcopy(item.textEdit) + + -- Guess the text edit if the item doesn't define it + if text_edit == nil then return text_edits.guess(item) end + + -- FIXME: temporarily convert insertReplaceEdit to regular textEdit + if text_edit.range == nil then + if config.completion.keyword.range == 'full' and text_edit.replace ~= nil then + text_edit.range = text_edit.replace + else + text_edit.range = text_edit.insert or text_edit.replace + end + end + text_edit.insert = nil + text_edit.replace = nil + --- @cast text_edit lsp.TextEdit + + -- Adjust the position of the text edit to be the current cursor position + -- since the data might be outdated. We compare the cursor column position + -- from when the items were fetched versus the current. + -- HACK: is there a better way? + -- TODO: take into account the offset_encoding + local offset = context.get_cursor()[2] - item.cursor_column + text_edit.range['end'].character = text_edit.range['end'].character + offset + + -- convert the offset encoding to utf-8 + -- TODO: we have to do this last because it applies a max on the position based on the length of the line + -- so it would break the offset code when removing characters at the end of the line + local offset_encoding = text_edits.offset_encoding_from_item(item) + text_edit = text_edits.to_utf_8(text_edit, offset_encoding) + + text_edit.range = text_edits.clamp_range_to_bounds(text_edit.range) + + return text_edit +end + +function text_edits.offset_encoding_from_item(item) + local client = vim.lsp.get_client_by_id(item.client_id) + return client ~= nil and client.offset_encoding or 'utf-8' +end + +function text_edits.to_utf_8(text_edit, offset_encoding) + if offset_encoding == 'utf-8' then return text_edit end + text_edit = vim.deepcopy(text_edit) + text_edit.range.start.character = get_line_byte_from_position(text_edit.range.start, offset_encoding) + text_edit.range['end'].character = get_line_byte_from_position(text_edit.range['end'], offset_encoding) + return text_edit +end + +--- Uses the keyword_regex to guess the text edit ranges +--- @param item blink.cmp.CompletionItem +--- TODO: doesnt work when the item contains characters not included in the context regex +function text_edits.guess(item) + local word = item.insertText or item.label + + local start_col, end_col = require('blink.cmp.fuzzy').guess_edit_range( + item, + context.get_line(), + context.get_cursor()[2], + config.completion.keyword.range + ) + local current_line = context.get_cursor()[1] + + -- convert to 0-index + return { + range = { + start = { line = current_line - 1, character = start_col }, + ['end'] = { line = current_line - 1, character = end_col }, + }, + newText = word, + } +end + +--- Clamps the range to the bounds of their respective lines +--- @param range lsp.Range +--- @return lsp.Range +--- TODO: clamp start and end lines +function text_edits.clamp_range_to_bounds(range) + range = vim.deepcopy(range) + + local start_line = context.get_line(range.start.line) + range.start.character = math.min(math.max(range.start.character, 0), #start_line) + + local end_line = context.get_line(range['end'].line) + range['end'].character = math.min( + math.max(range['end'].character, range.start.line == range['end'].line and range.start.character or 0), + #end_line + ) + + return range +end + +return text_edits diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/utils.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/utils.lua new file mode 100644 index 0000000..84bcc3d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/utils.lua @@ -0,0 +1,112 @@ +local utils = {} + +--- Shallow copy table +--- @generic T +--- @param t T +--- @return T +function utils.shallow_copy(t) + local t2 = {} + for k, v in pairs(t) do + t2[k] = v + end + return t2 +end + +--- Returns the union of the keys of two tables +--- @generic T +--- @param t1 T[] +--- @param t2 T[] +--- @return T[] +function utils.union_keys(t1, t2) + local t3 = {} + for k, _ in pairs(t1) do + t3[k] = true + end + for k, _ in pairs(t2) do + t3[k] = true + end + return vim.tbl_keys(t3) +end + +--- Returns a list of unique values from the input array +--- @generic T +--- @param arr T[] +--- @return T[] +function utils.deduplicate(arr) + local hash = {} + for _, v in ipairs(arr) do + hash[v] = true + end + return vim.tbl_keys(hash) +end + +function utils.schedule_if_needed(fn) + if vim.in_fast_event() then + vim.schedule(fn) + else + fn() + end +end + +--- Flattens an arbitrarily deep table into a single level table +--- @param t table +--- @return table +function utils.flatten(t) + if t[1] == nil then return t end + + local flattened = {} + for _, v in ipairs(t) do + if type(v) == 'table' and vim.tbl_isempty(v) then goto continue end + + if v[1] == nil then + table.insert(flattened, v) + else + vim.list_extend(flattened, utils.flatten(v)) + end + + ::continue:: + end + return flattened +end + +--- Returns the index of the first occurrence of the value in the array +--- @generic T +--- @param arr T[] +--- @param val T +--- @return number? +function utils.index_of(arr, val) + for idx, v in ipairs(arr) do + if v == val then return idx end + end + return nil +end + +--- Finds an item in an array using a predicate function +--- @generic T +--- @param arr T[] +--- @param predicate fun(item: T): boolean +--- @return number? +function utils.find_idx(arr, predicate) + for idx, v in ipairs(arr) do + if predicate(v) then return idx end + end + return nil +end + +--- Slices an array +--- @generic T +--- @param arr T[] +--- @param start number? +--- @param finish number? +--- @return T[] +function utils.slice(arr, start, finish) + start = start or 1 + finish = finish or #arr + local sliced = {} + for i = start, finish do + sliced[#sliced + 1] = arr[i] + end + return sliced +end + +return utils diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/docs.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/docs.lua new file mode 100644 index 0000000..f250701 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/docs.lua @@ -0,0 +1,224 @@ +local highlight_ns = require('blink.cmp.config').appearance.highlight_ns + +local docs = {} + +--- @class blink.cmp.RenderDetailAndDocumentationOpts +--- @field bufnr number +--- @field detail? string|string[] +--- @field documentation? lsp.MarkupContent | string +--- @field max_width number +--- @field use_treesitter_highlighting boolean? + +--- @class blink.cmp.RenderDetailAndDocumentationOptsPartial +--- @field bufnr? number +--- @field detail? string +--- @field documentation? lsp.MarkupContent | string +--- @field max_width? number +--- @field use_treesitter_highlighting boolean? + +--- @param opts blink.cmp.RenderDetailAndDocumentationOpts +function docs.render_detail_and_documentation(opts) + local detail_lines = {} + local details = type(opts.detail) == 'string' and { opts.detail } or opts.detail or {} + --- @cast details string[] + details = require('blink.cmp.lib.utils').deduplicate(details) + for _, v in ipairs(details) do + vim.list_extend(detail_lines, docs.split_lines(v)) + end + + local doc_lines = {} + if opts.documentation ~= nil then + local doc = type(opts.documentation) == 'string' and opts.documentation or opts.documentation.value + doc_lines = docs.split_lines(doc) + end + + detail_lines, doc_lines = docs.extract_detail_from_doc(detail_lines, doc_lines) + + ---@type string[] + local combined_lines = vim.list_extend({}, detail_lines) + + -- add a blank line for the --- separator + local doc_already_has_separator = #doc_lines > 1 and (doc_lines[1] == '---' or doc_lines[1] == '***') + if #detail_lines > 0 and #doc_lines > 0 then table.insert(combined_lines, '') end + -- skip original separator in doc_lines, so we can highlight it later + vim.list_extend(combined_lines, doc_lines, doc_already_has_separator and 2 or 1) + + vim.api.nvim_buf_set_lines(opts.bufnr, 0, -1, true, combined_lines) + vim.api.nvim_set_option_value('modified', false, { buf = opts.bufnr }) + + -- Highlight with treesitter + vim.api.nvim_buf_clear_namespace(opts.bufnr, highlight_ns, 0, -1) + + if #detail_lines > 0 and opts.use_treesitter_highlighting then + docs.highlight_with_treesitter(opts.bufnr, vim.bo.filetype, 0, #detail_lines) + end + + -- Only add the separator if there are documentation lines (otherwise only display the detail) + if #detail_lines > 0 and #doc_lines > 0 then + vim.api.nvim_buf_set_extmark(opts.bufnr, highlight_ns, #detail_lines, 0, { + virt_text = { { string.rep('─', opts.max_width), 'BlinkCmpDocSeparator' } }, + virt_text_pos = 'overlay', + }) + end + + if #doc_lines > 0 and opts.use_treesitter_highlighting then + local start = #detail_lines + (#detail_lines > 0 and 1 or 0) + docs.highlight_with_treesitter(opts.bufnr, 'markdown', start, start + #doc_lines) + end +end + +--- Highlights the given range with treesitter with the given filetype +--- @param bufnr number +--- @param filetype string +--- @param start_line number +--- @param end_line number +--- TODO: fallback to regex highlighting if treesitter fails +--- TODO: only render what's visible +function docs.highlight_with_treesitter(bufnr, filetype, start_line, end_line) + local Range = require('vim.treesitter._range') + + local root_lang = vim.treesitter.language.get_lang(filetype) + if root_lang == nil then return end + + local success, trees = pcall(vim.treesitter.get_parser, bufnr, root_lang) + if not success or not trees then return end + + trees:parse({ start_line, end_line }) + + trees:for_each_tree(function(tree, tstree) + local lang = tstree:lang() + local highlighter_query = vim.treesitter.query.get(lang, 'highlights') + if not highlighter_query then return end + + local root_node = tree:root() + local _, _, root_end_row, _ = root_node:range() + + local iter = highlighter_query:iter_captures(tree:root(), bufnr, start_line, end_line) + local line = start_line + while line < end_line do + local capture, node, metadata, _ = iter(line) + if capture == nil then break end + + local range = { root_end_row + 1, 0, root_end_row + 1, 0 } + if node then range = vim.treesitter.get_range(node, bufnr, metadata and metadata[capture]) end + local start_row, start_col, end_row, end_col = Range.unpack4(range) + + if capture then + local name = highlighter_query.captures[capture] + local hl = 0 + if not vim.startswith(name, '_') then hl = vim.api.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) end + + -- The "priority" attribute can be set at the pattern level or on a particular capture + local priority = ( + tonumber(metadata.priority or metadata[capture] and metadata[capture].priority) + or vim.highlight.priorities.treesitter + ) + + -- The "conceal" attribute can be set at the pattern level or on a particular capture + local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal + + if hl and end_row >= line then + vim.api.nvim_buf_set_extmark(bufnr, highlight_ns, start_row, start_col, { + end_line = end_row, + end_col = end_col, + hl_group = hl, + priority = priority, + conceal = conceal, + }) + end + end + + if start_row > line then line = start_row end + end + end) +end + +--- Gets the start and end row of the code block for the given row +--- Or returns nil if there's no code block +--- @param lines string[] +--- @param row number +--- @return number?, number? +function docs.get_code_block_range(lines, row) + if row < 1 or row > #lines then return end + -- get the start of the code block + local code_block_start = nil + for i = 1, row do + local line = lines[i] + if line:match('^%s*```') then + if code_block_start == nil then + code_block_start = i + else + code_block_start = nil + end + end + end + if code_block_start == nil then return end + + -- get the end of the code block + local code_block_end = nil + for i = row, #lines do + local line = lines[i] + if line:match('^%s*```') then + code_block_end = i + break + end + end + if code_block_end == nil then return end + + return code_block_start, code_block_end +end + +--- Avoids showing the detail if it's part of the documentation +--- or, if the detail is in a code block in the doc, +--- extracts the code block into the detail +---@param detail_lines string[] +---@param doc_lines string[] +---@return string[], string[] +--- TODO: Also move the code block into detail if it's at the start of the doc +--- and we have no detail +function docs.extract_detail_from_doc(detail_lines, doc_lines) + local detail_str = table.concat(detail_lines, '\n') + local doc_str = table.concat(doc_lines, '\n') + local doc_str_detail_row = doc_str:find(detail_str, 1, true) + + -- didn't find the detail in the doc, so return as is + if doc_str_detail_row == nil or #detail_str == 0 or #doc_str == 0 then return detail_lines, doc_lines end + + -- get the line of the match + -- hack: surely there's a better way to do this but it's late + -- and I can't be bothered + local offset = 1 + local detail_line = 1 + for line_num, line in ipairs(doc_lines) do + if #line + offset > doc_str_detail_row then + detail_line = line_num + break + end + offset = offset + #line + 1 + end + + -- extract the code block, if it exists, and use it as the detail + local code_block_start, code_block_end = docs.get_code_block_range(doc_lines, detail_line) + if code_block_start ~= nil and code_block_end ~= nil then + detail_lines = vim.list_slice(doc_lines, code_block_start + 1, code_block_end - 1) + + local doc_lines_start = vim.list_slice(doc_lines, 1, code_block_start - 1) + local doc_lines_end = vim.list_slice(doc_lines, code_block_end + 1, #doc_lines) + vim.list_extend(doc_lines_start, doc_lines_end) + doc_lines = doc_lines_start + else + detail_lines = {} + end + + return detail_lines, doc_lines +end + +function docs.split_lines(text) + local lines = {} + for s in text:gmatch('[^\r\n]+') do + table.insert(lines, s) + end + return lines +end + +return docs diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/init.lua new file mode 100644 index 0000000..9a5d18b --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/init.lua @@ -0,0 +1,445 @@ +-- TODO: The scrollbar and redrawing logic should be done by wrapping the functions that would +-- trigger a redraw or update the window + +--- @class blink.cmp.WindowOptions +--- @field min_width? number +--- @field max_width? number +--- @field max_height? number +--- @field cursorline? boolean +--- @field border? blink.cmp.WindowBorder +--- @field wrap? boolean +--- @field winblend? number +--- @field winhighlight? string +--- @field scrolloff? number +--- @field scrollbar? boolean +--- @field filetype string + +--- @class blink.cmp.Window +--- @field id? number +--- @field buf? number +--- @field config blink.cmp.WindowOptions +--- @field scrollbar? blink.cmp.Scrollbar +--- @field redraw_queued boolean +--- +--- @field new fun(config: blink.cmp.WindowOptions): blink.cmp.Window +--- @field get_buf fun(self: blink.cmp.Window): number +--- @field get_win fun(self: blink.cmp.Window): number +--- @field is_open fun(self: blink.cmp.Window): boolean +--- @field open fun(self: blink.cmp.Window) +--- @field close fun(self: blink.cmp.Window) +--- @field set_option_value fun(self: blink.cmp.Window, option: string, value: any) +--- @field update_size fun(self: blink.cmp.Window) +--- @field get_content_height fun(self: blink.cmp.Window): number +--- @field get_border_size fun(self: blink.cmp.Window, border?: 'none' | 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | 'padded' | string[]): { vertical: number, horizontal: number, left: number, right: number, top: number, bottom: number } +--- @field get_height fun(self: blink.cmp.Window): number +--- @field get_content_width fun(self: blink.cmp.Window): number +--- @field get_width fun(self: blink.cmp.Window): number +--- @field get_cursor_screen_position fun(): { distance_from_top: number, distance_from_bottom: number } +--- @field set_cursor fun(self: blink.cmp.Window, cursor: number[]) +--- @field set_height fun(self: blink.cmp.Window, height: number) +--- @field set_width fun(self: blink.cmp.Window, width: number) +--- @field set_win_config fun(self: blink.cmp.Window, config: table) +--- @field get_vertical_direction_and_height fun(self: blink.cmp.Window, direction_priority: ("n" | "s")[]): { height: number, direction: 'n' | 's' }? +--- @field get_direction_with_window_constraints fun(self: blink.cmp.Window, anchor_win: blink.cmp.Window, direction_priority: ("n" | "s" | "e" | "w")[], desired_min_size?: { width: number, height: number }): { width: number, height: number, direction: 'n' | 's' | 'e' | 'w' }? +--- @field redraw_if_needed fun(self: blink.cmp.Window) + +--- @type blink.cmp.Window +--- @diagnostic disable-next-line: missing-fields +local win = {} + +--- @param config blink.cmp.WindowOptions +function win.new(config) + local self = setmetatable({}, { __index = win }) + + self.id = nil + self.buf = nil + self.config = { + min_width = config.min_width, + max_width = config.max_width, + max_height = config.max_height or 10, + cursorline = config.cursorline or false, + border = config.border or 'none', + wrap = config.wrap or false, + winblend = config.winblend or 0, + winhighlight = config.winhighlight or 'Normal:NormalFloat,FloatBorder:NormalFloat', + scrolloff = config.scrolloff or 0, + scrollbar = config.scrollbar, + filetype = config.filetype, + } + self.redraw_queued = false + + if self.config.scrollbar then + self.scrollbar = require('blink.cmp.lib.window.scrollbar').new({ + enable_gutter = self.config.border == 'none' or self.config.border == 'padded', + }) + end + + return self +end + +function win:get_buf() + -- create buffer if it doesn't exist + if self.buf == nil or not vim.api.nvim_buf_is_valid(self.buf) then + self.buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_set_option_value('tabstop', 1, { buf = self.buf }) -- prevents tab widths from being unpredictable + end + return self.buf +end + +function win:get_win() + if self.id ~= nil and not vim.api.nvim_win_is_valid(self.id) then self.id = nil end + return self.id +end + +function win:is_open() return self.id ~= nil and vim.api.nvim_win_is_valid(self.id) end + +function win:open() + -- window already exists + if self.id ~= nil and vim.api.nvim_win_is_valid(self.id) then return end + + -- create window + self.id = vim.api.nvim_open_win(self:get_buf(), false, { + relative = 'cursor', + style = 'minimal', + width = self.config.min_width or 1, + height = self.config.max_height, + row = 1, + col = 1, + focusable = false, + zindex = 1001, + border = self.config.border == 'padded' and { ' ', '', '', ' ', '', '', ' ', ' ' } or self.config.border, + }) + vim.api.nvim_set_option_value('winblend', self.config.winblend, { win = self.id }) + vim.api.nvim_set_option_value('winhighlight', self.config.winhighlight, { win = self.id }) + vim.api.nvim_set_option_value('wrap', self.config.wrap, { win = self.id }) + vim.api.nvim_set_option_value('foldenable', false, { win = self.id }) + vim.api.nvim_set_option_value('conceallevel', 2, { win = self.id }) + vim.api.nvim_set_option_value('concealcursor', 'n', { win = self.id }) + vim.api.nvim_set_option_value('cursorlineopt', 'line', { win = self.id }) + vim.api.nvim_set_option_value('cursorline', self.config.cursorline, { win = self.id }) + vim.api.nvim_set_option_value('scrolloff', self.config.scrolloff, { win = self.id }) + vim.api.nvim_set_option_value('filetype', self.config.filetype, { buf = self.buf }) + + if self.scrollbar then self.scrollbar:update(self.id) end + self:redraw_if_needed() +end + +function win:set_option_value(option, value) + if self.id == nil or not vim.api.nvim_win_is_valid(self.id) then return end + vim.api.nvim_set_option_value(option, value, { win = self.id }) +end + +function win:close() + if self.id ~= nil then + vim.api.nvim_win_close(self.id, true) + self.id = nil + end + if self.scrollbar then self.scrollbar:update() end + self:redraw_if_needed() +end + +--- Updates the size of the window to match the max width and height of the content/config +function win:update_size() + if not self:is_open() then return end + local winnr = self:get_win() + local config = self.config + + -- todo: never go above the screen width and height + + -- set width to current content width, bounded by min and max + local width = self:get_content_width() + if config.max_width then width = math.min(width, config.max_width) end + if config.min_width then width = math.max(width, config.min_width) end + vim.api.nvim_win_set_width(winnr, width) + + -- set height to current line count, bounded by max + local height = math.min(self:get_content_height(), config.max_height) + vim.api.nvim_win_set_height(winnr, height) +end + +-- todo: fix nvim_win_text_height +-- @return number +function win:get_content_height() + if not self:is_open() then return 0 end + return vim.api.nvim_win_text_height(self:get_win(), {}).all +end + +--- Gets the size of the borders around the window +--- @return { vertical: number, horizontal: number, left: number, right: number, top: number, bottom: number } +function win:get_border_size() + if not self:is_open() then return { vertical = 0, horizontal = 0, left = 0, right = 0, top = 0, bottom = 0 } end + + local left = 0 + local right = 0 + local top = 0 + local bottom = 0 + + local border = self.config.border + if border == 'padded' then + left = 1 + right = 1 + elseif border == 'shadow' then + right = 1 + bottom = 1 + elseif type(border) == 'string' and border ~= 'none' then + left = 1 + right = 1 + top = 1 + bottom = 1 + elseif type(border) == 'table' then + -- borders can be a table of strings and act differently with different # of chars + -- so we normalize it: https://neovim.io/doc/user/api.html#nvim_open_win() + -- based on nvim-cmp + -- TODO: doesn't handle scrollbar + local resolved_border = {} + while #resolved_border <= 8 do + for _, b in ipairs(border) do + table.insert(resolved_border, type(b) == 'string' and b or b[1]) + end + end + + top = resolved_border[2] == '' and 0 or 1 + bottom = resolved_border[6] == '' and 0 or 1 + left = resolved_border[8] == '' and 0 or 1 + right = resolved_border[4] == '' and 0 or 1 + end + + if self.scrollbar and self.scrollbar:is_visible() then + local offset = (border == 'none' or border == 'padded') and 1 or 0 + right = right + offset + end + + return { vertical = top + bottom, horizontal = left + right, left = left, right = right, top = top, bottom = bottom } +end + +--- Gets the height of the window, taking into account the border +function win:get_height() + if not self:is_open() then return 0 end + return vim.api.nvim_win_get_height(self:get_win()) + self:get_border_size().vertical +end + +--- Gets the width of the longest line in the window +function win:get_content_width() + if not self:is_open() then return 0 end + local max_width = 0 + for _, line in ipairs(vim.api.nvim_buf_get_lines(self.buf, 0, -1, false)) do + max_width = math.max(max_width, vim.api.nvim_strwidth(line)) + end + return max_width +end + +--- Gets the width of the window, taking into account the border +function win:get_width() + if not self:is_open() then return 0 end + return vim.api.nvim_win_get_width(self:get_win()) + self:get_border_size().horizontal +end + +--- Gets the cursor's distance from all sides of the screen +function win.get_cursor_screen_position() + local screen_height = vim.o.lines + local screen_width = vim.o.columns + + -- command line + if vim.api.nvim_get_mode().mode == 'c' then + local config = require('blink.cmp.config').completion.menu + local cmdline_position = config.cmdline_position() + + return { + distance_from_top = cmdline_position[1], + distance_from_bottom = screen_height - cmdline_position[1] - 1, + distance_from_left = cmdline_position[2], + distance_from_right = screen_width - cmdline_position[2], + } + end + + -- default + local cursor_line, cursor_column = unpack(vim.api.nvim_win_get_cursor(0)) + -- todo: convert cursor_column to byte index + local pos = vim.fn.screenpos(vim.api.nvim_win_get_number(0), cursor_line, cursor_column) + + return { + distance_from_top = pos.row - 1, + distance_from_bottom = screen_height - pos.row, + distance_from_left = pos.col, + distance_from_right = screen_width - pos.col, + } +end + +function win:set_cursor(cursor) + local winnr = self:get_win() + assert(winnr ~= nil, 'Window must be open to set cursor') + + vim.api.nvim_win_set_cursor(winnr, cursor) + + if self.scrollbar then self.scrollbar:update(winnr) end + self:redraw_if_needed() +end + +function win:set_height(height) + local winnr = self:get_win() + assert(winnr ~= nil, 'Window must be open to set height') + + vim.api.nvim_win_set_height(winnr, height) + + if self.scrollbar then self.scrollbar:update(winnr) end + self:redraw_if_needed() +end + +function win:set_width(width) + local winnr = self:get_win() + assert(winnr ~= nil, 'Window must be open to set width') + + vim.api.nvim_win_set_width(winnr, width) + + if self.scrollbar then self.scrollbar:update(winnr) end + self:redraw_if_needed() +end + +function win:set_win_config(config) + local winnr = self:get_win() + assert(winnr ~= nil, 'Window must be open to set window config') + + vim.api.nvim_win_set_config(winnr, config) + + if self.scrollbar then self.scrollbar:update(winnr) end + self:redraw_if_needed() +end + +--- Gets the direction with the most space available, prioritizing the directions in the order of the +--- direction_priority list +function win:get_vertical_direction_and_height(direction_priority) + local constraints = self.get_cursor_screen_position() + local max_height = self:get_height() + local border_size = self:get_border_size() + local function get_distance(direction) + return direction == 's' and constraints.distance_from_bottom or constraints.distance_from_top + end + + local direction_priority_by_space = vim.fn.sort(vim.deepcopy(direction_priority), function(a, b) + local distance_a = math.min(max_height, get_distance(a)) + local distance_b = math.min(max_height, get_distance(b)) + return (distance_a < distance_b) and 1 or (distance_a > distance_b) and -1 or 0 + end) + + local direction = direction_priority_by_space[1] + local height = math.min(max_height, get_distance(direction)) + if height <= border_size.vertical then return end + return { height = height - border_size.vertical, direction = direction } +end + +function win:get_direction_with_window_constraints(anchor_win, direction_priority, desired_min_size) + local cursor_constraints = self.get_cursor_screen_position() + + -- nvim.api.nvim_win_get_position doesn't return the correct position most of the time + -- so we calculate the position ourselves + local anchor_config + local anchor_win_config = vim.api.nvim_win_get_config(anchor_win:get_win()) + if anchor_win_config.relative == 'win' then + local anchor_relative_win_position = vim.api.nvim_win_get_position(anchor_win_config.win) + anchor_config = { + row = anchor_win_config.row + anchor_relative_win_position[1] + 1, + col = anchor_win_config.col + anchor_relative_win_position[2] + 1, + } + elseif anchor_win_config.relative == 'editor' then + anchor_config = { + row = anchor_win_config.row + 1, + col = anchor_win_config.col + 1, + } + end + assert(anchor_config ~= nil, 'The anchor window must be relative to a window or the editor') + + -- compensate for the anchor window being too wide given the screen width and configured column + if anchor_config.col + anchor_win_config.width > vim.o.columns then + anchor_config.col = vim.o.columns - anchor_win_config.width + end + + local anchor_border_size = anchor_win:get_border_size() + local anchor_col = anchor_config.col - anchor_border_size.left + local anchor_row = anchor_config.row - anchor_border_size.top + local anchor_height = anchor_win:get_height() + local anchor_width = anchor_win:get_width() + + -- we want to avoid covering the cursor line, so we need to get the direction of the window + -- that we're anchoring against + local cursor_screen_row = vim.api.nvim_get_mode().mode == 'c' and vim.o.lines - 1 or vim.fn.winline() + local anchor_is_above_cursor = anchor_config.row - cursor_screen_row < 0 + + local screen_height = vim.o.lines + local screen_width = vim.o.columns + + local direction_constraints = { + n = { + vertical = anchor_is_above_cursor and (anchor_row - 1) or cursor_constraints.distance_from_top, + horizontal = screen_width - (anchor_col - 1), + }, + s = { + vertical = anchor_is_above_cursor and cursor_constraints.distance_from_bottom + or (screen_height - (anchor_height + anchor_row - 1 + anchor_border_size.vertical)), + horizontal = screen_width - (anchor_col - 1), + }, + e = { + vertical = anchor_is_above_cursor and cursor_constraints.distance_from_top + or cursor_constraints.distance_from_bottom, + horizontal = screen_width - (anchor_col - 1) - anchor_width - anchor_border_size.right, + }, + w = { + vertical = anchor_is_above_cursor and cursor_constraints.distance_from_top + or cursor_constraints.distance_from_bottom, + horizontal = anchor_col - 1 + anchor_border_size.left, + }, + } + + local max_height = self:get_height() + local max_width = self:get_width() + local direction_priority_by_space = vim.fn.sort(vim.deepcopy(direction_priority), function(a, b) + local constraints_a = direction_constraints[a] + local constraints_b = direction_constraints[b] + + local is_desired_a = desired_min_size.height <= constraints_a.vertical + and desired_min_size.width <= constraints_a.horizontal + local is_desired_b = desired_min_size.height <= constraints_b.vertical + and desired_min_size.width <= constraints_b.horizontal + + -- If both have desired size, preserve original priority + if is_desired_a and is_desired_b then return 0 end + + -- prioritize "a" if it has the desired size and "b" doesn't + if is_desired_a then return -1 end + + -- prioritize "b" if it has the desired size and "a" doesn't + if is_desired_b then return 1 end + + -- neither have the desired size, so pick based on which has the most space + local distance_a = math.min(max_height, constraints_a.vertical, constraints_a.horizontal) + local distance_b = math.min(max_height, constraints_b.vertical, constraints_b.horizontal) + return distance_a < distance_b and 1 or distance_a > distance_b and -1 or 0 + end) + + local border_size = self:get_border_size() + local direction = direction_priority_by_space[1] + local height = math.min(max_height, direction_constraints[direction].vertical) + if height <= border_size.vertical then return end + local width = math.min(max_width, direction_constraints[direction].horizontal) + if width <= border_size.horizontal then return end + + return { + width = width - border_size.horizontal, + height = height - border_size.vertical, + direction = direction, + } +end + +--- In cmdline mode, the window won't be redrawn automatically so we redraw ourselves on schedule +function win:redraw_if_needed() + if self.redraw_queued or vim.api.nvim_get_mode().mode ~= 'c' or self:get_win() == nil then return end + + -- We redraw on schedule to avoid the cmdline disappearing during redraw + -- and to batch multiple redraws together + self.redraw_queued = true + vim.schedule(function() + self.redraw_queued = false + vim.api.nvim__redraw({ win = self:get_win(), flush = true }) + end) +end + +return win diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/geometry.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/geometry.lua new file mode 100644 index 0000000..ad481a0 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/geometry.lua @@ -0,0 +1,92 @@ +--- Helper for calculating placement of the scrollbar thumb and gutter + +--- @class blink.cmp.ScrollbarGeometry +--- @field width number +--- @field height number +--- @field row number +--- @field col number +--- @field zindex number +--- @field relative string +--- @field win number + +local M = {} + +--- @param target_win number +--- @return number +local function get_win_buf_height(target_win) + local buf = vim.api.nvim_win_get_buf(target_win) + + -- not wrapping, so just get the line count + if not vim.wo[target_win].wrap then return vim.api.nvim_buf_line_count(buf) end + + local width = vim.api.nvim_win_get_width(target_win) + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local height = 0 + for _, l in ipairs(lines) do + height = height + math.max(1, (math.ceil(vim.fn.strwidth(l) / width))) + end + return height +end + +--- @param border string|string[] +--- @return number +local function get_col_offset(border) + -- we only need an extra offset when working with a padded window + if type(border) == 'table' and border[1] == ' ' and border[4] == ' ' and border[7] == ' ' and border[8] == ' ' then + return 1 + end + return 0 +end + +--- Gets the starting line, handling line wrapping if enabled +--- @param target_win number +--- @param width number +--- @return number +local get_content_start_line = function(target_win, width) + local start_line = math.max(1, vim.fn.line('w0', target_win)) + if not vim.wo[target_win].wrap then return start_line end + + local bufnr = vim.api.nvim_win_get_buf(target_win) + local wrapped_start_line = 1 + for _, text in ipairs(vim.api.nvim_buf_get_lines(bufnr, 0, start_line - 1, false)) do + -- nvim_buf_get_lines sometimes returns a blob. see hrsh7th/nvim-cmp#2050 + if vim.fn.type(text) == vim.v.t_blob then text = vim.fn.string(text) end + wrapped_start_line = wrapped_start_line + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / width)) + end + return wrapped_start_line +end + +--- @param target_win number +--- @return { should_hide: boolean, thumb: blink.cmp.ScrollbarGeometry, gutter: blink.cmp.ScrollbarGeometry } +function M.get_geometry(target_win) + local config = vim.api.nvim_win_get_config(target_win) + local width = config.width + local height = config.height + local zindex = config.zindex + + local buf_height = get_win_buf_height(target_win) + local thumb_height = math.max(1, math.floor(height * height / buf_height + 0.5) - 1) + + local start_line = get_content_start_line(target_win, width or 1) + + local pct = (start_line - 1) / (buf_height - height) + local thumb_offset = math.floor((pct * (height - thumb_height)) + 0.5) + thumb_height = thumb_offset + thumb_height > height and height - thumb_offset or thumb_height + thumb_height = math.max(1, thumb_height) + + local common_geometry = { + width = 1, + row = thumb_offset, + col = width + get_col_offset(config.border), + relative = 'win', + win = target_win, + } + + return { + should_hide = height >= buf_height, + thumb = vim.tbl_deep_extend('force', common_geometry, { height = thumb_height, zindex = zindex + 2 }), + gutter = vim.tbl_deep_extend('force', common_geometry, { row = 0, height = height, zindex = zindex + 1 }), + } +end + +return M diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/init.lua new file mode 100644 index 0000000..c72615a --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/init.lua @@ -0,0 +1,37 @@ +-- TODO: move the set_config and set_height calls from the menu/documentation/signature files +-- to helpers in the window lib, and call scrollbar updates from there. This way, consumers of +-- the window lib don't need to worry about scrollbars + +--- @class blink.cmp.ScrollbarConfig +--- @field enable_gutter boolean + +--- @class blink.cmp.Scrollbar +--- @field win blink.cmp.ScrollbarWin +--- +--- @field new fun(opts: blink.cmp.ScrollbarConfig): blink.cmp.Scrollbar +--- @field is_visible fun(self: blink.cmp.Scrollbar): boolean +--- @field update fun(self: blink.cmp.Scrollbar, target_win: number | nil) + +--- @type blink.cmp.Scrollbar +--- @diagnostic disable-next-line: missing-fields +local scrollbar = {} + +function scrollbar.new(opts) + local self = setmetatable({}, { __index = scrollbar }) + self.win = require('blink.cmp.lib.window.scrollbar.win').new(opts) + return self +end + +function scrollbar:is_visible() return self.win:is_visible() end + +function scrollbar:update(target_win) + if target_win == nil or not vim.api.nvim_win_is_valid(target_win) then return self.win:hide() end + + local geometry = require('blink.cmp.lib.window.scrollbar.geometry').get_geometry(target_win) + if geometry.should_hide then return self.win:hide() end + + self.win:show_thumb(geometry.thumb) + self.win:show_gutter(geometry.gutter) +end + +return scrollbar diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/win.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/win.lua new file mode 100644 index 0000000..9ac3193 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/lib/window/scrollbar/win.lua @@ -0,0 +1,107 @@ +--- Manages creating/updating scrollbar gutter and thumb windows + +--- @class blink.cmp.ScrollbarWin +--- @field enable_gutter boolean +--- @field thumb_win? number +--- @field gutter_win? number +--- @field buf? number +--- +--- @field new fun(opts: blink.cmp.ScrollbarConfig): blink.cmp.ScrollbarWin +--- @field is_visible fun(self: blink.cmp.ScrollbarWin): boolean +--- @field show_thumb fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry) +--- @field show_gutter fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry) +--- @field hide_thumb fun(self: blink.cmp.ScrollbarWin) +--- @field hide_gutter fun(self: blink.cmp.ScrollbarWin) +--- @field hide fun(self: blink.cmp.ScrollbarWin) +--- @field _make_win fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry, hl_group: string): number +--- @field redraw_if_needed fun(self: blink.cmp.ScrollbarWin) + +--- @type blink.cmp.ScrollbarWin +--- @diagnostic disable-next-line: missing-fields +local scrollbar_win = {} + +function scrollbar_win.new(opts) return setmetatable(opts, { __index = scrollbar_win }) end + +function scrollbar_win:is_visible() return self.thumb_win ~= nil and vim.api.nvim_win_is_valid(self.thumb_win) end + +function scrollbar_win:show_thumb(geometry) + -- create window if it doesn't exist + if self.thumb_win == nil or not vim.api.nvim_win_is_valid(self.thumb_win) then + self.thumb_win = self:_make_win(geometry, 'BlinkCmpScrollBarThumb') + else + -- update with the geometry + local thumb_existing_config = vim.api.nvim_win_get_config(self.thumb_win) + local thumb_config = vim.tbl_deep_extend('force', thumb_existing_config, geometry) + vim.api.nvim_win_set_config(self.thumb_win, thumb_config) + end + + self:redraw_if_needed() +end + +function scrollbar_win:show_gutter(geometry) + if not self.enable_gutter then return end + + -- create window if it doesn't exist + if self.gutter_win == nil or not vim.api.nvim_win_is_valid(self.gutter_win) then + self.gutter_win = self:_make_win(geometry, 'BlinkCmpScrollBarGutter') + else + -- update with the geometry + local gutter_existing_config = vim.api.nvim_win_get_config(self.gutter_win) + local gutter_config = vim.tbl_deep_extend('force', gutter_existing_config, geometry) + vim.api.nvim_win_set_config(self.gutter_win, gutter_config) + end + + self:redraw_if_needed() +end + +function scrollbar_win:hide_thumb() + if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim_win_close(self.thumb_win, true) + self.thumb_win = nil + self:redraw_if_needed() + end +end + +function scrollbar_win:hide_gutter() + if self.gutter_win and vim.api.nvim_win_is_valid(self.gutter_win) then + vim.api.nvim_win_close(self.gutter_win, true) + self.gutter_win = nil + self:redraw_if_needed() + end +end + +function scrollbar_win:hide() + self:hide_thumb() + self:hide_gutter() +end + +function scrollbar_win:_make_win(geometry, hl_group) + if self.buf == nil or not vim.api.nvim_buf_is_valid(self.buf) then self.buf = vim.api.nvim_create_buf(false, true) end + + local win_config = vim.tbl_deep_extend('force', geometry, { + style = 'minimal', + focusable = false, + noautocmd = true, + }) + local win = vim.api.nvim_open_win(self.buf, false, win_config) + vim.api.nvim_set_option_value('winhighlight', 'Normal:' .. hl_group .. ',EndOfBuffer:' .. hl_group, { win = win }) + return win +end + +local redraw_queued = false +function scrollbar_win:redraw_if_needed() + if redraw_queued or vim.api.nvim_get_mode().mode ~= 'c' then return end + + redraw_queued = true + vim.schedule(function() + redraw_queued = false + if self.gutter_win ~= nil and vim.api.nvim_win_is_valid(self.gutter_win) then + vim.api.nvim__redraw({ win = self.gutter_win, flush = true }) + end + if self.thumb_win ~= nil and vim.api.nvim_win_is_valid(self.thumb_win) then + vim.api.nvim__redraw({ win = self.thumb_win, flush = true }) + end + end) +end + +return scrollbar_win diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/init.lua new file mode 100644 index 0000000..8c70032 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/init.lua @@ -0,0 +1,25 @@ +local signature = {} + +function signature.setup() + local trigger = require('blink.cmp.signature.trigger') + trigger.activate() + local window = require('blink.cmp.signature.window') + + local sources = require('blink.cmp.sources.lib') + + trigger.show_emitter:on(function(event) + local context = event.context + sources.cancel_signature_help() + sources.get_signature_help(context, function(signature_help) + if signature_help ~= nil and trigger.context ~= nil and trigger.context.id == context.id then + trigger.set_active_signature_help(signature_help) + window.open_with_signature_help(context, signature_help) + else + trigger.hide() + end + end) + end) + trigger.hide_emitter:on(function() window.close() end) +end + +return signature diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/list.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/list.lua new file mode 100644 index 0000000..3a07f4a --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/list.lua @@ -0,0 +1 @@ +-- TODO: manage signature help state diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/trigger.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/trigger.lua new file mode 100644 index 0000000..2d874c5 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/trigger.lua @@ -0,0 +1,136 @@ +-- Handles hiding and showing the signature help window. When a user types a trigger character +-- (provided by the sources), we create a new `context`. This can be used downstream to determine +-- if we should make new requests to the sources or not. When a user types a re-trigger character, +-- we update the context's re-trigger counter. +-- TODO: ensure this always calls *after* the completion trigger to avoid increasing latency + +--- @class blink.cmp.SignatureHelpContext +--- @field id number +--- @field bufnr number +--- @field cursor number[] +--- @field line string +--- @field is_retrigger boolean +--- @field active_signature_help lsp.SignatureHelp | nil +--- @field trigger { kind: lsp.SignatureHelpTriggerKind, character?: string } + +--- @class blink.cmp.SignatureTrigger +--- @field current_context_id number +--- @field context? blink.cmp.SignatureHelpContext +--- @field show_emitter blink.cmp.EventEmitter<{ context: blink.cmp.SignatureHelpContext }> +--- @field hide_emitter blink.cmp.EventEmitter<{}> +--- @field buffer_events? blink.cmp.BufferEvents +--- +--- @field activate fun() +--- @field is_trigger_character fun(char: string, is_retrigger?: boolean): boolean +--- @field show_if_on_trigger_character fun() +--- @field show fun(opts?: { trigger_character: string }) +--- @field hide fun() +--- @field set_active_signature_help fun(signature_help: lsp.SignatureHelp) + +local config = require('blink.cmp.config').signature.trigger + +--- @type blink.cmp.SignatureTrigger +--- @diagnostic disable-next-line: missing-fields +local trigger = { + current_context_id = -1, + --- @type blink.cmp.SignatureHelpContext | nil + context = nil, + show_emitter = require('blink.cmp.lib.event_emitter').new('signature_help_show'), + hide_emitter = require('blink.cmp.lib.event_emitter').new('signature_help_hide'), +} + +function trigger.activate() + trigger.buffer_events = require('blink.cmp.lib.buffer_events').new({ + show_in_snippet = true, + has_context = function() return trigger.context ~= nil end, + }) + trigger.buffer_events:listen({ + on_char_added = function() + local cursor_col = vim.api.nvim_win_get_cursor(0)[2] + local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) + + -- ignore if disabled + if not require('blink.cmp.config').enabled() then + return trigger.hide() + -- character forces a trigger according to the sources, refresh the existing context if it exists + elseif trigger.is_trigger_character(char_under_cursor) then + return trigger.show({ trigger_character = char_under_cursor }) + -- character forces a re-trigger according to the sources, show if we have a context + elseif trigger.is_trigger_character(char_under_cursor, true) and trigger.context ~= nil then + return trigger.show() + end + end, + on_cursor_moved = function(event) + local cursor_col = vim.api.nvim_win_get_cursor(0)[2] + local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) + local is_on_trigger = trigger.is_trigger_character(char_under_cursor) + + if config.show_on_insert_on_trigger_character and is_on_trigger and event == 'InsertEnter' then + trigger.show({ trigger_character = char_under_cursor }) + elseif event == 'CursorMoved' and trigger.context ~= nil then + trigger.show() + end + end, + on_insert_leave = function() trigger.hide() end, + }) +end + +function trigger.is_trigger_character(char, is_retrigger) + -- TODO: should the get_mode() be moved to sources or somewhere else? + local mode = require('blink.cmp.completion.trigger.context').get_mode() + + local res = require('blink.cmp.sources.lib').get_signature_help_trigger_characters(mode) + local trigger_characters = is_retrigger and res.retrigger_characters or res.trigger_characters + local is_trigger = vim.tbl_contains(trigger_characters, char) + + local blocked_trigger_characters = is_retrigger and config.blocked_retrigger_characters + or config.blocked_trigger_characters + local is_blocked = vim.tbl_contains(blocked_trigger_characters, char) + + return is_trigger and not is_blocked +end + +function trigger.show_if_on_trigger_character() + if require('blink.cmp.completion.trigger.context').get_mode() ~= 'default' then return end + + local cursor_col = vim.api.nvim_win_get_cursor(0)[2] + local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) + if trigger.is_trigger_character(char_under_cursor) then trigger.show({ trigger_character = char_under_cursor }) end +end + +function trigger.show(opts) + opts = opts or {} + + -- update context + local cursor = vim.api.nvim_win_get_cursor(0) + if trigger.context == nil then trigger.current_context_id = trigger.current_context_id + 1 end + trigger.context = { + id = trigger.current_context_id, + bufnr = vim.api.nvim_get_current_buf(), + cursor = cursor, + line = vim.api.nvim_buf_get_lines(0, cursor[1] - 1, cursor[1], false)[1], + trigger = { + kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter + or vim.lsp.protocol.CompletionTriggerKind.Invoked, + character = opts.trigger_character, + }, + is_retrigger = trigger.context ~= nil, + active_signature_help = trigger.context and trigger.context.active_signature_help or nil, + } + + trigger.show_emitter:emit({ context = trigger.context }) +end + +function trigger.hide() + if not trigger.context then return end + + trigger.context = nil + trigger.hide_emitter:emit() +end + +function trigger.set_active_signature_help(signature_help) + if not trigger.context then return end + trigger.context.active_signature_help = signature_help +end + +return trigger diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/window.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/window.lua new file mode 100644 index 0000000..59a60b0 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/signature/window.lua @@ -0,0 +1,160 @@ +--- @class blink.cmp.SignatureWindow +--- @field win blink.cmp.Window +--- @field context? blink.cmp.SignatureHelpContext +--- +--- @field open_with_signature_help fun(context: blink.cmp.SignatureHelpContext, signature_help?: lsp.SignatureHelp) +--- @field close fun() +--- @field scroll_up fun(amount: number) +--- @field scroll_down fun(amount: number) +--- @field update_position fun() + +local config = require('blink.cmp.config').signature.window +local sources = require('blink.cmp.sources.lib') +local menu = require('blink.cmp.completion.windows.menu') + +local signature = { + win = require('blink.cmp.lib.window').new({ + min_width = config.min_width, + max_width = config.max_width, + max_height = config.max_height, + border = config.border, + winblend = config.winblend, + winhighlight = config.winhighlight, + scrollbar = config.scrollbar, + wrap = true, + filetype = 'blink-cmp-signature', + }), + context = nil, +} + +-- todo: deduplicate this +menu.position_update_emitter:on(function() signature.update_position() end) +vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, { + callback = function() + if signature.context then signature.update_position() end + end, +}) + +--- @param context blink.cmp.SignatureHelpContext +--- @param signature_help lsp.SignatureHelp | nil +function signature.open_with_signature_help(context, signature_help) + signature.context = context + -- check if there are any signatures in signature_help, since + -- convert_signature_help_to_markdown_lines errors with no signatures + if + signature_help == nil + or #signature_help.signatures == 0 + or signature_help.signatures[(signature_help.activeSignature or 0) + 1] == nil + then + signature.win:close() + return + end + + local active_signature = signature_help.signatures[(signature_help.activeSignature or 0) + 1] + + local labels = vim.tbl_map(function(signature) return signature.label end, signature_help.signatures) + + if signature.shown_signature ~= active_signature then + require('blink.cmp.lib.window.docs').render_detail_and_documentation({ + bufnr = signature.win:get_buf(), + detail = labels, + documentation = active_signature.documentation, + max_width = config.max_width, + use_treesitter_highlighting = config.treesitter_highlighting, + }) + end + signature.shown_signature = active_signature + + -- highlight active parameter + local _, active_highlight = vim.lsp.util.convert_signature_help_to_markdown_lines( + signature_help, + vim.bo.filetype, + sources.get_signature_help_trigger_characters().trigger_characters + ) + if active_highlight ~= nil then + -- TODO: nvim 0.11+ returns the start and end line which we should use + local start_region = vim.fn.has('nvim-0.11.0') == 1 and active_highlight[2] or active_highlight[1] + local end_region = vim.fn.has('nvim-0.11.0') == 1 and active_highlight[4] or active_highlight[2] + + vim.api.nvim_buf_add_highlight( + signature.win:get_buf(), + require('blink.cmp.config').appearance.highlight_ns, + 'BlinkCmpSignatureHelpActiveParameter', + 0, + start_region, + end_region + ) + end + + signature.win:open() + signature.update_position() +end + +function signature.close() + if not signature.win:is_open() then return end + signature.win:close() +end + +function signature.scroll_up(amount) + local winnr = signature.win:get_win() + local top_line = math.max(1, vim.fn.line('w0', winnr) - 1) + local desired_line = math.max(1, top_line - amount) + + vim.api.nvim_win_set_cursor(signature.win:get_win(), { desired_line, 0 }) +end + +function signature.scroll_down(amount) + local winnr = signature.win:get_win() + local line_count = vim.api.nvim_buf_line_count(signature.win:get_buf()) + local bottom_line = math.max(1, vim.fn.line('w$', winnr) + 1) + local desired_line = math.min(line_count, bottom_line + amount) + + vim.api.nvim_win_set_cursor(signature.win:get_win(), { desired_line, 0 }) +end + +function signature.update_position() + local win = signature.win + if not win:is_open() then return end + local winnr = win:get_win() + + win:update_size() + + local direction_priority = config.direction_priority + + -- if the menu window is open, we want to place the signature window on the opposite side + local menu_win_config = menu.win:get_win() and vim.api.nvim_win_get_config(menu.win:get_win()) + if menu.win:is_open() then + local cursor_screen_row = vim.fn.winline() + local menu_win_is_up = menu_win_config.row - cursor_screen_row < 0 + direction_priority = menu_win_is_up and { 's' } or { 'n' } + end + + local pos = win:get_vertical_direction_and_height(direction_priority) + + -- couldn't find anywhere to place the window + if not pos then + win:close() + return + end + + -- set height + vim.api.nvim_win_set_height(winnr, pos.height) + local height = win:get_height() + + -- default to the user's preference but attempt to use the other options + if menu_win_config then + assert(menu_win_config.relative == 'win', 'The menu window must be relative to a window') + local cursor_screen_row = vim.fn.winline() + local menu_win_is_up = menu_win_config.row - cursor_screen_row < 0 + vim.api.nvim_win_set_config(winnr, { + relative = menu_win_config.relative, + win = menu_win_config.win, + row = menu_win_is_up and menu_win_config.row + menu.win:get_height() + 1 or menu_win_config.row - height - 1, + col = menu_win_config.col, + }) + else + vim.api.nvim_win_set_config(winnr, { relative = 'cursor', row = pos.direction == 's' and 1 or -height, col = 0 }) + end +end + +return signature diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/buffer.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/buffer.lua new file mode 100644 index 0000000..bb679f2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/buffer.lua @@ -0,0 +1,118 @@ +-- todo: nvim-cmp only updates the lines that got changed which is better +-- but this is *speeeeeed* and simple. should add the better way +-- but ensure it doesn't add too much complexity + +local uv = vim.uv + +--- @param bufnr integer +--- @return string +local function get_buf_text(bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + if bufnr ~= vim.api.nvim_get_current_buf() then return table.concat(lines, '\n') end + + -- exclude word under the cursor for the current buffer + local line_number = vim.api.nvim_win_get_cursor(0)[1] + local column = vim.api.nvim_win_get_cursor(0)[2] + local line = lines[line_number] + local start_col = column + while start_col > 1 do + local char = line:sub(start_col, start_col) + if char:match('[%w_\\-]') == nil then break end + start_col = start_col - 1 + end + local end_col = column + while end_col < #line do + local char = line:sub(end_col + 1, end_col + 1) + if char:match('[%w_\\-]') == nil then break end + end_col = end_col + 1 + end + lines[line_number] = line:sub(1, start_col) .. ' ' .. line:sub(end_col + 1) + + return table.concat(lines, '\n') +end + +local function words_to_items(words) + local items = {} + for _, word in ipairs(words) do + table.insert(items, { + label = word, + kind = require('blink.cmp.types').CompletionItemKind.Text, + insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, + insertText = word, + }) + end + return items +end + +--- @param buf_text string +--- @param callback fun(items: blink.cmp.CompletionItem[]) +local function run_sync(buf_text, callback) callback(words_to_items(require('blink.cmp.fuzzy').get_words(buf_text))) end + +local function run_async(buf_text, callback) + local worker = uv.new_work( + -- must use ffi directly since the normal one requires the config which isnt present + function(items, cpath) + package.cpath = cpath + return table.concat(require('blink.cmp.fuzzy.rust').get_words(items), '\n') + end, + function(words) + local items = words_to_items(vim.split(words, '\n')) + vim.schedule(function() callback(items) end) + end + ) + worker:queue(buf_text, package.cpath) +end + +--- @class blink.cmp.BufferOpts +--- @field get_bufnrs fun(): integer[] + +--- Public API + +local buffer = {} + +function buffer.new(opts) + --- @cast opts blink.cmp.BufferOpts + + local self = setmetatable({}, { __index = buffer }) + self.get_bufnrs = opts.get_bufnrs + or function() + return vim + .iter(vim.api.nvim_list_wins()) + :map(function(win) return vim.api.nvim_win_get_buf(win) end) + :filter(function(buf) return vim.bo[buf].buftype ~= 'nofile' end) + :totable() + end + return self +end + +function buffer:get_completions(_, callback) + local transformed_callback = function(items) + callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = items }) + end + + vim.schedule(function() + local bufnrs = require('blink.cmp.lib.utils').deduplicate(self.get_bufnrs()) + local buf_texts = {} + for _, buf in ipairs(bufnrs) do + table.insert(buf_texts, get_buf_text(buf)) + end + local buf_text = table.concat(buf_texts, '\n') + + -- should take less than 2ms + if #buf_text < 20000 then + run_sync(buf_text, transformed_callback) + -- should take less than 10ms + elseif #buf_text < 500000 then + run_async(buf_text, transformed_callback) + -- too big so ignore + else + transformed_callback({}) + end + end) + + -- TODO: cancel run_async + return function() end +end + +return buffer diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/constants.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/constants.lua new file mode 100644 index 0000000..fd2ee85 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/constants.lua @@ -0,0 +1,40 @@ +return { + help_commands = { + 'help', + 'hel', + 'he', + 'h', + }, + file_commands = { + 'edit', + 'e', + 'read', + 'r', + 'write', + 'w', + 'saveas', + 'sav', + 'split', + 'sp', + 'vsplit', + 'vs', + 'tabedit', + 'tabe', + 'badd', + 'bad', + 'next', + 'n', + 'previous', + 'prev', + 'args', + 'source', + 'so', + 'find', + 'fin', + 'diffsplit', + 'diffs', + 'diffpatch', + 'diffp', + 'make', + }, +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/help.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/help.lua new file mode 100644 index 0000000..bae4988 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/help.lua @@ -0,0 +1,53 @@ +local async = require('blink.cmp.lib.async') + +local help = {} + +--- Processes a help file and returns a list of tags asynchronously +--- @param file string +--- @return blink.cmp.Task +--- TODO: rewrite using async lib, shared as a library in lib/fs.lua +local function read_tags_from_file(file) + return async.task.new(function(resolve) + vim.uv.fs_open(file, 'r', 438, function(err, fd) + if err or fd == nil then return resolve({}) end + + -- Read file content + vim.uv.fs_fstat(fd, function(stat_err, stat) + if stat_err or stat == nil then + vim.uv.fs_close(fd) + return resolve({}) + end + + vim.uv.fs_read(fd, stat.size, 0, function(read_err, data) + vim.uv.fs_close(fd) + + if read_err or data == nil then return resolve({}) end + + -- Process the file content + local tags = {} + for line in data:gmatch('[^\r\n]+') do + local tag = line:match('^([^\t]+)') + if tag then table.insert(tags, tag) end + end + + resolve(tags) + end) + end) + end) + end) +end + +--- @param arg_prefix string +function help.get_completions(arg_prefix) + local help_files = vim.api.nvim_get_runtime_file('doc/tags', true) + + return async.task + .await_all(vim.tbl_map(read_tags_from_file, help_files)) + :map(function(tags_arrs) return require('blink.cmp.lib.utils').flatten(tags_arrs) end) + :map(function(tags) + -- TODO: remove after adding support for fuzzy matching on custom range + return vim.tbl_filter(function(tag) return vim.startswith(tag, arg_prefix) end, tags) + end) +end + +return help diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/init.lua new file mode 100644 index 0000000..5ff5e11 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/cmdline/init.lua @@ -0,0 +1,107 @@ +-- Credit goes to @hrsh7th for the code that this was based on +-- https://github.com/hrsh7th/cmp-cmdline +-- License: MIT + +local async = require('blink.cmp.lib.async') +local constants = require('blink.cmp.sources.cmdline.constants') + +--- @class blink.cmp.Source +local cmdline = {} + +function cmdline.new() + local self = setmetatable({}, { __index = cmdline }) + self.before_line = '' + self.offset = -1 + self.ctype = '' + self.items = {} + return self +end + +function cmdline:get_trigger_characters() return { ' ', '.', '#', '-', '=', '/', ':' } end + +function cmdline:get_completions(context, callback) + local arguments = vim.split(context.line, ' ', { plain = true }) + local arg_number = #vim.split(context.line:sub(1, context.cursor[2] + 1), ' ', { plain = true }) + local text_before_argument = table.concat(require('blink.cmp.lib.utils').slice(arguments, 1, arg_number - 1), ' ') + .. (arg_number > 1 and ' ' or '') + + local current_arg = arguments[arg_number] + local keyword_config = require('blink.cmp.config').completion.keyword + local keyword = context.get_bounds(keyword_config.range) + local current_arg_prefix = current_arg:sub(1, keyword.start_col - #text_before_argument - 1) + + local task = async.task + .empty() + :map(function() + -- Special case for help where we read all the tags ourselves + if vim.tbl_contains(constants.help_commands, arguments[1] or '') then + return require('blink.cmp.sources.cmdline.help').get_completions(current_arg_prefix) + end + + local completions = {} + local completion_type = vim.fn.getcmdcompltype() + -- Handle custom completions explicitly, since otherwise they won't work in input() mode (getcmdtype() == '@') + if vim.startswith(completion_type, 'custom,') or vim.startswith(completion_type, 'customlist,') then + local fun = completion_type:gsub('custom,', ''):gsub('customlist,', '') + completions = vim.fn.call(fun, { current_arg_prefix, vim.fn.getcmdline(), vim.fn.getcmdpos() }) + -- `custom,` type returns a string, delimited by newlines + if type(completions) == 'string' then completions = vim.split(completions, '\n') end + else + local query = (text_before_argument .. current_arg_prefix):gsub([[\\]], [[\\\\]]) + completions = vim.fn.getcompletion(query, 'cmdline') + end + + -- Special case for files, escape special characters + if vim.tbl_contains(constants.file_commands, arguments[1] or '') then + completions = vim.tbl_map(function(completion) return vim.fn.fnameescape(completion) end, completions) + end + + return completions + end) + :map(function(completions) + local items = {} + for _, completion in ipairs(completions) do + local has_prefix = string.find(completion, current_arg_prefix, 1, true) == 1 + + -- remove prefix from the filter text + local filter_text = completion + if has_prefix then filter_text = completion:sub(#current_arg_prefix + 1) end + + -- for lua, use the filter text as the label since it doesn't include the prefix + local label = arguments[1] == 'lua' and filter_text or completion + + -- add prefix to the newText + local new_text = completion + if not has_prefix then new_text = current_arg_prefix .. completion end + + table.insert(items, { + label = label, + filterText = filter_text, + -- move items starting with special characters to the end of the list + sortText = label:lower():gsub('^([!-@\\[-`])', '~%1'), + textEdit = { + newText = new_text, + range = { + start = { line = 0, character = #text_before_argument }, + ['end'] = { line = 0, character = #text_before_argument + #current_arg }, + }, + }, + kind = require('blink.cmp.types').CompletionItemKind.Property, + }) + end + + callback({ + is_incomplete_backward = true, + is_incomplete_forward = false, + items = items, + }) + end) + :catch(function(err) + vim.notify('Error while fetching completions: ' .. err, vim.log.levels.ERROR, { title = 'blink.cmp' }) + callback({ is_incomplete_backward = false, is_incomplete_forward = false, items = {} }) + end) + + return function() task:cancel() end +end + +return cmdline diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/init.lua new file mode 100644 index 0000000..9b56cf5 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/init.lua @@ -0,0 +1,304 @@ +local async = require('blink.cmp.lib.async') +local config = require('blink.cmp.config') + +--- @class blink.cmp.Sources +--- @field completions_queue blink.cmp.SourcesQueue | nil +--- @field current_signature_help blink.cmp.Task | nil +--- @field sources_registered boolean +--- @field providers table<string, blink.cmp.SourceProvider> +--- @field completions_emitter blink.cmp.EventEmitter<blink.cmp.SourceCompletionsEvent> +--- +--- @field get_all_providers fun(): blink.cmp.SourceProvider[] +--- @field get_enabled_provider_ids fun(mode: blink.cmp.Mode): string[] +--- @field get_enabled_providers fun(mode: blink.cmp.Mode): table<string, blink.cmp.SourceProvider> +--- @field get_provider_by_id fun(id: string): blink.cmp.SourceProvider +--- @field get_trigger_characters fun(mode: blink.cmp.Mode): string[] +--- +--- @field emit_completions fun(context: blink.cmp.Context, responses: table<string, blink.cmp.CompletionResponse>) +--- @field request_completions fun(context: blink.cmp.Context) +--- @field cancel_completions fun() +--- @field apply_max_items_for_completions fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] +--- @field listen_on_completions fun(callback: fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[])) +--- @field resolve fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem): blink.cmp.Task +--- @field execute fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem): blink.cmp.Task +--- +--- @field get_signature_help_trigger_characters fun(mode: blink.cmp.Mode): { trigger_characters: string[], retrigger_characters: string[] } +--- @field get_signature_help fun(context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)) +--- @field cancel_signature_help fun() +--- +--- @field reload fun(provider?: string) +--- @field get_lsp_capabilities fun(override?: lsp.ClientCapabilities, include_nvim_defaults?: boolean): lsp.ClientCapabilities + +--- @class blink.cmp.SourceCompletionsEvent +--- @field context blink.cmp.Context +--- @field items table<string, blink.cmp.CompletionItem[]> + +--- @type blink.cmp.Sources +--- @diagnostic disable-next-line: missing-fields +local sources = { + completions_queue = nil, + providers = {}, + completions_emitter = require('blink.cmp.lib.event_emitter').new('source_completions', 'BlinkCmpSourceCompletions'), +} + +function sources.get_all_providers() + local providers = {} + for provider_id, _ in pairs(config.sources.providers) do + providers[provider_id] = sources.get_provider_by_id(provider_id) + end + return providers +end + +function sources.get_enabled_provider_ids(mode) + local enabled_providers = mode ~= 'default' and config.sources[mode] + or config.sources.per_filetype[vim.bo.filetype] + or config.sources.default + if type(enabled_providers) == 'function' then return enabled_providers() end + --- @cast enabled_providers string[] + return enabled_providers +end + +function sources.get_enabled_providers(mode) + local mode_providers = sources.get_enabled_provider_ids(mode) + + --- @type table<string, blink.cmp.SourceProvider> + local providers = {} + for _, provider_id in ipairs(mode_providers) do + local provider = sources.get_provider_by_id(provider_id) + if provider:enabled() then providers[provider_id] = sources.get_provider_by_id(provider_id) end + end + return providers +end + +function sources.get_provider_by_id(provider_id) + -- TODO: remove in v1.0 + if not sources.providers[provider_id] and provider_id == 'luasnip' then + error( + "Luasnip has been moved to the `snippets` source, alongside a new preset system (`snippets.preset = 'luasnip'`). See the documentation for more information." + ) + end + + assert( + sources.providers[provider_id] ~= nil or config.sources.providers[provider_id] ~= nil, + 'Requested provider "' + .. provider_id + .. '" has not been configured. Available providers: ' + .. vim.fn.join(vim.tbl_keys(sources.providers), ', ') + ) + + -- initialize the provider if it hasn't been initialized yet + if not sources.providers[provider_id] then + local provider_config = config.sources.providers[provider_id] + sources.providers[provider_id] = require('blink.cmp.sources.lib.provider').new(provider_id, provider_config) + end + + return sources.providers[provider_id] +end + +--- Completion --- + +function sources.get_trigger_characters(mode) + local providers = sources.get_enabled_providers(mode) + local trigger_characters = {} + for _, provider in pairs(providers) do + vim.list_extend(trigger_characters, provider:get_trigger_characters()) + end + return trigger_characters +end + +function sources.emit_completions(context, _items_by_provider) + local items_by_provider = {} + for id, items in pairs(_items_by_provider) do + if sources.providers[id]:should_show_items(context, items) then items_by_provider[id] = items end + end + sources.completions_emitter:emit({ context = context, items = items_by_provider }) +end + +function sources.request_completions(context) + -- create a new context if the id changed or if we haven't created one yet + if sources.completions_queue == nil or context.id ~= sources.completions_queue.id then + if sources.completions_queue ~= nil then sources.completions_queue:destroy() end + sources.completions_queue = + require('blink.cmp.sources.lib.queue').new(context, sources.get_all_providers(), sources.emit_completions) + -- send cached completions if they exist to immediately trigger updates + elseif sources.completions_queue:get_cached_completions() ~= nil then + sources.emit_completions( + context, + --- @diagnostic disable-next-line: param-type-mismatch + sources.completions_queue:get_cached_completions() + ) + end + + sources.completions_queue:get_completions(context) +end + +function sources.cancel_completions() + if sources.completions_queue ~= nil then + sources.completions_queue:destroy() + sources.completions_queue = nil + end +end + +--- Limits the number of items per source as configured +function sources.apply_max_items_for_completions(context, items) + -- get the configured max items for each source + local total_items_for_sources = {} + local max_items_for_sources = {} + for id, source in pairs(sources.providers) do + max_items_for_sources[id] = source.config.max_items(context, items) + total_items_for_sources[id] = 0 + end + + -- no max items configured, return as-is + if #vim.tbl_keys(max_items_for_sources) == 0 then return items end + + -- apply max items + local filtered_items = {} + for _, item in ipairs(items) do + local max_items = max_items_for_sources[item.source_id] + total_items_for_sources[item.source_id] = total_items_for_sources[item.source_id] + 1 + if max_items == nil or total_items_for_sources[item.source_id] <= max_items then + table.insert(filtered_items, item) + end + end + return filtered_items +end + +--- Resolve --- + +function sources.resolve(context, item) + --- @type blink.cmp.SourceProvider? + local item_source = nil + for _, source in pairs(sources.providers) do + if source.id == item.source_id then + item_source = source + break + end + end + if item_source == nil then + return async.task.new(function(resolve) resolve(item) end) + end + + return item_source + :resolve(context, item) + :catch(function(err) vim.print('failed to resolve item with error: ' .. err) end) +end + +--- Execute --- + +function sources.execute(context, item) + local item_source = nil + for _, source in pairs(sources.providers) do + if source.id == item.source_id then + item_source = source + break + end + end + if item_source == nil then + return async.task.new(function(resolve) resolve() end) + end + + return item_source + :execute(context, item) + :catch(function(err) vim.print('failed to execute item with error: ' .. err) end) +end + +--- Signature help --- + +function sources.get_signature_help_trigger_characters(mode) + local trigger_characters = {} + local retrigger_characters = {} + + -- todo: should this be all sources? or should it follow fallbacks? + for _, source in pairs(sources.get_enabled_providers(mode)) do + local res = source:get_signature_help_trigger_characters() + vim.list_extend(trigger_characters, res.trigger_characters) + vim.list_extend(retrigger_characters, res.retrigger_characters) + end + return { trigger_characters = trigger_characters, retrigger_characters = retrigger_characters } +end + +function sources.get_signature_help(context, callback) + local tasks = {} + for _, source in pairs(sources.providers) do + table.insert(tasks, source:get_signature_help(context)) + end + + sources.current_signature_help = async.task.await_all(tasks):map(function(signature_helps) + signature_helps = vim.tbl_filter(function(signature_help) return signature_help ~= nil end, signature_helps) + callback(signature_helps[1]) + end) +end + +function sources.cancel_signature_help() + if sources.current_signature_help ~= nil then + sources.current_signature_help:cancel() + sources.current_signature_help = nil + end +end + +--- Misc --- + +--- For external integrations to force reloading the source +function sources.reload(provider) + -- Reload specific provider + if provider ~= nil then + assert(type(provider) == 'string', 'Expected string for provider') + assert( + sources.providers[provider] ~= nil or config.sources.providers[provider] ~= nil, + 'Source ' .. provider .. ' does not exist' + ) + if sources.providers[provider] ~= nil then sources.providers[provider]:reload() end + return + end + + -- Reload all providers + for _, source in pairs(sources.providers) do + source:reload() + end +end + +function sources.get_lsp_capabilities(override, include_nvim_defaults) + return vim.tbl_deep_extend('force', include_nvim_defaults and vim.lsp.protocol.make_client_capabilities() or {}, { + textDocument = { + completion = { + completionItem = { + snippetSupport = true, + commitCharactersSupport = false, -- todo: + documentationFormat = { 'markdown', 'plaintext' }, + deprecatedSupport = true, + preselectSupport = false, -- todo: + tagSupport = { valueSet = { 1 } }, -- deprecated + insertReplaceSupport = true, -- todo: + resolveSupport = { + properties = { + 'documentation', + 'detail', + 'additionalTextEdits', + -- todo: support more properties? should test if it improves latency + }, + }, + insertTextModeSupport = { + -- todo: support adjustIndentation + valueSet = { 1 }, -- asIs + }, + labelDetailsSupport = true, + }, + completionList = { + itemDefaults = { + 'commitCharacters', + 'editRange', + 'insertTextFormat', + 'insertTextMode', + 'data', + }, + }, + + contextSupport = true, + insertTextMode = 1, -- asIs + }, + }, + }, override or {}) +end + +return sources diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/config.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/config.lua new file mode 100644 index 0000000..cda6a52 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/config.lua @@ -0,0 +1,46 @@ +--- @class blink.cmp.SourceProviderConfigWrapper +--- @field new fun(config: blink.cmp.SourceProviderConfig): blink.cmp.SourceProviderConfigWrapper +--- +--- @field name string +--- @field module string +--- @field enabled fun(): boolean +--- @field async fun(ctx: blink.cmp.Context): boolean +--- @field timeout_ms fun(ctx: blink.cmp.Context): number +--- @field transform_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] +--- @field should_show_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean +--- @field max_items? fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): number +--- @field min_keyword_length fun(ctx: blink.cmp.Context): number +--- @field fallbacks fun(ctx: blink.cmp.Context): string[] +--- @field score_offset fun(ctx: blink.cmp.Context): number + +--- @class blink.cmp.SourceProviderConfigWrapper +--- @diagnostic disable-next-line: missing-fields +local wrapper = {} + +function wrapper.new(config) + local function call_or_get(fn_or_val, default) + if fn_or_val == nil then + return function() return default end + end + return function(...) + if type(fn_or_val) == 'function' then return fn_or_val(...) end + return fn_or_val + end + end + + local self = setmetatable({}, { __index = config }) + self.name = config.name + self.module = config.module + self.enabled = call_or_get(config.enabled, true) + self.async = call_or_get(config.async, false) + self.timeout_ms = call_or_get(config.timeout_ms, 2000) + self.transform_items = config.transform_items or function(_, items) return items end + self.should_show_items = call_or_get(config.should_show_items, true) + self.max_items = call_or_get(config.max_items, nil) + self.min_keyword_length = call_or_get(config.min_keyword_length, 0) + self.fallbacks = call_or_get(config.fallbacks, {}) + self.score_offset = call_or_get(config.score_offset, 0) + return self +end + +return wrapper diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/init.lua new file mode 100644 index 0000000..0494dcc --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/init.lua @@ -0,0 +1,179 @@ +--- Wraps the sources to respect the configuration options and provide a unified interface +--- @class blink.cmp.SourceProvider +--- @field id string +--- @field name string +--- @field config blink.cmp.SourceProviderConfigWrapper +--- @field module blink.cmp.Source +--- @field list blink.cmp.SourceProviderList | nil +--- @field resolve_tasks table<blink.cmp.CompletionItem, blink.cmp.Task> +--- +--- @field new fun(id: string, config: blink.cmp.SourceProviderConfig): blink.cmp.SourceProvider +--- @field enabled fun(self: blink.cmp.SourceProvider): boolean +--- @field get_trigger_characters fun(self: blink.cmp.SourceProvider): string[] +--- @field get_completions fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, on_items: fun(items: blink.cmp.CompletionItem[], is_cached: boolean)) +--- @field should_show_items fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean +--- @field transform_items fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] +--- @field resolve fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, item: blink.cmp.CompletionItem): blink.cmp.Task +--- @field execute fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, item: blink.cmp.CompletionItem, callback: fun()): blink.cmp.Task +--- @field get_signature_help_trigger_characters fun(self: blink.cmp.SourceProvider): { trigger_characters: string[], retrigger_characters: string[] } +--- @field get_signature_help fun(self: blink.cmp.SourceProvider, context: blink.cmp.SignatureHelpContext): blink.cmp.Task +--- @field reload (fun(self: blink.cmp.SourceProvider): nil) | nil + +--- @type blink.cmp.SourceProvider +--- @diagnostic disable-next-line: missing-fields +local source = {} + +local async = require('blink.cmp.lib.async') + +function source.new(id, config) + assert(type(config.name) == 'string', 'Each source in config.sources.providers must have a "name" of type string') + assert(type(config.module) == 'string', 'Each source in config.sources.providers must have a "module" of type string') + + local self = setmetatable({}, { __index = source }) + self.id = id + self.name = config.name + self.module = require('blink.cmp.sources.lib.provider.override').new( + require(config.module).new(config.opts or {}, config), + config.override + ) + self.config = require('blink.cmp.sources.lib.provider.config').new(config) + self.list = nil + self.resolve_tasks = {} + + return self +end + +function source:enabled() + -- user defined + if not self.config.enabled() then return false end + + -- source defined + if self.module.enabled == nil then return true end + return self.module:enabled() +end + +--- Completion --- + +function source:get_trigger_characters() + if self.module.get_trigger_characters == nil then return {} end + return self.module:get_trigger_characters() +end + +function source:get_completions(context, on_items) + -- return the previous successful completions if the context is the same + -- and the data doesn't need to be updated + -- or if the list is async, since we don't want to cause a flash of no items + if self.list ~= nil and self.list:is_valid_for_context(context) then + self.list:set_on_items(on_items) + self.list:emit(true) + return + end + + -- the source indicates we should refetch when this character is typed + local trigger_character = context.trigger.character + and vim.tbl_contains(self:get_trigger_characters(), context.trigger.character) + + -- The TriggerForIncompleteCompletions kind is handled by the source provider itself + local source_context = require('blink.cmp.lib.utils').shallow_copy(context) + source_context.trigger = trigger_character + and { kind = vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter, character = context.trigger.character } + or { kind = vim.lsp.protocol.CompletionTriggerKind.Invoked } + + local async_initial_items = self.list ~= nil and self.list.context.id == context.id and self.list.items or {} + if self.list ~= nil then self.list:destroy() end + + self.list = require('blink.cmp.sources.lib.provider.list').new( + self, + context, + on_items, + -- HACK: if the source is async, we're not reusing the previous list and the response was marked as incomplete, + -- the user will see a flash of no items from the provider, since the list emits immediately. So we hack around + -- this for now + { async_initial_items = async_initial_items } + ) +end + +function source:should_show_items(context, items) + -- if keyword length is configured, check if the context is long enough + local provider_min_keyword_length = self.config.min_keyword_length(context) + + -- for manual trigger, we ignore the min_keyword_length set globally, but still respect per-provider + local global_min_keyword_length = 0 + if context.trigger.initial_kind ~= 'manual' then + local global_min_keyword_length_func_or_num = require('blink.cmp.config').sources.min_keyword_length + if type(global_min_keyword_length_func_or_num) == 'function' then + global_min_keyword_length = global_min_keyword_length_func_or_num(context) + else + global_min_keyword_length = global_min_keyword_length_func_or_num + end + end + + local min_keyword_length = math.max(provider_min_keyword_length, global_min_keyword_length) + local current_keyword_length = context.bounds.length + if current_keyword_length < min_keyword_length then return false end + + if self.config.should_show_items == nil then return true end + return self.config.should_show_items(context, items) +end + +function source:transform_items(context, items) + if self.config.transform_items ~= nil then items = self.config.transform_items(context, items) end + items = require('blink.cmp.config').sources.transform_items(context, items) + return items +end + +--- Resolve --- + +function source:resolve(context, item) + local tasks = self.resolve_tasks + if tasks[item] == nil or tasks[item].status == async.STATUS.CANCELLED then + tasks[item] = async.task.new(function(resolve) + if self.module.resolve == nil then return resolve(item) end + + return self.module:resolve(item, function(resolved_item) + -- HACK: it's out of spec to update keys not in resolveSupport.properties but some LSPs do it anyway + local merged_item = vim.tbl_deep_extend('force', item, resolved_item or {}) + local transformed_item = self:transform_items(context, { merged_item })[1] or merged_item + vim.schedule(function() resolve(transformed_item) end) + end) + end) + end + return tasks[item] +end + +--- Execute --- + +function source:execute(context, item) + if self.module.execute == nil then + return async.task.new(function(resolve) resolve() end) + end + return async.task.new(function(resolve) self.module:execute(context, item, resolve) end) +end + +--- Signature help --- + +function source:get_signature_help_trigger_characters() + if self.module.get_signature_help_trigger_characters == nil then + return { trigger_characters = {}, retrigger_characters = {} } + end + return self.module:get_signature_help_trigger_characters() +end + +function source:get_signature_help(context) + return async.task.new(function(resolve) + if self.module.get_signature_help == nil then return resolve(nil) end + return self.module:get_signature_help(context, function(signature_help) + vim.schedule(function() resolve(signature_help) end) + end) + end) +end + +--- Misc --- + +--- For external integrations to force reloading the source +function source:reload() + if self.module.reload == nil then return end + self.module:reload() +end + +return source diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/list.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/list.lua new file mode 100644 index 0000000..17d4bdf --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/list.lua @@ -0,0 +1,128 @@ +--- @class blink.cmp.SourceProviderList +--- @field provider blink.cmp.SourceProvider +--- @field context blink.cmp.Context +--- @field items blink.cmp.CompletionItem[] +--- @field on_items fun(items: blink.cmp.CompletionItem[], is_cached: boolean) +--- @field has_completed boolean +--- @field is_incomplete_backward boolean +--- @field is_incomplete_forward boolean +--- @field cancel_completions? fun(): nil +--- +--- @field new fun(provider: blink.cmp.SourceProvider,context: blink.cmp.Context, on_items: fun(items: blink.cmp.CompletionItem[], is_cached: boolean), opts: blink.cmp.SourceProviderListOpts): blink.cmp.SourceProviderList +--- @field append fun(self: blink.cmp.SourceProviderList, response: blink.cmp.CompletionResponse) +--- @field emit fun(self: blink.cmp.SourceProviderList, is_cached?: boolean) +--- @field destroy fun(self: blink.cmp.SourceProviderList): nil +--- @field set_on_items fun(self: blink.cmp.SourceProviderList, on_items: fun(items: blink.cmp.CompletionItem[], is_cached: boolean)) +--- @field is_valid_for_context fun(self: blink.cmp.SourceProviderList, context: blink.cmp.Context): boolean +--- +--- @class blink.cmp.SourceProviderListOpts +--- @field async_initial_items blink.cmp.CompletionItem[] + +--- @type blink.cmp.SourceProviderList +--- @diagnostic disable-next-line: missing-fields +local list = {} + +function list.new(provider, context, on_items, opts) + --- @type blink.cmp.SourceProviderList + local self = setmetatable({ + provider = provider, + context = context, + items = opts.async_initial_items, + on_items = on_items, + + has_completed = false, + is_incomplete_backward = true, + is_incomplete_forward = true, + }, { __index = list }) + + -- Immediately fetch completions + local default_response = { + is_incomplete_forward = true, + is_incomplete_backward = true, + items = {}, + } + if self.provider.module.get_completions == nil then + self:append(default_response) + else + self.cancel_completions = self.provider.module:get_completions( + self.context, + function(response) self:append(response or default_response) end + ) + end + + -- if async, immediately send the default response/initial items + local is_async = self.provider.config.async(self.context) + if is_async and not self.has_completed then self:emit() end + + -- if not async and timeout is set, send the default response after the timeout + local timeout_ms = self.provider.config.timeout_ms(self.context) + if not is_async and timeout_ms > 0 then + vim.defer_fn(function() + if not self.has_completed then self:append(default_response) end + end, timeout_ms) + end + + return self +end + +function list:append(response) + if self.has_completed and #response.items == 0 then return end + + if not self.has_completed then + self.has_completed = true + self.is_incomplete_backward = response.is_incomplete_backward + self.is_incomplete_forward = response.is_incomplete_forward + self.items = {} + end + + -- add metadata and default kind + local source_score_offset = self.provider.config.score_offset(self.context) or 0 + for _, item in ipairs(response.items) do + item.score_offset = (item.score_offset or 0) + source_score_offset + item.cursor_column = self.context.cursor[2] + item.source_id = self.provider.id + item.source_name = self.provider.name + item.kind = item.kind or require('blink.cmp.types').CompletionItemKind.Property + end + + -- combine with existing items + local new_items = {} + vim.list_extend(new_items, self.items) + vim.list_extend(new_items, response.items) + self.items = new_items + + -- run provider-local and global transform_items functions + self.items = self.provider:transform_items(self.context, self.items) + + self:emit() +end + +function list:emit(is_cached) + if is_cached == nil then is_cached = false end + self.on_items(self.items, is_cached) +end + +function list:destroy() + if self.cancel_completions ~= nil then self.cancel_completions() end + self.on_items = function() end +end + +function list:set_on_items(on_items) self.on_items = on_items end + +function list:is_valid_for_context(new_context) + if self.context.id ~= new_context.id then return false end + + -- get the text for the current and queued context + local old_context_query = self.context.line:sub(self.context.bounds.start_col, self.context.cursor[2]) + local new_context_query = new_context.line:sub(new_context.bounds.start_col, new_context.cursor[2]) + + -- check if the texts are overlapping + local is_before = vim.startswith(old_context_query, new_context_query) + local is_after = vim.startswith(new_context_query, old_context_query) + + return (is_before and not self.is_incomplete_backward) + or (is_after and not self.is_incomplete_forward) + or (is_after == is_before and not (self.is_incomplete_backward or self.is_incomplete_forward)) +end + +return list diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/override.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/override.lua new file mode 100644 index 0000000..96e8edd --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/provider/override.lua @@ -0,0 +1,17 @@ +--- @class blink.cmp.Override : blink.cmp.Source +--- @field new fun(module: blink.cmp.Source, override_config: blink.cmp.SourceOverride): blink.cmp.Override + +local override = {} + +function override.new(module, override_config) + override_config = override_config or {} + + return setmetatable({}, { + __index = function(_, key) + if override_config[key] ~= nil then return function(_, ...) return override_config[key](module, ...) end end + return module[key] + end, + }) +end + +return override diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/queue.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/queue.lua new file mode 100644 index 0000000..3947902 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/queue.lua @@ -0,0 +1,68 @@ +local async = require('blink.cmp.lib.async') + +--- @class blink.cmp.SourcesQueue +--- @field id number +--- @field providers table<string, blink.cmp.SourceProvider> +--- @field request blink.cmp.Task | nil +--- @field queued_request_context blink.cmp.Context | nil +--- @field cached_items_by_provider table<string, blink.cmp.CompletionResponse> | nil +--- @field on_completions_callback fun(context: blink.cmp.Context, responses: table<string, blink.cmp.CompletionResponse>) +--- +--- @field new fun(context: blink.cmp.Context, providers: table<string, blink.cmp.SourceProvider>, on_completions_callback: fun(context: blink.cmp.Context, responses: table<string, blink.cmp.CompletionResponse>)): blink.cmp.SourcesQueue +--- @field get_cached_completions fun(self: blink.cmp.SourcesQueue): table<string, blink.cmp.CompletionResponse> | nil +--- @field get_completions fun(self: blink.cmp.SourcesQueue, context: blink.cmp.Context) +--- @field destroy fun(self: blink.cmp.SourcesQueue) + +--- @type blink.cmp.SourcesQueue +--- @diagnostic disable-next-line: missing-fields +local queue = {} + +function queue.new(context, providers, on_completions_callback) + local self = setmetatable({}, { __index = queue }) + self.id = context.id + self.providers = providers + + self.request = nil + self.queued_request_context = nil + self.on_completions_callback = on_completions_callback + + return self +end + +function queue:get_cached_completions() return self.cached_items_by_provider end + +function queue:get_completions(context) + assert(context.id == self.id, 'Requested completions on a sources context with a different context ID') + + if self.request ~= nil then + if self.request.status == async.STATUS.RUNNING then + self.queued_request_context = context + return + else + self.request:cancel() + end + end + + -- Create a task to get the completions, send responses upstream + -- and run the queued request, if it exists + local tree = require('blink.cmp.sources.lib.tree').new(context, vim.tbl_values(self.providers)) + self.request = tree:get_completions(context, function(items_by_provider) + self.cached_items_by_provider = items_by_provider + self.on_completions_callback(context, items_by_provider) + + -- run the queued request, if it exists + local queued_context = self.queued_request_context + if queued_context ~= nil then + self.queued_request_context = nil + self.request:cancel() + self:get_completions(queued_context) + end + end) +end + +function queue:destroy() + self.on_completions_callback = function() end + if self.request ~= nil then self.request:cancel() end +end + +return queue diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/tree.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/tree.lua new file mode 100644 index 0000000..c89d04d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/tree.lua @@ -0,0 +1,168 @@ +--- @class blink.cmp.SourceTreeNode +--- @field id string +--- @field source blink.cmp.SourceProvider +--- @field dependencies blink.cmp.SourceTreeNode[] +--- @field dependents blink.cmp.SourceTreeNode[] + +--- @class blink.cmp.SourceTree +--- @field nodes blink.cmp.SourceTreeNode[] +--- @field new fun(context: blink.cmp.Context, all_sources: blink.cmp.SourceProvider[]): blink.cmp.SourceTree +--- @field get_completions fun(self: blink.cmp.SourceTree, context: blink.cmp.Context, on_items_by_provider: fun(items_by_provider: table<string, blink.cmp.CompletionItem[]>)): blink.cmp.Task +--- @field emit_completions fun(self: blink.cmp.SourceTree, items_by_provider: table<string, blink.cmp.CompletionItem[]>, on_items_by_provider: fun(items_by_provider: table<string, blink.cmp.CompletionItem[]>)): nil +--- @field get_top_level_nodes fun(self: blink.cmp.SourceTree): blink.cmp.SourceTreeNode[] +--- @field detect_cycle fun(node: blink.cmp.SourceTreeNode, visited?: table<string, boolean>, path?: table<string, boolean>): boolean + +local utils = require('blink.cmp.lib.utils') +local async = require('blink.cmp.lib.async') + +--- @type blink.cmp.SourceTree +--- @diagnostic disable-next-line: missing-fields +local tree = {} + +--- @param context blink.cmp.Context +--- @param all_sources blink.cmp.SourceProvider[] +function tree.new(context, all_sources) + -- only include enabled sources for the given context + local sources = vim.tbl_filter( + function(source) return vim.tbl_contains(context.providers, source.id) and source:enabled(context) end, + all_sources + ) + local source_ids = vim.tbl_map(function(source) return source.id end, sources) + + -- create a node for each source + local nodes = vim.tbl_map( + function(source) return { id = source.id, source = source, dependencies = {}, dependents = {} } end, + sources + ) + + -- build the tree + for idx, source in ipairs(sources) do + local node = nodes[idx] + for _, fallback_source_id in ipairs(source.config.fallbacks(context, source_ids)) do + local fallback_node = nodes[utils.index_of(source_ids, fallback_source_id)] + if fallback_node ~= nil then + table.insert(node.dependents, fallback_node) + table.insert(fallback_node.dependencies, node) + end + end + end + + -- circular dependency check + for _, node in ipairs(nodes) do + tree.detect_cycle(node) + end + + return setmetatable({ nodes = nodes }, { __index = tree }) +end + +function tree:get_completions(context, on_items_by_provider) + local should_push_upstream = false + local items_by_provider = {} + local is_all_cached = true + local nodes_falling_back = {} + + --- @param node blink.cmp.SourceTreeNode + local function get_completions_for_node(node) + -- check that all the dependencies have been triggered, and are falling back + for _, dependency in ipairs(node.dependencies) do + if not nodes_falling_back[dependency.id] then return async.task.empty() end + end + + return async.task.new(function(resolve, reject) + return node.source:get_completions(context, function(items, is_cached) + items_by_provider[node.id] = items + is_all_cached = is_all_cached and is_cached + + if should_push_upstream then self:emit_completions(items_by_provider, on_items_by_provider) end + if #items ~= 0 then return resolve() end + + -- run dependents if the source returned 0 items + nodes_falling_back[node.id] = true + local tasks = vim.tbl_map(function(dependent) return get_completions_for_node(dependent) end, node.dependents) + async.task.await_all(tasks):map(resolve):catch(reject) + end) + end) + end + + -- run the top level nodes and let them fall back to their dependents if needed + local tasks = vim.tbl_map(function(node) return get_completions_for_node(node) end, self:get_top_level_nodes()) + return async.task + .await_all(tasks) + :map(function() + should_push_upstream = true + + -- if atleast one of the results wasn't cached, emit the results + if not is_all_cached then self:emit_completions(items_by_provider, on_items_by_provider) end + end) + :catch(function(err) vim.print('failed to get completions with error: ' .. err) end) +end + +function tree:emit_completions(items_by_provider, on_items_by_provider) + local nodes_falling_back = {} + local final_items_by_provider = {} + + local add_node_items + add_node_items = function(node) + for _, dependency in ipairs(node.dependencies) do + if not nodes_falling_back[dependency.id] then return end + end + local items = items_by_provider[node.id] + if items ~= nil and #items > 0 then + final_items_by_provider[node.id] = items + else + nodes_falling_back[node.id] = true + for _, dependent in ipairs(node.dependents) do + add_node_items(dependent) + end + end + end + + for _, node in ipairs(self:get_top_level_nodes()) do + add_node_items(node) + end + + on_items_by_provider(final_items_by_provider) +end + +--- Internal --- + +function tree:get_top_level_nodes() + local top_level_nodes = {} + for _, node in ipairs(self.nodes) do + if #node.dependencies == 0 then table.insert(top_level_nodes, node) end + end + return top_level_nodes +end + +--- Helper function to detect cycles using DFS +--- @param node blink.cmp.SourceTreeNode +--- @param visited? table<string, boolean> +--- @param path? table<string, boolean> +--- @return boolean +function tree.detect_cycle(node, visited, path) + visited = visited or {} + path = path or {} + + if path[node.id] then + -- Found a cycle - construct the cycle path for error message + local cycle = { node.id } + for id, _ in pairs(path) do + table.insert(cycle, id) + end + error('Circular dependency detected: ' .. table.concat(cycle, ' -> ')) + end + + if visited[node.id] then return false end + + visited[node.id] = true + path[node.id] = true + + for _, dependent in ipairs(node.dependents) do + if tree.detect_cycle(dependent, visited, path) then return true end + end + + path[node.id] = nil + return false +end + +return tree diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/types.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/types.lua new file mode 100644 index 0000000..b2bd708 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/types.lua @@ -0,0 +1,31 @@ +--- @class blink.cmp.CompletionTriggerContext +--- @field kind number +--- @field character string | nil + +--- @class blink.cmp.CompletionResponse +--- @field is_incomplete_forward boolean +--- @field is_incomplete_backward boolean +--- @field items blink.cmp.CompletionItem[] + +--- @class blink.cmp.Source +--- @field new fun(opts: table, config: blink.cmp.SourceProviderConfig): blink.cmp.Source +--- @field enabled? fun(self: blink.cmp.Source): boolean +--- @field get_trigger_characters? fun(self: blink.cmp.Source): string[] +--- @field get_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, callback: fun(response?: blink.cmp.CompletionResponse)): (fun(): nil) | nil +--- @field should_show_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, response: blink.cmp.CompletionResponse): boolean +--- @field resolve? fun(self: blink.cmp.Source, item: blink.cmp.CompletionItem, callback: fun(resolved_item?: lsp.CompletionItem)): ((fun(): nil) | nil) +--- @field execute? fun(self: blink.cmp.Source, context: blink.cmp.Context, item: blink.cmp.CompletionItem, callback: fun()): (fun(): nil) | nil +--- @field get_signature_help_trigger_characters? fun(self: blink.cmp.Source): string[] +--- @field get_signature_help? fun(self: blink.cmp.Source, context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)): (fun(): nil) | nil +--- @field reload? fun(self: blink.cmp.Source): nil + +--- @class blink.cmp.SourceOverride +--- @field enabled? fun(self: blink.cmp.Source): boolean +--- @field get_trigger_characters? fun(self: blink.cmp.Source): string[] +--- @field get_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, callback: fun(response: blink.cmp.CompletionResponse | nil)): (fun(): nil) | nil +--- @field should_show_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, response: blink.cmp.CompletionResponse): boolean +--- @field resolve? fun(self: blink.cmp.Source, item: blink.cmp.CompletionItem, callback: fun(resolved_item: lsp.CompletionItem | nil)): ((fun(): nil) | nil) +--- @field execute? fun(self: blink.cmp.Source, context: blink.cmp.Context, item: blink.cmp.CompletionItem, callback: fun()): (fun(): nil) | nil +--- @field get_signature_help_trigger_characters? fun(self: blink.cmp.Source): string[] +--- @field get_signature_help? fun(self: blink.cmp.Source, context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)): (fun(): nil) | nil +--- @field reload? fun(self: blink.cmp.Source): nil diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/utils.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/utils.lua new file mode 100644 index 0000000..97025bb --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lib/utils.lua @@ -0,0 +1,15 @@ +local utils = {} + +--- @param item blink.cmp.CompletionItem +--- @return lsp.CompletionItem +function utils.blink_item_to_lsp_item(item) + local lsp_item = vim.deepcopy(item) + lsp_item.score_offset = nil + lsp_item.source_id = nil + lsp_item.source_name = nil + lsp_item.cursor_column = nil + lsp_item.client_id = nil + return lsp_item +end + +return utils diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lsp/completion.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lsp/completion.lua new file mode 100644 index 0000000..5d54280 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lsp/completion.lua @@ -0,0 +1,74 @@ +local async = require('blink.cmp.lib.async') +local known_defaults = { + 'commitCharacters', + 'insertTextFormat', + 'insertTextMode', + 'data', +} +local CompletionTriggerKind = vim.lsp.protocol.CompletionTriggerKind + +local completion = {} + +--- @param context blink.cmp.Context +--- @param client vim.lsp.Client +--- @return blink.cmp.Task +function completion.get_completion_for_client(context, client) + return async.task.new(function(resolve) + local params = vim.lsp.util.make_position_params(0, client.offset_encoding) + params.context = { + triggerKind = context.trigger.kind == 'trigger_character' and CompletionTriggerKind.TriggerCharacter + or CompletionTriggerKind.Invoked, + } + if context.trigger.kind == 'trigger_character' then params.context.triggerCharacter = context.trigger.character end + + local _, request_id = client.request('textDocument/completion', params, function(err, result) + if err or result == nil then + resolve({ is_incomplete_forward = true, is_incomplete_backward = true, items = {} }) + return + end + + local items = result.items or result + local default_edit_range = result.itemDefaults and result.itemDefaults.editRange + for _, item in ipairs(items) do + item.client_id = client.id + + -- score offset for deprecated items + -- todo: make configurable + if item.deprecated or (item.tags and vim.tbl_contains(item.tags, 1)) then item.score_offset = -2 end + + -- set defaults + for key, value in pairs(result.itemDefaults or {}) do + if vim.tbl_contains(known_defaults, key) then item[key] = item[key] or value end + end + if default_edit_range and item.textEdit == nil then + local new_text = item.textEditText or item.insertText or item.label + if default_edit_range.replace ~= nil then + item.textEdit = { + replace = default_edit_range.replace, + insert = default_edit_range.insert, + newText = new_text, + } + else + item.textEdit = { + range = result.itemDefaults.editRange, + newText = new_text, + } + end + end + end + + resolve({ + is_incomplete_forward = result.isIncomplete or false, + is_incomplete_backward = true, + items = items, + }) + end) + + -- cancellation function + return function() + if request_id ~= nil then client.cancel_request(request_id) end + end + end) +end + +return completion diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lsp/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lsp/init.lua new file mode 100644 index 0000000..4dbfd8f --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/lsp/init.lua @@ -0,0 +1,131 @@ +local async = require('blink.cmp.lib.async') + +--- @type blink.cmp.Source +--- @diagnostic disable-next-line: missing-fields +local lsp = {} + +function lsp.new() return setmetatable({}, { __index = lsp }) end + +--- Completion --- + +function lsp:get_trigger_characters() + local clients = vim.lsp.get_clients({ bufnr = 0 }) + local trigger_characters = {} + + for _, client in pairs(clients) do + local completion_provider = client.server_capabilities.completionProvider + if completion_provider and completion_provider.triggerCharacters then + for _, trigger_character in pairs(completion_provider.triggerCharacters) do + table.insert(trigger_characters, trigger_character) + end + end + end + + return trigger_characters +end + +function lsp:get_completions(context, callback) + local completion_lib = require('blink.cmp.sources.lsp.completion') + local clients = vim.tbl_filter( + function(client) return client.server_capabilities and client.server_capabilities.completionProvider end, + vim.lsp.get_clients({ bufnr = 0, method = 'textDocument/completion' }) + ) + + -- TODO: implement a timeout before returning the menu as-is. In the future, it would be neat + -- to detect slow LSPs and consistently run them async + local task = async.task + .await_all(vim.tbl_map(function(client) return completion_lib.get_completion_for_client(context, client) end, clients)) + :map(function(responses) + local final = { is_incomplete_forward = false, is_incomplete_backward = false, items = {} } + for _, response in ipairs(responses) do + final.is_incomplete_forward = final.is_incomplete_forward or response.is_incomplete_forward + final.is_incomplete_backward = final.is_incomplete_backward or response.is_incomplete_backward + vim.list_extend(final.items, response.items) + end + callback(final) + end) + return function() task:cancel() end +end + +--- Resolve --- + +function lsp:resolve(item, callback) + local client = vim.lsp.get_client_by_id(item.client_id) + if client == nil or not client.server_capabilities.completionProvider.resolveProvider then + callback(item) + return + end + + -- strip blink specific fields to avoid decoding errors on some LSPs + item = require('blink.cmp.sources.lib.utils').blink_item_to_lsp_item(item) + + local success, request_id = client.request('completionItem/resolve', item, function(error, resolved_item) + if error or resolved_item == nil then callback(item) end + callback(resolved_item) + end) + if not success then callback(item) end + if request_id ~= nil then + return function() client.cancel_request(request_id) end + end +end + +--- Signature help --- + +function lsp:get_signature_help_trigger_characters() + local clients = vim.lsp.get_clients({ bufnr = 0 }) + local trigger_characters = {} + local retrigger_characters = {} + + for _, client in pairs(clients) do + local signature_help_provider = client.server_capabilities.signatureHelpProvider + if signature_help_provider and signature_help_provider.triggerCharacters then + for _, trigger_character in pairs(signature_help_provider.triggerCharacters) do + table.insert(trigger_characters, trigger_character) + end + end + if signature_help_provider and signature_help_provider.retriggerCharacters then + for _, retrigger_character in pairs(signature_help_provider.retriggerCharacters) do + table.insert(retrigger_characters, retrigger_character) + end + end + end + + return { trigger_characters = trigger_characters, retrigger_characters = retrigger_characters } +end + +function lsp:get_signature_help(context, callback) + -- no providers with signature help support + if #vim.lsp.get_clients({ bufnr = 0, method = 'textDocument/signatureHelp' }) == 0 then + callback(nil) + return function() end + end + + -- TODO: offset encoding is global but should be per-client + local first_client = vim.lsp.get_clients({ bufnr = 0 })[1] + local offset_encoding = first_client and first_client.offset_encoding or 'utf-16' + + local params = vim.lsp.util.make_position_params(nil, offset_encoding) + params.context = { + triggerKind = context.trigger.kind, + triggerCharacter = context.trigger.character, + isRetrigger = context.is_retrigger, + activeSignatureHelp = context.active_signature_help, + } + + -- otherwise, we call all clients + -- TODO: some LSPs never response (typescript-tools.nvim) + return vim.lsp.buf_request_all(0, 'textDocument/signatureHelp', params, function(result) + local signature_helps = {} + for client_id, res in pairs(result) do + local signature_help = res.result + if signature_help ~= nil then + signature_help.client_id = client_id + table.insert(signature_helps, signature_help) + end + end + -- todo: pick intelligently + callback(signature_helps[1]) + end) +end + +return lsp diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/fs.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/fs.lua new file mode 100644 index 0000000..4ac79f0 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/fs.lua @@ -0,0 +1,70 @@ +local async = require('blink.cmp.lib.async') +local uv = vim.uv +local fs = {} + +--- Scans a directory asynchronously in a loop until +--- it finds all entries +--- @param path string +--- @return blink.cmp.Task +function fs.scan_dir_async(path) + local max_entries = 200 + return async.task.new(function(resolve, reject) + uv.fs_opendir(path, function(err, handle) + if err ~= nil or handle == nil then return reject(err) end + + local all_entries = {} + + local function read_dir() + uv.fs_readdir(handle, function(err, entries) + if err ~= nil or entries == nil then return reject(err) end + + vim.list_extend(all_entries, entries) + if #entries == max_entries then + read_dir() + else + resolve(all_entries) + end + end) + end + read_dir() + end, max_entries) + end) +end + +--- @param entries { name: string, type: string }[] +--- @return blink.cmp.Task +function fs.fs_stat_all(cwd, entries) + local tasks = {} + for _, entry in ipairs(entries) do + table.insert( + tasks, + async.task.new(function(resolve) + uv.fs_stat(cwd .. '/' .. entry.name, function(err, stat) + if err then return resolve(nil) end + resolve({ name = entry.name, type = entry.type, stat = stat }) + end) + end) + ) + end + return async.task.await_all(tasks):map(function(entries) + return vim.tbl_filter(function(entry) return entry ~= nil end, entries) + end) +end + +--- @param path string +--- @param byte_limit number +--- @return blink.cmp.Task +function fs.read_file(path, byte_limit) + return async.task.new(function(resolve, reject) + uv.fs_open(path, 'r', 438, function(open_err, fd) + if open_err or fd == nil then return reject(open_err) end + uv.fs_read(fd, byte_limit, 0, function(read_err, data) + uv.fs_close(fd, function() end) + if read_err or data == nil then return reject(read_err) end + resolve(data) + end) + end) + end) +end + +return fs diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/init.lua new file mode 100644 index 0000000..bb5f508 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/init.lua @@ -0,0 +1,89 @@ +-- credit to https://github.com/hrsh7th/cmp-path for the original implementation +-- and https://codeberg.org/FelipeLema/cmp-async-path for the async implementation + +--- @class blink.cmp.PathOpts +--- @field trailing_slash boolean +--- @field label_trailing_slash boolean +--- @field get_cwd fun(context: blink.cmp.Context): string +--- @field show_hidden_files_by_default boolean + +--- @class blink.cmp.Source +--- @field opts blink.cmp.PathOpts +local path = {} + +function path.new(opts) + local self = setmetatable({}, { __index = path }) + + --- @type blink.cmp.PathOpts + opts = vim.tbl_deep_extend('keep', opts, { + trailing_slash = true, + label_trailing_slash = true, + get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end, + show_hidden_files_by_default = false, + }) + require('blink.cmp.config.utils').validate('sources.providers.path', { + trailing_slash = { opts.trailing_slash, 'boolean' }, + label_trailing_slash = { opts.label_trailing_slash, 'boolean' }, + get_cwd = { opts.get_cwd, 'function' }, + show_hidden_files_by_default = { opts.show_hidden_files_by_default, 'boolean' }, + }, opts) + + self.opts = opts + return self +end + +function path:get_trigger_characters() return { '/', '.' } end + +function path:get_completions(context, callback) + -- we use libuv, but the rest of the library expects to be synchronous + callback = vim.schedule_wrap(callback) + + local lib = require('blink.cmp.sources.path.lib') + + local dirname = lib.dirname(self.opts.get_cwd, context) + if not dirname then return callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = {} }) end + + local include_hidden = self.opts.show_hidden_files_by_default + or (string.sub(context.line, context.bounds.start_col, context.bounds.start_col) == '.' and context.bounds.length == 0) + or ( + string.sub(context.line, context.bounds.start_col - 1, context.bounds.start_col - 1) == '.' + and context.bounds.length > 0 + ) + lib + .candidates(context, dirname, include_hidden, self.opts) + :map( + function(candidates) + callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = candidates }) + end + ) + :catch(function() callback() end) +end + +function path:resolve(item, callback) + require('blink.cmp.sources.path.fs') + .read_file(item.data.full_path, 1024) + :map(function(content) + local is_binary = content:find('\0') + + -- binary file + if is_binary then + item.documentation = { + kind = 'plaintext', + value = 'Binary file', + } + -- highlight with markdown + else + local ext = vim.fn.fnamemodify(item.data.path, ':e') + item.documentation = { + kind = 'markdown', + value = '```' .. ext .. '\n' .. content .. '```', + } + end + + return item + end) + :map(function(resolved_item) callback(resolved_item) end) + :catch(function() callback(item) end) +end + +return path diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/lib.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/lib.lua new file mode 100644 index 0000000..53fd970 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/lib.lua @@ -0,0 +1,125 @@ +local regex = require('blink.cmp.sources.path.regex') +local lib = {} + +--- @param get_cwd fun(context: blink.cmp.Context): string +--- @param context blink.cmp.Context +function lib.dirname(get_cwd, context) + -- HACK: move this :sub logic into the context? + -- it's not obvious that you need to avoid going back a char if the start_col == end_col + local line_before_cursor = context.line:sub(1, context.bounds.start_col - (context.bounds.length == 0 and 1 or 0)) + local s = regex.PATH:match_str(line_before_cursor) + if not s then return nil end + + local dirname = string.gsub(string.sub(line_before_cursor, s + 2), regex.NAME .. '*$', '') -- exclude '/' + local prefix = string.sub(line_before_cursor, 1, s + 1) -- include '/' + + local buf_dirname = get_cwd(context) + if vim.api.nvim_get_mode().mode == 'c' then buf_dirname = vim.fn.getcwd() end + if prefix:match('%.%./$') then return vim.fn.resolve(buf_dirname .. '/../' .. dirname) end + if prefix:match('%./$') or prefix:match('"$') or prefix:match("'$") then + return vim.fn.resolve(buf_dirname .. '/' .. dirname) + end + if prefix:match('~/$') then return vim.fn.resolve(vim.fn.expand('~') .. '/' .. dirname) end + local env_var_name = prefix:match('%$([%a_]+)/$') + if env_var_name then + local env_var_value = vim.fn.getenv(env_var_name) + if env_var_value ~= vim.NIL then return vim.fn.resolve(env_var_value .. '/' .. dirname) end + end + if prefix:match('/$') then + local accept = true + -- Ignore URL components + accept = accept and not prefix:match('%a/$') + -- Ignore URL scheme + accept = accept and not prefix:match('%a+:/$') and not prefix:match('%a+://$') + -- Ignore HTML closing tags + accept = accept and not prefix:match('</$') + -- Ignore math calculation + accept = accept and not prefix:match('[%d%)]%s*/$') + -- Ignore / comment + accept = accept and (not prefix:match('^[%s/]*$') or not lib.is_slash_comment()) + if accept then return vim.fn.resolve('/' .. dirname) end + end + -- Windows drive letter (C:/) + if prefix:match('(%a:)[/\\]$') then return vim.fn.resolve(prefix:match('(%a:)[/\\]$') .. '/' .. dirname) end + return nil +end + +--- @param context blink.cmp.Context +--- @param dirname string +--- @param include_hidden boolean +--- @param opts table +function lib.candidates(context, dirname, include_hidden, opts) + local fs = require('blink.cmp.sources.path.fs') + local ranges = lib.get_text_edit_ranges(context) + return fs.scan_dir_async(dirname) + :map(function(entries) return fs.fs_stat_all(dirname, entries) end) + :map(function(entries) + return vim.tbl_filter(function(entry) return include_hidden or entry.name:sub(1, 1) ~= '.' end, entries) + end) + :map(function(entries) + return vim.tbl_map( + function(entry) + return lib.entry_to_completion_item( + entry, + dirname, + entry.type == 'directory' and ranges.directory or ranges.file, + opts + ) + end, + entries + ) + end) +end + +function lib.is_slash_comment() + local commentstring = vim.bo.commentstring or '' + local no_filetype = vim.bo.filetype == '' + local is_slash_comment = false + is_slash_comment = is_slash_comment or commentstring:match('/%*') + is_slash_comment = is_slash_comment or commentstring:match('//') + return is_slash_comment and not no_filetype +end + +--- @param entry { name: string, type: string, stat: table } +--- @param dirname string +--- @param range lsp.Range +--- @param opts table +--- @return blink.cmp.CompletionItem[] +function lib.entry_to_completion_item(entry, dirname, range, opts) + local is_dir = entry.type == 'directory' + local CompletionItemKind = require('blink.cmp.types').CompletionItemKind + local insert_text = is_dir and opts.trailing_slash and entry.name .. '/' or entry.name + return { + label = (opts.label_trailing_slash and is_dir) and entry.name .. '/' or entry.name, + kind = is_dir and CompletionItemKind.Folder or CompletionItemKind.File, + insertText = insert_text, + textEdit = { newText = insert_text, range = range }, + sortText = (is_dir and '1' or '2') .. entry.name:lower(), -- Sort directories before files + data = { path = entry.name, full_path = dirname .. '/' .. entry.name, type = entry.type, stat = entry.stat }, + } +end + +--- @param context blink.cmp.Context +--- @return { file: lsp.Range, directory: lsp.Range } +function lib.get_text_edit_ranges(context) + local line_before_cursor = context.line:sub(1, context.cursor[2]) + local next_letter_is_slash = context.line:sub(context.cursor[2] + 1, context.cursor[2] + 1) == '/' + + local parts = vim.split(line_before_cursor, '/') + local last_part = parts[#parts] + + -- TODO: return the insert and replace ranges, instead of only the insert range + return { + file = { + start = { line = context.cursor[1] - 1, character = context.cursor[2] - #last_part }, + ['end'] = { line = context.cursor[1] - 1, character = context.cursor[2] }, + }, + directory = { + start = { line = context.cursor[1] - 1, character = context.cursor[2] - #last_part }, + -- replace the slash after the cursor, if it exists + ['end'] = { line = context.cursor[1] - 1, character = context.cursor[2] + (next_letter_is_slash and 1 or 0) }, + }, + } +end + +return lib diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/regex.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/regex.lua new file mode 100644 index 0000000..af27d25 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/path/regex.lua @@ -0,0 +1,10 @@ +local NAME_REGEX = '\\%([^/\\\\:\\*?<>\'"`\\|]\\)' +local PATH_REGEX = + assert(vim.regex(([[\%(\%(/PAT*[^/\\\\:\\*?<>\'"`\\| .~]\)\|\%(/\.\.\)\)*/\zePAT*$]]):gsub('PAT', NAME_REGEX))) + +return { + --- Lua pattern for matching file names + NAME = '[^/\\:*?<>\'"`|]', + --- Vim regex for matching file paths + PATH = PATH_REGEX, +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/builtin.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/builtin.lua new file mode 100644 index 0000000..b66ca1c --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/builtin.lua @@ -0,0 +1,188 @@ +-- credit to https://github.com/L3MON4D3 for these variables +-- see: https://github.com/L3MON4D3/LuaSnip/blob/master/lua/luasnip/util/_builtin_vars.lua +-- and credit to https://github.com/garymjr for his changes +-- see: https://github.com/garymjr/nvim-snippets/blob/main/lua/snippets/utils/builtin.lua + +local builtin = { + lazy = {}, +} + +function builtin.lazy.TM_FILENAME() return vim.fn.expand('%:t') end + +function builtin.lazy.TM_FILENAME_BASE() return vim.fn.expand('%:t:s?\\.[^\\.]\\+$??') end + +function builtin.lazy.TM_DIRECTORY() return vim.fn.expand('%:p:h') end + +function builtin.lazy.TM_FILEPATH() return vim.fn.expand('%:p') end + +function builtin.lazy.CLIPBOARD(opts) return vim.fn.getreg(opts.clipboard_register or vim.v.register, true) end + +local function buf_to_ws_part() + local LSP_WORSKPACE_PARTS = 'LSP_WORSKPACE_PARTS' + local ok, ws_parts = pcall(vim.api.nvim_buf_get_var, 0, LSP_WORSKPACE_PARTS) + if not ok then + local file_path = vim.fn.expand('%:p') + + for _, ws in pairs(vim.lsp.buf.list_workspace_folders()) do + if file_path:find(ws, 1, true) == 1 then + ws_parts = { ws, file_path:sub(#ws + 2, -1) } + break + end + end + -- If it can't be extracted from lsp, then we use the file path + if not ok and not ws_parts then ws_parts = { vim.fn.expand('%:p:h'), vim.fn.expand('%:p:t') } end + vim.api.nvim_buf_set_var(0, LSP_WORSKPACE_PARTS, ws_parts) + end + return ws_parts +end + +function builtin.lazy.RELATIVE_FILEPATH() -- The relative (to the opened workspace or folder) file path of the current document + return buf_to_ws_part()[2] +end + +function builtin.lazy.WORKSPACE_FOLDER() -- The path of the opened workspace or folder + return buf_to_ws_part()[1] +end + +function builtin.lazy.WORKSPACE_NAME() -- The name of the opened workspace or folder + local parts = vim.split(buf_to_ws_part()[1] or '', '[\\/]') + return parts[#parts] +end + +function builtin.lazy.CURRENT_YEAR() return os.date('%Y') end + +function builtin.lazy.CURRENT_YEAR_SHORT() return os.date('%y') end + +function builtin.lazy.CURRENT_MONTH() return os.date('%m') end + +function builtin.lazy.CURRENT_MONTH_NAME() return os.date('%B') end + +function builtin.lazy.CURRENT_MONTH_NAME_SHORT() return os.date('%b') end + +function builtin.lazy.CURRENT_DATE() return os.date('%d') end + +function builtin.lazy.CURRENT_DAY_NAME() return os.date('%A') end + +function builtin.lazy.CURRENT_DAY_NAME_SHORT() return os.date('%a') end + +function builtin.lazy.CURRENT_HOUR() return os.date('%H') end + +function builtin.lazy.CURRENT_MINUTE() return os.date('%M') end + +function builtin.lazy.CURRENT_SECOND() return os.date('%S') end + +function builtin.lazy.CURRENT_SECONDS_UNIX() return tostring(os.time()) end + +local function get_timezone_offset(ts) + local utcdate = os.date('!*t', ts) + local localdate = os.date('*t', ts) + localdate.isdst = false -- this is the trick + local diff = os.difftime(os.time(localdate), os.time(utcdate)) + local h, m = math.modf(diff / 3600) + return string.format('%+.4d', 100 * h + 60 * m) +end + +function builtin.lazy.CURRENT_TIMEZONE_OFFSET() + return get_timezone_offset(os.time()):gsub('([+-])(%d%d)(%d%d)$', '%1%2:%3') +end + +math.randomseed(os.time()) + +function builtin.lazy.RANDOM() return string.format('%06d', math.random(999999)) end + +function builtin.lazy.RANDOM_HEX() + return string.format('%06x', math.random(16777216)) --16^6 +end + +function builtin.lazy.UUID() + local random = math.random + local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + local out + local function subs(c) + local v = (((c == 'x') and random(0, 15)) or random(8, 11)) + return string.format('%x', v) + end + + out = template:gsub('[xy]', subs) + return out +end + +local _comments_cache = {} +local function buffer_comment_chars() + local commentstring = vim.bo.commentstring + if _comments_cache[commentstring] then return _comments_cache[commentstring] end + local comments = { '//', '/*', '*/' } + local placeholder = '%s' + local index_placeholder = commentstring:find(vim.pesc(placeholder)) + if index_placeholder then + index_placeholder = index_placeholder - 1 + if index_placeholder + #placeholder == #commentstring then + comments[1] = vim.trim(commentstring:sub(1, -#placeholder - 1)) + else + comments[2] = vim.trim(commentstring:sub(1, index_placeholder)) + comments[3] = vim.trim(commentstring:sub(index_placeholder + #placeholder + 1, -1)) + end + end + _comments_cache[commentstring] = comments + return comments +end + +function builtin.lazy.LINE_COMMENT() return buffer_comment_chars()[1] end + +function builtin.lazy.BLOCK_COMMENT_START() return buffer_comment_chars()[2] end + +function builtin.lazy.BLOCK_COMMENT_END() return buffer_comment_chars()[3] end + +local function get_cursor() + local c = vim.api.nvim_win_get_cursor(0) + c[1] = c[1] - 1 + return c +end + +local function get_current_line() + local pos = get_cursor() + return vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1] +end + +local function word_under_cursor(cur, line) + if line == nil then return end + + local ind_start = 1 + local ind_end = #line + + while true do + local tmp = string.find(line, '%W%w', ind_start) + if not tmp then break end + if tmp > cur[2] + 1 then break end + ind_start = tmp + 1 + end + + local tmp = string.find(line, '%w%W', cur[2] + 1) + if tmp then ind_end = tmp end + + return string.sub(line, ind_start, ind_end) +end + +local function get_selected_text() + if vim.fn.visualmode() == 'V' then return vim.fn.trim(vim.fn.getreg(vim.v.register, true), '\n', 2) end + return '' +end + +vim.api.nvim_create_autocmd('InsertEnter', { + group = vim.api.nvim_create_augroup('BlinkSnippetsEagerEnter', { clear = true }), + callback = function() + builtin.eager = {} + builtin.eager.TM_CURRENT_LINE = get_current_line() + builtin.eager.TM_CURRENT_WORD = word_under_cursor(get_cursor(), builtin.eager.TM_CURRENT_LINE) + builtin.eager.TM_LINE_INDEX = tostring(get_cursor()[1]) + builtin.eager.TM_LINE_NUMBER = tostring(get_cursor()[1] + 1) + builtin.eager.TM_SELECTED_TEXT = get_selected_text() + end, +}) + +vim.api.nvim_create_autocmd('InsertLeave', { + group = vim.api.nvim_create_augroup('BlinkSnippetsEagerLeave', { clear = true }), + callback = function() builtin.eager = nil end, +}) + +return builtin diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/init.lua new file mode 100644 index 0000000..db7fece --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/init.lua @@ -0,0 +1,65 @@ +--- @class blink.cmp.SnippetsOpts +--- @field friendly_snippets? boolean +--- @field search_paths? string[] +--- @field global_snippets? string[] +--- @field extended_filetypes? table<string, string[]> +--- @field ignored_filetypes? string[] +--- @field get_filetype? fun(context: blink.cmp.Context): string +--- @field clipboard_register? string + +local snippets = {} + +function snippets.new(opts) + --- @cast opts blink.cmp.SnippetsOpts + + local self = setmetatable({}, { __index = snippets }) + --- @type table<string, blink.cmp.CompletionItem[]> + self.cache = {} + self.registry = require('blink.cmp.sources.snippets.default.registry').new(opts) + self.get_filetype = opts.get_filetype or function() return vim.bo.filetype end + return self +end + +function snippets:get_completions(context, callback) + local filetype = self.get_filetype(context) + if vim.tbl_contains(self.registry.config.ignored_filetypes, filetype) then return callback() end + + if not self.cache[filetype] then + local global_snippets = self.registry:get_global_snippets() + local extended_snippets = self.registry:get_extended_snippets(filetype) + local ft_snippets = self.registry:get_snippets_for_ft(filetype) + local snips = vim.list_extend({}, global_snippets) + vim.list_extend(snips, extended_snippets) + vim.list_extend(snips, ft_snippets) + + self.cache[filetype] = snips + end + + local items = vim.tbl_map( + function(item) return self.registry:snippet_to_completion_item(item) end, + self.cache[filetype] + ) + callback({ + is_incomplete_forward = false, + is_incomplete_backward = false, + items = items, + }) +end + +function snippets:resolve(item, callback) + local parsed_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(item.insertText) + local snippet = parsed_snippet and tostring(parsed_snippet) or item.insertText + + local resolved_item = vim.deepcopy(item) + resolved_item.detail = snippet + resolved_item.documentation = { + kind = 'markdown', + value = item.description, + } + callback(resolved_item) +end + +--- For external integrations to force reloading the snippets +function snippets:reload() self.cache = {} end + +return snippets diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/registry.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/registry.lua new file mode 100644 index 0000000..5be225c --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/registry.lua @@ -0,0 +1,144 @@ +--- Credit to https://github.com/garymjr/nvim-snippets/blob/main/lua/snippets/utils/init.lua +--- for the original implementation +--- Original License: MIT + +--- @class blink.cmp.Snippet +--- @field prefix string +--- @field body string[] | string +--- @field description? string + +local registry = { + builtin_vars = require('blink.cmp.sources.snippets.default.builtin'), +} + +local utils = require('blink.cmp.sources.snippets.utils') +local default_config = { + friendly_snippets = true, + search_paths = { vim.fn.stdpath('config') .. '/snippets' }, + global_snippets = { 'all' }, + extended_filetypes = {}, + ignored_filetypes = {}, + --- @type string? + clipboard_register = nil, +} + +--- @param config blink.cmp.SnippetsOpts +function registry.new(config) + local self = setmetatable({}, { __index = registry }) + self.config = vim.tbl_deep_extend('force', default_config, config) + self.config.search_paths = vim.tbl_map(function(path) return vim.fs.normalize(path) end, self.config.search_paths) + + if self.config.friendly_snippets then + for _, path in ipairs(vim.api.nvim_list_runtime_paths()) do + if string.match(path, 'friendly.snippets') then table.insert(self.config.search_paths, path) end + end + end + self.registry = require('blink.cmp.sources.snippets.default.scan').register_snippets(self.config.search_paths) + + return self +end + +--- @param filetype string +--- @return blink.cmp.Snippet[] +function registry:get_snippets_for_ft(filetype) + local loaded_snippets = {} + local files = self.registry[filetype] + if not files then return loaded_snippets end + + files = type(files) == 'table' and files or { files } + + for _, f in ipairs(files) do + local contents = utils.read_file(f) + if contents then + local snippets = utils.parse_json_with_error_msg(f, contents) + for _, key in ipairs(vim.tbl_keys(snippets)) do + local snippet = utils.read_snippet(snippets[key], key) + for _, snippet_def in pairs(snippet) do + table.insert(loaded_snippets, snippet_def) + end + end + end + end + + return loaded_snippets +end + +--- @param filetype string +--- @return blink.cmp.Snippet[] +function registry:get_extended_snippets(filetype) + local loaded_snippets = {} + if not filetype then return loaded_snippets end + + local extended_snippets = self.config.extended_filetypes[filetype] or {} + for _, ft in ipairs(extended_snippets) do + if vim.tbl_contains(self.config.extended_filetypes, filetype) then + vim.list_extend(loaded_snippets, self:get_extended_snippets(ft)) + else + vim.list_extend(loaded_snippets, self:get_snippets_for_ft(ft)) + end + end + return loaded_snippets +end + +--- @return blink.cmp.Snippet[] +function registry:get_global_snippets() + local loaded_snippets = {} + local global_snippets = self.config.global_snippets + for _, ft in ipairs(global_snippets) do + if vim.tbl_contains(self.config.extended_filetypes, ft) then + vim.list_extend(loaded_snippets, self:get_extended_snippets(ft)) + else + vim.list_extend(loaded_snippets, self:get_snippets_for_ft(ft)) + end + end + return loaded_snippets +end + +--- @param snippet blink.cmp.Snippet +--- @return blink.cmp.CompletionItem +function registry:snippet_to_completion_item(snippet) + local body = type(snippet.body) == 'string' and snippet.body or table.concat(snippet.body, '\n') + return { + kind = require('blink.cmp.types').CompletionItemKind.Snippet, + label = snippet.prefix, + insertTextFormat = vim.lsp.protocol.InsertTextFormat.Snippet, + insertText = self:expand_vars(body), + description = snippet.description, + } +end + +--- @param snippet string +--- @return string +function registry:parse_body(snippet) + local parse = utils.safe_parse(self:expand_vars(snippet)) + return parse and tostring(parse) or snippet +end + +--- @param snippet string +--- @return string +function registry:expand_vars(snippet) + local lazy_vars = self.builtin_vars.lazy + local eager_vars = self.builtin_vars.eager or {} + + local resolved_snippet = snippet + local parsed_snippet = utils.safe_parse(snippet) + if not parsed_snippet then return snippet end + + for _, child in ipairs(parsed_snippet.data.children) do + local type, data = child.type, child.data + if type == vim.lsp._snippet_grammar.NodeType.Variable then + if eager_vars[data.name] then + resolved_snippet = resolved_snippet:gsub('%$[{]?(' .. data.name .. ')[}]?', eager_vars[data.name]) + elseif lazy_vars[data.name] then + resolved_snippet = resolved_snippet:gsub( + '%$[{]?(' .. data.name .. ')[}]?', + lazy_vars[data.name]({ clipboard_register = self.config.clipboard_register }) + ) + end + end + end + + return resolved_snippet +end + +return registry diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/scan.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/scan.lua new file mode 100644 index 0000000..6971691 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/default/scan.lua @@ -0,0 +1,94 @@ +local utils = require('blink.cmp.sources.snippets.utils') +local scan = {} + +function scan.register_snippets(search_paths) + local registry = {} + + for _, path in ipairs(search_paths) do + local files = scan.load_package_json(path) or scan.scan_for_snippets(path) + for ft, file in pairs(files) do + local key + if type(ft) == 'number' then + key = vim.fn.fnamemodify(files[ft], ':t:r') + else + key = ft + end + + if not key then return end + + registry[key] = registry[key] or {} + if type(file) == 'table' then + vim.list_extend(registry[key], file) + else + table.insert(registry[key], file) + end + end + end + + return registry +end + +---@type fun(self: utils, dir: string, result?: string[]): string[] +---@return string[] +function scan.scan_for_snippets(dir, result) + result = result or {} + + local stat = vim.uv.fs_stat(dir) + if not stat then return result end + + if stat.type == 'directory' then + local req = vim.uv.fs_scandir(dir) + if not req then return result end + + local function iter() return vim.uv.fs_scandir_next(req) end + + for name, ftype in iter do + local path = string.format('%s/%s', dir, name) + + if ftype == 'directory' then + result[name] = scan.scan_for_snippets(path, result[name] or {}) + else + scan.scan_for_snippets(path, result) + end + end + elseif stat.type == 'file' then + local name = vim.fn.fnamemodify(dir, ':t') + + if name:match('%.json$') then table.insert(result, dir) end + elseif stat.type == 'link' then + local target = vim.uv.fs_readlink(dir) + + if target then scan.scan_for_snippets(target, result) end + end + + return result +end + +--- This will try to load the snippets from the package.json file +---@param path string +function scan.load_package_json(path) + local file = path .. '/package.json' + -- todo: ideally this is async, although it takes 0.5ms on my system so it might not matter + local data = utils.read_file(file) + if not data then return end + + local pkg = require('blink.cmp.sources.snippets.utils').parse_json_with_error_msg(file, data) + + ---@type {path: string, language: string|string[]}[] + local snippets = vim.tbl_get(pkg, 'contributes', 'snippets') + if not snippets then return end + + local ret = {} ---@type table<string, string[]> + for _, s in ipairs(snippets) do + local langs = s.language or {} + langs = type(langs) == 'string' and { langs } or langs + ---@cast langs string[] + for _, lang in ipairs(langs) do + ret[lang] = ret[lang] or {} + table.insert(ret[lang], vim.fs.normalize(vim.fs.joinpath(path, s.path))) + end + end + return ret +end + +return scan diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/init.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/init.lua new file mode 100644 index 0000000..2a4b0ba --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/init.lua @@ -0,0 +1,9 @@ +local source = {} + +function source.new(opts) + local preset = opts.preset or require('blink.cmp.config').snippets.preset + local module = 'blink.cmp.sources.snippets.' .. preset + return require(module).new(opts) +end + +return source diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/luasnip.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/luasnip.lua new file mode 100644 index 0000000..91716d2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/luasnip.lua @@ -0,0 +1,168 @@ +--- @class blink.cmp.LuasnipSourceOptions +--- @field use_show_condition? boolean Whether to use show_condition for filtering snippets +--- @field show_autosnippets? boolean Whether to show autosnippets in the completion list + +--- @class blink.cmp.LuasnipSource : blink.cmp.Source +--- @field config blink.cmp.LuasnipSourceOptions +--- @field items_cache table<string, blink.cmp.CompletionItem[]> + +--- @type blink.cmp.LuasnipSource +--- @diagnostic disable-next-line: missing-fields +local source = {} + +local defaults_config = { + use_show_condition = true, + show_autosnippets = true, +} + +function source.new(opts) + local config = vim.tbl_deep_extend('keep', opts, defaults_config) + require('blink.cmp.config.utils').validate('sources.providers.luasnip', { + use_show_condition = { config.use_show_condition, 'boolean' }, + show_autosnippets = { config.show_autosnippets, 'boolean' }, + }, config) + + local self = setmetatable({}, { __index = source }) + self.config = config + self.items_cache = {} + + local luasnip_ag = vim.api.nvim_create_augroup('BlinkCmpLuaSnipReload', { clear = true }) + vim.api.nvim_create_autocmd('User', { + pattern = 'LuasnipSnippetsAdded', + callback = function() self:reload() end, + group = luasnip_ag, + desc = 'Reset internal cache of luasnip source of blink.cmp when new snippets are added', + }) + vim.api.nvim_create_autocmd('User', { + pattern = 'LuasnipCleanup', + callback = function() self:reload() end, + group = luasnip_ag, + desc = 'Reload luasnip source of blink.cmp when snippets are cleared', + }) + + return self +end + +function source:enabled() + local ok, _ = pcall(require, 'luasnip') + return ok +end + +function source:get_completions(ctx, callback) + --- @type blink.cmp.CompletionItem[] + local items = {} + + -- gather snippets from relevant filetypes, including extensions + for _, ft in ipairs(require('luasnip.util.util').get_snippet_filetypes()) do + if self.items_cache[ft] then + vim.list_extend(items, self.items_cache[ft]) + goto continue + end + + -- cache not yet available for this filetype + self.items_cache[ft] = {} + -- Gather filetype snippets and, optionally, autosnippets + local snippets = require('luasnip').get_snippets(ft, { type = 'snippets' }) + if self.config.show_autosnippets then + local autosnippets = require('luasnip').get_snippets(ft, { type = 'autosnippets' }) + snippets = require('blink.cmp.lib.utils').shallow_copy(snippets) + vim.list_extend(snippets, autosnippets) + end + snippets = vim.tbl_filter(function(snip) return not snip.hidden end, snippets) + + -- Get the max priority for use with sortText + local max_priority = 0 + for _, snip in ipairs(snippets) do + max_priority = math.max(max_priority, snip.effective_priority or 0) + end + + for _, snip in ipairs(snippets) do + -- Convert priority of 1000 (with max of 8000) to string like "00007000|||asd" for sorting + -- This will put high priority snippets at the top of the list, and break ties based on the trigger + local inversed_priority = max_priority - (snip.effective_priority or 0) + local sort_text = ('0'):rep(8 - #tostring(inversed_priority), '') .. inversed_priority .. '|||' .. snip.trigger + + --- @type lsp.CompletionItem + local item = { + kind = require('blink.cmp.types').CompletionItemKind.Snippet, + label = snip.trigger, + insertText = snip.trigger, + insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, + sortText = sort_text, + data = { snip_id = snip.id, show_condition = snip.show_condition }, + } + -- populate snippet cache for this filetype + table.insert(self.items_cache[ft], item) + -- while we're at it, also populate completion items for this request + table.insert(items, item) + end + + ::continue:: + end + + -- Filter items based on show_condition, if configured + if self.config.use_show_condition then + local line_to_cursor = ctx.line:sub(0, ctx.cursor[2] - 1) + items = vim.tbl_filter(function(item) return item.data.show_condition(line_to_cursor) end, items) + end + + callback({ + is_incomplete_forward = false, + is_incomplete_backward = false, + items = items, + context = ctx, + }) +end + +function source:resolve(item, callback) + local snip = require('luasnip').get_id_snippet(item.data.snip_id) + + local resolved_item = vim.deepcopy(item) + + local detail = snip:get_docstring() + if type(detail) == 'table' then detail = table.concat(detail, '\n') end + resolved_item.detail = detail + + if snip.dscr then + resolved_item.documentation = { + kind = 'markdown', + value = table.concat(vim.lsp.util.convert_input_to_markdown_lines(snip.dscr), '\n'), + } + end + + callback(resolved_item) +end + +function source:execute(_, item) + local luasnip = require('luasnip') + local snip = luasnip.get_id_snippet(item.data.snip_id) + + -- if trigger is a pattern, expand "pattern" instead of actual snippet + if snip.regTrig then snip = snip:get_pattern_expand_helper() end + + -- get (0, 0) indexed cursor position + -- the completion has been accepted by this point, so ctx.cursor is out of date + local cursor = vim.api.nvim_win_get_cursor(0) + cursor[1] = cursor[1] - 1 + + local expand_params = snip:matches(require('luasnip.util.util').get_current_line_to_cursor()) + + local clear_region = { + from = { cursor[1], cursor[2] - #item.insertText }, + to = cursor, + } + if expand_params ~= nil and expand_params.clear_region ~= nil then + clear_region = expand_params.clear_region + elseif expand_params ~= nil and expand_params.trigger ~= nil then + clear_region = { + from = { cursor[1], cursor[2] - #expand_params.trigger }, + to = cursor, + } + end + + luasnip.snip_expand(snip, { expand_params = expand_params, clear_region = clear_region }) +end + +function source:reload() self.items_cache = {} end + +return source diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/mini_snippets.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/mini_snippets.lua new file mode 100644 index 0000000..3923f20 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/mini_snippets.lua @@ -0,0 +1,143 @@ +--- @module 'mini.snippets' + +--- @class blink.cmp.MiniSnippetsSourceOptions +--- @field use_items_cache? boolean completion items are cached using default mini.snippets context + +--- @class blink.cmp.MiniSnippetsSource : blink.cmp.Source +--- @field config blink.cmp.MiniSnippetsSourceOptions +--- @field items_cache table<string, blink.cmp.CompletionItem[]> + +--- @class blink.cmp.MiniSnippetsSnippet +--- @field prefix string string snippet identifier. +--- @field body string string snippet content with appropriate syntax. +--- @field desc string string snippet description in human readable form. + +--- @type blink.cmp.MiniSnippetsSource +--- @diagnostic disable-next-line: missing-fields +local source = {} + +local defaults_config = { + --- Whether to use a cache for completion items + use_items_cache = true, +} + +function source.new(opts) + local config = vim.tbl_deep_extend('keep', opts, defaults_config) + vim.validate({ + use_items_cache = { config.use_items_cache, 'boolean' }, + }) + + local self = setmetatable({}, { __index = source }) + self.config = config + self.items_cache = {} + return self +end + +function source:enabled() + ---@diagnostic disable-next-line: undefined-field + return _G.MiniSnippets ~= nil -- ensure that user has explicitly setup mini.snippets +end + +local function to_completion_items(snippets) + local result = {} + + for _, snip in ipairs(snippets) do + --- @type lsp.CompletionItem + local item = { + kind = require('blink.cmp.types').CompletionItemKind.Snippet, + label = snip.prefix, + insertText = snip.prefix, + insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, + data = { snip = snip }, + } + table.insert(result, item) + end + return result +end + +-- NOTE: Completion items are cached by default using the default 'mini.snippets' context +-- +-- vim.b.minisnippets_config can contain buffer-local snippets. +-- a buffer can contain code in multiple languages +-- +-- See :h MiniSnippets.default_prepare +-- +-- Return completion items produced from snippets either directly or from cache +local function get_completion_items(cache) + if not cache then return to_completion_items(MiniSnippets.expand({ match = false, insert = false })) end + + -- Compute cache id + local _, context = MiniSnippets.default_prepare({}) + local id = 'buf=' .. context.buf_id .. ',lang=' .. context.lang + + -- Return the completion items for this context from cache + if cache[id] then return cache[id] end + + -- Retrieve all raw snippets in context and transform into completion items + local snippets = MiniSnippets.expand({ match = false, insert = false }) + --- @cast snippets table + local items = to_completion_items(vim.deepcopy(snippets)) + cache[id] = items + + return items +end + +function source:get_completions(ctx, callback) + local cache = self.config.use_items_cache and self.items_cache or nil + + --- @type blink.cmp.CompletionItem[] + local items = get_completion_items(cache) + callback({ + is_incomplete_forward = false, + is_incomplete_backward = false, + items = items, + context = ctx, + ---@diagnostic disable-next-line: missing-return + }) +end + +function source:resolve(item, callback) + --- @type blink.cmp.MiniSnippetsSnippet + local snip = item.data.snip + + local desc = snip.desc + if desc and not item.documentation then + item.documentation = { + kind = 'markdown', + value = table.concat(vim.lsp.util.convert_input_to_markdown_lines(desc), '\n'), + } + end + + local detail = snip.body + if not item.detail then + if type(detail) == 'table' then detail = table.concat(detail, '\n') end + item.detail = detail + end + + callback(item) +end + +function source:execute(_, item) + -- Remove the word inserted by blink and insert snippet + -- It's safe to assume that mode is insert during completion + + --- @type blink.cmp.MiniSnippetsSnippet + local snip = item.data.snip + + local cursor = vim.api.nvim_win_get_cursor(0) + cursor[1] = cursor[1] - 1 -- nvim_buf_set_text: line is zero based + local start_col = cursor[2] - #item.insertText + vim.api.nvim_buf_set_text(0, cursor[1], start_col, cursor[1], cursor[2], {}) + + local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert + ---@diagnostic disable-next-line: missing-return + insert({ body = snip.body }) -- insert at cursor +end + +-- For external integrations to force reloading the snippets +function source:reload() + MiniSnippets.setup(MiniSnippets.config) + self.items_cache = {} +end + +return source diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/utils.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/utils.lua new file mode 100644 index 0000000..f0903b7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/sources/snippets/utils.lua @@ -0,0 +1,89 @@ +local utils = { + parse_cache = {}, +} + +--- Parses the json file and notifies the user if there's an error +---@param path string +---@param json string +function utils.parse_json_with_error_msg(path, json) + local ok, parsed = pcall(vim.json.decode, json) + if not ok then + vim.notify( + 'Failed to parse json file "' .. path .. '" for blink.cmp snippets. Error: ' .. parsed, + vim.log.levels.ERROR, + { title = 'blink.cmp' } + ) + return {} + end + return parsed +end + +---@type fun(path: string): string|nil +function utils.read_file(path) + local file = io.open(path, 'r') + if not file then return nil end + local content = file:read('*a') + file:close() + return content +end + +---@type fun(input: string): vim.snippet.Node<vim.snippet.SnippetData>|nil +function utils.safe_parse(input) + if utils.parse_cache[input] then return utils.parse_cache[input] end + + local safe, parsed = pcall(vim.lsp._snippet_grammar.parse, input) + if not safe then return nil end + + utils.parse_cache[input] = parsed + return parsed +end + +---@type fun(snippet: blink.cmp.Snippet, fallback: string): table +function utils.read_snippet(snippet, fallback) + local snippets = {} + local prefix = snippet.prefix or fallback + local description = snippet.description or fallback + local body = snippet.body + + if type(description) == 'table' then description = vim.fn.join(description, '') end + + if type(prefix) == 'table' then + for _, p in ipairs(prefix) do + snippets[p] = { + prefix = p, + body = body, + description = description, + } + end + else + snippets[prefix] = { + prefix = prefix, + body = body, + description = description, + } + end + return snippets +end + +function utils.get_tab_stops(snippet) + local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(snippet) + if not expanded_snippet then return end + + local tabstops = {} + local grammar = require('vim.lsp._snippet_grammar') + local line = 1 + local character = 1 + for _, child in ipairs(expanded_snippet.data.children) do + local lines = tostring(child) == '' and {} or vim.split(tostring(child), '\n') + line = line + math.max(#lines - 1, 0) + character = #lines == 0 and character or #lines > 1 and #lines[#lines] or (character + #lines[#lines]) + if child.type == grammar.NodeType.Placeholder or child.type == grammar.NodeType.Tabstop then + table.insert(tabstops, { index = child.data.tabstop, line = line, character = character }) + end + end + + table.sort(tabstops, function(a, b) return a.index < b.index end) + return tabstops +end + +return utils diff --git a/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/types.lua b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/types.lua new file mode 100644 index 0000000..3904f9d --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/lua/blink/cmp/types.lua @@ -0,0 +1,74 @@ +--- @alias blink.cmp.Mode 'cmdline' | 'default' + +--- @class blink.cmp.CompletionItem : lsp.CompletionItem +--- @field documentation? string | { kind: lsp.MarkupKind, value: string, render?: blink.cmp.SourceRenderDocumentation } +--- @field score_offset? number +--- @field source_id string +--- @field source_name string +--- @field cursor_column number +--- @field client_id? number + +--- @class blink.cmp.SourceRenderDocumentationOpts +--- @field item blink.cmp.CompletionItem +--- @field window blink.cmp.Window +--- @field default_implementation fun(opts: blink.cmp.RenderDetailAndDocumentationOptsPartial) + +--- @alias blink.cmp.SourceRenderDocumentation fun(opts: blink.cmp.SourceRenderDocumentationOpts) + +return { + -- some plugins mutate the vim.lsp.protocol.CompletionItemKind table + -- so we use our own copy + CompletionItemKind = { + 'Text', + 'Method', + 'Function', + 'Constructor', + 'Field', + 'Variable', + 'Class', + 'Interface', + 'Module', + 'Property', + 'Unit', + 'Value', + 'Enum', + 'Keyword', + 'Snippet', + 'Color', + 'File', + 'Reference', + 'Folder', + 'EnumMember', + 'Constant', + 'Struct', + 'Event', + 'Operator', + 'TypeParameter', + + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, + }, +} diff --git a/mut/neovim/pack/plugins/start/blink.cmp/plugin/blink-cmp.lua b/mut/neovim/pack/plugins/start/blink.cmp/plugin/blink-cmp.lua new file mode 100644 index 0000000..3b7c46a --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/plugin/blink-cmp.lua @@ -0,0 +1,5 @@ +if vim.fn.has('nvim-0.11') == 1 and vim.lsp.config then + vim.lsp.config('*', { + capabilities = require('blink.cmp').get_lsp_capabilities(), + }) +end diff --git a/mut/neovim/pack/plugins/start/blink.cmp/repro.lua b/mut/neovim/pack/plugins/start/blink.cmp/repro.lua new file mode 100644 index 0000000..ec4bdb0 --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/repro.lua @@ -0,0 +1,34 @@ +-- Run with `nvim -u repro.lua` + +vim.env.LAZY_STDPATH = '.repro' +load(vim.fn.system('curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua'))() + +---@diagnostic disable-next-line: missing-fields +require('lazy.minit').repro({ + spec = { + { + 'saghen/blink.cmp', + -- please test on `main` if possible + -- otherwise, remove this line and set `version = '*'` + build = 'cargo build --release', + opts = {}, + }, + { + 'neovim/nvim-lspconfig', + opts = { + servers = { + lua_ls = {}, + }, + }, + config = function(_, opts) + local lspconfig = require('lspconfig') + for server, config in pairs(opts.servers) do + -- passing config.capabilities to blink.cmp merges with the capabilities in your + -- `opts[server].capabilities, if you've defined it + config.capabilities = require('blink.cmp').get_lsp_capabilities() + lspconfig[server].setup(config) + end + end, + }, + }, +}) diff --git a/mut/neovim/pack/plugins/start/blink.cmp/rust-toolchain.toml b/mut/neovim/pack/plugins/start/blink.cmp/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/mut/neovim/pack/plugins/start/blink.cmp/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.envrc b/mut/neovim/pack/plugins/start/quicker.nvim/.envrc new file mode 100644 index 0000000..94b55e4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.envrc @@ -0,0 +1,3 @@ +export VIRTUAL_ENV=venv +layout python +python -c 'import pyparsing' 2> /dev/null || pip install -r scripts/requirements.txt diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/bug_report.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..1fcb4d8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,116 @@ +name: Bug Report +description: File a bug/issue +title: "bug: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Before reporting a bug, make sure to search [existing issues](https://github.com/stevearc/quicker.nvim/issues) + - type: input + attributes: + label: "Neovim version (nvim -v)" + placeholder: "0.10.0 commit db1b0ee3b30f" + validations: + required: true + - type: input + attributes: + label: "Operating system/version" + placeholder: "MacOS 11.5" + validations: + required: true + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: dropdown + attributes: + label: What is the severity of this bug? + options: + - minor (annoyance) + - tolerable (can work around it) + - breaking (some functionality is broken) + - blocking (cannot use plugin) + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. nvim -u repro.lua + 2. + 3. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Minimal example file + description: A small example file you are editing that produces the issue + validations: + required: false + - type: textarea + attributes: + label: Minimal init.lua + description: + Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua` + This uses lazy.nvim (a plugin manager). + value: | + -- DO NOT change the paths and don't remove the colorscheme + local root = vim.fn.fnamemodify("./.repro", ":p") + + -- set stdpaths to use .repro + for _, name in ipairs({ "config", "data", "state", "cache" }) do + vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name + end + + -- bootstrap lazy + local lazypath = root .. "/plugins/lazy.nvim" + if not vim.loop.fs_stat(lazypath) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "--single-branch", + "https://github.com/folke/lazy.nvim.git", + lazypath, + }) + end + vim.opt.runtimepath:prepend(lazypath) + + -- install plugins + local plugins = { + "folke/tokyonight.nvim", + { + "stevearc/quicker.nvim", + config = function() + require("quicker").setup({ + -- add your config here + }) + end, + }, + -- add any other plugins here + } + require("lazy").setup(plugins, { + root = root .. "/plugins", + }) + + vim.cmd.colorscheme("tokyonight") + -- add anything else here + render: Lua + validations: + required: false + - type: textarea + attributes: + label: Additional context + description: Any additional information or screenshots you would like to provide + validations: + required: false diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/feature_request.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..f735c8c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +name: Feature Request +description: Submit a feature request +title: "feature request: " +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Before submitting a feature request, make sure to search for [existing requests](https://github.com/stevearc/quicker.nvim/issues) + - type: checkboxes + attributes: + label: Did you check existing requests? + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Describe the feature + description: A short summary of the feature you want + validations: + required: true + - type: textarea + attributes: + label: Provide background + description: Describe the reasoning behind why you want the feature. + placeholder: I am trying to do X. My current workflow is Y. + validations: + required: false + - type: dropdown + attributes: + label: What is the significance of this feature? + options: + - nice to have + - strongly desired + - cannot use this plugin without it + validations: + required: true + - type: textarea + attributes: + label: Additional details + description: Any additional information you would like to provide. Things you've tried, alternatives considered, examples from other plugins, etc. + validations: + required: false diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-commit b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-commit new file mode 100755 index 0000000..c64fbec --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-commit @@ -0,0 +1,3 @@ +#!/bin/bash +set -e +make fastlint diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-push b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-push new file mode 100755 index 0000000..ecb23a9 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-push @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +IFS=' ' +while read local_ref _local_sha _remote_ref _remote_sha; do + remote_main=$( (git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "///master") | cut -f 4 -d / | tr -d "[:space:]") + local_ref_short=$(echo "$local_ref" | cut -f 3 -d / | tr -d "[:space:]") + if [ "$local_ref_short" = "$remote_main" ]; then + make lint + make test + fi +done diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_remove_question_label_on_comment.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_remove_question_label_on_comment.yml new file mode 100644 index 0000000..f99bba8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_remove_question_label_on_comment.yml @@ -0,0 +1,16 @@ +name: Remove Question Label on Issue Comment + +on: [issue_comment] + +jobs: + # Remove the "question" label when a new comment is added. + # This lets me ask a question, tag the issue with "question", and filter out all "question"-tagged + # issues in my "needs triage" filter. + remove_question: + runs-on: ubuntu-latest + if: github.event.sender.login != 'stevearc' + steps: + - uses: actions/checkout@v4 + - uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: question diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_request_review.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_request_review.yml new file mode 100644 index 0000000..c31f582 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_request_review.yml @@ -0,0 +1,27 @@ +name: Request Review +permissions: + pull-requests: write +on: + pull_request_target: + types: [opened, reopened, ready_for_review, synchronize] + branches-ignore: + - "release-please--**" + +jobs: + # Request review automatically when PRs are opened + request_review: + runs-on: ubuntu-latest + steps: + - name: Request Review + uses: actions/github-script@v7 + if: github.actor != 'stevearc' + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const pr = context.payload.pull_request; + github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + reviewers: ['stevearc'] + }); diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/install_nvim.sh b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/install_nvim.sh new file mode 100644 index 0000000..4c0203c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/install_nvim.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start" +mkdir -p "$PLUGINS" + +wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG-stable}/nvim.appimage" +chmod +x nvim.appimage +./nvim.appimage --appimage-extract >/dev/null +rm -f nvim.appimage +mkdir -p ~/.local/share/nvim +mv squashfs-root ~/.local/share/nvim/appimage +sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/tests.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/tests.yml new file mode 100644 index 0000000..a053f5d --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/tests.yml @@ -0,0 +1,122 @@ +name: Run tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + luacheck: + name: Luacheck + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Prepare + run: | + sudo apt-get update + sudo add-apt-repository universe + sudo apt install luarocks -y + sudo luarocks install luacheck + + - name: Run Luacheck + run: luacheck lua tests + + typecheck: + name: typecheck + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: stevearc/nvim-typecheck-action@v2 + with: + path: lua + + stylua: + name: StyLua + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Stylua + uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: v0.20.0 + args: --check lua tests + + run_tests: + strategy: + matrix: + include: + - nvim_tag: v0.10.1 + + name: Run tests + runs-on: ubuntu-22.04 + env: + NVIM_TAG: ${{ matrix.nvim_tag }} + steps: + - uses: actions/checkout@v4 + + - name: Install Neovim and dependencies + run: | + bash ./.github/workflows/install_nvim.sh + + - name: Run tests + run: | + bash ./run_tests.sh + + update_docs: + name: Update docs + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install Neovim and dependencies + run: | + bash ./.github/workflows/install_nvim.sh + + - name: Update docs + run: | + python -m pip install pyparsing==3.0.9 + make doc + - name: Commit changes + if: ${{ github.ref == 'refs/heads/master' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_MSG: | + [docgen] Update docs + skip-checks: true + run: | + git config user.email "actions@github" + git config user.name "Github Actions" + git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git + git add README.md doc + # Only commit and push if we have changes + git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF}) + + release: + name: release + + if: ${{ github.ref == 'refs/heads/master' }} + needs: + - luacheck + - stylua + - typecheck + - run_tests + - update_docs + runs-on: ubuntu-22.04 + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + release-type: simple + - uses: actions/checkout@v4 + - uses: rickstaa/action-create-tag@v1 + if: ${{ steps.release.outputs.release_created }} + with: + tag: stable + message: "Current stable release: ${{ steps.release.outputs.tag_name }}" + tag_exists_error: false + force_push_tag: true diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.gitignore b/mut/neovim/pack/plugins/start/quicker.nvim/.gitignore new file mode 100644 index 0000000..9e6bcf7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.gitignore @@ -0,0 +1,48 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +.direnv/ +.testenv/ +venv/ +doc/tags +scripts/nvim_doc_tools +scripts/nvim-typecheck-action +tests/tmp diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.luacheckrc b/mut/neovim/pack/plugins/start/quicker.nvim/.luacheckrc new file mode 100644 index 0000000..5e100b1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.luacheckrc @@ -0,0 +1,17 @@ +max_comment_line_length = false +codes = true + +exclude_files = { + "tests/", +} + +ignore = { + "212", -- Unused argument + "631", -- Line is too long + "122", -- Setting a readonly global + "542", -- Empty if branch +} + +read_globals = { + "vim", +} diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.luarc.json b/mut/neovim/pack/plugins/start/quicker.nvim/.luarc.json new file mode 100644 index 0000000..68da2f2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.luarc.json @@ -0,0 +1,9 @@ +{ + "runtime": { + "version": "LuaJIT", + "pathStrict": true + }, + "type": { + "checkTableShape": true + } +} diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.stylua.toml b/mut/neovim/pack/plugins/start/quicker.nvim/.stylua.toml new file mode 100644 index 0000000..020ce91 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.stylua.toml @@ -0,0 +1,5 @@ +column_width = 100 +indent_type = "Spaces" +indent_width = 2 +[sort_requires] +enabled = true diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/CHANGELOG.md b/mut/neovim/pack/plugins/start/quicker.nvim/CHANGELOG.md new file mode 100644 index 0000000..da1f1e5 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +## [1.3.0](https://github.com/stevearc/quicker.nvim/compare/v1.2.0...v1.3.0) (2024-12-24) + + +### Features + +* add option to remove all leading whitespace from items ([#26](https://github.com/stevearc/quicker.nvim/issues/26)) ([da7e910](https://github.com/stevearc/quicker.nvim/commit/da7e9104de4ff9303e1c722f7c9216f994622067)) +* option to scroll to closest quickfix item ([#23](https://github.com/stevearc/quicker.nvim/issues/23)) ([cc8bb67](https://github.com/stevearc/quicker.nvim/commit/cc8bb67271c093a089d205def9dd69a188c45ae1)) +* toggle function for context ([#18](https://github.com/stevearc/quicker.nvim/issues/18)) ([049d665](https://github.com/stevearc/quicker.nvim/commit/049d66534d3de5920663ee1b8dd0096d70f55a67)) + + +### Bug Fixes + +* filter vim.NIL when deserializing buffer variables ([#30](https://github.com/stevearc/quicker.nvim/issues/30)) ([a3cf525](https://github.com/stevearc/quicker.nvim/commit/a3cf5256998f9387ad8e293c6f295d286be6453f)) + +## [1.2.0](https://github.com/stevearc/quicker.nvim/compare/v1.1.1...v1.2.0) (2024-11-06) + + +### Features + +* add command modifiers to the `toggle()` and `open()` APIs ([#24](https://github.com/stevearc/quicker.nvim/issues/24)) ([95a839f](https://github.com/stevearc/quicker.nvim/commit/95a839fafff1c0a7fe970492f5159f41a90974bf)) + + +### Bug Fixes + +* crash in highlighter ([11f9eb0](https://github.com/stevearc/quicker.nvim/commit/11f9eb0c803bb9ced8c6043805de89c62bd04515)) +* guard against out of date buffer contents ([1fc29de](https://github.com/stevearc/quicker.nvim/commit/1fc29de2172235c076aa1ead6f1ee772398de732)) +* trim_leading_whitespace works with mixed tabs and spaces ([#26](https://github.com/stevearc/quicker.nvim/issues/26)) ([46e0ad6](https://github.com/stevearc/quicker.nvim/commit/46e0ad6c6a1d998a294e13cbb8b7c398e140983a)) + +## [1.1.1](https://github.com/stevearc/quicker.nvim/compare/v1.1.0...v1.1.1) (2024-08-20) + + +### Bug Fixes + +* refresh replaces all item text with buffer source ([f28fca3](https://github.com/stevearc/quicker.nvim/commit/f28fca3863f8d3679e86d8ff30d023a43fba15c8)) + +## [1.1.0](https://github.com/stevearc/quicker.nvim/compare/v1.0.0...v1.1.0) (2024-08-20) + + +### Features + +* better support for lazy loading ([29ab2a6](https://github.com/stevearc/quicker.nvim/commit/29ab2a6d4771ace240f25df028129bfc85e16ffd)) +* display errors as virtual text when expanding context ([#16](https://github.com/stevearc/quicker.nvim/issues/16)) ([6b79167](https://github.com/stevearc/quicker.nvim/commit/6b79167543f1b18e76319217a29bb4e177a5e1ae)) +* quicker.refresh preserves and display diagnostic messages ([#19](https://github.com/stevearc/quicker.nvim/issues/19)) ([349e0de](https://github.com/stevearc/quicker.nvim/commit/349e0def74ddbfc47f64ca52202e84bedf064048)) + + +### Bug Fixes + +* editor works when filename is truncated ([7a64d4e](https://github.com/stevearc/quicker.nvim/commit/7a64d4ea2b641cc8671443d0ff26de2924894c9f)) +* **editor:** load buffer if necessary before save_changes ([#14](https://github.com/stevearc/quicker.nvim/issues/14)) ([59a610a](https://github.com/stevearc/quicker.nvim/commit/59a610a2163a51a019bde769bf2e2eec1654e4d4)) +* error when quickfix buffer is hidden and items are added ([#8](https://github.com/stevearc/quicker.nvim/issues/8)) ([a8b885b](https://github.com/stevearc/quicker.nvim/commit/a8b885be246666922aca7f296195986a1cae3344)) +* guard against double-replacing a diagnostic line ([2dc0f80](https://github.com/stevearc/quicker.nvim/commit/2dc0f800770f8956c24a6d70fa61e7ec2e102d8a)) +* **highlight:** check if src_line exists before trying to highlight it ([#6](https://github.com/stevearc/quicker.nvim/issues/6)) ([b6a3d2f](https://github.com/stevearc/quicker.nvim/commit/b6a3d2f6aed7882e8bea772f82ba80b5535157a9)) +* include number of files in editor message ([#13](https://github.com/stevearc/quicker.nvim/issues/13)) ([7d2f6d3](https://github.com/stevearc/quicker.nvim/commit/7d2f6d33c7d680b0a18580cfa5feb17302f389d4)) +* missing highlight groups for headers ([5dafd80](https://github.com/stevearc/quicker.nvim/commit/5dafd80225ba462517c38e7b176bd3df52ccfb35)) +* prevent error when treesitter parser is missing ([#4](https://github.com/stevearc/quicker.nvim/issues/4)) ([5cc096a](https://github.com/stevearc/quicker.nvim/commit/5cc096aad4ba1c1e17b6d76cb87fd7155cf9a559)) +* show filename for invalid items ([#11](https://github.com/stevearc/quicker.nvim/issues/11)) ([514817d](https://github.com/stevearc/quicker.nvim/commit/514817dfb0a2828fe2c6183f996a31847c8aa789)) + +## 1.0.0 (2024-08-07) + + +### Bug Fixes + +* guard against race condition in syntax highlighting ([#1](https://github.com/stevearc/quicker.nvim/issues/1)) ([03d9811](https://github.com/stevearc/quicker.nvim/commit/03d9811c8ac037e4e9c8f4ba0dfd1dff0367e0ac)) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/LICENSE b/mut/neovim/pack/plugins/start/quicker.nvim/LICENSE new file mode 100644 index 0000000..ce6136c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Steven Arcangeli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/Makefile b/mut/neovim/pack/plugins/start/quicker.nvim/Makefile new file mode 100644 index 0000000..8643a8d --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/Makefile @@ -0,0 +1,52 @@ +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## all: generate docs, lint, and run tests +.PHONY: all +all: doc lint test + +venv: + python3 -m venv venv + venv/bin/pip install -r scripts/requirements.txt + +## doc: generate documentation +.PHONY: doc +doc: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py generate + venv/bin/python scripts/main.py lint + +## test: run tests +.PHONY: test +test: + ./run_tests.sh + +## update_snapshots: Update the test snapshot files +.PHONY: update_snapshots +update_snapshots: + ./run_tests.sh --update + +## lint: run linters and LuaLS typechecking +.PHONY: lint +lint: scripts/nvim-typecheck-action fastlint + ./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua + +## fastlint: run only fast linters +.PHONY: fastlint +fastlint: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py lint + luacheck lua tests --formatter plain + stylua --check lua tests + +scripts/nvim_doc_tools: + git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools + +scripts/nvim-typecheck-action: + git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action + +## clean: reset the repository to a clean state +.PHONY: clean +clean: + rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/README.md b/mut/neovim/pack/plugins/start/quicker.nvim/README.md new file mode 100644 index 0000000..6e07620 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/README.md @@ -0,0 +1,375 @@ +# quicker.nvim + +Improved UI and workflow for the Neovim quickfix + +<!-- TOC --> + +- [Requirements](#requirements) +- [Features](#features) +- [Installation](#installation) +- [Setup](#setup) +- [Options](#options) +- [Highlights](#highlights) +- [API](#api) +- [Other Plugins](#other-plugins) + +<!-- /TOC --> + +## Requirements + +- Neovim 0.10+ + +## Features + +- **Improved styling** - including syntax highlighting of grep results. +- **Show context lines** - easily view lines above and below the quickfix results. +- **Editable buffer** - make changes across your whole project by editing the quickfix buffer and `:w`. +- **API helpers** - some helper methods for common tasks, such as toggling the quickfix. + +**Improved styling** (colorscheme: [Duskfox](https://github.com/EdenEast/nightfox.nvim/)) \ +Before \ +<img width="695" alt="Screenshot 2024-07-30 at 6 03 39 PM" src="https://github.com/user-attachments/assets/8faa4790-8a7a-4d05-882e-c4e8e7653b00"> + +After \ +<img width="686" alt="Screenshot 2024-07-30 at 2 05 49 PM" src="https://github.com/user-attachments/assets/90cf87dd-83ec-4967-88aa-5ffe3e1e6623"> + +**Context lines** around the results \ +<img width="816" alt="Screenshot 2024-07-30 at 2 06 17 PM" src="https://github.com/user-attachments/assets/844445c9-328f-4f18-91d9-b32d32d3ef39"> + +**Editing the quickfix** to apply changes across multiple files + +https://github.com/user-attachments/assets/5065ac4d-ec24-49d1-a95d-232344b17484 + +## Installation + +quicker.nvim supports all the usual plugin managers + +<details> + <summary>lazy.nvim</summary> + +```lua +{ + 'stevearc/quicker.nvim', + event = "FileType qf", + ---@module "quicker" + ---@type quicker.SetupOptions + opts = {}, +} +``` + +</details> + +<details> + <summary>Packer</summary> + +```lua +require("packer").startup(function() + use({ + "stevearc/quicker.nvim", + config = function() + require("quicker").setup() + end, + }) +end) +``` + +</details> + +<details> + <summary>Paq</summary> + +```lua +require("paq")({ + { "stevearc/quicker.nvim" }, +}) +``` + +</details> + +<details> + <summary>vim-plug</summary> + +```vim +Plug 'stevearc/quicker.nvim' +``` + +</details> + +<details> + <summary>dein</summary> + +```vim +call dein#add('stevearc/quicker.nvim') +``` + +</details> + +<details> + <summary>Pathogen</summary> + +```sh +git clone --depth=1 https://github.com/stevearc/quicker.nvim.git ~/.vim/bundle/ +``` + +</details> + +<details> + <summary>Neovim native package</summary> + +```sh +git clone --depth=1 https://github.com/stevearc/quicker.nvim.git \ + "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/pack/quicker/start/quicker.nvim +``` + +</details> + +## Setup + +You will need to call `setup()` for quicker to start working + +```lua +require("quicker").setup() +``` + +It's not required to pass in any options, but you may wish to to set some keymaps. + +```lua +vim.keymap.set("n", "<leader>q", function() + require("quicker").toggle() +end, { + desc = "Toggle quickfix", +}) +vim.keymap.set("n", "<leader>l", function() + require("quicker").toggle({ loclist = true }) +end, { + desc = "Toggle loclist", +}) +require("quicker").setup({ + keys = { + { + ">", + function() + require("quicker").expand({ before = 2, after = 2, add_to_existing = true }) + end, + desc = "Expand quickfix context", + }, + { + "<", + function() + require("quicker").collapse() + end, + desc = "Collapse quickfix context", + }, + }, +}) +``` + +## Options + +A complete list of all configuration options + +<!-- OPTIONS --> +```lua +require("quicker").setup({ + -- Local options to set for quickfix + opts = { + buflisted = false, + number = false, + relativenumber = false, + signcolumn = "auto", + winfixheight = true, + wrap = false, + }, + -- Set to false to disable the default options in `opts` + use_default_opts = true, + -- Keymaps to set for the quickfix buffer + keys = { + -- { ">", "<cmd>lua require('quicker').expand()<CR>", desc = "Expand quickfix content" }, + }, + -- Callback function to run any custom logic or keymaps for the quickfix buffer + on_qf = function(bufnr) end, + edit = { + -- Enable editing the quickfix like a normal buffer + enabled = true, + -- Set to true to write buffers after applying edits. + -- Set to "unmodified" to only write unmodified buffers. + autosave = "unmodified", + }, + -- Keep the cursor to the right of the filename and lnum columns + constrain_cursor = true, + highlight = { + -- Use treesitter highlighting + treesitter = true, + -- Use LSP semantic token highlighting + lsp = true, + -- Load the referenced buffers to apply more accurate highlights (may be slow) + load_buffers = true, + }, + follow = { + -- When quickfix window is open, scroll to closest item to the cursor + enabled = false, + }, + -- Map of quickfix item type to icon + type_icons = { + E = " ", + W = " ", + I = " ", + N = " ", + H = " ", + }, + -- Border characters + borders = { + vert = "┃", + -- Strong headers separate results from different files + strong_header = "━", + strong_cross = "╋", + strong_end = "┫", + -- Soft headers separate results within the same file + soft_header = "╌", + soft_cross = "╂", + soft_end = "┨", + }, + -- How to trim the leading whitespace from results. Can be 'all', 'common', or false + trim_leading_whitespace = "common", + -- Maximum width of the filename column + max_filename_width = function() + return math.floor(math.min(95, vim.o.columns / 2)) + end, + -- How far the header should extend to the right + header_length = function(type, start_col) + return vim.o.columns - start_col + end, +}) +``` + +<!-- /OPTIONS --> + +## Highlights + +These are the highlight groups that are used to style the quickfix buffer. You can set these +highlight groups yourself or use `:help winhighlight` in the setup `opts` option to override them +for just the quickfix window. + +- `QuickFixHeaderHard` - Used for the header that divides results from different files +- `QuickFixHeaderSoft` - Used for the header that divides results within the same file +- `Delimiter` - Used for the divider between filename, line number, and text +- `QuickFixLineNr` - Used for the line number +- `QuickFixFilename` - Used for the filename +- `QuickFixFilenameInvalid` - Used for the filename when `valid = 0` +- `DiagnosticSign*` - Used for the signs that display the quickfix error type + +## API + +<!-- API --> + +### expand(opts) + +`expand(opts)` \ +Expand the context around the quickfix results. + +| Param | Type | Desc | +| ---------------- | ------------------------- | -------------------------------------------------------------- | +| opts | `nil\|quicker.ExpandOpts` | | +| >before | `nil\|integer` | Number of lines of context to show before the line (default 2) | +| >after | `nil\|integer` | Number of lines of context to show after the line (default 2) | +| >add_to_existing | `nil\|boolean` | | +| >loclist_win | `nil\|integer` | | + +**Note:** +<pre> +If there are multiple quickfix items for the same line of a file, only the first +one will remain after calling expand(). +</pre> + +### collapse() + +`collapse()` \ +Collapse the context around quickfix results, leaving only the `valid` items. + + +### toggle_expand(opts) + +`toggle_expand(opts)` \ +Toggle the expanded context around the quickfix results. + +| Param | Type | Desc | +| ---------------- | ------------------------- | -------------------------------------------------------------- | +| opts | `nil\|quicker.ExpandOpts` | | +| >before | `nil\|integer` | Number of lines of context to show before the line (default 2) | +| >after | `nil\|integer` | Number of lines of context to show after the line (default 2) | +| >add_to_existing | `nil\|boolean` | | +| >loclist_win | `nil\|integer` | | + +### refresh(loclist_win, opts) + +`refresh(loclist_win, opts)` \ +Update the quickfix list with the current buffer text for each item. + +| Param | Type | Desc | +| ----------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| loclist_win | `nil\|integer` | | +| opts | `nil\|quicker.RefreshOpts` | | +| >keep_diagnostics | `nil\|boolean` | If a line has a diagnostic type, keep the original text and display it as virtual text after refreshing from source. | + +### is_open(loclist_win) + +`is_open(loclist_win)` + +| Param | Type | Desc | +| ----------- | -------------- | ---------------------------------------------------------------------- | +| loclist_win | `nil\|integer` | Check if loclist is open for the given window. If nil, check quickfix. | + +### toggle(opts) + +`toggle(opts)` \ +Toggle the quickfix or loclist window. + +| Param | Type | Desc | +| -------------- | -------------------------- | ----------------------------------------------------------------------------------- | +| opts | `nil\|quicker.OpenOpts` | | +| >loclist | `nil\|boolean` | Toggle the loclist instead of the quickfix list | +| >focus | `nil\|boolean` | Focus the quickfix window after toggling (default false) | +| >height | `nil\|integer` | Height of the quickfix window when opened. Defaults to number of items in the list. | +| >min_height | `nil\|integer` | Minimum height of the quickfix window. Default 4. | +| >max_height | `nil\|integer` | Maximum height of the quickfix window. Default 10. | +| >open_cmd_mods | `nil\|quicker.OpenCmdMods` | A table of modifiers for the quickfix or loclist open commands. | + +### open(opts) + +`open(opts)` \ +Open the quickfix or loclist window. + +| Param | Type | Desc | +| -------------- | -------------------------- | ----------------------------------------------------------------------------------- | +| opts | `nil\|quicker.OpenOpts` | | +| >loclist | `nil\|boolean` | Toggle the loclist instead of the quickfix list | +| >focus | `nil\|boolean` | Focus the quickfix window after toggling (default false) | +| >height | `nil\|integer` | Height of the quickfix window when opened. Defaults to number of items in the list. | +| >min_height | `nil\|integer` | Minimum height of the quickfix window. Default 4. | +| >max_height | `nil\|integer` | Maximum height of the quickfix window. Default 10. | +| >open_cmd_mods | `nil\|quicker.OpenCmdMods` | A table of modifiers for the quickfix or loclist open commands. | + +### close(opts) + +`close(opts)` \ +Close the quickfix or loclist window. + +| Param | Type | Desc | +| -------- | ------------------------ | ---------------------------------------------- | +| opts | `nil\|quicker.CloseOpts` | | +| >loclist | `nil\|boolean` | Close the loclist instead of the quickfix list | +<!-- /API --> + +## Other Plugins + +In general quicker.nvim should play nice with other quickfix plugins (🟢), except if they change the +format of the quickfix buffer. Quicker.nvim relies on owning the `:help quickfixtextfunc` for the +other features to function, so some other plugins you may need to disable or not use parts of their +functionality (🟡). Some plugins have features that completely conflict with quicker.nvim (🔴). + +- 🟢 [nvim-bqf](https://github.com/kevinhwang91/nvim-bqf) - Another bundle of several improvements including a floating preview window and fzf integration. +- 🟢 [vim-qf](https://github.com/romainl/vim-qf) - Adds some useful mappings and default behaviors. +- 🟡 [trouble.nvim](https://github.com/folke/trouble.nvim) - A custom UI for displaying quickfix and many other lists. Does not conflict with quicker.nvim, but instead presents an alternative way to manage and view the quickfix. +- 🟡 [listish.nvim](https://github.com/arsham/listish.nvim) - Provides utilities for adding items to the quickfix and theming (which conflicts with quicker.nvim). +- 🔴 [quickfix-reflector.vim](https://github.com/stefandtw/quickfix-reflector.vim) - Also provides an "editable quickfix". I used this for many years and would recommend it. +- 🔴 [replacer.nvim](https://github.com/gabrielpoca/replacer.nvim) - Another "editable quickfix" plugin. diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/doc/quicker.txt b/mut/neovim/pack/plugins/start/quicker.nvim/doc/quicker.txt new file mode 100644 index 0000000..cf3dbe7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/doc/quicker.txt @@ -0,0 +1,181 @@ +*quicker.txt* +*Quicker* *quicker* *quicker.nvim* +-------------------------------------------------------------------------------- +CONTENTS *quicker-contents* + + 1. Options |quicker-options| + 2. Api |quicker-api| + +-------------------------------------------------------------------------------- +OPTIONS *quicker-options* + +>lua + require("quicker").setup({ + -- Local options to set for quickfix + opts = { + buflisted = false, + number = false, + relativenumber = false, + signcolumn = "auto", + winfixheight = true, + wrap = false, + }, + -- Set to false to disable the default options in `opts` + use_default_opts = true, + -- Keymaps to set for the quickfix buffer + keys = { + -- { ">", "<cmd>lua require('quicker').expand()<CR>", desc = "Expand quickfix content" }, + }, + -- Callback function to run any custom logic or keymaps for the quickfix buffer + on_qf = function(bufnr) end, + edit = { + -- Enable editing the quickfix like a normal buffer + enabled = true, + -- Set to true to write buffers after applying edits. + -- Set to "unmodified" to only write unmodified buffers. + autosave = "unmodified", + }, + -- Keep the cursor to the right of the filename and lnum columns + constrain_cursor = true, + highlight = { + -- Use treesitter highlighting + treesitter = true, + -- Use LSP semantic token highlighting + lsp = true, + -- Load the referenced buffers to apply more accurate highlights (may be slow) + load_buffers = true, + }, + follow = { + -- When quickfix window is open, scroll to closest item to the cursor + enabled = false, + }, + -- Map of quickfix item type to icon + type_icons = { + E = " ", + W = " ", + I = " ", + N = " ", + H = " ", + }, + -- Border characters + borders = { + vert = "┃", + -- Strong headers separate results from different files + strong_header = "━", + strong_cross = "╋", + strong_end = "┫", + -- Soft headers separate results within the same file + soft_header = "╌", + soft_cross = "╂", + soft_end = "┨", + }, + -- How to trim the leading whitespace from results. Can be 'all', 'common', or false + trim_leading_whitespace = "common", + -- Maximum width of the filename column + max_filename_width = function() + return math.floor(math.min(95, vim.o.columns / 2)) + end, + -- How far the header should extend to the right + header_length = function(type, start_col) + return vim.o.columns - start_col + end, + }) +< + +-------------------------------------------------------------------------------- +API *quicker-api* + +expand({opts}) *quicker.expand* + Expand the context around the quickfix results. + + Parameters: + {opts} `nil|quicker.ExpandOpts` + {before} `nil|integer` Number of lines of context to show + before the line (default 2) + {after} `nil|integer` Number of lines of context to show + after the line (default 2) + {add_to_existing} `nil|boolean` + {loclist_win} `nil|integer` + + Note: + If there are multiple quickfix items for the same line of a file, only the first + one will remain after calling expand(). + +collapse() *quicker.collapse* + Collapse the context around quickfix results, leaving only the `valid` + items. + + +toggle_expand({opts}) *quicker.toggle_expand* + Toggle the expanded context around the quickfix results. + + Parameters: + {opts} `nil|quicker.ExpandOpts` + {before} `nil|integer` Number of lines of context to show + before the line (default 2) + {after} `nil|integer` Number of lines of context to show + after the line (default 2) + {add_to_existing} `nil|boolean` + {loclist_win} `nil|integer` + +refresh({loclist_win}, {opts}) *quicker.refresh* + Update the quickfix list with the current buffer text for each item. + + Parameters: + {loclist_win} `nil|integer` + {opts} `nil|quicker.RefreshOpts` + {keep_diagnostics} `nil|boolean` If a line has a diagnostic type, keep + the original text and display it as virtual text + after refreshing from source. + +is_open({loclist_win}) *quicker.is_open* + + Parameters: + {loclist_win} `nil|integer` Check if loclist is open for the given window. + If nil, check quickfix. + +toggle({opts}) *quicker.toggle* + Toggle the quickfix or loclist window. + + Parameters: + {opts} `nil|quicker.OpenOpts` + {loclist} `nil|boolean` Toggle the loclist instead of the + quickfix list + {focus} `nil|boolean` Focus the quickfix window after toggling + (default false) + {height} `nil|integer` Height of the quickfix window when + opened. Defaults to number of items in the list. + {min_height} `nil|integer` Minimum height of the quickfix window. + Default 4. + {max_height} `nil|integer` Maximum height of the quickfix window. + Default 10. + {open_cmd_mods} `nil|quicker.OpenCmdMods` A table of modifiers for the + quickfix or loclist open commands. + +open({opts}) *quicker.open* + Open the quickfix or loclist window. + + Parameters: + {opts} `nil|quicker.OpenOpts` + {loclist} `nil|boolean` Toggle the loclist instead of the + quickfix list + {focus} `nil|boolean` Focus the quickfix window after toggling + (default false) + {height} `nil|integer` Height of the quickfix window when + opened. Defaults to number of items in the list. + {min_height} `nil|integer` Minimum height of the quickfix window. + Default 4. + {max_height} `nil|integer` Maximum height of the quickfix window. + Default 10. + {open_cmd_mods} `nil|quicker.OpenCmdMods` A table of modifiers for the + quickfix or loclist open commands. + +close({opts}) *quicker.close* + Close the quickfix or loclist window. + + Parameters: + {opts} `nil|quicker.CloseOpts` + {loclist} `nil|boolean` Close the loclist instead of the quickfix list + +================================================================================ +vim:tw=80:ts=2:ft=help:norl:syntax=help: diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/config.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/config.lua new file mode 100644 index 0000000..716f010 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/config.lua @@ -0,0 +1,189 @@ +local default_config = { + -- Local options to set for quickfix + opts = { + buflisted = false, + number = false, + relativenumber = false, + signcolumn = "auto", + winfixheight = true, + wrap = false, + }, + -- Set to false to disable the default options in `opts` + use_default_opts = true, + -- Keymaps to set for the quickfix buffer + keys = { + -- { ">", "<cmd>lua require('quicker').expand()<CR>", desc = "Expand quickfix content" }, + }, + -- Callback function to run any custom logic or keymaps for the quickfix buffer + on_qf = function(bufnr) end, + edit = { + -- Enable editing the quickfix like a normal buffer + enabled = true, + -- Set to true to write buffers after applying edits. + -- Set to "unmodified" to only write unmodified buffers. + autosave = "unmodified", + }, + -- Keep the cursor to the right of the filename and lnum columns + constrain_cursor = true, + highlight = { + -- Use treesitter highlighting + treesitter = true, + -- Use LSP semantic token highlighting + lsp = true, + -- Load the referenced buffers to apply more accurate highlights (may be slow) + load_buffers = true, + }, + follow = { + -- When quickfix window is open, scroll to closest item to the cursor + enabled = false, + }, + -- Map of quickfix item type to icon + type_icons = { + E = " ", + W = " ", + I = " ", + N = " ", + H = " ", + }, + -- Border characters + borders = { + vert = "┃", + -- Strong headers separate results from different files + strong_header = "━", + strong_cross = "╋", + strong_end = "┫", + -- Soft headers separate results within the same file + soft_header = "╌", + soft_cross = "╂", + soft_end = "┨", + }, + -- How to trim the leading whitespace from results. Can be 'all', 'common', or false + trim_leading_whitespace = "common", + -- Maximum width of the filename column + max_filename_width = function() + return math.floor(math.min(95, vim.o.columns / 2)) + end, + -- How far the header should extend to the right + header_length = function(type, start_col) + return vim.o.columns - start_col + end, +} + +---@alias quicker.TrimEnum "all"|"common"|false + +---@class quicker.Config +---@field on_qf fun(bufnr: number) +---@field opts table<string, any> +---@field keys quicker.Keymap[] +---@field use_default_opts boolean +---@field constrain_cursor boolean +---@field highlight quicker.HighlightConfig +---@field follow quicker.FollowConfig +---@field edit quicker.EditConfig +---@field type_icons table<string, string> +---@field borders quicker.Borders +---@field trim_leading_whitespace quicker.TrimEnum +---@field max_filename_width fun(): integer +---@field header_length fun(type: "hard"|"soft", start_col: integer): integer +local M = {} + +---@class (exact) quicker.SetupOptions +---@field on_qf? fun(bufnr: number) Callback function to run any custom logic or keymaps for the quickfix buffer +---@field opts? table<string, any> Local options to set for quickfix +---@field keys? quicker.Keymap[] Keymaps to set for the quickfix buffer +---@field use_default_opts? boolean Set to false to disable the default options in `opts` +---@field constrain_cursor? boolean Keep the cursor to the right of the filename and lnum columns +---@field highlight? quicker.SetupHighlightConfig Configure syntax highlighting +---@field follow? quicker.SetupFollowConfig Configure cursor following +---@field edit? quicker.SetupEditConfig +---@field type_icons? table<string, string> Map of quickfix item type to icon +---@field borders? quicker.SetupBorders Characters used for drawing the borders +---@field trim_leading_whitespace? quicker.TrimEnum How to trim the leading whitespace from results +---@field max_filename_width? fun(): integer Maximum width of the filename column +---@field header_length? fun(type: "hard"|"soft", start_col: integer): integer How far the header should extend to the right + +local has_setup = false +---@param opts? quicker.SetupOptions +M.setup = function(opts) + opts = opts or {} + local new_conf = vim.tbl_deep_extend("keep", opts, default_config) + + for k, v in pairs(new_conf) do + M[k] = v + end + + -- Shim for when this was only a boolean. 'true' meant 'common' + if M.trim_leading_whitespace == true then + M.trim_leading_whitespace = "common" + end + + -- Remove the default opts values if use_default_opts is false + if not new_conf.use_default_opts then + M.opts = opts.opts or {} + end + has_setup = true +end + +---@class (exact) quicker.Keymap +---@field [1] string Key sequence +---@field [2] any Command to run +---@field desc? string +---@field mode? string +---@field expr? boolean +---@field nowait? boolean +---@field remap? boolean +---@field replace_keycodes? boolean +---@field silent? boolean + +---@class (exact) quicker.Borders +---@field vert string +---@field strong_header string +---@field strong_cross string +---@field strong_end string +---@field soft_header string +---@field soft_cross string +---@field soft_end string + +---@class (exact) quicker.SetupBorders +---@field vert? string +---@field strong_header? string Strong headers separate results from different files +---@field strong_cross? string +---@field strong_end? string +---@field soft_header? string Soft headers separate results within the same file +---@field soft_cross? string +---@field soft_end? string + +---@class (exact) quicker.HighlightConfig +---@field treesitter boolean +---@field lsp boolean +---@field load_buffers boolean + +---@class (exact) quicker.SetupHighlightConfig +---@field treesitter? boolean Enable treesitter syntax highlighting +---@field lsp? boolean Use LSP semantic token highlighting +---@field load_buffers? boolean Load the referenced buffers to apply more accurate highlights (may be slow) + +---@class (exact) quicker.FollowConfig +---@field enabled boolean + +---@class (exact) quicker.SetupFollowConfig +---@field enabled? boolean + +---@class (exact) quicker.EditConfig +---@field enabled boolean +---@field autosave boolean|"unmodified" + +---@class (exact) quicker.SetupEditConfig +---@field enabled? boolean +---@field autosave? boolean|"unmodified" + +return setmetatable(M, { + -- If the user hasn't called setup() yet, make sure we correctly set up the config object so there + -- aren't random crashes. + __index = function(self, key) + if not has_setup then + M.setup() + end + return rawget(self, key) + end, +}) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/context.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/context.lua new file mode 100644 index 0000000..f5bbb87 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/context.lua @@ -0,0 +1,315 @@ +local util = require("quicker.util") + +local M = {} + +---@class (exact) quicker.QFContext +---@field num_before integer +---@field num_after integer + +---@class (exact) quicker.ExpandOpts +---@field before? integer Number of lines of context to show before the line (default 2) +---@field after? integer Number of lines of context to show after the line (default 2) +---@field add_to_existing? boolean +---@field loclist_win? integer + +---@param item QuickFixItem +---@param new_text string +local function update_item_text_keep_diagnostics(item, new_text) + -- If this is an "error" item, replace the text with the source line and store that text + -- in the user data so we can add it as virtual text later + if item.type ~= "" and not vim.endswith(new_text, item.text) then + local user_data = util.get_user_data(item) + if not user_data.error_text then + user_data.error_text = item.text + item.user_data = user_data + end + end + item.text = new_text +end + +---@param opts? quicker.ExpandOpts +function M.expand(opts) + opts = opts or {} + if not opts.loclist_win and util.get_win_type(0) == "l" then + opts.loclist_win = vim.api.nvim_get_current_win() + end + local qf_list + if opts.loclist_win then + qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + local winid = qf_list.winid + if not winid then + vim.notify("Cannot find quickfix window", vim.log.levels.ERROR) + return + end + local ctx = qf_list.context or {} + if type(ctx) ~= "table" then + -- If the quickfix had a non-table context, we're going to have to overwrite it + ctx = {} + end + ---@type quicker.QFContext + local quicker_ctx = ctx.quicker + if not quicker_ctx then + quicker_ctx = { num_before = 0, num_after = 0 } + ctx.quicker = quicker_ctx + end + local curpos = vim.api.nvim_win_get_cursor(winid)[1] + local cur_item = qf_list.items[curpos] + local newpos + + -- calculate the number of lines to show before and after the current line + local num_before = opts.before or 2 + if opts.add_to_existing then + num_before = num_before + quicker_ctx.num_before + end + num_before = math.max(0, num_before) + quicker_ctx.num_before = num_before + local num_after = opts.after or 2 + if opts.add_to_existing then + num_after = num_after + quicker_ctx.num_after + end + num_after = math.max(0, num_after) + quicker_ctx.num_after = num_after + + local items = {} + ---@type nil|QuickFixItem + local prev_item + ---@param i integer + ---@return nil|QuickFixItem + local function get_next_item(i) + local item = qf_list.items[i] + for j = i + 1, #qf_list.items do + local next_item = qf_list.items[j] + -- Next valid item that is on a different line (since we dedupe same-line items) + if + next_item.valid == 1 and (item.bufnr ~= next_item.bufnr or item.lnum ~= next_item.lnum) + then + return next_item + end + end + end + + for i, item in ipairs(qf_list.items) do + (function() + ---@cast item QuickFixItem + if item.valid == 0 or item.bufnr == 0 then + return + end + + if not vim.api.nvim_buf_is_loaded(item.bufnr) then + vim.fn.bufload(item.bufnr) + end + + local overlaps_previous = false + local header_type = "hard" + local low = math.max(0, item.lnum - 1 - num_before) + if prev_item then + if prev_item.bufnr == item.bufnr then + -- If this is the second match on the same line, skip this item + if prev_item.lnum == item.lnum then + return + end + header_type = "soft" + if prev_item.lnum + num_after >= low then + low = math.min(item.lnum - 1, prev_item.lnum + num_after) + overlaps_previous = true + end + end + end + + local high = item.lnum + num_after + local next_item = get_next_item(i) + if next_item then + if next_item.bufnr == item.bufnr and next_item.lnum <= high then + high = next_item.lnum - 1 + end + end + + local item_start_idx = #items + local lines = vim.api.nvim_buf_get_lines(item.bufnr, low, high, false) + for j, line in ipairs(lines) do + if j + low == item.lnum then + update_item_text_keep_diagnostics(item, line) + table.insert(items, item) + else + table.insert(items, { + bufnr = item.bufnr, + lnum = low + j, + text = line, + valid = 0, + user_data = { lnum = low + j }, + }) + end + if cur_item.bufnr == item.bufnr and cur_item.lnum == low + j then + newpos = #items + end + end + + -- Add the header to the first item in this sequence, if one is needed + if prev_item and not overlaps_previous then + local first_item = items[item_start_idx + 1] + if first_item then + first_item.user_data = first_item.user_data or {} + first_item.user_data.header = header_type + end + end + + prev_item = item + end)() + + if i == curpos and not newpos then + newpos = #items + end + end + + if opts.loclist_win then + vim.fn.setloclist( + opts.loclist_win, + {}, + "r", + { items = items, title = qf_list.title, context = ctx } + ) + else + vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = ctx }) + end + + pcall(vim.api.nvim_win_set_cursor, qf_list.winid, { newpos, 0 }) +end + +---@class (exact) quicker.CollapseArgs +---@field loclist_win? integer +--- +function M.collapse(opts) + opts = opts or {} + if not opts.loclist_win and util.get_win_type(0) == "l" then + opts.loclist_win = vim.api.nvim_get_current_win() + end + local curpos = vim.api.nvim_win_get_cursor(0)[1] + local qf_list + if opts.loclist_win then + qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + local items = {} + local last_item + for i, item in ipairs(qf_list.items) do + if item.valid == 1 then + if item.user_data then + -- Clear the header, if present + item.user_data.header = nil + end + table.insert(items, item) + if i <= curpos then + last_item = #items + end + end + end + + vim.tbl_filter(function(item) + return item.valid == 1 + end, qf_list.items) + + local ctx = qf_list.context or {} + if type(ctx) == "table" then + local quicker_ctx = ctx.quicker + if quicker_ctx then + quicker_ctx = { num_before = 0, num_after = 0 } + ctx.quicker = quicker_ctx + end + end + + if opts.loclist_win then + vim.fn.setloclist( + opts.loclist_win, + {}, + "r", + { items = items, title = qf_list.title, context = qf_list.context } + ) + else + vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context }) + end + if qf_list.winid then + if last_item then + vim.api.nvim_win_set_cursor(qf_list.winid, { last_item, 0 }) + end + end +end + +---@param opts? quicker.ExpandOpts +function M.toggle(opts) + opts = opts or {} + local ctx + if opts.loclist_win then + ctx = vim.fn.getloclist(opts.loclist_win, { context = 0 }).context + else + ctx = vim.fn.getqflist({ context = 0 }).context + end + + if + type(ctx) == "table" + and ctx.quicker + and (ctx.quicker.num_before > 0 or ctx.quicker.num_after > 0) + then + M.collapse() + else + M.expand(opts) + end +end + +---@class (exact) quicker.RefreshOpts +---@field keep_diagnostics? boolean If a line has a diagnostic type, keep the original text and display it as virtual text after refreshing from source. + +---@param loclist_win? integer +---@param opts? quicker.RefreshOpts +function M.refresh(loclist_win, opts) + opts = vim.tbl_extend("keep", opts or {}, { keep_diagnostics = true }) + if not loclist_win then + local ok, qf = pcall(vim.fn.getloclist, 0, { filewinid = 0 }) + if ok and qf.filewinid and qf.filewinid ~= 0 then + loclist_win = qf.filewinid + end + end + + local qf_list + if loclist_win then + qf_list = vim.fn.getloclist(loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + + local items = {} + for _, item in ipairs(qf_list.items) do + if item.bufnr ~= 0 and item.lnum ~= 0 then + if not vim.api.nvim_buf_is_loaded(item.bufnr) then + vim.fn.bufload(item.bufnr) + end + local line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + if line then + if opts.keep_diagnostics then + update_item_text_keep_diagnostics(item, line) + else + item.text = line + end + table.insert(items, item) + end + else + table.insert(items, item) + end + end + + if loclist_win then + vim.fn.setloclist( + loclist_win, + {}, + "r", + { items = items, title = qf_list.title, context = qf_list.context } + ) + else + vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/cursor.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/cursor.lua new file mode 100644 index 0000000..58ee587 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/cursor.lua @@ -0,0 +1,44 @@ +local M = {} + +local function constrain_cursor() + local display = require("quicker.display") + local cur = vim.api.nvim_win_get_cursor(0) + local line = vim.api.nvim_buf_get_lines(0, cur[1] - 1, cur[1], true)[1] + local idx = line:find(display.EM_QUAD, 1, true) + if not idx then + return + end + local min_col = idx + display.EM_QUAD_LEN - 1 + if cur[2] < min_col then + vim.api.nvim_win_set_cursor(0, { cur[1], min_col }) + end +end + +---@param bufnr number +function M.constrain_cursor(bufnr) + -- HACK: we have to defer this call because sometimes the autocmds don't take effect. + vim.schedule(function() + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + local aug = vim.api.nvim_create_augroup("quicker", { clear = false }) + vim.api.nvim_create_autocmd("InsertEnter", { + desc = "Constrain quickfix cursor position", + group = aug, + nested = true, + buffer = bufnr, + -- For some reason the cursor bounces back to its original position, + -- so we have to defer the call + callback = vim.schedule_wrap(constrain_cursor), + }) + vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, { + desc = "Constrain quickfix cursor position", + nested = true, + group = aug, + buffer = bufnr, + callback = constrain_cursor, + }) + end) +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/display.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/display.lua new file mode 100644 index 0000000..5c551d3 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/display.lua @@ -0,0 +1,601 @@ +local config = require("quicker.config") +local fs = require("quicker.fs") +local highlight = require("quicker.highlight") +local util = require("quicker.util") + +local M = {} + +local EM_QUAD = " " +local EM_QUAD_LEN = EM_QUAD:len() +M.EM_QUAD = EM_QUAD +M.EM_QUAD_LEN = EM_QUAD_LEN + +---@class (exact) QuickFixUserData +---@field header? "hard"|"soft" When present, this line is a header +---@field lnum? integer Encode the lnum separately for valid=0 items +---@field error_text? string Error text to be added as virtual text on the line + +---@class (exact) QuickFixItem +---@field text string +---@field type string +---@field lnum integer line number in the buffer (first line is 1) +---@field end_lnum integer end of line number if the item is multiline +---@field col integer column number (first column is 1) +---@field end_col integer end of column number if the item has range +---@field vcol 0|1 if true "col" is visual column. If false "col" is byte index +---@field nr integer error number +---@field pattern string search pattern used to locate the error +---@field bufnr integer number of buffer that has the file name +---@field module string +---@field valid 0|1 +---@field user_data? any + +---@param type string +---@return string +local function get_icon(type) + return config.type_icons[type:upper()] or "U" +end + +local sign_highlight_map = { + E = "DiagnosticSignError", + W = "DiagnosticSignWarn", + I = "DiagnosticSignInfo", + H = "DiagnosticSignHint", + N = "DiagnosticSignHint", +} +local virt_text_highlight_map = { + E = "DiagnosticVirtualTextError", + W = "DiagnosticVirtualTextWarn", + I = "DiagnosticVirtualTextInfo", + H = "DiagnosticVirtualTextHint", + N = "DiagnosticVirtualTextHint", +} + +---@param item QuickFixItem +M.get_filename_from_item = function(item) + if item.module and item.module ~= "" then + return item.module + elseif item.bufnr > 0 then + local bufname = vim.api.nvim_buf_get_name(item.bufnr) + local path = fs.shorten_path(bufname) + local max_len = config.max_filename_width() + if max_len == 0 then + return "" + elseif path:len() > max_len then + path = "…" .. path:sub(path:len() - max_len - 1) + end + return path + else + return "" + end +end + +local _col_width_cache = {} +---@param id integer +---@param items QuickFixItem[] +---@return integer +local function get_cached_qf_col_width(id, items) + local cached = _col_width_cache[id] + if not cached or cached[2] ~= #items then + local max_len = 0 + for _, item in ipairs(items) do + max_len = math.max(max_len, vim.api.nvim_strwidth(M.get_filename_from_item(item))) + end + + cached = { max_len, #items } + _col_width_cache[id] = cached + end + return cached[1] +end + +---@param items QuickFixItem[] +---@return table<integer, string> +local function calc_whitespace_prefix(items) + local prefixes = {} + if config.trim_leading_whitespace ~= "common" then + return prefixes + end + + for _, item in ipairs(items) do + if item.bufnr ~= 0 and not item.text:match("^%s*$") then + local prefix = prefixes[item.bufnr] + if not prefix or not vim.startswith(item.text, prefix) then + local new_prefix = item.text:match("^%s*") + + -- The new line should have strictly less whitespace as the previous line. If not, then + -- there is some whitespace disagreement (e.g. tabs vs spaces) and we should not try to trim + -- anything. + if prefix and not vim.startswith(prefix, new_prefix) then + new_prefix = "" + end + prefixes[item.bufnr] = new_prefix + + if new_prefix == "" then + break + end + end + end + end + return prefixes +end + +-- Highlighting can be slow because it requires loading buffers and parsing them with treesitter, so +-- we pipeline it and break it up with defers to keep the editor responsive. +local add_qf_highlights +-- We have two queues, one to apply "fast" highlights, and one that will load the buffer (slow) +-- and then apply more correct highlights. The second queue is always processed after the first. +local _pending_fast_highlights = {} +local _pending_bufload_highlights = {} +local _running = false +local function do_next_highlight() + if _running then + return + end + _running = true + + local next_info = table.remove(_pending_fast_highlights, 1) + if not next_info then + next_info = table.remove(_pending_bufload_highlights, 1) + end + + if next_info then + local ok, err = xpcall(add_qf_highlights, debug.traceback, next_info) + if not ok then + vim.api.nvim_err_writeln(err) + end + else + _running = false + return + end + + vim.defer_fn(function() + _running = false + do_next_highlight() + end, 20) +end + +---@param queue QuickFixTextFuncInfo[] +---@param info QuickFixTextFuncInfo +local function add_info_to_queue(queue, info) + for _, i in ipairs(queue) do + -- If we're already processing a highlight for this quickfix, just expand the range + if i.id == info.id and i.winid == info.winid and i.quickfix == info.quickfix then + i.start_idx = math.min(i.start_idx, info.start_idx) + i.end_idx = math.max(i.end_idx, info.end_idx) + return + end + end + table.insert(queue, info) +end + +---@param info QuickFixTextFuncInfo +local function schedule_highlights(info) + -- If this info already has force_bufload, then we don't want to add it to the first queue. + if not info.force_bufload then + add_info_to_queue(_pending_fast_highlights, info) + end + + if config.highlight.load_buffers then + local info2 = vim.deepcopy(info) + info2.force_bufload = true + add_info_to_queue(_pending_bufload_highlights, info2) + end + + vim.schedule(do_next_highlight) +end + +---@param qfbufnr integer +---@param item QuickFixItem +---@param line string +---@param lnum integer +local function add_item_highlights_from_buf(qfbufnr, item, line, lnum) + local prefixes = vim.b[qfbufnr].qf_prefixes or {} + local ns = vim.api.nvim_create_namespace("quicker_highlights") + -- TODO re-apply highlights when a buffer is loaded or a LSP receives semantic tokens + local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + if not src_line then + return + end + + -- If the lines differ only in leading whitespace, we should add highlights anyway and adjust + -- the offset. + local item_space = item.text:match("^%s*"):len() + local src_space = src_line:match("^%s*"):len() + + -- Only add highlights if the text in the quickfix matches the source line + if item.text:sub(item_space + 1) == src_line:sub(src_space + 1) then + local offset = line:find(EM_QUAD, 1, true) + EM_QUAD_LEN - 1 + local prefix = prefixes[item.bufnr] + if type(prefix) == "string" then + -- Since prefixes get deserialized from vim.b, if there are holes in the map they get + -- filled with `vim.NIL`, so we have to check that the retrieved value is a string. + offset = offset - prefix:len() + end + offset = offset - src_space + item_space + if config.trim_leading_whitespace == "all" then + offset = offset - item_space + end + + -- Add treesitter highlights + if config.highlight.treesitter then + for _, hl in ipairs(highlight.buf_get_ts_highlights(item.bufnr, item.lnum)) do + local start_col, end_col, hl_group = hl[1], hl[2], hl[3] + if end_col == -1 then + end_col = src_line:len() + end + -- If the highlight starts at the beginning of the source line, then it might be off the + -- buffer in the quickfix because we've removed leading whitespace. If so, clamp the value + -- to 0. Except, for some reason 0 gives incorrect results, but -1 works properly even + -- though -1 should indicate the *end* of the line. Not sure why this work, but it does. + local hl_start = math.max(-1, start_col + offset) + vim.api.nvim_buf_set_extmark(qfbufnr, ns, lnum - 1, hl_start, { + hl_group = hl_group, + end_col = end_col + offset, + priority = 100, + strict = false, + }) + end + end + + -- Add LSP semantic token highlights + if config.highlight.lsp then + for _, hl in ipairs(highlight.buf_get_lsp_highlights(item.bufnr, item.lnum)) do + local start_col, end_col, hl_group, priority = hl[1], hl[2], hl[3], hl[4] + vim.api.nvim_buf_set_extmark(qfbufnr, ns, lnum - 1, start_col + offset, { + hl_group = hl_group, + end_col = end_col + offset, + priority = vim.highlight.priorities.semantic_tokens + priority, + strict = false, + }) + end + end + end +end + +---@param qfbufnr integer +---@param info QuickFixTextFuncInfo +local function highlight_buffer_when_entered(qfbufnr, info) + if vim.b[qfbufnr].pending_highlight then + return + end + vim.api.nvim_create_autocmd("BufEnter", { + desc = "Highlight quickfix buffer when entered", + buffer = qfbufnr, + nested = true, + once = true, + callback = function() + vim.b[qfbufnr].pending_highlight = nil + info.start_idx = 1 + info.end_idx = vim.api.nvim_buf_line_count(qfbufnr) + schedule_highlights(info) + end, + }) + vim.b[qfbufnr].pending_highlight = true +end + +---@param info QuickFixTextFuncInfo +---@return {qfbufnr: integer, id: integer, context?: any} +---@overload fun(info: QuickFixTextFuncInfo, all: true): {qfbufnr: integer, id: integer, items: QuickFixItem[], context?: any} +local function load_qf(info, all) + local query + if all then + query = { all = 0 } + else + query = { id = info.id, items = 0, qfbufnr = 0, context = 0 } + end + if info.quickfix == 1 then + return vim.fn.getqflist(query) + else + return vim.fn.getloclist(info.winid, query) + end +end + +---@param info QuickFixTextFuncInfo +add_qf_highlights = function(info) + local qf_list = load_qf(info, true) + local qfbufnr = qf_list.qfbufnr + if not qfbufnr or qfbufnr == 0 then + return + elseif info.end_idx < info.start_idx then + return + end + + local lines = vim.api.nvim_buf_get_lines(qfbufnr, 0, -1, false) + if #lines == 1 and lines[1] == "" then + -- If the quickfix buffer is not visible, it is possible that quickfixtextfunc has run but the + -- buffer has not been populated yet. If that is the case, we should exit early and ensure that + -- the highlighting task runs again when the buffer is opened in a window. + -- see https://github.com/stevearc/quicker.nvim/pull/8 + highlight_buffer_when_entered(qfbufnr, info) + return + end + local ns = vim.api.nvim_create_namespace("quicker_highlights") + + -- Only clear the error namespace during the first pass of "fast" highlighting + if not info.force_bufload then + local err_ns = vim.api.nvim_create_namespace("quicker_err") + vim.api.nvim_buf_clear_namespace(qfbufnr, err_ns, 0, -1) + end + + local start = vim.uv.hrtime() / 1e6 + for i = info.start_idx, info.end_idx do + vim.api.nvim_buf_clear_namespace(qfbufnr, ns, i - 1, i) + ---@type nil|QuickFixItem + local item = qf_list.items[i] + -- If the quickfix list has changed length since the async highlight job has started, + -- we should abort and let the next async highlight task pick it up. + if not item then + return + end + + local line = lines[i] + if not line then + break + end + if item.bufnr ~= 0 then + local loaded = vim.api.nvim_buf_is_loaded(item.bufnr) + if not loaded and info.force_bufload then + vim.fn.bufload(item.bufnr) + loaded = true + end + + if loaded then + add_item_highlights_from_buf(qfbufnr, item, line, i) + elseif config.highlight.treesitter then + local filename = vim.split(line, EM_QUAD, { plain = true })[1] + local offset = filename:len() + EM_QUAD_LEN + local text = line:sub(offset + 1) + for _, hl in ipairs(highlight.get_heuristic_ts_highlights(item, text)) do + local start_col, end_col, hl_group = hl[1], hl[2], hl[3] + start_col = start_col + offset + end_col = end_col + offset + vim.api.nvim_buf_set_extmark(qfbufnr, ns, i - 1, start_col, { + hl_group = hl_group, + end_col = end_col, + priority = 100, + strict = false, + }) + end + end + end + + local user_data = util.get_user_data(item) + -- Set sign if item has a type + if item.type and item.type ~= "" then + local mark = { + sign_text = get_icon(item.type), + sign_hl_group = sign_highlight_map[item.type:upper()], + invalidate = true, + } + if user_data.error_text then + mark.virt_text = { + { user_data.error_text, virt_text_highlight_map[item.type:upper()] or "Normal" }, + } + end + vim.api.nvim_buf_set_extmark(qfbufnr, ns, i - 1, 0, mark) + end + + -- If we've been processing for too long, defer to preserve editor responsiveness + local delta = vim.uv.hrtime() / 1e6 - start + if delta > 50 then + info.start_idx = i + 1 + schedule_highlights(info) + return + end + end + + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, ns, info.end_idx, -1) +end + +---@param str string +---@param len integer +---@return string +local function rpad(str, len) + return str .. string.rep(" ", len - vim.api.nvim_strwidth(str)) +end + +---@param items QuickFixItem[] +---@return integer +local function get_lnum_width(items) + local max_len = 2 + local max = 99 + for _, item in ipairs(items) do + if item.lnum > max then + max_len = tostring(item.lnum):len() + max = item.lnum + end + end + return max_len +end + +---@param text string +---@param prefix? string +local function remove_prefix(text, prefix) + local ret + if prefix and prefix ~= "" then + ret = text:sub(prefix:len() + 1) + else + ret = text + end + + return ret +end + +---@class QuickFixTextFuncInfo +---@field id integer +---@field start_idx integer +---@field end_idx integer +---@field winid integer +---@field quickfix 1|0 +---@field force_bufload? boolean field injected by us to control if we're forcing a bufload for the syntax highlighting + +-- TODO when appending to a qflist, the alignment can be thrown off +-- TODO when appending to a qflist, the prefix could mismatch earlier lines +---@param info QuickFixTextFuncInfo +---@return string[] +function M.quickfixtextfunc(info) + local b = config.borders + local qf_list = load_qf(info, true) + local locations = {} + local invalid_filenames = {} + local headers = {} + local ret = {} + local items = qf_list.items + local lnum_width = get_lnum_width(items) + local col_width = get_cached_qf_col_width(info.id, items) + local lnum_fmt = string.format("%%%ds", lnum_width) + local prefixes = calc_whitespace_prefix(items) + local no_filenames = col_width == 0 + + local function get_virt_text(lnum) + -- If none of the quickfix items have filenames, we don't need the lnum column and we only need + -- to show a single delimiter. Technically we don't need any delimiter, but this maintains some + -- of the original qf behavior while being a bit more visually appealing. + if no_filenames then + return { { b.vert, "Delimiter" } } + else + return { + { b.vert, "Delimiter" }, + { lnum_fmt:format(lnum), "QuickFixLineNr" }, + { b.vert, "Delimiter" }, + } + end + end + + for i = info.start_idx, info.end_idx do + local item = items[i] + local user_data = util.get_user_data(item) + + -- First check if there's a header that we need to save to render as virtual text later + if user_data.header == "hard" then + -- Header when expanded QF list + local pieces = { + string.rep(b.strong_header, col_width + 1), + b.strong_cross, + string.rep(b.strong_header, lnum_width), + } + local header_len = config.header_length("hard", col_width + lnum_width + 2) + if header_len > 0 then + table.insert(pieces, b.strong_cross) + table.insert(pieces, string.rep(b.strong_header, header_len)) + else + table.insert(pieces, b.strong_end) + end + table.insert(headers, { i, { { table.concat(pieces, ""), "QuickFixHeaderHard" } } }) + elseif user_data.header == "soft" then + -- Soft header when expanded QF list + local pieces = { + string.rep(b.soft_header, col_width + 1), + b.soft_cross, + string.rep(b.soft_header, lnum_width), + } + local header_len = config.header_length("soft", col_width + lnum_width + 2) + if header_len > 0 then + table.insert(pieces, b.soft_cross) + table.insert(pieces, string.rep(b.soft_header, header_len)) + else + table.insert(pieces, b.soft_end) + end + table.insert(headers, { i, { { table.concat(pieces, ""), "QuickFixHeaderSoft" } } }) + end + + -- Construct the lines and save the filename + lnum to render as virtual text later + local trimmed_text + if config.trim_leading_whitespace == "all" then + trimmed_text = item.text:gsub("^%s*", "") + elseif config.trim_leading_whitespace == "common" then + trimmed_text = remove_prefix(item.text, prefixes[item.bufnr]) + else + trimmed_text = item.text + end + if item.valid == 1 then + -- Matching line + local lnum = item.lnum == 0 and " " or item.lnum + local filename = rpad(M.get_filename_from_item(item), col_width) + table.insert(locations, get_virt_text(lnum)) + table.insert(ret, filename .. EM_QUAD .. trimmed_text) + elseif user_data.lnum then + -- Non-matching line from quicker.nvim context lines + local filename = string.rep(" ", col_width) + table.insert(locations, get_virt_text(user_data.lnum)) + table.insert(ret, filename .. EM_QUAD .. trimmed_text) + else + -- Other non-matching line + local lnum = item.lnum == 0 and " " or item.lnum + local filename = rpad(M.get_filename_from_item(item), col_width) + table.insert(locations, get_virt_text(lnum)) + invalid_filenames[#locations] = true + table.insert(ret, filename .. EM_QUAD .. trimmed_text) + end + end + + -- Render the filename+lnum and the headers as virtual text + local start_idx = info.start_idx + local set_virt_text + set_virt_text = function() + qf_list = load_qf(info) + if qf_list.qfbufnr > 0 then + -- Sometimes the buffer is not fully populated yet. If so, we should try again later. + local num_lines = vim.api.nvim_buf_line_count(qf_list.qfbufnr) + if num_lines < info.end_idx then + vim.schedule(set_virt_text) + return + end + + local ns = vim.api.nvim_create_namespace("quicker_locations") + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, ns, start_idx - 1, -1) + local header_ns = vim.api.nvim_create_namespace("quicker_headers") + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, header_ns, start_idx - 1, -1) + local filename_ns = vim.api.nvim_create_namespace("quicker_filenames") + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, filename_ns, start_idx - 1, -1) + + local idmap = {} + local lines = vim.api.nvim_buf_get_lines(qf_list.qfbufnr, start_idx - 1, -1, false) + for i, loc in ipairs(locations) do + local end_col = lines[i]:find(EM_QUAD, 1, true) or col_width + local lnum = start_idx + i - 1 + local id = + vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, ns, lnum - 1, end_col + EM_QUAD_LEN - 1, { + right_gravity = false, + virt_text = loc, + virt_text_pos = "inline", + invalidate = true, + }) + idmap[id] = lnum + + -- Highlight the filename + vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, filename_ns, lnum - 1, 0, { + hl_group = invalid_filenames[i] and "QuickFixFilenameInvalid" or "QuickFixFilename", + right_gravity = false, + end_col = end_col, + priority = 100, + invalidate = true, + }) + end + vim.b[qf_list.qfbufnr].qf_ext_id_to_item_idx = idmap + + for _, pair in ipairs(headers) do + local i, header = pair[1], pair[2] + local lnum = start_idx + i - 1 + vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, header_ns, lnum - 1, 0, { + virt_lines = { header }, + virt_lines_above = true, + }) + end + end + end + vim.schedule(set_virt_text) + + -- If we just rendered the last item, add highlights + if info.end_idx == #items then + schedule_highlights(info) + + if qf_list.qfbufnr > 0 then + vim.b[qf_list.qfbufnr].qf_prefixes = prefixes + end + end + + return ret +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/editor.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/editor.lua new file mode 100644 index 0000000..3f5db65 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/editor.lua @@ -0,0 +1,405 @@ +local config = require("quicker.config") +local display = require("quicker.display") +local util = require("quicker.util") +local M = {} + +---@class (exact) quicker.ParsedLine +---@field filename? string +---@field lnum? integer +---@field text? string + +---@param n integer +---@param base string +---@param pluralized? string +---@return string +local function plural(n, base, pluralized) + if n == 1 then + return base + elseif pluralized then + return pluralized + else + return base .. "s" + end +end + +---Replace the text in a quickfix line, preserving the lineno virt text +---@param bufnr integer +---@param lnum integer +---@param new_text string +local function replace_qf_line(bufnr, lnum, new_text) + local old_line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] + + local old_idx = old_line:find(display.EM_QUAD, 1, true) + local new_idx = new_text:find(display.EM_QUAD, 1, true) + + -- If we're missing the em quad delimiter in either the old or new text, the best we can do is + -- replace the whole line + if not old_idx or not new_idx then + vim.api.nvim_buf_set_text(bufnr, lnum - 1, 0, lnum - 1, -1, { new_text }) + return + end + + -- Replace first the text after the em quad, then the filename before. + -- This keeps the line number virtual text in the same location. + vim.api.nvim_buf_set_text( + bufnr, + lnum - 1, + old_idx + display.EM_QUAD_LEN - 1, + lnum - 1, + -1, + { new_text:sub(new_idx + display.EM_QUAD_LEN) } + ) + vim.api.nvim_buf_set_text(bufnr, lnum - 1, 0, lnum - 1, old_idx, { new_text:sub(1, new_idx) }) +end + +---@param bufnr integer +---@param lnum integer +---@param text string +---@param text_hl? string +local function add_qf_error(bufnr, lnum, text, text_hl) + local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] + local col = line:find(config.borders.vert, 1, true) + if col then + col = line:find(config.borders.vert, col + config.borders.vert:len(), true) + + config.borders.vert:len() + - 1 + else + col = 0 + end + local offset = vim.api.nvim_strwidth(line:sub(1, col)) + local ns = vim.api.nvim_create_namespace("quicker_err") + vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, col, { + virt_text = { { config.type_icons.E, "DiagnosticSignError" } }, + virt_text_pos = "inline", + virt_lines = { + { + { string.rep(" ", offset), "Normal" }, + { "↳ ", "DiagnosticError" }, + { text, text_hl or "Normal" }, + }, + }, + }) +end + +---@param item QuickFixItem +---@param needle string +---@param src_line nil|string +---@return nil|table text_change +---@return nil|string error +local function get_text_edit(item, needle, src_line) + if not src_line then + return nil + elseif item.text == needle then + return nil + elseif src_line ~= item.text then + if item.text:gsub("^%s*", "") == src_line:gsub("^%s*", "") then + -- If they only disagree in their leading whitespace, just take the changes after the + -- whitespace and assume that the whitespace hasn't changed. + -- This can happen if the setqflist caller doesn't use the same whitespace as the source file, + -- for example overseer.nvim Grep will convert tabs to spaces because the embedded terminal + -- will convert tabs to spaces. + needle = src_line:match("^%s*") .. needle:gsub("^%s*", "") + else + return nil, "buffer text does not match source text" + end + end + + return { + newText = needle, + range = { + start = { + line = item.lnum - 1, + character = 0, + }, + ["end"] = { + line = item.lnum - 1, + character = #src_line, + }, + }, + } +end + +---Deserialize qf_prefixes from the buffer, converting vim.NIL to nil +---@param bufnr integer +---@return table<integer, string> +local function load_qf_prefixes(bufnr) + local prefixes = vim.b[bufnr].qf_prefixes or {} + for k, v in pairs(prefixes) do + if v == vim.NIL then + prefixes[k] = nil + end + end + return prefixes +end + +---@param bufnr integer +---@param loclist_win? integer +local function save_changes(bufnr, loclist_win) + if not vim.bo[bufnr].modified then + return + end + local ns = vim.api.nvim_create_namespace("quicker_err") + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + local qf_list + if loclist_win then + qf_list = vim.fn.getloclist(loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + + local changes = {} + local function add_change(buf, text_edit) + if not changes[buf] then + changes[buf] = {} + end + local last_edit = changes[buf][#changes[buf]] + if last_edit and vim.deep_equal(last_edit.range, text_edit.range) then + if last_edit.newText == text_edit.newText then + return + else + return "conflicting changes on the same line" + end + end + table.insert(changes[buf], text_edit) + end + + -- Parse the buffer + local winid = util.buf_find_win(bufnr) + local new_items = {} + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) + local errors = {} + local exit_early = false + local prefixes = load_qf_prefixes(bufnr) + local ext_id_to_item_idx = vim.b[bufnr].qf_ext_id_to_item_idx + for i, line in ipairs(lines) do + (function() + local extmarks = util.get_lnum_extmarks(bufnr, i, line:len()) + assert(#extmarks <= 1, string.format("Found more than one extmark on line %d", i)) + local found_idx + if extmarks[1] then + found_idx = ext_id_to_item_idx[extmarks[1][1]] + end + + -- If we didn't find a match, the line was most likely added or reordered + if not found_idx then + add_qf_error( + bufnr, + i, + "quicker.nvim does not support adding or reordering quickfix items", + "DiagnosticError" + ) + if winid then + vim.api.nvim_win_set_cursor(winid, { i, 0 }) + end + exit_early = true + return + end + + -- Trim the filename off of the line + local idx = string.find(line, display.EM_QUAD, 1, true) + if not idx then + add_qf_error( + bufnr, + i, + "The delimiter between filename and text has been deleted. Undo, delete line, or :Refresh.", + "DiagnosticError" + ) + if winid then + vim.api.nvim_win_set_cursor(winid, { i, 0 }) + end + exit_early = true + return + end + local text = line:sub(idx + display.EM_QUAD_LEN) + + local item = qf_list.items[found_idx] + if item.bufnr ~= 0 and item.lnum ~= 0 then + if not vim.api.nvim_buf_is_loaded(item.bufnr) then + vim.fn.bufload(item.bufnr) + end + local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + + -- add the whitespace prefix back to the parsed line text + if config.trim_leading_whitespace == "common" then + text = (prefixes[item.bufnr] or "") .. text + elseif config.trim_leading_whitespace == "all" and src_line then + text = src_line:match("^%s*") .. text + end + + if src_line and text ~= src_line then + if text:gsub("^%s*", "") == src_line:gsub("^%s*", "") then + -- If they only disagree in their leading whitespace, just take the changes after the + -- whitespace and assume that the whitespace hasn't changed + text = src_line:match("^%s*") .. text:gsub("^%s*", "") + end + end + + local text_edit, err = get_text_edit(item, text, src_line) + if text_edit then + local chng_err = add_change(item.bufnr, text_edit) + if chng_err then + add_qf_error(bufnr, i, chng_err, "DiagnosticError") + if winid then + vim.api.nvim_win_set_cursor(winid, { i, 0 }) + end + exit_early = true + return + end + elseif err then + table.insert(new_items, item) + errors[#new_items] = line + return + end + end + + -- add item to future qflist + item.text = text + table.insert(new_items, item) + end)() + if exit_early then + vim.schedule(function() + vim.bo[bufnr].modified = true + end) + return + end + end + + ---@type table<integer, boolean> + local buf_was_modified = {} + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + buf_was_modified[buf] = vim.bo[buf].modified + end + local autosave = config.edit.autosave + local num_applied = 0 + local modified_bufs = {} + for chg_buf, text_edits in pairs(changes) do + modified_bufs[chg_buf] = true + num_applied = num_applied + #text_edits + vim.lsp.util.apply_text_edits(text_edits, chg_buf, "utf-8") + local was_modified = buf_was_modified[chg_buf] + local should_save = autosave == true or (autosave == "unmodified" and not was_modified) + -- Autosave changed buffers if they were not modified before + if should_save then + vim.api.nvim_buf_call(chg_buf, function() + vim.cmd.update({ mods = { emsg_silent = true, noautocmd = true } }) + end) + end + end + if num_applied > 0 then + local num_files = vim.tbl_count(modified_bufs) + local num_errors = vim.tbl_count(errors) + if num_errors > 0 then + local total = num_errors + num_applied + vim.notify( + string.format( + "Applied %d/%d %s in %d %s", + num_applied, + total, + plural(total, "change"), + num_files, + plural(num_files, "file") + ), + vim.log.levels.WARN + ) + else + vim.notify( + string.format( + "Applied %d %s in %d %s", + num_applied, + plural(num_applied, "change"), + num_files, + plural(num_files, "file") + ), + vim.log.levels.INFO + ) + end + end + + local view + if winid then + view = vim.api.nvim_win_call(winid, function() + return vim.fn.winsaveview() + end) + end + if loclist_win then + vim.fn.setloclist( + loclist_win, + {}, + "r", + { items = new_items, title = qf_list.title, context = qf_list.context } + ) + else + vim.fn.setqflist( + {}, + "r", + { items = new_items, title = qf_list.title, context = qf_list.context } + ) + end + if winid and view then + vim.api.nvim_win_call(winid, function() + vim.fn.winrestview(view) + end) + end + + -- Schedule this so it runs after the save completes, and the buffer will be correctly marked as modified + if not vim.tbl_isempty(errors) then + vim.schedule(function() + -- Mark the lines with changes that could not be applied + for lnum, new_text in pairs(errors) do + replace_qf_line(bufnr, lnum, new_text) + local item = new_items[lnum] + local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + add_qf_error(bufnr, lnum, src_line) + if winid and vim.api.nvim_win_is_valid(winid) then + vim.api.nvim_win_set_cursor(winid, { lnum, 0 }) + end + end + end) + + -- Notify user that some changes could not be applied + local cnt = vim.tbl_count(errors) + local change_text = cnt == 1 and "change" or "changes" + vim.notify( + string.format( + "%d %s could not be applied due to conflicts in the source buffer. Please :Refresh and try again.", + cnt, + change_text + ), + vim.log.levels.ERROR + ) + end +end + +-- TODO add support for undo past last change + +---@param bufnr integer +function M.setup_editor(bufnr) + local aug = vim.api.nvim_create_augroup("quicker", { clear = false }) + local loclist_win + vim.api.nvim_buf_call(bufnr, function() + local ok, qf = pcall(vim.fn.getloclist, 0, { filewinid = 0 }) + if ok and qf.filewinid and qf.filewinid ~= 0 then + loclist_win = qf.filewinid + end + end) + + -- Set a name for the buffer so we can save it + local bufname = string.format("quickfix-%d", bufnr) + if vim.api.nvim_buf_get_name(bufnr) == "" then + vim.api.nvim_buf_set_name(bufnr, bufname) + end + vim.bo[bufnr].modifiable = true + + vim.api.nvim_create_autocmd("BufWriteCmd", { + desc = "quicker.nvim apply changes on write", + group = aug, + buffer = bufnr, + nested = true, + callback = function(args) + save_changes(args.buf, loclist_win) + vim.bo[args.buf].modified = false + end, + }) +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/follow.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/follow.lua new file mode 100644 index 0000000..d5500a2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/follow.lua @@ -0,0 +1,84 @@ +local util = require("quicker.util") +local M = {} + +M.seek_to_position = function() + if util.is_open(0) then + local qf_list = vim.fn.getloclist(0, { winid = 0, items = 0 }) + local new_pos = M.calculate_pos(qf_list.items) + if new_pos then + M.set_pos(qf_list.winid, new_pos) + end + end + + if util.is_open() then + local qf_list = vim.fn.getqflist({ winid = 0, items = 0 }) + local new_pos = M.calculate_pos(qf_list.items) + if new_pos then + M.set_pos(qf_list.winid, new_pos) + end + end +end + +---Calculate the current buffer/cursor location in the quickfix list +---@param list QuickFixItem[] +---@return nil|integer +M.calculate_pos = function(list) + if vim.bo.buftype ~= "" then + return + end + local bufnr = vim.api.nvim_get_current_buf() + local cursor = vim.api.nvim_win_get_cursor(0) + local lnum, col = cursor[1], cursor[2] + 1 + local prev_lnum = -1 + local prev_col = -1 + local found_buf = false + local ret + for i, entry in ipairs(list) do + if entry.bufnr ~= bufnr then + if found_buf then + return ret + end + else + found_buf = true + + -- If we detect that the list isn't sorted, bail. + if + prev_lnum > -1 + and (entry.lnum < prev_lnum or (entry.lnum == prev_lnum and entry.col <= prev_col)) + then + return + end + + if prev_lnum == -1 or lnum > entry.lnum or (lnum == entry.lnum and col >= entry.col) then + ret = i + end + prev_lnum = entry.lnum + prev_col = entry.col + end + end + + return ret +end + +local timers = {} +---@param winid integer +---@param pos integer +M.set_pos = function(winid, pos) + local timer = timers[winid] + if timer then + timer:close() + end + timer = assert(vim.uv.new_timer()) + timers[winid] = timer + timer:start(10, 0, function() + timer:close() + timers[winid] = nil + vim.schedule(function() + if vim.api.nvim_win_is_valid(winid) then + pcall(vim.api.nvim_win_set_cursor, winid, { pos, 0 }) + end + end) + end) +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/fs.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/fs.lua new file mode 100644 index 0000000..f0e94dc --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/fs.lua @@ -0,0 +1,98 @@ +local M = {} + +---@type boolean +M.is_windows = vim.uv.os_uname().version:match("Windows") + +M.is_mac = vim.uv.os_uname().sysname == "Darwin" + +M.is_linux = not M.is_windows and not M.is_mac + +---@type string +M.sep = M.is_windows and "\\" or "/" + +---@param ... string +M.join = function(...) + return table.concat({ ... }, M.sep) +end + +---Check if OS path is absolute +---@param dir string +---@return boolean +M.is_absolute = function(dir) + if M.is_windows then + return dir:match("^%a:\\") + else + return vim.startswith(dir, "/") + end +end + +M.abspath = function(path) + if not M.is_absolute(path) then + path = vim.fn.fnamemodify(path, ":p") + end + return path +end + +local home_dir = assert(vim.uv.os_homedir()) + +---@param path string +---@param relative_to? string Shorten relative to this path (default cwd) +---@return string +M.shorten_path = function(path, relative_to) + if not relative_to then + relative_to = vim.fn.getcwd() + end + local relpath + if M.is_subpath(relative_to, path) then + local idx = relative_to:len() + 1 + -- Trim the dividing slash if it's not included in relative_to + if not vim.endswith(relative_to, "/") and not vim.endswith(relative_to, "\\") then + idx = idx + 1 + end + relpath = path:sub(idx) + if relpath == "" then + relpath = "." + end + end + if M.is_subpath(home_dir, path) then + local homepath = "~" .. path:sub(home_dir:len() + 1) + if not relpath or homepath:len() < relpath:len() then + return homepath + end + end + return relpath or path +end + +--- Returns true if candidate is a subpath of root, or if they are the same path. +---@param root string +---@param candidate string +---@return boolean +M.is_subpath = function(root, candidate) + if candidate == "" then + return false + end + root = vim.fs.normalize(M.abspath(root)) + -- Trim trailing "/" from the root + if root:find("/", -1) then + root = root:sub(1, -2) + end + candidate = vim.fs.normalize(M.abspath(candidate)) + if M.is_windows then + root = root:lower() + candidate = candidate:lower() + end + if root == candidate then + return true + end + local prefix = candidate:sub(1, root:len()) + if prefix ~= root then + return false + end + + local candidate_starts_with_sep = candidate:find("/", root:len() + 1, true) == root:len() + 1 + local root_ends_with_sep = root:find("/", root:len(), true) == root:len() + + return candidate_starts_with_sep or root_ends_with_sep +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/highlight.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/highlight.lua new file mode 100644 index 0000000..a7323aa --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/highlight.lua @@ -0,0 +1,222 @@ +local M = {} + +---@class quicker.TSHighlight +---@field [1] integer start_col +---@field [2] integer end_col +---@field [3] string highlight group + +local _cached_queries = {} +---@param lang string +---@return vim.treesitter.Query? +local function get_highlight_query(lang) + local query = _cached_queries[lang] + if query == nil then + query = vim.treesitter.query.get(lang, "highlights") or false + _cached_queries[lang] = query + end + if query then + return query + end +end + +---@param bufnr integer +---@param lnum integer +---@return quicker.TSHighlight[] +function M.buf_get_ts_highlights(bufnr, lnum) + local filetype = vim.bo[bufnr].filetype + if not filetype or filetype == "" then + filetype = vim.filetype.match({ buf = bufnr }) or "" + end + local lang = vim.treesitter.language.get_lang(filetype) or filetype + if lang == "" then + return {} + end + local ok, parser = pcall(vim.treesitter.get_parser, bufnr, lang) + if not ok or not parser then + return {} + end + + local row = lnum - 1 + if not parser:is_valid() then + parser:parse(true) + end + + local highlights = {} + parser:for_each_tree(function(tstree, tree) + if not tstree then + return + end + + local root_node = tstree:root() + local root_start_row, _, root_end_row, _ = root_node:range() + + -- Only worry about trees within the line range + if root_start_row > row or root_end_row < row then + return + end + + local query = get_highlight_query(tree:lang()) + + -- Some injected languages may not have highlight queries. + if not query then + return + end + + for capture, node, metadata in query:iter_captures(root_node, bufnr, row, root_end_row + 1) do + if capture == nil then + break + end + + local range = vim.treesitter.get_range(node, bufnr, metadata[capture]) + local start_row, start_col, _, end_row, end_col, _ = unpack(range) + if start_row > row then + break + end + local capture_name = query.captures[capture] + local hl = string.format("@%s.%s", capture_name, tree:lang()) + if end_row > start_row then + end_col = -1 + end + table.insert(highlights, { start_col, end_col, hl }) + end + end) + + return highlights +end + +---@class quicker.LSPHighlight +---@field [1] integer start_col +---@field [2] integer end_col +---@field [3] string highlight group +---@field [4] integer priority modifier + +-- We're accessing private APIs here. This could break in the future. +local STHighlighter = vim.lsp.semantic_tokens.__STHighlighter + +--- Copied from Neovim semantic_tokens.lua +--- Do a binary search of the tokens in the half-open range [lo, hi). +--- +--- Return the index i in range such that tokens[j].line < line for all j < i, and +--- tokens[j].line >= line for all j >= i, or return hi if no such index is found. +--- +---@private +local function lower_bound(tokens, line, lo, hi) + while lo < hi do + local mid = bit.rshift(lo + hi, 1) -- Equivalent to floor((lo + hi) / 2). + if tokens[mid].line < line then + lo = mid + 1 + else + hi = mid + end + end + return lo +end + +---@param bufnr integer +---@param lnum integer +---@return quicker.LSPHighlight[] +function M.buf_get_lsp_highlights(bufnr, lnum) + local highlighter = STHighlighter.active[bufnr] + if not highlighter then + return {} + end + local ft = vim.bo[bufnr].filetype + + local lsp_highlights = {} + for _, client in pairs(highlighter.client_state) do + local highlights = client.current_result.highlights + if highlights then + local idx = lower_bound(highlights, lnum - 1, 1, #highlights + 1) + for i = idx, #highlights do + local token = highlights[i] + + if token.line >= lnum then + break + end + + table.insert( + lsp_highlights, + { token.start_col, token.end_col, string.format("@lsp.type.%s.%s", token.type, ft), 0 } + ) + for modifier, _ in pairs(token.modifiers) do + table.insert( + lsp_highlights, + { token.start_col, token.end_col, string.format("@lsp.mod.%s.%s", modifier, ft), 1 } + ) + table.insert(lsp_highlights, { + token.start_col, + token.end_col, + string.format("@lsp.typemod.%s.%s.%s", token.type, modifier, ft), + 2, + }) + end + end + end + end + + return lsp_highlights +end + +---@param item QuickFixItem +---@param line string +---@return quicker.TSHighlight[] +M.get_heuristic_ts_highlights = function(item, line) + local filetype = vim.filetype.match({ buf = item.bufnr }) + if not filetype then + return {} + end + + local lang = vim.treesitter.language.get_lang(filetype) + if not lang then + return {} + end + + local has_parser, parser = pcall(vim.treesitter.get_string_parser, line, lang) + if not has_parser then + return {} + end + + local root = parser:parse(true)[1]:root() + local query = vim.treesitter.query.get(lang, "highlights") + if not query then + return {} + end + + local highlights = {} + for capture, node, metadata in query:iter_captures(root, line) do + if capture == nil then + break + end + + local range = vim.treesitter.get_range(node, line, metadata[capture]) + local start_row, start_col, _, end_row, end_col, _ = unpack(range) + local capture_name = query.captures[capture] + local hl = string.format("@%s.%s", capture_name, lang) + if end_row > start_row then + end_col = -1 + end + table.insert(highlights, { start_col, end_col, hl }) + end + + return highlights +end + +function M.set_highlight_groups() + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixHeaderHard" })) then + vim.api.nvim_set_hl(0, "QuickFixHeaderHard", { link = "Delimiter", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixHeaderSoft" })) then + vim.api.nvim_set_hl(0, "QuickFixHeaderSoft", { link = "Comment", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixFilename" })) then + vim.api.nvim_set_hl(0, "QuickFixFilename", { link = "Directory", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixFilenameInvalid" })) then + vim.api.nvim_set_hl(0, "QuickFixFilenameInvalid", { link = "Comment", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixLineNr" })) then + vim.api.nvim_set_hl(0, "QuickFixLineNr", { link = "LineNr", default = true }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/init.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/init.lua new file mode 100644 index 0000000..42ae32b --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/init.lua @@ -0,0 +1,189 @@ +local M = {} + +---@param opts? quicker.SetupOptions +local function setup(opts) + local config = require("quicker.config") + config.setup(opts) + + local aug = vim.api.nvim_create_augroup("quicker", { clear = true }) + vim.api.nvim_create_autocmd("FileType", { + pattern = "qf", + group = aug, + desc = "quicker.nvim set up quickfix mappings", + callback = function(args) + require("quicker.highlight").set_highlight_groups() + require("quicker.opts").set_opts(args.buf) + require("quicker.keys").set_keymaps(args.buf) + vim.api.nvim_buf_create_user_command(args.buf, "Refresh", function() + require("quicker.context").refresh() + end, { + desc = "Update the quickfix list with the current buffer text for each item", + }) + + if config.constrain_cursor then + require("quicker.cursor").constrain_cursor(args.buf) + end + + config.on_qf(args.buf) + end, + }) + vim.api.nvim_create_autocmd("ColorScheme", { + pattern = "*", + group = aug, + desc = "quicker.nvim set up quickfix highlight groups", + callback = function() + require("quicker.highlight").set_highlight_groups() + end, + }) + if config.edit.enabled then + vim.api.nvim_create_autocmd("BufReadPost", { + pattern = "quickfix", + group = aug, + desc = "quicker.nvim set up quickfix editing", + callback = function(args) + require("quicker.editor").setup_editor(args.buf) + end, + }) + end + if config.follow.enabled then + vim.api.nvim_create_autocmd({ "CursorMoved", "BufEnter" }, { + desc = "quicker.nvim scroll to nearest location in quickfix", + pattern = "*", + group = aug, + callback = function() + require("quicker.follow").seek_to_position() + end, + }) + end + + vim.o.quickfixtextfunc = "v:lua.require'quicker.display'.quickfixtextfunc" + + -- If the quickfix/loclist is already open, refresh it so the quickfixtextfunc will take effect. + -- This is required for lazy-loading to work properly. + local list = vim.fn.getqflist({ all = 0 }) + if not vim.tbl_isempty(list.items) then + vim.fn.setqflist({}, "r", list) + end + for _, winid in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(winid) then + local llist = vim.fn.getloclist(winid, { all = 0 }) + if not vim.tbl_isempty(list.items) then + vim.fn.setloclist(winid, {}, "r", llist) + end + end + end +end + +M.setup = setup + +---Expand the context around the quickfix results. +---@param opts? quicker.ExpandOpts +---@note +--- If there are multiple quickfix items for the same line of a file, only the first +--- one will remain after calling expand(). +M.expand = function(opts) + return require("quicker.context").expand(opts) +end + +---Collapse the context around quickfix results, leaving only the `valid` items. +M.collapse = function() + return require("quicker.context").collapse() +end + +---Toggle the expanded context around the quickfix results. +---@param opts? quicker.ExpandOpts +M.toggle_expand = function(opts) + return require("quicker.context").toggle(opts) +end + +---Update the quickfix list with the current buffer text for each item. +---@param loclist_win? integer +---@param opts? quicker.RefreshOpts +M.refresh = function(loclist_win, opts) + return require("quicker.context").refresh(loclist_win, opts) +end + +---@param loclist_win? integer Check if loclist is open for the given window. If nil, check quickfix. +M.is_open = function(loclist_win) + return require("quicker.util").is_open(loclist_win) +end + +---@class quicker.OpenCmdMods: vim.api.keyset.parse_cmd.mods + +---@class (exact) quicker.OpenOpts +---@field loclist? boolean Toggle the loclist instead of the quickfix list +---@field focus? boolean Focus the quickfix window after toggling (default false) +---@field height? integer Height of the quickfix window when opened. Defaults to number of items in the list. +---@field min_height? integer Minimum height of the quickfix window. Default 4. +---@field max_height? integer Maximum height of the quickfix window. Default 10. +---@field open_cmd_mods? quicker.OpenCmdMods A table of modifiers for the quickfix or loclist open commands. + +---Toggle the quickfix or loclist window. +---@param opts? quicker.OpenOpts +M.toggle = function(opts) + ---@type {loclist: boolean, focus: boolean, height?: integer, min_height: integer, max_height: integer, open_cmd_mods?: quicker.OpenCmdMods} + opts = vim.tbl_deep_extend("keep", opts or {}, { + loclist = false, + focus = false, + min_height = 4, + max_height = 10, + open_cmd_mods = {}, + }) + local loclist_win = opts.loclist and 0 or nil + if M.is_open(loclist_win) then + M.close({ loclist = opts.loclist }) + else + M.open(opts) + end +end + +---Open the quickfix or loclist window. +---@param opts? quicker.OpenOpts +M.open = function(opts) + local util = require("quicker.util") + ---@type {loclist: boolean, focus: boolean, height?: integer, min_height: integer, max_height: integer, open_cmd_mods?: quicker.OpenCmdMods} + opts = vim.tbl_deep_extend("keep", opts or {}, { + loclist = false, + focus = false, + min_height = 4, + max_height = 10, + open_cmd_mods = {}, + }) + local height + if opts.loclist then + local ok, err = pcall(vim.cmd.lopen, { mods = opts.open_cmd_mods }) + if not ok then + vim.notify(err, vim.log.levels.ERROR) + return + end + height = #vim.fn.getloclist(0) + else + vim.cmd.copen({ mods = opts.open_cmd_mods }) + height = #vim.fn.getqflist() + end + + -- only set the height if the quickfix is not a full-height vsplit + if not util.is_full_height_vsplit(0) then + height = math.min(opts.max_height, math.max(opts.min_height, height)) + vim.api.nvim_win_set_height(0, height) + end + + if not opts.focus then + vim.cmd.wincmd({ args = { "p" } }) + end +end + +---@class (exact) quicker.CloseOpts +---@field loclist? boolean Close the loclist instead of the quickfix list + +---Close the quickfix or loclist window. +---@param opts? quicker.CloseOpts +M.close = function(opts) + if opts and opts.loclist then + vim.cmd.lclose() + else + vim.cmd.cclose() + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/keys.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/keys.lua new file mode 100644 index 0000000..17ea331 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/keys.lua @@ -0,0 +1,20 @@ +local config = require("quicker.config") + +local M = {} + +---@param bufnr integer +function M.set_keymaps(bufnr) + for _, defn in ipairs(config.keys) do + vim.keymap.set(defn.mode or "n", defn[1], defn[2], { + buffer = bufnr, + desc = defn.desc, + expr = defn.expr, + nowait = defn.nowait, + remap = defn.remap, + replace_keycodes = defn.replace_keycodes, + silent = defn.silent, + }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/opts.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/opts.lua new file mode 100644 index 0000000..1cf77b6 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/opts.lua @@ -0,0 +1,61 @@ +local config = require("quicker.config") +local util = require("quicker.util") + +local M = {} + +---@param bufnr integer +local function set_buf_opts(bufnr) + for k, v in pairs(config.opts) do + local opt_info = vim.api.nvim_get_option_info2(k, {}) + if opt_info.scope == "buf" then + local ok, err = pcall(vim.api.nvim_set_option_value, k, v, { buf = bufnr }) + if not ok then + vim.notify( + string.format("Error setting quickfix option %s = %s: %s", k, vim.inspect(v), err), + vim.log.levels.ERROR + ) + end + end + end +end + +---@param winid integer +local function set_win_opts(winid) + for k, v in pairs(config.opts) do + local opt_info = vim.api.nvim_get_option_info2(k, {}) + if opt_info.scope == "win" then + local ok, err = pcall(vim.api.nvim_set_option_value, k, v, { scope = "local", win = winid }) + if not ok then + vim.notify( + string.format("Error setting quickfix window option %s = %s: %s", k, vim.inspect(v), err), + vim.log.levels.ERROR + ) + end + end + end +end + +---@param bufnr integer +function M.set_opts(bufnr) + set_buf_opts(bufnr) + local winid = util.buf_find_win(bufnr) + if winid then + set_win_opts(winid) + else + local aug = vim.api.nvim_create_augroup("quicker", { clear = false }) + vim.api.nvim_create_autocmd("BufWinEnter", { + desc = "Set quickfix window options", + buffer = bufnr, + group = aug, + callback = function() + winid = util.buf_find_win(bufnr) + if winid then + set_win_opts(winid) + end + return winid ~= nil + end, + }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/util.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/util.lua new file mode 100644 index 0000000..3794091 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/util.lua @@ -0,0 +1,95 @@ +local M = {} + +---@param bufnr integer +---@return nil|integer +function M.buf_find_win(bufnr) + for _, winid in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then + return winid + end + end +end + +---@param loclist_win? integer Check if loclist is open for the given window. If nil, check quickfix. +M.is_open = function(loclist_win) + if loclist_win then + return vim.fn.getloclist(loclist_win or 0, { winid = 0 }).winid ~= 0 + else + return vim.fn.getqflist({ winid = 0 }).winid ~= 0 + end +end + +---@param winid nil|integer +---@return nil|"c"|"l" +M.get_win_type = function(winid) + if not winid or winid == 0 then + winid = vim.api.nvim_get_current_win() + end + local info = vim.fn.getwininfo(winid)[1] + if info.quickfix == 0 then + return nil + elseif info.loclist == 0 then + return "c" + else + return "l" + end +end + +---@param item QuickFixItem +---@return QuickFixUserData +M.get_user_data = function(item) + if type(item.user_data) == "table" then + return item.user_data + else + return {} + end +end + +---Get valid location extmarks for a line in the quickfix +---@param bufnr integer +---@param lnum integer +---@param line_len? integer how long this particular line is +---@param ns? integer namespace of extmarks +---@return table[] extmarks +M.get_lnum_extmarks = function(bufnr, lnum, line_len, ns) + if not ns then + ns = vim.api.nvim_create_namespace("quicker_locations") + end + if not line_len then + local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1] + line_len = line:len() + end + local extmarks = vim.api.nvim_buf_get_extmarks( + bufnr, + ns, + { lnum - 1, 0 }, + { lnum - 1, line_len }, + { details = true } + ) + return vim.tbl_filter(function(mark) + return not mark[4].invalid + end, extmarks) +end + +---Return true if the window is a full-height leaf window +---@param winid? integer +---@return boolean +M.is_full_height_vsplit = function(winid) + if not winid or winid == 0 then + winid = vim.api.nvim_get_current_win() + end + local layout = vim.fn.winlayout() + -- If the top layout is not vsplit, then it's not a vertical leaf + if layout[1] ~= "row" then + return false + end + for _, v in ipairs(layout[2]) do + if v[1] == "leaf" and v[2] == winid then + return true + end + end + + return false +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/run_tests.sh b/mut/neovim/pack/plugins/start/quicker.nvim/run_tests.sh new file mode 100755 index 0000000..f7b5bab --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/run_tests.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +for arg in "$@"; do + shift + case "$arg" in + '--update') + export UPDATE_SNAPSHOTS=1 + ;; + *) + set -- "$@" "$arg" + ;; + esac +done + +mkdir -p ".testenv/config/nvim" +mkdir -p ".testenv/data/nvim" +mkdir -p ".testenv/state/nvim" +mkdir -p ".testenv/run/nvim" +mkdir -p ".testenv/cache/nvim" +PLUGINS=".testenv/data/nvim/site/pack/plugins/start" + +if [ ! -e "$PLUGINS/plenary.nvim" ]; then + git clone --depth=1 https://github.com/nvim-lua/plenary.nvim.git "$PLUGINS/plenary.nvim" +else + (cd "$PLUGINS/plenary.nvim" && git pull) +fi + +XDG_CONFIG_HOME=".testenv/config" \ + XDG_DATA_HOME=".testenv/data" \ + XDG_STATE_HOME=".testenv/state" \ + XDG_RUNTIME_DIR=".testenv/run" \ + XDG_CACHE_HOME=".testenv/cache" \ + nvim --headless -u tests/minimal_init.lua \ + -c "RunTests ${1-tests}" +echo "Success" diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/scripts/generate.py b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/generate.py new file mode 100755 index 0000000..e1ccded --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/generate.py @@ -0,0 +1,102 @@ +import os +import os.path +import re +from typing import List + +from nvim_doc_tools import ( + Vimdoc, + VimdocSection, + generate_md_toc, + indent, + parse_directory, + read_section, + render_md_api2, + render_vimdoc_api2, + replace_section, +) + +HERE = os.path.dirname(__file__) +ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir)) +README = os.path.join(ROOT, "README.md") +DOC = os.path.join(ROOT, "doc") +VIMDOC = os.path.join(DOC, "quicker.txt") + + +def add_md_link_path(path: str, lines: List[str]) -> List[str]: + ret = [] + for line in lines: + ret.append(re.sub(r"(\(#)", "(" + path + "#", line)) + return ret + + +def update_md_api(): + types = parse_directory(os.path.join(ROOT, "lua")) + funcs = types.files["quicker/init.lua"].functions + lines = ["\n"] + render_md_api2(funcs, types, 3)[:-1] # trim last newline + replace_section( + README, + r"^<!-- API -->$", + r"^<!-- /API -->$", + lines, + ) + + +def update_options(): + option_lines = ["\n", "```lua\n"] + config_file = os.path.join(ROOT, "lua", "quicker", "config.lua") + option_lines = read_section(config_file, r"^\s*local default_config =", r"^}$") + option_lines.insert(0, 'require("quicker").setup({\n') + option_lines.insert(0, "```lua\n") + option_lines.extend(["})\n", "```\n", "\n"]) + replace_section( + README, + r"^<!-- OPTIONS -->$", + r"^<!-- /OPTIONS -->$", + option_lines, + ) + + +def update_readme_toc(): + toc = ["\n"] + generate_md_toc(README, max_level=1) + ["\n"] + replace_section( + README, + r"^<!-- TOC -->$", + r"^<!-- /TOC -->$", + toc, + ) + + +def gen_options_vimdoc() -> VimdocSection: + section = VimdocSection("Options", "quicker-options", ["\n", ">lua\n"]) + config_file = os.path.join(ROOT, "lua", "quicker", "config.lua") + option_lines = read_section(config_file, r"^\s*local default_config =", r"^}$") + option_lines.insert(0, 'require("quicker").setup({\n') + option_lines.extend(["})\n"]) + section.body.extend(indent(option_lines, 4)) + section.body.append("<\n") + return section + + +def generate_vimdoc(): + doc = Vimdoc("quicker.txt", "quicker") + types = parse_directory(os.path.join(ROOT, "lua")) + funcs = types.files["quicker/init.lua"].functions + doc.sections.extend( + [ + gen_options_vimdoc(), + VimdocSection( + "API", "quicker-api", render_vimdoc_api2("quicker", funcs, types) + ), + ] + ) + + with open(VIMDOC, "w", encoding="utf-8") as ofile: + ofile.writelines(doc.render()) + + +def main() -> None: + """Update the README""" + update_md_api() + update_options() + update_readme_toc() + generate_vimdoc() diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/scripts/main.py b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/main.py new file mode 100755 index 0000000..4dffddf --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/main.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +import argparse +import os +import sys + +HERE = os.path.dirname(__file__) +ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir)) +DOC = os.path.join(ROOT, "doc") + + +def main() -> None: + """Generate docs""" + sys.path.append(HERE) + parser = argparse.ArgumentParser(description=main.__doc__) + parser.add_argument("command", choices=["generate", "lint"]) + args = parser.parse_args() + if args.command == "generate": + import generate + + generate.main() + elif args.command == "lint": + from nvim_doc_tools import lint_md_links + + files = [os.path.join(ROOT, "README.md")] + [ + os.path.join(DOC, file) for file in os.listdir(DOC) if file.endswith(".md") + ] + lint_md_links.main(ROOT, files) + + +if __name__ == "__main__": + main() diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/scripts/requirements.txt b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/requirements.txt new file mode 100644 index 0000000..2c6271f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/requirements.txt @@ -0,0 +1,4 @@ +pyparsing==3.0.9 +black +isort +mypy diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/syntax/qf.vim b/mut/neovim/pack/plugins/start/quicker.nvim/syntax/qf.vim new file mode 100644 index 0000000..8a19536 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/syntax/qf.vim @@ -0,0 +1,7 @@ +if exists('b:current_syntax') + finish +endif + +syn match QuickFixText /^.*/ + +let b:current_syntax = 'qf' diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/context_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/context_spec.lua new file mode 100644 index 0000000..4a4ef5a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/context_spec.lua @@ -0,0 +1,134 @@ +local quicker = require("quicker") +local test_util = require("tests.test_util") + +describe("context", function() + after_each(function() + test_util.reset_editor() + end) + + it("expand results", function() + local first = test_util.make_tmp_file("expand_1.txt", 10) + local second = test_util.make_tmp_file("expand_2.txt", 10) + local first_buf = vim.fn.bufadd(first) + local second_buf = vim.fn.bufadd(second) + vim.fn.setqflist({ + { + bufnr = first_buf, + text = "line 2", + lnum = 2, + valid = 1, + }, + { + bufnr = first_buf, + text = "line 8", + lnum = 8, + valid = 1, + }, + { + bufnr = second_buf, + text = "line 4", + lnum = 4, + valid = 1, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "expand_1") + + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + quicker.expand() + test_util.assert_snapshot(0, "expand_2") + -- Cursor stays on the same item + assert.equals(12, vim.api.nvim_win_get_cursor(0)[1]) + vim.api.nvim_win_set_cursor(0, { 14, 0 }) + + -- Expanding again will produce the same result + quicker.expand() + test_util.assert_snapshot(0, "expand_2") + assert.equals(14, vim.api.nvim_win_get_cursor(0)[1]) + + -- Expanding again will produce the same result + quicker.expand({ add_to_existing = true }) + test_util.assert_snapshot(0, "expand_3") + + -- Collapsing will return to the original state + quicker.collapse() + test_util.assert_snapshot(0, "expand_1") + assert.equals(3, vim.api.nvim_win_get_cursor(0)[1]) + end) + + it("expand loclist results", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("expand_loclist.txt", 10)) + vim.fn.setloclist(0, { + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + valid = 1, + }, + }) + vim.cmd.lopen() + quicker.expand() + test_util.assert_snapshot(0, "expand_loclist") + end) + + it("expand when items missing bufnr", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("expand_missing.txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + valid = 1, + }, + { + text = "Valid line with no bufnr", + lnum = 4, + valid = 1, + }, + { + bufnr = bufnr, + text = "Invalid line with a bufnr", + lnum = 5, + valid = 0, + }, + { + text = "Invalid line with no bufnr", + lnum = 6, + valid = 0, + }, + }) + vim.cmd.copen() + quicker.expand() + -- The last three lines should be stripped after expansion + test_util.assert_snapshot(0, "expand_missing") + end) + + it("expand removes duplicate line entries", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("expand_dupe.txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + valid = 1, + }, + { + bufnr = bufnr, + text = "line 3", + lnum = 3, + valid = 1, + }, + { + bufnr = bufnr, + text = "line 3", + lnum = 3, + valid = 1, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "expand_dupe_1") + + quicker.expand() + test_util.assert_snapshot(0, "expand_dupe_2") + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/display_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/display_spec.lua new file mode 100644 index 0000000..c3404ad --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/display_spec.lua @@ -0,0 +1,145 @@ +require("plenary.async").tests.add_to_env() +local config = require("quicker.config") +local test_util = require("tests.test_util") + +local sleep = require("plenary.async.util").sleep + +a.describe("display", function() + after_each(function() + test_util.reset_editor() + end) + + it("renders quickfix items", function() + vim.fn.setqflist({ + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + lnum = 5, + valid = 1, + }, + { + filename = "README.md", + text = "text", + lnum = 10, + col = 0, + end_col = 4, + nr = 3, + type = "E", + valid = 1, + }, + { + module = "mod", + bufnr = vim.fn.bufadd("README.md"), + text = "text", + valid = 1, + }, + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + valid = 0, + }, + { + bufnr = vim.fn.bufadd("README.md"), + lnum = 1, + text = "", + valid = 0, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "display_1") + end) + + a.it("truncates long filenames", function() + config.max_filename_width = function() + return 10 + end + local bufnr = vim.fn.bufadd(test_util.make_tmp_file(string.rep("f", 10) .. ".txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "text", + lnum = 5, + valid = 1, + }, + }) + vim.cmd.copen() + -- Wait for highlights to be applied + sleep(50) + test_util.assert_snapshot(0, "display_long_1") + end) + + a.it("renders minimal line when no filenames in results", function() + vim.fn.setqflist({ + { + text = "text", + }, + }) + vim.cmd.copen() + -- Wait for highlights to be applied + sleep(50) + test_util.assert_snapshot(0, "display_minimal_1") + end) + + a.it("sets signs for diagnostics", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("sign_test.txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "text", + lnum = 1, + type = "E", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 2, + type = "W", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 3, + type = "I", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 4, + type = "H", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 5, + type = "N", + valid = 1, + }, + }) + vim.cmd.copen() + + -- Wait for highlights to be applied + sleep(50) + local ns = vim.api.nvim_create_namespace("quicker_highlights") + local marks = vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, { type = "sign" }) + assert.equals(5, #marks) + local expected = { + { "DiagnosticSignError", config.type_icons.E }, + { "DiagnosticSignWarn", config.type_icons.W }, + { "DiagnosticSignInfo", config.type_icons.I }, + { "DiagnosticSignHint", config.type_icons.H }, + { "DiagnosticSignHint", config.type_icons.N }, + } + for i, mark_data in ipairs(marks) do + local extmark_id, row = mark_data[1], mark_data[2] + local mark = vim.api.nvim_buf_get_extmark_by_id(0, ns, extmark_id, { details = true }) + local hl_group, icon = unpack(expected[i]) + assert.equals(i - 1, row) + assert.equals(hl_group, mark[3].sign_hl_group) + assert.equals(icon, mark[3].sign_text) + end + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/editor_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/editor_spec.lua new file mode 100644 index 0000000..0999508 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/editor_spec.lua @@ -0,0 +1,347 @@ +local config = require("quicker.config") +local display = require("quicker.display") +local quicker = require("quicker") +local test_util = require("tests.test_util") + +---@param lnum integer +---@param line string +local function replace_text(lnum, line) + local prev_line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1] + local idx = prev_line:find(display.EM_QUAD, 1, true) + vim.api.nvim_buf_set_text(0, lnum - 1, idx + display.EM_QUAD_LEN - 1, lnum - 1, -1, { line }) +end + +---@param lnum integer +local function del_line(lnum) + vim.cmd.normal({ args = { string.format("%dggdd", lnum) }, bang = true }) +end + +local function wait_virt_text() + vim.wait(10, function() + return false + end) +end + +describe("editor", function() + after_each(function() + test_util.reset_editor() + end) + + it("can edit one line in file", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_1.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, "new text") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_1") + end) + + it("can edit across multiple files", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_multiple_1.txt", 10)) + vim.fn.bufload(bufnr) + local buf2 = vim.fn.bufadd(test_util.make_tmp_file("edit_multiple_2.txt", 10)) + vim.fn.bufload(buf2) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = "line 9", + lnum = 9, + }, + { + bufnr = buf2, + text = "line 5", + lnum = 5, + }, + }) + vim.cmd.copen() + quicker.expand() + wait_virt_text() + replace_text(2, "new text") + replace_text(3, "some text") + replace_text(7, "other text") + replace_text(11, "final text") + local last_line = vim.api.nvim_buf_line_count(0) + vim.api.nvim_win_set_cursor(0, { last_line, 0 }) + vim.cmd.write() + test_util.assert_snapshot(0, "edit_multiple_qf") + test_util.assert_snapshot(bufnr, "edit_multiple_1") + test_util.assert_snapshot(buf2, "edit_multiple_2") + -- We should keep the cursor position + assert.equals(last_line, vim.api.nvim_win_get_cursor(0)[1]) + end) + + it("can expand then edit expanded line", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_expanded.txt", 10)) + vim.fn.bufload(bufnr) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + }) + vim.cmd.copen() + quicker.expand() + wait_virt_text() + replace_text(1, "first") + replace_text(2, "second") + replace_text(3, "third") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_expanded") + test_util.assert_snapshot(0, "edit_expanded_qf") + end) + + it("fails when source text is different", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_fail.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "buzz buzz", + lnum = 2, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, "new text") + test_util.with(function() + local notify = vim.notify + ---@diagnostic disable-next-line: duplicate-set-field + vim.notify = function() end + return function() + vim.notify = notify + end + end, function() + vim.cmd.write() + end) + test_util.assert_snapshot(bufnr, "edit_fail") + test_util.assert_snapshot(0, "edit_fail_qf") + end) + + it("can handle multiple qf items on same lnum", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_dupe.txt", 10)) + vim.fn.bufload(bufnr) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + col = 0, + }, + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + col = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, "first") + replace_text(2, "second") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_dupe") + test_util.assert_snapshot(0, "edit_dupe_qf") + + -- If only one of them has a change, it should go through + replace_text(1, "line 2") + replace_text(2, "second") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_dupe_2") + test_util.assert_snapshot(0, "edit_dupe_qf_2") + end) + + it("handles deleting lines (shrinks quickfix)", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_delete.txt", 10)) + vim.fn.bufload(bufnr) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = "line 3", + lnum = 3, + }, + { + bufnr = bufnr, + text = "line 6", + lnum = 6, + }, + }) + vim.cmd.copen() + wait_virt_text() + del_line(3) + del_line(2) + vim.cmd.write() + assert.are.same({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + col = 0, + end_col = 0, + vcol = 0, + end_lnum = 0, + module = "", + nr = 0, + pattern = "", + type = "", + valid = 1, + }, + }, vim.fn.getqflist()) + end) + + it("handles loclist", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_ll.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setloclist(0, { + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + }) + vim.cmd.lopen() + wait_virt_text() + replace_text(1, "new text") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_ll") + end) + + it("handles text that contains the delimiter", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_delim.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + local line = "line 2 " .. config.borders.vert .. " text" + vim.api.nvim_buf_set_lines(bufnr, 1, 2, false, { line }) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = line, + lnum = 2, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, line .. " " .. config.borders.vert .. " more text") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_delim") + end) + + it("can edit lines with trimmed common whitespace", function() + require("quicker.config").trim_leading_whitespace = "common" + vim.cmd.edit({ + args = { + test_util.make_tmp_file("edit_whitespace.txt", { + " line 1", + " line 2", + " line 3", + " line 4", + }), + }, + }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + test_util.assert_snapshot(0, "edit_whitespace_qf") + replace_text(1, "foo") + replace_text(2, "bar") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_whitespace") + end) + + it("can edit lines with trimmed all whitespace", function() + require("quicker.config").trim_leading_whitespace = "all" + vim.cmd.edit({ + args = { + test_util.make_tmp_file("edit_whitespace.txt", { + " line 1", + " line 2", + " line 3", + " line 4", + }), + }, + }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + test_util.assert_snapshot(0, "edit_all_whitespace_qf") + replace_text(1, "foo") + replace_text(2, "bar") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_all_whitespace") + end) + + it("can edit lines with untrimmed whitespace", function() + require("quicker.config").trim_leading_whitespace = false + vim.cmd.edit({ + args = { + test_util.make_tmp_file("edit_whitespace.txt", { + " line 1", + " line 2", + " line 3", + " line 4", + }), + }, + }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + test_util.assert_snapshot(0, "edit_none_whitespace_qf") + replace_text(1, "foo") + replace_text(2, "bar") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_none_whitespace") + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/fs_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/fs_spec.lua new file mode 100644 index 0000000..2e1e54f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/fs_spec.lua @@ -0,0 +1,20 @@ +local fs = require("quicker.fs") + +local home = os.getenv("HOME") +local cwd = vim.fn.getcwd() + +describe("fs", function() + it("shortens path", function() + assert.equals("~/bar/baz.txt", fs.shorten_path(home .. "/bar/baz.txt")) + assert.equals("bar/baz.txt", fs.shorten_path(cwd .. "/bar/baz.txt")) + assert.equals("/foo/bar.txt", fs.shorten_path("/foo/bar.txt")) + end) + + it("finds subpath", function() + assert.truthy(fs.is_subpath("/root", "/root/foo")) + assert.truthy(fs.is_subpath(cwd, "foo")) + assert.falsy(fs.is_subpath("/root", "/foo")) + assert.falsy(fs.is_subpath("/root", "/rooter/foo")) + assert.falsy(fs.is_subpath("/root", "/root/../foo")) + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/minimal_init.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/minimal_init.lua new file mode 100644 index 0000000..486b213 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/minimal_init.lua @@ -0,0 +1,16 @@ +vim.cmd([[set runtimepath+=.]]) + +vim.o.swapfile = false +vim.bo.swapfile = false +require("tests.test_util").reset_editor() + +-- TODO test highlighting (both highlight.lua module and adding them in display.lua) +-- TODO test syntax highlighting when customizing delimiter + +vim.api.nvim_create_user_command("RunTests", function(opts) + local path = opts.fargs[1] or "tests" + require("plenary.test_harness").test_directory( + path, + { minimal_init = "./tests/minimal_init.lua" } + ) +end, { nargs = "?" }) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/opts_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/opts_spec.lua new file mode 100644 index 0000000..0732da2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/opts_spec.lua @@ -0,0 +1,52 @@ +local quicker = require("quicker") +local test_util = require("tests.test_util") + +describe("opts", function() + after_each(function() + test_util.reset_editor() + end) + + it("sets buffer opts", function() + quicker.setup({ + opts = { + buflisted = true, + bufhidden = "wipe", + cindent = true, + }, + }) + vim.fn.setqflist({ + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + lnum = 5, + valid = 1, + }, + }) + vim.cmd.copen() + assert.truthy(vim.bo.buflisted) + assert.equals("wipe", vim.bo.bufhidden) + assert.truthy(vim.bo.cindent) + end) + + it("sets window opts", function() + quicker.setup({ + opts = { + wrap = false, + number = true, + list = true, + }, + }) + vim.fn.setqflist({ + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + lnum = 5, + valid = 1, + }, + }) + vim.cmd.copen() + assert.falsy(vim.wo.wrap) + assert.truthy(vim.wo.number) + assert.truthy(vim.wo.list) + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_1 new file mode 100644 index 0000000..270a164 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_1 @@ -0,0 +1,5 @@ +README.md ┃ 5┃text +README.md ┃10┃text +mod ┃ ┃text +README.md ┃ ┃text +README.md ┃ 1┃
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_long_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_long_1 new file mode 100644 index 0000000..d585beb --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_long_1 @@ -0,0 +1 @@ +…ffffffff.txt ┃ 5┃text
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_minimal_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_minimal_1 new file mode 100644 index 0000000..46190e7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_minimal_1 @@ -0,0 +1 @@ + ┃text
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_1 new file mode 100644 index 0000000..a18ed5a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_1 @@ -0,0 +1,10 @@ +line 1 +new text +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace new file mode 100644 index 0000000..998c877 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace @@ -0,0 +1,4 @@ + line 1 + foo + bar + line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace_qf new file mode 100644 index 0000000..baf8533 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_whitespace.txt ┃ 2┃line 2 +tests/tmp/edit_whitespace.txt ┃ 3┃line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_delim b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_delim new file mode 100644 index 0000000..75a9e7f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_delim @@ -0,0 +1,10 @@ +line 1 +line 2 ┃ text ┃ more text +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe new file mode 100644 index 0000000..b3f56ae --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe @@ -0,0 +1,10 @@ +line 1 +line 2 +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_2 new file mode 100644 index 0000000..3ae9ccc --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_2 @@ -0,0 +1,10 @@ +line 1 +second +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf new file mode 100644 index 0000000..7e01207 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_dupe.txt ┃ 2┃first +tests/tmp/edit_dupe.txt ┃ 2┃second
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf_2 new file mode 100644 index 0000000..1acfd8e --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf_2 @@ -0,0 +1,2 @@ +tests/tmp/edit_dupe.txt ┃ 2┃line 2 +tests/tmp/edit_dupe.txt ┃ 2┃second
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded new file mode 100644 index 0000000..afc39ad --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded @@ -0,0 +1,10 @@ +first +second +third +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded_qf new file mode 100644 index 0000000..991dd06 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded_qf @@ -0,0 +1,4 @@ + ┃ 1┃first +tests/tmp/edit_expanded.txt ┃ 2┃second + ┃ 3┃third + ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail new file mode 100644 index 0000000..b3f56ae --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail @@ -0,0 +1,10 @@ +line 1 +line 2 +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail_qf new file mode 100644 index 0000000..3eb9f10 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail_qf @@ -0,0 +1 @@ +tests/tmp/edit_fail.txt ┃ 2┃new text
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_invalid b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_invalid new file mode 100644 index 0000000..386c994 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_invalid @@ -0,0 +1 @@ + ┃ ┃new text diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_ll b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_ll new file mode 100644 index 0000000..a18ed5a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_ll @@ -0,0 +1,10 @@ +line 1 +new text +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_1 new file mode 100644 index 0000000..765403a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_1 @@ -0,0 +1,10 @@ +line 1 +new text +some text +line 4 +line 5 +line 6 +line 7 +line 8 +other text +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_2 new file mode 100644 index 0000000..c988ea1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_2 @@ -0,0 +1,10 @@ +line 1 +line 2 +line 3 +line 4 +final text +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_qf new file mode 100644 index 0000000..3de41ad --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_qf @@ -0,0 +1,15 @@ + ┃ 1┃line 1 +tests/tmp/edit_multiple_1.txt ┃ 2┃new text + ┃ 3┃some text + ┃ 4┃line 4 +╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╂╌╌╂╌╌╌╌╌╌╌╌ + ┃ 7┃line 7 + ┃ 8┃line 8 +tests/tmp/edit_multiple_1.txt ┃ 9┃other text + ┃10┃line 10 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━╋━━━━━━━━ + ┃ 3┃line 3 + ┃ 4┃line 4 +tests/tmp/edit_multiple_2.txt ┃ 5┃final text + ┃ 6┃line 6 + ┃ 7┃line 7
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace new file mode 100644 index 0000000..4b592c2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace @@ -0,0 +1,4 @@ + line 1 +foo +bar + line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace_qf new file mode 100644 index 0000000..5be47f1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_whitespace.txt ┃ 2┃ line 2 +tests/tmp/edit_whitespace.txt ┃ 3┃ line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace new file mode 100644 index 0000000..6a5ca4b --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace @@ -0,0 +1,4 @@ + line 1 + foo + bar + line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace_qf new file mode 100644 index 0000000..e26d928 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_whitespace.txt ┃ 2┃line 2 +tests/tmp/edit_whitespace.txt ┃ 3┃ line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_1 new file mode 100644 index 0000000..ab1901f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_1 @@ -0,0 +1,3 @@ +tests/tmp/expand_1.txt ┃ 2┃line 2 +tests/tmp/expand_1.txt ┃ 8┃line 8 +tests/tmp/expand_2.txt ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_2 new file mode 100644 index 0000000..e0a2139 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_2 @@ -0,0 +1,16 @@ + ┃ 1┃line 1 +tests/tmp/expand_1.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4 +╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╂╌╌╂╌╌╌╌╌╌╌╌ + ┃ 6┃line 6 + ┃ 7┃line 7 +tests/tmp/expand_1.txt ┃ 8┃line 8 + ┃ 9┃line 9 + ┃10┃line 10 +━━━━━━━━━━━━━━━━━━━━━━━╋━━╋━━━━━━━━ + ┃ 2┃line 2 + ┃ 3┃line 3 +tests/tmp/expand_2.txt ┃ 4┃line 4 + ┃ 5┃line 5 + ┃ 6┃line 6
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_3 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_3 new file mode 100644 index 0000000..0a20a1c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_3 @@ -0,0 +1,19 @@ + ┃ 1┃line 1 +tests/tmp/expand_1.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4 + ┃ 5┃line 5 + ┃ 6┃line 6 + ┃ 7┃line 7 +tests/tmp/expand_1.txt ┃ 8┃line 8 + ┃ 9┃line 9 + ┃10┃line 10 +━━━━━━━━━━━━━━━━━━━━━━━╋━━╋━━━━━━━━ + ┃ 1┃line 1 + ┃ 2┃line 2 + ┃ 3┃line 3 +tests/tmp/expand_2.txt ┃ 4┃line 4 + ┃ 5┃line 5 + ┃ 6┃line 6 + ┃ 7┃line 7 + ┃ 8┃line 8
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_1 new file mode 100644 index 0000000..8e32cb4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_1 @@ -0,0 +1,3 @@ +tests/tmp/expand_dupe.txt ┃ 2┃line 2 +tests/tmp/expand_dupe.txt ┃ 3┃line 3 +tests/tmp/expand_dupe.txt ┃ 3┃line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_2 new file mode 100644 index 0000000..b51efa8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_2 @@ -0,0 +1,5 @@ + ┃ 1┃line 1 +tests/tmp/expand_dupe.txt ┃ 2┃line 2 +tests/tmp/expand_dupe.txt ┃ 3┃line 3 + ┃ 4┃line 4 + ┃ 5┃line 5
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_loclist b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_loclist new file mode 100644 index 0000000..66a6207 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_loclist @@ -0,0 +1,4 @@ + ┃ 1┃line 1 +tests/tmp/expand_loclist.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_missing b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_missing new file mode 100644 index 0000000..f29a273 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_missing @@ -0,0 +1,4 @@ + ┃ 1┃line 1 +tests/tmp/expand_missing.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_all_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_all_whitespace new file mode 100644 index 0000000..e664f80 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_all_whitespace @@ -0,0 +1,2 @@ +tests/tmp/whitespace_1.txt ┃ 2┃line 2 +tests/tmp/whitespace_1.txt ┃ 3┃line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_mixed_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_mixed_whitespace new file mode 100644 index 0000000..f6464d4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_mixed_whitespace @@ -0,0 +1,2 @@ +tests/tmp/mixed_whitespace.txt ┃ 1┃ line 1 +tests/tmp/mixed_whitespace.txt ┃ 2┃ line 2
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace new file mode 100644 index 0000000..49a6e20 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace @@ -0,0 +1,2 @@ +tests/tmp/whitespace.txt ┃ 2┃line 2 +tests/tmp/whitespace.txt ┃ 3┃ line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace_expanded b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace_expanded new file mode 100644 index 0000000..07b70d1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace_expanded @@ -0,0 +1,5 @@ + ┃ 1┃ line 1 +tests/tmp/whitespace.txt ┃ 2┃line 2 +tests/tmp/whitespace.txt ┃ 3┃ line 3 + ┃ 4┃ + ┃ 5┃ line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/test_util.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/test_util.lua new file mode 100644 index 0000000..94b0a40 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/test_util.lua @@ -0,0 +1,142 @@ +require("plenary.async").tests.add_to_env() +local M = {} + +local tmp_files = {} +M.reset_editor = function() + vim.cmd.tabonly({ mods = { silent = true } }) + for i, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + if i > 1 then + vim.api.nvim_win_close(winid, true) + end + end + vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(false, true)) + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + vim.fn.setqflist({}) + vim.fn.setloclist(0, {}) + for _, filename in ipairs(tmp_files) do + vim.uv.fs_unlink(filename) + end + tmp_files = {} + + require("quicker").setup({ + header_length = function() + -- Make this deterministic so the snapshots are stable + return 8 + end, + }) +end + +---@param basename string +---@param lines integer|string[] +---@return string +M.make_tmp_file = function(basename, lines) + vim.fn.mkdir("tests/tmp", "p") + local filename = "tests/tmp/" .. basename + table.insert(tmp_files, filename) + local f = assert(io.open(filename, "w")) + if type(lines) == "table" then + for _, line in ipairs(lines) do + f:write(line .. "\n") + end + else + for i = 1, lines do + f:write("line " .. i .. "\n") + end + end + f:close() + return filename +end + +---@param name string +---@return string[] +local function load_snapshot(name) + local path = "tests/snapshots/" .. name + if vim.fn.filereadable(path) == 0 then + return {} + end + local f = assert(io.open(path, "r")) + local lines = {} + for line in f:lines() do + table.insert(lines, line) + end + f:close() + return lines +end + +---@param name string +---@param lines string[] +local function save_snapshot(name, lines) + vim.fn.mkdir("tests/snapshots", "p") + local path = "tests/snapshots/" .. name + local f = assert(io.open(path, "w")) + f:write(table.concat(lines, "\n")) + f:close() + return lines +end + +---@param bufnr integer +---@param name string +M.assert_snapshot = function(bufnr, name) + -- Wait for the virtual text extmarks to be set + if vim.bo[bufnr].filetype == "qf" then + vim.wait(10, function() + return false + end) + end + local util = require("quicker.util") + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + -- Add virtual text to lines + local headers = {} + local header_ns = vim.api.nvim_create_namespace("quicker_headers") + for i, v in ipairs(lines) do + local extmarks = util.get_lnum_extmarks(bufnr, i, v:len()) + assert(#extmarks <= 1, "Expected at most one extmark per line") + local mark = extmarks[1] + if mark then + local start_col = mark[3] + local data = mark[4] + local virt_text = table.concat( + vim.tbl_map(function(vt) + return vt[1] + end, data.virt_text), + "" + ) + lines[i] = v:sub(0, start_col) .. virt_text .. v:sub(start_col + 1) + + extmarks = util.get_lnum_extmarks(bufnr, i, v:len(), header_ns) + assert(#extmarks <= 1, "Expected at most one extmark per line") + mark = extmarks[1] + if mark and mark[4].virt_lines then + table.insert(headers, { i, mark[4].virt_lines[1][1][1] }) + end + end + end + + for i = #headers, 1, -1 do + local lnum, header = unpack(headers[i]) + table.insert(lines, lnum, header) + end + + if os.getenv("UPDATE_SNAPSHOTS") then + save_snapshot(name, lines) + else + local expected = load_snapshot(name) + assert.are.same(expected, lines) + end +end + +---@param context fun(): fun() +---@param fn fun() +M.with = function(context, fn) + local cleanup = context() + local ok, err = pcall(fn) + cleanup() + if not ok then + error(err) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/whitespace_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/whitespace_spec.lua new file mode 100644 index 0000000..0933276 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/whitespace_spec.lua @@ -0,0 +1,83 @@ +local quicker = require("quicker") +local test_util = require("tests.test_util") + +describe("whitespace", function() + before_each(function() + require("quicker.config").trim_leading_whitespace = "common" + end) + after_each(function() + test_util.reset_editor() + end) + + it("removes common leading whitespace from valid results", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("whitespace.txt", { + " line 1", + " line 2", + " line 3", + "", + " line 4", + })) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "trim_whitespace") + quicker.expand() + test_util.assert_snapshot(0, "trim_whitespace_expanded") + end) + + it("handles mixed tabs and spaces", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("mixed_whitespace.txt", { + " line 1", + "\t\tline 2", + })) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 1", + lnum = 1, + }, + { + bufnr = bufnr, + text = "\t\tline 2", + lnum = 2, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "trim_mixed_whitespace") + end) + + it("removes all leading whitespace", function() + require("quicker.config").trim_leading_whitespace = "all" + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("whitespace_1.txt", { + " line 1", + " line 2", + " line 3", + "", + " line 4", + })) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "trim_all_whitespace") + end) +end) diff --git a/mut/nushell/config.nu b/mut/nushell/config.nu new file mode 100644 index 0000000..4602133 --- /dev/null +++ b/mut/nushell/config.nu @@ -0,0 +1,921 @@ +if ($env | default "" DOCKER_NAME | get DOCKER_NAME | is-empty) { + if (pidof gpg-agent | is-empty) { gpgconf --launch gpg-agent } + if ("~/.gnupg/S.gpg-agent.ssh" | path exists) { + ln -sf ("~/.gnupg/S.gpg-agent.ssh" | path expand) $env.SSH_AUTH_SOCK + } + try {pnsh-nvim} +} +# Nushell Config File +# +# version = "0.99.1" + +# For more information on defining custom themes, see +# https://www.nushell.sh/book/coloring_and_theming.html +# And here is the theme collection +# https://github.com/nushell/nu_scripts/tree/main/themes +let dark_theme = { + # color for nushell primitives + separator: white + leading_trailing_space_bg: { attr: n } # no fg, no bg, attr none effectively turns this off + header: green_bold + empty: blue + # Closures can be used to choose colors for specific values. + # The value (in this case, a bool) is piped into the closure. + # eg) {|| if $in { 'light_cyan' } else { 'light_gray' } } + bool: light_cyan + int: white + filesize: cyan + duration: white + date: purple + range: white + float: white + string: white + nothing: white + binary: white + cell-path: white + row_index: green_bold + record: white + list: white + block: white + hints: dark_gray + search_result: { bg: red fg: white } + shape_and: purple_bold + shape_binary: purple_bold + shape_block: blue_bold + shape_bool: light_cyan + shape_closure: green_bold + shape_custom: green + shape_datetime: cyan_bold + shape_directory: cyan + shape_external: cyan + shape_externalarg: green_bold + shape_external_resolved: light_yellow_bold + shape_filepath: cyan + shape_flag: blue_bold + shape_float: purple_bold + # shapes are used to change the cli syntax highlighting + shape_garbage: { fg: white bg: red attr: b } + shape_glob_interpolation: cyan_bold + shape_globpattern: cyan_bold + shape_int: purple_bold + shape_internalcall: cyan_bold + shape_keyword: cyan_bold + shape_list: cyan_bold + shape_literal: blue + shape_match_pattern: green + shape_matching_brackets: { attr: u } + shape_nothing: light_cyan + shape_operator: yellow + shape_or: purple_bold + shape_pipe: purple_bold + shape_range: yellow_bold + shape_record: cyan_bold + shape_redirection: purple_bold + shape_signature: green_bold + shape_string: green + shape_string_interpolation: cyan_bold + shape_table: blue_bold + shape_variable: purple + shape_vardecl: purple + shape_raw_string: light_purple +} + +let light_theme = { + # color for nushell primitives + separator: dark_gray + leading_trailing_space_bg: { attr: n } # no fg, no bg, attr none effectively turns this off + header: green_bold + empty: blue + # Closures can be used to choose colors for specific values. + # The value (in this case, a bool) is piped into the closure. + # eg) {|| if $in { 'dark_cyan' } else { 'dark_gray' } } + bool: dark_cyan + int: dark_gray + filesize: cyan_bold + duration: dark_gray + date: purple + range: dark_gray + float: dark_gray + string: dark_gray + nothing: dark_gray + binary: dark_gray + cell-path: dark_gray + row_index: green_bold + record: dark_gray + list: dark_gray + block: dark_gray + hints: dark_gray + search_result: { fg: white bg: red } + shape_and: purple_bold + shape_binary: purple_bold + shape_block: blue_bold + shape_bool: light_cyan + shape_closure: green_bold + shape_custom: green + shape_datetime: cyan_bold + shape_directory: cyan + shape_external: cyan + shape_externalarg: green_bold + shape_external_resolved: light_purple_bold + shape_filepath: cyan + shape_flag: blue_bold + shape_float: purple_bold + # shapes are used to change the cli syntax highlighting + shape_garbage: { fg: white bg: red attr: b } + shape_glob_interpolation: cyan_bold + shape_globpattern: cyan_bold + shape_int: purple_bold + shape_internalcall: cyan_bold + shape_keyword: cyan_bold + shape_list: cyan_bold + shape_literal: blue + shape_match_pattern: green + shape_matching_brackets: { attr: u } + shape_nothing: light_cyan + shape_operator: yellow + shape_or: purple_bold + shape_pipe: purple_bold + shape_range: yellow_bold + shape_record: cyan_bold + shape_redirection: purple_bold + shape_signature: green_bold + shape_string: green + shape_string_interpolation: cyan_bold + shape_table: blue_bold + shape_variable: purple + shape_vardecl: purple + shape_raw_string: light_purple +} + +# The default config record. This is where much of your global configuration is setup. +$env.config = { + show_banner: true # true or false to enable or disable the welcome banner at startup + + ls: { + use_ls_colors: true # use the LS_COLORS environment variable to colorize output + clickable_links: true # enable or disable clickable links. Your terminal has to support links. + } + + rm: { + always_trash: false # always act as if -t was given. Can be overridden with -p + } + + table: { + mode: rounded # basic, compact, compact_double, light, thin, with_love, rounded, reinforced, heavy, none, other + index_mode: always # "always" show indexes, "never" show indexes, "auto" = show indexes when a table has "index" column + show_empty: true # show 'empty list' and 'empty record' placeholders for command output + padding: { left: 1, right: 1 } # a left right padding of each column in a table + trim: { + methodology: wrapping # wrapping or truncating + wrapping_try_keep_words: true # A strategy used by the 'wrapping' methodology + truncating_suffix: "..." # A suffix used by the 'truncating' methodology + } + header_on_separator: false # show header text on separator/border line + # abbreviated_row_count: 10 # limit data rows from top and bottom after reaching a set point + } + + error_style: "fancy" # "fancy" or "plain" for screen reader-friendly error messages + + # Whether an error message should be printed if an error of a certain kind is triggered. + display_errors: { + exit_code: false # assume the external command prints an error message + # Core dump errors are always printed, and SIGPIPE never triggers an error. + # The setting below controls message printing for termination by all other signals. + termination_signal: true + } + + # datetime_format determines what a datetime rendered in the shell would look like. + # Behavior without this configuration point will be to "humanize" the datetime display, + # showing something like "a day ago." + datetime_format: { + # normal: '%a, %d %b %Y %H:%M:%S %z' # shows up in displays of variables or other datetime's outside of tables + # table: '%m/%d/%y %I:%M:%S%p' # generally shows up in tabular outputs such as ls. commenting this out will change it to the default human readable datetime format + } + + explore: { + status_bar_background: { fg: "#1D1F21", bg: "#C4C9C6" }, + command_bar_text: { fg: "#C4C9C6" }, + highlight: { fg: "black", bg: "yellow" }, + status: { + error: { fg: "white", bg: "red" }, + warn: {} + info: {} + }, + selected_cell: { bg: light_blue }, + } + + history: { + max_size: 100_000 # Session has to be reloaded for this to take effect + sync_on_enter: true # Enable to share history between multiple sessions, else you have to close the session to write history to file + file_format: "plaintext" # "sqlite" or "plaintext" + isolation: false # only available with sqlite file_format. true enables history isolation, false disables it. true will allow the history to be isolated to the current session using up/down arrows. false will allow the history to be shared across all sessions. + } + + completions: { + case_sensitive: false # set to true to enable case-sensitive completions + quick: true # set this to false to prevent auto-selecting completions when only one remains + partial: true # set this to false to prevent partial filling of the prompt + algorithm: "prefix" # prefix or fuzzy + sort: "smart" # "smart" (alphabetical for prefix matching, fuzzy score for fuzzy matching) or "alphabetical" + external: { + enable: true # set to false to prevent nushell looking into $env.PATH to find more suggestions, `false` recommended for WSL users as this look up may be very slow + max_results: 100 # setting it lower can improve completion performance at the cost of omitting some options + completer: null # check 'carapace_completer' above as an example + } + use_ls_colors: true # set this to true to enable file/path/directory completions using LS_COLORS + } + + filesize: { + metric: false # true => KB, MB, GB (ISO standard), false => KiB, MiB, GiB (Windows standard) + format: "auto" # b, kb, kib, mb, mib, gb, gib, tb, tib, pb, pib, eb, eib, auto + } + + cursor_shape: { + emacs: line # block, underscore, line, blink_block, blink_underscore, blink_line, inherit to skip setting cursor shape (line is the default) + vi_insert: block # block, underscore, line, blink_block, blink_underscore, blink_line, inherit to skip setting cursor shape (block is the default) + vi_normal: underscore # block, underscore, line, blink_block, blink_underscore, blink_line, inherit to skip setting cursor shape (underscore is the default) + } + + color_config: $dark_theme # if you want a more interesting theme, you can replace the empty record with `$dark_theme`, `$light_theme` or another custom record + footer_mode: 25 # always, never, number_of_rows, auto + float_precision: 2 # the precision for displaying floats in tables + buffer_editor: null # command that will be used to edit the current line buffer with ctrl+o, if unset fallback to $env.EDITOR and $env.VISUAL + use_ansi_coloring: true + bracketed_paste: true # enable bracketed paste, currently useless on windows + edit_mode: emacs # emacs, vi + shell_integration: { + # osc2 abbreviates the path if in the home_dir, sets the tab/window title, shows the running command in the tab/window title + osc2: true + # osc7 is a way to communicate the path to the terminal, this is helpful for spawning new tabs in the same directory + osc7: true + # osc8 is also implemented as the deprecated setting ls.show_clickable_links, it shows clickable links in ls output if your terminal supports it. show_clickable_links is deprecated in favor of osc8 + osc8: true + # osc9_9 is from ConEmu and is starting to get wider support. It's similar to osc7 in that it communicates the path to the terminal + osc9_9: false + # osc133 is several escapes invented by Final Term which include the supported ones below. + # 133;A - Mark prompt start + # 133;B - Mark prompt end + # 133;C - Mark pre-execution + # 133;D;exit - Mark execution finished with exit code + # This is used to enable terminals to know where the prompt is, the command is, where the command finishes, and where the output of the command is + osc133: true + # osc633 is closely related to osc133 but only exists in visual studio code (vscode) and supports their shell integration features + # 633;A - Mark prompt start + # 633;B - Mark prompt end + # 633;C - Mark pre-execution + # 633;D;exit - Mark execution finished with exit code + # 633;E - Explicitly set the command line with an optional nonce + # 633;P;Cwd=<path> - Mark the current working directory and communicate it to the terminal + # and also helps with the run recent menu in vscode + osc633: true + # reset_application_mode is escape \x1b[?1l and was added to help ssh work better + reset_application_mode: true + } + render_right_prompt_on_last_line: false # true or false to enable or disable right prompt to be rendered on last line of the prompt. + use_kitty_protocol: false # enables keyboard enhancement protocol implemented by kitty console, only if your terminal support this. + highlight_resolved_externals: false # true enables highlighting of external commands in the repl resolved by which. + recursion_limit: 50 # the maximum number of times nushell allows recursion before stopping it + + plugins: {} # Per-plugin configuration. See https://www.nushell.sh/contributor-book/plugins.html#configuration. + + plugin_gc: { + # Configuration for plugin garbage collection + default: { + enabled: true # true to enable stopping of inactive plugins + stop_after: 10sec # how long to wait after a plugin is inactive to stop it + } + plugins: { + # alternate configuration for specific plugins, by name, for example: + # + # gstat: { + # enabled: false + # } + } + } + + hooks: { + pre_prompt: [{ null }] # run before the prompt is shown + pre_execution: [{ null }] # run before the repl input is run + env_change: { + PWD: [{|before, after| null }] # run if the PWD environment is different since the last repl input + } + display_output: "if (term size).columns >= 100 { table -e } else { table }" # run to display the output of a pipeline + command_not_found: { null } # return an error message when a command is not found + } + + menus: [ + # Configuration for default nushell menus + # Note the lack of source parameter + { + name: completion_menu + only_buffer_difference: false + marker: "| " + type: { + layout: columnar + columns: 4 + col_width: 20 # Optional value. If missing all the screen width is used to calculate column width + col_padding: 2 + } + style: { + text: green + selected_text: { attr: r } + description_text: yellow + match_text: { attr: u } + selected_match_text: { attr: ur } + } + } + { + name: ide_completion_menu + only_buffer_difference: false + marker: "| " + type: { + layout: ide + min_completion_width: 0, + max_completion_width: 50, + max_completion_height: 10, # will be limited by the available lines in the terminal + padding: 0, + border: true, + cursor_offset: 0, + description_mode: "prefer_right" + min_description_width: 0 + max_description_width: 50 + max_description_height: 10 + description_offset: 1 + # If true, the cursor pos will be corrected, so the suggestions match up with the typed text + # + # C:\> str + # str join + # str trim + # str split + correct_cursor_pos: false + } + style: { + text: green + selected_text: { attr: r } + description_text: yellow + match_text: { attr: u } + selected_match_text: { attr: ur } + } + } + { + name: history_menu + only_buffer_difference: true + marker: "? " + type: { + layout: list + page_size: 10 + } + style: { + text: green + selected_text: green_reverse + description_text: yellow + } + } + { + name: help_menu + only_buffer_difference: true + marker: "? " + type: { + layout: description + columns: 4 + col_width: 20 # Optional value. If missing all the screen width is used to calculate column width + col_padding: 2 + selection_rows: 4 + description_rows: 10 + } + style: { + text: green + selected_text: green_reverse + description_text: yellow + } + } + ] + + keybindings: [ + { + name: completion_menu + modifier: none + keycode: tab + mode: [emacs vi_normal vi_insert] + event: { + until: [ + { send: menu name: completion_menu } + { send: menunext } + { edit: complete } + ] + } + } + { + name: completion_previous_menu + modifier: shift + keycode: backtab + mode: [emacs, vi_normal, vi_insert] + event: { send: menuprevious } + } + { + name: ide_completion_menu + modifier: control + keycode: space + mode: [emacs vi_normal vi_insert] + event: { + until: [ + { send: menu name: ide_completion_menu } + { send: menunext } + { edit: complete } + ] + } + } + { + name: history_menu + modifier: control + keycode: char_r + mode: [emacs, vi_insert, vi_normal] + event: { send: menu name: history_menu } + } + { + name: help_menu + modifier: none + keycode: f1 + mode: [emacs, vi_insert, vi_normal] + event: { send: menu name: help_menu } + } + { + name: next_page_menu + modifier: control + keycode: char_x + mode: emacs + event: { send: menupagenext } + } + { + name: undo_or_previous_page_menu + modifier: control + keycode: char_z + mode: emacs + event: { + until: [ + { send: menupageprevious } + { edit: undo } + ] + } + } + { + name: escape + modifier: none + keycode: escape + mode: [emacs, vi_normal, vi_insert] + event: { send: esc } # NOTE: does not appear to work + } + { + name: cancel_command + modifier: control + keycode: char_c + mode: [emacs, vi_normal, vi_insert] + event: { send: ctrlc } + } + { + name: quit_shell + modifier: control + keycode: char_d + mode: [emacs, vi_normal, vi_insert] + event: { send: ctrld } + } + { + name: clear_screen + modifier: control + keycode: char_l + mode: [emacs, vi_normal, vi_insert] + event: { send: clearscreen } + } + { + name: search_history + modifier: control + keycode: char_q + mode: [emacs, vi_normal, vi_insert] + event: { send: searchhistory } + } + { + name: open_command_editor + modifier: control + keycode: char_o + mode: [emacs, vi_normal, vi_insert] + event: { send: openeditor } + } + { + name: move_up + modifier: none + keycode: up + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: menuup } + { send: up } + ] + } + } + { + name: move_down + modifier: none + keycode: down + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: menudown } + { send: down } + ] + } + } + { + name: move_left + modifier: none + keycode: left + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: menuleft } + { send: left } + ] + } + } + { + name: move_right_or_take_history_hint + modifier: none + keycode: right + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: historyhintcomplete } + { send: menuright } + { send: right } + ] + } + } + { + name: move_one_word_left + modifier: control + keycode: left + mode: [emacs, vi_normal, vi_insert] + event: { edit: movewordleft } + } + { + name: move_one_word_right_or_take_history_hint + modifier: control + keycode: right + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: historyhintwordcomplete } + { edit: movewordright } + ] + } + } + { + name: move_to_line_start + modifier: none + keycode: home + mode: [emacs, vi_normal, vi_insert] + event: { edit: movetolinestart } + } + { + name: move_to_line_start + modifier: control + keycode: char_a + mode: [emacs, vi_normal, vi_insert] + event: { edit: movetolinestart } + } + { + name: move_to_line_end_or_take_history_hint + modifier: none + keycode: end + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: historyhintcomplete } + { edit: movetolineend } + ] + } + } + { + name: move_to_line_end_or_take_history_hint + modifier: control + keycode: char_e + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: historyhintcomplete } + { edit: movetolineend } + ] + } + } + { + name: move_to_line_start + modifier: control + keycode: home + mode: [emacs, vi_normal, vi_insert] + event: { edit: movetolinestart } + } + { + name: move_to_line_end + modifier: control + keycode: end + mode: [emacs, vi_normal, vi_insert] + event: { edit: movetolineend } + } + { + name: move_down + modifier: control + keycode: char_n + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: menudown } + { send: down } + ] + } + } + { + name: move_up + modifier: control + keycode: char_p + mode: [emacs, vi_normal, vi_insert] + event: { + until: [ + { send: menuup } + { send: up } + ] + } + } + { + name: delete_one_character_backward + modifier: none + keycode: backspace + mode: [emacs, vi_insert] + event: { edit: backspace } + } + { + name: delete_one_word_backward + modifier: control + keycode: backspace + mode: [emacs, vi_insert] + event: { edit: backspaceword } + } + { + name: delete_one_character_forward + modifier: none + keycode: delete + mode: [emacs, vi_insert] + event: { edit: delete } + } + { + name: delete_one_character_forward + modifier: control + keycode: delete + mode: [emacs, vi_insert] + event: { edit: delete } + } + { + name: delete_one_character_backward + modifier: control + keycode: char_h + mode: [emacs, vi_insert] + event: { edit: backspace } + } + { + name: delete_one_word_backward + modifier: control + keycode: char_w + mode: [emacs, vi_insert] + event: { edit: backspaceword } + } + { + name: move_left + modifier: none + keycode: backspace + mode: vi_normal + event: { edit: moveleft } + } + { + name: newline_or_run_command + modifier: none + keycode: enter + mode: emacs + event: { send: enter } + } + { + name: move_left + modifier: control + keycode: char_b + mode: emacs + event: { + until: [ + { send: menuleft } + { send: left } + ] + } + } + { + name: move_right_or_take_history_hint + modifier: control + keycode: char_f + mode: emacs + event: { + until: [ + { send: historyhintcomplete } + { send: menuright } + { send: right } + ] + } + } + { + name: redo_change + modifier: control + keycode: char_g + mode: emacs + event: { edit: redo } + } + { + name: undo_change + modifier: control + keycode: char_z + mode: emacs + event: { edit: undo } + } + { + name: paste_before + modifier: control + keycode: char_y + mode: emacs + event: { edit: pastecutbufferbefore } + } + { + name: cut_word_left + modifier: control + keycode: char_w + mode: emacs + event: { edit: cutwordleft } + } + { + name: cut_line_to_end + modifier: control + keycode: char_k + mode: emacs + event: { edit: cuttolineend } + } + { + name: cut_line_from_start + modifier: control + keycode: char_u + mode: emacs + event: { edit: cutfromstart } + } + { + name: swap_graphemes + modifier: control + keycode: char_t + mode: emacs + event: { edit: swapgraphemes } + } + { + name: move_one_word_left + modifier: alt + keycode: left + mode: emacs + event: { edit: movewordleft } + } + { + name: move_one_word_right_or_take_history_hint + modifier: alt + keycode: right + mode: emacs + event: { + until: [ + { send: historyhintwordcomplete } + { edit: movewordright } + ] + } + } + { + name: move_one_word_left + modifier: alt + keycode: char_b + mode: emacs + event: { edit: movewordleft } + } + { + name: move_one_word_right_or_take_history_hint + modifier: alt + keycode: char_f + mode: emacs + event: { + until: [ + { send: historyhintwordcomplete } + { edit: movewordright } + ] + } + } + { + name: delete_one_word_forward + modifier: alt + keycode: delete + mode: emacs + event: { edit: deleteword } + } + { + name: delete_one_word_backward + modifier: alt + keycode: backspace + mode: emacs + event: { edit: backspaceword } + } + { + name: delete_one_word_backward + modifier: alt + keycode: char_m + mode: emacs + event: { edit: backspaceword } + } + { + name: cut_word_to_right + modifier: alt + keycode: char_d + mode: emacs + event: { edit: cutwordright } + } + { + name: upper_case_word + modifier: alt + keycode: char_u + mode: emacs + event: { edit: uppercaseword } + } + { + name: lower_case_word + modifier: alt + keycode: char_l + mode: emacs + event: { edit: lowercaseword } + } + { + name: capitalize_char + modifier: alt + keycode: char_c + mode: emacs + event: { edit: capitalizechar } + } + # The following bindings with `*system` events require that Nushell has + # been compiled with the `system-clipboard` feature. + # If you want to use the system clipboard for visual selection or to + # paste directly, uncomment the respective lines and replace the version + # using the internal clipboard. + { + name: copy_selection + modifier: control_shift + keycode: char_c + mode: emacs + event: { edit: copyselection } + # event: { edit: copyselectionsystem } + } + { + name: cut_selection + modifier: control_shift + keycode: char_x + mode: emacs + event: { edit: cutselection } + # event: { edit: cutselectionsystem } + } + # { + # name: paste_system + # modifier: control_shift + # keycode: char_v + # mode: emacs + # event: { edit: pastesystem } + # } + { + name: select_all + modifier: control_shift + keycode: char_a + mode: emacs + event: { edit: selectall } + } + ] +} + +if ("~/.cache/wal/sequences" | path exists) { ^cat ~/.cache/wal/sequences } +source ~/.cache/zoxide.nu +if ("~/.cache/starship.nu" | path exists) { source ~/.cache/starship.nu } +if ("~/.cache/carapace.nu" | path exists) { source ~/.cache/carapace.nu } + +$env.K9S_DEFAULT_PF_ADDRESS = "0.0.0.0" +$env.config.show_banner = false + +alias p = pnsh-nvim +alias k = kubectl +alias d = docker +alias t = terraform +alias g = git +alias f = nvim +G +only + +let pistarchio_dir = "~/Programming/Pionative/pistarchio" | path expand +$env.PISTARCHIO_STACKS_DIR = $pistarchio_dir + "/stacks" +$env.PISTARCHIO_LIBRARY_DIR = $pistarchio_dir + "/library" +$env.PISTARCHIO_VENDOR_DESTINATION_DIR = ($pistarchio_dir + "/../clients") | path expand +overlay use ~/Programming/Pionative/quickstart/.venv/bin/activate.nu diff --git a/mut/nushell/env.nu b/mut/nushell/env.nu new file mode 100644 index 0000000..435a090 --- /dev/null +++ b/mut/nushell/env.nu @@ -0,0 +1,213 @@ +# Nushell Environment Config File +# +# version = "0.99.1" + +def create_left_prompt [] { + let dir = match (do --ignore-errors { $env.PWD | path relative-to $nu.home-path }) { + null => $env.PWD + '' => '~' + $relative_pwd => ([~ $relative_pwd] | path join) + } + + let path_color = (if (is-admin) { ansi red_bold } else { ansi green_bold }) + let separator_color = (if (is-admin) { ansi light_red_bold } else { ansi light_green_bold }) + let path_segment = $"($path_color)($dir)(ansi reset)" + + $path_segment | str replace --all (char path_sep) $"($separator_color)(char path_sep)($path_color)" +} + +def create_right_prompt [] { + # create a right prompt in magenta with green separators and am/pm underlined + let time_segment = ([ + (ansi reset) + (ansi magenta) + (date now | format date '%x %X') # try to respect user's locale + ] | str join | str replace --regex --all "([/:])" $"(ansi green)${1}(ansi magenta)" | + str replace --regex --all "([AP]M)" $"(ansi magenta_underline)${1}") + + let last_exit_code = if ($env.LAST_EXIT_CODE != 0) {([ + (ansi rb) + ($env.LAST_EXIT_CODE) + ] | str join) + } else { "" } + + ([$last_exit_code, (char space), $time_segment] | str join) +} + +# Use nushell functions to define your right and left prompt +$env.PROMPT_COMMAND = {|| create_left_prompt } +# FIXME: This default is not implemented in rust code as of 2023-09-08. +$env.PROMPT_COMMAND_RIGHT = {|| create_right_prompt } + +# The prompt indicators are environmental variables that represent +# the state of the prompt +$env.PROMPT_INDICATOR = {|| "> " } +$env.PROMPT_INDICATOR_VI_INSERT = {|| ": " } +$env.PROMPT_INDICATOR_VI_NORMAL = {|| "> " } +$env.PROMPT_MULTILINE_INDICATOR = {|| "::: " } + +# If you want previously entered commands to have a different prompt from the usual one, +# you can uncomment one or more of the following lines. +# This can be useful if you have a 2-line prompt and it's taking up a lot of space +# because every command entered takes up 2 lines instead of 1. You can then uncomment +# the line below so that previously entered commands show with a single `🚀`. +# $env.TRANSIENT_PROMPT_COMMAND = {|| "🚀 " } +# $env.TRANSIENT_PROMPT_INDICATOR = {|| "" } +# $env.TRANSIENT_PROMPT_INDICATOR_VI_INSERT = {|| "" } +# $env.TRANSIENT_PROMPT_INDICATOR_VI_NORMAL = {|| "" } +# $env.TRANSIENT_PROMPT_MULTILINE_INDICATOR = {|| "" } +# $env.TRANSIENT_PROMPT_COMMAND_RIGHT = {|| "" } + +# Specifies how environment variables are: +# - converted from a string to a value on Nushell startup (from_string) +# - converted from a value back to a string when running external commands (to_string) +# Note: The conversions happen *after* config.nu is loaded +$env.ENV_CONVERSIONS = { + "PATH": { + from_string: { |s| $s | split row (char esep) | path expand --no-symlink } + to_string: { |v| $v | path expand --no-symlink | str join (char esep) } + } + "Path": { + from_string: { |s| $s | split row (char esep) | path expand --no-symlink } + to_string: { |v| $v | path expand --no-symlink | str join (char esep) } + } +} + +# Directories to search for scripts when calling source or use +# The default for this is $nu.default-config-dir/scripts +$env.NU_LIB_DIRS = [ + ($nu.default-config-dir | path join 'scripts') # add <nushell-config-dir>/scripts + ($nu.data-dir | path join 'completions') # default home for nushell completions +] + +# Directories to search for plugin binaries when calling register +# The default for this is $nu.default-config-dir/plugins +$env.NU_PLUGIN_DIRS = [ + ($nu.default-config-dir | path join 'plugins') # add <nushell-config-dir>/plugins +] + +# To load from a custom file you can use: +# source ($nu.default-config-dir | path join 'custom.nu') + +let darwin: bool = (uname | get operating-system) == "Darwin" +let nix: bool = "/nix" | path exists + +if $darwin and $nix { + $env.__NIX_DARWIN_SET_ENVIRONMENT_DONE = 1 + + $env.PATH = [ + $"($env.HOME)/.nix-profile/bin" + $"/etc/profiles/per-user/($env.USER)/bin" + "/run/current-system/sw/bin" + "/nix/var/nix/profiles/default/bin" + "/usr/local/bin" + "/usr/bin" + "/usr/sbin" + "/bin" + "/sbin" + ] + $env.EDITOR = "VIM" + $env.NIX_PATH = [ + $"darwin-config=($env.HOME)/.nixpkgs/darwin-configuration.nix" + "/nix/var/nix/profiles/per-user/root/channels" + ] + $env.NIX_SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt" + $env.PAGER = "less -R" + $env.TERMINFO_DIRS = [ + $"($env.HOME)/.nix-profile/share/terminfo" + $"/etc/profiles/per-user/($env.USER)/share/terminfo" + "/run/current-system/sw/share/terminfo" + "/nix/var/nix/profiles/default/share/terminfo" + "/usr/share/terminfo" + ] + $env.XDG_CONFIG_DIRS = [ + $"($env.HOME)/.nix-profile/etc/xdg" + $"/etc/profiles/per-user/($env.USER)/etc/xdg" + "/run/current-system/sw/etc/xdg" + "/nix/var/nix/profiles/default/etc/xdg" + ] + $env.XDG_DATA_DIRS = [ + $"($env.HOME)/.nix-profile/share" + $"/etc/profiles/per-user/($env.USER)/share" + "/run/current-system/sw/share" + "/nix/var/nix/profiles/default/share" + ] + $env.TERM = $env.TERM + $env.NIX_USER_PROFILE_DIR = $"/nix/var/nix/profiles/per-user/($env.USER)" + $env.NIX_PROFILES = [ + "/nix/var/nix/profiles/default" + "/run/current-system/sw" + $"/etc/profiles/per-user/($env.USER)" + $"($env.HOME)/.nix-profile" + ] + + if ($"($env.HOME)/.nix-defexpr/channels" | path exists) { + $env.NIX_PATH = ($env.PATH | split row (char esep) | append $"($env.HOME)/.nix-defexpr/channels") + } + + if (false in (ls -l `/nix/var/nix`| where type == dir | where name == "/nix/var/nix/db" | get mode | str contains "w")) { + $env.NIX_REMOTE = "daemon" + } +} + +# To add entries to PATH (on Windows you might use Path), you can use the following pattern: +# $env.PATH = ($env.PATH | split row (char esep) | prepend '/some/path') +# An alternate way to add entries to $env.PATH is to use the custom command `path add` +# which is built into the nushell stdlib: +use std "path add" +# $env.PATH = ($env.PATH | split row (char esep)) +# path add /some/path +# path add ($env.CARGO_HOME | path join "bin") +try { + if $darwin { + $env.PATH = ["/opt/homebrew/bin" "/opt/X11/bin" "/opt/local/bin" "/opt/local/sbin"] ++ $env.PATH + } +} +path add ($env.HOME | path join ".local" "bin") +$env.PATH = ($env.PATH | uniq) + +$env.XDG_CACHE_HOME = "~/.cache" | path expand +$env.XDG_DATA_HOME = "~/.local/share" | path expand +$env.XDG_CONFIG_HOME = "~/.config" | path expand + +if (which carapace | is-not-empty) { + $env.CARAPACE_BRIDGES = 'zsh,fish,bash,inshellisense' # optional + carapace _carapace nushell | save --force ~/.cache/carapace.nu +} +if (which zoxide | is-not-empty) { + zoxide init nushell --cmd=cd | save --force ~/.cache/zoxide.nu +} +if (which starship | is-not-empty) { + starship init nu | save --force ~/.cache/starship.nu +} + +if (not ("/var/run/docker.sock" | path exists)) and (not darwin) { + $env.DOCKER_HOST = $"unix://($env | default $"/run/($env.USER)" XDG_RUNTIME_DIR | get XDG_RUNTIME_DIR)/docker.sock" +} + +# if not ("/.dockerenv" | path exists) { +# do --env { +# let ssh_agent_file = ( +# $nu.temp-path | path join $"ssh-agent-($env.USER).nuon" +# ) +# +# if ($ssh_agent_file | path exists) { +# let ssh_agent_env = open ($ssh_agent_file) +# if (ps | where pid == ($ssh_agent_env.SSH_AGENT_PID | into int) | is-not-empty) { +# load-env $ssh_agent_env +# return +# } else { +# rm $ssh_agent_file +# } +# } +# +# let ssh_agent_env = ssh-agent -c +# | lines +# | first 2 +# | parse "setenv {name} {value};" +# | transpose --header-row +# | into record +# load-env $ssh_agent_env +# $ssh_agent_env | save --force $ssh_agent_file +# } +# } diff --git a/mut/nushell/login.nu b/mut/nushell/login.nu new file mode 100644 index 0000000..4ca972e --- /dev/null +++ b/mut/nushell/login.nu @@ -0,0 +1,14 @@ +def load-posix-env [p: string] { + bash -c $"source ($p) && env" + | lines + | parse "{n}={v}" + | filter { |x| ($x.n not-in $env) or $x.v != ($env | get $x.n) } + | where n not-in ["_", "LAST_EXIT_CODE", "DIRS_POSITION"] + | transpose --header-row + | into record + | load-env +} + +load-posix-env /etc/profile +# load-posix-env $"($env.HOME)/.profile" +$env.PATH = $env.PATH ++ [$"($env.HOME)/.local/bin"] diff --git a/mut/nushell/scripts/task.nu b/mut/nushell/scripts/task.nu new file mode 100644 index 0000000..6c7968b --- /dev/null +++ b/mut/nushell/scripts/task.nu @@ -0,0 +1,434 @@ +# Spawn a task to run in the background, even when the shell is closed. +# +# Note that a fresh Nushell interpreter is spawned to execute the +# given task, so it won't inherit the current scope's variables, +# custom commands and alias definitions. +# +# It will only inherit environment variables which can be converted to a string. +# +# Note that the closure can't take arguments. +# +# Example usage: task spawn { echo "Hello, World!" } +export def spawn [ + command: closure # The closure to run. + --working-directory (-w): directory # Specify the working directory the task will be run in. + --immediate (-i) # Immediately start the task. + --stashed (-s) # Create the task in Stashed state. Useful to avoid immediate execution if the queue is empty + --delay (-d): duration # Queue the task for execution only after the duration. + --group (-g): string # The group to spawn the task under. + --after (-a): int # Start the task once all specified tasks have successfully finished. As soon as one of the dependencies fails, this task will fail as well. + --priority (-o): string # Start this task with a higher priority. The higher the number, the faster it will be processed. + --label (-l): string # Label the task. This string will be shown in the `status` column of `task status`. +] -> int { + mut args = [] + + if $working_directory != null { + $args = ($args | prepend ["--working-directory", $working_directory]) + } + if $immediate { + $args = ($args | prepend "--immediate") + } + if $stashed { + $args = ($args | prepend "--stashed") + } + if $delay != null { + $args = ($args | prepend ["--delay" ($delay | format duration sec | parse "{secs} {_}" | get 0.secs)]) + } + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $after != null { + $args = ($args | prepend ["--after" $after]) + } + if $priority != null { + $args = ($args | prepend ["--priority" $priority]) + } + if $label != null { + $args = ($args | prepend ["--label" $label]) + } + + let source_path = mktemp --tmpdir --suffix "-nu-task" + + ( + view source $command + | str trim --left --char "{" + | str trim --right --char "}" + ) + | save --force $source_path + + (pueue add --print-task-id ...$args $"nu --config '($nu.config-path)' --env-config '($nu.env-path)' ($source_path)") +} + +# Remove tasks from the queue. +# Running or paused tasks need to be killed first. +export def remove [ + ...ids: int # IDs of the tasks to remove from the status list. +] { + pueue remove ...$ids +} + +# Switches the queue position of two tasks. +# Only works for queued or stashed tasks. +export def switch [ + task_id_1: int # The first task ID. + task_id_2: int # The second task ID. +] { + pueue switch $task_id_1 $task_id_2 +} + +# Stash a task that is not currently running. +# +# Stashed tasks won't be automatically started. +# You will have to queue them or start them by hand. +export def stash [ + ...ids: int # IDs of the tasks to stash. +] { + pueue stash ...$ids +} + +# Queue stashed tasks for execution. +export def queue [ + ...ids: int # IDs of the tasks to queue. + --delay (-d): duration # Queue only after the specified delay. +] { + let args = if $delay != null { + ["--delay" ($delay | format duration sec | parse '{secs} {_}' | get 0.secs)] + } else { + [] + } + + pueue enqueue ...$args ...$ids +} + +# Resume operation of specific tasks or groups of tasks. +# +# By default, this resumes the default group and all its tasks. +# It can also be used force-start specific tasks or start whole groups. +export def start [ + ...ids: int # IDs of the tasks to start. By default all the tasks in the default group will be started. + --group (-g): string # Resume a specific group and all paused tasks in it. The group will be set to running and its paused tasks will be resumed. + --all (-a) # Resume all groups. All groups will be set to running and paused tasks will be resumed. +] { + mut args = [] + + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $all { + $args = ($args | prepend "--all") + } + + pueue start ...$args +} + +# Restart failed or successful task(s). +# +# By default, identical tasks will be created and +# enqueued, but it's possible to restart in-place. +# +# You can also edit a few properties, such as +# the path and the command of the task, before restarting. +export def restart [ + ...ids: int # IDs of the tasks to restart. + --all-failed (-a) # Restart all failed tasks across all groups. Nice to use in combination with `--in-place/i`. + --failed-in-group (-g): string # Like `--all-failed`, but only restart tasks failed tasks of a specific group. The group will be set to running and its paused tasks will be resumed. + --start-immediately (-k) # Immediately start the tasks, no matter how many open slots there are. This will ignore any dependencies tasks may have. + --stashed (-s) # Set the restarted task to a "Stashed" state. Useful to avoid immediate execution. + --in-place (-i) # Restart the tasks by reusing the already existing tasks. + --not-in-place (-n) # Opposite of `--in-place`. This is already the default unless you have `restart_in_place` set to true. + --edit (-e) # Edit the tasks' commands before restarting + --edit-path (-p) # Edit the tasks' paths before restarting + --edit-label (-l) # Edit the tasks' labels before restarting +] { + mut args = [] + + if $all_failed { + $args = ($args | prepend "--all-failed") + } + if $failed_in_group != null { + $args = ($args | prepend "--failed-in-group") + } + if $start_immediately { + $args = ($args | prepend "--start-immediately") + } + if $stashed { + $args = ($args | prepend "--stashed") + } + if $in_place { + $args = ($args | prepend "--in-place") + } + if $not_in_place { + $args = ($args | prepend "--not-in-place") + } + if $edit { + $args = ($args | prepend "--edit") + } + if $edit_path { + $args = ($args | prepend "--edit-path") + } + if $edit_label { + $args = ($args | prepend "--edit-label") + } + + pueue restart ...$args ...$ids +} + +# Either pause a running tasks or a specific groups of tasks. +# +# By default, pauses the default group and all its tasks. +# +# A paused group won't start any new tasks automatically. +export def pause [ + ...ids: int # IDs of the tasks to pause. + --group (-g) # Pause a specific group + --all (-a) # Pause all groups. + --wait (-w) # Only pause the specified group and let already running tasks finish by themselves +] { + mut args = [] + + if $group != null { + $args = ($args | prepend "--group") + } + if $all != null { + $args = ($args | prepend "--all") + } + if $wait != null { + $args = ($args | prepend "--wait") + } + + pueue pause ...$args ...$ids +} + +# Kill specific running tasks or whole task groups. +# +# Kills all tasks of the default group when no ids or a specific group are provided. +export def kill [ + ...ids: int # IDs of the tasks to kill. + --group (-g): string # Kill all running tasks in a group. This also pauses the group. + --all (-a) # Kill all running tasks across ALL groups. This also pauses all groups. + --signal (-s): string # Send a UNIX signal instead of simply killing the process. DISCLAIMER: This bypasses Pueue's process handling logic! You might enter weird invalid states, use at your own descretion. +] { + mut args = [] + + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $all { + $args = ($args | prepend "--all") + } + if $signal != null { + $args = ($args | prepend ["--signal" $signal]) + } + + pueue kill ...$args ...$ids +} + +# Send something to a task. Useful for sending confirmations such as "y\n". +export def send [ + id: int # ID of the task to send something to. + input: string # The input that should be sent to the process. +] { + pueue send $id $input +} + +# Edit the command, path or label of a stashed or queued task. +# +# By default only the command is edited. +# +# Multiple properties can be added in one go. +export def edit [ + id: int # ID of the task to edit. + --command (-c) # Edit the task's command + --path (-p) # Edit the task's path + --label (-l) # Edit the task's label +] { + mut args = [] + + if $command { + $args = ($args | prepend "--command") + } + if $path { + $args = ($args | prepend "--path") + } + if $label { + $args = ($args | prepend "--label") + } + + pueue edit ...$args $id +} + +# Use this to add or remove groups. +# +# By default, this will simply display all known groups. +export def group [] { + pueue group --json | from json +} + +# Create a new group with a name. +export def "group add" [ + name: string # The name of the new group. + --parallel (-p): int # The amount of parallel tasks the group can run at one time. +] { + let args = if $parallel != null { + ["--parallel" $parallel] + } else { + [] + } + + pueue group add ...$args $name +} + +# Remove a group with a name. +export def "group remove" [ + name: string # The name of the group to be removed. +] { + pueue group remove $name +} + +# Display the current status of all tasks. +export def status [ + --detailed (-d) # Return a table with more detailed information. +] { + let output = ( + pueue status --json + | from json + | get tasks + | transpose --ignore-titles status + | flatten + ) + + # TODO: Rename the Done column to done. + if not $detailed { + $output | select id label group Done? status? start? end? + } else { + $output + } +} + +# Display the output of tasks. +# +# Only the last few lines will be shown by default for multiple tasks. +# If you want to follow the output, use `--tail/-t`. +export def log [ + ...ids: int # The tasks to check the outputs of. + --last (-l): int # Only print the last N lines of each task's output. This is done by default if you're looking at multiple tasks. + --tail (-t) # Follow the output as it is printing. Only works with 1 task. When used in conjunction with `--last`, the last N lines will be printed before starting to wait for output. + --detailed (-d) # Include all fields, don't simplify output. +] { + def process_raw [raw: string] { + let full = ( + $raw + | from json + | transpose -i info + | flatten --all + | flatten --all + ) + + if $detailed { + $full + } else { + $full | select id label group Done? status? start? end? + } + } + + if (($ids | length) == 1) { + if $tail { + let args = if $last != null { + ["--lines" $last] + } else { + [] + } + + pueue follow ...$ids + } else { + let args = if $last != null { + ["--lines" $last] + } else { + [] + } + + process_raw (pueue log --full --json ...$args ...$ids) + | first + } + } else { + if $tail { + echo $"(ansi red)--tail can only be used with one task.(ansi reset)" + return + } + + let args = if $last != null { + ["--lines" $last] + } else { + [] + } + + process_raw (pueue log --full --json ...$args ...$ids) + } +} + +# Wait until the provided tasks are finished. +# +# This is like join() or await in many languages. +export def wait [ + ...ids: int # IDs of the tasks to wait for. + --group (-g): string # Wait for all tasks in a specific group. + --all (-a) # Wait for all tasks across all groups and the default group. + --quiet (-q) # Don't show any log output while waiting. + --status (-s): string # Wait for tasks to reach a specific task status. +] { + mut args = [] + + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + if $all { + $args = ($args | prepend $all) + } + if $quiet { + $args = ($args | prepend $quiet) + } + if $status != null { + $args = ($args | prepend ["--status" $status]) + } + + pueue wait ...$args ...$ids +} + +# Remove tasks from the status list. +export def clean [ + --successful-only (-s) # Only clean tasks that finished successfully + --group (-g): string # Only clean tasks of a specific group +] { + mut args = [] + + if $successful_only { + $args = ($args | prepend "--successful-only") + } + if $group != null { + $args = ($args | prepend ["--group" $group]) + } + + pueue clean ...$args +} + +# Shutdown pueue and thus this module. +export def shutdown [] { + pueue shutdown +} + +# Set the maximum parallel tasks for a group. +# +# Note that no tasks will be stopped if the number is lowered. +# The limit only applies when schelduing. +export def set-parallel-limit [ + max: int # The maximum parallel tasks allowed for a group when schelduing. + --group (-g): string # The group to set the limit for. By default this is `default`. +] { + let args = if $group != null { + ["--group" $group] + } else { + [] + } + + pueue parallel ...$args $max +} diff --git a/mut/st/FUNDING.yml b/mut/st/FUNDING.yml new file mode 100644 index 0000000..c7c9a22 --- /dev/null +++ b/mut/st/FUNDING.yml @@ -0,0 +1,2 @@ +custom: ["https://lukesmith.xyz/donate.html"] +github: lukesmithxyz diff --git a/mut/st/LICENSE b/mut/st/LICENSE new file mode 100644 index 0000000..3cbf420 --- /dev/null +++ b/mut/st/LICENSE @@ -0,0 +1,34 @@ +MIT/X Consortium License + +© 2014-2022 Hiltjo Posthuma <hiltjo at codemadness dot org> +© 2018 Devin J. Pohly <djpohly at gmail dot com> +© 2014-2017 Quentin Rameau <quinq at fifth dot space> +© 2009-2012 Aurélien APTEL <aurelien dot aptel at gmail dot com> +© 2008-2017 Anselm R Garbe <garbeam at gmail dot com> +© 2012-2017 Roberto E. Vargas Caballero <k0ga at shike2 dot com> +© 2012-2016 Christoph Lohmann <20h at r-36 dot net> +© 2013 Eon S. Jeon <esjeon at hyunmu dot am> +© 2013 Alexander Sedov <alex0player at gmail dot com> +© 2013 Mark Edgar <medgar123 at gmail dot com> +© 2013-2014 Eric Pruitt <eric.pruitt at gmail dot com> +© 2013 Michael Forney <mforney at mforney dot org> +© 2013-2014 Markus Teich <markus dot teich at stusta dot mhn dot de> +© 2014-2015 Laslo Hunhold <dev at frign dot de> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/mut/st/Makefile b/mut/st/Makefile new file mode 100644 index 0000000..02045f0 --- /dev/null +++ b/mut/st/Makefile @@ -0,0 +1,62 @@ +# st - simple terminal +# See LICENSE file for copyright and license details. +.POSIX: + +include config.mk + +SRC = st.c x.c boxdraw.c hb.c +OBJ = $(SRC:.c=.o) + +all: options st + +options: + @echo st build options: + @echo "CFLAGS = $(STCFLAGS)" + @echo "LDFLAGS = $(STLDFLAGS)" + @echo "CC = $(CC)" + +.c.o: + $(CC) $(STCFLAGS) -c $< + +st.o: config.h st.h win.h +x.o: arg.h config.h st.h win.h hb.h +hb.o: st.h +boxdraw.o: config.h st.h boxdraw_data.h + +$(OBJ): config.h config.mk + +st: $(OBJ) + $(CC) -o $@ $(OBJ) $(STLDFLAGS) + +clean: + rm -f st $(OBJ) st-$(VERSION).tar.gz *.rej *.orig *.o + +dist: clean + mkdir -p st-$(VERSION) + cp -R FAQ LEGACY TODO LICENSE Makefile README config.mk\ + config.h st.info st.1 arg.h st.h win.h $(SRC)\ + st-$(VERSION) + tar -cf - st-$(VERSION) | gzip > st-$(VERSION).tar.gz + rm -rf st-$(VERSION) + +install: st + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f st $(DESTDIR)$(PREFIX)/bin + cp -f st-copyout $(DESTDIR)$(PREFIX)/bin + cp -f st-urlhandler $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/st + chmod 755 $(DESTDIR)$(PREFIX)/bin/st-copyout + chmod 755 $(DESTDIR)$(PREFIX)/bin/st-urlhandler + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < st.1 > $(DESTDIR)$(MANPREFIX)/man1/st.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/st.1 + tic -sx st.info + @echo Please see the README file regarding the terminfo entry of st. + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/st + rm -f $(DESTDIR)$(PREFIX)/bin/st-copyout + rm -f $(DESTDIR)$(PREFIX)/bin/st-urlhandler + rm -f $(DESTDIR)$(MANPREFIX)/man1/st.1 + +.PHONY: all options clean dist install uninstall diff --git a/mut/st/PKGBUILD b/mut/st/PKGBUILD new file mode 100644 index 0000000..66dd05e --- /dev/null +++ b/mut/st/PKGBUILD @@ -0,0 +1,45 @@ +# Maintainer: + +pkgname=st-luke-git +_pkgname=st +pkgver=0.8.2.r1062.2087ab9 +pkgrel=1 +epoch=1 +pkgdesc="Luke's simple (suckless) terminal with vim-bindings, transparency, xresources, etc. " +url='https://github.com/LukeSmithxyz/st' +arch=('i686' 'x86_64') +license=('MIT') +options=('zipman') +depends=('libxft') +makedepends=('ncurses' 'libxext' 'git') +optdepends=('dmenu: feed urls to dmenu') +source=(git+https://github.com/LukeSmithxyz/st) +sha1sums=('SKIP') + +provides=("${_pkgname}") +conflicts=("${_pkgname}") + +pkgver() { + cd "${_pkgname}" + printf "%s.r%s.%s" "$(awk '/^VERSION =/ {print $3}' config.mk)" \ + "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} + +prepare() { + cd $srcdir/${_pkgname} + # skip terminfo which conflicts with ncurses + sed -i '/tic /d' Makefile +} + +build() { + cd "${_pkgname}" + make X11INC=/usr/include/X11 X11LIB=/usr/lib/X11 +} + +package() { + cd "${_pkgname}" + make PREFIX=/usr DESTDIR="${pkgdir}" install + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" + install -Dm644 Xdefaults "${pkgdir}/usr/share/doc/${pkgname}/Xdefaults.example" +} diff --git a/mut/st/README.md b/mut/st/README.md new file mode 100644 index 0000000..a233146 --- /dev/null +++ b/mut/st/README.md @@ -0,0 +1,89 @@ +# Luke's build of st - the simple (suckless) terminal + +The [suckless terminal (st)](https://st.suckless.org/) with some additional +features that make it literally the best terminal emulator ever: + +## Unique features (using dmenu) + ++ **follow urls** by pressing `alt-l` ++ **copy urls** in the same way with `alt-y` ++ **copy the output of commands** with `alt-o` + +## Bindings for + ++ **scrollback** with `alt-↑/↓` or `alt-pageup/down` or `shift` while scrolling the + mouse. ++ OR **vim-bindings**: scroll up/down in history with `alt-k` and `alt-j`. + Faster with `alt-u`/`alt-d`. ++ **zoom/change font size**: same bindings as above, but holding down shift as + well. `alt-home` returns to default ++ **copy text** with `alt-c`, **paste** is `alt-v` or `shift-insert` + +## Pretty stuff + ++ Compatibility with `Xresources` and `pywal` for dynamic colors. ++ Default [gruvbox](https://github.com/morhetz/gruvbox) colors otherwise. ++ Transparency/alpha, which is also adjustable from your `Xresources`. ++ Default font is system "mono" at 14pt, meaning the font will match your + system font. + +## Other st patches + ++ Boxdraw ++ Ligatures ++ font2 ++ updated to latest version 0.8.5 + +## Installation for newbs + +You should have xlib header files and libharfbuzz build files installed. + +``` +git clone https://github.com/LukeSmithxyz/st +cd st +sudo make install +``` + +Obviously, `make` is required to build. `fontconfig` is required for the +default build, since it asks `fontconfig` for your system monospace font. It +might be obvious, but `libX11` and `libXft` are required as well. Chances are, +you have all of this installed already. + +On OpenBSD, be sure to edit `config.mk` first and remove `-lrt` from the +`$LIBS` before compiling. + +Be sure to have a composite manager (`xcompmgr`, `picom`, etc.) running if you +want transparency. + +## How to configure dynamically with Xresources + +For many key variables, this build of `st` will look for X settings set in +either `~/.Xdefaults` or `~/.Xresources`. You must run `xrdb` on one of these +files to load the settings. + +For example, you can define your desired fonts, transparency or colors: + +``` +*.font: Liberation Mono:pixelsize=12:antialias=true:autohint=true; +*.alpha: 0.9 +*.color0: #111 +... +``` + +The `alpha` value (for transparency) goes from `0` (transparent) to `1` +(opaque). There is an example `Xdefaults` file in this respository. + +### Colors + +To be clear about the color settings: + +- This build will use gruvbox colors by default and as a fallback. +- If there are Xresources colors defined, those will take priority. +- But if `wal` has run in your session, its colors will take priority. + +Note that when you run `wal`, it will negate the transparency of existing windows, but new windows will continue with the previously defined transparency. + +## Contact + +- Luke Smith <luke@lukesmith.xyz> +- [https://lukesmith.xyz](https://lukesmith.xyz) diff --git a/mut/st/Xdefaults b/mut/st/Xdefaults new file mode 100644 index 0000000..040c772 --- /dev/null +++ b/mut/st/Xdefaults @@ -0,0 +1,128 @@ +!! Transparency (0-1): +st.alpha: 0.92 +st.alphaOffset: 0.3 + +!! Set a default font and font size as below: +st.font: Monospace-11; + +! st.termname: st-256color +! st.borderpx: 2 + +!! Set the background, foreground and cursor colors as below: + +!! gruvbox: +*.color0: #1d2021 +*.color1: #cc241d +*.color2: #98971a +*.color3: #d79921 +*.color4: #458588 +*.color5: #b16286 +*.color6: #689d6a +*.color7: #a89984 +*.color8: #928374 +*.color9: #fb4934 +*.color10: #b8bb26 +*.color11: #fabd2f +*.color12: #83a598 +*.color13: #d3869b +*.color14: #8ec07c +*.color15: #ebdbb2 +*.background: #282828 +*.foreground: white +*.cursorColor: white + +/* /1* !! gruvbox light: *1/ */ +/* *.color0: #fbf1c7 */ +/* *.color1: #cc241d */ +/* *.color2: #98971a */ +/* *.color3: #d79921 */ +/* *.color4: #458588 */ +/* *.color5: #b16286 */ +/* *.color6: #689d6a */ +/* *.color7: #7c6f64 */ +/* *.color8: #928374 */ +/* *.color9: #9d0006 */ +/* *.color10: #79740e */ +/* *.color11: #b57614 */ +/* *.color12: #076678 */ +/* *.color13: #8f3f71 */ +/* *.color14: #427b58 */ +/* *.color15: #3c3836 */ +/* *.background: #fbf1c7 */ +/* *.foreground: #282828 */ +/* *.cursorColor: #282828 */ + +/* !! brogrammer: */ +/* *.foreground: #d6dbe5 */ +/* *.background: #131313 */ +/* *.color0: #1f1f1f */ +/* *.color8: #d6dbe5 */ +/* *.color1: #f81118 */ +/* *.color9: #de352e */ +/* *.color2: #2dc55e */ +/* *.color10: #1dd361 */ +/* *.color3: #ecba0f */ +/* *.color11: #f3bd09 */ +/* *.color4: #2a84d2 */ +/* *.color12: #1081d6 */ +/* *.color5: #4e5ab7 */ +/* *.color13: #5350b9 */ +/* *.color6: #1081d6 */ +/* *.color14: #0f7ddb */ +/* *.color7: #d6dbe5 */ +/* *.color15: #ffffff */ +/* *.colorBD: #d6dbe5 */ + +/* ! base16 */ +/* *.color0: #181818 */ +/* *.color1: #ab4642 */ +/* *.color2: #a1b56c */ +/* *.color3: #f7ca88 */ +/* *.color4: #7cafc2 */ +/* *.color5: #ba8baf */ +/* *.color6: #86c1b9 */ +/* *.color7: #d8d8d8 */ +/* *.color8: #585858 */ +/* *.color9: #ab4642 */ +/* *.color10: #a1b56c */ +/* *.color11: #f7ca88 */ +/* *.color12: #7cafc2 */ +/* *.color13: #ba8baf */ +/* *.color14: #86c1b9 */ +/* *.color15: #f8f8f8 */ + +/* !! solarized */ +/* *.color0: #073642 */ +/* *.color1: #dc322f */ +/* *.color2: #859900 */ +/* *.color3: #b58900 */ +/* *.color4: #268bd2 */ +/* *.color5: #d33682 */ +/* *.color6: #2aa198 */ +/* *.color7: #eee8d5 */ +/* *.color9: #cb4b16 */ +/* *.color8: #fdf6e3 */ +/* *.color10: #586e75 */ +/* *.color11: #657b83 */ +/* *.color12: #839496 */ +/* *.color13: #6c71c4 */ +/* *.color14: #93a1a1 */ +/* *.color15: #fdf6e3 */ + +/* !! xterm */ +/* *.color0: #000000 */ +/* *.color1: #cd0000 */ +/* *.color2: #00cd00 */ +/* *.color3: #cdcd00 */ +/* *.color4: #0000cd */ +/* *.color5: #cd00cd */ +/* *.color6: #00cdcd */ +/* *.color7: #e5e5e5 */ +/* *.color8: #4d4d4d */ +/* *.color9: #ff0000 */ +/* *.color10: #00ff00 */ +/* *.color11: #ffff00 */ +/* *.color12: #0000ff */ +/* *.color13: #ff00ff */ +/* *.color14: #00ffff */ +/* *.color15: #aabac8 */ diff --git a/mut/st/arg.h b/mut/st/arg.h new file mode 100644 index 0000000..a22e019 --- /dev/null +++ b/mut/st/arg.h @@ -0,0 +1,50 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + int i_;\ + for (i_ = 1, brk_ = 0, argv_ = argv;\ + argv[0][i_] && !brk_;\ + i_++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][i_];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/mut/st/boxdraw.c b/mut/st/boxdraw.c new file mode 100644 index 0000000..28a92d0 --- /dev/null +++ b/mut/st/boxdraw.c @@ -0,0 +1,194 @@ +/* + * Copyright 2018 Avi Halachmi (:avih) avihpit@yahoo.com https://github.com/avih + * MIT/X Consortium License + */ + +#include <X11/Xft/Xft.h> +#include "st.h" +#include "boxdraw_data.h" + +/* Rounded non-negative integers division of n / d */ +#define DIV(n, d) (((n) + (d) / 2) / (d)) + +static Display *xdpy; +static Colormap xcmap; +static XftDraw *xd; +static Visual *xvis; + +static void drawbox(int, int, int, int, XftColor *, XftColor *, ushort); +static void drawboxlines(int, int, int, int, XftColor *, ushort); + +/* public API */ + +void +boxdraw_xinit(Display *dpy, Colormap cmap, XftDraw *draw, Visual *vis) +{ + xdpy = dpy; xcmap = cmap; xd = draw, xvis = vis; +} + +int +isboxdraw(Rune u) +{ + Rune block = u & ~0xff; + return (boxdraw && block == 0x2500 && boxdata[(uint8_t)u]) || + (boxdraw_braille && block == 0x2800); +} + +/* the "index" is actually the entire shape data encoded as ushort */ +ushort +boxdrawindex(const Glyph *g) +{ + if (boxdraw_braille && (g->u & ~0xff) == 0x2800) + return BRL | (uint8_t)g->u; + if (boxdraw_bold && (g->mode & ATTR_BOLD)) + return BDB | boxdata[(uint8_t)g->u]; + return boxdata[(uint8_t)g->u]; +} + +void +drawboxes(int x, int y, int cw, int ch, XftColor *fg, XftColor *bg, + const XftGlyphFontSpec *specs, int len) +{ + for ( ; len-- > 0; x += cw, specs++) + drawbox(x, y, cw, ch, fg, bg, (ushort)specs->glyph); +} + +/* implementation */ + +void +drawbox(int x, int y, int w, int h, XftColor *fg, XftColor *bg, ushort bd) +{ + ushort cat = bd & ~(BDB | 0xff); /* mask out bold and data */ + if (bd & (BDL | BDA)) { + /* lines (light/double/heavy/arcs) */ + drawboxlines(x, y, w, h, fg, bd); + + } else if (cat == BBD) { + /* lower (8-X)/8 block */ + int d = DIV((uint8_t)bd * h, 8); + XftDrawRect(xd, fg, x, y + d, w, h - d); + + } else if (cat == BBU) { + /* upper X/8 block */ + XftDrawRect(xd, fg, x, y, w, DIV((uint8_t)bd * h, 8)); + + } else if (cat == BBL) { + /* left X/8 block */ + XftDrawRect(xd, fg, x, y, DIV((uint8_t)bd * w, 8), h); + + } else if (cat == BBR) { + /* right (8-X)/8 block */ + int d = DIV((uint8_t)bd * w, 8); + XftDrawRect(xd, fg, x + d, y, w - d, h); + + } else if (cat == BBQ) { + /* Quadrants */ + int w2 = DIV(w, 2), h2 = DIV(h, 2); + if (bd & TL) + XftDrawRect(xd, fg, x, y, w2, h2); + if (bd & TR) + XftDrawRect(xd, fg, x + w2, y, w - w2, h2); + if (bd & BL) + XftDrawRect(xd, fg, x, y + h2, w2, h - h2); + if (bd & BR) + XftDrawRect(xd, fg, x + w2, y + h2, w - w2, h - h2); + + } else if (bd & BBS) { + /* Shades - data is 1/2/3 for 25%/50%/75% alpha, respectively */ + int d = (uint8_t)bd; + XftColor xfc; + XRenderColor xrc = { .alpha = 0xffff }; + + xrc.red = DIV(fg->color.red * d + bg->color.red * (4 - d), 4); + xrc.green = DIV(fg->color.green * d + bg->color.green * (4 - d), 4); + xrc.blue = DIV(fg->color.blue * d + bg->color.blue * (4 - d), 4); + + XftColorAllocValue(xdpy, xvis, xcmap, &xrc, &xfc); + XftDrawRect(xd, &xfc, x, y, w, h); + XftColorFree(xdpy, xvis, xcmap, &xfc); + + } else if (cat == BRL) { + /* braille, each data bit corresponds to one dot at 2x4 grid */ + int w1 = DIV(w, 2); + int h1 = DIV(h, 4), h2 = DIV(h, 2), h3 = DIV(3 * h, 4); + + if (bd & 1) XftDrawRect(xd, fg, x, y, w1, h1); + if (bd & 2) XftDrawRect(xd, fg, x, y + h1, w1, h2 - h1); + if (bd & 4) XftDrawRect(xd, fg, x, y + h2, w1, h3 - h2); + if (bd & 8) XftDrawRect(xd, fg, x + w1, y, w - w1, h1); + if (bd & 16) XftDrawRect(xd, fg, x + w1, y + h1, w - w1, h2 - h1); + if (bd & 32) XftDrawRect(xd, fg, x + w1, y + h2, w - w1, h3 - h2); + if (bd & 64) XftDrawRect(xd, fg, x, y + h3, w1, h - h3); + if (bd & 128) XftDrawRect(xd, fg, x + w1, y + h3, w - w1, h - h3); + + } +} + +void +drawboxlines(int x, int y, int w, int h, XftColor *fg, ushort bd) +{ + /* s: stem thickness. width/8 roughly matches underscore thickness. */ + /* We draw bold as 1.5 * normal-stem and at least 1px thicker. */ + /* doubles draw at least 3px, even when w or h < 3. bold needs 6px. */ + int mwh = MIN(w, h); + int base_s = MAX(1, DIV(mwh, 8)); + int bold = (bd & BDB) && mwh >= 6; /* possibly ignore boldness */ + int s = bold ? MAX(base_s + 1, DIV(3 * base_s, 2)) : base_s; + int w2 = DIV(w - s, 2), h2 = DIV(h - s, 2); + /* the s-by-s square (x + w2, y + h2, s, s) is the center texel. */ + /* The base length (per direction till edge) includes this square. */ + + int light = bd & (LL | LU | LR | LD); + int double_ = bd & (DL | DU | DR | DD); + + if (light) { + /* d: additional (negative) length to not-draw the center */ + /* texel - at arcs and avoid drawing inside (some) doubles */ + int arc = bd & BDA; + int multi_light = light & (light - 1); + int multi_double = double_ & (double_ - 1); + /* light crosses double only at DH+LV, DV+LH (ref. shapes) */ + int d = arc || (multi_double && !multi_light) ? -s : 0; + + if (bd & LL) + XftDrawRect(xd, fg, x, y + h2, w2 + s + d, s); + if (bd & LU) + XftDrawRect(xd, fg, x + w2, y, s, h2 + s + d); + if (bd & LR) + XftDrawRect(xd, fg, x + w2 - d, y + h2, w - w2 + d, s); + if (bd & LD) + XftDrawRect(xd, fg, x + w2, y + h2 - d, s, h - h2 + d); + } + + /* double lines - also align with light to form heavy when combined */ + if (double_) { + /* + * going clockwise, for each double-ray: p is additional length + * to the single-ray nearer to the previous direction, and n to + * the next. p and n adjust from the base length to lengths + * which consider other doubles - shorter to avoid intersections + * (p, n), or longer to draw the far-corner texel (n). + */ + int dl = bd & DL, du = bd & DU, dr = bd & DR, dd = bd & DD; + if (dl) { + int p = dd ? -s : 0, n = du ? -s : dd ? s : 0; + XftDrawRect(xd, fg, x, y + h2 + s, w2 + s + p, s); + XftDrawRect(xd, fg, x, y + h2 - s, w2 + s + n, s); + } + if (du) { + int p = dl ? -s : 0, n = dr ? -s : dl ? s : 0; + XftDrawRect(xd, fg, x + w2 - s, y, s, h2 + s + p); + XftDrawRect(xd, fg, x + w2 + s, y, s, h2 + s + n); + } + if (dr) { + int p = du ? -s : 0, n = dd ? -s : du ? s : 0; + XftDrawRect(xd, fg, x + w2 - p, y + h2 - s, w - w2 + p, s); + XftDrawRect(xd, fg, x + w2 - n, y + h2 + s, w - w2 + n, s); + } + if (dd) { + int p = dr ? -s : 0, n = dl ? -s : dr ? s : 0; + XftDrawRect(xd, fg, x + w2 + s, y + h2 - p, s, h - h2 + p); + XftDrawRect(xd, fg, x + w2 - s, y + h2 - n, s, h - h2 + n); + } + } +} diff --git a/mut/st/boxdraw_data.h b/mut/st/boxdraw_data.h new file mode 100644 index 0000000..7890500 --- /dev/null +++ b/mut/st/boxdraw_data.h @@ -0,0 +1,214 @@ +/* + * Copyright 2018 Avi Halachmi (:avih) avihpit@yahoo.com https://github.com/avih + * MIT/X Consortium License + */ + +/* + * U+25XX codepoints data + * + * References: + * http://www.unicode.org/charts/PDF/U2500.pdf + * http://www.unicode.org/charts/PDF/U2580.pdf + * + * Test page: + * https://github.com/GNOME/vte/blob/master/doc/boxes.txt + */ + +/* Each shape is encoded as 16-bits. Higher bits are category, lower are data */ +/* Categories (mutually exclusive except BDB): */ +/* For convenience, BDL/BDA/BBS/BDB are 1 bit each, the rest are enums */ +#define BDL (1<<8) /* Box Draw Lines (light/double/heavy) */ +#define BDA (1<<9) /* Box Draw Arc (light) */ + +#define BBD (1<<10) /* Box Block Down (lower) X/8 */ +#define BBL (2<<10) /* Box Block Left X/8 */ +#define BBU (3<<10) /* Box Block Upper X/8 */ +#define BBR (4<<10) /* Box Block Right X/8 */ +#define BBQ (5<<10) /* Box Block Quadrants */ +#define BRL (6<<10) /* Box Braille (data is lower byte of U28XX) */ + +#define BBS (1<<14) /* Box Block Shades */ +#define BDB (1<<15) /* Box Draw is Bold */ + +/* (BDL/BDA) Light/Double/Heavy x Left/Up/Right/Down/Horizontal/Vertical */ +/* Heavy is light+double (literally drawing light+double align to form heavy) */ +#define LL (1<<0) +#define LU (1<<1) +#define LR (1<<2) +#define LD (1<<3) +#define LH (LL+LR) +#define LV (LU+LD) + +#define DL (1<<4) +#define DU (1<<5) +#define DR (1<<6) +#define DD (1<<7) +#define DH (DL+DR) +#define DV (DU+DD) + +#define HL (LL+DL) +#define HU (LU+DU) +#define HR (LR+DR) +#define HD (LD+DD) +#define HH (HL+HR) +#define HV (HU+HD) + +/* (BBQ) Quadrants Top/Bottom x Left/Right */ +#define TL (1<<0) +#define TR (1<<1) +#define BL (1<<2) +#define BR (1<<3) + +/* Data for U+2500 - U+259F except dashes/diagonals */ +static const unsigned short boxdata[256] = { + /* light lines */ + [0x00] = BDL + LH, /* light horizontal */ + [0x02] = BDL + LV, /* light vertical */ + [0x0c] = BDL + LD + LR, /* light down and right */ + [0x10] = BDL + LD + LL, /* light down and left */ + [0x14] = BDL + LU + LR, /* light up and right */ + [0x18] = BDL + LU + LL, /* light up and left */ + [0x1c] = BDL + LV + LR, /* light vertical and right */ + [0x24] = BDL + LV + LL, /* light vertical and left */ + [0x2c] = BDL + LH + LD, /* light horizontal and down */ + [0x34] = BDL + LH + LU, /* light horizontal and up */ + [0x3c] = BDL + LV + LH, /* light vertical and horizontal */ + [0x74] = BDL + LL, /* light left */ + [0x75] = BDL + LU, /* light up */ + [0x76] = BDL + LR, /* light right */ + [0x77] = BDL + LD, /* light down */ + + /* heavy [+light] lines */ + [0x01] = BDL + HH, + [0x03] = BDL + HV, + [0x0d] = BDL + HR + LD, + [0x0e] = BDL + HD + LR, + [0x0f] = BDL + HD + HR, + [0x11] = BDL + HL + LD, + [0x12] = BDL + HD + LL, + [0x13] = BDL + HD + HL, + [0x15] = BDL + HR + LU, + [0x16] = BDL + HU + LR, + [0x17] = BDL + HU + HR, + [0x19] = BDL + HL + LU, + [0x1a] = BDL + HU + LL, + [0x1b] = BDL + HU + HL, + [0x1d] = BDL + HR + LV, + [0x1e] = BDL + HU + LD + LR, + [0x1f] = BDL + HD + LR + LU, + [0x20] = BDL + HV + LR, + [0x21] = BDL + HU + HR + LD, + [0x22] = BDL + HD + HR + LU, + [0x23] = BDL + HV + HR, + [0x25] = BDL + HL + LV, + [0x26] = BDL + HU + LD + LL, + [0x27] = BDL + HD + LU + LL, + [0x28] = BDL + HV + LL, + [0x29] = BDL + HU + HL + LD, + [0x2a] = BDL + HD + HL + LU, + [0x2b] = BDL + HV + HL, + [0x2d] = BDL + HL + LD + LR, + [0x2e] = BDL + HR + LL + LD, + [0x2f] = BDL + HH + LD, + [0x30] = BDL + HD + LH, + [0x31] = BDL + HD + HL + LR, + [0x32] = BDL + HR + HD + LL, + [0x33] = BDL + HH + HD, + [0x35] = BDL + HL + LU + LR, + [0x36] = BDL + HR + LU + LL, + [0x37] = BDL + HH + LU, + [0x38] = BDL + HU + LH, + [0x39] = BDL + HU + HL + LR, + [0x3a] = BDL + HU + HR + LL, + [0x3b] = BDL + HH + HU, + [0x3d] = BDL + HL + LV + LR, + [0x3e] = BDL + HR + LV + LL, + [0x3f] = BDL + HH + LV, + [0x40] = BDL + HU + LH + LD, + [0x41] = BDL + HD + LH + LU, + [0x42] = BDL + HV + LH, + [0x43] = BDL + HU + HL + LD + LR, + [0x44] = BDL + HU + HR + LD + LL, + [0x45] = BDL + HD + HL + LU + LR, + [0x46] = BDL + HD + HR + LU + LL, + [0x47] = BDL + HH + HU + LD, + [0x48] = BDL + HH + HD + LU, + [0x49] = BDL + HV + HL + LR, + [0x4a] = BDL + HV + HR + LL, + [0x4b] = BDL + HV + HH, + [0x78] = BDL + HL, + [0x79] = BDL + HU, + [0x7a] = BDL + HR, + [0x7b] = BDL + HD, + [0x7c] = BDL + HR + LL, + [0x7d] = BDL + HD + LU, + [0x7e] = BDL + HL + LR, + [0x7f] = BDL + HU + LD, + + /* double [+light] lines */ + [0x50] = BDL + DH, + [0x51] = BDL + DV, + [0x52] = BDL + DR + LD, + [0x53] = BDL + DD + LR, + [0x54] = BDL + DR + DD, + [0x55] = BDL + DL + LD, + [0x56] = BDL + DD + LL, + [0x57] = BDL + DL + DD, + [0x58] = BDL + DR + LU, + [0x59] = BDL + DU + LR, + [0x5a] = BDL + DU + DR, + [0x5b] = BDL + DL + LU, + [0x5c] = BDL + DU + LL, + [0x5d] = BDL + DL + DU, + [0x5e] = BDL + DR + LV, + [0x5f] = BDL + DV + LR, + [0x60] = BDL + DV + DR, + [0x61] = BDL + DL + LV, + [0x62] = BDL + DV + LL, + [0x63] = BDL + DV + DL, + [0x64] = BDL + DH + LD, + [0x65] = BDL + DD + LH, + [0x66] = BDL + DD + DH, + [0x67] = BDL + DH + LU, + [0x68] = BDL + DU + LH, + [0x69] = BDL + DH + DU, + [0x6a] = BDL + DH + LV, + [0x6b] = BDL + DV + LH, + [0x6c] = BDL + DH + DV, + + /* (light) arcs */ + [0x6d] = BDA + LD + LR, + [0x6e] = BDA + LD + LL, + [0x6f] = BDA + LU + LL, + [0x70] = BDA + LU + LR, + + /* Lower (Down) X/8 block (data is 8 - X) */ + [0x81] = BBD + 7, [0x82] = BBD + 6, [0x83] = BBD + 5, [0x84] = BBD + 4, + [0x85] = BBD + 3, [0x86] = BBD + 2, [0x87] = BBD + 1, [0x88] = BBD + 0, + + /* Left X/8 block (data is X) */ + [0x89] = BBL + 7, [0x8a] = BBL + 6, [0x8b] = BBL + 5, [0x8c] = BBL + 4, + [0x8d] = BBL + 3, [0x8e] = BBL + 2, [0x8f] = BBL + 1, + + /* upper 1/2 (4/8), 1/8 block (X), right 1/2, 1/8 block (8-X) */ + [0x80] = BBU + 4, [0x94] = BBU + 1, + [0x90] = BBR + 4, [0x95] = BBR + 7, + + /* Quadrants */ + [0x96] = BBQ + BL, + [0x97] = BBQ + BR, + [0x98] = BBQ + TL, + [0x99] = BBQ + TL + BL + BR, + [0x9a] = BBQ + TL + BR, + [0x9b] = BBQ + TL + TR + BL, + [0x9c] = BBQ + TL + TR + BR, + [0x9d] = BBQ + TR, + [0x9e] = BBQ + BL + TR, + [0x9f] = BBQ + BL + TR + BR, + + /* Shades, data is an alpha value in 25% units (1/4, 1/2, 3/4) */ + [0x91] = BBS + 1, [0x92] = BBS + 2, [0x93] = BBS + 3, + + /* U+2504 - U+250B, U+254C - U+254F: unsupported (dashes) */ + /* U+2571 - U+2573: unsupported (diagonals) */ +}; diff --git a/mut/st/config.h b/mut/st/config.h new file mode 100644 index 0000000..c390ed9 --- /dev/null +++ b/mut/st/config.h @@ -0,0 +1,559 @@ +/* See LICENSE file for copyright and license details. */ + +/* + * appearance + * + * font: see http://freedesktop.org/software/fontconfig/fontconfig-user.html + */ +static char *font = "JetBrainsMonoNLNerdFontMono:pixelsize=16:antialias=true:autohint=true"; +static char *font2[] = { "NotoColorEmoji:pixelsize=14:antialias=true:autohint=true" }; +static int borderpx = 2; + +/* + * What program is execed by st depends of these precedence rules: + * 1: program passed with -e + * 2: scroll and/or utmp + * 3: SHELL environment variable + * 4: value of shell in /etc/passwd + * 5: value of shell in config.h + */ +static char *shell = "/bin/sh"; +char *utmp = NULL; +/* scroll program: to enable use a string like "scroll" */ +char *scroll = NULL; +char *stty_args = "stty raw pass8 nl -echo -iexten -cstopb 38400"; + +/* identification sequence returned in DA and DECID */ +char *vtiden = "\033[?6c"; + +/* Kerning / character bounding-box multipliers */ +static float cwscale = 1.0; +static float chscale = 1.0; + +/* + * word delimiter string + * + * More advanced example: L" `'\"()[]{}" + */ +wchar_t *worddelimiters = L" "; + +/* selection timeouts (in milliseconds) */ +static unsigned int doubleclicktimeout = 300; +static unsigned int tripleclicktimeout = 600; + +/* alt screens */ +int allowaltscreen = 1; + +/* allow certain non-interactive (insecure) window operations such as: + setting the clipboard text */ +int allowwindowops = 0; + +/* + * draw latency range in ms - from new content/keypress/etc until drawing. + * within this range, st draws when content stops arriving (idle). mostly it's + * near minlatency, but it waits longer for slow updates to avoid partial draw. + * low minlatency will tear/flicker more, as it can "detect" idle too early. + */ +static double minlatency = 8; +static double maxlatency = 33; + +/* + * blinking timeout (set to 0 to disable blinking) for the terminal blinking + * attribute. + */ +static unsigned int blinktimeout = 800; + +/* + * thickness of underline and bar cursors + */ +static unsigned int cursorthickness = 2; + +/* + * 1: render most of the lines/blocks characters without using the font for + * perfect alignment between cells (U2500 - U259F except dashes/diagonals). + * Bold affects lines thickness if boxdraw_bold is not 0. Italic is ignored. + * 0: disable (render all U25XX glyphs normally from the font). + */ +const int boxdraw = 1; +const int boxdraw_bold = 0; + +/* braille (U28XX): 1: render as adjacent "pixels", 0: use font */ +const int boxdraw_braille = 0; + +/* + * bell volume. It must be a value between -100 and 100. Use 0 for disabling + * it + */ +static int bellvolume = 0; + +/* default TERM value */ +char *termname = "st-256color"; + +/* + * spaces per tab + * + * When you are changing this value, don't forget to adapt the »it« value in + * the st.info and appropriately install the st.info in the environment where + * you use this st version. + * + * it#$tabspaces, + * + * Secondly make sure your kernel is not expanding tabs. When running `stty + * -a` »tab0« should appear. You can tell the terminal to not expand tabs by + * running following command: + * + * stty tabs + */ +unsigned int tabspaces = 8; + +/* bg opacity */ +float alpha = 1.0; +float alphaOffset = 0.0; +float alphaUnfocus; + +/* Terminal colors (16 first used in escape sequence) */ +static const char *colorname[] = { + "#282828", /* hard contrast: #1d2021 / soft contrast: #32302f */ + "#cc241d", + "#98971a", + "#d79921", + "#458588", + "#b16286", + "#689d6a", + "#a89984", + "#928374", + "#fb4934", + "#b8bb26", + "#fabd2f", + "#83a598", + "#d3869b", + "#8ec07c", + "#ebdbb2", + [255] = 0, + /* more colors can be added after 255 to use with DefaultXX */ + "#add8e6", /* 256 -> cursor */ + "#555555", /* 257 -> rev cursor*/ + "#282828", /* 258 -> bg */ + "#ebdbb2", /* 259 -> fg */ +}; + + +/* + * Default colors (colorname index) + * foreground, background, cursor, reverse cursor + */ +unsigned int defaultfg = 259; +unsigned int defaultbg = 258; +unsigned int defaultcs = 256; +unsigned int defaultrcs = 257; +unsigned int background = 258; + +/* + * Default shape of cursor + * 2: Block ("█") + * 4: Underline ("_") + * 6: Bar ("|") + * 7: Snowman ("☃") + */ +static unsigned int cursorshape = 2; + +/* + * Default columns and rows numbers + */ + +static unsigned int cols = 80; +static unsigned int rows = 24; + +/* + * Default colour and shape of the mouse cursor + */ +static unsigned int mouseshape = XC_xterm; +static unsigned int mousefg = 7; +static unsigned int mousebg = 0; + +/* + * Color used to display font attributes when fontconfig selected a font which + * doesn't match the ones requested. + */ +static unsigned int defaultattr = 11; + +/* + * Force mouse select/shortcuts while mask is active (when MODE_MOUSE is set). + * Note that if you want to use ShiftMask with selmasks, set this to an other + * modifier, set to 0 to not use it. + */ +static uint forcemousemod = ShiftMask; + +/* + * Xresources preferences to load at startup + */ +ResourcePref resources[] = { + { "font", STRING, &font }, + { "fontalt0", STRING, &font2[0] }, + { "color0", STRING, &colorname[0] }, + { "color1", STRING, &colorname[1] }, + { "color2", STRING, &colorname[2] }, + { "color3", STRING, &colorname[3] }, + { "color4", STRING, &colorname[4] }, + { "color5", STRING, &colorname[5] }, + { "color6", STRING, &colorname[6] }, + { "color7", STRING, &colorname[7] }, + { "color8", STRING, &colorname[8] }, + { "color9", STRING, &colorname[9] }, + { "color10", STRING, &colorname[10] }, + { "color11", STRING, &colorname[11] }, + { "color12", STRING, &colorname[12] }, + { "color13", STRING, &colorname[13] }, + { "color14", STRING, &colorname[14] }, + { "color15", STRING, &colorname[15] }, + { "background", STRING, &colorname[258] }, + { "foreground", STRING, &colorname[259] }, + { "cursorColor", STRING, &colorname[256] }, + { "termname", STRING, &termname }, + { "shell", STRING, &shell }, + { "minlatency", INTEGER, &minlatency }, + { "maxlatency", INTEGER, &maxlatency }, + { "blinktimeout", INTEGER, &blinktimeout }, + { "bellvolume", INTEGER, &bellvolume }, + { "tabspaces", INTEGER, &tabspaces }, + { "borderpx", INTEGER, &borderpx }, + { "cwscale", FLOAT, &cwscale }, + { "chscale", FLOAT, &chscale }, + { "alpha", FLOAT, &alpha }, + { "alphaOffset", FLOAT, &alphaOffset }, +}; + +/* + * Internal mouse shortcuts. + * Beware that overloading Button1 will disable the selection. + */ +static MouseShortcut mshortcuts[] = { + /* mask button function argument release */ + { XK_NO_MOD, Button4, kscrollup, {.i = 1} }, + { XK_NO_MOD, Button5, kscrolldown, {.i = 1} }, + { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 }, + { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} }, + { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} }, + { ShiftMask, Button5, ttysend, {.s = "\033[6;2~"} }, + { XK_ANY_MOD, Button5, ttysend, {.s = "\005"} }, +}; + +/* Internal keyboard shortcuts. */ +#define MODKEY Mod1Mask +#define TERMMOD (Mod1Mask|ShiftMask) + +static char *openurlcmd[] = { "/bin/sh", "-c", "st-urlhandler -o", "externalpipe", NULL }; +static char *copyurlcmd[] = { "/bin/sh", "-c", "st-urlhandler -c", "externalpipe", NULL }; +static char *copyoutput[] = { "/bin/sh", "-c", "st-copyout", "externalpipe", NULL }; + +static Shortcut shortcuts[] = { + /* mask keysym function argument */ + { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, + { ControlMask, XK_Print, toggleprinter, {.i = 0} }, + { ShiftMask, XK_Print, printscreen, {.i = 0} }, + { XK_ANY_MOD, XK_Print, printsel, {.i = 0} }, + { TERMMOD, XK_Prior, zoom, {.f = +1} }, + { TERMMOD, XK_Next, zoom, {.f = -1} }, + { TERMMOD, XK_Home, zoomreset, {.f = 0} }, + { TERMMOD, XK_C, clipcopy, {.i = 0} }, + { TERMMOD, XK_V, clippaste, {.i = 0} }, + { MODKEY, XK_c, clipcopy, {.i = 0} }, + { ShiftMask, XK_Insert, clippaste, {.i = 0} }, + { MODKEY, XK_v, clippaste, {.i = 0} }, + { ShiftMask, XK_Insert, selpaste, {.i = 0} }, + { TERMMOD, XK_Num_Lock, numlock, {.i = 0} }, + { ShiftMask, XK_Page_Up, kscrollup, {.i = -1} }, + { ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} }, + { MODKEY, XK_Page_Up, kscrollup, {.i = -1} }, + { MODKEY, XK_Page_Down, kscrolldown, {.i = -1} }, + { MODKEY, XK_k, kscrollup, {.i = 1} }, + { MODKEY, XK_j, kscrolldown, {.i = 1} }, + { MODKEY, XK_Up, kscrollup, {.i = 1} }, + { MODKEY, XK_Down, kscrolldown, {.i = 1} }, + // { MODKEY, XK_u, kscrollup, {.i = -1} }, + // { MODKEY, XK_d, kscrolldown, {.i = -1} }, + { MODKEY, XK_s, changealpha, {.f = -0.05} }, + { MODKEY, XK_a, changealpha, {.f = +0.05} }, + { TERMMOD, XK_Up, zoom, {.f = +1} }, + { TERMMOD, XK_Down, zoom, {.f = -1} }, + { TERMMOD, XK_K, zoom, {.f = +1} }, + { TERMMOD, XK_J, zoom, {.f = -1} }, + // { TERMMOD, XK_U, zoom, {.f = +2} }, + // { TERMMOD, XK_D, zoom, {.f = -2} }, + { TERMMOD, XK_U, kscrollup, {.i = -1} }, + { TERMMOD, XK_D, kscrolldown, {.i = -1} }, + { MODKEY, XK_l, externalpipe, {.v = openurlcmd } }, + { MODKEY, XK_y, externalpipe, {.v = copyurlcmd } }, + { MODKEY, XK_o, externalpipe, {.v = copyoutput } }, +}; + +/* + * Special keys (change & recompile st.info accordingly) + * + * Mask value: + * * Use XK_ANY_MOD to match the key no matter modifiers state + * * Use XK_NO_MOD to match the key alone (no modifiers) + * appkey value: + * * 0: no value + * * > 0: keypad application mode enabled + * * = 2: term.numlock = 1 + * * < 0: keypad application mode disabled + * appcursor value: + * * 0: no value + * * > 0: cursor application mode enabled + * * < 0: cursor application mode disabled + * + * Be careful with the order of the definitions because st searches in + * this table sequentially, so any XK_ANY_MOD must be in the last + * position for a key. + */ + +/* + * If you want keys other than the X11 function keys (0xFD00 - 0xFFFF) + * to be mapped below, add them to this array. + */ +static KeySym mappedkeys[] = { -1 }; + +/* + * State bits to ignore when matching key or button events. By default, + * numlock (Mod2Mask) and keyboard layout (XK_SWITCH_MOD) are ignored. + */ +static uint ignoremod = Mod2Mask|XK_SWITCH_MOD; + +/* + * This is the huge key array which defines all compatibility to the Linux + * world. Please decide about changes wisely. + */ +static Key key[] = { + /* keysym mask string appkey appcursor */ + { XK_KP_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_KP_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_KP_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_KP_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_KP_Up, XK_ANY_MOD, "\033Ox", +1, 0}, + { XK_KP_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_KP_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_KP_Down, XK_ANY_MOD, "\033Or", +1, 0}, + { XK_KP_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_KP_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_KP_Left, XK_ANY_MOD, "\033Ot", +1, 0}, + { XK_KP_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_KP_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_KP_Right, XK_ANY_MOD, "\033Ov", +1, 0}, + { XK_KP_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_KP_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_KP_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_KP_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_KP_Begin, XK_ANY_MOD, "\033[E", 0, 0}, + { XK_KP_End, ControlMask, "\033[J", -1, 0}, + { XK_KP_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_KP_End, ShiftMask, "\033[K", -1, 0}, + { XK_KP_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_KP_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_KP_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_KP_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_KP_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_KP_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[L", -1, 0}, + { XK_KP_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_KP_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_KP_Delete, ControlMask, "\033[M", -1, 0}, + { XK_KP_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_KP_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_KP_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_KP_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_KP_Multiply, XK_ANY_MOD, "\033Oj", +2, 0}, + { XK_KP_Add, XK_ANY_MOD, "\033Ok", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\033OM", +2, 0}, + { XK_KP_Enter, XK_ANY_MOD, "\r", -1, 0}, + { XK_KP_Subtract, XK_ANY_MOD, "\033Om", +2, 0}, + { XK_KP_Decimal, XK_ANY_MOD, "\033On", +2, 0}, + { XK_KP_Divide, XK_ANY_MOD, "\033Oo", +2, 0}, + { XK_KP_0, XK_ANY_MOD, "\033Op", +2, 0}, + { XK_KP_1, XK_ANY_MOD, "\033Oq", +2, 0}, + { XK_KP_2, XK_ANY_MOD, "\033Or", +2, 0}, + { XK_KP_3, XK_ANY_MOD, "\033Os", +2, 0}, + { XK_KP_4, XK_ANY_MOD, "\033Ot", +2, 0}, + { XK_KP_5, XK_ANY_MOD, "\033Ou", +2, 0}, + { XK_KP_6, XK_ANY_MOD, "\033Ov", +2, 0}, + { XK_KP_7, XK_ANY_MOD, "\033Ow", +2, 0}, + { XK_KP_8, XK_ANY_MOD, "\033Ox", +2, 0}, + { XK_KP_9, XK_ANY_MOD, "\033Oy", +2, 0}, + { XK_Up, ShiftMask, "\033[1;2A", 0, 0}, + { XK_Up, Mod1Mask, "\033[1;3A", 0, 0}, + { XK_Up, ShiftMask|Mod1Mask,"\033[1;4A", 0, 0}, + { XK_Up, ControlMask, "\033[1;5A", 0, 0}, + { XK_Up, ShiftMask|ControlMask,"\033[1;6A", 0, 0}, + { XK_Up, ControlMask|Mod1Mask,"\033[1;7A", 0, 0}, + { XK_Up,ShiftMask|ControlMask|Mod1Mask,"\033[1;8A", 0, 0}, + { XK_Up, XK_ANY_MOD, "\033[A", 0, -1}, + { XK_Up, XK_ANY_MOD, "\033OA", 0, +1}, + { XK_Down, ShiftMask, "\033[1;2B", 0, 0}, + { XK_Down, Mod1Mask, "\033[1;3B", 0, 0}, + { XK_Down, ShiftMask|Mod1Mask,"\033[1;4B", 0, 0}, + { XK_Down, ControlMask, "\033[1;5B", 0, 0}, + { XK_Down, ShiftMask|ControlMask,"\033[1;6B", 0, 0}, + { XK_Down, ControlMask|Mod1Mask,"\033[1;7B", 0, 0}, + { XK_Down,ShiftMask|ControlMask|Mod1Mask,"\033[1;8B",0, 0}, + { XK_Down, XK_ANY_MOD, "\033[B", 0, -1}, + { XK_Down, XK_ANY_MOD, "\033OB", 0, +1}, + { XK_Left, ShiftMask, "\033[1;2D", 0, 0}, + { XK_Left, Mod1Mask, "\033[1;3D", 0, 0}, + { XK_Left, ShiftMask|Mod1Mask,"\033[1;4D", 0, 0}, + { XK_Left, ControlMask, "\033[1;5D", 0, 0}, + { XK_Left, ShiftMask|ControlMask,"\033[1;6D", 0, 0}, + { XK_Left, ControlMask|Mod1Mask,"\033[1;7D", 0, 0}, + { XK_Left,ShiftMask|ControlMask|Mod1Mask,"\033[1;8D",0, 0}, + { XK_Left, XK_ANY_MOD, "\033[D", 0, -1}, + { XK_Left, XK_ANY_MOD, "\033OD", 0, +1}, + { XK_Right, ShiftMask, "\033[1;2C", 0, 0}, + { XK_Right, Mod1Mask, "\033[1;3C", 0, 0}, + { XK_Right, ShiftMask|Mod1Mask,"\033[1;4C", 0, 0}, + { XK_Right, ControlMask, "\033[1;5C", 0, 0}, + { XK_Right, ShiftMask|ControlMask,"\033[1;6C", 0, 0}, + { XK_Right, ControlMask|Mod1Mask,"\033[1;7C", 0, 0}, + { XK_Right,ShiftMask|ControlMask|Mod1Mask,"\033[1;8C",0, 0}, + { XK_Right, XK_ANY_MOD, "\033[C", 0, -1}, + { XK_Right, XK_ANY_MOD, "\033OC", 0, +1}, + { XK_ISO_Left_Tab, ShiftMask, "\033[Z", 0, 0}, + { XK_Return, Mod1Mask, "\033\r", 0, 0}, + { XK_Return, XK_ANY_MOD, "\r", 0, 0}, + { XK_Insert, ShiftMask, "\033[4l", -1, 0}, + { XK_Insert, ShiftMask, "\033[2;2~", +1, 0}, + { XK_Insert, ControlMask, "\033[L", -1, 0}, + { XK_Insert, ControlMask, "\033[2;5~", +1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[4h", -1, 0}, + { XK_Insert, XK_ANY_MOD, "\033[2~", +1, 0}, + { XK_Delete, ControlMask, "\033[M", -1, 0}, + { XK_Delete, ControlMask, "\033[3;5~", +1, 0}, + { XK_Delete, ShiftMask, "\033[2K", -1, 0}, + { XK_Delete, ShiftMask, "\033[3;2~", +1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[P", -1, 0}, + { XK_Delete, XK_ANY_MOD, "\033[3~", +1, 0}, + { XK_BackSpace, XK_NO_MOD, "\177", 0, 0}, + { XK_BackSpace, Mod1Mask, "\033\177", 0, 0}, + { XK_Home, ShiftMask, "\033[2J", 0, -1}, + { XK_Home, ShiftMask, "\033[1;2H", 0, +1}, + { XK_Home, XK_ANY_MOD, "\033[H", 0, -1}, + { XK_Home, XK_ANY_MOD, "\033[1~", 0, +1}, + { XK_End, ControlMask, "\033[J", -1, 0}, + { XK_End, ControlMask, "\033[1;5F", +1, 0}, + { XK_End, ShiftMask, "\033[K", -1, 0}, + { XK_End, ShiftMask, "\033[1;2F", +1, 0}, + { XK_End, XK_ANY_MOD, "\033[4~", 0, 0}, + { XK_Prior, ControlMask, "\033[5;5~", 0, 0}, + { XK_Prior, ShiftMask, "\033[5;2~", 0, 0}, + { XK_Prior, XK_ANY_MOD, "\033[5~", 0, 0}, + { XK_Next, ControlMask, "\033[6;5~", 0, 0}, + { XK_Next, ShiftMask, "\033[6;2~", 0, 0}, + { XK_Next, XK_ANY_MOD, "\033[6~", 0, 0}, + { XK_F1, XK_NO_MOD, "\033OP" , 0, 0}, + { XK_F1, /* F13 */ ShiftMask, "\033[1;2P", 0, 0}, + { XK_F1, /* F25 */ ControlMask, "\033[1;5P", 0, 0}, + { XK_F1, /* F37 */ Mod4Mask, "\033[1;6P", 0, 0}, + { XK_F1, /* F49 */ Mod1Mask, "\033[1;3P", 0, 0}, + { XK_F1, /* F61 */ Mod3Mask, "\033[1;4P", 0, 0}, + { XK_F2, XK_NO_MOD, "\033OQ" , 0, 0}, + { XK_F2, /* F14 */ ShiftMask, "\033[1;2Q", 0, 0}, + { XK_F2, /* F26 */ ControlMask, "\033[1;5Q", 0, 0}, + { XK_F2, /* F38 */ Mod4Mask, "\033[1;6Q", 0, 0}, + { XK_F2, /* F50 */ Mod1Mask, "\033[1;3Q", 0, 0}, + { XK_F2, /* F62 */ Mod3Mask, "\033[1;4Q", 0, 0}, + { XK_F3, XK_NO_MOD, "\033OR" , 0, 0}, + { XK_F3, /* F15 */ ShiftMask, "\033[1;2R", 0, 0}, + { XK_F3, /* F27 */ ControlMask, "\033[1;5R", 0, 0}, + { XK_F3, /* F39 */ Mod4Mask, "\033[1;6R", 0, 0}, + { XK_F3, /* F51 */ Mod1Mask, "\033[1;3R", 0, 0}, + { XK_F3, /* F63 */ Mod3Mask, "\033[1;4R", 0, 0}, + { XK_F4, XK_NO_MOD, "\033OS" , 0, 0}, + { XK_F4, /* F16 */ ShiftMask, "\033[1;2S", 0, 0}, + { XK_F4, /* F28 */ ControlMask, "\033[1;5S", 0, 0}, + { XK_F4, /* F40 */ Mod4Mask, "\033[1;6S", 0, 0}, + { XK_F4, /* F52 */ Mod1Mask, "\033[1;3S", 0, 0}, + { XK_F5, XK_NO_MOD, "\033[15~", 0, 0}, + { XK_F5, /* F17 */ ShiftMask, "\033[15;2~", 0, 0}, + { XK_F5, /* F29 */ ControlMask, "\033[15;5~", 0, 0}, + { XK_F5, /* F41 */ Mod4Mask, "\033[15;6~", 0, 0}, + { XK_F5, /* F53 */ Mod1Mask, "\033[15;3~", 0, 0}, + { XK_F6, XK_NO_MOD, "\033[17~", 0, 0}, + { XK_F6, /* F18 */ ShiftMask, "\033[17;2~", 0, 0}, + { XK_F6, /* F30 */ ControlMask, "\033[17;5~", 0, 0}, + { XK_F6, /* F42 */ Mod4Mask, "\033[17;6~", 0, 0}, + { XK_F6, /* F54 */ Mod1Mask, "\033[17;3~", 0, 0}, + { XK_F7, XK_NO_MOD, "\033[18~", 0, 0}, + { XK_F7, /* F19 */ ShiftMask, "\033[18;2~", 0, 0}, + { XK_F7, /* F31 */ ControlMask, "\033[18;5~", 0, 0}, + { XK_F7, /* F43 */ Mod4Mask, "\033[18;6~", 0, 0}, + { XK_F7, /* F55 */ Mod1Mask, "\033[18;3~", 0, 0}, + { XK_F8, XK_NO_MOD, "\033[19~", 0, 0}, + { XK_F8, /* F20 */ ShiftMask, "\033[19;2~", 0, 0}, + { XK_F8, /* F32 */ ControlMask, "\033[19;5~", 0, 0}, + { XK_F8, /* F44 */ Mod4Mask, "\033[19;6~", 0, 0}, + { XK_F8, /* F56 */ Mod1Mask, "\033[19;3~", 0, 0}, + { XK_F9, XK_NO_MOD, "\033[20~", 0, 0}, + { XK_F9, /* F21 */ ShiftMask, "\033[20;2~", 0, 0}, + { XK_F9, /* F33 */ ControlMask, "\033[20;5~", 0, 0}, + { XK_F9, /* F45 */ Mod4Mask, "\033[20;6~", 0, 0}, + { XK_F9, /* F57 */ Mod1Mask, "\033[20;3~", 0, 0}, + { XK_F10, XK_NO_MOD, "\033[21~", 0, 0}, + { XK_F10, /* F22 */ ShiftMask, "\033[21;2~", 0, 0}, + { XK_F10, /* F34 */ ControlMask, "\033[21;5~", 0, 0}, + { XK_F10, /* F46 */ Mod4Mask, "\033[21;6~", 0, 0}, + { XK_F10, /* F58 */ Mod1Mask, "\033[21;3~", 0, 0}, + { XK_F11, XK_NO_MOD, "\033[23~", 0, 0}, + { XK_F11, /* F23 */ ShiftMask, "\033[23;2~", 0, 0}, + { XK_F11, /* F35 */ ControlMask, "\033[23;5~", 0, 0}, + { XK_F11, /* F47 */ Mod4Mask, "\033[23;6~", 0, 0}, + { XK_F11, /* F59 */ Mod1Mask, "\033[23;3~", 0, 0}, + { XK_F12, XK_NO_MOD, "\033[24~", 0, 0}, + { XK_F12, /* F24 */ ShiftMask, "\033[24;2~", 0, 0}, + { XK_F12, /* F36 */ ControlMask, "\033[24;5~", 0, 0}, + { XK_F12, /* F48 */ Mod4Mask, "\033[24;6~", 0, 0}, + { XK_F12, /* F60 */ Mod1Mask, "\033[24;3~", 0, 0}, + { XK_F13, XK_NO_MOD, "\033[1;2P", 0, 0}, + { XK_F14, XK_NO_MOD, "\033[1;2Q", 0, 0}, + { XK_F15, XK_NO_MOD, "\033[1;2R", 0, 0}, + { XK_F16, XK_NO_MOD, "\033[1;2S", 0, 0}, + { XK_F17, XK_NO_MOD, "\033[15;2~", 0, 0}, + { XK_F18, XK_NO_MOD, "\033[17;2~", 0, 0}, + { XK_F19, XK_NO_MOD, "\033[18;2~", 0, 0}, + { XK_F20, XK_NO_MOD, "\033[19;2~", 0, 0}, + { XK_F21, XK_NO_MOD, "\033[20;2~", 0, 0}, + { XK_F22, XK_NO_MOD, "\033[21;2~", 0, 0}, + { XK_F23, XK_NO_MOD, "\033[23;2~", 0, 0}, + { XK_F24, XK_NO_MOD, "\033[24;2~", 0, 0}, + { XK_F25, XK_NO_MOD, "\033[1;5P", 0, 0}, + { XK_F26, XK_NO_MOD, "\033[1;5Q", 0, 0}, + { XK_F27, XK_NO_MOD, "\033[1;5R", 0, 0}, + { XK_F28, XK_NO_MOD, "\033[1;5S", 0, 0}, + { XK_F29, XK_NO_MOD, "\033[15;5~", 0, 0}, + { XK_F30, XK_NO_MOD, "\033[17;5~", 0, 0}, + { XK_F31, XK_NO_MOD, "\033[18;5~", 0, 0}, + { XK_F32, XK_NO_MOD, "\033[19;5~", 0, 0}, + { XK_F33, XK_NO_MOD, "\033[20;5~", 0, 0}, + { XK_F34, XK_NO_MOD, "\033[21;5~", 0, 0}, + { XK_F35, XK_NO_MOD, "\033[23;5~", 0, 0}, +}; + +/* + * Selection types' masks. + * Use the same masks as usual. + * Button1Mask is always unset, to make masks match between ButtonPress. + * ButtonRelease and MotionNotify. + * If no match is found, regular selection is used. + */ +static uint selmasks[] = { + [SEL_RECTANGULAR] = Mod1Mask, +}; + +/* + * Printable characters in ASCII, used to estimate the advance width + * of single wide characters. + */ +static char ascii_printable[] = + " !\"#$%&'()*+,-./0123456789:;<=>?" + "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" + "`abcdefghijklmnopqrstuvwxyz{|}~"; + diff --git a/mut/st/config.mk b/mut/st/config.mk new file mode 100644 index 0000000..ef6de39 --- /dev/null +++ b/mut/st/config.mk @@ -0,0 +1,37 @@ +# st version +VERSION = 0.8.5 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +PKG_CONFIG = pkg-config + +# includes and libs +INCS = -I$(X11INC) \ + `$(PKG_CONFIG) --cflags fontconfig` \ + `$(PKG_CONFIG) --cflags freetype2` \ + `$(PKG_CONFIG) --cflags harfbuzz` +LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft -lXrender\ + `$(PKG_CONFIG) --libs fontconfig` \ + `$(PKG_CONFIG) --libs freetype2` \ + `$(PKG_CONFIG) --libs harfbuzz` + +# flags +STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 +STCFLAGS = $(INCS) $(STCPPFLAGS) $(CPPFLAGS) $(CFLAGS) +STLDFLAGS = $(LIBS) $(LDFLAGS) + +# OpenBSD: +#CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 -D_BSD_SOURCE +#LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \ +# `$(PKG_CONFIG) --libs fontconfig` \ +# `$(PKG_CONFIG) --libs freetype2` + +# compiler and linker +# CC = c99 diff --git a/mut/st/hb.c b/mut/st/hb.c new file mode 100644 index 0000000..8000afa --- /dev/null +++ b/mut/st/hb.c @@ -0,0 +1,154 @@ +#include <stdlib.h> +#include <stdio.h> +#include <math.h> +#include <X11/Xft/Xft.h> +#include <X11/cursorfont.h> +#include <hb.h> +#include <hb-ft.h> + +#include "st.h" + +#define FEATURE(c1,c2,c3,c4) { .tag = HB_TAG(c1,c2,c3,c4), .value = 1, .start = HB_FEATURE_GLOBAL_START, .end = HB_FEATURE_GLOBAL_END } + +/* + * Replace 0 with a list of font features, wrapped in FEATURE macro, e.g. + * FEATURE('c', 'a', 'l', 't'), FEATURE('d', 'l', 'i', 'g') + * + * Uncomment either one of the 2 lines below. Uncomment the prior to disable (any) font features. Uncomment the + * latter to enable the (selected) font features. + */ + +hb_feature_t features[] = { 0 }; +//hb_feature_t features[] = { FEATURE('s','s','0','1'), FEATURE('s','s','0','2'), FEATURE('s','s','0','3'), FEATURE('s','s','0','5'), FEATURE('s','s','0','6'), FEATURE('s','s','0','7'), FEATURE('s','s','0','8'), FEATURE('z','e','r','o') }; + +void hbtransformsegment(XftFont *xfont, const Glyph *string, hb_codepoint_t *codepoints, int start, int length); +hb_font_t *hbfindfont(XftFont *match); + +typedef struct { + XftFont *match; + hb_font_t *font; +} HbFontMatch; + +static int hbfontslen = 0; +static HbFontMatch *hbfontcache = NULL; + +void +hbunloadfonts() +{ + for (int i = 0; i < hbfontslen; i++) { + hb_font_destroy(hbfontcache[i].font); + XftUnlockFace(hbfontcache[i].match); + } + + if (hbfontcache != NULL) { + free(hbfontcache); + hbfontcache = NULL; + } + hbfontslen = 0; +} + +hb_font_t * +hbfindfont(XftFont *match) +{ + for (int i = 0; i < hbfontslen; i++) { + if (hbfontcache[i].match == match) + return hbfontcache[i].font; + } + + /* Font not found in cache, caching it now. */ + hbfontcache = realloc(hbfontcache, sizeof(HbFontMatch) * (hbfontslen + 1)); + FT_Face face = XftLockFace(match); + hb_font_t *font = hb_ft_font_create(face, NULL); + if (font == NULL) + die("Failed to load Harfbuzz font."); + + hbfontcache[hbfontslen].match = match; + hbfontcache[hbfontslen].font = font; + hbfontslen += 1; + + return font; +} + +void +hbtransform(XftGlyphFontSpec *specs, const Glyph *glyphs, size_t len, int x, int y) +{ + int start = 0, length = 1, gstart = 0; + hb_codepoint_t *codepoints = calloc((unsigned int)len, sizeof(hb_codepoint_t)); + + for (int idx = 1, specidx = 1; idx < len; idx++) { + if (glyphs[idx].mode & ATTR_WDUMMY) { + length += 1; + continue; + } + + if (specs[specidx].font != specs[start].font || ATTRCMP(glyphs[gstart], glyphs[idx]) || selected(x + idx, y) != selected(x + gstart, y)) { + hbtransformsegment(specs[start].font, glyphs, codepoints, gstart, length); + + /* Reset the sequence. */ + length = 1; + start = specidx; + gstart = idx; + } else { + length += 1; + } + + specidx++; + } + + /* EOL. */ + hbtransformsegment(specs[start].font, glyphs, codepoints, gstart, length); + + /* Apply the transformation to glyph specs. */ + for (int i = 0, specidx = 0; i < len; i++) { + if (glyphs[i].mode & ATTR_WDUMMY) + continue; + if (glyphs[i].mode & ATTR_BOXDRAW) { + specidx++; + continue; + } + + if (codepoints[i] != specs[specidx].glyph) + ((Glyph *)glyphs)[i].mode |= ATTR_LIGA; + + specs[specidx++].glyph = codepoints[i]; + } + + free(codepoints); +} + +void +hbtransformsegment(XftFont *xfont, const Glyph *string, hb_codepoint_t *codepoints, int start, int length) +{ + hb_font_t *font = hbfindfont(xfont); + if (font == NULL) + return; + + Rune rune; + ushort mode = USHRT_MAX; + hb_buffer_t *buffer = hb_buffer_create(); + hb_buffer_set_direction(buffer, HB_DIRECTION_LTR); + + /* Fill buffer with codepoints. */ + for (int i = start; i < (start+length); i++) { + rune = string[i].u; + mode = string[i].mode; + if (mode & ATTR_WDUMMY) + rune = 0x0020; + hb_buffer_add_codepoints(buffer, &rune, 1, 0, 1); + } + + /* Shape the segment. */ + hb_shape(font, buffer, features, sizeof(features)); + + /* Get new glyph info. */ + hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, NULL); + + /* Write new codepoints. */ + for (int i = 0; i < length; i++) { + hb_codepoint_t gid = info[i].codepoint; + codepoints[start+i] = gid; + } + + /* Cleanup. */ + hb_buffer_destroy(buffer); +} diff --git a/mut/st/hb.h b/mut/st/hb.h new file mode 100644 index 0000000..b3e02d0 --- /dev/null +++ b/mut/st/hb.h @@ -0,0 +1,7 @@ +#include <X11/Xft/Xft.h> +#include <hb.h> +#include <hb-ft.h> + +void hbunloadfonts(); +void hbtransform(XftGlyphFontSpec *, const Glyph *, size_t, int, int); + diff --git a/mut/st/st-copyout b/mut/st/st-copyout new file mode 100755 index 0000000..0d19e5a --- /dev/null +++ b/mut/st/st-copyout @@ -0,0 +1,13 @@ +#!/bin/sh +# Using external pipe with st, give a dmenu prompt of recent commands, +# allowing the user to copy the output of one. +# xclip required for this script. +# By Jaywalker and Luke +tmpfile=$(mktemp /tmp/st-cmd-output.XXXXXX) +trap 'rm "$tmpfile"' 0 1 15 +sed -n "w $tmpfile" +sed -i 's/\x0//g' "$tmpfile" +ps1="$(grep "\S" "$tmpfile" | tail -n 1 | sed 's/^\s*//' | cut -d' ' -f1)" +chosen="$(grep -F "$ps1" "$tmpfile" | sed '$ d' | tac | dmenu -p "Copy which command's output?" -i -l 10 | sed 's/[^^]/[&]/g; s/\^/\\^/g')" +eps1="$(echo "$ps1" | sed 's/[^^]/[&]/g; s/\^/\\^/g')" +awk "/^$chosen$/{p=1;print;next} p&&/$eps1/{p=0};p" "$tmpfile" | xclip -selection clipboard diff --git a/mut/st/st-urlhandler b/mut/st/st-urlhandler new file mode 100755 index 0000000..0eb4586 --- /dev/null +++ b/mut/st/st-urlhandler @@ -0,0 +1,19 @@ +#!/bin/sh + +urlregex="(((http|https|gopher|gemini|ftp|ftps|git)://|www\\.)[a-zA-Z0-9.]*[:;a-zA-Z0-9./+@$&%?$\#=_~-]*)|((magnet:\\?xt=urn:btih:)[a-zA-Z0-9]*)" + +urls="$(sed 's/.*│//g' | tr -d '\n' | # First remove linebreaks and mutt sidebars: + grep -aEo "$urlregex" | # grep only urls as defined above. + uniq | # Ignore neighboring duplicates. + sed "s/\(\.\|,\|;\|\!\\|\?\)$//; + s/^www./http:\/\/www\./")" # xdg-open will not detect url without http + +[ -z "$urls" ] && exit 1 + +while getopts "hoc" o; do case "${o}" in + h) printf "Optional arguments for custom use:\\n -c: copy\\n -o: xdg-open\\n -h: Show this message\\n" && exit 1 ;; + o) chosen="$(echo "$urls" | dmenu -i -p 'Follow which url?' -l 10)" + setsid xdg-open "$chosen" >/dev/null 2>&1 & ;; + c) echo "$urls" | dmenu -i -p 'Copy which url?' -l 10 | tr -d '\n' | xclip -selection clipboard ;; + *) printf "Invalid option: -%s\\n" "$OPTARG" && exit 1 ;; +esac done diff --git a/mut/st/st.1 b/mut/st/st.1 new file mode 100644 index 0000000..37f7e84 --- /dev/null +++ b/mut/st/st.1 @@ -0,0 +1,193 @@ +.TH ST 1 st\-VERSION +.SH NAME +st \- simple terminal +.SH SYNOPSIS +.B st +.RB [ \-aiv ] +.RB [ \-c +.IR class ] +.RB [ \-f +.IR font ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-o +.IR iofile ] +.RB [ \-T +.IR title ] +.RB [ \-t +.IR title ] +.RB [ \-l +.IR line ] +.RB [ \-w +.IR windowid ] +.RB [[ \-e ] +.IR command +.RI [ arguments ...]] +.PP +.B st +.RB [ \-aiv ] +.RB [ \-c +.IR class ] +.RB [ \-f +.IR font ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-o +.IR iofile ] +.RB [ \-T +.IR title ] +.RB [ \-t +.IR title ] +.RB [ \-w +.IR windowid ] +.RB \-l +.IR line +.RI [ stty_args ...] +.SH DESCRIPTION +.B st +is a simple terminal emulator. +.SH OPTIONS +.TP +.B \-a +disable alternate screens in terminal +.TP +.BI \-c " class" +defines the window class (default $TERM). +.TP +.BI \-f " font" +defines the +.I font +to use when st is run. +.TP +.BI \-g " geometry" +defines the X11 geometry string. +The form is [=][<cols>{xX}<rows>][{+-}<xoffset>{+-}<yoffset>]. See +.BR XParseGeometry (3) +for further details. +.TP +.B \-i +will fixate the position given with the -g option. +.TP +.BI \-n " name" +defines the window instance name (default $TERM). +.TP +.BI \-o " iofile" +writes all the I/O to +.I iofile. +This feature is useful when recording st sessions. A value of "-" means +standard output. +.TP +.BI \-T " title" +defines the window title (default 'st'). +.TP +.BI \-t " title" +defines the window title (default 'st'). +.TP +.BI \-w " windowid" +embeds st within the window identified by +.I windowid +.TP +.BI \-l " line" +use a tty +.I line +instead of a pseudo terminal. +.I line +should be a (pseudo-)serial device (e.g. /dev/ttyS0 on Linux for serial port +0). +When this flag is given +remaining arguments are used as flags for +.BR stty(1). +By default st initializes the serial line to 8 bits, no parity, 1 stop bit +and a 38400 baud rate. The speed is set by appending it as last argument +(e.g. 'st -l /dev/ttyS0 115200'). Arguments before the last one are +.BR stty(1) +flags. If you want to set odd parity on 115200 baud use for example 'st -l +/dev/ttyS0 parenb parodd 115200'. Set the number of bits by using for +example 'st -l /dev/ttyS0 cs7 115200'. See +.BR stty(1) +for more arguments and cases. +.TP +.B \-v +prints version information to stderr, then exits. +.TP +.BI \-e " command " [ " arguments " "... ]" +st executes +.I command +instead of the shell. If this is used it +.B must be the last option +on the command line, as in xterm / rxvt. +This option is only intended for compatibility, +and all the remaining arguments are used as a command +even without it. +.SH SHORTCUTS +.TP +.B Alt-j/k or Alt-Up/Down or Alt-Mouse Wheel +Scroll up/down one line at a time. +.TP +.B Alt-u/d or Alt-Page Up/Page Down +Scroll up/down one screen at a time. +.TP +.B Alt-Shift-k/j or Alt-Shift-Page Up/Page Down or Alt-Shift-Mouse Wheel +Increase or decrease font size. +.TP +.B Alt-Home +Reset to default font size. +.TP +.B Shift-Insert or Alt-v +Paste from clipboard. +.TP +.B Alt-c +Copy to clipboard. +.TP +.B Alt-p +Paste/input primary selection. +.TP +.B Alt-l +Show dmenu menu of all URLs on screen and choose one to open. +.TP +.B Alt-y +Show dmenu menu of all URLs on screen and choose one to copy. +.TP +.B Alt-o +Show dmenu menu of all recently run commands and copy the output of the chosen command to the clipboard. +.I xclip +required. +.TP +.B Alt-a/s +Increase or decrease opacity/alpha value (make window more or less transparent). +.TP +.B Break +Send a break in the serial line. +Break key is obtained in PC keyboards +pressing at the same time control and pause. +.TP +.B Ctrl-Print Screen +Toggle if st should print to the +.I iofile. +.TP +.B Shift-Print Screen +Print the full screen to the +.I iofile. +.TP +.B Print Screen +Print the selection to the +.I iofile. +.SH CUSTOMIZATION +.B st +can be customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.SH AUTHORS +See the LICENSE file for the authors. +.SH LICENSE +See the LICENSE file for the terms of redistribution. +.SH SEE ALSO +.BR tabbed (1), +.BR utmp (1), +.BR stty (1), +.BR scroll (1) +.SH BUGS +See the TODO file in the distribution. diff --git a/mut/st/st.c b/mut/st/st.c new file mode 100644 index 0000000..0f4593e --- /dev/null +++ b/mut/st/st.c @@ -0,0 +1,2816 @@ +/* See LICENSE for license details. */ +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <signal.h> +#include <sys/ioctl.h> +#include <sys/select.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <termios.h> +#include <unistd.h> +#include <wchar.h> + +#include "st.h" +#include "win.h" + +#if defined(__linux) + #include <pty.h> +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) + #include <util.h> +#elif defined(__FreeBSD__) || defined(__DragonFly__) + #include <libutil.h> +#endif + +/* Arbitrary sizes */ +#define UTF_INVALID 0xFFFD +#define UTF_SIZ 4 +#define ESC_BUF_SIZ (128*UTF_SIZ) +#define ESC_ARG_SIZ 16 +#define STR_BUF_SIZ ESC_BUF_SIZ +#define STR_ARG_SIZ ESC_ARG_SIZ +#define HISTSIZE 2000 + +/* macros */ +#define IS_SET(flag) ((term.mode & (flag)) != 0) +#define ISCONTROLC0(c) (BETWEEN(c, 0, 0x1f) || (c) == 0x7f) +#define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f)) +#define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c)) +#define ISDELIM(u) (u && wcschr(worddelimiters, u)) +#define TLINE(y) ((y) < term.scr ? term.hist[((y) + term.histi - \ + term.scr + HISTSIZE + 1) % HISTSIZE] : \ + term.line[(y) - term.scr]) +#define TLINE_HIST(y) ((y) <= HISTSIZE-term.row+2 ? term.hist[(y)] : term.line[(y-HISTSIZE+term.row-3)]) + +enum term_mode { + MODE_WRAP = 1 << 0, + MODE_INSERT = 1 << 1, + MODE_ALTSCREEN = 1 << 2, + MODE_CRLF = 1 << 3, + MODE_ECHO = 1 << 4, + MODE_PRINT = 1 << 5, + MODE_UTF8 = 1 << 6, +}; + +enum cursor_movement { + CURSOR_SAVE, + CURSOR_LOAD +}; + +enum cursor_state { + CURSOR_DEFAULT = 0, + CURSOR_WRAPNEXT = 1, + CURSOR_ORIGIN = 2 +}; + +enum charset { + CS_GRAPHIC0, + CS_GRAPHIC1, + CS_UK, + CS_USA, + CS_MULTI, + CS_GER, + CS_FIN +}; + +enum escape_state { + ESC_START = 1, + ESC_CSI = 2, + ESC_STR = 4, /* DCS, OSC, PM, APC */ + ESC_ALTCHARSET = 8, + ESC_STR_END = 16, /* a final string was encountered */ + ESC_TEST = 32, /* Enter in test mode */ + ESC_UTF8 = 64, +}; + +typedef struct { + Glyph attr; /* current char attributes */ + int x; + int y; + char state; +} TCursor; + +typedef struct { + int mode; + int type; + int snap; + /* + * Selection variables: + * nb – normalized coordinates of the beginning of the selection + * ne – normalized coordinates of the end of the selection + * ob – original coordinates of the beginning of the selection + * oe – original coordinates of the end of the selection + */ + struct { + int x, y; + } nb, ne, ob, oe; + + int alt; +} Selection; + +/* Internal representation of the screen */ +typedef struct { + int row; /* nb row */ + int col; /* nb col */ + int maxcol; + Line *line; /* screen */ + Line *alt; /* alternate screen */ + Line hist[HISTSIZE]; /* history buffer */ + int histi; /* history index */ + int scr; /* scroll back */ + int *dirty; /* dirtyness of lines */ + TCursor c; /* cursor */ + int ocx; /* old cursor col */ + int ocy; /* old cursor row */ + int top; /* top scroll limit */ + int bot; /* bottom scroll limit */ + int mode; /* terminal mode flags */ + int esc; /* escape state flags */ + char trantbl[4]; /* charset table translation */ + int charset; /* current charset */ + int icharset; /* selected charset for sequence */ + int *tabs; + Rune lastc; /* last printed char outside of sequence, 0 if control */ +} Term; + +/* CSI Escape sequence structs */ +/* ESC '[' [[ [<priv>] <arg> [;]] <mode> [<mode>]] */ +typedef struct { + char buf[ESC_BUF_SIZ]; /* raw string */ + size_t len; /* raw string length */ + char priv; + int arg[ESC_ARG_SIZ]; + int narg; /* nb of args */ + char mode[2]; +} CSIEscape; + +/* STR Escape sequence structs */ +/* ESC type [[ [<priv>] <arg> [;]] <mode>] ESC '\' */ +typedef struct { + char type; /* ESC type ... */ + char *buf; /* allocated raw string */ + size_t siz; /* allocation size */ + size_t len; /* raw string length */ + char *args[STR_ARG_SIZ]; + int narg; /* nb of args */ +} STREscape; + +static void execsh(char *, char **); +static void stty(char **); +static void sigchld(int); +static void ttywriteraw(const char *, size_t); + +static void csidump(void); +static void csihandle(void); +static void csiparse(void); +static void csireset(void); +static void osc_color_response(int, int, int); +static int eschandle(uchar); +static void strdump(void); +static void strhandle(void); +static void strparse(void); +static void strreset(void); + +static void tprinter(char *, size_t); +static void tdumpsel(void); +static void tdumpline(int); +static void tdump(void); +static void tclearregion(int, int, int, int); +static void tcursor(int); +static void tdeletechar(int); +static void tdeleteline(int); +static void tinsertblank(int); +static void tinsertblankline(int); +static int tlinelen(int); +static void tmoveto(int, int); +static void tmoveato(int, int); +static void tnewline(int); +static void tputtab(int); +static void tputc(Rune); +static void treset(void); +static void tscrollup(int, int, int); +static void tscrolldown(int, int, int); +static void tsetattr(const int *, int); +static void tsetchar(Rune, const Glyph *, int, int); +static void tsetdirt(int, int); +static void tsetscroll(int, int); +static void tswapscreen(void); +static void tsetmode(int, int, const int *, int); +static int twrite(const char *, int, int); +static void tcontrolcode(uchar ); +static void tdectest(char ); +static void tdefutf8(char); +static int32_t tdefcolor(const int *, int *, int); +static void tdeftran(char); +static void tstrsequence(uchar); + +static void drawregion(int, int, int, int); + +static void selnormalize(void); +static void selscroll(int, int); +static void selsnap(int *, int *, int); + +static size_t utf8decode(const char *, Rune *, size_t); +static Rune utf8decodebyte(char, size_t *); +static char utf8encodebyte(Rune, size_t); +static size_t utf8validate(Rune *, size_t); + +static char *base64dec(const char *); +static char base64dec_getc(const char **); + +static ssize_t xwrite(int, const char *, size_t); + +/* Globals */ +static Term term; +static Selection sel; +static CSIEscape csiescseq; +static STREscape strescseq; +static int iofd = 1; +static int cmdfd; +static pid_t pid; + +static const uchar utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; +static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; +static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + +ssize_t +xwrite(int fd, const char *s, size_t len) +{ + size_t aux = len; + ssize_t r; + + while (len > 0) { + r = write(fd, s, len); + if (r < 0) + return r; + len -= r; + s += r; + } + + return aux; +} + +void * +xmalloc(size_t len) +{ + void *p; + + if (!(p = malloc(len))) + die("malloc: %s\n", strerror(errno)); + + return p; +} + +void * +xrealloc(void *p, size_t len) +{ + if ((p = realloc(p, len)) == NULL) + die("realloc: %s\n", strerror(errno)); + + return p; +} + +char * +xstrdup(const char *s) +{ + if ((s = strdup(s)) == NULL) + die("strdup: %s\n", strerror(errno)); + char *p; + + if ((p = strdup(s)) == NULL) + die("strdup: %s\n", strerror(errno)); + + return p; +} + +size_t +utf8decode(const char *c, Rune *u, size_t clen) +{ + size_t i, j, len, type; + Rune udecoded; + + *u = UTF_INVALID; + if (!clen) + return 0; + udecoded = utf8decodebyte(c[0], &len); + if (!BETWEEN(len, 1, UTF_SIZ)) + return 1; + for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { + udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); + if (type != 0) + return j; + } + if (j < len) + return 0; + *u = udecoded; + utf8validate(u, len); + + return len; +} + +Rune +utf8decodebyte(char c, size_t *i) +{ + for (*i = 0; *i < LEN(utfmask); ++(*i)) + if (((uchar)c & utfmask[*i]) == utfbyte[*i]) + return (uchar)c & ~utfmask[*i]; + + return 0; +} + +size_t +utf8encode(Rune u, char *c) +{ + size_t len, i; + + len = utf8validate(&u, 0); + if (len > UTF_SIZ) + return 0; + + for (i = len - 1; i != 0; --i) { + c[i] = utf8encodebyte(u, 0); + u >>= 6; + } + c[0] = utf8encodebyte(u, len); + + return len; +} + +char +utf8encodebyte(Rune u, size_t i) +{ + return utfbyte[i] | (u & ~utfmask[i]); +} + +size_t +utf8validate(Rune *u, size_t i) +{ + if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) + *u = UTF_INVALID; + for (i = 1; *u > utfmax[i]; ++i) + ; + + return i; +} + +char +base64dec_getc(const char **src) +{ + while (**src && !isprint((unsigned char)**src)) + (*src)++; + return **src ? *((*src)++) : '='; /* emulate padding if string ends */ +} + +char * +base64dec(const char *src) +{ + size_t in_len = strlen(src); + char *result, *dst; + static const char base64_digits[256] = { + [43] = 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 0, 0, 0, -1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, + 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + }; + + if (in_len % 4) + in_len += 4 - (in_len % 4); + result = dst = xmalloc(in_len / 4 * 3 + 1); + while (*src) { + int a = base64_digits[(unsigned char) base64dec_getc(&src)]; + int b = base64_digits[(unsigned char) base64dec_getc(&src)]; + int c = base64_digits[(unsigned char) base64dec_getc(&src)]; + int d = base64_digits[(unsigned char) base64dec_getc(&src)]; + + /* invalid input. 'a' can be -1, e.g. if src is "\n" (c-str) */ + if (a == -1 || b == -1) + break; + + *dst++ = (a << 2) | ((b & 0x30) >> 4); + if (c == -1) + break; + *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2); + if (d == -1) + break; + *dst++ = ((c & 0x03) << 6) | d; + } + *dst = '\0'; + return result; +} + +void +selinit(void) +{ + sel.mode = SEL_IDLE; + sel.snap = 0; + sel.ob.x = -1; +} + +int +tlinelen(int y) +{ + int i = term.col; + + if (TLINE(y)[i - 1].mode & ATTR_WRAP) + return i; + + while (i > 0 && TLINE(y)[i - 1].u == ' ') + --i; + + return i; +} + +int +tlinehistlen(int y) +{ + int i = term.col; + + if (TLINE_HIST(y)[i - 1].mode & ATTR_WRAP) + return i; + + while (i > 0 && TLINE_HIST(y)[i - 1].u == ' ') + --i; + + return i; +} + +void +selstart(int col, int row, int snap) +{ + selclear(); + sel.mode = SEL_EMPTY; + sel.type = SEL_REGULAR; + sel.alt = IS_SET(MODE_ALTSCREEN); + sel.snap = snap; + sel.oe.x = sel.ob.x = col; + sel.oe.y = sel.ob.y = row; + selnormalize(); + + if (sel.snap != 0) + sel.mode = SEL_READY; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +selextend(int col, int row, int type, int done) +{ + int oldey, oldex, oldsby, oldsey, oldtype; + + if (sel.mode == SEL_IDLE) + return; + if (done && sel.mode == SEL_EMPTY) { + selclear(); + return; + } + + oldey = sel.oe.y; + oldex = sel.oe.x; + oldsby = sel.nb.y; + oldsey = sel.ne.y; + oldtype = sel.type; + + sel.oe.x = col; + sel.oe.y = row; + selnormalize(); + sel.type = type; + + if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type || sel.mode == SEL_EMPTY) + tsetdirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey)); + + sel.mode = done ? SEL_IDLE : SEL_READY; +} + + +void +selnormalize(void) +{ + int i; + + if (sel.type == SEL_REGULAR && sel.ob.y != sel.oe.y) { + sel.nb.x = sel.ob.y < sel.oe.y ? sel.ob.x : sel.oe.x; + sel.ne.x = sel.ob.y < sel.oe.y ? sel.oe.x : sel.ob.x; + } else { + sel.nb.x = MIN(sel.ob.x, sel.oe.x); + sel.ne.x = MAX(sel.ob.x, sel.oe.x); + } + sel.nb.y = MIN(sel.ob.y, sel.oe.y); + sel.ne.y = MAX(sel.ob.y, sel.oe.y); + + selsnap(&sel.nb.x, &sel.nb.y, -1); + selsnap(&sel.ne.x, &sel.ne.y, +1); + + /* expand selection over line breaks */ + if (sel.type == SEL_RECTANGULAR) + return; + i = tlinelen(sel.nb.y); + if (i < sel.nb.x) + sel.nb.x = i; + if (tlinelen(sel.ne.y) <= sel.ne.x) + sel.ne.x = term.col - 1; +} + +int +selected(int x, int y) +{ + if (sel.mode == SEL_EMPTY || sel.ob.x == -1 || + sel.alt != IS_SET(MODE_ALTSCREEN)) + return 0; + + if (sel.type == SEL_RECTANGULAR) + return BETWEEN(y, sel.nb.y, sel.ne.y) + && BETWEEN(x, sel.nb.x, sel.ne.x); + + return BETWEEN(y, sel.nb.y, sel.ne.y) + && (y != sel.nb.y || x >= sel.nb.x) + && (y != sel.ne.y || x <= sel.ne.x); +} + +void +selsnap(int *x, int *y, int direction) +{ + int newx, newy, xt, yt; + int delim, prevdelim; + const Glyph *gp, *prevgp; + + switch (sel.snap) { + case SNAP_WORD: + /* + * Snap around if the word wraps around at the end or + * beginning of a line. + */ + prevgp = &TLINE(*y)[*x]; + prevdelim = ISDELIM(prevgp->u); + for (;;) { + newx = *x + direction; + newy = *y; + if (!BETWEEN(newx, 0, term.col - 1)) { + newy += direction; + newx = (newx + term.col) % term.col; + if (!BETWEEN(newy, 0, term.row - 1)) + break; + + if (direction > 0) + yt = *y, xt = *x; + else + yt = newy, xt = newx; + if (!(TLINE(yt)[xt].mode & ATTR_WRAP)) + break; + } + + if (newx >= tlinelen(newy)) + break; + + gp = &TLINE(newy)[newx]; + delim = ISDELIM(gp->u); + if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim + || (delim && gp->u != prevgp->u))) + break; + + *x = newx; + *y = newy; + prevgp = gp; + prevdelim = delim; + } + break; + case SNAP_LINE: + /* + * Snap around if the the previous line or the current one + * has set ATTR_WRAP at its end. Then the whole next or + * previous line will be selected. + */ + *x = (direction < 0) ? 0 : term.col - 1; + if (direction < 0) { + for (; *y > 0; *y += direction) { + if (!(TLINE(*y-1)[term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } else if (direction > 0) { + for (; *y < term.row-1; *y += direction) { + if (!(TLINE(*y)[term.col-1].mode + & ATTR_WRAP)) { + break; + } + } + } + break; + } +} + +char * +getsel(void) +{ + char *str, *ptr; + int y, bufsize, lastx, linelen; + const Glyph *gp, *last; + + if (sel.ob.x == -1) + return NULL; + + bufsize = (term.col+1) * (sel.ne.y-sel.nb.y+1) * UTF_SIZ; + ptr = str = xmalloc(bufsize); + + /* append every set & selected glyph to the selection */ + for (y = sel.nb.y; y <= sel.ne.y; y++) { + if ((linelen = tlinelen(y)) == 0) { + *ptr++ = '\n'; + continue; + } + + if (sel.type == SEL_RECTANGULAR) { + gp = &TLINE(y)[sel.nb.x]; + lastx = sel.ne.x; + } else { + gp = &TLINE(y)[sel.nb.y == y ? sel.nb.x : 0]; + lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1; + } + last = &TLINE(y)[MIN(lastx, linelen-1)]; + while (last >= gp && last->u == ' ') + --last; + + for ( ; gp <= last; ++gp) { + if (gp->mode & ATTR_WDUMMY) + continue; + + ptr += utf8encode(gp->u, ptr); + } + + /* + * Copy and pasting of line endings is inconsistent + * in the inconsistent terminal and GUI world. + * The best solution seems like to produce '\n' when + * something is copied from st and convert '\n' to + * '\r', when something to be pasted is received by + * st. + * FIXME: Fix the computer world. + */ + if ((y < sel.ne.y || lastx >= linelen) && + (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR)) + *ptr++ = '\n'; + } + *ptr = 0; + return str; +} + +void +selclear(void) +{ + if (sel.ob.x == -1) + return; + sel.mode = SEL_IDLE; + sel.ob.x = -1; + tsetdirt(sel.nb.y, sel.ne.y); +} + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(1); +} + +void +execsh(char *cmd, char **args) +{ + char *sh, *prog, *arg; + const struct passwd *pw; + + errno = 0; + if ((pw = getpwuid(getuid())) == NULL) { + if (errno) + die("getpwuid: %s\n", strerror(errno)); + else + die("who are you?\n"); + } + + if ((sh = getenv("SHELL")) == NULL) + sh = (pw->pw_shell[0]) ? pw->pw_shell : cmd; + + if (args) { + prog = args[0]; + arg = NULL; + } else if (scroll) { + prog = scroll; + arg = utmp ? utmp : sh; + } else if (utmp) { + prog = utmp; + arg = NULL; + } else { + prog = sh; + arg = NULL; + } + DEFAULT(args, ((char *[]) {prog, arg, NULL})); + + unsetenv("COLUMNS"); + unsetenv("LINES"); + unsetenv("TERMCAP"); + setenv("LOGNAME", pw->pw_name, 1); + setenv("USER", pw->pw_name, 1); + setenv("SHELL", sh, 1); + setenv("HOME", pw->pw_dir, 1); + setenv("TERM", termname, 1); + + signal(SIGCHLD, SIG_DFL); + signal(SIGHUP, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGALRM, SIG_DFL); + + execvp(prog, args); + _exit(1); +} + +void +sigchld(int a) +{ + int stat; + pid_t p; + + if ((p = waitpid(pid, &stat, WNOHANG)) < 0) + die("waiting for pid %hd failed: %s\n", pid, strerror(errno)); + + if (pid != p) + return; + + if (WIFEXITED(stat) && WEXITSTATUS(stat)) + die("child exited with status %d\n", WEXITSTATUS(stat)); + else if (WIFSIGNALED(stat)) + die("child terminated due to signal %d\n", WTERMSIG(stat)); + _exit(0); +} + +void +stty(char **args) +{ + char cmd[_POSIX_ARG_MAX], **p, *q, *s; + size_t n, siz; + + if ((n = strlen(stty_args)) > sizeof(cmd)-1) + die("incorrect stty parameters\n"); + memcpy(cmd, stty_args, n); + q = cmd + n; + siz = sizeof(cmd) - n; + for (p = args; p && (s = *p); ++p) { + if ((n = strlen(s)) > siz-1) + die("stty parameter length too long\n"); + *q++ = ' '; + memcpy(q, s, n); + q += n; + siz -= n + 1; + } + *q = '\0'; + if (system(cmd) != 0) + perror("Couldn't call stty"); +} + +int +ttynew(const char *line, char *cmd, const char *out, char **args) +{ + int m, s; + + if (out) { + term.mode |= MODE_PRINT; + iofd = (!strcmp(out, "-")) ? + 1 : open(out, O_WRONLY | O_CREAT, 0666); + if (iofd < 0) { + fprintf(stderr, "Error opening %s:%s\n", + out, strerror(errno)); + } + } + + if (line) { + if ((cmdfd = open(line, O_RDWR)) < 0) + die("open line '%s' failed: %s\n", + line, strerror(errno)); + dup2(cmdfd, 0); + stty(args); + return cmdfd; + } + + /* seems to work fine on linux, openbsd and freebsd */ + if (openpty(&m, &s, NULL, NULL, NULL) < 0) + die("openpty failed: %s\n", strerror(errno)); + + switch (pid = fork()) { + case -1: + die("fork failed: %s\n", strerror(errno)); + break; + case 0: + close(iofd); + close(m); + setsid(); /* create a new process group */ + dup2(s, 0); + dup2(s, 1); + dup2(s, 2); + if (ioctl(s, TIOCSCTTY, NULL) < 0) + die("ioctl TIOCSCTTY failed: %s\n", strerror(errno)); + if (s > 2) + close(s); +#ifdef __OpenBSD__ + if (pledge("stdio getpw proc exec", NULL) == -1) + die("pledge\n"); +#endif + execsh(cmd, args); + break; + default: +#ifdef __OpenBSD__ + if (pledge("stdio rpath tty proc", NULL) == -1) + die("pledge\n"); +#endif + close(s); + cmdfd = m; + signal(SIGCHLD, sigchld); + break; + } + return cmdfd; +} + +size_t +ttyread(void) +{ + static char buf[BUFSIZ]; + static int buflen = 0; + int ret, written; + + /* append read bytes to unprocessed bytes */ + ret = read(cmdfd, buf+buflen, LEN(buf)-buflen); + + switch (ret) { + case 0: + exit(0); + case -1: + die("couldn't read from shell: %s\n", strerror(errno)); + default: + buflen += ret; + written = twrite(buf, buflen, 0); + buflen -= written; + /* keep any incomplete UTF-8 byte sequence for the next call */ + if (buflen > 0) + memmove(buf, buf + written, buflen); + return ret; + } +} + +void +ttywrite(const char *s, size_t n, int may_echo) +{ + const char *next; + Arg arg = (Arg) { .i = term.scr }; + + kscrolldown(&arg); + + if (may_echo && IS_SET(MODE_ECHO)) + twrite(s, n, 1); + + if (!IS_SET(MODE_CRLF)) { + ttywriteraw(s, n); + return; + } + + /* This is similar to how the kernel handles ONLCR for ttys */ + while (n > 0) { + if (*s == '\r') { + next = s + 1; + ttywriteraw("\r\n", 2); + } else { + next = memchr(s, '\r', n); + DEFAULT(next, s + n); + ttywriteraw(s, next - s); + } + n -= next - s; + s = next; + } +} + +void +ttywriteraw(const char *s, size_t n) +{ + fd_set wfd, rfd; + ssize_t r; + size_t lim = 256; + + /* + * Remember that we are using a pty, which might be a modem line. + * Writing too much will clog the line. That's why we are doing this + * dance. + * FIXME: Migrate the world to Plan 9. + */ + while (n > 0) { + FD_ZERO(&wfd); + FD_ZERO(&rfd); + FD_SET(cmdfd, &wfd); + FD_SET(cmdfd, &rfd); + + /* Check if we can write. */ + if (pselect(cmdfd+1, &rfd, &wfd, NULL, NULL, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + if (FD_ISSET(cmdfd, &wfd)) { + /* + * Only write the bytes written by ttywrite() or the + * default of 256. This seems to be a reasonable value + * for a serial line. Bigger values might clog the I/O. + */ + if ((r = write(cmdfd, s, (n < lim)? n : lim)) < 0) + goto write_error; + if (r < n) { + /* + * We weren't able to write out everything. + * This means the buffer is getting full + * again. Empty it. + */ + if (n < lim) + lim = ttyread(); + n -= r; + s += r; + } else { + /* All bytes have been written. */ + break; + } + } + if (FD_ISSET(cmdfd, &rfd)) + lim = ttyread(); + } + return; + +write_error: + die("write error on tty: %s\n", strerror(errno)); +} + +void +ttyresize(int tw, int th) +{ + struct winsize w; + + w.ws_row = term.row; + w.ws_col = term.col; + w.ws_xpixel = tw; + w.ws_ypixel = th; + if (ioctl(cmdfd, TIOCSWINSZ, &w) < 0) + fprintf(stderr, "Couldn't set window size: %s\n", strerror(errno)); +} + +void +ttyhangup(void) +{ + /* Send SIGHUP to shell */ + kill(pid, SIGHUP); +} + +int +tattrset(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) + return 1; + } + } + + return 0; +} + +void +tsetdirt(int top, int bot) +{ + int i; + + LIMIT(top, 0, term.row-1); + LIMIT(bot, 0, term.row-1); + + for (i = top; i <= bot; i++) + term.dirty[i] = 1; +} + +void +tsetdirtattr(int attr) +{ + int i, j; + + for (i = 0; i < term.row-1; i++) { + for (j = 0; j < term.col-1; j++) { + if (term.line[i][j].mode & attr) { + tsetdirt(i, i); + break; + } + } + } +} + +void +tfulldirt(void) +{ + tsetdirt(0, term.row-1); +} + +void +tcursor(int mode) +{ + static TCursor c[2]; + int alt = IS_SET(MODE_ALTSCREEN); + + if (mode == CURSOR_SAVE) { + c[alt] = term.c; + } else if (mode == CURSOR_LOAD) { + term.c = c[alt]; + tmoveto(c[alt].x, c[alt].y); + } +} + +void +treset(void) +{ + uint i; + + term.c = (TCursor){{ + .mode = ATTR_NULL, + .fg = defaultfg, + .bg = defaultbg + }, .x = 0, .y = 0, .state = CURSOR_DEFAULT}; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + for (i = tabspaces; i < term.col; i += tabspaces) + term.tabs[i] = 1; + term.top = 0; + term.bot = term.row - 1; + term.mode = MODE_WRAP|MODE_UTF8; + memset(term.trantbl, CS_USA, sizeof(term.trantbl)); + term.charset = 0; + + for (i = 0; i < 2; i++) { + tmoveto(0, 0); + tcursor(CURSOR_SAVE); + tclearregion(0, 0, term.col-1, term.row-1); + tswapscreen(); + } +} + +void +tnew(int col, int row) +{ + term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; + tresize(col, row); + treset(); +} + +void +tswapscreen(void) +{ + Line *tmp = term.line; + + term.line = term.alt; + term.alt = tmp; + term.mode ^= MODE_ALTSCREEN; + tfulldirt(); +} + +void +kscrolldown(const Arg* a) +{ + int n = a->i; + + if (n < 0) + n = term.row + n; + + if (n > term.scr) + n = term.scr; + + if (term.scr > 0) { + term.scr -= n; + selscroll(0, -n); + tfulldirt(); + } +} + +void +kscrollup(const Arg* a) +{ + int n = a->i; + + if (n < 0) + n = term.row + n; + + if (term.scr <= HISTSIZE-n) { + term.scr += n; + selscroll(0, n); + tfulldirt(); + } +} + +void +tscrolldown(int orig, int n, int copyhist) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + + if (copyhist) { + term.histi = (term.histi - 1 + HISTSIZE) % HISTSIZE; + temp = term.hist[term.histi]; + term.hist[term.histi] = term.line[term.bot]; + term.line[term.bot] = temp; + } + + tsetdirt(orig, term.bot-n); + tclearregion(0, term.bot-n+1, term.col-1, term.bot); + + for (i = term.bot; i >= orig+n; i--) { + temp = term.line[i]; + term.line[i] = term.line[i-n]; + term.line[i-n] = temp; + } + + if (term.scr == 0) + selscroll(orig, n); +} + +void +tscrollup(int orig, int n, int copyhist) +{ + int i; + Line temp; + + LIMIT(n, 0, term.bot-orig+1); + + if (copyhist) { + term.histi = (term.histi + 1) % HISTSIZE; + temp = term.hist[term.histi]; + term.hist[term.histi] = term.line[orig]; + term.line[orig] = temp; + } + + if (term.scr > 0 && term.scr < HISTSIZE) + term.scr = MIN(term.scr + n, HISTSIZE-1); + + tclearregion(0, orig, term.col-1, orig+n-1); + tsetdirt(orig+n, term.bot); + + for (i = orig; i <= term.bot-n; i++) { + temp = term.line[i]; + term.line[i] = term.line[i+n]; + term.line[i+n] = temp; + } + + if (term.scr == 0) + selscroll(orig, -n); +} + +void +selscroll(int orig, int n) +{ + if (sel.ob.x == -1) + return; + + if (BETWEEN(sel.nb.y, orig, term.bot) != BETWEEN(sel.ne.y, orig, term.bot)) { + selclear(); + } else if (BETWEEN(sel.nb.y, orig, term.bot)) { + sel.ob.y += n; + sel.oe.y += n; + if (sel.ob.y < term.top || sel.ob.y > term.bot || + sel.oe.y < term.top || sel.oe.y > term.bot) { + selclear(); + } else { + selnormalize(); + } + } +} + +void +tnewline(int first_col) +{ + int y = term.c.y; + + if (y == term.bot) { + tscrollup(term.top, 1, 1); + } else { + y++; + } + tmoveto(first_col ? 0 : term.c.x, y); +} + +void +csiparse(void) +{ + char *p = csiescseq.buf, *np; + long int v; + + csiescseq.narg = 0; + if (*p == '?') { + csiescseq.priv = 1; + p++; + } + + csiescseq.buf[csiescseq.len] = '\0'; + while (p < csiescseq.buf+csiescseq.len) { + np = NULL; + v = strtol(p, &np, 10); + if (np == p) + v = 0; + if (v == LONG_MAX || v == LONG_MIN) + v = -1; + csiescseq.arg[csiescseq.narg++] = v; + p = np; + if (*p != ';' || csiescseq.narg == ESC_ARG_SIZ) + break; + p++; + } + csiescseq.mode[0] = *p++; + csiescseq.mode[1] = (p < csiescseq.buf+csiescseq.len) ? *p : '\0'; +} + +/* for absolute user moves, when decom is set */ +void +tmoveato(int x, int y) +{ + tmoveto(x, y + ((term.c.state & CURSOR_ORIGIN) ? term.top: 0)); +} + +void +tmoveto(int x, int y) +{ + int miny, maxy; + + if (term.c.state & CURSOR_ORIGIN) { + miny = term.top; + maxy = term.bot; + } else { + miny = 0; + maxy = term.row - 1; + } + term.c.state &= ~CURSOR_WRAPNEXT; + term.c.x = LIMIT(x, 0, term.col-1); + term.c.y = LIMIT(y, miny, maxy); +} + +void +tsetchar(Rune u, const Glyph *attr, int x, int y) +{ + static const char *vt100_0[62] = { /* 0x41 - 0x7e */ + "↑", "↓", "→", "←", "█", "▚", "☃", /* A - G */ + 0, 0, 0, 0, 0, 0, 0, 0, /* H - O */ + 0, 0, 0, 0, 0, 0, 0, 0, /* P - W */ + 0, 0, 0, 0, 0, 0, 0, " ", /* X - _ */ + "◆", "▒", "␉", "␌", "␍", "␊", "°", "±", /* ` - g */ + "", "␋", "┘", "┐", "┌", "└", "┼", "⎺", /* h - o */ + "⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */ + "│", "≤", "≥", "π", "≠", "£", "·", /* x - ~ */ + }; + + /* + * The table is proudly stolen from rxvt. + */ + if (term.trantbl[term.charset] == CS_GRAPHIC0 && + BETWEEN(u, 0x41, 0x7e) && vt100_0[u - 0x41]) + utf8decode(vt100_0[u - 0x41], &u, UTF_SIZ); + + if (term.line[y][x].mode & ATTR_WIDE) { + if (x+1 < term.col) { + term.line[y][x+1].u = ' '; + term.line[y][x+1].mode &= ~ATTR_WDUMMY; + } + } else if (term.line[y][x].mode & ATTR_WDUMMY) { + term.line[y][x-1].u = ' '; + term.line[y][x-1].mode &= ~ATTR_WIDE; + } + + term.dirty[y] = 1; + term.line[y][x] = *attr; + term.line[y][x].u = u; + + if (isboxdraw(u)) + term.line[y][x].mode |= ATTR_BOXDRAW; +} + +void +tclearregion(int x1, int y1, int x2, int y2) +{ + int x, y, temp; + Glyph *gp; + + if (x1 > x2) + temp = x1, x1 = x2, x2 = temp; + if (y1 > y2) + temp = y1, y1 = y2, y2 = temp; + + LIMIT(x1, 0, term.maxcol-1); + LIMIT(x2, 0, term.maxcol-1); + LIMIT(y1, 0, term.row-1); + LIMIT(y2, 0, term.row-1); + + for (y = y1; y <= y2; y++) { + term.dirty[y] = 1; + for (x = x1; x <= x2; x++) { + gp = &term.line[y][x]; + if (selected(x, y)) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; + gp->mode = 0; + gp->u = ' '; + } + } +} + +void +tdeletechar(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x; + src = term.c.x + n; + size = term.col - src; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(term.col-n, term.c.y, term.col-1, term.c.y); +} + +void +tinsertblank(int n) +{ + int dst, src, size; + Glyph *line; + + LIMIT(n, 0, term.col - term.c.x); + + dst = term.c.x + n; + src = term.c.x; + size = term.col - dst; + line = term.line[term.c.y]; + + memmove(&line[dst], &line[src], size * sizeof(Glyph)); + tclearregion(src, term.c.y, dst - 1, term.c.y); +} + +void +tinsertblankline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrolldown(term.c.y, n, 0); +} + +void +tdeleteline(int n) +{ + if (BETWEEN(term.c.y, term.top, term.bot)) + tscrollup(term.c.y, n, 0); +} + +int32_t +tdefcolor(const int *attr, int *npar, int l) +{ + int32_t idx = -1; + uint r, g, b; + + switch (attr[*npar + 1]) { + case 2: /* direct color in RGB space */ + if (*npar + 4 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + r = attr[*npar + 2]; + g = attr[*npar + 3]; + b = attr[*npar + 4]; + *npar += 4; + if (!BETWEEN(r, 0, 255) || !BETWEEN(g, 0, 255) || !BETWEEN(b, 0, 255)) + fprintf(stderr, "erresc: bad rgb color (%u,%u,%u)\n", + r, g, b); + else + idx = TRUECOLOR(r, g, b); + break; + case 5: /* indexed color */ + if (*npar + 2 >= l) { + fprintf(stderr, + "erresc(38): Incorrect number of parameters (%d)\n", + *npar); + break; + } + *npar += 2; + if (!BETWEEN(attr[*npar], 0, 255)) + fprintf(stderr, "erresc: bad fgcolor %d\n", attr[*npar]); + else + idx = attr[*npar]; + break; + case 0: /* implemented defined (only foreground) */ + case 1: /* transparent */ + case 3: /* direct color in CMY space */ + case 4: /* direct color in CMYK space */ + default: + fprintf(stderr, + "erresc(38): gfx attr %d unknown\n", attr[*npar]); + break; + } + + return idx; +} + +void +tsetattr(const int *attr, int l) +{ + int i; + int32_t idx; + + for (i = 0; i < l; i++) { + switch (attr[i]) { + case 0: + term.c.attr.mode &= ~( + ATTR_BOLD | + ATTR_FAINT | + ATTR_ITALIC | + ATTR_UNDERLINE | + ATTR_BLINK | + ATTR_REVERSE | + ATTR_INVISIBLE | + ATTR_STRUCK ); + term.c.attr.fg = defaultfg; + term.c.attr.bg = defaultbg; + break; + case 1: + term.c.attr.mode |= ATTR_BOLD; + break; + case 2: + term.c.attr.mode |= ATTR_FAINT; + break; + case 3: + term.c.attr.mode |= ATTR_ITALIC; + break; + case 4: + term.c.attr.mode |= ATTR_UNDERLINE; + break; + case 5: /* slow blink */ + /* FALLTHROUGH */ + case 6: /* rapid blink */ + term.c.attr.mode |= ATTR_BLINK; + break; + case 7: + term.c.attr.mode |= ATTR_REVERSE; + break; + case 8: + term.c.attr.mode |= ATTR_INVISIBLE; + break; + case 9: + term.c.attr.mode |= ATTR_STRUCK; + break; + case 22: + term.c.attr.mode &= ~(ATTR_BOLD | ATTR_FAINT); + break; + case 23: + term.c.attr.mode &= ~ATTR_ITALIC; + break; + case 24: + term.c.attr.mode &= ~ATTR_UNDERLINE; + break; + case 25: + term.c.attr.mode &= ~ATTR_BLINK; + break; + case 27: + term.c.attr.mode &= ~ATTR_REVERSE; + break; + case 28: + term.c.attr.mode &= ~ATTR_INVISIBLE; + break; + case 29: + term.c.attr.mode &= ~ATTR_STRUCK; + break; + case 38: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.fg = idx; + break; + case 39: + term.c.attr.fg = defaultfg; + break; + case 48: + if ((idx = tdefcolor(attr, &i, l)) >= 0) + term.c.attr.bg = idx; + break; + case 49: + term.c.attr.bg = defaultbg; + break; + default: + if (BETWEEN(attr[i], 30, 37)) { + term.c.attr.fg = attr[i] - 30; + } else if (BETWEEN(attr[i], 40, 47)) { + term.c.attr.bg = attr[i] - 40; + } else if (BETWEEN(attr[i], 90, 97)) { + term.c.attr.fg = attr[i] - 90 + 8; + } else if (BETWEEN(attr[i], 100, 107)) { + term.c.attr.bg = attr[i] - 100 + 8; + } else { + fprintf(stderr, + "erresc(default): gfx attr %d unknown\n", + attr[i]); + csidump(); + } + break; + } + } +} + +void +tsetscroll(int t, int b) +{ + int temp; + + LIMIT(t, 0, term.row-1); + LIMIT(b, 0, term.row-1); + if (t > b) { + temp = t; + t = b; + b = temp; + } + term.top = t; + term.bot = b; +} + +void +tsetmode(int priv, int set, const int *args, int narg) +{ + int alt; const int *lim; + + for (lim = args + narg; args < lim; ++args) { + if (priv) { + switch (*args) { + case 1: /* DECCKM -- Cursor key */ + xsetmode(set, MODE_APPCURSOR); + break; + case 5: /* DECSCNM -- Reverse video */ + xsetmode(set, MODE_REVERSE); + break; + case 6: /* DECOM -- Origin */ + MODBIT(term.c.state, set, CURSOR_ORIGIN); + tmoveato(0, 0); + break; + case 7: /* DECAWM -- Auto wrap */ + MODBIT(term.mode, set, MODE_WRAP); + break; + case 0: /* Error (IGNORED) */ + case 2: /* DECANM -- ANSI/VT52 (IGNORED) */ + case 3: /* DECCOLM -- Column (IGNORED) */ + case 4: /* DECSCLM -- Scroll (IGNORED) */ + case 8: /* DECARM -- Auto repeat (IGNORED) */ + case 18: /* DECPFF -- Printer feed (IGNORED) */ + case 19: /* DECPEX -- Printer extent (IGNORED) */ + case 42: /* DECNRCM -- National characters (IGNORED) */ + case 12: /* att610 -- Start blinking cursor (IGNORED) */ + break; + case 25: /* DECTCEM -- Text Cursor Enable Mode */ + xsetmode(!set, MODE_HIDE); + break; + case 9: /* X10 mouse compatibility mode */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEX10); + break; + case 1000: /* 1000: report button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEBTN); + break; + case 1002: /* 1002: report motion on button press */ + xsetpointermotion(0); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMOTION); + break; + case 1003: /* 1003: enable all mouse motions */ + xsetpointermotion(set); + xsetmode(0, MODE_MOUSE); + xsetmode(set, MODE_MOUSEMANY); + break; + case 1004: /* 1004: send focus events to tty */ + xsetmode(set, MODE_FOCUS); + break; + case 1006: /* 1006: extended reporting mode */ + xsetmode(set, MODE_MOUSESGR); + break; + case 1034: + xsetmode(set, MODE_8BIT); + break; + case 1049: /* swap screen & set/restore cursor as xterm */ + if (!allowaltscreen) + break; + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + /* FALLTHROUGH */ + case 47: /* swap screen */ + case 1047: + if (!allowaltscreen) + break; + alt = IS_SET(MODE_ALTSCREEN); + if (alt) { + tclearregion(0, 0, term.col-1, + term.row-1); + } + if (set ^ alt) /* set is always 1 or 0 */ + tswapscreen(); + if (*args != 1049) + break; + /* FALLTHROUGH */ + case 1048: + tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD); + break; + case 2004: /* 2004: bracketed paste mode */ + xsetmode(set, MODE_BRCKTPASTE); + break; + /* Not implemented mouse modes. See comments there. */ + case 1001: /* mouse highlight mode; can hang the + terminal by design when implemented. */ + case 1005: /* UTF-8 mouse mode; will confuse + applications not supporting UTF-8 + and luit. */ + case 1015: /* urxvt mangled mouse mode; incompatible + and can be mistaken for other control + codes. */ + break; + default: + fprintf(stderr, + "erresc: unknown private set/reset mode %d\n", + *args); + break; + } + } else { + switch (*args) { + case 0: /* Error (IGNORED) */ + break; + case 2: + xsetmode(set, MODE_KBDLOCK); + break; + case 4: /* IRM -- Insertion-replacement */ + MODBIT(term.mode, set, MODE_INSERT); + break; + case 12: /* SRM -- Send/Receive */ + MODBIT(term.mode, !set, MODE_ECHO); + break; + case 20: /* LNM -- Linefeed/new line */ + MODBIT(term.mode, set, MODE_CRLF); + break; + default: + fprintf(stderr, + "erresc: unknown set/reset mode %d\n", + *args); + break; + } + } + } +} + +void +csihandle(void) +{ + char buf[40]; + int len; + + switch (csiescseq.mode[0]) { + default: + unknown: + fprintf(stderr, "erresc: unknown csi "); + csidump(); + /* die(""); */ + break; + case '@': /* ICH -- Insert <n> blank char */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblank(csiescseq.arg[0]); + break; + case 'A': /* CUU -- Cursor <n> Up */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y-csiescseq.arg[0]); + break; + case 'B': /* CUD -- Cursor <n> Down */ + case 'e': /* VPR --Cursor <n> Down */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x, term.c.y+csiescseq.arg[0]); + break; + case 'i': /* MC -- Media Copy */ + switch (csiescseq.arg[0]) { + case 0: + tdump(); + break; + case 1: + tdumpline(term.c.y); + break; + case 2: + tdumpsel(); + break; + case 4: + term.mode &= ~MODE_PRINT; + break; + case 5: + term.mode |= MODE_PRINT; + break; + } + break; + case 'c': /* DA -- Device Attributes */ + if (csiescseq.arg[0] == 0) + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'b': /* REP -- if last char is printable print it <n> more times */ + DEFAULT(csiescseq.arg[0], 1); + if (term.lastc) + while (csiescseq.arg[0]-- > 0) + tputc(term.lastc); + break; + case 'C': /* CUF -- Cursor <n> Forward */ + case 'a': /* HPR -- Cursor <n> Forward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x+csiescseq.arg[0], term.c.y); + break; + case 'D': /* CUB -- Cursor <n> Backward */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(term.c.x-csiescseq.arg[0], term.c.y); + break; + case 'E': /* CNL -- Cursor <n> Down and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y+csiescseq.arg[0]); + break; + case 'F': /* CPL -- Cursor <n> Up and first col */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(0, term.c.y-csiescseq.arg[0]); + break; + case 'g': /* TBC -- Tabulation clear */ + switch (csiescseq.arg[0]) { + case 0: /* clear current tab stop */ + term.tabs[term.c.x] = 0; + break; + case 3: /* clear all the tabs */ + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + break; + default: + goto unknown; + } + break; + case 'G': /* CHA -- Move to <col> */ + case '`': /* HPA */ + DEFAULT(csiescseq.arg[0], 1); + tmoveto(csiescseq.arg[0]-1, term.c.y); + break; + case 'H': /* CUP -- Move to <row> <col> */ + case 'f': /* HVP */ + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], 1); + tmoveato(csiescseq.arg[1]-1, csiescseq.arg[0]-1); + break; + case 'I': /* CHT -- Cursor Forward Tabulation <n> tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(csiescseq.arg[0]); + break; + case 'J': /* ED -- Clear screen */ + switch (csiescseq.arg[0]) { + case 0: /* below */ + tclearregion(term.c.x, term.c.y, term.col-1, term.c.y); + if (term.c.y < term.row-1) { + tclearregion(0, term.c.y+1, term.col-1, + term.row-1); + } + break; + case 1: /* above */ + if (term.c.y > 1) + tclearregion(0, 0, term.col-1, term.c.y-1); + tclearregion(0, term.c.y, term.c.x, term.c.y); + break; + case 2: /* all */ + tclearregion(0, 0, term.col-1, term.row-1); + break; + default: + goto unknown; + } + break; + case 'K': /* EL -- Clear line */ + switch (csiescseq.arg[0]) { + case 0: /* right */ + tclearregion(term.c.x, term.c.y, term.col-1, + term.c.y); + break; + case 1: /* left */ + tclearregion(0, term.c.y, term.c.x, term.c.y); + break; + case 2: /* all */ + tclearregion(0, term.c.y, term.col-1, term.c.y); + break; + } + break; + case 'S': /* SU -- Scroll <n> line up */ + DEFAULT(csiescseq.arg[0], 1); + tscrollup(term.top, csiescseq.arg[0], 0); + break; + case 'T': /* SD -- Scroll <n> line down */ + DEFAULT(csiescseq.arg[0], 1); + tscrolldown(term.top, csiescseq.arg[0], 0); + break; + case 'L': /* IL -- Insert <n> blank lines */ + DEFAULT(csiescseq.arg[0], 1); + tinsertblankline(csiescseq.arg[0]); + break; + case 'l': /* RM -- Reset Mode */ + tsetmode(csiescseq.priv, 0, csiescseq.arg, csiescseq.narg); + break; + case 'M': /* DL -- Delete <n> lines */ + DEFAULT(csiescseq.arg[0], 1); + tdeleteline(csiescseq.arg[0]); + break; + case 'X': /* ECH -- Erase <n> char */ + DEFAULT(csiescseq.arg[0], 1); + tclearregion(term.c.x, term.c.y, + term.c.x + csiescseq.arg[0] - 1, term.c.y); + break; + case 'P': /* DCH -- Delete <n> char */ + DEFAULT(csiescseq.arg[0], 1); + tdeletechar(csiescseq.arg[0]); + break; + case 'Z': /* CBT -- Cursor Backward Tabulation <n> tab stops */ + DEFAULT(csiescseq.arg[0], 1); + tputtab(-csiescseq.arg[0]); + break; + case 'd': /* VPA -- Move to <row> */ + DEFAULT(csiescseq.arg[0], 1); + tmoveato(term.c.x, csiescseq.arg[0]-1); + break; + case 'h': /* SM -- Set terminal mode */ + tsetmode(csiescseq.priv, 1, csiescseq.arg, csiescseq.narg); + break; + case 'm': /* SGR -- Terminal attribute (color) */ + tsetattr(csiescseq.arg, csiescseq.narg); + break; + case 'n': /* DSR – Device Status Report (cursor position) */ + if (csiescseq.arg[0] == 6) { + len = snprintf(buf, sizeof(buf), "\033[%i;%iR", + term.c.y+1, term.c.x+1); + ttywrite(buf, len, 0); + } + break; + case 'r': /* DECSTBM -- Set Scrolling Region */ + if (csiescseq.priv) { + goto unknown; + } else { + DEFAULT(csiescseq.arg[0], 1); + DEFAULT(csiescseq.arg[1], term.row); + tsetscroll(csiescseq.arg[0]-1, csiescseq.arg[1]-1); + tmoveato(0, 0); + } + break; + case 's': /* DECSC -- Save cursor position (ANSI.SYS) */ + tcursor(CURSOR_SAVE); + break; + case 'u': /* DECRC -- Restore cursor position (ANSI.SYS) */ + tcursor(CURSOR_LOAD); + break; + case ' ': + switch (csiescseq.mode[1]) { + case 'q': /* DECSCUSR -- Set Cursor Style */ + if (xsetcursor(csiescseq.arg[0])) + goto unknown; + break; + default: + goto unknown; + } + break; + } +} + +void +csidump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC["); + for (i = 0; i < csiescseq.len; i++) { + c = csiescseq.buf[i] & 0xff; + if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + putc('\n', stderr); +} + +void +csireset(void) +{ + memset(&csiescseq, 0, sizeof(csiescseq)); +} + +void +osc_color_response(int num, int index, int is_osc4) +{ + int n; + char buf[32]; + unsigned char r, g, b; + + if (xgetcolor(is_osc4 ? num : index, &r, &g, &b)) { + fprintf(stderr, "erresc: failed to fetch %s color %d\n", + is_osc4 ? "osc4" : "osc", + is_osc4 ? num : index); + return; + } + + n = snprintf(buf, sizeof buf, "\033]%s%d;rgb:%02x%02x/%02x%02x/%02x%02x\007", + is_osc4 ? "4;" : "", num, r, r, g, g, b, b); + if (n < 0 || n >= sizeof(buf)) { + fprintf(stderr, "error: %s while printing %s response\n", + n < 0 ? "snprintf failed" : "truncation occurred", + is_osc4 ? "osc4" : "osc"); + } else { + ttywrite(buf, n, 1); + } +} + +void +strhandle(void) +{ + char *p = NULL, *dec; + int j, narg, par; + const struct { int idx; char *str; } osc_table[] = { + { defaultfg, "foreground" }, + { defaultbg, "background" }, + { defaultcs, "cursor" } + }; + + term.esc &= ~(ESC_STR_END|ESC_STR); + strparse(); + par = (narg = strescseq.narg) ? atoi(strescseq.args[0]) : 0; + + switch (strescseq.type) { + case ']': /* OSC -- Operating System Command */ + switch (par) { + case 0: + if (narg > 1) { + xsettitle(strescseq.args[1]); + xseticontitle(strescseq.args[1]); + } + return; + case 1: + if (narg > 1) + xseticontitle(strescseq.args[1]); + return; + case 2: + if (narg > 1) + xsettitle(strescseq.args[1]); + return; + case 52: + if (narg > 2 && allowwindowops) { + dec = base64dec(strescseq.args[2]); + if (dec) { + xsetsel(dec); + xclipcopy(); + } else { + fprintf(stderr, "erresc: invalid base64\n"); + } + } + return; + case 10: + case 11: + case 12: + if (narg < 2) + break; + p = strescseq.args[1]; + if ((j = par - 10) < 0 || j >= LEN(osc_table)) + break; /* shouldn't be possible */ + + if (!strcmp(p, "?")) { + osc_color_response(par, osc_table[j].idx, 0); + } else if (xsetcolorname(osc_table[j].idx, p)) { + fprintf(stderr, "erresc: invalid %s color: %s\n", + osc_table[j].str, p); + } else { + tfulldirt(); + } + return; + case 4: /* color set */ + if (narg < 3) + break; + p = strescseq.args[2]; + /* FALLTHROUGH */ + case 104: /* color reset */ + j = (narg > 1) ? atoi(strescseq.args[1]) : -1; + + if (p && !strcmp(p, "?")) { + osc_color_response(j, 0, 1); + } else if (xsetcolorname(j, p)) { + if (par == 104 && narg <= 1) + return; /* color reset without parameter */ + fprintf(stderr, "erresc: invalid color j=%d, p=%s\n", + j, p ? p : "(null)"); + } else { + /* + * TODO if defaultbg color is changed, borders + * are dirty + */ + tfulldirt(); + } + return; + } + break; + case 'k': /* old title set compatibility */ + xsettitle(strescseq.args[0]); + return; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + return; + } + + fprintf(stderr, "erresc: unknown str "); + strdump(); +} + +void +strparse(void) +{ + int c; + char *p = strescseq.buf; + + strescseq.narg = 0; + strescseq.buf[strescseq.len] = '\0'; + + if (*p == '\0') + return; + + while (strescseq.narg < STR_ARG_SIZ) { + strescseq.args[strescseq.narg++] = p; + while ((c = *p) != ';' && c != '\0') + ++p; + if (c == '\0') + return; + *p++ = '\0'; + } +} + +void +externalpipe(const Arg *arg) +{ + int to[2]; + char buf[UTF_SIZ]; + void (*oldsigpipe)(int); + Glyph *bp, *end; + int lastpos, n, newline; + + if (pipe(to) == -1) + return; + + switch (fork()) { + case -1: + close(to[0]); + close(to[1]); + return; + case 0: + dup2(to[0], STDIN_FILENO); + close(to[0]); + close(to[1]); + execvp(((char **)arg->v)[0], (char **)arg->v); + fprintf(stderr, "st: execvp %s\n", ((char **)arg->v)[0]); + perror("failed"); + exit(0); + } + + close(to[0]); + /* ignore sigpipe for now, in case child exists early */ + oldsigpipe = signal(SIGPIPE, SIG_IGN); + newline = 0; + for (n = 0; n <= HISTSIZE + 2; n++) { + bp = TLINE_HIST(n); + lastpos = MIN(tlinehistlen(n) + 1, term.col) - 1; + if (lastpos < 0) + break; + if (lastpos == 0) + continue; + end = &bp[lastpos + 1]; + for (; bp < end; ++bp) + if (xwrite(to[1], buf, utf8encode(bp->u, buf)) < 0) + break; + if ((newline = TLINE_HIST(n)[lastpos].mode & ATTR_WRAP)) + continue; + if (xwrite(to[1], "\n", 1) < 0) + break; + newline = 0; + } + if (newline) + (void)xwrite(to[1], "\n", 1); + close(to[1]); + /* restore */ + signal(SIGPIPE, oldsigpipe); +} + +void +strdump(void) +{ + size_t i; + uint c; + + fprintf(stderr, "ESC%c", strescseq.type); + for (i = 0; i < strescseq.len; i++) { + c = strescseq.buf[i] & 0xff; + if (c == '\0') { + putc('\n', stderr); + return; + } else if (isprint(c)) { + putc(c, stderr); + } else if (c == '\n') { + fprintf(stderr, "(\\n)"); + } else if (c == '\r') { + fprintf(stderr, "(\\r)"); + } else if (c == 0x1b) { + fprintf(stderr, "(\\e)"); + } else { + fprintf(stderr, "(%02x)", c); + } + } + fprintf(stderr, "ESC\\\n"); +} + +void +strreset(void) +{ + strescseq = (STREscape){ + .buf = xrealloc(strescseq.buf, STR_BUF_SIZ), + .siz = STR_BUF_SIZ, + }; +} + +void +sendbreak(const Arg *arg) +{ + if (tcsendbreak(cmdfd, 0)) + perror("Error sending break"); +} + +void +tprinter(char *s, size_t len) +{ + if (iofd != -1 && xwrite(iofd, s, len) < 0) { + perror("Error writing to output file"); + close(iofd); + iofd = -1; + } +} + +void +toggleprinter(const Arg *arg) +{ + term.mode ^= MODE_PRINT; +} + +void +printscreen(const Arg *arg) +{ + tdump(); +} + +void +printsel(const Arg *arg) +{ + tdumpsel(); +} + +void +tdumpsel(void) +{ + char *ptr; + + if ((ptr = getsel())) { + tprinter(ptr, strlen(ptr)); + free(ptr); + } +} + +void +tdumpline(int n) +{ + char buf[UTF_SIZ]; + const Glyph *bp, *end; + + bp = &term.line[n][0]; + end = &bp[MIN(tlinelen(n), term.col) - 1]; + if (bp != end || bp->u != ' ') { + for ( ; bp <= end; ++bp) + tprinter(buf, utf8encode(bp->u, buf)); + } + tprinter("\n", 1); +} + +void +tdump(void) +{ + int i; + + for (i = 0; i < term.row; ++i) + tdumpline(i); +} + +void +tputtab(int n) +{ + uint x = term.c.x; + + if (n > 0) { + while (x < term.col && n--) + for (++x; x < term.col && !term.tabs[x]; ++x) + /* nothing */ ; + } else if (n < 0) { + while (x > 0 && n++) + for (--x; x > 0 && !term.tabs[x]; --x) + /* nothing */ ; + } + term.c.x = LIMIT(x, 0, term.col-1); +} + +void +tdefutf8(char ascii) +{ + if (ascii == 'G') + term.mode |= MODE_UTF8; + else if (ascii == '@') + term.mode &= ~MODE_UTF8; +} + +void +tdeftran(char ascii) +{ + static char cs[] = "0B"; + static int vcs[] = {CS_GRAPHIC0, CS_USA}; + char *p; + + if ((p = strchr(cs, ascii)) == NULL) { + fprintf(stderr, "esc unhandled charset: ESC ( %c\n", ascii); + } else { + term.trantbl[term.icharset] = vcs[p - cs]; + } +} + +void +tdectest(char c) +{ + int x, y; + + if (c == '8') { /* DEC screen alignment test. */ + for (x = 0; x < term.col; ++x) { + for (y = 0; y < term.row; ++y) + tsetchar('E', &term.c.attr, x, y); + } + } +} + +void +tstrsequence(uchar c) +{ + switch (c) { + case 0x90: /* DCS -- Device Control String */ + c = 'P'; + break; + case 0x9f: /* APC -- Application Program Command */ + c = '_'; + break; + case 0x9e: /* PM -- Privacy Message */ + c = '^'; + break; + case 0x9d: /* OSC -- Operating System Command */ + c = ']'; + break; + } + strreset(); + strescseq.type = c; + term.esc |= ESC_STR; +} + +void +tcontrolcode(uchar ascii) +{ + switch (ascii) { + case '\t': /* HT */ + tputtab(1); + return; + case '\b': /* BS */ + tmoveto(term.c.x-1, term.c.y); + return; + case '\r': /* CR */ + tmoveto(0, term.c.y); + return; + case '\f': /* LF */ + case '\v': /* VT */ + case '\n': /* LF */ + /* go to first col if the mode is set */ + tnewline(IS_SET(MODE_CRLF)); + return; + case '\a': /* BEL */ + if (term.esc & ESC_STR_END) { + /* backwards compatibility to xterm */ + strhandle(); + } else { + xbell(); + } + break; + case '\033': /* ESC */ + csireset(); + term.esc &= ~(ESC_CSI|ESC_ALTCHARSET|ESC_TEST); + term.esc |= ESC_START; + return; + case '\016': /* SO (LS1 -- Locking shift 1) */ + case '\017': /* SI (LS0 -- Locking shift 0) */ + term.charset = 1 - (ascii - '\016'); + return; + case '\032': /* SUB */ + tsetchar('?', &term.c.attr, term.c.x, term.c.y); + /* FALLTHROUGH */ + case '\030': /* CAN */ + csireset(); + break; + case '\005': /* ENQ (IGNORED) */ + case '\000': /* NUL (IGNORED) */ + case '\021': /* XON (IGNORED) */ + case '\023': /* XOFF (IGNORED) */ + case 0177: /* DEL (IGNORED) */ + return; + case 0x80: /* TODO: PAD */ + case 0x81: /* TODO: HOP */ + case 0x82: /* TODO: BPH */ + case 0x83: /* TODO: NBH */ + case 0x84: /* TODO: IND */ + break; + case 0x85: /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 0x86: /* TODO: SSA */ + case 0x87: /* TODO: ESA */ + break; + case 0x88: /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 0x89: /* TODO: HTJ */ + case 0x8a: /* TODO: VTS */ + case 0x8b: /* TODO: PLD */ + case 0x8c: /* TODO: PLU */ + case 0x8d: /* TODO: RI */ + case 0x8e: /* TODO: SS2 */ + case 0x8f: /* TODO: SS3 */ + case 0x91: /* TODO: PU1 */ + case 0x92: /* TODO: PU2 */ + case 0x93: /* TODO: STS */ + case 0x94: /* TODO: CCH */ + case 0x95: /* TODO: MW */ + case 0x96: /* TODO: SPA */ + case 0x97: /* TODO: EPA */ + case 0x98: /* TODO: SOS */ + case 0x99: /* TODO: SGCI */ + break; + case 0x9a: /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 0x9b: /* TODO: CSI */ + case 0x9c: /* TODO: ST */ + break; + case 0x90: /* DCS -- Device Control String */ + case 0x9d: /* OSC -- Operating System Command */ + case 0x9e: /* PM -- Privacy Message */ + case 0x9f: /* APC -- Application Program Command */ + tstrsequence(ascii); + return; + } + /* only CAN, SUB, \a and C1 chars interrupt a sequence */ + term.esc &= ~(ESC_STR_END|ESC_STR); +} + +/* + * returns 1 when the sequence is finished and it hasn't to read + * more characters for this sequence, otherwise 0 + */ +int +eschandle(uchar ascii) +{ + switch (ascii) { + case '[': + term.esc |= ESC_CSI; + return 0; + case '#': + term.esc |= ESC_TEST; + return 0; + case '%': + term.esc |= ESC_UTF8; + return 0; + case 'P': /* DCS -- Device Control String */ + case '_': /* APC -- Application Program Command */ + case '^': /* PM -- Privacy Message */ + case ']': /* OSC -- Operating System Command */ + case 'k': /* old title set compatibility */ + tstrsequence(ascii); + return 0; + case 'n': /* LS2 -- Locking shift 2 */ + case 'o': /* LS3 -- Locking shift 3 */ + term.charset = 2 + (ascii - 'n'); + break; + case '(': /* GZD4 -- set primary charset G0 */ + case ')': /* G1D4 -- set secondary charset G1 */ + case '*': /* G2D4 -- set tertiary charset G2 */ + case '+': /* G3D4 -- set quaternary charset G3 */ + term.icharset = ascii - '('; + term.esc |= ESC_ALTCHARSET; + return 0; + case 'D': /* IND -- Linefeed */ + if (term.c.y == term.bot) { + tscrollup(term.top, 1, 1); + } else { + tmoveto(term.c.x, term.c.y+1); + } + break; + case 'E': /* NEL -- Next line */ + tnewline(1); /* always go to first col */ + break; + case 'H': /* HTS -- Horizontal tab stop */ + term.tabs[term.c.x] = 1; + break; + case 'M': /* RI -- Reverse index */ + if (term.c.y == term.top) { + tscrolldown(term.top, 1, 1); + } else { + tmoveto(term.c.x, term.c.y-1); + } + break; + case 'Z': /* DECID -- Identify Terminal */ + ttywrite(vtiden, strlen(vtiden), 0); + break; + case 'c': /* RIS -- Reset to initial state */ + treset(); + resettitle(); + xloadcols(); + break; + case '=': /* DECPAM -- Application keypad */ + xsetmode(1, MODE_APPKEYPAD); + break; + case '>': /* DECPNM -- Normal keypad */ + xsetmode(0, MODE_APPKEYPAD); + break; + case '7': /* DECSC -- Save Cursor */ + tcursor(CURSOR_SAVE); + break; + case '8': /* DECRC -- Restore Cursor */ + tcursor(CURSOR_LOAD); + break; + case '\\': /* ST -- String Terminator */ + if (term.esc & ESC_STR_END) + strhandle(); + break; + default: + fprintf(stderr, "erresc: unknown sequence ESC 0x%02X '%c'\n", + (uchar) ascii, isprint(ascii)? ascii:'.'); + break; + } + return 1; +} + +void +tputc(Rune u) +{ + char c[UTF_SIZ]; + int control; + int width, len; + Glyph *gp; + + control = ISCONTROL(u); + if (u < 127 || !IS_SET(MODE_UTF8)) { + c[0] = u; + width = len = 1; + } else { + len = utf8encode(u, c); + if (!control && (width = wcwidth(u)) == -1) + width = 1; + } + + if (IS_SET(MODE_PRINT)) + tprinter(c, len); + + /* + * STR sequence must be checked before anything else + * because it uses all following characters until it + * receives a ESC, a SUB, a ST or any other C1 control + * character. + */ + if (term.esc & ESC_STR) { + if (u == '\a' || u == 030 || u == 032 || u == 033 || + ISCONTROLC1(u)) { + term.esc &= ~(ESC_START|ESC_STR); + term.esc |= ESC_STR_END; + goto check_control_code; + } + + if (strescseq.len+len >= strescseq.siz) { + /* + * Here is a bug in terminals. If the user never sends + * some code to stop the str or esc command, then st + * will stop responding. But this is better than + * silently failing with unknown characters. At least + * then users will report back. + * + * In the case users ever get fixed, here is the code: + */ + /* + * term.esc = 0; + * strhandle(); + */ + if (strescseq.siz > (SIZE_MAX - UTF_SIZ) / 2) + return; + strescseq.siz *= 2; + strescseq.buf = xrealloc(strescseq.buf, strescseq.siz); + } + + memmove(&strescseq.buf[strescseq.len], c, len); + strescseq.len += len; + return; + } + +check_control_code: + /* + * Actions of control codes must be performed as soon they arrive + * because they can be embedded inside a control sequence, and + * they must not cause conflicts with sequences. + */ + if (control) { + tcontrolcode(u); + /* + * control codes are not shown ever + */ + if (!term.esc) + term.lastc = 0; + return; + } else if (term.esc & ESC_START) { + if (term.esc & ESC_CSI) { + csiescseq.buf[csiescseq.len++] = u; + if (BETWEEN(u, 0x40, 0x7E) + || csiescseq.len >= \ + sizeof(csiescseq.buf)-1) { + term.esc = 0; + csiparse(); + csihandle(); + } + return; + } else if (term.esc & ESC_UTF8) { + tdefutf8(u); + } else if (term.esc & ESC_ALTCHARSET) { + tdeftran(u); + } else if (term.esc & ESC_TEST) { + tdectest(u); + } else { + if (!eschandle(u)) + return; + /* sequence already finished */ + } + term.esc = 0; + /* + * All characters which form part of a sequence are not + * printed + */ + return; + } + if (selected(term.c.x, term.c.y)) + selclear(); + + gp = &term.line[term.c.y][term.c.x]; + if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) { + gp->mode |= ATTR_WRAP; + tnewline(1); + gp = &term.line[term.c.y][term.c.x]; + } + + if (IS_SET(MODE_INSERT) && term.c.x+width < term.col) + memmove(gp+width, gp, (term.col - term.c.x - width) * sizeof(Glyph)); + + if (term.c.x+width > term.col) { + tnewline(1); + gp = &term.line[term.c.y][term.c.x]; + } + + tsetchar(u, &term.c.attr, term.c.x, term.c.y); + term.lastc = u; + + if (width == 2) { + gp->mode |= ATTR_WIDE; + if (term.c.x+1 < term.col) { + if (gp[1].mode == ATTR_WIDE && term.c.x+2 < term.col) { + gp[2].u = ' '; + gp[2].mode &= ~ATTR_WDUMMY; + } + gp[1].u = '\0'; + gp[1].mode = ATTR_WDUMMY; + } + } + if (term.c.x+width < term.col) { + tmoveto(term.c.x+width, term.c.y); + } else { + term.c.state |= CURSOR_WRAPNEXT; + } +} + +int +twrite(const char *buf, int buflen, int show_ctrl) +{ + int charsize; + Rune u; + int n; + + for (n = 0; n < buflen; n += charsize) { + if (IS_SET(MODE_UTF8)) { + /* process a complete utf8 char */ + charsize = utf8decode(buf + n, &u, buflen - n); + if (charsize == 0) + break; + } else { + u = buf[n] & 0xFF; + charsize = 1; + } + if (show_ctrl && ISCONTROL(u)) { + if (u & 0x80) { + u &= 0x7f; + tputc('^'); + tputc('['); + } else if (u != '\n' && u != '\r' && u != '\t') { + u ^= 0x40; + tputc('^'); + } + } + tputc(u); + } + return n; +} + +void +tresize(int col, int row) +{ + int i, j; + int tmp; + int minrow, mincol; + int *bp; + TCursor c; + + tmp = col; + if (!term.maxcol) + term.maxcol = term.col; + col = MAX(col, term.maxcol); + minrow = MIN(row, term.row); + mincol = MIN(col, term.maxcol); + + if (col < 1 || row < 1) { + fprintf(stderr, + "tresize: error resizing to %dx%d\n", col, row); + return; + } + + /* + * slide screen to keep cursor where we expect it - + * tscrollup would work here, but we can optimize to + * memmove because we're freeing the earlier lines + */ + for (i = 0; i <= term.c.y - row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + /* ensure that both src and dst are not NULL */ + if (i > 0) { + memmove(term.line, term.line + i, row * sizeof(Line)); + memmove(term.alt, term.alt + i, row * sizeof(Line)); + } + for (i += row; i < term.row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + + /* resize to new height */ + term.line = xrealloc(term.line, row * sizeof(Line)); + term.alt = xrealloc(term.alt, row * sizeof(Line)); + term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty)); + term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs)); + + for (i = 0; i < HISTSIZE; i++) { + term.hist[i] = xrealloc(term.hist[i], col * sizeof(Glyph)); + for (j = mincol; j < col; j++) { + term.hist[i][j] = term.c.attr; + term.hist[i][j].u = ' '; + } + } + + /* resize each row to new width, zero-pad if needed */ + for (i = 0; i < minrow; i++) { + term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph)); + term.alt[i] = xrealloc(term.alt[i], col * sizeof(Glyph)); + } + + /* allocate any new rows */ + for (/* i = minrow */; i < row; i++) { + term.line[i] = xmalloc(col * sizeof(Glyph)); + term.alt[i] = xmalloc(col * sizeof(Glyph)); + } + if (col > term.maxcol) { + bp = term.tabs + term.maxcol; + + memset(bp, 0, sizeof(*term.tabs) * (col - term.maxcol)); + while (--bp > term.tabs && !*bp) + /* nothing */ ; + for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces) + *bp = 1; + } + /* update terminal size */ + term.col = tmp; + term.maxcol = col; + term.row = row; + /* reset scrolling region */ + tsetscroll(0, row-1); + /* make use of the LIMIT in tmoveto */ + tmoveto(term.c.x, term.c.y); + /* Clearing both screens (it makes dirty all lines) */ + c = term.c; + for (i = 0; i < 2; i++) { + if (mincol < col && 0 < minrow) { + tclearregion(mincol, 0, col - 1, minrow - 1); + } + if (0 < col && minrow < row) { + tclearregion(0, minrow, col - 1, row - 1); + } + tswapscreen(); + tcursor(CURSOR_LOAD); + } + term.c = c; +} + +void +resettitle(void) +{ + xsettitle(NULL); +} + +void +drawregion(int x1, int y1, int x2, int y2) +{ + int y; + + for (y = y1; y < y2; y++) { + if (!term.dirty[y]) + continue; + + term.dirty[y] = 0; + xdrawline(TLINE(y), x1, y, x2); + } +} + +void +draw(void) +{ + int cx = term.c.x, ocx = term.ocx, ocy = term.ocy; + + if (!xstartdraw()) + return; + + /* adjust cursor position */ + LIMIT(term.ocx, 0, term.col-1); + LIMIT(term.ocy, 0, term.row-1); + if (term.line[term.ocy][term.ocx].mode & ATTR_WDUMMY) + term.ocx--; + if (term.line[term.c.y][cx].mode & ATTR_WDUMMY) + cx--; + + drawregion(0, 0, term.col, term.row); + if (term.scr == 0) + xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], + term.ocx, term.ocy, term.line[term.ocy][term.ocx], + term.line[term.ocy], term.col); + /* xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], */ + /* term.ocx, term.ocy, term.line[term.ocy][term.ocx], */ + /* term.line[term.ocy], term.col); */ + term.ocx = cx; + term.ocy = term.c.y; + xfinishdraw(); + if (ocx != term.ocx || ocy != term.ocy) + xximspot(term.ocx, term.ocy); +} + +void +redraw(void) +{ + tfulldirt(); + draw(); +} + diff --git a/mut/st/st.h b/mut/st/st.h new file mode 100644 index 0000000..60d973a --- /dev/null +++ b/mut/st/st.h @@ -0,0 +1,146 @@ +/* See LICENSE for license details. */ + +#include <stdint.h> +#include <sys/types.h> + +/* macros */ +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define MAX(a, b) ((a) < (b) ? (b) : (a)) +#define LEN(a) (sizeof(a) / sizeof(a)[0]) +#define BETWEEN(x, a, b) ((a) <= (x) && (x) <= (b)) +#define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d)) +#define DEFAULT(a, b) (a) = (a) ? (a) : (b) +#define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) +#define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP) & (~ATTR_LIGA)) != ((b).mode & (~ATTR_WRAP) & (~ATTR_LIGA)) || \ + (a).fg != (b).fg || \ + (a).bg != (b).bg) +#define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ + (t1.tv_nsec-t2.tv_nsec)/1E6) +#define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) + +#define TRUECOLOR(r,g,b) (1 << 24 | (r) << 16 | (g) << 8 | (b)) +#define IS_TRUECOL(x) (1 << 24 & (x)) + +enum glyph_attribute { + ATTR_NULL = 0, + ATTR_BOLD = 1 << 0, + ATTR_FAINT = 1 << 1, + ATTR_ITALIC = 1 << 2, + ATTR_UNDERLINE = 1 << 3, + ATTR_BLINK = 1 << 4, + ATTR_REVERSE = 1 << 5, + ATTR_INVISIBLE = 1 << 6, + ATTR_STRUCK = 1 << 7, + ATTR_WRAP = 1 << 8, + ATTR_WIDE = 1 << 9, + ATTR_WDUMMY = 1 << 10, + ATTR_BOXDRAW = 1 << 11, + ATTR_LIGA = 1 << 12, + ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, +}; + +enum selection_mode { + SEL_IDLE = 0, + SEL_EMPTY = 1, + SEL_READY = 2 +}; + +enum selection_type { + SEL_REGULAR = 1, + SEL_RECTANGULAR = 2 +}; + +enum selection_snap { + SNAP_WORD = 1, + SNAP_LINE = 2 +}; + +typedef unsigned char uchar; +typedef unsigned int uint; +typedef unsigned long ulong; +typedef unsigned short ushort; + +typedef uint_least32_t Rune; + +#define Glyph Glyph_ +typedef struct { + Rune u; /* character code */ + ushort mode; /* attribute flags */ + uint32_t fg; /* foreground */ + uint32_t bg; /* background */ +} Glyph; + +typedef Glyph *Line; + +typedef union { + int i; + uint ui; + float f; + const void *v; + const char *s; +} Arg; + +void die(const char *, ...); +void redraw(void); +void tfulldirt(void); +void draw(void); + +void externalpipe(const Arg *); +void kscrolldown(const Arg *); +void kscrollup(const Arg *); + +void printscreen(const Arg *); +void printsel(const Arg *); +void sendbreak(const Arg *); +void toggleprinter(const Arg *); + +int tattrset(int); +void tnew(int, int); +void tresize(int, int); +void tsetdirtattr(int); +void ttyhangup(void); +int ttynew(const char *, char *, const char *, char **); +size_t ttyread(void); +void ttyresize(int, int); +void ttywrite(const char *, size_t, int); + +void resettitle(void); + +void selclear(void); +void selinit(void); +void selstart(int, int, int); +void selextend(int, int, int, int); +int selected(int, int); +char *getsel(void); + +size_t utf8encode(Rune, char *); + +void *xmalloc(size_t); +void *xrealloc(void *, size_t); +char *xstrdup(const char *); + +int isboxdraw(Rune); +ushort boxdrawindex(const Glyph *); +#ifdef XFT_VERSION +/* only exposed to x.c, otherwise we'll need Xft.h for the types */ +void boxdraw_xinit(Display *, Colormap, XftDraw *, Visual *); +void drawboxes(int, int, int, int, XftColor *, XftColor *, const XftGlyphFontSpec *, int); +#endif + +/* config.h globals */ +extern char *utmp; +extern char *scroll; +extern char *stty_args; +extern char *vtiden; +extern wchar_t *worddelimiters; +extern int allowaltscreen; +extern int allowwindowops; +extern char *termname; +extern unsigned int tabspaces; +extern unsigned int defaultfg; +extern unsigned int defaultbg; +extern float alpha; +extern float alphaUnfocus; +extern const int boxdraw, boxdraw_bold, boxdraw_braille; +extern unsigned int defaultcs; + diff --git a/mut/st/st.info b/mut/st/st.info new file mode 100644 index 0000000..8201ad6 --- /dev/null +++ b/mut/st/st.info @@ -0,0 +1,239 @@ +st-mono| simpleterm monocolor, + acsc=+C\,D-A.B0E``aaffgghFiGjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, + am, + bce, + bel=^G, + blink=\E[5m, + bold=\E[1m, + cbt=\E[Z, + cvvis=\E[?25h, + civis=\E[?25l, + clear=\E[H\E[2J, + cnorm=\E[?12l\E[?25h, + colors#2, + cols#80, + cr=^M, + csr=\E[%i%p1%d;%p2%dr, + cub=\E[%p1%dD, + cub1=^H, + cud1=^J, + cud=\E[%p1%dB, + cuf1=\E[C, + cuf=\E[%p1%dC, + cup=\E[%i%p1%d;%p2%dH, + cuu1=\E[A, + cuu=\E[%p1%dA, + dch=\E[%p1%dP, + dch1=\E[P, + dim=\E[2m, + dl=\E[%p1%dM, + dl1=\E[M, + ech=\E[%p1%dX, + ed=\E[J, + el=\E[K, + el1=\E[1K, + enacs=\E)0, + flash=\E[?5h$<80/>\E[?5l, + fsl=^G, + home=\E[H, + hpa=\E[%i%p1%dG, + hs, + ht=^I, + hts=\EH, + ich=\E[%p1%d@, + il1=\E[L, + il=\E[%p1%dL, + ind=^J, + indn=\E[%p1%dS, + invis=\E[8m, + is2=\E[4l\E>\E[?1034l, + it#8, + kel=\E[1;2F, + ked=\E[1;5F, + ka1=\E[1~, + ka3=\E[5~, + kc1=\E[4~, + kc3=\E[6~, + kbs=\177, + kcbt=\E[Z, + kb2=\EOu, + kcub1=\EOD, + kcud1=\EOB, + kcuf1=\EOC, + kcuu1=\EOA, + kDC=\E[3;2~, + kent=\EOM, + kEND=\E[1;2F, + kIC=\E[2;2~, + kNXT=\E[6;2~, + kPRV=\E[5;2~, + kHOM=\E[1;2H, + kLFT=\E[1;2D, + kRIT=\E[1;2C, + kind=\E[1;2B, + kri=\E[1;2A, + kclr=\E[3;5~, + kdl1=\E[3;2~, + kdch1=\E[3~, + kich1=\E[2~, + kend=\E[4~, + kf1=\EOP, + kf2=\EOQ, + kf3=\EOR, + kf4=\EOS, + kf5=\E[15~, + kf6=\E[17~, + kf7=\E[18~, + kf8=\E[19~, + kf9=\E[20~, + kf10=\E[21~, + kf11=\E[23~, + kf12=\E[24~, + kf13=\E[1;2P, + kf14=\E[1;2Q, + kf15=\E[1;2R, + kf16=\E[1;2S, + kf17=\E[15;2~, + kf18=\E[17;2~, + kf19=\E[18;2~, + kf20=\E[19;2~, + kf21=\E[20;2~, + kf22=\E[21;2~, + kf23=\E[23;2~, + kf24=\E[24;2~, + kf25=\E[1;5P, + kf26=\E[1;5Q, + kf27=\E[1;5R, + kf28=\E[1;5S, + kf29=\E[15;5~, + kf30=\E[17;5~, + kf31=\E[18;5~, + kf32=\E[19;5~, + kf33=\E[20;5~, + kf34=\E[21;5~, + kf35=\E[23;5~, + kf36=\E[24;5~, + kf37=\E[1;6P, + kf38=\E[1;6Q, + kf39=\E[1;6R, + kf40=\E[1;6S, + kf41=\E[15;6~, + kf42=\E[17;6~, + kf43=\E[18;6~, + kf44=\E[19;6~, + kf45=\E[20;6~, + kf46=\E[21;6~, + kf47=\E[23;6~, + kf48=\E[24;6~, + kf49=\E[1;3P, + kf50=\E[1;3Q, + kf51=\E[1;3R, + kf52=\E[1;3S, + kf53=\E[15;3~, + kf54=\E[17;3~, + kf55=\E[18;3~, + kf56=\E[19;3~, + kf57=\E[20;3~, + kf58=\E[21;3~, + kf59=\E[23;3~, + kf60=\E[24;3~, + kf61=\E[1;4P, + kf62=\E[1;4Q, + kf63=\E[1;4R, + khome=\E[1~, + kil1=\E[2;5~, + krmir=\E[2;2~, + knp=\E[6~, + kmous=\E[M, + kpp=\E[5~, + lines#24, + mir, + msgr, + npc, + op=\E[39;49m, + pairs#64, + mc0=\E[i, + mc4=\E[4i, + mc5=\E[5i, + rc=\E8, + rev=\E[7m, + ri=\EM, + rin=\E[%p1%dT, + ritm=\E[23m, + rmacs=\E(B, + rmcup=\E[?1049l, + rmir=\E[4l, + rmkx=\E[?1l\E>, + rmso=\E[27m, + rmul=\E[24m, + rs1=\Ec, + rs2=\E[4l\E>\E[?1034l, + sc=\E7, + sitm=\E[3m, + sgr0=\E[0m, + smacs=\E(0, + smcup=\E[?1049h, + smir=\E[4h, + smkx=\E[?1h\E=, + smso=\E[7m, + smul=\E[4m, + tbc=\E[3g, + tsl=\E]0;, + xenl, + vpa=\E[%i%p1%dd, +# XTerm extensions + rmxx=\E[29m, + smxx=\E[9m, +# disabled rep for now: causes some issues with older ncurses versions. +# rep=%p1%c\E[%p2%{1}%-%db, +# tmux extensions, see TERMINFO EXTENSIONS in tmux(1) + Tc, + Ms=\E]52;%p1%s;%p2%s\007, + Se=\E[2 q, + Ss=\E[%p1%d q, + +st| simpleterm, + use=st-mono, + colors#8, + setab=\E[4%p1%dm, + setaf=\E[3%p1%dm, + setb=\E[4%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m, + setf=\E[3%?%p1%{1}%=%t4%e%p1%{3}%=%t6%e%p1%{4}%=%t1%e%p1%{6}%=%t3%e%p1%d%;m, + sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, + +st-256color| simpleterm with 256 colors, + use=st, + ccc, + colors#256, + oc=\E]104\007, + pairs#32767, +# Nicked from xterm-256color + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, + +st-meta| simpleterm with meta key, + use=st, + km, + rmm=\E[?1034l, + smm=\E[?1034h, + rs2=\E[4l\E>\E[?1034h, + is2=\E[4l\E>\E[?1034h, + +st-meta-256color| simpleterm with meta key and 256 colors, + use=st-256color, + km, + rmm=\E[?1034l, + smm=\E[?1034h, + rs2=\E[4l\E>\E[?1034h, + is2=\E[4l\E>\E[?1034h, + +st-bs| simpleterm with backspace as backspace, + use=st, + kbs=\010, + kdch1=\177, + +st-bs-256color| simpleterm with backspace as backspace and 256colors, + use=st-256color, + kbs=\010, + kdch1=\177, diff --git a/mut/st/win.h b/mut/st/win.h new file mode 100644 index 0000000..8839e31 --- /dev/null +++ b/mut/st/win.h @@ -0,0 +1,42 @@ +/* See LICENSE for license details. */ + +enum win_mode { + MODE_VISIBLE = 1 << 0, + MODE_FOCUSED = 1 << 1, + MODE_APPKEYPAD = 1 << 2, + MODE_MOUSEBTN = 1 << 3, + MODE_MOUSEMOTION = 1 << 4, + MODE_REVERSE = 1 << 5, + MODE_KBDLOCK = 1 << 6, + MODE_HIDE = 1 << 7, + MODE_APPCURSOR = 1 << 8, + MODE_MOUSESGR = 1 << 9, + MODE_8BIT = 1 << 10, + MODE_BLINK = 1 << 11, + MODE_FBLINK = 1 << 12, + MODE_FOCUS = 1 << 13, + MODE_MOUSEX10 = 1 << 14, + MODE_MOUSEMANY = 1 << 15, + MODE_BRCKTPASTE = 1 << 16, + MODE_NUMLOCK = 1 << 17, + MODE_MOUSE = MODE_MOUSEBTN|MODE_MOUSEMOTION|MODE_MOUSEX10\ + |MODE_MOUSEMANY, +}; + +void xbell(void); +void xclipcopy(void); +void xdrawcursor(int, int, Glyph, int, int, Glyph, Line, int); +void xdrawline(Line, int, int, int); +void xfinishdraw(void); +void xloadcols(void); +int xsetcolorname(int, const char *); +int xgetcolor(int, unsigned char *, unsigned char *, unsigned char *); +void xseticontitle(char *); +void xsettitle(char *); +int xsetcursor(int); +void xsetmode(int, unsigned int); +void xsetpointermotion(int); +void xsetsel(char *); +int xstartdraw(void); +void xximspot(int, int); + diff --git a/mut/st/x.c b/mut/st/x.c new file mode 100644 index 0000000..e2ec9db --- /dev/null +++ b/mut/st/x.c @@ -0,0 +1,2366 @@ +/* See LICENSE for license details. */ +#include <errno.h> +#include <math.h> +#include <limits.h> +#include <locale.h> +#include <signal.h> +#include <sys/select.h> +#include <time.h> +#include <unistd.h> +#include <libgen.h> +#include <X11/Xatom.h> +#include <X11/Xlib.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> +#include <X11/Xft/Xft.h> +#include <X11/XKBlib.h> +#include <X11/Xresource.h> + +char *argv0; +#include "arg.h" +#include "st.h" +#include "win.h" +#include "hb.h" + +/* types used in config.h */ +typedef struct { + uint mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Shortcut; + +typedef struct { + uint mod; + uint button; + void (*func)(const Arg *); + const Arg arg; + uint release; +} MouseShortcut; + +typedef struct { + KeySym k; + uint mask; + char *s; + /* three-valued logic variables: 0 indifferent, 1 on, -1 off */ + signed char appkey; /* application keypad */ + signed char appcursor; /* application cursor */ +} Key; + +/* Xresources preferences */ +enum resource_type { + STRING = 0, + INTEGER = 1, + FLOAT = 2 +}; + +typedef struct { + char *name; + enum resource_type type; + void *dst; +} ResourcePref; + +/* X modifiers */ +#define XK_ANY_MOD UINT_MAX +#define XK_NO_MOD 0 +#define XK_SWITCH_MOD (1<<13|1<<14) + +/* function definitions used in config.h */ +static void clipcopy(const Arg *); +static void clippaste(const Arg *); +static void numlock(const Arg *); +static void selpaste(const Arg *); +static void changealpha(const Arg *); +static void zoom(const Arg *); +static void zoomabs(const Arg *); +static void zoomreset(const Arg *); +static void ttysend(const Arg *); + +/* config.h for applying patches and the configuration. */ +#include "config.h" + +/* XEMBED messages */ +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 + +/* macros */ +#define IS_SET(flag) ((win.mode & (flag)) != 0) +#define TRUERED(x) (((x) & 0xff0000) >> 8) +#define TRUEGREEN(x) (((x) & 0xff00)) +#define TRUEBLUE(x) (((x) & 0xff) << 8) + +typedef XftDraw *Draw; +typedef XftColor Color; +typedef XftGlyphFontSpec GlyphFontSpec; + +/* Purely graphic info */ +typedef struct { + int tw, th; /* tty width and height */ + int w, h; /* window width and height */ + int ch; /* char height */ + int cw; /* char width */ + int mode; /* window state/mode flags */ + int cursor; /* cursor style */ +} TermWindow; + +typedef struct { + Display *dpy; + Colormap cmap; + Window win; + Drawable buf; + GlyphFontSpec *specbuf; /* font spec buffer used for rendering */ + Atom xembed, wmdeletewin, netwmname, netwmiconname, netwmpid; + struct { + XIM xim; + XIC xic; + XPoint spot; + XVaNestedList spotlist; + } ime; + Draw draw; + Visual *vis; + XSetWindowAttributes attrs; + int scr; + int isfixed; /* is fixed geometry? */ + int depth; /* bit depth */ + int l, t; /* left and top offset */ + int gm; /* geometry mask */ +} XWindow; + +typedef struct { + Atom xtarget; + char *primary, *clipboard; + struct timespec tclick1; + struct timespec tclick2; +} XSelection; + +/* Font structure */ +#define Font Font_ +typedef struct { + int height; + int width; + int ascent; + int descent; + int badslant; + int badweight; + short lbearing; + short rbearing; + XftFont *match; + FcFontSet *set; + FcPattern *pattern; +} Font; + +/* Drawing Context */ +typedef struct { + Color *col; + size_t collen; + Font font, bfont, ifont, ibfont; + GC gc; +} DC; + +static inline ushort sixd_to_16bit(int); +static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int); +static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int); +static void xdrawglyph(Glyph, int, int); +static void xclear(int, int, int, int); +static int xgeommasktogravity(int); +static int ximopen(Display *); +static void ximinstantiate(Display *, XPointer, XPointer); +static void ximdestroy(XIM, XPointer, XPointer); +static int xicdestroy(XIC, XPointer, XPointer); +static void xinit(int, int); +static void cresize(int, int); +static void xresize(int, int); +static void xhints(void); +static int xloadcolor(int, const char *, Color *); +static int xloadfont(Font *, FcPattern *); +static int xloadsparefont(FcPattern *, int); +static void xloadsparefonts(void); +static void xloadfonts(const char *, double); +static void xunloadfont(Font *); +static void xunloadfonts(void); +static void xsetenv(void); +static void xseturgency(int); +static int evcol(XEvent *); +static int evrow(XEvent *); +static float clamp(float, float, float); + +static void expose(XEvent *); +static void visibility(XEvent *); +static void unmap(XEvent *); +static void kpress(XEvent *); +static void cmessage(XEvent *); +static void resize(XEvent *); +static void focus(XEvent *); +static uint buttonmask(uint); +static int mouseaction(XEvent *, uint); +static void brelease(XEvent *); +static void bpress(XEvent *); +static void bmotion(XEvent *); +static void propnotify(XEvent *); +static void selnotify(XEvent *); +static void selclear_(XEvent *); +static void selrequest(XEvent *); +static void setsel(char *, Time); +static void mousesel(XEvent *, int); +static void mousereport(XEvent *); +static char *kmap(KeySym, uint); +static int match(uint, uint); + +static void run(void); +static void usage(void); + +static void (*handler[LASTEvent])(XEvent *) = { + [KeyPress] = kpress, + [ClientMessage] = cmessage, + [ConfigureNotify] = resize, + [VisibilityNotify] = visibility, + [UnmapNotify] = unmap, + [Expose] = expose, + [FocusIn] = focus, + [FocusOut] = focus, + [MotionNotify] = bmotion, + [ButtonPress] = bpress, + [ButtonRelease] = brelease, +/* + * Uncomment if you want the selection to disappear when you select something + * different in another window. + */ +/* [SelectionClear] = selclear_, */ + [SelectionNotify] = selnotify, +/* + * PropertyNotify is only turned on when there is some INCR transfer happening + * for the selection retrieval. + */ + [PropertyNotify] = propnotify, + [SelectionRequest] = selrequest, +}; + +/* Globals */ +static DC dc; +static XWindow xw; +static XSelection xsel; +static TermWindow win; + +/* Font Ring Cache */ +enum { + FRC_NORMAL, + FRC_ITALIC, + FRC_BOLD, + FRC_ITALICBOLD +}; + +typedef struct { + XftFont *font; + int flags; + Rune unicodep; +} Fontcache; + +/* Fontcache is an array now. A new font will be appended to the array. */ +static Fontcache *frc = NULL; +static int frclen = 0; +static int frccap = 0; +static char *usedfont = NULL; +static double usedfontsize = 0; +static double defaultfontsize = 0; + +static char *opt_alpha = NULL; +static char *opt_class = NULL; +static char **opt_cmd = NULL; +static char *opt_embed = NULL; +static char *opt_font = NULL; +static char *opt_io = NULL; +static char *opt_line = NULL; +static char *opt_name = NULL; +static char *opt_title = NULL; + +static int focused = 0; + +static int oldbutton = 3; /* button event on startup: 3 = release */ +static uint buttons; /* bit field of pressed buttons */ + +void +clipcopy(const Arg *dummy) +{ + Atom clipboard; + + free(xsel.clipboard); + xsel.clipboard = NULL; + + if (xsel.primary != NULL) { + xsel.clipboard = xstrdup(xsel.primary); + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XSetSelectionOwner(xw.dpy, clipboard, xw.win, CurrentTime); + } +} + +void +clippaste(const Arg *dummy) +{ + Atom clipboard; + + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + XConvertSelection(xw.dpy, clipboard, xsel.xtarget, clipboard, + xw.win, CurrentTime); +} + +void +selpaste(const Arg *dummy) +{ + XConvertSelection(xw.dpy, XA_PRIMARY, xsel.xtarget, XA_PRIMARY, + xw.win, CurrentTime); +} + +void +numlock(const Arg *dummy) +{ + win.mode ^= MODE_NUMLOCK; +} + +void +changealpha(const Arg *arg) +{ + if((alpha > 0 && arg->f < 0) || (alpha < 1 && arg->f > 0)) + alpha += arg->f; + alpha = clamp(alpha, 0.0, 1.0); + alphaUnfocus = clamp(alpha-alphaOffset, 0.0, 1.0); + + xloadcols(); + redraw(); +} + +void +zoom(const Arg *arg) +{ + Arg larg; + + larg.f = usedfontsize + arg->f; + zoomabs(&larg); +} + +void +zoomabs(const Arg *arg) +{ + xunloadfonts(); + xloadfonts(usedfont, arg->f); + xloadsparefonts(); + cresize(0, 0); + redraw(); + xhints(); +} + +void +zoomreset(const Arg *arg) +{ + Arg larg; + + if (defaultfontsize > 0) { + larg.f = defaultfontsize; + zoomabs(&larg); + } +} + +void +ttysend(const Arg *arg) +{ + ttywrite(arg->s, strlen(arg->s), 1); +} + +int +evcol(XEvent *e) +{ + int x = e->xbutton.x - borderpx; + LIMIT(x, 0, win.tw - 1); + return x / win.cw; +} + +int +evrow(XEvent *e) +{ + int y = e->xbutton.y - borderpx; + LIMIT(y, 0, win.th - 1); + return y / win.ch; +} + +float +clamp(float value, float lower, float upper) { + if(value < lower) + return lower; + if(value > upper) + return upper; + return value; +} + +void +mousesel(XEvent *e, int done) +{ + int type, seltype = SEL_REGULAR; + uint state = e->xbutton.state & ~(Button1Mask | forcemousemod); + + for (type = 1; type < LEN(selmasks); ++type) { + if (match(selmasks[type], state)) { + seltype = type; + break; + } + } + selextend(evcol(e), evrow(e), seltype, done); + if (done) + setsel(getsel(), e->xbutton.time); +} + +void +mousereport(XEvent *e) +{ + int len, x = evcol(e), y = evrow(e), + button = e->xbutton.button, state = e->xbutton.state; + char buf[40]; + static int ox, oy; + + /* from urxvt */ + if (e->xbutton.type == MotionNotify) { + if (x == ox && y == oy) + return; + if (!IS_SET(MODE_MOUSEMOTION) && !IS_SET(MODE_MOUSEMANY)) + return; + /* MOUSE_MOTION: no reporting if no button is pressed */ + if (IS_SET(MODE_MOUSEMOTION) && oldbutton == 3) + return; + + button = oldbutton + 32; + ox = x; + oy = y; + } else { + if (!IS_SET(MODE_MOUSESGR) && e->xbutton.type == ButtonRelease) { + button = 3; + } else { + button -= Button1; + if (button >= 3) + button += 64 - 3; + } + if (e->xbutton.type == ButtonPress) { + oldbutton = button; + ox = x; + oy = y; + } else if (e->xbutton.type == ButtonRelease) { + oldbutton = 3; + /* MODE_MOUSEX10: no button release reporting */ + if (IS_SET(MODE_MOUSEX10)) + return; + if (button == 64 || button == 65) + return; + } + } + + if (!IS_SET(MODE_MOUSEX10)) { + button += ((state & ShiftMask ) ? 4 : 0) + + ((state & Mod4Mask ) ? 8 : 0) + + ((state & ControlMask) ? 16 : 0); + } + + if (IS_SET(MODE_MOUSESGR)) { + len = snprintf(buf, sizeof(buf), "\033[<%d;%d;%d%c", + button, x+1, y+1, + e->type == ButtonRelease ? 'm' : 'M'); + } else if (x < 223 && y < 223) { + len = snprintf(buf, sizeof(buf), "\033[M%c%c%c", + 32+button, 32+x+1, 32+y+1); + } else { + return; + } + + ttywrite(buf, len, 0); +} + +uint +buttonmask(uint button) +{ + return button == Button1 ? Button1Mask + : button == Button2 ? Button2Mask + : button == Button3 ? Button3Mask + : button == Button4 ? Button4Mask + : button == Button5 ? Button5Mask + : 0; +} + +int +mouseaction(XEvent *e, uint release) +{ + MouseShortcut *ms; + + /* ignore Button<N>mask for Button<N> - it's set on release */ + uint state = e->xbutton.state & ~buttonmask(e->xbutton.button); + + for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) { + if (ms->release == release && + ms->button == e->xbutton.button && + (match(ms->mod, state) || /* exact or forced */ + match(ms->mod, state & ~forcemousemod))) { + ms->func(&(ms->arg)); + return 1; + } + } + + return 0; +} + +void +bpress(XEvent *e) +{ + int btn = e->xbutton.button; + struct timespec now; + int snap; + + if (1 <= btn && btn <= 11) + buttons |= 1 << (btn-1); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 0)) + return; + + if (btn == Button1) { + /* + * If the user clicks below predefined timeouts specific + * snapping behaviour is exposed. + */ + clock_gettime(CLOCK_MONOTONIC, &now); + if (TIMEDIFF(now, xsel.tclick2) <= tripleclicktimeout) { + snap = SNAP_LINE; + } else if (TIMEDIFF(now, xsel.tclick1) <= doubleclicktimeout) { + snap = SNAP_WORD; + } else { + snap = 0; + } + xsel.tclick2 = xsel.tclick1; + xsel.tclick1 = now; + + selstart(evcol(e), evrow(e), snap); + } +} + +void +propnotify(XEvent *e) +{ + XPropertyEvent *xpev; + Atom clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + + xpev = &e->xproperty; + if (xpev->state == PropertyNewValue && + (xpev->atom == XA_PRIMARY || + xpev->atom == clipboard)) { + selnotify(e); + } +} + +void +selnotify(XEvent *e) +{ + ulong nitems, ofs, rem; + int format; + uchar *data, *last, *repl; + Atom type, incratom, property = None; + + incratom = XInternAtom(xw.dpy, "INCR", 0); + + ofs = 0; + if (e->type == SelectionNotify) + property = e->xselection.property; + else if (e->type == PropertyNotify) + property = e->xproperty.atom; + + if (property == None) + return; + + do { + if (XGetWindowProperty(xw.dpy, xw.win, property, ofs, + BUFSIZ/4, False, AnyPropertyType, + &type, &format, &nitems, &rem, + &data)) { + fprintf(stderr, "Clipboard allocation failed\n"); + return; + } + + if (e->type == PropertyNotify && nitems == 0 && rem == 0) { + /* + * If there is some PropertyNotify with no data, then + * this is the signal of the selection owner that all + * data has been transferred. We won't need to receive + * PropertyNotify events anymore. + */ + MODBIT(xw.attrs.event_mask, 0, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + } + + if (type == incratom) { + /* + * Activate the PropertyNotify events so we receive + * when the selection owner does send us the next + * chunk of data. + */ + MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, + &xw.attrs); + + /* + * Deleting the property is the transfer start signal. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); + continue; + } + + /* + * As seen in getsel: + * Line endings are inconsistent in the terminal and GUI world + * copy and pasting. When receiving some selection data, + * replace all '\n' with '\r'. + * FIXME: Fix the computer world. + */ + repl = data; + last = data + nitems * format / 8; + while ((repl = memchr(repl, '\n', last - repl))) { + *repl++ = '\r'; + } + + if (IS_SET(MODE_BRCKTPASTE) && ofs == 0) + ttywrite("\033[200~", 6, 0); + ttywrite((char *)data, nitems * format / 8, 1); + if (IS_SET(MODE_BRCKTPASTE) && rem == 0) + ttywrite("\033[201~", 6, 0); + XFree(data); + /* number of 32-bit chunks returned */ + ofs += nitems * format / 32; + } while (rem > 0); + + /* + * Deleting the property again tells the selection owner to send the + * next data chunk in the property. + */ + XDeleteProperty(xw.dpy, xw.win, (int)property); +} + +void +xclipcopy(void) +{ + clipcopy(NULL); +} + +void +selclear_(XEvent *e) +{ + selclear(); +} + +void +selrequest(XEvent *e) +{ + XSelectionRequestEvent *xsre; + XSelectionEvent xev; + Atom xa_targets, string, clipboard; + char *seltext; + + xsre = (XSelectionRequestEvent *) e; + xev.type = SelectionNotify; + xev.requestor = xsre->requestor; + xev.selection = xsre->selection; + xev.target = xsre->target; + xev.time = xsre->time; + if (xsre->property == None) + xsre->property = xsre->target; + + /* reject */ + xev.property = None; + + xa_targets = XInternAtom(xw.dpy, "TARGETS", 0); + if (xsre->target == xa_targets) { + /* respond with the supported type */ + string = xsel.xtarget; + XChangeProperty(xsre->display, xsre->requestor, xsre->property, + XA_ATOM, 32, PropModeReplace, + (uchar *) &string, 1); + xev.property = xsre->property; + } else if (xsre->target == xsel.xtarget || xsre->target == XA_STRING) { + /* + * xith XA_STRING non ascii characters may be incorrect in the + * requestor. It is not our problem, use utf8. + */ + clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0); + if (xsre->selection == XA_PRIMARY) { + seltext = xsel.primary; + } else if (xsre->selection == clipboard) { + seltext = xsel.clipboard; + } else { + fprintf(stderr, + "Unhandled clipboard selection 0x%lx\n", + xsre->selection); + return; + } + if (seltext != NULL) { + XChangeProperty(xsre->display, xsre->requestor, + xsre->property, xsre->target, + 8, PropModeReplace, + (uchar *)seltext, strlen(seltext)); + xev.property = xsre->property; + } + } + + /* all done, send a notification to the listener */ + if (!XSendEvent(xsre->display, xsre->requestor, 1, 0, (XEvent *) &xev)) + fprintf(stderr, "Error sending SelectionNotify event\n"); +} + +void +setsel(char *str, Time t) +{ + if (!str) + return; + + free(xsel.primary); + xsel.primary = str; + + XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t); + if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win) + selclear(); +} + +void +xsetsel(char *str) +{ + setsel(str, CurrentTime); +} + +void +brelease(XEvent *e) +{ + int btn = e->xbutton.button; + + if (1 <= btn && btn <= 11) + buttons &= ~(1 << (btn-1)); + + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + if (mouseaction(e, 1)) + return; + if (btn == Button1) + mousesel(e, 1); +} + +void +bmotion(XEvent *e) +{ + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + + mousesel(e, 0); +} + +void +cresize(int width, int height) +{ + int col, row; + + if (width != 0) + win.w = width; + if (height != 0) + win.h = height; + + col = (win.w - 2 * borderpx) / win.cw; + row = (win.h - 2 * borderpx) / win.ch; + col = MAX(1, col); + row = MAX(1, row); + + tresize(col, row); + xresize(col, row); + ttyresize(win.tw, win.th); +} + +void +xresize(int col, int row) +{ + win.tw = col * win.cw; + win.th = row * win.ch; + + XFreePixmap(xw.dpy, xw.buf); + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, + xw.depth); + XftDrawChange(xw.draw, xw.buf); + xclear(0, 0, win.w, win.h); + + /* resize to new width */ + xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec)); +} + +ushort +sixd_to_16bit(int x) +{ + return x == 0 ? 0 : 0x3737 + 0x2828 * x; +} + +int +xloadcolor(int i, const char *name, Color *ncolor) +{ + XRenderColor color = { .alpha = 0xffff }; + + if (!name) { + if (BETWEEN(i, 16, 255)) { /* 256 color */ + if (i < 6*6*6+16) { /* same colors as xterm */ + color.red = sixd_to_16bit( ((i-16)/36)%6 ); + color.green = sixd_to_16bit( ((i-16)/6) %6 ); + color.blue = sixd_to_16bit( ((i-16)/1) %6 ); + } else { /* greyscale */ + color.red = 0x0808 + 0x0a0a * (i - (6*6*6+16)); + color.green = color.blue = color.red; + } + return XftColorAllocValue(xw.dpy, xw.vis, + xw.cmap, &color, ncolor); + } else + name = colorname[i]; + } + + return XftColorAllocName(xw.dpy, xw.vis, xw.cmap, name, ncolor); +} + +void +xloadalpha(void) +{ + float const usedAlpha = focused ? alpha : alphaUnfocus; + if (opt_alpha) alpha = strtof(opt_alpha, NULL); + dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * usedAlpha); + dc.col[defaultbg].pixel &= 0x00FFFFFF; + dc.col[defaultbg].pixel |= (unsigned char)(0xff * usedAlpha) << 24; +} + +void +xloadcols(void) +{ + int i; + static int loaded; + Color *cp; + + if (!loaded) { + dc.collen = 1 + (defaultbg = MAX(LEN(colorname), 256)); + dc.col = xmalloc(dc.collen * sizeof(Color)); + } + + for (i = 0; i+1 < dc.collen; i++) + if (!xloadcolor(i, NULL, &dc.col[i])) { + if (colorname[i]) + die("could not allocate color '%s'\n", colorname[i]); + else + die("could not allocate color %d\n", i); + } + + if (dc.collen) // cannot die, as the color is already loaded. + xloadcolor(background, NULL, &dc.col[defaultbg]); + + xloadalpha(); + loaded = 1; +} + +int +xgetcolor(int x, unsigned char *r, unsigned char *g, unsigned char *b) +{ + if (!BETWEEN(x, 0, dc.collen)) + return 1; + + *r = dc.col[x].color.red >> 8; + *g = dc.col[x].color.green >> 8; + *b = dc.col[x].color.blue >> 8; + + return 0; +} + +int +xsetcolorname(int x, const char *name) +{ + Color ncolor; + + if (!BETWEEN(x, 0, dc.collen)) + return 1; + + if (!xloadcolor(x, name, &ncolor)) + return 1; + + XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]); + dc.col[x] = ncolor; + + return 0; +} + +/* + * Absolute coordinates. + */ +void +xclear(int x1, int y1, int x2, int y2) +{ + XftDrawRect(xw.draw, + &dc.col[IS_SET(MODE_REVERSE)? defaultfg : defaultbg], + x1, y1, x2-x1, y2-y1); +} + +void +xhints(void) +{ + XClassHint class = {opt_name ? opt_name : "st", + opt_class ? opt_class : "St"}; + XWMHints wm = {.flags = InputHint, .input = 1}; + XSizeHints *sizeh; + + sizeh = XAllocSizeHints(); + + sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize; + sizeh->height = win.h; + sizeh->width = win.w; + sizeh->height_inc = win.ch; + sizeh->width_inc = win.cw; + sizeh->base_height = 2 * borderpx; + sizeh->base_width = 2 * borderpx; + sizeh->min_height = win.ch + 2 * borderpx; + sizeh->min_width = win.cw + 2 * borderpx; + if (xw.isfixed) { + sizeh->flags |= PMaxSize; + sizeh->min_width = sizeh->max_width = win.w; + sizeh->min_height = sizeh->max_height = win.h; + } + if (xw.gm & (XValue|YValue)) { + sizeh->flags |= USPosition | PWinGravity; + sizeh->x = xw.l; + sizeh->y = xw.t; + sizeh->win_gravity = xgeommasktogravity(xw.gm); + } + + XSetWMProperties(xw.dpy, xw.win, NULL, NULL, NULL, 0, sizeh, &wm, + &class); + XFree(sizeh); +} + +int +xgeommasktogravity(int mask) +{ + switch (mask & (XNegative|YNegative)) { + case 0: + return NorthWestGravity; + case XNegative: + return NorthEastGravity; + case YNegative: + return SouthWestGravity; + } + + return SouthEastGravity; +} + +int +xloadfont(Font *f, FcPattern *pattern) +{ + FcPattern *configured; + FcPattern *match; + FcResult result; + XGlyphInfo extents; + int wantattr, haveattr; + + /* + * Manually configure instead of calling XftMatchFont + * so that we can use the configured pattern for + * "missing glyph" lookups. + */ + configured = FcPatternDuplicate(pattern); + if (!configured) + return 1; + + FcConfigSubstitute(NULL, configured, FcMatchPattern); + XftDefaultSubstitute(xw.dpy, xw.scr, configured); + + match = FcFontMatch(NULL, configured, &result); + if (!match) { + FcPatternDestroy(configured); + return 1; + } + + if (!(f->match = XftFontOpenPattern(xw.dpy, match))) { + FcPatternDestroy(configured); + FcPatternDestroy(match); + return 1; + } + + if ((XftPatternGetInteger(pattern, "slant", 0, &wantattr) == + XftResultMatch)) { + /* + * Check if xft was unable to find a font with the appropriate + * slant but gave us one anyway. Try to mitigate. + */ + if ((XftPatternGetInteger(f->match->pattern, "slant", 0, + &haveattr) != XftResultMatch) || haveattr < wantattr) { + f->badslant = 1; + fputs("font slant does not match\n", stderr); + } + } + + if ((XftPatternGetInteger(pattern, "weight", 0, &wantattr) == + XftResultMatch)) { + if ((XftPatternGetInteger(f->match->pattern, "weight", 0, + &haveattr) != XftResultMatch) || haveattr != wantattr) { + f->badweight = 1; + fputs("font weight does not match\n", stderr); + } + } + + XftTextExtentsUtf8(xw.dpy, f->match, + (const FcChar8 *) ascii_printable, + strlen(ascii_printable), &extents); + + f->set = NULL; + f->pattern = configured; + + f->ascent = f->match->ascent; + f->descent = f->match->descent; + f->lbearing = 0; + f->rbearing = f->match->max_advance_width; + + f->height = f->ascent + f->descent; + f->width = DIVCEIL(extents.xOff, strlen(ascii_printable)); + + return 0; +} + +void +xloadfonts(const char *fontstr, double fontsize) +{ + FcPattern *pattern; + double fontval; + + if (fontstr[0] == '-') + pattern = XftXlfdParse(fontstr, False, False); + else + pattern = FcNameParse((const FcChar8 *)fontstr); + + if (!pattern) + die("can't open font %s\n", fontstr); + + if (fontsize > 1) { + FcPatternDel(pattern, FC_PIXEL_SIZE); + FcPatternDel(pattern, FC_SIZE); + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, (double)fontsize); + usedfontsize = fontsize; + } else { + if (FcPatternGetDouble(pattern, FC_PIXEL_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = fontval; + } else if (FcPatternGetDouble(pattern, FC_SIZE, 0, &fontval) == + FcResultMatch) { + usedfontsize = -1; + } else { + /* + * Default font size is 12, if none given. This is to + * have a known usedfontsize value. + */ + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, 12); + usedfontsize = 12; + } + defaultfontsize = usedfontsize; + } + + if (xloadfont(&dc.font, pattern)) + die("can't open font %s\n", fontstr); + + if (usedfontsize < 0) { + FcPatternGetDouble(dc.font.match->pattern, + FC_PIXEL_SIZE, 0, &fontval); + usedfontsize = fontval; + if (fontsize == 0) + defaultfontsize = fontval; + } + + /* Setting character width and height. */ + win.cw = ceilf(dc.font.width * cwscale); + win.ch = ceilf(dc.font.height * chscale); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC); + if (xloadfont(&dc.ifont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_WEIGHT); + FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD); + if (xloadfont(&dc.ibfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ROMAN); + if (xloadfont(&dc.bfont, pattern)) + die("can't open font %s\n", fontstr); + + FcPatternDestroy(pattern); +} + +int +xloadsparefont(FcPattern *pattern, int flags) +{ + FcPattern *match; + FcResult result; + + match = FcFontMatch(NULL, pattern, &result); + if (!match) { + return 1; + } + + if (!(frc[frclen].font = XftFontOpenPattern(xw.dpy, match))) { + FcPatternDestroy(match); + return 1; + } + + frc[frclen].flags = flags; + /* Believe U+0000 glyph will present in each default font */ + frc[frclen].unicodep = 0; + frclen++; + + return 0; +} + +void +xloadsparefonts(void) +{ + FcPattern *pattern; + double sizeshift, fontval; + int fc; + char **fp; + + if (frclen != 0) + die("can't embed spare fonts. cache isn't empty"); + + /* Calculate count of spare fonts */ + fc = sizeof(font2) / sizeof(*font2); + if (fc == 0) + return; + + /* Allocate memory for cache entries. */ + if (frccap < 4 * fc) { + frccap += 4 * fc - frccap; + frc = xrealloc(frc, frccap * sizeof(Fontcache)); + } + + for (fp = font2; fp - font2 < fc; ++fp) { + + if (**fp == '-') + pattern = XftXlfdParse(*fp, False, False); + else + pattern = FcNameParse((FcChar8 *)*fp); + + if (!pattern) + die("can't open spare font %s\n", *fp); + + if (defaultfontsize > 0) { + sizeshift = usedfontsize - defaultfontsize; + if (sizeshift != 0 && + FcPatternGetDouble(pattern, FC_PIXEL_SIZE, 0, &fontval) == + FcResultMatch) { + fontval += sizeshift; + FcPatternDel(pattern, FC_PIXEL_SIZE); + FcPatternDel(pattern, FC_SIZE); + FcPatternAddDouble(pattern, FC_PIXEL_SIZE, fontval); + } + } + + FcPatternAddBool(pattern, FC_SCALABLE, 1); + + FcConfigSubstitute(NULL, pattern, FcMatchPattern); + XftDefaultSubstitute(xw.dpy, xw.scr, pattern); + + if (xloadsparefont(pattern, FRC_NORMAL)) + die("can't open spare font %s\n", *fp); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC); + if (xloadsparefont(pattern, FRC_ITALIC)) + die("can't open spare font %s\n", *fp); + + FcPatternDel(pattern, FC_WEIGHT); + FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD); + if (xloadsparefont(pattern, FRC_ITALICBOLD)) + die("can't open spare font %s\n", *fp); + + FcPatternDel(pattern, FC_SLANT); + FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ROMAN); + if (xloadsparefont(pattern, FRC_BOLD)) + die("can't open spare font %s\n", *fp); + + FcPatternDestroy(pattern); + } +} + +void +xunloadfont(Font *f) +{ + XftFontClose(xw.dpy, f->match); + FcPatternDestroy(f->pattern); + if (f->set) + FcFontSetDestroy(f->set); +} + +void +xunloadfonts(void) +{ + /* Clear Harfbuzz font cache. */ + hbunloadfonts(); + + /* Free the loaded fonts in the font cache. */ + while (frclen > 0) + XftFontClose(xw.dpy, frc[--frclen].font); + + xunloadfont(&dc.font); + xunloadfont(&dc.bfont); + xunloadfont(&dc.ifont); + xunloadfont(&dc.ibfont); +} + +int +ximopen(Display *dpy) +{ + XIMCallback imdestroy = { .client_data = NULL, .callback = ximdestroy }; + XICCallback icdestroy = { .client_data = NULL, .callback = xicdestroy }; + + xw.ime.xim = XOpenIM(xw.dpy, NULL, NULL, NULL); + if (xw.ime.xim == NULL) + return 0; + + if (XSetIMValues(xw.ime.xim, XNDestroyCallback, &imdestroy, NULL)) + fprintf(stderr, "XSetIMValues: " + "Could not set XNDestroyCallback.\n"); + + xw.ime.spotlist = XVaCreateNestedList(0, XNSpotLocation, &xw.ime.spot, + NULL); + + if (xw.ime.xic == NULL) { + xw.ime.xic = XCreateIC(xw.ime.xim, XNInputStyle, + XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, xw.win, + XNDestroyCallback, &icdestroy, + NULL); + } + if (xw.ime.xic == NULL) + fprintf(stderr, "XCreateIC: Could not create input context.\n"); + + return 1; +} + +void +ximinstantiate(Display *dpy, XPointer client, XPointer call) +{ + if (ximopen(dpy)) + XUnregisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); +} + +void +ximdestroy(XIM xim, XPointer client, XPointer call) +{ + xw.ime.xim = NULL; + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + XFree(xw.ime.spotlist); +} + +int +xicdestroy(XIC xim, XPointer client, XPointer call) +{ + xw.ime.xic = NULL; + return 1; +} + +void +xinit(int cols, int rows) +{ + XGCValues gcvalues; + Cursor cursor; + Window parent; + pid_t thispid = getpid(); + XColor xmousefg, xmousebg; + XWindowAttributes attr; + XVisualInfo vis; + + xw.scr = XDefaultScreen(xw.dpy); + + if (!(opt_embed && (parent = strtol(opt_embed, NULL, 0)))) { + parent = XRootWindow(xw.dpy, xw.scr); + xw.depth = 32; + } else { + XGetWindowAttributes(xw.dpy, parent, &attr); + xw.depth = attr.depth; + } + + XMatchVisualInfo(xw.dpy, xw.scr, xw.depth, TrueColor, &vis); + xw.vis = vis.visual; + + /* font */ + if (!FcInit()) + die("could not init fontconfig.\n"); + + usedfont = (opt_font == NULL)? font : opt_font; + xloadfonts(usedfont, 0); + + /* spare fonts */ + xloadsparefonts(); + + /* colors */ + xw.cmap = XCreateColormap(xw.dpy, parent, xw.vis, None); + xloadcols(); + + /* adjust fixed window geometry */ + win.w = 2 * borderpx + cols * win.cw; + win.h = 2 * borderpx + rows * win.ch; + if (xw.gm & XNegative) + xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2; + if (xw.gm & YNegative) + xw.t += DisplayHeight(xw.dpy, xw.scr) - win.h - 2; + + /* Events */ + xw.attrs.background_pixel = dc.col[defaultbg].pixel; + xw.attrs.border_pixel = dc.col[defaultbg].pixel; + xw.attrs.bit_gravity = NorthWestGravity; + xw.attrs.event_mask = FocusChangeMask | KeyPressMask | KeyReleaseMask + | ExposureMask | VisibilityChangeMask | StructureNotifyMask + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + xw.attrs.colormap = xw.cmap; + + xw.win = XCreateWindow(xw.dpy, parent, xw.l, xw.t, + win.w, win.h, 0, xw.depth, InputOutput, + xw.vis, CWBackPixel | CWBorderPixel | CWBitGravity + | CWEventMask | CWColormap, &xw.attrs); + + memset(&gcvalues, 0, sizeof(gcvalues)); + gcvalues.graphics_exposures = False; + xw.buf = XCreatePixmap(xw.dpy, xw.win, win.w, win.h, xw.depth); + dc.gc = XCreateGC(xw.dpy, xw.buf, GCGraphicsExposures, &gcvalues); + XSetForeground(xw.dpy, dc.gc, dc.col[defaultbg].pixel); + XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h); + + /* font spec buffer */ + xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec)); + + /* Xft rendering context */ + xw.draw = XftDrawCreate(xw.dpy, xw.buf, xw.vis, xw.cmap); + + /* input methods */ + if (!ximopen(xw.dpy)) { + XRegisterIMInstantiateCallback(xw.dpy, NULL, NULL, NULL, + ximinstantiate, NULL); + } + + /* white cursor, black outline */ + cursor = XCreateFontCursor(xw.dpy, mouseshape); + XDefineCursor(xw.dpy, xw.win, cursor); + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousefg], &xmousefg) == 0) { + xmousefg.red = 0xffff; + xmousefg.green = 0xffff; + xmousefg.blue = 0xffff; + } + + if (XParseColor(xw.dpy, xw.cmap, colorname[mousebg], &xmousebg) == 0) { + xmousebg.red = 0x0000; + xmousebg.green = 0x0000; + xmousebg.blue = 0x0000; + } + + XRecolorCursor(xw.dpy, cursor, &xmousefg, &xmousebg); + + xw.xembed = XInternAtom(xw.dpy, "_XEMBED", False); + xw.wmdeletewin = XInternAtom(xw.dpy, "WM_DELETE_WINDOW", False); + xw.netwmname = XInternAtom(xw.dpy, "_NET_WM_NAME", False); + xw.netwmiconname = XInternAtom(xw.dpy, "_NET_WM_ICON_NAME", False); + XSetWMProtocols(xw.dpy, xw.win, &xw.wmdeletewin, 1); + + xw.netwmpid = XInternAtom(xw.dpy, "_NET_WM_PID", False); + XChangeProperty(xw.dpy, xw.win, xw.netwmpid, XA_CARDINAL, 32, + PropModeReplace, (uchar *)&thispid, 1); + + win.mode = MODE_NUMLOCK; + resettitle(); + xhints(); + XMapWindow(xw.dpy, xw.win); + XSync(xw.dpy, False); + + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick1); + clock_gettime(CLOCK_MONOTONIC, &xsel.tclick2); + xsel.primary = NULL; + xsel.clipboard = NULL; + xsel.xtarget = XInternAtom(xw.dpy, "UTF8_STRING", 0); + if (xsel.xtarget == None) + xsel.xtarget = XA_STRING; + + boxdraw_xinit(xw.dpy, xw.cmap, xw.draw, xw.vis); +} + +int +xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y) +{ + float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp; + ushort mode, prevmode = USHRT_MAX; + Font *font = &dc.font; + int frcflags = FRC_NORMAL; + float runewidth = win.cw; + Rune rune; + FT_UInt glyphidx; + FcResult fcres; + FcPattern *fcpattern, *fontpattern; + FcFontSet *fcsets[] = { NULL }; + FcCharSet *fccharset; + int i, f, numspecs = 0; + + for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) { + /* Fetch rune and mode for current glyph. */ + rune = glyphs[i].u; + mode = glyphs[i].mode; + + /* Skip dummy wide-character spacing. */ + if (mode & ATTR_WDUMMY) + continue; + + /* Determine font for glyph if different from previous glyph. */ + if (prevmode != mode) { + prevmode = mode; + font = &dc.font; + frcflags = FRC_NORMAL; + runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f); + if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) { + font = &dc.ibfont; + frcflags = FRC_ITALICBOLD; + } else if (mode & ATTR_ITALIC) { + font = &dc.ifont; + frcflags = FRC_ITALIC; + } else if (mode & ATTR_BOLD) { + font = &dc.bfont; + frcflags = FRC_BOLD; + } + yp = winy + font->ascent; + } + + if (mode & ATTR_BOXDRAW) { + /* minor shoehorning: boxdraw uses only this ushort */ + glyphidx = boxdrawindex(&glyphs[i]); + } else { + /* Lookup character index with default font. */ + glyphidx = XftCharIndex(xw.dpy, font->match, rune); + } + if (glyphidx) { + specs[numspecs].font = font->match; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + xp += runewidth; + numspecs++; + continue; + } + + /* Fallback on font cache, search the font cache for match. */ + for (f = 0; f < frclen; f++) { + glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune); + /* Everything correct. */ + if (glyphidx && frc[f].flags == frcflags) + break; + /* We got a default font for a not found glyph. */ + if (!glyphidx && frc[f].flags == frcflags + && frc[f].unicodep == rune) { + break; + } + } + + /* Nothing was found. Use fontconfig to find matching font. */ + if (f >= frclen) { + if (!font->set) + font->set = FcFontSort(0, font->pattern, + 1, 0, &fcres); + fcsets[0] = font->set; + + /* + * Nothing was found in the cache. Now use + * some dozen of Fontconfig calls to get the + * font for one single character. + * + * Xft and fontconfig are design failures. + */ + fcpattern = FcPatternDuplicate(font->pattern); + fccharset = FcCharSetCreate(); + + FcCharSetAddChar(fccharset, rune); + FcPatternAddCharSet(fcpattern, FC_CHARSET, + fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, 1); + + FcConfigSubstitute(0, fcpattern, + FcMatchPattern); + FcDefaultSubstitute(fcpattern); + + fontpattern = FcFontSetMatch(0, fcsets, 1, + fcpattern, &fcres); + + /* Allocate memory for the new cache entry. */ + if (frclen >= frccap) { + frccap += 16; + frc = xrealloc(frc, frccap * sizeof(Fontcache)); + } + + frc[frclen].font = XftFontOpenPattern(xw.dpy, + fontpattern); + if (!frc[frclen].font) + die("XftFontOpenPattern failed seeking fallback font: %s\n", + strerror(errno)); + frc[frclen].flags = frcflags; + frc[frclen].unicodep = rune; + + glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune); + + f = frclen; + frclen++; + + FcPatternDestroy(fcpattern); + FcCharSetDestroy(fccharset); + } + + specs[numspecs].font = frc[f].font; + specs[numspecs].glyph = glyphidx; + specs[numspecs].x = (short)xp; + specs[numspecs].y = (short)yp; + xp += runewidth; + numspecs++; + } + + /* Harfbuzz transformation for ligatures. */ + hbtransform(specs, glyphs, len, x, y); + + return numspecs; +} + +void +xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y) +{ + int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1); + int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, + width = charlen * win.cw; + Color *fg, *bg, *temp, revfg, revbg, truefg, truebg; + XRenderColor colfg, colbg; + XRectangle r; + + /* Fallback on color display for attributes not supported by the font */ + if (base.mode & ATTR_ITALIC && base.mode & ATTR_BOLD) { + if (dc.ibfont.badslant || dc.ibfont.badweight) + base.fg = defaultattr; + } else if ((base.mode & ATTR_ITALIC && dc.ifont.badslant) || + (base.mode & ATTR_BOLD && dc.bfont.badweight)) { + base.fg = defaultattr; + } + + if (IS_TRUECOL(base.fg)) { + colfg.alpha = 0xffff; + colfg.red = TRUERED(base.fg); + colfg.green = TRUEGREEN(base.fg); + colfg.blue = TRUEBLUE(base.fg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &truefg); + fg = &truefg; + } else { + fg = &dc.col[base.fg]; + } + + if (IS_TRUECOL(base.bg)) { + colbg.alpha = 0xffff; + colbg.green = TRUEGREEN(base.bg); + colbg.red = TRUERED(base.bg); + colbg.blue = TRUEBLUE(base.bg); + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, &truebg); + bg = &truebg; + } else { + bg = &dc.col[base.bg]; + } + + /* Change basic system colors [0-7] to bright system colors [8-15] */ + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_BOLD && BETWEEN(base.fg, 0, 7)) + fg = &dc.col[base.fg + 8]; + + if (IS_SET(MODE_REVERSE)) { + if (fg == &dc.col[defaultfg]) { + fg = &dc.col[defaultbg]; + } else { + colfg.red = ~fg->color.red; + colfg.green = ~fg->color.green; + colfg.blue = ~fg->color.blue; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, + &revfg); + fg = &revfg; + } + + if (bg == &dc.col[defaultbg]) { + bg = &dc.col[defaultfg]; + } else { + colbg.red = ~bg->color.red; + colbg.green = ~bg->color.green; + colbg.blue = ~bg->color.blue; + colbg.alpha = bg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colbg, + &revbg); + bg = &revbg; + } + } + + if ((base.mode & ATTR_BOLD_FAINT) == ATTR_FAINT) { + colfg.red = fg->color.red / 2; + colfg.green = fg->color.green / 2; + colfg.blue = fg->color.blue / 2; + colfg.alpha = fg->color.alpha; + XftColorAllocValue(xw.dpy, xw.vis, xw.cmap, &colfg, &revfg); + fg = &revfg; + } + + if (base.mode & ATTR_REVERSE) { + temp = fg; + fg = bg; + bg = temp; + } + + if (base.mode & ATTR_BLINK && win.mode & MODE_BLINK) + fg = bg; + + if (base.mode & ATTR_INVISIBLE) + fg = bg; + + /* Intelligent cleaning up of the borders. */ + if (x == 0) { + xclear(0, (y == 0)? 0 : winy, borderpx, + winy + win.ch + + ((winy + win.ch >= borderpx + win.th)? win.h : 0)); + } + if (winx + width >= borderpx + win.tw) { + xclear(winx + width, (y == 0)? 0 : winy, win.w, + ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch))); + } + if (y == 0) + xclear(winx, 0, winx + width, borderpx); + if (winy + win.ch >= borderpx + win.th) + xclear(winx, winy + win.ch, winx + width, win.h); + + /* Clean up the region we want to draw to. */ + XftDrawRect(xw.draw, bg, winx, winy, width, win.ch); + + /* Set the clip region because Xft is sometimes dirty. */ + r.x = 0; + r.y = 0; + r.height = win.ch; + r.width = width; + XftDrawSetClipRectangles(xw.draw, winx, winy, &r, 1); + + if (base.mode & ATTR_BOXDRAW) { + drawboxes(winx, winy, width / len, win.ch, fg, bg, specs, len); + } else { + /* Render the glyphs. */ + XftDrawGlyphFontSpec(xw.draw, fg, specs, len); + } + + /* Render underline and strikethrough. */ + if (base.mode & ATTR_UNDERLINE) { + XftDrawRect(xw.draw, fg, winx, winy + dc.font.ascent + 1, + width, 1); + } + + if (base.mode & ATTR_STRUCK) { + XftDrawRect(xw.draw, fg, winx, winy + 2 * dc.font.ascent * chscale / 3, + width, 1); + } + + /* Reset clip to none. */ + XftDrawSetClip(xw.draw, 0); +} + +void +xdrawglyph(Glyph g, int x, int y) +{ + int numspecs; + XftGlyphFontSpec spec; + + numspecs = xmakeglyphfontspecs(&spec, &g, 1, x, y); + xdrawglyphfontspecs(&spec, g, numspecs, x, y); +} + +void +xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int len) +{ + Color drawcol; + + /* remove the old cursor */ + if (selected(ox, oy)) + og.mode ^= ATTR_REVERSE; + + /* Redraw the line where cursor was previously. + * It will restore the ligatures broken by the cursor. */ + xdrawline(line, 0, oy, len); + + if (IS_SET(MODE_HIDE)) + return; + + /* + * Select the right color for the right mode. + */ + g.mode &= ATTR_BOLD|ATTR_ITALIC|ATTR_UNDERLINE|ATTR_STRUCK|ATTR_WIDE|ATTR_BOXDRAW; + + if (IS_SET(MODE_REVERSE)) { + g.mode |= ATTR_REVERSE; + g.bg = defaultfg; + if (selected(cx, cy)) { + drawcol = dc.col[defaultcs]; + g.fg = defaultrcs; + } else { + drawcol = dc.col[defaultrcs]; + g.fg = defaultcs; + } + } else { + if (selected(cx, cy)) { + g.fg = defaultfg; + g.bg = defaultrcs; + } else { + g.fg = defaultbg; + g.bg = defaultcs; + } + drawcol = dc.col[g.bg]; + } + + /* draw the new one */ + if (IS_SET(MODE_FOCUSED)) { + switch (win.cursor) { + case 7: /* st extension */ + g.u = 0x2603; /* snowman (U+2603) */ + /* FALLTHROUGH */ + case 0: /* Blinking Block */ + case 1: /* Blinking Block (Default) */ + case 2: /* Steady Block */ + xdrawglyph(g, cx, cy); + break; + case 3: /* Blinking Underline */ + case 4: /* Steady Underline */ + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + (cy + 1) * win.ch - \ + cursorthickness, + win.cw, cursorthickness); + break; + case 5: /* Blinking bar */ + case 6: /* Steady bar */ + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + cursorthickness, win.ch); + break; + } + } else { + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + win.cw - 1, 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + (cx + 1) * win.cw - 1, + borderpx + cy * win.ch, + 1, win.ch - 1); + XftDrawRect(xw.draw, &drawcol, + borderpx + cx * win.cw, + borderpx + (cy + 1) * win.ch - 1, + win.cw, 1); + } +} + +void +xsetenv(void) +{ + char buf[sizeof(long) * 8 + 1]; + + snprintf(buf, sizeof(buf), "%lu", xw.win); + setenv("WINDOWID", buf, 1); +} + +void +xseticontitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMIconName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmiconname); + XFree(prop.value); +} + +void +xsettitle(char *p) +{ + XTextProperty prop; + DEFAULT(p, opt_title); + + if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, + &prop) != Success) + return; + XSetWMName(xw.dpy, xw.win, &prop); + XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname); + XFree(prop.value); +} + +int +xstartdraw(void) +{ + return IS_SET(MODE_VISIBLE); +} + +void +xdrawline(Line line, int x1, int y1, int x2) +{ + int i, x, ox, numspecs; + Glyph base, new; + XftGlyphFontSpec *specs = xw.specbuf; + + numspecs = xmakeglyphfontspecs(specs, &line[x1], x2 - x1, x1, y1); + i = ox = 0; + for (x = x1; x < x2 && i < numspecs; x++) { + new = line[x]; + if (new.mode == ATTR_WDUMMY) + continue; + if (selected(x, y1)) + new.mode ^= ATTR_REVERSE; + if (i > 0 && ATTRCMP(base, new)) { + xdrawglyphfontspecs(specs, base, i, ox, y1); + specs += i; + numspecs -= i; + i = 0; + } + if (i == 0) { + ox = x; + base = new; + } + i++; + } + if (i > 0) + xdrawglyphfontspecs(specs, base, i, ox, y1); +} + +void +xfinishdraw(void) +{ + XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, + win.h, 0, 0); + XSetForeground(xw.dpy, dc.gc, + dc.col[IS_SET(MODE_REVERSE)? + defaultfg : defaultbg].pixel); +} + +void +xximspot(int x, int y) +{ + if (xw.ime.xic == NULL) + return; + + xw.ime.spot.x = borderpx + x * win.cw; + xw.ime.spot.y = borderpx + (y + 1) * win.ch; + + XSetICValues(xw.ime.xic, XNPreeditAttributes, xw.ime.spotlist, NULL); +} + +void +expose(XEvent *ev) +{ + redraw(); +} + +void +visibility(XEvent *ev) +{ + XVisibilityEvent *e = &ev->xvisibility; + + MODBIT(win.mode, e->state != VisibilityFullyObscured, MODE_VISIBLE); +} + +void +unmap(XEvent *ev) +{ + win.mode &= ~MODE_VISIBLE; +} + +void +xsetpointermotion(int set) +{ + MODBIT(xw.attrs.event_mask, set, PointerMotionMask); + XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs); +} + +void +xsetmode(int set, unsigned int flags) +{ + int mode = win.mode; + MODBIT(win.mode, set, flags); + if ((win.mode & MODE_REVERSE) != (mode & MODE_REVERSE)) + redraw(); +} + +int +xsetcursor(int cursor) +{ + if (!BETWEEN(cursor, 0, 7)) /* 7: st extension */ + return 1; + win.cursor = cursor; + return 0; +} + +void +xseturgency(int add) +{ + XWMHints *h = XGetWMHints(xw.dpy, xw.win); + + MODBIT(h->flags, add, XUrgencyHint); + XSetWMHints(xw.dpy, xw.win, h); + XFree(h); +} + +void +xbell(void) +{ + if (!(IS_SET(MODE_FOCUSED))) + xseturgency(1); + if (bellvolume) + XkbBell(xw.dpy, xw.win, bellvolume, (Atom)NULL); +} + +void +focus(XEvent *ev) +{ + XFocusChangeEvent *e = &ev->xfocus; + + if (e->mode == NotifyGrab) + return; + + if (ev->type == FocusIn) { + if (xw.ime.xic) + XSetICFocus(xw.ime.xic); + win.mode |= MODE_FOCUSED; + xseturgency(0); + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[I", 3, 0); + if (!focused) { + focused = 1; + xloadcols(); + tfulldirt(); + } + } else { + if (xw.ime.xic) + XUnsetICFocus(xw.ime.xic); + win.mode &= ~MODE_FOCUSED; + if (IS_SET(MODE_FOCUS)) + ttywrite("\033[O", 3, 0); + if (focused) { + focused = 0; + xloadcols(); + tfulldirt(); + } + } +} + +int +match(uint mask, uint state) +{ + return mask == XK_ANY_MOD || mask == (state & ~ignoremod); +} + +char* +kmap(KeySym k, uint state) +{ + Key *kp; + int i; + + /* Check for mapped keys out of X11 function keys. */ + for (i = 0; i < LEN(mappedkeys); i++) { + if (mappedkeys[i] == k) + break; + } + if (i == LEN(mappedkeys)) { + if ((k & 0xFFFF) < 0xFD00) + return NULL; + } + + for (kp = key; kp < key + LEN(key); kp++) { + if (kp->k != k) + continue; + + if (!match(kp->mask, state)) + continue; + + if (IS_SET(MODE_APPKEYPAD) ? kp->appkey < 0 : kp->appkey > 0) + continue; + if (IS_SET(MODE_NUMLOCK) && kp->appkey == 2) + continue; + + if (IS_SET(MODE_APPCURSOR) ? kp->appcursor < 0 : kp->appcursor > 0) + continue; + + return kp->s; + } + + return NULL; +} + +void +kpress(XEvent *ev) +{ + XKeyEvent *e = &ev->xkey; + KeySym ksym; + char *buf = NULL, *customkey; + int len = 0; + int buf_size = 64; + int critical = - 1; + Rune c; + Status status; + Shortcut *bp; + + if (IS_SET(MODE_KBDLOCK)) + return; + +reallocbuf: + if (critical > 0) + goto cleanup; + if (buf) + free(buf); + + buf = xmalloc((buf_size) * sizeof(char)); + critical += 1; + + if (xw.ime.xic) { + len = XmbLookupString(xw.ime.xic, e, buf, buf_size, &ksym, &status); + if (status == XBufferOverflow) { + buf_size = len; + goto reallocbuf; + } + } else { + // Not sure how to fix this and if it is fixable + // but at least it does write something into the buffer + // so it is not as critical + len = XLookupString(e, buf, buf_size, &ksym, NULL); + } + /* 1. shortcuts */ + for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) { + if (ksym == bp->keysym && match(bp->mod, e->state)) { + bp->func(&(bp->arg)); + goto cleanup; + } + } + + /* 2. custom keys from config.h */ + if ((customkey = kmap(ksym, e->state))) { + ttywrite(customkey, strlen(customkey), 1); + goto cleanup; + } + + /* 3. composed string from input method */ + if (len == 0) + goto cleanup; + if (len == 1 && e->state & Mod1Mask) { + if (IS_SET(MODE_8BIT)) { + if (*buf < 0177) { + c = *buf | 0x80; + len = utf8encode(c, buf); + } + } else { + buf[1] = buf[0]; + buf[0] = '\033'; + len = 2; + } + } + if (len <= buf_size) + ttywrite(buf, len, 1); +cleanup: + if (buf) + free(buf); +} + +void +cmessage(XEvent *e) +{ + /* + * See xembed specs + * http://standards.freedesktop.org/xembed-spec/xembed-spec-latest.html + */ + if (e->xclient.message_type == xw.xembed && e->xclient.format == 32) { + if (e->xclient.data.l[1] == XEMBED_FOCUS_IN) { + win.mode |= MODE_FOCUSED; + xseturgency(0); + } else if (e->xclient.data.l[1] == XEMBED_FOCUS_OUT) { + win.mode &= ~MODE_FOCUSED; + } + } else if (e->xclient.data.l[0] == xw.wmdeletewin) { + ttyhangup(); + exit(0); + } +} + +void +resize(XEvent *e) +{ + if (e->xconfigure.width == win.w && e->xconfigure.height == win.h) + return; + + cresize(e->xconfigure.width, e->xconfigure.height); +} + +void +run(void) +{ + XEvent ev; + int w = win.w, h = win.h; + fd_set rfd; + int xfd = XConnectionNumber(xw.dpy), ttyfd, xev, drawing; + struct timespec seltv, *tv, now, lastblink, trigger; + double timeout; + + /* Waiting for window mapping */ + do { + XNextEvent(xw.dpy, &ev); + /* + * This XFilterEvent call is required because of XOpenIM. It + * does filter out the key event and some client message for + * the input method too. + */ + if (XFilterEvent(&ev, None)) + continue; + if (ev.type == ConfigureNotify) { + w = ev.xconfigure.width; + h = ev.xconfigure.height; + } + } while (ev.type != MapNotify); + + ttyfd = ttynew(opt_line, shell, opt_io, opt_cmd); + cresize(w, h); + + for (timeout = -1, drawing = 0, lastblink = (struct timespec){0};;) { + FD_ZERO(&rfd); + FD_SET(ttyfd, &rfd); + FD_SET(xfd, &rfd); + + if (XPending(xw.dpy)) + timeout = 0; /* existing events might not set xfd */ + + seltv.tv_sec = timeout / 1E3; + seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec); + tv = timeout >= 0 ? &seltv : NULL; + + if (pselect(MAX(xfd, ttyfd)+1, &rfd, NULL, NULL, tv, NULL) < 0) { + if (errno == EINTR) + continue; + die("select failed: %s\n", strerror(errno)); + } + clock_gettime(CLOCK_MONOTONIC, &now); + + if (FD_ISSET(ttyfd, &rfd)) + ttyread(); + + xev = 0; + while (XPending(xw.dpy)) { + xev = 1; + XNextEvent(xw.dpy, &ev); + if (XFilterEvent(&ev, None)) + continue; + if (handler[ev.type]) + (handler[ev.type])(&ev); + } + + /* + * To reduce flicker and tearing, when new content or event + * triggers drawing, we first wait a bit to ensure we got + * everything, and if nothing new arrives - we draw. + * We start with trying to wait minlatency ms. If more content + * arrives sooner, we retry with shorter and shorter periods, + * and eventually draw even without idle after maxlatency ms. + * Typically this results in low latency while interacting, + * maximum latency intervals during `cat huge.txt`, and perfect + * sync with periodic updates from animations/key-repeats/etc. + */ + if (FD_ISSET(ttyfd, &rfd) || xev) { + if (!drawing) { + trigger = now; + drawing = 1; + } + timeout = (maxlatency - TIMEDIFF(now, trigger)) \ + / maxlatency * minlatency; + if (timeout > 0) + continue; /* we have time, try to find idle */ + } + + /* idle detected or maxlatency exhausted -> draw */ + timeout = -1; + if (blinktimeout && tattrset(ATTR_BLINK)) { + timeout = blinktimeout - TIMEDIFF(now, lastblink); + if (timeout <= 0) { + if (-timeout > blinktimeout) /* start visible */ + win.mode |= MODE_BLINK; + win.mode ^= MODE_BLINK; + tsetdirtattr(ATTR_BLINK); + lastblink = now; + timeout = blinktimeout; + } + } + + draw(); + XFlush(xw.dpy); + drawing = 0; + } +} + +int +resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst) +{ + char **sdst = dst; + int *idst = dst; + float *fdst = dst; + + char fullname[256]; + char fullclass[256]; + char *type; + XrmValue ret; + + snprintf(fullname, sizeof(fullname), "%s.%s", + opt_name ? opt_name : "st", name); + snprintf(fullclass, sizeof(fullclass), "%s.%s", + opt_class ? opt_class : "St", name); + fullname[sizeof(fullname) - 1] = fullclass[sizeof(fullclass) - 1] = '\0'; + + XrmGetResource(db, fullname, fullclass, &type, &ret); + if (ret.addr == NULL || strncmp("String", type, 64)) + return 1; + + switch (rtype) { + case STRING: + *sdst = ret.addr; + break; + case INTEGER: + *idst = strtoul(ret.addr, NULL, 10); + break; + case FLOAT: + *fdst = strtof(ret.addr, NULL); + break; + } + return 0; +} + +void +config_init(void) +{ + char *resm; + XrmDatabase db; + ResourcePref *p; + + XrmInitialize(); + resm = XResourceManagerString(xw.dpy); + if (!resm) + return; + + db = XrmGetStringDatabase(resm); + for (p = resources; p < resources + LEN(resources); p++) + resource_load(db, p->name, p->type, p->dst); +} + +void +usage(void) +{ + die("usage: %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid]" + " [[-e] command [args ...]]\n" + " %s [-aiv] [-c class] [-f font] [-g geometry]" + " [-n name] [-o file]\n" + " [-T title] [-t title] [-w windowid] -l line" + " [stty_args ...]\n", argv0, argv0); +} + +int +main(int argc, char *argv[]) +{ + xw.l = xw.t = 0; + xw.isfixed = False; + xsetcursor(cursorshape); + + ARGBEGIN { + case 'a': + allowaltscreen = 0; + break; + case 'A': + opt_alpha = EARGF(usage()); + break; + case 'c': + opt_class = EARGF(usage()); + break; + case 'e': + if (argc > 0) + --argc, ++argv; + goto run; + case 'f': + opt_font = EARGF(usage()); + break; + case 'g': + xw.gm = XParseGeometry(EARGF(usage()), + &xw.l, &xw.t, &cols, &rows); + break; + case 'i': + xw.isfixed = 1; + break; + case 'o': + opt_io = EARGF(usage()); + break; + case 'l': + opt_line = EARGF(usage()); + break; + case 'n': + opt_name = EARGF(usage()); + break; + case 't': + case 'T': + opt_title = EARGF(usage()); + break; + case 'w': + opt_embed = EARGF(usage()); + break; + case 'v': + die("%s " VERSION "\n", argv0); + break; + default: + usage(); + } ARGEND; + +run: + if (argc > 0) /* eat all remaining arguments */ + opt_cmd = argv; + + if (!opt_title) + opt_title = (opt_line || !opt_cmd) ? "st" : opt_cmd[0]; + + setlocale(LC_CTYPE, ""); + XSetLocaleModifiers(""); + + if(!(xw.dpy = XOpenDisplay(NULL))) + die("Can't open display\n"); + + config_init(); + cols = MAX(cols, 1); + rows = MAX(rows, 1); + defaultbg = MAX(LEN(colorname), 256); + alphaUnfocus = alpha-alphaOffset; + tnew(cols, rows); + xinit(cols, rows); + xsetenv(); + selinit(); + run(); + + return 0; +} + diff --git a/mut/surf/FAQ.md b/mut/surf/FAQ.md new file mode 100644 index 0000000..48e6097 --- /dev/null +++ b/mut/surf/FAQ.md @@ -0,0 +1,10 @@ +# Frequently Asked Questions + +## Surf is starting up slowly. What might be causing this? + +The first suspect for such behaviour is the plugin handling. Run surf on +the commandline and see if there are errors because of “nspluginwrapper” +or failed RPCs to them. If that is true, go to ~/.mozilla/plugins and +try removing stale links to plugins not on your system anymore. This +will stop surf from trying to load them. + diff --git a/mut/surf/LICENSE b/mut/surf/LICENSE new file mode 100644 index 0000000..2cdab7c --- /dev/null +++ b/mut/surf/LICENSE @@ -0,0 +1,48 @@ +MIT/X Consortium License + +© 2009-2010 Enno Boland <tox@s01.de> +© 2009 Thomas Menari <spaceinvader@chaotika.org> +© 2009 Simon Rozet <simon@rozet.name> +© 2009 Andrew Antle <andrew.antle@gmail.com> +© 2010-2011 pancake <nopcode.org> +© 2011-2013 Anselm R Garbe <anselm@garbe.us> +© 2011-2012 Troels Henriksen <athas@sigkill.dk> +© 2011 Connor Lane Smith <cls@lubutu.com> +© 2012-2017 Christoph Lohmann <20h@r-36.net> +© 2013 Shayan Pooya <shayan@liveve.org> +© 2013 Jens Nyberg <jens.nyberg@gmail.com> +© 2013 Carlos J. Torres <vlaadbrain@gmail.com> +© 2013 Alexander Sedov <alex0player@gmail.com> +© 2013 Nick White <git@njw.me.uk> +© 2013 David Dufberg <david@dufberg.se> +© 2014-2017 Quentin Rameau <quinq@fifth.space> +© 2014-2016 Markus Teich <markus.teich@stusta.mhn.de> +© 2015 Jakukyo Friel <weakish@gmail.com> +© 2015 Ben Woolley <tautolog@gmail.com> +© 2015 Greg Reagle <greg.reagle@umbc.edu> +© 2015 GhostAV <ghostav@riseup.net> +© 2015 Ivan Tham <pickfire@riseup.net> +© 2015 Alexander Huemer <alexander.huemer@xx.vu> +© 2015 Michael Stevens <mstevens@etla.org> +© 2015 Felix Janda <felix.janda@posteo.de> +© 2016 Charles Lehner <cel@celehner.com> +© 2016 Dmitry Bogatov <KAction@gnu.org> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + diff --git a/mut/surf/Makefile b/mut/surf/Makefile new file mode 100644 index 0000000..e5d4172 --- /dev/null +++ b/mut/surf/Makefile @@ -0,0 +1,76 @@ +# surf - simple browser +# See LICENSE file for copyright and license details. +.POSIX: + +include config.mk + +SRC = surf.c +WSRC = webext-surf.c +OBJ = $(SRC:.c=.o) +WOBJ = $(WSRC:.c=.o) +WLIB = $(WSRC:.c=.so) + +all: options surf $(WLIB) + +options: + @echo surf build options: + @echo "CC = $(CC)" + @echo "CFLAGS = $(SURFCFLAGS) $(CFLAGS)" + @echo "WEBEXTCFLAGS = $(WEBEXTCFLAGS) $(CFLAGS)" + @echo "LDFLAGS = $(LDFLAGS)" + +surf: $(OBJ) + $(CC) $(SURFLDFLAGS) $(LDFLAGS) -o $@ $(OBJ) $(LIBS) + +$(OBJ) $(WOBJ): config.h common.h config.mk + +config.h: + cp config.def.h $@ + +$(OBJ): $(SRC) + $(CC) $(SURFCFLAGS) $(CFLAGS) -c $(SRC) + +$(WLIB): $(WOBJ) + $(CC) -shared -Wl,-soname,$@ $(LDFLAGS) -o $@ $? $(WEBEXTLIBS) + +$(WOBJ): $(WSRC) + $(CC) $(WEBEXTCFLAGS) $(CFLAGS) -c $(WSRC) + +clean: + rm -f surf $(OBJ) + rm -f $(WLIB) $(WOBJ) + +distclean: clean + rm -f config.h surf-$(VERSION).tar.gz + +dist: distclean + mkdir -p surf-$(VERSION) + cp -R LICENSE Makefile config.mk config.def.h README \ + surf-open.sh arg.h TODO.md surf.png \ + surf.1 $(SRC) $(CSRC) $(WSRC) surf-$(VERSION) + tar -cf surf-$(VERSION).tar surf-$(VERSION) + gzip surf-$(VERSION).tar + rm -rf surf-$(VERSION) + +install: all + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f surf $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/surf + mkdir -p $(DESTDIR)$(LIBDIR) + cp -f $(WLIB) $(DESTDIR)$(LIBDIR) + for wlib in $(WLIB); do \ + chmod 644 $(DESTDIR)$(LIBDIR)/$$wlib; \ + done + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < surf.1 > $(DESTDIR)$(MANPREFIX)/man1/surf.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/surf.1 + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/surf + rm -f $(DESTDIR)$(MANPREFIX)/man1/surf.1 + for wlib in $(WLIB); do \ + rm -f $(DESTDIR)$(LIBDIR)/$$wlib; \ + done + - rmdir $(DESTDIR)$(LIBDIR) + +.PHONY: all options distclean clean dist install uninstall diff --git a/mut/surf/README b/mut/surf/README new file mode 100644 index 0000000..da4577f --- /dev/null +++ b/mut/surf/README @@ -0,0 +1,40 @@ +surf - simple webkit-based browser +================================== +surf is a simple Web browser based on WebKit/GTK+. + +Requirements +------------ +In order to build surf you need GTK+ and Webkit/GTK+ header files. + +In order to use the functionality of the url-bar, also install dmenu[0]. + +Installation +------------ +Edit config.mk to match your local setup (surf is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install surf (if +necessary as root): + + make clean install + +Running surf +------------ +run + surf [URI] + +See the manpage for further options. + +Running surf in tabbed +---------------------- +For running surf in tabbed[1] there is a script included in the distribution, +which is run like this: + + surf-open.sh [URI] + +Further invocations of the script will run surf with the specified URI in this +instance of tabbed. + +[0] http://tools.suckless.org/dmenu +[1] http://tools.suckless.org/tabbed + diff --git a/mut/surf/TODO.md b/mut/surf/TODO.md new file mode 100644 index 0000000..da5f44d --- /dev/null +++ b/mut/surf/TODO.md @@ -0,0 +1,10 @@ +# TODO + +* suckless adblocking +* replace twitch() with proper gtk calls to make scrollbars reappear +* replace webkit with something sane +* add video player options + * play in plugin + * play in video player + * call command with URI (quvi + cclive) + diff --git a/mut/surf/arg.h b/mut/surf/arg.h new file mode 100644 index 0000000..ba3fb3f --- /dev/null +++ b/mut/surf/arg.h @@ -0,0 +1,48 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/mut/surf/common.h b/mut/surf/common.h new file mode 100644 index 0000000..3990c42 --- /dev/null +++ b/mut/surf/common.h @@ -0,0 +1 @@ +#define MSGBUFSZ 8 diff --git a/mut/surf/config.def.h b/mut/surf/config.def.h new file mode 100644 index 0000000..766b224 --- /dev/null +++ b/mut/surf/config.def.h @@ -0,0 +1,204 @@ +/* modifier 0 means no modifier */ +static int surfuseragent = 1; /* Append Surf version to default WebKit user agent */ +static char *fulluseragent = ""; /* Or override the whole user agent string */ +static char *scriptfile = "~/.surf/script.js"; +static char *styledir = "~/.surf/styles/"; +static char *certdir = "~/.surf/certificates/"; +static char *cachedir = "~/.surf/cache/"; +static char *cookiefile = "~/.surf/cookies.txt"; + +/* Webkit default features */ +/* Highest priority value will be used. + * Default parameters are priority 0 + * Per-uri parameters are priority 1 + * Command parameters are priority 2 + */ +static Parameter defconfig[ParameterLast] = { + /* parameter Arg value priority */ + [AccessMicrophone] = { { .i = 0 }, }, + [AccessWebcam] = { { .i = 0 }, }, + [Certificate] = { { .i = 0 }, }, + [CaretBrowsing] = { { .i = 0 }, }, + [CookiePolicies] = { { .v = "@Aa" }, }, + [DefaultCharset] = { { .v = "UTF-8" }, }, + [DiskCache] = { { .i = 1 }, }, + [DNSPrefetch] = { { .i = 0 }, }, + [Ephemeral] = { { .i = 0 }, }, + [FileURLsCrossAccess] = { { .i = 0 }, }, + [FontSize] = { { .i = 12 }, }, + [FrameFlattening] = { { .i = 0 }, }, + [Geolocation] = { { .i = 0 }, }, + [HideBackground] = { { .i = 0 }, }, + [Inspector] = { { .i = 0 }, }, + [Java] = { { .i = 1 }, }, + [JavaScript] = { { .i = 1 }, }, + [KioskMode] = { { .i = 0 }, }, + [LoadImages] = { { .i = 1 }, }, + [MediaManualPlay] = { { .i = 1 }, }, + [PreferredLanguages] = { { .v = (char *[]){ NULL } }, }, + [RunInFullscreen] = { { .i = 0 }, }, + [ScrollBars] = { { .i = 1 }, }, + [ShowIndicators] = { { .i = 1 }, }, + [SiteQuirks] = { { .i = 1 }, }, + [SmoothScrolling] = { { .i = 0 }, }, + [SpellChecking] = { { .i = 0 }, }, + [SpellLanguages] = { { .v = ((char *[]){ "en_US", NULL }) }, }, + [StrictTLS] = { { .i = 1 }, }, + [Style] = { { .i = 1 }, }, + [WebGL] = { { .i = 0 }, }, + [ZoomLevel] = { { .f = 1.0 }, }, + [ClipboardNotPrimary] = { { .i = 1 }, }, +}; + +static UriParameters uriparams[] = { + { "(://|\\.)suckless\\.org(/|$)", { + [JavaScript] = { { .i = 0 }, 1 }, + }, }, +}; + +/* default window size: width, height */ +static int winsize[] = { 800, 600 }; + +static WebKitFindOptions findopts = WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE | + WEBKIT_FIND_OPTIONS_WRAP_AROUND; + +#define PROMPT_GO "Go:" +#define PROMPT_FIND "Find:" + +/* SETPROP(readprop, setprop, prompt)*/ +#define SETPROP(r, s, p) { \ + .v = (const char *[]){ "/bin/sh", "-c", \ + "prop=\"$(printf '%b' \"$(xprop -id $1 $2 " \ + "| sed \"s/^$2(STRING) = //;s/^\\\"\\(.*\\)\\\"$/\\1/\")\" " \ + "| dmenu -p \"$4\" -w $1)\" && xprop -id $1 -f $3 8s -set $3 \"$prop\"", \ + "surf-setprop", winid, r, s, p, NULL \ + } \ +} + +/* DOWNLOAD(URI, referer) */ +#define DOWNLOAD(u, r) { \ + .v = (const char *[]){ "st", "-e", "/bin/sh", "-c",\ + "curl -g -L -J -O -A \"$1\" -b \"$2\" -c \"$2\"" \ + " -e \"$3\" \"$4\"; read", \ + "surf-download", useragent, cookiefile, r, u, NULL \ + } \ +} + +/* PLUMB(URI) */ +/* This called when some URI which does not begin with "about:", + * "http://" or "https://" should be opened. + */ +#define PLUMB(u) {\ + .v = (const char *[]){ "/bin/sh", "-c", \ + "xdg-open \"$0\"", u, NULL \ + } \ +} + +/* VIDEOPLAY(URI) */ +#define VIDEOPLAY(u) {\ + .v = (const char *[]){ "/bin/sh", "-c", \ + "mpv --really-quiet \"$0\"", u, NULL \ + } \ +} + +#define SR_SEARCH {\ + .v = (char *[]){\ + "/bin/sh", "-c", "xprop -id $0 -f _SURF_GO 8s -set _SURF_GO $(sr -p $(sr -elvi | tail -n +2 | cut -s -f1 | dmenu))", \ + winid, \ + NULL \ + } \ +} + +/* styles */ +/* + * The iteration will stop at the first match, beginning at the beginning of + * the list. + */ +static SiteSpecific styles[] = { + /* regexp file in $styledir */ + { ".*", "default.css" }, +}; + +/* certificates */ +/* + * Provide custom certificate for urls + */ +static SiteSpecific certs[] = { + /* regexp file in $certdir */ + { "://suckless\\.org/", "suckless.org.crt" }, +}; + +#define MODKEY GDK_CONTROL_MASK + +/* hotkeys */ +/* + * If you use anything else but MODKEY and GDK_SHIFT_MASK, don't forget to + * edit the CLEANMASK() macro. + */ +static Key keys[] = { + /* modifier keyval function arg */ + { MODKEY, GDK_KEY_g, spawn, SETPROP("_SURF_URI", "_SURF_GO", PROMPT_GO) }, + { MODKEY, GDK_KEY_f, spawn, SETPROP("_SURF_FIND", "_SURF_FIND", PROMPT_FIND) }, + { MODKEY, GDK_KEY_slash, spawn, SETPROP("_SURF_FIND", "_SURF_FIND", PROMPT_FIND) }, + + { MODKEY, GDK_KEY_s, spawn, SR_SEARCH }, + + { 0, GDK_KEY_Escape, stop, { 0 } }, + /* { MODKEY, GDK_KEY_c, stop, { 0 } }, */ + + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_r, reload, { .i = 1 } }, + { MODKEY, GDK_KEY_r, reload, { .i = 0 } }, + + { MODKEY, GDK_KEY_l, navigate, { .i = +1 } }, + { MODKEY, GDK_KEY_h, navigate, { .i = -1 } }, + + /* vertical and horizontal scrolling, in viewport percentage */ + { MODKEY, GDK_KEY_d, scrollv, { .i = +10 } }, + { MODKEY, GDK_KEY_u, scrollv, { .i = -10 } }, + { MODKEY, GDK_KEY_space, scrollv, { .i = +50 } }, + { MODKEY, GDK_KEY_b, scrollv, { .i = -50 } }, + /* { MODKEY, GDK_KEY_i, scrollh, { .i = +10 } }, */ + /* { MODKEY, GDK_KEY_u, scrollh, { .i = -10 } }, */ + + + /* { MODKEY|GDK_SHIFT_MASK, GDK_KEY_j, zoom, { .i = -1 } }, */ + /* { MODKEY|GDK_SHIFT_MASK, GDK_KEY_k, zoom, { .i = +1 } }, */ + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_q, zoom, { .i = 0 } }, + { MODKEY, GDK_KEY_minus, zoom, { .i = -1 } }, + { MODKEY, GDK_KEY_plus, zoom, { .i = +1 } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_plus, zoom, { .i = +1 } }, + + { MODKEY, GDK_KEY_p, clipboard, { .i = 1 } }, + { MODKEY, GDK_KEY_y, clipboard, { .i = 0 } }, + + { MODKEY, GDK_KEY_n, find, { .i = +1 } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_n, find, { .i = -1 } }, + + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_p, print, { 0 } }, + { MODKEY, GDK_KEY_o, showcert, { 0 } }, + + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_a, togglecookiepolicy, { 0 } }, + { 0, GDK_KEY_F11, togglefullscreen, { 0 } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_o, toggleinspector, { 0 } }, + + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_c, toggle, { .i = CaretBrowsing } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_f, toggle, { .i = FrameFlattening } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_g, toggle, { .i = Geolocation } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_s, toggle, { .i = JavaScript } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_i, toggle, { .i = LoadImages } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_b, toggle, { .i = ScrollBars } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_t, toggle, { .i = StrictTLS } }, + { MODKEY|GDK_SHIFT_MASK, GDK_KEY_m, toggle, { .i = Style } }, +}; + +/* button definitions */ +/* target can be OnDoc, OnLink, OnImg, OnMedia, OnEdit, OnBar, OnSel, OnAny */ +static Button buttons[] = { + /* target event mask button function argument stop event */ + { OnLink, 0, 2, clicknewwindow, { .i = 0 }, 1 }, + { OnLink, MODKEY, 2, clicknewwindow, { .i = 1 }, 1 }, + { OnLink, MODKEY, 1, clicknewwindow, { .i = 1 }, 1 }, + { OnAny, 0, 8, clicknavigate, { .i = -1 }, 1 }, + { OnAny, 0, 9, clicknavigate, { .i = +1 }, 1 }, + { OnMedia, MODKEY, 1, clickexternplayer, { 0 }, 1 }, +}; diff --git a/mut/surf/config.mk b/mut/surf/config.mk new file mode 100644 index 0000000..2eb9fb0 --- /dev/null +++ b/mut/surf/config.mk @@ -0,0 +1,32 @@ +# surf version +VERSION = 2.1 + +# Customize below to fit your system + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man +LIBPREFIX = $(PREFIX)/lib +LIBDIR = $(LIBPREFIX)/surf + +X11INC = `pkg-config --cflags x11` +X11LIB = `pkg-config --libs x11` + +GTKINC = `pkg-config --cflags gtk+-3.0 gcr-3 webkit2gtk-4.0` +GTKLIB = `pkg-config --libs gtk+-3.0 gcr-3 webkit2gtk-4.0` +WEBEXTINC = `pkg-config --cflags webkit2gtk-4.0 webkit2gtk-web-extension-4.0 gio-2.0` +WEBEXTLIBS = `pkg-config --libs webkit2gtk-4.0 webkit2gtk-web-extension-4.0 gio-2.0` + +# includes and libs +INCS = $(X11INC) $(GTKINC) +LIBS = $(X11LIB) $(GTKLIB) -lgthread-2.0 + +# flags +CPPFLAGS = -DVERSION=\"$(VERSION)\" -DGCR_API_SUBJECT_TO_CHANGE \ + -DLIBPREFIX=\"$(LIBPREFIX)\" -DWEBEXTDIR=\"$(LIBDIR)\" \ + -D_DEFAULT_SOURCE +SURFCFLAGS = -fPIC $(INCS) $(CPPFLAGS) +WEBEXTCFLAGS = -fPIC $(WEBEXTINC) + +# compiler +#CC = c99 diff --git a/mut/surf/patches/scroll-message-as-signed-char.patch b/mut/surf/patches/scroll-message-as-signed-char.patch new file mode 100644 index 0000000..2737e6e --- /dev/null +++ b/mut/surf/patches/scroll-message-as-signed-char.patch @@ -0,0 +1,27 @@ +diff --git a/mut/surf/surf.c b/mut/surf/surf.c +index af0fa74..bcd8d6a 100644 +--- a/mut/surf/surf.c ++++ b/mut/surf/surf.c +@@ -1856,7 +1856,7 @@ zoom(Client *c, const Arg *a) + static void + msgext(Client *c, char type, const Arg *a) + { +- static char msg[MSGBUFSZ]; ++ static signed char msg[MSGBUFSZ]; + int ret; + + if (spair[0] < 0) +diff --git a/mut/surf/webext-surf.c b/mut/surf/webext-surf.c +index d087219..7eeb55f 100644 +--- a/mut/surf/webext-surf.c ++++ b/mut/surf/webext-surf.c +@@ -38,7 +38,8 @@ msgsurf(guint64 pageid, const char *s) + static gboolean + readsock(GIOChannel *s, GIOCondition c, gpointer unused) + { +- static char js[48], msg[MSGBUFSZ]; ++ static char js[48]; ++ static signed char msg[MSGBUFSZ]; + WebKitWebPage *page; + JSCContext *jsc; + GError *gerr = NULL; diff --git a/mut/surf/patches/surf-clipboard-20200112-a6a8878.diff b/mut/surf/patches/surf-clipboard-20200112-a6a8878.diff new file mode 100644 index 0000000..5c43025 --- /dev/null +++ b/mut/surf/patches/surf-clipboard-20200112-a6a8878.diff @@ -0,0 +1,67 @@ +From a6a8878bb6a203b589d559025b94a78214f22878 Mon Sep 17 00:00:00 2001 +From: Olivier Moreau <m242@protonmail.com> +Date: Sun, 12 Jan 2020 11:23:11 +0000 +Subject: [PATCH] Added choice between PRIMARY and CLIPBOARD Gtk selections, as + a config option + +--- + config.def.h | 1 + + surf.c | 11 +++++++++-- + 2 files changed, 10 insertions(+), 2 deletions(-) + +diff --git a/config.def.h b/config.def.h +index 34265f6..03bbe2b 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -48,6 +48,7 @@ static Parameter defconfig[ParameterLast] = { + [Style] = { { .i = 1 }, }, + [WebGL] = { { .i = 0 }, }, + [ZoomLevel] = { { .f = 1.0 }, }, ++ [ClipboardNotPrimary] = { { .i = 1 }, }, + }; + + static UriParameters uriparams[] = { +diff --git a/surf.c b/surf.c +index 2b54e3c..b8a9b2f 100644 +--- a/surf.c ++++ b/surf.c +@@ -82,6 +82,7 @@ typedef enum { + Style, + WebGL, + ZoomLevel, ++ ClipboardNotPrimary, + ParameterLast + } ParamName; + +@@ -291,6 +292,7 @@ static ParamName loadcommitted[] = { + SpellLanguages, + Style, + ZoomLevel, ++ ClipboardNotPrimary, + ParameterLast + }; + +@@ -1816,13 +1818,18 @@ showcert(Client *c, const Arg *a) + void + clipboard(Client *c, const Arg *a) + { ++ /* User defined choice of selection, see config.h */ ++ GdkAtom selection = GDK_SELECTION_PRIMARY; ++ if (curconfig[ClipboardNotPrimary].val.i > 0) ++ selection = GDK_SELECTION_CLIPBOARD; ++ + if (a->i) { /* load clipboard uri */ + gtk_clipboard_request_text(gtk_clipboard_get( +- GDK_SELECTION_PRIMARY), ++ selection), + pasteuri, c); + } else { /* copy uri */ + gtk_clipboard_set_text(gtk_clipboard_get( +- GDK_SELECTION_PRIMARY), c->targeturi ++ selection), c->targeturi + ? c->targeturi : geturi(c), -1); + } + } +-- +2.24.1 + diff --git a/mut/surf/surf-open.sh b/mut/surf/surf-open.sh new file mode 100755 index 0000000..c22edc2 --- /dev/null +++ b/mut/surf/surf-open.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# +# See the LICENSE file for copyright and license details. +# + +xidfile="$HOME/tmp/tabbed-surf.xid" +uri="" + +if [ "$#" -gt 0 ]; +then + uri="$1" +fi + +runtabbed() { + tabbed -dn tabbed-surf -r 2 surf -e '' "$uri" >"$xidfile" \ + 2>/dev/null & +} + +if [ ! -r "$xidfile" ]; +then + runtabbed +else + xid=$(cat "$xidfile") + xprop -id "$xid" >/dev/null 2>&1 + if [ $? -gt 0 ]; + then + runtabbed + else + surf -e "$xid" "$uri" >/dev/null 2>&1 & + fi +fi + diff --git a/mut/surf/surf.1 b/mut/surf/surf.1 new file mode 100644 index 0000000..496afb9 --- /dev/null +++ b/mut/surf/surf.1 @@ -0,0 +1,305 @@ +.TH SURF 1 surf\-VERSION +.SH NAME +surf \- simple webkit-based browser +.SH SYNOPSIS +.B surf +.RB [-bBdDfFgGiIkKmMnNpPsStTvwxX] +.RB [-a\ cookiepolicies] +.RB [-c\ cookiefile] +.RB [-C\ stylefile] +.RB [-e\ xid] +.RB [-r\ scriptfile] +.RB [-u\ useragent] +.RB [-z\ zoomlevel] +.RB [URI] +.SH DESCRIPTION +surf is a simple Web browser based on WebKit/GTK+. It is able +to display websites and follow links. It supports the XEmbed protocol +which makes it possible to embed it in another application. Furthermore, +one can point surf to another URI by setting its XProperties. +.SH OPTIONS +.TP +.B \-a cookiepolicies +Define the order of +.I cookie policies\fR. +The default is "@Aa" but could be +redefined in the +.IR config.h , +with "A" meaning to +accept all cookies, "a" to deny all cookies and "@", which tells surf to +accept no third party cookies. +.TP +.B \-b +Disable Scrollbars. +.TP +.B \-B +Enable Scrollbars. +.TP +.B \-c cookiefile +Specify the +.I cookiefile +to use. +.TP +.B \-C stylefile +Specify the user +.IR stylefile . +This does disable the site-specific styles. +.TP +.B \-d +Disable the disk cache. +.TP +.B \-D +Enable the disk cache. +.TP +.B \-e xid +Reparents to window specified by +.IR xid . +.TP +.B \-f +Start surf in windowed mode (not fullscreen). +.TP +.B \-F +Start surf in fullscreen mode. +.TP +.B \-g +Disable giving the geolocation to websites. +.TP +.B \-G +Enable giving the geolocation to websites. +.TP +.B \-i +Disable Images. +.TP +.B \-I +Enable Images. +.TP +.B \-k +Disable kiosk mode (disable key strokes and right click). +.TP +.B \-K +Enable kiosk mode (disable key strokes and right click). +.TP +.B \-m +Disable application of user style sheets. +.TP +.B \-M +Enable application of user style sheets. +.TP +.B \-n +Disable the Web Inspector (Developer Tools). +.TP +.B \-N +Enable the Web Inspector (Developer Tools). +.TP +.B \-r scriptfile +Specify the user +.IR scriptfile . +.TP +.B \-s +Disable Javascript. +.TP +.B \-S +Enable Javascript. +.TP +.B \-t +Disable strict TLS check. +.TP +.B \-T +Enable strict TLS check. +.TP +.B \-u useragent +Specify the +.I useragent +which surf should use. +.TP +.B \-v +Prints version information to standard output, then exits. +.TP +.B \-w +Prints xid to standard output. This can be used to script the browser in for +example +.BR xdotool(1) . +.TP +.B -x +Disable custom certificates. +.TP +.B -X +Enable custom certificates. +.TP +.B \-z zoomlevel +Specify the +.I zoomlevel +which surf should use. +.SH USAGE +.B Escape +Stops loading current page or stops download. +.TP +.B Ctrl\-h +Walks back the history. +.TP +.B Ctrl\-l +Walks forward the history. +.TP +.B Ctrl\-k +Scrolls page upwards. +.TP +.B Ctrl\-j +Scrolls page downwards. +.TP +.B Ctrl\-b +Scroll up one whole page view. +.TP +.B Ctrl\-Space +Scroll down one whole page view. +.TP +.B Ctrl\-i +Scroll horizontally to the right. +.TP +.B Ctrl\-u +Scroll horizontally to the left. +.TP +.B Ctrl\-Shift\-k or Ctrl\-+ +Zooms page in. +.TP +.B Ctrl\-Shift\-j or Ctrl\-- +Zooms page out. +.TP +.B Ctrl\-Shift\-q +Resets Zoom. +.TP +.B Ctrl\-f and Ctrl\-/ +Opens the search-bar. +.TP +.B Ctrl\-n +Go to next search result. +.TP +.B Ctrl\-Shift\-n +Go to previous search result. +.TP +.B Ctrl\-g +Opens the URL-bar (requires dmenu installed). +.TP +.B Ctrl\-p +Loads URI from primary selection. +.TP +.B Ctrl\-Shift\-p +Calls Printpage Dialog. +.TP +.B Ctrl\-r +Reloads the website. +.TP +.B Ctrl\-Shift\-r +Reloads the website without using the cache. +.TP +.B Ctrl\-y +Copies current URI to primary selection. +.TP +.B Ctrl\-t +Display the current TLS certificate in a popup window. +.TP +.B Ctrl\-Shift\-a +Toggle through the the +.I cookie policies\fR. +This will not reload the page. +.TP +.B Ctrl\-Shift\-b +Toggle scrollbars. This will reload the page. +.TP +.B Ctrl\-Shift\-c +Toggle caret browsing. This will reload the page. +.TP +.B Ctrl\-Shift\-i +Toggle auto-loading of images. This will reload the page. +.TP +.B Ctrl\-Shift\-m +Toggle if the +.I stylefile +file should be loaded. This will reload the page. +.TP +.B Ctrl\-Shift\-o +Open the Web Inspector (Developer Tools) window for the current page. +.TP +.B Ctrl\-Shift\-s +Toggle script execution. This will reload the page. +.TP +.B Ctrl\-Shift\-t +Toggle strict TLS check. This will reload the page. +.TP +.B F11 +Toggle fullscreen mode. +.SH INDICATORS OF OPERATION +Surf is showing indicators of operation in front of the site title. +For all indicators, unless otherwise specified, a lower case letter means disabled and an upper case letter means enabled. +.TP +.B A +all cookies accepted +.TP +.B a +no cookies accepted +.TP +.B @ +all except third-party cookies accepted +.TP +.B c C +caret browsing +.TP +.B g G +geolocation +.TP +.B d D +disk cache +.TP +.B i I +images +.TP +.B s S +scripts +.TP +.B m M +styles +.TP +.B f F +frame flattening +.TP +.B x X +custom certificates +.TP +.B t T +strict TLS +.SH INDICATORS OF WEB PAGE +The second part of the indicators specifies modes of the web page itself. +.SS First character: encryption +.TP +.B - +unencrypted +.TP +.B T +encrypted (TLS) +.TP +.B U +attempted encryption but failed +.SS Second character: proxying +.TP +.B - +no proxy +.TP +.B P +using proxy +.SH ENVIRONMENT +.B SURF_USERAGENT +If this variable is set upon startup, surf will use it as the +.I useragent +string. +.TP +.B http_proxy +If this variable is set and not empty upon startup, surf will use it as the http proxy. +.SH SIGNALS +Surf will reload the current page on +.BR SIGHUP . +.SH SEE ALSO +.BR dmenu(1), +.BR xprop(1), +.BR tabbed(1), +.BR xdotool(1) +.SH BUGS +Please report them! diff --git a/mut/surf/surf.c b/mut/surf/surf.c new file mode 100644 index 0000000..d0ab984 --- /dev/null +++ b/mut/surf/surf.c @@ -0,0 +1,2140 @@ +/* See LICENSE file for copyright and license details. + * + * To understand surf, start reading main(). + */ +#include <sys/file.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <glib.h> +#include <inttypes.h> +#include <libgen.h> +#include <limits.h> +#include <pwd.h> +#include <regex.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <gdk/gdk.h> +#include <gdk/gdkkeysyms.h> +#include <gdk/gdkx.h> +#include <glib/gstdio.h> +#include <gtk/gtk.h> +#include <gtk/gtkx.h> +#include <gcr/gcr.h> +#include <JavaScriptCore/JavaScript.h> +#include <webkit2/webkit2.h> +#include <X11/X.h> +#include <X11/Xatom.h> +#include <glib.h> + +#include "arg.h" +#include "common.h" + +#define LENGTH(x) (sizeof(x) / sizeof(x[0])) +#define CLEANMASK(mask) (mask & (MODKEY|GDK_SHIFT_MASK)) + +enum { AtomFind, AtomGo, AtomUri, AtomLast }; + +enum { + OnDoc = WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT, + OnLink = WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK, + OnImg = WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE, + OnMedia = WEBKIT_HIT_TEST_RESULT_CONTEXT_MEDIA, + OnEdit = WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE, + OnBar = WEBKIT_HIT_TEST_RESULT_CONTEXT_SCROLLBAR, + OnSel = WEBKIT_HIT_TEST_RESULT_CONTEXT_SELECTION, + OnAny = OnDoc | OnLink | OnImg | OnMedia | OnEdit | OnBar | OnSel, +}; + +typedef enum { + AccessMicrophone, + AccessWebcam, + CaretBrowsing, + Certificate, + CookiePolicies, + DiskCache, + DefaultCharset, + DNSPrefetch, + Ephemeral, + FileURLsCrossAccess, + FontSize, + FrameFlattening, + Geolocation, + HideBackground, + Inspector, + Java, + JavaScript, + KioskMode, + LoadImages, + MediaManualPlay, + PreferredLanguages, + RunInFullscreen, + ScrollBars, + ShowIndicators, + SiteQuirks, + SmoothScrolling, + SpellChecking, + SpellLanguages, + StrictTLS, + Style, + WebGL, + ZoomLevel, + ClipboardNotPrimary, + ParameterLast +} ParamName; + +typedef union { + int i; + float f; + const void *v; +} Arg; + +typedef struct { + Arg val; + int prio; +} Parameter; + +typedef struct Client { + GtkWidget *win; + WebKitWebView *view; + WebKitWebInspector *inspector; + WebKitFindController *finder; + WebKitHitTestResult *mousepos; + GTlsCertificate *cert, *failedcert; + GTlsCertificateFlags tlserr; + Window xid; + guint64 pageid; + int progress, fullscreen, https, insecure, errorpage; + const char *title, *overtitle, *targeturi; + const char *needle; + struct Client *next; +} Client; + +typedef struct { + guint mod; + guint keyval; + void (*func)(Client *c, const Arg *a); + const Arg arg; +} Key; + +typedef struct { + unsigned int target; + unsigned int mask; + guint button; + void (*func)(Client *c, const Arg *a, WebKitHitTestResult *h); + const Arg arg; + unsigned int stopevent; +} Button; + +typedef struct { + const char *uri; + Parameter config[ParameterLast]; + regex_t re; +} UriParameters; + +typedef struct { + char *regex; + char *file; + regex_t re; +} SiteSpecific; + +/* Surf */ +static void die(const char *errstr, ...); +static void usage(void); +static void setup(void); +static void sigchld(int unused); +static void sighup(int unused); +static char *buildfile(const char *path); +static char *buildpath(const char *path); +static char *untildepath(const char *path); +static const char *getuserhomedir(const char *user); +static const char *getcurrentuserhomedir(void); +static Client *newclient(Client *c); +static void loaduri(Client *c, const Arg *a); +static const char *geturi(Client *c); +static void setatom(Client *c, int a, const char *v); +static const char *getatom(Client *c, int a); +static void updatetitle(Client *c); +static void gettogglestats(Client *c); +static void getpagestats(Client *c); +static WebKitCookieAcceptPolicy cookiepolicy_get(void); +static char cookiepolicy_set(const WebKitCookieAcceptPolicy p); +static void seturiparameters(Client *c, const char *uri, ParamName *params); +static void setparameter(Client *c, int refresh, ParamName p, const Arg *a); +static const char *getcert(const char *uri); +static void setcert(Client *c, const char *file); +static const char *getstyle(const char *uri); +static void setstyle(Client *c, const char *file); +static void runscript(Client *c); +static void evalscript(Client *c, const char *jsstr, ...); +static void updatewinid(Client *c); +static void handleplumb(Client *c, const char *uri); +static void newwindow(Client *c, const Arg *a, int noembed); +static void spawn(Client *c, const Arg *a); +static void msgext(Client *c, char type, const Arg *a); +static void destroyclient(Client *c); +static void cleanup(void); + +/* GTK/WebKit */ +static WebKitWebView *newview(Client *c, WebKitWebView *rv); +static void initwebextensions(WebKitWebContext *wc, Client *c); +static GtkWidget *createview(WebKitWebView *v, WebKitNavigationAction *a, + Client *c); +static gboolean buttonreleased(GtkWidget *w, GdkEvent *e, Client *c); +static GdkFilterReturn processx(GdkXEvent *xevent, GdkEvent *event, + gpointer d); +static gboolean winevent(GtkWidget *w, GdkEvent *e, Client *c); +static gboolean readsock(GIOChannel *s, GIOCondition ioc, gpointer unused); +static void showview(WebKitWebView *v, Client *c); +static GtkWidget *createwindow(Client *c); +static gboolean loadfailedtls(WebKitWebView *v, gchar *uri, + GTlsCertificate *cert, + GTlsCertificateFlags err, Client *c); +static void loadchanged(WebKitWebView *v, WebKitLoadEvent e, Client *c); +static void progresschanged(WebKitWebView *v, GParamSpec *ps, Client *c); +static void titlechanged(WebKitWebView *view, GParamSpec *ps, Client *c); +static void mousetargetchanged(WebKitWebView *v, WebKitHitTestResult *h, + guint modifiers, Client *c); +static gboolean permissionrequested(WebKitWebView *v, + WebKitPermissionRequest *r, Client *c); +static gboolean decidepolicy(WebKitWebView *v, WebKitPolicyDecision *d, + WebKitPolicyDecisionType dt, Client *c); +static void decidenavigation(WebKitPolicyDecision *d, Client *c); +static void decidenewwindow(WebKitPolicyDecision *d, Client *c); +static void decideresource(WebKitPolicyDecision *d, Client *c); +static void insecurecontent(WebKitWebView *v, WebKitInsecureContentEvent e, + Client *c); +static void downloadstarted(WebKitWebContext *wc, WebKitDownload *d, + Client *c); +static void responsereceived(WebKitDownload *d, GParamSpec *ps, Client *c); +static void download(Client *c, WebKitURIResponse *r); +static void webprocessterminated(WebKitWebView *v, + WebKitWebProcessTerminationReason r, + Client *c); +static void closeview(WebKitWebView *v, Client *c); +static void destroywin(GtkWidget* w, Client *c); + +/* Hotkeys */ +static void pasteuri(GtkClipboard *clipboard, const char *text, gpointer d); +static void reload(Client *c, const Arg *a); +static void print(Client *c, const Arg *a); +static void showcert(Client *c, const Arg *a); +static void clipboard(Client *c, const Arg *a); +static void zoom(Client *c, const Arg *a); +static void scrollv(Client *c, const Arg *a); +static void scrollh(Client *c, const Arg *a); +static void navigate(Client *c, const Arg *a); +static void stop(Client *c, const Arg *a); +static void toggle(Client *c, const Arg *a); +static void togglefullscreen(Client *c, const Arg *a); +static void togglecookiepolicy(Client *c, const Arg *a); +static void toggleinspector(Client *c, const Arg *a); +static void find(Client *c, const Arg *a); + +/* Buttons */ +static void clicknavigate(Client *c, const Arg *a, WebKitHitTestResult *h); +static void clicknewwindow(Client *c, const Arg *a, WebKitHitTestResult *h); +static void clickexternplayer(Client *c, const Arg *a, WebKitHitTestResult *h); + +static char winid[64]; +static char togglestats[12]; +static char pagestats[2]; +static Atom atoms[AtomLast]; +static Window embed; +static int showxid; +static int cookiepolicy; +static Display *dpy; +static Client *clients; +static GdkDevice *gdkkb; +static char *stylefile; +static const char *useragent; +static Parameter *curconfig; +static int modparams[ParameterLast]; +static int spair[2]; +char *argv0; + +static ParamName loadtransient[] = { + Certificate, + CookiePolicies, + DiskCache, + DNSPrefetch, + FileURLsCrossAccess, + JavaScript, + LoadImages, + PreferredLanguages, + ShowIndicators, + StrictTLS, + ParameterLast +}; + +static ParamName loadcommitted[] = { +// AccessMicrophone, +// AccessWebcam, + CaretBrowsing, + DefaultCharset, + FontSize, + FrameFlattening, + Geolocation, + HideBackground, + Inspector, + Java, +// KioskMode, + MediaManualPlay, + RunInFullscreen, + ScrollBars, + SiteQuirks, + SmoothScrolling, + SpellChecking, + SpellLanguages, + Style, + ZoomLevel, + ClipboardNotPrimary, + ParameterLast +}; + +static ParamName loadfinished[] = { + ParameterLast +}; + +/* configuration, allows nested code to access above variables */ +#include "config.h" + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(1); +} + +void +usage(void) +{ + die("usage: surf [-bBdDfFgGiIkKmMnNpPsStTvwxX]\n" + "[-a cookiepolicies ] [-c cookiefile] [-C stylefile] [-e xid]\n" + "[-r scriptfile] [-u useragent] [-z zoomlevel] [uri]\n"); +} + +void +setup(void) +{ + GIOChannel *gchanin; + GdkDisplay *gdpy; + int i, j; + + /* clean up any zombies immediately */ + sigchld(0); + if (signal(SIGHUP, sighup) == SIG_ERR) + die("Can't install SIGHUP handler"); + + if (!(dpy = XOpenDisplay(NULL))) + die("Can't open default display"); + + /* atoms */ + atoms[AtomFind] = XInternAtom(dpy, "_SURF_FIND", False); + atoms[AtomGo] = XInternAtom(dpy, "_SURF_GO", False); + atoms[AtomUri] = XInternAtom(dpy, "_SURF_URI", False); + + gtk_init(NULL, NULL); + + gdpy = gdk_display_get_default(); + + curconfig = defconfig; + + /* dirs and files */ + cookiefile = buildfile(cookiefile); + scriptfile = buildfile(scriptfile); + certdir = buildpath(certdir); + if (curconfig[Ephemeral].val.i) + cachedir = NULL; + else + cachedir = buildpath(cachedir); + + gdkkb = gdk_seat_get_keyboard(gdk_display_get_default_seat(gdpy)); + + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, spair) < 0) { + fputs("Unable to create sockets\n", stderr); + spair[0] = spair[1] = -1; + } else { + gchanin = g_io_channel_unix_new(spair[0]); + g_io_channel_set_encoding(gchanin, NULL, NULL); + g_io_channel_set_flags(gchanin, g_io_channel_get_flags(gchanin) + | G_IO_FLAG_NONBLOCK, NULL); + g_io_channel_set_close_on_unref(gchanin, TRUE); + g_io_add_watch(gchanin, G_IO_IN, readsock, NULL); + } + + + for (i = 0; i < LENGTH(certs); ++i) { + if (!regcomp(&(certs[i].re), certs[i].regex, REG_EXTENDED)) { + certs[i].file = g_strconcat(certdir, "/", certs[i].file, + NULL); + } else { + fprintf(stderr, "Could not compile regex: %s\n", + certs[i].regex); + certs[i].regex = NULL; + } + } + + if (!stylefile) { + styledir = buildpath(styledir); + for (i = 0; i < LENGTH(styles); ++i) { + if (!regcomp(&(styles[i].re), styles[i].regex, + REG_EXTENDED)) { + styles[i].file = g_strconcat(styledir, "/", + styles[i].file, NULL); + } else { + fprintf(stderr, "Could not compile regex: %s\n", + styles[i].regex); + styles[i].regex = NULL; + } + } + g_free(styledir); + } else { + stylefile = buildfile(stylefile); + } + + for (i = 0; i < LENGTH(uriparams); ++i) { + if (regcomp(&(uriparams[i].re), uriparams[i].uri, + REG_EXTENDED)) { + fprintf(stderr, "Could not compile regex: %s\n", + uriparams[i].uri); + uriparams[i].uri = NULL; + continue; + } + + /* copy default parameters with higher priority */ + for (j = 0; j < ParameterLast; ++j) { + if (defconfig[j].prio >= uriparams[i].config[j].prio) + uriparams[i].config[j] = defconfig[j]; + } + } +} + +void +sigchld(int unused) +{ + if (signal(SIGCHLD, sigchld) == SIG_ERR) + die("Can't install SIGCHLD handler"); + while (waitpid(-1, NULL, WNOHANG) > 0) + ; +} + +void +sighup(int unused) +{ + Arg a = { .i = 0 }; + Client *c; + + for (c = clients; c; c = c->next) + reload(c, &a); +} + +char * +buildfile(const char *path) +{ + char *dname, *bname, *bpath, *fpath; + FILE *f; + + dname = g_path_get_dirname(path); + bname = g_path_get_basename(path); + + bpath = buildpath(dname); + g_free(dname); + + fpath = g_build_filename(bpath, bname, NULL); + g_free(bpath); + g_free(bname); + + if (!(f = fopen(fpath, "a"))) + die("Could not open file: %s\n", fpath); + + g_chmod(fpath, 0600); /* always */ + fclose(f); + + return fpath; +} + +static const char* +getuserhomedir(const char *user) +{ + struct passwd *pw = getpwnam(user); + + if (!pw) + die("Can't get user %s login information.\n", user); + + return pw->pw_dir; +} + +static const char* +getcurrentuserhomedir(void) +{ + const char *homedir; + const char *user; + struct passwd *pw; + + homedir = getenv("HOME"); + if (homedir) + return homedir; + + user = getenv("USER"); + if (user) + return getuserhomedir(user); + + pw = getpwuid(getuid()); + if (!pw) + die("Can't get current user home directory\n"); + + return pw->pw_dir; +} + +char * +buildpath(const char *path) +{ + char *apath, *fpath; + + if (path[0] == '~') + apath = untildepath(path); + else + apath = g_strdup(path); + + /* creating directory */ + if (g_mkdir_with_parents(apath, 0700) < 0) + die("Could not access directory: %s\n", apath); + + fpath = realpath(apath, NULL); + g_free(apath); + + return fpath; +} + +char * +untildepath(const char *path) +{ + char *apath, *name, *p; + const char *homedir; + + if (path[1] == '/' || path[1] == '\0') { + p = (char *)&path[1]; + homedir = getcurrentuserhomedir(); + } else { + if ((p = strchr(path, '/'))) + name = g_strndup(&path[1], p - (path + 1)); + else + name = g_strdup(&path[1]); + + homedir = getuserhomedir(name); + g_free(name); + } + apath = g_build_filename(homedir, p, NULL); + return apath; +} + +Client * +newclient(Client *rc) +{ + Client *c; + + if (!(c = calloc(1, sizeof(Client)))) + die("Cannot malloc!\n"); + + c->next = clients; + clients = c; + + c->progress = 100; + c->view = newview(c, rc ? rc->view : NULL); + + return c; +} + +void +loaduri(Client *c, const Arg *a) +{ + struct stat st; + char *url, *path, *apath; + const char *uri = a->v; + + if (g_strcmp0(uri, "") == 0) + return; + + if (g_str_has_prefix(uri, "http://") || + g_str_has_prefix(uri, "https://") || + g_str_has_prefix(uri, "file://") || + g_str_has_prefix(uri, "about:")) { + url = g_strdup(uri); + } else { + if (uri[0] == '~') + apath = untildepath(uri); + else + apath = (char *)uri; + if (!stat(apath, &st) && (path = realpath(apath, NULL))) { + url = g_strdup_printf("file://%s", path); + free(path); + } else { + url = g_strdup_printf("http://%s", uri); + } + if (apath != uri) + free(apath); + } + + setatom(c, AtomUri, url); + + if (strcmp(url, geturi(c)) == 0) { + reload(c, a); + } else { + webkit_web_view_load_uri(c->view, url); + updatetitle(c); + } + + g_free(url); +} + +const char * +geturi(Client *c) +{ + const char *uri; + + if (!(uri = webkit_web_view_get_uri(c->view))) + uri = "about:blank"; + return uri; +} + +void +setatom(Client *c, int a, const char *v) +{ + XChangeProperty(dpy, c->xid, + atoms[a], XA_STRING, 8, PropModeReplace, + (unsigned char *)v, strlen(v) + 1); + XSync(dpy, False); +} + +const char * +getatom(Client *c, int a) +{ + static char buf[BUFSIZ]; + Atom adummy; + int idummy; + unsigned long ldummy; + unsigned char *p = NULL; + + XSync(dpy, False); + XGetWindowProperty(dpy, c->xid, atoms[a], 0L, BUFSIZ, False, XA_STRING, + &adummy, &idummy, &ldummy, &ldummy, &p); + if (p) + strncpy(buf, (char *)p, LENGTH(buf) - 1); + else + buf[0] = '\0'; + XFree(p); + + return buf; +} + +void +updatetitle(Client *c) +{ + char *title; + const char *name = c->overtitle ? c->overtitle : + c->title ? c->title : ""; + + if (curconfig[ShowIndicators].val.i) { + gettogglestats(c); + getpagestats(c); + + if (c->progress != 100) + title = g_strdup_printf("[%i%%] %s:%s | %s", + c->progress, togglestats, pagestats, name); + else + title = g_strdup_printf("%s:%s | %s", + togglestats, pagestats, name); + + gtk_window_set_title(GTK_WINDOW(c->win), title); + g_free(title); + } else { + gtk_window_set_title(GTK_WINDOW(c->win), name); + } +} + +void +gettogglestats(Client *c) +{ + togglestats[0] = cookiepolicy_set(cookiepolicy_get()); + togglestats[1] = curconfig[CaretBrowsing].val.i ? 'C' : 'c'; + togglestats[2] = curconfig[Geolocation].val.i ? 'G' : 'g'; + togglestats[3] = curconfig[DiskCache].val.i ? 'D' : 'd'; + togglestats[4] = curconfig[LoadImages].val.i ? 'I' : 'i'; + togglestats[5] = curconfig[JavaScript].val.i ? 'S' : 's'; + togglestats[7] = curconfig[Style].val.i ? 'M' : 'm'; + togglestats[8] = curconfig[FrameFlattening].val.i ? 'F' : 'f'; + togglestats[9] = curconfig[Certificate].val.i ? 'X' : 'x'; + togglestats[10] = curconfig[StrictTLS].val.i ? 'T' : 't'; + togglestats[11] = '\0'; +} + +void +getpagestats(Client *c) +{ + if (c->https) + pagestats[0] = (c->tlserr || c->insecure) ? 'U' : 'T'; + else + pagestats[0] = '-'; + pagestats[1] = '\0'; +} + +WebKitCookieAcceptPolicy +cookiepolicy_get(void) +{ + switch (((char *)curconfig[CookiePolicies].val.v)[cookiepolicy]) { + case 'a': + return WEBKIT_COOKIE_POLICY_ACCEPT_NEVER; + case '@': + return WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY; + default: /* fallthrough */ + case 'A': + return WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS; + } +} + +char +cookiepolicy_set(const WebKitCookieAcceptPolicy p) +{ + switch (p) { + case WEBKIT_COOKIE_POLICY_ACCEPT_NEVER: + return 'a'; + case WEBKIT_COOKIE_POLICY_ACCEPT_NO_THIRD_PARTY: + return '@'; + default: /* fallthrough */ + case WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS: + return 'A'; + } +} + +void +seturiparameters(Client *c, const char *uri, ParamName *params) +{ + Parameter *config, *uriconfig = NULL; + int i, p; + + for (i = 0; i < LENGTH(uriparams); ++i) { + if (uriparams[i].uri && + !regexec(&(uriparams[i].re), uri, 0, NULL, 0)) { + uriconfig = uriparams[i].config; + break; + } + } + + curconfig = uriconfig ? uriconfig : defconfig; + + for (i = 0; (p = params[i]) != ParameterLast; ++i) { + switch(p) { + default: /* FALLTHROUGH */ + if (!(defconfig[p].prio < curconfig[p].prio || + defconfig[p].prio < modparams[p])) + continue; + case Certificate: + case CookiePolicies: + case Style: + setparameter(c, 0, p, &curconfig[p].val); + } + } +} + +void +setparameter(Client *c, int refresh, ParamName p, const Arg *a) +{ + GdkRGBA bgcolor = { 0 }; + WebKitSettings *s = webkit_web_view_get_settings(c->view); + + modparams[p] = curconfig[p].prio; + + switch (p) { + case AccessMicrophone: + return; /* do nothing */ + case AccessWebcam: + return; /* do nothing */ + case CaretBrowsing: + webkit_settings_set_enable_caret_browsing(s, a->i); + refresh = 0; + break; + case Certificate: + if (a->i) + setcert(c, geturi(c)); + return; /* do not update */ + case CookiePolicies: + webkit_cookie_manager_set_accept_policy( + webkit_web_context_get_cookie_manager( + webkit_web_view_get_context(c->view)), + cookiepolicy_get()); + refresh = 0; + break; + case DiskCache: + webkit_web_context_set_cache_model( + webkit_web_view_get_context(c->view), a->i ? + WEBKIT_CACHE_MODEL_WEB_BROWSER : + WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER); + return; /* do not update */ + case DefaultCharset: + webkit_settings_set_default_charset(s, a->v); + return; /* do not update */ + case DNSPrefetch: + webkit_settings_set_enable_dns_prefetching(s, a->i); + return; /* do not update */ + case FileURLsCrossAccess: + webkit_settings_set_allow_file_access_from_file_urls(s, a->i); + webkit_settings_set_allow_universal_access_from_file_urls(s, a->i); + return; /* do not update */ + case FontSize: + webkit_settings_set_default_font_size(s, a->i); + return; /* do not update */ + case FrameFlattening: + webkit_settings_set_enable_frame_flattening(s, a->i); + break; + case Geolocation: + refresh = 0; + break; + case HideBackground: + if (a->i) + webkit_web_view_set_background_color(c->view, &bgcolor); + return; /* do not update */ + case Inspector: + webkit_settings_set_enable_developer_extras(s, a->i); + return; /* do not update */ + case Java: + webkit_settings_set_enable_java(s, a->i); + return; /* do not update */ + case JavaScript: + webkit_settings_set_enable_javascript(s, a->i); + break; + case KioskMode: + return; /* do nothing */ + case LoadImages: + webkit_settings_set_auto_load_images(s, a->i); + break; + case MediaManualPlay: + webkit_settings_set_media_playback_requires_user_gesture(s, a->i); + break; + case PreferredLanguages: + return; /* do nothing */ + case RunInFullscreen: + return; /* do nothing */ + case ScrollBars: + /* Disabled until we write some WebKitWebExtension for + * manipulating the DOM directly. + enablescrollbars = !enablescrollbars; + evalscript(c, "document.documentElement.style.overflow = '%s'", + enablescrollbars ? "auto" : "hidden"); + */ + return; /* do not update */ + case ShowIndicators: + break; + case SmoothScrolling: + webkit_settings_set_enable_smooth_scrolling(s, a->i); + return; /* do not update */ + case SiteQuirks: + webkit_settings_set_enable_site_specific_quirks(s, a->i); + break; + case SpellChecking: + webkit_web_context_set_spell_checking_enabled( + webkit_web_view_get_context(c->view), a->i); + return; /* do not update */ + case SpellLanguages: + return; /* do nothing */ + case StrictTLS: + webkit_web_context_set_tls_errors_policy( + webkit_web_view_get_context(c->view), a->i ? + WEBKIT_TLS_ERRORS_POLICY_FAIL : + WEBKIT_TLS_ERRORS_POLICY_IGNORE); + break; + case Style: + webkit_user_content_manager_remove_all_style_sheets( + webkit_web_view_get_user_content_manager(c->view)); + if (a->i) + setstyle(c, getstyle(geturi(c))); + refresh = 0; + break; + case WebGL: + webkit_settings_set_enable_webgl(s, a->i); + break; + case ZoomLevel: + webkit_web_view_set_zoom_level(c->view, a->f); + return; /* do not update */ + default: + return; /* do nothing */ + } + + updatetitle(c); + if (refresh) + reload(c, a); +} + +const char * +getcert(const char *uri) +{ + int i; + + for (i = 0; i < LENGTH(certs); ++i) { + if (certs[i].regex && + !regexec(&(certs[i].re), uri, 0, NULL, 0)) + return certs[i].file; + } + + return NULL; +} + +void +setcert(Client *c, const char *uri) +{ + const char *file = getcert(uri); + char *host; + GTlsCertificate *cert; + + if (!file) + return; + + if (!(cert = g_tls_certificate_new_from_file(file, NULL))) { + fprintf(stderr, "Could not read certificate file: %s\n", file); + return; + } + + if ((uri = strstr(uri, "https://"))) { + uri += sizeof("https://") - 1; + host = g_strndup(uri, strchr(uri, '/') - uri); + webkit_web_context_allow_tls_certificate_for_host( + webkit_web_view_get_context(c->view), cert, host); + g_free(host); + } + + g_object_unref(cert); + +} + +const char * +getstyle(const char *uri) +{ + int i; + + if (stylefile) + return stylefile; + + for (i = 0; i < LENGTH(styles); ++i) { + if (styles[i].regex && + !regexec(&(styles[i].re), uri, 0, NULL, 0)) + return styles[i].file; + } + + return ""; +} + +void +setstyle(Client *c, const char *file) +{ + gchar *style; + + if (!g_file_get_contents(file, &style, NULL, NULL)) { + fprintf(stderr, "Could not read style file: %s\n", file); + return; + } + + webkit_user_content_manager_add_style_sheet( + webkit_web_view_get_user_content_manager(c->view), + webkit_user_style_sheet_new(style, + WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, + WEBKIT_USER_STYLE_LEVEL_USER, + NULL, NULL)); + + g_free(style); +} + +void +runscript(Client *c) +{ + gchar *script; + gsize l; + + if (g_file_get_contents(scriptfile, &script, &l, NULL) && l) + evalscript(c, "%s", script); + g_free(script); +} + +void +evalscript(Client *c, const char *jsstr, ...) +{ + va_list ap; + gchar *script; + + va_start(ap, jsstr); + script = g_strdup_vprintf(jsstr, ap); + va_end(ap); + + webkit_web_view_run_javascript(c->view, script, NULL, NULL, NULL); + g_free(script); +} + +void +updatewinid(Client *c) +{ + snprintf(winid, LENGTH(winid), "%lu", c->xid); +} + +void +handleplumb(Client *c, const char *uri) +{ + Arg a = (Arg)PLUMB(uri); + spawn(c, &a); +} + +void +newwindow(Client *c, const Arg *a, int noembed) +{ + int i = 0; + char tmp[64]; + const char *cmd[29], *uri; + const Arg arg = { .v = cmd }; + + cmd[i++] = argv0; + cmd[i++] = "-a"; + cmd[i++] = curconfig[CookiePolicies].val.v; + cmd[i++] = curconfig[ScrollBars].val.i ? "-B" : "-b"; + if (cookiefile && g_strcmp0(cookiefile, "")) { + cmd[i++] = "-c"; + cmd[i++] = cookiefile; + } + if (stylefile && g_strcmp0(stylefile, "")) { + cmd[i++] = "-C"; + cmd[i++] = stylefile; + } + cmd[i++] = curconfig[DiskCache].val.i ? "-D" : "-d"; + if (embed && !noembed) { + cmd[i++] = "-e"; + snprintf(tmp, LENGTH(tmp), "%lu", embed); + cmd[i++] = tmp; + } + cmd[i++] = curconfig[RunInFullscreen].val.i ? "-F" : "-f" ; + cmd[i++] = curconfig[Geolocation].val.i ? "-G" : "-g" ; + cmd[i++] = curconfig[LoadImages].val.i ? "-I" : "-i" ; + cmd[i++] = curconfig[KioskMode].val.i ? "-K" : "-k" ; + cmd[i++] = curconfig[Style].val.i ? "-M" : "-m" ; + cmd[i++] = curconfig[Inspector].val.i ? "-N" : "-n" ; + if (scriptfile && g_strcmp0(scriptfile, "")) { + cmd[i++] = "-r"; + cmd[i++] = scriptfile; + } + cmd[i++] = curconfig[JavaScript].val.i ? "-S" : "-s"; + cmd[i++] = curconfig[StrictTLS].val.i ? "-T" : "-t"; + if (fulluseragent && g_strcmp0(fulluseragent, "")) { + cmd[i++] = "-u"; + cmd[i++] = fulluseragent; + } + if (showxid) + cmd[i++] = "-w"; + cmd[i++] = curconfig[Certificate].val.i ? "-X" : "-x" ; + /* do not keep zoom level */ + cmd[i++] = "--"; + if ((uri = a->v)) + cmd[i++] = uri; + cmd[i] = NULL; + + spawn(c, &arg); +} + +void +spawn(Client *c, const Arg *a) +{ + if (fork() == 0) { + if (dpy) + close(ConnectionNumber(dpy)); + close(spair[0]); + close(spair[1]); + setsid(); + execvp(((char **)a->v)[0], (char **)a->v); + fprintf(stderr, "%s: execvp %s", argv0, ((char **)a->v)[0]); + perror(" failed"); + exit(1); + } +} + +void +destroyclient(Client *c) +{ + Client *p; + + webkit_web_view_stop_loading(c->view); + /* Not needed, has already been called + gtk_widget_destroy(c->win); + */ + + for (p = clients; p && p->next != c; p = p->next) + ; + if (p) + p->next = c->next; + else + clients = c->next; + free(c); +} + +void +cleanup(void) +{ + while (clients) + destroyclient(clients); + + close(spair[0]); + close(spair[1]); + g_free(cookiefile); + g_free(scriptfile); + g_free(stylefile); + g_free(cachedir); + XCloseDisplay(dpy); +} + +WebKitWebView * +newview(Client *c, WebKitWebView *rv) +{ + WebKitWebView *v; + WebKitSettings *settings; + WebKitWebContext *context; + WebKitCookieManager *cookiemanager; + WebKitUserContentManager *contentmanager; + + /* Webview */ + if (rv) { + v = WEBKIT_WEB_VIEW(webkit_web_view_new_with_related_view(rv)); + } else { + settings = webkit_settings_new_with_settings( + "allow-file-access-from-file-urls", curconfig[FileURLsCrossAccess].val.i, + "allow-universal-access-from-file-urls", curconfig[FileURLsCrossAccess].val.i, + "auto-load-images", curconfig[LoadImages].val.i, + "default-charset", curconfig[DefaultCharset].val.v, + "default-font-size", curconfig[FontSize].val.i, + "enable-caret-browsing", curconfig[CaretBrowsing].val.i, + "enable-developer-extras", curconfig[Inspector].val.i, + "enable-dns-prefetching", curconfig[DNSPrefetch].val.i, + "enable-frame-flattening", curconfig[FrameFlattening].val.i, + "enable-html5-database", curconfig[DiskCache].val.i, + "enable-html5-local-storage", curconfig[DiskCache].val.i, + "enable-java", curconfig[Java].val.i, + "enable-javascript", curconfig[JavaScript].val.i, + "enable-site-specific-quirks", curconfig[SiteQuirks].val.i, + "enable-smooth-scrolling", curconfig[SmoothScrolling].val.i, + "enable-webgl", curconfig[WebGL].val.i, + "media-playback-requires-user-gesture", curconfig[MediaManualPlay].val.i, + NULL); +/* For more interesting settings, have a look at + * http://webkitgtk.org/reference/webkit2gtk/stable/WebKitSettings.html */ + + if (strcmp(fulluseragent, "")) { + webkit_settings_set_user_agent(settings, fulluseragent); + } else if (surfuseragent) { + webkit_settings_set_user_agent_with_application_details( + settings, "Surf", VERSION); + } + useragent = webkit_settings_get_user_agent(settings); + + contentmanager = webkit_user_content_manager_new(); + + if (curconfig[Ephemeral].val.i) { + context = webkit_web_context_new_ephemeral(); + } else { + context = webkit_web_context_new_with_website_data_manager( + webkit_website_data_manager_new( + "base-cache-directory", cachedir, + "base-data-directory", cachedir, + NULL)); + } + + + cookiemanager = webkit_web_context_get_cookie_manager(context); + + /* rendering process model, can be a shared unique one + * or one for each view */ + webkit_web_context_set_process_model(context, + WEBKIT_PROCESS_MODEL_MULTIPLE_SECONDARY_PROCESSES); + /* TLS */ + webkit_web_context_set_tls_errors_policy(context, + curconfig[StrictTLS].val.i ? WEBKIT_TLS_ERRORS_POLICY_FAIL : + WEBKIT_TLS_ERRORS_POLICY_IGNORE); + /* disk cache */ + webkit_web_context_set_cache_model(context, + curconfig[DiskCache].val.i ? WEBKIT_CACHE_MODEL_WEB_BROWSER : + WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER); + + /* Currently only works with text file to be compatible with curl */ + if (!curconfig[Ephemeral].val.i) + webkit_cookie_manager_set_persistent_storage(cookiemanager, + cookiefile, WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); + /* cookie policy */ + webkit_cookie_manager_set_accept_policy(cookiemanager, + cookiepolicy_get()); + /* languages */ + webkit_web_context_set_preferred_languages(context, + curconfig[PreferredLanguages].val.v); + webkit_web_context_set_spell_checking_languages(context, + curconfig[SpellLanguages].val.v); + webkit_web_context_set_spell_checking_enabled(context, + curconfig[SpellChecking].val.i); + + g_signal_connect(G_OBJECT(context), "download-started", + G_CALLBACK(downloadstarted), c); + g_signal_connect(G_OBJECT(context), "initialize-web-extensions", + G_CALLBACK(initwebextensions), c); + + v = g_object_new(WEBKIT_TYPE_WEB_VIEW, + "settings", settings, + "user-content-manager", contentmanager, + "web-context", context, + NULL); + } + + g_signal_connect(G_OBJECT(v), "notify::estimated-load-progress", + G_CALLBACK(progresschanged), c); + g_signal_connect(G_OBJECT(v), "notify::title", + G_CALLBACK(titlechanged), c); + g_signal_connect(G_OBJECT(v), "button-release-event", + G_CALLBACK(buttonreleased), c); + g_signal_connect(G_OBJECT(v), "close", + G_CALLBACK(closeview), c); + g_signal_connect(G_OBJECT(v), "create", + G_CALLBACK(createview), c); + g_signal_connect(G_OBJECT(v), "decide-policy", + G_CALLBACK(decidepolicy), c); + g_signal_connect(G_OBJECT(v), "insecure-content-detected", + G_CALLBACK(insecurecontent), c); + g_signal_connect(G_OBJECT(v), "load-failed-with-tls-errors", + G_CALLBACK(loadfailedtls), c); + g_signal_connect(G_OBJECT(v), "load-changed", + G_CALLBACK(loadchanged), c); + g_signal_connect(G_OBJECT(v), "mouse-target-changed", + G_CALLBACK(mousetargetchanged), c); + g_signal_connect(G_OBJECT(v), "permission-request", + G_CALLBACK(permissionrequested), c); + g_signal_connect(G_OBJECT(v), "ready-to-show", + G_CALLBACK(showview), c); + g_signal_connect(G_OBJECT(v), "web-process-terminated", + G_CALLBACK(webprocessterminated), c); + + return v; +} + +static gboolean +readsock(GIOChannel *s, GIOCondition ioc, gpointer unused) +{ + static char msg[MSGBUFSZ]; + GError *gerr = NULL; + gsize msgsz; + + if (g_io_channel_read_chars(s, msg, sizeof(msg), &msgsz, &gerr) != + G_IO_STATUS_NORMAL) { + if (gerr) { + fprintf(stderr, "surf: error reading socket: %s\n", + gerr->message); + g_error_free(gerr); + } + return TRUE; + } + if (msgsz < 2) { + fprintf(stderr, "surf: message too short: %d\n", msgsz); + return TRUE; + } + + return TRUE; +} + +void +initwebextensions(WebKitWebContext *wc, Client *c) +{ + GVariant *gv; + + if (spair[1] < 0) + return; + + gv = g_variant_new("i", spair[1]); + + webkit_web_context_set_web_extensions_initialization_user_data(wc, gv); + webkit_web_context_set_web_extensions_directory(wc, WEBEXTDIR); +} + +GtkWidget * +createview(WebKitWebView *v, WebKitNavigationAction *a, Client *c) +{ + Client *n; + + switch (webkit_navigation_action_get_navigation_type(a)) { + case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */ + /* + * popup windows of type “other” are almost always triggered + * by user gesture, so inverse the logic here + */ +/* instead of this, compare destination uri to mouse-over uri for validating window */ + if (webkit_navigation_action_is_user_gesture(a)) + return NULL; + case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: + n = newclient(c); + break; + default: + return NULL; + } + + return GTK_WIDGET(n->view); +} + +gboolean +buttonreleased(GtkWidget *w, GdkEvent *e, Client *c) +{ + WebKitHitTestResultContext element; + int i; + + element = webkit_hit_test_result_get_context(c->mousepos); + + for (i = 0; i < LENGTH(buttons); ++i) { + if (element & buttons[i].target && + e->button.button == buttons[i].button && + CLEANMASK(e->button.state) == CLEANMASK(buttons[i].mask) && + buttons[i].func) { + buttons[i].func(c, &buttons[i].arg, c->mousepos); + return buttons[i].stopevent; + } + } + + return FALSE; +} + +GdkFilterReturn +processx(GdkXEvent *e, GdkEvent *event, gpointer d) +{ + Client *c = (Client *)d; + XPropertyEvent *ev; + Arg a; + + if (((XEvent *)e)->type == PropertyNotify) { + ev = &((XEvent *)e)->xproperty; + if (ev->state == PropertyNewValue) { + if (ev->atom == atoms[AtomFind]) { + find(c, NULL); + + return GDK_FILTER_REMOVE; + } else if (ev->atom == atoms[AtomGo]) { + a.v = getatom(c, AtomGo); + loaduri(c, &a); + + return GDK_FILTER_REMOVE; + } + } + } + return GDK_FILTER_CONTINUE; +} + +gboolean +winevent(GtkWidget *w, GdkEvent *e, Client *c) +{ + int i; + + switch (e->type) { + case GDK_ENTER_NOTIFY: + c->overtitle = c->targeturi; + updatetitle(c); + break; + case GDK_KEY_PRESS: + if (!curconfig[KioskMode].val.i) { + for (i = 0; i < LENGTH(keys); ++i) { + if (gdk_keyval_to_lower(e->key.keyval) == + keys[i].keyval && + CLEANMASK(e->key.state) == keys[i].mod && + keys[i].func) { + updatewinid(c); + keys[i].func(c, &(keys[i].arg)); + return TRUE; + } + } + } + case GDK_LEAVE_NOTIFY: + c->overtitle = NULL; + updatetitle(c); + break; + case GDK_WINDOW_STATE: + if (e->window_state.changed_mask == + GDK_WINDOW_STATE_FULLSCREEN) + c->fullscreen = e->window_state.new_window_state & + GDK_WINDOW_STATE_FULLSCREEN; + break; + default: + break; + } + + return FALSE; +} + +void +showview(WebKitWebView *v, Client *c) +{ + GdkRGBA bgcolor = { 0 }; + GdkWindow *gwin; + + c->finder = webkit_web_view_get_find_controller(c->view); + c->inspector = webkit_web_view_get_inspector(c->view); + + c->pageid = webkit_web_view_get_page_id(c->view); + c->win = createwindow(c); + + gtk_container_add(GTK_CONTAINER(c->win), GTK_WIDGET(c->view)); + gtk_widget_show_all(c->win); + gtk_widget_grab_focus(GTK_WIDGET(c->view)); + + gwin = gtk_widget_get_window(GTK_WIDGET(c->win)); + c->xid = gdk_x11_window_get_xid(gwin); + updatewinid(c); + if (showxid) { + gdk_display_sync(gtk_widget_get_display(c->win)); + puts(winid); + fflush(stdout); + } + + if (curconfig[HideBackground].val.i) + webkit_web_view_set_background_color(c->view, &bgcolor); + + if (!curconfig[KioskMode].val.i) { + gdk_window_set_events(gwin, GDK_ALL_EVENTS_MASK); + gdk_window_add_filter(gwin, processx, c); + } + + if (curconfig[RunInFullscreen].val.i) + togglefullscreen(c, NULL); + + if (curconfig[ZoomLevel].val.f != 1.0) + webkit_web_view_set_zoom_level(c->view, + curconfig[ZoomLevel].val.f); + + setatom(c, AtomFind, ""); + setatom(c, AtomUri, "about:blank"); +} + +GtkWidget * +createwindow(Client *c) +{ + char *wmstr; + GtkWidget *w; + + if (embed) { + w = gtk_plug_new(embed); + } else { + w = gtk_window_new(GTK_WINDOW_TOPLEVEL); + + wmstr = g_path_get_basename(argv0); + gtk_window_set_wmclass(GTK_WINDOW(w), wmstr, "Surf"); + g_free(wmstr); + + wmstr = g_strdup_printf("%s[%"PRIu64"]", "Surf", c->pageid); + gtk_window_set_role(GTK_WINDOW(w), wmstr); + g_free(wmstr); + + gtk_window_set_default_size(GTK_WINDOW(w), winsize[0], winsize[1]); + } + + g_signal_connect(G_OBJECT(w), "destroy", + G_CALLBACK(destroywin), c); + g_signal_connect(G_OBJECT(w), "enter-notify-event", + G_CALLBACK(winevent), c); + g_signal_connect(G_OBJECT(w), "key-press-event", + G_CALLBACK(winevent), c); + g_signal_connect(G_OBJECT(w), "leave-notify-event", + G_CALLBACK(winevent), c); + g_signal_connect(G_OBJECT(w), "window-state-event", + G_CALLBACK(winevent), c); + + return w; +} + +gboolean +loadfailedtls(WebKitWebView *v, gchar *uri, GTlsCertificate *cert, + GTlsCertificateFlags err, Client *c) +{ + GString *errmsg = g_string_new(NULL); + gchar *html, *pem; + + c->failedcert = g_object_ref(cert); + c->tlserr = err; + c->errorpage = 1; + + if (err & G_TLS_CERTIFICATE_UNKNOWN_CA) + g_string_append(errmsg, + "The signing certificate authority is not known.<br>"); + if (err & G_TLS_CERTIFICATE_BAD_IDENTITY) + g_string_append(errmsg, + "The certificate does not match the expected identity " + "of the site that it was retrieved from.<br>"); + if (err & G_TLS_CERTIFICATE_NOT_ACTIVATED) + g_string_append(errmsg, + "The certificate's activation time " + "is still in the future.<br>"); + if (err & G_TLS_CERTIFICATE_EXPIRED) + g_string_append(errmsg, "The certificate has expired.<br>"); + if (err & G_TLS_CERTIFICATE_REVOKED) + g_string_append(errmsg, + "The certificate has been revoked according to " + "the GTlsConnection's certificate revocation list.<br>"); + if (err & G_TLS_CERTIFICATE_INSECURE) + g_string_append(errmsg, + "The certificate's algorithm is considered insecure.<br>"); + if (err & G_TLS_CERTIFICATE_GENERIC_ERROR) + g_string_append(errmsg, + "Some error occurred validating the certificate.<br>"); + + g_object_get(cert, "certificate-pem", &pem, NULL); + html = g_strdup_printf("<p>Could not validate TLS for “%s”<br>%s</p>" + "<p>You can inspect the following certificate " + "with Ctrl-t (default keybinding).</p>" + "<p><pre>%s</pre></p>", uri, errmsg->str, pem); + g_free(pem); + g_string_free(errmsg, TRUE); + + webkit_web_view_load_alternate_html(c->view, html, uri, NULL); + g_free(html); + + return TRUE; +} + +void +loadchanged(WebKitWebView *v, WebKitLoadEvent e, Client *c) +{ + const char *uri = geturi(c); + + switch (e) { + case WEBKIT_LOAD_STARTED: + setatom(c, AtomUri, uri); + c->title = uri; + c->https = c->insecure = 0; + seturiparameters(c, uri, loadtransient); + if (c->errorpage) + c->errorpage = 0; + else + g_clear_object(&c->failedcert); + break; + case WEBKIT_LOAD_REDIRECTED: + setatom(c, AtomUri, uri); + c->title = uri; + seturiparameters(c, uri, loadtransient); + break; + case WEBKIT_LOAD_COMMITTED: + setatom(c, AtomUri, uri); + c->title = uri; + seturiparameters(c, uri, loadcommitted); + c->https = webkit_web_view_get_tls_info(c->view, &c->cert, + &c->tlserr); + break; + case WEBKIT_LOAD_FINISHED: + seturiparameters(c, uri, loadfinished); + /* Disabled until we write some WebKitWebExtension for + * manipulating the DOM directly. + evalscript(c, "document.documentElement.style.overflow = '%s'", + enablescrollbars ? "auto" : "hidden"); + */ + runscript(c); + break; + } + updatetitle(c); +} + +void +progresschanged(WebKitWebView *v, GParamSpec *ps, Client *c) +{ + c->progress = webkit_web_view_get_estimated_load_progress(c->view) * + 100; + updatetitle(c); +} + +void +titlechanged(WebKitWebView *view, GParamSpec *ps, Client *c) +{ + c->title = webkit_web_view_get_title(c->view); + updatetitle(c); +} + +void +mousetargetchanged(WebKitWebView *v, WebKitHitTestResult *h, guint modifiers, + Client *c) +{ + WebKitHitTestResultContext hc = webkit_hit_test_result_get_context(h); + + /* Keep the hit test to know where is the pointer on the next click */ + c->mousepos = h; + + if (hc & OnLink) + c->targeturi = webkit_hit_test_result_get_link_uri(h); + else if (hc & OnImg) + c->targeturi = webkit_hit_test_result_get_image_uri(h); + else if (hc & OnMedia) + c->targeturi = webkit_hit_test_result_get_media_uri(h); + else + c->targeturi = NULL; + + c->overtitle = c->targeturi; + updatetitle(c); +} + +gboolean +permissionrequested(WebKitWebView *v, WebKitPermissionRequest *r, Client *c) +{ + ParamName param = ParameterLast; + + if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST(r)) { + param = Geolocation; + } else if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(r)) { + if (webkit_user_media_permission_is_for_audio_device( + WEBKIT_USER_MEDIA_PERMISSION_REQUEST(r))) + param = AccessMicrophone; + else if (webkit_user_media_permission_is_for_video_device( + WEBKIT_USER_MEDIA_PERMISSION_REQUEST(r))) + param = AccessWebcam; + } else { + return FALSE; + } + + if (curconfig[param].val.i) + webkit_permission_request_allow(r); + else + webkit_permission_request_deny(r); + + return TRUE; +} + +gboolean +decidepolicy(WebKitWebView *v, WebKitPolicyDecision *d, + WebKitPolicyDecisionType dt, Client *c) +{ + switch (dt) { + case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION: + decidenavigation(d, c); + break; + case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION: + decidenewwindow(d, c); + break; + case WEBKIT_POLICY_DECISION_TYPE_RESPONSE: + decideresource(d, c); + break; + default: + webkit_policy_decision_ignore(d); + break; + } + return TRUE; +} + +void +decidenavigation(WebKitPolicyDecision *d, Client *c) +{ + WebKitNavigationAction *a = + webkit_navigation_policy_decision_get_navigation_action( + WEBKIT_NAVIGATION_POLICY_DECISION(d)); + + switch (webkit_navigation_action_get_navigation_type(a)) { + case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */ + default: + /* Do not navigate to links with a "_blank" target (popup) */ + if (webkit_navigation_policy_decision_get_frame_name( + WEBKIT_NAVIGATION_POLICY_DECISION(d))) { + webkit_policy_decision_ignore(d); + } else { + /* Filter out navigation to different domain ? */ + /* get action→urirequest, copy and load in new window+view + * on Ctrl+Click ? */ + webkit_policy_decision_use(d); + } + break; + } +} + +void +decidenewwindow(WebKitPolicyDecision *d, Client *c) +{ + Arg arg; + WebKitNavigationAction *a = + webkit_navigation_policy_decision_get_navigation_action( + WEBKIT_NAVIGATION_POLICY_DECISION(d)); + + + switch (webkit_navigation_action_get_navigation_type(a)) { + case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_RELOAD: /* fallthrough */ + case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED: + /* Filter domains here */ +/* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event. + * test for link clicked but no button ? */ + arg.v = webkit_uri_request_get_uri( + webkit_navigation_action_get_request(a)); + newwindow(c, &arg, 0); + break; + case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */ + default: + break; + } + + webkit_policy_decision_ignore(d); +} + +void +decideresource(WebKitPolicyDecision *d, Client *c) +{ + int i, isascii = 1; + WebKitResponsePolicyDecision *r = WEBKIT_RESPONSE_POLICY_DECISION(d); + WebKitURIResponse *res = + webkit_response_policy_decision_get_response(r); + const gchar *uri = webkit_uri_response_get_uri(res); + + if (g_str_has_suffix(uri, "/favicon.ico")) { + webkit_policy_decision_ignore(d); + return; + } + + if (!g_str_has_prefix(uri, "http://") + && !g_str_has_prefix(uri, "https://") + && !g_str_has_prefix(uri, "about:") + && !g_str_has_prefix(uri, "file://") + && !g_str_has_prefix(uri, "data:") + && !g_str_has_prefix(uri, "blob:") + && strlen(uri) > 0) { + for (i = 0; i < strlen(uri); i++) { + if (!g_ascii_isprint(uri[i])) { + isascii = 0; + break; + } + } + if (isascii) { + handleplumb(c, uri); + webkit_policy_decision_ignore(d); + return; + } + } + + if (webkit_response_policy_decision_is_mime_type_supported(r)) { + webkit_policy_decision_use(d); + } else { + webkit_policy_decision_ignore(d); + download(c, res); + } +} + +void +insecurecontent(WebKitWebView *v, WebKitInsecureContentEvent e, Client *c) +{ + c->insecure = 1; +} + +void +downloadstarted(WebKitWebContext *wc, WebKitDownload *d, Client *c) +{ + g_signal_connect(G_OBJECT(d), "notify::response", + G_CALLBACK(responsereceived), c); +} + +void +responsereceived(WebKitDownload *d, GParamSpec *ps, Client *c) +{ + download(c, webkit_download_get_response(d)); + webkit_download_cancel(d); +} + +void +download(Client *c, WebKitURIResponse *r) +{ + Arg a = (Arg)DOWNLOAD(webkit_uri_response_get_uri(r), geturi(c)); + spawn(c, &a); +} + +void +webprocessterminated(WebKitWebView *v, WebKitWebProcessTerminationReason r, + Client *c) +{ + fprintf(stderr, "web process terminated: %s\n", + r == WEBKIT_WEB_PROCESS_CRASHED ? "crashed" : "no memory"); + closeview(v, c); +} + +void +closeview(WebKitWebView *v, Client *c) +{ + gtk_widget_destroy(c->win); +} + +void +destroywin(GtkWidget* w, Client *c) +{ + destroyclient(c); + if (!clients) + gtk_main_quit(); +} + +void +pasteuri(GtkClipboard *clipboard, const char *text, gpointer d) +{ + Arg a = {.v = text }; + if (text) + loaduri((Client *) d, &a); +} + +void +reload(Client *c, const Arg *a) +{ + if (a->i) + webkit_web_view_reload_bypass_cache(c->view); + else + webkit_web_view_reload(c->view); +} + +void +print(Client *c, const Arg *a) +{ + webkit_print_operation_run_dialog(webkit_print_operation_new(c->view), + GTK_WINDOW(c->win)); +} + +void +showcert(Client *c, const Arg *a) +{ + GTlsCertificate *cert = c->failedcert ? c->failedcert : c->cert; + GcrCertificate *gcrt; + GByteArray *crt; + GtkWidget *win; + GcrCertificateWidget *wcert; + + if (!cert) + return; + + g_object_get(cert, "certificate", &crt, NULL); + gcrt = gcr_simple_certificate_new(crt->data, crt->len); + g_byte_array_unref(crt); + + win = gtk_window_new(GTK_WINDOW_TOPLEVEL); + wcert = gcr_certificate_widget_new(gcrt); + g_object_unref(gcrt); + + gtk_container_add(GTK_CONTAINER(win), GTK_WIDGET(wcert)); + gtk_widget_show_all(win); +} + +void +clipboard(Client *c, const Arg *a) +{ + /* User defined choice of selection, see config.h */ + GdkAtom selection = GDK_SELECTION_PRIMARY; + if (curconfig[ClipboardNotPrimary].val.i > 0) + selection = GDK_SELECTION_CLIPBOARD; + + if (a->i) { /* load clipboard uri */ + gtk_clipboard_request_text(gtk_clipboard_get( + selection), + pasteuri, c); + } else { /* copy uri */ + gtk_clipboard_set_text(gtk_clipboard_get( + selection), c->targeturi + ? c->targeturi : geturi(c), -1); + } +} + +void +zoom(Client *c, const Arg *a) +{ + if (a->i > 0) + webkit_web_view_set_zoom_level(c->view, + curconfig[ZoomLevel].val.f + 0.1); + else if (a->i < 0) + webkit_web_view_set_zoom_level(c->view, + curconfig[ZoomLevel].val.f - 0.1); + else + webkit_web_view_set_zoom_level(c->view, 1.0); + + curconfig[ZoomLevel].val.f = webkit_web_view_get_zoom_level(c->view); +} + +static void +msgext(Client *c, char type, const Arg *a) +{ + static signed char msg[MSGBUFSZ]; + int ret; + + if (spair[0] < 0) + return; + + if ((ret = snprintf(msg, sizeof(msg), "%c%c%c", c->pageid, type, a->i)) + >= sizeof(msg)) { + fprintf(stderr, "surf: message too long: %d\n", ret); + return; + } + + if (send(spair[0], msg, ret, 0) != ret) + fprintf(stderr, "surf: error sending: %u%c%d (%d)\n", + c->pageid, type, a->i, ret); +} + +void +scrollv(Client *c, const Arg *a) +{ + msgext(c, 'v', a); +} + +void +scrollh(Client *c, const Arg *a) +{ + msgext(c, 'h', a); +} + +void +navigate(Client *c, const Arg *a) +{ + if (a->i < 0) + webkit_web_view_go_back(c->view); + else if (a->i > 0) + webkit_web_view_go_forward(c->view); +} + +void +stop(Client *c, const Arg *a) +{ + webkit_web_view_stop_loading(c->view); +} + +void +toggle(Client *c, const Arg *a) +{ + curconfig[a->i].val.i ^= 1; + setparameter(c, 1, (ParamName)a->i, &curconfig[a->i].val); +} + +void +togglefullscreen(Client *c, const Arg *a) +{ + /* toggling value is handled in winevent() */ + if (c->fullscreen) + gtk_window_unfullscreen(GTK_WINDOW(c->win)); + else + gtk_window_fullscreen(GTK_WINDOW(c->win)); +} + +void +togglecookiepolicy(Client *c, const Arg *a) +{ + ++cookiepolicy; + cookiepolicy %= strlen(curconfig[CookiePolicies].val.v); + + setparameter(c, 0, CookiePolicies, NULL); +} + +void +toggleinspector(Client *c, const Arg *a) +{ + if (webkit_web_inspector_is_attached(c->inspector)) + webkit_web_inspector_close(c->inspector); + else if (curconfig[Inspector].val.i) + webkit_web_inspector_show(c->inspector); +} + +void +find(Client *c, const Arg *a) +{ + const char *s, *f; + + if (a && a->i) { + if (a->i > 0) + webkit_find_controller_search_next(c->finder); + else + webkit_find_controller_search_previous(c->finder); + } else { + s = getatom(c, AtomFind); + f = webkit_find_controller_get_search_text(c->finder); + + if (g_strcmp0(f, s) == 0) /* reset search */ + webkit_find_controller_search(c->finder, "", findopts, + G_MAXUINT); + + webkit_find_controller_search(c->finder, s, findopts, + G_MAXUINT); + + if (strcmp(s, "") == 0) + webkit_find_controller_search_finish(c->finder); + } +} + +void +clicknavigate(Client *c, const Arg *a, WebKitHitTestResult *h) +{ + navigate(c, a); +} + +void +clicknewwindow(Client *c, const Arg *a, WebKitHitTestResult *h) +{ + Arg arg; + + arg.v = webkit_hit_test_result_get_link_uri(h); + newwindow(c, &arg, a->i); +} + +void +clickexternplayer(Client *c, const Arg *a, WebKitHitTestResult *h) +{ + Arg arg; + + arg = (Arg)VIDEOPLAY(webkit_hit_test_result_get_media_uri(h)); + spawn(c, &arg); +} + +int +main(int argc, char *argv[]) +{ + Arg arg; + Client *c; + + memset(&arg, 0, sizeof(arg)); + + /* command line args */ + ARGBEGIN { + case 'a': + defconfig[CookiePolicies].val.v = EARGF(usage()); + defconfig[CookiePolicies].prio = 2; + break; + case 'b': + defconfig[ScrollBars].val.i = 0; + defconfig[ScrollBars].prio = 2; + break; + case 'B': + defconfig[ScrollBars].val.i = 1; + defconfig[ScrollBars].prio = 2; + break; + case 'c': + cookiefile = EARGF(usage()); + break; + case 'C': + stylefile = EARGF(usage()); + break; + case 'd': + defconfig[DiskCache].val.i = 0; + defconfig[DiskCache].prio = 2; + break; + case 'D': + defconfig[DiskCache].val.i = 1; + defconfig[DiskCache].prio = 2; + break; + case 'e': + embed = strtol(EARGF(usage()), NULL, 0); + break; + case 'f': + defconfig[RunInFullscreen].val.i = 0; + defconfig[RunInFullscreen].prio = 2; + break; + case 'F': + defconfig[RunInFullscreen].val.i = 1; + defconfig[RunInFullscreen].prio = 2; + break; + case 'g': + defconfig[Geolocation].val.i = 0; + defconfig[Geolocation].prio = 2; + break; + case 'G': + defconfig[Geolocation].val.i = 1; + defconfig[Geolocation].prio = 2; + break; + case 'i': + defconfig[LoadImages].val.i = 0; + defconfig[LoadImages].prio = 2; + break; + case 'I': + defconfig[LoadImages].val.i = 1; + defconfig[LoadImages].prio = 2; + break; + case 'k': + defconfig[KioskMode].val.i = 0; + defconfig[KioskMode].prio = 2; + break; + case 'K': + defconfig[KioskMode].val.i = 1; + defconfig[KioskMode].prio = 2; + break; + case 'm': + defconfig[Style].val.i = 0; + defconfig[Style].prio = 2; + break; + case 'M': + defconfig[Style].val.i = 1; + defconfig[Style].prio = 2; + break; + case 'n': + defconfig[Inspector].val.i = 0; + defconfig[Inspector].prio = 2; + break; + case 'N': + defconfig[Inspector].val.i = 1; + defconfig[Inspector].prio = 2; + break; + case 'r': + scriptfile = EARGF(usage()); + break; + case 's': + defconfig[JavaScript].val.i = 0; + defconfig[JavaScript].prio = 2; + break; + case 'S': + defconfig[JavaScript].val.i = 1; + defconfig[JavaScript].prio = 2; + break; + case 't': + defconfig[StrictTLS].val.i = 0; + defconfig[StrictTLS].prio = 2; + break; + case 'T': + defconfig[StrictTLS].val.i = 1; + defconfig[StrictTLS].prio = 2; + break; + case 'u': + fulluseragent = EARGF(usage()); + break; + case 'v': + die("surf-"VERSION", see LICENSE for © details\n"); + case 'w': + showxid = 1; + break; + case 'x': + defconfig[Certificate].val.i = 0; + defconfig[Certificate].prio = 2; + break; + case 'X': + defconfig[Certificate].val.i = 1; + defconfig[Certificate].prio = 2; + break; + case 'z': + defconfig[ZoomLevel].val.f = strtof(EARGF(usage()), NULL); + defconfig[ZoomLevel].prio = 2; + break; + default: + usage(); + } ARGEND; + if (argc > 0) + arg.v = argv[0]; + else + arg.v = "about:blank"; + + setup(); + c = newclient(NULL); + showview(NULL, c); + + loaduri(c, &arg); + updatetitle(c); + + gtk_main(); + cleanup(); + + return 0; +} diff --git a/mut/surf/surf.png b/mut/surf/surf.png Binary files differnew file mode 100644 index 0000000..f5b2ab1 --- /dev/null +++ b/mut/surf/surf.png diff --git a/mut/surf/webext-surf.c b/mut/surf/webext-surf.c new file mode 100644 index 0000000..7eeb55f --- /dev/null +++ b/mut/surf/webext-surf.c @@ -0,0 +1,107 @@ +#include <sys/socket.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <inttypes.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> + +#include <gio/gio.h> +#include <webkit2/webkit-web-extension.h> +#include <webkitdom/webkitdom.h> +#include <webkitdom/WebKitDOMDOMWindowUnstable.h> + +#include "common.h" + +#define LENGTH(x) (sizeof(x) / sizeof(x[0])) + +static WebKitWebExtension *webext; +static int sock; + +static void +msgsurf(guint64 pageid, const char *s) +{ + static char msg[MSGBUFSZ]; + size_t sln = strlen(s); + int ret; + + if ((ret = snprintf(msg, sizeof(msg), "%c%s", pageid, s)) + >= sizeof(msg)) { + fprintf(stderr, "webext: msg: message too long: %d\n", ret); + return; + } + + if (send(sock, msg, ret, 0) < 0) + fprintf(stderr, "webext: error sending: %s\n", msg+1); +} + +static gboolean +readsock(GIOChannel *s, GIOCondition c, gpointer unused) +{ + static char js[48]; + static signed char msg[MSGBUFSZ]; + WebKitWebPage *page; + JSCContext *jsc; + GError *gerr = NULL; + gsize msgsz; + + if (g_io_channel_read_chars(s, msg, sizeof(msg), &msgsz, &gerr) != + G_IO_STATUS_NORMAL) { + if (gerr) { + fprintf(stderr, "webext: error reading socket: %s\n", + gerr->message); + g_error_free(gerr); + } + return TRUE; + } + + if (msgsz < 2) { + fprintf(stderr, "webext: readsock: message too short: %d\n", + msgsz); + return TRUE; + } + + if (!(page = webkit_web_extension_get_page(webext, msg[0]))) + return TRUE; + + jsc = webkit_frame_get_js_context(webkit_web_page_get_main_frame(page)); + + switch (msg[1]) { + case 'h': + if (msgsz != 3) + return TRUE; + snprintf(js, sizeof(js), + "window.scrollBy(window.innerWidth/100*%d,0);", + msg[2]); + jsc_context_evaluate(jsc, js, -1); + break; + case 'v': + if (msgsz != 3) + return TRUE; + snprintf(js, sizeof(js), + "window.scrollBy(0,window.innerHeight/100*%d);", + msg[2]); + jsc_context_evaluate(jsc, js, -1); + break; + } + + return TRUE; +} + +G_MODULE_EXPORT void +webkit_web_extension_initialize_with_user_data(WebKitWebExtension *e, + const GVariant *gv) +{ + GIOChannel *gchansock; + + webext = e; + + g_variant_get(gv, "i", &sock); + + gchansock = g_io_channel_unix_new(sock); + g_io_channel_set_encoding(gchansock, NULL, NULL); + g_io_channel_set_flags(gchansock, g_io_channel_get_flags(gchansock) + | G_IO_FLAG_NONBLOCK, NULL); + g_io_channel_set_close_on_unref(gchansock, TRUE); + g_io_add_watch(gchansock, G_IO_IN, readsock, NULL); +} diff --git a/mut/tabbed/LICENSE b/mut/tabbed/LICENSE new file mode 100644 index 0000000..d8e9678 --- /dev/null +++ b/mut/tabbed/LICENSE @@ -0,0 +1,23 @@ +MIT/X Consortium License + +© 2009-2011 Enno Boland <g s01 de> +© 2011,2015 Connor Lane Smith <cls@lubutu.com> +© 2012-2015 Christoph Lohmann <20h@r-36.net> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/mut/tabbed/Makefile b/mut/tabbed/Makefile new file mode 100644 index 0000000..dda3cdb --- /dev/null +++ b/mut/tabbed/Makefile @@ -0,0 +1,69 @@ +.POSIX: + +NAME = tabbed +VERSION = 0.8 + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man +DOCPREFIX = ${PREFIX}/share/doc/${NAME} + +# use system flags. +TABBED_CFLAGS = -I/usr/X11R6/include -I/usr/include/freetype2 ${CFLAGS} +TABBED_LDFLAGS = -L/usr/X11R6/lib -lX11 -lfontconfig -lXft ${LDFLAGS} +TABBED_CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700L + +# OpenBSD (uncomment) +#TABBED_CFLAGS = -I/usr/X11R6/include -I/usr/X11R6/include/freetype2 ${CFLAGS} + +SRC = tabbed.c xembed.c +OBJ = ${SRC:.c=.o} +BIN = ${OBJ:.o=} +MAN1 = ${BIN:=.1} +HDR = arg.h config.def.h +DOC = LICENSE README + +all: ${BIN} + +.c.o: + ${CC} -o $@ -c $< ${TABBED_CFLAGS} ${TABBED_CPPFLAGS} + +${OBJ}: config.h + +config.h: + cp config.def.h $@ + +.o: + ${CC} -o $@ $< ${TABBED_LDFLAGS} + +clean: + rm -f ${BIN} ${OBJ} "${NAME}-${VERSION}.tar.gz" + +dist: clean + mkdir -p "${NAME}-${VERSION}" + cp -fR Makefile ${MAN1} ${DOC} ${HDR} ${SRC} "${NAME}-${VERSION}" + tar -cf - "${NAME}-${VERSION}" | gzip -c > "${NAME}-${VERSION}.tar.gz" + rm -rf ${NAME}-${VERSION} + +install: all + # installing executable files. + mkdir -p "${DESTDIR}${PREFIX}/bin" + cp -f ${BIN} "${DESTDIR}${PREFIX}/bin" + for f in ${BIN}; do chmod 755 "${DESTDIR}${PREFIX}/bin/$$f"; done + # installing doc files. + mkdir -p "${DESTDIR}${DOCPREFIX}" + cp -f README "${DESTDIR}${DOCPREFIX}" + # installing manual pages for general commands: section 1. + mkdir -p "${DESTDIR}${MANPREFIX}/man1" + for m in ${MAN1}; do sed "s/VERSION/${VERSION}/g" < $$m > "${DESTDIR}${MANPREFIX}/man1/$$m"; done + +uninstall: + # removing executable files. + for f in ${BIN}; do rm -f "${DESTDIR}${PREFIX}/bin/$$f"; done + # removing doc files. + rm -f "${DESTDIR}${DOCPREFIX}/README" + # removing manual pages. + for m in ${MAN1}; do rm -f "${DESTDIR}${MANPREFIX}/man1/$$m"; done + -rmdir "${DESTDIR}${DOCPREFIX}" + +.PHONY: all clean dist install uninstall diff --git a/mut/tabbed/README b/mut/tabbed/README new file mode 100644 index 0000000..4ed6bbe --- /dev/null +++ b/mut/tabbed/README @@ -0,0 +1,22 @@ +tabbed - generic tabbed interface +================================= +tabbed is a simple tabbed X window container. + +Requirements +------------ +In order to build tabbed you need the Xlib header files. + +Installation +------------ +Edit config.mk to match your local setup (tabbed is installed into +the /usr/local namespace by default). + +Afterwards enter the following command to build and install tabbed +(if necessary as root): + + make clean install + +Running tabbed +-------------- +See the man page for details. + diff --git a/mut/tabbed/TODO b/mut/tabbed/TODO new file mode 100644 index 0000000..8e1986d --- /dev/null +++ b/mut/tabbed/TODO @@ -0,0 +1,4 @@ +# TODO +* add some way to detach windows +* add some way to attach windows + diff --git a/mut/tabbed/arg.h b/mut/tabbed/arg.h new file mode 100644 index 0000000..ba3fb3f --- /dev/null +++ b/mut/tabbed/arg.h @@ -0,0 +1,48 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/mut/tabbed/config.def.h b/mut/tabbed/config.def.h new file mode 100644 index 0000000..cb20e73 --- /dev/null +++ b/mut/tabbed/config.def.h @@ -0,0 +1,67 @@ +/* See LICENSE file for copyright and license details. */ + +/* appearance */ +static const char font[] = "monospace:size=9"; +static const char* normbgcolor = "#222222"; +static const char* normfgcolor = "#cccccc"; +static const char* selbgcolor = "#555555"; +static const char* selfgcolor = "#ffffff"; +static const char* urgbgcolor = "#111111"; +static const char* urgfgcolor = "#cc0000"; +static const char before[] = "<"; +static const char after[] = ">"; +static const char titletrim[] = "..."; +static const int tabwidth = 200; +static const Bool foreground = True; +static Bool urgentswitch = False; + +/* + * Where to place a new tab when it is opened. When npisrelative is True, + * then the current position is changed + newposition. If npisrelative + * is False, then newposition is an absolute position. + */ +static int newposition = 0; +static Bool npisrelative = False; + +#define SETPROP(p) { \ + .v = (char *[]){ "/bin/sh", "-c", \ + "prop=\"`xwininfo -children -id $1 | grep '^ 0x' |" \ + "sed -e's@^ *\\(0x[0-9a-f]*\\) \"\\([^\"]*\\)\".*@\\1 \\2@' |" \ + "xargs -0 printf %b | dmenu -l 10 -w $1`\" &&" \ + "xprop -id $1 -f $0 8s -set $0 \"$prop\"", \ + p, winid, NULL \ + } \ +} + +#define MODKEY ControlMask +static const Key keys[] = { + /* modifier key function argument */ + { MODKEY|ShiftMask, XK_Return, focusonce, { 0 } }, + { MODKEY|ShiftMask, XK_Return, spawn, { 0 } }, + + { MODKEY|ShiftMask, XK_l, rotate, { .i = +1 } }, + { MODKEY|ShiftMask, XK_h, rotate, { .i = -1 } }, + { MODKEY|ShiftMask, XK_j, movetab, { .i = -1 } }, + { MODKEY|ShiftMask, XK_k, movetab, { .i = +1 } }, + { MODKEY, XK_Tab, rotate, { .i = 0 } }, + + { MODKEY, XK_grave, spawn, SETPROP("_TABBED_SELECT_TAB") }, + { MODKEY, XK_1, move, { .i = 0 } }, + { MODKEY, XK_2, move, { .i = 1 } }, + { MODKEY, XK_3, move, { .i = 2 } }, + { MODKEY, XK_4, move, { .i = 3 } }, + { MODKEY, XK_5, move, { .i = 4 } }, + { MODKEY, XK_6, move, { .i = 5 } }, + { MODKEY, XK_7, move, { .i = 6 } }, + { MODKEY, XK_8, move, { .i = 7 } }, + { MODKEY, XK_9, move, { .i = 8 } }, + { MODKEY, XK_0, move, { .i = 9 } }, + + { MODKEY, XK_q, killclient, { 0 } }, + + /* TODO: Does this even work? */ + /* { MODKEY, XK_u, focusurgent, { 0 } }, */ + /* { MODKEY|ShiftMask, XK_u, toggle, { .v = (void*) &urgentswitch } }, */ + + { 0, XK_F11, fullscreen, { 0 } }, +}; diff --git a/mut/tabbed/tabbed.1 b/mut/tabbed/tabbed.1 new file mode 100644 index 0000000..07bdbd7 --- /dev/null +++ b/mut/tabbed/tabbed.1 @@ -0,0 +1,171 @@ +.TH TABBED 1 tabbed\-VERSION +.SH NAME +tabbed \- generic tabbed interface +.SH SYNOPSIS +.B tabbed +.RB [ \-c ] +.RB [ \-d ] +.RB [ \-k ] +.RB [ \-s ] +.RB [ \-v ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-p +.RB [ s {+/-} ] \fIpos\fR ] +.RB [ \-o +.IR normbgcol ] +.RB [ \-O +.IR normfgcol ] +.RB [ \-t +.IR selbgcol ] +.RB [ \-T +.IR selfgcol ] +.RB [ \-u +.IR urgbgcol ] +.RB [ \-U +.IR urgfgcol ] +.RB [ \-r +.IR narg ] +.RI [ "command ..." ] +.SH DESCRIPTION +.B tabbed +is a simple tabbed container for applications which support XEmbed. Tabbed +will then run the provided command with the xid of tabbed as appended +argument. (See EXAMPLES.) The automatic spawning of the command can be +disabled by providing the -s parameter. If no command is provided +tabbed will just print its xid and run no command. +.SH OPTIONS +.TP +.B \-c +close tabbed when the last tab is closed. Mutually exclusive with -f. +.TP +.B \-d +detaches tabbed from the terminal and prints its XID to stdout. +.TP +.B \-f +fill up tabbed again by spawning the provided command, when the last tab is +closed. Mutually exclusive with -c. +.TP +.BI \-g " geometry" +defines the X11 geometry string, which will fixate the height and width of +tabbed. +The syntax is +.RI [=][ width {xX} height ][{+-} xoffset {+-} yoffset ]. +See +.BR XParseGeometry (3) +for further details. +.TP +.B \-k +close foreground tabbed client (instead of tabbed and all clients) when +WM_DELETE_WINDOW is sent. +.TP +.BI \-n " name" +will set the WM_CLASS attribute to +.I name. +.TP +.BR \-p " [" s {+-}] \fIpos\fR +will set the absolute or relative position of where to start a new tab. When +.I pos +is is given without 's' in front it is an absolute position. Then negative +numbers will be the position from the last tab, where -1 is the last tab. +If 's' is given, then +.I pos +is a relative position to the current selected tab. If this reaches the limits +of the tabs; those limits then apply. +.TP +.BI \-r " narg" +will replace the +.I narg +th argument in +.I command +with the window id, rather than appending it to the end. +.TP +.B \-s +will disable automatic spawning of the command. +.TP +.BI \-o " normbgcol" +defines the normal background color. +.RI # RGB , +.RI # RRGGBB , +and X color names are supported. +.TP +.BI \-O " normfgcol" +defines the normal foreground color. +.TP +.BI \-t " selbgcol" +defines the selected background color. +.TP +.BI \-T " selfgbcol" +defines the selected foreground color. +.TP +.BI \-u " urgbgcol" +defines the urgent background color. +.TP +.BI \-U " urgfgbcol" +defines the urgent foreground color. +.TP +.B \-v +prints version information to stderr, then exits. +.SH USAGE +.TP +.B Ctrl\-Shift\-Return +open new tab +.TP +.B Ctrl\-Shift\-h +previous tab +.TP +.B Ctrl\-Shift\-l +next tab +.TP +.B Ctrl\-Shift\-j +move selected tab one to the left +.TP +.B Ctrl\-Shift\-k +move selected tab one to the right +.TP +.B Ctrl\-Shift\-u +toggle autofocus of urgent tabs +.TP +.B Ctrl\-Tab +toggle between the selected and last selected tab +.TP +.B Ctrl\-` +open dmenu to either create a new tab appending the entered string or select +an already existing tab. +.TP +.B Ctrl\-q +close tab +.TP +.B Ctrl\-u +focus next urgent tab +.TP +.B Ctrl\-[0..9] +jumps to nth tab +.TP +.B F11 +Toggle fullscreen mode. +.SH EXAMPLES +$ tabbed surf -e +.TP +$ tabbed urxvt -embed +.TP +$ tabbed xterm -into +.TP +$ $(tabbed -d >/tmp/tabbed.xid); urxvt -embed $(</tmp/tabbed.xid); +.TP +$ tabbed -r 2 st -w '' -e tmux +.SH CUSTOMIZATION +.B tabbed +can be customized by creating a custom config.h and (re)compiling the source +code. This keeps it fast, secure and simple. +.SH AUTHORS +See the LICENSE file for the authors. +.SH LICENSE +See the LICENSE file for the terms of redistribution. +.SH SEE ALSO +.BR st (1), +.BR xembed (1) +.SH BUGS +Please report them. diff --git a/mut/tabbed/tabbed.c b/mut/tabbed/tabbed.c new file mode 100644 index 0000000..e5664aa --- /dev/null +++ b/mut/tabbed/tabbed.c @@ -0,0 +1,1382 @@ +/* + * See LICENSE file for copyright and license details. + */ + +#include <sys/wait.h> +#include <locale.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <X11/Xatom.h> +#include <X11/keysym.h> +#include <X11/Xlib.h> +#include <X11/Xproto.h> +#include <X11/Xutil.h> +#include <X11/XKBlib.h> +#include <X11/Xft/Xft.h> + +#include "arg.h" + +/* XEMBED messages */ +#define XEMBED_EMBEDDED_NOTIFY 0 +#define XEMBED_WINDOW_ACTIVATE 1 +#define XEMBED_WINDOW_DEACTIVATE 2 +#define XEMBED_REQUEST_FOCUS 3 +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 +#define XEMBED_FOCUS_NEXT 6 +#define XEMBED_FOCUS_PREV 7 +/* 8-9 were used for XEMBED_GRAB_KEY/XEMBED_UNGRAB_KEY */ +#define XEMBED_MODALITY_ON 10 +#define XEMBED_MODALITY_OFF 11 +#define XEMBED_REGISTER_ACCELERATOR 12 +#define XEMBED_UNREGISTER_ACCELERATOR 13 +#define XEMBED_ACTIVATE_ACCELERATOR 14 + +/* Details for XEMBED_FOCUS_IN: */ +#define XEMBED_FOCUS_CURRENT 0 +#define XEMBED_FOCUS_FIRST 1 +#define XEMBED_FOCUS_LAST 2 + +/* Macros */ +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define LENGTH(x) (sizeof((x)) / sizeof(*(x))) +#define CLEANMASK(mask) (mask & ~(numlockmask | LockMask)) +#define TEXTW(x) (textnw(x, strlen(x)) + dc.font.height) + +enum { ColFG, ColBG, ColLast }; /* color */ +enum { WMProtocols, WMDelete, WMName, WMState, WMFullscreen, + XEmbed, WMSelectTab, WMLast }; /* default atoms */ + +typedef union { + int i; + const void *v; +} Arg; + +typedef struct { + unsigned int mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Key; + +typedef struct { + int x, y, w, h; + XftColor norm[ColLast]; + XftColor sel[ColLast]; + XftColor urg[ColLast]; + Drawable drawable; + GC gc; + struct { + int ascent; + int descent; + int height; + XftFont *xfont; + } font; +} DC; /* draw context */ + +typedef struct { + char name[256]; + Window win; + int tabx; + Bool urgent; + Bool closed; +} Client; + +/* function declarations */ +static void buttonpress(const XEvent *e); +static void cleanup(void); +static void clientmessage(const XEvent *e); +static void configurenotify(const XEvent *e); +static void configurerequest(const XEvent *e); +static void createnotify(const XEvent *e); +static void destroynotify(const XEvent *e); +static void die(const char *errstr, ...); +static void drawbar(void); +static void drawtext(const char *text, XftColor col[ColLast]); +static void *ecalloc(size_t n, size_t size); +static void *erealloc(void *o, size_t size); +static void expose(const XEvent *e); +static void focus(int c); +static void focusin(const XEvent *e); +static void focusonce(const Arg *arg); +static void focusurgent(const Arg *arg); +static void fullscreen(const Arg *arg); +static char *getatom(int a); +static int getclient(Window w); +static XftColor getcolor(const char *colstr); +static int getfirsttab(void); +static Bool gettextprop(Window w, Atom atom, char *text, unsigned int size); +static void initfont(const char *fontstr); +static Bool isprotodel(int c); +static void keypress(const XEvent *e); +static void killclient(const Arg *arg); +static void manage(Window win); +static void maprequest(const XEvent *e); +static void move(const Arg *arg); +static void movetab(const Arg *arg); +static void propertynotify(const XEvent *e); +static void resize(int c, int w, int h); +static void rotate(const Arg *arg); +static void run(void); +static void sendxembed(int c, long msg, long detail, long d1, long d2); +static void setcmd(int argc, char *argv[], int); +static void setup(void); +static void spawn(const Arg *arg); +static int textnw(const char *text, unsigned int len); +static void toggle(const Arg *arg); +static void unmanage(int c); +static void unmapnotify(const XEvent *e); +static void updatenumlockmask(void); +static void updatetitle(int c); +static int xerror(Display *dpy, XErrorEvent *ee); +static void xsettitle(Window w, const char *str); + +/* variables */ +static int screen; +static void (*handler[LASTEvent]) (const XEvent *) = { + [ButtonPress] = buttonpress, + [ClientMessage] = clientmessage, + [ConfigureNotify] = configurenotify, + [ConfigureRequest] = configurerequest, + [CreateNotify] = createnotify, + [UnmapNotify] = unmapnotify, + [DestroyNotify] = destroynotify, + [Expose] = expose, + [FocusIn] = focusin, + [KeyPress] = keypress, + [MapRequest] = maprequest, + [PropertyNotify] = propertynotify, +}; +static int bh, obh, wx, wy, ww, wh; +static unsigned int numlockmask; +static Bool running = True, nextfocus, doinitspawn = True, + fillagain = False, closelastclient = False, + killclientsfirst = False; +static Display *dpy; +static DC dc; +static Atom wmatom[WMLast]; +static Window root, win; +static Client **clients; +static int nclients, sel = -1, lastsel = -1; +static int (*xerrorxlib)(Display *, XErrorEvent *); +static int cmd_append_pos; +static char winid[64]; +static char **cmd; +static char *wmname = "tabbed"; +static const char *geometry; + +char *argv0; + +/* configuration, allows nested code to access above variables */ +#include "config.h" + +void +buttonpress(const XEvent *e) +{ + const XButtonPressedEvent *ev = &e->xbutton; + int i, fc; + Arg arg; + + if (ev->y < 0 || ev->y > bh) + return; + + if (((fc = getfirsttab()) > 0 && ev->x < TEXTW(before)) || ev->x < 0) + return; + + for (i = fc; i < nclients; i++) { + if (clients[i]->tabx > ev->x) { + switch (ev->button) { + case Button1: + focus(i); + break; + case Button2: + focus(i); + killclient(NULL); + break; + case Button4: /* FALLTHROUGH */ + case Button5: + arg.i = ev->button == Button4 ? -1 : 1; + rotate(&arg); + break; + } + break; + } + } +} + +void +cleanup(void) +{ + int i; + + for (i = 0; i < nclients; i++) { + focus(i); + killclient(NULL); + XReparentWindow(dpy, clients[i]->win, root, 0, 0); + unmanage(i); + } + free(clients); + clients = NULL; + + XFreePixmap(dpy, dc.drawable); + XFreeGC(dpy, dc.gc); + XDestroyWindow(dpy, win); + XSync(dpy, False); + free(cmd); +} + +void +clientmessage(const XEvent *e) +{ + const XClientMessageEvent *ev = &e->xclient; + + if (ev->message_type == wmatom[WMProtocols] && + ev->data.l[0] == wmatom[WMDelete]) { + if (nclients > 1 && killclientsfirst) { + killclient(0); + return; + } + running = False; + } +} + +void +configurenotify(const XEvent *e) +{ + const XConfigureEvent *ev = &e->xconfigure; + + if (ev->window == win && (ev->width != ww || ev->height != wh)) { + ww = ev->width; + wh = ev->height; + XFreePixmap(dpy, dc.drawable); + dc.drawable = XCreatePixmap(dpy, root, ww, wh, + DefaultDepth(dpy, screen)); + + if (!obh && (wh <= bh)) { + obh = bh; + bh = 0; + } else if (!bh && (wh > obh)) { + bh = obh; + obh = 0; + } + + if (sel > -1) + resize(sel, ww, wh - bh); + XSync(dpy, False); + } +} + +void +configurerequest(const XEvent *e) +{ + const XConfigureRequestEvent *ev = &e->xconfigurerequest; + XWindowChanges wc; + int c; + + if ((c = getclient(ev->window)) > -1) { + wc.x = 0; + wc.y = bh; + wc.width = ww; + wc.height = wh - bh; + wc.border_width = 0; + wc.sibling = ev->above; + wc.stack_mode = ev->detail; + XConfigureWindow(dpy, clients[c]->win, ev->value_mask, &wc); + } +} + +void +createnotify(const XEvent *e) +{ + const XCreateWindowEvent *ev = &e->xcreatewindow; + + if (ev->window != win && getclient(ev->window) < 0) + manage(ev->window); +} + +void +destroynotify(const XEvent *e) +{ + const XDestroyWindowEvent *ev = &e->xdestroywindow; + int c; + + if ((c = getclient(ev->window)) > -1) + unmanage(c); +} + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(EXIT_FAILURE); +} + +void +drawbar(void) +{ + XftColor *col; + int c, cc, fc, width; + char *name = NULL; + + if (nclients == 0) { + dc.x = 0; + dc.w = ww; + XFetchName(dpy, win, &name); + drawtext(name ? name : "", dc.norm); + XCopyArea(dpy, dc.drawable, win, dc.gc, 0, 0, ww, bh, 0, 0); + XSync(dpy, False); + + return; + } + + width = ww; + cc = ww / tabwidth; + if (nclients > cc) + cc = (ww - TEXTW(before) - TEXTW(after)) / tabwidth; + + if ((fc = getfirsttab()) + cc < nclients) { + dc.w = TEXTW(after); + dc.x = width - dc.w; + drawtext(after, dc.sel); + width -= dc.w; + } + dc.x = 0; + + if (fc > 0) { + dc.w = TEXTW(before); + drawtext(before, dc.sel); + dc.x += dc.w; + width -= dc.w; + } + + cc = MIN(cc, nclients); + for (c = fc; c < fc + cc; c++) { + dc.w = width / cc; + if (c == sel) { + col = dc.sel; + dc.w += width % cc; + } else { + col = clients[c]->urgent ? dc.urg : dc.norm; + } + drawtext(clients[c]->name, col); + dc.x += dc.w; + clients[c]->tabx = dc.x; + } + XCopyArea(dpy, dc.drawable, win, dc.gc, 0, 0, ww, bh, 0, 0); + XSync(dpy, False); +} + +void +drawtext(const char *text, XftColor col[ColLast]) +{ + int i, j, x, y, h, len, olen; + char buf[256]; + XftDraw *d; + XRectangle r = { dc.x, dc.y, dc.w, dc.h }; + + XSetForeground(dpy, dc.gc, col[ColBG].pixel); + XFillRectangles(dpy, dc.drawable, dc.gc, &r, 1); + if (!text) + return; + + olen = strlen(text); + h = dc.font.ascent + dc.font.descent; + y = dc.y + (dc.h / 2) - (h / 2) + dc.font.ascent; + x = dc.x + (h / 2); + + /* shorten text if necessary */ + for (len = MIN(olen, sizeof(buf)); + len && textnw(text, len) > dc.w - h; len--); + + if (!len) + return; + + memcpy(buf, text, len); + if (len < olen) { + for (i = len, j = strlen(titletrim); j && i; + buf[--i] = titletrim[--j]) + ; + } + + d = XftDrawCreate(dpy, dc.drawable, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen)); + XftDrawStringUtf8(d, &col[ColFG], dc.font.xfont, x, y, (XftChar8 *) buf, len); + XftDrawDestroy(d); +} + +void * +ecalloc(size_t n, size_t size) +{ + void *p; + + if (!(p = calloc(n, size))) + die("%s: cannot calloc\n", argv0); + return p; +} + +void * +erealloc(void *o, size_t size) +{ + void *p; + + if (!(p = realloc(o, size))) + die("%s: cannot realloc\n", argv0); + return p; +} + +void +expose(const XEvent *e) +{ + const XExposeEvent *ev = &e->xexpose; + + if (ev->count == 0 && win == ev->window) + drawbar(); +} + +void +focus(int c) +{ + char buf[BUFSIZ] = "tabbed-"VERSION" ::"; + size_t i, n; + XWMHints* wmh; + + /* If c, sel and clients are -1, raise tabbed-win itself */ + if (nclients == 0) { + cmd[cmd_append_pos] = NULL; + for(i = 0, n = strlen(buf); cmd[i] && n < sizeof(buf); i++) + n += snprintf(&buf[n], sizeof(buf) - n, " %s", cmd[i]); + + xsettitle(win, buf); + XRaiseWindow(dpy, win); + + return; + } + + if (c < 0 || c >= nclients) + return; + + resize(c, ww, wh - bh); + XRaiseWindow(dpy, clients[c]->win); + XSetInputFocus(dpy, clients[c]->win, RevertToParent, CurrentTime); + sendxembed(c, XEMBED_FOCUS_IN, XEMBED_FOCUS_CURRENT, 0, 0); + sendxembed(c, XEMBED_WINDOW_ACTIVATE, 0, 0, 0); + xsettitle(win, clients[c]->name); + + if (sel != c) { + lastsel = sel; + sel = c; + } + + if (clients[c]->urgent && (wmh = XGetWMHints(dpy, clients[c]->win))) { + wmh->flags &= ~XUrgencyHint; + XSetWMHints(dpy, clients[c]->win, wmh); + clients[c]->urgent = False; + XFree(wmh); + } + + drawbar(); + XSync(dpy, False); +} + +void +focusin(const XEvent *e) +{ + const XFocusChangeEvent *ev = &e->xfocus; + int dummy; + Window focused; + + if (ev->mode != NotifyUngrab) { + XGetInputFocus(dpy, &focused, &dummy); + if (focused == win) + focus(sel); + } +} + +void +focusonce(const Arg *arg) +{ + nextfocus = True; +} + +void +focusurgent(const Arg *arg) +{ + int c; + + if (sel < 0) + return; + + for (c = (sel + 1) % nclients; c != sel; c = (c + 1) % nclients) { + if (clients[c]->urgent) { + focus(c); + return; + } + } +} + +void +fullscreen(const Arg *arg) +{ + XEvent e; + + e.type = ClientMessage; + e.xclient.window = win; + e.xclient.message_type = wmatom[WMState]; + e.xclient.format = 32; + e.xclient.data.l[0] = 2; + e.xclient.data.l[1] = wmatom[WMFullscreen]; + e.xclient.data.l[2] = 0; + XSendEvent(dpy, root, False, SubstructureNotifyMask, &e); +} + +char * +getatom(int a) +{ + static char buf[BUFSIZ]; + Atom adummy; + int idummy; + unsigned long ldummy; + unsigned char *p = NULL; + + XGetWindowProperty(dpy, win, wmatom[a], 0L, BUFSIZ, False, XA_STRING, + &adummy, &idummy, &ldummy, &ldummy, &p); + if (p) + strncpy(buf, (char *)p, LENGTH(buf)-1); + else + buf[0] = '\0'; + XFree(p); + + return buf; +} + +int +getclient(Window w) +{ + int i; + + for (i = 0; i < nclients; i++) { + if (clients[i]->win == w) + return i; + } + + return -1; +} + +XftColor +getcolor(const char *colstr) +{ + XftColor color; + + if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen), colstr, &color)) + die("%s: cannot allocate color '%s'\n", argv0, colstr); + + return color; +} + +int +getfirsttab(void) +{ + int cc, ret; + + if (sel < 0) + return 0; + + cc = ww / tabwidth; + if (nclients > cc) + cc = (ww - TEXTW(before) - TEXTW(after)) / tabwidth; + + ret = sel - cc / 2 + (cc + 1) % 2; + return ret < 0 ? 0 : + ret + cc > nclients ? MAX(0, nclients - cc) : + ret; +} + +Bool +gettextprop(Window w, Atom atom, char *text, unsigned int size) +{ + char **list = NULL; + int n; + XTextProperty name; + + if (!text || size == 0) + return False; + + text[0] = '\0'; + XGetTextProperty(dpy, w, &name, atom); + if (!name.nitems) + return False; + + if (name.encoding == XA_STRING) { + strncpy(text, (char *)name.value, size - 1); + } else if (XmbTextPropertyToTextList(dpy, &name, &list, &n) >= Success + && n > 0 && *list) { + strncpy(text, *list, size - 1); + XFreeStringList(list); + } + text[size - 1] = '\0'; + XFree(name.value); + + return True; +} + +void +initfont(const char *fontstr) +{ + if (!(dc.font.xfont = XftFontOpenName(dpy, screen, fontstr)) + && !(dc.font.xfont = XftFontOpenName(dpy, screen, "fixed"))) + die("error, cannot load font: '%s'\n", fontstr); + + dc.font.ascent = dc.font.xfont->ascent; + dc.font.descent = dc.font.xfont->descent; + dc.font.height = dc.font.ascent + dc.font.descent; +} + +Bool +isprotodel(int c) +{ + int i, n; + Atom *protocols; + Bool ret = False; + + if (XGetWMProtocols(dpy, clients[c]->win, &protocols, &n)) { + for (i = 0; !ret && i < n; i++) { + if (protocols[i] == wmatom[WMDelete]) + ret = True; + } + XFree(protocols); + } + + return ret; +} + +void +keypress(const XEvent *e) +{ + const XKeyEvent *ev = &e->xkey; + unsigned int i; + KeySym keysym; + + keysym = XkbKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0, 0); + for (i = 0; i < LENGTH(keys); i++) { + if (keysym == keys[i].keysym && + CLEANMASK(keys[i].mod) == CLEANMASK(ev->state) && + keys[i].func) + keys[i].func(&(keys[i].arg)); + } +} + +void +killclient(const Arg *arg) +{ + XEvent ev; + + if (sel < 0) + return; + + if (isprotodel(sel) && !clients[sel]->closed) { + ev.type = ClientMessage; + ev.xclient.window = clients[sel]->win; + ev.xclient.message_type = wmatom[WMProtocols]; + ev.xclient.format = 32; + ev.xclient.data.l[0] = wmatom[WMDelete]; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(dpy, clients[sel]->win, False, NoEventMask, &ev); + clients[sel]->closed = True; + } else { + XKillClient(dpy, clients[sel]->win); + } +} + +void +manage(Window w) +{ + updatenumlockmask(); + { + int i, j, nextpos; + unsigned int modifiers[] = { 0, LockMask, numlockmask, + numlockmask | LockMask }; + KeyCode code; + Client *c; + XEvent e; + + XWithdrawWindow(dpy, w, 0); + XReparentWindow(dpy, w, win, 0, bh); + XSelectInput(dpy, w, PropertyChangeMask | + StructureNotifyMask | EnterWindowMask); + XSync(dpy, False); + + for (i = 0; i < LENGTH(keys); i++) { + if ((code = XKeysymToKeycode(dpy, keys[i].keysym))) { + for (j = 0; j < LENGTH(modifiers); j++) { + XGrabKey(dpy, code, keys[i].mod | + modifiers[j], w, True, + GrabModeAsync, GrabModeAsync); + } + } + } + + c = ecalloc(1, sizeof *c); + c->win = w; + + nclients++; + clients = erealloc(clients, sizeof(Client *) * nclients); + + if(npisrelative) { + nextpos = sel + newposition; + } else { + if (newposition < 0) + nextpos = nclients - newposition; + else + nextpos = newposition; + } + if (nextpos >= nclients) + nextpos = nclients - 1; + if (nextpos < 0) + nextpos = 0; + + if (nclients > 1 && nextpos < nclients - 1) + memmove(&clients[nextpos + 1], &clients[nextpos], + sizeof(Client *) * (nclients - nextpos - 1)); + + clients[nextpos] = c; + updatetitle(nextpos); + + XLowerWindow(dpy, w); + XMapWindow(dpy, w); + + e.xclient.window = w; + e.xclient.type = ClientMessage; + e.xclient.message_type = wmatom[XEmbed]; + e.xclient.format = 32; + e.xclient.data.l[0] = CurrentTime; + e.xclient.data.l[1] = XEMBED_EMBEDDED_NOTIFY; + e.xclient.data.l[2] = 0; + e.xclient.data.l[3] = win; + e.xclient.data.l[4] = 0; + XSendEvent(dpy, root, False, NoEventMask, &e); + + XSync(dpy, False); + + /* Adjust sel before focus does set it to lastsel. */ + if (sel >= nextpos) + sel++; + focus(nextfocus ? nextpos : + sel < 0 ? 0 : + sel); + nextfocus = foreground; + } +} + +void +maprequest(const XEvent *e) +{ + const XMapRequestEvent *ev = &e->xmaprequest; + + if (getclient(ev->window) < 0) + manage(ev->window); +} + +void +move(const Arg *arg) +{ + if (arg->i >= 0 && arg->i < nclients) + focus(arg->i); +} + +void +movetab(const Arg *arg) +{ + int c; + Client *new; + + if (sel < 0) + return; + + c = (sel + arg->i) % nclients; + if (c < 0) + c += nclients; + + if (c == sel) + return; + + new = clients[sel]; + if (sel < c) + memmove(&clients[sel], &clients[sel+1], + sizeof(Client *) * (c - sel)); + else + memmove(&clients[c+1], &clients[c], + sizeof(Client *) * (sel - c)); + clients[c] = new; + sel = c; + + drawbar(); +} + +void +propertynotify(const XEvent *e) +{ + const XPropertyEvent *ev = &e->xproperty; + XWMHints *wmh; + int c; + char* selection = NULL; + Arg arg; + + if (ev->state == PropertyNewValue && ev->atom == wmatom[WMSelectTab]) { + selection = getatom(WMSelectTab); + if (!strncmp(selection, "0x", 2)) { + arg.i = getclient(strtoul(selection, NULL, 0)); + move(&arg); + } else { + cmd[cmd_append_pos] = selection; + arg.v = cmd; + spawn(&arg); + } + } else if (ev->state == PropertyNewValue && ev->atom == XA_WM_HINTS && + (c = getclient(ev->window)) > -1 && + (wmh = XGetWMHints(dpy, clients[c]->win))) { + if (wmh->flags & XUrgencyHint) { + XFree(wmh); + wmh = XGetWMHints(dpy, win); + if (c != sel) { + if (urgentswitch && wmh && + !(wmh->flags & XUrgencyHint)) { + /* only switch, if tabbed was focused + * since last urgency hint if WMHints + * could not be received, + * default to no switch */ + focus(c); + } else { + /* if no switch should be performed, + * mark tab as urgent */ + clients[c]->urgent = True; + drawbar(); + } + } + if (wmh && !(wmh->flags & XUrgencyHint)) { + /* update tabbed urgency hint + * if not set already */ + wmh->flags |= XUrgencyHint; + XSetWMHints(dpy, win, wmh); + } + } + XFree(wmh); + } else if (ev->state != PropertyDelete && ev->atom == XA_WM_NAME && + (c = getclient(ev->window)) > -1) { + updatetitle(c); + } +} + +void +resize(int c, int w, int h) +{ + XConfigureEvent ce; + XWindowChanges wc; + + ce.x = 0; + ce.y = wc.y = bh; + ce.width = wc.width = w; + ce.height = wc.height = h; + ce.type = ConfigureNotify; + ce.display = dpy; + ce.event = clients[c]->win; + ce.window = clients[c]->win; + ce.above = None; + ce.override_redirect = False; + ce.border_width = 0; + + XConfigureWindow(dpy, clients[c]->win, CWY | CWWidth | CWHeight, &wc); + XSendEvent(dpy, clients[c]->win, False, StructureNotifyMask, + (XEvent *)&ce); +} + +void +rotate(const Arg *arg) +{ + int nsel = -1; + + if (sel < 0) + return; + + if (arg->i == 0) { + if (lastsel > -1) + focus(lastsel); + } else if (sel > -1) { + /* Rotating in an arg->i step around the clients. */ + nsel = sel + arg->i; + while (nsel >= nclients) + nsel -= nclients; + while (nsel < 0) + nsel += nclients; + focus(nsel); + } +} + +void +run(void) +{ + XEvent ev; + + /* main event loop */ + XSync(dpy, False); + drawbar(); + if (doinitspawn == True) + spawn(NULL); + + while (running) { + XNextEvent(dpy, &ev); + if (handler[ev.type]) + (handler[ev.type])(&ev); /* call handler */ + } +} + +void +sendxembed(int c, long msg, long detail, long d1, long d2) +{ + XEvent e = { 0 }; + + e.xclient.window = clients[c]->win; + e.xclient.type = ClientMessage; + e.xclient.message_type = wmatom[XEmbed]; + e.xclient.format = 32; + e.xclient.data.l[0] = CurrentTime; + e.xclient.data.l[1] = msg; + e.xclient.data.l[2] = detail; + e.xclient.data.l[3] = d1; + e.xclient.data.l[4] = d2; + XSendEvent(dpy, clients[c]->win, False, NoEventMask, &e); +} + +void +setcmd(int argc, char *argv[], int replace) +{ + int i; + + cmd = ecalloc(argc + 3, sizeof(*cmd)); + if (argc == 0) + return; + for (i = 0; i < argc; i++) + cmd[i] = argv[i]; + cmd[replace > 0 ? replace : argc] = winid; + cmd_append_pos = argc + !replace; + cmd[cmd_append_pos] = cmd[cmd_append_pos + 1] = NULL; +} + +void +setup(void) +{ + int bitm, tx, ty, tw, th, dh, dw, isfixed; + XWMHints *wmh; + XClassHint class_hint; + XSizeHints *size_hint; + struct sigaction sa; + + /* do not transform children into zombies when they terminate */ + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_NOCLDSTOP | SA_NOCLDWAIT | SA_RESTART; + sa.sa_handler = SIG_IGN; + sigaction(SIGCHLD, &sa, NULL); + + /* clean up any zombies that might have been inherited */ + while (waitpid(-1, NULL, WNOHANG) > 0); + + /* init screen */ + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + initfont(font); + bh = dc.h = dc.font.height + 2; + + /* init atoms */ + wmatom[WMDelete] = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + wmatom[WMFullscreen] = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", + False); + wmatom[WMName] = XInternAtom(dpy, "_NET_WM_NAME", False); + wmatom[WMProtocols] = XInternAtom(dpy, "WM_PROTOCOLS", False); + wmatom[WMSelectTab] = XInternAtom(dpy, "_TABBED_SELECT_TAB", False); + wmatom[WMState] = XInternAtom(dpy, "_NET_WM_STATE", False); + wmatom[XEmbed] = XInternAtom(dpy, "_XEMBED", False); + + /* init appearance */ + wx = 0; + wy = 0; + ww = 800; + wh = 600; + isfixed = 0; + + if (geometry) { + tx = ty = tw = th = 0; + bitm = XParseGeometry(geometry, &tx, &ty, (unsigned *)&tw, + (unsigned *)&th); + if (bitm & XValue) + wx = tx; + if (bitm & YValue) + wy = ty; + if (bitm & WidthValue) + ww = tw; + if (bitm & HeightValue) + wh = th; + if (bitm & XNegative && wx == 0) + wx = -1; + if (bitm & YNegative && wy == 0) + wy = -1; + if (bitm & (HeightValue | WidthValue)) + isfixed = 1; + + dw = DisplayWidth(dpy, screen); + dh = DisplayHeight(dpy, screen); + if (wx < 0) + wx = dw + wx - ww - 1; + if (wy < 0) + wy = dh + wy - wh - 1; + } + + dc.norm[ColBG] = getcolor(normbgcolor); + dc.norm[ColFG] = getcolor(normfgcolor); + dc.sel[ColBG] = getcolor(selbgcolor); + dc.sel[ColFG] = getcolor(selfgcolor); + dc.urg[ColBG] = getcolor(urgbgcolor); + dc.urg[ColFG] = getcolor(urgfgcolor); + dc.drawable = XCreatePixmap(dpy, root, ww, wh, + DefaultDepth(dpy, screen)); + dc.gc = XCreateGC(dpy, root, 0, 0); + + win = XCreateSimpleWindow(dpy, root, wx, wy, ww, wh, 0, + dc.norm[ColFG].pixel, dc.norm[ColBG].pixel); + XMapRaised(dpy, win); + XSelectInput(dpy, win, SubstructureNotifyMask | FocusChangeMask | + ButtonPressMask | ExposureMask | KeyPressMask | + PropertyChangeMask | StructureNotifyMask | + SubstructureRedirectMask); + xerrorxlib = XSetErrorHandler(xerror); + + class_hint.res_name = wmname; + class_hint.res_class = "tabbed"; + XSetClassHint(dpy, win, &class_hint); + + size_hint = XAllocSizeHints(); + if (!isfixed) { + size_hint->flags = PSize | PMinSize; + size_hint->height = wh; + size_hint->width = ww; + size_hint->min_height = bh + 1; + } else { + size_hint->flags = PMaxSize | PMinSize; + size_hint->min_width = size_hint->max_width = ww; + size_hint->min_height = size_hint->max_height = wh; + } + wmh = XAllocWMHints(); + XSetWMProperties(dpy, win, NULL, NULL, NULL, 0, size_hint, wmh, NULL); + XFree(size_hint); + XFree(wmh); + + XSetWMProtocols(dpy, win, &wmatom[WMDelete], 1); + + snprintf(winid, sizeof(winid), "%lu", win); + setenv("XEMBED", winid, 1); + + nextfocus = foreground; + focus(-1); +} + +void +spawn(const Arg *arg) +{ + struct sigaction sa; + + if (fork() == 0) { + if(dpy) + close(ConnectionNumber(dpy)); + + setsid(); + + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sa.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa, NULL); + + if (arg && arg->v) { + execvp(((char **)arg->v)[0], (char **)arg->v); + fprintf(stderr, "%s: execvp %s", argv0, + ((char **)arg->v)[0]); + } else { + cmd[cmd_append_pos] = NULL; + execvp(cmd[0], cmd); + fprintf(stderr, "%s: execvp %s", argv0, cmd[0]); + } + perror(" failed"); + exit(0); + } +} + +int +textnw(const char *text, unsigned int len) +{ + XGlyphInfo ext; + XftTextExtentsUtf8(dpy, dc.font.xfont, (XftChar8 *) text, len, &ext); + return ext.xOff; +} + +void +toggle(const Arg *arg) +{ + *(Bool*) arg->v = !*(Bool*) arg->v; +} + +void +unmanage(int c) +{ + if (c < 0 || c >= nclients) { + drawbar(); + XSync(dpy, False); + return; + } + + if (!nclients) + return; + + if (c == 0) { + /* First client. */ + nclients--; + free(clients[0]); + memmove(&clients[0], &clients[1], sizeof(Client *) * nclients); + } else if (c == nclients - 1) { + /* Last client. */ + nclients--; + free(clients[c]); + clients = erealloc(clients, sizeof(Client *) * nclients); + } else { + /* Somewhere inbetween. */ + free(clients[c]); + memmove(&clients[c], &clients[c+1], + sizeof(Client *) * (nclients - (c + 1))); + nclients--; + } + + if (nclients <= 0) { + lastsel = sel = -1; + + if (closelastclient) + running = False; + else if (fillagain && running) + spawn(NULL); + } else { + if (lastsel >= nclients) + lastsel = nclients - 1; + else if (lastsel > c) + lastsel--; + + if (c == sel && lastsel >= 0) { + focus(lastsel); + } else { + if (sel > c) + sel--; + if (sel >= nclients) + sel = nclients - 1; + + focus(sel); + } + } + + drawbar(); + XSync(dpy, False); +} + +void +unmapnotify(const XEvent *e) +{ + const XUnmapEvent *ev = &e->xunmap; + int c; + + if ((c = getclient(ev->window)) > -1) + unmanage(c); +} + +void +updatenumlockmask(void) +{ + unsigned int i, j; + XModifierKeymap *modmap; + + numlockmask = 0; + modmap = XGetModifierMapping(dpy); + for (i = 0; i < 8; i++) { + for (j = 0; j < modmap->max_keypermod; j++) { + if (modmap->modifiermap[i * modmap->max_keypermod + j] + == XKeysymToKeycode(dpy, XK_Num_Lock)) + numlockmask = (1 << i); + } + } + XFreeModifiermap(modmap); +} + +void +updatetitle(int c) +{ + if (!gettextprop(clients[c]->win, wmatom[WMName], clients[c]->name, + sizeof(clients[c]->name))) + gettextprop(clients[c]->win, XA_WM_NAME, clients[c]->name, + sizeof(clients[c]->name)); + if (sel == c) + xsettitle(win, clients[c]->name); + drawbar(); +} + +/* There's no way to check accesses to destroyed windows, thus those cases are + * ignored (especially on UnmapNotify's). Other types of errors call Xlibs + * default error handler, which may call exit. */ +int +xerror(Display *dpy, XErrorEvent *ee) +{ + if (ee->error_code == BadWindow + || (ee->request_code == X_SetInputFocus && + ee->error_code == BadMatch) + || (ee->request_code == X_PolyText8 && + ee->error_code == BadDrawable) + || (ee->request_code == X_PolyFillRectangle && + ee->error_code == BadDrawable) + || (ee->request_code == X_PolySegment && + ee->error_code == BadDrawable) + || (ee->request_code == X_ConfigureWindow && + ee->error_code == BadMatch) + || (ee->request_code == X_GrabButton && + ee->error_code == BadAccess) + || (ee->request_code == X_GrabKey && + ee->error_code == BadAccess) + || (ee->request_code == X_CopyArea && + ee->error_code == BadDrawable)) + return 0; + + fprintf(stderr, "%s: fatal error: request code=%d, error code=%d\n", + argv0, ee->request_code, ee->error_code); + return xerrorxlib(dpy, ee); /* may call exit */ +} + +void +xsettitle(Window w, const char *str) +{ + XTextProperty xtp; + + if (XmbTextListToTextProperty(dpy, (char **)&str, 1, + XUTF8StringStyle, &xtp) == Success) { + XSetTextProperty(dpy, w, &xtp, wmatom[WMName]); + XSetTextProperty(dpy, w, &xtp, XA_WM_NAME); + XFree(xtp.value); + } +} + +void +usage(void) +{ + die("usage: %s [-dfksv] [-g geometry] [-n name] [-p [s+/-]pos]\n" + " [-r narg] [-o color] [-O color] [-t color] [-T color]\n" + " [-u color] [-U color] command...\n", argv0); +} + +int +main(int argc, char *argv[]) +{ + Bool detach = False; + int replace = 0; + char *pstr; + + ARGBEGIN { + case 'c': + closelastclient = True; + fillagain = False; + break; + case 'd': + detach = True; + break; + case 'f': + fillagain = True; + break; + case 'g': + geometry = EARGF(usage()); + break; + case 'k': + killclientsfirst = True; + break; + case 'n': + wmname = EARGF(usage()); + break; + case 'O': + normfgcolor = EARGF(usage()); + break; + case 'o': + normbgcolor = EARGF(usage()); + break; + case 'p': + pstr = EARGF(usage()); + if (pstr[0] == 's') { + npisrelative = True; + newposition = atoi(&pstr[1]); + } else { + newposition = atoi(pstr); + } + break; + case 'r': + replace = atoi(EARGF(usage())); + break; + case 's': + doinitspawn = False; + break; + case 'T': + selfgcolor = EARGF(usage()); + break; + case 't': + selbgcolor = EARGF(usage()); + break; + case 'U': + urgfgcolor = EARGF(usage()); + break; + case 'u': + urgbgcolor = EARGF(usage()); + break; + case 'v': + die("tabbed-"VERSION", © 2009-2016 tabbed engineers, " + "see LICENSE for details.\n"); + break; + default: + usage(); + break; + } ARGEND; + + if (argc < 1) { + doinitspawn = False; + fillagain = False; + } + + setcmd(argc, argv, replace); + + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fprintf(stderr, "%s: no locale support\n", argv0); + if (!(dpy = XOpenDisplay(NULL))) + die("%s: cannot open display\n", argv0); + + setup(); + printf("0x%lx\n", win); + fflush(NULL); + + if (detach) { + if (fork() == 0) { + fclose(stdout); + } else { + if (dpy) + close(ConnectionNumber(dpy)); + return EXIT_SUCCESS; + } + } + + run(); + cleanup(); + XCloseDisplay(dpy); + + return EXIT_SUCCESS; +} diff --git a/mut/tabbed/xembed.1 b/mut/tabbed/xembed.1 new file mode 100644 index 0000000..5b0c28c --- /dev/null +++ b/mut/tabbed/xembed.1 @@ -0,0 +1,35 @@ +.TH XEMBED 1 tabbed\-VERSION +.SH NAME +xembed \- XEmbed foreground process +.SH SYNOPSIS +.B xembed +.I flag command +.RI [ "argument ..." ] +.SH DESCRIPTION +If the environment variable XEMBED is set, and +.B xembed +is in the foreground of its controlling tty, it will execute +.IP +command flag $XEMBED [argument ...] +.LP +Otherwise it will execute +.IP +command [argument ...] +.LP +.SH EXAMPLE +In a terminal emulator within a +.B tabbed +session, the shell alias +.IP +$ alias surf='xembed -e surf' +.LP +will cause `surf' to open in a new tab, unless it is run in the background, +i.e. `surf &', in which case it will instead open in a new window. +.SH AUTHORS +See the LICENSE file for the authors. +.SH LICENSE +See the LICENSE file for the terms of redistribution. +.SH SEE ALSO +.BR tabbed (1) +.SH BUGS +Please report them. diff --git a/mut/tabbed/xembed.c b/mut/tabbed/xembed.c new file mode 100644 index 0000000..cbb0e97 --- /dev/null +++ b/mut/tabbed/xembed.c @@ -0,0 +1,45 @@ +/* + * See LICENSE file for copyright and license details. + */ + +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +int +main(int argc, char *argv[]) +{ + char *xembed; + int tty; + pid_t pgrp, tcpgrp; + + if (argc < 3) { + fprintf(stderr, "usage: %s flag cmd ...\n", argv[0]); + return 2; + } + + if (!(xembed = getenv("XEMBED"))) + goto noembed; + + if ((tty = open("/dev/tty", O_RDONLY)) < 0) + goto noembed; + + pgrp = getpgrp(); + tcpgrp = tcgetpgrp(tty); + + close(tty); + + if (pgrp == tcpgrp) { /* in foreground of tty */ + argv[0] = argv[2]; + argv[2] = xembed; + } else { +noembed: + argv += 2; + } + + execvp(argv[0], argv); + + perror(argv[0]); /* failed to execute */ + return 1; +} diff --git a/mut/vis/vis-editorconfig/.editorconfig b/mut/vis/vis-editorconfig/.editorconfig new file mode 100644 index 0000000..d60c879 --- /dev/null +++ b/mut/vis/vis-editorconfig/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.lua] +indent_style = space +indent_size = 2 diff --git a/mut/vis/vis-editorconfig/README.md b/mut/vis/vis-editorconfig/README.md new file mode 100644 index 0000000..9b9b72c --- /dev/null +++ b/mut/vis/vis-editorconfig/README.md @@ -0,0 +1,89 @@ +# vis-editorconfig + +A [vis][vis] plugin for [editorconfig][ec]. + +[vis]: https://github.com/martanne/vis +[ec]: http://editorconfig.org/ + +## Installation + +You'll need the Lua wrapper for editorconfig-core installed. This can +be done through luarocks: `luarocks install editorconfig-core` + +```shell +git clone https://github.com/seifferth/vis-editorconfig "$HOME/.config/vis/edconf" +``` + +Then add `require('edconf')` to your `visrc.lua`. + +You may use a different name for the local repository, if you like. +You could, for instance, use `$HOME/.config/vis/vis-editorconfig` and +`require('vis-editorconfig')`. Note, however, that the repository **must +not** be called `editorconfig`. Since the editorconfig-core lua library +is also called `editorconfig`, naming the repository `editorconfig` +will cause name conflicts that result in infinite recursion. + +## Functionality + +Not all editorconfig functionality is supported by vis and hence by this +plugin. At this moment, there is full support for the following settings: + +- indent_style +- indent_size +- tab_width +- insert_final_newline + +The following settings are implemented partially and / or support is +turned off by default: + +- spell_language: This is not yet part of the editorconfig specification + (cf. <https://github.com/editorconfig/editorconfig/issues/315>), but + it is implemented anyway. Since vis does not support spellchecking + natively, this plugin will only set `vis.win.file.spell_language` to + the specified value. It is then up to the spellchecking plugin to + respect that variable. +- trim_trailing_whitespace: Turned off by default, can be enabled + via `:set edconfhooks on`. +- end_of_line: Turned off by default, partial support can be enabled + via `:set edconfhooks on`. Only `crlf` and `lf` are supported, plain + `cr` is not. The implementation is also very basic. If end_of_line + is set to `crlf`, a return character will be inserted at the end of + each line that does not yet end with `crlf`. If end_of_line is set + to `lf`, return characters at the end of a line will be stripped. + While I would encourage every vis user to stick to `lf` terminated + files, this might be convenient if, for some reason, they do need + to compose `crlf` terminated files. +- max_line_length: Turned off by default, partial support can be + enabled via `:set edconfhooks on`. There is no straightforward way + to automatically wrap content that might be written in arbitrary + programming (or non-programming) languages. For that reason, + vis-editorconfig doesn't even try. If max_line_length is enabled, + vis-editorconfig issues a warning, however, indicating which lines + are longer than the specified length. Note that you will miss this + warning if you write your file with `:wq`, so if you care about it, + write it with `:w<RETURN>:q`. + +The reason those last three settings are optional and turned off by +default is their scalability. Each of those operations is implemented +as a pre-save-hook with a complexity of O(n), where n is the filesize. +Since vis is incredibly good at editing huge files efficiently, there +seems to be a very real danger that those hooks could cause the editor +to freeze just before a user's valuable changes are written to disk. + +You can turn support for those pre-save-hooks on or off at any time +by running + + :set edconfhooks on + +or + + :set edconfhooks off + +If `edconfhooks` are enabled, they will be executed as configured in +`.editorconfig`. If you want to take a less cautious approach and enable +these hooks by default, simply add an additional line below the module +import in `visrc.lua`: + + require('editorconfig/edconf') + vis:command('set edconfhooks on') -- supposing you did previously + -- require('vis') diff --git a/mut/vis/vis-editorconfig/edconf.lua b/mut/vis/vis-editorconfig/edconf.lua new file mode 100644 index 0000000..7c92f4d --- /dev/null +++ b/mut/vis/vis-editorconfig/edconf.lua @@ -0,0 +1,208 @@ +require "vis" +local ec = require "editorconfig" +local M = {} + +-- Simple wrapper +local function vis_set(option, value) + if type(value) == "boolean" then + if value then + value = "yes" + else + value = "no" + end + end + + vis:command("set " .. option .. " " .. value) +end + +local function set_pre_save(f, value) + if value == "true" then + vis.events.subscribe(vis.events.FILE_SAVE_PRE, f) + else + vis.events.unsubscribe(vis.events.FILE_SAVE_PRE, f) + end +end + +local function set_file_open(f, value) + if value == "true" then + vis.events.subscribe(vis.events.FILE_OPEN, f) + else + vis.events.unsubscribe(vis.events.FILE_OPEN, f) + end +end + +-- Custom functionality +M.hooks_enabled = false +vis:option_register("edconfhooks", "bool", function(value) + M.hooks_enabled = value +end, "Enable optional pre-save-hooks for certain editorconfig settings") + +local function insert_final_newline(file) + -- Technically speaking, this is a pre-save-hook as well and could + -- therefore respect edconf_hooks_enabled. Since this function runs + -- blazingly fast and scales with a complexity of O(1), however, + -- there is no need to disable it. + if file.size > 0 and file:content(file.size-1, 1) ~= '\n' then + file:insert(file.size, '\n') + end +end + +local function strip_final_newline(file) + -- In theory, this would have a complexity of O(n) as well and could + -- thus be made optional via edconf_hooks_enabled. On the other hand, + -- this is probably a very rare edge case, so stripping all trailing + -- newline characters is probably safe enough. + while file:content(file.size-1, 1) == '\n' do + file:delete(file.size-1, 1) + end +end + +local function trim_trailing_whitespace(file) + if not M.hooks_enabled then return end + for i=1, #file.lines do + if string.match(file.lines[i], '[ \t]$') then + file.lines[i] = string.gsub(file.lines[i], '[ \t]*$', '') + end + end +end + +local function enforce_crlf_eol(file) + if not M.hooks_enabled then return end + for i=1, #file.lines do + if not string.match(file.lines[i], '\r$') then + file.lines[i] = string.gsub(file.lines[i], '$', '\r') + end + end +end + +local function enforce_lf_eol(file) + if not M.hooks_enabled then return end + for i=1, #file.lines do + if string.match(file.lines[i], '\r$') then + file.lines[i] = string.gsub(file.lines[i], '\r$', '') + end + end +end + +M.max_line_length = 80 -- This is ugly, but we do want to use + -- single function that we can register + -- or unregister as needed +local function max_line_length(file) + if not M.hooks_enabled then return end + local overlong_lines = {} + for i=1, #file.lines do + if string.len(file.lines[i]) > M.max_line_length then + table.insert(overlong_lines, i) + end + end + if #overlong_lines > 0 then + local lines_are = (function(x) + if x>1 then return "lines are" else return "line is" end + end)(#overlong_lines) + vis:info(string.format( + "%d %s longer than %d characters: %s", + #overlong_lines, lines_are, M.max_line_length, + table.concat(overlong_lines, ",") + )) + end +end + +local OPTIONS = { + indent_style = function (value) + vis_set("expandtab", (value == "space")) + end, + + indent_size = function (value) + if value ~= "tab" then -- tab_width is a synonym anyway + vis_set("tabwidth", value) + end + end, + + tab_width = function (value) + vis_set("tabwidth", value) + end, + + spelling_language = function (value, file) + file.spelling_language = value + end, + + insert_final_newline = function (value) + -- According to the editorconfig specification, insert_final_newline + -- false is supposed to mean stripping the final newline, if present. + -- See https://editorconfig-specification.readthedocs.io/#supported-pairs + -- + -- Quote: insert_final_newline Set to true ensure file ends with a + -- newline when saving and false to ensure it doesn’t. + -- + set_pre_save(insert_final_newline, tostring(value == "true")) + set_pre_save(strip_final_newline, tostring(value == "false")) + end, + + trim_trailing_whitespace = function (value) + set_pre_save(trim_trailing_whitespace, value) + end, + + -- End of line is only partially implemented. While vis does not + -- support customized newlines, it does work well enough with crlf + -- newlines. Therefore, setting end_of_line=crlf will just ensure + -- that there is a cr at the end of each line. Setting end_of_line=lf + -- will strip any cr characters at the end of lines. This hopefully + -- eases the pain of working with crlf files a little. + end_of_line = function (value) + set_pre_save(enforce_crlf_eol, tostring(value == "crlf")) + set_pre_save(enforce_lf_eol, tostring(value == "lf")) + end, + + -- There is probably no straightforward way to enforce a maximum line + -- length across different programming languages. If a maximum line + -- length is set, we can at least issue a warning, however. + max_line_length = function(value) + if value ~= "off" then + M.max_line_length = tonumber(value) + end + set_pre_save(max_line_length, tostring(value ~= "off")) + end, + + -- Not supported by vis + -- charset + -- Partial support + -- end_of_line + -- max_line_length +} + +-- Compatible with editorconfig-core-lua v0.3.0 +local function ec_iter(p) + local i = 0 + local props, keys = ec.parse(p) + local n = #keys + return function () + i = i + 1 + if i <= n then + return keys[i], props[keys[i]] + end + end +end + +local function ec_set_values(win) + if not win or not win.file or not win.file.path then return end + for name, value in ec_iter(win.file.path) do + if OPTIONS[name] then + OPTIONS[name](value, win.file) + end + end +end + + +vis:command_register("econfig_parse", function() + ec_set_values(vis.win) +end, "(Re)parse an editorconfig file") + +vis.events.subscribe(vis.events.WIN_OPEN, function (win) + ec_set_values(win) +end) + +vis.events.subscribe(vis.events.FILE_SAVE_POST, function() + ec_set_values(vis.win) +end) + +return M diff --git a/mut/vis/vis-editorconfig/init.lua b/mut/vis/vis-editorconfig/init.lua new file mode 100644 index 0000000..39291d4 --- /dev/null +++ b/mut/vis/vis-editorconfig/init.lua @@ -0,0 +1,4 @@ +local source_str = debug.getinfo(1, 'S').source:sub(2) +local script_path = source_str:match('(.*/)') + +return dofile(script_path .. 'edconf.lua') diff --git a/mut/vis/vis-format/.editorconfig b/mut/vis/vis-format/.editorconfig new file mode 100644 index 0000000..7c67854 --- /dev/null +++ b/mut/vis/vis-format/.editorconfig @@ -0,0 +1,10 @@ +[*.lua] +indent_style = space +indent_size = 2 +max_line_length = 79 +quote_type = single + +[*.md] +max_line_length = 79 +indent_style = space +indent_size = 2 diff --git a/mut/vis/vis-format/LICENSE b/mut/vis/vis-format/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/mut/vis/vis-format/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/mut/vis/vis-format/README.md b/mut/vis/vis-format/README.md new file mode 100644 index 0000000..c310de6 --- /dev/null +++ b/mut/vis/vis-format/README.md @@ -0,0 +1,157 @@ +# vis-format - integrates vis with external formatters + +A plugin for [vis](https://github.com/martanne/vis) to integrate `prettier`, +`rustfmt` etc. + +### Installation + +Clone this repository to where you install your plugins. (If this is your first +plugin, running `git clone https://github.com/milhnl/vis-format` in +`~/.config/vis/` will probably work). + +Then, add something like the following to your `visrc`. + + local format = require('vis-format') + vis:map(vis.modes.NORMAL, '=', format.apply) + +### Usage + +Press `=` to format the whole file. + +Currently, the following formatters are supported out-of-the-box. + +- `bash`: [shfmt](https://github.com/mvdan/sh) +- `csharp`: [CSharpier](https://csharpier.com/). Although + [dotnet-format](https://github.com/dotnet/format) is the 'default' formatter + for dotnet, it does not support formatting stdin (and does not break lines). +- `git-commit`: Actually `diff` with an extra check, as that's how + `vis.ftdetect` ends up labeling it. Uses `fmt -w` and some glue code to + reformat the commit body, but not the summary and comments. +- `go`: `gofmt` +- `lua`: [StyLua](https://github.com/JohnnyMorganz/StyLua) and + [LuaFormatter](https://github.com/Koihik/LuaFormatter), depending on which + config file is in the working directory. +- `markdown`: `prettier` with `--prose-wrap` enabled if `colorcolumn` is set. +- `powershell`: + [PSScriptAnalyzer](https://learn.microsoft.com/en-gb/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules#installing-psscriptanalyzer) + via `powershell.exe` in the WSL if available or `pwsh`. +- `rust`: `rustfmt` +- `text`: `fmt` with width based on colorcolumn if set, otherwise joined + paragraphs. + +I'm working on some more heuristics for detecting which formatter to use for +languages without a 'blessed' formatter. In the meantime, this is how you add +the ones you want to use: + + format.formatters.html = format.stdio_formatter("prettier --parser html") + +### Advanced usage + +The following methods and tables are fields of the table that is returned by +`require('vis-format')` (e.g. `format`). You can use them to extend or +configure `vis-format`. + +#### `formatters` + +A table containing the configured formatters. There are some predefined (see +the list above under "Usage"), and you can add or override those by assigning +your formatter to a key corresponding to the vis `syntax` it is relevant for. +Each entry is of either of these forms: + + { + apply = func(win, range, pos) end, + options = { + ranged = nil, + check_same = nil, + } + } + +or + + { + pick = func(win, range, pos) end, + } + +The first form directly defines a formatter. An entry containing `pick` is +executed and its result is directly used as a formatter. A recommended way to +use `pick` is to return a different formatter from the `formatters` table. Note +that using the vis `syntax` as keys in this table is only necessary for +formatters that are run directly. This means that `lua` is a good key for a +`pick` entry, which chooses between the formatters found at `stylua` and +`luaformatter`. + +#### `stdio_formatter(command[, options])` + +The `stdio_formatter` function wraps the command to produce something like +this: + + { + apply = function(win, range, pos) end, + options = { ranged = false } + } + +The command given can also be a function, which is expected to return a string, +which is then used as a command. This allows you to use the given range, or +options from `win`. `ranged` is automatically set to true in this case. + +Apart from mapping `vis-format` in normal mode, you can also define an operator +(with `vis:operator_new`) to format ranges of code/text. This will require a +formatter that can work with ranges of text. Configuring that looks like this: + + -- Add a formatter that can use a range + format.formatters.lua = format.stdio_formatter(function(win, range, pos) + return 'stylua -s --range-start ' .. range.start .. ' --range-end ' + .. range.finish .. ' -' + end) + + -- Bind it to keys + vis:operator_new('=', format.apply) -- this'll handle ranges + vis:map(vis.modes.NORMAL, '=', function() -- this'll format whole files + format.apply(vis.win.file, nil, vis.win.selection.pos) + end) + +#### `with_filename` + +Most formatters take a path for where `stdin` would be. If the file has a path +`with_filename` concatenates the option and the shell-escaped path. Note that +it does not add any spaces to separate options/arguments. + + stdio_formatter(function(win) + return 'shfmt ' .. with_filename(win, '--filename ') .. ' -' + end, { ranged = false }) + +#### `options` + +The options listed here are also available in the options field of a formatter. + +- `options.check_same` (`boolean|number`) — After formatting, to avoid updating + the file, `vis-format` can compare the old and the new. If this is set to a + number, that's the maximum size of the file for which it is enabled. +- `options.on_save` (`boolean|function(win)`) — Enable a pre-save hook that + formats the file. If `on_save` is a function, it is run before saving to + determine whether formatting is required. + +### Bugs + +Ranged formatting is not enabled and will currently not work with `prettier`. +Prettier extends the range given on the command line to the beginning and end +of the statement containing it. This will not work with how `vis-format` +currently applies the output. I have some ideas on how to fix this, but wanted +to release what works first. + +#### Note on editor options and vis versions before 0.9 + +`vis` has an `options` table with editor settings like tab width etc. built-in +since 0.9. `vis-format` will not read this most of the time, because the +correct way to configure formatters is with their dedicated configuration +files, or `.editorconfig` (Which can be read in vis with +[vis-editorconfig](https://github.com/seifferth/vis-editorconfig) and +[vis-editorconfig-options](https://github.com/milhnl/vis-editorconfig-options) +). + +The included formatter integrations that _do_ use this information assume that +the `options` table in `vis` and `win` is present. If you use an older version, +`vis-format` will still work, but can't detect your editor settings. To fix +that, look at the +[vis-options-backport](https://github.com/milhnl/vis-options-backport) plugin. +This will 'polyfill' that for older versions. diff --git a/mut/vis/vis-format/init.lua b/mut/vis/vis-format/init.lua new file mode 100644 index 0000000..1843afd --- /dev/null +++ b/mut/vis/vis-format/init.lua @@ -0,0 +1,364 @@ +local format = { + options = { + check_same = true, + }, +} + +local with_filename = function(win, option) + if win.file.path then + return option .. "'" .. win.file.path:gsub("'", "\\'") .. "'" + else + return '' + end +end + +local heuristic_debug = '' +vis:command_register('format-debug', function() + vis:message(heuristic_debug) + return true +end) + +local new_pos_heuristic = function(win, new, pos) + local new_size = #new + do -- Try creating a pattern that'll match one position in the new content + local converters = { + function(fragment) + return fragment + :gsub('([(%:)%:.%:%%:+%:-%:*%:?%:[%:^%:$])', '%%%1') -- all rgx chars + :gsub('(%S)%s+', '%1%%s*') -- only leading space literal, rest flex + end, + function(fragment) + return fragment + :gsub('^%s+', '') -- ignore leading space + :gsub('([(%:)%:.%:%%:+%:-%:*%:?%:[%:^%:$])', '%%%1') -- all rgx chars + :gsub('%s+', '%%s*') -- flexibly match all space + end, + function(fragment) + return fragment + :gsub('%W+', '%%W+') -- only match on alphanumerics + :gsub('^%%W%+', '') -- ignore non-alphanumerics at start + end, + } + local converter_index = 1 + local fragment_size = 4 + heuristic_debug = heuristic_debug .. '\n-----------------------------\n' + while + fragment_size <= 1024 + and fragment_size <= (win.file.size - pos) * 2 + and converter_index <= #converters + do + local pattern = + converters[converter_index](win.file:content(pos, fragment_size)) + local new_pos = new:find(pattern) + heuristic_debug = heuristic_debug + .. ('pattern: ' .. pattern .. '\n') + .. ('pos: ' .. pos .. '\n') + .. ('new_pos: ' .. (new_pos or 'nil') .. '\n') + .. ('posdiff: ' .. math.abs((new_pos or 0) - pos) .. '\n') + .. ('sizediff: ' .. math.abs(new_size - win.file.size) .. '\n') + .. '\n' + if new_pos == nil then + converter_index = converter_index + 1 + elseif -- pattern has 1 match, and it isn't too far away (false positive) + math.abs(new_pos - pos) + < (math.abs(new_size - win.file.size) * 10 + 30) + and new:find(pattern, new_pos + 1) == nil + then + heuristic_debug = heuristic_debug .. '\nsuccess: ' .. new_pos .. '\n' + return new_pos - 1 + else + fragment_size = fragment_size * 2 + end + end + end + + do -- Try same offset of right side of the same line if # of lines matches + local new_pos, new_lines, new_line_start = nil, 1, nil + for i = 1, new_size do + if new:sub(i, i) == '\n' then + if new_lines == win.selection.line and new_line_start ~= nil then + local line_length = #win.file.lines[win.selection.line] + new_pos = i - line_length + win.selection.col - 2 + new_pos = new_line_start < new_pos and new_pos or new_line_start + end + new_lines = new_lines + 1 + if new_lines == win.selection.line then + new_line_start = i + end + end + end + if (new_lines - 1) == #win.file.lines then + return new_pos + end + end + + return nil +end + +local win_formatter = function(func, options) + return { + apply = function(win, range, pos) + if + range ~= nil + and not options.ranged + and range.start ~= 0 + and range.finish ~= win.file.size + then + return nil, + 'Formatter for ' .. win.syntax .. ' does not support ranges', + pos + end + local _, err, new_pos = func(win, range, pos) + vis:insert('') -- redraw and friends don't work + return nil, err, new_pos or pos + end, + options = options, + } +end + +local func_formatter = function(func, options) + return win_formatter(function(win, range, pos) + local size = win.file.size + local all = { start = 0, finish = size } + if range == nil then + range = all + end + local check_same = (options and options.check_same ~= nil) + and options.check_same + or format.options.check_same + local check = check_same == true + or (type(check_same) == 'number' and check_same >= size) + + local out, err, new_pos = func(win, range, pos) + if err ~= nil then + return nil, err, pos + elseif out == nil or out == '' then + return nil, 'No output from formatter', pos + elseif not check or win.file:content(all) ~= out then + new_pos = new_pos or new_pos_heuristic(win, out, pos) or pos + local start, finish = range.start, range.finish + win.file:delete(range) + win.file:insert(start, out:sub(start + 1, finish + (out:len() - size))) + end + return nil, nil, new_pos + end, options) +end + +local stdio_formatter = function(cmd, options) + return func_formatter(function(win, range, pos) + local command = type(cmd) == 'function' and cmd(win, range, pos) or cmd + local status, out, err = vis:pipe(win.file, range, command) + if status ~= 0 then + return nil, err, nil + end + return out, nil, nil + end, options or { ranged = type(cmd) == 'function' }) +end + +local prettier_formatter = function(cmd, options) + return func_formatter(function(win, range, pos) + local command = type(cmd) == 'function' and cmd(win, range, pos) or cmd + command = command + .. with_filename(win, ' --stdin-filepath ') + .. (' --cursor-offset ' .. pos) + local status, out, err = vis:pipe(win.file, range, command) + if status ~= 0 then + return nil, err, nil + end + local new_pos = tonumber(err) + return out, nil, new_pos >= 0 and new_pos or nil + end, options or { ranged = type(cmd) == 'function' }) +end + +local formatters = {} +formatters = { + bash = stdio_formatter(function(win) + return 'shfmt ' .. with_filename(win, '--filename ') .. ' -' + end), + csharp = stdio_formatter('dotnet csharpier'), + diff = { + pick = function(win) + for _, pattern in ipairs(vis.ftdetect.filetypes['git-commit'].ext) do + if ((win.file.name or ''):match('[^/]+$') or ''):match(pattern) then + return formatters['git-commit'] + end + end + end, + }, + ['git-commit'] = func_formatter(function(win, range, pos) + local width = (win.options and win.options.colorcolumn ~= 0) + and (win.options.colorcolumn - 1) + or 72 + local parts = {} + local fmt = nil + local summary = true + for line in win.file:lines_iterator() do + local txt = not line:match('^#') + if fmt == nil or fmt ~= txt then + fmt = txt and not summary + local prev = parts[#parts] and parts[#parts].finish or 0 + parts[#parts + 1] = { + fmt = fmt, + start = prev, + finish = prev + #line + 1, + } + summary = summary and not txt + else + parts[#parts].finish = parts[#parts].finish + #line + 1 + end + end + local out = '' + for _, part in ipairs(parts) do + if part.fmt then + local status, partout, err = + vis:pipe(win.file, part, 'fmt -w ' .. width) + if status ~= 0 then + return nil, err + end + out = out .. (partout or '') + else + out = out .. win.file:content(part) + end + end + return out + end, { ranged = false }), + go = stdio_formatter('gofmt'), + lua = { + pick = function(win) + local fz = io.popen([[ + test -e .lua-format && echo luaformatter || echo stylua + ]]) + if fz then + local out = fz:read('*a') + local _, _, status = fz:close() + if status == 0 then + return formatters[out:gsub('\n$', '')] + end + end + end, + }, + luaformatter = stdio_formatter('lua-format'), + markdown = prettier_formatter(function(win) + if win.options and win.options.colorcolumn ~= 0 then + return 'prettier --parser markdown --prose-wrap always ' + .. ('--print-width ' .. (win.options.colorcolumn - 1)) + else + return 'prettier --parser markdown' + end + end, { ranged = false }), + powershell = stdio_formatter([[ + "$( (command -v powershell.exe || command -v pwsh) 2>/dev/null )" -c ' + Invoke-Formatter -ScriptDefinition ` + ([IO.StreamReader]::new([Console]::OpenStandardInput()).ReadToEnd()) + ' | sed -e :a -e '/^[\r\n]*$/{$d;N;};/\n$/ba' + ]]), + rust = stdio_formatter('rustfmt'), + stylua = stdio_formatter(function(win, range) + if range and (range.start ~= 0 or range.finish ~= win.file.size) then + return 'stylua -s --range-start ' + .. range.start + .. ' --range-end ' + .. range.finish + .. with_filename(win, ' --stdin-filepath ') + .. ' -' + else + return 'stylua -s ' .. with_filename(win, '--stdin-filepath ') .. ' -' + end + end), + text = stdio_formatter(function(win) + if win.options and win.options.colorcolumn ~= 0 then + return 'fmt -w ' .. (win.options.colorcolumn - 1) + else + return "fmt | awk -v n=-1 '" + .. ' {' + .. ' if ($0 == "") {' + .. ' n = n <= 0 ? 2 : 1' + .. ' } else {' + .. ' if (n == 0) sub(/^ */, "");' + .. ' n = 0;' + .. ' }' + .. ' printf("%s", $0 (n == 0 ? " " : ""));' + .. ' for(i = 0; i < n; i++)' + .. ' printf("\\n");' + .. ' }' + .. "'" + end + end, { ranged = false }), +} + +local getwinforfile = function(file) + for win in vis:windows() do + if win and win.file and win.file.path == file.path then + return win + end + end +end + +local pick = function(win) + local formatter = formatters[win.syntax] + if formatter and formatter.pick then + formatter = formatter.pick(win) + end + return formatter +end + +local keyhandler = function(file_or_keys, range, pos) + local _, err + local win = type(file_or_keys) ~= 'string' and getwinforfile(file_or_keys) + or vis.win + local ret = type(file_or_keys) ~= 'string' + and function() + return pos + end + or function() + win.selection.pos = pos + return 0 + end + pos = pos ~= nil and pos or win.selection.pos + local formatter = format.pick(win) + if formatter == nil then + vis:info('No formatter for ' .. win.syntax) + return ret() + end + _, err, pos = formatter.apply(win, range, pos) + if err ~= nil then + if err:match('\n') then + vis:message(err) + else + vis:info(err) + end + end + vis:insert('') -- redraw and friends don't work + return ret() +end + +vis.events.subscribe(vis.events.FILE_SAVE_PRE, function(file) + local win = type(file) ~= 'string' and getwinforfile(file) or vis.win + local formatter = format.pick(win) + if formatter == nil then + return + end + local on_save = (formatter.options and formatter.options.on_save ~= nil) + and formatter.options.on_save + or format.options.on_save + if type(on_save) == 'function' and not on_save(win) then + return + elseif not on_save then + return + end + local _, err, pos = formatter.apply(win, nil, win.selection.pos) + if err ~= nil then + vis:info('Warning: formatting failed. Run manually for details') + else + win.selection.pos = pos + vis:insert('') -- redraw and friends don't work + end +end) + +format.formatters = formatters +format.pick = pick +format.apply = keyhandler +format.stdio_formatter = stdio_formatter +format.with_filename = with_filename + +return format diff --git a/gitw b/mut/vis/vis-quickfix/gitw index 0f17645..0f17645 100755 --- a/gitw +++ b/mut/vis/vis-quickfix/gitw diff --git a/init.lua b/mut/vis/vis-quickfix/init.lua index f1e1809..f1e1809 100644 --- a/init.lua +++ b/mut/vis/vis-quickfix/init.lua diff --git a/mut/vis/visrc.lua b/mut/vis/visrc.lua new file mode 100644 index 0000000..b6479b3 --- /dev/null +++ b/mut/vis/visrc.lua @@ -0,0 +1,18 @@ +-- load standard vis module, providing parts of the Lua API +require('vis') +require('vis-editorconfig') + +local format = require('vis-format') +for k, _ in pairs(format.formatters) do + format.formatters[k] = nil +end +format.formatters.python = format.stdio_formatter("ruff format -", {on_save=true}) + + +vis.events.subscribe(vis.events.INIT, function() + vis:command"set shell '/usr/bin/bash'" + vis:command"set edconfhooks on" +end) + +vis.events.subscribe(vis.events.WIN_OPEN, function(win) +end) diff --git a/mut/wal/colorschemes/dark/default.json b/mut/wal/colorschemes/dark/default.json new file mode 100644 index 0000000..eed9503 --- /dev/null +++ b/mut/wal/colorschemes/dark/default.json @@ -0,0 +1,26 @@ +{ + "wallpaper": "/home/ivi/Wallpapers/nixos.png", + "special": { + "background": "#262626", + "foreground": "#dab997", + "cursor": "#dab997" + }, + "colors": { + "color0": "#262626", + "color1": "#d75f5f", + "color2": "#afaf00", + "color3": "#ffaf00", + "color4": "#83adad", + "color5": "#d485ad", + "color6": "#85ad85", + "color7": "#dab997", + "color8": "#8a8a8a", + "color9": "#d75f5f", + "color10": "#afaf00", + "color11": "#ffaf00", + "color12": "#83adad", + "color13": "#d485ad", + "color14": "#85ad85", + "color15": "#ebdbb2" + } +} diff --git a/nix-ontopof-docker2.example.nix b/nix-ontopof-docker2.example.nix new file mode 100644 index 0000000..a996c32 --- /dev/null +++ b/nix-ontopof-docker2.example.nix @@ -0,0 +1,157 @@ + +{ + description = "Nix packages on top of docker pattern"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + + # System types to support. + supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; + + # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'. + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + # Nixpkgs instantiated for supported system types. + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + + in + { + + # Requires dirty nixbld with access to docker daemon + packages = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + + # from: https://github.com/nix-community/home-manager/blob/70824bb5c790b820b189f62f643f795b1d2ade2e/modules/programs/neovim.nix#L412 + neovimConfig = pkgs.neovimUtils.makeNeovimConfig { + viAlias = true; + vimAlias = true; + withPython3 = false; + withRuby = false; + withNodeJs = false; + extraPython3Packages = _: []; + extraLuaPackages = _: []; + + plugins = with pkgs.vimPlugins; [ + # highlighting + nvim-treesitter.withAllGrammars + playground + gruvbox-material + kanagawa-nvim + lsp_lines-nvim + gitsigns-nvim + vim-helm + lualine-nvim + + # external + oil-nvim + vim-fugitive + venn-nvim + gv-vim + zoxide-vim + obsidian-nvim + go-nvim + + # Coding + fzf-lua + nvim-lspconfig + null-ls-nvim + lsp_signature-nvim + nvim-dap + nvim-dap-ui + nvim-nio + nvim-dap-python + luasnip + vim-test + nvim-lint + vim-surround + conform-nvim + trouble-nvim + vim-easy-align + nvim-comment + + # cmp + nvim-cmp + cmp-cmdline + cmp-nvim-lsp + cmp-buffer + cmp-path + cmp_luasnip + + # conjure + vim-racket + nvim-parinfer + hotpot-nvim + ]; + customRC = ""; + }; + + neovim-package = pkgs.wrapNeovimUnstable pkgs.neovim-unwrapped (neovimConfig + // { + wrapRc = false; + }); + + st-terminfo = with pkgs; stdenv.mkDerivation rec { + pname = "st-terminfo"; + version = "0.9.2"; + + src = fetchurl { + url = "https://dl.suckless.org/st/st-${version}.tar.gz"; + hash = "sha256-ayFdT0crIdYjLzDyIRF6d34kvP7miVXd77dCZGf5SUs="; + }; + + nativeBuildInputs = [ ncurses ]; + + outputs = [ "out" ]; + + buildPhase = '' + echo no build + ''; + installPhase = '' + export HOME=$(mktemp -d) + mkdir -p "$out/share/terminfo" + cat st.info | tic -x -o "$out/share/terminfo" - + ''; + }; + + nvim-container = pkgs.dockerTools.buildImage { + name = "nvim-container"; + tag = "latest"; + copyToRoot = pkgs.buildEnv { + extraPrefix = "/usr"; + name = "neovim-nix-ide-usr"; + paths = with pkgs; [ + neovim-package + docker-client + zoxide + xclip + k9s + st-terminfo + ]; + }; + }; +# # TODO: this is just for me +# RUN curl -sSL https://raw.githubusercontent.com/Shourai/st/refs/heads/master/st.info | tic -x - + in + { + nvim-container-install = pkgs.writeShellScriptBin "nvim-container-install" '' + #!/bin/sh + docker image load -i ${nvim-container} + Dockerfile="/tmp/$(id -u)/nix-ontop-docker" + mkdir -p "$(dirname "$Dockerfile")" + cat <<DOCKERFILE >"$Dockerfile" + FROM pionativedev.azurecr.io/pionative/pnsh-ide-support:latest + COPY --from=nvim-container:latest /usr /usr + DOCKERFILE + docker build -f "$Dockerfile" -t pionativedev.azurecr.io/pionative/pnsh-nvim:latest -- . + ''; + }); + + # The default package for 'nix build'. This makes sense if the + # flake provides only one package or there is a clear "main" + # package. + defaultPackage = forAllSystems (system: self.packages.${system}.neovim-container); + }; +} diff --git a/nix-ontopof-dockerfile.nix.example b/nix-ontopof-dockerfile.nix.example new file mode 100644 index 0000000..69f8ea4 --- /dev/null +++ b/nix-ontopof-dockerfile.nix.example @@ -0,0 +1,176 @@ +{ + description = "A simple Go package"; + + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + + # System types to support. + supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; + + # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'. + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + # Nixpkgs instantiated for supported system types. + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + + in + { + + # Requires dirty nixbld with access to docker daemon + packages = forAllSystems (system: + let + pkgs = nixpkgsFor.${system}; + getImageWithSkopeo = + let + fixName = name: builtins.replaceStrings [ "/" ":" ] [ "-" "-" ] name; + in + { imageName + , transport + # To find the digest of an image, you can use skopeo: + # see doc/functions.xml + , imageDigest + , sha256 + , os ? "linux" + , # Image architecture, defaults to the architecture of the `hostPlatform` when unset + arch ? pkgs.go.GOARCH + # This is used to set name to the pulled image + , finalImageName ? imageName + # This used to set a tag to the pulled image + , finalImageTag ? "latest" + # This is used to disable TLS certificate verification, allowing access to http registries on (hopefully) trusted networks + , tlsVerify ? true + + , name ? fixName "image-${finalImageName}-${finalImageTag}.tar" + }: + pkgs.runCommand name + { + inherit imageDigest; + imageName = finalImageName; + imageTag = finalImageTag; + impureEnvVars = pkgs.lib.fetchers.proxyImpureEnvVars; + outputHashMode = "flat"; + outputHashAlgo = "sha256"; + outputHash = sha256; + + nativeBuildInputs = [ pkgs.skopeo ]; + SSL_CERT_FILE = "${pkgs.cacert.out}/etc/ssl/certs/ca-bundle.crt"; + + sourceURL = if transport == "docker-daemon:" then "${transport}${imageDigest}" else "${transport}${imageName}@${imageDigest}"; + destNameTag = "${finalImageName}:${finalImageTag}"; + } '' + skopeo \ + --insecure-policy \ + --tmpdir=$TMPDIR \ + --override-os ${os} \ + --override-arch ${arch} \ + copy \ + --src-tls-verify=${pkgs.lib.boolToString tlsVerify} \ + "$sourceURL" "docker-archive://$out:$destNameTag" \ + | cat # pipe through cat to force-disable progress bar + ''; + pnsh = getImageWithSkopeo { + transport = "docker-daemon:"; + imageName = ""; + imageDigest = ""; + sha256 = ""; + }; + + # from: https://github.com/nix-community/home-manager/blob/70824bb5c790b820b189f62f643f795b1d2ade2e/modules/programs/neovim.nix#L412 + neovimConfig = pkgs.neovimUtils.makeNeovimConfig { + viAlias = true; + vimAlias = true; + withPython3 = false; + withRuby = false; + withNodeJs = false; + extraPython3Packages = _: []; + extraLuaPackages = _: []; + + plugins = with pkgs.vimPlugins; [ + # highlighting + nvim-treesitter.withAllGrammars + playground + gruvbox-material + kanagawa-nvim + lsp_lines-nvim + gitsigns-nvim + vim-helm + lualine-nvim + + # external + oil-nvim + vim-fugitive + venn-nvim + gv-vim + zoxide-vim + obsidian-nvim + go-nvim + + # Coding + fzf-lua + nvim-lspconfig + null-ls-nvim + lsp_signature-nvim + nvim-dap + nvim-dap-ui + nvim-nio + nvim-dap-python + luasnip + vim-test + nvim-lint + vim-surround + conform-nvim + trouble-nvim + vim-easy-align + nvim-comment + + # cmp + nvim-cmp + cmp-cmdline + cmp-nvim-lsp + cmp-buffer + cmp-path + cmp_luasnip + + # conjure + vim-racket + nvim-parinfer + hotpot-nvim + ]; + customRC = ""; + }; + + neovim-package = pkgs.wrapNeovimUnstable pkgs.neovim-unwrapped (neovimConfig + // { + wrapRc = false; + }); + in + { + pnsh-container = pnsh; + neovim-container = pkgs.dockerTools.buildImage { + name = ""; + fromImage = pnsh; + copyToRoot = pkgs.buildEnv { + extraPrefix = "/usr"; + name = "neovim-nix-ide-usr"; + paths = with pkgs; [ + neovim-package + docker-client + zoxide + ]; + }; + config = { + Entrypoint = ["/bin/zsh"]; + Cmd = ["-c" "boot"]; + }; + }; + }); + + # The default package for 'nix build'. This makes sense if the + # flake provides only one package or there is a clear "main" + # package. + defaultPackage = forAllSystems (system: self.packages.${system}.neovim-container); + }; +} + diff --git a/overlays/fennel-language-server.nix b/overlays/fennel-language-server.nix new file mode 100644 index 0000000..95be490 --- /dev/null +++ b/overlays/fennel-language-server.nix @@ -0,0 +1,13 @@ +{pkgs, ...}: (final: prev: + with pkgs; { + fennel-language-server = rustPlatform.buildRustPackage rec { + name = "fennel-language-server"; + src = fetchGit { + url = "https://github.com/rydesun/fennel-language-server"; + submodules = true; + rev = "d0c65db2ef43fd56390db14c422983040b41dd9c"; + ref = "refs/heads/main"; + }; + cargoHash = "sha256-B4JV1rgW59FYUuqjPzkFF+/T+4Gpr7o4z7Cmpcszcb8="; + }; + }) diff --git a/overlays/fzf.nix b/overlays/fzf.nix new file mode 100644 index 0000000..80b6f07 --- /dev/null +++ b/overlays/fzf.nix @@ -0,0 +1,12 @@ +{pkgs, lib, ...}: with pkgs; (final: prev: { + fzf = (prev.fzf.overrideAttrs (oldAttrs: rec { + version = "0.53.0"; + src = fetchFromGitHub { + owner = "junegunn"; + repo = "fzf"; + rev = version; + hash = "sha256-2g1ouyXTo4EoCub+6ngYPy+OUFoZhXbVT3FI7r5Y7Ws="; + }; + vendorHash = "sha256-Kd/bYzakx9c/bF42LYyE1t8JCUqBsJQFtczrFocx/Ps="; + })); +}) diff --git a/overlays/openpomodoro-cli.nix b/overlays/openpomodoro-cli.nix new file mode 100644 index 0000000..6443868 --- /dev/null +++ b/overlays/openpomodoro-cli.nix @@ -0,0 +1,19 @@ +{pkgs, lib, ...}: (final: prev: + with pkgs; { + openpomodoro-cli = buildGoModule rec { + pname = "openpomodoro-cli"; + version = "0.3.0"; + + src = fetchFromGitHub { + owner = "open-pomodoro"; + repo = "openpomodoro-cli"; + rev = "v${version}"; + hash = "sha256-h/o4yxrZ8ViHhN2JS0ZJMfvcJBPCsyZ9ZQw9OmKnOfY="; + }; + + + vendorHash = "sha256-BR9d/PMQ1ZUYWSDO5ID2bkTN+A+VbaLTlz5t0vbkO60="; + + }; + } +) diff --git a/overlays/suckless.nix b/overlays/suckless.nix new file mode 100644 index 0000000..8fd5529 --- /dev/null +++ b/overlays/suckless.nix @@ -0,0 +1,27 @@ +{pkgs, home, ...}: (final: prev: { + st = (prev.st.overrideAttrs (oldAttrs: { + src = home + "/mut/st"; + version = "0.3.2"; + buildInputs = oldAttrs.buildInputs ++ [prev.harfbuzz]; + })); + dwm = (prev.dwm.overrideAttrs (oldAttrs: { + src = home + "/mut/dwm"; + version = "0.1.6"; + })); + dwmblocks =(prev.stdenv.mkDerivation { + pname = "dwmblocks"; + version = "1.1.4"; + src = home + "/mut/dwmblocks"; + buildInputs = [prev.xorg.libX11]; + installPhase = '' + install -m755 -D dwmblocks $out/bin/dwmblocks + ''; + }); + surf = (prev.surf.overrideAttrs (oldAttrs: { + src = home + "/mut/surf"; + version = "2.1-ivi-vink.1"; + })); + tabbed = prev.tabbed.overrideAttrs (oldAttrs: { + src = home + "/mut/tabbed"; + }); +}) diff --git a/overlays/vimPlugins.nix b/overlays/vimPlugins.nix new file mode 100644 index 0000000..8885e9b --- /dev/null +++ b/overlays/vimPlugins.nix @@ -0,0 +1,48 @@ +{pkgs, ...}: (final: prev: { + vimPlugins = let + getVimPlugin = { + name, + git, + rev, + ref ? "master", + }: + pkgs.vimUtils.buildVimPlugin { + inherit name; + src = builtins.fetchGit { + url = "https://github.com/${git}"; + submodules = true; + inherit rev; + inherit ref; + }; + }; + in + prev.vimPlugins + // { + avante-nvim = getVimPlugin { + name = "avante-nvim"; + git = "yetone/avante.nvim"; + rev = "a8e2b9a00c73b11d28857f0f5de79a9022281182"; + }; + nvim-cinnamon = getVimPlugin { + name = "vim-easygrep"; + git = "declancm/cinnamon.nvim"; + rev = "e48538cba26f079822329a6d12b8cf2b916e925a"; + }; + nvim-nio = getVimPlugin { + name = "nvim-nio"; + git = "nvim-neotest/nvim-nio"; + rev = "5800f585def265d52f1d8848133217c800bcb25d"; + }; + nvim-parinfer = getVimPlugin { + name = "nvim-parinfer"; + git = "gpanders/nvim-parinfer"; + rev = "82bce5798993f4fe5ced20e74003b492490b4fe8"; + }; + codeium-vim = getVimPlugin { + name = "codeium-vim"; + git = "Exafunction/codeium.vim"; + rev = "be2fa21f4f63850382a0cefeaa9f766b977c9f0c"; + ref = "refs/heads/main"; + }; + }; +}) diff --git a/profiles/core/configuration.nix b/profiles/core/configuration.nix new file mode 100644 index 0000000..fdbb94c --- /dev/null +++ b/profiles/core/configuration.nix @@ -0,0 +1,58 @@ +{ + machine, + config, + pkgs, + lib, + ... +}: with lib; { + imports = [ (mkAliasOptionModule [ "my" ] [ "users" "users" my.username ]) ]; + + nixpkgs.config.allowUnfree = true; + + services = { + resolved.fallbackDns = [ + "1.1.1.1#one.one.one.one" + "1.0.0.1#one.one.one.one" + "2606:4700:4700::1111#one.one.one.one" + "2606:4700:4700::1001#one.one.one.one" + ]; + }; + security = { + sudo = { + wheelNeedsPassword = false; + extraConfig = '' + Defaults env_keep+="EDITOR" + Defaults env_keep+="SSH_CONNECTION SSH_CLIENT SSH_TTY" + Defaults env_keep+="http_proxy https_proxy" + ''; + }; + }; + + time.timeZone = "Europe/Amsterdam"; + users.users = { + ${my.username} = { + uid = mkIf (!machine.isDarwin) 1000; + description = my.realName; + openssh.authorizedKeys.keys = my.sshKeys; + extraGroups = ["wheel" "networkmanager" "docker" "transmission" "dialout" "test" "libvirtd"]; + isNormalUser = true; + }; + root = { + openssh.authorizedKeys.keys = config.my.openssh.authorizedKeys.keys; + }; + }; + + nix.package = pkgs.nixVersions.latest; + nix.extraOptions = '' + experimental-features = nix-command flakes configurable-impure-env + ''; + + hm.xdg.configFile."gtk-2.0/gtkrc".text = '' + gtk-key-theme-name = "Emacs" + ''; + + hm.xdg.configFile."gtk-3.0/settings.ini".text = '' + [Settings] + gtk-key-theme-name = Emacs + ''; +} diff --git a/profiles/core/git.nix b/profiles/core/git.nix new file mode 100644 index 0000000..f651f65 --- /dev/null +++ b/profiles/core/git.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + ... +}: +with lib; { + hm = { + programs.git = { + enable = true; + userName = my.realName; + userEmail = my.email; + extraConfig = { + worktree.guessRemote = true; + mergetool.fugitive.cmd = "vim -f -c \"Gdiff\" \"$MERGED\""; + merge.tool = "fugitive"; + gpg.format = "ssh"; + user.signingKey = "${config.my.home}/.ssh/id_ed25519_sk.pub"; + commit.gpgsign = false; + }; + + includes = let + gh-no-reply-email = { + user = { + email = "59492084+ivi-vink@users.noreply.github.com"; + }; + }; + in [ + { + condition = "hasconfig:remote.*.url:git@github.com:**/**"; + contents = gh-no-reply-email; + } + { + condition = "hasconfig:remote.*.url:https://github.com/**/**"; + contents = gh-no-reply-email; + } + ]; + + ignores = [ + "/.direnv/" + "/.envrc" + "/.env" + ".vimsession.vim" + "tfplan" + "plan" + ]; + }; + }; +} diff --git a/profiles/core/hm.nix b/profiles/core/hm.nix new file mode 100644 index 0000000..57c7d0f --- /dev/null +++ b/profiles/core/hm.nix @@ -0,0 +1,21 @@ +{inputs, config, lib, pkgs, ...}: with lib; { + imports = [ + (mkAliasOptionModule [ "hm" ] [ "home-manager" "users" my.username ]) + ]; + + home-manager = { + useGlobalPkgs = true; + useUserPackages = true; + verbose = true; + extraSpecialArgs = { inherit inputs; }; + }; + + hm = { + home.stateVersion = if pkgs.stdenv.isDarwin then "24.05" else config.system.stateVersion; + home.enableNixpkgsReleaseCheck = false; + + systemd.user.startServices = "sd-switch"; + + manual.html.enable = true; + }; +} diff --git a/profiles/core/home.nix b/profiles/core/home.nix new file mode 100644 index 0000000..cc34a65 --- /dev/null +++ b/profiles/core/home.nix @@ -0,0 +1,407 @@ +{ + inputs, + machine, + lib, + config, + pkgs, + ... +}: +with lib; { + hm = { + programs.password-store = { + enable = true; + settings = { + PASSWORD_STORE_DIR = config.synced.password-store.path; + }; + }; + # home.file.".config/ghostty".source = config.lib.meta.mkMutableSymlink /mut/ghostty; + # home.file.".config/nushell".source = config.lib.meta.mkMutableSymlink /mut/nushell; + # xdg.configFile."nvim".source = config.lib.meta.mkMutableSymlink /mut/neovim; + xdg = { + enable = true; + mime.enable = !machine.isDarwin; + mimeApps = optionalAttrs (!machine.isDarwin) { + enable = true; + defaultApplications = { + "text/x-shellscript" = ["text.desktop"]; + "application/x-bittorrent" = ["torrent.desktop"]; + "text/plain" = ["text.desktop"]; + "application/postscript" = ["pdf.desktop"]; + "application/pdf" = ["pdf.desktop"]; + "image/png" = ["img.desktop"]; + "image/jpeg" = ["img.desktop"]; + "image/gif" = ["img.desktop"]; + "application/rss+xml" = ["rss.desktop"]; + "video/x-matroska" = ["video.desktop"]; + "video/mp4" = ["video.desktop"]; + "x-scheme-handler/lbry" = ["lbry.desktop"]; + "inode/directory" = ["file.desktop"]; + "application/x-ica" = ["wfica.desktop"]; + "x-scheme-handler/magnet" = ["torrent.desktop"]; + "x-scheme-handler/mailto" = ["mail.desktop"]; + "x-scheme-handler/msteams" = ["teams.desktop"]; + "x-scheme-handler/http" = ["surf.desktop"]; + "x-scheme-handler/https" = ["surf.desktop"]; + }; + }; + desktopEntries = with pkgs; + optionalAttrs (!machine.isDarwin) { + surf = { + type = "Application"; + name = "Browser"; + exec = "${inputs.self}/mut/surf/surf-open.sh %u"; + }; + text = { + type = "Application"; + name = "Text editor"; + exec = "${st}/bin/st -e nvim %u"; + }; + file = { + type = "Application"; + name = "File Manager"; + exec = "${st}/bin/st -e lfub %u"; + }; + torrent = { + type = "Application"; + name = "Torrent"; + exec = "${coreutils}/bin/env transadd %U"; + }; + img = { + type = "Application"; + name = "Image Viewer"; + exec = "${sxiv}/bin/sxiv -a %u"; + }; + video = { + type = "Application"; + name = "Video Viewer"; + exec = "${mpv}/bin/mpv -quiet %f"; + }; + mail = { + type = "Application"; + name = "Mail"; + exec = "${st}/bin/st -e neomutt %u"; + }; + pdf = { + type = "Application"; + name = "PDF reader"; + exec = "${zathura}/bin/zathura %u"; + }; + rss = { + type = "Application"; + name = "RSS feed addition"; + exec = "${coreutils}/bin/env rssadd %u"; + }; + }; + }; + + programs.starship = { + enable = true; + enableZshIntegration = false; + }; + + programs.direnv = { + enable = true; + nix-direnv.enable = true; + }; + + programs.readline = { + enable = true; + extraConfig = '' + $if mode=vi + + set keymap vi-command + # these are for vi-command mode + Control-l: clear-screen + + set keymap vi-insert + # these are for vi-insert mode + Control-l: clear-screen + $endif + ''; + }; + + programs = { + zsh = { + enable = true; + autosuggestion.enable = true; + completionInit = '' + if [ -z "$ZSHRC_IVI" ]; then + zmodload zsh/complist + autoload -Uz +X compinit bashcompinit select-word-style + select-word-style bash + zstyle ':completion:*' menu select + _comp_options+=(globdots) # Include hidden files. + compinit + bashcompinit + fi + ''; + initExtra = '' + ZSH_AUTOSUGGEST_MANUAL_REBIND=1 + if command -v pnsh-nvim >/dev/null 2>&1 && [ -z "$ZSHRC_IVI" ]; then + export COLORTERM=truecolor + export GPG_TTY="$(tty)" + gpgconf --launch gpg-agent + + if [ ! -S ~/.ssh/ssh_auth_sock ]; then + eval `ssh-agent` + ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock + fi + export SSH_AUTH_SOCK=~/.ssh/ssh_auth_sock + # ssh-add -l > /dev/null || ssh-add ~/.ssh/id_ed25519_sk + + if [[ $TERM != "dumb" ]]; then + eval "$(/etc/profiles/per-user/ivi/bin/starship init zsh)" + fi + + pnsh-nvim true || { + echo "Pnsh exited badly :(" + } + fi + export MANPAGER='nvim +Man!' + export EDITOR="nvim" + # export TERMINAL="st" + export PATH="''${KREW_ROOT:-$HOME/.krew}/bin:$PATH" + export PASSWORD_STORE_GPG_OPTS='--no-throw-keyids' + + export GNUPGHOME="''${HOME}/.gnupg" + export LOCALE_ARCHIVE_2_27="/nix/store/l8hm9q8ndlg2rvav47y7549llh6npznf-glibc-locales-2.39-52/lib/locale/locale-archive" + export PASSWORD_STORE_DIR="''${HOME}/sync/password-store" + export XDG_CACHE_HOME="''${HOME}/.cache" + export XDG_CONFIG_HOME="''${HOME}/.config" + export XDG_DATA_HOME="''${HOME}/.local/share" + export XDG_STATE_HOME="''${HOME}/.local/state" + export PATH="$PATH:$HOME/.local/bin:/opt/homebrew/bin:${config.my.home}/.krew/bin:${config.my.home}/.cargo/bin:${pkgs.ncurses}/bin" + export STARSHIP_CONFIG="''${HOME}/.config/starship.toml" + command -v nu >/dev/null 2>&1 && exec nu --login + + # Use vim keys in tab complete menu: + export ZLE_REMOVE_SUFFIX_CHARS=$' ,=\t\n;&|/@' + export ZSH_AUTOSUGGEST_STRATEGY=(history completion) + bindkey -M menuselect 'h' vi-backward-char + bindkey -M menuselect 'k' vi-up-line-or-history + bindkey -M menuselect 'l' vi-forward-char + bindkey -M menuselect 'j' vi-down-line-or-history + set -o emacs + + # Use lf to switch directories and bind it to ctrl-o + lfcd () { + tmp="$(mktemp -uq)" + trap 'rm -f $tmp >/dev/null 2>&1 && trap - HUP INT QUIT TERM EXIT' HUP INT QUIT TERM EXIT + EDITOR=vremote lfub -last-dir-path="$tmp" "$@" + if [ -f "$tmp" ]; then + dir="$(cat "$tmp")" + [ -d "$dir" ] && [ "$dir" != "$(pwd)" ] && cd "$dir" + fi + } + bindkey -s '^o' '^ulfcd\n' + + export FZF_DEFAULT_OPTS='-m --bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all' + + # Options to fzf command + export FZF_COMPLETION_OPTS='--border --info=inline' + + # Use fd (https://github.com/sharkdp/fd) for listing path candidates. + # - The first argument to the function ($1) is the base path to start traversal + # - See the source code (completion.{bash,zsh}) for the details. + _fzf_compgen_path() { + fd --hidden --follow --exclude ".git" . "$1" + } + + # Use fd to generate the list for directory completion + _fzf_compgen_dir() { + fd --type d --hidden --follow --exclude ".git" . "$1" + } + + + fzf-tail () { + fzf --tail 100000 --tac --no-sort --exact + } + + fzf-stern () { + kubectl config set-context --current --namespace "$1" && + kubectl stern -n "$1" "$2" --color always 2>&1 | + fzf --ansi --tail 100000 --tac --no-sort --exact \ + --bind 'ctrl-o:execute:kubectl logs {1} | nvim -' \ + --bind 'enter:execute:kubectl exec -it {1} -- bash' \ + --preview 'echo {}' --preview-window down:20%:wrap \ + --header '╱ Enter (kubectl exec) ╱ CTRL-O (open log in vim) ╱' + } + + helmball() { + tar --extract --verbose --file "$1" && + mv --verbose "''${1%-*}" "''${1%.tgz}" && + rm --verbose "$1" + } + + G () { vi +"chdir ''${1:-.}" +G +only ; } + + login_aws() { + aws configure list-profiles | + grep -E -v -e default -e '.*_.*' | + parallel --jobs 4 --quote \ + sh -c 'aws sts get-caller-identity --profile {} 1>&2 || echo {}' | + xargs -I{} 'aws sso login --profile {}' + + AWS_PROFILE="$(aws configure list-profiles | grep -v default | fzf)" + [ -z "$AWS_PROFILE" ] && + { + echo Selected empty aws profile! + exit 1 + } + export AWS_PROFILE + if ! error="$(aws sts get-caller-identity --profile "$AWS_PROFILE" 2>&1)"; then + case "$error" in + *'SSO session associated with this profile has expired'*) aws sso login ;; + *'Error loading SSO Token'*) aws sso login ;; + *) echo "Not sure what to do with error: $error"; echo "trying to sign in"; aws sso login ;; + esac + fi + eval "$(aws configure export-credentials --profile "$AWS_PROFILE" --format env)" + } + + login_gcp() { + gcloud config configurations activate "$(gcloud config configurations list --format json | jq '.[] | "\(.name) \(.properties.core.account)"' -r | fzf | awk '{print $1}')" + projects="$(gcloud projects list --format='value(name,projectId)' 2>/dev/null)" || + { + gcloud auth login + projects="$(gcloud projects list --format='value(name,projectId)')" + } + project="$(printf '%s' "$projects" | fzf | awk '{ print $2 }')" + gcloud auth application-default set-quota-project "$project" + gcloud config set project "$project" + + gcloud auth application-default print-access-token >/dev/null 2>&1 || + { + gcloud auth application-default login + } + } + + login_azure() { + missing_tenants="$( + grep -v \ + -f /dev/fd/3 /dev/fd/4 3<<-EOF 4<<-EOF + $(az account list --output json | jq -r '.[] | .tenantId') + EOF + $(az account tenant list --output json 2>/dev/null | jq -r '.[].tenantId') + EOF + )" && { + echo "Found tenants that were not logged in! Logging into all of them" + printf '%s' "$missing_tenants + " | xargs -n1 az login --allow-no-subscriptions --tenant + } + set -- $( + { + az account list --all 2>/dev/null || + { + az login --allow-no-subscriptions && az account list --all + } + } | jq '.[] | select(.name | test("N/A.*") | not) | "\(.name)\t\(.id)"' -r | fzf + ) + + sub="$2" + az account set --subscription "''${sub:?}" + if ! az resource list >/dev/null 2>&1; then + az login --allow-no-subscriptions --tenant "$(az account show | jq '.tenantId' -r)" + fi + } + + log_in () { + case $1 in + aws) login_aws ;; + gcp) login_gcp ;; + azure) login_azure ;; + all) + login_aws + login_gcp + login_azure + ;; + *) echo "Don't know how to switch context for: $1" ;; + esac + } + + export ZLE_REMOVE_SUFFIX_CHARS=$' ,=\t\n;&|/@' + + # Workarounds for completion here... + command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init zsh)" + if [ -z "$ZSHRC_IVI" ]; then + krew info stern >/dev/null 2>&1 && eval "$(kubectl stern --completion zsh)" + command -v brew >/dev/null 2>&1 && eval "$(/opt/homebrew/bin/brew shellenv)" + command -v docker >/dev/null 2>&1 && eval "$(docker completion zsh)" + command -v kubectl >/dev/null 2>&1 && eval "$(kubectl completion zsh)" + command -v pioctl >/dev/null 2>&1 && eval "$(_PIOCTL_COMPLETE=zsh_source pioctl)" + command -v aws >/dev/null 2>&1 && source /run/current-system/sw/share/zsh/site-functions/_aws + command -v az >/dev/null 2>&1 && { + source /run/current-system/sw/share/zsh/site-functions/_az + } + fi + + [[ -f ~/.cache/wal/sequences ]] && (cat ~/.cache/wal/sequences &) + unset LD_PRELOAD + + alias g="git " + alias t="terraform " + alias c="xclip -f | xclip -sel c -f " + alias o="xdg-open " + alias k="kubectl " + alias d="docker " + alias l="ls --color=auto" + alias s="${ + if machine.isDarwin + then "sudo darwin-rebuild switch --flake ~/nix-config" + else "sudo nixos-rebuild switch --flake /nix-config" + }" + alias b="/run/current-system/bin/switch-to-configuration boot" + alias v="vi " + alias e="vi " + alias l="lfub" + alias M="xrandr --output HDMI1 --auto --output eDP1 --off" + alias m="xrandr --output eDP1 --auto --output HDMI1 --off" + alias m="xrandr --output eDP1 --auto --output HDMI1 --off" + alias n="nix flake new -t ~/flake " + alias use-gpg-ssh="export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)" + alias use-fido-ssh="export SSH_AUTH_SOCK=~/.ssh/ssh_auth_sock" + ''; + }; + }; + + # https://github.com/drduh/config/blob/master/gpg.conf + # https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html + # https://www.gnupg.org/documentation/manuals/gnupg/GPG-Esoteric-Options.html + programs.gpg = { + enable = true; + scdaemonSettings = { + disable-ccid = true; + }; + settings = { + personal-cipher-preferences = "AES256 AES192 AES"; + personal-digest-preferences = "SHA512 SHA384 SHA256"; + personal-compress-preferences = "ZLIB BZIP2 ZIP Uncompressed"; + default-preference-list = "SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed"; + cert-digest-algo = "SHA512"; + s2k-digest-algo = "SHA512"; + s2k-cipher-algo = "AES256"; + charset = "utf-8"; + fixed-list-mode = true; + no-comments = true; + no-emit-version = true; + no-greeting = true; + keyid-format = "0xlong"; + list-options = "show-uid-validity"; + verify-options = "show-uid-validity"; + "with-fingerprint" = true; + require-cross-certification = true; + no-symkey-cache = true; + use-agent = true; + throw-keyids = true; + }; + }; + services.gpg-agent = { + enable = !machine.isDarwin; + enableSshSupport = false; + defaultCacheTtl = 34550000; + maxCacheTtl = 34550000; + pinentryPackage = pkgs.pinentry-gtk2; + # pinentryFlavor = "gtk2"; + }; + }; +} diff --git a/profiles/core/lf.nix b/profiles/core/lf.nix new file mode 100644 index 0000000..ab7d686 --- /dev/null +++ b/profiles/core/lf.nix @@ -0,0 +1,87 @@ +{pkgs,...}: { + hm.home.packages = [pkgs.ueberzugpp pkgs.lf pkgs.nsxiv]; + hm.xdg.configFile = { + # "lf/cleaner".source = config.lib.meta.mkMutableSymlink /mut/lf/cleaner; + # "lf/scope".source = config.lib.meta.mkMutableSymlink /mut/lf/scope; + # "lf/lfrc".source = config.lib.meta.mkMutableSymlink /mut/lf/lfrc; + "lf/icons".text = '' + di 📁 + fi 📃 + tw 🤝 + ow 📂 + ln ⛓ + or ❌ + ex 🎯 + *.txt ✍ + *.mom ✍ + *.me ✍ + *.ms ✍ + *.avif 🖼 + *.png 🖼 + *.webp 🖼 + *.ico 🖼 + *.jpg 📸 + *.jpe 📸 + *.jpeg 📸 + *.gif 🖼 + *.svg 🗺 + *.tif 🖼 + *.tiff 🖼 + *.xcf 🖌 + *.html 🌎 + *.xml 📰 + *.gpg 🔒 + *.css 🎨 + *.pdf 📚 + *.djvu 📚 + *.epub 📚 + *.csv 📓 + *.xlsx 📓 + *.tex 📜 + *.md 📘 + *.r 📊 + *.R 📊 + *.rmd 📊 + *.Rmd 📊 + *.m 📊 + *.mp3 🎵 + *.opus 🎵 + *.ogg 🎵 + *.m4a 🎵 + *.flac 🎼 + *.wav 🎼 + *.mkv 🎥 + *.mp4 🎥 + *.webm 🎥 + *.mpeg 🎥 + *.avi 🎥 + *.mov 🎥 + *.mpg 🎥 + *.wmv 🎥 + *.m4b 🎥 + *.flv 🎥 + *.zip 📦 + *.rar 📦 + *.7z 📦 + *.tar 📦 + *.z64 🎮 + *.v64 🎮 + *.n64 🎮 + *.gba 🎮 + *.nes 🎮 + *.gdi 🎮 + *.1 ℹ + *.nfo ℹ + *.info ℹ + *.log 📙 + *.iso 📀 + *.img 📀 + *.bib 🎓 + *.ged 👪 + *.part 💔 + *.torrent 🔽 + *.jar ♨ + *.java ♨ + ''; + }; +} diff --git a/profiles/core/meta.nix b/profiles/core/meta.nix new file mode 100644 index 0000000..0cf0c1c --- /dev/null +++ b/profiles/core/meta.nix @@ -0,0 +1,8 @@ +{inputs,lib,config, ...}: with lib; { + lib.meta = { + configPath = "/nix-config"; + mkMutableSymlink = path: + config.hm.lib.file.mkOutOfStoreSymlink + (config.lib.meta.configPath + removePrefix (toString inputs.self) (toString path)); + }; +} diff --git a/profiles/core/neovim.nix b/profiles/core/neovim.nix new file mode 100644 index 0000000..8ec65a0 --- /dev/null +++ b/profiles/core/neovim.nix @@ -0,0 +1,76 @@ +{ + pkgs, + config, + ... +}: { + hm = { + editorconfig = { + enable = true; + settings = { + "*" = { + trim_trailing_whitespace = true; + insert_final_newline = true; + }; + "*.{yaml,nix,sh}" = { + indent_style = "space"; + indent_size = 2; + }; + }; + }; + + programs.neovim = { + enable = true; + package = pkgs.neovim-unwrapped; + viAlias = true; + vimAlias = true; + extraPackages = with pkgs; [ + # bashInteractive + # pyright + # gopls + # fennel + # fnlfmt + # alejandra + # statix + # fzf + # nil + # shellcheck + # vale + ]; + plugins = with pkgs.vimPlugins; [ + # highlighting + nvim-treesitter.withAllGrammars + gruvbox-material + kanagawa-nvim + lsp_lines-nvim + gitsigns-nvim + vim-helm + + # external + oil-nvim + vim-fugitive + gv-vim + zoxide-vim + obsidian-nvim + go-nvim + + # Debug adapter + nvim-dap + nvim-dap-ui + nvim-nio + nvim-dap-python + + # editing + fzf-lua + nvim-lspconfig + lsp_signature-nvim + luasnip + nvim-lint + vim-surround + conform-nvim + trouble-nvim + vim-easy-align + nvim-comment + ]; + }; + }; +} diff --git a/profiles/core/packages.nix b/profiles/core/packages.nix new file mode 100644 index 0000000..4370616 --- /dev/null +++ b/profiles/core/packages.nix @@ -0,0 +1,54 @@ +{ + machine, + config, + pkgs, + lib, + ... +}: + +with lib; + +{ + environment.systemPackages = with pkgs; [ + vim + wget + git + subversion + htop + jq + yq-go + curl + fd + lf + fzf + ripgrep + parallel + pinentry-curses + gnused + gnutls + zoxide + binwalk + unzip + # gcc + gnumake + file + bc + mediainfo + bat + openpomodoro-cli + coreutils + killall + carapace + ] ++ (optionals (!machine.isDarwin) [ + man-pages + man-pages-posix + psmisc + # pkgsi686Linux.glibc + gdb + pciutils + dnsutils + iputils + inetutils + usbutils + ]); +} diff --git a/profiles/core/secrets.nix b/profiles/core/secrets.nix new file mode 100644 index 0000000..192bacf --- /dev/null +++ b/profiles/core/secrets.nix @@ -0,0 +1,39 @@ +{machine,inputs,config,lib,pkgs,...}: with lib; +let + getSecrets = dir: + mapAttrs' (name: _: let + parts = splitString "." name; + base = head parts; + format = if length parts > 1 then elemAt parts 1 else "binary"; + in nameValuePair base { + sopsFile = "${dir}/${name}"; + inherit format; + key = machine.hostname; + }) (if (filesystem.pathIsDirectory dir) then + (filterAttrs (n: v: v != "directory") (builtins.readDir dir)) + else + {}); +in +{ + imports = [ + inputs.sops-nix.nixosModules.sops + (mkAliasOptionModule [ "secrets" ] [ "sops" "secrets" ]) # TODO: get my username(s) from machine config + ]; + config = mkIf (!machine.isFake) { + sops = { + secrets = attrsets.mergeAttrsList + [ + (getSecrets "${inputs.self}/secrets") + (getSecrets "${inputs.self}/secrets/${machine.hostname}") + ]; + }; + + environment = { + systemPackages = [ + pkgs.sops + pkgs.age + ]; + }; + + }; +} diff --git a/profiles/core/syncthing.nix b/profiles/core/syncthing.nix new file mode 100644 index 0000000..796a3d7 --- /dev/null +++ b/profiles/core/syncthing.nix @@ -0,0 +1,64 @@ +{machines, machine, config, lib,...}: with lib; let + group = if machine.isDarwin then (builtins.toString config.my.gid) else config.my.group; +in { + imports = [ + (mkAliasOptionModule [ "synced" ] [ "services" "syncthing" "settings" "folders" ]) + ]; + + services.syncthing = { + enable = machine.syncthing.enable; + user = my.username; + inherit group; + dataDir = config.my.home; + overrideDevices = true; + overrideFolders = true; + + key = config.secrets.syncthing.path; + + settings = let + allDevices = (filterAttrs (_: m: m.syncthing.id != "") machines); + in { + gui = { + theme = "default"; + insecureAdminAccess = true; + }; + + devices = mapAttrs (_: m: { + inherit (m.syncthing) id; + introducer = m.isServer; + }) allDevices; + + folders = let + trashcan = { + type = "trashcan"; + params.cleanoutDays = "0"; + }; + simple = { + type = "simple"; + params = { + keep = "5"; + cleanoutDays = "0"; + }; + }; + allNames = attrNames allDevices; + in { + my = { + path = "${config.my.home}/sync/my"; + devices = allNames; + versioning = simple; + }; + pictures = { + path = "${config.my.home}/sync/pictures"; + devices = allNames; + versioning = trashcan; + }; + password-store = { + path = "${config.my.home}/sync/password-store"; + devices = allNames; + versioning = trashcan; + }; + }; + }; + }; + environment.systemPackages = [config.services.syncthing.package]; +} diff --git a/profiles/email/gmail.nix b/profiles/email/gmail.nix new file mode 100644 index 0000000..229738b --- /dev/null +++ b/profiles/email/gmail.nix @@ -0,0 +1,118 @@ +{ + config, + pkgs, + ... +}: { + hm = { + accounts.email = { + maildirBasePath = "${config.hm.xdg.dataHome}/mail"; + accounts = { + gmail = { + primary = true; + realName = "Mike Vink"; + userName = "mike1994vink@gmail.com"; + address = "mike1994vink@gmail.com"; + passwordCommand = ["${pkgs.pass}/bin/pass" "personal/neomutt"]; + imap = { host = "imap.gmail.com"; port = 993; tls = { enable = true; }; }; + smtp = { host = "smtp.gmail.com"; port = 587; tls = { enable = true; useStartTls = true; }; }; + msmtp = { + enable = true; + }; + neomutt = { + enable = true; + sendMailCommand = "msmtp -a gmail"; + mailboxName = "=== mike1994vink ==="; + extraConfig = '' + set spoolfile='Inbox' + unvirtual-mailboxes * + ''; + }; + mbsync = { + enable = true; + create = "both"; remove = "both"; expunge = "both"; + groups = { + gmail = { + channels = { + Inbox = { farPattern = "INBOX"; nearPattern = "INBOX"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Archive = { farPattern = "[Gmail]/All Mail"; nearPattern = "Archive"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Spam = { farPattern = "[Gmail]/Spam"; nearPattern = "Spam"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Trash = { farPattern = "[Gmail]/Bin"; nearPattern = "Trash"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Important = { farPattern = "[Gmail]/Important"; nearPattern = "Important"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Sent = { farPattern = "[Gmail]/Sent Mail"; nearPattern = "Sent"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + FarDrafts = { farPattern = "[Gmail]/Drafts"; nearPattern = "FarDrafts"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + }; + }; + }; + }; + notmuch = { + enable = true; + neomutt = { + enable = true; + virtualMailboxes = [ + { name = "Inbox"; query = "folder:/gmail/ tag:inbox"; } + { name = "Archive"; query = "folder:/gmail/ tag:archive"; } + { name = "Sent"; query = "folder:/gmail/ tag:sent"; } + { name = "Spam"; query = "folder:/gmail/ tag:spam"; } + { name = "Trash"; query = "folder:/gmail/ tag:trash"; } + { name = "Jobs"; query = "folder:/gmail/ tag:jobs"; } + { name = "Houses"; query = "folder:/gmail/ tag:houses"; } + { name = "Development"; query = "folder:/gmail/ tag:dev"; } + ]; + }; + }; + }; + family = { + primary = false; + realName = "Natalia & Mike Vink"; + userName = "natalia.mike.vink@gmail.com"; + address = "natalia.mike.vink@gmail.com"; + passwordCommand = ["${pkgs.pass}/bin/pass" "personal/neomutt-family"]; + imap = { host = "imap.gmail.com"; port = 993; tls = { enable = true; }; }; + smtp = { host = "smtp.gmail.com"; port = 587; tls = { enable = true; useStartTls = true; }; }; + msmtp = { + enable = true; + }; + neomutt = { + enable = true; + sendMailCommand = "msmtp -a family"; + mailboxName = "=== family ==="; + extraConfig = '' + set spoolfile='Inbox' + unvirtual-mailboxes * + ''; + }; + mbsync = { + enable = true; + create = "both"; remove = "both"; expunge = "both"; + groups = { + family = { + channels = { + Inbox = { farPattern = "INBOX"; nearPattern = "INBOX"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Archive = { farPattern = "[Gmail]/All Mail"; nearPattern = "Archive"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Spam = { farPattern = "[Gmail]/Spam"; nearPattern = "Spam"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Trash = { farPattern = "[Gmail]/Trash"; nearPattern = "Trash"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Important = { farPattern = "[Gmail]/Important"; nearPattern = "Important"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + Sent = { farPattern = "[Gmail]/Sent Mail"; nearPattern = "Sent"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + FarDrafts = { farPattern = "[Gmail]/Drafts"; nearPattern = "FarDrafts"; extraConfig = { Create = "Near"; Expunge = "Both"; }; }; + }; + }; + }; + }; + notmuch = { + enable = true; + neomutt = { + enable = true; + virtualMailboxes = [ + { name = "Inbox"; query = "folder:/family/ tag:inbox"; } + { name = "Archive"; query = "folder:/family/ tag:archive"; } + { name = "Sent"; query = "folder:/family/ tag:sent"; } + { name = "Spam"; query = "folder:/family/ tag:spam"; } + { name = "Trash"; query = "folder:/family/ tag:trash"; } + ]; + }; + }; + }; + }; + }; + }; +} diff --git a/profiles/email/mailsync.nix b/profiles/email/mailsync.nix new file mode 100644 index 0000000..42620d6 --- /dev/null +++ b/profiles/email/mailsync.nix @@ -0,0 +1,34 @@ +{ + config, + ... +}: { + hm = { + programs.mbsync = { + enable = true; + }; + systemd.user.timers.mailsync = { + Unit = { + Description = "daemon that syncs mail"; + }; + Timer = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + Unit = "mailsync.service"; + }; + Install = { + WantedBy = [ "timers.target" ]; + }; + }; + systemd.user.services.mailsync = { + Unit = { + Description = "daemon that syncs mail"; + }; + Service = { + Type = "oneshot"; + RemainAfterExit = "no"; + ExecSearchPath = "${config.my.home}/.local/bin:${config.hm.home.profileDirectory}/bin:/run/current-system/sw/bin"; + ExecStart = "mailsync"; + }; + }; + }; +} diff --git a/profiles/email/neomutt.nix b/profiles/email/neomutt.nix new file mode 100644 index 0000000..7940fc6 --- /dev/null +++ b/profiles/email/neomutt.nix @@ -0,0 +1,202 @@ +{ + ... +}: { + hm = { + programs.msmtp = { + enable = true; + }; + + xdg.configFile."neomutt/mailcap" = { + text = '' + text/plain; $EDITOR %s ; + text/html; openfile %s ; nametemplate=%s.html + text/html; lynx -assume_charset=%{charset} -display_charset=utf-8 -dump -width=1024 %s; nametemplate=%s.html; copiousoutput; + image/*; openfile %s ; + video/*; setsid mpv --quiet %s &; copiousoutput + audio/*; mpv %s ; + application/pdf; openfile %s ; + application/pgp-encrypted; gpg -d '%s'; copiousoutput; + application/pgp-keys; gpg --import '%s'; copiousoutput; + application/x-subrip; $EDITOR %s ; + ''; + }; + + programs.neomutt = { + enable = true; + sort = "reverse-date"; + sidebar = { + enable = true; + }; + extraConfig = '' + set use_threads=yes + set send_charset="us-ascii:utf-8" + set mailcap_path = $HOME/.config/neomutt/mailcap + set mime_type_query_command = "file --mime-type -b %s" + set date_format="%y/%m/%d %I:%M%p" + set index_format="%2C %Z %?X?A& ? %D %-15.15F %s (%-4.4c)" + set smtp_authenticators = 'gssapi:login' + set query_command = "echo %s | xargs khard email --parsable --" + set rfc2047_parameters = yes + set sleep_time = 0 # Pause 0 seconds for informational messages + set markers = no # Disables the `+` displayed at line wraps + set mark_old = no # Unread mail stay unread until read + set mime_forward = no # mail body is forwarded as text + set forward_attachments = yes # attachments are forwarded with mail + set wait_key = no # mutt won't ask "press key to continue" + set fast_reply # skip to compose when replying + set fcc_attach # save attachments with the body + set forward_format = "Fwd: %s" # format of subject when forwarding + set forward_quote # include message in forwards + set reverse_name # reply as whomever it was to + set include # include message in replies + set mail_check=0 # to avoid lags using IMAP with some email providers (yahoo for example) + auto_view text/html # automatically show html (mailcap uses lynx) + auto_view application/pgp-encrypted + #set display_filter = "tac | sed '/\\\[-- Autoview/,+1d' | tac" # Suppress autoview messages. + alternative_order text/plain text/enriched text/html + + set sidebar_visible = yes + set sidebar_width = 20 + set sidebar_short_path = yes + set sidebar_next_new_wrap = yes + set mail_check_stats + set sidebar_format = '%D%?F? [%F]?%* %?N?%N/? %?S?%S?' + bind index,pager \Co sidebar-open + bind index,pager \Cp sidebar-prev-new + bind index,pager \Cn sidebar-next-new + bind index,pager B sidebar-toggle-visible + + # Default index colors: + color index yellow default '.*' + color index_author red default '.*' + color index_number blue default + color index_subject cyan default '.*' + + # New mail is boldened: + color index brightyellow black "~N" + color index_author brightred black "~N" + color index_subject brightcyan black "~N" + + # Tagged mail is highlighted: + color index brightyellow blue "~T" + color index_author brightred blue "~T" + color index_subject brightcyan blue "~T" + + # Flagged mail is highlighted: + color index brightgreen default "~F" + color index_subject brightgreen default "~F" + color index_author brightgreen default "~F" + + # Other colors and aesthetic settings: + mono bold bold + mono underline underline + mono indicator reverse + mono error bold + color normal default default + color indicator brightblack white + color sidebar_highlight red default + color sidebar_divider brightblack black + color sidebar_flagged red black + color sidebar_new green black + color error red default + color tilde black default + color message cyan default + color markers red white + color attachment white default + color search brightmagenta default + color status brightyellow black + color hdrdefault brightgreen default + color quoted green default + color quoted1 blue default + color quoted2 cyan default + color quoted3 yellow default + color quoted4 red default + color quoted5 brightred default + color signature brightgreen default + color bold black default + color underline black default + + # Regex highlighting: + color header brightmagenta default "^From" + color header brightcyan default "^Subject" + color header brightwhite default "^(CC|BCC)" + color header blue default ".*" + color body brightred default "[\-\.+_a-zA-Z0-9]+@[\-\.a-zA-Z0-9]+" # Email addresses + color body brightblue default "(https?|ftp)://[\-\.,/%~_:?&=\#a-zA-Z0-9]+" # URL + color body green default "\`[^\`]*\`" # Green text between ` and ` + color body brightblue default "^# \.*" # Headings as bold blue + color body brightcyan default "^## \.*" # Subheadings as bold cyan + color body brightgreen default "^### \.*" # Subsubheadings as bold green + color body yellow default "^(\t| )*(-|\\*) \.*" # List items as yellow + color body brightcyan default "[;:][-o][)/(|]" # emoticons + color body brightcyan default "[;:][)(|]" # emoticons + color body brightcyan default "[ ][*][^*]*[*][ ]?" # more emoticon? + color body brightcyan default "[ ]?[*][^*]*[*][ ]" # more emoticon? + color body red default "(BAD signature)" + color body cyan default "(Good signature)" + color body brightblack default "^gpg: Good signature .*" + color body brightyellow default "^gpg: " + color body brightyellow red "^gpg: BAD signature from.*" + mono body bold "^gpg: Good signature" + mono body bold "^gpg: BAD signature from.*" + color body red default "([a-z][a-z0-9+-]*://(((([a-z0-9_.!~*'();:&=+$,-]|%[0-9a-f][0-9a-f])*@)?((([a-z0-9]([a-z0-9-]*[a-z0-9])?)\\.)*([a-z]([a-z0-9-]*[a-z0-9])?)\\.?|[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+)(:[0-9]+)?)|([a-z0-9_.!~*'()$,;:@&=+-]|%[0-9a-f][0-9a-f])+)(/([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*(;([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*)*(/([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*(;([a-z0-9_.!~*'():@&=+$,-]|%[0-9a-f][0-9a-f])*)*)*)?(\\?([a-z0-9_.!~*'();/?:@&=+$,-]|%[0-9a-f][0-9a-f])*)?(#([a-z0-9_.!~*'();/?:@&=+$,-]|%[0-9a-f][0-9a-f])*)?|(www|ftp)\\.(([a-z0-9]([a-z0-9-]*[a-z0-9])?)\\.)*([a-z]([a-z0-9-]*[a-z0-9])?)\\.?(:[0-9]+)?(/([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*(;([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*)*(/([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*(;([-a-z0-9_.!~*'():@&=+$,]|%[0-9a-f][0-9a-f])*)*)*)?(\\?([-a-z0-9_.!~*'();/?:@&=+$,]|%[0-9a-f][0-9a-f])*)?(#([-a-z0-9_.!~*'();/?:@&=+$,]|%[0-9a-f][0-9a-f])*)?)[^].,:;!)? \t\r\n<>\"]" + ''; + binds = [ + { map = ["index" "pager"]; key = "x"; action = "modify-labels"; } + { map = ["index" "pager"]; key = "i"; action = "noop"; } + { map = ["index" "pager"]; key = "g"; action = "noop"; } + { map = ["index"]; key = "\\Cf"; action = "noop"; } + { map = ["index" "pager"]; key = "M"; action = "noop"; } + { map = ["index" "pager"]; key = "C"; action = "noop"; } + { map = ["index"]; key = "gg"; action = "first-entry"; } + { map = ["index"]; key = "j"; action = "next-entry"; } + { map = ["index"]; key = "k"; action = "previous-entry"; } + { map = ["attach"]; key = "<return>"; action = "view-mailcap"; } + { map = ["attach"]; key = "l"; action = "view-mailcap"; } + { map = ["editor"]; key = "<space>"; action = "noop"; } + { map = ["index"]; key = "G"; action = "last-entry"; } + { map = ["pager" "attach"]; key = "h"; action = "exit"; } + { map = ["pager"]; key = "j"; action = "next-line"; } + { map = ["pager"]; key = "k"; action = "previous-line"; } + { map = ["pager"]; key = "l"; action = "view-attachments"; } + { map = ["index"]; key = "U"; action = "undelete-message"; } + { map = ["index"]; key = "L"; action = "limit"; } + { map = ["index"]; key = "h"; action = "noop"; } + { map = ["index"]; key = "l"; action = "display-message"; } + { map = ["index" "query"]; key = "<space>"; action = "tag-entry"; } + { map = ["index" "pager"]; key = "H"; action = "view-raw-message"; } + { map = ["browser"]; key = "l"; action = "select-entry"; } + { map = ["browser"]; key = "gg"; action = "top-page"; } + { map = ["browser"]; key = "G"; action = "bottom-page"; } + { map = ["pager"]; key = "gg"; action = "top"; } + { map = ["pager"]; key = "G"; action = "bottom"; } + { map = ["index" "pager" "browser"]; key = "d"; action = "half-down"; } + { map = ["index" "pager" "browser"]; key = "u"; action = "half-up"; } + { map = ["index" "pager"]; key = "\\Cr"; action = "group-reply"; } + { map = ["index" "pager"]; key = "R"; action = "group-chat-reply"; } + { map = ["index"]; key = "\031"; action = "previous-undeleted"; } + { map = ["index"]; key = "\005"; action = "next-undeleted"; } + { map = ["pager"]; key = "\031"; action = "previous-line"; } + { map = ["pager"]; key = "\005"; action = "next-line"; } + { map = ["editor"]; key = "<Tab>"; action = "complete-query"; } + { map = ["editor"]; key = "\\Ct"; action = "complete"; } + ]; + macros = [ + { map = ["index" "pager"]; key = ","; action = "<pipe-message>khard add-email<return>"; } + { map = ["index"]; key = "X"; action = "<save-message>=Spam<enter>y"; } + { map = ["index"]; key = "Ma"; action = "<save-message>=Archive<enter>y"; } + { map = ["index"]; key = "A"; action = "<modify-labels-then-hide>+archive -unread -inbox<enter><mark-message>z<enter><change-folder>^<enter>'z"; } + { map = ["index"]; key = "D"; action = "<delete-message>"; } + { map = ["index" "pager"]; key = "S"; action = "<sync-mailbox>!notmuch-hook<enter><mark-message>z<enter><change-folder>^<enter>'z"; } + { map = ["index"]; key = "hi"; action = "<change-folder>~/.local/share/mail/ivi/Inbox<enter><change-vfolder>Inbox<enter>"; } + { map = ["index"]; key = "hg"; action = "<change-folder>~/.local/share/mail/gmail/Inbox<enter><change-vfolder>Inbox<enter>"; } + { map = ["index"]; key = "hf"; action = "<change-folder>~/.local/share/mail/family/Inbox<enter><change-vfolder>Inbox<enter>"; } + { map = ["index"]; key = "\\\\"; action = "<vfolder-from-query>"; } + { map = ["browser"]; key = "h"; action = "<change-dir><kill-line>..<enter>"; } + { map = ["index"]; key = "\\Ck"; action = "<sidebar-prev><sidebar-open>"; } + { map = ["index"]; key = "\\Cj"; action = "<sidebar-next><sidebar-open>"; } + { map = ["index"]; key = "c"; action = "<change-folder>?/"; } + ]; + }; + }; +} diff --git a/profiles/email/notmuch.nix b/profiles/email/notmuch.nix new file mode 100644 index 0000000..304e573 --- /dev/null +++ b/profiles/email/notmuch.nix @@ -0,0 +1,22 @@ +{ + config, + ... +}: { + hm = { + programs.notmuch = { + enable = true; + new = { + tags = ["new"]; + ignore = [".mbsyncstate" ".uidvalidity"]; + }; + search.excludeTags = ["deleted" "spam"]; + maildir.synchronizeFlags = true; + extraConfig = { + database.path = "${config.hm.xdg.dataHome}/mail"; + user.name = "Mike Vink"; + user.primary_email = "mike1994vink@gmail.com"; + crypto.gpg_path="gpg"; + }; + }; + }; +} diff --git a/profiles/email/server.nix b/profiles/email/server.nix new file mode 100644 index 0000000..f95828f --- /dev/null +++ b/profiles/email/server.nix @@ -0,0 +1,57 @@ +{ + pkgs, + lib, + ... +}: with lib; { + hm = { + accounts.email = { + accounts = { + ${my.username} = { + realName = "${my.realName}"; + userName = "${my.email}"; + address = "${my.email}"; + passwordCommand = ["${pkgs.pass}/bin/pass" "personal/mailserver"]; + imap = { host = "${my.domain}"; port = 993; tls = { enable = true; }; }; + smtp = { host = "${my.domain}"; port = 587; tls = { enable = true; useStartTls = true; }; }; + msmtp = { + enable = true; + }; + neomutt = { + enable = true; + sendMailCommand = "msmtp -a ${my.username}"; + mailboxName = "=== ${my.username} ==="; + extraConfig = '' + set spoolfile='Inbox' + unvirtual-mailboxes * + ''; + }; + mbsync = { + enable = true; + create = "both"; remove = "both"; expunge = "both"; + groups = { + ${my.username} = { + channels = { + All = { patterns = ["*"]; extraConfig = { Create = "Both"; Expunge = "Both"; Remove = "Both"; }; }; + }; + }; + }; + }; + notmuch = { + enable = true; + neomutt = { + enable = true; + virtualMailboxes = [ + { name = "Inbox"; query = "folder:/${my.username}/ tag:inbox"; } + { name = "Sent"; query = "folder:/${my.username}/ tag:sent"; } + { name = "Archive"; query = "folder:/${my.username}/ tag:archive"; } + { name = "Drafts"; query = "folder:/${my.username}/ tag:drafts"; } + { name = "Junk"; query = "folder:/${my.username}/ tag:spam"; } + { name = "Trash"; query = "folder:/${my.username}/ tag:trash"; } + ]; + }; + }; + }; + }; + }; + }; +} diff --git a/profiles/graphical/suckless.nix b/profiles/graphical/suckless.nix new file mode 100644 index 0000000..5a51ebb --- /dev/null +++ b/profiles/graphical/suckless.nix @@ -0,0 +1,110 @@ +{ + self, + pkgs, + lib, + machine, + ... +}: +with lib; + mkIf (!machine.isDarwin) { + nixpkgs.overlays = [ + (import (self + "/overlays/suckless.nix") { + inherit pkgs; + home = self; + }) + ]; + services.xserver.enable = true; + services.xserver.displayManager.startx.enable = true; + services.libinput.enable = true; + hm = { + xsession = { + enable = true; + }; + services.picom = { + enable = true; + activeOpacity = 1; + inactiveOpacity = 0.7; + opacityRules = [ + "100:class_g = 'Wfica'" + "100:class_g = 'dwm'" + "100:class_g = 'Zathura'" + "100:name *= 'Firefox'" + "100:name *= 'mpv'" + "100:name *= 'LibreWolf'" + "100:name *= 'Steam'" + "100:name *= 'Risk of Rain'" + "100:name *= 'KVM'" + ]; + settings = { + inactive-opacity-override = false; + frame-opacity = 1; + }; + }; + services.dunst = { + enable = true; + settings = { + global = { + monitor = 0; + follow = "keyboard"; + width = 370; + height = 350; + offset = "0x19"; + padding = 2; + horizontal_padding = 2; + transparency = 0; + font = "Monospace 12"; + format = "<b>%s</b>\\n%b"; + }; + urgency_low = { + background = "#1d2021"; + foreground = "#928374"; + timeout = 3; + }; + urgency_normal = { + foreground = "#ebdbb2"; + background = "#458588"; + timeout = 5; + }; + urgency_critical = { + background = "#1cc24d"; + foreground = "#ebdbb2"; + frame_color = "#fabd2f"; + timeout = 10; + }; + }; + }; + home.packages = with pkgs; [ + libnotify + pywal + inotify-tools + + dwm + dwmblocks + sxiv + st + dmenu + tabbed + surfraw + surf + + # librewolf + ungoogled-chromium + xclip + xorg.xwininfo + xdotool + maim + asciinema + asciinema-agg + fontconfig + ]; + }; + fonts = { + fontconfig = { + enable = true; + }; + packages = with pkgs; [ + nerd-fonts.fira-code + nerd-fonts.jetbrains-mono + ]; + }; + } diff --git a/profiles/homeserver/acme.nix b/profiles/homeserver/acme.nix new file mode 100644 index 0000000..e72e8fe --- /dev/null +++ b/profiles/homeserver/acme.nix @@ -0,0 +1,16 @@ +{ config, lib, ... }: with lib; { + security.acme = { + acceptTerms = true; + defaults = { + extraLegoFlags = [ "--dns.disable-cp" ]; + extraLegoRunFlags = ["--preferred-chain" "ISRG Root X1"]; + email = my.email; + dnsProvider = "porkbun"; + environmentFile = config.secrets.porkbun.path; + }; + certs."${my.domain}" = { + # NOTE(ivi): use dns wildcard certs for local services + domain = "*.${my.domain}"; + }; + }; +} diff --git a/profiles/homeserver/dns.nix b/profiles/homeserver/dns.nix new file mode 100644 index 0000000..21ccf7e --- /dev/null +++ b/profiles/homeserver/dns.nix @@ -0,0 +1,66 @@ +{ config, machines, machine, inputs, lib, ... }: with lib; let + dns = inputs.dns.lib; + in { + system.extraDependencies = collectFlakeInputs inputs.dns; + networking.firewall.allowedTCPPorts = [ 53 ]; + networking.firewall.allowedUDPPorts = [ 53 ]; + services.unbound = { + enable = true; + localControlSocketPath = "/run/unbound/unbound.ctl"; + settings = { + server = { + tls-system-cert = true; + interface = [ + "0.0.0.0" "::" + ]; + do-not-query-localhost = false; + access-control = [ + "192.168.2.0/24 allow" + "100.0.0.0/8 allow" + ]; + }; + stub-zone = [ { + name = my.domain; + stub-addr = "127.0.0.1@10053"; + } ]; + forward-zone = [ + { + name = "_acme-challenge.${my.domain}"; + forward-addr = config.services.resolved.fallbackDns; + forward-tls-upstream = true; + } + { + name = "."; + forward-addr = config.services.resolved.fallbackDns; + forward-tls-upstream = true; + } ]; + }; + }; + networking.nameservers = [ "127.0.0.1" "::1" ]; + services.nsd = { + enable = true; + interfaces = ["127.0.0.1@10053"]; + ipTransparent = true; + ratelimit.enable = true; + + zones = with dns.combinators; let + here = { + A = map a machines.serber.ipv4; + AAAA = map a machines.serber.ipv6; + }; + in { + ${my.domain}.data = dns.toString my.domain (here // { + TTL = 60 * 60; + SOA = { + nameServer = "@"; + adminEmail = "dns@${my.domain}"; + serial = 0; + }; + NS = [ "@" ]; + subdomains = { + "*" = {A = map a machine.ipv4;}; + }; + }); + }; + }; +} diff --git a/profiles/homeserver/nginx.nix b/profiles/homeserver/nginx.nix new file mode 100644 index 0000000..22fd74e --- /dev/null +++ b/profiles/homeserver/nginx.nix @@ -0,0 +1,26 @@ +{ lib, ... }: with lib; { + # apparently you can set defaults on existing modules? + options.services.nginx.virtualHosts = mkOption { + type = types.attrsOf (types.submodule ({ name, ... }: { + config = mkIf (name != "default") { + forceSSL = mkDefault true; + sslCertificateKey = "/var/lib/acme/${my.domain}/key.pem"; + sslCertificate = "/var/lib/acme/${my.domain}/fullchain.pem"; + }; + })); + }; + config = { + services.nginx = { + enable = true; + enableReload = true; + recommendedTlsSettings = true; + recommendedProxySettings = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + }; + systemd.services.nginx.serviceConfig = { + SupplementaryGroups = [ "acme" ]; + }; + networking.firewall.allowedTCPPorts = [ 80 443 ]; + }; +} diff --git a/profiles/homeserver/radicale.nix b/profiles/homeserver/radicale.nix new file mode 100644 index 0000000..6f07245 --- /dev/null +++ b/profiles/homeserver/radicale.nix @@ -0,0 +1,13 @@ +{ lib, ... }: with lib; { + services.nginx = { + virtualHosts."cal.${my.domain}" = { + locations."/" = { + proxyPass = "http://127.0.0.1:5232"; + }; + }; + }; + services.radicale = { + enable = true; + settings.server.hosts = [ "0.0.0.0:5232" ]; + }; +} diff --git a/profiles/homeserver/tailscale.nix b/profiles/homeserver/tailscale.nix new file mode 100644 index 0000000..0fb821f --- /dev/null +++ b/profiles/homeserver/tailscale.nix @@ -0,0 +1,14 @@ +{ machine, config, pkgs, ... }: { + environment.systemPackages = [ pkgs.tailscale ]; + services.tailscale = { + enable = true; + useRoutingFeatures = "server"; + extraUpFlags = ["--advertise-exit-node" "--advertise-routes=${builtins.head machine.ipv4}/32"]; + authKeyFile = config.secrets.tailscale.path; + }; + + networking.firewall = { + trustedInterfaces = [ "tailscale0" ]; + allowedUDPPorts = [ config.services.tailscale.port ]; + }; +} diff --git a/profiles/homeserver/transmission.nix b/profiles/homeserver/transmission.nix new file mode 100644 index 0000000..8d047d7 --- /dev/null +++ b/profiles/homeserver/transmission.nix @@ -0,0 +1,154 @@ +{ + config, + lib, + ... +}: +with lib; let + multimediaUsernames = [ + "prowlarr" + "sonarr" + "radarr" + "bazarr" + "jellyfin" + "transmission" + ]; + mkMultimediaUsers = names: + mergeAttrsList (imap0 (i: name: { + ${name} = { + uid = 2007 + i; + isSystemUser = true; + group = name; + createHome = false; + }; + }) + names); + mkMultimediaGroups = names: mergeAttrsList (map (name: {${name} = {};}) names); +in { + virtualisation.docker.rootless = { + enable = true; + setSocketVariable = true; + }; + + users.groups = + { + multimedia = { + gid = 1994; + members = multimediaUsernames; + }; + } + // mkMultimediaGroups multimediaUsernames; + users.users = + { + ${my.username}.extraGroups = ["multimedia"]; + } + // mkMultimediaUsers multimediaUsernames; + + systemd.tmpfiles.rules = [ + "d /data 0770 - multimedia - -" + ]; + + services.nginx = { + virtualHosts = { + "sonarr.${my.domain}" = {locations."/" = {proxyPass = "http://127.0.0.1:8989";};}; + "radarr.${my.domain}" = {locations."/" = {proxyPass = "http://127.0.0.1:7878";};}; + "bazarr.${my.domain}" = {locations."/" = {proxyPass = "http://127.0.0.1:${toString config.services.bazarr.listenPort}";};}; + # "readarr.${my.domain}" = { locations."/" = { proxyPass = "http://127.0.0.1:8787"; }; }; + "prowlarr.${my.domain}" = {locations."/" = {proxyPass = "http://127.0.0.1:9696";};}; + "transmission.${my.domain}" = {locations."/" = {proxyPass = "http://127.0.0.1:9091";};}; + "jellyfin.${my.domain}" = {locations."/" = {proxyPass = "http://127.0.0.1:8096";};}; + }; + }; + # services = { + # jellyfin = { enable = true; group = "multimedia"; }; + # sonarr = { enable = true; group = "multimedia"; }; + # radarr = { enable = true; group = "multimedia"; }; + # bazarr = { enable = true; group = "multimedia"; }; + # readarr = { enable = true; group = "multimedia"; }; + # prowlarr = { enable = true; }; + # }; + + # TODO: use one shared data drive + virtualisation.oci-containers = { + backend = "docker"; + containers = { + prowlarr = { + image = "linuxserver/prowlarr"; + extraOptions = ["--net=host"]; + environment = { + PUID = "${toString config.users.users.prowlarr.uid}"; + PGID = "${toString config.users.groups.multimedia.gid}"; + }; + volumes = [ + # "/data/config/prowlarr/data:/config" + "/data:/data" + ]; + }; + bazarr = { + image = "linuxserver/bazarr"; + extraOptions = ["--net=host"]; + environment = { + PUID = "${toString config.users.users.bazarr.uid}"; + PGID = "${toString config.users.groups.multimedia.gid}"; + }; + volumes = [ + # "/data/media:/data" + # "/data/config/bazarr/data:/config" + "/data:/data" + ]; + }; + radarr = { + image = "linuxserver/radarr"; + extraOptions = ["--net=host"]; + environment = { + PUID = "${toString config.users.users.radarr.uid}"; + PGID = "${toString config.users.groups.multimedia.gid}"; + }; + volumes = [ + # "/data/config/radarr/data:/config" + "/data:/data" + ]; + }; + sonarr = { + image = "linuxserver/sonarr"; + extraOptions = ["--net=host"]; + environment = { + PUID = "${toString config.users.users.sonarr.uid}"; + PGID = "${toString config.users.groups.multimedia.gid}"; + }; + volumes = [ + # "/data/config/sonarr/data:/config" + "/data:/data" + ]; + }; + jellyfin = { + image = "jellyfin/jellyfin"; + extraOptions = ["--net=host"]; + user = "${toString config.users.users.jellyfin.uid}:${toString config.users.groups.multimedia.gid}"; + volumes = [ + # "/data/media:/media" + # "/data/config/jellyfin/config:/config" + # "/data/config/jellyfin/cache:/cache" + "/data:/data" + ]; + }; + transmission = { + image = "haugene/transmission-openvpn"; + extraOptions = ["--cap-add=NET_ADMIN" "--group-add=${toString config.users.groups.multimedia.gid}"]; + volumes = [ + # "/data/config/ovpn:/etc/openvpn/custom" + # "/data/config/transmission:/config" + # "/data/torrents:/data/torrents" + "/data:/data" + ]; + ports = [ + "9091:9091" + "5299:5299" + "8081:80" + ]; + environmentFiles = [ + config.secrets.transmission.path + ]; + }; + }; + }; +} diff --git a/profiles/netboot/system.nix b/profiles/netboot/system.nix new file mode 100644 index 0000000..b0e7945 --- /dev/null +++ b/profiles/netboot/system.nix @@ -0,0 +1,29 @@ +sys: { pkgs, lib, ... }: let + run-pixiecore = let + build = sys.config.system.build; + in pkgs.writeShellApplication { + name = "run-pixiecore"; + text = '' + sudo ${pkgs.pixiecore}/bin/pixiecore \ + boot kernel/bzImage initrd/initrd \ + --cmdline "init=init/init loglevel=4" \ + --debug --dhcp-no-bind \ + --port 64172 --status-port 64172 "$@" + ''; + }; + build-pixie = pkgs.writeShellApplication { + name = "build-pixie"; + text = '' + nix build /nix-config\#nixosConfigurations."$1".config.system.build.kernel --impure -o kernel + nix build /nix-config\#nixosConfigurations."$1".config.system.build.toplevel --impure -o init + nix build /nix-config\#nixosConfigurations."$1".config.system.build.netbootRamdisk --impure -o initrd + ''; + }; +in { + networking.firewall.allowedUDPPorts = [ 67 69 4011 ]; + networking.firewall.allowedTCPPorts = [ 64172 ]; + environment.systemPackages = [ + run-pixiecore + build-pixie + ]; +} diff --git a/profiles/server/acme.nix b/profiles/server/acme.nix new file mode 100644 index 0000000..a9fc594 --- /dev/null +++ b/profiles/server/acme.nix @@ -0,0 +1,11 @@ +{ config, lib, ... }: with lib; { + security.acme = { + acceptTerms = true; + defaults = { + extraLegoRunFlags = ["--preferred-chain" "ISRG Root X1"]; + email = my.email; + dnsProvider = "porkbun"; + credentialsFile = config.secrets.porkbun.path; + }; + }; +} diff --git a/profiles/server/mail.nix b/profiles/server/mail.nix new file mode 100644 index 0000000..7bf0a88 --- /dev/null +++ b/profiles/server/mail.nix @@ -0,0 +1,26 @@ +{ inputs, config, lib, ... }: with lib; { + imports = [ + inputs.simple-nixos-mailserver.nixosModule + ]; + + mailserver = { + enable = true; + enableImap = false; + enableSubmission = true; + enableImapSsl = true; + enableSubmissionSsl = true; + # TODO: configurate a local dns server? + + fqdn = my.domain; + domains = [ my.domain ]; + loginAccounts = { + ${my.email} = { + hashedPasswordFile = config.secrets.my.path; + aliases = [ "@${my.domain}" ]; + }; + }; + certificateScheme = "acme"; + + lmtpSaveToDetailMailbox = "no"; + }; +} diff --git a/profiles/server/nginx.nix b/profiles/server/nginx.nix new file mode 100644 index 0000000..dbabebd --- /dev/null +++ b/profiles/server/nginx.nix @@ -0,0 +1,25 @@ +{ lib, ... }: with lib; { + # apparently you can set defaults on existing modules? + options.services.nginx.virtualHosts = mkOption { + type = types.attrsOf (types.submodule ({ name, ... }: { + config = mkIf (name != "default") { + forceSSL = mkDefault true; + enableACME = mkDefault true; + }; + })); + }; + config = { + services.nginx = { + enable = true; + enableReload = true; + recommendedTlsSettings = true; + recommendedProxySettings = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + + virtualHosts."${my.domain}" = { + }; + }; + networking.firewall.allowedTCPPorts = [ 80 443 ]; + }; +} diff --git a/profiles/station/caldav.nix b/profiles/station/caldav.nix new file mode 100644 index 0000000..98674e4 --- /dev/null +++ b/profiles/station/caldav.nix @@ -0,0 +1,65 @@ +{ config, lib, pkgs, ... }: with lib; { + environment.systemPackages = with pkgs; [khard]; + hm = { + xdg.configFile."khard/khard.conf".text = '' + [addressbooks] + [[mike]] + path = ${config.hm.accounts.contact.accounts.mike.local.path}/contacts + + [general] + default_action=list + ''; + services.vdirsyncer.enable = true; + programs = { + vdirsyncer.enable = true; + khal.enable = true; + }; + accounts.calendar.basePath = "Cal"; + accounts.calendar.accounts = { + mike = { + primary = true; + primaryCollection = "tasks"; + local = { + type = "filesystem"; + fileExt = ".ics"; + }; + remote = { + type = "caldav"; + url = "https://cal.${my.domain}"; + userName = "mike"; + passwordCommand = ["echo" "''"]; + }; + vdirsyncer = { + enable = true; + collections = ["tasks" "pomp"]; + conflictResolution = "remote wins"; + }; + khal = { + enable = true; + type = "discover"; + color = "light green"; + }; + }; + }; + accounts.contact.basePath = "Cal"; + accounts.contact.accounts = { + mike = { + local = { + type = "filesystem"; + fileExt = ".vcf"; + }; + remote = { + type = "carddav"; + url = "https://cal.${my.domain}"; + userName = "mike"; + passwordCommand = ["echo" "''"]; + }; + vdirsyncer = { + enable = true; + collections = ["contacts"]; + conflictResolution = "remote wins"; + }; + }; + }; + }; +} diff --git a/profiles/station/codeium.nix b/profiles/station/codeium.nix new file mode 100644 index 0000000..f63a9b3 --- /dev/null +++ b/profiles/station/codeium.nix @@ -0,0 +1,58 @@ +{ + inputs, + config, + pkgs, + ... +}: let + codeium = with pkgs; stdenv.mkDerivation rec { + pname = "codeium"; + version = "1.1.39"; + + ls-sha = "c8fda9657259bb7f3d432c1b558db921db4257aa"; + + src = fetchurl { + url = "https://github.com/Exafunction/codeium/releases/download/language-server-v${version}/language_server_linux_x64.gz"; + sha256 = "sha256-LA1VVW4X30a8UD9aDUCTmBKVXM7G0WE7dSsZ73TaaVo="; + }; + + nativeBuildInputs = [ + autoPatchelfHook + ]; + + sourceRoot = "."; + + unpackPhase = '' + cp $src language_server_linux_x64.gz + gzip -d language_server_linux_x64.gz + ''; + + installPhase = '' + install -m755 -D language_server_linux_x64 $out + ''; + + preFixup = '' + patchelf \ + --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \ + $out + ''; + + meta = with lib; { + homepage = "https://www.codeium.com/"; + description = "Codeium language server"; + platforms = platforms.linux; + }; + }; +in { + # home.activation = { + # # links codeium into place + # codium-symlink = inputs.home-manager.lib.hm.dag.entryAfter ["writeBoundary"] '' + # CODEIUM_TARGET="${config.home.homeDirectory}/.codeium/bin/c8fda9657259bb7f3d432c1b558db921db4257aa" + # if [ -L $CODEIUM_TARGET ] && [ -e $CODEIUM_TARGET ]; then + # $DRY_RUN_CMD echo "codeium linked" + # else + # mkdir -p $CODEIUM_TARGET + # $DRY_RUN_CMD ln -sf ${codeium} "$CODEIUM_TARGET/language_server_linux_x64" + # fi + # ''; + # }; +} diff --git a/profiles/station/irc.nix b/profiles/station/irc.nix new file mode 100644 index 0000000..9feb293 --- /dev/null +++ b/profiles/station/irc.nix @@ -0,0 +1,27 @@ +{...}: { + hm.programs.tiny = { + enable = true; + settings = { + servers = [ + { + addr = "irc.libera.chat"; + port = 6697; + tls = true; + realname = "Mike Vink"; + nicks = [ "ivi-v" ]; + join = ["#nixos"]; + sasl = { + username = "ivi-v"; + password.command = "pass show personal/liberachat"; + }; + } + ]; + defaults = { + nicks = [ "ivi-v" ]; + realname = "Mike Vink"; + join = []; + tls = true; + }; + }; + }; +} diff --git a/profiles/station/k8s.nix b/profiles/station/k8s.nix new file mode 100644 index 0000000..bc2b8c3 --- /dev/null +++ b/profiles/station/k8s.nix @@ -0,0 +1,7 @@ +{ pkgs, ... }: { + environment.systemPackages = with pkgs; [ + kubernetes-helm + kubectl + kind + ]; +} diff --git a/profiles/station/mpv.nix b/profiles/station/mpv.nix new file mode 100644 index 0000000..46baf96 --- /dev/null +++ b/profiles/station/mpv.nix @@ -0,0 +1,48 @@ +{ + machine, + pkgs, + lib, + ... +}: lib.mkIf (!machine.isDarwin) { + hm = { + programs.mpv = { + enable = true; + scripts = [ + (with pkgs; stdenv.mkDerivation { + pname = "mpv-sockets"; + version = "1.0"; + + src = fetchFromGitHub { + owner = "wis"; + repo = "mpvSockets"; + rev = "be9b7ca84456466e54331bab59441ac207659c1c"; + sha256 = "sha256-tcY+cHvkQpVNohZ9yHpVlq0bU7iiKMxeUsO/BRwGzAs="; + }; + + # installFlags = [ "SCRIPTS_DIR=$(out)/share/mpv/scripts" ]; + passthru.scriptName = "mpvSockets.lua"; + installPhase = '' + install -m755 -D mpvSockets.lua $out/share/mpv/scripts/mpvSockets.lua + ''; + + meta = with lib; { + description = "mpvSockets lua module for mpv"; + homepage = "https://github.com/wis/mpvSockets"; + license = licenses.mit; + platforms = platforms.linux; + }; + }) + ]; + config = { + gpu-context = "drm"; + }; + bindings = { + l="seek 5"; + h="seek -5"; + j="seek -60"; + k="seek 60"; + S="cycle sub"; + }; + }; + }; +} diff --git a/profiles/station/music.nix b/profiles/station/music.nix new file mode 100644 index 0000000..fbb316a --- /dev/null +++ b/profiles/station/music.nix @@ -0,0 +1,206 @@ +{ + machine, + config, + pkgs, + lib, + ... +}: +with lib; + mkIf (!machine.isDarwin) { + # TODO: what about secrets on nix-darwin... + # secrets.mopidy.owner = lib.my.username; + hm.home.packages = [pkgs.mpc-cli]; + hm.services.mopidy = { + enable = true; + extensionPackages = with pkgs; [mopidy-spotify mopidy-mpd]; + settings = { + mpd = { + enabled = true; + hostname = "127.0.0.1"; + port = 6600; + max_connections = 20; + connection_timeout = 60; + }; + spotify = { + allow_cache = true; + cache_size = 0; + }; + }; + extraConfigFiles = [ + config.secrets.mopidy.path + ]; + }; + secrets.mopidy.owner = my.username; + + hm.programs.ncmpcpp = { + enable = true; + bindings = [ + { + key = "+"; + command = "show_clock"; + } + { + key = "+"; + command = "show_clock"; + } + { + key = "="; + command = "volume_up"; + } + { + key = "j"; + command = "scroll_down"; + } + { + key = "k"; + command = "scroll_up"; + } + { + key = "ctrl-u"; + command = "page_up"; + } + { + key = "ctrl-d"; + command = "page_down"; + } + { + key = "u"; + command = "page_up"; + } + { + key = "d"; + command = "page_down"; + } + { + key = "h"; + command = "previous_column"; + } + { + key = "l"; + command = "next_column"; + } + { + key = "."; + command = "show_lyrics"; + } + { + key = "n"; + command = "next_found_item"; + } + { + key = "N"; + command = "previous_found_item"; + } + { + key = "J"; + command = "move_sort_order_down"; + } + { + key = "K"; + command = "move_sort_order_up"; + } + { + key = "h"; + command = "jump_to_parent_directory"; + } + { + key = "l"; + command = "enter_directory"; + } + { + key = "l"; + command = "run_action"; + } + { + key = "l"; + command = "play_item"; + } + { + key = "m"; + command = "show_media_library"; + } + { + key = "m"; + command = "toggle_media_library_columns_mode"; + } + { + key = "t"; + command = "show_tag_editor"; + } + { + key = "v"; + command = "show_visualizer"; + } + { + key = "G"; + command = "move_end"; + } + { + key = "g"; + command = "move_home"; + } + { + key = "U"; + command = "update_database"; + } + { + key = "s"; + command = "reset_search_engine"; + } + { + key = "s"; + command = "show_search_engine"; + } + { + key = "f"; + command = "show_browser"; + } + { + key = "f"; + command = "change_browse_mode"; + } + { + key = "x"; + command = "delete_playlist_items"; + } + { + key = "P"; + command = "show_playlist"; + } + ]; + settings = { + ncmpcpp_directory = "~/.config/ncmpcpp"; + lyrics_directory = "~/.local/share/lyrics"; + message_delay_time = "1"; + # visualizer_type = "spectrum"; + song_list_format = "{$4%a - }{%t}|{$8%f$9}$R{$3(%l)$9}"; + song_status_format = ''$b{{$8"%t"}} $3by {$4%a{ $3in $7%b{ (%y)}} $3}|{$8%f}''; + song_library_format = "{%n - }{%t}|{%f}"; + alternative_header_first_line_format = "$b$1$aqqu$/a$9 {%t}|{%f} $1$atqq$/a$9$/b"; + alternative_header_second_line_format = "{{$4$b%a$/b$9}{ - $7%b$9}{ ($4%y$9)}}|{%D}"; + current_item_prefix = "$(cyan)$r$b"; + current_item_suffix = "$/r$(end)$/b"; + current_item_inactive_column_prefix = "$(magenta)$r"; + current_item_inactive_column_suffix = "$/r$(end)"; + playlist_display_mode = "columns"; + browser_display_mode = "columns"; + progressbar_look = "->"; + media_library_primary_tag = "album_artist"; + media_library_albums_split_by_date = "no"; + startup_screen = "media_library"; + display_volume_level = "no"; + ignore_leading_the = "yes"; + external_editor = "nvim"; + use_console_editor = "yes"; + empty_tag_color = "magenta"; + main_window_color = "white"; + progressbar_color = "black:b"; + progressbar_elapsed_color = "blue:b"; + statusbar_color = "red"; + statusbar_time_color = "cyan:b"; + execute_on_song_change = ''"pkill -RTMIN+11 dwmblocks"''; + execute_on_player_state_change = ''"pkill -RTMIN+11 dwmblocks"''; + mpd_connection_timeout = 60; + }; + }; + } diff --git a/profiles/station/newsboat.nix b/profiles/station/newsboat.nix new file mode 100644 index 0000000..2def1d7 --- /dev/null +++ b/profiles/station/newsboat.nix @@ -0,0 +1,86 @@ +{...}: { + hm = { + programs.newsboat = { + enable = true; + autoReload = true; + urls = [ + {url = "https://nginx.org/index.rss";} + {url = "https://github.com/neovim/neovim/releases.atom";} + {url = "https://github.com/rancher/rancher/releases.atom";} + {url = "https://github.com/istio/istio/releases.atom";} + {url = "https://github.com/argoproj/argo-cd/releases.atom";} + {url = "https://github.com/argoproj/argo-cd/releases.atom";} + {url = "https://github.com/kyverno/kyverno/releases.atom";} + {url = "https://github.com/hashicorp/terraform/releases.atom";} + {url = "https://github.com/ansible/ansible/releases.atom";} + {url = "https://github.com/ansible/awx/releases.atom";} + {url = "https://kubeshark.co/rss.xml";} + {url = "https://azurecomcdn.azureedge.net/en-us/updates/feed/?product=azure-devops";} + {url = "https://www.hashicorp.com/blog/categories/products-technology/feed.xml";} + {url = "https://kubernetes.io/feed.xml";} + {url = "https://www.cncf.io/rss";} + {url = "https://blog.alexellis.io/rss/";} + {url = "https://www.openfaas.com/feed";} + {url = "https://istio.io/latest/blog/feed.xml";} + {url = "https://www.youtube.com/feeds/videos.xml?channel_id=UCUyeluBRhGPCW4rPe_UvBZQ";} + ]; + extraConfig = '' + #show-read-feeds no + auto-reload yes + + external-url-viewer "urlscan -dc -r 'linkhandler {}'" + + bind-key j down + bind-key k up + bind-key j next articlelist + bind-key k prev articlelist + bind-key J next-feed articlelist + bind-key K prev-feed articlelist + bind-key G end + bind-key g home + bind-key d pagedown + bind-key u pageup + bind-key l open + bind-key h quit + bind-key a toggle-article-read + bind-key n next-unread + bind-key N prev-unread + bind-key D pb-download + bind-key U show-urls + bind-key x pb-delete + + color listnormal cyan default + color listfocus black yellow standout bold + color listnormal_unread blue default + color listfocus_unread yellow default bold + color info red black bold + color article white default bold + + browser linkhandler + macro , open-in-browser + macro t set browser "qndl" ; open-in-browser ; set browser linkhandler + macro a set browser "tsp yt-dlp --embed-metadata -xic -f bestaudio/best --restrict-filenames" ; open-in-browser ; set browser linkhandler + macro v set browser "setsid -f mpv" ; open-in-browser ; set browser linkhandler + macro w set browser "lynx" ; open-in-browser ; set browser linkhandler + macro d set browser "dmenuhandler" ; open-in-browser ; set browser linkhandler + macro c set browser "echo %u | xclip -r -sel c" ; open-in-browser ; set browser linkhandler + macro C set browser "youtube-viewer --comments=%u" ; open-in-browser ; set browser linkhandler + macro p set browser "peertubetorrent %u 480" ; open-in-browser ; set browser linkhandler + macro P set browser "peertubetorrent %u 1080" ; open-in-browser ; set browser linkhandler + + highlight all "---.*---" yellow + highlight feedlist ".*(0/0))" black + highlight article "(^Feed:.*|^Title:.*|^Author:.*)" cyan default bold + highlight article "(^Link:.*|^Date:.*)" default default + highlight article "https?://[^ ]+" green default + highlight article "^(Title):.*$" blue default + highlight article "\\[[0-9][0-9]*\\]" magenta default bold + highlight article "\\[image\\ [0-9]+\\]" green default bold + highlight article "\\[embedded flash: [0-9][0-9]*\\]" green default bold + highlight article ":.*\\(link\\)$" cyan default + highlight article ":.*\\(image\\)$" blue default + highlight article ":.*\\(embedded flash\\)$" magenta default + ''; + }; + }; +} diff --git a/profiles/station/nonfree.nix b/profiles/station/nonfree.nix new file mode 100644 index 0000000..f9d6506 --- /dev/null +++ b/profiles/station/nonfree.nix @@ -0,0 +1,27 @@ +{pkgs, lib, ...}: with lib; { + hm.home.packages = with pkgs; [ + discord + ]; + nixpkgs.config.allowUnfreePredicate = pkg: + builtins.elem (lib.getName pkg) [ + # Add additional package names here + "teams" + "discord" + "discord-ptb" + "discord-canary" + "slack" + "citrix-workspace" + "steam" + "steam-original" + "steam-run" + ]; + + programs = { + steam = { + enable = true; + remotePlay.openFirewall = true; + dedicatedServer.openFirewall = true; + }; + }; + # hardware.opengl.driSupport32Bit = true; +} diff --git a/profiles/station/packages.nix b/profiles/station/packages.nix new file mode 100644 index 0000000..0be6dd7 --- /dev/null +++ b/profiles/station/packages.nix @@ -0,0 +1,44 @@ +{ + machine, + pkgs, + lib, + ... +}: with lib; { + hm = { + home.packages = with pkgs; [ + python311Packages.editorconfig + calcurse + bashInteractive + powershell + + k9s + krew + azure-cli + argocd + (google-cloud-sdk.withExtraComponents (with google-cloud-sdk.components; [ + gke-gcloud-auth-plugin + ])) + imagemagick + poppler_utils + inkscape + ] ++ (optionals (!machine.isDarwin) [ + arduino-ide + arduino-cli + xdotool + pywal + dasel + inotify-tools + raylib + maim + profanity + lynx + sent + initool + dmenu + librewolf + firefox-wayland + libreoffice + xclip + ]); + }; +} diff --git a/profiles/station/virtualisation.nix b/profiles/station/virtualisation.nix new file mode 100644 index 0000000..440dc6e --- /dev/null +++ b/profiles/station/virtualisation.nix @@ -0,0 +1,14 @@ +{ pkgs, lib, ... }: with lib; { + environment.systemPackages = with pkgs; mkIf (!pkgs.stdenv.isDarwin) [ + virt-viewer + ]; + virtualisation.libvirtd.enable = true; + programs.virt-manager.enable = true; + hm.dconf.settings = { + "org/virt-manager/virt-manager/connections" = { + autoconnect = ["qemu:///system"]; + uris = ["qemu:///system"]; + }; + }; + my.extraGroups = [ "libvirtd" ]; +} diff --git a/profiles/station/zathura.nix b/profiles/station/zathura.nix new file mode 100644 index 0000000..73f7e3c --- /dev/null +++ b/profiles/station/zathura.nix @@ -0,0 +1,32 @@ +{ + flake, + config, + pkgs, + ... +}: { + hm = { + programs.zathura = { + enable = true; + extraConfig = '' + set sandbox none + set statusbar-h-padding 0 + set statusbar-v-padding 0 + set page-padding 1 + set selection-clipboard clipboard + map u scroll half-up + map d scroll half-down + map D toggle_page_mode + map r reload + map R rotate + map K zoom in + map J zoom out + map i recolor + map p print + map g goto top + + map m mark_add + map ' mark_evaluate + ''; + }; + }; +} diff --git a/profiles/vmware-guest.nix b/profiles/vmware-guest.nix new file mode 100644 index 0000000..5f1d7af --- /dev/null +++ b/profiles/vmware-guest.nix @@ -0,0 +1,95 @@ +# This is based on the official vmware-guest module, but modified +# for aarch64 to disable certain features and add support. I'm unsure +# how to upstream this because I just don't use certain features... maybe +# making them toggle-able? I'm not sure. +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.virtualisation.vmware.guest; + open-vm-tools = + if cfg.headless + then pkgs.open-vm-tools-headless + else pkgs.open-vm-tools; +in { + imports = [ + (mkRenamedOptionModule ["services" "vmwareGuest"] ["virtualisation" "vmware" "guest"]) + ]; + + options.virtualisation.vmware.guest = { + enable = mkEnableOption "VMWare Guest Support"; + headless = mkOption { + type = types.bool; + default = false; + description = "Whether to disable X11-related features."; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64 || pkgs.stdenv.isAarch64; + message = "VMWare guest is not currently supported on ${pkgs.stdenv.hostPlatform.system}"; + } + ]; + + boot.initrd.availableKernelModules = ["mptspi"]; + # boot.initrd.kernelModules = [ "vmw_pvscsi" ]; + + environment.systemPackages = [open-vm-tools]; + + systemd.services.vmware = { + description = "VMWare Guest Service"; + wantedBy = ["multi-user.target"]; + after = ["display-manager.service"]; + unitConfig.ConditionVirtualization = "vmware"; + serviceConfig.ExecStart = "${open-vm-tools}/bin/vmtoolsd"; + }; + + # Mount the vmblock for drag-and-drop and copy-and-paste. + systemd.mounts = [ + { + description = "VMware vmblock fuse mount"; + documentation = ["https://github.com/vmware/open-vm-tools/blob/master/open-vm-tools/vmblock-fuse/design.txt"]; + unitConfig.ConditionVirtualization = "vmware"; + what = "${open-vm-tools}/bin/vmware-vmblock-fuse"; + where = "/run/vmblock-fuse"; + type = "fuse"; + options = "subtype=vmware-vmblock,default_permissions,allow_other"; + wantedBy = ["multi-user.target"]; + } + ]; + + security.wrappers.vmware-user-suid-wrapper = { + setuid = true; + owner = "root"; + group = "root"; + source = "${open-vm-tools}/bin/vmware-user-suid-wrapper"; + }; + + environment.etc.vmware-tools.source = "${open-vm-tools}/etc/vmware-tools/*"; + + services.xserver = { + # TODO: does not build on aarch64 + # modules = [ xf86inputvmmouse ]; + + config = '' + Section "InputClass" + Identifier "VMMouse" + MatchDevicePath "/dev/input/event*" + MatchProduct "ImPS/2 Generic Wheel Mouse" + Driver "vmmouse" + EndSection + ''; + + displayManager.sessionCommands = '' + ${open-vm-tools}/bin/vmware-user-suid-wrapper + ''; + }; + + services.udev.packages = [open-vm-tools]; + }; +} diff --git a/secrets/lemptop/mopidy b/secrets/lemptop/mopidy new file mode 100644 index 0000000..75ba7bd --- /dev/null +++ b/secrets/lemptop/mopidy @@ -0,0 +1,20 @@ +{ + "data": "ENC[AES256_GCM,data:/Bf9QtdOgFI52DAwyUvJhWOusax3EFa1pL7d8R7okMLJbYdr7pvOwmGYUXPIr9nFJO/oHhqWNoimlU/eXK7OlS/FSSu5U1zmmYs0eNtia4V7Fn0blf+zV+b5jTAc4biLhmc9IfOswxaQjxvnN64LjXFYPuch+lkT/a0yaL4Bd4TfFgHHcayiVVX+xv2MCPeTFRoFLhUYJNYK1/IgbJel0bjP8fBpq2ilu+poXeEtU70n44NM,iv:SOgD2JZBlA1smoNUZy7DSRypHK5nXlRRV9JXlEcUCB4=,tag:RP0snciqGrcH8UIL+hqHTg==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByNDRIYkJrVUxZQ0FsTVB4\nc3oxNE5tQmhURHhldGtYNDFvUHZQWTFXZ0FzCmRKbHBvdEVXMTE0djdsNlk5b29R\nMVZ6N3FGVzVodjhzYVZUcnptQWhMZHMKLS0tIHVkNjRKMW9zMnZYSVJ4WkVsazdG\nN3daL1NJY3lzMnpnc2FDanh1N0ZyU00KqJ+hlgKYPJtP9dLT+BbrSN8F+nSfO4/n\nJxAVuMj7ErettV1i4qNuuUnczMXvvwvvw91j0rzHig42It1Cmr7Jsg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2023-10-17T23:20:47Z", + "mac": "ENC[AES256_GCM,data:QsWDDzQmn4hJM2lzhgMjIGlye0o8tJLOAo0cxkWg8Ltzvz7fiXvBID9mRO3hIxbiGlCn6zhbUf8jzq00URIt0Vqx5EqBaCU4kZEbCxjUqpY8iCzYhW9OQZ/tHxY6FjIONN4zD8eVbESHeWWnQGRTG3HovQeXKYavVtR0ZAj9cI4=,iv:FFyL2vsXfzSspa1rI2d+TbEzYY3LmJBeB1koippmamg=,tag:jxNqZn5iQqtmTIdmKDFAZQ==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.8.0" + } +}
\ No newline at end of file diff --git a/secrets/porkbun b/secrets/porkbun new file mode 100644 index 0000000..d21f11d --- /dev/null +++ b/secrets/porkbun @@ -0,0 +1,28 @@ +{ + "data": "ENC[AES256_GCM,data:FTPzhFADwDaHzKpJ5fqMkRwkvFMPLm4aGWAGVzcqsNJv9bPeKK/WTSbthAW8XDZ6XeIc+W43R2S85ewcuPKDTlUlrHK7h6qJhyKpmDTn7nc3YkTImjKxCTqifVKYJBrCqTj3GqzL613gLzQgWxypwzBI3WM5xEKb6Jls7OqezPMIrfWOPI9EzBBoOfyonAUrJjTunl9rL1qu+rsoAJ56OhYPTzLpYzaOYv04xKR3+GGS,iv:D0eRsFsDWPnqzh1yOu/iGF1noAYT7KpccYnmuJN2pEU=,tag:jX5N/BsrPPw2GyjOTPuHmw==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvVk1tczJNTUI4Y1lJcFJz\ncTdYRmNyQk8yeWcwM3VTamJ1RWNlVWNxWFJJCkZQenp4WDhDZTdKNkNPVW5XWlVR\nSHRISWJoZExCaWNONjNacHl2K2pWckkKLS0tIG93M2J0ZjRxWGJtZjg0d25SZWxE\ncFdnQ0RicDgwSlNtUWVJQVQvSjh0RHMK2V/MG+0dppLnuk+IMe0hzaIZu/2jKjzN\nW6rl6PYgQ5szCrZC8iPa5LZZxNCKsmNAOR98J/yFSC3TU6x4RFFu+Q==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoL0tZZjFjN0d0VHl4Z0lX\nV1RRSmRSNEtOK3NVWDB2TzI1SXZpR2dVc25jCmNrR25PQ1JaNG5IQnlXY0V5c0tl\nY0doNzZiVkgzcmsyUTFuTFd3dmpVL00KLS0tIEdxY3JGenIzenNIeHZYb1cyaDRY\nNHoreGxxMEpQS09JNmhmdmpsaU1nYzQKgJi/yxe4gaVu6fM2JMkKE8TgF/vbJ4Nr\nmUORdA23xXVlwZO6iE+T/REnmSrnBnOtFgMklNLH921MkUxhkPR3vQ==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1tzsvgxaxwvh4874d977fk0z7ghm4mqpm0c80vhxft87dv46p5uesq7mk42", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqZmpqSERsd3QybmVSY290\nWUUyNkpSUU9UeWFOaGkzK3gwRUI0azVERW1FCmQzSXVZUU1xM1VReHFVK0VxTFFW\nTlpsNkE3dVZrakdnSWFaajRlbzlEUUkKLS0tIFlBVGM0Z3BVaFhGdUUrZVl0UnA0\naGRCUUpwSHg5TmRoa2w5aHlIY25naUkKUlCfzm3WYG0Ua50i1nnjRgzVcMxWrz14\nvaTqnC1SW+/0kbK3HWBQZyrYaUgOYY1zIJGjErvQ1qZB9HlnIef0ZA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2024-01-09T00:22:54Z", + "mac": "ENC[AES256_GCM,data:B+Bsoq+VahzcZM/0s7OGGNql3+b+8JT2fvPaps9f8sNQO/Vs8rsR20xIhlqFP0XCE9mkt5CQLz8A7z5hhMYW2cE+hNJdAAToEcFLgx3q9y3YAuGZc2fsb3dPL1revEHPdZg46wqi2bQomIhXUMBEZED5yEnzNto5MmoqZV3D6Yw=,iv:VHrU2KrTsV+8xH9RiQZqf8ynZTvnRZylLIvi/YTnUJU=,tag:N0MVEe/n3s/YPIQHNaetuw==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.8.1" + } +}
\ No newline at end of file diff --git a/secrets/root.yaml b/secrets/root.yaml new file mode 100644 index 0000000..cb40a7d --- /dev/null +++ b/secrets/root.yaml @@ -0,0 +1,43 @@ +serber: ENC[AES256_GCM,data:YJLm1K1eW7QPFN5t3j1ni+J5m9hZemDBMHy/1X8CcMfoMPn/OJDpN4Hyz0CvdblxDNrHHCYGhDPJjZIt,iv:5j1/9sthguwv7a6JD/7OwbKB+jaj+E+ezA0/TiHHsSw=,tag:x690F9djFbnvtGbXeOFytQ==,type:str] +lemptop: ENC[AES256_GCM,data:Ga7/9T9r2yPui30iGDN0XJ8kGYkBz4AILHMHpTo0kuT2DQiMoW0cVypABZK84hnVZcooATWpNHNoiFGs,iv:YcZEmRGeHg6RZmPpJueLlf2VznAenP5e40D7DHsKiOc=,tag:I57ssbo2CBIGLfnLlG25Ig==,type:str] +vm-aarch64: ENC[AES256_GCM,data:icELFMMOdg8VHq3Lcq8WJ9OV/ps5a94ZL4SVSo8lni7DmSXmmSv9lywIjQ0ZQ7nFIVKUY21tT3x4H4Yu,iv:NsEZ5HXEvlHyr+uSgd0ieWfKewWRJ6o7Xl6KOFGbniM=,tag:VlzgnZyt9HwBD/OKvqTMcw==,type:str] +cal: ENC[AES256_GCM,data:FV9wdQ4IXvQe+KaqdVyaWkrhQu5lpeWkH5Zcz2isY/nrxWF/yAj8hNdXbzwvyzxQ7P3nd90kxSh5+BU5,iv:/bs7ERZucexZff/VJoDj5S3ANrVHwsDA9uO/Jr+NsmA=,tag:Y7OtAiflkpM3kLnKye2Wjw==,type:str] +pump: ENC[AES256_GCM,data:u66iqqcBBXkrM6Qz88HD5XydOg/D3p33ewKHjmF6zOm9ej0ZWVPU35Kh9yMFb7hAPDw8ORvsXBT5Hgvr,iv:f60wnqabPaiq3o2rh9lce+/2Y6YLR4QIFriTgCMG3H0=,tag:PuQsRAUv96MW41xZeR0m0Q==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOd0xmbldJZ00yOW4wZnNr + RlNUY09qYmdzNW9TSnlqQTlqTjYwNURQbFdZCjZmbkVtc2NnWDhtNnZPT0c0Q1do + clFvR3ZBc2doLytGZE4wckVybXJUbDQKLS0tIEtSRXJxNlFGKzQ2RWxXZGNEazQx + OWVYc0dvaFRJWTRwMkJnVytxc3ljd0kKVvgQlHF/ajIv430u1FPHnh+qct7Rsclp + vEIs/R01Hj9IeNkW+MdNT7SIO3TlbDM9rGs10Y+tzB18GlusgC0hsw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0alhMK3RSQ1BzdEhXSDEz + V3Vob2wvWHIza28xNTMyNVY4UGJ5cmUvcXdFCitKTkZzcXdMdE5HZ1pIK2xxc3A0 + K202bVBlQUNXVmxyL2VqZnd5elRRaFUKLS0tIFRkV1Yrd0JvL3kvYkFtYXEvWmpt + RDk2TGVsOEpoV3JxTEFrTWZCZENNMzAKA3zRgeq5ocuO+q/atXgtn1ijdRQ0eNNe + IXkw+v3mJgTq9ukgkgmgcbjZvBqKVKm9tDML8XhoBtcCAKFINuH3Bw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1tzsvgxaxwvh4874d977fk0z7ghm4mqpm0c80vhxft87dv46p5uesq7mk42 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEWVFaajJTa0FBZnk3VFhj + anpwaWxaazFtenlrdjRCNmNZeUZtSWowZkM0Ck9rR1c3SS9BOXBoZmY1TERVS1Mw + dWRHS3VKWWprNTZZZURzSFRUUkNpd2sKLS0tIGEwVDcweXpRb1dwR3dGODhhTEpr + MEk2L1dEMnNOVWtsZmZPeEpYd3lNOEUKgd61tQUwe9bMItjEKzQ3LPoNphM/aYV+ + gf2yIwMCHXzbY1B5e3zsFyVR1W9x2DhqkXDoehCKoDpFoOz+R8b7Kw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-06-20T07:38:46Z" + mac: ENC[AES256_GCM,data:ucCoVJuKwfwWOmeLkAY3X3b092tWwFJzh/YUAVoz6/9S8n1foctIiP+Z61E2FlIY9Psad/tUxS7EA+WqnmZFO329dTutYWxfiDyqTv+/ZRra5ADFMmn3YmFgi52Yc2vr7xgCmFXD/s1hXCcNyhdcnMoj5z1BaM7RSv4c13HZqlA=,iv:S7ujEndKsA+MFKIuGzHoTm9P8lsO07d5gl7Y3BmoNi0=,tag:544CPDv0t/ZtWdCyB9fg9w==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.8.1 diff --git a/secrets/sabnzb b/secrets/sabnzb new file mode 100644 index 0000000..66326c6 --- /dev/null +++ b/secrets/sabnzb @@ -0,0 +1,28 @@ +{ + "data": "ENC[AES256_GCM,data:z2vDmKMT6bJOu7IEUtA87I/ytceAIE3GqeRfw+ETEEQxbiTOQgMQSAubstwKmuJyohiipMAcn1mzOTui6eP7PrXLG9mB57yF/QutZ8rQIyhC61qGEA9g6sGUvG9ONDf58zuV9qhWiBtYbRYxy5utwiuoCIzbX5SrtYo+HMhniS8LqrPKk+snfGK/KNcPvgWr758uMD5vtgP/8JpOrWaAXQm9MAn3YATb/x87T/Hie+Uac75eIRgSJ22iLEl1cDN5Z/5B+nZUCFmm/PHKRORNisdP6p7+2tieq+v0Kasi20pIz9g+tws2x1HFphxc/58hn+s5UsWTLae+VP/4MvBSUeuzalFR1pTKd3CwkIFiDE10tQLtYJmb2ZSAdWLzL9FD4B9PSxCa+a8iH4ES8tzLgZlE1Srzp4YEVd4MDWkP1v44YBHPXoJ2ba5wAFjR09wuDzJ4xUoyn4WlYhFu,iv:v4mO+KvlU8mpIj/LWV+K00MSybJqyvRhYCZlRPwzeLw=,tag:8OD4/MhCnH10L9oWW8IuVQ==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtRkw0SytGZTVYSGpmN2tj\nS1BXU3lnVHlYMFI4TnhReGtGdXZaM25pRGk4ClhNWDhGSHB6Yk9kL25oYlFvaHg1\nQVNtR000akVzdXBEZkR3cTQ4aFlxU1UKLS0tIFNlejdFSkdVTnMxRkJGZUU1ejY2\nZ05GUVFaSzErTGE0MVk4OEdsVEg3QmMK6aiT9L6gSQmA6qLiGWQJ12EZ55/ocVWI\nqsEd7YhgbYrUParORvThuL82yf07L7Rti1sXy9EqWIpMrJkbh/ONTg==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6R0hzY2M1Y3JCd1lqOGp4\nay9aNktXL2IyeHNmZkhESHp2c3JZRTYwVFY4CjE4bDRsbmFTQXpLVG8wT0p1UFRp\nc0ltQ1NIdzlvaUtWY0xDVzZrcFhKclEKLS0tIDVJMGRtK09OZXREenU1VnNOYWxs\ndktYd2hkMGo4U2k3R3pnbkNlREFuTG8KyT0ZWtnqUKFEiLYTFaIHcHTi+My9aZ6B\n6DHbpR+GwtU16W58rusJ1o/l7L8OffZwxL5EuakCIA36DA75GNDIiA==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1tzsvgxaxwvh4874d977fk0z7ghm4mqpm0c80vhxft87dv46p5uesq7mk42", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqWHlUSjdXKzlBN3NHNkZ6\nQnRVbEN4Q0NPeVJhYzIvNTAxKzNUUUw4QlNRCmFBc2FvYTNiYjJJY3M2RG9qcllU\nVG9uOThYTFc1S3ZzUjAvZDNpOWhNMFkKLS0tIEkrV1pqQjFtVHlLTzNZMk1ZVTA5\nMmk4Um5KbXVXZkRIZ20xZU5uNmdtdmcK1Sl/LOiW15mW8DMZPw0nIPrqsSv5NOGs\nVtGDI5DLKVMAejd8dk+fM/RuG1dm1Ao8Qs+2of6FbCYF37KAwg4qug==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2024-01-11T00:28:27Z", + "mac": "ENC[AES256_GCM,data:Lh1VY0l0JpJ5PfjZ3a1OKyBYZ/93g8kbJaOUIF2v2tJSTYCQu0z4mAdmKVv37/BmdfHJ+zc8DM9NEuniLqoRgNkOYQWGU/ze79Dy9ZEdObPD4l4xr2p5naSIuOVgPsdf1pnKHOM1rYN29LvknOvcuzocThDRCY9urCvtOFw4v4E=,iv:HYkz5DILMdAtsuarBZqh5QCfldvOhXqH7Oqltrio680=,tag:UEuaU1o9j6Zmb61p4S3r7Q==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.8.1" + } +}
\ No newline at end of file diff --git a/secrets/serber/ivi b/secrets/serber/ivi new file mode 100644 index 0000000..2692d0c --- /dev/null +++ b/secrets/serber/ivi @@ -0,0 +1,24 @@ +{ + "data": "ENC[AES256_GCM,data:/NAB+9AtvXxWu3Rsf6zWWCS2HyO3XK7moOPhtrr28E7WVwTO/UCI1yuBqYg3REbH0onWZraZ1jp1Tp2dUw==,iv:Mt6hIoCqQWrbmwOSdFxhjjk+OVY4P9wkwXoNnF0mOQY=,tag:skPV7thTBk7YAarL5fvCXg==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGamJFMm5xZ3NCTFI3c1pC\nMnQxUnhBTUhpMEVUa2dhMnFBVXB5clJxU1Z3Cm16a3hPMEhVeXRoZHQydjZXRjZC\nZjdKYndwRlZ1cWZYaFNJa3VKVTBpNDQKLS0tIFBaUS9pYVRxSUo2YUhpcGthb0ZQ\nV3QyQTZCWCtZSnljeTdYQnpBdXE1YncK8hSuccbmqav9p4SzRIJXHqfxMon+1/YS\n4bWoK6v+75iatFrl9s6p+Z2Z7k8adFLVNoETXvpMHNZXQxCa/hg1Kg==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjYkZOT1lTQTNqZG5odXpL\nbTJyWXBrclRqZ2FqeldZQTZiSHhhbWYrTld3Cm5kVlczcEZ5TEFQMzZlQnoxa0hL\nR0JJcUdxeThUWFhyVE5XSjV1VnNMUkkKLS0tIDNCUHdGa2hvSkF0S3VZTy9vZzFh\nM0VjRUdHSEdwZmF4ZXRBMlZicDc1eG8KgqOoSiW5LnJrg7GkcDSiXCKsS7NejTpl\nhodvS9OdiV/T5xhkhzFQXj7rgTbVw3wAk22LvymwHKBD5DWpN16Vyg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2023-10-19T19:27:04Z", + "mac": "ENC[AES256_GCM,data:XCZ7NlOGUHO835ETBioMOHoLDS/xWrpu0VXOhp9GuCMXOFiquPM3D/oXK5UeFmLbvOfCTVAXoid2dPAilxeWGaZ+uouBtRWZDRzNuw/lbiBeshf+9wSrU4W59edpDN41l1dB0KNVV0vTI2UVXw9NrRDF1YBJLBwpbsy8UpycZQ8=,iv:dii8q2Tdhj5cUPD0Q2pk+3uXJoWJeOlNnF0QQumfxM8=,tag:Gf8KEorRPAMknglwcdeDyg==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.8.0" + } +}
\ No newline at end of file diff --git a/secrets/syncthing.yaml b/secrets/syncthing.yaml new file mode 100644 index 0000000..d19240c --- /dev/null +++ b/secrets/syncthing.yaml @@ -0,0 +1,43 @@ +lemptop: ENC[AES256_GCM,data:3dmcPh8EtBYe2KQQ1HMddLey5Qdhtz7kGvMFZaqidMZ099ycd+EnXrHsJIRHoWFrGsRbBs6vgWytKX49JBcrl5im8u7Jw6AbFtCh81XOau8+EaKD+Z+uynRhbJ31y+AH5MTGIniM+7RviGUDeBM8oZAvtazbaiswckFkR8HrJ8WcGOi2xkq+HY/OIqTnBpy83Q7A0oD6YPfNjvFJUB0LJFU/mYfrbmADEkobeKQz57sHc22scjrfszWmxcgcjrriuqRReucClU9uQ3GO8bEMvWFT7epjZAkwht7Oq1K2U17kt6xsrqTWRPNwQsB3P2w7i5YQMBnGAtz0b9VC5hH8GyZJGBFLRkk0fzxhUL9SXRid2wvTKrCAoMnjWTCw8K2D,iv:ojRT/RzCcxQlGh2FFz5tdUYOq4bekGcmE8Hm9tUSrDg=,tag:jd/g0vpTCOmf2EdQCcpcZQ==,type:str] +pump: ENC[AES256_GCM,data:2Vu1idorw/kMsDThT2ywGmdPMgQdDHQItpZRukpdiapcKxMa65U/AQzshkbuQVTN5AaDkMNnLQrrLt8qQY0QxhTpddc4+y1kLaVAE5G+8di/2GJiGKUAjHOwyX72BXqjkAYOZ6u96PThOs3PmyhHhiH5ge9ZpOh1zOG2CD4dzoMLHHPHgSv8NLuhZ3kuc3yE3a/YgMgs9NjCvL44Pks8ktVq9DZAJfJB+eRGJPA9k6sN1NP1vMW9RKnk6dI+ZwOz1OHnQvfyVqe/vJxG96m4ALq4oeqn003+me72GB4DO9GLx2IkAsK0Jw9ZoiiJDSfEMVGzhH348mZXfAsTTb2coN9+834V5tBIT9OVDx+cJfHF7+7sm1FHH+fkzbteSH4q,iv:2IY08X5IYjGPEEZYqB/Sa8B1GOkURQg8nqgRwgTJs5c=,tag:ey3TMSDpt5xuEB9eH1ylOw==,type:str] +work: ENC[AES256_GCM,data:Kfw00ljs0JUEMET3Ii+pQwdNAe7A49oZUB+f5+rKU/doKqW5KC5T4vRV+AY2xIle6Gz2qQI4tN9ffdFZnKS6HvS/aoSnPwSrZo9VYyyBFlhcEwqfdhtzspu+oDkz6EQtqOxZAqzKP5mEPN5YRT0FWTWT99oYtXEHtuG7h80ivZbnY2gjQgkGGieq/c2TDVotS6Av/ycUd5ZQrd9iNXgeuHuQbfLF7/xhOZweYgcDuTqcGNaPdz4y/TRWQa05VkhkcByvHZ+6fG8SkZ7RjUuRsAC5D6ErJqqQmRznOZ6E6RElLWZdkIr2ahXtdU8t7VCDsInA8ua15V2vTEcVNoNYRFjDCAx3lbgO0pelHUno1bwXah6YFEPCMqlieSOMtT3p,iv:jsPrGHem6Qq87/ePRjGLcPWfAqWcy13yNCuZjN2I8pw=,tag:ED7trfDcmuIB/ljyqPMB8Q==,type:str] +vm-aarch64: ENC[AES256_GCM,data:ft+KvxDX3N2E4mj6kyOtEjXNnYaG+jm2dVJ8wx0QO8vX8wgdvWTvA4477LlXbE20jFIqrVsEggQQliJl5i1i8CxHrS+Io3k0HS1ECSRMlB/BTk04ZeCH4GNohJD42oD85xhEkAj7IJV+LhadetQoFKWVTKzAblKfitvKX26mxHrGtzimvrk6Mx65VGFNaBomVmlVn0l4fUpbmCJQKkqsQklN3axsHzsUsx2S8hc+x1QeFm3m3lxU/hlJlDZMf2scEXHRI7Bs/HUrbXc/xlbJ1a/JNv+ym9QGzpZ2MeTrBnfTP7rM2h111YNhPz/ZQlfb1EFUIr2EeaTaPXCl7wZI/oEQpv4L00k4jTDSFxrKevzL7Kgjm3eMdRXwXAPUJlco,iv:mkpZyeqAmZZsH5M+MN1IGDt6zXG9F2++G7a4a1JdJ/Q=,tag:fZYauE9H/QlZz62Lnupisw==,type:str] +serber: ENC[AES256_GCM,data:iQTU+w==,iv:FbnGkujV72nsFIk74CerwT7dxmHPQNuSMFx4vesR5gQ=,tag:xCsJYRRYwhD5r5kf2buOTw==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHU0hJNjJ4cVAvSGpjZkRa + aGt2TEpOaWhSazM4dWwrKzljcUtVdDBqUVFjCmVQcm5OSWJzSG9ibmp3U1lYdGVv + SFJML0p6UTRBR2Vncng2dlJ0eGcrencKLS0tIHZFMmt4M1hoYTZEdjZzMU9FeWFF + Nk5wWnlrZEZzYnVEbzlFcHNKSkdWbUEKN+daKhXayvBqMbSFnGzTwhaM9ux1x4zd + LUN4ocbYWmCcJ2CQZS5X4eJzuN9GKcw2eIrbf6SKgexDAvZWgAbOdA== + -----END AGE ENCRYPTED FILE----- + - recipient: age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSeXBMTE84U0RndFhLMkZ0 + a1R4ZDdrcGxQc0JQM0Z1Z2JucGxpM0tRZEFZCjVSaTRNbnY3M3VXR2wrdjdUc3pI + Vi9yWkJZenE1clNQdzdKZ2k2aFFUU0EKLS0tIGxZSTlxRWs4d2FHbWgzeERHYmVz + Rlo3aUZRN0J4QzZvK2E2SUcxVWF6R1UKzyCkTzozx1H/9jyLhlTBda5zb9sCFv28 + xz22HqPQKJNwoj6dzHlrfLYyursBFQsnIGmNThoKn7v2RXp5xe2RQg== + -----END AGE ENCRYPTED FILE----- + - recipient: age1tzsvgxaxwvh4874d977fk0z7ghm4mqpm0c80vhxft87dv46p5uesq7mk42 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsTjRSQ1ZGQkN3QzZoN25Z + Vjg5WlRJakhsSnZEYkthU3R4WUZRMEdacVZVCjdLcEY5QXhNS29yckQvOFlUWlR5 + cmM3dHR4ZllqMGhiTGUvUTk2V0sydlEKLS0tIDBGaTRlT0h6ZHc5MSt4ZERickE4 + WUUxdCtNbHhtbWo0NGloY1NSMHFEYzAKZG5k7qu7N4SyUogiO+qDQIoEXcT2B4zQ + L7bA4NDUJBFNfekX6R/VWTuOdPHHIZkcbjEj79iEbFSo4DBeSOatRw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2024-06-26T08:21:16Z" + mac: ENC[AES256_GCM,data:ihtuC/CsMopUj4TbWNO3jIEgWY2Dfm0X0nLdwf2OOIOC9HCFvLZEVd0SxnIq1tSsmzUjvKZIVPFpFgN6nQjM7bO9VBwC4dCSlZogp3HxLl9iMjcGcwVO0xmz4NN7qYZyz2QXaTIMl4/0/cgv/gLcm7CL8eg/EaKIzwtp89AlksQ=,iv:z1vV0xwU+2ecz3pv4KBULSjWuX+eIFmypGXdbjzFM5s=,tag:sIzRpLx+sIKgIItc/tUa+w==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.8.1 diff --git a/secrets/tailscale b/secrets/tailscale new file mode 100644 index 0000000..d807b3c --- /dev/null +++ b/secrets/tailscale @@ -0,0 +1,28 @@ +{ + "data": "ENC[AES256_GCM,data:BhalEDjLu/jyr++gq0OxcZkpbBB42Yn1/rCaI74ECyQdx1rc19c5m54m94KfkLXZ1KXOMe9M7/uPg/6cgNoe,iv:TT4zsEgPv64gOjLBBSawIJxkbBeAYr9uGevNryoGFkc=,tag:g3T2bCJwWwP846vPyKJZvA==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEVVBpemUyOS83OTczeEta\nSjBtMk9oOU1PTzFkSXVpSjdDQURtaWlDNG40CkFTWTRDZlJwOWJnaTI0STFGanhC\nNUVXRGgyTFdVMmZmZzU1THpUTzIwVjgKLS0tIHpYYWwvZmFuNG9LckxtUmRtVGFQ\nSTZSZnJYMzI5eVAwYTRSVTczZ056c2sKTS5nL2/rA3IrhjTLu8wlQZJYKliglFLo\n7nxiLkuM+WI7XKNDY0yp15vn41H4p53DgmWpfmLVYxzY1uFReDBxPw==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCTnV0R2Q5bE9JLzlIZXhI\nVVJBYnFMWTdNOGNhUkpZUWp5WEpUYnV5N2lZCkxucVRldGdZcnpXczE4cnY2Znhp\nVVZSQmxSMGZiWG5DdzNIM0dJc1NnSDgKLS0tIGZJTTdCNE5HRWdFZWJtUlN1ZnRn\nUVFhRWQvZEV5ZHByVkdUQkc2Y2FtL1kKYqpsvYckyfVn5y6Gq7urhbgbK2gSvPl9\nuFPSb9DxK+uhYg/fyAiYngscVqHdtX1fbrR4LN3/klru3i0n9pzJdw==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1tzsvgxaxwvh4874d977fk0z7ghm4mqpm0c80vhxft87dv46p5uesq7mk42", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTZk0zbGl3cE5JTldVaGU1\nZ1AwMVpod2swejVsVDFjM3VQVEtqRjM1VmdrCnFsRzM1aDVhZE00YnFQTnozNG5h\ndGZYNk1WUU5pdDZkSWZXYVpPekM1clkKLS0tIHZ2SThXMjFTdzhicjE5SE5qd3o4\nZFZPQytCaE9QTUtLaHFHZHZRSkN6cGcKS4+8Y7maCbuwOtjQKc/M4l3w/L6M1ZVB\nFVClHX5Ru6IlfPlfZhjIiWV1IqKExsIJewmAxP9EFMw2y5ex0Qxj9A==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2024-07-14T09:35:40Z", + "mac": "ENC[AES256_GCM,data:r4vM54o3s+7a6BNOPIiSi7cFViJaumQNT2vLSHGYfncZGgOpFu9tfhK3gVSrrn32uLHpYt9o8AShgoU9FfUZAudnaJu+c2cmXMmUElcOF7W5jcUM643caI7F/sKgPOnTDmxhpt2F3ecQ3dm59akHqfdQpX9497WR4/LJpVOcZQc=,iv:sIgdKy+B6do8bp8mkbKydaee/PFPt6vpbQOuOIC09UM=,tag:9pvtVi0Y5MDX1UJP2sllUA==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.9.0" + } +}
\ No newline at end of file diff --git a/secrets/transmission b/secrets/transmission new file mode 100644 index 0000000..6016952 --- /dev/null +++ b/secrets/transmission @@ -0,0 +1,28 @@ +{ + "data": "ENC[AES256_GCM,data:t8jcfNoO4czch8M2qOV5GiRv2YuCfmDKsYA8X1vB3R1AFzkn0nQmLFPp/bi5hEmXrW56+ZGVYlndvZl6nOyVYPOfel0ApQaKKvNu/gVmLlAYPZ8QDFoUAf81aWjyi5TPFIjb7R01CuGCPJ8Ye8dR5HJQmmXsVpk2eCKiYGVnefihC57RgNqVX5DfT9pvrNmfh/3e+Gf+B5DandbXVzi3k6F+2H/JnMQayNhlMJJgjya7Ty4mhjfdX+GVhY0GEMY04sRFrIEf9kQXpjCJcMiwBKQTCX67Dy++QS0pjXA5a/K6g7WIcu7Hax5Gzn7W4S/urFOTyjEeqTczuwZoellXe5AeCTAQPtflkVOAlrvjnGPPHf276wK+9G8=,iv:MWfCbUdCPcwN4n6mUXXmbfbrReme4NjBpzhw5NqxOXs=,tag:0OODcrBRJ3pQxTr0Iji3DA==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10q9wse8dh0749ffj576q775q496pycucxlla9rjdq5rd7f4csyhqqrmkk0", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1S0hRMVA0cTI3QTcwbUJE\ndGdUYUtlQjZKQkJpOGdTUk5WYVgzTGdZRjFFCmw0eTVLaXY3SXZWeEhuK3IrVmNi\nY2RwbEpSVng3SDd1amZsdXlZdy9mbzgKLS0tIHZDTEppeGRGa1dISVQxZlM4d2Iw\nQkRPVWwrdWdpUUpXRDV0Uk5yZWxTbmsKSfQgFfbtqTyhPhoMOYHzObalLAvRhluT\nnMwqYB8A8PXYrVujSbmfJVTPpmCjEm9hLk5Q2wZBEfhGMjhiSdfJ/Q==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1sjqz54u07jv9ykpmg6s5fqms2jqyxzdwf7q940veapqzuafzr5es9nnl2v", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLQ3NuT2V4b0tNTStKYTR0\nL2dWTFQ1N3lzcktrMy9JeElCanNTcjU2OVNZCnVQWjBMYjNvbHVMd2dLZUdiRWpk\nVHVtZXBqU3AvREM3ZzB3UW5POWpkQk0KLS0tIHFvKy9oRkI4ZFVlTENLK1Brd2ZT\na3FZZUM1VEd3c1ZJK2xXMXRPM1FmSFEKNqpyLR68SgZZd1Yn+89oWEeCsslNxHYH\nXUDImp5gln4bPJw8nzyDdpEtKFutD5CRnUEGJELNY2FyIIJyuqF+Sw==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1tzsvgxaxwvh4874d977fk0z7ghm4mqpm0c80vhxft87dv46p5uesq7mk42", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFUVFaRDE4U2psU1E1Z3BM\nYXYyaVEvK3pOYk1MbUN4cWZHWllYZW9pQVRrCldUL3dlQVUyaFJyWUNiQmhKQzNM\nMHNPejJJdlVwNk40eWZNdkd5eVVCcmcKLS0tIEtBUUFpWUFGK0dscUJLcGk5Vm1a\nVDBpYjlCK096cGZBYmdoa09xb0x2VGMKUdJpHVXVxl4YysnTwt6mIw28rveDWyKI\n22FLXv5jUsCOYxUSYa5tEPGRhecmc7BXMK24Nnea8yagSBYvzW/Ufg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2024-01-14T13:50:21Z", + "mac": "ENC[AES256_GCM,data:GVzsw98Zh4zJbghAv2qq134PLsw50Gc4AHxeVERDHK8VlHW4BixN6Qy1Dm+/KUKYDyqpdoSVRxe7b/UudY2qNnVSq8AtKQ3WjrAY3+IaeVxvsIjUbT+VudDgJvMIcd/I/ACKwlz9G1BbSR9aaPJC+bm/OsGwX96uX/Ct5rU7chQ=,iv:aW90dTO6SO6ldcfsu283J+47jyNyhrEEopiuYRpLqrU=,tag:ZkI5YyOrFWVMKuinhoY0tA==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.8.1" + } +}
\ No newline at end of file diff --git a/templates/ansible/.envrc b/templates/ansible/.envrc new file mode 100644 index 0000000..1305de8 --- /dev/null +++ b/templates/ansible/.envrc @@ -0,0 +1,4 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc=" +fi +use flake diff --git a/templates/ansible/flake.nix b/templates/ansible/flake.nix new file mode 100644 index 0000000..df49972 --- /dev/null +++ b/templates/ansible/flake.nix @@ -0,0 +1,39 @@ +{ + inputs = { + nixpkgs.url = "nixpkgs"; + nix-filter.url = "github:numtide/nix-filter"; + flake-utils.url = "github:numtide/flake-utils"; + }; + outputs = { + self, + flake-utils, + ... + } @ inputs: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = + import inputs.nixpkgs + { + inherit system; + }; + in { + devShells.default = pkgs.mkShell { + name = "dev"; + buildInputs = [ + pkgs.ansible-language-server + pkgs.bashInteractive + ]; + shellHook = '' + [[ -f ./.venv/bin/activate ]] && { + source ./.venv/bin/activate + source ~/awx-login.sh + # NOTE(mike): this is necessary to make ansible-lint work with + # playbooks that use: + #vars_files: + # - ./secrets/vault.yaml + initool s ansible.cfg defaults vault_identity devena | initool s - defaults vault_password_file ~/pass-ansible-vault-client > /tmp/ansible.cfg + cp /tmp/ansible.cfg ansible.cfg + } + ''; + }; + }); +} diff --git a/templates/ansible/pyproject.toml b/templates/ansible/pyproject.toml new file mode 100644 index 0000000..037c592 --- /dev/null +++ b/templates/ansible/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "dev-env" +version = "0.1.0" +description = "Virtualenv" +authors = ["Mike Vink"] +readme = "README.md" +packages = [] + +[tool.poetry.dependencies] +python = "^3.10" +awxkit = "^22.0.0" +ansible-lint = {version="^6.14.0", markers = "platform_system != 'Windows'"} + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/templates/go/.envrc b/templates/go/.envrc new file mode 100644 index 0000000..1305de8 --- /dev/null +++ b/templates/go/.envrc @@ -0,0 +1,4 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc=" +fi +use flake diff --git a/templates/go/flake.nix b/templates/go/flake.nix new file mode 100644 index 0000000..d99ce9a --- /dev/null +++ b/templates/go/flake.nix @@ -0,0 +1,28 @@ +{ + inputs = { + nixpkgs.url = "nixpkgs"; + nix-filter.url = "github:numtide/nix-filter"; + flake-utils.url = "github:numtide/flake-utils"; + }; + outputs = { + self, + flake-utils, + ... + } @ inputs: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = + import inputs.nixpkgs + { + inherit system; + }; + in { + devShells.default = pkgs.mkShell { + name = "dev"; + buildInputs = with pkgs; [ + go + gotools + gofumpt + ]; + }; + }); +} diff --git a/templates/rust/.envrc b/templates/rust/.envrc new file mode 100644 index 0000000..1305de8 --- /dev/null +++ b/templates/rust/.envrc @@ -0,0 +1,4 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc=" +fi +use flake diff --git a/templates/rust/flake.nix b/templates/rust/flake.nix new file mode 100644 index 0000000..4196ad9 --- /dev/null +++ b/templates/rust/flake.nix @@ -0,0 +1,48 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; + imports = [ + inputs.treefmt-nix.flakeModule + ]; + perSystem = { config, self', pkgs, lib, system, ... }: + let + nonRustDeps = [ + pkgs.libiconv + ]; + in + { + # Rust dev environment + devShells.default = pkgs.mkShell { + inputsFrom = [ + config.treefmt.build.devShell + ]; + buildInputs = nonRustDeps; + nativeBuildInputs = with pkgs; [ + just + rustc + cargo + cargo-watch + rust-analyzer + ]; + }; + + # Add your auto-formatters here. + # cf. https://numtide.github.io/treefmt/ + treefmt.config = { + projectRootFile = "flake.nix"; + programs = { + nixpkgs-fmt.enable = true; + rustfmt.enable = true; + }; + }; + }; + }; +} |
