diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 60b99e3..5d2b915 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -17,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v3
with:
- go-version: '1.22'
+ go-version: '1.23'
- name: Get the latest tag
id: get_latest_tag
diff --git a/.gitignore b/.gitignore
index 8355cdf..a58caf4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ vendor/
# Go workspace file
go.work
+go.work.sum
# ide data
.idea
@@ -30,4 +31,4 @@ release/
# binary
main
-gama
\ No newline at end of file
+gama
diff --git a/README.md b/README.md
index 8a5feb6..fb042cd 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,17 @@ GAMA is a powerful terminal-based user interface tool designed to streamline the
+## Table of Contents
+- [Key Features](#key-features)
+- [Live Mode](#live-mode)
+- [Getting Started](#getting-started)
+ - [Prerequisites](#prerequisites)
+ - [Configuration](#configuration)
+- [Build & Installation](#build--installation)
+- [Contributing](#contributing)
+- [License](#license)
+- [Contact & Author](#contact--author)
+
![gama demo](docs/gama.gif)
## Key Features
@@ -14,6 +25,18 @@ GAMA is a powerful terminal-based user interface tool designed to streamline the
- **Workflow History**: Conveniently list all historical runs of workflows in a repository.
- **Discoverability**: Easily list all triggerable (dispatchable) workflows in a repository.
- **Workflow Management**: Trigger specific workflows with custom inputs.
+- **Live Updates**: Automatically refresh workflow status at configurable intervals.
+- **Docker Support**: Run directly from a container for easy deployment.
+
+### Live Mode
+
+GAMA includes a live mode feature that automatically refreshes the workflow status at regular intervals:
+
+- **Toggle Live Updates**: Press `ctrl+l` to turn live mode on/off
+- **Auto-start**: Set `settings.live_mode.enabled: true` to start GAMA with live mode enabled
+- **Refresh Interval**: Configure how often the view updates with `settings.live_mode.interval` (e.g., "15s", "1m")
+
+Live mode is particularly useful when monitoring ongoing workflow runs, as it eliminates the need for manual refreshing.
## Getting Started
@@ -36,8 +59,14 @@ keys:
switch_tab_left: shift+left
quit: ctrl+c
refresh: ctrl+r
+ live_mode: ctrl+l # Toggle live mode on/off
enter: enter
tab: tab
+
+settings:
+ live_mode:
+ enabled: true # Enable live mode at startup
+ interval: 15s # Refresh interval for live updates
```
#### Environment Variable Configuration
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..8b8f3c1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,54 +1,50 @@
module github.com/termkit/gama
-go 1.22.1
+go 1.23
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.1
+ github.com/charmbracelet/bubbles v0.20.0
+ github.com/charmbracelet/bubbletea v1.2.4
+ github.com/charmbracelet/lipgloss v1.0.0
github.com/spf13/viper v1.19.0
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.10.0
+ github.com/termkit/skeleton v0.2.0
+ golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329
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.6.0 // indirect
+ github.com/charmbracelet/x/term v0.2.1 // 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
+ github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
- github.com/magiconair/properties v1.8.7 // indirect
+ github.com/magiconair/properties v1.8.9 // 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/pelletier/go-toml/v2 v2.2.3 // 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/locafero v0.6.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/cast v1.7.1 // 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
+ golang.org/x/sync v0.10.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
diff --git a/go.sum b/go.sum
index 4c16123..a2f5c25 100644
--- a/go.sum
+++ b/go.sum
@@ -1,34 +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.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
+github.com/Masterminds/semver/v3 v3.3.1/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
+github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
+github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
+github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
+github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
+github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
+github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
+github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
+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.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
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=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -36,8 +25,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
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/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -46,92 +35,77 @@ 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=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
+github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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=
-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
-github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
+github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
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=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
-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/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
+github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/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.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=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.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=
+github.com/termkit/skeleton v0.1.3 h1:XiCqLDQXhtU4LhrgDKYHaPFRumngLPs9ssebL8WXHVI=
+github.com/termkit/skeleton v0.1.3/go.mod h1:KjHXehkpVm8i3pli9PTG+Lat2PrUBsw6QQe5kbgYXHs=
+github.com/termkit/skeleton v0.2.0 h1:Nbs7i5+vsouK25Gl4+vuMB1/7jokZ77HN2wV1IYgpBQ=
+github.com/termkit/skeleton v0.2.0/go.mod h1:KjHXehkpVm8i3pli9PTG+Lat2PrUBsw6QQe5kbgYXHs=
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=
+golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4=
+golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
+golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
+golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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..a4d7955 100644
--- a/internal/github/repository/repository.go
+++ b/internal/github/repository/repository.go
@@ -184,22 +184,31 @@ func (r *Repo) GetTriggerableWorkflows(ctx context.Context, repository string) (
return nil, err
}
- // Create a buffered channel for results and errors
- results := make(chan *Workflow, len(workflows.Workflows))
- errs := make(chan error, len(workflows.Workflows))
+ // Count how many workflows we'll actually process
+ var validWorkflowCount int
+ for _, workflow := range workflows.Workflows {
+ if strings.HasPrefix(workflow.Path, ".github/workflows/") {
+ validWorkflowCount++
+ }
+ }
- // Filter workflows to only include those that are dispatchable and manually triggerable
+ // Create buffered channels only for valid workflows
+ results := make(chan *Workflow, validWorkflowCount)
+ errs := make(chan error, validWorkflowCount)
+
+ // Only process workflows with valid paths
for _, workflow := range workflows.Workflows {
- go r.workerGetTriggerableWorkflows(ctx, repository, workflow, results, errs)
+ if strings.HasPrefix(workflow.Path, ".github/workflows/") {
+ go r.workerGetTriggerableWorkflows(ctx, repository, workflow, results, errs)
+ }
}
// Collect the results and errors
var result []Workflow
var resultErrs []error
- for range workflows.Workflows {
+ for i := 0; i < validWorkflowCount; i++ {
select {
case res := <-results:
- // append only triggerable (dispatch) workflows
if res != nil {
result = append(result, *res)
}
@@ -261,20 +270,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/ports.go b/internal/github/usecase/ports.go
index a0f706f..b825753 100644
--- a/internal/github/usecase/ports.go
+++ b/internal/github/usecase/ports.go
@@ -7,6 +7,7 @@ import (
type UseCase interface {
GetAuthUser(ctx context.Context) (*GetAuthUserOutput, error)
ListRepositories(ctx context.Context, input ListRepositoriesInput) (*ListRepositoriesOutput, error)
+ GetRepositoryBranches(ctx context.Context, input GetRepositoryBranchesInput) (*GetRepositoryBranchesOutput, error)
GetWorkflowHistory(ctx context.Context, input GetWorkflowHistoryInput) (*GetWorkflowHistoryOutput, error)
GetTriggerableWorkflows(ctx context.Context, input GetTriggerableWorkflowsInput) (*GetTriggerableWorkflowsOutput, error)
InspectWorkflow(ctx context.Context, input InspectWorkflowInput) (*InspectWorkflowOutput, error)
diff --git a/internal/github/usecase/types.go b/internal/github/usecase/types.go
index 84cc7f8..b398a8e 100644
--- a/internal/github/usecase/types.go
+++ b/internal/github/usecase/types.go
@@ -31,6 +31,23 @@ type ListRepositoriesOutput struct {
Repositories []GithubRepository
}
+// ------------------------------------------------------------
+
+type GetRepositoryBranchesInput struct {
+ Repository string
+}
+
+type GetRepositoryBranchesOutput struct {
+ Branches []GithubBranch
+}
+
+type GithubBranch struct {
+ Name string
+ IsDefault bool
+}
+
+// ------------------------------------------------------------
+
type GithubRepository struct {
Name string
Private bool
diff --git a/internal/github/usecase/usecase.go b/internal/github/usecase/usecase.go
index f2486fb..f278a7a 100644
--- a/internal/github/usecase/usecase.go
+++ b/internal/github/usecase/usecase.go
@@ -75,6 +75,42 @@ func (u useCase) ListRepositories(ctx context.Context, input ListRepositoriesInp
}, errors.Join(resultErrs...)
}
+func (u useCase) GetRepositoryBranches(ctx context.Context, input GetRepositoryBranchesInput) (*GetRepositoryBranchesOutput, error) {
+ // Get Repository to get the default branch
+ repository, err := u.githubRepository.GetRepository(ctx, input.Repository)
+ if err != nil {
+ return nil, err
+ }
+
+ var mainBranch = repository.DefaultBranch
+
+ branches, err := u.githubRepository.ListBranches(ctx, input.Repository)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(branches) == 0 {
+ return &GetRepositoryBranchesOutput{}, nil
+ }
+
+ var result = []GithubBranch{
+ {
+ Name: mainBranch,
+ IsDefault: true,
+ },
+ }
+
+ for _, branch := range branches {
+ result = append(result, GithubBranch{
+ Name: branch.Name,
+ })
+ }
+
+ return &GetRepositoryBranchesOutput{
+ Branches: result,
+ }, nil
+}
+
func (u useCase) workerListRepositories(ctx context.Context, repository gr.GithubRepository, results chan<- GithubRepository, errs chan<- error) {
getWorkflows, err := u.githubRepository.GetWorkflows(ctx, repository.FullName)
if err != nil {
@@ -213,21 +249,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 diff.Seconds() < 60 {
+ if status != "completed" {
+ diff = time.Since(localStartTime)
+ } else {
+ diff = localEndTime.Sub(localStartTime)
+ }
+
+ 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..c6fd829
--- /dev/null
+++ b/internal/terminal/handler/ghinformation.go
@@ -0,0 +1,221 @@
+package handler
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ gu "github.com/termkit/gama/internal/github/usecase"
+ pkgversion "github.com/termkit/gama/pkg/version"
+ "github.com/termkit/skeleton"
+)
+
+// -----------------------------------------------------------------------------
+// Model Definition
+// -----------------------------------------------------------------------------
+
+type ModelInfo struct {
+ // Core dependencies
+ skeleton *skeleton.Skeleton
+ github gu.UseCase
+ version pkgversion.Version
+
+ // UI Components
+ help help.Model
+ status *ModelStatus
+ keys githubInformationKeyMap
+
+ // Application state
+ logo string
+ releaseURL string
+ applicationDescription string
+ newVersionAvailableMsg string
+}
+
+// -----------------------------------------------------------------------------
+// Constructor & Initialization
+// -----------------------------------------------------------------------------
+
+func SetupModelInfo(s *skeleton.Skeleton, githubUseCase gu.UseCase, version pkgversion.Version) *ModelInfo {
+ const releaseURL = "https://github.com/termkit/gama/releases"
+
+ return &ModelInfo{
+ // Initialize core dependencies
+ skeleton: s,
+ github: githubUseCase,
+ version: version,
+
+ // Initialize UI components
+ help: help.New(),
+ status: SetupModelStatus(s),
+ keys: githubInformationKeys,
+
+ // Initialize application state
+ logo: defaultLogo,
+ releaseURL: releaseURL,
+ }
+}
+
+const defaultLogo = `
+ ..|'''.| | '|| ||' |
+.|' ' ||| ||| ||| |||
+|| .... | || |'|..'|| | ||
+'|. || .''''|. | '|' || .''''|.
+''|...'| .|. .||. .|. | .||. .|. .||.
+`
+
+// -----------------------------------------------------------------------------
+// Bubbletea Model Implementation
+// -----------------------------------------------------------------------------
+
+func (m *ModelInfo) Init() tea.Cmd {
+ m.initializeAppDescription()
+ m.startBackgroundTasks()
+
+ return tea.Batch(
+ tea.EnterAltScreen,
+ tea.SetWindowTitle("GitHub Actions Manager (GAMA)"),
+ )
+}
+
+func (m *ModelInfo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, m.keys.Quit):
+ return m, tea.Quit
+ }
+ }
+ return m, nil
+}
+
+func (m *ModelInfo) View() string {
+ return lipgloss.JoinVertical(lipgloss.Center,
+ m.renderMainContent(),
+ m.status.View(),
+ m.renderHelpWindow(),
+ )
+}
+
+// -----------------------------------------------------------------------------
+// UI Rendering
+// -----------------------------------------------------------------------------
+
+func (m *ModelInfo) renderMainContent() string {
+ content := strings.Builder{}
+
+ // Add vertical centering
+ centerPadding := m.calculateCenterPadding()
+ content.WriteString(strings.Repeat("\n", centerPadding))
+
+ // Add main content
+ content.WriteString(lipgloss.JoinVertical(lipgloss.Center,
+ m.logo,
+ m.applicationDescription,
+ m.newVersionAvailableMsg,
+ ))
+
+ // Add bottom padding
+ bottomPadding := m.calculateBottomPadding(content.String())
+ content.WriteString(strings.Repeat("\n", bottomPadding))
+
+ return content.String()
+}
+
+func (m *ModelInfo) renderHelpWindow() string {
+ helpStyle := WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4)
+ return helpStyle.Render(m.ViewHelp())
+}
+
+// -----------------------------------------------------------------------------
+// Layout Calculations
+// -----------------------------------------------------------------------------
+
+func (m *ModelInfo) calculateCenterPadding() int {
+ padding := m.skeleton.GetTerminalHeight()/2 - 11
+ return max(0, padding)
+}
+
+func (m *ModelInfo) calculateBottomPadding(content string) int {
+ padding := m.skeleton.GetTerminalHeight() - lipgloss.Height(content) - 12
+ return max(0, padding)
+}
+
+// -----------------------------------------------------------------------------
+// Application State Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelInfo) initializeAppDescription() {
+ m.applicationDescription = fmt.Sprintf("Github Actions Manager (%s)", m.version.CurrentVersion())
+}
+
+func (m *ModelInfo) startBackgroundTasks() {
+ go m.checkUpdates(context.Background())
+ go m.testConnection(context.Background())
+}
+
+// -----------------------------------------------------------------------------
+// Background Tasks
+// -----------------------------------------------------------------------------
+
+func (m *ModelInfo) checkUpdates(ctx context.Context) {
+ defer m.skeleton.TriggerUpdate()
+
+ isUpdateAvailable, version, err := m.version.IsUpdateAvailable(ctx)
+ if err != nil {
+ m.handleUpdateError(err)
+ 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.skeleton.TriggerUpdate()
+
+ m.status.SetProgressMessage("Checking your token...")
+ m.skeleton.LockTabs()
+
+ _, err := m.github.GetAuthUser(ctx)
+ if err != nil {
+ m.handleConnectionError(err)
+ return
+ }
+
+ m.handleSuccessfulConnection()
+}
+
+// -----------------------------------------------------------------------------
+// Error Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelInfo) handleUpdateError(err error) {
+ m.status.SetError(err)
+ m.status.SetErrorMessage("failed to check updates")
+ m.newVersionAvailableMsg = fmt.Sprintf(
+ "failed to check updates.\nPlease visit: %s",
+ m.releaseURL,
+ )
+}
+
+func (m *ModelInfo) handleConnectionError(err error) {
+ m.status.SetError(err)
+ m.status.SetErrorMessage("failed to test connection, please check your token&permission")
+ m.skeleton.LockTabs()
+}
+
+func (m *ModelInfo) handleSuccessfulConnection() {
+ m.status.Reset()
+ m.status.SetSuccessMessage("Welcome to GAMA!")
+ m.skeleton.UnlockTabs()
+}
diff --git a/internal/terminal/handler/ghrepository.go b/internal/terminal/handler/ghrepository.go
new file mode 100644
index 0000000..e876409
--- /dev/null
+++ b/internal/terminal/handler/ghrepository.go
@@ -0,0 +1,523 @@
+package handler
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/table"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/termkit/gama/internal/github/domain"
+ gu "github.com/termkit/gama/internal/github/usecase"
+ "github.com/termkit/gama/pkg/browser"
+ "github.com/termkit/skeleton"
+)
+
+// -----------------------------------------------------------------------------
+// Model Definition
+// -----------------------------------------------------------------------------
+
+type ModelGithubRepository struct {
+ // Core dependencies
+ skeleton *skeleton.Skeleton
+ github gu.UseCase
+
+ // UI State
+ tableReady bool
+
+ // Context management
+ syncRepositoriesContext context.Context
+ cancelSyncRepositories context.CancelFunc
+
+ // Shared state
+ selectedRepository *SelectedRepository
+
+ // UI Components
+ help help.Model
+ Keys githubRepositoryKeyMap
+ tableGithubRepository table.Model
+ searchTableGithubRepository table.Model
+ status *ModelStatus
+ textInput textinput.Model
+ modelTabOptions *ModelTabOptions
+}
+
+// -----------------------------------------------------------------------------
+// Constructor & Initialization
+// -----------------------------------------------------------------------------
+
+func SetupModelGithubRepository(s *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubRepository {
+ m := &ModelGithubRepository{
+ // Initialize core dependencies
+ skeleton: s,
+ github: githubUseCase,
+
+ // Initialize UI components
+ help: help.New(),
+ Keys: githubRepositoryKeys,
+ status: SetupModelStatus(s),
+ textInput: setupTextInput(),
+ modelTabOptions: NewOptions(s, SetupModelStatus(s)),
+
+ // Initialize state
+ selectedRepository: NewSelectedRepository(),
+ syncRepositoriesContext: context.Background(),
+ cancelSyncRepositories: func() {},
+ }
+
+ // Setup tables
+ m.tableGithubRepository = setupMainTable()
+ m.searchTableGithubRepository = setupSearchTable()
+
+ return m
+}
+
+func setupMainTable() table.Model {
+ t := table.New(
+ table.WithColumns(tableColumnsGithubRepository),
+ table.WithRows([]table.Row{}),
+ table.WithFocused(true),
+ table.WithHeight(13),
+ )
+
+ // Apply styles
+ t.SetStyles(defaultTableStyles())
+
+ // Apply keymap
+ t.KeyMap = defaultTableKeyMap()
+
+ return t
+}
+
+func setupSearchTable() table.Model {
+ return table.New(
+ table.WithColumns(tableColumnsGithubRepository),
+ table.WithRows([]table.Row{}),
+ )
+}
+
+func setupTextInput() textinput.Model {
+ ti := textinput.New()
+ ti.Blur()
+ ti.CharLimit = 128
+ ti.Placeholder = "Type to search repository"
+ ti.ShowSuggestions = false
+ return ti
+}
+
+func defaultTableStyles() table.Styles {
+ 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)
+ return s
+}
+
+func defaultTableKeyMap() table.KeyMap {
+ // We use "up" and "down" for both line up and down, we do not use "k" and "j" to prevent conflict with text input
+ return table.KeyMap{
+ LineUp: key.NewBinding(key.WithKeys("up"), key.WithHelp("↑", "up")),
+ LineDown: key.NewBinding(key.WithKeys("down"), key.WithHelp("↓", "down")),
+ PageUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("pgup", "page up")),
+ PageDown: key.NewBinding(key.WithKeys("pgdown", " "), key.WithHelp("pgdn", "page down")),
+ GotoTop: key.NewBinding(key.WithKeys("home"), key.WithHelp("home", "go to start")),
+ GotoBottom: key.NewBinding(key.WithKeys("end"), key.WithHelp("end", "go to end")),
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Bubbletea Model Implementation
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) Init() tea.Cmd {
+ m.setupBrowserOption()
+ go m.syncRepositories(m.syncRepositoriesContext)
+
+ return tea.Tick(time.Millisecond*100, func(t time.Time) tea.Msg {
+ return initSyncMsg{}
+ })
+}
+
+func (m *ModelGithubRepository) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ inputMsg := msg
+ switch msg := msg.(type) {
+ case initSyncMsg:
+ m.modelTabOptions.SetStatus(StatusIdle)
+ m.tableGithubRepository.SetCursor(0)
+ return m, nil
+ case tea.KeyMsg:
+ // Handle number keys for tab options
+ if m.isNumber(msg.String()) {
+ inputMsg = tea.KeyMsg{}
+ }
+
+ // Handle refresh key
+ if key.Matches(msg, m.Keys.Refresh) {
+ m.tableReady = false
+ m.cancelSyncRepositories()
+ m.syncRepositoriesContext, m.cancelSyncRepositories = context.WithCancel(context.Background())
+ go m.syncRepositories(m.syncRepositoriesContext)
+ return m, nil
+ }
+
+ // Handle character input for search
+ if m.isCharAndSymbol(msg.Runes) {
+ m.resetTableCursors()
+ }
+ }
+
+ // Update text input and search functionality
+ if cmd := m.updateTextInput(inputMsg); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+
+ // Update main table and handle row selection
+ if cmd := m.updateTable(msg); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+
+ m.modelTabOptions, cmd = m.modelTabOptions.Update(msg)
+ cmds = append(cmds, cmd)
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *ModelGithubRepository) updateTextInput(msg tea.Msg) tea.Cmd {
+ var cmd tea.Cmd
+ m.textInput, cmd = m.textInput.Update(msg)
+ m.updateTableRowsBySearchBar()
+ return cmd
+}
+
+func (m *ModelGithubRepository) updateTable(msg tea.Msg) tea.Cmd {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ // Update main table
+ m.tableGithubRepository, cmd = m.tableGithubRepository.Update(msg)
+ cmds = append(cmds, cmd)
+
+ // Update search table
+ m.searchTableGithubRepository, cmd = m.searchTableGithubRepository.Update(msg)
+ cmds = append(cmds, cmd)
+
+ // Handle table selection
+ m.handleTableInputs(m.syncRepositoriesContext)
+
+ m.modelTabOptions, cmd = m.modelTabOptions.Update(msg)
+ cmds = append(cmds, cmd)
+
+ return tea.Batch(cmds...)
+}
+
+func (m *ModelGithubRepository) View() string {
+ return lipgloss.JoinVertical(lipgloss.Top,
+ m.renderTable(),
+ m.renderSearchBar(),
+ m.modelTabOptions.View(),
+ m.status.View(),
+ m.renderHelp(),
+ )
+}
+
+// -----------------------------------------------------------------------------
+// UI Rendering
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) renderTable() string {
+ baseStyle := lipgloss.NewStyle().
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#3b698f")).
+ MarginLeft(1)
+
+ // Update table dimensions
+ m.updateTableDimensions()
+
+ return baseStyle.Render(m.tableGithubRepository.View())
+}
+
+func (m *ModelGithubRepository) renderSearchBar() string {
+ style := lipgloss.NewStyle().
+ Border(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#3b698f")).
+ Padding(0, 1).
+ Width(m.skeleton.GetTerminalWidth() - 6).
+ MarginLeft(1)
+
+ if len(m.textInput.Value()) > 0 {
+ style = style.BorderForeground(lipgloss.Color("39"))
+ }
+
+ return style.Render(m.textInput.View())
+}
+
+func (m *ModelGithubRepository) renderHelp() string {
+ helpStyle := WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4)
+ return helpStyle.Render(m.ViewHelp())
+}
+
+// -----------------------------------------------------------------------------
+// Data Synchronization
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) syncRepositories(ctx context.Context) {
+ defer m.skeleton.TriggerUpdate()
+
+ m.status.Reset()
+ m.status.SetProgressMessage("Fetching repositories...")
+ m.clearTables()
+
+ // Add timeout to prevent hanging
+ ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+
+ repos, err := m.fetchRepositories(ctx)
+ if err != nil {
+ m.handleFetchError(err)
+ return
+ }
+
+ m.updateRepositoryData(repos)
+}
+
+func (m *ModelGithubRepository) fetchRepositories(ctx context.Context) (*gu.ListRepositoriesOutput, error) {
+ return m.github.ListRepositories(ctx, gu.ListRepositoriesInput{
+ Limit: 100,
+ Page: 5,
+ Sort: domain.SortByUpdated,
+ })
+}
+
+// -----------------------------------------------------------------------------
+// Table Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) clearTables() {
+ m.tableGithubRepository.SetRows([]table.Row{})
+ m.searchTableGithubRepository.SetRows([]table.Row{})
+}
+
+func (m *ModelGithubRepository) updateTableDimensions() {
+ const minTableWidth = 60 // Minimum width to maintain readability
+ const tablePadding = 14 // Account for borders and margins
+
+ var tableWidth int
+ for _, t := range tableColumnsGithubRepository {
+ tableWidth += t.Width
+ }
+
+ termWidth := m.skeleton.GetTerminalWidth()
+ if termWidth <= minTableWidth {
+ return // Prevent table from becoming too narrow
+ }
+
+ newTableColumns := make([]table.Column, len(tableColumnsGithubRepository))
+ copy(newTableColumns, tableColumnsGithubRepository)
+
+ widthDiff := termWidth - tableWidth - tablePadding
+ if widthDiff > 0 {
+ // Add extra width to repository name column
+ newTableColumns[0].Width += widthDiff
+ m.tableGithubRepository.SetColumns(newTableColumns)
+
+ // Adjust height while maintaining some padding
+ maxHeight := m.skeleton.GetTerminalHeight() - 20
+ if maxHeight > 0 {
+ m.tableGithubRepository.SetHeight(maxHeight)
+ }
+ }
+}
+
+func (m *ModelGithubRepository) resetTableCursors() {
+ m.tableGithubRepository.GotoTop()
+ m.tableGithubRepository.SetCursor(0)
+ m.searchTableGithubRepository.GotoTop()
+ m.searchTableGithubRepository.SetCursor(0)
+}
+
+// -----------------------------------------------------------------------------
+// Repository Data Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) updateRepositoryData(repos *gu.ListRepositoriesOutput) {
+ if len(repos.Repositories) == 0 {
+ m.modelTabOptions.SetStatus(StatusNone)
+ m.status.SetDefaultMessage("No repositories found")
+ m.textInput.Blur()
+ return
+ }
+
+ m.skeleton.UpdateWidgetValue("repositories", fmt.Sprintf("Repository Count: %d", len(repos.Repositories)))
+ m.updateTableRows(repos.Repositories)
+ m.finalizeTableUpdate()
+}
+
+func (m *ModelGithubRepository) updateTableRows(repositories []gu.GithubRepository) {
+ rows := make([]table.Row, 0, len(repositories))
+ for _, repo := range repositories {
+ rows = append(rows, table.Row{
+ repo.Name,
+ repo.DefaultBranch,
+ strconv.Itoa(repo.Stars),
+ strconv.Itoa(len(repo.Workflows)),
+ })
+ }
+
+ m.tableGithubRepository.SetRows(rows)
+ m.searchTableGithubRepository.SetRows(rows)
+}
+
+func (m *ModelGithubRepository) finalizeTableUpdate() {
+ m.tableGithubRepository.SetCursor(0)
+ m.searchTableGithubRepository.SetCursor(0)
+ m.tableReady = true
+ m.textInput.Focus()
+ m.status.SetSuccessMessage("Repositories fetched")
+
+ m.skeleton.TriggerUpdateWithMsg(initSyncMsg{})
+}
+
+// -----------------------------------------------------------------------------
+// Search Functionality
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) updateTableRowsBySearchBar() {
+ searchValue := strings.ToLower(m.textInput.Value())
+ if searchValue == "" {
+ // If search is empty, restore original rows
+ m.tableGithubRepository.SetRows(m.searchTableGithubRepository.Rows())
+ return
+ }
+
+ rows := m.searchTableGithubRepository.Rows()
+ filteredRows := make([]table.Row, 0, len(rows))
+
+ for _, row := range rows {
+ if strings.Contains(strings.ToLower(row[0]), searchValue) {
+ filteredRows = append(filteredRows, row)
+ }
+ }
+
+ m.tableGithubRepository.SetRows(filteredRows)
+ if len(filteredRows) == 0 {
+ m.clearSelectedRepository()
+ }
+}
+
+func (m *ModelGithubRepository) clearSelectedRepository() {
+ m.selectedRepository.RepositoryName = ""
+ m.selectedRepository.BranchName = ""
+ m.selectedRepository.WorkflowName = ""
+}
+
+// -----------------------------------------------------------------------------
+// Input Validation & Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) isNumber(s string) bool {
+ _, err := strconv.Atoi(s)
+ return err == nil
+}
+
+func (m *ModelGithubRepository) isCharAndSymbol(r []rune) bool {
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_./"
+ for _, c := range r {
+ if strings.ContainsRune(chars, c) {
+ return true
+ }
+ }
+ return false
+}
+
+// -----------------------------------------------------------------------------
+// Browser Integration
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) setupBrowserOption() {
+ openInBrowser := func() {
+ m.status.SetProgressMessage("Opening in browser...")
+
+ url := fmt.Sprintf("https://github.com/%s", m.selectedRepository.RepositoryName)
+ if err := browser.OpenInBrowser(url); err != nil {
+ m.status.SetError(err)
+ m.status.SetErrorMessage(fmt.Sprintf("Cannot open in browser: %v", err))
+ return
+ }
+
+ m.status.SetSuccessMessage("Opened in browser")
+ }
+
+ m.modelTabOptions.AddOption("Open in browser", openInBrowser)
+}
+
+// -----------------------------------------------------------------------------
+// Error Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) handleFetchError(err error) {
+ if errors.Is(err, context.Canceled) {
+ m.status.SetDefaultMessage("Repository fetch cancelled")
+ return
+ }
+ if errors.Is(err, context.DeadlineExceeded) {
+ m.status.SetErrorMessage("Repository fetch timed out")
+ return
+ }
+
+ m.status.SetError(err)
+ m.status.SetErrorMessage(fmt.Sprintf("Failed to list repositories: %v", err))
+}
+
+// -----------------------------------------------------------------------------
+// Table Selection Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubRepository) handleTableInputs(_ context.Context) {
+ if !m.tableReady {
+ return
+ }
+
+ selectedRow := m.tableGithubRepository.SelectedRow()
+ if len(selectedRow) > 0 && selectedRow[0] != "" {
+ m.updateSelectedRepository(selectedRow)
+ }
+}
+
+func (m *ModelGithubRepository) updateSelectedRepository(row []string) {
+ m.selectedRepository.RepositoryName = row[0]
+ m.selectedRepository.BranchName = row[1]
+
+ if workflowCount := row[3]; workflowCount != "" {
+ m.handleWorkflowTabLocking(workflowCount)
+ }
+}
+
+func (m *ModelGithubRepository) handleWorkflowTabLocking(workflowCount string) {
+ count, _ := strconv.Atoi(workflowCount)
+ if count == 0 {
+ m.skeleton.LockTab("workflow")
+ m.skeleton.LockTab("trigger")
+ } else {
+ m.skeleton.UnlockTab("workflow")
+ m.skeleton.UnlockTab("trigger")
+ }
+}
+
+// initSyncMsg is a message type used to trigger a UI update after the initial sync
+type initSyncMsg struct{}
diff --git a/internal/terminal/handler/ghrepository/ghrepository.go b/internal/terminal/handler/ghrepository/ghrepository.go
deleted file mode 100644
index 75892ea..0000000
--- a/internal/terminal/handler/ghrepository/ghrepository.go
+++ /dev/null
@@ -1,352 +0,0 @@
-package ghrepository
-
-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/taboptions"
- hdltypes "github.com/termkit/gama/internal/terminal/handler/types"
- "github.com/termkit/gama/pkg/browser"
-)
-
-type ModelGithubRepository struct {
- // current handler's properties
- syncRepositoriesContext context.Context
- cancelSyncRepositories context.CancelFunc
- tableReady bool
-
- // shared properties
- SelectedRepository *hdltypes.SelectedRepository
-
- // use cases
- github gu.UseCase
-
- // keymap
- Keys keyMap
-
- // models
- Help help.Model
- Viewport *viewport.Model
- tableGithubRepository table.Model
- searchTableGithubRepository table.Model
- modelError *hdlerror.ModelError
-
- modelTabOptions tea.Model
- actualModelTabOptions *taboptions.Options
-
- textInput textinput.Model
-}
-
-var baseStyle = lipgloss.NewStyle().
- BorderStyle(lipgloss.NormalBorder()).
- BorderForeground(lipgloss.Color("240"))
-
-func SetupModelGithubRepository(githubUseCase gu.UseCase, selectedRepository *hdltypes.SelectedRepository) *ModelGithubRepository {
- var tableRowsGithubRepository []table.Row
-
- tableGithubRepository := table.New(
- table.WithColumns(tableColumnsGithubRepository),
- table.WithRows(tableRowsGithubRepository),
- table.WithFocused(true),
- table.WithHeight(13),
- )
-
- 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)
- tableGithubRepository.SetStyles(s)
-
- tableGithubRepository.KeyMap = table.KeyMap{
- LineUp: key.NewBinding(
- key.WithKeys("up"),
- key.WithHelp("↑", "up"),
- ),
- LineDown: key.NewBinding(
- key.WithKeys("down"),
- key.WithHelp("↓", "down"),
- ),
- PageUp: key.NewBinding(
- key.WithKeys("pgup"),
- key.WithHelp("pgup", "page up"),
- ),
- PageDown: key.NewBinding(
- key.WithKeys("pgdown", " "),
- key.WithHelp("pgdn", "page down"),
- ),
- GotoTop: key.NewBinding(
- key.WithKeys("home"),
- key.WithHelp("home", "go to start"),
- ),
- GotoBottom: key.NewBinding(
- key.WithKeys("end"),
- key.WithHelp("end", "go to end"),
- ),
- }
-
- ti := textinput.New()
- ti.Blur()
- ti.CharLimit = 72
- ti.Placeholder = "Type to search repository"
- ti.ShowSuggestions = false // disable suggestions, it will be enabled future.
-
- // setup models
- modelError := hdlerror.SetupModelError()
- tabOptions := taboptions.NewOptions(&modelError)
-
- return &ModelGithubRepository{
- Help: help.New(),
- Keys: keys,
- github: githubUseCase,
- tableGithubRepository: tableGithubRepository,
- modelError: &modelError,
- SelectedRepository: selectedRepository,
- modelTabOptions: tabOptions,
- actualModelTabOptions: tabOptions,
- textInput: ti,
- syncRepositoriesContext: context.Background(),
- cancelSyncRepositories: func() {},
- }
-}
-
-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) 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
- }
-
- 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
- }
-
- // To avoid go routine leak
- selectedRow := m.tableGithubRepository.SelectedRow()
-
- // Synchronize selected repository name with parent model
- if len(selectedRow) > 0 && selectedRow[0] != "" {
- m.SelectedRepository.RepositoryName = selectedRow[0]
- m.SelectedRepository.BranchName = selectedRow[1]
- }
-
- m.actualModelTabOptions.SetStatus(taboptions.OptionIdle)
-}
-
-func (m *ModelGithubRepository) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- var cmd tea.Cmd
-
- var textInputMsg = msg
- switch msg := msg.(type) {
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, m.Keys.Refresh):
- m.tableReady = false // reset table ready status
- m.cancelSyncRepositories() // cancel previous sync
- m.syncRepositoriesContext, m.cancelSyncRepositories = context.WithCancel(context.Background())
- go m.syncRepositories(m.syncRepositoriesContext)
- case msg.String() == " " || m.isNumber(msg.String()):
- textInputMsg = tea.KeyMsg{}
- case m.isCharAndSymbol(msg.Runes):
- m.tableGithubRepository.GotoTop()
- m.tableGithubRepository.SetCursor(0)
- m.searchTableGithubRepository.GotoTop()
- m.searchTableGithubRepository.SetCursor(0)
- }
- }
-
- m.textInput, cmd = m.textInput.Update(textInputMsg)
- cmds = append(cmds, cmd)
-
- m.updateTableRowsBySearchBar()
-
- m.tableGithubRepository, cmd = m.tableGithubRepository.Update(msg)
- cmds = append(cmds, cmd)
-
- m.searchTableGithubRepository, cmd = m.searchTableGithubRepository.Update(msg)
- cmds = append(cmds, cmd)
-
- m.modelTabOptions, cmd = m.modelTabOptions.Update(msg)
- cmds = append(cmds, cmd)
-
- m.handleTableInputs(m.syncRepositoriesContext)
-
- return m, tea.Batch(cmds...)
-}
-
-func (m *ModelGithubRepository) View() string {
- termWidth := m.Viewport.Width
- termHeight := m.Viewport.Height
-
- var tableWidth int
- for _, t := range tableColumnsGithubRepository {
- tableWidth += t.Width
- }
-
- newTableColumns := tableColumnsGithubRepository
- widthDiff := termWidth - tableWidth
- if widthDiff > 0 {
- newTableColumns[0].Width += widthDiff - 15
- m.tableGithubRepository.SetColumns(newTableColumns)
- m.tableGithubRepository.SetHeight(termHeight - 20)
- }
-
- doc := strings.Builder{}
- doc.WriteString(baseStyle.Render(m.tableGithubRepository.View()))
-
- return lipgloss.JoinVertical(lipgloss.Top, doc.String(), m.viewSearchBar(), m.actualModelTabOptions.View())
-}
-
-func (m *ModelGithubRepository) viewSearchBar() string {
- // Define window style
- windowStyle := lipgloss.NewStyle().
- Border(lipgloss.NormalBorder()).
- Padding(0, 1).
- Width(*hdltypes.ScreenWidth - 2)
-
- // Build the options list
- doc := strings.Builder{}
-
- if len(m.textInput.Value()) > 0 {
- windowStyle = windowStyle.BorderForeground(lipgloss.Color("39"))
- }
-
- doc.WriteString(m.textInput.View())
-
- return windowStyle.Render(doc.String())
-}
-
-func (m *ModelGithubRepository) updateSearchBarSuggestions() {
- m.textInput.SetSuggestions([]string{})
-
- var suggestions = make([]string, 0, len(m.tableGithubRepository.Rows()))
- for _, r := range m.tableGithubRepository.Rows() {
- suggestions = append(suggestions, r[0])
- }
-
- m.textInput.SetSuggestions(suggestions)
-}
-
-func (m *ModelGithubRepository) updateTableRowsBySearchBar() {
- var tableRowsGithubRepository = make([]table.Row, 0, len(m.tableGithubRepository.Rows()))
-
- for _, r := range m.searchTableGithubRepository.Rows() {
- if strings.Contains(r[0], m.textInput.Value()) {
- tableRowsGithubRepository = append(tableRowsGithubRepository, r)
- }
- }
-
- if len(tableRowsGithubRepository) == 0 {
- m.SelectedRepository.RepositoryName = ""
- m.SelectedRepository.BranchName = ""
- m.SelectedRepository.WorkflowName = ""
- }
-
- m.tableGithubRepository.SetRows(tableRowsGithubRepository)
-}
-
-func (m *ModelGithubRepository) isNumber(s string) bool {
- if _, err := strconv.Atoi(s); err == nil {
- return true
- }
-
- return false
-}
-
-func (m *ModelGithubRepository) isCharAndSymbol(r []rune) bool {
- const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_./"
- for _, c := range r {
- if strings.ContainsRune(chars, c) {
- return true
- }
- }
-
- 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.go b/internal/terminal/handler/ghtrigger.go
new file mode 100644
index 0000000..e9d8392
--- /dev/null
+++ b/internal/terminal/handler/ghtrigger.go
@@ -0,0 +1,910 @@
+package handler
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/table"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ gu "github.com/termkit/gama/internal/github/usecase"
+ "github.com/termkit/gama/pkg/workflow"
+ "github.com/termkit/skeleton"
+ "golang.org/x/exp/slices"
+)
+
+// -----------------------------------------------------------------------------
+// Model Definition
+// -----------------------------------------------------------------------------
+
+type ModelGithubTrigger struct {
+ // Core dependencies
+ skeleton *skeleton.Skeleton
+ github gu.UseCase
+
+ // UI Components
+ help help.Model
+ Keys githubTriggerKeyMap
+ tableTrigger table.Model
+ status *ModelStatus
+ textInput textinput.Model
+
+ // Workflow state
+ workflowContent *workflow.Pretty
+ selectedWorkflow string
+ selectedRepositoryName string
+ isTriggerable bool
+
+ // Table state
+ tableReady bool
+
+ // Option state
+ optionInit bool
+ optionCursor int
+ optionValues []string
+ currentOption string
+
+ // Input state
+ triggerFocused bool
+
+ // Context management
+ syncWorkflowContext context.Context
+ cancelSyncWorkflow context.CancelFunc
+
+ // Shared state
+ selectedRepository *SelectedRepository
+
+ // Track last branch for refresh
+ lastBranch string
+}
+
+// -----------------------------------------------------------------------------
+// Constructor & Initialization
+// -----------------------------------------------------------------------------
+
+func SetupModelGithubTrigger(s *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubTrigger {
+ m := &ModelGithubTrigger{
+ // Initialize core dependencies
+ skeleton: s,
+ github: githubUseCase,
+
+ // Initialize UI components
+ help: help.New(),
+ Keys: githubTriggerKeys,
+ status: SetupModelStatus(s),
+ textInput: setupTriggerInput(),
+ tableTrigger: setupTriggerTable(),
+
+ // Initialize state
+ selectedRepository: NewSelectedRepository(),
+ syncWorkflowContext: context.Background(),
+ cancelSyncWorkflow: func() {},
+ }
+
+ return m
+}
+
+func setupTriggerInput() textinput.Model {
+ ti := textinput.New()
+ ti.Blur()
+ ti.CharLimit = 72
+ return ti
+}
+
+func setupTriggerTable() table.Model {
+ t := table.New(
+ table.WithColumns(tableColumnsTrigger),
+ table.WithRows([]table.Row{}),
+ table.WithFocused(true),
+ table.WithHeight(7),
+ )
+
+ // Apply styles
+ 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)
+ t.SetStyles(s)
+
+ return t
+}
+
+// -----------------------------------------------------------------------------
+// Bubbletea Model Implementation
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) Init() tea.Cmd {
+ return tea.Batch(textinput.Blink)
+}
+
+func (m *ModelGithubTrigger) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ if cmd := m.handleWorkflowChange(); cmd != nil {
+ return m, cmd
+ }
+
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ // Handle key messages
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ cmd = m.handleKeyMsg(msg)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+
+ // Update UI components
+ if cmd = m.updateUIComponents(msg); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *ModelGithubTrigger) View() string {
+ return lipgloss.JoinVertical(lipgloss.Top,
+ m.renderTable(),
+ m.renderInputArea(),
+ m.status.View(),
+ m.renderHelp(),
+ )
+}
+
+// -----------------------------------------------------------------------------
+// Workflow Change Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) handleWorkflowChange() tea.Cmd {
+ if m.selectedRepository.WorkflowName == "" {
+ m.handleNoWorkflow()
+ return nil
+ }
+
+ if m.shouldSyncWorkflow() {
+ m.lastBranch = m.selectedRepository.BranchName
+ return m.initializeWorkflowSync()
+ }
+
+ return nil
+}
+
+func (m *ModelGithubTrigger) handleNoWorkflow() {
+ m.status.Reset()
+ m.status.SetDefaultMessage("No workflow selected.")
+ m.fillTableWithEmptyMessage()
+}
+
+func (m *ModelGithubTrigger) shouldSyncWorkflow() bool {
+ return m.selectedRepository.WorkflowName != "" &&
+ (m.selectedRepository.WorkflowName != m.selectedWorkflow ||
+ m.selectedRepository.RepositoryName != m.selectedRepositoryName ||
+ m.lastBranch != m.selectedRepository.BranchName)
+}
+
+func (m *ModelGithubTrigger) initializeWorkflowSync() tea.Cmd {
+ m.tableReady = false
+ m.isTriggerable = false
+ m.triggerFocused = false
+
+ m.cancelSyncWorkflow()
+
+ m.selectedWorkflow = m.selectedRepository.WorkflowName
+ m.selectedRepositoryName = m.selectedRepository.RepositoryName
+ m.syncWorkflowContext, m.cancelSyncWorkflow = context.WithCancel(context.Background())
+
+ go m.syncWorkflowContent(m.syncWorkflowContext)
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+// Key & Input Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
+ switch {
+ case msg.String() == "up":
+ return m.handleUpKey()
+ case msg.String() == "down":
+ return m.handleDownKey()
+ case key.Matches(msg, m.Keys.Refresh):
+ go m.syncWorkflowContent(m.syncWorkflowContext)
+ case msg.String() == "left":
+ m.handleLeftKey()
+ case msg.String() == "right":
+ m.handleRightKey()
+ case msg.String() == "tab":
+ m.handleTabKey()
+ case msg.String() == "enter":
+ if m.triggerFocused && m.isTriggerable {
+ go m.triggerWorkflow()
+ }
+ }
+ return nil
+}
+
+func (m *ModelGithubTrigger) handleUpKey() tea.Cmd {
+ if len(m.tableTrigger.Rows()) > 0 && !m.triggerFocused {
+ m.tableTrigger.MoveUp(1)
+ m.switchBetweenInputAndTable()
+ m.optionInit = false
+ }
+ return nil
+}
+
+func (m *ModelGithubTrigger) handleDownKey() tea.Cmd {
+ if len(m.tableTrigger.Rows()) > 0 && !m.triggerFocused {
+ m.tableTrigger.MoveDown(1)
+ m.switchBetweenInputAndTable()
+ m.optionInit = false
+ }
+ return nil
+}
+
+func (m *ModelGithubTrigger) handleLeftKey() {
+ if !m.triggerFocused {
+ m.optionCursor = max(m.optionCursor-1, 0)
+ }
+}
+
+func (m *ModelGithubTrigger) handleRightKey() {
+ if !m.triggerFocused {
+ m.optionCursor = min(m.optionCursor+1, len(m.optionValues)-1)
+ }
+}
+
+func (m *ModelGithubTrigger) handleTabKey() {
+ if m.isTriggerable {
+ m.triggerFocused = !m.triggerFocused
+ if m.triggerFocused {
+ m.tableTrigger.Blur()
+ m.textInput.Blur()
+ m.showInformationIfAnyEmptyValue()
+ } else {
+ m.tableTrigger.Focus()
+ m.textInput.Focus()
+ }
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Input Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) switchBetweenInputAndTable() {
+ selectedRow := m.tableTrigger.SelectedRow()
+ if len(selectedRow) == 0 {
+ return
+ }
+
+ if selectedRow[1] == "input" || selectedRow[1] == "bool" {
+ m.textInput.Focus()
+ m.tableTrigger.Blur()
+ } else {
+ m.textInput.Blur()
+ m.tableTrigger.Focus()
+ }
+
+ m.textInput.SetValue(selectedRow[4])
+ m.textInput.SetCursor(len(m.textInput.Value()))
+}
+
+func (m *ModelGithubTrigger) inputController() {
+ if m.workflowContent == nil {
+ return
+ }
+
+ selectedRow := m.tableTrigger.SelectedRow()
+ if len(selectedRow) == 0 {
+ return
+ }
+
+ switch selectedRow[1] {
+ case "choice", "bool":
+ m.handleChoiceInput(selectedRow)
+ default:
+ m.handleTextInput()
+ }
+}
+
+func (m *ModelGithubTrigger) handleChoiceInput(row []string) {
+ var optionValues []string
+ if row[1] == "choice" {
+ optionValues = m.getChoiceValues(row[0])
+ } else {
+ optionValues = m.getBooleanValues(row[0])
+ }
+
+ m.optionValues = optionValues
+ if !m.optionInit {
+ for i, option := range m.optionValues {
+ if option == row[4] {
+ m.optionCursor = i
+ }
+ }
+ }
+ m.optionInit = true
+
+ state := &InputState{
+ Type: row[1],
+ Options: m.optionValues,
+ Cursor: m.optionCursor,
+ }
+ m.updateInputState(state)
+}
+
+func (m *ModelGithubTrigger) handleTextInput() {
+ m.optionValues = nil
+ m.optionCursor = 0
+
+ if !m.triggerFocused {
+ m.textInput.Focus()
+ }
+
+ if m.textInput.Focused() {
+ state := &InputState{
+ Type: "input",
+ Value: m.textInput.Value(),
+ IsFocused: true,
+ }
+ m.updateInputState(state)
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Workflow Content Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) syncWorkflowContent(ctx context.Context) {
+ defer m.skeleton.TriggerUpdate()
+
+ m.status.Reset()
+ m.status.SetProgressMessage(fmt.Sprintf("[%s@%s] Fetching workflow contents...",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+
+ m.tableTrigger.SetRows([]table.Row{})
+
+ workflowContent, err := m.github.InspectWorkflow(ctx, gu.InspectWorkflowInput{
+ Repository: m.selectedRepository.RepositoryName,
+ Branch: m.selectedRepository.BranchName,
+ WorkflowFile: m.selectedWorkflow,
+ })
+
+ if err != nil {
+ m.handleWorkflowError(err)
+ return
+ }
+
+ m.processWorkflowContent(workflowContent)
+}
+
+func (m *ModelGithubTrigger) processWorkflowContent(content *gu.InspectWorkflowOutput) {
+ if content.Workflow == nil {
+ m.status.SetError(errors.New("workflow contents cannot be empty"))
+ m.status.SetErrorMessage("You have no workflow contents")
+ return
+ }
+
+ m.workflowContent = content.Workflow
+ m.updateTriggerTable()
+ m.finalizeWorkflowUpdate()
+}
+
+// -----------------------------------------------------------------------------
+// Table Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) updateTriggerTable() {
+ var rows []table.Row
+
+ // Add key-value inputs
+ for _, keyVal := range m.workflowContent.KeyVals {
+ rows = append(rows, table.Row{
+ fmt.Sprintf("%d", keyVal.ID),
+ "input",
+ keyVal.Key,
+ keyVal.Default,
+ keyVal.Value,
+ })
+ }
+
+ // Add choice inputs
+ for _, choice := range m.workflowContent.Choices {
+ rows = append(rows, table.Row{
+ fmt.Sprintf("%d", choice.ID),
+ "choice",
+ choice.Key,
+ choice.Default,
+ choice.Value,
+ })
+ }
+
+ // Add regular inputs
+ for _, input := range m.workflowContent.Inputs {
+ rows = append(rows, table.Row{
+ fmt.Sprintf("%d", input.ID),
+ "input",
+ input.Key,
+ input.Default,
+ input.Value,
+ })
+ }
+
+ // Add boolean inputs
+ for _, boolean := range m.workflowContent.Boolean {
+ rows = append(rows, table.Row{
+ fmt.Sprintf("%d", boolean.ID),
+ "bool",
+ boolean.Key,
+ boolean.Default,
+ boolean.Value,
+ })
+ }
+
+ m.tableTrigger.SetRows(rows)
+ m.sortTableItemsByName()
+}
+
+func (m *ModelGithubTrigger) sortTableItemsByName() {
+ rows := m.tableTrigger.Rows()
+ slices.SortFunc(rows, func(a, b table.Row) int {
+ return strings.Compare(a[2], b[2])
+ })
+ m.tableTrigger.SetRows(rows)
+}
+
+func (m *ModelGithubTrigger) finalizeWorkflowUpdate() {
+ m.tableTrigger.SetCursor(0)
+ m.optionCursor = 0
+ m.optionValues = nil
+ m.triggerFocused = false
+ m.tableTrigger.Focus()
+
+ m.textInput.SetCursor(0)
+ m.textInput.SetValue("")
+ m.textInput.Placeholder = ""
+
+ m.tableReady = true
+ m.isTriggerable = true
+
+ if m.hasNoWorkflowOptions() {
+ m.handleEmptyWorkflow()
+ } else {
+ m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s] Workflow contents fetched.",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Trigger Logic
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) triggerWorkflow() {
+ if m.triggerFocused {
+ m.fillEmptyValuesWithDefault()
+ }
+
+ m.status.SetProgressMessage(fmt.Sprintf("[%s@%s]:[%s] Triggering workflow...",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName, m.selectedWorkflow))
+
+ if m.workflowContent == nil {
+ m.status.SetErrorMessage("Workflow contents cannot be empty")
+ return
+ }
+
+ content, err := m.workflowContent.ToJson()
+ if err != nil {
+ 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,
+ WorkflowFile: m.selectedWorkflow,
+ Content: content,
+ })
+
+ if err != nil {
+ m.status.SetError(err)
+ m.status.SetErrorMessage("Workflow cannot be triggered")
+ return
+ }
+
+ m.handleSuccessfulTrigger()
+}
+
+func (m *ModelGithubTrigger) handleSuccessfulTrigger() {
+ m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s]:[%s] Workflow triggered.",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName, m.selectedWorkflow))
+
+ m.status.SetProgressMessage("Switching to workflow history tab...")
+ time.Sleep(2000 * time.Millisecond)
+
+ m.resetTriggerState()
+ m.switchToHistoryTab()
+}
+
+// -----------------------------------------------------------------------------
+// UI Rendering
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) renderTable() string {
+ baseStyle := lipgloss.NewStyle().
+ BorderStyle(lipgloss.NormalBorder()).
+ MarginLeft(1)
+
+ if m.triggerFocused {
+ baseStyle = baseStyle.BorderForeground(lipgloss.Color("240"))
+ } else {
+ baseStyle = baseStyle.BorderForeground(lipgloss.Color("#3b698f"))
+ }
+
+ m.updateTableDimensions()
+ return baseStyle.Render(m.tableTrigger.View())
+}
+
+func (m *ModelGithubTrigger) renderInputArea() string {
+ 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.JoinHorizontal(lipgloss.Top, selector, m.triggerButton())
+}
+
+func (m *ModelGithubTrigger) renderHelp() string {
+ helpStyle := WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4)
+ return helpStyle.Render(m.help.View(m.Keys))
+}
+
+// -----------------------------------------------------------------------------
+// Helper Functions - Selectors
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) emptySelector() string {
+ return m.getSelectorStyle().Render("")
+}
+
+func (m *ModelGithubTrigger) inputSelector() string {
+ return m.getSelectorStyle().Render(m.textInput.View())
+}
+
+func (m *ModelGithubTrigger) optionSelector() string {
+ var processedValues []string
+ for i, option := range m.optionValues {
+ if i == m.optionCursor {
+ processedValues = append(processedValues, m.getSelectedOptionStyle().Render(option))
+ } else {
+ processedValues = append(processedValues, m.getUnselectedOptionStyle().Render(option))
+ }
+ }
+
+ return m.getSelectorStyle().Render(lipgloss.JoinHorizontal(lipgloss.Left, processedValues...))
+}
+
+func (m *ModelGithubTrigger) triggerButton() string {
+ button := lipgloss.NewStyle().
+ Border(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("255")).
+ Padding(0, 1).
+ Align(lipgloss.Center)
+
+ if m.triggerFocused {
+ button = button.
+ BorderForeground(lipgloss.Color("#399adb")).
+ Foreground(lipgloss.Color("#399adb")).
+ BorderStyle(lipgloss.DoubleBorder())
+ }
+
+ return button.Render("Trigger")
+}
+
+// -----------------------------------------------------------------------------
+// Helper Functions - Styles
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) getSelectorStyle() lipgloss.Style {
+ return lipgloss.NewStyle().
+ Border(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#3b698f")).
+ Padding(0, 1).
+ Width(m.skeleton.GetTerminalWidth() - 17).
+ MarginLeft(1)
+}
+
+func (m *ModelGithubTrigger) getSelectedOptionStyle() lipgloss.Style {
+ return lipgloss.NewStyle().
+ Foreground(lipgloss.Color("120")).
+ Padding(0, 1)
+}
+
+func (m *ModelGithubTrigger) getUnselectedOptionStyle() lipgloss.Style {
+ return lipgloss.NewStyle().
+ Foreground(lipgloss.Color("140")).
+ Padding(0, 1)
+}
+
+// -----------------------------------------------------------------------------
+// Helper Functions - Value Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) getChoiceValues(id string) []string {
+ for _, choice := range m.workflowContent.Choices {
+ if fmt.Sprintf("%d", choice.ID) == id {
+ return choice.Values
+ }
+ }
+ return nil
+}
+
+func (m *ModelGithubTrigger) getBooleanValues(id string) []string {
+ for _, boolean := range m.workflowContent.Boolean {
+ if fmt.Sprintf("%d", boolean.ID) == id {
+ return boolean.Values
+ }
+ }
+ return nil
+}
+
+func (m *ModelGithubTrigger) updateChoiceValue(row []string, value string) {
+ rows := m.tableTrigger.Rows()
+ for i, r := range rows {
+ if r[0] == row[0] {
+ rows[i][4] = value
+ }
+ }
+ m.tableTrigger.SetRows(rows)
+
+ if row[1] == "choice" {
+ m.updateWorkflowChoiceValue(row[0])
+ } else {
+ m.updateWorkflowBooleanValue(row[0])
+ }
+}
+
+func (m *ModelGithubTrigger) updateWorkflowChoiceValue(id string) {
+ for i, choice := range m.workflowContent.Choices {
+ if fmt.Sprintf("%d", choice.ID) == id {
+ m.workflowContent.Choices[i].SetValue(m.optionValues[m.optionCursor])
+ break
+ }
+ }
+}
+
+func (m *ModelGithubTrigger) updateWorkflowBooleanValue(id string) {
+ for i, boolean := range m.workflowContent.Boolean {
+ if fmt.Sprintf("%d", boolean.ID) == id {
+ m.workflowContent.Boolean[i].SetValue(m.optionValues[m.optionCursor])
+ break
+ }
+ }
+}
+
+func (m *ModelGithubTrigger) updateTextInputValue(row []string, value string) {
+ if strings.HasPrefix(value, " ") {
+ return
+ }
+
+ rows := m.tableTrigger.Rows()
+ for i, r := range rows {
+ if r[0] == row[0] {
+ rows[i][4] = value
+ }
+ }
+ m.tableTrigger.SetRows(rows)
+
+ m.updateWorkflowInputValue(row)
+}
+
+func (m *ModelGithubTrigger) updateWorkflowInputValue(row []string) {
+ for i, input := range m.workflowContent.Inputs {
+ if fmt.Sprintf("%d", input.ID) == row[0] {
+ m.textInput.Placeholder = input.Default
+ m.workflowContent.Inputs[i].SetValue(m.textInput.Value())
+ return
+ }
+ }
+
+ for i, keyVal := range m.workflowContent.KeyVals {
+ if fmt.Sprintf("%d", keyVal.ID) == row[0] {
+ m.textInput.Placeholder = keyVal.Default
+ m.workflowContent.KeyVals[i].SetValue(m.textInput.Value())
+ return
+ }
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Helper Functions - State Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) fillEmptyValuesWithDefault() {
+ if m.workflowContent == nil {
+ return
+ }
+
+ rows := m.tableTrigger.Rows()
+ for i, row := range rows {
+ if row[4] == "" {
+ rows[i][4] = rows[i][3]
+ }
+ }
+ m.tableTrigger.SetRows(rows)
+
+ m.fillWorkflowEmptyValues()
+}
+
+func (m *ModelGithubTrigger) fillWorkflowEmptyValues() {
+ for i, choice := range m.workflowContent.Choices {
+ if choice.Value == "" {
+ m.workflowContent.Choices[i].SetValue(choice.Default)
+ }
+ }
+
+ for i, input := range m.workflowContent.Inputs {
+ if input.Value == "" {
+ m.workflowContent.Inputs[i].SetValue(input.Default)
+ }
+ }
+
+ for i, keyVal := range m.workflowContent.KeyVals {
+ if keyVal.Value == "" {
+ m.workflowContent.KeyVals[i].SetValue(keyVal.Default)
+ }
+ }
+
+ for i, boolean := range m.workflowContent.Boolean {
+ if boolean.Value == "" {
+ m.workflowContent.Boolean[i].SetValue(boolean.Default)
+ }
+ }
+}
+
+func (m *ModelGithubTrigger) resetTriggerState() {
+ m.workflowContent = nil
+ m.selectedWorkflow = ""
+ m.currentOption = ""
+ m.optionValues = nil
+ m.selectedRepositoryName = ""
+}
+
+func (m *ModelGithubTrigger) switchToHistoryTab() {
+ m.skeleton.TriggerUpdateWithMsg(workflowHistoryUpdateMsg{time.Second * 3})
+ m.skeleton.SetActivePage("history")
+}
+
+func (m *ModelGithubTrigger) hasNoWorkflowOptions() bool {
+ return len(m.workflowContent.KeyVals) == 0 &&
+ len(m.workflowContent.Choices) == 0 &&
+ len(m.workflowContent.Inputs) == 0
+}
+
+func (m *ModelGithubTrigger) handleEmptyWorkflow() {
+ m.fillTableWithEmptyMessage()
+ m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] Workflow doesn't contain options but still triggerable",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+}
+
+func (m *ModelGithubTrigger) fillTableWithEmptyMessage() {
+ var rows []table.Row
+ for i := 0; i < 100; i++ {
+ idx := fmt.Sprintf("%d", i)
+ rows = append(rows, table.Row{
+ idx, "EMPTY", "EMPTY", "EMPTY", "No workflow input found",
+ })
+ }
+
+ m.tableTrigger.SetRows(rows)
+ m.tableTrigger.SetCursor(0)
+}
+
+func (m *ModelGithubTrigger) showInformationIfAnyEmptyValue() {
+ for _, row := range m.tableTrigger.Rows() {
+ if row[4] == "" {
+ m.status.SetDefaultMessage("Info: You have empty values. These values uses their default values.")
+ return
+ }
+ }
+}
+
+func (m *ModelGithubTrigger) handleWorkflowError(err error) {
+ if errors.Is(err, context.Canceled) {
+ return
+ }
+ m.status.SetError(err)
+ m.status.SetErrorMessage("Workflow contents cannot be fetched")
+}
+
+// -----------------------------------------------------------------------------
+// Helper Functions - Table Dimensions
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) updateTableDimensions() {
+ 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)
+ }
+}
+
+// -----------------------------------------------------------------------------
+// UI Component Updates
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubTrigger) updateUIComponents(msg tea.Msg) tea.Cmd {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ // Update text input
+ m.textInput, cmd = m.textInput.Update(msg)
+ cmds = append(cmds, cmd)
+
+ // Update table
+ m.tableTrigger, cmd = m.tableTrigger.Update(msg)
+ cmds = append(cmds, cmd)
+
+ // Handle input controller
+ m.inputController()
+
+ return tea.Batch(cmds...)
+}
+
+// -----------------------------------------------------------------------------
+// Input & Table State Management
+// -----------------------------------------------------------------------------
+
+type InputState struct {
+ Type string // "input", "choice", "bool"
+ Value string
+ Default string
+ Options []string
+ Cursor int
+ IsFocused bool
+}
+
+func (m *ModelGithubTrigger) updateInputState(state *InputState) {
+ row := m.tableTrigger.SelectedRow()
+ if len(row) == 0 {
+ return
+ }
+
+ switch state.Type {
+ case "choice", "bool":
+ m.updateChoiceValue(row, state.Options[state.Cursor])
+ case "input":
+ m.updateTextInputValue(row, state.Value)
+ }
+}
diff --git a/internal/terminal/handler/ghtrigger/ghtrigger.go b/internal/terminal/handler/ghtrigger/ghtrigger.go
deleted file mode 100644
index 290b654..0000000
--- a/internal/terminal/handler/ghtrigger/ghtrigger.go
+++ /dev/null
@@ -1,688 +0,0 @@
-package ghtrigger
-
-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"
- hdltypes "github.com/termkit/gama/internal/terminal/handler/types"
- "github.com/termkit/gama/pkg/workflow"
-)
-
-type ModelGithubTrigger struct {
- // 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
-
- // shared properties
- SelectedRepository *hdltypes.SelectedRepository
-
- // use cases
- github gu.UseCase
-
- // keymap
- Keys keyMap
-
- // models
- Help help.Model
- Viewport *viewport.Model
- modelError hdlerror.ModelError
- textInput textinput.Model
- tableTrigger table.Model
-}
-
-func SetupModelGithubTrigger(githubUseCase gu.UseCase, selectedRepository *hdltypes.SelectedRepository, currentTab *int, forceUpdateWorkflowHistory *bool) *ModelGithubTrigger {
- var tableRowsTrigger []table.Row
-
- tableTrigger := table.New(
- table.WithColumns(tableColumnsTrigger),
- table.WithRows(tableRowsTrigger),
- 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)
- tableTrigger.SetStyles(s)
-
- ti := textinput.New()
- ti.Blur()
- ti.CharLimit = 72
-
- 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() {},
- }
-}
-
-func (m *ModelGithubTrigger) Init() tea.Cmd {
- m.modelError.SetDefaultMessage("No workflow contents found.")
- return textinput.Blink
-}
-
-func (m *ModelGithubTrigger) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- if m.SelectedRepository.WorkflowName == "" {
- m.modelError.Reset()
- m.modelError.SetDefaultMessage("No workflow selected.")
- return m, nil
- }
- 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.syncWorkflowContext, m.cancelSyncWorkflow = context.WithCancel(context.Background())
-
- go m.syncWorkflowContent(m.syncWorkflowContext)
- }
-
- var cmds []tea.Cmd
- var cmd tea.Cmd
-
- switch shadowMsg := msg.(type) {
- case tea.KeyMsg:
- switch shadowMsg.String() {
- case "up":
- if len(m.tableTrigger.Rows()) > 0 && !m.triggerFocused {
- m.tableTrigger.MoveUp(1)
- m.switchBetweenInputAndTable()
- // delete msg key to prevent moving cursor
- msg = tea.KeyMsg{Type: tea.KeyNull}
-
- m.optionInit = false
- }
- case "down":
- if len(m.tableTrigger.Rows()) > 0 && !m.triggerFocused {
- m.tableTrigger.MoveDown(1)
- m.switchBetweenInputAndTable()
- // delete msg key to prevent moving cursor
- msg = tea.KeyMsg{Type: tea.KeyNull}
-
- m.optionInit = false
- }
- case "ctrl+r", "ctrl+R":
- go m.syncWorkflowContent(m.syncWorkflowContext)
- case "left":
- if !m.triggerFocused {
- m.optionCursor = max(m.optionCursor-1, 0)
- }
- case "right":
- if !m.triggerFocused {
- m.optionCursor = min(m.optionCursor+1, len(m.optionValues)-1)
- }
- case "tab":
- if m.isTriggerable {
- m.triggerFocused = !m.triggerFocused
- if m.triggerFocused {
- m.tableTrigger.Blur()
- m.textInput.Blur()
- m.showInformationIfAnyEmptyValue()
- } else {
- m.tableTrigger.Focus()
- m.textInput.Focus()
- }
- }
- case "enter":
- if m.triggerFocused && m.isTriggerable {
- go m.triggerWorkflow()
- }
- }
- }
-
- m.tableTrigger, cmd = m.tableTrigger.Update(msg)
- cmds = append(cmds, cmd)
-
- m.textInput, cmd = m.textInput.Update(msg)
- cmds = append(cmds, cmd)
-
- m.inputController(m.syncWorkflowContext)
-
- return m, tea.Batch(cmds...)
-}
-
-func (m *ModelGithubTrigger) switchBetweenInputAndTable() {
- var selectedRow = m.tableTrigger.SelectedRow()
-
- if selectedRow[1] == "input" || selectedRow[1] == "bool" {
- m.textInput.Focus()
- m.tableTrigger.Blur()
- } else {
- m.textInput.Blur()
- m.tableTrigger.Focus()
- }
- m.textInput.SetValue(m.tableTrigger.SelectedRow()[4])
- m.textInput.SetCursor(len(m.textInput.Value()))
-}
-
-func (m *ModelGithubTrigger) inputController(_ context.Context) {
- if m.workflowContent == nil {
- return
- }
-
- if len(m.tableTrigger.Rows()) > 0 {
- var selectedRow = m.tableTrigger.SelectedRow()
- if len(selectedRow) == 0 {
- return
- }
-
- switch selectedRow[1] {
- case "choice":
- var optionValues []string
- for _, choice := range m.workflowContent.Choices {
- if fmt.Sprintf("%d", choice.ID) == selectedRow[0] {
- optionValues = append(optionValues, choice.Values...)
- }
- }
- m.optionValues = optionValues
- if !m.optionInit {
- for i, option := range m.optionValues {
- if option == selectedRow[4] {
- m.optionCursor = i
- }
- }
- }
- m.optionInit = true
- case "bool":
- var optionValues []string
- for _, choice := range m.workflowContent.Boolean {
- if fmt.Sprintf("%d", choice.ID) == selectedRow[0] {
- optionValues = append(optionValues, choice.Values...)
- }
- }
- m.optionValues = optionValues
- if !m.optionInit {
- for i, option := range m.optionValues {
- if option == selectedRow[4] {
- m.optionCursor = i
- }
- }
- }
- m.optionInit = true
- default:
- m.optionValues = nil
- m.optionCursor = 0
-
- if !m.triggerFocused {
- m.textInput.Focus()
- }
- }
- }
-
- for i, choice := range m.workflowContent.Choices {
- var selectedRow = m.tableTrigger.SelectedRow()
- var rows = m.tableTrigger.Rows()
-
- if len(selectedRow) == 0 || len(rows) == 0 {
- return
- }
- if fmt.Sprintf("%d", choice.ID) == selectedRow[0] {
- m.workflowContent.Choices[i].SetValue(m.optionValues[m.optionCursor])
-
- for i, row := range rows {
- if row[0] == selectedRow[0] {
- rows[i][4] = m.optionValues[m.optionCursor]
- }
- }
-
- m.tableTrigger.SetRows(rows)
- }
- }
-
- if m.workflowContent.Boolean != nil {
- for i, boolean := range m.workflowContent.Boolean {
- var selectedRow = m.tableTrigger.SelectedRow()
- var rows = m.tableTrigger.Rows()
- if len(selectedRow) == 0 || len(rows) == 0 {
- return
- }
- if fmt.Sprintf("%d", boolean.ID) == selectedRow[0] {
- m.workflowContent.Boolean[i].SetValue(m.optionValues[m.optionCursor])
-
- for i, row := range rows {
- if row[0] == selectedRow[0] {
- rows[i][4] = m.optionValues[m.optionCursor]
- }
- }
-
- m.tableTrigger.SetRows(rows)
- }
- }
- }
-
- if m.textInput.Focused() {
- if strings.HasPrefix(m.textInput.Value(), " ") {
- m.textInput.SetValue("")
- }
-
- var selectedRow = m.tableTrigger.SelectedRow()
- var rows = m.tableTrigger.Rows()
- if len(selectedRow) == 0 || len(rows) == 0 {
- return
- }
-
- for i, input := range m.workflowContent.Inputs {
- if fmt.Sprintf("%d", input.ID) == selectedRow[0] {
- m.textInput.Placeholder = input.Default
- m.workflowContent.Inputs[i].SetValue(m.textInput.Value())
-
- for i, row := range rows {
- if row[0] == selectedRow[0] {
- rows[i][4] = m.textInput.Value()
- }
- }
-
- m.tableTrigger.SetRows(rows)
- }
- }
-
- for i, keyVal := range m.workflowContent.KeyVals {
- if fmt.Sprintf("%d", keyVal.ID) == selectedRow[0] {
- m.textInput.Placeholder = keyVal.Default
- m.workflowContent.KeyVals[i].SetValue(m.textInput.Value())
-
- for i, row := range rows {
- if row[0] == selectedRow[0] {
- rows[i][4] = m.textInput.Value()
- }
- }
-
- m.tableTrigger.SetRows(rows)
- }
- }
- }
-}
-
-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(
- fmt.Sprintf("[%s@%s] Fetching workflow contents...",
- 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,
- 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")
- return
- }
-
- if workflowContent.Workflow == nil {
- m.modelError.SetError(errors.New("workflow contents cannot be empty"))
- m.modelError.SetErrorMessage("You have no workflow contents")
- return
- }
-
- m.workflowContent = workflowContent.Workflow
-
- var tableRowsTrigger []table.Row
- for _, keyVal := range m.workflowContent.KeyVals {
- tableRowsTrigger = append(tableRowsTrigger, table.Row{
- fmt.Sprintf("%d", keyVal.ID),
- "input", // json type
- keyVal.Key,
- keyVal.Default,
- keyVal.Value,
- })
- }
-
- for _, choice := range m.workflowContent.Choices {
- tableRowsTrigger = append(tableRowsTrigger, table.Row{
- fmt.Sprintf("%d", choice.ID),
- "choice",
- choice.Key,
- choice.Default,
- choice.Value,
- })
- }
-
- for _, input := range m.workflowContent.Inputs {
- tableRowsTrigger = append(tableRowsTrigger, table.Row{
- fmt.Sprintf("%d", input.ID),
- "input",
- input.Key,
- input.Default,
- input.Value,
- })
- }
-
- for _, boolean := range m.workflowContent.Boolean {
- tableRowsTrigger = append(tableRowsTrigger, table.Row{
- fmt.Sprintf("%d", boolean.ID),
- "bool",
- boolean.Key,
- boolean.Default,
- boolean.Value,
- })
- }
-
- m.tableTrigger.SetRows(tableRowsTrigger)
- m.sortTableItemsByName()
- m.tableTrigger.SetCursor(0)
- m.optionCursor = 0
- m.optionValues = nil
- m.triggerFocused = false
- m.tableTrigger.Focus()
-
- // reset input value
- m.textInput.SetCursor(0)
- m.textInput.SetValue("")
- m.textInput.Placeholder = ""
-
- m.tableReady = true
- m.isTriggerable = true
-
- if len(workflowContent.Workflow.KeyVals) == 0 &&
- 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))
- } else {
- m.modelError.SetSuccessMessage(fmt.Sprintf("[%s@%s] Workflow contents fetched.",
- m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName))
- }
-
- go m.Update(m) // update model
-}
-
-func (m *ModelGithubTrigger) fillTableWithEmptyMessage() {
- var rows []table.Row
- for i := 0; i < 100; i++ {
- idx := fmt.Sprintf("%d", i)
- rows = append(rows, table.Row{
- idx, "EMPTY", "EMPTY", "EMPTY", "No workflow input found",
- })
- }
-
- m.tableTrigger.SetRows(rows)
- m.tableTrigger.SetCursor(0)
-}
-
-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.")
- return
- }
- }
-}
-
-func (m *ModelGithubTrigger) triggerButton() string {
- button := lipgloss.NewStyle().
- Border(lipgloss.NormalBorder()).
- BorderForeground(lipgloss.Color("255")).
- Padding(0, 1).
- Align(lipgloss.Center)
-
- if m.triggerFocused {
- button = button.BorderForeground(lipgloss.Color("130")).
- Foreground(lipgloss.Color("130")).
- BorderStyle(lipgloss.DoubleBorder())
- }
-
- return button.Render("Trigger")
-}
-
-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")
- return
- }
-
- rows := m.tableTrigger.Rows()
- for i, row := range rows {
- if row[4] == "" {
- rows[i][4] = rows[i][3]
- }
- }
- m.tableTrigger.SetRows(rows)
-
- for i, choice := range m.workflowContent.Choices {
- if choice.Value == "" {
- m.workflowContent.Choices[i].SetValue(choice.Default)
- }
-
- }
-
- for i, input := range m.workflowContent.Inputs {
- if input.Value == "" {
- m.workflowContent.Inputs[i].SetValue(input.Default)
- }
- }
-
- for i, keyVal := range m.workflowContent.KeyVals {
- if keyVal.Value == "" {
- m.workflowContent.KeyVals[i].SetValue(keyVal.Default)
- }
- }
-
- for i, boolean := range m.workflowContent.Boolean {
- if boolean.Value == "" {
- m.workflowContent.Boolean[i].SetValue(boolean.Default)
- }
- }
-}
-
-func (m *ModelGithubTrigger) triggerWorkflow() {
- if m.triggerFocused {
- m.fillEmptyValuesWithDefault()
- }
-
- m.modelError.SetProgressMessage(
- fmt.Sprintf("[%s@%s]:[%s] Triggering workflow...",
- m.SelectedRepository.RepositoryName, m.SelectedRepository.BranchName, m.selectedWorkflow))
-
- if m.workflowContent == nil {
- m.modelError.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")
- return
- }
-
- _, err = m.github.TriggerWorkflow(context.Background(), gu.TriggerWorkflowInput{
- 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")
- return
- }
-
- m.modelError.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)
-
- // move these operations under new function named "resetTabSettings"
- m.workflowContent = nil // reset workflow content
- m.selectedWorkflow = "" // reset selected workflow
- m.currentOption = "" // reset current option
- 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
-}
-
-func (m *ModelGithubTrigger) emptySelector() string {
- // Define window style
- windowStyle := lipgloss.NewStyle().
- Border(lipgloss.NormalBorder()).
- Padding(0, 1).
- Width(*hdltypes.ScreenWidth - 13)
-
- // Build the options list
- doc := strings.Builder{}
-
- return windowStyle.Render(doc.String())
-}
-
-func (m *ModelGithubTrigger) inputSelector() string {
- // Define window style
- windowStyle := lipgloss.NewStyle().
- Border(lipgloss.NormalBorder()).
- Padding(0, 1).
- Width(*hdltypes.ScreenWidth - 13)
-
- // Build the options list
- doc := strings.Builder{}
-
- doc.WriteString(m.textInput.View())
-
- return windowStyle.Render(doc.String())
-}
-
-// optionSelector renders the options list
-// TODO: Make this dynamic limited&sized.
-func (m *ModelGithubTrigger) optionSelector() string {
- // Define window style
- windowStyle := lipgloss.NewStyle().
- Border(lipgloss.NormalBorder()).
- Padding(0, 1).
- Width(*hdltypes.ScreenWidth - 13)
-
- // 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 {
- processedValues = append(processedValues, selectedOptionStyle.Render(option))
- } else {
- processedValues = append(processedValues, unselectedOptionStyle.Render(option))
- }
- }
-
- horizontal := lipgloss.JoinHorizontal(lipgloss.Left, processedValues...)
-
- doc.WriteString(horizontal)
-
- // Apply window style to the entire list
- return windowStyle.Render(doc.String())
-}
-
-func (m *ModelGithubTrigger) sortTableItemsByName() {
- rows := m.tableTrigger.Rows()
- slices.SortFunc(rows, func(a, b table.Row) int {
- return strings.Compare(a[2], b[2])
- })
- 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.go b/internal/terminal/handler/ghworkflow.go
new file mode 100644
index 0000000..ab0d40c
--- /dev/null
+++ b/internal/terminal/handler/ghworkflow.go
@@ -0,0 +1,498 @@
+package handler
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sort"
+
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/table"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ gu "github.com/termkit/gama/internal/github/usecase"
+ "github.com/termkit/skeleton"
+)
+
+// -----------------------------------------------------------------------------
+// Model Definition
+// -----------------------------------------------------------------------------
+
+type ModelGithubWorkflow struct {
+ // Core dependencies
+ skeleton *skeleton.Skeleton
+ github gu.UseCase
+
+ // UI Components
+ help help.Model
+ keys githubWorkflowKeyMap
+ tableTriggerableWorkflow table.Model
+ status *ModelStatus
+ textInput textinput.Model
+
+ // Table state
+ tableReady bool
+
+ // Context management
+ syncTriggerableWorkflowsContext context.Context
+ cancelSyncTriggerableWorkflows context.CancelFunc
+
+ // Shared state
+ selectedRepository *SelectedRepository
+
+ // Indicates if there are any available workflows
+ hasWorkflows bool
+ lastSelectedRepository string // Track last repository for state persistence
+
+ // State management
+ state struct {
+ Ready bool
+ Repository struct {
+ Current string
+ Last string
+ Branch string
+ HasFlows bool
+ }
+ Syncing bool
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Constructor & Initialization
+// -----------------------------------------------------------------------------
+
+func SetupModelGithubWorkflow(s *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubWorkflow {
+ m := &ModelGithubWorkflow{
+ // Initialize core dependencies
+ skeleton: s,
+ github: githubUseCase,
+
+ // Initialize UI components
+ help: help.New(),
+ keys: githubWorkflowKeys,
+ status: SetupModelStatus(s),
+ textInput: setupBranchInput(),
+
+ // Initialize state
+ selectedRepository: NewSelectedRepository(),
+ syncTriggerableWorkflowsContext: context.Background(),
+ cancelSyncTriggerableWorkflows: func() {},
+ }
+
+ // Setup table and blur initially
+ m.tableTriggerableWorkflow = setupWorkflowTable()
+ m.tableTriggerableWorkflow.Blur()
+ m.textInput.Blur()
+
+ return m
+}
+
+func setupBranchInput() textinput.Model {
+ ti := textinput.New()
+ ti.Focus()
+ ti.CharLimit = 128
+ ti.Placeholder = "Type to switch branch"
+ ti.ShowSuggestions = true
+ return ti
+}
+
+func setupWorkflowTable() table.Model {
+ t := table.New(
+ table.WithColumns(tableColumnsWorkflow),
+ table.WithRows([]table.Row{}),
+ table.WithFocused(true),
+ table.WithHeight(7),
+ )
+
+ // Apply styles
+ s := table.DefaultStyles()
+ s.Header = s.Header.
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#3b698f")).
+ BorderBottom(true).
+ Bold(false)
+ s.Selected = s.Selected.
+ Foreground(lipgloss.Color("229")).
+ Background(lipgloss.Color("57")).
+ Bold(false)
+ t.SetStyles(s)
+
+ // Set keymap
+ t.KeyMap = table.KeyMap{
+ LineUp: key.NewBinding(key.WithKeys("up"), key.WithHelp("↑", "up")),
+ LineDown: key.NewBinding(key.WithKeys("down"), key.WithHelp("↓", "down")),
+ PageUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("pgup", "page up")),
+ PageDown: key.NewBinding(key.WithKeys("pgdown"), key.WithHelp("pgdn", "page down")),
+ GotoTop: key.NewBinding(key.WithKeys("home"), key.WithHelp("home", "go to start")),
+ GotoBottom: key.NewBinding(key.WithKeys("end"), key.WithHelp("end", "go to end")),
+ }
+
+ return t
+}
+
+// -----------------------------------------------------------------------------
+// Bubbletea Model Implementation
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) Init() tea.Cmd {
+ // Check initial state
+ if m.lastSelectedRepository == m.selectedRepository.RepositoryName && !m.hasWorkflows {
+ m.skeleton.LockTab("trigger")
+ // Blur components initially
+ m.tableTriggerableWorkflow.Blur()
+ m.textInput.Blur()
+ }
+ return nil
+}
+
+func (m *ModelGithubWorkflow) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ // Check repository change and return command if exists
+ if cmd := m.handleRepositoryChange(); cmd != nil {
+ return m, cmd
+ }
+
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ // Update text input and handle branch selection
+ m.textInput, cmd = m.textInput.Update(msg)
+ cmds = append(cmds, cmd)
+ m.handleBranchSelection()
+
+ // Update table and handle workflow selection
+ m.tableTriggerableWorkflow, cmd = m.tableTriggerableWorkflow.Update(msg)
+ cmds = append(cmds, cmd)
+ m.handleTableInputs()
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *ModelGithubWorkflow) View() string {
+ return lipgloss.JoinVertical(lipgloss.Top,
+ m.renderTable(),
+ m.renderBranchInput(),
+ m.status.View(),
+ m.renderHelp(),
+ )
+}
+
+// -----------------------------------------------------------------------------
+// Repository Change Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) handleRepositoryChange() tea.Cmd {
+ if m.state.Repository.Current != m.selectedRepository.RepositoryName {
+ m.state.Ready = false
+ m.state.Repository.Current = m.selectedRepository.RepositoryName
+ m.state.Repository.Branch = m.selectedRepository.BranchName
+ m.syncWorkflows()
+ } else if !m.state.Repository.HasFlows {
+ m.skeleton.LockTab("trigger")
+ }
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+// Branch Selection & Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) handleBranchSelection() {
+ selectedBranch := m.textInput.Value()
+
+ // Set branch
+ if selectedBranch == "" {
+ m.selectedRepository.BranchName = m.state.Repository.Branch
+ } else if m.isBranchValid(selectedBranch) {
+ m.selectedRepository.BranchName = selectedBranch
+ } else {
+ m.status.SetErrorMessage(fmt.Sprintf("Branch %s does not exist", selectedBranch))
+ m.skeleton.LockTabsToTheRight()
+ return
+ }
+
+ // Update tab state
+ m.updateTabState()
+}
+
+func (m *ModelGithubWorkflow) isBranchValid(branch string) bool {
+ for _, suggestion := range m.textInput.AvailableSuggestions() {
+ if suggestion == branch {
+ return true
+ }
+ }
+ return false
+}
+
+// -----------------------------------------------------------------------------
+// Workflow Sync & Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) syncWorkflows() {
+ if m.state.Syncing {
+ m.cancelSyncTriggerableWorkflows()
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ m.cancelSyncTriggerableWorkflows = cancel
+ m.state.Syncing = true
+
+ go func() {
+ defer func() {
+ m.state.Syncing = false
+ m.skeleton.TriggerUpdate()
+ }()
+
+ m.syncBranches(ctx)
+ m.syncTriggerableWorkflows(ctx)
+ }()
+}
+
+func (m *ModelGithubWorkflow) syncTriggerableWorkflows(ctx context.Context) {
+ defer m.skeleton.TriggerUpdate()
+
+ m.initializeSyncState()
+ workflows, err := m.fetchTriggerableWorkflows(ctx)
+ if err != nil {
+ return
+ }
+
+ m.processWorkflows(workflows)
+}
+
+func (m *ModelGithubWorkflow) initializeSyncState() {
+ m.status.Reset()
+ m.status.SetProgressMessage(fmt.Sprintf("[%s@%s] Fetching triggerable workflows...",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+ m.tableTriggerableWorkflow.SetRows([]table.Row{})
+}
+
+func (m *ModelGithubWorkflow) fetchTriggerableWorkflows(ctx context.Context) (*gu.GetTriggerableWorkflowsOutput, error) {
+ workflows, err := m.github.GetTriggerableWorkflows(ctx, gu.GetTriggerableWorkflowsInput{
+ Repository: m.selectedRepository.RepositoryName,
+ Branch: m.selectedRepository.BranchName,
+ })
+
+ if err != nil {
+ if !errors.Is(err, context.Canceled) {
+ m.status.SetError(err)
+ m.status.SetErrorMessage("Triggerable workflows cannot be listed")
+ }
+ return nil, err
+ }
+
+ return workflows, nil
+}
+
+func (m *ModelGithubWorkflow) processWorkflows(workflows *gu.GetTriggerableWorkflowsOutput) {
+ m.state.Repository.HasFlows = len(workflows.TriggerableWorkflows) > 0
+ m.state.Repository.Current = m.selectedRepository.RepositoryName
+ m.state.Ready = true
+
+ if !m.state.Repository.HasFlows {
+ m.handleEmptyWorkflows()
+ return
+ }
+
+ m.updateWorkflowTable(workflows.TriggerableWorkflows)
+ m.updateTabState()
+ m.finalizeUpdate()
+
+ // Focus components when workflows exist
+ m.tableTriggerableWorkflow.Focus()
+ m.textInput.Focus()
+}
+
+func (m *ModelGithubWorkflow) handleEmptyWorkflows() {
+ m.selectedRepository.WorkflowName = ""
+ m.skeleton.LockTab("trigger")
+
+ // Blur components when no workflows
+ m.tableTriggerableWorkflow.Blur()
+ m.textInput.Blur()
+
+ m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] No triggerable workflow found.",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+
+ m.fillTableWithEmptyMessage()
+}
+
+func (m *ModelGithubWorkflow) fillTableWithEmptyMessage() {
+ var rows []table.Row
+ for i := 0; i < 100; i++ {
+ rows = append(rows, table.Row{
+ "EMPTY",
+ "No triggerable workflow found",
+ })
+ }
+
+ m.tableTriggerableWorkflow.SetRows(rows)
+ m.tableTriggerableWorkflow.SetCursor(0)
+}
+
+// -----------------------------------------------------------------------------
+// Branch Sync & Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) syncBranches(ctx context.Context) {
+ defer m.skeleton.TriggerUpdate()
+
+ m.status.Reset()
+ m.status.SetProgressMessage(fmt.Sprintf("[%s] Fetching branches...",
+ m.selectedRepository.RepositoryName))
+
+ branches, err := m.fetchBranches(ctx)
+ if err != nil {
+ return
+ }
+
+ m.processBranches(branches)
+}
+
+func (m *ModelGithubWorkflow) fetchBranches(ctx context.Context) (*gu.GetRepositoryBranchesOutput, error) {
+ branches, err := m.github.GetRepositoryBranches(ctx, gu.GetRepositoryBranchesInput{
+ Repository: m.selectedRepository.RepositoryName,
+ })
+
+ if err != nil {
+ if !errors.Is(err, context.Canceled) {
+ m.status.SetError(err)
+ m.status.SetErrorMessage("Branches cannot be listed")
+ }
+ return nil, err
+ }
+
+ return branches, nil
+}
+
+func (m *ModelGithubWorkflow) processBranches(branches *gu.GetRepositoryBranchesOutput) {
+ if branches == nil || len(branches.Branches) == 0 {
+ m.handleEmptyBranches()
+ return
+ }
+
+ branchNames := make([]string, len(branches.Branches))
+ for i, branch := range branches.Branches {
+ branchNames[i] = branch.Name
+ }
+
+ m.textInput.SetSuggestions(branchNames)
+ m.status.SetSuccessMessage(fmt.Sprintf("[%s] Branches fetched.",
+ m.selectedRepository.RepositoryName))
+}
+
+func (m *ModelGithubWorkflow) handleEmptyBranches() {
+ m.selectedRepository.BranchName = ""
+ m.status.SetDefaultMessage(fmt.Sprintf("[%s] No branches found.",
+ m.selectedRepository.RepositoryName))
+}
+
+// -----------------------------------------------------------------------------
+// Table Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) updateWorkflowTable(workflows []gu.TriggerableWorkflow) {
+ rows := make([]table.Row, 0, len(workflows))
+ for _, workflow := range workflows {
+ rows = append(rows, table.Row{
+ workflow.Name,
+ workflow.Path,
+ })
+ }
+
+ sort.SliceStable(rows, func(i, j int) bool {
+ return rows[i][0] < rows[j][0]
+ })
+
+ m.tableTriggerableWorkflow.SetRows(rows)
+ if len(rows) > 0 {
+ m.tableTriggerableWorkflow.SetCursor(0)
+ }
+}
+
+func (m *ModelGithubWorkflow) finalizeUpdate() {
+ m.tableReady = true
+ m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s] Triggerable workflows fetched.",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+}
+
+func (m *ModelGithubWorkflow) handleTableInputs() {
+ if !m.tableReady {
+ return
+ }
+
+ rows := m.tableTriggerableWorkflow.Rows()
+ selectedRow := m.tableTriggerableWorkflow.SelectedRow()
+ if len(rows) > 0 && len(selectedRow) > 0 {
+ m.selectedRepository.WorkflowName = selectedRow[1]
+ }
+}
+
+// -----------------------------------------------------------------------------
+// UI Rendering
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) renderTable() string {
+ style := lipgloss.NewStyle().
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#3b698f")).
+ MarginLeft(1)
+
+ m.updateTableDimensions()
+ return style.Render(m.tableTriggerableWorkflow.View())
+}
+
+func (m *ModelGithubWorkflow) renderBranchInput() string {
+ style := lipgloss.NewStyle().
+ Border(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#3b698f")).
+ Padding(0, 1).
+ Width(m.skeleton.GetTerminalWidth() - 6).
+ MarginLeft(1)
+
+ if len(m.textInput.AvailableSuggestions()) > 0 && m.textInput.Value() == "" {
+ if !m.state.Repository.HasFlows {
+ m.textInput.Placeholder = "Branch selection disabled - No triggerable workflows available"
+ } else {
+ m.textInput.Placeholder = fmt.Sprintf("Type to switch branch (default: %s)", m.state.Repository.Branch)
+ }
+ }
+
+ return style.Render(m.textInput.View())
+}
+
+func (m *ModelGithubWorkflow) renderHelp() string {
+ helpStyle := WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4)
+ return helpStyle.Render(m.help.View(m.keys))
+}
+
+func (m *ModelGithubWorkflow) updateTableDimensions() {
+ termWidth := m.skeleton.GetTerminalWidth()
+ termHeight := m.skeleton.GetTerminalHeight()
+
+ var tableWidth int
+ for _, t := range tableColumnsWorkflow {
+ tableWidth += t.Width
+ }
+
+ newTableColumns := tableColumnsWorkflow
+ widthDiff := termWidth - tableWidth
+ if widthDiff > 0 {
+ newTableColumns[1].Width += widthDiff - 10
+ m.tableTriggerableWorkflow.SetColumns(newTableColumns)
+ m.tableTriggerableWorkflow.SetHeight(termHeight - 17)
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Tab Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflow) updateTabState() {
+ if !m.state.Repository.HasFlows {
+ m.skeleton.LockTab("trigger")
+ return
+ }
+ m.skeleton.UnlockTabs()
+}
diff --git a/internal/terminal/handler/ghworkflow/ghworkflow.go b/internal/terminal/handler/ghworkflow/ghworkflow.go
deleted file mode 100644
index c79cf12..0000000
--- a/internal/terminal/handler/ghworkflow/ghworkflow.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package ghworkflow
-
-import (
- "context"
- "errors"
- "fmt"
- "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"
- 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 {
- // current handler's properties
- syncTriggerableWorkflowsContext context.Context
- cancelSyncTriggerableWorkflows context.CancelFunc
- tableReady bool
- lastRepository string
-
- // shared properties
- SelectedRepository *hdltypes.SelectedRepository
-
- // use cases
- github gu.UseCase
-
- // keymap
- Keys keyMap
-
- // models
- Help help.Model
- Viewport *viewport.Model
- list list.Model
- tableTriggerableWorkflow table.Model
- modelError *hdlerror.ModelError
-
- modelTabOptions tea.Model
- actualModelTabOptions *taboptions.Options
-
- modelGithubTrigger tea.Model
- actualModelGithubTrigger *ghtrigger.ModelGithubTrigger
-}
-
-var baseStyle = lipgloss.NewStyle().
- BorderStyle(lipgloss.NormalBorder()).
- BorderForeground(lipgloss.Color("240"))
-
-func SetupModelGithubWorkflow(githubUseCase gu.UseCase, selectedRepository *hdltypes.SelectedRepository) *ModelGithubWorkflow {
- var tableRowsTriggerableWorkflow []table.Row
-
- tableTriggerableWorkflow := table.New(
- table.WithColumns(tableColumnsWorkflow),
- table.WithRows(tableRowsTriggerableWorkflow),
- 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)
- tableTriggerableWorkflow.SetStyles(s)
-
- modelError := hdlerror.SetupModelError()
- tabOptions := taboptions.NewOptions(&modelError)
-
- return &ModelGithubWorkflow{
- Help: help.New(),
- Keys: keys,
- github: githubUseCase,
- modelError: &modelError,
- tableTriggerableWorkflow: tableTriggerableWorkflow,
- SelectedRepository: selectedRepository,
- modelTabOptions: tabOptions,
- actualModelTabOptions: tabOptions,
- syncTriggerableWorkflowsContext: context.Background(),
- cancelSyncTriggerableWorkflows: func() {},
- }
-}
-
-func (m *ModelGithubWorkflow) Init() tea.Cmd {
- return nil
-}
-
-func (m *ModelGithubWorkflow) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmd tea.Cmd
-
- 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
-
- go m.syncTriggerableWorkflows(m.syncTriggerableWorkflowsContext)
- }
-
- m.tableTriggerableWorkflow, cmd = m.tableTriggerableWorkflow.Update(msg)
-
- m.handleTableInputs(m.syncTriggerableWorkflowsContext) // update table operations
-
- return m, cmd
-}
-
-func (m *ModelGithubWorkflow) View() string {
- termWidth := m.Viewport.Width
- termHeight := m.Viewport.Height
-
- var tableWidth int
- for _, t := range tableColumnsWorkflow {
- tableWidth += t.Width
- }
-
- newTableColumns := tableColumnsWorkflow
- widthDiff := termWidth - tableWidth
- if widthDiff > 0 {
- newTableColumns[1].Width += widthDiff - 11
- m.tableTriggerableWorkflow.SetColumns(newTableColumns)
- m.tableTriggerableWorkflow.SetHeight(termHeight - 17)
- }
-
- doc := strings.Builder{}
- doc.WriteString(baseStyle.Render(m.tableTriggerableWorkflow.View()))
-
- return doc.String()
-}
-
-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)
-
- // delete all rows
- m.tableTriggerableWorkflow.SetRows([]table.Row{})
-
- triggerableWorkflows, err := m.github.GetTriggerableWorkflows(ctx, gu.GetTriggerableWorkflowsInput{
- 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")
- 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))
- return
- }
-
- var tableRowsTriggerableWorkflow []table.Row
- for _, workflow := range triggerableWorkflows.TriggerableWorkflows {
- tableRowsTriggerableWorkflow = append(tableRowsTriggerableWorkflow, table.Row{
- workflow.Name,
- workflow.Path,
- })
- }
-
- sort.SliceStable(tableRowsTriggerableWorkflow, func(i, j int) bool {
- return tableRowsTriggerableWorkflow[i][0] < tableRowsTriggerableWorkflow[j][0]
- })
-
- 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
-}
-
-func (m *ModelGithubWorkflow) handleTableInputs(_ context.Context) {
- if !m.tableReady {
- return
- }
-
- // To avoid go routine leak
- rows := m.tableTriggerableWorkflow.Rows()
- selectedRow := m.tableTriggerableWorkflow.SelectedRow()
-
- if len(rows) > 0 && len(selectedRow) > 0 {
- 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..86e649b
--- /dev/null
+++ b/internal/terminal/handler/ghworkflowhistory.go
@@ -0,0 +1,556 @@
+package handler
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "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"
+ "github.com/termkit/gama/internal/config"
+ gu "github.com/termkit/gama/internal/github/usecase"
+ "github.com/termkit/gama/pkg/browser"
+ "github.com/termkit/skeleton"
+)
+
+// -----------------------------------------------------------------------------
+// Model Definition
+// -----------------------------------------------------------------------------
+
+type ModelGithubWorkflowHistory struct {
+ // Core dependencies
+ skeleton *skeleton.Skeleton
+ github gu.UseCase
+
+ // UI Components
+ Help help.Model
+ keys githubWorkflowHistoryKeyMap
+ tableWorkflowHistory table.Model
+ status *ModelStatus
+ modelTabOptions *ModelTabOptions
+
+ // Table state
+ tableReady bool
+ tableStyle lipgloss.Style
+ workflows []gu.Workflow
+ lastRepository string
+
+ // Live mode state
+ liveMode bool
+ liveModeInterval time.Duration
+
+ // Workflow state
+ selectedWorkflowID int64
+
+ // Context management
+ syncWorkflowHistoryContext context.Context
+ cancelSyncWorkflowHistory context.CancelFunc
+
+ // Shared state
+ selectedRepository *SelectedRepository
+}
+
+type workflowHistoryUpdateMsg struct {
+ UpdateAfter time.Duration
+}
+
+// -----------------------------------------------------------------------------
+// Constructor & Initialization
+// -----------------------------------------------------------------------------
+
+func SetupModelGithubWorkflowHistory(s *skeleton.Skeleton, githubUseCase gu.UseCase) *ModelGithubWorkflowHistory {
+ cfg, err := config.LoadConfig()
+ if err != nil {
+ panic(fmt.Sprintf("failed to load config: %v", err))
+ }
+
+ m := &ModelGithubWorkflowHistory{
+ // Initialize core dependencies
+ skeleton: s,
+ github: githubUseCase,
+
+ // Initialize UI components
+ Help: help.New(),
+ keys: githubWorkflowHistoryKeys,
+ status: SetupModelStatus(s),
+ modelTabOptions: NewOptions(s, SetupModelStatus(s)),
+
+ // Initialize state
+ selectedRepository: NewSelectedRepository(),
+ syncWorkflowHistoryContext: context.Background(),
+ cancelSyncWorkflowHistory: func() {},
+ liveMode: cfg.Settings.LiveMode.Enabled,
+ liveModeInterval: cfg.Settings.LiveMode.Interval,
+ tableStyle: setupTableStyle(),
+ }
+
+ // Setup table
+ m.tableWorkflowHistory = setupWorkflowHistoryTable()
+
+ return m
+}
+
+func setupTableStyle() lipgloss.Style {
+ return lipgloss.NewStyle().
+ BorderStyle(lipgloss.NormalBorder()).
+ BorderForeground(lipgloss.Color("#3b698f")).
+ MarginLeft(1)
+}
+
+func setupWorkflowHistoryTable() table.Model {
+ t := table.New(
+ table.WithColumns(tableColumnsWorkflowHistory),
+ table.WithRows([]table.Row{}),
+ table.WithFocused(true),
+ table.WithHeight(7),
+ )
+
+ // Apply styles
+ 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)
+ t.SetStyles(s)
+
+ t.KeyMap = table.KeyMap{
+ LineUp: key.NewBinding(key.WithKeys("up"), key.WithHelp("↑", "up")),
+ LineDown: key.NewBinding(key.WithKeys("down"), key.WithHelp("↓", "down")),
+ PageUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("pgup", "page up")),
+ PageDown: key.NewBinding(key.WithKeys("pgdown"), key.WithHelp("pgdn", "page down")),
+ GotoTop: key.NewBinding(key.WithKeys("home"), key.WithHelp("home", "go to start")),
+ GotoBottom: key.NewBinding(key.WithKeys("end"), key.WithHelp("end", "go to end")),
+ }
+
+ return t
+}
+
+// -----------------------------------------------------------------------------
+// Bubbletea Model Implementation
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) Init() tea.Cmd {
+ m.setupOptions()
+ m.startLiveMode()
+ return tea.Batch(m.modelTabOptions.Init())
+}
+
+func (m *ModelGithubWorkflowHistory) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ // Handle repository changes
+ if cmd := m.handleRepositoryChange(); cmd != nil {
+ return m, cmd
+ }
+
+ cursor := m.tableWorkflowHistory.Cursor()
+ if m.workflows != nil && cursor >= 0 && cursor < len(m.workflows) {
+ m.selectedWorkflowID = m.workflows[cursor].ID
+ }
+
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ // Handle different message types
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ cmd = m.handleKeyMsg(msg)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ case workflowHistoryUpdateMsg:
+ cmd = m.handleUpdateMsg(msg)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+
+ // Update UI components
+ if cmd = m.updateUIComponents(msg); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+
+ return m, tea.Batch(cmds...)
+}
+
+func (m *ModelGithubWorkflowHistory) View() string {
+ return lipgloss.JoinVertical(lipgloss.Top,
+ m.renderTable(),
+ m.modelTabOptions.View(),
+ m.status.View(),
+ m.renderHelp(),
+ )
+}
+
+// -----------------------------------------------------------------------------
+// Event Handlers
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
+ switch {
+ case key.Matches(msg, m.keys.Refresh):
+ go m.syncWorkflowHistory(m.syncWorkflowHistoryContext)
+ return nil
+ case key.Matches(msg, m.keys.LiveMode):
+ return m.toggleLiveMode()
+ }
+ return nil
+}
+
+func (m *ModelGithubWorkflowHistory) handleUpdateMsg(msg workflowHistoryUpdateMsg) tea.Cmd {
+ go func() {
+ time.Sleep(msg.UpdateAfter)
+ m.syncWorkflowHistory(m.syncWorkflowHistoryContext)
+ m.skeleton.TriggerUpdate()
+ }()
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+// Live Mode Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) startLiveMode() {
+ ticker := time.NewTicker(m.liveModeInterval)
+ go func() {
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ if m.liveMode {
+ m.skeleton.TriggerUpdateWithMsg(workflowHistoryUpdateMsg{
+ UpdateAfter: time.Nanosecond,
+ })
+ }
+ case <-m.syncWorkflowHistoryContext.Done():
+ return
+ }
+ }
+ }()
+}
+
+func (m *ModelGithubWorkflowHistory) toggleLiveMode() tea.Cmd {
+ m.liveMode = !m.liveMode
+
+ status := "Off"
+ message := "Live mode disabled"
+
+ if m.liveMode {
+ status = "On"
+ message = "Live mode enabled"
+ // Trigger immediate update when enabling
+ go m.syncWorkflowHistory(m.syncWorkflowHistoryContext)
+ }
+
+ m.status.SetSuccessMessage(message)
+ m.skeleton.UpdateWidgetValue("live", fmt.Sprintf("Live Mode: %s", status))
+
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+// Repository Change Handling
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) handleRepositoryChange() tea.Cmd {
+ if m.lastRepository == m.selectedRepository.RepositoryName {
+ return nil
+ }
+
+ if m.cancelSyncWorkflowHistory != nil {
+ m.cancelSyncWorkflowHistory()
+ }
+
+ m.lastRepository = m.selectedRepository.RepositoryName
+ m.syncWorkflowHistoryContext, m.cancelSyncWorkflowHistory = context.WithCancel(context.Background())
+
+ go m.syncWorkflowHistory(m.syncWorkflowHistoryContext)
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+// Workflow History Sync
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) syncWorkflowHistory(ctx context.Context) {
+ defer m.skeleton.TriggerUpdate()
+
+ // Add timeout to prevent hanging
+ ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+
+ m.initializeSyncState()
+
+ // Check context before proceeding
+ if ctx.Err() != nil {
+ m.status.SetDefaultMessage("Operation cancelled")
+ return
+ }
+
+ workflowHistory, err := m.fetchWorkflowHistory(ctx)
+ if err != nil {
+ m.handleFetchError(err)
+ return
+ }
+
+ m.processWorkflowHistory(workflowHistory)
+}
+
+func (m *ModelGithubWorkflowHistory) handleFetchError(err error) {
+ switch {
+ case errors.Is(err, context.Canceled):
+ m.status.SetDefaultMessage("Workflow history fetch cancelled")
+ case errors.Is(err, context.DeadlineExceeded):
+ m.status.SetErrorMessage("Workflow history fetch timed out")
+ default:
+ m.status.SetError(err)
+ m.status.SetErrorMessage(fmt.Sprintf("Failed to fetch workflow history: %v", err))
+ }
+}
+
+func (m *ModelGithubWorkflowHistory) initializeSyncState() {
+ 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(StatusWait)
+ m.clearWorkflowHistory()
+}
+
+func (m *ModelGithubWorkflowHistory) clearWorkflowHistory() {
+ m.tableWorkflowHistory.SetRows([]table.Row{})
+ m.workflows = nil
+}
+
+func (m *ModelGithubWorkflowHistory) fetchWorkflowHistory(ctx context.Context) (*gu.GetWorkflowHistoryOutput, error) {
+ history, err := m.github.GetWorkflowHistory(ctx, gu.GetWorkflowHistoryInput{
+ Repository: m.selectedRepository.RepositoryName,
+ Branch: m.selectedRepository.BranchName,
+ })
+
+ if err != nil {
+ if !errors.Is(err, context.Canceled) {
+ m.status.SetError(err)
+ m.status.SetErrorMessage("Workflow history cannot be listed")
+ }
+ return nil, err
+ }
+
+ return history, nil
+}
+
+// -----------------------------------------------------------------------------
+// Workflow History Processing
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) processWorkflowHistory(history *gu.GetWorkflowHistoryOutput) {
+ if len(history.Workflows) == 0 {
+ m.handleEmptyWorkflowHistory()
+ return
+ }
+
+ m.workflows = history.Workflows
+ m.updateWorkflowTable()
+ m.finalizeUpdate()
+}
+
+func (m *ModelGithubWorkflowHistory) handleEmptyWorkflowHistory() {
+ m.modelTabOptions.SetStatus(StatusNone)
+ m.status.SetDefaultMessage(fmt.Sprintf("[%s@%s] No workflow history found.",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+}
+
+func (m *ModelGithubWorkflowHistory) updateWorkflowTable() {
+ rows := make([]table.Row, 0, len(m.workflows))
+ for _, workflow := range m.workflows {
+ rows = append(rows, table.Row{
+ workflow.WorkflowName,
+ workflow.ActionName,
+ workflow.TriggeredBy,
+ workflow.StartedAt,
+ workflow.Status,
+ workflow.Duration,
+ })
+ }
+ m.tableWorkflowHistory.SetRows(rows)
+}
+
+func (m *ModelGithubWorkflowHistory) finalizeUpdate() {
+ m.tableReady = true
+ m.tableWorkflowHistory.SetCursor(0)
+ m.modelTabOptions.SetStatus(StatusIdle)
+ m.status.SetSuccessMessage(fmt.Sprintf("[%s@%s] Workflow history fetched.",
+ m.selectedRepository.RepositoryName, m.selectedRepository.BranchName))
+}
+
+// -----------------------------------------------------------------------------
+// UI Component Updates
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) updateUIComponents(msg tea.Msg) tea.Cmd {
+ var cmds []tea.Cmd
+ var cmd tea.Cmd
+
+ // Update table and handle navigation
+ m.tableWorkflowHistory, cmd = m.tableWorkflowHistory.Update(msg)
+ cmds = append(cmds, cmd)
+
+ // Update tab options
+ m.modelTabOptions, cmd = m.modelTabOptions.Update(msg)
+ cmds = append(cmds, cmd)
+
+ return tea.Batch(cmds...)
+}
+
+// -----------------------------------------------------------------------------
+// UI Rendering
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) renderTable() string {
+ m.updateTableDimensions()
+ return m.tableStyle.Render(m.tableWorkflowHistory.View())
+}
+
+func (m *ModelGithubWorkflowHistory) renderHelp() string {
+ helpStyle := WindowStyleHelp.Width(m.skeleton.GetTerminalWidth() - 4)
+ return helpStyle.Render(m.ViewHelp())
+}
+
+func (m *ModelGithubWorkflowHistory) updateTableDimensions() {
+ const (
+ minTableWidth = 80 // Minimum width to maintain readability
+ tablePadding = 18 // Account for borders and margins
+ minColumnWidth = 10 // Minimum width for any column
+ )
+
+ termWidth := m.skeleton.GetTerminalWidth()
+ termHeight := m.skeleton.GetTerminalHeight()
+
+ if termWidth <= minTableWidth {
+ return // Prevent table from becoming too narrow
+ }
+
+ var tableWidth int
+ for _, t := range tableColumnsWorkflowHistory {
+ tableWidth += t.Width
+ }
+
+ newTableColumns := make([]table.Column, len(tableColumnsWorkflowHistory))
+ copy(newTableColumns, tableColumnsWorkflowHistory)
+
+ widthDiff := termWidth - tableWidth - tablePadding
+ if widthDiff > 0 {
+ // Distribute extra width between workflow name and action name columns
+ extraWidth := widthDiff / 2
+ newTableColumns[0].Width = max(newTableColumns[0].Width+extraWidth, minColumnWidth)
+ newTableColumns[1].Width = max(newTableColumns[1].Width+extraWidth, minColumnWidth)
+
+ m.tableWorkflowHistory.SetColumns(newTableColumns)
+ }
+
+ // Ensure reasonable table height
+ maxHeight := termHeight - 17
+ if maxHeight > 0 {
+ m.tableWorkflowHistory.SetHeight(maxHeight)
+ }
+}
+
+// -----------------------------------------------------------------------------
+// Option Management
+// -----------------------------------------------------------------------------
+
+func (m *ModelGithubWorkflowHistory) setupOptions() {
+ m.modelTabOptions.AddOption("Open in browser", m.openInBrowser)
+ m.modelTabOptions.AddOption("Rerun failed jobs", m.rerunFailedJobs)
+ m.modelTabOptions.AddOption("Rerun workflow", m.rerunWorkflow)
+ m.modelTabOptions.AddOption("Cancel workflow", m.cancelWorkflow)
+}
+
+func (m *ModelGithubWorkflowHistory) openInBrowser() {
+ m.status.SetProgressMessage("Opening in browser...")
+
+ url := fmt.Sprintf("https://github.com/%s/actions/runs/%d",
+ m.selectedRepository.RepositoryName,
+ m.selectedWorkflowID)
+
+ if err := browser.OpenInBrowser(url); err != nil {
+ m.status.SetError(err)
+ m.status.SetErrorMessage("Failed to open in browser")
+ return
+ }
+ m.status.SetSuccessMessage("Opened in browser")
+}
+
+func (m *ModelGithubWorkflowHistory) rerunFailedJobs() {
+ 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")
+}
+
+func (m *ModelGithubWorkflowHistory) rerunWorkflow() {
+ if m.selectedWorkflowID == 0 {
+ m.status.SetErrorMessage("No workflow selected")
+ return
+ }
+
+ m.status.SetProgressMessage("Re-running workflow...")
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ _, err := m.github.ReRunWorkflow(ctx, gu.ReRunWorkflowInput{
+ Repository: m.selectedRepository.RepositoryName,
+ WorkflowID: m.selectedWorkflowID,
+ })
+
+ if err != nil {
+ if errors.Is(err, context.DeadlineExceeded) {
+ m.status.SetErrorMessage("Workflow re-run request timed out")
+ } else {
+ m.status.SetError(err)
+ m.status.SetErrorMessage(fmt.Sprintf("Failed to re-run workflow: %v", err))
+ }
+ return
+ }
+
+ m.status.SetSuccessMessage("Workflow re-run initiated")
+ // Trigger refresh after short delay to show updated status
+ go func() {
+ time.Sleep(2 * time.Second)
+ m.syncWorkflowHistory(m.syncWorkflowHistoryContext)
+ }()
+}
+
+func (m *ModelGithubWorkflowHistory) cancelWorkflow() {
+ 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")
+}
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..91d33bf 100644
--- a/internal/terminal/handler/handler.go
+++ b/internal/terminal/handler/handler.go
@@ -2,262 +2,44 @@ 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"
+ "github.com/termkit/gama/internal/config"
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"
)
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{}
-
- // 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,
+ cfg, err := config.LoadConfig()
+ if err != nil {
+ panic(fmt.Sprintf("failed to load config: %v", err))
}
- 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 := skeleton.NewSkeleton()
-func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmd tea.Cmd
- var cmds []tea.Cmd
+ s.AddPage("info", "Info", SetupModelInfo(s, githubUseCase, version))
+ s.AddPage("repository", "Repository", SetupModelGithubRepository(s, githubUseCase))
+ s.AddPage("history", "Workflow History", SetupModelGithubWorkflowHistory(s, githubUseCase))
+ s.AddPage("workflow", "Workflow", SetupModelGithubWorkflow(s, githubUseCase))
+ s.AddPage("trigger", "Trigger", SetupModelGithubTrigger(s, githubUseCase))
- 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)
- }
-
- return m, tea.Batch(cmds...)
-}
+ s.SetBorderColor("#ff0055").
+ SetActiveTabBorderColor("#ff0055").
+ SetInactiveTabBorderColor("#82636f").
+ SetWidgetBorderColor("#ff0055")
-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)
+ if cfg.Settings.LiveMode.Enabled {
+ s.AddWidget("live", "Live Mode: On")
+ } else {
+ s.AddWidget("live", "Live Mode: Off")
}
- 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(MinTerminalWidth)
+ s.SetTerminalViewportHeight(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..1feb45c 100644
--- a/internal/terminal/handler/keymap.go
+++ b/internal/terminal/handler/keymap.go
@@ -2,23 +2,32 @@ package handler
import (
"fmt"
+
"github.com/termkit/gama/internal/config"
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 +39,210 @@ 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
+ SwitchTab teakey.Binding
+}
+
+func (k githubRepositoryKeyMap) ShortHelp() []teakey.Binding {
+ return []teakey.Binding{k.SwitchTab, k.Refresh}
+}
+
+func (k githubRepositoryKeyMap) FullHelp() [][]teakey.Binding {
+ return [][]teakey.Binding{
+ {k.SwitchTab},
+ {k.Refresh},
+ }
+}
+
+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"),
+ ),
+ 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 {
+ Refresh teakey.Binding
+ SwitchTab teakey.Binding
+ LiveMode teakey.Binding
+}
+
+func (k githubWorkflowHistoryKeyMap) ShortHelp() []teakey.Binding {
+ return []teakey.Binding{k.SwitchTab, k.Refresh, k.LiveMode}
+}
+
+func (k githubWorkflowHistoryKeyMap) FullHelp() [][]teakey.Binding {
+ return [][]teakey.Binding{
+ {k.SwitchTab},
+ {k.Refresh},
+ {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"),
+ ),
+ 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.go
similarity index 52%
rename from internal/terminal/handler/error/error.go
rename to internal/terminal/handler/status.go
index fdecb07..fa8c8ac 100644
--- a/internal/terminal/handler/error/error.go
+++ b/internal/terminal/handler/status.go
@@ -1,15 +1,16 @@
-package error
+package handler
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
- hdltypes "github.com/termkit/gama/internal/terminal/handler/types"
- ts "github.com/termkit/gama/internal/terminal/style"
+ "github.com/termkit/skeleton"
)
-type ModelError struct {
+type ModelStatus struct {
+ skeleton *skeleton.Skeleton
+
// err is hold the error
err error
@@ -36,100 +37,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 = WindowStyleError.Width(width)
+ doc.WriteString(windowStyle.Render(m.viewError()))
+ return lipgloss.JoinHorizontal(lipgloss.Top, doc.String())
+ }
+
+ switch m.messageType {
+ case MessageTypeDefault:
+ windowStyle = WindowStyleDefault.Width(width)
+ case MessageTypeProgress:
+ windowStyle = WindowStyleProgress.Width(width)
+ case MessageTypeSuccess:
+ windowStyle = WindowStyleSuccess.Width(width)
+ default:
+ windowStyle = 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..65dac36
--- /dev/null
+++ b/internal/terminal/handler/table.go
@@ -0,0 +1,38 @@
+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: "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.go
similarity index 57%
rename from internal/terminal/handler/taboptions/taboptions.go
rename to internal/terminal/handler/taboptions.go
index ce9f29d..5ac3c21 100644
--- a/internal/terminal/handler/taboptions/taboptions.go
+++ b/internal/terminal/handler/taboptions.go
@@ -1,23 +1,24 @@
-package taboptions
+package handler
import (
"fmt"
"strings"
"time"
+ "github.com/termkit/skeleton"
+
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- hdlerror "github.com/termkit/gama/internal/terminal/handler/error"
)
-type Options struct {
- Style lipgloss.Style
+type ModelTabOptions struct {
+ skeleton *skeleton.Skeleton
- modelError *hdlerror.ModelError
- previousModelError hdlerror.ModelError
- modelLock bool
+ status *ModelStatus
+ previousStatus ModelStatus
+ modelLock bool
- status OptionStatus
+ optionStatus OptionStatus
options []string
optionsAction []string
@@ -34,119 +35,49 @@ type Options struct {
type OptionStatus string
const (
- // OptionIdle is for when the options are ready to use
- OptionIdle OptionStatus = "Idle"
+ // StatusIdle is for when the options are ready to use
+ StatusIdle OptionStatus = "Idle"
- // OptionWait is for when the options are not ready to use
- OptionWait OptionStatus = "Wait"
+ // StatusWait is for when the options are not ready to use
+ StatusWait OptionStatus = "Wait"
- // OptionNone is for when the options are not usable
- OptionNone OptionStatus = "None"
+ // StatusNone is for when the options are not usable
+ StatusNone OptionStatus = "None"
)
func (o OptionStatus) String() string {
return string(o)
}
-func NewOptions(modelError *hdlerror.ModelError) *Options {
- var b = lipgloss.RoundedBorder()
- b.Right = "├"
- b.Left = "┤"
-
- var OptionsStyle = lipgloss.NewStyle().
- Foreground(lipgloss.Color("15")).
- Align(lipgloss.Center).Padding(0, 1, 0, 1).
- Border(b)
-
+func NewOptions(sk *skeleton.Skeleton, modelStatus *ModelStatus) *ModelTabOptions {
var initialOptions = []string{
- OptionWait.String(),
+ StatusWait.String(),
}
var initialOptionsAction = []string{
- OptionWait.String(),
+ StatusWait.String(),
}
optionsWithFunc := make(map[int]func())
optionsWithFunc[0] = func() {} // NO OPERATION
- return &Options{
- Style: OptionsStyle,
+ return &ModelTabOptions{
+ skeleton: sk,
options: initialOptions,
optionsAction: initialOptionsAction,
optionsWithFunc: optionsWithFunc,
- status: OptionWait,
- modelError: modelError,
- }
-}
-
-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
+ optionStatus: StatusWait,
+ status: modelStatus,
}
- *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 {
+func (o *ModelTabOptions) 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 *ModelTabOptions) Update(msg tea.Msg) (*ModelTabOptions, tea.Cmd) {
var cmd tea.Cmd
- if o.status == OptionWait || o.status == OptionNone {
+ if o.optionStatus == StatusWait || o.optionStatus == StatusNone {
return o, cmd
}
@@ -169,24 +100,24 @@ 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 *ModelTabOptions) View() string {
+ var b = lipgloss.RoundedBorder()
+ b.Right = "├"
+ b.Left = "┤"
-func (o *Options) View() string {
- var style = o.Style.Foreground(lipgloss.Color("15"))
+ var style = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("15")).
+ Align(lipgloss.Center).Padding(0, 1, 0, 1).
+ Border(b).Foreground(lipgloss.Color("15"))
var opts []string
+ opts = append(opts, " ")
for i, option := range o.optionsAction {
- switch o.status {
- case OptionWait:
+ switch o.optionStatus {
+ case StatusWait:
style = style.BorderForeground(lipgloss.Color("208"))
- case OptionNone:
+ case StatusNone:
style = style.BorderForeground(lipgloss.Color("240"))
default:
isActive := i == o.cursor
@@ -202,3 +133,80 @@ func (o *Options) View() string {
return lipgloss.JoinHorizontal(lipgloss.Top, opts...)
}
+
+func (o *ModelTabOptions) 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.skeleton.TriggerUpdate()
+ }
+ o.modelLock = false
+ o.switchToPreviousError()
+ o.optionsAction[0] = string(StatusIdle)
+ o.cursor = 0
+ o.isTabSelected = false
+}
+
+func (o *ModelTabOptions) updateCursor(cursor int) {
+ if cursor < len(o.options) {
+ o.cursor = cursor
+ o.showAreYouSure()
+ go o.resetOptionsWithOriginal()
+ }
+}
+
+func (o *ModelTabOptions) SetStatus(status OptionStatus) {
+ o.optionStatus = status
+ o.options[0] = status.String()
+ o.optionsAction[0] = status.String()
+}
+
+func (o *ModelTabOptions) 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 *ModelTabOptions) getOptionMessage() string {
+ option := o.options[o.cursor]
+ option = strings.TrimPrefix(option, fmt.Sprintf("%d) ", o.cursor))
+ return option
+}
+
+func (o *ModelTabOptions) showAreYouSure() {
+ var yellowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("4")).Blink(true)
+
+ 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? %s",
+ o.getOptionMessage(),
+ yellowStyle.Render("[ Press Enter ]"),
+ ))
+
+}
+
+func (o *ModelTabOptions) switchToPreviousError() {
+ if o.modelLock {
+ return
+ }
+ *o.status = o.previousStatus
+}
+
+func (o *ModelTabOptions) 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..27a48f5
--- /dev/null
+++ b/internal/terminal/handler/types.go
@@ -0,0 +1,49 @@
+package handler
+
+import (
+ "sync"
+
+ "github.com/charmbracelet/lipgloss"
+)
+
+// SelectedRepository is a struct that holds the selected repository, workflow, and branch
+// It is a shared state between the different tabs
+type SelectedRepository struct {
+ RepositoryName string
+ WorkflowName string
+ BranchName string
+}
+
+// Constants
+const (
+ MinTerminalWidth = 102
+ MinTerminalHeight = 24
+)
+
+// Styles
+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)
+)
+
+// Constructor
+var (
+ onceSelectedRepository sync.Once
+ selectedRepository *SelectedRepository
+)
+
+func NewSelectedRepository() *SelectedRepository {
+ onceSelectedRepository.Do(func() {
+ selectedRepository = &SelectedRepository{}
+ })
+ return selectedRepository
+}
diff --git a/internal/terminal/handler/types/types.go b/internal/terminal/handler/types/types.go
deleted file mode 100644
index 8c9fcc3..0000000
--- a/internal/terminal/handler/types/types.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package types
-
-type SelectedRepository struct {
- RepositoryName string // full repository name (owner/name)
- WorkflowName string // workflow name
- BranchName string // branch name
-}
-
-var ScreenWidth *int
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{