Compare commits

...
28 Commits
Author SHA1 Message Date
Jesse Duffield 3fb478a30e add tests 2021-11-07 13:32:36 +11:00
Jesse Duffield 5d12a6bf99 restore some code that was erroneously removed 2021-11-07 13:32:36 +11:00
Jesse Duffield 1d40d03bb2 refactor 2021-11-05 07:58:21 +11:00
Jesse Duffield 9a9e3d506d more consistent rendering 2021-11-05 07:58:21 +11:00
Jesse Duffield 06ca71e955 fix bug 2021-11-05 07:58:21 +11:00
Jesse Duffield 308a3b51b3 some more throttling stuff 2021-11-05 07:58:21 +11:00
Jesse Duffield ccd80a0e4b add menu options for log stuff 2021-11-05 07:58:21 +11:00
Jesse Duffield 37be9dbea1 support scrolling left and right 2021-11-05 07:58:21 +11:00
Jesse Duffield f6ec7babf5 add some config 2021-11-05 07:58:21 +11:00
Jesse Duffield 802cfb1a04 render commit graph 2021-11-05 07:58:21 +11:00
Jesse Duffield 2fc1498517 some refactoring in anticipation of the graph feature 2021-11-01 10:03:49 +11:00
Jesse Duffield 7a464ae5b7 add graph algorithm 2021-11-01 10:03:49 +11:00
Jesse Duffield 927ee63106 support aborting a merge or rebase with esc 2021-11-01 09:18:30 +11:00
Jesse Duffield 9989c96321 better formatting 2021-10-31 22:33:39 +11:00
Jesse Duffield f91892b8f1 fix truncation 2021-10-30 20:19:40 +11:00
Jesse Duffield 72bce201df support scrolling the list in the integrations app 2021-10-30 18:26:06 +11:00
Jesse Duffield 5df0fe0765 fix crash 2021-10-30 18:26:06 +11:00
Jesse Duffield c47c539e12 support user-configurable author colours 2021-10-30 18:26:06 +11:00
Jesse Duffield c96496c3a7 show author info in rebase commits 2021-10-30 18:26:06 +11:00
Jesse Duffield 7561703e8d move author name colouring code into its own file 2021-10-30 18:26:06 +11:00
Jesse Duffield b04b457246 fix yet another issue with indentation 2021-10-30 18:26:06 +11:00
Jesse Duffield 6457800748 fix another issue with indentation 2021-10-30 18:26:06 +11:00
Jesse Duffield 7d9461877a fix issue with indentation 2021-10-30 18:26:06 +11:00
Jesse Duffield e122f421e6 only use a single initial for double sized runes 2021-10-30 18:26:06 +11:00
Ryooooooga 6171690b00 Fix multibyte initial characters 2021-10-30 18:26:06 +11:00
Jesse Duffield 253504a094 associate random colours with authors 2021-10-30 18:26:06 +11:00
Jesse Duffield f704707d29 stream output from certain git commands in command log panel 2021-10-30 18:26:06 +11:00
Jesse Duffield 01d82749b1 fix commit message prefix thingo 2021-10-25 22:40:15 +00:00
125 changed files with 4184 additions and 942 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)
+14 -1
View File
@@ -50,6 +50,8 @@ gui:
showRandomTip: true
showCommandLog: true
commandLogSize: 8
authorColors: # in case you're not happy with the randomly assigned colour
'John Smith': '#ff0000'
git:
paging:
colorArg: always
@@ -59,6 +61,14 @@ git:
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ''
log:
# one of date-order, author-date-order, topo-order.
# topo-order makes it easier to read the git log graph, but commits may not
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
order: 'topo-order'
# one of always, never, when-maximised
# this determines whether the git graph is rendered in the commits panel
showGraph: 'when-maximised'
skipHookPrefix: WIP
autoFetch: true
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
@@ -97,11 +107,13 @@ keybinding:
nextPage: '.' # go to previous page in list
gotoTop: '<' # go to top of list
gotoBottom: '>' # go to bottom of list
scrollLeft: 'H' # scroll left within list view
scrollRight: 'L' # scroll right within list view
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
nextBlock-alt: 'l' # goto the next block / panel
jumpToBlock: ["1", "2", "3", "4", "5"] # goto the Nth block / panel
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
@@ -189,6 +201,7 @@ keybinding:
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
copyCommitMessageToClipboard: '<c-y>'
openLogMenu: '<c-l>'
stash:
popStash: 'g'
commitFiles:
+6 -9
View File
@@ -9,33 +9,30 @@ require (
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.4.1
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/gookit/color v1.4.2
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/iriri/minimal/gitignore v0.3.2 // indirect
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/kyokomi/emoji/v2 v2.2.8
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-runewidth v0.0.13
github.com/mgutz/str v1.2.0
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible // indirect
github.com/sahilm/fuzzy v0.1.0
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
@@ -43,7 +40,7 @@ require (
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744 // indirect
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
+14 -27
View File
@@ -56,8 +56,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
@@ -67,24 +67,12 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/iriri/minimal v0.0.0-20180828191352-9b2348d09c1a h1:mCZYG6QcX0dz/J0rFc1tcRYGeixlDcCGSPXuPMbiS5U=
github.com/iriri/minimal/gitignore v0.3.2 h1:MnTVH89iuwiyZ/a1pByw/mAU2ShWai1yvv0tgHSq5Ww=
github.com/iriri/minimal/gitignore v0.3.2/go.mod h1:v7YhsYBAInyAnQligwCIGRuQmtwQyYxkVy5vEdy2wPU=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20211017035223-b68948e63cc3 h1:J5s/4Y860tas8J0AMQ3gJKCbJPx8zNpiTm5UjEgPQfY=
github.com/jesseduffield/gocui v0.3.1-0.20211017035223-b68948e63cc3/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017041119-0ec562dfd23b h1:kepukaDQfZ6LBSvHUYReFvVSW5Lx5ZQZDgGhXj0Mx7U=
github.com/jesseduffield/gocui v0.3.1-0.20211017041119-0ec562dfd23b/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017063715-c74848d8ad00 h1:5TusU8ir9OHg3By2PPmLwa2y+2G9F+16QRK8bpofsC0=
github.com/jesseduffield/gocui v0.3.1-0.20211017063715-c74848d8ad00/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77 h1:MQUxSxVBTZQpSYybEiFA4+oIi02ycTKGCqgHItYi/20=
github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f h1:JHrb78pj+gYC3KiJKL1WW6lYzlatBIF46oREn68plTM=
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/minimal v0.0.0-20211018110810-9cde264e6b1e h1:WZc73tBVMMhcO6zXyZBItLEF4jgBpBH0lFCZzDgrjDg=
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474 h1:4H/oJcUmwJpqyXzqfn+lsjQ/bjpm/HszzLrVbCjgqj4=
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
@@ -113,13 +101,13 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -134,9 +122,6 @@ github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/ozeidan/fuzzy-patricia v1.0.1 h1:YExnavqXH3OvCCqE2TunuJJHdFcFQdVEfUoWzrnPxSg=
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible h1:Pl61eMyfJqgY/wytiI4vamqPYribq6d8VxeP1CNyg9M=
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible/go.mod h1:zgvuCcYS7wB7fVCGblsaFFmEe8+aAH13dTYm8FbrpsM=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -186,15 +171,15 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744 h1:KzbpndAYEM+4oHRp9JmB2ewj0NHHxO3Z0g7Gus2O1kk=
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs=
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -203,6 +188,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+7 -1
View File
@@ -127,7 +127,13 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config, git_config.NewStdCachedGitConfig(app.Log))
app.GitCommand, err = commands.NewGitCommand(
app.Log,
app.OSCommand,
app.Tr,
app.Config,
git_config.NewStdCachedGitConfig(app.Log),
)
if err != nil {
return app, err
}
+9 -5
View File
@@ -1,6 +1,9 @@
package commands
import (
"io"
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -17,10 +20,11 @@ func NewDummyGitCommand() *GitCommand {
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
newAppConfig := config.NewDummyAppConfig()
return &GitCommand{
Log: utils.NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
Config: newAppConfig,
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
Log: utils.NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
Config: newAppConfig,
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
}
+7
View File
@@ -1,6 +1,7 @@
package commands
import (
"io"
"io/ioutil"
"os"
"path/filepath"
@@ -40,6 +41,11 @@ type GitCommand struct {
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
PushToCurrent bool
// this is just a view that we write to when running certain commands.
// Coincidentally at the moment it's the same view that OnRunCommand logs to
// but that need not always be the case.
GetCmdWriter func() io.Writer
}
// NewGitCommand it runs git commands
@@ -77,6 +83,7 @@ func NewGitCommand(
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
GitConfig: gitConfig,
GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
+80 -9
View File
@@ -37,7 +37,12 @@ type CommitListBuilder struct {
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
func NewCommitListBuilder(
log *logrus.Entry,
gitCommand *GitCommand,
osCommand *oscommands.OSCommand,
tr *i18n.TranslationSet,
) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
@@ -88,6 +93,8 @@ type GetCommitsOptions struct {
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
@@ -110,7 +117,7 @@ func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*m
return result, nil
}
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
rebasingCommits, err := c.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
@@ -149,7 +156,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
cmd := c.getLogCmd(opts)
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
if strings.Split(line, " ")[0] != "gpg:" {
if canExtractCommit(line) {
commit := c.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
@@ -177,6 +184,51 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
return commits, nil
}
func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
commits, err := c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(commits) == 0 {
return nil, nil
}
commitShas := make([]string, len(commits))
for i, commit := range commits {
commitShas[i] = commit.Sha
}
// note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage
cmd := c.OSCommand.ExecutableFromString(
fmt.Sprintf(
"git show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
prettyFormat,
20,
),
)
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
if canExtractCommit(line) {
commit := c.extractCommitFromLine(line)
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
i++
}
return false, nil
})
if err != nil {
return nil, err
}
return hydratedCommits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
switch rebaseMode {
@@ -359,18 +411,37 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
}
config := c.GitCommand.Config.GetUserConfig().Git.Log
orderFlag := "--" + config.Order
allFlag := ""
if opts.All {
allFlag = " --all"
}
return c.OSCommand.ExecutableFromString(
fmt.Sprintf(
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d %s",
"git log %s %s %s --oneline %s %s --abbrev=%d %s",
c.OSCommand.Quote(opts.RefName),
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
orderFlag,
allFlag,
prettyFormat,
limitFlag,
20,
filterFlag,
),
)
}
var prettyFormat = fmt.Sprintf(
"--pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\"",
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
)
func canExtractCommit(line string) bool {
return strings.Split(line, " ")[0] != "gpg:"
}
+153
View File
@@ -0,0 +1,153 @@
package oscommands
import (
"bufio"
"bytes"
"io"
"os/exec"
"regexp"
"strings"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// DetectUnamePass detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
`.+'s password:`: "password",
`Password\s*for\s*'.+':`: "password",
`Username\s*for\s*'.+':`: "username",
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
}
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return promptUserForCredential(askFor)
}
}
return ""
})
return errMessage
}
// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined
// separate for windows and other OS's
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput)
}
type cmdHandler struct {
stdoutPipe io.Reader
stdinPipe io.Writer
close func() error
}
// RunCommandWithOutputLiveAux runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't write anything to stdin
func RunCommandWithOutputLiveAux(
c *OSCommand,
cmdObj ICmdObj,
writer io.Writer,
// handleOutput takes a word from stdout and returns a string to be written to stdin.
// See DetectUnamePass above for how this is used to check for a username/password request
handleOutput func(string) string,
startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
) error {
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
c.LogCommand(cmdObj.ToString(), true)
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
var stderr bytes.Buffer
cmd.Stderr = io.MultiWriter(writer, &stderr)
handler, err := startCmd(cmd)
if err != nil {
return err
}
defer func() {
if closeErr := handler.close(); closeErr != nil {
c.Log.Error(closeErr)
}
}()
tr := io.TeeReader(handler.stdoutPipe, writer)
go utils.Safe(func() {
scanner := bufio.NewScanner(tr)
scanner.Split(scanWordsWithNewLines)
for scanner.Scan() {
text := scanner.Text()
output := strings.Trim(text, " ")
toInput := handleOutput(output)
if toInput != "" {
_, _ = handler.stdinPipe.Write([]byte(toInput))
}
}
})
err = cmd.Wait()
if err != nil {
return errors.New(stderr.String())
}
return nil
}
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
// For specific comments about this function take a look at: bufio.ScanWords
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
}
}
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if isSpace(r) {
return i + width, data[start:i], nil
}
}
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
return start, nil, nil
}
// isSpace is also copied from the bufio package and has been modified to also captures new lines
// For specific comments about this function take a look at: bufio.isSpace
func isSpace(r rune) bool {
if r <= '\u00FF' {
switch r {
case ' ', '\t', '\v', '\f':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
}
+25 -86
View File
@@ -4,95 +4,34 @@
package oscommands
import (
"bufio"
"bytes"
"strings"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"io"
"os/exec"
"github.com/creack/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error {
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
c.LogCommand(cmdObj.ToString(), true)
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
func RunCommandWithOutputLiveWrapper(
c *OSCommand,
cmdObj ICmdObj,
writer io.Writer,
output func(string) string,
) error {
return RunCommandWithOutputLiveAux(
c,
cmdObj,
writer,
output,
func(cmd *exec.Cmd) (*cmdHandler, error) {
ptmx, err := pty.Start(cmd)
if err != nil {
return nil, err
}
var stderr bytes.Buffer
cmd.Stderr = &stderr
ptmx, err := pty.Start(cmd)
if err != nil {
return err
}
go utils.Safe(func() {
scanner := bufio.NewScanner(ptmx)
scanner.Split(scanWordsWithNewLines)
for scanner.Scan() {
toOutput := strings.Trim(scanner.Text(), " ")
_, _ = ptmx.WriteString(output(toOutput))
}
})
err = cmd.Wait()
ptmx.Close()
if err != nil {
return errors.New(stderr.String())
}
return nil
}
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
// For specific comments about this function take a look at: bufio.ScanWords
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
}
}
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if isSpace(r) {
return i + width, data[start:i], nil
}
}
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
return start, nil, nil
}
// isSpace is also copied from the bufio package and has been modified to also captures new lines
// For specific comments about this function take a look at: bufio.isSpace
func isSpace(r rune) bool {
if r <= '\u00FF' {
switch r {
case ' ', '\t', '\v', '\f':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
return &cmdHandler{
stdoutPipe: ptmx,
stdinPipe: ptmx,
close: ptmx.Close,
}, nil
},
)
}
+56 -3
View File
@@ -3,8 +3,61 @@
package oscommands
import (
"bytes"
"io"
"os/exec"
"sync"
)
type Buffer struct {
b bytes.Buffer
m sync.Mutex
}
func (b *Buffer) Read(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Read(p)
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.m.Lock()
defer b.m.Unlock()
return b.b.Write(p)
}
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error {
return c.RunCommand(cmdObj.ToString())
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
func RunCommandWithOutputLiveWrapper(
c *OSCommand,
cmdObj ICmdObj,
writer io.Writer,
output func(string) string,
) error {
return RunCommandWithOutputLiveAux(
c,
cmdObj,
writer,
output,
func(cmd *exec.Cmd) (*cmdHandler, error) {
stdoutReader, stdoutWriter := io.Pipe()
cmd.Stdout = stdoutWriter
buf := &Buffer{}
cmd.Stdin = buf
if err := cmd.Start(); err != nil {
return nil, err
}
// because we don't yet have windows support for a pty, we instead just
// pass our standard stream handlers and because there's no pty to close
// we pass a no-op function for that.
return &cmdHandler{
stdoutPipe: stdoutReader,
stdinPipe: buf,
close: func() error { return nil },
}, nil
},
)
}
-33
View File
@@ -7,7 +7,6 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
@@ -217,38 +216,6 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
return c.ExecutableFromString(shellCommand)
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, cmdObj, output)
}
// DetectUnamePass detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, promptUserForCredential func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(cmdObj, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
`.+'s password:`: "password",
`Password\s*for\s*'.+':`: "password",
`Username\s*for\s*'.+':`: "username",
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
}
for pattern, askFor := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return promptUserForCredential(askFor)
}
}
return ""
})
return errMessage
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
+7 -1
View File
@@ -2,6 +2,8 @@ package commands
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
func (c *GitCommand) AddRemote(name string, url string) error {
@@ -23,7 +25,11 @@ func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(command)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCredential func(string) string) error {
return c.OSCommand.DetectUnamePass(cmdObj, c.GetCmdWriter(), promptUserForCredential)
}
// CheckRemoteBranchExists Returns remote branch
+5 -5
View File
@@ -38,7 +38,7 @@ func (c *GitCommand) Push(opts PushOpts) error {
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
type FetchOptions struct {
@@ -59,7 +59,7 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, func(question string) string {
return c.DetectUnamePass(cmdObj, func(question string) string {
if opts.PromptUserForCredential != nil {
return opts.PromptUserForCredential(question)
}
@@ -95,17 +95,17 @@ func (c *GitCommand) Pull(opts PullOptions) error {
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
+1 -1
View File
@@ -15,5 +15,5 @@ func (c *GitCommand) DeleteTag(tagName string) error {
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
+19 -1
View File
@@ -24,6 +24,7 @@ type RefresherConfig struct {
}
type GuiConfig struct {
AuthorColors map[string]string `yaml:"authorColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
@@ -68,7 +69,9 @@ type GitConfig struct {
OverrideGpg bool `yaml:"overrideGpg"`
DisableForcePushing bool `yaml:"disableForcePushing"`
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
ParseEmoji bool `yaml:"parseEmoji"`
// this shoudl really be under 'gui', not 'git'
ParseEmoji bool `yaml:"parseEmoji"`
Log LogConfig `yaml:"log"`
}
type PagingConfig struct {
@@ -82,6 +85,11 @@ type MergingConfig struct {
Args string `yaml:"args"`
}
type LogConfig struct {
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
}
type CommitPrefixConfig struct {
Pattern string `yaml:"pattern"`
Replace string `yaml:"replace"`
@@ -117,6 +125,8 @@ type KeybindingUniversalConfig struct {
NextItemAlt string `yaml:"nextItem-alt"`
PrevPage string `yaml:"prevPage"`
NextPage string `yaml:"nextPage"`
ScrollLeft string `yaml:"scrollLeft"`
ScrollRight string `yaml:"scrollRight"`
GotoTop string `yaml:"gotoTop"`
GotoBottom string `yaml:"gotoBottom"`
PrevBlock string `yaml:"prevBlock"`
@@ -227,6 +237,7 @@ type KeybindingCommitsConfig struct {
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
OpenLogMenu string `yaml:"openLogMenu"`
}
type KeybindingStashConfig struct {
@@ -337,6 +348,10 @@ func GetDefaultConfig() *UserConfig {
ManualCommit: false,
Args: "",
},
Log: LogConfig{
Order: "topo-order",
ShowGraph: "when-maximised",
},
SkipHookPrefix: "WIP",
AutoFetch: true,
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
@@ -370,6 +385,8 @@ func GetDefaultConfig() *UserConfig {
NextItemAlt: "j",
PrevPage: ",",
NextPage: ".",
ScrollLeft: "H",
ScrollRight: "L",
GotoTop: "<",
GotoBottom: ">",
PrevBlock: "<left>",
@@ -476,6 +493,7 @@ func GetDefaultConfig() *UserConfig {
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
CopyCommitMessageToClipboard: "<c-y>",
OpenLogMenu: "<c-l>",
},
Stash: KeybindingStashConfig{
PopStash: "g",
+1 -1
View File
@@ -33,7 +33,7 @@ func (gui *Gui) handleCopyCommit() error {
return err
}
item, ok := context.SelectedItem()
item, ok := context.GetSelectedItem()
if !ok {
return nil
}
+1 -1
View File
@@ -22,9 +22,9 @@ func (gui *Gui) handleCommitConfirm() error {
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
_ = gui.returnFromContext()
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
gui.Views.CommitMessage.ClearTextArea()
_ = gui.returnFromContext()
return nil
})
}
+90 -2
View File
@@ -10,6 +10,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// after selecting the 200th commit, we'll load in all the rest
const COMMIT_THRESHOLD = 200
// list panel functions
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
@@ -23,7 +26,7 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
func (gui *Gui) handleCommitSelect() error {
state := gui.State.Panels.Commits
if state.SelectedLineIdx > 290 && state.LimitCommits {
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
state.LimitCommits = false
go utils.Safe(func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
@@ -122,6 +125,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
FilterPath: gui.State.Modes.Filtering.GetPath(),
IncludeRebaseCommits: true,
RefName: "HEAD",
All: gui.State.ShowWholeGitGraph,
},
)
if err != nil {
@@ -640,7 +644,7 @@ func (gui *Gui) handleGotoBottomForCommitsPanel() error {
}
for _, context := range gui.getListContexts() {
if context.ViewName == "commits" {
if context.GetViewName() == "commits" {
return context.handleGotoBottom()
}
}
@@ -667,3 +671,87 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
return nil
}
func (gui *Gui) handleOpenLogMenu() error {
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
{
displayString: gui.Tr.ToggleShowGitGraphAll,
onPress: func() error {
gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph
if gui.State.ShowWholeGitGraph {
gui.State.Panels.Commits.LimitCommits = false
}
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
})
},
},
{
displayString: gui.Tr.ShowGitGraph,
opensMenu: true,
onPress: func() error {
onSelect := func(value string) {
gui.Config.GetUserConfig().Git.Log.ShowGraph = value
gui.render()
}
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
{
displayString: "always",
onPress: func() error {
onSelect("always")
return nil
},
},
{
displayString: "never",
onPress: func() error {
onSelect("never")
return nil
},
},
{
displayString: "when maximised",
onPress: func() error {
onSelect("when-maximised")
return nil
},
},
}, createMenuOptions{showCancel: true})
},
},
{
displayString: gui.Tr.SortCommits,
opensMenu: true,
onPress: func() error {
onSelect := func(value string) error {
gui.Config.GetUserConfig().Git.Log.Order = value
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
})
}
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
{
displayString: "topological (topo-order)",
onPress: func() error {
return onSelect("topo-order")
},
},
{
displayString: "date-order",
onPress: func() error {
return onSelect("date-order")
},
},
{
displayString: "author-date-order",
onPress: func() error {
return onSelect("author-date-order")
},
},
}, createMenuOptions{showCancel: true})
},
},
}, createMenuOptions{showCancel: true})
}
+4 -2
View File
@@ -285,9 +285,9 @@ func (gui *Gui) currentContextWithoutLock() Context {
// the status panel is not yet a list context (and may never be), so this method is not
// quite the same as currentSideContext()
func (gui *Gui) currentSideListContext() *ListContext {
func (gui *Gui) currentSideListContext() IListContext {
context := gui.currentSideContext()
listContext, ok := context.(*ListContext)
listContext, ok := context.(IListContext)
if !ok {
return nil
}
@@ -399,6 +399,8 @@ func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error
return nil
}
_ = oldView.SetOriginX(0)
if oldView == gui.Views.CommitFiles && newView != gui.Views.Main && newView != gui.Views.Secondary && newView != gui.Views.Search {
gui.resetWindowForView(gui.Views.CommitFiles)
if err := gui.deactivateContext(gui.State.Contexts.CommitFiles); err != nil {
+13 -13
View File
@@ -56,19 +56,19 @@ var allContextKeys = []ContextKey{
type ContextTree struct {
Status Context
Files *ListContext
Submodules *ListContext
Menu *ListContext
Branches *ListContext
Remotes *ListContext
RemoteBranches *ListContext
Tags *ListContext
BranchCommits *ListContext
CommitFiles *ListContext
ReflogCommits *ListContext
SubCommits *ListContext
Stash *ListContext
Suggestions *ListContext
Files IListContext
Submodules IListContext
Menu IListContext
Branches IListContext
Remotes IListContext
RemoteBranches IListContext
Tags IListContext
BranchCommits IListContext
CommitFiles IListContext
ReflogCommits IListContext
SubCommits IListContext
Stash IListContext
Suggestions IListContext
Normal Context
Staging Context
PatchBuilding Context
+31
View File
@@ -1,5 +1,11 @@
package gui
import (
"io"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
func (gui *Gui) handleCreateExtrasMenuPanel() error {
menuItems := []*menuItem{
{
@@ -48,3 +54,28 @@ func (gui *Gui) scrollDownExtra() error {
return nil
}
func (gui *Gui) getCmdWriter() io.Writer {
return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.Tr.GitOutput)}
}
// Ensures that the first write is preceded by writing a prefix.
// This allows us to say 'Git output:' before writing the actual git output.
// We could just write directly to the view in this package before running the command but we already have code in the commands package that writes to the same view beforehand (with the command it's about to run) so things would be out of order.
type prefixWriter struct {
prefix string
prefixWritten bool
writer io.Writer
}
func (self *prefixWriter) Write(p []byte) (n int, err error) {
if !self.prefixWritten {
self.prefixWritten = true
// assuming we can write this prefix in one go
_, err = self.writer.Write([]byte(self.prefix))
if err != nil {
return
}
}
return self.writer.Write(p)
}
+3 -4
View File
@@ -393,10 +393,9 @@ func (gui *Gui) handleCommitPress() error {
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error()))
}
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
gui.renderString(gui.Views.CommitMessage, prefix)
if err := gui.Views.CommitMessage.SetCursor(len(prefix), 0); err != nil {
return err
}
gui.Views.CommitMessage.ClearTextArea()
gui.Views.CommitMessage.TextArea.TypeString(prefix)
gui.render()
}
gui.g.Update(func(g *gocui.Gui) error {
+3 -1
View File
@@ -9,9 +9,11 @@ import (
"github.com/xo/terminfo"
)
func TestRender(t *testing.T) {
func init() {
color.ForceSetColorLevel(terminfo.ColorLevelNone)
}
func TestRender(t *testing.T) {
scenarios := []struct {
name string
root *FileNode
+25 -2
View File
@@ -10,6 +10,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
const HORIZONTAL_SCROLL_FACTOR = 3
// these views need to be re-rendered when the screen mode changes. The commits view,
// for example, will show authorship information in half and full screen mode.
func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
@@ -114,7 +116,7 @@ func (gui *Gui) linesToScrollDown(view *gocui.View) int {
func (gui *Gui) scrollUpMain() error {
if gui.canScrollMergePanel() {
gui.State.Panels.Merging.UserScrolling = true
gui.State.Panels.Merging.UserVerticalScrolling = true
}
return gui.scrollUpView(gui.Views.Main)
@@ -122,12 +124,33 @@ func (gui *Gui) scrollUpMain() error {
func (gui *Gui) scrollDownMain() error {
if gui.canScrollMergePanel() {
gui.State.Panels.Merging.UserScrolling = true
gui.State.Panels.Merging.UserVerticalScrolling = true
}
return gui.scrollDownView(gui.Views.Main)
}
func (gui *Gui) scrollLeftMain() error {
gui.scrollLeft(gui.Views.Main)
return nil
}
func (gui *Gui) scrollRightMain() error {
gui.scrollRight(gui.Views.Main)
return nil
}
func (gui *Gui) scrollLeft(view *gocui.View) {
newOriginX := utils.Max(view.OriginX()-view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR, 0)
_ = view.SetOriginX(newOriginX)
}
func (gui *Gui) scrollRight(view *gocui.View) {
_ = view.SetOriginX(view.OriginX() + view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR)
}
func (gui *Gui) scrollUpSecondary() error {
return gui.scrollUpView(gui.Views.Secondary)
}
+37 -16
View File
@@ -1,5 +1,11 @@
package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
// Currently there is a bug where if we switch to a subprocess from within
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
// fix this bug, or just stop running subprocesses from within there, given that
@@ -19,23 +25,38 @@ func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess f
return err
}
if err != nil {
return err
}
return err
} else {
return gui.WithWaitingStatus(waitingStatus, func() error {
err := gui.OSCommand.RunCommand(cmdStr)
if err != nil {
return err
} else if onSuccess != nil {
if err := onSuccess(); err != nil {
return err
}
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
return gui.RunAndStream(cmdStr, waitingStatus, onSuccess)
}
}
return nil
func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func() error) error {
return gui.WithWaitingStatus(waitingStatus, func() error {
cmd := gui.OSCommand.ShellCommandFromString(cmdStr)
cmd.Env = append(cmd.Env, "TERM=dumb")
cmdWriter := gui.getCmdWriter()
cmd.Stdout = cmdWriter
cmd.Stderr = cmdWriter
if err := cmd.Run(); err != nil {
if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); err != nil {
gui.Log.Error(err)
}
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.surfaceError(
fmt.Errorf(
gui.Tr.GitCommandFailed, gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu,
),
)
}
if onSuccess != nil {
if err := onSuccess(); err != nil {
return err
}
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}
+19 -6
View File
@@ -22,6 +22,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
@@ -144,9 +146,9 @@ type LblPanelState struct {
type MergingPanelState struct {
*mergeconflicts.State
// UserScrolling tells us if the user has started scrolling through the file themselves
// UserVerticalScrolling tells us if the user has started scrolling through the file themselves
// in which case we won't auto-scroll to a conflict.
UserScrolling bool
UserVerticalScrolling bool
}
type filePanelState struct {
@@ -313,6 +315,8 @@ type guiState struct {
RetainOriginalDir bool
IsRefreshingFiles bool
Searching searchingState
// if this is true, we'll load our commits using `git log --all`
ShowWholeGitGraph bool
ScreenMode WindowMaximisation
Ptmx *os.File
PrevMainWidth int
@@ -394,7 +398,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
Tags: &tagsPanelState{listPanelState{SelectedLineIdx: -1}},
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, LimitCommits: true},
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, LimitCommits: true},
ReflogCommits: &reflogCommitPanelState{listPanelState{SelectedLineIdx: 0}},
SubCommits: &subCommitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, refName: ""},
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},
@@ -402,8 +406,8 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
Merging: &MergingPanelState{
State: mergeconflicts.NewState(),
UserScrolling: false,
State: mergeconflicts.NewState(),
UserVerticalScrolling: false,
},
},
Ptmx: nil,
@@ -452,9 +456,17 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
oSCommand.SetOnRunCommand(onRunCommand)
gui.OnRunCommand = onRunCommand
authors.SetCustomAuthors(gui.Config.GetUserConfig().Gui.AuthorColors)
return gui, nil
}
var RuneReplacements = map[rune]string{
// for the commit graph
graph.MergeSymbol: "M",
graph.CommitSymbol: "o",
}
// Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error {
recordEvents := recordingEvents()
@@ -465,7 +477,7 @@ func (gui *Gui) Run() error {
playMode = gocui.REPLAYING
}
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless())
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless(), RuneReplacements)
if err != nil {
return err
}
@@ -709,6 +721,7 @@ func (gui *Gui) startBackgroundFetch() {
} else {
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error {
err := gui.fetch(false, "")
gui.render()
return err
})
}
+22
View File
@@ -724,6 +724,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleFetchRemote,
Description: gui.Tr.LcFetchRemote,
},
{
ViewName: "commits",
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
Key: gui.getKey(config.Commits.OpenLogMenu),
Handler: gui.handleOpenLogMenu,
Description: gui.Tr.LcOpenLogMenu,
OpensMenu: true,
},
{
ViewName: "commits",
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
@@ -1426,6 +1434,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY), string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.ScrollLeft),
Handler: gui.scrollLeftMain,
Description: gui.Tr.LcScrollLeft,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY), string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.ScrollRight),
Handler: gui.scrollRightMain,
Description: gui.Tr.LcScrollRight,
},
{
ViewName: "main",
Contexts: []string{string(MAIN_STAGING_CONTEXT_KEY)},
+5 -9
View File
@@ -52,24 +52,19 @@ func (gui *Gui) createAllViews() error {
gui.Views.Stash.Title = gui.Tr.StashTitle
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
gui.Views.Stash.ContainsList = true
gui.Views.Commits.Title = gui.Tr.CommitsTitle
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
gui.Views.Commits.ContainsList = true
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitFiles.ContainsList = true
gui.Views.Branches.Title = gui.Tr.BranchesTitle
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Branches.ContainsList = true
gui.Views.Files.Highlight = true
gui.Views.Files.Title = gui.Tr.FilesTitle
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.ContainsList = true
gui.Views.Secondary.Title = gui.Tr.DiffTitle
gui.Views.Secondary.Wrap = true
@@ -111,7 +106,6 @@ func (gui *Gui) createAllViews() error {
gui.Views.Credentials.Editable = true
gui.Views.Suggestions.Visible = false
gui.Views.Suggestions.ContainsList = true
gui.Views.Menu.Visible = false
@@ -123,12 +117,15 @@ func (gui *Gui) createAllViews() error {
gui.Views.Extras.FgColor = theme.GocuiDefaultTextColor
gui.Views.Extras.Autoscroll = true
gui.Views.Extras.Wrap = true
gui.printCommandLogHeader()
if _, err := gui.g.SetCurrentView(gui.defaultSideContext().GetViewName()); err != nil {
return err
}
gui.GitCommand.GetCmdWriter = gui.getCmdWriter
return nil
}
@@ -261,7 +258,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
for _, listContext := range gui.getListContexts() {
view, err := gui.g.View(listContext.ViewName)
view, err := gui.g.View(listContext.GetViewName())
if err != nil {
continue
}
@@ -271,8 +268,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
continue
}
// check if the selected line is now out of view and if so refocus it
view.FocusPoint(0, listContext.GetPanelState().GetSelectedLineIdx())
listContext.FocusLine()
view.SelBgColor = theme.GocuiSelectedLineBgColor
+1 -1
View File
@@ -174,7 +174,7 @@ func (gui *Gui) focusSelection(state *LblPanelState) error {
newOrigin := state.CalculateOrigin(origin, bufferHeight)
gui.g.Update(func(*gocui.Gui) error {
if err := stagingView.SetOrigin(0, newOrigin); err != nil {
if err := stagingView.SetOriginY(newOrigin); err != nil {
return err
}
+158 -76
View File
@@ -1,21 +1,57 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
)
type ListContext struct {
GetItemsLength func() int
GetDisplayStrings func() [][]string
GetDisplayStrings func(startIdx int, length int) [][]string
OnFocus func() error
OnFocusLost func() error
OnClickSelectedItem func() error
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
SelectedItem func() (ListItem, bool)
GetPanelState func() IListPanelState
SelectedItem func() (ListItem, bool)
OnGetPanelState func() IListPanelState
// if this is true, we'll call GetDisplayStrings for just the visible part of the
// view and re-render that. This is useful when you need to render different
// content based on the selection (e.g. for showing the selected commit)
RenderSelection bool
Gui *Gui
*BasicContext
}
type IListContext interface {
GetSelectedItem() (ListItem, bool)
GetSelectedItemId() string
OnRender() error
handlePrevLine() error
handleNextLine() error
handleScrollLeft() error
handleScrollRight() error
handleLineChange(change int) error
handleNextPage() error
handleGotoTop() error
handleGotoBottom() error
handlePrevPage() error
handleClick() error
onSearchSelect(selectedLineIdx int) error
FocusLine()
GetPanelState() IListPanelState
Context
}
func (self *ListContext) GetPanelState() IListPanelState {
return self.OnGetPanelState()
}
type IListPanelState interface {
SetSelectedLineIdx(int)
GetSelectedLineIdx() int
@@ -29,12 +65,33 @@ type ListItem interface {
Description() string
}
func (lc *ListContext) GetSelectedItem() (ListItem, bool) {
return lc.SelectedItem()
func (self *ListContext) FocusLine() {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
// ignoring error for now
return
}
// we need a way of knowing whether we've rendered to the view yet.
view.FocusPoint(view.OriginX(), self.GetPanelState().GetSelectedLineIdx())
if self.RenderSelection {
_, originY := view.Origin()
displayStrings := self.GetDisplayStrings(originY, view.InnerHeight())
self.Gui.renderDisplayStringsAtPos(view, originY, displayStrings)
}
view.Footer = formatListFooter(self.GetPanelState().GetSelectedLineIdx(), self.GetItemsLength())
}
func (lc *ListContext) GetSelectedItemId() string {
item, ok := lc.SelectedItem()
func formatListFooter(selectedLineIdx int, length int) string {
return fmt.Sprintf("%d of %d", selectedLineIdx+1, length)
}
func (self *ListContext) GetSelectedItem() (ListItem, bool) {
return self.SelectedItem()
}
func (self *ListContext) GetSelectedItemId() string {
item, ok := self.GetSelectedItem()
if !ok {
return ""
@@ -44,145 +101,170 @@ func (lc *ListContext) GetSelectedItemId() string {
}
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
func (lc *ListContext) OnRender() error {
view, err := lc.Gui.g.View(lc.ViewName)
func (self *ListContext) OnRender() error {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
}
if lc.GetDisplayStrings != nil {
lc.Gui.refreshSelectedLine(lc.GetPanelState(), lc.GetItemsLength())
lc.Gui.renderDisplayStrings(view, lc.GetDisplayStrings())
if self.GetDisplayStrings != nil {
self.Gui.refreshSelectedLine(self.GetPanelState(), self.GetItemsLength())
self.Gui.renderDisplayStrings(view, self.GetDisplayStrings(0, self.GetItemsLength()))
self.Gui.render()
}
return nil
}
func (lc *ListContext) HandleFocusLost() error {
if lc.OnFocusLost != nil {
return lc.OnFocusLost()
func (self *ListContext) HandleFocusLost() error {
if self.OnFocusLost != nil {
return self.OnFocusLost()
}
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
}
_ = view.SetOriginX(0)
return nil
}
func (self *ListContext) HandleFocus() error {
if self.Gui.popupPanelFocused() {
return nil
}
self.FocusLine()
if self.Gui.State.Modes.Diffing.Active() {
return self.Gui.renderDiff()
}
if self.OnFocus != nil {
return self.OnFocus()
}
return nil
}
func (lc *ListContext) HandleFocus() error {
if lc.Gui.popupPanelFocused() {
func (self *ListContext) HandleRender() error {
return self.OnRender()
}
func (self *ListContext) handlePrevLine() error {
return self.handleLineChange(-1)
}
func (self *ListContext) handleNextLine() error {
return self.handleLineChange(1)
}
func (self *ListContext) handleScrollLeft() error {
return self.scroll(self.Gui.scrollLeft)
}
func (self *ListContext) handleScrollRight() error {
return self.scroll(self.Gui.scrollRight)
}
func (self *ListContext) scroll(scrollFunc func(*gocui.View)) error {
if self.ignoreKeybinding() {
return nil
}
view, err := lc.Gui.g.View(lc.ViewName)
// get the view, move the origin
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
}
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
scrollFunc(view)
if lc.Gui.State.Modes.Diffing.Active() {
return lc.Gui.renderDiff()
}
if lc.OnFocus != nil {
return lc.OnFocus()
}
return nil
return self.HandleFocus()
}
func (lc *ListContext) HandleRender() error {
return lc.OnRender()
func (self *ListContext) ignoreKeybinding() bool {
return !self.Gui.isPopupPanel(self.ViewName) && self.Gui.popupPanelFocused()
}
func (lc *ListContext) handlePrevLine() error {
return lc.handleLineChange(-1)
}
func (lc *ListContext) handleNextLine() error {
return lc.handleLineChange(1)
}
func (lc *ListContext) handleLineChange(change int) error {
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
func (self *ListContext) handleLineChange(change int) error {
if self.ignoreKeybinding() {
return nil
}
view, err := lc.Gui.g.View(lc.ViewName)
if err != nil {
return err
}
selectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
if (change < 0 && selectedLineIdx == 0) || (change > 0 && selectedLineIdx == lc.GetItemsLength()-1) {
selectedLineIdx := self.GetPanelState().GetSelectedLineIdx()
if (change < 0 && selectedLineIdx == 0) || (change > 0 && selectedLineIdx == self.GetItemsLength()-1) {
return nil
}
lc.Gui.changeSelectedLine(lc.GetPanelState(), lc.GetItemsLength(), change)
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
self.Gui.changeSelectedLine(self.GetPanelState(), self.GetItemsLength(), change)
return lc.HandleFocus()
return self.HandleFocus()
}
func (lc *ListContext) handleNextPage() error {
view, err := lc.Gui.g.View(lc.ViewName)
func (self *ListContext) handleNextPage() error {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
}
delta := lc.Gui.pageDelta(view)
delta := self.Gui.pageDelta(view)
return lc.handleLineChange(delta)
return self.handleLineChange(delta)
}
func (lc *ListContext) handleGotoTop() error {
return lc.handleLineChange(-lc.GetItemsLength())
func (self *ListContext) handleGotoTop() error {
return self.handleLineChange(-self.GetItemsLength())
}
func (lc *ListContext) handleGotoBottom() error {
return lc.handleLineChange(lc.GetItemsLength())
func (self *ListContext) handleGotoBottom() error {
return self.handleLineChange(self.GetItemsLength())
}
func (lc *ListContext) handlePrevPage() error {
view, err := lc.Gui.g.View(lc.ViewName)
func (self *ListContext) handlePrevPage() error {
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
}
delta := lc.Gui.pageDelta(view)
delta := self.Gui.pageDelta(view)
return lc.handleLineChange(-delta)
return self.handleLineChange(-delta)
}
func (lc *ListContext) handleClick() error {
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
func (self *ListContext) handleClick() error {
if self.ignoreKeybinding() {
return nil
}
view, err := lc.Gui.g.View(lc.ViewName)
view, err := self.Gui.g.View(self.ViewName)
if err != nil {
return nil
}
prevSelectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
prevSelectedLineIdx := self.GetPanelState().GetSelectedLineIdx()
newSelectedLineIdx := view.SelectedLineIdx()
// we need to focus the view
if err := lc.Gui.pushContext(lc); err != nil {
if err := self.Gui.pushContext(self); err != nil {
return err
}
if newSelectedLineIdx > lc.GetItemsLength()-1 {
if newSelectedLineIdx > self.GetItemsLength()-1 {
return nil
}
lc.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
self.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
prevViewName := lc.Gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lc.ViewName && lc.OnClickSelectedItem != nil {
return lc.OnClickSelectedItem()
prevViewName := self.Gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == self.ViewName && self.OnClickSelectedItem != nil {
return self.OnClickSelectedItem()
}
return lc.HandleFocus()
return self.HandleFocus()
}
func (lc *ListContext) onSearchSelect(selectedLineIdx int) error {
lc.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
return lc.HandleFocus()
func (self *ListContext) onSearchSelect(selectedLineIdx int) error {
self.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
return self.HandleFocus()
}
+126 -84
View File
@@ -1,12 +1,14 @@
package gui
import (
"log"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
func (gui *Gui) menuListContext() *ListContext {
func (gui *Gui) menuListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "menu",
@@ -15,7 +17,7 @@ func (gui *Gui) menuListContext() *ListContext {
OnGetOptionsMap: gui.getMenuOptions,
},
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: gui.onMenuPress,
Gui: gui,
@@ -24,7 +26,7 @@ func (gui *Gui) menuListContext() *ListContext {
}
}
func (gui *Gui) filesListContext() *ListContext {
func (gui *Gui) filesListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "files",
@@ -33,11 +35,11 @@ func (gui *Gui) filesListContext() *ListContext {
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnFocus: gui.focusAndSelectFile,
OnClickSelectedItem: gui.handleFilePress,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetDisplayStrings: func(startIdx int, length int) [][]string {
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
@@ -53,7 +55,7 @@ func (gui *Gui) filesListContext() *ListContext {
}
}
func (gui *Gui) branchesListContext() *ListContext {
func (gui *Gui) branchesListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
@@ -61,11 +63,11 @@ func (gui *Gui) branchesListContext() *ListContext {
Key: LOCAL_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Branches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
OnFocus: gui.handleBranchSelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.Branches) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
OnFocus: gui.handleBranchSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
@@ -75,7 +77,7 @@ func (gui *Gui) branchesListContext() *ListContext {
}
}
func (gui *Gui) remotesListContext() *ListContext {
func (gui *Gui) remotesListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
@@ -84,11 +86,11 @@ func (gui *Gui) remotesListContext() *ListContext {
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Remotes) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
OnFocus: gui.handleRemoteSelect,
OnClickSelectedItem: gui.handleRemoteEnter,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
@@ -98,7 +100,7 @@ func (gui *Gui) remotesListContext() *ListContext {
}
}
func (gui *Gui) remoteBranchesListContext() *ListContext {
func (gui *Gui) remoteBranchesListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
@@ -106,11 +108,11 @@ func (gui *Gui) remoteBranchesListContext() *ListContext {
Key: REMOTE_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
OnFocus: gui.handleRemoteBranchSelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
OnFocus: gui.handleRemoteBranchSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
@@ -120,7 +122,7 @@ func (gui *Gui) remoteBranchesListContext() *ListContext {
}
}
func (gui *Gui) tagsListContext() *ListContext {
func (gui *Gui) tagsListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
@@ -128,11 +130,11 @@ func (gui *Gui) tagsListContext() *ListContext {
Key: TAGS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Tags) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
OnFocus: gui.handleTagSelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.Tags) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
OnFocus: gui.handleTagSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
@@ -142,7 +144,7 @@ func (gui *Gui) tagsListContext() *ListContext {
}
}
func (gui *Gui) branchCommitsListContext() *ListContext {
func (gui *Gui) branchCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
@@ -152,27 +154,54 @@ func (gui *Gui) branchCommitsListContext() *ListContext {
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Commits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
OnFocus: gui.handleCommitSelect,
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetDisplayStrings: func(startIdx int, length int) [][]string {
selectedCommitSha := ""
if gui.currentContext().GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
selectedCommit := gui.getSelectedLocalCommit()
if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha
}
}
return presentation.GetCommitListDisplayStrings(
gui.State.Commits,
gui.State.ScreenMode != SCREEN_NORMAL,
gui.cherryPickedCommitShaMap(),
gui.State.Modes.Diffing.Ref,
parseEmoji,
selectedCommitSha,
startIdx,
length,
gui.shouldShowGraph(),
)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedLocalCommit()
return item, item != nil
},
RenderSelection: true,
}
}
func (gui *Gui) reflogCommitsListContext() *ListContext {
func (gui *Gui) shouldShowGraph() bool {
value := gui.Config.GetUserConfig().Git.Log.ShowGraph
switch value {
case "always":
return true
case "never":
return false
case "when-maximised":
return gui.State.ScreenMode != SCREEN_NORMAL
}
log.Fatalf("Unknown value for git.log.showGraph: %s. Expected one of: 'always', 'never', 'when-maximised'", value)
return false
}
func (gui *Gui) reflogCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
@@ -181,11 +210,11 @@ func (gui *Gui) reflogCommitsListContext() *ListContext {
Key: REFLOG_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
OnFocus: gui.handleReflogCommitSelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
OnFocus: gui.handleReflogCommitSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetReflogCommitListDisplayStrings(
gui.State.FilteredReflogCommits,
gui.State.ScreenMode != SCREEN_NORMAL,
@@ -201,7 +230,7 @@ func (gui *Gui) reflogCommitsListContext() *ListContext {
}
}
func (gui *Gui) subCommitsListContext() *ListContext {
func (gui *Gui) subCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
@@ -210,27 +239,39 @@ func (gui *Gui) subCommitsListContext() *ListContext {
Key: SUB_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.SubCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
OnFocus: gui.handleSubCommitSelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.SubCommits) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
OnFocus: gui.handleSubCommitSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
selectedCommitSha := ""
if gui.currentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY {
selectedCommit := gui.getSelectedSubCommit()
if selectedCommit != nil {
selectedCommitSha = selectedCommit.Sha
}
}
return presentation.GetCommitListDisplayStrings(
gui.State.SubCommits,
gui.State.ScreenMode != SCREEN_NORMAL,
gui.cherryPickedCommitShaMap(),
gui.State.Modes.Diffing.Ref,
parseEmoji,
selectedCommitSha,
0,
len(gui.State.SubCommits),
gui.shouldShowGraph(),
)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubCommit()
return item, item != nil
},
RenderSelection: true,
}
}
func (gui *Gui) stashListContext() *ListContext {
func (gui *Gui) stashListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "stash",
@@ -238,11 +279,11 @@ func (gui *Gui) stashListContext() *ListContext {
Key: STASH_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.StashEntries) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
OnFocus: gui.handleStashEntrySelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.StashEntries) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
OnFocus: gui.handleStashEntrySelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
@@ -252,7 +293,7 @@ func (gui *Gui) stashListContext() *ListContext {
}
}
func (gui *Gui) commitFilesListContext() *ListContext {
func (gui *Gui) commitFilesListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commitFiles",
@@ -260,11 +301,11 @@ func (gui *Gui) commitFilesListContext() *ListContext {
Key: COMMIT_FILES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
OnFocus: gui.handleCommitFileSelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
OnFocus: gui.handleCommitFileSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
if gui.State.CommitFileManager.GetItemsLength() == 0 {
return [][]string{{style.FgRed.Sprint("(none)")}}
}
@@ -284,7 +325,7 @@ func (gui *Gui) commitFilesListContext() *ListContext {
}
}
func (gui *Gui) submodulesListContext() *ListContext {
func (gui *Gui) submodulesListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "files",
@@ -292,11 +333,11 @@ func (gui *Gui) submodulesListContext() *ListContext {
Key: SUBMODULES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Submodules) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
OnFocus: gui.handleSubmoduleSelect,
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.Submodules) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
OnFocus: gui.handleSubmoduleSelect,
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
},
SelectedItem: func() (ListItem, bool) {
@@ -306,7 +347,7 @@ func (gui *Gui) submodulesListContext() *ListContext {
}
}
func (gui *Gui) suggestionsListContext() *ListContext {
func (gui *Gui) suggestionsListContext() IListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "suggestions",
@@ -314,18 +355,18 @@ func (gui *Gui) suggestionsListContext() *ListContext {
Key: SUGGESTIONS_CONTEXT_KEY,
Kind: PERSISTENT_POPUP,
},
GetItemsLength: func() int { return len(gui.State.Suggestions) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnFocus: func() error { return nil },
Gui: gui,
GetDisplayStrings: func() [][]string {
GetItemsLength: func() int { return len(gui.State.Suggestions) },
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnFocus: func() error { return nil },
Gui: gui,
GetDisplayStrings: func(startIdx int, length int) [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
},
}
}
func (gui *Gui) getListContexts() []*ListContext {
return []*ListContext{
func (gui *Gui) getListContexts() []IListContext {
return []IListContext{
gui.State.Contexts.Menu,
gui.State.Contexts.Files,
gui.State.Contexts.Branches,
@@ -333,7 +374,6 @@ func (gui *Gui) getListContexts() []*ListContext {
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
@@ -352,38 +392,40 @@ func (gui *Gui) getListContextKeyBindings() []*Binding {
listContext := listContext
bindings = append(bindings, []*Binding{
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Contexts: []string{string(listContext.Key)}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.GetViewName(), Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.ScrollLeft), Modifier: gocui.ModNone, Handler: listContext.handleScrollLeft},
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.ScrollRight), Modifier: gocui.ModNone, Handler: listContext.handleScrollRight},
}...)
// the commits panel needs to lazyload things so it has a couple of its own handlers
openSearchHandler := gui.handleOpenSearch
gotoBottomHandler := listContext.handleGotoBottom
if listContext.ViewName == "commits" {
if listContext.GetViewName() == "commits" {
openSearchHandler = gui.handleOpenSearchForCommitsPanel
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
}
bindings = append(bindings, []*Binding{
{
ViewName: listContext.ViewName,
Contexts: []string{string(listContext.Key)},
ViewName: listContext.GetViewName(),
Contexts: []string{string(listContext.GetKey())},
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
Handler: func() error { return openSearchHandler(listContext.ViewName) },
Handler: func() error { return openSearchHandler(listContext.GetViewName()) },
Description: gui.Tr.LcStartSearch,
Tag: "navigation",
},
{
ViewName: listContext.ViewName,
Contexts: []string{string(listContext.Key)},
ViewName: listContext.GetViewName(),
Contexts: []string{string(listContext.GetKey())},
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
Handler: gotoBottomHandler,
Description: gui.Tr.LcGotoBottom,
+13 -4
View File
@@ -1,6 +1,7 @@
package gui
import (
"errors"
"fmt"
"strings"
@@ -13,6 +14,8 @@ type menuItem struct {
displayString string
displayStrings []string
onPress func() error
// only applies when displayString is used
opensMenu bool
}
// every item in a list context needs an ID
@@ -65,8 +68,16 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
stringArrays := make([][]string, len(items))
for i, item := range items {
if item.opensMenu && item.displayStrings != nil {
return errors.New("Message for the developer of this app: you've set opensMenu with displaystrings on the menu panel. Bad developer!. Apologies, user")
}
if item.displayStrings == nil {
stringArrays[i] = []string{item.displayString}
styledStr := item.displayString
if item.opensMenu {
styledStr = opensMenuStyle(styledStr)
}
stringArrays[i] = []string{styledStr}
} else {
stringArrays[i] = item.displayStrings
}
@@ -78,12 +89,10 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = theme.GocuiDefaultTextColor
menuView.ContainsList = true
menuView.Clear()
menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
return nil
}))
fmt.Fprint(menuView, list)
menuView.SetContent(list)
gui.State.Panels.Menu.SelectedLineIdx = 0
gui.g.Update(func(g *gocui.Gui) error {
+3 -3
View File
@@ -203,7 +203,7 @@ func (gui *Gui) catSelectedFile() (string, error) {
}
func (gui *Gui) scrollToConflict() error {
if gui.State.Panels.Merging.UserScrolling {
if gui.State.Panels.Merging.UserVerticalScrolling {
return nil
}
@@ -285,7 +285,7 @@ func (gui *Gui) promptToContinueRebase() error {
return err
}
return gui.genericMergeCommand("continue")
return gui.genericMergeCommand(REBASE_OPTION_CONTINUE)
},
handleClose: func() error {
return gui.pushContext(gui.State.Contexts.Files)
@@ -315,5 +315,5 @@ func (gui *Gui) withMergeConflictLock(f func() error) error {
}
func (gui *Gui) takeOverMergeConflictScrolling() {
gui.State.Panels.Merging.UserScrolling = false
gui.State.Panels.Merging.UserVerticalScrolling = false
}
+15
View File
@@ -1,6 +1,7 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
@@ -58,5 +59,19 @@ func (gui *Gui) modeStatuses() []modeStatus {
},
reset: gui.exitCherryPickingMode,
},
{
isActive: func() bool {
return gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL
},
description: func() string {
workingTreeState := gui.GitCommand.WorkingTreeState()
return style.FgYellow.Sprintf(
"%s %s",
workingTreeState,
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
)
},
reset: gui.abortMergeOrRebaseWithConfirm,
},
}
}
+5 -1
View File
@@ -36,12 +36,16 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
func (gui *Gui) displayDescription(binding *Binding) string {
if binding.OpensMenu {
return style.FgMagenta.Sprintf("%s...", binding.Description)
return opensMenuStyle(binding.Description)
}
return style.FgCyan.Sprint(binding.Description)
}
func opensMenuStyle(str string) string {
return style.FgMagenta.Sprintf("%s...", str)
}
func (gui *Gui) handleCreateOptionsMenu() error {
view := gui.g.CurrentView()
if view == nil {
+112
View File
@@ -0,0 +1,112 @@
package authors
import (
"crypto/md5"
"strings"
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/lucasb-eyer/go-colorful"
"github.com/mattn/go-runewidth"
)
// if these being global variables causes trouble we can wrap them in a struct
// attached to the gui state.
var authorInitialCache = make(map[string]string)
var authorNameCache = make(map[string]string)
var authorStyleCache = make(map[string]style.TextStyle)
func ShortAuthor(authorName string) string {
if value, ok := authorInitialCache[authorName]; ok {
return value
}
initials := getInitials(authorName)
if initials == "" {
return ""
}
value := AuthorStyle(authorName).Sprint(initials)
authorInitialCache[authorName] = value
return value
}
func LongAuthor(authorName string) string {
if value, ok := authorNameCache[authorName]; ok {
return value
}
paddedAuthorName := utils.WithPadding(authorName, 17)
truncatedName := utils.TruncateWithEllipsis(paddedAuthorName, 17)
value := AuthorStyle(authorName).Sprint(truncatedName)
authorNameCache[authorName] = value
return value
}
func AuthorStyle(authorName string) style.TextStyle {
if value, ok := authorStyleCache[authorName]; ok {
return value
}
value := trueColorStyle(authorName)
authorStyleCache[authorName] = value
return value
}
func trueColorStyle(str string) style.TextStyle {
hash := md5.Sum([]byte(str))
c := colorful.Hsl(randFloat(hash[0:4])*360.0, 0.6+0.4*randFloat(hash[4:8]), 0.4+randFloat(hash[8:12])*0.2)
return style.New().SetFg(style.NewRGBColor(color.RGB(uint8(c.R*255), uint8(c.G*255), uint8(c.B*255))))
}
func randFloat(hash []byte) float64 {
return float64(randInt(hash, 100)) / 100
}
func randInt(hash []byte, max int) int {
sum := 0
for _, b := range hash {
sum = (sum + int(b)) % max
}
return sum
}
func getInitials(authorName string) string {
if authorName == "" {
return authorName
}
firstRune := getFirstRune(authorName)
if runewidth.RuneWidth(firstRune) > 1 {
return string(firstRune)
}
split := strings.Split(authorName, " ")
if len(split) == 1 {
return utils.LimitStr(authorName, 2)
}
return utils.LimitStr(split[0], 1) + utils.LimitStr(split[1], 1)
}
func getFirstRune(str string) rune {
// just using the loop for the sake of getting the first rune
for _, r := range str {
return r
}
// should never land here
return 0
}
func SetCustomAuthors(customAuthorColors map[string]string) {
for authorName, colorSequence := range customAuthorColors {
style := style.New().SetFg(style.NewRGBColor(color.HEX(colorSequence, false)))
authorStyleCache[authorName] = style
}
}
@@ -0,0 +1,20 @@
package authors
import "testing"
func TestGetInitials(t *testing.T) {
for input, expectedOutput := range map[string]string{
"Jesse Duffield": "JD",
"Jesse Duffield Man": "JD",
"JesseDuffield": "Je",
"J": "J",
"六书六書": "六",
"書": "書",
"": "",
} {
output := getInitials(input)
if output != expectedOutput {
t.Errorf("Expected %s to be %s", output, expectedOutput)
}
}
}
+131 -88
View File
@@ -2,120 +2,163 @@ package presentation
import (
"strings"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/kyokomi/emoji/v2"
)
func GetCommitListDisplayStrings(commits []*models.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string, parseEmoji bool) [][]string {
lines := make([][]string, len(commits))
type pipeSetCacheKey struct {
commitSha string
commitCount int
}
var displayFunc func(*models.Commit, map[string]bool, bool, bool) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForCommit
var pipeSetCache = make(map[pipeSetCacheKey][][]*graph.Pipe)
var mutex sync.Mutex
func GetCommitListDisplayStrings(
commits []*models.Commit,
fullDescription bool,
cherryPickedCommitShaMap map[string]bool,
diffName string,
parseEmoji bool,
selectedCommitSha string,
startIdx int,
length int,
showGraph bool,
) [][]string {
mutex.Lock()
defer mutex.Unlock()
if len(commits) == 0 {
return nil
}
// given that our cache key is a commit sha and a commit count, it's very important that we don't actually try to render pipes
// when dealing with things like filtered commits.
cacheKey := pipeSetCacheKey{
commitSha: commits[0].Sha,
commitCount: len(commits),
}
pipeSets, ok := pipeSetCache[cacheKey]
if !ok {
// pipe sets are unique to a commit head. and a commit count. Sometimes we haven't loaded everything for that.
// so let's just cache it based on that.
getStyle := func(commit *models.Commit) style.TextStyle {
return authors.AuthorStyle(commit.Author)
}
pipeSets = graph.GetPipeSets(commits, getStyle)
pipeSetCache[cacheKey] = pipeSets
}
if startIdx > len(commits) {
return nil
}
end := startIdx + length
if end > len(commits)-1 {
end = len(commits) - 1
}
filteredCommits := commits[startIdx : end+1]
var getGraphLine func(int) string
if showGraph {
filteredPipeSets := pipeSets[startIdx : end+1]
graphLines := graph.RenderAux(filteredPipeSets, filteredCommits, selectedCommitSha)
getGraphLine = func(idx int) string { return graphLines[idx] }
} else {
displayFunc = getDisplayStringsForCommit
getGraphLine = func(idx int) string { return "" }
}
for i := range commits {
diffed := commits[i].Sha == diffName
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed, parseEmoji)
lines := make([][]string, 0, len(filteredCommits))
for i, commit := range filteredCommits {
lines = append(lines, displayCommit(commit, cherryPickedCommitShaMap, diffName, parseEmoji, getGraphLine(i), fullDescription))
}
return lines
}
func getFullDescriptionDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[string]bool, diffed, parseEmoji bool) []string {
shaColor := theme.DefaultTextColor
switch c.Status {
case "unpushed":
shaColor = style.FgRed
case "pushed":
shaColor = style.FgYellow
case "merged":
shaColor = style.FgGreen
case "rebasing":
shaColor = style.FgBlue
case "reflog":
shaColor = style.FgBlue
}
func displayCommit(
commit *models.Commit,
cherryPickedCommitShaMap map[string]bool,
diffName string,
parseEmoji bool,
graphLine string,
fullDescription bool,
) []string {
if diffed {
shaColor = theme.DiffTerminalColor
} else if cherryPickedCommitShaMap[c.Sha] {
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
shaColor = theme.CherryPickedCommitTextStyle
}
tagString := ""
secondColumnString := style.FgBlue.Sprint(utils.UnixToDate(c.UnixTimestamp))
if c.Action != "" {
secondColumnString = actionColorMap(c.Action).Sprint(c.Action)
} else if c.ExtraInfo != "" {
tagString = style.FgMagenta.SetBold().Sprint(c.ExtraInfo) + " "
}
truncatedAuthor := utils.TruncateWithEllipsis(c.Author, 17)
name := c.Name
if parseEmoji {
name = emoji.Sprint(name)
}
return []string{
shaColor.Sprint(c.ShortSha()),
secondColumnString,
style.FgYellow.Sprint(truncatedAuthor),
tagString + theme.DefaultTextColor.Sprint(name),
}
}
func getDisplayStringsForCommit(c *models.Commit, cherryPickedCommitShaMap map[string]bool, diffed, parseEmoji bool) []string {
shaColor := theme.DefaultTextColor
switch c.Status {
case "unpushed":
shaColor = style.FgRed
case "pushed":
shaColor = style.FgYellow
case "merged":
shaColor = style.FgGreen
case "rebasing":
shaColor = style.FgBlue
case "reflog":
shaColor = style.FgBlue
}
if diffed {
shaColor = theme.DiffTerminalColor
} else if cherryPickedCommitShaMap[c.Sha] {
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
shaColor = theme.CherryPickedCommitTextStyle
}
shaColor := getShaColor(commit, diffName, cherryPickedCommitShaMap)
actionString := ""
tagString := ""
if c.Action != "" {
actionString = actionColorMap(c.Action).Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(c.Tags, " ")) + " "
if commit.Action != "" {
actionString = actionColorMap(commit.Action).Sprint(commit.Action) + " "
}
name := c.Name
tagString := ""
if fullDescription {
if commit.ExtraInfo != "" {
tagString = style.FgMagenta.SetBold().Sprint(commit.ExtraInfo) + " "
}
} else {
if len(commit.Tags) > 0 {
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
}
}
name := commit.Name
if parseEmoji {
name = emoji.Sprint(name)
}
return []string{
shaColor.Sprint(c.ShortSha()),
actionString + tagString + theme.DefaultTextColor.Sprint(name),
authorFunc := authors.ShortAuthor
if fullDescription {
authorFunc = authors.LongAuthor
}
cols := make([]string, 0, 5)
cols = append(cols, shaColor.Sprint(commit.ShortSha()))
if fullDescription {
cols = append(cols, style.FgBlue.Sprint(utils.UnixToDate(commit.UnixTimestamp)))
}
cols = append(
cols,
actionString,
authorFunc(commit.Author),
graphLine+tagString+theme.DefaultTextColor.Sprint(name),
)
return cols
}
func getShaColor(commit *models.Commit, diffName string, cherryPickedCommitShaMap map[string]bool) style.TextStyle {
diffed := commit.Sha == diffName
shaColor := theme.DefaultTextColor
switch commit.Status {
case "unpushed":
shaColor = style.FgRed
case "pushed":
shaColor = style.FgYellow
case "merged":
shaColor = style.FgGreen
case "rebasing":
shaColor = style.FgBlue
case "reflog":
shaColor = style.FgBlue
}
if diffed {
shaColor = theme.DiffTerminalColor
} else if cherryPickedCommitShaMap[commit.Sha] {
shaColor = theme.CherryPickedCommitTextStyle
}
return shaColor
}
func actionColorMap(str string) style.TextStyle {
+178
View File
@@ -0,0 +1,178 @@
package graph
import (
"io"
"sync"
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
const MergeSymbol = '⏣'
const CommitSymbol = '◯'
type cellType int
const (
CONNECTION cellType = iota
COMMIT
MERGE
)
type Cell struct {
up, down, left, right bool
cellType cellType
rightStyle *style.TextStyle
style style.TextStyle
}
func (cell *Cell) render(writer io.StringWriter) {
up, down, left, right := cell.up, cell.down, cell.left, cell.right
first, second := getBoxDrawingChars(up, down, left, right)
var adjustedFirst string
switch cell.cellType {
case CONNECTION:
adjustedFirst = first
case COMMIT:
adjustedFirst = string(CommitSymbol)
case MERGE:
adjustedFirst = string(MergeSymbol)
}
var rightStyle *style.TextStyle
if cell.rightStyle == nil {
rightStyle = &cell.style
} else {
rightStyle = cell.rightStyle
}
// just doing this for the sake of easy testing, so that we don't need to
// assert on the style of a space given a space has no styling (assuming we
// stick to only using foreground styles)
var styledSecondChar string
if second == " " {
styledSecondChar = " "
} else {
styledSecondChar = cachedSprint(*rightStyle, second)
}
_, _ = writer.WriteString(cachedSprint(cell.style, adjustedFirst))
_, _ = writer.WriteString(styledSecondChar)
}
type rgbCacheKey struct {
*color.RGBStyle
str string
}
var rgbCache = make(map[rgbCacheKey]string)
var rgbCacheMutex sync.RWMutex
func cachedSprint(style style.TextStyle, str string) string {
switch v := style.Style.(type) {
case *color.RGBStyle:
rgbCacheMutex.RLock()
key := rgbCacheKey{v, str}
value, ok := rgbCache[key]
rgbCacheMutex.RUnlock()
if ok {
return value
}
value = style.Sprint(str)
rgbCacheMutex.Lock()
rgbCache[key] = value
rgbCacheMutex.Unlock()
return value
case color.Basic:
return style.Sprint(str)
case color.Style:
value := style.Sprint(str)
return value
}
return style.Sprint(str)
}
func (cell *Cell) reset() {
cell.up = false
cell.down = false
cell.left = false
cell.right = false
}
func (cell *Cell) setUp(style style.TextStyle) *Cell {
cell.up = true
cell.style = style
return cell
}
func (cell *Cell) setDown(style style.TextStyle) *Cell {
cell.down = true
cell.style = style
return cell
}
func (cell *Cell) setLeft(style style.TextStyle) *Cell {
cell.left = true
if !cell.up && !cell.down {
// vertical trumps left
cell.style = style
}
return cell
}
func (cell *Cell) setRight(style style.TextStyle, override bool) *Cell {
cell.right = true
if cell.rightStyle == nil || override {
cell.rightStyle = &style
}
return cell
}
func (cell *Cell) setStyle(style style.TextStyle) *Cell {
cell.style = style
return cell
}
func (cell *Cell) setType(cellType cellType) *Cell {
cell.cellType = cellType
return cell
}
func getBoxDrawingChars(up, down, left, right bool) (string, string) {
if up && down && left && right {
return "│", "─"
} else if up && down && left && !right {
return "│", " "
} else if up && down && !left && right {
return "│", "─"
} else if up && down && !left && !right {
return "│", " "
} else if up && !down && left && right {
return "┴", "─"
} else if up && !down && left && !right {
return "╯", " "
} else if up && !down && !left && right {
return "╰", "─"
} else if up && !down && !left && !right {
return "╵", " "
} else if !up && down && left && right {
return "┬", "─"
} else if !up && down && left && !right {
return "╮", " "
} else if !up && down && !left && right {
return "╭", "─"
} else if !up && down && !left && !right {
return "╷", " "
} else if !up && !down && left && right {
return "─", "─"
} else if !up && !down && left && !right {
return "─", " "
} else if !up && !down && !left && right {
return "╶", "─"
} else if !up && !down && !left && !right {
return " ", " "
} else {
panic("should not be possible")
}
}
+400
View File
@@ -0,0 +1,400 @@
package graph
import (
"runtime"
"sort"
"strings"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type PipeKind uint8
const (
TERMINATES PipeKind = iota
STARTS
CONTINUES
)
type Pipe struct {
fromPos int
toPos int
fromSha string
toSha string
kind PipeKind
style style.TextStyle
}
var highlightStyle = style.FgLightWhite.SetBold()
func ContainsCommitSha(pipes []*Pipe, sha string) bool {
for _, pipe := range pipes {
if equalHashes(pipe.fromSha, sha) {
return true
}
}
return false
}
func (self Pipe) left() int {
return utils.Min(self.fromPos, self.toPos)
}
func (self Pipe) right() int {
return utils.Max(self.fromPos, self.toPos)
}
func RenderCommitGraph(commits []*models.Commit, selectedCommitSha string, getStyle func(c *models.Commit) style.TextStyle) []string {
pipeSets := GetPipeSets(commits, getStyle)
if len(pipeSets) == 0 {
return nil
}
lines := RenderAux(pipeSets, commits, selectedCommitSha)
return lines
}
func GetPipeSets(commits []*models.Commit, getStyle func(c *models.Commit) style.TextStyle) [][]*Pipe {
if len(commits) == 0 {
return nil
}
pipes := []*Pipe{{fromPos: 0, toPos: 0, fromSha: "START", toSha: commits[0].Sha, kind: STARTS, style: style.FgDefault}}
pipeSets := [][]*Pipe{}
for _, commit := range commits {
pipes = getNextPipes(pipes, commit, getStyle)
pipeSets = append(pipeSets, pipes)
}
return pipeSets
}
func RenderAux(pipeSets [][]*Pipe, commits []*models.Commit, selectedCommitSha string) []string {
maxProcs := runtime.GOMAXPROCS(0)
lines := make([]string, 0, len(pipeSets))
// splitting up the rendering of the graph into multiple goroutines allows us to render the graph in parallel
chunks := make([][]string, maxProcs)
perProc := len(pipeSets) / maxProcs
wg := sync.WaitGroup{}
wg.Add(maxProcs)
for i := 0; i < maxProcs; i++ {
i := i
go func() {
from := i * perProc
to := (i + 1) * perProc
if i == maxProcs-1 {
to = len(pipeSets)
}
innerLines := make([]string, 0, to-from)
for j, pipeSet := range pipeSets[from:to] {
k := from + j
var prevCommit *models.Commit
if k > 0 {
prevCommit = commits[k-1]
}
line := renderPipeSet(pipeSet, selectedCommitSha, prevCommit)
innerLines = append(innerLines, line)
}
chunks[i] = innerLines
wg.Done()
}()
}
wg.Wait()
for _, chunk := range chunks {
lines = append(lines, chunk...)
}
return lines
}
func getNextPipes(prevPipes []*Pipe, commit *models.Commit, getStyle func(c *models.Commit) style.TextStyle) []*Pipe {
currentPipes := make([]*Pipe, 0, len(prevPipes))
maxPos := 0
for _, pipe := range prevPipes {
// a pipe that terminated in the previous line has no bearing on the current line
// so we'll filter those out
if pipe.kind != TERMINATES {
currentPipes = append(currentPipes, pipe)
}
maxPos = utils.Max(maxPos, pipe.toPos)
}
newPipes := make([]*Pipe, 0, len(currentPipes)+len(commit.Parents))
// start by assuming that we've got a brand new commit not related to any preceding commit.
// (this only happens when we're doing `git log --all`). These will be tacked onto the far end.
pos := maxPos + 1
for _, pipe := range currentPipes {
if equalHashes(pipe.toSha, commit.Sha) {
// turns out this commit does have a descendant so we'll place it right under the first instance
pos = pipe.toPos
break
}
}
// a taken spot is one where a current pipe is ending on
takenSpots := make(map[int]bool)
// a traversed spot is one where a current pipe is starting on, ending on, or passing through
traversedSpots := make(map[int]bool)
if len(commit.Parents) > 0 {
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: pos,
fromSha: commit.Sha,
toSha: commit.Parents[0],
kind: STARTS,
style: getStyle(commit),
})
}
traversedSpotsForContinuingPipes := make(map[int]bool)
for _, pipe := range currentPipes {
if !equalHashes(pipe.toSha, commit.Sha) {
traversedSpotsForContinuingPipes[pipe.toPos] = true
}
}
getNextAvailablePosForContinuingPipe := func() int {
i := 0
for {
if !traversedSpots[i] {
return i
}
i++
}
}
getNextAvailablePosForNewPipe := func() int {
i := 0
for {
// a newly created pipe is not allowed to end on a spot that's already taken,
// nor on a spot that's been traversed by a continuing pipe.
if !takenSpots[i] && !traversedSpotsForContinuingPipes[i] {
return i
}
i++
}
}
traverse := func(from, to int) {
left, right := from, to
if left > right {
left, right = right, left
}
for i := left; i <= right; i++ {
traversedSpots[i] = true
}
takenSpots[to] = true
}
for _, pipe := range currentPipes {
if equalHashes(pipe.toSha, commit.Sha) {
// terminating here
newPipes = append(newPipes, &Pipe{
fromPos: pipe.toPos,
toPos: pos,
fromSha: pipe.fromSha,
toSha: pipe.toSha,
kind: TERMINATES,
style: pipe.style,
})
traverse(pipe.toPos, pos)
} else if pipe.toPos < pos {
// continuing here
availablePos := getNextAvailablePosForContinuingPipe()
newPipes = append(newPipes, &Pipe{
fromPos: pipe.toPos,
toPos: availablePos,
fromSha: pipe.fromSha,
toSha: pipe.toSha,
kind: CONTINUES,
style: pipe.style,
})
traverse(pipe.toPos, availablePos)
}
}
if commit.IsMerge() {
for _, parent := range commit.Parents[1:] {
availablePos := getNextAvailablePosForNewPipe()
// need to act as if continuing pipes are going to continue on the same line.
newPipes = append(newPipes, &Pipe{
fromPos: pos,
toPos: availablePos,
fromSha: commit.Sha,
toSha: parent,
kind: STARTS,
style: getStyle(commit),
})
takenSpots[availablePos] = true
}
}
for _, pipe := range currentPipes {
if !equalHashes(pipe.toSha, commit.Sha) && pipe.toPos > pos {
// continuing on, potentially moving left to fill in a blank spot
last := pipe.toPos
for i := pipe.toPos; i > pos; i-- {
if takenSpots[i] || traversedSpots[i] {
break
} else {
last = i
}
}
newPipes = append(newPipes, &Pipe{
fromPos: pipe.toPos,
toPos: last,
fromSha: pipe.fromSha,
toSha: pipe.toSha,
kind: CONTINUES,
style: pipe.style,
})
traverse(pipe.toPos, last)
}
}
// not efficient but doing it for now: sorting my pipes by toPos, then by kind
sort.Slice(newPipes, func(i, j int) bool {
if newPipes[i].toPos == newPipes[j].toPos {
return newPipes[i].kind < newPipes[j].kind
}
return newPipes[i].toPos < newPipes[j].toPos
})
return newPipes
}
func renderPipeSet(
pipes []*Pipe,
selectedCommitSha string,
prevCommit *models.Commit,
) string {
maxPos := 0
commitPos := 0
startCount := 0
for _, pipe := range pipes {
if pipe.kind == STARTS {
startCount++
commitPos = pipe.fromPos
} else if pipe.kind == TERMINATES {
commitPos = pipe.toPos
}
if pipe.right() > maxPos {
maxPos = pipe.right()
}
}
isMerge := startCount > 1
cells := make([]*Cell, maxPos+1)
for i := range cells {
cells[i] = &Cell{cellType: CONNECTION, style: style.FgDefault}
}
renderPipe := func(pipe *Pipe, style style.TextStyle, overrideRightStyle bool) {
left := pipe.left()
right := pipe.right()
if left != right {
for i := left + 1; i < right; i++ {
cells[i].setLeft(style).setRight(style, overrideRightStyle)
}
cells[left].setRight(style, overrideRightStyle)
cells[right].setLeft(style)
}
if pipe.kind == STARTS || pipe.kind == CONTINUES {
cells[pipe.toPos].setDown(style)
}
if pipe.kind == TERMINATES || pipe.kind == CONTINUES {
cells[pipe.fromPos].setUp(style)
}
}
// we don't want to highlight two commits if they're contiguous. We only want
// to highlight multiple things if there's an actual visible pipe involved.
highlight := true
if prevCommit != nil && equalHashes(prevCommit.Sha, selectedCommitSha) {
highlight = false
for _, pipe := range pipes {
if equalHashes(pipe.fromSha, selectedCommitSha) && (pipe.kind != TERMINATES || pipe.fromPos != pipe.toPos) {
highlight = true
}
}
}
// so we have our commit pos again, now it's time to build the cells.
// we'll handle the one that's sourced from our selected commit last so that it can override the other cells.
selectedPipes := []*Pipe{}
// pre-allocating this one because most of the time we'll only have non-selected pipes
nonSelectedPipes := make([]*Pipe, 0, len(pipes))
for _, pipe := range pipes {
if highlight && equalHashes(pipe.fromSha, selectedCommitSha) {
selectedPipes = append(selectedPipes, pipe)
} else {
nonSelectedPipes = append(nonSelectedPipes, pipe)
}
}
for _, pipe := range nonSelectedPipes {
if pipe.kind == STARTS {
renderPipe(pipe, pipe.style, true)
}
}
for _, pipe := range nonSelectedPipes {
if pipe.kind != STARTS && !(pipe.kind == TERMINATES && pipe.fromPos == commitPos && pipe.toPos == commitPos) {
renderPipe(pipe, pipe.style, false)
}
}
for _, pipe := range selectedPipes {
for i := pipe.left(); i <= pipe.right(); i++ {
cells[i].reset()
}
}
for _, pipe := range selectedPipes {
renderPipe(pipe, highlightStyle, true)
if pipe.toPos == commitPos {
cells[pipe.toPos].setStyle(highlightStyle)
}
}
cType := COMMIT
if isMerge {
cType = MERGE
}
cells[commitPos].setType(cType)
// using a string builder here for the sake of performance
writer := &strings.Builder{}
writer.Grow(len(cells) * 2)
for _, cell := range cells {
cell.render(writer)
}
return writer.String()
}
func equalHashes(a, b string) bool {
// if our selectedCommitSha is an empty string we treat that as meaning there is no selected commit sha
if a == "" || b == "" {
return false
}
length := utils.Min(len(a), len(b))
// parent hashes are only stored up to 20 characters for some reason so we'll truncate to that for comparison
return a[:length] == b[:length]
}
+570
View File
@@ -0,0 +1,570 @@
package graph
import (
"fmt"
"math/rand"
"strings"
"testing"
"github.com/gookit/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
"github.com/xo/terminfo"
)
func init() {
// on CI we've got no color capability so we're forcing it here
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
}
func TestRenderCommitGraph(t *testing.T) {
tests := []struct {
name string
commits []*models.Commit
expectedOutput string
}{
{
name: "with some merges",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"3"}},
{Sha: "3", Parents: []string{"4"}},
{Sha: "4", Parents: []string{"5", "7"}},
{Sha: "7", Parents: []string{"5"}},
{Sha: "5", Parents: []string{"8"}},
{Sha: "8", Parents: []string{"9"}},
{Sha: "9", Parents: []string{"A", "B"}},
{Sha: "B", Parents: []string{"D"}},
{Sha: "D", Parents: []string{"D"}},
{Sha: "A", Parents: []string{"E"}},
{Sha: "E", Parents: []string{"F"}},
{Sha: "F", Parents: []string{"D"}},
{Sha: "D", Parents: []string{"G"}},
},
expectedOutput: `
1 ◯
2 ◯
3 ◯
4 ⏣─╮
7 │ ◯
5 ◯─╯
8 ◯
9 ⏣─╮
B │ ◯
D │ ◯
A ◯ │
E ◯ │
F ◯ │
D ◯─╯`,
},
{
name: "with a path that has room to move to the left",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"3", "4"}},
{Sha: "4", Parents: []string{"3", "5"}},
{Sha: "3", Parents: []string{"5"}},
{Sha: "5", Parents: []string{"6"}},
{Sha: "6", Parents: []string{"7"}},
},
expectedOutput: `
1 ◯
2 ⏣─╮
4 │ ⏣─╮
3 ◯─╯ │
5 ◯───╯
6 ◯`,
},
{
name: "with a new commit",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"3", "4"}},
{Sha: "4", Parents: []string{"3", "5"}},
{Sha: "Z", Parents: []string{"Z"}},
{Sha: "3", Parents: []string{"5"}},
{Sha: "5", Parents: []string{"6"}},
{Sha: "6", Parents: []string{"7"}},
},
expectedOutput: `
1 ◯
2 ⏣─╮
4 │ ⏣─╮
Z │ │ │ ◯
3 ◯─╯ │ │
5 ◯───╯ │
6 ◯ ╭───╯`,
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"3", "4"}},
{Sha: "3", Parents: []string{"5", "4"}},
{Sha: "5", Parents: []string{"7", "8"}},
{Sha: "4", Parents: []string{"7"}},
{Sha: "7", Parents: []string{"11"}},
},
expectedOutput: `
1 ◯
2 ⏣─╮
3 ⏣─│─╮
5 ⏣─│─│─╮
4 │ ◯─╯ │
7 ◯─╯ ╭─╯`,
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"3", "4"}},
{Sha: "3", Parents: []string{"5", "4"}},
{Sha: "5", Parents: []string{"7", "8"}},
{Sha: "7", Parents: []string{"4", "A"}},
{Sha: "4", Parents: []string{"B"}},
{Sha: "B", Parents: []string{"C"}},
},
expectedOutput: `
1 ◯
2 ⏣─╮
3 ⏣─│─╮
5 ⏣─│─│─╮
7 ⏣─│─│─│─╮
4 ◯─┴─╯ │ │
B ◯ ╭───╯ │`,
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2", "3"}},
{Sha: "3", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"4", "5"}},
{Sha: "4", Parents: []string{"6", "7"}},
{Sha: "6", Parents: []string{"8"}},
},
expectedOutput: `
1 ⏣─╮
3 │ ◯
2 ⏣─│
4 ⏣─│─╮
6 ◯ │ │`,
},
{
name: "new merge path fills gap before continuing path on right",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2", "3", "4", "5"}},
{Sha: "4", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"A"}},
{Sha: "A", Parents: []string{"6", "B"}},
{Sha: "B", Parents: []string{"C"}},
},
expectedOutput: `
1 ⏣─┬─┬─╮
4 │ │ ◯ │
2 ◯─│─╯ │
A ⏣─│─╮ │
B │ │ ◯ │`,
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"3", "4"}},
{Sha: "3", Parents: []string{"5", "4"}},
{Sha: "5", Parents: []string{"7", "8"}},
{Sha: "7", Parents: []string{"4", "A"}},
{Sha: "4", Parents: []string{"B"}},
{Sha: "B", Parents: []string{"C"}},
{Sha: "C", Parents: []string{"D"}},
},
expectedOutput: `
1 ◯
2 ⏣─╮
3 ⏣─│─╮
5 ⏣─│─│─╮
7 ⏣─│─│─│─╮
4 ◯─┴─╯ │ │
B ◯ ╭───╯ │
C ◯ │ ╭───╯`,
},
{
name: "with a path that has room to move to the left and continues",
commits: []*models.Commit{
{Sha: "1", Parents: []string{"2"}},
{Sha: "2", Parents: []string{"3", "4"}},
{Sha: "3", Parents: []string{"5", "4"}},
{Sha: "5", Parents: []string{"7", "G"}},
{Sha: "7", Parents: []string{"8", "A"}},
{Sha: "8", Parents: []string{"4", "E"}},
{Sha: "4", Parents: []string{"B"}},
{Sha: "B", Parents: []string{"C"}},
{Sha: "C", Parents: []string{"D"}},
{Sha: "D", Parents: []string{"F"}},
},
expectedOutput: `
1 ◯
2 ⏣─╮
3 ⏣─│─╮
5 ⏣─│─│─╮
7 ⏣─│─│─│─╮
8 ⏣─│─│─│─│─╮
4 ◯─┴─╯ │ │ │
B ◯ ╭───╯ │ │
C ◯ │ ╭───╯ │
D ◯ │ │ ╭───╯`,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
lines := RenderCommitGraph(test.commits, "blah", getStyle)
trimmedExpectedOutput := ""
for _, line := range strings.Split(strings.TrimPrefix(test.expectedOutput, "\n"), "\n") {
trimmedExpectedOutput += strings.TrimSpace(line) + "\n"
}
t.Log("\nexpected: \n" + trimmedExpectedOutput)
output := ""
for i, line := range lines {
description := test.commits[i].Sha
output += strings.TrimSpace(description+" "+utils.Decolorise(line)) + "\n"
}
t.Log("\nactual: \n" + output)
assert.Equal(t,
trimmedExpectedOutput,
output)
})
}
}
func TestRenderPipeSet(t *testing.T) {
cyan := style.FgCyan
red := style.FgRed
green := style.FgGreen
// blue := style.FgBlue
yellow := style.FgYellow
magenta := style.FgMagenta
nothing := style.Nothing
tests := []struct {
name string
pipes []*Pipe
commit *models.Commit
prevCommit *models.Commit
expectedStr string
expectedStyles []style.TextStyle
}{
{
name: "single cell",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: green},
},
prevCommit: &models.Commit{Sha: "a"},
expectedStr: "◯",
expectedStyles: []style.TextStyle{green},
},
{
name: "single cell, selected",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "selected", kind: TERMINATES, style: cyan},
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "c", kind: STARTS, style: green},
},
prevCommit: &models.Commit{Sha: "a"},
expectedStr: "◯",
expectedStyles: []style.TextStyle{highlightStyle},
},
{
name: "terminating hook and starting hook, selected",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "selected", kind: TERMINATES, style: cyan},
{fromPos: 1, toPos: 0, fromSha: "c", toSha: "selected", kind: TERMINATES, style: yellow},
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "d", kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromSha: "selected", toSha: "e", kind: STARTS, style: green},
},
prevCommit: &models.Commit{Sha: "a"},
expectedStr: "⏣─╮",
expectedStyles: []style.TextStyle{
highlightStyle, highlightStyle, highlightStyle,
},
},
{
name: "terminating hook and starting hook, prioritise the terminating one",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 0, fromSha: "c", toSha: "b", kind: TERMINATES, style: magenta},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "d", kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromSha: "b", toSha: "e", kind: STARTS, style: green},
},
prevCommit: &models.Commit{Sha: "a"},
expectedStr: "⏣─│",
expectedStyles: []style.TextStyle{
green, green, magenta,
},
},
{
name: "starting and terminating pipe sharing some space",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b2", kind: CONTINUES, style: magenta},
{fromPos: 3, toPos: 0, fromSha: "e1", toSha: "a2", kind: TERMINATES, style: green},
{fromPos: 0, toPos: 2, fromSha: "a2", toSha: "c3", kind: STARTS, style: yellow},
},
prevCommit: &models.Commit{Sha: "a1"},
expectedStr: "⏣─│─┬─╯",
expectedStyles: []style.TextStyle{
yellow, yellow, magenta, yellow, yellow, green, green,
},
},
{
name: "starting and terminating pipe sharing some space, with selection",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "selected", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b2", kind: CONTINUES, style: magenta},
{fromPos: 3, toPos: 0, fromSha: "e1", toSha: "selected", kind: TERMINATES, style: green},
{fromPos: 0, toPos: 2, fromSha: "selected", toSha: "c3", kind: STARTS, style: yellow},
},
prevCommit: &models.Commit{Sha: "a1"},
expectedStr: "⏣───╮ ╯",
expectedStyles: []style.TextStyle{
highlightStyle, highlightStyle, highlightStyle, highlightStyle, highlightStyle, nothing, green,
},
},
{
name: "many terminating pipes",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 0, fromSha: "b1", toSha: "a2", kind: TERMINATES, style: magenta},
{fromPos: 2, toPos: 0, fromSha: "c1", toSha: "a2", kind: TERMINATES, style: green},
},
prevCommit: &models.Commit{Sha: "a1"},
expectedStr: "◯─┴─╯",
expectedStyles: []style.TextStyle{
yellow, magenta, magenta, green, green,
},
},
{
name: "starting pipe passing through",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 3, fromSha: "a2", toSha: "d3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "b3", kind: CONTINUES, style: magenta},
{fromPos: 2, toPos: 2, fromSha: "c1", toSha: "c3", kind: CONTINUES, style: green},
},
prevCommit: &models.Commit{Sha: "a1"},
expectedStr: "⏣─│─│─╮",
expectedStyles: []style.TextStyle{
yellow, yellow, magenta, yellow, green, yellow, yellow,
},
},
{
name: "starting and terminating path crossing continuing path",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: yellow},
{fromPos: 1, toPos: 1, fromSha: "b1", toSha: "a2", kind: CONTINUES, style: green},
{fromPos: 2, toPos: 0, fromSha: "c1", toSha: "a2", kind: TERMINATES, style: magenta},
},
prevCommit: &models.Commit{Sha: "a1"},
expectedStr: "⏣─│─╯",
expectedStyles: []style.TextStyle{
yellow, yellow, green, magenta, magenta,
},
},
{
name: "another clash of starting and terminating paths",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: yellow},
{fromPos: 2, toPos: 2, fromSha: "c1", toSha: "c3", kind: CONTINUES, style: green},
{fromPos: 3, toPos: 0, fromSha: "d1", toSha: "a2", kind: TERMINATES, style: magenta},
},
prevCommit: &models.Commit{Sha: "a1"},
expectedStr: "⏣─┬─│─╯",
expectedStyles: []style.TextStyle{
yellow, yellow, yellow, magenta, green, magenta, magenta,
},
},
{
name: "commit whose previous commit is selected",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: yellow},
},
prevCommit: &models.Commit{Sha: "selected"},
expectedStr: "◯",
expectedStyles: []style.TextStyle{
yellow,
},
},
{
name: "commit whose previous commit is selected and is a merge commit",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromSha: "selected", toSha: "b3", kind: CONTINUES, style: red},
},
prevCommit: &models.Commit{Sha: "selected"},
expectedStr: "◯ │",
expectedStyles: []style.TextStyle{
highlightStyle, nothing, highlightStyle,
},
},
{
name: "commit whose previous commit is selected and is a merge commit, with continuing pipe inbetween",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 1, toPos: 1, fromSha: "z1", toSha: "z3", kind: CONTINUES, style: green},
{fromPos: 2, toPos: 2, fromSha: "selected", toSha: "b3", kind: CONTINUES, style: red},
},
prevCommit: &models.Commit{Sha: "selected"},
expectedStr: "◯ │ │",
expectedStyles: []style.TextStyle{
highlightStyle, nothing, green, nothing, highlightStyle,
},
},
{
name: "when previous commit is selected, not a merge commit, and spawns a continuing pipe",
pipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a1", toSha: "a2", kind: TERMINATES, style: red},
{fromPos: 0, toPos: 0, fromSha: "a2", toSha: "a3", kind: STARTS, style: green},
{fromPos: 0, toPos: 1, fromSha: "a2", toSha: "b3", kind: STARTS, style: green},
{fromPos: 1, toPos: 0, fromSha: "selected", toSha: "a2", kind: TERMINATES, style: yellow},
},
prevCommit: &models.Commit{Sha: "selected"},
expectedStr: "⏣─╯",
expectedStyles: []style.TextStyle{
highlightStyle, highlightStyle, highlightStyle,
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
actualStr := renderPipeSet(test.pipes, "selected", test.prevCommit)
t.Log("actual cells:")
t.Log(actualStr)
expectedStr := ""
if len([]rune(test.expectedStr)) != len(test.expectedStyles) {
t.Fatalf("Error in test setup: you have %d characters in the expected output (%s) but have specified %d styles", len([]rune(test.expectedStr)), test.expectedStr, len(test.expectedStyles))
}
for i, char := range []rune(test.expectedStr) {
expectedStr += test.expectedStyles[i].Sprint(string(char))
}
expectedStr += " "
t.Log("expected cells:")
t.Log(expectedStr)
assert.Equal(t, expectedStr, actualStr)
})
}
}
func TestGetNextPipes(t *testing.T) {
tests := []struct {
prevPipes []*Pipe
commit *models.Commit
expected []*Pipe
}{
{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: STARTS, style: style.FgDefault},
},
commit: &models.Commit{
Sha: "b",
Parents: []string{"c"},
},
expected: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: style.FgDefault},
},
},
{
prevPipes: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "a", toSha: "b", kind: TERMINATES, style: style.FgDefault},
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: STARTS, style: style.FgDefault},
{fromPos: 0, toPos: 1, fromSha: "b", toSha: "d", kind: STARTS, style: style.FgDefault},
},
commit: &models.Commit{
Sha: "d",
Parents: []string{"e"},
},
expected: []*Pipe{
{fromPos: 0, toPos: 0, fromSha: "b", toSha: "c", kind: CONTINUES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromSha: "b", toSha: "d", kind: TERMINATES, style: style.FgDefault},
{fromPos: 1, toPos: 1, fromSha: "d", toSha: "e", kind: STARTS, style: style.FgDefault},
},
},
}
for _, test := range tests {
getStyle := func(c *models.Commit) style.TextStyle { return style.FgDefault }
pipes := getNextPipes(test.prevPipes, test.commit, getStyle)
// rendering cells so that it's easier to see what went wrong
actualStr := renderPipeSet(pipes, "selected", nil)
expectedStr := renderPipeSet(test.expected, "selected", nil)
t.Log("expected cells:")
t.Log(expectedStr)
t.Log("actual cells:")
t.Log(actualStr)
assert.EqualValues(t, test.expected, pipes)
}
}
func BenchmarkRenderCommitGraph(b *testing.B) {
commits := generateCommits(50)
getStyle := func(commit *models.Commit) style.TextStyle {
return authors.AuthorStyle(commit.Author)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
RenderCommitGraph(commits, "selected", getStyle)
}
}
func generateCommits(count int) []*models.Commit {
rand.Seed(1234)
pool := []*models.Commit{{Sha: "a", Author: "A"}}
commits := make([]*models.Commit, 0, count)
authorPool := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
for len(commits) < count {
currentCommitIdx := rand.Intn(len(pool))
currentCommit := pool[currentCommitIdx]
pool = append(pool[0:currentCommitIdx], pool[currentCommitIdx+1:]...)
// I need to pick a random number of parents to add
parentCount := rand.Intn(2) + 1
for j := 0; j < parentCount; j++ {
reuseParent := rand.Intn(6) != 1 && j <= len(pool)-1 && j != 0
var newParent *models.Commit
if reuseParent {
newParent = pool[j]
} else {
newParent = &models.Commit{
Sha: fmt.Sprintf("%s%d", currentCommit.Sha, j),
Author: authorPool[rand.Intn(len(authorPool))],
}
pool = append(pool, newParent)
}
currentCommit.Parents = append(currentCommit.Parents, newParent.Sha)
}
commits = append(commits, currentCommit)
}
return commits
}
+12 -6
View File
@@ -4,6 +4,7 @@
package gui
import (
"io"
"os/exec"
"strings"
@@ -50,14 +51,19 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
manager := gui.getManager(view)
ptmx, err := pty.Start(cmd)
if err != nil {
return err
start := func() (*exec.Cmd, io.Reader) {
ptmx, err := pty.Start(cmd)
if err != nil {
gui.Log.Error(err)
}
gui.State.Ptmx = ptmx
return cmd, ptmx
}
gui.State.Ptmx = ptmx
onClose := func() {
ptmx.Close()
gui.State.Ptmx.Close()
gui.State.Ptmx = nil
}
@@ -65,7 +71,7 @@ func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
return err
}
if err := manager.NewTask(manager.NewCmdTask(ptmx, cmd, prefix, height+oy+10, onClose), cmdStr); err != nil {
if err := manager.NewTask(manager.NewCmdTask(start, prefix, height+oy+10, onClose), cmdStr); err != nil {
return err
}
+38 -6
View File
@@ -7,11 +7,19 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
)
type RebaseOption string
const (
REBASE_OPTION_CONTINUE = "continue"
REBASE_OPTION_ABORT = "abort"
REBASE_OPTION_SKIP = "skip"
)
func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{"continue", "abort"}
options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT}
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
options = append(options, "skip")
options = append(options, REBASE_OPTION_SKIP)
}
menuItems := make([]*menuItem, len(options))
@@ -49,7 +57,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
if status == commands.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
sub := gitCommand.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if sub != nil {
return gui.runSubprocessWithSuspenseAndRefresh(sub)
@@ -87,9 +95,9 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
if result == nil {
return nil
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return gui.genericMergeCommand("skip")
return gui.genericMergeCommand(REBASE_OPTION_SKIP)
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
return gui.genericMergeCommand("continue")
return gui.genericMergeCommand(REBASE_OPTION_CONTINUE)
} else if strings.Contains(result.Error(), "No rebase in progress?") {
// assume in this case that we're already done
return nil
@@ -106,10 +114,34 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
return err
}
return gui.genericMergeCommand("abort")
return gui.genericMergeCommand(REBASE_OPTION_ABORT)
},
})
} else {
return gui.createErrorPanel(result.Error())
}
}
func (gui *Gui) abortMergeOrRebaseWithConfirm() error {
// prompt user to confirm that they want to abort, then do it
mode := gui.workingTreeStateNoun()
return gui.ask(askOpts{
title: fmt.Sprintf(gui.Tr.AbortTitle, mode),
prompt: fmt.Sprintf(gui.Tr.AbortPrompt, mode),
handleConfirm: func() error {
return gui.genericMergeCommand(REBASE_OPTION_ABORT)
},
})
}
func (gui *Gui) workingTreeStateNoun() string {
workingTreeState := gui.GitCommand.WorkingTreeState()
switch workingTreeState {
case commands.REBASE_MODE_NORMAL:
return ""
case commands.REBASE_MODE_MERGING:
return "merge"
default:
return "rebase"
}
}
+6 -1
View File
@@ -1,8 +1,9 @@
package style
import (
"github.com/gookit/color"
"text/template"
"github.com/gookit/color"
)
var (
@@ -16,6 +17,7 @@ var (
FgBlue = FromBasicFg(color.FgBlue)
FgYellow = FromBasicFg(color.FgYellow)
FgMagenta = FromBasicFg(color.FgMagenta)
FgDefault = FromBasicFg(color.FgDefault)
BgWhite = FromBasicBg(color.BgWhite)
BgBlack = FromBasicBg(color.BgBlack)
@@ -26,6 +28,9 @@ var (
BgMagenta = FromBasicBg(color.BgMagenta)
BgCyan = FromBasicBg(color.BgCyan)
// will not print any colour escape codes, including the reset escape code
Nothing = New()
AttrUnderline = New().SetUnderline()
AttrBold = New().SetBold()
+16 -14
View File
@@ -10,6 +10,11 @@ import (
"github.com/xo/terminfo"
)
func init() {
// on CI we've got no color capability so we're forcing it here
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
}
func TestMerge(t *testing.T) {
type scenario struct {
name string
@@ -18,9 +23,6 @@ func TestMerge(t *testing.T) {
expectedStr string
}
// on CI we've got no color capability so we're forcing it here
color.ForceSetColorLevel(terminfo.ColorLevelMillions)
fgRed := color.FgRed
bgRed := color.BgRed
fgBlue := color.FgBlue
@@ -37,19 +39,19 @@ func TestMerge(t *testing.T) {
{
"no color",
nil,
TextStyle{style: color.Style{}},
TextStyle{Style: color.Style{}},
"foo",
},
{
"only fg color",
[]TextStyle{FgRed},
TextStyle{fg: &Color{basic: &fgRed}, style: color.Style{fgRed}},
TextStyle{fg: &Color{basic: &fgRed}, Style: color.Style{fgRed}},
"\x1b[31mfoo\x1b[0m",
},
{
"only bg color",
[]TextStyle{BgRed},
TextStyle{bg: &Color{basic: &bgRed}, style: color.Style{bgRed}},
TextStyle{bg: &Color{basic: &bgRed}, Style: color.Style{bgRed}},
"\x1b[41mfoo\x1b[0m",
},
{
@@ -58,7 +60,7 @@ func TestMerge(t *testing.T) {
TextStyle{
fg: &Color{basic: &fgBlue},
bg: &Color{basic: &bgRed},
style: color.Style{fgBlue, bgRed},
Style: color.Style{fgBlue, bgRed},
},
"\x1b[34;41mfoo\x1b[0m",
},
@@ -67,7 +69,7 @@ func TestMerge(t *testing.T) {
[]TextStyle{AttrBold},
TextStyle{
decoration: Decoration{bold: true},
style: color.Style{color.OpBold},
Style: color.Style{color.OpBold},
},
"\x1b[1mfoo\x1b[0m",
},
@@ -79,7 +81,7 @@ func TestMerge(t *testing.T) {
bold: true,
underline: true,
},
style: color.Style{color.OpBold, color.OpUnderscore},
Style: color.Style{color.OpBold, color.OpUnderscore},
},
"\x1b[1;4mfoo\x1b[0m",
},
@@ -93,7 +95,7 @@ func TestMerge(t *testing.T) {
bold: true,
underline: true,
},
style: color.Style{fgBlue, bgRed, color.OpBold, color.OpUnderscore},
Style: color.Style{fgBlue, bgRed, color.OpBold, color.OpUnderscore},
},
"\x1b[34;41;1;4mfoo\x1b[0m",
},
@@ -102,7 +104,7 @@ func TestMerge(t *testing.T) {
[]TextStyle{New().SetFg(rgbPink)},
TextStyle{
fg: &rgbPink,
style: color.NewRGBStyle(rgbPinkLib).SetOpts(color.Opts{}),
Style: color.NewRGBStyle(rgbPinkLib).SetOpts(color.Opts{}),
},
// '38;2' qualifies an RGB foreground color
"\x1b[38;2;255;0;255mfoo\x1b[0m",
@@ -113,7 +115,7 @@ func TestMerge(t *testing.T) {
TextStyle{
fg: &rgbPink,
bg: &rgbYellow,
style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{}),
Style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{}),
},
// '48;2' qualifies an RGB background color
"\x1b[38;2;255;0;255;48;2;255;255;0mfoo\x1b[0m",
@@ -128,7 +130,7 @@ func TestMerge(t *testing.T) {
bold: true,
underline: true,
},
style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{color.OpBold, color.OpUnderscore}),
Style: color.NewRGBStyle(rgbPinkLib, rgbYellowLib).SetOpts(color.Opts{color.OpBold, color.OpUnderscore}),
},
"\x1b[38;2;255;0;255;48;2;255;255;0;1;4mfoo\x1b[0m",
},
@@ -138,7 +140,7 @@ func TestMerge(t *testing.T) {
TextStyle{
fg: &rgbYellow,
bg: &Color{basic: &bgRed},
style: color.NewRGBStyle(
Style: color.NewRGBStyle(
rgbYellowLib,
fgRed.RGB(), // We need to use FG here, https://github.com/gookit/color/issues/39
).SetOpts(color.Opts{}),
+12 -10
View File
@@ -30,7 +30,9 @@ type TextStyle struct {
bg *Color
decoration Decoration
style Sprinter
// making this public so that we can use a type switch to get to the underlying
// value so we can cache styles. This is very much a hack.
Style Sprinter
}
type Sprinter interface {
@@ -40,16 +42,16 @@ type Sprinter interface {
func New() TextStyle {
s := TextStyle{}
s.style = s.deriveStyle()
s.Style = s.deriveStyle()
return s
}
func (b TextStyle) Sprint(a ...interface{}) string {
return b.style.Sprint(a...)
return b.Style.Sprint(a...)
}
func (b TextStyle) Sprintf(format string, a ...interface{}) string {
return b.style.Sprintf(format, a...)
return b.Style.Sprintf(format, a...)
}
// note that our receiver here is not a pointer which means we're receiving a
@@ -57,31 +59,31 @@ func (b TextStyle) Sprintf(format string, a ...interface{}) string {
// TextStyle receiver without actually modifying the original.
func (b TextStyle) SetBold() TextStyle {
b.decoration.SetBold()
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetUnderline() TextStyle {
b.decoration.SetUnderline()
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetReverse() TextStyle {
b.decoration.SetReverse()
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetBg(color Color) TextStyle {
b.bg = &color
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
func (b TextStyle) SetFg(color Color) TextStyle {
b.fg = &color
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
@@ -96,7 +98,7 @@ func (b TextStyle) MergeStyle(other TextStyle) TextStyle {
b.bg = other.bg
}
b.style = b.deriveStyle()
b.Style = b.deriveStyle()
return b
}
+15 -12
View File
@@ -1,6 +1,7 @@
package gui
import (
"io"
"os/exec"
"strings"
@@ -20,18 +21,22 @@ func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error
manager := gui.getManager(view)
r, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout
start := func() (*exec.Cmd, io.Reader) {
r, err := cmd.StdoutPipe()
if err != nil {
gui.Log.Warn(err)
}
cmd.Stderr = cmd.Stdout
if err := cmd.Start(); err != nil {
return err
if err := cmd.Start(); err != nil {
gui.Log.Warn(err)
}
return cmd, r
}
if err := manager.NewTask(manager.NewCmdTask(r, cmd, prefix, height+oy+10, nil), cmdStr); err != nil {
return err
if err := manager.NewTask(manager.NewCmdTask(start, prefix, height+oy+10, nil), cmdStr); err != nil {
gui.Log.Warn(err)
}
return nil
@@ -90,9 +95,7 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
view.Reset()
},
func() {
gui.g.Update(func(*gocui.Gui) error {
return nil
})
gui.render()
},
func() {
// Need to check if the content of the view is well past the origin.
+13 -8
View File
@@ -202,8 +202,7 @@ func (gui *Gui) cleanString(s string) string {
}
func (gui *Gui) setViewContentSync(v *gocui.View, s string) {
v.Clear()
fmt.Fprint(v, gui.cleanString(s))
v.SetContent(gui.cleanString(s))
}
func (gui *Gui) setViewContent(v *gocui.View, s string) {
@@ -305,12 +304,13 @@ func (gui *Gui) refreshSelectedLine(panelState IListPanelState, total int) {
}
func (gui *Gui) renderDisplayStrings(v *gocui.View, displayStrings [][]string) {
gui.g.Update(func(g *gocui.Gui) error {
list := utils.RenderDisplayStrings(displayStrings)
v.Clear()
fmt.Fprint(v, list)
return nil
})
list := utils.RenderDisplayStrings(displayStrings)
v.SetContent(list)
}
func (gui *Gui) renderDisplayStringsAtPos(v *gocui.View, y int, displayStrings [][]string) {
list := utils.RenderDisplayStrings(displayStrings)
v.OverwriteLines(y, list)
}
func (gui *Gui) globalOptionsMap() map[string]string {
@@ -323,6 +323,7 @@ func (gui *Gui) globalOptionsMap() map[string]string {
gui.getKeyDisplay(keybindingConfig.Universal.Quit): gui.Tr.LcQuit,
gui.getKeyDisplay(keybindingConfig.Universal.OptionMenu): gui.Tr.LcMenu,
fmt.Sprintf("%s-%s", gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[0]), gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): gui.Tr.LcJump,
fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollLeft), gui.getKeyDisplay(keybindingConfig.Universal.ScrollRight)): gui.Tr.LcScrollLeftRight,
}
}
@@ -388,3 +389,7 @@ func getTabbedView(gui *Gui) *gocui.View {
view, _ := gui.g.View(context.GetViewName())
return view
}
func (gui *Gui) render() {
gui.g.Update(func(g *gocui.Gui) error { return nil })
}
+26
View File
@@ -271,6 +271,9 @@ type TranslationSet struct {
LcStashOptions string
NotARepository string
LcJump string
LcScrollLeftRight string
LcScrollLeft string
LcScrollRight string
DiscardPatch string
DiscardPatchConfirm string
CantPatchWhileRebasingError string
@@ -433,7 +436,17 @@ type TranslationSet struct {
SelectConfigFile string
NoConfigFileFoundErr string
LcLoadingFileSuggestions string
LcLoadingCommits string
MustSpecifyOriginError string
GitOutput string
GitCommandFailed string
AbortTitle string
AbortPrompt string
LcOpenLogMenu string
LogMenuTitle string
ToggleShowGitGraphAll string
ShowGitGraph string
SortCommits string
Spans Spans
}
@@ -798,6 +811,9 @@ func englishTranslationSet() TranslationSet {
LcStashOptions: "Stash options",
NotARepository: "Error: must be run inside a git repository",
LcJump: "jump to panel",
LcScrollLeftRight: "scroll left/right",
LcScrollLeft: "scroll left",
LcScrollRight: "scroll right",
DiscardPatch: "Discard Patch",
DiscardPatchConfirm: "You can only build a patch from one commit/stash-entry at a time. Discard current patch?",
CantPatchWhileRebasingError: "You cannot build a patch or run patch commands while in a merging or rebasing state",
@@ -960,7 +976,17 @@ func englishTranslationSet() TranslationSet {
SelectConfigFile: "Select config file",
NoConfigFileFoundErr: "No config file found",
LcLoadingFileSuggestions: "loading file suggestions",
LcLoadingCommits: "loading commits",
MustSpecifyOriginError: "Must specify a remote if specifying a branch",
GitOutput: "Git output:",
GitCommandFailed: "Git command failed. Check command log for details (open with %s)",
AbortTitle: "Abort %s",
AbortPrompt: "Are you sure you want to abort the current %s?",
LcOpenLogMenu: "open log menu",
LogMenuTitle: "Commit Log Options",
ToggleShowGitGraphAll: "toggle show whole git graph (pass the `--all` flag to `git log`)",
ShowGitGraph: "show git graph",
SortCommits: "commit sort order",
Spans: Spans{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",
+93 -68
View File
@@ -14,18 +14,19 @@ import (
"github.com/sirupsen/logrus"
)
type Task struct {
stop chan struct{}
stopped bool
stopMutex sync.Mutex
notifyStopped chan struct{}
Log *logrus.Entry
f func(chan struct{}) error
}
const THROTTLE_TIME = time.Millisecond * 30
// we use this to check if the system is under stress right now. Hopefully this makes sense on other machines
const COMMAND_START_THRESHOLD = time.Millisecond * 10
type ViewBufferManager struct {
writer io.Writer
currentTask *Task
// this blocks until the task has been properly stopped
stopCurrentTask func()
// this is what we write the output of the task to. It's typically a view
writer io.Writer
// this is for when we wait to get
waitingMutex sync.Mutex
taskIDMutex sync.Mutex
Log *logrus.Entry
@@ -38,6 +39,12 @@ type ViewBufferManager struct {
beforeStart func()
refreshView func()
onEndOfInput func()
// if the user flicks through a heap of items, with each one
// spawning a process to render something to the main view,
// it can slow things down quite a bit. In these situations we
// want to throttle the spawning of processes.
throttle bool
}
func (m *ViewBufferManager) GetTaskKey() string {
@@ -69,17 +76,46 @@ func (m *ViewBufferManager) ReadLines(n int) {
})
}
func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string, linesToRead int, onDone func()) func(chan struct{}) error {
// note: onDone may be called twice
func (m *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), prefix string, linesToRead int, onDone func()) func(chan struct{}) error {
return func(stop chan struct{}) error {
var once sync.Once
var onDoneWrapper func()
if onDone != nil {
onDoneWrapper = func() { once.Do(onDone) }
}
if m.throttle {
m.Log.Info("throttling task")
time.Sleep(THROTTLE_TIME)
}
select {
case <-stop:
return nil
default:
}
startTime := time.Now()
cmd, r := start()
timeToStart := time.Since(startTime)
go utils.Safe(func() {
<-stop
// we use the time it took to start the program as a way of checking if things
// are running slow at the moment. This is admittedly a crude estimate, but
// the point is that we only want to throttle when things are running slow
// and the user is flicking through a bunch of items.
m.throttle = time.Since(startTime) < THROTTLE_TIME && timeToStart > COMMAND_START_THRESHOLD
if err := oscommands.Kill(cmd); err != nil {
if !strings.Contains(err.Error(), "process already finished") {
m.Log.Errorf("error when running cmd task: %v", err)
}
}
if onDone != nil {
onDone()
// for pty's we need to call onDone here so that cmd.Wait() doesn't block forever
if onDoneWrapper != nil {
onDoneWrapper()
}
})
@@ -90,34 +126,42 @@ func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string
done := make(chan struct{})
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
loaded := false
go utils.Safe(func() {
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
loaded := false
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 200)
defer ticker.Stop()
select {
case <-ticker.C:
loadingMutex.Lock()
if !loaded {
m.beforeStart()
_, _ = m.writer.Write([]byte("loading..."))
m.refreshView()
}
loadingMutex.Unlock()
case <-stop:
return
ticker := time.NewTicker(time.Millisecond * 200)
defer ticker.Stop()
select {
case <-stop:
return
case <-ticker.C:
loadingMutex.Lock()
if !loaded {
m.beforeStart()
_, _ = m.writer.Write([]byte("loading..."))
m.refreshView()
}
})
loadingMutex.Unlock()
}
})
go utils.Safe(func() {
outer:
for {
select {
case <-stop:
break outer
case linesToRead := <-m.readLines:
for i := 0; i < linesToRead; i++ {
select {
case <-stop:
break outer
default:
}
ok := scanner.Scan()
loadingMutex.Lock()
if !loaded {
@@ -129,28 +173,20 @@ func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string
}
loadingMutex.Unlock()
select {
case <-stop:
m.refreshView()
break outer
default:
}
if !ok {
// if we're here then there's nothing left to scan from the source
// so we're at the EOF and can flush the stale content
m.onEndOfInput()
m.refreshView()
break outer
}
_, _ = m.writer.Write(append(scanner.Bytes(), '\n'))
}
m.refreshView()
case <-stop:
m.refreshView()
break outer
}
}
m.refreshView()
if err := cmd.Wait(); err != nil {
// it's fine if we've killed this program ourselves
if !strings.Contains(err.Error(), "signal: killed") {
@@ -158,10 +194,9 @@ func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string
}
}
m.refreshView()
if onDone != nil {
onDone()
// calling onDoneWrapper here again in case the program ended on its own accord
if onDoneWrapper != nil {
onDoneWrapper()
}
close(done)
@@ -177,14 +212,14 @@ func (m *ViewBufferManager) NewCmdTask(r io.Reader, cmd *exec.Cmd, prefix string
// Close closes the task manager, killing whatever task may currently be running
func (t *ViewBufferManager) Close() {
if t.currentTask == nil {
if t.stopCurrentTask == nil {
return
}
c := make(chan struct{})
go utils.Safe(func() {
t.currentTask.Stop()
t.stopCurrentTask()
c <- struct{}{}
})
@@ -220,19 +255,20 @@ func (m *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key string
return
}
if m.stopCurrentTask != nil {
m.stopCurrentTask()
}
stop := make(chan struct{})
notifyStopped := make(chan struct{})
if m.currentTask != nil {
m.currentTask.Stop()
var once sync.Once
onStop := func() {
close(stop)
<-notifyStopped
}
m.currentTask = &Task{
stop: stop,
notifyStopped: notifyStopped,
Log: m.Log,
f: f,
}
m.stopCurrentTask = func() { once.Do(onStop) }
go utils.Safe(func() {
if err := f(stop); err != nil {
@@ -245,14 +281,3 @@ func (m *ViewBufferManager) NewTask(f func(stop chan struct{}) error, key string
return nil
}
func (t *Task) Stop() {
t.stopMutex.Lock()
defer t.stopMutex.Unlock()
if t.stopped {
return
}
close(t.stop)
<-t.notifyStopped
t.stopped = true
}
+136
View File
@@ -0,0 +1,136 @@
package tasks
import (
"bytes"
"io"
"os/exec"
"sync"
"testing"
"time"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func getCounter() (func(), func() int) {
counter := 0
return func() { counter++ }, func() int { return counter }
}
func TestNewCmdTaskInstantStop(t *testing.T) {
writer := bytes.NewBuffer(nil)
beforeStart, getBeforeStartCallCount := getCounter()
refreshView, getRefreshViewCallCount := getCounter()
onEndOfInput, getOnEndOfInputCallCount := getCounter()
onNewKey, getOnNewKeyCallCount := getCounter()
onDone, getOnDoneCallCount := getCounter()
manager := NewViewBufferManager(
utils.NewDummyLog(),
writer,
beforeStart,
refreshView,
onEndOfInput,
onNewKey,
)
stop := make(chan struct{})
reader := bytes.NewBufferString("test")
start := func() (*exec.Cmd, io.Reader) {
// not actually starting this because it's not necessary
cmd := secureexec.Command("blah blah")
close(stop)
return cmd, reader
}
fn := manager.NewCmdTask(start, "prefix\n", 20, onDone)
_ = fn(stop)
callCountExpectations := []struct {
expected int
actual int
name string
}{
{0, getBeforeStartCallCount(), "beforeStart"},
{1, getRefreshViewCallCount(), "refreshView"},
{0, getOnEndOfInputCallCount(), "onEndOfInput"},
{0, getOnNewKeyCallCount(), "onNewKey"},
{1, getOnDoneCallCount(), "onDone"},
}
for _, expectation := range callCountExpectations {
if expectation.actual != expectation.expected {
t.Errorf("expected %s to be called %d times, got %d", expectation.name, expectation.expected, expectation.actual)
}
}
expectedContent := ""
actualContent := writer.String()
if actualContent != expectedContent {
t.Errorf("expected writer to receive the following content: \n%s\n. But instead it recevied: %s", expectedContent, actualContent)
}
}
func TestNewCmdTask(t *testing.T) {
writer := bytes.NewBuffer(nil)
beforeStart, getBeforeStartCallCount := getCounter()
refreshView, getRefreshViewCallCount := getCounter()
onEndOfInput, getOnEndOfInputCallCount := getCounter()
onNewKey, getOnNewKeyCallCount := getCounter()
onDone, getOnDoneCallCount := getCounter()
manager := NewViewBufferManager(
utils.NewDummyLog(),
writer,
beforeStart,
refreshView,
onEndOfInput,
onNewKey,
)
stop := make(chan struct{})
reader := bytes.NewBufferString("test")
start := func() (*exec.Cmd, io.Reader) {
// not actually starting this because it's not necessary
cmd := secureexec.Command("blah blah")
return cmd, reader
}
fn := manager.NewCmdTask(start, "prefix\n", 20, onDone)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
time.Sleep(100 * time.Millisecond)
close(stop)
wg.Done()
}()
_ = fn(stop)
wg.Wait()
callCountExpectations := []struct {
expected int
actual int
name string
}{
{1, getBeforeStartCallCount(), "beforeStart"},
{1, getRefreshViewCallCount(), "refreshView"},
{1, getOnEndOfInputCallCount(), "onEndOfInput"},
{0, getOnNewKeyCallCount(), "onNewKey"},
{1, getOnDoneCallCount(), "onDone"},
}
for _, expectation := range callCountExpectations {
if expectation.actual != expectation.expected {
t.Errorf("expected %s to be called %d times, got %d", expectation.name, expectation.expected, expectation.actual)
}
}
expectedContent := "prefix\ntest\n"
actualContent := writer.String()
if actualContent != expectedContent {
t.Errorf("expected writer to receive the following content: \n%s\n. But instead it recevied: %s", expectedContent, actualContent)
}
}
+18 -22
View File
@@ -2,34 +2,30 @@ package utils
import (
"regexp"
"sync"
)
var decoloriseCache = make(map[string]string)
var decoloriseMutex sync.RWMutex
// Decolorise strips a string of color
func Decolorise(str string) string {
re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`)
return re.ReplaceAllString(str, "")
}
decoloriseMutex.RLock()
val := decoloriseCache[str]
decoloriseMutex.RUnlock()
func getPadWidths(stringArrays [][]string) []int {
maxWidth := 0
for _, stringArray := range stringArrays {
if len(stringArray) > maxWidth {
maxWidth = len(stringArray)
}
if val != "" {
return val
}
if maxWidth-1 < 0 {
return []int{}
}
padWidths := make([]int, maxWidth-1)
for i := range padWidths {
for _, strings := range stringArrays {
uncoloredString := Decolorise(strings[i])
if len(uncoloredString) > padWidths[i] {
padWidths[i] = len(uncoloredString)
}
}
}
return padWidths
re := regexp.MustCompile(`\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[mGK]`)
ret := re.ReplaceAllString(str, "")
decoloriseMutex.Lock()
decoloriseCache[str] = ret
decoloriseMutex.Unlock()
return ret
}
func IsValidHexValue(v string) bool {
+200
View File
@@ -0,0 +1,200 @@
package utils
import (
"testing"
)
func TestDecolorise(t *testing.T) {
var tests = []struct {
input string
output string
}{
{
input: "",
output: "",
},
{
input: "hello",
output: "hello",
},
{
input: "hello\x1b[31m",
output: "hello",
},
{
input: "hello\x1b[31mworld",
output: "helloworld",
},
{
input: "hello\x1b[31m\x1b[32mworld",
output: "helloworld",
},
{
input: "hello\x1b[31m\x1b[32m\x1b[33mworld",
output: "helloworld",
},
{
input: "hello\x1b[31m\x1b[32m\x1b[33m\x1b[34mworld",
output: "helloworld",
},
{
input: "hello\x1b[31m\x1b[32m\x1b[33m\x1b[34m\x1b[35mworld",
output: "helloworld",
},
{
input: "hello\x1b[31m\x1b[32m\x1b[33m\x1b[34m\x1b[35m\x1b[36mworld",
output: "helloworld",
},
{
input: "hello\x1b[31m\x1b[32m\x1b[33m\x1b[34m\x1b[35m\x1b[36m\x1b[37mworld",
output: "helloworld",
},
{
input: "hello\x1b[31m\x1b[32m\x1b[33m\x1b[34m\x1b[35m\x1b[36m\x1b[37mworld",
output: "helloworld",
},
{
input: "\x1b[38;2;47;228;2mJD\x1b[0m",
output: "JD",
},
{
input: "\x1b[38;2;160;47;213mRy\x1b[0m",
output: "Ry",
},
{
input: "\x1b[38;2;179;217;72mSB\x1b[0m",
output: "SB",
},
{
input: "\x1b[38;2;48;34;214mMK\x1b[0m",
output: "MK",
},
{
input: "\x1b[38;2;28;152;222mAŁ\x1b[0m",
output: "AŁ",
},
{
input: "\x1b[38;2;237;230;56mHH\x1b[0m",
output: "HH",
},
{
input: "\x1b[38;2;63;232;69mmj\x1b[0m",
output: "mj",
},
{
input: "\x1b[38;2;111;207;16mbl\x1b[0m",
output: "bl",
},
{
input: "\x1b[38;2;250;31;163msa\x1b[0m",
output: "sa",
},
{
input: "\x1b[38;2;195;10;54mbt\x1b[0m",
output: "bt",
},
{
input: "\x1b[38;2;232;147;68mco\x1b[0m",
output: "co",
},
{
input: "\x1b[38;2;116;180;35mDY\x1b[0m",
output: "DY",
},
{
input: "\x1b[38;2;232;1;195mDB\x1b[0m",
output: "DB",
},
{
input: "\x1b[38;2;245;101;55mLi\x1b[0m",
output: "Li",
},
{
input: "\x1b[38;2;47;4;217mRy\x1b[0m",
output: "Ry",
},
{
input: "\x1b[38;2;252;197;1mEl\x1b[0m",
output: "El",
},
{
input: "\x1b[38;2;41;131;237mMG\x1b[0m",
output: "MG",
},
{
input: "\x1b[38;2;65;240;62mDP\x1b[0m",
output: "DP",
},
{
input: "\x1b[38;2;29;201;139mFM\x1b[0m",
output: "FM",
},
{
input: "\x1b[38;2;141;20;198mEB\x1b[0m",
output: "EB",
},
{
input: "\x1b[38;2;60;215;140mDM\x1b[0m",
output: "DM",
},
{
input: "\x1b[38;2;247;63;38mDE\x1b[0m",
output: "DE",
},
{
input: "\x1b[38;2;67;210;17mCB\x1b[0m",
output: "CB",
},
{
input: "\x1b[38;2;220;190;84mST\x1b[0m",
output: "ST",
},
{
input: "\x1b[38;2;137;239;6mER\x1b[0m",
output: "ER",
},
{
input: "\x1b[38;2;47;249;225mAY\x1b[0m",
output: "AY",
},
{
input: "\x1b[38;2;215;16;195mca\x1b[0m",
output: "ca",
},
{
input: "\x1b[38;2;73;215;122mRV\x1b[0m",
output: "RV",
},
{
input: "\x1b[38;2;118;15;221mJP\x1b[0m",
output: "JP",
},
{
input: "\x1b[38;2;186;163;39mHJ\x1b[0m",
output: "HJ",
},
{
input: "\x1b[38;2;54;222;111mDD\x1b[0m",
output: "DD",
},
{
input: "\x1b[38;2;56;209;108mPZ\x1b[0m",
output: "PZ",
},
{
input: "\x1b[38;2;9;179;216mPM\x1b[0m",
output: "PM",
},
{
input: "\x1b[38;2;157;205;18mta\x1b[0m",
output: "ta",
},
}
for _, test := range tests {
output := Decolorise(test.input)
if output != test.output {
t.Errorf("Decolorise(%s) = %s, want %s", test.input, output, test.output)
}
}
}
+94 -10
View File
@@ -1,25 +1,64 @@
package utils
import "strings"
import (
"strings"
"github.com/mattn/go-runewidth"
)
// WithPadding pads a string as much as you want
func WithPadding(str string, padding int) string {
uncoloredStr := Decolorise(str)
if padding < len(uncoloredStr) {
width := runewidth.StringWidth(uncoloredStr)
if padding < width {
return str
}
return str + strings.Repeat(" ", padding-len(uncoloredStr))
return str + strings.Repeat(" ", padding-width)
}
func RenderDisplayStrings(displayStringsArr [][]string) string {
displayStringsArr = excludeBlankColumns(displayStringsArr)
padWidths := getPadWidths(displayStringsArr)
paddedDisplayStrings := getPaddedDisplayStrings(displayStringsArr, padWidths)
output := getPaddedDisplayStrings(displayStringsArr, padWidths)
return strings.Join(paddedDisplayStrings, "\n")
return output
}
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
paddedDisplayStrings := make([]string, len(stringArrays))
// NOTE: this mutates the input slice for the sake of performance
func excludeBlankColumns(displayStringsArr [][]string) [][]string {
if len(displayStringsArr) == 0 {
return displayStringsArr
}
// if all rows share a blank column, we want to remove that column
toRemove := []int{}
outer:
for i := range displayStringsArr[0] {
for _, strings := range displayStringsArr {
if strings[i] != "" {
continue outer
}
}
toRemove = append(toRemove, i)
}
if len(toRemove) == 0 {
return displayStringsArr
}
// remove the columns
for i, strings := range displayStringsArr {
for j := len(toRemove) - 1; j >= 0; j-- {
strings = append(strings[:toRemove[j]], strings[toRemove[j]+1:]...)
}
displayStringsArr[i] = strings
}
return displayStringsArr
}
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) string {
builder := strings.Builder{}
for i, stringArray := range stringArrays {
if len(stringArray) == 0 {
continue
@@ -28,12 +67,57 @@ func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string
if len(stringArray)-1 < j {
continue
}
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
builder.WriteString(WithPadding(stringArray[j], padWidth))
builder.WriteString(" ")
}
if len(stringArray)-1 < len(padWidths) {
continue
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
builder.WriteString(stringArray[len(padWidths)])
if i < len(stringArrays)-1 {
builder.WriteString("\n")
}
}
return builder.String()
}
func getPadWidths(stringArrays [][]string) []int {
maxWidth := 0
for _, stringArray := range stringArrays {
if len(stringArray) > maxWidth {
maxWidth = len(stringArray)
}
}
if maxWidth-1 < 0 {
return []int{}
}
padWidths := make([]int, maxWidth-1)
for i := range padWidths {
for _, strings := range stringArrays {
uncoloredStr := Decolorise(strings[i])
width := runewidth.StringWidth(uncoloredStr)
if width > padWidths[i] {
padWidths[i] = width
}
}
}
return padWidths
}
// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
func TruncateWithEllipsis(str string, limit int) string {
if runewidth.StringWidth(str) > limit && limit <= 3 {
return strings.Repeat(".", limit)
}
return runewidth.Truncate(str, limit, "...")
}
func SafeTruncate(str string, limit int) string {
if len(str) > limit {
return str[0:limit]
} else {
return str
}
return paddedDisplayStrings
}
+129 -26
View File
@@ -25,6 +25,11 @@ func TestWithPadding(t *testing.T) {
14,
"hello world ! ",
},
{
"Güçlü",
7,
"Güçlü ",
},
}
for _, s := range scenarios {
@@ -32,35 +37,13 @@ func TestWithPadding(t *testing.T) {
}
}
// TestGetPaddedDisplayStrings is a function.
func TestGetPaddedDisplayStrings(t *testing.T) {
type scenario struct {
stringArrays [][]string
padWidths []int
expected []string
}
scenarios := []scenario{
{
[][]string{{"a", "b"}, {"c", "d"}},
[]int{1},
[]string{"a b", "c d"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
}
}
// TestGetPadWidths is a function.
func TestGetPadWidths(t *testing.T) {
type scenario struct {
stringArrays [][]string
expected []int
input [][]string
expected []int
}
scenarios := []scenario{
tests := []scenario{
{
[][]string{{""}, {""}},
[]int{},
@@ -73,9 +56,129 @@ func TestGetPadWidths(t *testing.T) {
[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
[]int{2, 1},
},
{
[][]string{{"AŁ", "b", "ccc"}, {"c", "d", "e"}},
[]int{2, 1},
},
}
for _, test := range tests {
output := getPadWidths(test.input)
if !assert.EqualValues(t, output, test.expected) {
t.Errorf("getPadWidths(%v) = %v, want %v", test.input, output, test.expected)
}
}
}
func TestTruncateWithEllipsis(t *testing.T) {
// will need to check chinese characters as well
// important that we have a three dot ellipsis within the limit
type scenario struct {
str string
limit int
expected string
}
scenarios := []scenario{
{
"hello world !",
1,
".",
},
{
"hello world !",
2,
"..",
},
{
"hello world !",
3,
"...",
},
{
"hello world !",
4,
"h...",
},
{
"hello world !",
5,
"he...",
},
{
"hello world !",
12,
"hello wor...",
},
{
"hello world !",
13,
"hello world !",
},
{
"hello world !",
14,
"hello world !",
},
{
"大大大大",
5,
"大...",
},
{
"大大大大",
2,
"..",
},
{
"大大大大",
0,
"",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
assert.EqualValues(t, s.expected, TruncateWithEllipsis(s.str, s.limit))
}
}
func TestRenderDisplayStrings(t *testing.T) {
type scenario struct {
input [][]string
expected string
}
tests := []scenario{
{
[][]string{{""}, {""}},
"",
},
{
[][]string{{"a"}, {""}},
"a\n",
},
{
[][]string{{"a"}, {"b"}},
"a\nb",
},
{
[][]string{{"a", "b"}, {"c", "d"}},
"a b\nc d",
},
{
[][]string{{"a", "", "c"}, {"d", "", "f"}},
"a c\nd f",
},
{
[][]string{{"a", "", "c", ""}, {"d", "", "f", ""}},
"a c\nd f",
},
}
for _, test := range tests {
output := RenderDisplayStrings(test.input)
if !assert.EqualValues(t, output, test.expected) {
t.Errorf("RenderDisplayStrings(%v) = %v, want %v", test.input, output, test.expected)
}
}
}
+11
View File
@@ -144,3 +144,14 @@ func Reverse(values []string) []string {
}
return result
}
func LimitStr(value string, limit int) string {
n := 0
for i := range value {
if n >= limit {
return value[:i]
}
n++
}
return value
}
+58
View File
@@ -248,3 +248,61 @@ func TestReverse(t *testing.T) {
}
}
}
func TestLimitStr(t *testing.T) {
for _, test := range []struct {
values string
limit int
want string
}{
{
values: "",
limit: 10,
want: "",
},
{
values: "",
limit: 0,
want: "",
},
{
values: "a",
limit: 1,
want: "a",
},
{
values: "ab",
limit: 2,
want: "ab",
},
{
values: "abc",
limit: 3,
want: "abc",
},
{
values: "abcd",
limit: 3,
want: "abc",
},
{
values: "abcde",
limit: 3,
want: "abc",
},
{
values: "あいう",
limit: 1,
want: "あ",
},
{
values: "あいう",
limit: 2,
want: "あい",
},
} {
if got := LimitStr(test.values, test.limit); !assert.EqualValues(t, got, test.want) {
t.Errorf("LimitString(%v, %d) = %v; want %v", test.values, test.limit, got, test.want)
}
}
}
+7 -27
View File
@@ -52,6 +52,13 @@ func Min(x, y int) int {
return y
}
func Max(x, y int) int {
if x > y {
return x
}
return y
}
func AsJson(i interface{}) string {
bytes, _ := json.MarshalIndent(i, "", " ")
return string(bytes)
@@ -68,33 +75,6 @@ func ModuloWithWrap(n, max int) int {
}
}
// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
func TruncateWithEllipsis(str string, limit int) string {
if limit == 1 && len(str) > 1 {
return "."
}
if limit == 2 && len(str) > 2 {
return ".."
}
ellipsis := "..."
if len(str) <= limit {
return str
}
remainingLength := limit - len(ellipsis)
return str[0:remainingLength] + "..."
}
func SafeTruncate(str string, limit int) string {
if len(str) > limit {
return str[0:limit]
} else {
return str
}
}
func FindStringSubmatch(str string, regexpStr string) (bool, []string) {
re := regexp.MustCompile(regexpStr)
match := re.FindStringSubmatch(str)
+13 -4
View File
@@ -8,6 +8,7 @@ import (
"path/filepath"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/integration"
"github.com/jesseduffield/lazygit/pkg/secureexec"
@@ -66,7 +67,7 @@ func main() {
app := &App{testDir: testDir}
app.loadTests()
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL, false)
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL, false, gui.RuneReplacements)
if err != nil {
log.Panicln(err)
}
@@ -81,6 +82,11 @@ func main() {
if app.itemIdx > 0 {
app.itemIdx--
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
return nil
}); err != nil {
log.Panicln(err)
@@ -324,9 +330,6 @@ func (app *App) layout(g *gocui.Gui) error {
return err
}
}
if err := listView.SetCursor(0, app.itemIdx); err != nil {
return err
}
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
if err != nil {
@@ -371,6 +374,12 @@ func (app *App) layout(g *gocui.Gui) error {
if app.itemIdx < len(app.tests)-1 {
app.itemIdx++
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
return nil
}); err != nil {
log.Panicln(err)
+26 -13
View File
@@ -29,12 +29,15 @@ type (
fontEffect int
)
type instruction struct {
kind int
param1 int
param2 int
toWrite []rune
}
type instruction interface{ isInstruction() }
type eraseInLineFromCursor struct{}
func (self eraseInLineFromCursor) isInstruction() {}
type noInstruction struct{}
func (self noInstruction) isInstruction() {}
const (
stateNone escapeState = iota
@@ -87,7 +90,7 @@ func newEscapeInterpreter(mode OutputMode) *escapeInterpreter {
curFgColor: ColorDefault,
curBgColor: ColorDefault,
mode: mode,
instruction: instruction{kind: NONE},
instruction: noInstruction{},
}
return ei
}
@@ -101,7 +104,7 @@ func (ei *escapeInterpreter) reset() {
}
func (ei *escapeInterpreter) instructionRead() {
ei.instruction.kind = NONE
ei.instruction = noInstruction{}
}
// parseOne parses a rune. If isEscape is true, it means that the rune is part
@@ -137,6 +140,8 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
ei.csiParam = append(ei.csiParam, "")
case ch == 'm':
ei.csiParam = append(ei.csiParam, "0")
case ch == 'K':
// fall through
default:
return false, errCSIParseError
}
@@ -168,12 +173,20 @@ func (ei *escapeInterpreter) parseOne(ch rune) (isEscape bool, err error) {
ei.csiParam = nil
return true, nil
case ch == 'K':
p, err := strconv.Atoi(ei.csiParam[0])
if err != nil {
return false, errCSIParseError
p := 0
if len(ei.csiParam) != 0 && ei.csiParam[0] != "" {
p, err = strconv.Atoi(ei.csiParam[0])
if err != nil {
return false, errCSIParseError
}
}
if p == 0 {
ei.instruction = eraseInLineFromCursor{}
} else {
// non-zero values of P not supported
ei.instruction = noInstruction{}
}
ei.instruction.kind = ERASE_IN_LINE
ei.instruction.param1 = p
ei.state = stateNone
ei.csiParam = nil
+79 -78
View File
@@ -6,7 +6,6 @@ package gocui
import (
standardErrors "errors"
"fmt"
"log"
"runtime"
"strings"
@@ -166,14 +165,14 @@ type Gui struct {
}
// NewGui returns a new Gui object with a given output mode.
func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode, headless bool) (*Gui, error) {
func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode, headless bool, runeReplacements map[rune]string) (*Gui, error) {
g := &Gui{}
var err error
if headless {
err = g.tcellInitSimulation()
} else {
err = g.tcellInit()
err = g.tcellInit(runeReplacements)
}
if err != nil {
return nil, err
@@ -664,79 +663,6 @@ func (g *Gui) onResize() {
// g.screen.Sync()
}
// flush updates the gui, re-drawing frames and buffers.
func (g *Gui) flush() error {
// pretty sure we don't need this, but keeping it here in case we get weird visual artifacts
// g.clear(g.FgColor, g.BgColor)
maxX, maxY := Screen.Size()
// if GUI's size has changed, we need to redraw all views
if maxX != g.maxX || maxY != g.maxY {
for _, v := range g.views {
v.clearViewLines()
}
}
g.maxX, g.maxY = maxX, maxY
for _, m := range g.managers {
if err := m.Layout(g); err != nil {
return err
}
}
for _, v := range g.views {
if !v.Visible || v.y1 < v.y0 {
continue
}
if v.Frame {
var fgColor, bgColor, frameColor Attribute
if g.Highlight && v == g.currentView {
fgColor = g.SelFgColor
bgColor = g.SelBgColor
frameColor = g.SelFrameColor
} else {
bgColor = g.BgColor
if v.TitleColor != ColorDefault {
fgColor = v.TitleColor
} else {
fgColor = g.FgColor
}
if v.FrameColor != ColorDefault {
frameColor = v.FrameColor
} else {
frameColor = g.FrameColor
}
}
if err := g.drawFrameEdges(v, frameColor, bgColor); err != nil {
return err
}
if err := g.drawFrameCorners(v, frameColor, bgColor); err != nil {
return err
}
if v.Title != "" || len(v.Tabs) > 0 {
if err := g.drawTitle(v, fgColor, bgColor); err != nil {
return err
}
}
if v.Subtitle != "" {
if err := g.drawSubtitle(v, fgColor, bgColor); err != nil {
return err
}
}
if v.ContainsList && g.ShowListFooter {
if err := g.drawListFooter(v, fgColor, bgColor); err != nil {
return err
}
}
}
if err := g.draw(v); err != nil {
return err
}
}
Screen.Show()
return nil
}
func (g *Gui) clear(fg, bg Attribute) (int, int) {
st := getTcellStyle(oldStyle{fg: fg, bg: bg, outputMode: g.outputMode})
w, h := Screen.Size()
@@ -983,7 +909,7 @@ func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
return nil
}
message := fmt.Sprintf("%d of %d", v.cy+v.oy+1, len(v.lines))
message := v.Footer
if v.y1 < 0 || v.y1 >= g.maxY {
return nil
@@ -1006,12 +932,88 @@ func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
return nil
}
// flush updates the gui, re-drawing frames and buffers.
func (g *Gui) flush() error {
// pretty sure we don't need this, but keeping it here in case we get weird visual artifacts
// g.clear(g.FgColor, g.BgColor)
maxX, maxY := Screen.Size()
// if GUI's size has changed, we need to redraw all views
if maxX != g.maxX || maxY != g.maxY {
for _, v := range g.views {
v.clearViewLines()
}
}
g.maxX, g.maxY = maxX, maxY
for _, m := range g.managers {
if err := m.Layout(g); err != nil {
return err
}
}
for _, v := range g.views {
if err := g.draw(v); err != nil {
return err
}
}
Screen.Show()
return nil
}
// draw manages the cursor and calls the draw function of a view.
func (g *Gui) draw(v *View) error {
if g.suspended {
return nil
}
if !v.Visible || v.y1 < v.y0 {
return nil
}
if v.Frame {
var fgColor, bgColor, frameColor Attribute
if g.Highlight && v == g.currentView {
fgColor = g.SelFgColor
bgColor = g.SelBgColor
frameColor = g.SelFrameColor
} else {
bgColor = g.BgColor
if v.TitleColor != ColorDefault {
fgColor = v.TitleColor
} else {
fgColor = g.FgColor
}
if v.FrameColor != ColorDefault {
frameColor = v.FrameColor
} else {
frameColor = g.FrameColor
}
}
if err := g.drawFrameEdges(v, frameColor, bgColor); err != nil {
return err
}
if err := g.drawFrameCorners(v, frameColor, bgColor); err != nil {
return err
}
if v.Title != "" || len(v.Tabs) > 0 {
if err := g.drawTitle(v, fgColor, bgColor); err != nil {
return err
}
}
if v.Subtitle != "" {
if err := g.drawSubtitle(v, fgColor, bgColor); err != nil {
return err
}
}
if v.Footer != "" && g.ShowListFooter {
if err := g.drawListFooter(v, fgColor, bgColor); err != nil {
return err
}
}
}
if g.Cursor {
if curview := g.currentView; curview != nil {
vMaxX, vMaxY := curview.Size()
@@ -1041,7 +1043,6 @@ func (g *Gui) draw(v *View) error {
Screen.HideCursor()
}
v.clearRunes()
if err := v.draw(); err != nil {
return err
}
+14 -3
View File
@@ -26,6 +26,10 @@ var runeReplacements = map[rune]string{
'┐': "+",
'└': "+",
'┘': "+",
'╭': "+",
'╮': "+",
'╰': "+",
'╯': "+",
'─': "-",
// using a hyphen here actually looks weird.
@@ -33,6 +37,9 @@ var runeReplacements = map[rune]string{
'╶': " ",
'╴': " ",
'┴': "+",
'┬': "+",
'╷': "|",
'├': "+",
'│': "|",
'▼': "v",
@@ -42,7 +49,7 @@ var runeReplacements = map[rune]string{
}
// tcellInit initializes tcell screen for use.
func (g *Gui) tcellInit() error {
func (g *Gui) tcellInit(runeReplacements map[rune]string) error {
runewidth.DefaultCondition.EastAsianWidth = false
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
@@ -51,7 +58,7 @@ func (g *Gui) tcellInit() error {
} else if e = s.Init(); e != nil {
return e
} else {
registerRuneFallbacks(s)
registerRuneFallbacks(s, runeReplacements)
g.screen = s
Screen = s
@@ -59,10 +66,14 @@ func (g *Gui) tcellInit() error {
}
}
func registerRuneFallbacks(s tcell.Screen) {
func registerRuneFallbacks(s tcell.Screen, additional map[rune]string) {
for before, after := range runeReplacements {
s.RegisterRuneFallback(before, after)
}
for before, after := range additional {
s.RegisterRuneFallback(before, after)
}
}
// tcellInitSimulation initializes tcell screen for use.
+169 -74
View File
@@ -153,14 +153,14 @@ type View struct {
searcher *searcher
// when ContainsList is true, we show the current index and total count in the view
ContainsList bool
// KeybindOnEdit should be set to true when you want to execute keybindings even when the view is editable
// (this is usually not the case)
KeybindOnEdit bool
TextArea *TextArea
// something like '1 of 20' for a list view
Footer string
}
// call this in the event of a view resize, or if you want to render new content
@@ -218,7 +218,7 @@ func (v *View) SelectSearchResult(index int) error {
}
y := v.searcher.searchPositions[index].y
v.FocusPoint(0, y)
v.FocusPoint(v.ox, y)
if v.searcher.onSelectItem != nil {
return v.searcher.onSelectItem(y, index, itemCount)
}
@@ -227,10 +227,9 @@ func (v *View) SelectSearchResult(index int) error {
func (v *View) Search(str string) error {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.searcher.search(str)
v.updateSearchPositions()
if len(v.searcher.searchPositions) > 0 {
// get the first result past the current cursor
currentIndex := 0
@@ -243,8 +242,10 @@ func (v *View) Search(str string) error {
}
}
v.searcher.currentSearchIndex = currentIndex
v.writeMutex.Unlock()
return v.SelectSearchResult(currentIndex)
} else {
v.writeMutex.Unlock()
return v.searcher.onSelectItem(-1, -1, 0)
}
}
@@ -359,7 +360,42 @@ func (v *View) Dimensions() (int, int, int, int) {
// Size returns the number of visible columns and rows in the View.
func (v *View) Size() (x, y int) {
return v.x1 - v.x0 - 1, v.y1 - v.y0 - 1
return v.Width(), v.Height()
}
func (v *View) Width() int {
return v.x1 - v.x0 - 1
}
func (v *View) Height() int {
return v.y1 - v.y0 - 1
}
// if a view has a frame, that leaves less space for its writeable area
func (v *View) InnerWidth() int {
innerWidth := v.Width() - v.frameOffset()
if innerWidth < 0 {
return 0
}
return innerWidth
}
func (v *View) InnerHeight() int {
innerHeight := v.Height() - v.frameOffset()
if innerHeight < 0 {
return 0
}
return innerHeight
}
func (v *View) frameOffset() int {
if v.Frame {
return 1
} else {
return 0
}
}
// Name returns the name of the view.
@@ -440,9 +476,33 @@ func (v *View) SetOrigin(x, y int) error {
return nil
}
func (v *View) SetOriginX(x int) error {
if x < 0 {
return ErrInvalidPoint
}
v.ox = x
return nil
}
func (v *View) SetOriginY(y int) error {
if y < 0 {
return ErrInvalidPoint
}
v.oy = y
return nil
}
// Origin returns the origin position of the view.
func (v *View) Origin() (x, y int) {
return v.ox, v.oy
return v.OriginX(), v.OriginY()
}
func (v *View) OriginX() int {
return v.ox
}
func (v *View) OriginY() int {
return v.oy
}
// SetWritePos sets the write position of the view's internal buffer.
@@ -538,8 +598,6 @@ func (v *View) Write(p []byte) (n int, err error) {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.tainted = true
v.makeWriteable(v.wx, v.wy)
v.writeRunes(bytes.Runes(p))
return len(p), nil
@@ -549,20 +607,16 @@ func (v *View) WriteRunes(p []rune) {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.writeRunes(p)
}
// writeRunes copies slice of runes into internal lines buffer.
func (v *View) writeRunes(p []rune) {
v.tainted = true
// Fill with empty cells, if writing outside current view buffer
v.makeWriteable(v.wx, v.wy)
v.writeRunes(p)
}
func (v *View) WriteString(s string) {
v.WriteRunes([]rune(s))
}
// writeRunes copies slice of runes into internal lines buffer.
// caller must make sure that writing position is accessable.
func (v *View) writeRunes(p []rune) {
for _, r := range p {
switch r {
case '\n':
@@ -576,21 +630,34 @@ func (v *View) writeRunes(p []rune) {
case '\r':
v.wx = 0
default:
cells := v.parseInput(r)
moveCursor, cells := v.parseInput(r)
if cells == nil {
continue
}
v.writeCells(v.wx, v.wy, cells)
v.wx += len(cells)
if moveCursor {
v.wx += len(cells)
}
}
}
}
// exported functions use the mutex. Non-exported functions are for internal use
// and a calling function should use a mutex
func (v *View) WriteString(s string) {
v.WriteRunes([]rune(s))
}
func (v *View) writeString(s string) {
v.writeRunes([]rune(s))
}
// parseInput parses char by char the input written to the View. It returns nil
// while processing ESC sequences. Otherwise, it returns a cell slice that
// contains the processed data.
func (v *View) parseInput(ch rune) []cell {
func (v *View) parseInput(ch rune) (bool, []cell) {
cells := []cell{}
moveCursor := true
isEscape, err := v.ei.parseOne(ch)
if err != nil {
@@ -604,25 +671,32 @@ func (v *View) parseInput(ch rune) []cell {
}
v.ei.reset()
} else {
if isEscape {
return nil
}
repeatCount := 1
if ch == '\t' {
if _, ok := v.ei.instruction.(eraseInLineFromCursor); ok {
// fill rest of line
v.ei.instructionRead()
repeatCount = v.InnerWidth() - v.wx
ch = ' '
moveCursor = false
} else if isEscape {
// do not output anything
return moveCursor, nil
} else if ch == '\t' {
// fill tab-sized space
ch = ' '
repeatCount = 4
}
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ch,
}
for i := 0; i < repeatCount; i++ {
c := cell{
fgColor: v.ei.curFgColor,
bgColor: v.ei.curBgColor,
chr: ch,
}
cells = append(cells, c)
}
}
return cells
return moveCursor, cells
}
// Read reads data into p from the current reading position set by SetReadPos.
@@ -661,16 +735,28 @@ func (v *View) Read(p []byte) (n int, err error) {
return offset, io.EOF
}
// only use this if the calling function has a lock on writeMutex
func (v *View) clear() {
v.rewind()
v.lines = nil
v.clearViewLines()
}
// Clear empties the view's internal buffer.
// And resets reading and writing offsets.
func (v *View) Clear() {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.rewind()
v.lines = nil
v.clearViewLines()
v.clearRunes()
v.clear()
}
func (v *View) SetContent(str string) {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.clear()
v.writeString(str)
}
// Rewind sets read and write pos to (0, 0).
@@ -767,6 +853,11 @@ func (v *View) IsTainted() bool {
// draw re-draws the view's contents.
func (v *View) draw() error {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.clearRunes()
if !v.Visible {
return nil
}
@@ -809,14 +900,22 @@ func (v *View) draw() error {
}
}
if v.Autoscroll && len(v.viewLines) > maxY {
v.oy = len(v.viewLines) - maxY
visibleViewLinesHeight := v.viewLineLengthIgnoringTrailingBlankLines()
if v.Autoscroll && visibleViewLinesHeight > maxY {
v.oy = visibleViewLinesHeight - maxY
}
if len(v.viewLines) == 0 {
return nil
}
start := v.oy
if start > len(v.viewLines)-1 {
start = len(v.viewLines) - 1
}
y := 0
for i, vline := range v.viewLines {
if i < v.oy {
continue
}
for _, vline := range v.viewLines[start:] {
if y >= maxY {
break
}
@@ -858,6 +957,18 @@ func (v *View) draw() error {
return nil
}
// if autoscroll is enabled but we only have a single row of cells shown to the
// user, we don't want to scroll to the final line if it contains no text. So
// this tells us the view lines height when we ignore any trailing blank lines
func (v *View) viewLineLengthIgnoringTrailingBlankLines() int {
for i := len(v.viewLines) - 1; i >= 0; i-- {
if len(v.viewLines[i].line) > 0 {
return i + 1
}
}
return 0
}
func (v *View) isPatternMatchedRune(x, y int) (bool, bool) {
searchStringLength := len(v.searcher.searchString)
for i, pos := range v.searcher.searchPositions {
@@ -1008,23 +1119,6 @@ func indexFunc(r rune) bool {
return r == ' ' || r == 0
}
// SetLine changes the contents of an existing line.
func (v *View) SetLine(y int, text string) error {
if y < 0 || y >= len(v.lines) {
err := ErrInvalidPoint
return err
}
v.tainted = true
line := make([]cell, 0)
for _, r := range text {
c := v.parseInput(r)
line = append(line, c...)
}
v.lines[y] = line
return nil
}
// SetHighlight toggles highlighting of separate lines, for custom lists
// or multiple selection in views.
func (v *View) SetHighlight(y int, on bool) error {
@@ -1050,14 +1144,6 @@ func (v *View) SetHighlight(y int, on bool) error {
return nil
}
func lineWidth(line []cell) (n int) {
for i := range line {
n += runewidth.RuneWidth(line[i].chr)
}
return
}
func lineWrap(line []cell, columns int) [][]cell {
if columns == 0 {
return [][]cell{line}
@@ -1129,14 +1215,10 @@ func (v *View) RenderTextArea() {
fmt.Fprint(v, v.TextArea.GetContent())
cursorX, cursorY := v.TextArea.GetCursorXY()
prevOriginX, prevOriginY := v.Origin()
width, height := v.Size()
width, height := v.InnerWidth(), v.InnerHeight()
frameAdjustment := 0
if v.Frame {
frameAdjustment = -1
}
newViewCursorX, newOriginX := updatedCursorAndOrigin(prevOriginX, width+frameAdjustment, cursorX)
newViewCursorY, newOriginY := updatedCursorAndOrigin(prevOriginY, height+frameAdjustment, cursorY)
newViewCursorX, newOriginX := updatedCursorAndOrigin(prevOriginX, width, cursorX)
newViewCursorY, newOriginY := updatedCursorAndOrigin(prevOriginY, height, cursorY)
_ = v.SetCursor(newViewCursorX, newViewCursorY)
_ = v.SetOrigin(newOriginX, newOriginY)
@@ -1169,3 +1251,16 @@ func (v *View) ClearTextArea() {
_ = v.SetOrigin(0, 0)
_ = v.SetCursor(0, 0)
}
// only call this function if you don't care where v.wx and v.wy end up
func (v *View) OverwriteLines(y int, content string) {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
// break by newline, then for each line, write it, then add that erase command
v.wx = 0
v.wy = y
lines := strings.Replace(content, "\n", "\x1b[K\n", -1)
v.writeString(lines)
}
+1 -1
View File
@@ -1,6 +1,6 @@
# go-colorable
[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable)
[![Build Status](https://github.com/mattn/go-colorable/workflows/test/badge.svg)](https://github.com/mattn/go-colorable/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/mattn/go-colorable/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-colorable)
[![GoDoc](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable)
[![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable)
+1
View File
@@ -1,3 +1,4 @@
//go:build appengine
// +build appengine
package colorable
+2 -2
View File
@@ -1,5 +1,5 @@
// +build !windows
// +build !appengine
//go:build !windows && !appengine
// +build !windows,!appengine
package colorable
+11 -7
View File
@@ -1,5 +1,5 @@
// +build windows
// +build !appengine
//go:build windows && !appengine
// +build windows,!appengine
package colorable
@@ -452,18 +452,22 @@ func (w *Writer) Write(data []byte) (n int, err error) {
} else {
er = bytes.NewReader(data)
}
var bw [1]byte
var plaintext bytes.Buffer
loop:
for {
c1, err := er.ReadByte()
if err != nil {
plaintext.WriteTo(w.out)
break loop
}
if c1 != 0x1b {
bw[0] = c1
w.out.Write(bw[:])
plaintext.WriteByte(c1)
continue
}
_, err = plaintext.WriteTo(w.out)
if err != nil {
break loop
}
c2, err := er.ReadByte()
if err != nil {
break loop
@@ -719,7 +723,7 @@ loop:
n256setup()
}
attr &= backgroundMask
attr |= n256foreAttr[n256]
attr |= n256foreAttr[n256%len(n256foreAttr)]
i += 2
}
} else if len(token) == 5 && token[i+1] == "2" {
@@ -761,7 +765,7 @@ loop:
n256setup()
}
attr &= foregroundMask
attr |= n256backAttr[n256]
attr |= n256backAttr[n256%len(n256backAttr)]
i += 2
}
} else if len(token) == 5 && token[i+1] == "2" {
+2 -2
View File
@@ -1,8 +1,8 @@
module github.com/mattn/go-colorable
require (
github.com/mattn/go-isatty v0.0.12
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect
github.com/mattn/go-isatty v0.0.14
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
)
go 1.13
+5 -5
View File
@@ -1,5 +1,5 @@
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+7 -3
View File
@@ -18,18 +18,22 @@ func NewNonColorable(w io.Writer) io.Writer {
// Write writes data on console
func (w *NonColorable) Write(data []byte) (n int, err error) {
er := bytes.NewReader(data)
var bw [1]byte
var plaintext bytes.Buffer
loop:
for {
c1, err := er.ReadByte()
if err != nil {
plaintext.WriteTo(w.out)
break loop
}
if c1 != 0x1b {
bw[0] = c1
w.out.Write(bw[:])
plaintext.WriteByte(c1)
continue
}
_, err = plaintext.WriteTo(w.out)
if err != nil {
break loop
}
c2, err := er.ReadByte()
if err != nil {
break loop
+1 -1
View File
@@ -2,4 +2,4 @@ module github.com/mattn/go-isatty
go 1.12
require golang.org/x/sys v0.0.0-20200116001909-b77594299b42
require golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
+1
View File
@@ -1,3 +1,4 @@
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly) && !appengine
// +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine
+2 -1
View File
@@ -1,4 +1,5 @@
// +build appengine js nacl
//go:build appengine || js || nacl || wasm
// +build appengine js nacl wasm
package isatty
+1
View File
@@ -1,3 +1,4 @@
//go:build plan9
// +build plan9
package isatty
+4 -5
View File
@@ -1,5 +1,5 @@
// +build solaris
// +build !appengine
//go:build solaris && !appengine
// +build solaris,!appengine
package isatty
@@ -8,10 +8,9 @@ import (
)
// IsTerminal returns true if the given file descriptor is a terminal.
// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c
// see: https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c
func IsTerminal(fd uintptr) bool {
var termio unix.Termio
err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio)
_, err := unix.IoctlGetTermio(int(fd), unix.TCGETA)
return err == nil
}
+2 -1
View File
@@ -1,4 +1,5 @@
// +build linux aix
//go:build (linux || aix || zos) && !appengine
// +build linux aix zos
// +build !appengine
package isatty
+3 -3
View File
@@ -1,5 +1,5 @@
// +build windows
// +build !appengine
//go:build windows && !appengine
// +build windows,!appengine
package isatty
@@ -76,7 +76,7 @@ func isCygwinPipeName(name string) bool {
}
// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler
// since GetFileInformationByHandleEx is not avilable under windows Vista and still some old fashion
// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion
// guys are using Windows XP, this is a workaround for those guys, it will also work on system from
// Windows vista to 10
// see https://stackoverflow.com/a/18792477 for details
-8
View File
@@ -1,8 +0,0 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": [
"gomodTidy"
]
}
+53
View File
@@ -34,3 +34,56 @@ func ParseUnixCredentials(m *SocketControlMessage) (*Ucred, error) {
ucred := *(*Ucred)(unsafe.Pointer(&m.Data[0]))
return &ucred, nil
}
// PktInfo4 encodes Inet4Pktinfo into a socket control message of type IP_PKTINFO.
func PktInfo4(info *Inet4Pktinfo) []byte {
b := make([]byte, CmsgSpace(SizeofInet4Pktinfo))
h := (*Cmsghdr)(unsafe.Pointer(&b[0]))
h.Level = SOL_IP
h.Type = IP_PKTINFO
h.SetLen(CmsgLen(SizeofInet4Pktinfo))
*(*Inet4Pktinfo)(h.data(0)) = *info
return b
}
// PktInfo6 encodes Inet6Pktinfo into a socket control message of type IPV6_PKTINFO.
func PktInfo6(info *Inet6Pktinfo) []byte {
b := make([]byte, CmsgSpace(SizeofInet6Pktinfo))
h := (*Cmsghdr)(unsafe.Pointer(&b[0]))
h.Level = SOL_IPV6
h.Type = IPV6_PKTINFO
h.SetLen(CmsgLen(SizeofInet6Pktinfo))
*(*Inet6Pktinfo)(h.data(0)) = *info
return b
}
// ParseOrigDstAddr decodes a socket control message containing the original
// destination address. To receive such a message the IP_RECVORIGDSTADDR or
// IPV6_RECVORIGDSTADDR option must be enabled on the socket.
func ParseOrigDstAddr(m *SocketControlMessage) (Sockaddr, error) {
switch {
case m.Header.Level == SOL_IP && m.Header.Type == IP_ORIGDSTADDR:
pp := (*RawSockaddrInet4)(unsafe.Pointer(&m.Data[0]))
sa := new(SockaddrInet4)
p := (*[2]byte)(unsafe.Pointer(&pp.Port))
sa.Port = int(p[0])<<8 + int(p[1])
for i := 0; i < len(sa.Addr); i++ {
sa.Addr[i] = pp.Addr[i]
}
return sa, nil
case m.Header.Level == SOL_IPV6 && m.Header.Type == IPV6_ORIGDSTADDR:
pp := (*RawSockaddrInet6)(unsafe.Pointer(&m.Data[0]))
sa := new(SockaddrInet6)
p := (*[2]byte)(unsafe.Pointer(&pp.Port))
sa.Port = int(p[0])<<8 + int(p[1])
sa.ZoneId = pp.Scope_id
for i := 0; i < len(sa.Addr); i++ {
sa.Addr[i] = pp.Addr[i]
}
return sa, nil
default:
return nil, EINVAL
}
}
+19 -2
View File
@@ -430,8 +430,25 @@ func GetsockoptXucred(fd, level, opt int) (*Xucred, error) {
return x, err
}
func SysctlKinfoProcSlice(name string) ([]KinfoProc, error) {
mib, err := sysctlmib(name)
func SysctlKinfoProc(name string, args ...int) (*KinfoProc, error) {
mib, err := sysctlmib(name, args...)
if err != nil {
return nil, err
}
var kinfo KinfoProc
n := uintptr(SizeofKinfoProc)
if err := sysctl(mib, (*byte)(unsafe.Pointer(&kinfo)), &n, nil, 0); err != nil {
return nil, err
}
if n != SizeofKinfoProc {
return nil, EIO
}
return &kinfo, nil
}
func SysctlKinfoProcSlice(name string, args ...int) ([]KinfoProc, error) {
mib, err := sysctlmib(name, args...)
if err != nil {
return nil, err
}
+26
View File
@@ -116,6 +116,7 @@ const (
ARPHRD_LAPB = 0x204
ARPHRD_LOCALTLK = 0x305
ARPHRD_LOOPBACK = 0x304
ARPHRD_MCTP = 0x122
ARPHRD_METRICOM = 0x17
ARPHRD_NETLINK = 0x338
ARPHRD_NETROM = 0x0
@@ -472,6 +473,7 @@ const (
DM_DEV_WAIT = 0xc138fd08
DM_DIR = "mapper"
DM_GET_TARGET_VERSION = 0xc138fd11
DM_IMA_MEASUREMENT_FLAG = 0x80000
DM_INACTIVE_PRESENT_FLAG = 0x40
DM_INTERNAL_SUSPEND_FLAG = 0x40000
DM_IOCTL = 0xfd
@@ -716,6 +718,7 @@ const (
ETH_P_LOOPBACK = 0x9000
ETH_P_MACSEC = 0x88e5
ETH_P_MAP = 0xf9
ETH_P_MCTP = 0xfa
ETH_P_MOBITEX = 0x15
ETH_P_MPLS_MC = 0x8848
ETH_P_MPLS_UC = 0x8847
@@ -751,6 +754,21 @@ const (
ETH_P_WCCP = 0x883e
ETH_P_X25 = 0x805
ETH_P_XDSA = 0xf8
EV_ABS = 0x3
EV_CNT = 0x20
EV_FF = 0x15
EV_FF_STATUS = 0x17
EV_KEY = 0x1
EV_LED = 0x11
EV_MAX = 0x1f
EV_MSC = 0x4
EV_PWR = 0x16
EV_REL = 0x2
EV_REP = 0x14
EV_SND = 0x12
EV_SW = 0x5
EV_SYN = 0x0
EV_VERSION = 0x10001
EXABYTE_ENABLE_NEST = 0xf0
EXT2_SUPER_MAGIC = 0xef53
EXT3_SUPER_MAGIC = 0xef53
@@ -789,9 +807,11 @@ const (
FAN_DELETE_SELF = 0x400
FAN_DENY = 0x2
FAN_ENABLE_AUDIT = 0x40
FAN_EPIDFD = -0x2
FAN_EVENT_INFO_TYPE_DFID = 0x3
FAN_EVENT_INFO_TYPE_DFID_NAME = 0x2
FAN_EVENT_INFO_TYPE_FID = 0x1
FAN_EVENT_INFO_TYPE_PIDFD = 0x4
FAN_EVENT_METADATA_LEN = 0x18
FAN_EVENT_ON_CHILD = 0x8000000
FAN_MARK_ADD = 0x1
@@ -811,6 +831,7 @@ const (
FAN_MOVE_SELF = 0x800
FAN_NOFD = -0x1
FAN_NONBLOCK = 0x2
FAN_NOPIDFD = -0x1
FAN_ONDIR = 0x40000000
FAN_OPEN = 0x20
FAN_OPEN_EXEC = 0x1000
@@ -821,6 +842,7 @@ const (
FAN_REPORT_DIR_FID = 0x400
FAN_REPORT_FID = 0x200
FAN_REPORT_NAME = 0x800
FAN_REPORT_PIDFD = 0x80
FAN_REPORT_TID = 0x100
FAN_UNLIMITED_MARKS = 0x20
FAN_UNLIMITED_QUEUE = 0x10
@@ -1997,6 +2019,7 @@ const (
PR_SPEC_ENABLE = 0x2
PR_SPEC_FORCE_DISABLE = 0x8
PR_SPEC_INDIRECT_BRANCH = 0x1
PR_SPEC_L1D_FLUSH = 0x2
PR_SPEC_NOT_AFFECTED = 0x0
PR_SPEC_PRCTL = 0x1
PR_SPEC_STORE_BYPASS = 0x0
@@ -2432,12 +2455,15 @@ const (
SMART_WRITE_THRESHOLDS = 0xd7
SMB_SUPER_MAGIC = 0x517b
SOCKFS_MAGIC = 0x534f434b
SOCK_BUF_LOCK_MASK = 0x3
SOCK_DCCP = 0x6
SOCK_IOC_TYPE = 0x89
SOCK_PACKET = 0xa
SOCK_RAW = 0x3
SOCK_RCVBUF_LOCK = 0x2
SOCK_RDM = 0x4
SOCK_SEQPACKET = 0x5
SOCK_SNDBUF_LOCK = 0x1
SOL_AAL = 0x109
SOL_ALG = 0x117
SOL_ATM = 0x108
+1
View File
@@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -294,6 +294,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -300,6 +300,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -290,6 +290,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -293,6 +293,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -348,6 +348,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -352,6 +352,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -352,6 +352,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -281,6 +281,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -356,6 +356,7 @@ const (
SO_BPF_EXTENSIONS = 0x30
SO_BROADCAST = 0x6
SO_BSDCOMPAT = 0xe
SO_BUF_LOCK = 0x48
SO_BUSY_POLL = 0x2e
SO_BUSY_POLL_BUDGET = 0x46
SO_CNX_ADVICE = 0x35
+1
View File
@@ -347,6 +347,7 @@ const (
SO_BPF_EXTENSIONS = 0x32
SO_BROADCAST = 0x20
SO_BSDCOMPAT = 0x400
SO_BUF_LOCK = 0x51
SO_BUSY_POLL = 0x30
SO_BUSY_POLL_BUDGET = 0x49
SO_CNX_ADVICE = 0x37
+1
View File
@@ -444,4 +444,5 @@ const (
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
)
+1
View File
@@ -366,4 +366,5 @@ const (
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
)
+2
View File
@@ -7,6 +7,7 @@
package unix
const (
SYS_SYSCALL_MASK = 0
SYS_RESTART_SYSCALL = 0
SYS_EXIT = 1
SYS_FORK = 2
@@ -407,4 +408,5 @@ const (
SYS_LANDLOCK_CREATE_RULESET = 444
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_PROCESS_MRELEASE = 448
)
+1
View File
@@ -311,4 +311,5 @@ const (
SYS_LANDLOCK_ADD_RULE = 445
SYS_LANDLOCK_RESTRICT_SELF = 446
SYS_MEMFD_SECRET = 447
SYS_PROCESS_MRELEASE = 448
)

Some files were not shown because too many files have changed in this diff Show More