From db75653e3d7688dc592773e38f52e4981f5e4784 Mon Sep 17 00:00:00 2001 From: oxpoe <324556608+oxpoe@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:47:55 +0700 Subject: [PATCH] chore(phase-0): struktur repo + hello TUI Bubble Tea v2 (T-0.1, T-0.2) --- LICENSE | 21 +++++++++++++ Makefile | 21 +++++++++++++ README.md | 41 ++++++++++++++++++++++++ THIRD_PARTY_NOTICES.md | 28 +++++++++++++++++ cmd/tia/main.go | 71 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 26 ++++++++++++++++ go.sum | 40 ++++++++++++++++++++++++ internal/app/app.go | 66 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 314 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 cmd/tia/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app/app.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6da4880 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 oxpoe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f08d5da --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +BINARY := tia + +.PHONY: build run test vet fmt clean + +build: + go build -o $(BINARY) ./cmd/tia + +run: build + ./$(BINARY) + +test: + go test ./... + +vet: + go vet ./... + +fmt: + gofmt -w . + +clean: + rm -f $(BINARY) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f115031 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# tia — Terminal Is A My Way + +> "Satu binary untuk semua kebutuhan Docker kamu." · Tagline: **Just Work** + +TUI modern (Go + Charm v2) + CLI pendamping untuk manajemen Docker: +stack & container, konverter `docker run` → compose, multi-host, mouse-first, +audio feedback, animasi ASCII. + +Status: **Phase 0 — fondasi** (lihat roadmap di `docs/PRD.md` §8). + +## Build & jalankan + +```bash +make build # menghasilkan ./tia +./tia # buka TUI +./tia version # versi & build info +make test # unit test +``` + +## Struktur repo + +``` +cmd/tia/ entry point — router TUI default vs CLI companion +internal/app/ root Bubble Tea model +internal/ (menyusul) docker, stack, config, ui, i18n, host, cli, audio +docs/PRD.md product requirements +docs/rea/ laporan analisis proyek referensi (playbook + modul) +reference/ repo referensi untuk REA — git-ignored, jangan di-commit +``` + +## Prinsip + +- **Satu binary** (`CGO_ENABLED=0`), tanpa dependency runtime selain Docker. +- **Core tanpa UI**: logika dibagi TUI & CLI → hasil identik (F-028). +- **Mouse & keyboard setara** sejak hari pertama (F-022). +- **i18n dari awal** — Indonesia default, English fallback (F-023). + +## Lisensi + +MIT — lihat `LICENSE`. Atribusi pihak ketiga: `THIRD_PARTY_NOTICES.md`, +register aset: `docs/ASSETS.md`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..62d234e --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,28 @@ +# Third-Party Notices + +## Proyek referensi (analisis perilaku — tanpa copy kode verbatim) + +Sesuai aturan REA (`docs/rea/README.md`): kami mempelajari **perilaku** proyek berikut lalu +mengimplementasikan ulang dalam Go. Tidak ada kode yang disalin. Tabel pemetaan flag di +`docs/rea/composerize.md` diadaptasi dari perilaku proyek aslinya (lisensi MIT). + +| Proyek | URL | Commit dianalisis | Lisensi | +|---|---|---|---| +| composerize | https://github.com/composerize/composerize | `3e39eb7` | MIT | +| Dockge | https://github.com/louislam/dockge | `f809ae1` | MIT | +| lazydocker | https://github.com/jesseduffield/lazydocker | `7e7aadc` | MIT | + +Repo referensi hanya ada di `reference/` (git-ignored). + +## Library Go yang dipakai + +| Library | Versi | Lisensi | +|---|---|---| +| bubbletea | v2.1.0 | MIT | +| lipgloss | v2.0.6 | MIT | + +*(Daftar bertambah setiap dependency baru diadopsi; wajib selaras dengan kebijakan aset PRD Appendix C.)* + +## Aset + +Lihat `docs/ASSETS.md`. diff --git a/cmd/tia/main.go b/cmd/tia/main.go new file mode 100644 index 0000000..63179b0 --- /dev/null +++ b/cmd/tia/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + "os" + "runtime/debug" + + tea "charm.land/bubbletea/v2" + + "github.com/oxpoe/tia/internal/app" +) + +const usage = `tia — Terminal Is A My Way (Just Work) + +Pemakaian: + tia Buka TUI interaktif + tia version Tampilkan versi & build info + +Subcommand CLI pendamping (convert, ps, up, down, update, doctor) +menyusul di Phase 1–3 — lihat docs/PRD.md §F-028. +` + +func main() { + if len(os.Args) > 1 { + switch os.Args[1] { + case "version", "--version": + fmt.Println(versionString()) + return + default: + fmt.Fprintf(os.Stderr, "perintah tidak dikenal: %q\n\n%s", os.Args[1], usage) + os.Exit(2) + } + } + + if _, err := tea.NewProgram(app.New()).Run(); err != nil { + fmt.Fprintln(os.Stderr, "tia:", err) + os.Exit(1) + } +} + +// versionString memakai build info VCS — tanpa ldflags. +func versionString() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "tia dev" + } + ver := info.Main.Version + if ver == "" || ver == "(devel)" { + ver = "dev" + } + rev, dirty := "", false + for _, s := range info.Settings { + switch s.Key { + case "vcs.revision": + if len(s.Value) >= 7 { + rev = s.Value[:7] + } + case "vcs.modified": + dirty = s.Value == "true" + } + } + out := "tia " + ver + if rev != "" { + out += " (" + rev + if dirty { + out += "-dirty" + } + out += ")" + } + return out +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a8821af --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/oxpoe/tia + +go 1.27.1 + +require ( + charm.land/bubbletea/v2 v2.1.0 + charm.land/lipgloss/v2 v2.0.6 +) + +require ( + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 // indirect + github.com/charmbracelet/x/ansi v0.11.9 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect + github.com/mattn/go-runewidth v0.0.30 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v1.0.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..55205f3 --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +charm.land/bubbletea/v2 v2.1.0 h1:A9fj9h5LFrvQhQHFNggp8K8rvY6CUfgn9dMbb6p2HIw= +charm.land/bubbletea/v2 v2.1.0/go.mod h1:xWHeyxIX9/wiFU2kgPW04ojOZyTzrWf+8lGjUCaftiU= +charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ= +charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 h1:hISLzUbwcvJbGoNR6A3L/TpguRRnACBmZOee8LcHLJw= +github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4/go.mod h1:aJDVk+WOVM1K04rdc/71PMPoUGfjxjhvXfP3Pjz1Zio= +github.com/charmbracelet/x/ansi v0.11.9 h1:kIrqLBxtTVbLyyHO2Evoi93EjQo1mylWmpNEsfPxXAM= +github.com/charmbracelet/x/ansi v0.11.9/go.mod h1:DG/hpOnaI2QJ/KMMqLL2h0KVhluuJLazf8973P+Vy5w= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk= +github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= +github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..840fee4 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,66 @@ +package app + +import ( + "fmt" + + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" +) + +// Model root sementara (T-0.2, Phase 0). +// Tujuannya membuktikan Bubble Tea v2 (View struct, mouse, resize) sebelum +// layout penuh Phase 1 menggantikannya. +type Model struct { + width, height int + clicks int + lastClick string +} + +func New() Model { + return Model{lastClick: "belum ada klik"} +} + +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, m.height = msg.Width, msg.Height + + case tea.KeyPressMsg: + switch msg.String() { + case "q", "ctrl+c", "esc": + return m, tea.Quit + } + + case tea.MouseClickMsg: + if msg.Button == tea.MouseLeft { + m.clicks++ + m.lastClick = fmt.Sprintf("klik #%d di (%d, %d)", m.clicks, msg.X, msg.Y) + } + } + return m, nil +} + +func (m Model) View() tea.View { + title := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#cba6f7")). + Render("tia — Terminal Is A My Way") + tagline := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#a6adc8")). + Render("Just Work · Phase 0 · skeleton TUI") + box := lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("#6c7086")). + Padding(0, 2). + Render(m.lastClick + "\n\nklik mouse di mana saja · tekan q untuk keluar") + + content := fmt.Sprintf("%s\n%s\n\n%s\n\nukuran jendela: %dx%d", + title, tagline, box, m.width, m.height) + + v := tea.NewView(content) + v.AltScreen = true + v.MouseMode = tea.MouseModeCellMotion + return v +}