diff --git a/config.yaml b/config.yaml index 65d04b4..e236939 100644 --- a/config.yaml +++ b/config.yaml @@ -6,5 +6,11 @@ keys: switch_tab_left: shift+left quit: ctrl+c refresh: ctrl+r + live_mode: ctrl+l enter: enter tab: tab + +settings: + live_mode: + enabled: true # to enable live mode at startup + interval: 15s # interval to refresh the page diff --git a/go.mod b/go.mod index fbeae81..2313b28 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,23 @@ module github.com/termkit/gama -go 1.22.1 +go 1.22.6 require ( - github.com/Masterminds/semver/v3 v3.2.1 - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.26.4 - github.com/charmbracelet/lipgloss v0.11.0 + github.com/Masterminds/semver/v3 v3.3.0 + github.com/charmbracelet/bubbles v0.19.0 + github.com/charmbracelet/bubbletea v1.1.0 + github.com/charmbracelet/lipgloss v0.13.0 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + github.com/termkit/skeleton v0.1.2 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/x/ansi v0.1.2 // indirect - github.com/charmbracelet/x/input v0.1.1 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -27,28 +26,26 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index 4c16123..dd0dbee 100644 --- a/go.sum +++ b/go.sum @@ -1,33 +1,23 @@ -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg= -github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q= -github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= -github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= -github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= -github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= -github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= -github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= -github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= -github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= -github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4= -github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= -github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= +github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -38,16 +28,14 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -56,17 +44,14 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -74,7 +59,6 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -84,8 +68,6 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -94,14 +76,13 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -109,24 +90,22 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d h1:N0hmiNbwsSNwHBAvR3QB5w25pUwH4tK0Y/RltD1j1h4= -golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg= -golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +github.com/termkit/skeleton v0.1.2 h1:8bQD1w0lHI1VF/13JE2Udg65HN3UUcRPJdIvwzLvFq8= +github.com/termkit/skeleton v0.1.2/go.mod h1:guH2iOGt7SkjmvwFytqDVHFv3yh5ZiGv7B0HpuNW4ME= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..1e16e82 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,155 @@ +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= +cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= +cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= +cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= +cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg= +github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU= +github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= +github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ= +github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= +github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= +github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nats-io/nats.go v1.34.0 h1:fnxnPCNiwIG5w08rlMcEKTUw4AV/nKyGCOJE8TdhSPk= +github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo= +github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= +github.com/sagikazarmark/crypt v0.19.0 h1:WMyLTjHBo64UvNcWqpzY3pbZTYgnemZU8FBZigKc42E= +github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= +go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= +go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A= +go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= +go.etcd.io/etcd/client/v2 v2.305.12 h1:0m4ovXYo1CHaA/Mp3X/Fak5sRNIWf01wk/X1/G3sGKI= +go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E= +go.etcd.io/etcd/client/v3 v3.5.12 h1:v5lCPXn1pf1Uu3M4laUE2hp/geOTc5uPcYYsNe1lDxg= +go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.171.0 h1:w174hnBPqut76FzW5Qaupt7zY8Kql6fiVjgys4f58sU= +google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/internal/config/config.go b/internal/config/config.go index d9493a9..80bfd53 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,16 +2,24 @@ package config import ( "fmt" + "github.com/spf13/viper" "os" "path/filepath" "strings" - - "github.com/spf13/viper" + "time" ) type Config struct { Github Github `mapstructure:"github"` Shortcuts Shortcuts `mapstructure:"keys"` + Settings Settings `mapstructure:"settings"` +} + +type Settings struct { + LiveMode struct { + Enabled bool `mapstructure:"enabled"` + Interval time.Duration `mapstructure:"interval"` + } `mapstructure:"live_mode"` } type Github struct { @@ -24,6 +32,7 @@ type Shortcuts struct { Quit string `mapstructure:"quit"` Refresh string `mapstructure:"refresh"` Enter string `mapstructure:"enter"` + LiveMode string `mapstructure:"live_mode"` Tab string `mapstructure:"tab"` } @@ -31,6 +40,7 @@ func LoadConfig() (*Config, error) { var config = new(Config) defer func() { config = fillDefaultShortcuts(config) + config = fillDefaultSettings(config) }() setConfig() diff --git a/internal/config/settings.go b/internal/config/settings.go new file mode 100644 index 0000000..9a2f8a2 --- /dev/null +++ b/internal/config/settings.go @@ -0,0 +1,11 @@ +package config + +import "time" + +func fillDefaultSettings(cfg *Config) *Config { + if cfg.Settings.LiveMode.Interval == time.Duration(0) { + cfg.Settings.LiveMode.Interval = 15 * time.Second + } + + return cfg +} diff --git a/internal/config/shortcuts.go b/internal/config/shortcuts.go index 123c728..17286ff 100644 --- a/internal/config/shortcuts.go +++ b/internal/config/shortcuts.go @@ -25,11 +25,16 @@ func fillDefaultShortcuts(cfg *Config) *Config { if tab == "" { tab = defaultKeyMap.Tab } + var liveMode = cfg.Shortcuts.LiveMode + if liveMode == "" { + liveMode = defaultKeyMap.LiveMode + } cfg.Shortcuts = Shortcuts{ SwitchTabRight: switchTabRight, SwitchTabLeft: switchTabLeft, Quit: quit, Refresh: refresh, + LiveMode: liveMode, Enter: enter, Tab: tab, } @@ -44,6 +49,7 @@ type defaultMap struct { Refresh string Enter string Tab string + LiveMode string } var defaultKeyMap = defaultMap{ @@ -53,4 +59,5 @@ var defaultKeyMap = defaultMap{ Refresh: "ctrl+r", Enter: "enter", Tab: "tab", + LiveMode: "ctrl+l", } diff --git a/internal/github/repository/repository.go b/internal/github/repository/repository.go index 646c5c2..454bd32 100644 --- a/internal/github/repository/repository.go +++ b/internal/github/repository/repository.go @@ -261,20 +261,6 @@ func (r *Repo) InspectWorkflowContent(ctx context.Context, repository string, br return decodedContent, nil } -//func (r *Repo) GetWorkflowRun(ctx context.Context, repository string, runID int64) (GithubWorkflowRun, error) { -// // Get a workflow run for the given repository and runID -// var workflowRun GithubWorkflowRun -// err := r.do(ctx, nil, &workflowRun, requestOptions{ -// method: http.MethodGet, -// path: []string{"repos",repository,"actions","runs",strconv.FormatInt(runID, 10)}, -// }) -// if err != nil { -// return GithubWorkflowRun{}, err -// } -// -// return workflowRun, nil -//} - func (r *Repo) getWorkflowFile(ctx context.Context, repository string, path string) (string, error) { // Get the content of the workflow file var githubFile githubFile diff --git a/internal/github/usecase/usecase.go b/internal/github/usecase/usecase.go index f2486fb..2c78943 100644 --- a/internal/github/usecase/usecase.go +++ b/internal/github/usecase/usecase.go @@ -213,21 +213,24 @@ func (u useCase) timeToString(t time.Time) string { } func (u useCase) getDuration(startTime time.Time, endTime time.Time, status string) string { - if status != "completed" { - return "running" - } - // Convert UTC times to local timezone localStartTime := startTime.In(time.Local) localEndTime := endTime.In(time.Local) - diff := localEndTime.Sub(localStartTime) + var diff time.Duration + + if status != "completed" { + diff = time.Since(localStartTime) + } else { + diff = localEndTime.Sub(localStartTime) + } - if diff.Seconds() < 60 { + switch { + case diff.Seconds() < 60: return fmt.Sprintf("%ds", int(diff.Seconds())) - } else if diff.Seconds() < 3600 { + case diff.Seconds() < 3600: return fmt.Sprintf("%dm %ds", int(diff.Minutes()), int(diff.Seconds())%60) - } else { + default: return fmt.Sprintf("%dh %dm %ds", int(diff.Hours()), int(diff.Minutes())%60, int(diff.Seconds())%60) } } diff --git a/internal/terminal/handler/ghinformation.go b/internal/terminal/handler/ghinformation.go new file mode 100644 index 0000000..8a99149 --- /dev/null +++ b/internal/terminal/handler/ghinformation.go @@ -0,0 +1,151 @@ +package handler + +import ( + "context" + "fmt" + "github.com/charmbracelet/bubbles/help" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + gu "github.com/termkit/gama/internal/github/usecase" + "github.com/termkit/gama/internal/terminal/handler/status" + ts "github.com/termkit/gama/internal/terminal/handler/types" + pkgversion "github.com/termkit/gama/pkg/version" + "github.com/termkit/skeleton" + "strings" +) + +type ModelInfo struct { + skeleton *skeleton.Skeleton + + logo string + releaseURL string + newVersionAvailableMsg string + applicationDescription string + + version pkgversion.Version + + // use cases + github gu.UseCase + + // models + help help.Model + status *status.ModelStatus + + // keymap + keys githubInformationKeyMap + + updateSelfChan chan selfUpdateMsg +} + +func SetupModelInfo(skeleton *skeleton.Skeleton, githubUseCase gu.UseCase, version pkgversion.Version) *ModelInfo { + modelStatus := status.SetupModelStatus(skeleton) + + const ( + releaseURL = "https://github.com/termkit/gama/releases" + + applicationName = ` + ..|'''.| | '|| ||' | +.|' ' ||| ||| ||| ||| +|| .... | || |'|..'|| | || +'|. || .''''|. | '|' || .''''|. +''|...'| .|. .||. .|. | .||. .|. .||. +` + ) + + return &ModelInfo{ + skeleton: skeleton, + releaseURL: releaseURL, + logo: applicationName, + github: githubUseCase, + version: version, + help: help.New(), + keys: githubInformationKeys, + status: &modelStatus, + } +} + +func (m *ModelInfo) selfUpdate() { + m.updateSelfChan <- selfUpdateMsg{} +} + +func (m *ModelInfo) selfListen() tea.Cmd { + return func() tea.Msg { + return <-m.updateSelfChan + } +} + +func (m *ModelInfo) Init() tea.Cmd { + m.applicationDescription = fmt.Sprintf("Github Actions Manager (%s)", m.version.CurrentVersion()) + + go m.checkUpdates(context.Background()) + go m.testConnection(context.Background()) + + return tea.Batch(tea.EnterAltScreen, tea.SetWindowTitle("GitHub Actions Manager (GAMA)"), m.selfListen()) +} + +func (m *ModelInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case selfUpdateMsg: + _ = msg + cmds = append(cmds, m.selfListen()) + } + + return m, tea.Batch(cmds...) +} + +func (m *ModelInfo) View() string { + infoDoc := strings.Builder{} + + helpWindowStyle := ts.WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4) + + requiredNewLinesForCenter := m.skeleton.GetTerminalHeight()/2 - 11 + if requiredNewLinesForCenter < 0 { + requiredNewLinesForCenter = 0 + } + + infoDoc.WriteString(strings.Repeat("\n", requiredNewLinesForCenter)) + infoDoc.WriteString(lipgloss.JoinVertical(lipgloss.Center, m.logo, m.applicationDescription, m.newVersionAvailableMsg)) + + requiredNewlinesForPadding := m.skeleton.GetTerminalHeight() - lipgloss.Height(infoDoc.String()) - 12 + + infoDoc.WriteString(strings.Repeat("\n", max(0, requiredNewlinesForPadding))) + + return lipgloss.JoinVertical(lipgloss.Center, infoDoc.String(), m.status.View(), helpWindowStyle.Render(m.ViewHelp())) +} + +func (m *ModelInfo) checkUpdates(ctx context.Context) { + defer m.selfUpdate() + + isUpdateAvailable, version, err := m.version.IsUpdateAvailable(ctx) + if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("failed to check updates") + m.newVersionAvailableMsg = fmt.Sprintf("failed to check updates.\nPlease visit: %s", m.releaseURL) + return + } + + if isUpdateAvailable { + m.newVersionAvailableMsg = fmt.Sprintf("New version available: %s\nPlease visit: %s", version, m.releaseURL) + } +} + +func (m *ModelInfo) testConnection(ctx context.Context) { + defer m.selfUpdate() + + m.status.SetProgressMessage("Checking your token...") + m.skeleton.LockTabs() + + _, err := m.github.GetAuthUser(ctx) + if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("failed to test connection, please check your token&permission") + m.skeleton.LockTabs() + return + } + + m.status.Reset() + m.status.SetSuccessMessage("Welcome to GAMA!") + m.skeleton.UnlockTabs() +} diff --git a/internal/terminal/handler/ghrepository/ghrepository.go b/internal/terminal/handler/ghrepository.go similarity index 68% rename from internal/terminal/handler/ghrepository/ghrepository.go rename to internal/terminal/handler/ghrepository.go index 75892ea..bd569d2 100644 --- a/internal/terminal/handler/ghrepository/ghrepository.go +++ b/internal/terminal/handler/ghrepository.go @@ -1,60 +1,56 @@ -package ghrepository +package handler import ( "context" "errors" "fmt" - "strconv" - "strings" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/termkit/gama/internal/github/domain" gu "github.com/termkit/gama/internal/github/usecase" - hdlerror "github.com/termkit/gama/internal/terminal/handler/error" + "github.com/termkit/gama/internal/terminal/handler/status" "github.com/termkit/gama/internal/terminal/handler/taboptions" hdltypes "github.com/termkit/gama/internal/terminal/handler/types" "github.com/termkit/gama/pkg/browser" + "github.com/termkit/skeleton" + "strconv" + "strings" ) type ModelGithubRepository struct { + skeleton *skeleton.Skeleton // current handler's properties syncRepositoriesContext context.Context cancelSyncRepositories context.CancelFunc tableReady bool // shared properties - SelectedRepository *hdltypes.SelectedRepository + selectedRepository *hdltypes.SelectedRepository // use cases github gu.UseCase // keymap - Keys keyMap + Keys githubRepositoryKeyMap // models - Help help.Model - Viewport *viewport.Model + help help.Model tableGithubRepository table.Model searchTableGithubRepository table.Model - modelError *hdlerror.ModelError + status *status.ModelStatus - modelTabOptions tea.Model - actualModelTabOptions *taboptions.Options + modelTabOptions *taboptions.Options textInput textinput.Model -} -var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) + updateSelfChan chan selfUpdateMsg +} -func SetupModelGithubRepository(githubUseCase gu.UseCase, selectedRepository *hdltypes.SelectedRepository) *ModelGithubRepository { +func SetupModelGithubRepository(skeleton *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubRepository { var tableRowsGithubRepository []table.Row tableGithubRepository := table.New( @@ -110,109 +106,52 @@ func SetupModelGithubRepository(githubUseCase gu.UseCase, selectedRepository *hd ti.ShowSuggestions = false // disable suggestions, it will be enabled future. // setup models - modelError := hdlerror.SetupModelError() - tabOptions := taboptions.NewOptions(&modelError) + modelStatus := status.SetupModelStatus(skeleton) + tabOptions := taboptions.NewOptions(&modelStatus) return &ModelGithubRepository{ - Help: help.New(), - Keys: keys, + skeleton: skeleton, + help: help.New(), + Keys: githubRepositoryKeys, github: githubUseCase, tableGithubRepository: tableGithubRepository, - modelError: &modelError, - SelectedRepository: selectedRepository, + status: &modelStatus, + selectedRepository: hdltypes.NewSelectedRepository(), modelTabOptions: tabOptions, - actualModelTabOptions: tabOptions, textInput: ti, syncRepositoriesContext: context.Background(), cancelSyncRepositories: func() {}, + updateSelfChan: make(chan selfUpdateMsg), } } -func (m *ModelGithubRepository) Init() tea.Cmd { - go m.syncRepositories(m.syncRepositoriesContext) - - openInBrowser := func() { - m.modelError.SetProgressMessage("Opening in browser...") - - err := browser.OpenInBrowser(fmt.Sprintf("https://github.com/%s", m.SelectedRepository.RepositoryName)) - if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage(fmt.Sprintf("Cannot open in browser: %v", err)) - return - } - - m.modelError.SetSuccessMessage("Opened in browser") - } - - m.actualModelTabOptions.AddOption("Open in browser", openInBrowser) - - return nil +func (m *ModelGithubRepository) selfUpdate() { + m.updateSelfChan <- selfUpdateMsg{} } -func (m *ModelGithubRepository) syncRepositories(ctx context.Context) { - m.modelError.ResetError() // reset previous errors - m.actualModelTabOptions.SetStatus(taboptions.OptionWait) - m.modelError.SetProgressMessage("Fetching repositories...") - - // delete all rows - m.tableGithubRepository.SetRows([]table.Row{}) - m.searchTableGithubRepository.SetRows([]table.Row{}) - - repositories, err := m.github.ListRepositories(ctx, gu.ListRepositoriesInput{ - Limit: 100, // limit to 100 repositories - Page: 5, // page 1 to page 5, at summary we fetch 500 repositories - Sort: domain.SortByUpdated, - }) - if errors.Is(err, context.Canceled) { - return - } else if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Repositories cannot be listed") - return +func (m *ModelGithubRepository) selfListen() tea.Cmd { + return func() tea.Msg { + return <-m.updateSelfChan } - - if len(repositories.Repositories) == 0 { - m.actualModelTabOptions.SetStatus(taboptions.OptionNone) - m.modelError.SetDefaultMessage("No repositories found") - m.textInput.Blur() - return - } - - tableRowsGithubRepository := make([]table.Row, 0, len(repositories.Repositories)) - for _, repository := range repositories.Repositories { - tableRowsGithubRepository = append(tableRowsGithubRepository, - table.Row{repository.Name, repository.DefaultBranch, strconv.Itoa(repository.Stars), strconv.Itoa(len(repository.Workflows))}) - } - - m.tableGithubRepository.SetRows(tableRowsGithubRepository) - m.searchTableGithubRepository.SetRows(tableRowsGithubRepository) - - // set cursor to 0 - m.tableGithubRepository.SetCursor(0) - m.searchTableGithubRepository.SetCursor(0) - - m.tableReady = true - //m.updateSearchBarSuggestions() - m.textInput.Focus() - m.modelError.SetSuccessMessage("Repositories fetched") - go m.Update(m) // update model } -func (m *ModelGithubRepository) handleTableInputs(_ context.Context) { - if !m.tableReady { - return - } +func (m *ModelGithubRepository) Init() tea.Cmd { + openInBrowser := func() { + m.status.SetProgressMessage("Opening in browser...") - // To avoid go routine leak - selectedRow := m.tableGithubRepository.SelectedRow() + err := browser.OpenInBrowser(fmt.Sprintf("https://github.com/%s", m.selectedRepository.RepositoryName)) + if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage(fmt.Sprintf("Cannot open in browser: %v", err)) + return + } - // Synchronize selected repository name with parent model - if len(selectedRow) > 0 && selectedRow[0] != "" { - m.SelectedRepository.RepositoryName = selectedRow[0] - m.SelectedRepository.BranchName = selectedRow[1] + m.status.SetSuccessMessage("Opened in browser") } - m.actualModelTabOptions.SetStatus(taboptions.OptionIdle) + m.modelTabOptions.AddOption("Open in browser", openInBrowser) + go m.syncRepositories(m.syncRepositoriesContext) + return tea.Batch(m.modelTabOptions.Init(), m.SelfUpdater()) } func (m *ModelGithubRepository) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -236,6 +175,8 @@ func (m *ModelGithubRepository) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.searchTableGithubRepository.GotoTop() m.searchTableGithubRepository.SetCursor(0) } + case selfUpdateMsg: + cmds = append(cmds, m.SelfUpdater()) } m.textInput, cmd = m.textInput.Update(textInputMsg) @@ -258,8 +199,11 @@ func (m *ModelGithubRepository) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *ModelGithubRepository) View() string { - termWidth := m.Viewport.Width - termHeight := m.Viewport.Height + var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")).MarginLeft(1) + + helpWindowStyle := hdltypes.WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4) var tableWidth int for _, t := range tableColumnsGithubRepository { @@ -267,47 +211,105 @@ func (m *ModelGithubRepository) View() string { } newTableColumns := tableColumnsGithubRepository - widthDiff := termWidth - tableWidth + widthDiff := m.skeleton.GetTerminalWidth() - tableWidth if widthDiff > 0 { - newTableColumns[0].Width += widthDiff - 15 + newTableColumns[0].Width += widthDiff - 14 m.tableGithubRepository.SetColumns(newTableColumns) - m.tableGithubRepository.SetHeight(termHeight - 20) + m.tableGithubRepository.SetHeight(m.skeleton.GetTerminalHeight() - 20) } - doc := strings.Builder{} - doc.WriteString(baseStyle.Render(m.tableGithubRepository.View())) + return lipgloss.JoinVertical(lipgloss.Top, + baseStyle.Render(m.tableGithubRepository.View()), m.viewSearchBar(), + m.modelTabOptions.View(), m.status.View(), helpWindowStyle.Render(m.ViewHelp())) +} - return lipgloss.JoinVertical(lipgloss.Top, doc.String(), m.viewSearchBar(), m.actualModelTabOptions.View()) +func (m *ModelGithubRepository) SelfUpdater() tea.Cmd { + return func() tea.Msg { + return <-m.updateSelfChan + } } -func (m *ModelGithubRepository) viewSearchBar() string { - // Define window style - windowStyle := lipgloss.NewStyle(). - Border(lipgloss.NormalBorder()). - Padding(0, 1). - Width(*hdltypes.ScreenWidth - 2) +func (m *ModelGithubRepository) syncRepositories(ctx context.Context) { + defer m.selfUpdate() - // Build the options list - doc := strings.Builder{} + m.status.Reset() // reset previous errors + m.modelTabOptions.SetStatus(taboptions.OptionWait) + m.status.SetProgressMessage("Fetching repositories...") - if len(m.textInput.Value()) > 0 { - windowStyle = windowStyle.BorderForeground(lipgloss.Color("39")) + // delete all rows + m.tableGithubRepository.SetRows([]table.Row{}) + m.searchTableGithubRepository.SetRows([]table.Row{}) + + repositories, err := m.github.ListRepositories(ctx, gu.ListRepositoriesInput{ + Limit: 100, // limit to 100 repositories + Page: 5, // page 1 to page 5, at summary we fetch 500 repositories + Sort: domain.SortByUpdated, + }) + if errors.Is(err, context.Canceled) { + return + } else if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("Repositories cannot be listed") + return + } + + if len(repositories.Repositories) == 0 { + m.modelTabOptions.SetStatus(taboptions.OptionNone) + m.status.SetDefaultMessage("No repositories found") + m.textInput.Blur() + return + } + + m.skeleton.UpdateWidgetValue("repositories", fmt.Sprintf("Repository Count: %d", len(repositories.Repositories))) + + tableRowsGithubRepository := make([]table.Row, 0, len(repositories.Repositories)) + for _, repository := range repositories.Repositories { + tableRowsGithubRepository = append(tableRowsGithubRepository, + table.Row{repository.Name, repository.DefaultBranch, strconv.Itoa(repository.Stars), strconv.Itoa(len(repository.Workflows))}) + } + + m.tableGithubRepository.SetRows(tableRowsGithubRepository) + m.searchTableGithubRepository.SetRows(tableRowsGithubRepository) + + // set cursor to 0 + m.tableGithubRepository.SetCursor(0) + m.searchTableGithubRepository.SetCursor(0) + + m.tableReady = true + m.textInput.Focus() + m.status.SetSuccessMessage("Repositories fetched") +} + +func (m *ModelGithubRepository) handleTableInputs(_ context.Context) { + if !m.tableReady { + return } - doc.WriteString(m.textInput.View()) + // To avoid go routine leak + selectedRow := m.tableGithubRepository.SelectedRow() - return windowStyle.Render(doc.String()) + // Synchronize selected repository name with parent model + if len(selectedRow) > 0 && selectedRow[0] != "" { + m.selectedRepository.RepositoryName = selectedRow[0] + m.selectedRepository.BranchName = selectedRow[1] + } + + m.modelTabOptions.SetStatus(taboptions.OptionIdle) } -func (m *ModelGithubRepository) updateSearchBarSuggestions() { - m.textInput.SetSuggestions([]string{}) +func (m *ModelGithubRepository) viewSearchBar() string { + // Define window style + windowStyle := lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")). + Padding(0, 1). + Width(m.skeleton.GetTerminalWidth() - 6).MarginLeft(1) - var suggestions = make([]string, 0, len(m.tableGithubRepository.Rows())) - for _, r := range m.tableGithubRepository.Rows() { - suggestions = append(suggestions, r[0]) + if len(m.textInput.Value()) > 0 { + windowStyle = windowStyle.BorderForeground(lipgloss.Color("39")) } - m.textInput.SetSuggestions(suggestions) + return windowStyle.Render(m.textInput.View()) } func (m *ModelGithubRepository) updateTableRowsBySearchBar() { @@ -320,9 +322,9 @@ func (m *ModelGithubRepository) updateTableRowsBySearchBar() { } if len(tableRowsGithubRepository) == 0 { - m.SelectedRepository.RepositoryName = "" - m.SelectedRepository.BranchName = "" - m.SelectedRepository.WorkflowName = "" + m.selectedRepository.RepositoryName = "" + m.selectedRepository.BranchName = "" + m.selectedRepository.WorkflowName = "" } m.tableGithubRepository.SetRows(tableRowsGithubRepository) @@ -346,7 +348,3 @@ func (m *ModelGithubRepository) isCharAndSymbol(r []rune) bool { return false } - -func (m *ModelGithubRepository) ViewStatus() string { - return m.modelError.View() -} diff --git a/internal/terminal/handler/ghrepository/keymap.go b/internal/terminal/handler/ghrepository/keymap.go deleted file mode 100644 index c6fecc7..0000000 --- a/internal/terminal/handler/ghrepository/keymap.go +++ /dev/null @@ -1,55 +0,0 @@ -package ghrepository - -import ( - "fmt" - - "github.com/termkit/gama/internal/config" - - teakey "github.com/charmbracelet/bubbles/key" -) - -type keyMap struct { - Refresh teakey.Binding - LaunchTab teakey.Binding - SwitchTab teakey.Binding -} - -func (k keyMap) ShortHelp() []teakey.Binding { - return []teakey.Binding{k.SwitchTab, k.Refresh, k.LaunchTab} -} - -func (k keyMap) FullHelp() [][]teakey.Binding { - return [][]teakey.Binding{ - {k.SwitchTab}, - {k.Refresh}, - {k.LaunchTab}, - } -} - -var keys = func() keyMap { - cfg, err := config.LoadConfig() - if err != nil { - panic(fmt.Sprintf("failed to load config: %v", err)) - } - - var tabSwitch = fmt.Sprintf("%s | %s", cfg.Shortcuts.SwitchTabLeft, cfg.Shortcuts.SwitchTabRight) - - return keyMap{ - Refresh: teakey.NewBinding( - teakey.WithKeys(cfg.Shortcuts.Refresh), - teakey.WithHelp(cfg.Shortcuts.Refresh, "Refresh list"), - ), - LaunchTab: teakey.NewBinding( - teakey.WithKeys(cfg.Shortcuts.Enter), - teakey.WithHelp(cfg.Shortcuts.Enter, "Launch the selected option"), - ), - SwitchTab: teakey.NewBinding( - teakey.WithKeys(""), // help-only binding - teakey.WithHelp(tabSwitch, "switch tab"), - ), - } -}() - -func (m *ModelGithubRepository) ViewHelp() string { - return m.Help.View(m.Keys) -} diff --git a/internal/terminal/handler/ghrepository/table.go b/internal/terminal/handler/ghrepository/table.go deleted file mode 100644 index 256735e..0000000 --- a/internal/terminal/handler/ghrepository/table.go +++ /dev/null @@ -1,12 +0,0 @@ -package ghrepository - -import ( - "github.com/charmbracelet/bubbles/table" -) - -var tableColumnsGithubRepository = []table.Column{ - {Title: "Repository", Width: 24}, - {Title: "Default Branch", Width: 16}, - {Title: "Stars", Width: 6}, - {Title: "Workflows", Width: 9}, -} diff --git a/internal/terminal/handler/ghtrigger/ghtrigger.go b/internal/terminal/handler/ghtrigger.go similarity index 72% rename from internal/terminal/handler/ghtrigger/ghtrigger.go rename to internal/terminal/handler/ghtrigger.go index 290b654..771c2e7 100644 --- a/internal/terminal/handler/ghtrigger/ghtrigger.go +++ b/internal/terminal/handler/ghtrigger.go @@ -1,60 +1,60 @@ -package ghtrigger +package handler import ( "context" "errors" "fmt" - "slices" - "strings" - "time" - "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" gu "github.com/termkit/gama/internal/github/usecase" - hdlerror "github.com/termkit/gama/internal/terminal/handler/error" + "github.com/termkit/gama/internal/terminal/handler/status" hdltypes "github.com/termkit/gama/internal/terminal/handler/types" "github.com/termkit/gama/pkg/workflow" + "github.com/termkit/skeleton" + "slices" + "strings" + "time" ) type ModelGithubTrigger struct { + skeleton *skeleton.Skeleton + // current handler's properties - syncWorkflowContext context.Context - cancelSyncWorkflow context.CancelFunc - workflowContent *workflow.Pretty - tableReady bool - isTriggerable bool - currentTab *int - forceUpdateWorkflowHistory *bool - optionInit bool - optionCursor int - optionValues []string - currentOption string - selectedWorkflow string - selectedRepositoryName string - triggerFocused bool + syncWorkflowContext context.Context + cancelSyncWorkflow context.CancelFunc + workflowContent *workflow.Pretty + tableReady bool + isTriggerable bool + optionInit bool + optionCursor int + optionValues []string + currentOption string + selectedWorkflow string + selectedRepositoryName string + triggerFocused bool // shared properties - SelectedRepository *hdltypes.SelectedRepository + selectedRepository *hdltypes.SelectedRepository // use cases github gu.UseCase // keymap - Keys keyMap + Keys githubTriggerKeyMap // models - Help help.Model - Viewport *viewport.Model - modelError hdlerror.ModelError + help help.Model + status *status.ModelStatus textInput textinput.Model tableTrigger table.Model + + updateSelfChan chan any } -func SetupModelGithubTrigger(githubUseCase gu.UseCase, selectedRepository *hdltypes.SelectedRepository, currentTab *int, forceUpdateWorkflowHistory *bool) *ModelGithubTrigger { +func SetupModelGithubTrigger(skeleton *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubTrigger { var tableRowsTrigger []table.Row tableTrigger := table.New( @@ -80,41 +80,53 @@ func SetupModelGithubTrigger(githubUseCase gu.UseCase, selectedRepository *hdlty ti.Blur() ti.CharLimit = 72 + modelStatus := status.SetupModelStatus(skeleton) return &ModelGithubTrigger{ - currentTab: currentTab, - forceUpdateWorkflowHistory: forceUpdateWorkflowHistory, - Help: help.New(), - Keys: keys, - github: githubUseCase, - SelectedRepository: selectedRepository, - modelError: hdlerror.SetupModelError(), - tableTrigger: tableTrigger, - textInput: ti, - syncWorkflowContext: context.Background(), - cancelSyncWorkflow: func() {}, + skeleton: skeleton, + help: help.New(), + Keys: githubTriggerKeys, + github: githubUseCase, + selectedRepository: hdltypes.NewSelectedRepository(), + status: &modelStatus, + tableTrigger: tableTrigger, + textInput: ti, + syncWorkflowContext: context.Background(), + cancelSyncWorkflow: func() {}, + updateSelfChan: make(chan any), + } +} + +func (m *ModelGithubTrigger) selfUpdate() { + m.updateSelfChan <- selfUpdateMsg{} +} + +func (m *ModelGithubTrigger) selfListen() tea.Cmd { + return func() tea.Msg { + return <-m.updateSelfChan } } func (m *ModelGithubTrigger) Init() tea.Cmd { - m.modelError.SetDefaultMessage("No workflow contents found.") - return textinput.Blink + return tea.Batch(textinput.Blink, m.selfListen()) } func (m *ModelGithubTrigger) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if m.SelectedRepository.WorkflowName == "" { - m.modelError.Reset() - m.modelError.SetDefaultMessage("No workflow selected.") + if m.selectedRepository.WorkflowName == "" { + m.status.Reset() + m.status.SetDefaultMessage("No workflow selected.") + m.fillTableWithEmptyMessage() return m, nil } - if m.SelectedRepository.WorkflowName != "" && (m.SelectedRepository.WorkflowName != m.selectedWorkflow || m.SelectedRepository.RepositoryName != m.selectedRepositoryName) { + + if m.selectedRepository.WorkflowName != "" && (m.selectedRepository.WorkflowName != m.selectedWorkflow || m.selectedRepository.RepositoryName != m.selectedRepositoryName) { m.tableReady = false m.isTriggerable = false m.triggerFocused = false m.cancelSyncWorkflow() // cancel previous sync workflow - m.selectedWorkflow = m.SelectedRepository.WorkflowName - m.selectedRepositoryName = m.SelectedRepository.RepositoryName + m.selectedWorkflow = m.selectedRepository.WorkflowName + m.selectedRepositoryName = m.selectedRepository.RepositoryName m.syncWorkflowContext, m.cancelSyncWorkflow = context.WithCancel(context.Background()) go m.syncWorkflowContent(m.syncWorkflowContext) @@ -166,11 +178,13 @@ func (m *ModelGithubTrigger) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.textInput.Focus() } } - case "enter": + case "enter", tea.KeyEnter.String(): if m.triggerFocused && m.isTriggerable { go m.triggerWorkflow() } } + case selfUpdateMsg: + cmds = append(cmds, m.selfListen()) } m.tableTrigger, cmd = m.tableTrigger.Update(msg) @@ -184,6 +198,50 @@ func (m *ModelGithubTrigger) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } +func (m *ModelGithubTrigger) View() string { + baseStyle := lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).MarginLeft(1) + helpWindowStyle := hdltypes.WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4) + + if m.triggerFocused { + baseStyle = baseStyle.BorderForeground(lipgloss.Color("240")) + } else { + baseStyle = baseStyle.BorderForeground(lipgloss.Color("#3b698f")) + } + + var tableWidth int + for _, t := range tableColumnsTrigger { + tableWidth += t.Width + } + + newTableColumns := tableColumnsTrigger + widthDiff := m.skeleton.GetTerminalWidth() - tableWidth + if widthDiff > 0 { + keyWidth := &newTableColumns[2].Width + valueWidth := &newTableColumns[4].Width + + *valueWidth += widthDiff - 16 + if *valueWidth%2 == 0 { + *keyWidth = *valueWidth / 2 + } + m.tableTrigger.SetColumns(newTableColumns) + m.tableTrigger.SetHeight(m.skeleton.GetTerminalHeight() - 17) + } + + var selectedRow = m.tableTrigger.SelectedRow() + var selector = m.emptySelector() + if len(m.tableTrigger.Rows()) > 0 { + if selectedRow[1] == "input" { + selector = m.inputSelector() + } else { + selector = m.optionSelector() + } + } + + return lipgloss.JoinVertical(lipgloss.Top, + baseStyle.Render(m.tableTrigger.View()), lipgloss.JoinHorizontal(lipgloss.Top, selector, m.triggerButton()), + m.status.View(), helpWindowStyle.Render(m.ViewHelp())) +} + func (m *ModelGithubTrigger) switchBetweenInputAndTable() { var selectedRow = m.tableTrigger.SelectedRow() @@ -336,75 +394,33 @@ func (m *ModelGithubTrigger) inputController(_ context.Context) { } } -func (m *ModelGithubTrigger) View() string { - baseStyle := lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - - termWidth := m.Viewport.Width - termHeight := m.Viewport.Height - - var tableWidth int - for _, t := range tableColumnsTrigger { - tableWidth += t.Width - } - - newTableColumns := tableColumnsTrigger - widthDiff := termWidth - tableWidth - if widthDiff > 0 { - keyWidth := &newTableColumns[2].Width - valueWidth := &newTableColumns[4].Width - - *valueWidth += widthDiff - 17 - if *valueWidth%2 == 0 { - *keyWidth = *valueWidth / 2 - } - m.tableTrigger.SetColumns(newTableColumns) - m.tableTrigger.SetHeight(termHeight - 17) - } - - doc := strings.Builder{} - doc.WriteString(baseStyle.Render(m.tableTrigger.View())) - - var selectedRow = m.tableTrigger.SelectedRow() - var selector = m.emptySelector() - if len(m.tableTrigger.Rows()) > 0 { - if selectedRow[1] == "input" { - selector = m.inputSelector() - } else { - selector = m.optionSelector() - } - } - - return lipgloss.JoinVertical(lipgloss.Top, doc.String(), - lipgloss.JoinHorizontal(lipgloss.Top, selector, m.triggerButton())) -} - func (m *ModelGithubTrigger) syncWorkflowContent(ctx context.Context) { - m.modelError.Reset() - m.modelError.SetProgressMessage( + defer m.selfUpdate() + + m.status.Reset() + m.status.SetProgressMessage( fmt.Sprintf("[%s@%s] Fetching workflow contents...", - m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) + m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) // reset table rows m.tableTrigger.SetRows([]table.Row{}) workflowContent, err := m.github.InspectWorkflow(ctx, gu.InspectWorkflowInput{ - Repository: m.SelectedRepository.RepositoryName, - Branch: m.SelectedRepository.BranchName, + Repository: m.selectedRepository.RepositoryName, + Branch: m.selectedRepository.BranchName, WorkflowFile: m.selectedWorkflow, }) if errors.Is(err, context.Canceled) { return } else if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Workflow contents cannot be fetched") + m.status.SetError(err) + m.status.SetErrorMessage("Workflow contents cannot be fetched") return } if workflowContent.Workflow == nil { - m.modelError.SetError(errors.New("workflow contents cannot be empty")) - m.modelError.SetErrorMessage("You have no workflow contents") + m.status.SetError(errors.New("workflow contents cannot be empty")) + m.status.SetErrorMessage("You have no workflow contents") return } @@ -471,14 +487,12 @@ func (m *ModelGithubTrigger) syncWorkflowContent(ctx context.Context) { len(workflowContent.Workflow.Choices) == 0 && len(workflowContent.Workflow.Inputs) == 0 { m.fillTableWithEmptyMessage() - m.modelError.SetDefaultMessage(fmt.Sprintf("[%s@%s] Workflow doesn't contain options but still triggerable", - m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) + m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] Workflow doesn't contain options but still triggerable", + m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) } else { - m.modelError.SetSuccessMessage(fmt.Sprintf("[%s@%s] Workflow contents fetched.", - m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) + m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s] Workflow contents fetched.", + m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) } - - go m.Update(m) // update model } func (m *ModelGithubTrigger) fillTableWithEmptyMessage() { @@ -497,7 +511,7 @@ func (m *ModelGithubTrigger) fillTableWithEmptyMessage() { func (m *ModelGithubTrigger) showInformationIfAnyEmptyValue() { for _, row := range m.tableTrigger.Rows() { if row[4] == "" { - m.modelError.SetDefaultMessage("Info: You have empty values. These values uses their default values.") + m.status.SetDefaultMessage("Info: You have empty values. These values uses their default values.") return } } @@ -511,8 +525,8 @@ func (m *ModelGithubTrigger) triggerButton() string { Align(lipgloss.Center) if m.triggerFocused { - button = button.BorderForeground(lipgloss.Color("130")). - Foreground(lipgloss.Color("130")). + button = button.BorderForeground(lipgloss.Color("#399adb")). + Foreground(lipgloss.Color("#399adb")). BorderStyle(lipgloss.DoubleBorder()) } @@ -521,8 +535,8 @@ func (m *ModelGithubTrigger) triggerButton() string { func (m *ModelGithubTrigger) fillEmptyValuesWithDefault() { if m.workflowContent == nil { - m.modelError.SetError(errors.New("workflow contents cannot be empty")) - m.modelError.SetErrorMessage("You have no workflow contents") + m.status.SetError(errors.New("workflow contents cannot be empty")) + m.status.SetErrorMessage("You have no workflow contents") return } @@ -565,40 +579,41 @@ func (m *ModelGithubTrigger) triggerWorkflow() { m.fillEmptyValuesWithDefault() } - m.modelError.SetProgressMessage( + m.status.SetProgressMessage( fmt.Sprintf("[%s@%s]:[%s] Triggering workflow...", - m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName, m.selectedWorkflow)) + m.selectedRepository.RepositoryName, m.selectedRepository.BranchName, m.selectedWorkflow)) if m.workflowContent == nil { - m.modelError.SetErrorMessage("Workflow contents cannot be empty") + m.status.SetErrorMessage("Workflow contents cannot be empty") return } content, err := m.workflowContent.ToJson() if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Workflow contents cannot be converted to JSON") + m.status.SetError(err) + m.status.SetErrorMessage("Workflow contents cannot be converted to JSON") return } _, err = m.github.TriggerWorkflow(context.Background(), gu.TriggerWorkflowInput{ - Repository: m.SelectedRepository.RepositoryName, - Branch: m.SelectedRepository.BranchName, + Repository: m.selectedRepository.RepositoryName, + Branch: m.selectedRepository.BranchName, WorkflowFile: m.selectedWorkflow, Content: content, }) if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Workflow cannot be triggered") + m.status.SetError(err) + m.status.SetErrorMessage("Workflow cannot be triggered") return } - m.modelError.SetSuccessMessage(fmt.Sprintf("[%s@%s]:[%s] Workflow triggered.", - m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName, m.selectedWorkflow)) + m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s]:[%s] Workflow triggered.", + m.selectedRepository.RepositoryName, m.selectedRepository.BranchName, m.selectedWorkflow)) time.Sleep(1 * time.Second) - m.modelError.SetProgressMessage("Switching to workflow history tab...") - time.Sleep(1 * time.Second) + m.selfUpdate() + m.status.SetProgressMessage("Switching to workflow history tab...") + time.Sleep(1500 * time.Millisecond) // move these operations under new function named "resetTabSettings" m.workflowContent = nil // reset workflow content @@ -607,39 +622,30 @@ func (m *ModelGithubTrigger) triggerWorkflow() { m.optionValues = nil // reset option values m.selectedRepositoryName = "" // reset selected repository name - go func() { - time.Sleep(1 * time.Second) - *m.forceUpdateWorkflowHistory = true // force update workflow history - }() - *m.currentTab = 2 // switch tab to workflow history + UpdateWorkflowHistory(time.Second * 3) // update workflow history + m.skeleton.SetActivePage("history") // switch tab to workflow history } func (m *ModelGithubTrigger) emptySelector() string { // Define window style windowStyle := lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")). Padding(0, 1). - Width(*hdltypes.ScreenWidth - 13) + Width(m.skeleton.GetTerminalWidth() - 18).MarginLeft(1) - // Build the options list - doc := strings.Builder{} - - return windowStyle.Render(doc.String()) + return windowStyle.Render("") } func (m *ModelGithubTrigger) inputSelector() string { // Define window style windowStyle := lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")). Padding(0, 1). - Width(*hdltypes.ScreenWidth - 13) - - // Build the options list - doc := strings.Builder{} - - doc.WriteString(m.textInput.View()) + Width(m.skeleton.GetTerminalWidth() - 18).MarginLeft(1) - return windowStyle.Render(doc.String()) + return windowStyle.Render(m.textInput.View()) } // optionSelector renders the options list @@ -648,16 +654,14 @@ func (m *ModelGithubTrigger) optionSelector() string { // Define window style windowStyle := lipgloss.NewStyle(). Border(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")). Padding(0, 1). - Width(*hdltypes.ScreenWidth - 13) + Width(m.skeleton.GetTerminalWidth() - 18).MarginLeft(1) // Define styles for selected and unselected options selectedOptionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("120")).Padding(0, 1) unselectedOptionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("140")).Padding(0, 1) - // Build the options list - doc := strings.Builder{} - var processedValues []string for i, option := range m.optionValues { if i == m.optionCursor { @@ -667,12 +671,7 @@ func (m *ModelGithubTrigger) optionSelector() string { } } - horizontal := lipgloss.JoinHorizontal(lipgloss.Left, processedValues...) - - doc.WriteString(horizontal) - - // Apply window style to the entire list - return windowStyle.Render(doc.String()) + return windowStyle.Render(lipgloss.JoinHorizontal(lipgloss.Left, processedValues...)) } func (m *ModelGithubTrigger) sortTableItemsByName() { @@ -682,7 +681,3 @@ func (m *ModelGithubTrigger) sortTableItemsByName() { }) m.tableTrigger.SetRows(rows) } - -func (m *ModelGithubTrigger) ViewStatus() string { - return m.modelError.View() -} diff --git a/internal/terminal/handler/ghtrigger/keymap.go b/internal/terminal/handler/ghtrigger/keymap.go deleted file mode 100644 index e0682f7..0000000 --- a/internal/terminal/handler/ghtrigger/keymap.go +++ /dev/null @@ -1,61 +0,0 @@ -package ghtrigger - -import ( - "fmt" - - "github.com/termkit/gama/internal/config" - - teakey "github.com/charmbracelet/bubbles/key" -) - -type keyMap struct { - SwitchTabLeft teakey.Binding - SwitchTab teakey.Binding - Trigger teakey.Binding - Refresh teakey.Binding -} - -func (k keyMap) ShortHelp() []teakey.Binding { - return []teakey.Binding{k.SwitchTabLeft, k.Refresh, k.SwitchTab, k.Trigger} -} - -func (k keyMap) FullHelp() [][]teakey.Binding { - return [][]teakey.Binding{ - {k.SwitchTabLeft}, - {k.Refresh}, - {k.SwitchTab}, - {k.Trigger}, - } -} - -var keys = func() keyMap { - cfg, err := config.LoadConfig() - if err != nil { - panic(fmt.Sprintf("failed to load config: %v", err)) - } - - previousTab := cfg.Shortcuts.SwitchTabLeft - - return keyMap{ - SwitchTabLeft: teakey.NewBinding( - teakey.WithKeys(""), // help-only binding - teakey.WithHelp(previousTab, "previous tab"), - ), - Refresh: teakey.NewBinding( - teakey.WithKeys(cfg.Shortcuts.Refresh), - teakey.WithHelp(cfg.Shortcuts.Refresh, "Refresh list"), - ), - SwitchTab: teakey.NewBinding( - teakey.WithKeys(cfg.Shortcuts.Tab), - teakey.WithHelp(cfg.Shortcuts.Tab, "switch button"), - ), - Trigger: teakey.NewBinding( - teakey.WithKeys(cfg.Shortcuts.Enter), - teakey.WithHelp(cfg.Shortcuts.Enter, "trigger workflow"), - ), - } -}() - -func (m *ModelGithubTrigger) ViewHelp() string { - return m.Help.View(m.Keys) -} diff --git a/internal/terminal/handler/ghtrigger/table.go b/internal/terminal/handler/ghtrigger/table.go deleted file mode 100644 index 538bdc3..0000000 --- a/internal/terminal/handler/ghtrigger/table.go +++ /dev/null @@ -1,14 +0,0 @@ -package ghtrigger - -import ( - "github.com/charmbracelet/bubbles/table" -) - -var tableColumnsTrigger = []table.Column{ - {Title: "ID", Width: 2}, - {Title: "Type", Width: 6}, - {Title: "Key", Width: 24}, - {Title: "Default", Width: 16}, - //{Title: "Description", Width: 64}, - {Title: "Value", Width: 44}, -} diff --git a/internal/terminal/handler/ghworkflow/ghworkflow.go b/internal/terminal/handler/ghworkflow.go similarity index 57% rename from internal/terminal/handler/ghworkflow/ghworkflow.go rename to internal/terminal/handler/ghworkflow.go index c79cf12..90007be 100644 --- a/internal/terminal/handler/ghworkflow/ghworkflow.go +++ b/internal/terminal/handler/ghworkflow.go @@ -1,27 +1,25 @@ -package ghworkflow +package handler import ( "context" "errors" "fmt" + "github.com/termkit/skeleton" "sort" "strings" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/table" - hdlerror "github.com/termkit/gama/internal/terminal/handler/error" - "github.com/termkit/gama/internal/terminal/handler/ghtrigger" - "github.com/termkit/gama/internal/terminal/handler/taboptions" + "github.com/termkit/gama/internal/terminal/handler/status" hdltypes "github.com/termkit/gama/internal/terminal/handler/types" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" gu "github.com/termkit/gama/internal/github/usecase" ) type ModelGithubWorkflow struct { + skeleton *skeleton.Skeleton // current handler's properties syncTriggerableWorkflowsContext context.Context cancelSyncTriggerableWorkflows context.CancelFunc @@ -29,33 +27,23 @@ type ModelGithubWorkflow struct { lastRepository string // shared properties - SelectedRepository *hdltypes.SelectedRepository + selectedRepository *hdltypes.SelectedRepository // use cases github gu.UseCase // keymap - Keys keyMap + keys githubWorkflowKeyMap // models - Help help.Model - Viewport *viewport.Model - list list.Model + help help.Model tableTriggerableWorkflow table.Model - modelError *hdlerror.ModelError + status *status.ModelStatus - modelTabOptions tea.Model - actualModelTabOptions *taboptions.Options - - modelGithubTrigger tea.Model - actualModelGithubTrigger *ghtrigger.ModelGithubTrigger + updateSelfChan chan any } -var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - -func SetupModelGithubWorkflow(githubUseCase gu.UseCase, selectedRepository *hdltypes.SelectedRepository) *ModelGithubWorkflow { +func SetupModelGithubWorkflow(skeleton *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubWorkflow { var tableRowsTriggerableWorkflow []table.Row tableTriggerableWorkflow := table.New( @@ -68,7 +56,7 @@ func SetupModelGithubWorkflow(githubUseCase gu.UseCase, selectedRepository *hdlt s := table.DefaultStyles() s.Header = s.Header. BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). + BorderForeground(lipgloss.Color("#3b698f")). BorderBottom(true). Bold(false) s.Selected = s.Selected. @@ -77,50 +65,73 @@ func SetupModelGithubWorkflow(githubUseCase gu.UseCase, selectedRepository *hdlt Bold(false) tableTriggerableWorkflow.SetStyles(s) - modelError := hdlerror.SetupModelError() - tabOptions := taboptions.NewOptions(&modelError) + modelStatus := status.SetupModelStatus(skeleton) return &ModelGithubWorkflow{ - Help: help.New(), - Keys: keys, + skeleton: skeleton, + help: help.New(), + keys: githubWorkflowKeys, github: githubUseCase, - modelError: &modelError, + status: &modelStatus, tableTriggerableWorkflow: tableTriggerableWorkflow, - SelectedRepository: selectedRepository, - modelTabOptions: tabOptions, - actualModelTabOptions: tabOptions, + selectedRepository: hdltypes.NewSelectedRepository(), syncTriggerableWorkflowsContext: context.Background(), cancelSyncTriggerableWorkflows: func() {}, + updateSelfChan: make(chan any), + } +} + +func (m *ModelGithubWorkflow) selfUpdate() { + m.updateSelfChan <- selfUpdateMsg{} +} + +func (m *ModelGithubWorkflow) selfListen() tea.Cmd { + return func() tea.Msg { + return <-m.updateSelfChan } } func (m *ModelGithubWorkflow) Init() tea.Cmd { - return nil + return tea.Batch(m.selfListen()) } func (m *ModelGithubWorkflow) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd var cmd tea.Cmd - if m.lastRepository != m.SelectedRepository.RepositoryName { + switch msg := msg.(type) { + case selfUpdateMsg: + _ = msg + cmds = append(cmds, m.selfListen()) + } + + if m.lastRepository != m.selectedRepository.RepositoryName { m.tableReady = false // reset table ready status m.cancelSyncTriggerableWorkflows() // cancel previous sync m.syncTriggerableWorkflowsContext, m.cancelSyncTriggerableWorkflows = context.WithCancel(context.Background()) - m.lastRepository = m.SelectedRepository.RepositoryName + m.lastRepository = m.selectedRepository.RepositoryName go m.syncTriggerableWorkflows(m.syncTriggerableWorkflowsContext) } m.tableTriggerableWorkflow, cmd = m.tableTriggerableWorkflow.Update(msg) + cmds = append(cmds, cmd) m.handleTableInputs(m.syncTriggerableWorkflowsContext) // update table operations - return m, cmd + return m, tea.Batch(cmds...) } func (m *ModelGithubWorkflow) View() string { - termWidth := m.Viewport.Width - termHeight := m.Viewport.Height + var style = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")).MarginLeft(1) + + helpWindowStyle := hdltypes.WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4) + + termWidth := m.skeleton.GetTerminalWidth() + termHeight := m.skeleton.GetTerminalHeight() var tableWidth int for _, t := range tableColumnsWorkflow { @@ -130,41 +141,42 @@ func (m *ModelGithubWorkflow) View() string { newTableColumns := tableColumnsWorkflow widthDiff := termWidth - tableWidth if widthDiff > 0 { - newTableColumns[1].Width += widthDiff - 11 + newTableColumns[1].Width += widthDiff - 10 m.tableTriggerableWorkflow.SetColumns(newTableColumns) m.tableTriggerableWorkflow.SetHeight(termHeight - 17) } doc := strings.Builder{} - doc.WriteString(baseStyle.Render(m.tableTriggerableWorkflow.View())) + doc.WriteString(style.Render(m.tableTriggerableWorkflow.View())) + doc.WriteString("\n\n\n") - return doc.String() + return lipgloss.JoinVertical(lipgloss.Top, doc.String(), m.status.View(), helpWindowStyle.Render(m.ViewHelp())) } func (m *ModelGithubWorkflow) syncTriggerableWorkflows(ctx context.Context) { - m.modelError.Reset() - m.modelError.SetProgressMessage( - fmt.Sprintf("[%s@%s] Fetching triggerable workflows...", m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) - m.actualModelTabOptions.SetStatus(taboptions.OptionWait) + defer m.selfUpdate() + + m.status.Reset() + m.status.SetProgressMessage(fmt.Sprintf("[%s@%s] Fetching triggerable workflows...", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) // delete all rows m.tableTriggerableWorkflow.SetRows([]table.Row{}) triggerableWorkflows, err := m.github.GetTriggerableWorkflows(ctx, gu.GetTriggerableWorkflowsInput{ - Repository: m.SelectedRepository.RepositoryName, - Branch: m.SelectedRepository.BranchName, + Repository: m.selectedRepository.RepositoryName, + Branch: m.selectedRepository.BranchName, }) if errors.Is(err, context.Canceled) { return } else if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Triggerable workflows cannot be listed") + m.status.SetError(err) + m.status.SetErrorMessage("Triggerable workflows cannot be listed") return } if len(triggerableWorkflows.TriggerableWorkflows) == 0 { - m.actualModelTabOptions.SetStatus(taboptions.OptionNone) - m.modelError.SetDefaultMessage(fmt.Sprintf("[%s@%s] No triggerable workflow found.", m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) + m.selectedRepository.WorkflowName = "" + m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] No triggerable workflow found.", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) return } @@ -183,9 +195,7 @@ func (m *ModelGithubWorkflow) syncTriggerableWorkflows(ctx context.Context) { m.tableTriggerableWorkflow.SetRows(tableRowsTriggerableWorkflow) m.tableReady = true - m.modelError.SetSuccessMessage(fmt.Sprintf("[%s@%s] Triggerable workflows fetched.", m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) - - go m.Update(m) // update model + m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s] Triggerable workflows fetched.", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) } func (m *ModelGithubWorkflow) handleTableInputs(_ context.Context) { @@ -198,12 +208,6 @@ func (m *ModelGithubWorkflow) handleTableInputs(_ context.Context) { selectedRow := m.tableTriggerableWorkflow.SelectedRow() if len(rows) > 0 && len(selectedRow) > 0 { - m.SelectedRepository.WorkflowName = selectedRow[1] + m.selectedRepository.WorkflowName = selectedRow[1] } - - m.actualModelTabOptions.SetStatus(taboptions.OptionIdle) -} - -func (m *ModelGithubWorkflow) ViewStatus() string { - return m.modelError.View() } diff --git a/internal/terminal/handler/ghworkflow/keymap.go b/internal/terminal/handler/ghworkflow/keymap.go deleted file mode 100644 index 5e6f61b..0000000 --- a/internal/terminal/handler/ghworkflow/keymap.go +++ /dev/null @@ -1,43 +0,0 @@ -package ghworkflow - -import ( - "fmt" - - "github.com/termkit/gama/internal/config" - - teakey "github.com/charmbracelet/bubbles/key" -) - -type keyMap struct { - SwitchTab teakey.Binding -} - -func (k keyMap) ShortHelp() []teakey.Binding { - return []teakey.Binding{k.SwitchTab} -} - -func (k keyMap) FullHelp() [][]teakey.Binding { - return [][]teakey.Binding{ - {k.SwitchTab}, - } -} - -var keys = func() keyMap { - cfg, err := config.LoadConfig() - if err != nil { - panic(fmt.Sprintf("failed to load config: %v", err)) - } - - var tabSwitch = fmt.Sprintf("%s | %s", cfg.Shortcuts.SwitchTabLeft, cfg.Shortcuts.SwitchTabRight) - - return keyMap{ - SwitchTab: teakey.NewBinding( - teakey.WithKeys(""), // help-only binding - teakey.WithHelp(tabSwitch, "switch tab"), - ), - } -}() - -func (m *ModelGithubWorkflow) ViewHelp() string { - return m.Help.View(m.Keys) -} diff --git a/internal/terminal/handler/ghworkflow/table.go b/internal/terminal/handler/ghworkflow/table.go deleted file mode 100644 index 344eeff..0000000 --- a/internal/terminal/handler/ghworkflow/table.go +++ /dev/null @@ -1,10 +0,0 @@ -package ghworkflow - -import ( - "github.com/charmbracelet/bubbles/table" -) - -var tableColumnsWorkflow = []table.Column{ - {Title: "Workflow", Width: 32}, - {Title: "File", Width: 48}, -} diff --git a/internal/terminal/handler/ghworkflowhistory.go b/internal/terminal/handler/ghworkflowhistory.go new file mode 100644 index 0000000..462d504 --- /dev/null +++ b/internal/terminal/handler/ghworkflowhistory.go @@ -0,0 +1,372 @@ +package handler + +import ( + "context" + "errors" + "fmt" + "github.com/termkit/gama/internal/config" + "github.com/termkit/skeleton" + "time" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + gu "github.com/termkit/gama/internal/github/usecase" + "github.com/termkit/gama/internal/terminal/handler/status" + "github.com/termkit/gama/internal/terminal/handler/taboptions" + hdltypes "github.com/termkit/gama/internal/terminal/handler/types" + "github.com/termkit/gama/pkg/browser" +) + +type ModelGithubWorkflowHistory struct { + skeleton *skeleton.Skeleton + // current handler's properties + tableReady bool + liveMode bool + liveModeInterval time.Duration + tableStyle lipgloss.Style + updateRound int + selectedWorkflowID int64 + lastRepository string + syncWorkflowHistoryContext context.Context + cancelSyncWorkflowHistory context.CancelFunc + workflows []gu.Workflow + + // shared properties + selectedRepository *hdltypes.SelectedRepository + + // use cases + github gu.UseCase + + // keymap + keys githubWorkflowHistoryKeyMap + + // models + Help help.Model + tableWorkflowHistory table.Model + status *status.ModelStatus + + modelTabOptions *taboptions.Options + + updateSelfChan chan any +} + +type workflowHistoryUpdateMsg struct { + UpdateAfter time.Duration +} + +var githubWorkflowHistoryUpdateChan = make(chan workflowHistoryUpdateMsg) + +func UpdateWorkflowHistory(timeAfter time.Duration) { + go func() { + githubWorkflowHistoryUpdateChan <- workflowHistoryUpdateMsg{UpdateAfter: timeAfter} + }() +} + +func SetupModelGithubWorkflowHistory(skeleton *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubWorkflowHistory { + cfg, err := config.LoadConfig() + if err != nil { + panic(fmt.Sprintf("failed to load config: %v", err)) + } + + var tableRowsWorkflowHistory []table.Row + + tableWorkflowHistory := table.New( + table.WithColumns(tableColumnsWorkflowHistory), + table.WithRows(tableRowsWorkflowHistory), + table.WithFocused(true), + table.WithHeight(7), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + tableWorkflowHistory.SetStyles(s) + + var tableStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#3b698f")).MarginLeft(1) + + modelStatus := status.SetupModelStatus(skeleton) + tabOptions := taboptions.NewOptions(&modelStatus) + + githubWorkflowHistoryUpdateChan = make(chan workflowHistoryUpdateMsg) + + return &ModelGithubWorkflowHistory{ + skeleton: skeleton, + liveMode: cfg.Settings.LiveMode.Enabled, + liveModeInterval: cfg.Settings.LiveMode.Interval, + Help: help.New(), + keys: githubWorkflowHistoryKeys, + github: githubUseCase, + tableWorkflowHistory: tableWorkflowHistory, + status: &modelStatus, + selectedRepository: hdltypes.NewSelectedRepository(), + modelTabOptions: tabOptions, + syncWorkflowHistoryContext: context.Background(), + cancelSyncWorkflowHistory: func() {}, + tableStyle: tableStyle, + updateSelfChan: make(chan any), + } +} + +func (m *ModelGithubWorkflowHistory) selfUpdate() { + m.updateSelfChan <- selfUpdateMsg{} +} + +func (m *ModelGithubWorkflowHistory) selfListen() tea.Cmd { + return func() tea.Msg { + return <-m.updateSelfChan + } +} + +func (m *ModelGithubWorkflowHistory) Init() tea.Cmd { + m.setupOptions() + m.ToggleLiveMode() + return tea.Batch( + m.modelTabOptions.Init(), + func() tea.Msg { + return workflowHistoryUpdateMsg{UpdateAfter: time.Second * 1} + }, + m.SelfUpdater(), + m.selfListen(), + ) +} +func (m *ModelGithubWorkflowHistory) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.lastRepository != m.selectedRepository.RepositoryName { + m.tableReady = false + m.cancelSyncWorkflowHistory() // cancel previous sync + + m.lastRepository = m.selectedRepository.RepositoryName + + m.syncWorkflowHistoryContext, m.cancelSyncWorkflowHistory = context.WithCancel(context.Background()) + go m.syncWorkflowHistory(m.syncWorkflowHistoryContext) + } + + if m.workflows != nil { + m.selectedWorkflowID = m.workflows[m.tableWorkflowHistory.Cursor()].ID + } + + var cmds []tea.Cmd + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keys.Refresh): + go m.syncWorkflowHistory(m.syncWorkflowHistoryContext) + cmds = append(cmds, m.SelfUpdater()) + case key.Matches(msg, m.keys.LiveMode): + m.liveMode = !m.liveMode + if m.liveMode { + m.status.SetSuccessMessage("Live mode enabled") + m.skeleton.UpdateWidgetValue("live", "Live Mode: On") + } else { + m.status.SetSuccessMessage("Live mode disabled") + m.skeleton.UpdateWidgetValue("live", "Live Mode: Off") + } + } + case workflowHistoryUpdateMsg: + go func() { + time.Sleep(msg.UpdateAfter) + m.syncWorkflowHistory(m.syncWorkflowHistoryContext) + }() + cmds = append(cmds, m.SelfUpdater()) + case selfUpdateMsg: + // do nothing + cmds = append(cmds, m.selfListen()) + } + + m.modelTabOptions, cmd = m.modelTabOptions.Update(msg) + cmds = append(cmds, cmd) + + m.tableWorkflowHistory, cmd = m.tableWorkflowHistory.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m *ModelGithubWorkflowHistory) View() string { + helpWindowStyle := hdltypes.WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4) + + termWidth := m.skeleton.GetTerminalWidth() + termHeight := m.skeleton.GetTerminalHeight() + + var tableWidth int + for _, t := range tableColumnsWorkflowHistory { + tableWidth += t.Width + } + + newTableColumns := tableColumnsWorkflowHistory + widthDiff := termWidth - tableWidth + + if widthDiff > 0 { + if m.updateRound%2 == 0 { + newTableColumns[0].Width += widthDiff - 18 + } else { + newTableColumns[1].Width += widthDiff - 18 + } + m.updateRound++ + m.tableWorkflowHistory.SetColumns(newTableColumns) + } + + m.tableWorkflowHistory.SetHeight(termHeight - 18) + + return lipgloss.JoinVertical(lipgloss.Top, + m.tableStyle.Render(m.tableWorkflowHistory.View()), m.modelTabOptions.View(), + m.status.View(), helpWindowStyle.Render(m.ViewHelp())) +} + +func (m *ModelGithubWorkflowHistory) SelfUpdater() tea.Cmd { + return func() tea.Msg { + return <-githubWorkflowHistoryUpdateChan + } +} + +func (m *ModelGithubWorkflowHistory) setupOptions() { + openInBrowser := func() { + m.status.SetProgressMessage("Opening in browser...") + + var selectedWorkflow = fmt.Sprintf("https://github.com/%s/actions/runs/%d", m.selectedRepository.RepositoryName, m.selectedWorkflowID) + + err := browser.OpenInBrowser(selectedWorkflow) + if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("Failed to open in browser") + return + } + m.status.SetSuccessMessage("Opened in browser") + } + + reRunFailedJobs := func() { + m.status.SetProgressMessage("Re-running failed jobs...") + + _, err := m.github.ReRunFailedJobs(context.Background(), gu.ReRunFailedJobsInput{ + Repository: m.selectedRepository.RepositoryName, + WorkflowID: m.selectedWorkflowID, + }) + + if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("Failed to re-run failed jobs") + return + } + + m.status.SetSuccessMessage("Re-ran failed jobs") + } + + reRunWorkflow := func() { + m.status.SetProgressMessage("Re-running workflow...") + + _, err := m.github.ReRunWorkflow(context.Background(), gu.ReRunWorkflowInput{ + Repository: m.selectedRepository.RepositoryName, + WorkflowID: m.selectedWorkflowID, + }) + + if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("Failed to re-run workflow") + return + } + + m.status.SetSuccessMessage("Re-ran workflow") + } + + cancelWorkflow := func() { + m.status.SetProgressMessage("Canceling workflow...") + + _, err := m.github.CancelWorkflow(context.Background(), gu.CancelWorkflowInput{ + Repository: m.selectedRepository.RepositoryName, + WorkflowID: m.selectedWorkflowID, + }) + + if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("Failed to cancel workflow") + return + } + + m.status.SetSuccessMessage("Canceled workflow") + } + m.modelTabOptions.AddOption("Open in browser", openInBrowser) + m.modelTabOptions.AddOption("Rerun failed jobs", reRunFailedJobs) + m.modelTabOptions.AddOption("Rerun workflow", reRunWorkflow) + m.modelTabOptions.AddOption("Cancel workflow", cancelWorkflow) +} + +func (m *ModelGithubWorkflowHistory) ToggleLiveMode() { + // send UpdateWorkflowHistoryMsg to update the workflow history every 5 seconds with ticker + // send only if liveMode is true + go func() { + t := time.NewTicker(m.liveModeInterval) + for { + select { + case <-t.C: + if m.liveMode { + githubWorkflowHistoryUpdateChan <- workflowHistoryUpdateMsg{UpdateAfter: time.Nanosecond} + } + } + } + }() +} +func (m *ModelGithubWorkflowHistory) syncWorkflowHistory(ctx context.Context) { + defer m.selfUpdate() + + m.tableReady = false + m.status.Reset() + m.status.SetProgressMessage( + fmt.Sprintf("[%s@%s] Fetching workflow history...", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) + m.modelTabOptions.SetStatus(taboptions.OptionWait) + + // delete all rows + m.tableWorkflowHistory.SetRows([]table.Row{}) + + // delete old workflows + m.workflows = nil + + workflowHistory, err := m.github.GetWorkflowHistory(ctx, gu.GetWorkflowHistoryInput{ + Repository: m.selectedRepository.RepositoryName, + Branch: m.selectedRepository.BranchName, + }) + if errors.Is(err, context.Canceled) { + return + } else if err != nil { + m.status.SetError(err) + m.status.SetErrorMessage("Workflow history cannot be listed") + return + } + + if len(workflowHistory.Workflows) == 0 { + m.modelTabOptions.SetStatus(taboptions.OptionNone) + m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] No workflows found.", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) + return + } + + m.workflows = workflowHistory.Workflows + + var tableRowsWorkflowHistory []table.Row + for _, workflowRun := range m.workflows { + tableRowsWorkflowHistory = append(tableRowsWorkflowHistory, table.Row{ + workflowRun.WorkflowName, + workflowRun.ActionName, + workflowRun.TriggeredBy, + workflowRun.StartedAt, + workflowRun.Status, + workflowRun.Duration, + }) + } + + m.tableReady = true + m.tableWorkflowHistory.SetRows(tableRowsWorkflowHistory) + m.tableWorkflowHistory.SetCursor(0) + m.modelTabOptions.SetStatus(taboptions.OptionIdle) + m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s] Workflow history fetched.", m.selectedRepository.RepositoryName, m.selectedRepository.BranchName)) +} diff --git a/internal/terminal/handler/ghworkflowhistory/ghworkflowhistory.go b/internal/terminal/handler/ghworkflowhistory/ghworkflowhistory.go deleted file mode 100644 index 1fa798a..0000000 --- a/internal/terminal/handler/ghworkflowhistory/ghworkflowhistory.go +++ /dev/null @@ -1,300 +0,0 @@ -package ghworkflowhistory - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - gu "github.com/termkit/gama/internal/github/usecase" - hdlerror "github.com/termkit/gama/internal/terminal/handler/error" - "github.com/termkit/gama/internal/terminal/handler/taboptions" - hdltypes "github.com/termkit/gama/internal/terminal/handler/types" - "github.com/termkit/gama/pkg/browser" -) - -type ModelGithubWorkflowHistory struct { - // current handler's properties - tableReady bool - updateRound int - selectedWorkflowID int64 - isTableFocused bool - lastRepository string - forceUpdate *bool - syncWorkflowHistoryContext context.Context - cancelSyncWorkflowHistory context.CancelFunc - Workflows []gu.Workflow - - // shared properties - SelectedRepository *hdltypes.SelectedRepository - - // use cases - github gu.UseCase - - // keymap - Keys keyMap - - // models - Help help.Model - Viewport *viewport.Model - tableWorkflowHistory table.Model - modelError *hdlerror.ModelError - - modelTabOptions tea.Model - actualModelTabOptions *taboptions.Options -} - -var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - -func SetupModelGithubWorkflowHistory(githubUseCase gu.UseCase, selectedRepository *hdltypes.SelectedRepository, forceUpdate *bool) *ModelGithubWorkflowHistory { - var tableRowsWorkflowHistory []table.Row - - tableWorkflowHistory := table.New( - table.WithColumns(tableColumnsWorkflowHistory), - table.WithRows(tableRowsWorkflowHistory), - table.WithFocused(true), - table.WithHeight(7), - ) - - s := table.DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) - s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). - Bold(false) - tableWorkflowHistory.SetStyles(s) - - modelError := hdlerror.SetupModelError() - tabOptions := taboptions.NewOptions(&modelError) - - return &ModelGithubWorkflowHistory{ - Help: help.New(), - Keys: keys, - github: githubUseCase, - tableWorkflowHistory: tableWorkflowHistory, - modelError: &modelError, - SelectedRepository: selectedRepository, - modelTabOptions: tabOptions, - actualModelTabOptions: tabOptions, - forceUpdate: forceUpdate, - syncWorkflowHistoryContext: context.Background(), - cancelSyncWorkflowHistory: func() {}, - } -} - -func (m *ModelGithubWorkflowHistory) Init() tea.Cmd { - openInBrowser := func() { - m.modelError.SetProgressMessage("Opening in browser...") - - var selectedWorkflow = fmt.Sprintf("https://github.com/%s/actions/runs/%d", m.SelectedRepository.RepositoryName, m.selectedWorkflowID) - - err := browser.OpenInBrowser(selectedWorkflow) - if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Failed to open in browser") - return - } - m.modelError.SetSuccessMessage("Opened in browser") - } - - reRunFailedJobs := func() { - m.modelError.SetProgressMessage("Re-running failed jobs...") - - _, err := m.github.ReRunFailedJobs(context.Background(), gu.ReRunFailedJobsInput{ - Repository: m.SelectedRepository.RepositoryName, - WorkflowID: m.selectedWorkflowID, - }) - - if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Failed to re-run failed jobs") - return - } - - m.modelError.SetSuccessMessage("Re-ran failed jobs") - } - - reRunWorkflow := func() { - m.modelError.SetProgressMessage("Re-running workflow...") - - _, err := m.github.ReRunWorkflow(context.Background(), gu.ReRunWorkflowInput{ - Repository: m.SelectedRepository.RepositoryName, - WorkflowID: m.selectedWorkflowID, - }) - - if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Failed to re-run workflow") - return - } - - m.modelError.SetSuccessMessage("Re-ran workflow") - } - - cancelWorkflow := func() { - m.modelError.SetProgressMessage("Canceling workflow...") - - _, err := m.github.CancelWorkflow(context.Background(), gu.CancelWorkflowInput{ - Repository: m.SelectedRepository.RepositoryName, - WorkflowID: m.selectedWorkflowID, - }) - - if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Failed to cancel workflow") - return - } - - m.modelError.SetSuccessMessage("Canceled workflow") - } - m.actualModelTabOptions.AddOption("Open in browser", openInBrowser) - m.actualModelTabOptions.AddOption("Rerun failed jobs", reRunFailedJobs) - m.actualModelTabOptions.AddOption("Rerun workflow", reRunWorkflow) - m.actualModelTabOptions.AddOption("Cancel workflow", cancelWorkflow) - - go func() { - // Make it works with to channels - for { - if *m.forceUpdate { - m.tableReady = false - go m.syncWorkflowHistory(m.syncWorkflowHistoryContext) - *m.forceUpdate = false - } - } - }() - - return tea.Batch(m.modelTabOptions.Init()) -} - -func (m *ModelGithubWorkflowHistory) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if m.lastRepository != m.SelectedRepository.RepositoryName { - m.tableReady = false - m.cancelSyncWorkflowHistory() // cancel previous sync - - m.lastRepository = m.SelectedRepository.RepositoryName - - m.syncWorkflowHistoryContext, m.cancelSyncWorkflowHistory = context.WithCancel(context.Background()) - go m.syncWorkflowHistory(m.syncWorkflowHistoryContext) - } - - if m.Workflows != nil { - m.selectedWorkflowID = m.Workflows[m.tableWorkflowHistory.Cursor()].ID - } - - var cmds []tea.Cmd - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch { - case key.Matches(msg, m.Keys.Refresh): - m.tableReady = false - go m.syncWorkflowHistory(m.syncWorkflowHistoryContext) - } - } - - m.modelTabOptions, cmd = m.modelTabOptions.Update(msg) - cmds = append(cmds, cmd) - - m.tableWorkflowHistory, cmd = m.tableWorkflowHistory.Update(msg) - cmds = append(cmds, cmd) - - return m, tea.Batch(cmds...) -} - -func (m *ModelGithubWorkflowHistory) syncWorkflowHistory(ctx context.Context) { - m.modelError.Reset() - m.modelError.SetProgressMessage( - fmt.Sprintf("[%s@%s] Fetching workflow history...", m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) - m.actualModelTabOptions.SetStatus(taboptions.OptionWait) - - // delete all rows - m.tableWorkflowHistory.SetRows([]table.Row{}) - - // delete old workflows - m.Workflows = nil - - workflowHistory, err := m.github.GetWorkflowHistory(ctx, gu.GetWorkflowHistoryInput{ - Repository: m.SelectedRepository.RepositoryName, - Branch: m.SelectedRepository.BranchName, - }) - if errors.Is(err, context.Canceled) { - return - } else if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("Workflow history cannot be listed") - return - } - - if len(workflowHistory.Workflows) == 0 { - m.actualModelTabOptions.SetStatus(taboptions.OptionNone) - m.modelError.SetDefaultMessage(fmt.Sprintf("[%s@%s] No workflows found.", m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) - return - } - - m.Workflows = workflowHistory.Workflows - - var tableRowsWorkflowHistory []table.Row - for _, workflowRun := range m.Workflows { - tableRowsWorkflowHistory = append(tableRowsWorkflowHistory, table.Row{ - workflowRun.WorkflowName, - workflowRun.ActionName, - workflowRun.TriggeredBy, - workflowRun.StartedAt, - workflowRun.Conclusion, - workflowRun.Duration, - }) - } - - m.tableReady = true - m.tableWorkflowHistory.SetRows(tableRowsWorkflowHistory) - m.tableWorkflowHistory.SetCursor(0) - m.actualModelTabOptions.SetStatus(taboptions.OptionIdle) - m.modelError.SetSuccessMessage(fmt.Sprintf("[%s@%s] Workflow history fetched.", m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName)) - go m.Update(m) // update model -} - -func (m *ModelGithubWorkflowHistory) View() string { - termWidth := m.Viewport.Width - termHeight := m.Viewport.Height - - var tableWidth int - for _, t := range tableColumnsWorkflowHistory { - tableWidth += t.Width - } - - newTableColumns := tableColumnsWorkflowHistory - widthDiff := termWidth - tableWidth - - if widthDiff > 0 { - if m.updateRound%2 == 0 { - newTableColumns[0].Width += widthDiff - 19 - } else { - newTableColumns[1].Width += widthDiff - 19 - } - m.updateRound++ - m.tableWorkflowHistory.SetColumns(newTableColumns) - } - - m.tableWorkflowHistory.SetHeight(termHeight - 17) - - doc := strings.Builder{} - doc.WriteString(baseStyle.Render(m.tableWorkflowHistory.View())) - - return lipgloss.JoinVertical(lipgloss.Top, doc.String(), m.actualModelTabOptions.View()) -} - -func (m *ModelGithubWorkflowHistory) ViewStatus() string { - return m.modelError.View() -} diff --git a/internal/terminal/handler/ghworkflowhistory/keymap.go b/internal/terminal/handler/ghworkflowhistory/keymap.go deleted file mode 100644 index e0ff75f..0000000 --- a/internal/terminal/handler/ghworkflowhistory/keymap.go +++ /dev/null @@ -1,55 +0,0 @@ -package ghworkflowhistory - -import ( - "fmt" - - "github.com/termkit/gama/internal/config" - - teakey "github.com/charmbracelet/bubbles/key" -) - -type keyMap struct { - LaunchTab teakey.Binding - Refresh teakey.Binding - SwitchTab teakey.Binding -} - -func (k keyMap) ShortHelp() []teakey.Binding { - return []teakey.Binding{k.SwitchTab, k.Refresh, k.LaunchTab} -} - -func (k keyMap) FullHelp() [][]teakey.Binding { - return [][]teakey.Binding{ - {k.SwitchTab}, - {k.Refresh}, - {k.LaunchTab}, - } -} - -var keys = func() keyMap { - cfg, err := config.LoadConfig() - if err != nil { - panic(fmt.Sprintf("failed to load config: %v", err)) - } - - var tabSwitch = fmt.Sprintf("%s | %s", cfg.Shortcuts.SwitchTabLeft, cfg.Shortcuts.SwitchTabRight) - - return keyMap{ - Refresh: teakey.NewBinding( - teakey.WithKeys(cfg.Shortcuts.Refresh), - teakey.WithHelp(cfg.Shortcuts.Refresh, "Refresh list"), - ), - LaunchTab: teakey.NewBinding( - teakey.WithKeys(cfg.Shortcuts.Enter), - teakey.WithHelp(cfg.Shortcuts.Enter, "Launch the selected option"), - ), - SwitchTab: teakey.NewBinding( - teakey.WithKeys(""), // help-only binding - teakey.WithHelp(tabSwitch, "switch tab"), - ), - } -}() - -func (m *ModelGithubWorkflowHistory) ViewHelp() string { - return m.Help.View(m.Keys) -} diff --git a/internal/terminal/handler/ghworkflowhistory/table.go b/internal/terminal/handler/ghworkflowhistory/table.go deleted file mode 100644 index 18ada96..0000000 --- a/internal/terminal/handler/ghworkflowhistory/table.go +++ /dev/null @@ -1,14 +0,0 @@ -package ghworkflowhistory - -import ( - "github.com/charmbracelet/bubbles/table" -) - -var tableColumnsWorkflowHistory = []table.Column{ - {Title: "Workflow", Width: 12}, - {Title: "Commit Message", Width: 16}, - {Title: "Triggered", Width: 12}, - {Title: "Started At", Width: 19}, - {Title: "Status", Width: 9}, - {Title: "Duration", Width: 8}, -} diff --git a/internal/terminal/handler/handler.go b/internal/terminal/handler/handler.go index b2cd4a0..adeda9f 100644 --- a/internal/terminal/handler/handler.go +++ b/internal/terminal/handler/handler.go @@ -1,263 +1,36 @@ package handler import ( - "fmt" - "strings" - "time" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/timer" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" gu "github.com/termkit/gama/internal/github/usecase" - hdlgithubrepo "github.com/termkit/gama/internal/terminal/handler/ghrepository" - hdltrigger "github.com/termkit/gama/internal/terminal/handler/ghtrigger" - hdlWorkflow "github.com/termkit/gama/internal/terminal/handler/ghworkflow" - hdlworkflowhistory "github.com/termkit/gama/internal/terminal/handler/ghworkflowhistory" - hdlinfo "github.com/termkit/gama/internal/terminal/handler/information" hdltypes "github.com/termkit/gama/internal/terminal/handler/types" - ts "github.com/termkit/gama/internal/terminal/style" pkgversion "github.com/termkit/gama/pkg/version" -) - -type model struct { - // current handler's properties - TabsWithColor []string - currentTab *int - isTabActive bool - - // Shared properties - SelectedRepository *hdltypes.SelectedRepository - lockTabs *bool // lockTabs will be set true if test connection fails - - // models - viewport viewport.Model - timer timer.Model - - modelInfo tea.Model - actualModelInfo *hdlinfo.ModelInfo - - modelGithubRepository tea.Model - actualModelGithubRepository *hdlgithubrepo.ModelGithubRepository - - modelWorkflow tea.Model - directModelWorkflow *hdlWorkflow.ModelGithubWorkflow - - modelWorkflowHistory tea.Model - directModelWorkflowHistory *hdlworkflowhistory.ModelGithubWorkflowHistory - - modelTrigger tea.Model - actualModelTrigger *hdltrigger.ModelGithubTrigger - - // keymap - keys keyMap -} - -const ( - minTerminalWidth = 102 - minTerminalHeight = 24 + "github.com/termkit/skeleton" + "time" ) func SetupTerminal(githubUseCase gu.UseCase, version pkgversion.Version) tea.Model { - var currentTab = new(int) - var forceUpdateWorkflowHistory = new(bool) - var lockTabs = new(bool) - - *lockTabs = true // by default lock tabs - - tabsWithColor := []string{"Info", "Repository", "Workflow History", "Workflow", "Trigger"} - - selectedRepository := hdltypes.SelectedRepository{} + s := skeleton.NewSkeleton() - // setup models - hdlModelInfo := hdlinfo.SetupModelInfo(githubUseCase, version, lockTabs) - hdlModelGithubRepository := hdlgithubrepo.SetupModelGithubRepository(githubUseCase, &selectedRepository) - hdlModelWorkflowHistory := hdlworkflowhistory.SetupModelGithubWorkflowHistory(githubUseCase, &selectedRepository, forceUpdateWorkflowHistory) - hdlModelWorkflow := hdlWorkflow.SetupModelGithubWorkflow(githubUseCase, &selectedRepository) - hdlModelTrigger := hdltrigger.SetupModelGithubTrigger(githubUseCase, &selectedRepository, currentTab, forceUpdateWorkflowHistory) - - m := model{ - lockTabs: lockTabs, - currentTab: currentTab, - TabsWithColor: tabsWithColor, - timer: timer.NewWithInterval(1<<63-1, time.Millisecond*200), - modelInfo: hdlModelInfo, actualModelInfo: hdlModelInfo, - SelectedRepository: &selectedRepository, - modelGithubRepository: hdlModelGithubRepository, actualModelGithubRepository: hdlModelGithubRepository, - modelWorkflowHistory: hdlModelWorkflowHistory, directModelWorkflowHistory: hdlModelWorkflowHistory, - modelWorkflow: hdlModelWorkflow, directModelWorkflow: hdlModelWorkflow, - modelTrigger: hdlModelTrigger, actualModelTrigger: hdlModelTrigger, - keys: keys, - } - - hdlModelInfo.Viewport = &m.viewport - hdlModelGithubRepository.Viewport = &m.viewport - hdlModelWorkflowHistory.Viewport = &m.viewport - hdlModelWorkflow.Viewport = &m.viewport - hdlModelTrigger.Viewport = &m.viewport - - return &m -} - -func (m *model) Init() tea.Cmd { - m.viewport = viewport.Model{Width: minTerminalWidth, Height: minTerminalHeight} - return tea.Batch( - tea.EnterAltScreen, - tea.SetWindowTitle("GitHub Actions Manager (GAMA)"), - m.timer.Init(), - m.modelInfo.Init(), - m.modelGithubRepository.Init(), - m.modelWorkflowHistory.Init(), - m.modelWorkflow.Init(), - m.modelTrigger.Init()) -} + s.AddPage("info", "Info", SetupModelInfo(s, githubUseCase, version)). + AddPage("repository", "Repository", SetupModelGithubRepository(s, githubUseCase)). + AddPage("history", "Workflow History", SetupModelGithubWorkflowHistory(s, githubUseCase)). + AddPage("workflow", "Workflow", SetupModelGithubWorkflow(s, githubUseCase)). + AddPage("trigger", "Trigger", SetupModelGithubTrigger(s, githubUseCase)) -func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - var cmds []tea.Cmd + s.SetBorderColor("#ff0055").SetActiveTabBorderColor("#ff0055").SetInactiveTabBorderColor("#82636f").SetWidgetBorderColor("#ff0055") - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.viewport.Width = msg.Width - m.viewport.Height = msg.Height - case tea.KeyMsg: - switch { - case key.Matches(msg, m.keys.SwitchTabLeft): - if !*m.lockTabs { - *m.currentTab = max(*m.currentTab-1, 0) - } - cmds = append(cmds, m.handleTabContent(cmd, msg)) - case key.Matches(msg, m.keys.SwitchTabRight): - if !*m.lockTabs { - *m.currentTab = min(*m.currentTab+1, len(m.TabsWithColor)-1) - } - cmds = append(cmds, m.handleTabContent(cmd, msg)) - case key.Matches(msg, m.keys.Quit): - return m, tea.Quit - default: - cmds = append(cmds, m.handleTabContent(cmd, msg)) - } - case timer.TickMsg: - m.timer, cmd = m.timer.Update(msg) - cmds = append(cmds, cmd) - } + time.Sleep(100 * time.Millisecond) + s.AddWidget("repositories", "Repository Count: 0") + time.Sleep(100 * time.Millisecond) + s.AddWidget("live", "Live Mode: Off") - return m, tea.Batch(cmds...) -} - -func (m *model) View() string { - if m.viewport.Width < minTerminalWidth || m.viewport.Height < minTerminalHeight { - return fmt.Sprintf("Terminal window is too small. Please resize to at least %dx%d.", minTerminalWidth, minTerminalHeight) - } - - var mainDoc strings.Builder - var helpDoc string - var operationDoc string - var helpDocHeight int - - header := newHeader(&m.viewport) - for i, t := range m.TabsWithColor { - var tabStyle lipgloss.Style - isActive := i == *m.currentTab - if isActive { - tabStyle = ts.TitleStyleActive - } else { - if *m.lockTabs { - tabStyle = ts.TitleStyleDisabled - } else { - tabStyle = ts.TitleStyleInactive - } - } - header.addHeaderTab(t, tabStyle) - } - - mainDoc.WriteString("\n" + header.renderHeader() + "\n") - - var width = lipgloss.Width(strings.Repeat("-", m.viewport.Width)) - 4 - hdltypes.ScreenWidth = &width - - dynamicWindowStyle := ts.WindowStyleCyan.Width(width).Height(m.viewport.Height - 20) - - helpWindowStyle := ts.WindowStyleHelp.Width(width) - operationWindowStyle := lipgloss.NewStyle() - - switch *m.currentTab { - case 0: - mainDoc.WriteString(dynamicWindowStyle.Render(m.modelInfo.View())) - operationDoc = operationWindowStyle.Render(m.actualModelInfo.ViewStatus()) - helpDoc = helpWindowStyle.Render(m.actualModelInfo.ViewHelp()) - case 1: - mainDoc.WriteString(dynamicWindowStyle.Render(m.modelGithubRepository.View())) - operationDoc = operationWindowStyle.Render(m.actualModelGithubRepository.ViewStatus()) - helpDoc = helpWindowStyle.Render(m.actualModelGithubRepository.ViewHelp()) - case 2: - mainDoc.WriteString(dynamicWindowStyle.Render(m.modelWorkflowHistory.View())) - operationDoc = operationWindowStyle.Render(m.directModelWorkflowHistory.ViewStatus()) - helpDoc = helpWindowStyle.Render(m.directModelWorkflowHistory.ViewHelp()) - case 3: - mainDoc.WriteString(dynamicWindowStyle.Render(m.modelWorkflow.View())) - operationDoc = operationWindowStyle.Render(m.directModelWorkflow.ViewStatus()) - helpDoc = helpWindowStyle.Render(m.directModelWorkflow.ViewHelp()) - case 4: - mainDoc.WriteString(dynamicWindowStyle.Render(m.modelTrigger.View())) - operationDoc = operationWindowStyle.Render(m.actualModelTrigger.ViewStatus()) - helpDoc = helpWindowStyle.Render(m.actualModelTrigger.ViewHelp()) - } - - mainDocContent := ts.DocStyle.Render(mainDoc.String()) - - mainDocHeight := strings.Count(mainDocContent, "\n") - helpDocHeight = strings.Count(helpDoc, "\n") - errorDocHeight := strings.Count(operationDoc, "\n") - requiredNewlinesForPadding := m.viewport.Height - mainDocHeight - helpDocHeight - errorDocHeight - padding := strings.Repeat("\n", max(0, requiredNewlinesForPadding)) - - informationPane := lipgloss.JoinVertical(lipgloss.Top, operationDoc, helpDoc) - - return mainDocContent + padding + informationPane -} - -func (m *model) handleTabContent(cmd tea.Cmd, msg tea.Msg) tea.Cmd { - switch *m.currentTab { - case 0: - m.modelInfo, cmd = m.modelInfo.Update(msg) - case 1: - m.modelGithubRepository, cmd = m.modelGithubRepository.Update(msg) - case 2: - m.modelWorkflowHistory, cmd = m.modelWorkflowHistory.Update(msg) - case 3: - m.modelWorkflow, cmd = m.modelWorkflow.Update(msg) - case 4: - m.modelTrigger, cmd = m.modelTrigger.Update(msg) - } - return cmd -} - -// --- Header --- - -// header is a helper for rendering the header of the terminal. -type header struct { - viewport *viewport.Model - titles []string -} - -// newHeader returns a new header. -func newHeader(viewport *viewport.Model) *header { - return &header{ - titles: make([]string, 0), - viewport: viewport, - } -} - -// addHeaderTab adds a tab to the header. -func (h *header) addHeaderTab(title string, style lipgloss.Style) { - h.titles = append(h.titles, style.Render(title)) -} + s.SetTerminalViewportWidth(hdltypes.MinTerminalWidth) + s.SetTerminalViewportHeight(hdltypes.MinTerminalHeight) -// renderHeader renders the header. -func (h *header) renderHeader() string { - line := strings.Repeat("─", max(0, h.viewport.Width-79)) + s.KeyMap.SetKeyNextTab(handlerKeys.SwitchTabRight) + s.KeyMap.SetKeyPrevTab(handlerKeys.SwitchTabLeft) + s.KeyMap.SetKeyQuit(handlerKeys.Quit) - return lipgloss.JoinHorizontal(lipgloss.Center, append(h.titles, line)...) + return s } diff --git a/internal/terminal/handler/information/information.go b/internal/terminal/handler/information/information.go deleted file mode 100644 index f06515d..0000000 --- a/internal/terminal/handler/information/information.go +++ /dev/null @@ -1,169 +0,0 @@ -package information - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - gu "github.com/termkit/gama/internal/github/usecase" - hdlerror "github.com/termkit/gama/internal/terminal/handler/error" - pkgversion "github.com/termkit/gama/pkg/version" -) - -type ModelInfo struct { - version pkgversion.Version - - // use cases - github gu.UseCase - - // lockTabs will be set true if test connection fails - lockTabs *bool - - // models - Help help.Model - Viewport *viewport.Model - modelError hdlerror.ModelError - spinner spinner.Model - - // keymap - Keys keyMap -} - -const ( - releaseURL = "https://github.com/termkit/gama/releases" - - applicationName = ` - ..|'''.| | '|| ||' | -.|' ' ||| ||| ||| ||| -|| .... | || |'|..'|| | || -'|. || .''''|. | '|' || .''''|. -''|...'| .|. .||. .|. | .||. .|. .||. -` -) - -var ( - currentVersion string - newVersionAvailableMsg string - applicationDescription string -) - -func SetupModelInfo(githubUseCase gu.UseCase, version pkgversion.Version, lockTabs *bool) *ModelInfo { - modelError := hdlerror.SetupModelError() - - s := spinner.New() - s.Spinner = spinner.Pulse - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("120")) - - return &ModelInfo{ - github: githubUseCase, - version: version, - Help: help.New(), - Keys: keys, - modelError: modelError, - lockTabs: lockTabs, - spinner: s, - } -} - -func (m *ModelInfo) Init() tea.Cmd { - currentVersion = m.version.CurrentVersion() - applicationDescription = fmt.Sprintf("Github Actions Manager (%s)", currentVersion) - - go m.testConnection(context.Background()) - go m.checkUpdates(context.Background()) - return nil -} - -func (m *ModelInfo) checkUpdates(ctx context.Context) { - isUpdateAvailable, version, err := m.version.IsUpdateAvailable(ctx) - if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("failed to check updates") - newVersionAvailableMsg = fmt.Sprintf("failed to check updates: %v\nPlease visit: %s", err, releaseURL) - return - } - - if isUpdateAvailable { - newVersionAvailableMsg = fmt.Sprintf("New version available: %s\nPlease visit: %s", version, releaseURL) - } - - go m.Update(m) -} - -func (m *ModelInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.Help.Width = msg.Width - case tea.KeyMsg: - switch { - case key.Matches(msg, m.Keys.Quit): - return m, tea.Quit - } - } - - return m, cmd -} - -func (m *ModelInfo) View() string { - infoDoc := strings.Builder{} - - ws := lipgloss.NewStyle(). - BorderForeground(lipgloss.Color("39")). - Align(lipgloss.Center). - Border(lipgloss.RoundedBorder()). - Width(m.Viewport.Width - 7) - - infoDoc.WriteString(lipgloss.JoinVertical(lipgloss.Center, applicationName, applicationDescription, newVersionAvailableMsg)) - - docHeight := strings.Count(infoDoc.String(), "\n") - requiredNewlinesForPadding := m.Viewport.Height - docHeight - 13 - - infoDoc.WriteString(strings.Repeat("\n", max(0, requiredNewlinesForPadding))) - - return ws.Render(infoDoc.String()) -} - -func (m *ModelInfo) testConnection(ctx context.Context) { - ctxWithCancel, cancel := context.WithCancel(ctx) - - // TODO: make it better - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - default: - m.spinner, _ = m.spinner.Update(m.spinner.Tick()) - m.modelError.SetProgressMessage("Checking your token " + m.spinner.View()) - time.Sleep(200 * time.Millisecond) - } - } - }(ctxWithCancel) - defer cancel() - - _, err := m.github.GetAuthUser(ctx) - if err != nil { - m.modelError.SetError(err) - m.modelError.SetErrorMessage("failed to test connection, please check your token&permission") - *m.lockTabs = true - return - } - - m.modelError.Reset() - m.modelError.SetSuccessMessage("Welcome to GAMA!") - *m.lockTabs = false - - go m.Update(m) -} - -func (m *ModelInfo) ViewStatus() string { - return m.modelError.View() -} diff --git a/internal/terminal/handler/information/keymap.go b/internal/terminal/handler/information/keymap.go deleted file mode 100644 index ad1ec59..0000000 --- a/internal/terminal/handler/information/keymap.go +++ /dev/null @@ -1,49 +0,0 @@ -package information - -import ( - "fmt" - - "github.com/termkit/gama/internal/config" - - teakey "github.com/charmbracelet/bubbles/key" -) - -type keyMap struct { - SwitchTabRight teakey.Binding - Quit teakey.Binding -} - -func (k keyMap) ShortHelp() []teakey.Binding { - return []teakey.Binding{k.SwitchTabRight, k.Quit} -} - -func (k keyMap) FullHelp() [][]teakey.Binding { - return [][]teakey.Binding{ - {k.SwitchTabRight}, - {k.Quit}, - } -} - -var keys = func() keyMap { - cfg, err := config.LoadConfig() - if err != nil { - panic(fmt.Sprintf("failed to load config: %v", err)) - } - - switchTabRight := cfg.Shortcuts.SwitchTabRight - - return keyMap{ - SwitchTabRight: teakey.NewBinding( - teakey.WithKeys(""), // help-only binding - teakey.WithHelp(switchTabRight, "next tab"), - ), - Quit: teakey.NewBinding( - teakey.WithKeys("q", cfg.Shortcuts.Quit), - teakey.WithHelp("q", "quit"), - ), - } -}() - -func (m *ModelInfo) ViewHelp() string { - return m.Help.View(m.Keys) -} diff --git a/internal/terminal/handler/keymap.go b/internal/terminal/handler/keymap.go index 034c703..b2781e0 100644 --- a/internal/terminal/handler/keymap.go +++ b/internal/terminal/handler/keymap.go @@ -7,18 +7,26 @@ import ( teakey "github.com/charmbracelet/bubbles/key" ) -type keyMap struct { +func loadConfig() *config.Config { + cfg, err := config.LoadConfig() + if err != nil { + panic(fmt.Sprintf("failed to load config: %v", err)) + } + return cfg +} + +// --------------------------------------------------------------------------- + +type handlerKeyMap struct { SwitchTabRight teakey.Binding SwitchTabLeft teakey.Binding Quit teakey.Binding } -var keys = func() keyMap { - cfg, err := config.LoadConfig() - if err != nil { - panic(fmt.Sprintf("failed to load config: %v", err)) - } - return keyMap{ +var handlerKeys = func() handlerKeyMap { + cfg := loadConfig() + + return handlerKeyMap{ SwitchTabRight: teakey.NewBinding( teakey.WithKeys(cfg.Shortcuts.SwitchTabRight), ), @@ -30,3 +38,222 @@ var keys = func() keyMap { ), } }() + +// --------------------------------------------------------------------------- + +type githubInformationKeyMap struct { + SwitchTabRight teakey.Binding + Quit teakey.Binding +} + +func (k githubInformationKeyMap) ShortHelp() []teakey.Binding { + return []teakey.Binding{k.SwitchTabRight, k.Quit} +} + +func (k githubInformationKeyMap) FullHelp() [][]teakey.Binding { + return [][]teakey.Binding{ + {k.SwitchTabRight}, + {k.Quit}, + } +} + +var githubInformationKeys = func() githubInformationKeyMap { + cfg := loadConfig() + + switchTabRight := cfg.Shortcuts.SwitchTabRight + + return githubInformationKeyMap{ + SwitchTabRight: teakey.NewBinding( + teakey.WithKeys(""), // help-only binding + teakey.WithHelp(switchTabRight, "next tab"), + ), + Quit: teakey.NewBinding( + teakey.WithKeys("q", cfg.Shortcuts.Quit), + teakey.WithHelp("q", "quit"), + ), + } +}() + +func (m *ModelInfo) ViewHelp() string { + return m.help.View(m.keys) +} + +// --------------------------------------------------------------------------- + +type githubRepositoryKeyMap struct { + Refresh teakey.Binding + LaunchTab teakey.Binding + SwitchTab teakey.Binding +} + +func (k githubRepositoryKeyMap) ShortHelp() []teakey.Binding { + return []teakey.Binding{k.SwitchTab, k.Refresh, k.LaunchTab} +} + +func (k githubRepositoryKeyMap) FullHelp() [][]teakey.Binding { + return [][]teakey.Binding{ + {k.SwitchTab}, + {k.Refresh}, + {k.LaunchTab}, + } +} + +var githubRepositoryKeys = func() githubRepositoryKeyMap { + cfg := loadConfig() + + var tabSwitch = fmt.Sprintf("%s | %s", cfg.Shortcuts.SwitchTabLeft, cfg.Shortcuts.SwitchTabRight) + + return githubRepositoryKeyMap{ + Refresh: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.Refresh), + teakey.WithHelp(cfg.Shortcuts.Refresh, "Refresh list"), + ), + LaunchTab: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.Enter), + teakey.WithHelp(cfg.Shortcuts.Enter, "Launch the selected option"), + ), + SwitchTab: teakey.NewBinding( + teakey.WithKeys(""), // help-only binding + teakey.WithHelp(tabSwitch, "switch tab"), + ), + } +}() + +func (m *ModelGithubRepository) ViewHelp() string { + return m.help.View(m.Keys) +} + +// --------------------------------------------------------------------------- + +type githubWorkflowHistoryKeyMap struct { + LaunchTab teakey.Binding + Refresh teakey.Binding + SwitchTab teakey.Binding + LiveMode teakey.Binding +} + +func (k githubWorkflowHistoryKeyMap) ShortHelp() []teakey.Binding { + return []teakey.Binding{k.SwitchTab, k.Refresh, k.LaunchTab, k.LiveMode} +} + +func (k githubWorkflowHistoryKeyMap) FullHelp() [][]teakey.Binding { + return [][]teakey.Binding{ + {k.SwitchTab}, + {k.Refresh}, + {k.LaunchTab}, + {k.LiveMode}, + } +} + +var githubWorkflowHistoryKeys = func() githubWorkflowHistoryKeyMap { + cfg := loadConfig() + + var tabSwitch = fmt.Sprintf("%s | %s", cfg.Shortcuts.SwitchTabLeft, cfg.Shortcuts.SwitchTabRight) + + return githubWorkflowHistoryKeyMap{ + Refresh: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.Refresh), + teakey.WithHelp(cfg.Shortcuts.Refresh, "Refresh list"), + ), + LaunchTab: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.Enter), + teakey.WithHelp(cfg.Shortcuts.Enter, "Launch the selected option"), + ), + LiveMode: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.LiveMode), + teakey.WithHelp(cfg.Shortcuts.LiveMode, "Toggle live mode"), + ), + SwitchTab: teakey.NewBinding( + teakey.WithKeys(""), // help-only binding + teakey.WithHelp(tabSwitch, "switch tab"), + ), + } +}() + +func (m *ModelGithubWorkflowHistory) ViewHelp() string { + return m.Help.View(m.keys) +} + +// --------------------------------------------------------------------------- + +type githubWorkflowKeyMap struct { + SwitchTab teakey.Binding +} + +func (k githubWorkflowKeyMap) ShortHelp() []teakey.Binding { + return []teakey.Binding{k.SwitchTab} +} + +func (k githubWorkflowKeyMap) FullHelp() [][]teakey.Binding { + return [][]teakey.Binding{ + {k.SwitchTab}, + } +} + +var githubWorkflowKeys = func() githubWorkflowKeyMap { + cfg := loadConfig() + + var tabSwitch = fmt.Sprintf("%s | %s", cfg.Shortcuts.SwitchTabLeft, cfg.Shortcuts.SwitchTabRight) + + return githubWorkflowKeyMap{ + SwitchTab: teakey.NewBinding( + teakey.WithKeys(""), // help-only binding + teakey.WithHelp(tabSwitch, "switch tab"), + ), + } +}() + +func (m *ModelGithubWorkflow) ViewHelp() string { + return m.help.View(m.keys) +} + +// --------------------------------------------------------------------------- + +type githubTriggerKeyMap struct { + SwitchTabLeft teakey.Binding + SwitchTab teakey.Binding + Trigger teakey.Binding + Refresh teakey.Binding +} + +func (k githubTriggerKeyMap) ShortHelp() []teakey.Binding { + return []teakey.Binding{k.SwitchTabLeft, k.Refresh, k.SwitchTab, k.Trigger} +} + +func (k githubTriggerKeyMap) FullHelp() [][]teakey.Binding { + return [][]teakey.Binding{ + {k.SwitchTabLeft}, + {k.Refresh}, + {k.SwitchTab}, + {k.Trigger}, + } +} + +var githubTriggerKeys = func() githubTriggerKeyMap { + cfg := loadConfig() + + previousTab := cfg.Shortcuts.SwitchTabLeft + + return githubTriggerKeyMap{ + SwitchTabLeft: teakey.NewBinding( + teakey.WithKeys(""), // help-only binding + teakey.WithHelp(previousTab, "previous tab"), + ), + Refresh: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.Refresh), + teakey.WithHelp(cfg.Shortcuts.Refresh, "Refresh list"), + ), + SwitchTab: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.Tab), + teakey.WithHelp(cfg.Shortcuts.Tab, "switch button"), + ), + Trigger: teakey.NewBinding( + teakey.WithKeys(cfg.Shortcuts.Enter), + teakey.WithHelp(cfg.Shortcuts.Enter, "trigger workflow"), + ), + } +}() + +func (m *ModelGithubTrigger) ViewHelp() string { + return m.help.View(m.Keys) +} diff --git a/internal/terminal/handler/error/error.go b/internal/terminal/handler/status/error.go similarity index 50% rename from internal/terminal/handler/error/error.go rename to internal/terminal/handler/status/error.go index fdecb07..1ab8147 100644 --- a/internal/terminal/handler/error/error.go +++ b/internal/terminal/handler/status/error.go @@ -1,15 +1,15 @@ -package error +package status import ( "fmt" - "strings" - "github.com/charmbracelet/lipgloss" - hdltypes "github.com/termkit/gama/internal/terminal/handler/types" - ts "github.com/termkit/gama/internal/terminal/style" + ts "github.com/termkit/gama/internal/terminal/handler/types" + "github.com/termkit/skeleton" + "strings" ) -type ModelError struct { +type ModelStatus struct { + skeleton *skeleton.Skeleton // err is hold the error err error @@ -23,6 +23,11 @@ type ModelError struct { messageType MessageType } +type UpdateSelf struct { + Message string + InProgress bool +} + type MessageType string const ( @@ -36,100 +41,101 @@ const ( MessageTypeSuccess MessageType = "success" ) -func SetupModelError() ModelError { - return ModelError{ +func SetupModelStatus(skeleton *skeleton.Skeleton) ModelStatus { + return ModelStatus{ + skeleton: skeleton, err: nil, errorMessage: "", } } -func (m *ModelError) SetError(err error) { +func (m *ModelStatus) View() string { + var windowStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()) + width := m.skeleton.GetTerminalWidth() - 4 + doc := strings.Builder{} + + if m.HaveError() { + windowStyle = ts.WindowStyleError.Width(width) + doc.WriteString(windowStyle.Render(m.viewError())) + return lipgloss.JoinHorizontal(lipgloss.Top, doc.String()) + } + + switch m.messageType { + case MessageTypeDefault: + windowStyle = ts.WindowStyleDefault.Width(width) + case MessageTypeProgress: + windowStyle = ts.WindowStyleProgress.Width(width) + case MessageTypeSuccess: + windowStyle = ts.WindowStyleSuccess.Width(width) + default: + windowStyle = ts.WindowStyleDefault.Width(width) + } + + doc.WriteString(windowStyle.Render(m.viewMessage())) + return doc.String() +} + +func (m *ModelStatus) SetError(err error) { m.err = err } -func (m *ModelError) SetErrorMessage(errorMessage string) { - m.errorMessage = errorMessage +func (m *ModelStatus) SetErrorMessage(message string) { + m.errorMessage = message } -func (m *ModelError) SetProgressMessage(message string) { +func (m *ModelStatus) SetProgressMessage(message string) { m.messageType = MessageTypeProgress m.message = message } -func (m *ModelError) SetSuccessMessage(message string) { +func (m *ModelStatus) SetSuccessMessage(message string) { m.messageType = MessageTypeSuccess m.message = message } -func (m *ModelError) SetDefaultMessage(message string) { +func (m *ModelStatus) SetDefaultMessage(message string) { m.messageType = MessageTypeDefault m.message = message } -func (m *ModelError) GetError() error { +func (m *ModelStatus) GetError() error { return m.err } -func (m *ModelError) GetErrorMessage() string { +func (m *ModelStatus) GetErrorMessage() string { return m.errorMessage } -func (m *ModelError) GetMessage() string { +func (m *ModelStatus) GetMessage() string { return m.message } -func (m *ModelError) ResetError() { +func (m *ModelStatus) ResetError() { m.err = nil m.errorMessage = "" } -func (m *ModelError) ResetMessage() { +func (m *ModelStatus) ResetMessage() { m.message = "" } -func (m *ModelError) Reset() { +func (m *ModelStatus) Reset() { m.ResetError() m.ResetMessage() } -func (m *ModelError) HaveError() bool { +func (m *ModelStatus) HaveError() bool { return m.err != nil } -func (m *ModelError) ViewError() string { +func (m *ModelStatus) viewError() string { doc := strings.Builder{} doc.WriteString(fmt.Sprintf("Error [%v]: %s", m.err, m.errorMessage)) return doc.String() } -func (m *ModelError) ViewMessage() string { +func (m *ModelStatus) viewMessage() string { doc := strings.Builder{} doc.WriteString(m.message) return doc.String() } - -func (m *ModelError) View() string { - var windowStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()) - - doc := strings.Builder{} - if m.HaveError() { - windowStyle = ts.WindowStyleError.Width(*hdltypes.ScreenWidth) - doc.WriteString(windowStyle.Render(m.ViewError())) - return doc.String() - } - - switch m.messageType { - case MessageTypeDefault: - windowStyle = ts.WindowStyleDefault.Width(*hdltypes.ScreenWidth) - case MessageTypeProgress: - windowStyle = ts.WindowStyleProgress.Width(*hdltypes.ScreenWidth) - case MessageTypeSuccess: - windowStyle = ts.WindowStyleSuccess.Width(*hdltypes.ScreenWidth) - default: - windowStyle = ts.WindowStyleDefault.Width(*hdltypes.ScreenWidth) - } - - doc.WriteString(windowStyle.Render(m.ViewMessage())) - - return doc.String() -} diff --git a/internal/terminal/handler/table.go b/internal/terminal/handler/table.go new file mode 100644 index 0000000..ce59527 --- /dev/null +++ b/internal/terminal/handler/table.go @@ -0,0 +1,39 @@ +package handler + +import "github.com/charmbracelet/bubbles/table" + +var tableColumnsGithubRepository = []table.Column{ + {Title: "Repository", Width: 24}, + {Title: "Default Branch", Width: 16}, + {Title: "Stars", Width: 6}, + {Title: "Workflows", Width: 9}, +} + +// --------------------------------------------------------------------------- + +var tableColumnsTrigger = []table.Column{ + {Title: "ID", Width: 2}, + {Title: "Type", Width: 6}, + {Title: "Key", Width: 24}, + {Title: "Default", Width: 16}, + //{Title: "Description", Width: 64}, + {Title: "Value", Width: 44}, +} + +// --------------------------------------------------------------------------- + +var tableColumnsWorkflow = []table.Column{ + {Title: "Workflow", Width: 32}, + {Title: "File", Width: 48}, +} + +// --------------------------------------------------------------------------- + +var tableColumnsWorkflowHistory = []table.Column{ + {Title: "Workflow", Width: 12}, + {Title: "Commit Message", Width: 16}, + {Title: "Triggered", Width: 12}, + {Title: "Started At", Width: 19}, + {Title: "Status", Width: 9}, + {Title: "Duration", Width: 8}, +} diff --git a/internal/terminal/handler/taboptions/taboptions.go b/internal/terminal/handler/taboptions/taboptions.go index ce9f29d..038e0c5 100644 --- a/internal/terminal/handler/taboptions/taboptions.go +++ b/internal/terminal/handler/taboptions/taboptions.go @@ -7,17 +7,17 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - hdlerror "github.com/termkit/gama/internal/terminal/handler/error" + "github.com/termkit/gama/internal/terminal/handler/status" ) type Options struct { Style lipgloss.Style - modelError *hdlerror.ModelError - previousModelError hdlerror.ModelError - modelLock bool + status *status.ModelStatus + previousStatus status.ModelStatus + modelLock bool - status OptionStatus + optionStatus OptionStatus options []string optionsAction []string @@ -48,7 +48,7 @@ func (o OptionStatus) String() string { return string(o) } -func NewOptions(modelError *hdlerror.ModelError) *Options { +func NewOptions(modelStatus *status.ModelStatus) *Options { var b = lipgloss.RoundedBorder() b.Right = "├" b.Left = "┤" @@ -73,80 +73,19 @@ func NewOptions(modelError *hdlerror.ModelError) *Options { options: initialOptions, optionsAction: initialOptionsAction, optionsWithFunc: optionsWithFunc, - status: OptionWait, - modelError: modelError, + optionStatus: OptionWait, + status: modelStatus, } } -func (o *Options) SetStatus(status OptionStatus) { - o.status = status - o.options[0] = status.String() - o.optionsAction[0] = status.String() -} - -func (o *Options) AddOption(option string, action func()) { - var optionWithNumber string - var optionNumber = len(o.options) - optionWithNumber = fmt.Sprintf("%d) %s", optionNumber, option) - o.options = append(o.options, optionWithNumber) - o.optionsAction = append(o.optionsAction, optionWithNumber) - o.optionsWithFunc[optionNumber] = action -} - -func (o *Options) getOptionMessage() string { - option := o.options[o.cursor] - option = strings.TrimPrefix(option, fmt.Sprintf("%d) ", o.cursor)) - return option -} - -func (o *Options) showAreYouSure() { - if !o.modelLock { - o.previousModelError = *o.modelError - o.modelLock = true - } - o.modelError.Reset() - o.modelError.SetProgressMessage(fmt.Sprintf("Are you sure you want to %s?", o.getOptionMessage())) -} - -func (o *Options) switchToPreviousError() { - if o.modelLock { - return - } - *o.modelError = o.previousModelError -} - -func (o *Options) executeOption() { - go o.optionsWithFunc[o.cursor]() - o.cursor = 0 - o.timer = -1 -} - func (o *Options) Init() tea.Cmd { return nil } -func (o *Options) resetOptionsWithOriginal() { - if o.isTabSelected { - return - } - o.isTabSelected = true - o.timer = 3 - for o.timer >= 0 { - o.optionsAction[0] = fmt.Sprintf("> %ds", o.timer) - time.Sleep(1 * time.Second) - o.timer-- - } - o.modelLock = false - o.switchToPreviousError() - o.optionsAction[0] = string(OptionIdle) - o.cursor = 0 - o.isTabSelected = false -} - -func (o *Options) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (o *Options) Update(msg tea.Msg) (*Options, tea.Cmd) { var cmd tea.Cmd - if o.status == OptionWait || o.status == OptionNone { + if o.optionStatus == OptionWait || o.optionStatus == OptionNone { return o, cmd } @@ -169,21 +108,14 @@ func (o *Options) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return o, cmd } -func (o *Options) updateCursor(cursor int) { - if cursor < len(o.options) { - o.cursor = cursor - o.showAreYouSure() - go o.resetOptionsWithOriginal() - } -} - func (o *Options) View() string { var style = o.Style.Foreground(lipgloss.Color("15")) var opts []string + opts = append(opts, " ") for i, option := range o.optionsAction { - switch o.status { + switch o.optionStatus { case OptionWait: style = style.BorderForeground(lipgloss.Color("208")) case OptionNone: @@ -202,3 +134,72 @@ func (o *Options) View() string { return lipgloss.JoinHorizontal(lipgloss.Top, opts...) } + +func (o *Options) resetOptionsWithOriginal() { + if o.isTabSelected { + return + } + o.isTabSelected = true + o.timer = 3 + for o.timer >= 0 { + o.optionsAction[0] = fmt.Sprintf("> %ds", o.timer) + time.Sleep(1 * time.Second) + o.timer-- + } + o.modelLock = false + o.switchToPreviousError() + o.optionsAction[0] = string(OptionIdle) + o.cursor = 0 + o.isTabSelected = false +} + +func (o *Options) updateCursor(cursor int) { + if cursor < len(o.options) { + o.cursor = cursor + o.showAreYouSure() + go o.resetOptionsWithOriginal() + } +} + +func (o *Options) SetStatus(status OptionStatus) { + o.optionStatus = status + o.options[0] = status.String() + o.optionsAction[0] = status.String() +} + +func (o *Options) AddOption(option string, action func()) { + var optionWithNumber string + var optionNumber = len(o.options) + optionWithNumber = fmt.Sprintf("%d) %s", optionNumber, option) + o.options = append(o.options, optionWithNumber) + o.optionsAction = append(o.optionsAction, optionWithNumber) + o.optionsWithFunc[optionNumber] = action +} + +func (o *Options) getOptionMessage() string { + option := o.options[o.cursor] + option = strings.TrimPrefix(option, fmt.Sprintf("%d) ", o.cursor)) + return option +} + +func (o *Options) showAreYouSure() { + if !o.modelLock { + o.previousStatus = *o.status + o.modelLock = true + } + o.status.Reset() + o.status.SetProgressMessage(fmt.Sprintf("Are you sure you want to %s?", o.getOptionMessage())) +} + +func (o *Options) switchToPreviousError() { + if o.modelLock { + return + } + *o.status = o.previousStatus +} + +func (o *Options) executeOption() { + go o.optionsWithFunc[o.cursor]() + o.cursor = 0 + o.timer = -1 +} diff --git a/internal/terminal/handler/types.go b/internal/terminal/handler/types.go new file mode 100644 index 0000000..c51a3d2 --- /dev/null +++ b/internal/terminal/handler/types.go @@ -0,0 +1,5 @@ +package handler + +// selfUpdateMsg is a message to trigger update & view +type selfUpdateMsg struct { +} diff --git a/internal/terminal/handler/types/style.go b/internal/terminal/handler/types/style.go new file mode 100644 index 0000000..72f4347 --- /dev/null +++ b/internal/terminal/handler/types/style.go @@ -0,0 +1,19 @@ +package types + +import ( + "github.com/charmbracelet/lipgloss" +) + +var ( + WindowStyleOrange = lipgloss.NewStyle().BorderForeground(lipgloss.Color("#ffaf00")).Border(lipgloss.RoundedBorder()) + WindowStyleRed = lipgloss.NewStyle().BorderForeground(lipgloss.Color("9")).Border(lipgloss.RoundedBorder()) + WindowStyleGreen = lipgloss.NewStyle().BorderForeground(lipgloss.Color("10")).Border(lipgloss.RoundedBorder()) + WindowStyleGray = lipgloss.NewStyle().BorderForeground(lipgloss.Color("240")).Border(lipgloss.RoundedBorder()) + WindowStyleWhite = lipgloss.NewStyle().BorderForeground(lipgloss.Color("255")).Border(lipgloss.RoundedBorder()) + + WindowStyleHelp = WindowStyleGray.Margin(0, 0, 0, 0).Padding(0, 2, 0, 2) + WindowStyleError = WindowStyleRed.Margin(0, 0, 0, 0).Padding(0, 2, 0, 2) + WindowStyleProgress = WindowStyleOrange.Margin(0, 0, 0, 0).Padding(0, 2, 0, 2) + WindowStyleSuccess = WindowStyleGreen.Margin(0, 0, 0, 0).Padding(0, 2, 0, 2) + WindowStyleDefault = WindowStyleWhite.Margin(0, 0, 0, 0).Padding(0, 2, 0, 2) +) diff --git a/internal/terminal/handler/types/types.go b/internal/terminal/handler/types/types.go index 8c9fcc3..b4df73f 100644 --- a/internal/terminal/handler/types/types.go +++ b/internal/terminal/handler/types/types.go @@ -1,9 +1,30 @@ package types +import ( + "sync" +) + type SelectedRepository struct { RepositoryName string // full repository name (owner/name) WorkflowName string // workflow name BranchName string // branch name } -var ScreenWidth *int +var ( + onceSelectedRepository sync.Once + selectedRepository *SelectedRepository +) + +func NewSelectedRepository() *SelectedRepository { + onceSelectedRepository.Do(func() { + selectedRepository = &SelectedRepository{} + }) + return selectedRepository +} + +// ---------------------------------------------- + +const ( + MinTerminalWidth = 102 + MinTerminalHeight = 24 +) diff --git a/internal/terminal/style/style.go b/internal/terminal/style/style.go deleted file mode 100644 index 254d5b5..0000000 --- a/internal/terminal/style/style.go +++ /dev/null @@ -1,47 +0,0 @@ -package style - -import ( - "github.com/charmbracelet/lipgloss" -) - -var ( - DocStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) - WindowStyleCyan = lipgloss.NewStyle().BorderForeground(lipgloss.Color("39")) - WindowStyleOrange = lipgloss.NewStyle().BorderForeground(lipgloss.Color("#ffaf00")).Border(lipgloss.RoundedBorder()) - WindowStyleRed = lipgloss.NewStyle().BorderForeground(lipgloss.Color("9")).Border(lipgloss.RoundedBorder()) - WindowStyleGreen = lipgloss.NewStyle().BorderForeground(lipgloss.Color("10")).Border(lipgloss.RoundedBorder()) - WindowStyleGray = lipgloss.NewStyle().BorderForeground(lipgloss.Color("240")).Border(lipgloss.NormalBorder()) - WindowStyleWhite = lipgloss.NewStyle().BorderForeground(lipgloss.Color("255")).Border(lipgloss.NormalBorder()) - WindowStyleYellow = lipgloss.NewStyle().BorderForeground(lipgloss.Color("11")).Border(lipgloss.NormalBorder()) - WindowStylePink = lipgloss.NewStyle().BorderForeground(lipgloss.Color("205")).Border(lipgloss.RoundedBorder()) - - WindowStyleHelp = WindowStyleGray.Margin(0, 0, 0, 1).Padding(0, 2, 0, 2).Border(lipgloss.RoundedBorder()) - WindowStyleError = WindowStyleRed.Margin(0, 0, 0, 1).Padding(0, 2, 0, 2).Border(lipgloss.RoundedBorder()) - WindowStyleProgress = WindowStyleOrange.Margin(0, 0, 0, 1).Padding(0, 2, 0, 2).Border(lipgloss.RoundedBorder()) - WindowStyleSuccess = WindowStyleGreen.Margin(0, 0, 0, 1).Padding(0, 2, 0, 2).Border(lipgloss.RoundedBorder()) - WindowStyleDefault = WindowStyleWhite.Margin(0, 0, 0, 1).Padding(0, 2, 0, 2).Border(lipgloss.RoundedBorder()) - WindowStyleOptionSelector = WindowStylePink.Margin(0, 0, 0, 1).Padding(0, 2, 0, 2).Border(lipgloss.RoundedBorder()) -) - -var ( - TitleStyleActive = func() lipgloss.Style { - b := lipgloss.DoubleBorder() - b.Right = "├" - b.Left = "┤" - return lipgloss.NewStyle().BorderStyle(b).Padding(0, 2).BorderForeground(lipgloss.Color("205")) - }() - - TitleStyleInactive = func() lipgloss.Style { - b := lipgloss.RoundedBorder() - b.Right = "├" - b.Left = "┤" - return lipgloss.NewStyle().BorderStyle(b).Padding(0, 2).BorderForeground(lipgloss.Color("255")) - }() - - TitleStyleDisabled = func() lipgloss.Style { - b := lipgloss.RoundedBorder() - b.Right = "├" - b.Left = "┤" - return lipgloss.NewStyle().BorderStyle(b).Padding(0, 2).BorderForeground(lipgloss.Color("240")).Foreground(lipgloss.Color("240")) - }() -) diff --git a/main.go b/main.go index 8b7a27a..6d7e1d5 100644 --- a/main.go +++ b/main.go @@ -2,14 +2,15 @@ package main import ( "fmt" - "github.com/termkit/gama/internal/config" - pkgversion "github.com/termkit/gama/pkg/version" "os" - tea "github.com/charmbracelet/bubbletea" + "github.com/termkit/gama/internal/config" gr "github.com/termkit/gama/internal/github/repository" gu "github.com/termkit/gama/internal/github/usecase" th "github.com/termkit/gama/internal/terminal/handler" + pkgversion "github.com/termkit/gama/pkg/version" + + tea "github.com/charmbracelet/bubbletea" ) const ( diff --git a/pkg/workflow/workflow.go b/pkg/workflow/workflow.go index bce1a0e..72f4957 100644 --- a/pkg/workflow/workflow.go +++ b/pkg/workflow/workflow.go @@ -49,15 +49,13 @@ type Choice struct { Value string } -// TODO: Add support for boolean - func ParseWorkflow(content py.WorkflowContent) (*Workflow, error) { var w = &Workflow{ Content: make(map[string]Content), } for key, value := range content.On.WorkflowDispatch.Inputs { - if value.JSONContent != nil && len(value.JSONContent) > 0 { + if len(value.JSONContent) > 0 { var keyValue []KeyValue for k, v := range value.JSONContent { keyValue = append(keyValue, KeyValue{