diff --git a/api/api/versions.json b/api/api/versions.json index b49780bd..48ad5f66 100644 --- a/api/api/versions.json +++ b/api/api/versions.json @@ -3,7 +3,7 @@ { "version": "v1", "status": "active", - "release_date": "2025-04-13T11:09:59.056579+05:30", + "release_date": "2025-04-13T11:31:07.017069+05:30", "end_of_life": "0001-01-01T00:00:00Z", "changes": [ "Initial API version" diff --git a/api/go.mod b/api/go.mod index 8156f261..2c5c2651 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,6 +5,8 @@ go 1.23.6 toolchain go1.23.7 require ( + github.com/charmbracelet/bubbletea v1.3.4 + github.com/charmbracelet/lipgloss v1.0.0 github.com/docker/docker v28.0.1+incompatible github.com/docker/go-connections v0.5.0 github.com/getkin/kin-openapi v0.129.0 @@ -35,11 +37,15 @@ require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/creack/pty v1.1.24 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -63,10 +69,13 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // 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.16 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect @@ -74,6 +83,9 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/oasdiff/yaml v0.0.0-20241214135536-5f7845c759c8 // indirect github.com/oasdiff/yaml3 v0.0.0-20241214160948-977117996672 // indirect github.com/onsi/ginkgo v1.16.5 // indirect @@ -84,6 +96,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect diff --git a/api/go.sum b/api/go.sum index 5af4c797..52721055 100644 --- a/api/go.sum +++ b/api/go.sum @@ -4,8 +4,18 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +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.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= @@ -22,6 +32,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -117,6 +129,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -125,6 +139,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs= github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -143,6 +161,12 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -175,6 +199,9 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= @@ -286,6 +313,7 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/api/main.go b/api/main.go index bce07c6c..617cb422 100644 --- a/api/main.go +++ b/api/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "flag" "log" "net/http" @@ -23,6 +24,14 @@ func main() { ctx := context.Background() app := storage.NewApp(&types.Config{}, store, ctx) + tuiMode := flag.Bool("tui", false, "Run in TUI mode") + flag.Parse() + + if *tuiMode { + runTUI() + return + } + // cacheClient, err := cache.NewCache(config.AppConfig.RedisURL) // if err != nil { // log.Fatalf("Failed to initialize cache: %v", err) diff --git a/api/tui.go b/api/tui.go new file mode 100644 index 00000000..88500d07 --- /dev/null +++ b/api/tui.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "log" + "os" + tea "github.com/charmbracelet/bubbletea" + "github.com/raghavyuva/nixopus-api/internal/config" + "github.com/raghavyuva/nixopus-api/internal/storage" + "github.com/raghavyuva/nixopus-api/tui/views" + "github.com/raghavyuva/nixopus-api/internal/types" +) + +type model struct { + store *storage.Store + ctx context.Context + mainView *views.MainView + authView *views.AuthView + width int + height int +} + +func initialModel(store *storage.Store, ctx context.Context) model { + return model{ + store: store, + ctx: ctx, + mainView: views.NewMainView(store, ctx), + authView: views.NewAuthView(store, ctx), + } +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + m.mainView.Update(msg) + m.authView.Update(msg) + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + } + } + + // Update the current view based on authentication state + if m.authView.IsAuthenticated { + m.mainView.Update(msg) + } else { + m.authView.Update(msg) + } + + return m, nil +} + +// View renders the current view +// If the user is authenticated, it renders the main view +// Otherwise, it renders the auth view +func (m model) View() string { + if m.authView.IsAuthenticated { + return m.mainView.View() + } + return m.authView.View() +} + +func runTUI() { + store := config.Init() + ctx := context.Background() + _ = storage.NewApp(&types.Config{}, store, ctx) + + p := tea.NewProgram(initialModel(store, ctx), tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + log.Fatal(err) + os.Exit(1) + } +} diff --git a/api/tui/views/auth.go b/api/tui/views/auth.go new file mode 100644 index 00000000..2c862e5d --- /dev/null +++ b/api/tui/views/auth.go @@ -0,0 +1,179 @@ +package views + +import ( + "context" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/go-fuego/fuego" + auth "github.com/raghavyuva/nixopus-api/internal/features/auth/controller" + authService "github.com/raghavyuva/nixopus-api/internal/features/auth/service" + authStorage "github.com/raghavyuva/nixopus-api/internal/features/auth/storage" + auth_types "github.com/raghavyuva/nixopus-api/internal/features/auth/types" + "github.com/raghavyuva/nixopus-api/internal/features/logger" + "github.com/raghavyuva/nixopus-api/internal/features/notification" + orgService "github.com/raghavyuva/nixopus-api/internal/features/organization/service" + orgStorage "github.com/raghavyuva/nixopus-api/internal/features/organization/storage" + permService "github.com/raghavyuva/nixopus-api/internal/features/permission/service" + permStorage "github.com/raghavyuva/nixopus-api/internal/features/permission/storage" + roleService "github.com/raghavyuva/nixopus-api/internal/features/role/service" + roleStorage "github.com/raghavyuva/nixopus-api/internal/features/role/storage" + appStorage "github.com/raghavyuva/nixopus-api/internal/storage" +) + +type AuthView struct { + store *appStorage.Store + ctx context.Context + width int + height int + authController *auth.AuthController + username string + password string + showPassword bool + errorMessage string + IsAuthenticated bool + isPasswordField bool + cursor int + choices []string +} + +func NewAuthView(store *appStorage.Store, ctx context.Context) *AuthView { + userStorage := &authStorage.UserStorage{DB: store.DB, Ctx: ctx} + permStorage := &permStorage.PermissionStorage{DB: store.DB, Ctx: ctx} + roleStorage := &roleStorage.RoleStorage{DB: store.DB, Ctx: ctx} + orgStorage := &orgStorage.OrganizationStore{DB: store.DB, Ctx: ctx} + + permService := permService.NewPermissionService(store, ctx, logger.NewLogger(), permStorage) + roleService := roleService.NewRoleService(store, ctx, logger.NewLogger(), roleStorage) + orgService := orgService.NewOrganizationService(store, ctx, logger.NewLogger(), orgStorage) + + authService := authService.NewAuthService(userStorage, logger.NewLogger(), permService, roleService, orgService, ctx) + notificationManager := notification.NewNotificationManager(store.DB) + + return &AuthView{ + store: store, + ctx: ctx, + authController: auth.NewAuthController(ctx, logger.NewLogger(), notificationManager, *authService), + username: "", + password: "", + showPassword: false, + errorMessage: "", + IsAuthenticated: false, + isPasswordField: false, + } +} + +func (a *AuthView) Init() tea.Cmd { + return nil +} + +func (a *AuthView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter": + if a.username != "" && a.password != "" { + _, err := a.authController.Login(fuego.NewMockContext(auth_types.LoginRequest{ + Email: a.username, + Password: a.password, + })) + if err != nil { + a.errorMessage = err.Error() + return a, nil + } + a.IsAuthenticated = true + return a, nil + } + case "tab": + a.isPasswordField = !a.isPasswordField + case "up", "k": + if a.cursor > 0 { + a.cursor-- + } + case "down", "j": + if a.cursor < len(a.choices)-1 { + a.cursor++ + } + case "backspace": + if a.isPasswordField { + if len(a.password) > 0 { + a.password = a.password[:len(a.password)-1] + } + } else { + if len(a.username) > 0 { + a.username = a.username[:len(a.username)-1] + } + } + default: + if len(msg.String()) == 1 { + if a.isPasswordField { + a.password += msg.String() + } else { + a.username += msg.String() + } + } + } + case tea.WindowSizeMsg: + a.width = msg.Width + a.height = msg.Height + } + + return a, nil +} + +func (a *AuthView) View() string { + doc := strings.Builder{} + + title := lipgloss.NewStyle(). + Foreground(lipgloss.Color("62")). + Bold(true). + Render("Nixopus Authentication") + doc.WriteString(title + "\n\n") + + usernameLabel := lipgloss.NewStyle(). + Render("Username: ") + usernameInput := lipgloss.NewStyle(). + Foreground(lipgloss.Color("255")). + Render(a.username) + if !a.isPasswordField { + usernameInput = lipgloss.NewStyle(). + Foreground(lipgloss.Color("255")). + Render(a.username) + } + doc.WriteString(usernameLabel + usernameInput + "\n") + + passwordLabel := lipgloss.NewStyle(). + Foreground(lipgloss.Color("240")). + Render("Password: ") + passwordInput := lipgloss.NewStyle(). + Foreground(lipgloss.Color("255")). + Render(strings.Repeat("*", len(a.password))) + if a.isPasswordField { + passwordInput = lipgloss.NewStyle(). + Foreground(lipgloss.Color("255")). + Render(strings.Repeat("*", len(a.password))) + } + doc.WriteString(passwordLabel + passwordInput + "\n") + + instructions := lipgloss.NewStyle(). + Foreground(lipgloss.Color("240")). + Render("\nTab: Switch between username/password\nEnter: Login\nCtrl+C: Quit") + doc.WriteString(instructions) + + if a.errorMessage != "" { + errorStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("1")). + Render(a.errorMessage) + doc.WriteString("\n\n" + errorStyle) + } + + border := lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("62")). + Padding(1, 2). + Width(a.width - 4). + Height(a.height - 4) + + return border.Render(doc.String()) +} diff --git a/api/tui/views/main.go b/api/tui/views/main.go new file mode 100644 index 00000000..34eb72f4 --- /dev/null +++ b/api/tui/views/main.go @@ -0,0 +1,106 @@ +package views + +import ( + "context" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + appStorage "github.com/raghavyuva/nixopus-api/internal/storage" +) + +type MainView struct { + store *appStorage.Store + ctx context.Context + width int + height int + activeTab int + tabs []string +} + +func NewMainView(store *appStorage.Store, ctx context.Context) *MainView { + return &MainView{ + store: store, + ctx: ctx, + tabs: []string{"Auth", "Organizations", "Deployments", "Settings"}, + activeTab: 0, + } +} + +func (m *MainView) Init() tea.Cmd { + return nil +} + +func (m *MainView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + case tea.KeyMsg: + switch msg.String() { + case "left", "h": + if m.activeTab > 0 { + m.activeTab-- + } + case "right", "l": + if m.activeTab < len(m.tabs)-1 { + m.activeTab++ + } + case "q", "ctrl+c": + return m, tea.Quit + } + } + return m, nil +} + +func (m *MainView) View() string { + doc := strings.Builder{} + + renderedTabs := []string{} + for i, t := range m.tabs { + var style lipgloss.Style + if i == m.activeTab { + style = lipgloss.NewStyle().Foreground(lipgloss.Color("62")).Bold(true) + } else { + style = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + } + renderedTabs = append(renderedTabs, style.Render(t)) + } + + row := lipgloss.JoinHorizontal( + lipgloss.Top, + renderedTabs..., + ) + + gap := strings.Repeat(" ", max(0, m.width-lipgloss.Width(row)-2)) + row = lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap) + doc.WriteString(row + "\n\n") + + var content string + switch m.activeTab { + case 0: + content = "Auth View - Login/Register" + case 1: + content = "Organizations View - Manage Organizations" + case 2: + content = "Deployments View - Manage Deployments" + case 3: + content = "Settings View - Configure Settings" + } + + border := lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("62")). + Padding(1, 2). + Width(m.width - 4). + Height(m.height - 6) + + return border.Render(doc.String() + content) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +}