├── .github └── workflows │ ├── coverage.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── .markdownlint.yaml ├── Dockerfile ├── Dockerfile.demo ├── Dockerfile.release ├── LICENSE ├── README.md ├── Taskfile.yaml ├── cmd ├── hook.go ├── hook_test.go ├── root.go ├── root_development.go ├── root_release.go ├── root_test.go ├── testdata │ ├── empty.golden │ ├── error.golden │ ├── help.golden │ ├── help_arg.golden │ ├── help_flag.golden │ ├── hook_invalid.golden │ ├── install.golden │ ├── root_invalid.golden │ ├── uninstall.golden │ ├── version_arg.golden │ └── version_flag.golden ├── version.go ├── version.gotmpl └── version_test.go ├── docs ├── assets │ ├── cancel-24x24.svg │ ├── cancel-icon.svg │ ├── confirm-24x24.svg │ └── confirm-icon.svg ├── demo.gif ├── interface-author.monopic ├── interface-commit.monopic ├── interface-emoji.monopic ├── interface-files.monopic ├── interface-main.monopic ├── interface-options.monopic ├── internal.md ├── keyboard-options-iterm2.png ├── keyboard-options-macos-terminal.png ├── model.monopic └── social.png ├── go.mod ├── go.sum ├── internal ├── commit │ ├── commit.go │ ├── commit_test.go │ ├── file.go │ ├── file_test.go │ ├── help.txt │ ├── message.txt │ ├── state.go │ ├── testdata │ │ ├── comment.golden │ │ ├── comment.input │ │ ├── comment_multiline.golden │ │ ├── comment_multiline.input │ │ ├── comment_multiline_long.golden │ │ ├── comment_multiline_long.input │ │ ├── mixed_comment_multiline.golden │ │ ├── mixed_comment_multiline.input │ │ ├── mixed_comment_multiline_long.golden │ │ ├── mixed_comment_multiline_long.input │ │ ├── no_comment.golden │ │ ├── no_comment.input │ │ ├── no_comment_multiline.golden │ │ ├── no_comment_multiline.input │ │ ├── no_comment_multiline_long.golden │ │ └── no_comment_multiline_long.input │ ├── transform.go │ └── transform_test.go ├── config │ ├── config.go │ ├── config_test.go │ ├── emoji.go │ ├── emoji_test.go │ ├── visual_test.go │ └── visuals.go ├── emoji │ ├── committed.yaml │ ├── devmoji.yaml │ ├── emoji.go │ ├── emoji_test.go │ ├── emojilog.yaml │ ├── gitmoji.json │ └── gitmoji.yaml ├── fuzzy │ ├── fuzzy.go │ └── fuzzy_test.go ├── hook │ ├── hook.go │ ├── install.go │ ├── install_test.go │ ├── locate.go │ ├── locate_test.go │ ├── prepare-commit-msg.sh │ ├── uninstall.go │ └── uninstall_test.go ├── repository │ ├── branch.go │ ├── branch_test.go │ ├── commit.go │ ├── commit_test.go │ ├── head.go │ ├── head_test.go │ ├── remote.go │ ├── remote_test.go │ ├── repository.go │ ├── repository_test.go │ ├── user.go │ ├── user_test.go │ ├── worktree.go │ └── worktree_test.go ├── shell │ ├── shell.go │ └── shell_test.go ├── snapshot │ ├── snapshot.go │ └── snapshot_test.go ├── terminal │ ├── terminal.go │ └── terminal_test.go ├── theme │ ├── theme.go │ ├── theme_test.go │ └── themetest │ │ └── themetest.go └── ui │ ├── body │ ├── body.go │ ├── body_test.go │ ├── style.go │ └── testdata │ │ ├── blur.golden │ │ ├── body.golden │ │ ├── body_multiline.golden │ │ ├── default.golden │ │ ├── dimensions.golden │ │ ├── empty.golden │ │ ├── focus.golden │ │ ├── placeholder.golden │ │ ├── reflow_combination.golden │ │ ├── reflow_multiple_words.golden │ │ ├── reflow_single_world.golden │ │ └── tab.golden │ ├── colour │ ├── colour.go │ └── colour_test.go │ ├── defaults.go │ ├── filterlist │ ├── filterlist.go │ ├── filterlist_test.go │ ├── paginator.go │ ├── style.go │ └── testdata │ │ ├── blur.golden │ │ ├── default.golden │ │ ├── down.golden │ │ ├── enter.golden │ │ ├── escape.golden │ │ ├── focus.golden │ │ ├── height.golden │ │ ├── multiple_pages.golden │ │ ├── no_border.golden │ │ ├── no_items.golden │ │ ├── no_title.golden │ │ ├── one.golden │ │ ├── overflow_pages.golden │ │ ├── pagedown.golden │ │ ├── pagedown_lastpage.golden │ │ ├── pageup.golden │ │ ├── setitems.golden │ │ ├── type.golden │ │ ├── up.golden │ │ ├── width.golden │ │ └── width_height.golden │ ├── footer │ ├── footer.go │ ├── footer_test.go │ └── testdata │ │ ├── default.golden │ │ ├── empty.golden │ │ └── signoff.golden │ ├── header │ ├── emoji.go │ ├── header.go │ ├── header_test.go │ ├── style.go │ └── testdata │ │ ├── amend_emoji.golden │ │ ├── amend_emoji_summary.golden │ │ ├── amend_summary.golden │ │ ├── blur_emoji.golden │ │ ├── blur_summary.golden │ │ ├── config_above.golden │ │ ├── config_below.golden │ │ ├── default.golden │ │ ├── emoji_empty_delete.golden │ │ ├── expand.golden │ │ ├── expand_emojis.golden │ │ ├── expand_emojis_page_down.golden │ │ ├── filter_emoji.golden │ │ ├── filter_emoji_no_match.golden │ │ ├── focus.golden │ │ ├── placeholder.golden │ │ ├── select_emoji.golden │ │ ├── select_emoji_delete.golden │ │ ├── select_emoji_down.golden │ │ ├── select_emoji_down_up.golden │ │ ├── select_emoji_filter.golden │ │ ├── select_emoji_filter_clear.golden │ │ ├── select_emoji_page_down_last_page.golden │ │ ├── select_emoji_page_down_last_page_exceeded.golden │ │ ├── select_emoji_page_down_last_page_exceeded_page_up.golden │ │ ├── select_emoji_page_down_last_page_page_up.golden │ │ ├── select_emoji_page_down_page_up.golden │ │ ├── summary_emoji.golden │ │ ├── summary_emoji_text.golden │ │ ├── summary_empty.golden │ │ ├── summary_exceed.golden │ │ ├── summary_maximum_boundary_high.golden │ │ ├── summary_maximum_boundary_low.golden │ │ ├── summary_normal_boundary_high.golden │ │ ├── summary_normal_boundary_low.golden │ │ ├── summary_short_boundary_high.golden │ │ ├── summary_short_boundary_low.golden │ │ ├── summary_text.golden │ │ ├── summary_warning_boundary_high.golden │ │ └── summary_warning_boundary_low.golden │ ├── help │ ├── help.go │ ├── help_test.go │ ├── style.go │ └── testdata │ │ ├── blur.golden │ │ ├── default.golden │ │ ├── focus.golden │ │ ├── multiline.golden │ │ └── singleline.golden │ ├── info │ ├── author.go │ ├── info.go │ ├── info_test.go │ ├── styles.go │ └── testdata │ │ ├── blur.golden │ │ ├── config_user_only.golden │ │ ├── default.golden │ │ ├── expand.golden │ │ ├── focus.golden │ │ ├── multiple_users.golden │ │ ├── multiple_users_config_default.golden │ │ ├── multiple_users_filtered.golden │ │ ├── multiple_users_repository_config_default.golden │ │ ├── multiple_users_repository_config_default_multiple.golden │ │ ├── multiple_users_selected.golden │ │ ├── no_local.golden │ │ ├── no_users.golden │ │ ├── remote.golden │ │ ├── repository_user_only.golden │ │ ├── tags.golden │ │ ├── users_both_empty.golden │ │ ├── users_both_nil.golden │ │ ├── users_mixed.golden │ │ └── users_mixed_multiple.golden │ ├── message │ ├── message.go │ ├── message_test.go │ ├── styles.go │ └── testdata │ │ ├── all.golden │ │ ├── default.golden │ │ ├── emoji_summary.golden │ │ ├── summary.golden │ │ ├── summary_body.golden │ │ ├── summary_body_multiline.golden │ │ └── summary_footer.golden │ ├── option │ ├── help │ │ ├── help.go │ │ ├── help_test.go │ │ └── testdata │ │ │ ├── help.golden │ │ │ ├── help_long.golden │ │ │ └── help_multiline.golden │ ├── option.go │ ├── option_test.go │ ├── section │ │ ├── section.go │ │ ├── section_test.go │ │ ├── style.go │ │ └── testdata │ │ │ ├── category_only.golden │ │ │ ├── first.golden │ │ │ ├── invalid.golden │ │ │ ├── last.golden │ │ │ ├── next.golden │ │ │ ├── next_category.golden │ │ │ ├── next_limit.golden │ │ │ ├── previous.golden │ │ │ ├── previous_category.golden │ │ │ ├── previous_limit.golden │ │ │ └── reset.golden │ ├── setting │ │ ├── noop.go │ │ ├── radio.go │ │ ├── setting.go │ │ ├── setting_test.go │ │ ├── style.go │ │ ├── testdata │ │ │ ├── radio.golden │ │ │ ├── radio_invalid.golden │ │ │ ├── radio_multiple.golden │ │ │ ├── radio_multiple_select.golden │ │ │ ├── radio_multiple_select_next.golden │ │ │ ├── radio_next.golden │ │ │ ├── radio_next_last.golden │ │ │ ├── radio_previous.golden │ │ │ ├── radio_previous_first.golden │ │ │ ├── radio_select.golden │ │ │ └── toggle.golden │ │ └── toggle.go │ ├── style.go │ ├── testdata │ │ ├── combined.golden │ │ ├── combined_modified.golden │ │ ├── combined_modified_width_height.golden │ │ ├── default.golden │ │ ├── help.golden │ │ ├── section.golden │ │ ├── section_category_only.golden │ │ ├── section_set.golden │ │ ├── section_set_category_only.golden │ │ ├── setting.golden │ │ ├── setting_next.golden │ │ ├── setting_previous.golden │ │ ├── setting_select.golden │ │ └── theme.golden │ └── theme │ │ ├── style.go │ │ ├── testdata │ │ ├── default.golden │ │ └── down_enter.golden │ │ ├── theme.go │ │ ├── theme_test.go │ │ └── tints.go │ ├── options.go │ ├── options_test.go │ ├── save.go │ ├── shortcut │ ├── set.go │ ├── shortcut.go │ ├── shortcut_test.go │ ├── style.go │ └── testdata │ │ ├── default.golden │ │ ├── empty_left.golden │ │ ├── empty_left_bottom.golden │ │ ├── empty_left_top.golden │ │ ├── empty_right.golden │ │ ├── empty_right_bottom.golden │ │ ├── empty_right_top.golden │ │ ├── left.golden │ │ ├── multiple_different_left.golden │ │ ├── multiple_different_right.golden │ │ ├── multiple_same_left.golden │ │ ├── multiple_same_right.golden │ │ └── right.golden │ ├── status │ ├── status.go │ ├── status_test.go │ └── testdata │ │ ├── default.golden │ │ ├── help.golden │ │ ├── next.golden │ │ ├── next_previous.golden │ │ ├── option.golden │ │ └── previous.golden │ ├── testdata │ ├── alt+1.golden │ ├── alt+1_twice.golden │ ├── alt+2.golden │ ├── alt+2_twice.golden │ ├── alt+3.golden │ ├── alt+3_twice.golden │ ├── alt+4.golden │ ├── alt+4_twice.golden │ ├── alt+enter_invalid.golden │ ├── alt+enter_summary.golden │ ├── alt+enter_summary_author.golden │ ├── alt+enter_summary_body.golden │ ├── alt+enter_summary_emoji.golden │ ├── alt+enter_summary_footer.golden │ ├── alt+s.golden │ ├── alt+s_change_author.golden │ ├── alt+s_twice.golden │ ├── alt+t.golden │ ├── amend_empty.golden │ ├── amend_existing.golden │ ├── config_author.golden │ ├── config_emoji.golden │ ├── config_emoji_type_character.golden │ ├── config_emoji_type_shortcode.golden │ ├── config_signoff_off.golden │ ├── config_signoff_on.golden │ ├── config_summary.golden │ ├── ctrl+c.golden │ ├── ctrl+h.golden │ ├── ctrl+h_twice.golden │ ├── ctrl+h_twice_body.golden │ ├── enter_author.golden │ ├── enter_body.golden │ ├── enter_emoji.golden │ ├── enter_summary.golden │ ├── escape_help.golden │ ├── shift_tab_author.golden │ ├── shift_tab_body.golden │ ├── shift_tab_emoji.golden │ ├── shift_tab_summary.golden │ ├── snapshot_restore_amend_from_amend_to_new.golden │ ├── snapshot_restore_amend_from_amend_to_new_to_amend.golden │ ├── snapshot_restore_from_new_to_amend.golden │ ├── snapshot_restore_from_new_to_amend_to_new.golden │ ├── snapshot_restore_previous_commit_fail.golden │ ├── snapshot_restore_previous_commit_success.golden │ ├── tab_author.golden │ ├── tab_body.golden │ ├── tab_emoji.golden │ └── tab_summary.golden │ ├── ui.go │ ├── ui_test.go │ └── uitest │ ├── uitest.go │ └── uitest_test.go ├── main.go └── scripts └── demo.tape /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | - uses: actions/setup-go@v3 13 | with: 14 | go-version: stable 15 | - run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 16 | - uses: codecov/codecov-action@v3 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - run: git fetch --force --tags 19 | - uses: actions/setup-go@v3 20 | with: 21 | go-version: stable 22 | cache: true 23 | - uses: docker/login-action@v2 24 | with: 25 | registry: ghcr.io 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GH_PAT }} 28 | - uses: goreleaser/goreleaser-action@v4 29 | with: 30 | distribution: goreleaser 31 | version: latest 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | dist/ 24 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - err113 5 | - errname 6 | - errorlint 7 | - gocritic 8 | - ireturn 9 | - makezero 10 | - nestif 11 | - nilnil 12 | - revive 13 | - whitespace 14 | disable: 15 | - errcheck 16 | - unused 17 | settings: 18 | revive: 19 | rules: 20 | - name: unused-parameter 21 | disabled: true 22 | exclusions: 23 | generated: lax 24 | presets: 25 | - comments 26 | - common-false-positives 27 | - legacy 28 | - std-error-handling 29 | paths: 30 | - third_party$ 31 | - builtin$ 32 | - examples$ 33 | formatters: 34 | enable: 35 | - gofmt 36 | - gofumpt 37 | - goimports 38 | exclusions: 39 | generated: lax 40 | paths: 41 | - third_party$ 42 | - builtin$ 43 | - examples$ 44 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | builds: 4 | - env: 5 | - CGO_ENABLED=0 6 | goos: 7 | - darwin 8 | - linux 9 | goarch: 10 | - amd64 11 | - arm64 12 | ldflags: 13 | - -s 14 | - -w 15 | - -X github.com/mikelorant/committed/cmd.version={{.Version}} 16 | tags: 17 | - release 18 | 19 | release: 20 | github: 21 | owner: mikelorant 22 | name: committed 23 | 24 | brews: 25 | - repository: 26 | owner: mikelorant 27 | name: homebrew-committed 28 | homepage: https://github.com/mikelorant/committed 29 | description: >- 30 | WYSIWYG Git commit editor that helps improve the quality of your 31 | commits by showing you the layout in the same format as git log 32 | license: MIT 33 | dependencies: 34 | - name: git 35 | test: | 36 | system "#{bin}/committed --help" 37 | 38 | dockers: 39 | - image_templates: 40 | - ghcr.io/mikelorant/committed:{{.Version}} 41 | - ghcr.io/mikelorant/committed:latest 42 | dockerfile: Dockerfile.release 43 | build_flag_templates: 44 | - "--pull" 45 | - "--label=org.opencontainers.image.created={{.Date}}" 46 | - "--label=org.opencontainers.image.title={{.ProjectName}}" 47 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 48 | - "--label=org.opencontainers.image.version={{.Version}}" 49 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | no-inline-html: 2 | allowed_elements: 3 | - br 4 | - kbd 5 | line-length: 6 | code_block_line_length: 120 7 | heading_line_length: 400 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.2-alpine3.21 AS base 2 | WORKDIR /usr/src/app 3 | RUN --mount=type=cache,target=/var/cache/apk \ 4 | ln -vs /var/cache/apk /etc/apk/cache && \ 5 | apk add \ 6 | git=~2 7 | 8 | FROM base AS dependencies 9 | ENV CGO_ENABLED=0 10 | COPY go.* ./ 11 | RUN go mod download 12 | 13 | FROM dependencies AS build 14 | RUN --mount=target=. \ 15 | --mount=type=cache,target=/root/.cache/go-build \ 16 | go build -o /usr/local/bin 17 | 18 | FROM base AS unit-test 19 | RUN --mount=target=. \ 20 | --mount=type=cache,target=/root/.cache/go-build \ 21 | go test -v ./... 22 | 23 | FROM golangci/golangci-lint:v1.64.6-go1.24.1 AS lint-base 24 | 25 | FROM base AS lint 26 | RUN --mount=target=. \ 27 | --mount=from=lint-base,src=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \ 28 | --mount=type=cache,target=/root/.cache/go-build \ 29 | --mount=type=cache,target=/root/.cache/golangci-lint \ 30 | golangci-lint run ./... 31 | 32 | FROM alpine:3.21.3 AS release 33 | ENV TERM=xterm-256color 34 | RUN --mount=type=cache,target=/var/cache/apk \ 35 | ln -vs /var/cache/apk /etc/apk/cache && \ 36 | apk add \ 37 | git=~2 38 | COPY --from=build /usr/local/bin/committed /usr/local/bin 39 | 40 | FROM release AS test 41 | WORKDIR /root/repository 42 | RUN git config --global user.email "you@example.com" && \ 43 | git config --global user.name "Your Name" && \ 44 | git init 45 | 46 | ENTRYPOINT ["committed"] 47 | -------------------------------------------------------------------------------- /Dockerfile.demo: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/mikelorant/committed:latest AS committed 2 | 3 | FROM ghcr.io/charmbracelet/vhs:v0.9.0 4 | ENV TERM=xterm-256color 5 | RUN --mount=type=cache,target=/var/cache/apt \ 6 | --mount=type=cache,target=/var/lib/apt \ 7 | apt-get update && \ 8 | apt-get install --yes \ 9 | git 10 | ADD https://github.com/samuelngs/apple-emoji-linux/releases/download/v18.4/AppleColorEmoji.ttf /usr/share/fonts/apple/ 11 | RUN sed -i '/Noto Color Emoji/d' /usr/share/fontconfig/conf.avail/60-generic.conf 12 | COPY --from=committed /usr/local/bin/committed /usr/local/bin/ 13 | RUN git config --global user.email "john.doe@example.com" && \ 14 | git config --global user.name "John Doe" && \ 15 | git config --global init.defaultBranch main 16 | COPY < 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/assets/cancel-icon.svg: -------------------------------------------------------------------------------- 1 | cancel -------------------------------------------------------------------------------- /docs/assets/confirm-24x24.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/assets/confirm-icon.svg: -------------------------------------------------------------------------------- 1 | confirm -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/demo.gif -------------------------------------------------------------------------------- /docs/interface-author.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/interface-author.monopic -------------------------------------------------------------------------------- /docs/interface-commit.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/interface-commit.monopic -------------------------------------------------------------------------------- /docs/interface-emoji.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/interface-emoji.monopic -------------------------------------------------------------------------------- /docs/interface-files.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/interface-files.monopic -------------------------------------------------------------------------------- /docs/interface-main.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/interface-main.monopic -------------------------------------------------------------------------------- /docs/interface-options.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/interface-options.monopic -------------------------------------------------------------------------------- /docs/keyboard-options-iterm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/keyboard-options-iterm2.png -------------------------------------------------------------------------------- /docs/keyboard-options-macos-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/keyboard-options-macos-terminal.png -------------------------------------------------------------------------------- /docs/model.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/model.monopic -------------------------------------------------------------------------------- /docs/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikelorant/committed/e5db6661e6584bedf55b3c19ea6d270ec2828ee3/docs/social.png -------------------------------------------------------------------------------- /internal/commit/file.go: -------------------------------------------------------------------------------- 1 | package commit 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "path" 10 | "strings" 11 | ) 12 | 13 | func FileOpen() func(string) (io.Reader, error) { 14 | return func(file string) (io.Reader, error) { 15 | var pathError *fs.PathError 16 | 17 | fh, err := os.Open(os.ExpandEnv(file)) 18 | switch { 19 | case err == nil: 20 | case errors.As(err, &pathError): 21 | return strings.NewReader(""), nil 22 | default: 23 | return nil, fmt.Errorf("unable to open file: %w", err) 24 | } 25 | 26 | return fh, nil 27 | } 28 | } 29 | 30 | func FileCreate() func(string) (io.WriteCloser, error) { 31 | return func(file string) (io.WriteCloser, error) { 32 | err := os.MkdirAll(path.Dir(os.ExpandEnv(file)), 0o755) 33 | switch { 34 | case err == nil: 35 | case os.IsExist(err): 36 | default: 37 | return nil, err 38 | } 39 | 40 | fh, err := os.Create(os.ExpandEnv(file)) 41 | if err != nil { 42 | return nil, fmt.Errorf("unable to create file: %w", err) 43 | } 44 | 45 | return fh, nil 46 | } 47 | } 48 | 49 | func FileRemove() func(string) error { 50 | return func(file string) error { 51 | if !FileExists(os.ExpandEnv(file)) { 52 | return nil 53 | } 54 | 55 | if err := os.Remove(os.ExpandEnv(file)); err != nil { 56 | return fmt.Errorf("unable to remove file: %w", err) 57 | } 58 | 59 | return nil 60 | } 61 | } 62 | 63 | func FileExists(file string) bool { 64 | _, err := os.Stat(os.ExpandEnv(file)) 65 | 66 | return !errors.Is(err, os.ErrNotExist) 67 | } 68 | -------------------------------------------------------------------------------- /internal/commit/help.txt: -------------------------------------------------------------------------------- 1 | Global Emoji 2 | 3 | Commit alt+enter Clear emoji delete 4 | Toggle sign-off alt+s Reset filter escape 5 | Toggle theme alt+t Next page page down 6 | Help alt+/ Previous page page up 7 | Focus author alt+1 8 | Focus emoji alt+2 9 | Focus summary alt+3 10 | Focus body alt+4 11 | Cancel ctrl+c 12 | Next component tab 13 | Previous component shift+tab 14 | -------------------------------------------------------------------------------- /internal/commit/message.txt: -------------------------------------------------------------------------------- 1 | More detailed explanatory text, if necessary. Wrap it to about 72 2 | characters or so. In some contexts, the first line is treated as the 3 | subject of an email and the rest of the text as the body. The blank 4 | line separating the summary from the body is critical (unless you omit 5 | the body entirely); tools like rebase can get confused if you run the 6 | two together. 7 | 8 | Write your commit message in the imperative: "Fix bug" and not 9 | "Fixed bug" or "Fixes bug." This convention matches up with commit 10 | messages generated by commands like git merge and git revert. 11 | 12 | Further paragraphs come after blank lines. 13 | 14 | - Bullet points are okay, too 15 | 16 | - Typically a hyphen or asterisk is used for the bullet, followed by a 17 | single space, with blank lines in between, but conventions vary here 18 | 19 | - Use a hanging indent 20 | -------------------------------------------------------------------------------- /internal/commit/state.go: -------------------------------------------------------------------------------- 1 | package commit 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/mikelorant/committed/internal/config" 7 | "github.com/mikelorant/committed/internal/emoji" 8 | "github.com/mikelorant/committed/internal/repository" 9 | "github.com/mikelorant/committed/internal/snapshot" 10 | "github.com/mikelorant/committed/internal/theme" 11 | ) 12 | 13 | type State struct { 14 | Placeholders Placeholders 15 | Repository repository.Description 16 | Emojis *emoji.Set 17 | Theme theme.Theme 18 | Config config.Config 19 | Snapshot snapshot.Snapshot 20 | Options Options 21 | File File 22 | } 23 | 24 | type Placeholders struct { 25 | Hash string 26 | Summary string 27 | Body string 28 | Help string 29 | } 30 | 31 | type Config struct { 32 | View config.View 33 | Commit config.Commit 34 | Authors []repository.User 35 | } 36 | 37 | type File struct { 38 | Amend bool 39 | Message string 40 | } 41 | 42 | //go:embed message.txt 43 | var PlaceholderMessage string 44 | 45 | //go:embed help.txt 46 | var PlaceholderHelp string 47 | 48 | const ( 49 | PlaceholderHash string = "1234567890abcdef1234567890abcdef12345678" 50 | PlaceholderSummary string = "Capitalized, short (50 chars or less) summary" 51 | ) 52 | 53 | func placeholders() Placeholders { 54 | return Placeholders{ 55 | Hash: PlaceholderHash, 56 | Summary: PlaceholderSummary, 57 | Body: PlaceholderMessage, 58 | Help: PlaceholderHelp, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/commit/testdata/comment.golden: -------------------------------------------------------------------------------- 1 | # 1234567890123456789012345678901234567890 -------------------------------------------------------------------------------- /internal/commit/testdata/comment.input: -------------------------------------------------------------------------------- 1 | # 1234567890123456789012345678901234567890 2 | -------------------------------------------------------------------------------- /internal/commit/testdata/comment_multiline.golden: -------------------------------------------------------------------------------- 1 | # 1234567890123456789012345678901234567890 2 | # 1234567890123456789012345678901234567890 3 | # 1234567890123456789012345678901234567890 4 | # 1234567890123456789012345678901234567890 -------------------------------------------------------------------------------- /internal/commit/testdata/comment_multiline.input: -------------------------------------------------------------------------------- 1 | # 1234567890123456789012345678901234567890 2 | # 1234567890123456789012345678901234567890 3 | # 1234567890123456789012345678901234567890 4 | # 1234567890123456789012345678901234567890 5 | -------------------------------------------------------------------------------- /internal/commit/testdata/comment_multiline_long.golden: -------------------------------------------------------------------------------- 1 | # 12345678901234567890123456789012345678901234567890123456789012345678 2 | # 12345678901234567890123456789012345678901234567890123456789012345678 3 | # 12345678901234567890123456789012345678901234567890123456789012345678 4 | # 12345678901234567890123456789012345678901234567890123456789012345678 -------------------------------------------------------------------------------- /internal/commit/testdata/comment_multiline_long.input: -------------------------------------------------------------------------------- 1 | # 12345678901234567890123456789012345678901234567890123456789012345678901234567890 2 | # 12345678901234567890123456789012345678901234567890123456789012345678901234567890 3 | # 12345678901234567890123456789012345678901234567890123456789012345678901234567890 4 | # 12345678901234567890123456789012345678901234567890123456789012345678901234567890 5 | -------------------------------------------------------------------------------- /internal/commit/testdata/mixed_comment_multiline.golden: -------------------------------------------------------------------------------- 1 | # 1234567890123456789012345678901234567890 2 | 1234567890123456789012345678901234567890 3 | # 1234567890123456789012345678901234567890 4 | 1234567890123456789012345678901234567890 -------------------------------------------------------------------------------- /internal/commit/testdata/mixed_comment_multiline.input: -------------------------------------------------------------------------------- 1 | # 1234567890123456789012345678901234567890 2 | 1234567890123456789012345678901234567890 3 | # 1234567890123456789012345678901234567890 4 | 1234567890123456789012345678901234567890 5 | -------------------------------------------------------------------------------- /internal/commit/testdata/mixed_comment_multiline_long.golden: -------------------------------------------------------------------------------- 1 | # 12345678901234567890123456789012345678901234567890123456789012345678 2 | 1234567890123456789012345678901234567890 3 | # 1234567890123456789012345678901234567890 4 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 -------------------------------------------------------------------------------- /internal/commit/testdata/mixed_comment_multiline_long.input: -------------------------------------------------------------------------------- 1 | # 12345678901234567890123456789012345678901234567890123456789012345678901234567890 2 | 1234567890123456789012345678901234567890 3 | # 1234567890123456789012345678901234567890 4 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 5 | -------------------------------------------------------------------------------- /internal/commit/testdata/no_comment.golden: -------------------------------------------------------------------------------- 1 | 1234567890123456789012345678901234567890 -------------------------------------------------------------------------------- /internal/commit/testdata/no_comment.input: -------------------------------------------------------------------------------- 1 | 1234567890123456789012345678901234567890 2 | -------------------------------------------------------------------------------- /internal/commit/testdata/no_comment_multiline.golden: -------------------------------------------------------------------------------- 1 | 1234567890123456789012345678901234567890 2 | 1234567890123456789012345678901234567890 3 | 1234567890123456789012345678901234567890 4 | 1234567890123456789012345678901234567890 -------------------------------------------------------------------------------- /internal/commit/testdata/no_comment_multiline.input: -------------------------------------------------------------------------------- 1 | 1234567890123456789012345678901234567890 2 | 1234567890123456789012345678901234567890 3 | 1234567890123456789012345678901234567890 4 | 1234567890123456789012345678901234567890 5 | -------------------------------------------------------------------------------- /internal/commit/testdata/no_comment_multiline_long.golden: -------------------------------------------------------------------------------- 1 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 2 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 3 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 4 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 -------------------------------------------------------------------------------- /internal/commit/testdata/no_comment_multiline_long.input: -------------------------------------------------------------------------------- 1 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 2 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 3 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 4 | 12345678901234567890123456789012345678901234567890123456789012345678901234567890 5 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/mikelorant/committed/internal/repository" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type Config struct { 14 | View View `yaml:"view,omitempty"` 15 | Commit Commit `yaml:"commit,omitempty"` 16 | Authors []repository.User `yaml:"authors,omitempty"` 17 | Update bool `yaml:"-"` 18 | } 19 | 20 | type View struct { 21 | Focus Focus `yaml:"focus,omitempty"` 22 | EmojiSet EmojiSet `yaml:"emojiSet,omitempty"` 23 | EmojiSelector EmojiSelector `yaml:"emojiSelector,omitempty"` 24 | Compatibility Compatibility `yaml:"compatibility,omitempty"` 25 | Theme string `yaml:"theme,omitempty"` 26 | Colour Colour `yaml:"colour,omitempty"` 27 | HighlightActive bool `yaml:"highlightActive,omitempty"` 28 | IgnoreGlobalAuthor bool `yaml:"ignoreGlobalAuthor,omitempty"` 29 | } 30 | 31 | type Commit struct { 32 | EmojiType EmojiType `yaml:"emojiType,omitempty"` 33 | Signoff bool `yaml:"signoff,omitempty"` 34 | } 35 | 36 | func (c *Config) Load(fh io.Reader) (Config, error) { 37 | var cfg Config 38 | 39 | err := yaml.NewDecoder(fh).Decode(&cfg) 40 | switch { 41 | case err == nil: 42 | case errors.Is(err, io.EOF): 43 | default: 44 | return cfg, fmt.Errorf("unable to decode config: %w", err) 45 | } 46 | 47 | return cfg, nil 48 | } 49 | 50 | func (c *Config) Save(fh io.WriteCloser, cfg Config) error { 51 | err := yaml.NewEncoder(fh).Encode(&cfg) 52 | if err != nil { 53 | return fmt.Errorf("unable to encode config: %w", err) 54 | } 55 | defer fh.Close() 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/emoji/emojilog.yaml: -------------------------------------------------------------------------------- 1 | - name: new 2 | emoji: 📦 3 | description: "Use when you add something entirely." 4 | characters: 1 5 | codepoint: 1f4e6 6 | hex: F0 9F 93 A6 7 | shortcode: ":package:" 8 | 9 | - name: improve 10 | emoji: 👌 11 | description: "Use when you improve/enhance piece of code." 12 | characters: 1 13 | codepoint: 1f44c 14 | hex: F0 9F 91 8C 15 | shortcode: ":ok_hand:" 16 | 17 | - name: fix 18 | emoji: 🐛 19 | description: "Use when you fix a bug." 20 | characters: 1 21 | codepoint: e525 22 | hex: F0 9F 90 9B 23 | shortcode: ":bug:" 24 | 25 | - name: doc 26 | emoji: 📖 27 | description: "Use when you add documentation." 28 | characters: 1 29 | codepoint: 1f4d6 30 | hex: F0 9F 93 96 31 | shortcode: ":book:" 32 | 33 | - name: release 34 | emoji: 🚀 35 | description: "Use when you release a new version." 36 | characters: 1 37 | codepoint: 1f680 38 | hex: F0 9F 9A 80 39 | shortcode: ":rocket:" 40 | 41 | - name: test 42 | emoji: 🤖 43 | description: "Use when it's related to testing." 44 | characters: 1 45 | codepoint: 1f916 46 | hex: F0 9F A4 96 47 | shortcode: ":robot:" 48 | 49 | - name: breaking 50 | emoji: ‼️ 51 | description: "Use when releasing a change that breaks previous versions." 52 | characters: 2 53 | codepoint: 203c, fe0f 54 | hex: E2 80 BC, EF B8 8F 55 | shortcode: ":bangbang:" 56 | -------------------------------------------------------------------------------- /internal/fuzzy/fuzzy.go: -------------------------------------------------------------------------------- 1 | package fuzzy 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/lithammer/fuzzysearch/fuzzy" 10 | ) 11 | 12 | type Item interface { 13 | Terms() []string 14 | } 15 | 16 | func Rank(term string, items []Item) []int { 17 | idx := make([]int, len(items)) 18 | null := string(rune(0)) 19 | 20 | if len(term) < 3 { 21 | for i := 0; i < len(items); i++ { 22 | idx[i] = i 23 | } 24 | return idx 25 | } 26 | 27 | var lines []string 28 | for i, item := range items { 29 | for _, t := range item.Terms() { 30 | line := fmt.Sprintf("%v%v%v", i, null, t) 31 | lines = append(lines, line) 32 | } 33 | } 34 | 35 | ranks := fuzzy.RankFindFold(term, lines) 36 | sort.Sort(ranks) 37 | 38 | idx = make([]int, 0) 39 | for _, v := range ranks { 40 | pos := strings.Split(v.Target, null)[0] 41 | i, _ := strconv.Atoi(pos) 42 | if !contains(idx, i) { 43 | //nolint:makezero 44 | idx = append(idx, i) 45 | } 46 | } 47 | 48 | return idx 49 | } 50 | 51 | func contains[T comparable](vs []T, val T) bool { 52 | for _, v := range vs { 53 | if v == val { 54 | return true 55 | } 56 | } 57 | return false 58 | } 59 | -------------------------------------------------------------------------------- /internal/hook/hook.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "io" 7 | "os" 8 | 9 | "github.com/mikelorant/committed/internal/shell" 10 | ) 11 | 12 | type Hook struct { 13 | Creator Creator 14 | Locater Locater 15 | Deleter Deleter 16 | Opener Opener 17 | Runner Runner 18 | Stater Stater 19 | 20 | Location string 21 | Directory string 22 | 23 | file *os.File 24 | } 25 | 26 | type Options struct { 27 | Install bool 28 | Uninstall bool 29 | Commit bool 30 | } 31 | 32 | type ( 33 | Creator func(name string, flag int, perm os.FileMode) (*os.File, error) 34 | Deleter func(string) error 35 | Opener func(string) (*os.File, error) 36 | Locater func(run Runner) (string, error) 37 | Runner func(io.Writer, string, []string) error 38 | Stater func(string) (os.FileInfo, error) 39 | ) 40 | 41 | type ( 42 | Action int 43 | ) 44 | 45 | //go:embed prepare-commit-msg.sh 46 | var PrepareGitMessage string 47 | 48 | var GitHook = "hooks/prepare-commit-msg" 49 | 50 | var ( 51 | ErrAction = errors.New("invalid hook action") 52 | ErrUnmanaged = errors.New("hook file unmanaged") 53 | ) 54 | 55 | const ( 56 | ActionUnset Action = iota 57 | ActionInstall 58 | ActionUninstall 59 | ActionCommit 60 | ) 61 | 62 | const ( 63 | Marker = "Code generated by Committed. DO NOT EDIT." 64 | ) 65 | 66 | func New() Hook { 67 | return Hook{ 68 | Creator: os.OpenFile, 69 | Deleter: os.Remove, 70 | Opener: os.Open, 71 | Locater: Locate, 72 | Runner: shell.Run, 73 | Stater: os.Stat, 74 | } 75 | } 76 | 77 | func (h *Hook) Do(opts Options) error { 78 | switch { 79 | case opts.Install: 80 | return h.Install() 81 | case opts.Uninstall: 82 | return h.Uninstall() 83 | } 84 | 85 | return ErrAction 86 | } 87 | -------------------------------------------------------------------------------- /internal/hook/install.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | func (h *Hook) Install() error { 13 | loc, err := h.Locater(h.Runner) 14 | if err != nil { 15 | return fmt.Errorf("unable to determine hook location: %w", err) 16 | } 17 | 18 | if loc == "" { 19 | return ErrLocation 20 | } 21 | 22 | h.Location = path.Join(loc, GitHook) 23 | 24 | manage, err := h.manage() 25 | if err != nil { 26 | return fmt.Errorf("unable to determine managed state: %w", err) 27 | } 28 | 29 | if !manage { 30 | return ErrUnmanaged 31 | } 32 | 33 | _, err = h.file.WriteString(PrepareGitMessage) 34 | if err != nil { 35 | return fmt.Errorf("unable to write message: %w", err) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (h *Hook) manage() (bool, error) { 42 | managed, err := h.isManaged() 43 | if err != nil { 44 | return false, err 45 | } 46 | 47 | if !managed { 48 | return false, nil 49 | } 50 | 51 | fh, err := h.Creator(h.Location, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) 52 | if err != nil { 53 | return false, fmt.Errorf("unable to create file: %w", err) 54 | } 55 | 56 | h.file = fh 57 | 58 | return true, nil 59 | } 60 | 61 | func (h *Hook) isManaged() (bool, error) { 62 | if !h.exists() { 63 | return true, nil 64 | } 65 | 66 | fh, err := h.Opener(h.Location) 67 | if err != nil { 68 | return false, fmt.Errorf("unable to open file: %w", err) 69 | } 70 | 71 | return checkSignature(fh) 72 | } 73 | 74 | func (h *Hook) exists() bool { 75 | _, err := h.Stater(h.Location) 76 | 77 | return err == nil 78 | } 79 | 80 | func checkSignature(fh io.ReadWriter) (bool, error) { 81 | var line string 82 | 83 | scanner := bufio.NewScanner(fh) 84 | for scanner.Scan() { 85 | line = scanner.Text() 86 | break 87 | } 88 | 89 | if err := scanner.Err(); err != nil { 90 | return false, fmt.Errorf("unable to scan file: %w", err) 91 | } 92 | 93 | if strings.Contains(line, Marker) { 94 | return true, nil 95 | } 96 | 97 | return false, nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/hook/locate.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | gitCommand = "git" 13 | gitGlobalArgs = []string{"config", "--get", "core.hooksPath"} 14 | gitRepositoryArgs = []string{"rev-parse", "--absolute-git-dir"} 15 | ) 16 | 17 | var ErrLocation = errors.New("no hook location found") 18 | 19 | func Locate(run Runner) (string, error) { 20 | glob, _ := runCmd(run, gitCommand, gitGlobalArgs) 21 | 22 | if glob != "" { 23 | return glob, nil 24 | } 25 | 26 | repo, _ := runCmd(run, gitCommand, gitRepositoryArgs) 27 | 28 | if repo != "" { 29 | return repo, nil 30 | } 31 | 32 | return "", ErrLocation 33 | } 34 | 35 | func runCmd(run Runner, cmd string, args []string) (string, error) { 36 | var buf bytes.Buffer 37 | 38 | if err := run(&buf, cmd, args); err != nil { 39 | return "", fmt.Errorf("unable to run command: %w", err) 40 | } 41 | 42 | out, err := io.ReadAll(&buf) 43 | if err != nil { 44 | return "", fmt.Errorf("unable to read buffer: %w", err) 45 | } 46 | 47 | return strings.TrimSpace(string(out)), nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/hook/prepare-commit-msg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash # Code generated by Committed. DO NOT EDIT. 2 | 3 | # It takes one to three parameters. The first is the name of the file 4 | # that contains the commit log message. The second is the source of the 5 | # commit message, and can be: message (if a -m or -F option was given); 6 | # template (if a -t option was given or the configuration option 7 | # commit.template is set); merge (if the commit is a merge or a 8 | # .git/MERGE_MSG file exists); squash (if a .git/SQUASH_MSG file exists); 9 | # or commit, followed by a commit object name (if a -c, -C or --amend 10 | # option was given). 11 | # 12 | # Source: https://git-scm.com/docs/githooks#_prepare_commit_msg 13 | 14 | exec < /dev/tty 15 | 16 | # name of the file that contains the commit log message 17 | : "${message_file:=$1}" 18 | 19 | # source of the commit message 20 | : "${source:=$2}" 21 | 22 | # sha of the commit 23 | : "${sha:=$3}" 24 | 25 | declare -a args 26 | 27 | [[ "$source" = "merge" ]] && exit 28 | [[ "$source" = "squash" ]] && exit 29 | [[ "$source" = "template" ]] && exit 30 | 31 | if [[ -n ${message_file} ]]; then 32 | args+=(--message-file "$message_file") 33 | fi 34 | 35 | if [[ -n "${sha}" ]]; then 36 | args+=(--sha "${sha}") 37 | fi 38 | 39 | case ${source} in 40 | HEAD) 41 | args+=(--amend) 42 | ;; 43 | *) 44 | esac 45 | 46 | committed --hook "${args[@]}" 47 | -------------------------------------------------------------------------------- /internal/hook/uninstall.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | ) 7 | 8 | func (h *Hook) Uninstall() error { 9 | loc, err := h.Locater(h.Runner) 10 | if err != nil { 11 | return fmt.Errorf("unable to determine hook location: %w", err) 12 | } 13 | 14 | if loc == "" { 15 | return ErrLocation 16 | } 17 | 18 | h.Location = path.Join(loc, GitHook) 19 | 20 | manage, err := h.unmanage() 21 | if err != nil { 22 | return fmt.Errorf("unable to determine managed state: %w", err) 23 | } 24 | 25 | if !manage { 26 | return ErrUnmanaged 27 | } 28 | 29 | return nil 30 | } 31 | 32 | func (h *Hook) unmanage() (bool, error) { 33 | managed, err := h.isManaged() 34 | if err != nil { 35 | return false, err 36 | } 37 | 38 | if !managed { 39 | return false, nil 40 | } 41 | 42 | if err := h.Deleter(h.Location); err != nil { 43 | return false, fmt.Errorf("unable to delete file: %w", err) 44 | } 45 | 46 | return true, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/repository/head.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/go-git/go-git/v5/plumbing" 8 | ) 9 | 10 | type Head struct { 11 | Hash string 12 | Author User 13 | When time.Time 14 | Message string 15 | } 16 | 17 | func (r *Repository) Head() (Head, error) { 18 | h, err := r.Header.Head() 19 | 20 | switch { 21 | case err == nil: 22 | case err.Error() == plumbing.ErrReferenceNotFound.Error(): 23 | return Head{}, nil 24 | default: 25 | return Head{}, fmt.Errorf("unable to get head reference: %w", err) 26 | } 27 | 28 | o, err := r.Header.CommitObject(h.Hash()) 29 | if err != nil { 30 | return Head{}, fmt.Errorf("unable to get head commit: %w", err) 31 | } 32 | 33 | return Head{ 34 | Hash: o.Hash.String(), 35 | Author: User{ 36 | Name: o.Author.Name, 37 | Email: o.Author.Email, 38 | }, 39 | When: o.Author.When, 40 | Message: o.Message, 41 | }, nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/repository/remote.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func (r *Repository) Remotes() ([]string, error) { 8 | var remotes []string 9 | 10 | rs, err := r.Remoter.Remotes() 11 | if err != nil { 12 | return remotes, fmt.Errorf("unable to list remotes: %w", err) 13 | } 14 | 15 | for _, r := range rs { 16 | remotes = append(remotes, r.Config().Name) 17 | } 18 | 19 | return remotes, nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/repository/user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-git/go-git/v5/config" 7 | ) 8 | 9 | type User struct { 10 | Name string `yaml:"name,omitempty"` 11 | Email string `yaml:"email,omitempty"` 12 | Default bool `yaml:"default,omitempty"` 13 | } 14 | 15 | func (r *Repository) Users() ([]User, error) { 16 | var users []User 17 | 18 | cfg, err := r.Configer.Config() 19 | if err != nil { 20 | return users, fmt.Errorf("unable to get repository config: %w", err) 21 | } 22 | if (cfg.User.Name != "") || (cfg.User.Email != "") { 23 | users = append(users, user(cfg)) 24 | } 25 | 26 | cfg, err = r.GlobalConfig(config.GlobalScope) 27 | if err != nil { 28 | return users, fmt.Errorf("unable to get global config: %w", err) 29 | } 30 | if (cfg.User.Name != "") || (cfg.User.Email != "") { 31 | users = append(users, user(cfg)) 32 | } 33 | 34 | return users, nil 35 | } 36 | 37 | func (r *Repository) IgnoreGlobalConfig() { 38 | r.GlobalConfig = func(config.Scope) (*config.Config, error) { 39 | return &config.Config{}, nil 40 | } 41 | } 42 | 43 | func user(c *config.Config) User { 44 | return User{ 45 | Name: c.User.Name, 46 | Email: c.User.Email, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/repository/worktree.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | 8 | "github.com/go-git/go-git/v5" 9 | ) 10 | 11 | type Worktree struct { 12 | Status git.Status 13 | } 14 | 15 | func (r *Repository) Worktree() (Worktree, error) { 16 | var wt Worktree 17 | 18 | w, err := r.Worktreer.Worktree() 19 | if err != nil { 20 | return Worktree{}, fmt.Errorf("unable to get worktree: %w", err) 21 | } 22 | 23 | // Performance of internal status was poor. Replace with external call 24 | // which is significantly faster. 25 | // s, err := w.Status() 26 | s, err := status(w) 27 | if err != nil { 28 | return Worktree{}, fmt.Errorf("unable to get status of worktree: %w", err) 29 | } 30 | wt.Status = s 31 | 32 | return wt, nil 33 | } 34 | 35 | func (w *Worktree) IsStaged() bool { 36 | for _, s := range w.Status { 37 | if s.Staging != git.Unmodified && s.Staging != git.Untracked { 38 | return true 39 | } 40 | } 41 | 42 | return false 43 | } 44 | 45 | // Alternative method to determine file status. Modified from original 46 | // version which was part of the following pull request. 47 | // https://github.com/zricethezav/gitleaks/pull/463 48 | func status(wt *git.Worktree) (git.Status, error) { 49 | c := exec.Command("git", "status", "--porcelain", "-z") 50 | c.Dir = wt.Filesystem.Root() 51 | 52 | out, err := c.Output() 53 | if err != nil { 54 | return wt.Status() 55 | } 56 | 57 | lines := strings.Split(string(out), "\000") 58 | status := make(map[string]*git.FileStatus, len(lines)) 59 | 60 | for _, line := range lines { 61 | if len(line) == 0 { 62 | continue 63 | } 64 | 65 | ltrim := strings.TrimLeft(line, " ") 66 | 67 | pathStatusCode := strings.SplitN(ltrim, " ", 2) 68 | if len(pathStatusCode) != 2 { 69 | continue 70 | } 71 | 72 | statusCode := []byte(pathStatusCode[0])[0] 73 | path := strings.Trim(pathStatusCode[1], " ") 74 | 75 | status[path] = &git.FileStatus{ 76 | Staging: git.StatusCode(statusCode), 77 | } 78 | } 79 | 80 | return status, err 81 | } 82 | -------------------------------------------------------------------------------- /internal/shell/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "os/exec" 9 | 10 | "github.com/creack/pty" 11 | ) 12 | 13 | func Run(w io.Writer, command string, args []string) error { 14 | cmd := exec.Command(command, args...) 15 | fh, err := pty.Start(cmd) 16 | if err != nil { 17 | var execError *exec.Error 18 | 19 | if errors.As(err, &execError) { 20 | return fmt.Errorf("unable to exec command: %v: %w", execError.Name, execError.Err) 21 | } 22 | 23 | return fmt.Errorf("unable to exec command: %w", err) 24 | } 25 | defer fh.Close() 26 | 27 | if _, err = io.Copy(w, fh); err != nil { 28 | var pathError *fs.PathError 29 | 30 | if !errors.As(err, &pathError) { 31 | return fmt.Errorf("unable to copy commit output: %w", err) 32 | } 33 | 34 | if pathError.Path != "/dev/ptmx" { 35 | return fmt.Errorf("unable to copy commit output: %v: %w", pathError.Path, pathError.Err) 36 | } 37 | } 38 | 39 | if err := cmd.Wait(); err != nil { 40 | var exitErr *exec.ExitError 41 | 42 | if errors.As(err, &exitErr) { 43 | return fmt.Errorf("non-zero exit code returned: %w", err) 44 | } 45 | 46 | return fmt.Errorf("unable to exec command: %w", err) 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/snapshot/snapshot.go: -------------------------------------------------------------------------------- 1 | package snapshot 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/mikelorant/committed/internal/repository" 9 | 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type Snapshot struct { 14 | Emoji string `yaml:"emoji,omitempty"` 15 | Summary string `yaml:"summary,omitempty"` 16 | Body string `yaml:"body,omitempty"` 17 | Footer string `yaml:"footer,omitempty"` 18 | Author repository.User `yaml:"author,omitempty"` 19 | Amend bool `yaml:"amend,omitempty"` 20 | Restore bool `yaml:"restore,omitempty"` 21 | } 22 | 23 | var ( 24 | errReader = errors.New("empty reader") 25 | errWriter = errors.New("empty writer") 26 | ) 27 | 28 | func (s *Snapshot) Load(fh io.Reader) (Snapshot, error) { 29 | var snap Snapshot 30 | 31 | if fh == nil { 32 | return snap, errReader 33 | } 34 | 35 | err := yaml.NewDecoder(fh).Decode(&snap) 36 | switch { 37 | case err == nil: 38 | case errors.Is(err, io.EOF): 39 | default: 40 | return snap, fmt.Errorf("unable to decode snapshot: %w", err) 41 | } 42 | 43 | return snap, nil 44 | } 45 | 46 | func (s *Snapshot) Save(fh io.WriteCloser, snap Snapshot) error { 47 | if fh == nil { 48 | return errWriter 49 | } 50 | 51 | err := yaml.NewEncoder(fh).Encode(&snap) 52 | if err != nil { 53 | return fmt.Errorf("unable to encode snapshot: %w", err) 54 | } 55 | defer fh.Close() 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/terminal/terminal.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/config" 5 | 6 | "github.com/rivo/uniseg" 7 | ) 8 | 9 | type graphemes struct { 10 | codepoints []rune 11 | width int 12 | } 13 | 14 | func Set(c config.Compatibility) { 15 | uniseg.GraphemeClusterWidthOverrides = overrideGraphemeClusterWidth(c) 16 | } 17 | 18 | func Clear() { 19 | uniseg.GraphemeClusterWidthOverrides = nil 20 | } 21 | 22 | func overrideGraphemeClusterWidth(c config.Compatibility) map[string]int { 23 | gs := make([]graphemes, 0) 24 | 25 | switch c { 26 | case config.CompatibilityUnicode9: 27 | gs = append(gs, overrideVS16()...) 28 | default: 29 | } 30 | 31 | return overrides(gs) 32 | } 33 | 34 | // Grapheme clusters using variant selector 16 35 | // had their widths changed as part of Unicode 14. 36 | // Unicode < 14 = 1. 37 | // Unicode >= 14 = 2. 38 | // Required for: 39 | // - macOS Terminal (2.12.7) 40 | // - iTerm2 (3.4.23) 41 | // - VSCode (1.87.0) 42 | // - Alacritty (0.13.1) 43 | // - WezTerm (20240203) 44 | func overrideVS16() []graphemes { 45 | return []graphemes{ 46 | {codepoints: []rune{0x203c, 0xfe0f}, width: 1}, // ‼️ 47 | {codepoints: []rune{0x21a9, 0xfe0f}, width: 1}, // ↩️ 48 | {codepoints: []rune{0x2601, 0xfe0f}, width: 1}, // ☁️ 49 | {codepoints: []rune{0x267b, 0xfe0f}, width: 1}, // ♻️ 50 | {codepoints: []rune{0x2697, 0xfe0f}, width: 1}, // ⚗️ 51 | {codepoints: []rune{0x2699, 0xfe0f}, width: 1}, // ⚙️ 52 | {codepoints: []rune{0x26b0, 0xfe0f}, width: 1}, // ⚰️ 53 | {codepoints: []rune{0x270f, 0xfe0f}, width: 1}, // ✏️ 54 | {codepoints: []rune{0x2b06, 0xfe0f}, width: 1}, // ⬆️ 55 | {codepoints: []rune{0x2b07, 0xfe0f}, width: 1}, // ⬇️ 56 | } 57 | } 58 | 59 | func overrides(gs []graphemes) map[string]int { 60 | overrides := make(map[string]int, len(gs)) 61 | for _, g := range gs { 62 | key := string(g.codepoints) 63 | overrides[key] = g.width 64 | } 65 | 66 | return overrides 67 | } 68 | -------------------------------------------------------------------------------- /internal/theme/theme.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/mikelorant/committed/internal/config" 7 | 8 | tint "github.com/lrstanley/bubbletint" 9 | "github.com/muesli/termenv" 10 | ) 11 | 12 | type Theme struct { 13 | ID string 14 | Registry *tint.Registry 15 | } 16 | 17 | type Tint struct { 18 | Default tint.Tint 19 | Defaults []tint.Tint 20 | } 21 | 22 | func New(t Tint) Theme { 23 | reg := tint.NewRegistry(t.Default, t.Defaults...) 24 | 25 | return Theme{ 26 | ID: reg.ID(), 27 | Registry: reg, 28 | } 29 | } 30 | 31 | func (t *Theme) Next() { 32 | ids := t.ListID() 33 | l := len(t.ListID()) 34 | 35 | switch t.ID { 36 | case ids[l-1]: 37 | t.Set(ids[0]) 38 | default: 39 | t.Registry.NextTint() 40 | } 41 | 42 | t.ID = t.Registry.ID() 43 | } 44 | 45 | func (t *Theme) Set(id string) bool { 46 | if ok := t.Registry.SetTintID(id); !ok { 47 | return false 48 | } 49 | 50 | t.ID = id 51 | 52 | return true 53 | } 54 | 55 | func (t *Theme) ListID() []string { 56 | var ts []string 57 | 58 | for _, t := range t.Registry.Tints() { 59 | ts = append(ts, t.ID()) 60 | } 61 | 62 | return ts 63 | } 64 | 65 | func (t *Theme) List() []tint.Tint { 66 | return t.Registry.Tints() 67 | } 68 | 69 | func Default(clr config.Colour) Tint { 70 | dark := Tint{ 71 | Default: tint.DefaultTints()[29], 72 | Defaults: tint.DefaultTints(), 73 | } 74 | 75 | light := Tint{ 76 | Default: tint.DefaultTints()[30], 77 | Defaults: tint.DefaultTints(), 78 | } 79 | 80 | switch { 81 | case clr == config.ColourDark: 82 | return dark 83 | case clr == config.ColourLight: 84 | return light 85 | case termenv.NewOutput(os.Stdout).HasDarkBackground(): 86 | return dark 87 | } 88 | 89 | return light 90 | } 91 | -------------------------------------------------------------------------------- /internal/ui/body/style.go: -------------------------------------------------------------------------------- 1 | package body 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/theme" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | type Styles struct { 11 | boundary lipgloss.Style 12 | focusBoundary lipgloss.Style 13 | textAreaPlaceholder lipgloss.Style 14 | textAreaPrompt lipgloss.Style 15 | textAreaFocusedText lipgloss.Style 16 | textAreaBlurredText lipgloss.Style 17 | textAreaCursorStyle lipgloss.Style 18 | } 19 | 20 | func defaultStyles(th theme.Theme) Styles { 21 | var s Styles 22 | 23 | clr := colour.New(th).Body() 24 | 25 | s.boundary = lipgloss.NewStyle(). 26 | Width(74). 27 | MarginTop(1). 28 | MarginBottom(1). 29 | MarginLeft(4). 30 | Align(lipgloss.Left, lipgloss.Top). 31 | BorderStyle(lipgloss.NormalBorder()). 32 | BorderForeground(clr.Boundary). 33 | Padding(0, 1, 0, 1) 34 | 35 | s.focusBoundary = s.boundary. 36 | BorderForeground(clr.FocusBoundary) 37 | 38 | s.textAreaPlaceholder = lipgloss.NewStyle(). 39 | Foreground(clr.TextAreaPlaceholder) 40 | 41 | s.textAreaPrompt = lipgloss.NewStyle(). 42 | Foreground(clr.TextAreaPrompt) 43 | 44 | s.textAreaFocusedText = lipgloss.NewStyle(). 45 | Foreground(clr.TextAreaFocusedText) 46 | 47 | s.textAreaBlurredText = lipgloss.NewStyle(). 48 | Foreground(clr.TextAreaBlurredText) 49 | 50 | s.textAreaCursorStyle = lipgloss.NewStyle(). 51 | Foreground(clr.TextAreaCursorStyle) 52 | 53 | return s 54 | } 55 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/blur.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ │ 4 | └──────────────────────────────────────────────────────────────────────────┘ 5 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/body.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ body │ 4 | │ │ 5 | │ │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | └──────────────────────────────────────────────────────────────────────────┘ 10 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/body_multiline.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ line 1 │ 4 | │ line 2 │ 5 | │ line 3 │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | └──────────────────────────────────────────────────────────────────────────┘ 10 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/default.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ │ 4 | │ │ 5 | │ │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | └──────────────────────────────────────────────────────────────────────────┘ 10 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/dimensions.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ body │ 4 | │ │ 5 | │ │ 6 | └──────────────────────────────────────────────────────────────────────────┘ 7 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/empty.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ │ 4 | │ │ 5 | │ │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | └──────────────────────────────────────────────────────────────────────────┘ 10 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/focus.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ │ 4 | └──────────────────────────────────────────────────────────────────────────┘ 5 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/placeholder.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ placeholder │ 4 | └──────────────────────────────────────────────────────────────────────────┘ 5 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/reflow_combination.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ 1234567890 │ 4 | │ 1 2 3 4 │ 5 | │ 56 │ 6 | └──────────────────────────────────────────────────────────────────────────┘ 7 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/reflow_multiple_words.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ 1 2 3 4 │ 4 | │ 56 │ 5 | │ │ 6 | └──────────────────────────────────────────────────────────────────────────┘ 7 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/reflow_single_world.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ 1234567890 │ 4 | │ │ 5 | │ │ 6 | └──────────────────────────────────────────────────────────────────────────┘ 7 | -------------------------------------------------------------------------------- /internal/ui/body/testdata/tab.golden: -------------------------------------------------------------------------------- 1 | 2 | ┌──────────────────────────────────────────────────────────────────────────┐ 3 | │ before after │ 4 | └──────────────────────────────────────────────────────────────────────────┘ 5 | -------------------------------------------------------------------------------- /internal/ui/defaults.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/commit" 5 | "github.com/mikelorant/committed/internal/config" 6 | "github.com/mikelorant/committed/internal/theme" 7 | ) 8 | 9 | func (m *Model) defaults(cfg config.Config) { 10 | m.defaultEmojiType(cfg.Commit.EmojiType) 11 | m.defaultFocus(cfg.View.Focus) 12 | m.defaultSignoff(cfg.Commit.Signoff) 13 | m.defaultTheme(cfg.View.Theme, cfg.View.Colour) 14 | } 15 | 16 | func (m *Model) defaultEmojiType(et config.EmojiType) { 17 | m.emojiType = et 18 | } 19 | 20 | func (m *Model) defaultFocus(focus config.Focus) { 21 | switch focus { 22 | case config.FocusAuthor: 23 | m.focus = authorComponent 24 | case config.FocusEmoji: 25 | m.focus = emojiComponent 26 | case config.FocusSummary: 27 | m.focus = summaryComponent 28 | default: 29 | m.focus = emojiComponent 30 | } 31 | } 32 | 33 | func (m *Model) defaultSignoff(signoff bool) { 34 | m.signoff = signoff 35 | } 36 | 37 | func (m *Model) defaultTheme(th string, clr config.Colour) { 38 | t := theme.New(theme.Default(clr)) 39 | t.Set(th) 40 | 41 | m.state.Theme = t 42 | } 43 | 44 | func defaultAmendSave(st *commit.State) savedState { 45 | s := savedState{ 46 | amend: true, 47 | summary: commit.MessageToSummary(st.Repository.Head.Message), 48 | body: commit.MessageToBody(st.Repository.Head.Message), 49 | } 50 | 51 | if e := commit.MessageToEmoji(st.Emojis, st.Repository.Head.Message); e.Valid { 52 | s.emoji = e.Emoji 53 | } 54 | 55 | return s 56 | } 57 | 58 | func defaultHookEditorSave(st *commit.State) savedState { 59 | msg := st.File.Message 60 | 61 | s := savedState{ 62 | summary: commit.TrimComments(commit.MessageToSummary(msg)), 63 | body: commit.TrimComments(commit.MessageToBody(msg)), 64 | } 65 | 66 | if e := commit.MessageToEmoji(st.Emojis, msg); e.Valid { 67 | s.emoji = e.Emoji 68 | } 69 | 70 | return s 71 | } 72 | -------------------------------------------------------------------------------- /internal/ui/filterlist/paginator.go: -------------------------------------------------------------------------------- 1 | package filterlist 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/mikelorant/committed/internal/commit" 7 | "github.com/mikelorant/committed/internal/ui/colour" 8 | 9 | "github.com/charmbracelet/lipgloss" 10 | ) 11 | 12 | func verticalPaginator(pos, total int, state *commit.State) string { 13 | return strings.Join(dots(pos, total, state), "\n") 14 | } 15 | 16 | func horizontalPaginator(pos, total int, state *commit.State) string { 17 | return strings.Join(dots(pos, total, state), "") 18 | } 19 | 20 | func dots(pos, total int, state *commit.State) []string { 21 | clr := colour.New(state.Theme).FilterList() 22 | 23 | dots := make([]string, total) 24 | for i := range dots { 25 | dots[i] = paginatorDot 26 | } 27 | 28 | dots = append(dots[:pos], dots[pos:]...) 29 | dots[pos] = lipgloss.NewStyle(). 30 | Foreground(clr.PaginatorDots). 31 | Render(paginatorActiveDot) 32 | 33 | return dots 34 | } 35 | -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/blur.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? ● │ 3 | │No items. │ 4 | │ │ 5 | │ │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/default.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? test ● │ 3 | │❯ item 1 │ 4 | │ item 2 │ 5 | │ item 3 │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/down.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? title ● │ 3 | │ item 1 │ 4 | │❯ item 2 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/enter.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? title ● │ 3 | │❯ item 1 │ 4 | │ item 2 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/escape.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? Prompt: ● │ 3 | │❯ item 1 │ 4 | │ item 2 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/focus.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? ● │ 3 | │No items. │ 4 | │ │ 5 | │ │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/height.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? test ● │ 3 | │❯ item 1 │ 4 | │ item 2 │ 5 | │ item 3 │ 6 | │ │ 7 | │ │ 8 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/multiple_pages.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? ● │ 3 | │❯ item 1 ○ │ 4 | │ ○ │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/no_border.golden: -------------------------------------------------------------------------------- 1 | ? test ● 2 | ❯ item 1 ○ 3 | item 2 -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/no_items.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? test ● │ 3 | │No items. │ 4 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/no_title.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? ● │ 3 | │No items. │ 4 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/one.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? test ● │ 3 | │❯ item 1 │ 4 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/overflow_pages.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? ● │ 3 | │❯ item 1 ○ │ 4 | │ ○ │ 5 | │ ○ │ 6 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/pagedown.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? title ○ │ 3 | │❯ item 3 ● │ 4 | │ item 4 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/pagedown_lastpage.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? title ○ │ 3 | │❯ item 3 ● │ 4 | │ item 4 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/pageup.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? title ● │ 3 | │❯ item 1 ○ │ 4 | │ item 2 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/setitems.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? title ● │ 3 | │❯ newitem 1 │ 4 | │ newitem 2 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/type.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? Prompt: item ● │ 3 | │❯ item 1 │ 4 | │ item 2 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/up.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? title ● │ 3 | │❯ item 1 │ 4 | │ item 2 │ 5 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/width.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────┐ 2 | │? test ● │ 3 | │❯ item 1 ○ │ 4 | │ item 2 │ 5 | └──────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/filterlist/testdata/width_height.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────┐ 2 | │? test ● │ 3 | │❯ item 1 │ 4 | │ item 2 │ 5 | │ item 3 │ 6 | │ │ 7 | │ │ 8 | └────────────────┘ -------------------------------------------------------------------------------- /internal/ui/footer/footer.go: -------------------------------------------------------------------------------- 1 | package footer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mikelorant/committed/internal/commit" 7 | "github.com/mikelorant/committed/internal/repository" 8 | "github.com/mikelorant/committed/internal/ui/colour" 9 | 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | ) 13 | 14 | type Model struct { 15 | Author repository.User 16 | Signoff bool 17 | 18 | state *commit.State 19 | } 20 | 21 | func New(state *commit.State) Model { 22 | authors := concatSlice(state.Repository.Users, state.Config.Authors) 23 | 24 | if len(authors) == 0 { 25 | authors = []repository.User{{}} 26 | } 27 | 28 | return Model{ 29 | Author: authors[0], 30 | state: state, 31 | } 32 | } 33 | 34 | func (m Model) Init() tea.Cmd { 35 | return nil 36 | } 37 | 38 | //nolint:ireturn 39 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | return m, nil 41 | } 42 | 43 | func (m Model) View() string { 44 | clr := colour.New(m.state.Theme).Footer() 45 | 46 | return lipgloss.NewStyle(). 47 | Width(74). 48 | Height(1). 49 | MarginLeft(4). 50 | MarginBottom(1). 51 | Align(lipgloss.Left, lipgloss.Center). 52 | Border(lipgloss.HiddenBorder(), false, true). 53 | Padding(0, 1, 0, 1). 54 | Foreground(clr.View). 55 | Render(m.signoff()) 56 | } 57 | 58 | func (m *Model) ToggleSignoff() { 59 | m.Signoff = !m.Signoff 60 | } 61 | 62 | func (m Model) Value() string { 63 | if !m.Signoff { 64 | return "" 65 | } 66 | 67 | return m.signoff() 68 | } 69 | 70 | func (m Model) signoff() string { 71 | return fmt.Sprintf("Signed-off-by: %s <%s>", m.Author.Name, m.Author.Email) 72 | } 73 | 74 | func ToModel(m tea.Model, c tea.Cmd) (Model, tea.Cmd) { 75 | return m.(Model), c 76 | } 77 | 78 | func concatSlice[T any](first []T, second []T) []T { 79 | n := len(first) 80 | return append(first[:n:n], second...) 81 | } 82 | -------------------------------------------------------------------------------- /internal/ui/footer/testdata/default.golden: -------------------------------------------------------------------------------- 1 | Signed-off-by: John Doe 2 | -------------------------------------------------------------------------------- /internal/ui/footer/testdata/empty.golden: -------------------------------------------------------------------------------- 1 | Signed-off-by: <> 2 | -------------------------------------------------------------------------------- /internal/ui/footer/testdata/signoff.golden: -------------------------------------------------------------------------------- 1 | Signed-off-by: John Doe 2 | -------------------------------------------------------------------------------- /internal/ui/header/emoji.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/mikelorant/committed/internal/config" 8 | "github.com/mikelorant/committed/internal/emoji" 9 | "github.com/mikelorant/committed/internal/fuzzy" 10 | 11 | "github.com/charmbracelet/bubbles/list" 12 | "github.com/rivo/uniseg" 13 | ) 14 | 15 | type listItem struct { 16 | emoji emoji.Emoji 17 | compatibility config.Compatibility 18 | } 19 | 20 | type fuzzyItem struct { 21 | emoji emoji.Emoji 22 | } 23 | 24 | func (i listItem) Title() string { 25 | const maxEmojiWidth = 2 26 | 27 | padLen := maxEmojiWidth - uniseg.StringWidth(i.emoji.Character) 28 | padding := strings.Repeat(" ", padLen) 29 | 30 | return fmt.Sprintf("%s%s - %s", i.emoji.Character, padding, i.emoji.Description) 31 | } 32 | 33 | func (i listItem) Description() string { 34 | return i.emoji.Name 35 | } 36 | 37 | func (i listItem) FilterValue() string { 38 | return i.emoji.Name 39 | } 40 | 41 | func (i fuzzyItem) Terms() []string { 42 | return []string{ 43 | i.emoji.Description, 44 | i.emoji.Shortcode, 45 | } 46 | } 47 | 48 | func WithCompatibility(c config.Compatibility) func(*listItem) { 49 | return func(i *listItem) { 50 | i.compatibility = c 51 | } 52 | } 53 | 54 | func castToListItems(emojis []emoji.Emoji, opts ...func(*listItem)) []list.Item { 55 | res := make([]list.Item, len(emojis)) 56 | for i, e := range emojis { 57 | var item listItem 58 | item.emoji = e 59 | for _, o := range opts { 60 | o(&item) 61 | } 62 | res[i] = item 63 | } 64 | 65 | return res 66 | } 67 | 68 | func castToFuzzyItems(emojis []emoji.Emoji) []fuzzy.Item { 69 | res := make([]fuzzy.Item, len(emojis)) 70 | for i, e := range emojis { 71 | var item fuzzyItem 72 | item.emoji = e 73 | res[i] = item 74 | } 75 | 76 | return res 77 | } 78 | -------------------------------------------------------------------------------- /internal/ui/header/testdata/amend_emoji.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🎨 │ │ │ 3/50 ● Amend 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/amend_emoji_summary.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🎨 │ │ summary │ 10/50 ● Amend 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/amend_summary.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ summary │ 7/50 ● Amend 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/blur_emoji.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/blur_summary.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/config_above.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │? Choose an emoji: ● │ 3 | │❯ 🎨 - test │ 4 | │ │ 5 | │ │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | └──────────────────────────────────────────────────────────────────────────┘ 13 | 14 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 15 | │ │ │ │ 0/50 ● New 16 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/config_below.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │❯ 🎨 - test │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/default.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/emoji_empty_delete.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │❯ 🎨 - Improve structure / format of the code. ○ │ 8 | │ ⚡️ - Improve performance. ○ │ 9 | │ 🔥 - Remove code or files. ○ │ 10 | │ 🐛 - Fix a bug. ○ │ 11 | │ 🚑 - Critical hotfix. ○ │ 12 | │ ✨ - Introduce new features. ○ │ 13 | │ 📝 - Add or update documentation. ○ │ 14 | │ 🚀 - Deploy stuff. │ 15 | │ 💄 - Add or update the UI and style files. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/expand.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │❯ 🎨 - test │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/expand_emojis.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │❯ 🎨 - Improve structure / format of the code. ○ │ 8 | │ ⚡️ - Improve performance. ○ │ 9 | │ 🔥 - Remove code or files. ○ │ 10 | │ 🐛 - Fix a bug. ○ │ 11 | │ 🚑 - Critical hotfix. ○ │ 12 | │ ✨ - Introduce new features. ○ │ 13 | │ 📝 - Add or update documentation. ○ │ 14 | │ 🚀 - Deploy stuff. │ 15 | │ 💄 - Add or update the UI and style files. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/expand_emojis_page_down.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ○ │ 7 | │❯ 🎉 - Begin a project. ● │ 8 | │ ✅ - Add or update tests. ○ │ 9 | │ 🔒 - Fix security issues. ○ │ 10 | │ 🔐 - Add or update secrets. ○ │ 11 | │ 🔖 - Release / Version tags. ○ │ 12 | │ 🚨 - Remove linter warnings. ○ │ 13 | │ 🚧 - Work in progress. ○ │ 14 | │ 💚 - Fix CI Build. │ 15 | │ ⬇️ - Downgrade dependencies. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/filter_emoji.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: bug ● │ 7 | │❯ 🐛 - Fix a bug. │ 8 | │ 🏗 - Make architectural changes. │ 9 | │ 👔 - Add or update business logic. │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/filter_emoji_no_match.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: test test test ● │ 7 | │No items. │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/focus.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/placeholder.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ placeholder │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🎨 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │❯ 🎨 - Improve structure / format of the code. ○ │ 8 | │ ⚡️ - Improve performance. ○ │ 9 | │ 🔥 - Remove code or files. ○ │ 10 | │ 🐛 - Fix a bug. ○ │ 11 | │ 🚑 - Critical hotfix. ○ │ 12 | │ ✨ - Introduce new features. ○ │ 13 | │ 📝 - Add or update documentation. ○ │ 14 | │ 🚀 - Deploy stuff. │ 15 | │ 💄 - Add or update the UI and style files. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_delete.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │❯ 🎨 - Improve structure / format of the code. ○ │ 8 | │ ⚡️ - Improve performance. ○ │ 9 | │ 🔥 - Remove code or files. ○ │ 10 | │ 🐛 - Fix a bug. ○ │ 11 | │ 🚑 - Critical hotfix. ○ │ 12 | │ ✨ - Introduce new features. ○ │ 13 | │ 📝 - Add or update documentation. ○ │ 14 | │ 🚀 - Deploy stuff. │ 15 | │ 💄 - Add or update the UI and style files. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_down.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🔥 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │ 🎨 - Improve structure / format of the code. ○ │ 8 | │ ⚡️ - Improve performance. ○ │ 9 | │❯ 🔥 - Remove code or files. ○ │ 10 | │ 🐛 - Fix a bug. ○ │ 11 | │ 🚑 - Critical hotfix. ○ │ 12 | │ ✨ - Introduce new features. ○ │ 13 | │ 📝 - Add or update documentation. ○ │ 14 | │ 🚀 - Deploy stuff. │ 15 | │ 💄 - Add or update the UI and style files. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_down_up.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ ⚡️ │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │ 🎨 - Improve structure / format of the code. ○ │ 8 | │❯ ⚡️ - Improve performance. ○ │ 9 | │ 🔥 - Remove code or files. ○ │ 10 | │ 🐛 - Fix a bug. ○ │ 11 | │ 🚑 - Critical hotfix. ○ │ 12 | │ ✨ - Introduce new features. ○ │ 13 | │ 📝 - Add or update documentation. ○ │ 14 | │ 🚀 - Deploy stuff. │ 15 | │ 💄 - Add or update the UI and style files. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_filter.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🐛 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: bug ● │ 7 | │❯ 🐛 - Fix a bug. │ 8 | │ 🏗 - Make architectural changes. │ 9 | │ 👔 - Add or update business logic. │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_filter_clear.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🎨 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ● │ 7 | │❯ 🎨 - Improve structure / format of the code. ○ │ 8 | │ ⚡️ - Improve performance. ○ │ 9 | │ 🔥 - Remove code or files. ○ │ 10 | │ 🐛 - Fix a bug. ○ │ 11 | │ 🚑 - Critical hotfix. ○ │ 12 | │ ✨ - Introduce new features. ○ │ 13 | │ 📝 - Add or update documentation. ○ │ 14 | │ 🚀 - Deploy stuff. │ 15 | │ 💄 - Add or update the UI and style files. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_page_down_last_page.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🧐 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ○ │ 7 | │❯ 🧐 - Data exploration/inspection. ○ │ 8 | │ ⚰️ - Remove dead code. ○ │ 9 | │ 🧪 - Add a failing test. ○ │ 10 | │ 👔 - Add or update business logic. ○ │ 11 | │ 🩺 - Add or update healthcheck. ○ │ 12 | │ 🧱 - Infrastructure related changes. ○ │ 13 | │ 💸 - Add sponsorships or money related infrastructure. ● │ 14 | │ 🧵 - Add or update code related to multithreading or concurrency. │ 15 | │ 🦺 - Add or update code related to validation. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_page_down_last_page_exceeded.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🧐 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ○ │ 7 | │❯ 🧐 - Data exploration/inspection. ○ │ 8 | │ ⚰️ - Remove dead code. ○ │ 9 | │ 🧪 - Add a failing test. ○ │ 10 | │ 👔 - Add or update business logic. ○ │ 11 | │ 🩺 - Add or update healthcheck. ○ │ 12 | │ 🧱 - Infrastructure related changes. ○ │ 13 | │ 💸 - Add sponsorships or money related infrastructure. ● │ 14 | │ 🧵 - Add or update code related to multithreading or concurrency. │ 15 | │ 🦺 - Add or update code related to validation. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_page_down_last_page_exceeded_page_up.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🔍 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ○ │ 7 | │❯ 🔍 - Improve SEO. ○ │ 8 | │ 🏷 - Add or update types. ○ │ 9 | │ 🌱 - Add or update seed files. ○ │ 10 | │ 🚩 - Add, update, or remove feature flags. ○ │ 11 | │ 🥅 - Catch errors. ○ │ 12 | │ 💫 - Add or update animations and transitions. ● │ 13 | │ 🗑 - Deprecate code that needs to be cleaned up. ○ │ 14 | │ 🛂 - Work on code related to authorization, roles and permissions. │ 15 | │ 🩹 - Simple fix for a non-critical issue. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_page_down_last_page_page_up.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🔍 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ○ │ 7 | │❯ 🔍 - Improve SEO. ○ │ 8 | │ 🏷 - Add or update types. ○ │ 9 | │ 🌱 - Add or update seed files. ○ │ 10 | │ 🚩 - Add, update, or remove feature flags. ○ │ 11 | │ 🥅 - Catch errors. ○ │ 12 | │ 💫 - Add or update animations and transitions. ● │ 13 | │ 🗑 - Deprecate code that needs to be cleaned up. ○ │ 14 | │ 🛂 - Work on code related to authorization, roles and permissions. │ 15 | │ 🩹 - Simple fix for a non-critical issue. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/select_emoji_page_down_page_up.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ 🎉 │ │ │ 3/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an emoji: ○ │ 7 | │❯ 🎉 - Begin a project. ● │ 8 | │ ✅ - Add or update tests. ○ │ 9 | │ 🔒 - Fix security issues. ○ │ 10 | │ 🔐 - Add or update secrets. ○ │ 11 | │ 🔖 - Release / Version tags. ○ │ 12 | │ 🚨 - Remove linter warnings. ○ │ 13 | │ 🚧 - Work in progress. ○ │ 14 | │ 💚 - Fix CI Build. │ 15 | │ ⬇️ - Downgrade dependencies. │ 16 | └──────────────────────────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_emoji.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ 🎨 │ 4/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_emoji_text.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ 🎨 text │ 9/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_empty.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ │ 0/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_exceed.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ ************************************************** │ 72/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_maximum_boundary_high.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ ************************************************** │ 72/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_maximum_boundary_low.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ ************************************************** │ 51/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_normal_boundary_high.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ **************************************** │ 40/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_normal_boundary_low.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ ***** │ 5/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_short_boundary_high.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ **** │ 4/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_short_boundary_low.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ * │ 1/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_text.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ test │ 4/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_warning_boundary_high.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ ************************************************** │ 50/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/header/testdata/summary_warning_boundary_low.golden: -------------------------------------------------------------------------------- 1 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 2 | │ │ │ ***************************************** │ 41/50 ● New 3 | └────┘ └─────────────────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/help/help.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/commit" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/bubbles/viewport" 8 | tea "github.com/charmbracelet/bubbletea" 9 | ) 10 | 11 | type Model struct { 12 | focus bool 13 | state *commit.State 14 | styles Styles 15 | viewport viewport.Model 16 | } 17 | 18 | const ( 19 | defaultWidth = 72 20 | defaultHeight = 23 21 | ) 22 | 23 | func New(state *commit.State) Model { 24 | return Model{ 25 | state: state, 26 | styles: defaultStyles(state.Theme), 27 | viewport: newViewport(defaultWidth, defaultHeight, state), 28 | } 29 | } 30 | 31 | func (m Model) Init() tea.Cmd { 32 | return nil 33 | } 34 | 35 | //nolint:ireturn 36 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 37 | var cmd tea.Cmd 38 | 39 | //nolint:gocritic 40 | switch msg.(type) { 41 | case colour.Msg: 42 | m.styles = defaultStyles(m.state.Theme) 43 | styleViewport(&m.viewport, m.state) 44 | } 45 | 46 | if m.focus { 47 | m.viewport, cmd = m.viewport.Update(msg) 48 | } 49 | 50 | return m, cmd 51 | } 52 | 53 | func (m Model) View() string { 54 | return m.styles.boundary.Render(m.viewport.View()) 55 | } 56 | 57 | func (m *Model) Focus() { 58 | m.focus = true 59 | } 60 | 61 | func (m *Model) Blur() { 62 | m.focus = false 63 | } 64 | 65 | func (m Model) Focused() bool { 66 | return m.focus 67 | } 68 | 69 | func (m *Model) SetContent(str string) { 70 | m.viewport.SetContent(str) 71 | } 72 | 73 | func ToModel(m tea.Model, c tea.Cmd) (Model, tea.Cmd) { 74 | return m.(Model), c 75 | } 76 | 77 | func newViewport(w, h int, state *commit.State) viewport.Model { 78 | vp := viewport.New(w, h) 79 | vp.SetContent(state.Placeholders.Help) 80 | 81 | styleViewport(&vp, state) 82 | 83 | return vp 84 | } 85 | 86 | func styleViewport(vp *viewport.Model, state *commit.State) { 87 | vp.Style = defaultStyles(state.Theme).viewport 88 | } 89 | -------------------------------------------------------------------------------- /internal/ui/help/style.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/theme" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | type Styles struct { 11 | boundary lipgloss.Style 12 | viewport lipgloss.Style 13 | } 14 | 15 | func defaultStyles(th theme.Theme) Styles { 16 | var s Styles 17 | 18 | clr := colour.New(th).Help() 19 | 20 | s.boundary = lipgloss.NewStyle(). 21 | Width(74). 22 | MarginBottom(1). 23 | MarginLeft(4). 24 | Align(lipgloss.Left, lipgloss.Top). 25 | BorderStyle(lipgloss.NormalBorder()). 26 | BorderForeground(clr.Boundary). 27 | Padding(0, 1, 0, 1) 28 | 29 | s.viewport = lipgloss.NewStyle(). 30 | Foreground(clr.Viewport) 31 | 32 | return s 33 | } 34 | -------------------------------------------------------------------------------- /internal/ui/help/testdata/blur.golden: -------------------------------------------------------------------------------- 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 | └──────────────────────────────────────────────────────────────────────────┘ 26 | -------------------------------------------------------------------------------- /internal/ui/help/testdata/default.golden: -------------------------------------------------------------------------------- 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 | └──────────────────────────────────────────────────────────────────────────┘ 26 | -------------------------------------------------------------------------------- /internal/ui/help/testdata/focus.golden: -------------------------------------------------------------------------------- 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 | └──────────────────────────────────────────────────────────────────────────┘ 26 | -------------------------------------------------------------------------------- /internal/ui/help/testdata/multiline.golden: -------------------------------------------------------------------------------- 1 | ┌──────────────────────────────────────────────────────────────────────────┐ 2 | │ line 1 │ 3 | │ line 2 │ 4 | │ line 3 │ 5 | │ │ 6 | │ │ 7 | │ │ 8 | │ │ 9 | │ │ 10 | │ │ 11 | │ │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | └──────────────────────────────────────────────────────────────────────────┘ 26 | -------------------------------------------------------------------------------- /internal/ui/info/author.go: -------------------------------------------------------------------------------- 1 | package info 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mikelorant/committed/internal/fuzzy" 7 | "github.com/mikelorant/committed/internal/repository" 8 | 9 | "github.com/charmbracelet/bubbles/list" 10 | ) 11 | 12 | type listItem struct { 13 | author repository.User 14 | } 15 | 16 | type fuzzyItem struct { 17 | author repository.User 18 | } 19 | 20 | func (i listItem) Title() string { 21 | return fmt.Sprintf("%s <%s>", i.author.Name, i.author.Email) 22 | } 23 | 24 | func (i listItem) Description() string { 25 | return i.author.Name 26 | } 27 | 28 | func (i listItem) FilterValue() string { 29 | return i.author.Name 30 | } 31 | 32 | func (i fuzzyItem) Terms() []string { 33 | return []string{ 34 | i.author.Name, 35 | i.author.Email, 36 | } 37 | } 38 | 39 | func castToListItems(authors []repository.User) []list.Item { 40 | res := make([]list.Item, len(authors)) 41 | for i, a := range authors { 42 | var item listItem 43 | item.author = a 44 | res[i] = item 45 | } 46 | 47 | return res 48 | } 49 | 50 | func castToFuzzyItems(authors []repository.User) []fuzzy.Item { 51 | res := make([]fuzzy.Item, len(authors)) 52 | for i, e := range authors { 53 | var item fuzzyItem 54 | item.author = e 55 | res[i] = item 56 | } 57 | 58 | return res 59 | } 60 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/blur.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/config_user_only.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe │ 8 | │ │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/default.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/expand.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe │ 8 | │ │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/focus.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/multiple_users.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │ John Doe │ 8 | │❯ John Doe │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/multiple_users_config_default.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe │ 8 | │ John Doe │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/multiple_users_filtered.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: example.org ● │ 7 | │❯ John Doe │ 8 | │ │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/multiple_users_repository_config_default.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe │ 8 | │ John Doe │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/multiple_users_repository_config_default_multiple.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe ○ │ 8 | │ John Doe │ 9 | │ John Doe │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/multiple_users_selected.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │ John Doe │ 8 | │❯ John Doe │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/no_local.golden: -------------------------------------------------------------------------------- 1 | commit 1 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/no_users.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: <> 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/remote.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master, origin/master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/repository_user_only.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe │ 8 | │ │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/tags.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master, tag: v1.0.0) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/users_both_empty.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: <> 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ <> │ 8 | │ │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/users_both_nil.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: <> 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ <> │ 8 | │ │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/users_mixed.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe │ 8 | │ John Doe │ 9 | │ │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/info/testdata/users_mixed_multiple.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌──────────────────────────────────────────────────────────────────────────┐ 6 | │? Choose an author: ● │ 7 | │❯ John Doe ○ │ 8 | │ John Doe │ 9 | │ John Doe │ 10 | └──────────────────────────────────────────────────────────────────────────┘ 11 | -------------------------------------------------------------------------------- /internal/ui/message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/mikelorant/committed/internal/theme" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | ) 11 | 12 | type Model struct { 13 | emoji string 14 | summary string 15 | body string 16 | footer string 17 | styles Styles 18 | } 19 | 20 | type State struct { 21 | Emoji string 22 | Summary string 23 | Body string 24 | Footer string 25 | Theme theme.Theme 26 | } 27 | 28 | func New(state State) Model { 29 | return Model{ 30 | emoji: state.Emoji, 31 | summary: state.Summary, 32 | body: state.Body, 33 | footer: state.Footer, 34 | styles: defaultStyles(state.Theme), 35 | } 36 | } 37 | 38 | func (m Model) Init() tea.Cmd { 39 | return nil 40 | } 41 | 42 | //nolint:ireturn 43 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 44 | return m, nil 45 | } 46 | 47 | func (m Model) View() string { 48 | var str []string 49 | 50 | if m.summary != "" { 51 | switch { 52 | case m.emoji != "": 53 | str = append(str, fmt.Sprintf("%v %v", m.emoji, m.summary)) 54 | default: 55 | str = append(str, m.summary) 56 | } 57 | } 58 | 59 | if m.body != "" { 60 | str = append(str, m.body) 61 | } 62 | 63 | if m.footer != "" { 64 | str = append(str, m.footer) 65 | } 66 | 67 | msg := strings.Join(str, "\n\n") 68 | 69 | return m.styles.message.Render(msg) 70 | } 71 | -------------------------------------------------------------------------------- /internal/ui/message/message_test.go: -------------------------------------------------------------------------------- 1 | package message_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mikelorant/committed/internal/config" 7 | "github.com/mikelorant/committed/internal/theme" 8 | "github.com/mikelorant/committed/internal/ui/message" 9 | "github.com/mikelorant/committed/internal/ui/uitest" 10 | 11 | "github.com/hexops/autogold/v2" 12 | ) 13 | 14 | func TestModel(t *testing.T) { 15 | t.Parallel() 16 | 17 | type args struct { 18 | emoji string 19 | summary string 20 | body string 21 | footer string 22 | } 23 | 24 | tests := []struct { 25 | name string 26 | args args 27 | }{ 28 | { 29 | name: "default", 30 | }, 31 | { 32 | name: "summary", 33 | args: args{ 34 | summary: "summary", 35 | }, 36 | }, 37 | { 38 | name: "emoji_summary", 39 | args: args{ 40 | emoji: ":art:", 41 | summary: "summary", 42 | }, 43 | }, 44 | { 45 | name: "summary_body", 46 | args: args{ 47 | summary: "summary", 48 | body: "body", 49 | }, 50 | }, 51 | { 52 | name: "summary_body_multiline", 53 | args: args{ 54 | summary: "summary", 55 | body: "line 1\nline 2\nline 3", 56 | }, 57 | }, 58 | { 59 | name: "summary_footer", 60 | args: args{ 61 | summary: "summary", 62 | footer: "footer", 63 | }, 64 | }, 65 | { 66 | name: "all", 67 | args: args{ 68 | emoji: ":art:", 69 | summary: "summary", 70 | body: "body", 71 | footer: "footer", 72 | }, 73 | }, 74 | } 75 | 76 | for _, tt := range tests { 77 | tt := tt 78 | 79 | t.Run(tt.name, func(t *testing.T) { 80 | t.Parallel() 81 | 82 | c := message.State{ 83 | Theme: theme.New(theme.Default(config.ColourAdaptive)), 84 | Emoji: tt.args.emoji, 85 | Summary: tt.args.summary, 86 | Body: tt.args.body, 87 | Footer: tt.args.footer, 88 | } 89 | 90 | m := message.New(c) 91 | 92 | v := uitest.StripString(m.View()) 93 | autogold.ExpectFile(t, autogold.Raw(v), autogold.Name(tt.name)) 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /internal/ui/message/styles.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/theme" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | type Styles struct { 11 | message lipgloss.Style 12 | summary lipgloss.Style 13 | body lipgloss.Style 14 | footer lipgloss.Style 15 | } 16 | 17 | func defaultStyles(th theme.Theme) Styles { 18 | var s Styles 19 | 20 | clr := colour.New(th).Message() 21 | 22 | s.message = lipgloss.NewStyle(). 23 | MarginLeft(4). 24 | MarginBottom(2). 25 | Foreground(clr.Message) 26 | 27 | return s 28 | } 29 | -------------------------------------------------------------------------------- /internal/ui/message/testdata/all.golden: -------------------------------------------------------------------------------- 1 | :art: summary 2 | 3 | body 4 | 5 | footer 6 | 7 | -------------------------------------------------------------------------------- /internal/ui/message/testdata/default.golden: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /internal/ui/message/testdata/emoji_summary.golden: -------------------------------------------------------------------------------- 1 | :art: summary 2 | 3 | -------------------------------------------------------------------------------- /internal/ui/message/testdata/summary.golden: -------------------------------------------------------------------------------- 1 | summary 2 | 3 | -------------------------------------------------------------------------------- /internal/ui/message/testdata/summary_body.golden: -------------------------------------------------------------------------------- 1 | summary 2 | 3 | body 4 | 5 | -------------------------------------------------------------------------------- /internal/ui/message/testdata/summary_body_multiline.golden: -------------------------------------------------------------------------------- 1 | summary 2 | 3 | line 1 4 | line 2 5 | line 3 6 | 7 | -------------------------------------------------------------------------------- /internal/ui/message/testdata/summary_footer.golden: -------------------------------------------------------------------------------- 1 | summary 2 | 3 | footer 4 | 5 | -------------------------------------------------------------------------------- /internal/ui/option/help/help.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | "github.com/charmbracelet/lipgloss" 6 | ) 7 | 8 | type Model struct { 9 | Width int 10 | Height int 11 | Content string 12 | Styles Styles 13 | } 14 | 15 | type Styles struct{} 16 | 17 | const ( 18 | defaultWidth = 20 19 | defaultHeight = 5 20 | ) 21 | 22 | func New() Model { 23 | return Model{ 24 | Width: defaultWidth, 25 | Height: defaultHeight, 26 | } 27 | } 28 | 29 | func (m Model) Init() tea.Cmd { 30 | return nil 31 | } 32 | 33 | //nolint:ireturn 34 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 35 | return m, nil 36 | } 37 | 38 | func (m Model) View() string { 39 | return lipgloss.NewStyle(). 40 | Width(m.Width). 41 | Height(m.Height). 42 | MaxWidth(m.Width). 43 | MaxHeight(m.Height). 44 | Render(m.renderHelp()) 45 | } 46 | 47 | func (m *Model) SetContent(content string) { 48 | m.Content = content 49 | } 50 | 51 | func ToModel(m tea.Model, c tea.Cmd) (Model, tea.Cmd) { 52 | return m.(Model), c 53 | } 54 | 55 | func (m Model) renderHelp() string { 56 | return m.Content 57 | } 58 | -------------------------------------------------------------------------------- /internal/ui/option/help/help_test.go: -------------------------------------------------------------------------------- 1 | package help_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/mikelorant/committed/internal/ui/option/help" 8 | "github.com/mikelorant/committed/internal/ui/uitest" 9 | 10 | "github.com/hexops/autogold/v2" 11 | ) 12 | 13 | func TestModel(t *testing.T) { 14 | t.Parallel() 15 | 16 | type args struct { 17 | model func(help.Model) help.Model 18 | } 19 | 20 | type want struct { 21 | model func(help.Model) 22 | content string 23 | } 24 | 25 | tests := []struct { 26 | name string 27 | args args 28 | want want 29 | }{ 30 | { 31 | name: "help", 32 | args: args{ 33 | model: func(m help.Model) help.Model { 34 | m.SetContent("help") 35 | 36 | return m 37 | }, 38 | }, 39 | }, 40 | { 41 | name: "help_long", 42 | args: args{ 43 | model: func(m help.Model) help.Model { 44 | m.SetContent(strings.Repeat("1234567890", 10)) 45 | 46 | return m 47 | }, 48 | }, 49 | }, 50 | { 51 | name: "help_multiline", 52 | args: args{ 53 | model: func(m help.Model) help.Model { 54 | m.SetContent(strings.Repeat("1234567890\n", 10)) 55 | 56 | return m 57 | }, 58 | }, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | tt := tt 64 | 65 | t.Run(tt.name, func(t *testing.T) { 66 | t.Parallel() 67 | 68 | m := help.New() 69 | 70 | if tt.args.model != nil { 71 | m = tt.args.model(m) 72 | } 73 | 74 | v := uitest.StripString(m.View()) 75 | autogold.ExpectFile(t, autogold.Raw(v), autogold.Name(tt.name)) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/ui/option/help/testdata/help.golden: -------------------------------------------------------------------------------- 1 | help 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /internal/ui/option/help/testdata/help_long.golden: -------------------------------------------------------------------------------- 1 | 12345678901234567890 2 | 12345678901234567890 3 | 12345678901234567890 4 | 12345678901234567890 5 | 12345678901234567890 -------------------------------------------------------------------------------- /internal/ui/option/help/testdata/help_multiline.golden: -------------------------------------------------------------------------------- 1 | 1234567890 2 | 1234567890 3 | 1234567890 4 | 1234567890 5 | 1234567890 -------------------------------------------------------------------------------- /internal/ui/option/section/style.go: -------------------------------------------------------------------------------- 1 | package section 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/theme" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | type Styles struct { 11 | category lipgloss.Style 12 | categorySelected lipgloss.Style 13 | categorySpacer lipgloss.Style 14 | categoryPrompt lipgloss.Style 15 | setting lipgloss.Style 16 | settingSelected lipgloss.Style 17 | settingSpacer lipgloss.Style 18 | settingPrompt lipgloss.Style 19 | settingJoiner lipgloss.Style 20 | } 21 | 22 | func defaultStyles(th theme.Theme) Styles { 23 | var s Styles 24 | 25 | clr := colour.New(th).OptionSection() 26 | 27 | s.category = lipgloss.NewStyle(). 28 | Foreground(clr.Category) 29 | 30 | s.categorySelected = lipgloss.NewStyle(). 31 | Foreground(clr.CategorySelected) 32 | 33 | s.categorySpacer = lipgloss.NewStyle(). 34 | Foreground(clr.CategorySpacer). 35 | SetString(" ") 36 | 37 | s.categoryPrompt = lipgloss.NewStyle(). 38 | Foreground(clr.CategoryPrompt). 39 | SetString("❯") 40 | 41 | s.setting = lipgloss.NewStyle(). 42 | Foreground(clr.Setting) 43 | 44 | s.settingSelected = lipgloss.NewStyle(). 45 | Foreground(clr.SettingSelected) 46 | 47 | s.settingSpacer = lipgloss.NewStyle(). 48 | Foreground(clr.SettingSpacer). 49 | SetString(" ") 50 | 51 | s.settingPrompt = lipgloss.NewStyle(). 52 | Foreground(clr.SettingPrompt). 53 | SetString("└▸") 54 | 55 | s.settingJoiner = lipgloss.NewStyle(). 56 | Foreground(clr.SettingJoiner). 57 | SetString("│ ") 58 | 59 | return s 60 | } 61 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/category_only.golden: -------------------------------------------------------------------------------- 1 | First 2 | 1 3 | 2 4 | 3 5 | 4 6 | 7 | ❯ Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/first.golden: -------------------------------------------------------------------------------- 1 | ❯ First 2 | └▸1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/invalid.golden: -------------------------------------------------------------------------------- 1 | First 2 | 1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/last.golden: -------------------------------------------------------------------------------- 1 | First 2 | 1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | ❯ Forth 15 | │ 1 16 | └▸2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/next.golden: -------------------------------------------------------------------------------- 1 | ❯ First 2 | │ 1 3 | └▸2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/next_category.golden: -------------------------------------------------------------------------------- 1 | First 2 | 1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | ❯ Forth 15 | └▸1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/next_limit.golden: -------------------------------------------------------------------------------- 1 | First 2 | 1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | ❯ Forth 15 | │ 1 16 | └▸2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/previous.golden: -------------------------------------------------------------------------------- 1 | ❯ First 2 | │ 1 3 | │ 2 4 | └▸3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/previous_category.golden: -------------------------------------------------------------------------------- 1 | First 2 | 1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | ❯ Third 10 | │ 1 11 | │ 2 12 | └▸3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/previous_limit.golden: -------------------------------------------------------------------------------- 1 | ❯ First 2 | └▸1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/section/testdata/reset.golden: -------------------------------------------------------------------------------- 1 | ❯ First 2 | └▸1 3 | 2 4 | 3 5 | 4 6 | 7 | Second 8 | 9 | Third 10 | 1 11 | 2 12 | 3 13 | 14 | Forth 15 | 1 16 | 2 17 | -------------------------------------------------------------------------------- /internal/ui/option/setting/noop.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | type Noop struct{} 4 | 5 | func (n *Noop) Render(styles Styles) string { 6 | return "" 7 | } 8 | 9 | func (n *Noop) Focus() {} 10 | 11 | func (n *Noop) Blur() {} 12 | 13 | func (n *Noop) Type() Type { 14 | return TypeNoop 15 | } 16 | -------------------------------------------------------------------------------- /internal/ui/option/setting/radio.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Radio struct { 9 | Title string 10 | Values []string 11 | Index int 12 | 13 | focus bool 14 | } 15 | 16 | func (r *Radio) Render(styles Styles) string { 17 | var str []string 18 | 19 | switch r.focus { 20 | case true: 21 | str = append(str, styles.settingTitleSelected.Render(r.Title)) 22 | default: 23 | str = append(str, styles.settingTitle.Render(r.Title)) 24 | } 25 | 26 | for idx, val := range r.Values { 27 | if idx == r.Index { 28 | v := styles.settingSelected.Render(val) 29 | str = append(str, fmt.Sprintf("%v %v", styles.settingDotFilled, v)) 30 | 31 | continue 32 | } 33 | 34 | v := styles.setting.Render(val) 35 | str = append(str, fmt.Sprintf("%v %v", styles.settingDotEmpty, v)) 36 | } 37 | 38 | return strings.Join(str, "\n") 39 | } 40 | 41 | func (r *Radio) Focus() { 42 | r.focus = true 43 | } 44 | 45 | func (r *Radio) Blur() { 46 | r.focus = false 47 | } 48 | 49 | func (r *Radio) Value() string { 50 | return r.Values[r.Index] 51 | } 52 | 53 | func (r *Radio) Next() { 54 | if r.Index >= len(r.Values)-1 { 55 | r.Index = 0 56 | return 57 | } 58 | 59 | r.Index++ 60 | } 61 | 62 | func (r *Radio) Previous() { 63 | if r.Index <= 0 { 64 | r.Index = len(r.Values) - 1 65 | return 66 | } 67 | 68 | r.Index-- 69 | } 70 | 71 | func (r *Radio) Select(i int) { 72 | r.Index = i 73 | } 74 | 75 | func (r *Radio) Type() Type { 76 | return TypeRadio 77 | } 78 | 79 | func ToRadio(p Paner) *Radio { 80 | r, _ := p.(*Radio) 81 | 82 | return r 83 | } 84 | -------------------------------------------------------------------------------- /internal/ui/option/setting/style.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/theme" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | type Styles struct { 11 | setting lipgloss.Style 12 | settingTitle lipgloss.Style 13 | settingSelected lipgloss.Style 14 | settingTitleSelected lipgloss.Style 15 | settingDotEmpty lipgloss.Style 16 | settingDotFilled lipgloss.Style 17 | settingSquareEmpty lipgloss.Style 18 | settingSquareFilled lipgloss.Style 19 | } 20 | 21 | func defaultStyles(th theme.Theme) Styles { 22 | var s Styles 23 | 24 | clr := colour.New(th).OptionSetting() 25 | 26 | s.setting = lipgloss.NewStyle(). 27 | Foreground(clr.Setting) 28 | 29 | s.settingTitle = lipgloss.NewStyle(). 30 | Foreground(clr.SettingTitle) 31 | 32 | s.settingSelected = lipgloss.NewStyle(). 33 | Foreground(clr.SettingSelected) 34 | 35 | s.settingTitleSelected = lipgloss.NewStyle(). 36 | Foreground(clr.SettingTitleSelected) 37 | 38 | s.settingDotEmpty = lipgloss.NewStyle(). 39 | Foreground(clr.SettingDotEmpty). 40 | SetString("○") 41 | 42 | s.settingDotFilled = lipgloss.NewStyle(). 43 | Foreground(clr.SettingDotFilled). 44 | SetString("●") 45 | 46 | s.settingSquareEmpty = lipgloss.NewStyle(). 47 | Foreground(clr.SettingSquareEmpty). 48 | SetString("▢") 49 | 50 | s.settingSquareFilled = lipgloss.NewStyle(). 51 | Foreground(clr.SettingSquareFilled). 52 | SetString("▣") 53 | 54 | return s 55 | } 56 | -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio.golden: -------------------------------------------------------------------------------- 1 | Title 2 | ● 1 3 | ○ 2 4 | ○ 3 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_invalid.golden: -------------------------------------------------------------------------------- 1 | First 2 | ● 1 3 | ○ 2 4 | ○ 3 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_multiple.golden: -------------------------------------------------------------------------------- 1 | First 2 | ● 1 3 | ○ 2 4 | ○ 3 5 | 6 | Second 7 | ● 4 8 | ○ 5 9 | ○ 6 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_multiple_select.golden: -------------------------------------------------------------------------------- 1 | First 2 | ● 1 3 | ○ 2 4 | ○ 3 5 | 6 | Second 7 | ● 4 8 | ○ 5 9 | ○ 6 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_multiple_select_next.golden: -------------------------------------------------------------------------------- 1 | First 2 | ● 1 3 | ○ 2 4 | ○ 3 5 | 6 | Second 7 | ○ 4 8 | ● 5 9 | ○ 6 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_next.golden: -------------------------------------------------------------------------------- 1 | Title 2 | ○ 1 3 | ● 2 4 | ○ 3 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_next_last.golden: -------------------------------------------------------------------------------- 1 | Title 2 | ● 1 3 | ○ 2 4 | ○ 3 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_previous.golden: -------------------------------------------------------------------------------- 1 | Title 2 | ○ 1 3 | ● 2 4 | ○ 3 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_previous_first.golden: -------------------------------------------------------------------------------- 1 | Title 2 | ○ 1 3 | ○ 2 4 | ● 3 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/radio_select.golden: -------------------------------------------------------------------------------- 1 | Title 2 | ○ 1 3 | ○ 2 4 | ● 3 -------------------------------------------------------------------------------- /internal/ui/option/setting/testdata/toggle.golden: -------------------------------------------------------------------------------- 1 | Title 2 | ▣ Enable -------------------------------------------------------------------------------- /internal/ui/option/setting/toggle.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Toggle struct { 9 | Title string 10 | Enable bool 11 | 12 | focus bool 13 | } 14 | 15 | func (t *Toggle) Render(styles Styles) string { 16 | var str []string 17 | 18 | switch t.focus { 19 | case true: 20 | str = append(str, styles.settingTitleSelected.Render(t.Title)) 21 | default: 22 | str = append(str, styles.settingTitle.Render(t.Title)) 23 | } 24 | 25 | switch t.Enable { 26 | case true: 27 | v := styles.settingSelected.Render("Enable") 28 | str = append(str, fmt.Sprintf("%v %v", styles.settingSquareFilled, v)) 29 | default: 30 | v := styles.setting.Render("Enable") 31 | str = append(str, fmt.Sprintf("%v %v", styles.settingSquareEmpty, v)) 32 | } 33 | 34 | return strings.Join(str, "\n") 35 | } 36 | 37 | func (t *Toggle) Focus() { 38 | t.focus = true 39 | } 40 | 41 | func (t *Toggle) Blur() { 42 | t.focus = false 43 | } 44 | 45 | func (t *Toggle) Value() bool { 46 | return t.Enable 47 | } 48 | 49 | func (t *Toggle) Toggle() { 50 | t.Enable = !t.Enable 51 | } 52 | 53 | func (t *Toggle) Type() Type { 54 | return TypeToggle 55 | } 56 | 57 | func ToToggle(p Paner) *Toggle { 58 | return p.(*Toggle) 59 | } 60 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/combined.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │❯ A │ │ A │ 3 | │ └▸1 │ │ ● 0.0 │ 4 | │ 2 │ │ ○ 0.1 │ 5 | │ 3 │ │ ○ 0.2 │ 6 | │ │ │ │ 7 | │ B │ │ B │ 8 | │ │ │ ● 1.0 │ 9 | │ C │ │ ○ 1.1 │ 10 | │ 1 │ │ ○ 1.2 │ 11 | │ 2 │ │ │ 12 | │ 3 │ │ C │ 13 | │ │ │ ● 2.0 │ 14 | │ D │ │ ○ 2.1 │ 15 | │ 1 │ │ ○ 2.2 │ 16 | │ 2 │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/combined_modified.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ A │ │ A │ 3 | │ 1 │ │ ● 0.0 │ 4 | │ 2 │ │ ○ 0.1 │ 5 | │ 3 │ │ ○ 0.2 │ 6 | │ │ │ │ 7 | │ B │ │ B │ 8 | │ │ │ ● 1.0 │ 9 | │❯ C │ │ ○ 1.1 │ 10 | │ │ 1 │ │ ○ 1.2 │ 11 | │ └▸2 │ │ │ 12 | │ 3 │ │ C │ 13 | │ │ │ ○ 2.0 │ 14 | │ D │ │ ○ 2.1 │ 15 | │ 1 │ │ ● 2.2 │ 16 | │ 2 │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ help │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/combined_modified_width_height.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ A │ │ A │ 3 | │ 1 │ │ ● 0.0 │ 4 | │ 2 │ │ ○ 0.1 │ 5 | │ 3 │ │ ○ 0.2 │ 6 | │ │ │ │ 7 | │ B │ │ B │ 8 | │ │ │ ○ 1.0 │ 9 | │❯ C │ │ ○ 1.1 │ 10 | │ │ 1 │ │ ● 1.2 │ 11 | │ └▸2 │ │ │ 12 | │ 3 │ │ Toggle │ 13 | │ │ │ ▣ Enable │ 14 | │ D │ │ │ 15 | │ 1 │ │ │ 16 | │ 2 │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ help │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/default.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ │ │ │ 3 | │ │ │ │ 4 | │ │ │ │ 5 | │ │ │ │ 6 | │ │ │ │ 7 | │ │ │ │ 8 | │ │ │ │ 9 | │ │ │ │ 10 | │ │ │ │ 11 | │ │ │ │ 12 | │ │ │ │ 13 | │ │ │ │ 14 | │ │ │ │ 15 | │ │ │ │ 16 | │ │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/help.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ │ │ │ 3 | │ │ │ │ 4 | │ │ │ │ 5 | │ │ │ │ 6 | │ │ │ │ 7 | │ │ │ │ 8 | │ │ │ │ 9 | │ │ │ │ 10 | │ │ │ │ 11 | │ │ │ │ 12 | │ │ │ │ 13 | │ │ │ │ 14 | │ │ │ │ 15 | │ │ │ │ 16 | │ │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ help │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/section.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │❯ A │ │ │ 3 | │ └▸1 │ │ │ 4 | │ 2 │ │ │ 5 | │ 3 │ │ │ 6 | │ │ │ │ 7 | │ B │ │ │ 8 | │ │ │ │ 9 | │ C │ │ │ 10 | │ 1 │ │ │ 11 | │ 2 │ │ │ 12 | │ 3 │ │ │ 13 | │ │ │ │ 14 | │ D │ │ │ 15 | │ 1 │ │ │ 16 | │ 2 │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/section_category_only.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │❯ First │ │ │ 3 | │ │ │ │ 4 | │ │ │ │ 5 | │ │ │ │ 6 | │ │ │ │ 7 | │ │ │ │ 8 | │ │ │ │ 9 | │ │ │ │ 10 | │ │ │ │ 11 | │ │ │ │ 12 | │ │ │ │ 13 | │ │ │ │ 14 | │ │ │ │ 15 | │ │ │ │ 16 | │ │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/section_set.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ A │ │ │ 3 | │ 1 │ │ │ 4 | │ 2 │ │ │ 5 | │ 3 │ │ │ 6 | │ │ │ │ 7 | │ B │ │ │ 8 | │ │ │ │ 9 | │❯ C │ │ │ 10 | │ │ 1 │ │ │ 11 | │ └▸2 │ │ │ 12 | │ 3 │ │ │ 13 | │ │ │ │ 14 | │ D │ │ │ 15 | │ 1 │ │ │ 16 | │ 2 │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/section_set_category_only.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ A │ │ │ 3 | │ 1 │ │ │ 4 | │ 2 │ │ │ 5 | │ 3 │ │ │ 6 | │ │ │ │ 7 | │❯ B │ │ │ 8 | │ │ │ │ 9 | │ C │ │ │ 10 | │ 1 │ │ │ 11 | │ 2 │ │ │ 12 | │ 3 │ │ │ 13 | │ │ │ │ 14 | │ D │ │ │ 15 | │ 1 │ │ │ 16 | │ 2 │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/setting.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ │ │ A │ 3 | │ │ │ ● 0.0 │ 4 | │ │ │ ○ 0.1 │ 5 | │ │ │ ○ 0.2 │ 6 | │ │ │ │ 7 | │ │ │ B │ 8 | │ │ │ ● 1.0 │ 9 | │ │ │ ○ 1.1 │ 10 | │ │ │ ○ 1.2 │ 11 | │ │ │ │ 12 | │ │ │ C │ 13 | │ │ │ ● 2.0 │ 14 | │ │ │ ○ 2.1 │ 15 | │ │ │ ○ 2.2 │ 16 | │ │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/setting_next.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ │ │ A │ 3 | │ │ │ ○ 0.0 │ 4 | │ │ │ ● 0.1 │ 5 | │ │ │ ○ 0.2 │ 6 | │ │ │ │ 7 | │ │ │ B │ 8 | │ │ │ ● 1.0 │ 9 | │ │ │ ○ 1.1 │ 10 | │ │ │ ○ 1.2 │ 11 | │ │ │ │ 12 | │ │ │ C │ 13 | │ │ │ ● 2.0 │ 14 | │ │ │ ○ 2.1 │ 15 | │ │ │ ○ 2.2 │ 16 | │ │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/setting_previous.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ │ │ A │ 3 | │ │ │ ● 0.0 │ 4 | │ │ │ ○ 0.1 │ 5 | │ │ │ ○ 0.2 │ 6 | │ │ │ │ 7 | │ │ │ B │ 8 | │ │ │ ● 1.0 │ 9 | │ │ │ ○ 1.1 │ 10 | │ │ │ ○ 1.2 │ 11 | │ │ │ │ 12 | │ │ │ C │ 13 | │ │ │ ○ 2.0 │ 14 | │ │ │ ● 2.1 │ 15 | │ │ │ ○ 2.2 │ 16 | │ │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/setting_select.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐ 2 | │ │ │ A │ 3 | │ │ │ ● 0.0 │ 4 | │ │ │ ○ 0.1 │ 5 | │ │ │ ○ 0.2 │ 6 | │ │ │ │ 7 | │ │ │ B │ 8 | │ │ │ ○ 1.0 │ 9 | │ │ │ ● 1.1 │ 10 | │ │ │ ○ 1.2 │ 11 | │ │ │ │ 12 | │ │ │ C │ 13 | │ │ │ ● 2.0 │ 14 | │ │ │ ○ 2.1 │ 15 | │ │ │ ○ 2.2 │ 16 | │ │ └────────────────────────────────────────┘ 17 | │ │ 18 | │ │ ┌────────────────────────────────────────┐ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | └────────────────────────────────────────┘ └────────────────────────────────────────┘ 23 | -------------------------------------------------------------------------------- /internal/ui/option/testdata/theme.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────┐ ┌────────────────────────────────────────┐ 2 | │❯ Theme │ │ Theme: 1 │ 3 | │ │ └────────────────────────────────────────┘ 4 | │ │ 5 | │ │ ┌────────────────────────────────────────┐ 6 | │ │ │? Select a theme: ● │ 7 | │ │ │ 0 │ 8 | │ │ │❯ 1 │ 9 | │ │ │ 2 │ 10 | │ │ │ 3 │ 11 | │ │ │ 4 │ 12 | │ │ │ │ 13 | │ │ │ │ 14 | │ │ │ │ 15 | │ │ │ │ 16 | │ │ │ │ 17 | │ │ │ │ 18 | │ │ │ │ 19 | │ │ │ │ 20 | │ │ │ │ 21 | │ │ │ │ 22 | │ │ │ │ 23 | │ │ │ │ 24 | │ │ │ │ 25 | └────────────────────┘ └────────────────────────────────────────┘ 26 | -------------------------------------------------------------------------------- /internal/ui/option/theme/style.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/theme" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | type Styles struct { 11 | themeTitle lipgloss.Style 12 | themeTitleFocus lipgloss.Style 13 | themeTitleLabel lipgloss.Style 14 | themeTitleText lipgloss.Style 15 | themeListBoundary lipgloss.Style 16 | themeListBoundaryFocus lipgloss.Style 17 | } 18 | 19 | func defaultStyles(th theme.Theme) Styles { 20 | var s Styles 21 | 22 | clr := colour.New(th).OptionTheme() 23 | 24 | s.themeTitle = lipgloss.NewStyle(). 25 | Border(lipgloss.NormalBorder()). 26 | BorderForeground(clr.Title). 27 | Width(40). 28 | Padding(0, 1, 0, 1). 29 | MarginBottom(1) 30 | 31 | s.themeTitleFocus = s.themeTitle. 32 | BorderForeground(clr.TitleFocus) 33 | 34 | s.themeTitleLabel = lipgloss.NewStyle(). 35 | Foreground(clr.TitleLabel). 36 | SetString("Theme:") 37 | 38 | s.themeTitleText = lipgloss.NewStyle(). 39 | Foreground(clr.TitleText) 40 | 41 | s.themeListBoundary = lipgloss.NewStyle(). 42 | Border(lipgloss.NormalBorder()). 43 | Width(40). 44 | BorderForeground(clr.Boundary) 45 | 46 | s.themeListBoundaryFocus = s.themeListBoundary. 47 | BorderForeground(clr.BoundaryFocus) 48 | 49 | return s 50 | } 51 | -------------------------------------------------------------------------------- /internal/ui/option/theme/testdata/default.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ 2 | │ Theme: 0 │ 3 | └────────────────────────────────────────┘ 4 | 5 | ┌────────────────────────────────────────┐ 6 | │? Select a theme: ● │ 7 | │❯ 0 │ 8 | │ 1 │ 9 | │ 2 │ 10 | │ 3 │ 11 | │ 4 │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | └────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/option/theme/testdata/down_enter.golden: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────┐ 2 | │ Theme: 1 │ 3 | └────────────────────────────────────────┘ 4 | 5 | ┌────────────────────────────────────────┐ 6 | │? Select a theme: ● │ 7 | │ 0 │ 8 | │❯ 1 │ 9 | │ 2 │ 10 | │ 3 │ 11 | │ 4 │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | │ │ 21 | │ │ 22 | │ │ 23 | │ │ 24 | │ │ 25 | └────────────────────────────────────────┘ -------------------------------------------------------------------------------- /internal/ui/option/theme/theme_test.go: -------------------------------------------------------------------------------- 1 | package theme_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mikelorant/committed/internal/commit" 7 | inttheme "github.com/mikelorant/committed/internal/theme" 8 | "github.com/mikelorant/committed/internal/theme/themetest" 9 | "github.com/mikelorant/committed/internal/ui/option/theme" 10 | "github.com/mikelorant/committed/internal/ui/uitest" 11 | 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/hexops/autogold/v2" 14 | ) 15 | 16 | func TestModel(t *testing.T) { 17 | t.Parallel() 18 | 19 | type args struct { 20 | model func(m theme.Model) theme.Model 21 | } 22 | 23 | type want struct { 24 | model func(m theme.Model) 25 | } 26 | 27 | tests := []struct { 28 | name string 29 | args args 30 | want want 31 | }{ 32 | { 33 | name: "default", 34 | }, 35 | { 36 | name: "down_enter", 37 | args: args{ 38 | model: func(m theme.Model) theme.Model { 39 | m.Focus() 40 | m, _ = theme.ToModel(m.Update(tea.KeyMsg{Type: tea.KeyDown})) 41 | m, _ = theme.ToModel(m.Update(tea.KeyMsg{Type: tea.KeyEnter})) 42 | 43 | return m 44 | }, 45 | }, 46 | }, 47 | } 48 | 49 | for _, tt := range tests { 50 | tt := tt 51 | 52 | t.Run(tt.name, func(t *testing.T) { 53 | t.Parallel() 54 | 55 | tint := inttheme.Tint{ 56 | Default: themetest.NewStubTints(5)[0], 57 | Defaults: themetest.NewStubTints(5), 58 | } 59 | 60 | state := &commit.State{ 61 | Theme: inttheme.New(tint), 62 | } 63 | 64 | m := theme.New(state) 65 | 66 | if tt.args.model != nil { 67 | m = tt.args.model(m) 68 | } 69 | 70 | if tt.want.model != nil { 71 | tt.want.model(m) 72 | } 73 | 74 | v := uitest.StripString(m.View()) 75 | autogold.ExpectFile(t, autogold.Raw(v), autogold.Name(tt.name)) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/ui/option/theme/tints.go: -------------------------------------------------------------------------------- 1 | package theme 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/fuzzy" 5 | 6 | "github.com/charmbracelet/bubbles/list" 7 | tint "github.com/lrstanley/bubbletint" 8 | ) 9 | 10 | type listItem struct { 11 | tint tint.Tint 12 | } 13 | 14 | type fuzzyItem struct { 15 | tint tint.Tint 16 | } 17 | 18 | func (i listItem) Title() string { 19 | return i.tint.DisplayName() 20 | } 21 | 22 | func (i listItem) Description() string { 23 | return i.tint.ID() 24 | } 25 | 26 | func (i listItem) FilterValue() string { 27 | return i.tint.DisplayName() 28 | } 29 | 30 | func (i fuzzyItem) Terms() []string { 31 | return []string{ 32 | i.tint.DisplayName(), 33 | i.tint.ID(), 34 | } 35 | } 36 | 37 | func castToListItems(tints []tint.Tint) []list.Item { 38 | res := make([]list.Item, len(tints)) 39 | for i, t := range tints { 40 | var item listItem 41 | item.tint = t 42 | res[i] = item 43 | } 44 | 45 | return res 46 | } 47 | 48 | func castToFuzzyItems(tints []tint.Tint) []fuzzy.Item { 49 | res := make([]fuzzy.Item, len(tints)) 50 | for i, t := range tints { 51 | var item fuzzyItem 52 | item.tint = t 53 | res[i] = item 54 | } 55 | 56 | return res 57 | } 58 | -------------------------------------------------------------------------------- /internal/ui/shortcut/style.go: -------------------------------------------------------------------------------- 1 | package shortcut 2 | 3 | import ( 4 | "github.com/mikelorant/committed/internal/theme" 5 | "github.com/mikelorant/committed/internal/ui/colour" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | type Styles struct { 11 | boundary lipgloss.Style 12 | columnLeft lipgloss.Style 13 | columnRight lipgloss.Style 14 | key lipgloss.Style 15 | label lipgloss.Style 16 | modifierPlus lipgloss.Style 17 | angleBracket lipgloss.Style 18 | } 19 | 20 | func defaultStyles(th theme.Theme) Styles { 21 | var s Styles 22 | 23 | clr := colour.New(th).Shortcut() 24 | 25 | s.boundary = lipgloss.NewStyle(). 26 | MarginBottom(1) 27 | 28 | s.columnRight = lipgloss.NewStyle(). 29 | MarginLeft(1) 30 | 31 | s.columnLeft = lipgloss.NewStyle(). 32 | MarginRight(1) 33 | 34 | s.key = lipgloss.NewStyle(). 35 | Foreground(clr.Key) 36 | 37 | s.label = lipgloss.NewStyle(). 38 | Foreground(clr.Label) 39 | 40 | s.modifierPlus = lipgloss.NewStyle(). 41 | Foreground(clr.Plus). 42 | SetString("+") 43 | 44 | s.angleBracket = lipgloss.NewStyle(). 45 | Foreground(clr.AngleBracket) 46 | 47 | return s 48 | } 49 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/default.golden: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/empty_left.golden: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/empty_left_bottom.golden: -------------------------------------------------------------------------------- 1 | Test1 + test1 2 | test2 3 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/empty_left_top.golden: -------------------------------------------------------------------------------- 1 | test1 2 | Test2 + test2 3 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/empty_right.golden: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/empty_right_bottom.golden: -------------------------------------------------------------------------------- 1 | test1 + Test 2 | test2 3 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/empty_right_top.golden: -------------------------------------------------------------------------------- 1 | test1 2 | test2 + Test2 3 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/left.golden: -------------------------------------------------------------------------------- 1 | Test + test 2 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/multiple_different_left.golden: -------------------------------------------------------------------------------- 1 | Test1 + test1 2 | Test2 + test2 3 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/multiple_different_right.golden: -------------------------------------------------------------------------------- 1 | test1 + Test1 2 | test2 + Test2 3 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/multiple_same_left.golden: -------------------------------------------------------------------------------- 1 | Test1 + test1 test2 2 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/multiple_same_right.golden: -------------------------------------------------------------------------------- 1 | test1 test2 + Test1 2 | -------------------------------------------------------------------------------- /internal/ui/shortcut/testdata/right.golden: -------------------------------------------------------------------------------- 1 | test + Test 2 | -------------------------------------------------------------------------------- /internal/ui/status/testdata/default.golden: -------------------------------------------------------------------------------- 1 | Alt + Commit Amend Load Sign-off 2 | Ctrl + Cancel Options Help 3 | -------------------------------------------------------------------------------- /internal/ui/status/testdata/help.golden: -------------------------------------------------------------------------------- 1 | Alt + Commit Amend Load Sign-off Exit 2 | Ctrl + Cancel Options Help 3 | -------------------------------------------------------------------------------- /internal/ui/status/testdata/next.golden: -------------------------------------------------------------------------------- 1 | Alt + Commit Amend Load Sign-off next 2 | Ctrl + Cancel Options Help 3 | -------------------------------------------------------------------------------- /internal/ui/status/testdata/next_previous.golden: -------------------------------------------------------------------------------- 1 | Alt + Commit Amend Load Sign-off next 2 | Ctrl + Cancel Options Help previous + Shift 3 | -------------------------------------------------------------------------------- /internal/ui/status/testdata/option.golden: -------------------------------------------------------------------------------- 1 | Alt + Write Move <↑→↓←> Toggle Select Exit 2 | Ctrl + Cancel 3 | -------------------------------------------------------------------------------- /internal/ui/status/testdata/previous.golden: -------------------------------------------------------------------------------- 1 | Alt + Commit Amend Load Sign-off 2 | Ctrl + Cancel Options Help previous + Shift 3 | -------------------------------------------------------------------------------- /internal/ui/testdata/alt+enter_summary.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | test 6 | 7 | -------------------------------------------------------------------------------- /internal/ui/testdata/alt+enter_summary_author.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | test 6 | 7 | -------------------------------------------------------------------------------- /internal/ui/testdata/alt+enter_summary_body.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | test 6 | 7 | test 8 | 9 | -------------------------------------------------------------------------------- /internal/ui/testdata/alt+enter_summary_emoji.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | 🎨 test 6 | 7 | -------------------------------------------------------------------------------- /internal/ui/testdata/alt+enter_summary_footer.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | test 6 | 7 | Signed-off-by: John Doe 8 | 9 | -------------------------------------------------------------------------------- /internal/ui/testdata/alt+s.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | ┌────┐ ┌─────────────────────────────────────────────────────┐ 6 | │ │ │ placeholder │ 0/50 ● New 7 | └────┘ └─────────────────────────────────────────────────────┘ 8 | 9 | ┌──────────────────────────────────────────────────────────────────────────┐ 10 | │? Choose an emoji: ● │ 11 | │❯ 🎨 - Improve structure / format of the code. │ 12 | │ │ 13 | │ │ 14 | │ │ 15 | │ │ 16 | │ │ 17 | │ │ 18 | │ │ 19 | │ │ 20 | └──────────────────────────────────────────────────────────────────────────┘ 21 | 22 | ┌──────────────────────────────────────────────────────────────────────────┐ 23 | │ placeholder │ 24 | │ │ 25 | │ │ 26 | │ │ 27 | └──────────────────────────────────────────────────────────────────────────┘ 28 | 29 | Signed-off-by: John Doe 30 | 31 | Alt + Commit Amend Load Sign-off Summary 32 | Ctrl + Cancel Options Help Author + Shift 33 | -------------------------------------------------------------------------------- /internal/ui/testdata/config_emoji_type_character.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | 🎨 test 6 | 7 | -------------------------------------------------------------------------------- /internal/ui/testdata/config_emoji_type_shortcode.golden: -------------------------------------------------------------------------------- 1 | commit 1 (HEAD -> master) 2 | author: John Doe 3 | date: Sat Jan 1 01:00:00 2022 +0000 4 | 5 | :art: test 6 | 7 | -------------------------------------------------------------------------------- /internal/ui/uitest/uitest.go: -------------------------------------------------------------------------------- 1 | package uitest 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | 7 | "github.com/acarl005/stripansi" 8 | tea "github.com/charmbracelet/bubbletea" 9 | ) 10 | 11 | //nolint:ireturn 12 | func KeyPress(key rune) tea.Msg { 13 | return tea.KeyMsg{ 14 | Type: tea.KeyRunes, 15 | Runes: []rune{key}, 16 | } 17 | } 18 | 19 | func StripString(str string) string { 20 | s := stripansi.Strip(str) 21 | ss := strings.Split(s, "\n") 22 | 23 | var lines []string 24 | for _, l := range ss { 25 | trim := strings.TrimRightFunc(l, unicode.IsSpace) 26 | lines = append(lines, trim) 27 | } 28 | 29 | return strings.Join(lines, "\n") 30 | } 31 | 32 | //nolint:ireturn 33 | func SendString(m tea.Model, str string) tea.Model { 34 | for _, r := range str { 35 | msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}} 36 | m, _ = m.Update(msg) 37 | } 38 | 39 | return m 40 | } 41 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/mikelorant/committed/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /scripts/demo.tape: -------------------------------------------------------------------------------- 1 | # Requirements: 2 | # - Build release: go build -tags release 3 | 4 | Output demo.gif 5 | 6 | Require committed 7 | Require git 8 | 9 | Set Width 794 10 | Set Height 760 11 | Set Padding 18 12 | Set FontSize 16 13 | Set Theme "Builtin Dark" 14 | Set LetterSpacing 0 15 | 16 | Hide 17 | # Sets up a fake commit. 18 | Set TypingSpeed 1ms 19 | Type "cd /tmp" Enter Wait 20 | Type "git init" Enter Wait 21 | Type "git commit --allow-empty --allow-empty-message --no-edit" Enter Wait 22 | Type "touch test" Enter Wait 23 | Type "git add test" Enter Wait 24 | Type "clear" Enter Wait 25 | Set TypingSpeed 50ms 26 | 27 | Show 28 | Sleep 1s 29 | Type "committed" 30 | Sleep 1s 31 | Enter 32 | 33 | Sleep 3s 34 | Shift+Tab 35 | Sleep 3s 36 | Enter 37 | 38 | Sleep 2s 39 | PageDown 40 | Sleep 2s 41 | PageDown 42 | Sleep 2s 43 | Type@250ms "bug" 44 | Sleep 2s 45 | Enter 46 | 47 | Sleep 1s 48 | Type "Prevent racing of requests" 49 | Sleep 1s 50 | Enter 51 | 52 | Sleep 1s 53 | Type "Introduce a request id and a reference to latest request. Dismiss" 54 | Type " incoming responses other than from latest request." 55 | Sleep 0.5s Enter Sleep 0.25s 56 | Sleep 0.25s Enter Sleep 0.25s 57 | Type "Remove timeouts which were used to mitigate the racing issue but are" 58 | Type " obsolete now." 59 | Sleep 0.5s Enter Sleep 0.25s 60 | Sleep 0.25s Enter Sleep 0.25s 61 | Type "Resolves: #1" 62 | Sleep 0.5s Enter Sleep 2s 63 | 64 | Alt+S Sleep 3s 65 | 66 | Alt+Enter Sleep 5s 67 | 68 | Hide 69 | Ctrl+D 70 | --------------------------------------------------------------------------------