diff options
63 files changed, 3103 insertions, 1262 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 1ba2978c..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,22 +0,0 @@ -<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED --> - -<!-- Check all that apply [x] --> - -- [ ] I have read through the manual page (`man fzf`) -- [ ] I have the latest version of fzf -- [ ] I have searched through the existing issues - -## Info - -- OS - - [ ] Linux - - [ ] Mac OS X - - [ ] Windows - - [ ] Etc. -- Shell - - [ ] bash - - [ ] zsh - - [ ] fish - -## Problem / Steps to reproduce - diff --git a/.github/ISSUE_TEMPLATE/issue_template.yml b/.github/ISSUE_TEMPLATE/issue_template.yml new file mode 100644 index 00000000..06e857b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_template.yml @@ -0,0 +1,49 @@ +--- +name: Issue Template +description: Report a problem or bug related to fzf to help us improve + +body: + - type: markdown + attributes: + value: ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED + + - type: checkboxes + attributes: + label: Checklist + options: + - label: I have read through the manual page (`man fzf`) + required: true + - label: I have searched through the existing issues + required: true + - label: For bug reports, I have checked if the bug is reproducible in the latest version of fzf + required: false + + - type: input + attributes: + label: Output of `fzf --version` + placeholder: e.g. 0.48.1 (d579e33) + validations: + required: true + + - type: checkboxes + attributes: + label: OS + options: + - label: Linux + - label: macOS + - label: Windows + - label: Etc. + + - type: checkboxes + attributes: + label: Shell + options: + - label: bash + - label: zsh + - label: fish + + - type: textarea + attributes: + label: Problem / Steps to reproduce + validations: + required: true diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index c6c2a976..aecf02ee 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version: "1.20" - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 5925f921..cbdcb537 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.18 + go-version: "1.20" - name: Setup Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 752c58c6..f3d67d22 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -7,4 +7,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: crate-ci/typos@v1.19.0 + - uses: crate-ci/typos@v1.20.10 diff --git a/.goreleaser.yml b/.goreleaser.yml index 7f670da2..cf130d75 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -12,6 +12,8 @@ builds: - darwin goarch: - amd64 + flags: + - -trimpath ldflags: - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" hooks: @@ -19,11 +21,7 @@ builds: sh -c ' cat > /tmp/fzf-gon-amd64.hcl << EOF source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"] - bundle_id = "kr.junegunn.fzf" - apple_id { - username = "junegunn.c@gmail.com" - password = "@env:AC_PASSWORD" - } + bundle_id = "junegunn.fzf" sign { application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" } @@ -40,6 +38,8 @@ builds: - darwin goarch: - arm64 + flags: + - -trimpath ldflags: - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" hooks: @@ -47,11 +47,7 @@ builds: sh -c ' cat > /tmp/fzf-gon-arm64.hcl << EOF source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"] - bundle_id = "kr.junegunn.fzf" - apple_id { - username = "junegunn.c@gmail.com" - password = "@env:AC_PASSWORD" - } + bundle_id = "junegunn.fzf" sign { application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" } @@ -79,6 +75,8 @@ builds: - 5 - 6 - 7 + flags: + - -trimpath ldflags: - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" ignore: @@ -6,7 +6,7 @@ Build instructions ### Prerequisites -- Go 1.18 or above +- Go 1.20 or above ### Using Makefile @@ -24,13 +24,23 @@ make build make release ``` -> :warning: Makefile uses git commands to determine the version and the -> revision information for `fzf --version`. So if you're building fzf from an +> [!WARNING] +> Makefile uses git commands to determine the version and the revision +> information for `fzf --version`. So if you're building fzf from an > environment where its git information is not available, you have to manually > set `$FZF_VERSION` and `$FZF_REVISION`. > > e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make` +> [!TIP] +> To build fzf with profiling options enabled, set `TAGS=pprof` +> +> ```sh +> TAGS=pprof make clean install +> fzf --profile-cpu /tmp/cpu.pprof --profile-mem /tmp/mem.pprof \ +> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof +> ``` + Third-party libraries used -------------------------- diff --git a/CHANGELOG.md b/CHANGELOG.md index f902833e..fd594864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,118 @@ CHANGELOG ========= +0.51.0 +------ +- Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1. + ```sh + # Toggle selection to the top or to the bottom + seq 30 | fzf --multi --bind 'load:pos(10)' \ + --bind 'shift-up:transform:for _ in $(seq $FZF_POS $FZF_MATCH_COUNT); do echo -n +toggle+up; done' \ + --bind 'shift-down:transform:for _ in $(seq 1 $FZF_POS); do echo -n +toggle+down; done' + ``` +- Added `--with-shell` option to start child processes with a custom shell command and flags + ```sh + gem list | fzf --with-shell 'ruby -e' \ + --preview 'pp Gem::Specification.find_by_name({1})' \ + --bind 'ctrl-o:execute-silent: + spec = Gem::Specification.find_by_name({1}) + [spec.homepage, *spec.metadata.filter { _1.end_with?("uri") }.values].uniq.each do + system "open", _1 + end + ' + ``` +- Added `change-multi` action for dynamically changing `--multi` option + - `change-multi` - enable multi-select mode with no limit + - `change-multi(NUM)` - enable multi-select mode with a limit + - `change-multi(0)` - disable multi-select mode +- Windows improvements + - `become` action is now supported on Windows + - Unlike in *nix, this does not use `execve(2)`. Instead it spawns a new process and waits for it to finish, so the exact behavior may differ. + - Fixed argument escaping for Windows cmd.exe. No redundant escaping of backslashes. +- Bug fixes and improvements + +0.50.0 +------ +- Search performance optimization. You can observe 50%+ improvement in some scenarios. + ``` + $ rg --line-number --no-heading --smart-case . > $DATA + + $ wc < $DATA + 5520118 26862362 897487793 + + $ hyperfine -w 1 -L bin fzf-0.49.0,fzf-7ce6452,fzf-a5447b8,fzf '{bin} --filter "///" < $DATA | head -30' + Summary + fzf --filter "///" < $DATA | head -30 ran + 1.16 ± 0.03 times faster than fzf-a5447b8 --filter "///" < $DATA | head -30 + 1.23 ± 0.03 times faster than fzf-7ce6452 --filter "///" < $DATA | head -30 + 1.52 ± 0.03 times faster than fzf-0.49.0 --filter "///" < $DATA | head -30 + ``` +- Added `jump` and `jump-cancel` events that are triggered when leaving `jump` mode + ```sh + # Default behavior + fzf --bind space:jump + + # Same as jump-accept action + fzf --bind space:jump,jump:accept + + # Accept on jump, abort on cancel + fzf --bind space:jump,jump:accept,jump-cancel:abort + + # Change header on jump-cancel + fzf --bind 'space:change-header(Type jump label)+jump,jump-cancel:change-header:Jump cancelled' + ``` +- Added a new environment variable `$FZF_KEY` exported to the child processes. It's the name of the last key pressed. + ```sh + fzf --bind 'space:jump,jump:accept,jump-cancel:transform:[[ $FZF_KEY =~ ctrl-c ]] && echo abort' + ``` +- fzf can be built with profiling options. See [BUILD.md](BUILD.md) for more information. +- Bug fixes + +0.49.0 +------ +- Ingestion performance improved by around 40% (more or less depending on options) +- `--info=hidden` and `--info=inline-right` will no longer hide the horizontal separator by default. This gives you more flexibility in customizing the layout. + ```sh + fzf --border --info=inline-right + fzf --border --info=inline-right --separator ═ + fzf --border --info=inline-right --no-separator + fzf --border --info=hidden + fzf --border --info=hidden --separator ━ + fzf --border --info=hidden --no-separator + ``` +- Added two environment variables exported to the child processes + - `FZF_PREVIEW_LABEL` + - `FZF_BORDER_LABEL` + ```sh + # Use the current value of $FZF_PREVIEW_LABEL to determine which actions to perform + git ls-files | + fzf --header 'Press CTRL-P to change preview mode' \ + --bind='ctrl-p:transform:[[ $FZF_PREVIEW_LABEL =~ cat ]] \ + && echo "change-preview(git log --color=always \{})+change-preview-label([[ log ]])" \ + || echo "change-preview(bat --color=always \{})+change-preview-label([[ cat ]])"' + ``` +- Renamed `track` action to `track-current` to highlight the difference between the global tracking state set by `--track` and a one-off tracking action + - `track` is still available as an alias +- Added `untrack-current` and `toggle-track-current` actions + - `*-current` actions are no-op when the global tracking state is set +- Bug fixes and minor improvements + +0.48.1 +------ +- CTRL-T and ALT-C bindings can be disabled by setting `FZF_CTRL_T_COMMAND` and `FZF_ALT_C_COMMAND` to empty strings respectively when sourcing the script + ```sh + # bash + FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --bash)" + + # zsh + FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --zsh)" + + # fish + fzf --fish | FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= source + ``` + - Setting the variables after sourcing the script will have no effect +- Bug fixes + 0.48.0 ------ - Shell integration scripts are now embedded in the fzf binary. This simplifies the distribution, and the users are less likely to have problems caused by using incompatible scripts and binaries. @@ -1,6 +1,6 @@ -FROM --platform=linux/amd64 ubuntu:22.04 +FROM ubuntu:24.04 RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux -RUN gem install --no-document -v 5.14.2 minitest +RUN gem install --no-document -v 5.22.3 minitest RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. ~/.bashrc' >> ~/.bash_profile @@ -1,10 +1,10 @@ SHELL := bash GO ?= go -GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version)))) +GOOS ?= $(shell $(GO) env GOOS) MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) ROOT_DIR := $(shell dirname $(MAKEFILE)) -SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE) +SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE) ifdef FZF_VERSION VERSION := $(FZF_VERSION) @@ -25,7 +25,7 @@ endif ifeq ($(REVISION),) $(error Not on git repository; cannot determine $$FZF_REVISION) endif -BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" +BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" -trimpath BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 @@ -79,6 +79,7 @@ all: target/$(BINARY) test: $(SOURCES) [ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1) SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \ + github.com/junegunn/fzf \ github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src/algo \ github.com/junegunn/fzf/src/tui \ @@ -173,12 +174,12 @@ bin/fzf: target/$(BINARY) | bin cp -f target/$(BINARY) bin/fzf docker: - docker build -t fzf-arch . - docker run -it fzf-arch tmux + docker build -t fzf-ubuntu . + docker run -it fzf-ubuntu tmux docker-test: - docker build -t fzf-arch . - docker run -it fzf-arch + docker build -t fzf-ubuntu . + docker run -it fzf-ubuntu update: $(GO) get -u @@ -44,7 +44,7 @@ I would like to thank all the sponsors of this project who make it possible for If you'd like to sponsor this project, please visit https://github.com/sponsors/junegunn. -<!-- sponsors --><a href="https://github.com/miyanokomiya"><img src="https://github.com/miyanokomiya.png" width="60px" alt="miyanokomiya" /></a><a href="https://github.com/jonhoo"><img src="https://github.com/jonhoo.png" width="60px" alt="Jon Gjengset" /></a><a href="https://github.com/AceofSpades5757"><img src="https://github.com/AceofSpades5757.png" width="60px" alt="Kyle L. Davis" /></a><a href="https://github.com/Frederick888"><img src="https://github.com/Frederick888.png" width="60px" alt="Frederick Zhang" /></a><a href="https://github.com/moritzdietz"><img src="https://github.com/moritzdietz.png" width="60px" alt="Moritz Dietz" /></a><a href="https://github.com/mikker"><img src="https://github.com/mikker.png" width="60px" alt="Mikkel Malmberg" /></a><a href="https://github.com/pldubouilh"><img src="https://github.com/pldubouilh.png" width="60px" alt="Pierre Dubouilh" /></a><a href="https://github.com/rcorre"><img src="https://github.com/rcorre.png" width="60px" alt="Ryan Roden-Corrent" /></a><a href="https://github.com/blissdev"><img src="https://github.com/blissdev.png" width="60px" alt="Jordan Arentsen" /></a><a href="https://github.com/mislav"><img src="https://github.com/mislav.png" width="60px" alt="Mislav Marohnić" /></a><a href="https://github.com/aexvir"><img src="https://github.com/aexvir.png" width="60px" alt="Alex Viscreanu" /></a><a href="https://github.com/dbalatero"><img src="https://github.com/dbalatero.png" width="60px" alt="David Balatero" /></a><a href="https://github.com/comatory"><img src="https://github.com/comatory.png" width="60px" alt="Ondrej Synacek" /></a><a href="https://github.com/moobar"><img src="https://github.com/moobar.png" width="60px" alt="" /></a><a href="https://github.com/majjoha"><img src="https://github.com/majjoha.png" width="60px" alt="Mathias Jean Johansen" /></a><a href="https://github.com/benelan"><img src="https://github.com/benelan.png" width="60px" alt="Ben Elan" /></a><a href="https://github.com/pawelduda"><img src="https://github.com/pawelduda.png" width="60px" alt="Paweł Duda" /></a><a href="https://github.com/slezica"><img src="https://github.com/slezica.png" width="60px" alt="Santiago Lezica" /></a><a href="https://github.com/pbwn"><img src="https://github.com/pbwn.png" width="60px" alt="" /></a><a href="https://github.com/timgluz"><img src="https://github.com/timgluz.png" width="60px" alt="Timo Sulg" /></a><a href="https://github.com/pyrho"><img src="https://github.com/pyrho.png" width="60px" alt="Damien Rajon" /></a><a href="https://github.com/ArtBIT"><img src="https://github.com/ArtBIT.png" width="60px" alt="ArtBIT" /></a><a href="https://github.com/da-moon"><img src="https://github.com/da-moon.png" width="60px" alt="" /></a><a href="https://github.com/hovissimo"><img src="https://github.com/hovissimo.png" width="60px" alt="Hovis" /></a><a href="https://github.com/dariusjonda"><img src="https://github.com/dariusjonda.png" width="60px" alt="Darius Jonda" /></a><a href="https://github.com/cristiand391"><img src="https://github.com/cristiand391.png" width="60px" alt="Cristian Dominguez" /></a><a href="https://github.com/eliangcs"><img src="https://github.com/eliangcs.png" width="60px" alt="Chang-Hung Liang" /></a><a href="https://github.com/asphaltbuffet"><img src="https://github.com/asphaltbuffet.png" width="60px" alt="Ben Lechlitner" /></a><a href="https://github.com/yash1th"><img src="https://github.com/yash1th.png" width="60px" alt="yash" /></a><a href="https://github.com/looshch"><img src="https://github.com/looshch.png" width="60px" alt="george looshch" /></a><a href="https://github.com/kg8m"><img src="https://github.com/kg8m.png" width="60px" alt="Takumi KAGIYAMA" /></a><a href="https://github.com/polm"><img src="https://github.com/polm.png" width="60px" alt="Paul O'Leary McCann" /></a><a href="https://github.com/rbeeger"><img src="https://github.com/rbeeger.png" width="60px" alt="Robert Beeger" /></a><a href="https://github.com/veebch"><img src="https://github.com/veebch.png" width="60px" alt="VEEB Projects" /></a><a href="https://github.com/khuedoan"><img src="https://github.com/khuedoan.png" width="60px" alt="Khue Doan" /></a><a href="https://github.com/yowayb"><img src="https://github.com/yowayb.png" width="60px" alt="Yoway Buorn" /></a><a href="https://github.com/scalisi"><img src="https://github.com/scalisi.png" width="60px" alt="Josh Scalisi" /></a><a href="https://github.com/alecbcs"><img src="https://github.com/alecbcs.png" width="60px" alt="Alec Scott" /></a><a href="https://github.com/thnxdev"><img src="https://github.com/thnxdev.png" width="60px" alt="thanks.dev" /></a><a href="https://github.com/artursapek"><img src="https://github.com/artursapek.png" width="60px" alt="Artur Sapek" /></a><a href="https://github.com/ramnes"><img src="https://github.com/ramnes.png" width="60px" alt="Guillaume Gelin" /></a><a href="https://github.com/jyc"><img src="https://github.com/jyc.png" width="60px" alt="" /></a><a href="https://github.com/mrcnski"><img src="https://github.com/mrcnski.png" width="60px" alt="Marcin S." /></a><a href="https://github.com/roblevy"><img src="https://github.com/roblevy.png" width="60px" alt="Rob Levy" /></a><a href="https://github.com/michael-dwan"><img src="https://github.com/michael-dwan.png" width="60px" alt="Michael Dwan" /></a><a href="https://github.com/glozow"><img src="https://github.com/glozow.png" width="60px" alt="Gloria Zhao" /></a><a href="https://github.com/pooriajr"><img src="https://github.com/pooriajr.png" width="60px" alt="Pooria" /></a><a href="https://github.com/wjt"><img src="https://github.com/wjt.png" width="60px" alt="Will Thompson" /></a><a href="https://github.com/faruzzy"><img src="https://github.com/faruzzy.png" width="60px" alt="Roland" /></a><!-- sponsors --> +<!-- sponsors --><a href="https://github.com/miyanokomiya"><img src="https://github.com/miyanokomiya.png" width="60px" alt="miyanokomiya" /></a><a href="https://github.com/jonhoo"><img src="https://github.com/jonhoo.png" width="60px" alt="Jon Gjengset" /></a><a href="https://github.com/AceofSpades5757"><img src="https://github.com/AceofSpades5757.png" width="60px" alt="Kyle L. Davis" /></a><a href="https://github.com/Frederick888"><img src="https://github.com/Frederick888.png" width="60px" alt="Frederick Zhang" /></a><a href="https://github.com/moritzdietz"><img src="https://github.com/moritzdietz.png" width="60px" alt="Moritz Dietz" /></a><a href="https://github.com/mikker"><img src="https://github.com/mikker.png" width="60px" alt="Mikkel Malmberg" /></a><a href="https://github.com/pldubouilh"><img src="https://github.com/pldubouilh.png" width="60px" alt="Pierre Dubouilh" /></a><a href="https://github.com/rcorre"><img src="https://github.com/rcorre.png" width="60px" alt="Ryan Roden-Corrent" /></a><a href="https://github.com/blissdev"><img src="https://github.com/blissdev.png" width="60px" alt="Jordan Arentsen" /></a><a href="https://github.com/mislav"><img src="https://github.com/mislav.png" width="60px" alt="Mislav Marohnić" /></a><a href="https://github.com/aexvir"><img src="https://github.com/aexvir.png" width="60px" alt="Alex Viscreanu" /></a><a href="https://github.com/dbalatero"><img src="https://github.com/dbalatero.png" width="60px" alt="David Balatero" /></a><a href="https://github.com/comatory"><img src="https://github.com/comatory.png" width="60px" alt="Ondrej Synacek" /></a><a href="https://github.com/moobar"><img src="https://github.com/moobar.png" width="60px" alt="" /></a><a href="https://github.com/majjoha"><img src="https://github.com/majjoha.png" width="60px" alt="Mathias Jean Johansen" /></a><a href="https://github.com/benelan"><img src="https://github.com/benelan.png" width="60px" alt="Ben Elan" /></a><a href="https://github.com/pawelduda"><img src="https://github.com/pawelduda.png" width="60px" alt="Paweł Duda" /></a><a href="https://github.com/slezica"><img src="https://github.com/slezica.png" width="60px" alt="Santiago Lezica" /></a><a href="https://github.com/pbwn"><img src="https://github.com/pbwn.png" width="60px" alt="" /></a><a href="https://github.com/timgluz"><img src="https://github.com/timgluz.png" width="60px" alt="Timo Sulg" /></a><a href="https://github.com/pyrho"><img src="https://github.com/pyrho.png" width="60px" alt="Damien Rajon" /></a><a href="https://github.com/ArtBIT"><img src="https://github.com/ArtBIT.png" width="60px" alt="ArtBIT" /></a><a href="https://github.com/da-moon"><img src="https://github.com/da-moon.png" width="60px" alt="" /></a><a href="https://github.com/hovissimo"><img src="https://github.com/hovissimo.png" width="60px" alt="Hovis" /></a><a href="https://github.com/dariusjonda"><img src="https://github.com/dariusjonda.png" width="60px" alt="Darius Jonda" /></a><a href="https://github.com/cristiand391"><img src="https://github.com/cristiand391.png" width="60px" alt="Cristian Dominguez" /></a><a href="https://github.com/eliangcs"><img src="https://github.com/eliangcs.png" width="60px" alt="Chang-Hung Liang" /></a><a href="https://github.com/asphaltbuffet"><img src="https://github.com/asphaltbuffet.png" width="60px" alt="Ben Lechlitner" /></a><a href="https://github.com/yash1th"><img src="https://github.com/yash1th.png" width="60px" alt="yash" /></a><a href="https://github.com/looshch"><img src="https://github.com/looshch.png" width="60px" alt="george looshch" /></a><a href="https://github.com/kg8m"><img src="https://github.com/kg8m.png" width="60px" alt="Takumi KAGIYAMA" /></a><a href="https://github.com/polm"><img src="https://github.com/polm.png" width="60px" alt="Paul O'Leary McCann" /></a><a href="https://github.com/rbeeger"><img src="https://github.com/rbeeger.png" width="60px" alt="Robert Beeger" /></a><a href="https://github.com/veebch"><img src="https://github.com/veebch.png" width="60px" alt="VEEB Projects" /></a><a href="https://github.com/khuedoan"><img src="https://github.com/khuedoan.png" width="60px" alt="Khue Doan" /></a& |