From 4981340e16e0e296c9786ff920edda2ed9fa590f Mon Sep 17 00:00:00 2001 From: towhid Date: Thu, 2 Nov 2023 00:13:35 +0600 Subject: [PATCH 1/6] started file picker --- cmd/aesir-cli/main.go | 89 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 17 +++++++++ go.sum | 42 +++++++++++++++++++- 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 cmd/aesir-cli/main.go diff --git a/cmd/aesir-cli/main.go b/cmd/aesir-cli/main.go new file mode 100644 index 0000000..245e37e --- /dev/null +++ b/cmd/aesir-cli/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/charmbracelet/bubbles/filepicker" + tea "github.com/charmbracelet/bubbletea" +) + +type model struct { + filepicker filepicker.Model + selectedFile string + quitting bool + err error +} + +type clearErrorMsg struct{} + +func clearErrorAfter(t time.Duration) tea.Cmd { + return tea.Tick(t, func(_ time.Time) tea.Msg { + return clearErrorMsg{} + }) +} + +func (m model) Init() tea.Cmd { + return m.filepicker.Init() +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + m.quitting = true + return m, tea.Quit + } + case clearErrorMsg: + m.err = nil + } + + var cmd tea.Cmd + m.filepicker, cmd = m.filepicker.Update(msg) + + if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { + m.selectedFile = path + } + + if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { + m.err = errors.New(path + " is not valid.") + m.selectedFile = "" + return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) + } + + return m, cmd +} + +func (m model) View() string { + if m.quitting { + return "" + } + var s strings.Builder + s.WriteString("\n ") + if m.err != nil { + s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) + } else if m.selectedFile == "" { + s.WriteString("Pick a file:") + } else { + s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile)) + } + s.WriteString("\n\n" + m.filepicker.View() + "\n") + return s.String() +} + +func main() { + fp := filepicker.New() + fp.AllowedTypes = []string{".ae"} + fp.CurrentDirectory, _ = os.UserHomeDir() + + m := model{ + filepicker: fp, + } + tm, _ := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run() + mm := tm.(model) + fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") +} \ No newline at end of file diff --git a/go.mod b/go.mod index e84f19f..7979699 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,9 @@ require ( entgo.io/contrib v0.4.5 entgo.io/ent v0.12.4 github.com/99designs/gqlgen v0.17.40 + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.9.1 github.com/hashicorp/go-multierror v1.1.1 golang.org/x/sync v0.4.0 ) @@ -14,15 +17,29 @@ require ( ariga.io/atlas v0.15.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/hcl/v2 v2.19.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // 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/rivo/uniseg v0.2.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.14.1 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/term v0.6.0 // indirect ) require ( diff --git a/go.sum b/go.sum index d8fc2ef..cd07b62 100644 --- a/go.sum +++ b/go.sum @@ -18,12 +18,26 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +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.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -47,16 +61,36 @@ github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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 v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +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/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/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-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 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-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -87,8 +121,12 @@ golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= From aae97ceb9f2de3d325858355e5d1e6e8c4542bbe Mon Sep 17 00:00:00 2001 From: towhid Date: Thu, 2 Nov 2023 23:12:17 +0600 Subject: [PATCH 2/6] paging in progress --- cmd/aesir-cli/main.go | 288 +++++++++++++++++++++++++++++++++++------- go.mod | 1 + go.sum | 2 + 3 files changed, 243 insertions(+), 48 deletions(-) diff --git a/cmd/aesir-cli/main.go b/cmd/aesir-cli/main.go index 245e37e..3038bfe 100644 --- a/cmd/aesir-cli/main.go +++ b/cmd/aesir-cli/main.go @@ -1,89 +1,281 @@ package main +// An example demonstrating an application with multiple views. +// +// Note that this example was produced before the Bubbles progress component +// was available (github.com/charmbracelet/bubbles/progress) and thus, we're +// implementing a progress bar from scratch here. + import ( - "errors" "fmt" - "os" + "math" + "strconv" "strings" "time" - "github.com/charmbracelet/bubbles/filepicker" tea "github.com/charmbracelet/bubbletea" + "github.com/fogleman/ease" + "github.com/lucasb-eyer/go-colorful" + "github.com/muesli/reflow/indent" + "github.com/muesli/termenv" ) -type model struct { - filepicker filepicker.Model - selectedFile string - quitting bool - err error +const ( + progressBarWidth = 71 + progressFullChar = "█" + progressEmptyChar = "░" +) + +// General stuff for styling the view +var ( + term = termenv.EnvColorProfile() + keyword = makeFgStyle("211") + subtle = makeFgStyle("241") + progressEmpty = subtle(progressEmptyChar) + dot = colorFg(" • ", "236") + + // Gradient colors we'll use for the progress bar + ramp = makeRamp("#B14FFF", "#00FFA3", progressBarWidth) +) + +func main() { + initialModel := model{0, false, 10, 0, 0, false, false} + p := tea.NewProgram(initialModel) + if _, err := p.Run(); err != nil { + fmt.Println("could not start program:", err) + } } -type clearErrorMsg struct{} +type ( + tickMsg struct{} + frameMsg struct{} +) + +func tick() tea.Cmd { + return tea.Tick(time.Second, func(time.Time) tea.Msg { + return tickMsg{} + }) +} -func clearErrorAfter(t time.Duration) tea.Cmd { - return tea.Tick(t, func(_ time.Time) tea.Msg { - return clearErrorMsg{} +func frame() tea.Cmd { + return tea.Tick(time.Second/60, func(time.Time) tea.Msg { + return frameMsg{} }) } +type model struct { + Choice int + Chosen bool + Ticks int + Frames int + Progress float64 + Loaded bool + Quitting bool +} + func (m model) Init() tea.Cmd { - return m.filepicker.Init() + return tick() } +// Main update function. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Make sure these keys always quit + if msg, ok := msg.(tea.KeyMsg); ok { + k := msg.String() + if k == "q" || k == "esc" || k == "ctrl+c" { + m.Quitting = true + return m, tea.Quit + } + } + + // Hand off the message and model to the appropriate update function for the + // appropriate view based on the current state. + if !m.Chosen { + return updateChoices(msg, m) + } + return updateChosen(msg, m) +} + +// The main view, which just calls the appropriate sub-view +func (m model) View() string { + var s string + if m.Quitting { + return "\n See you later!\n\n" + } + if !m.Chosen { + s = choicesView(m) + } else { + s = chosenView(m) + } + return indent.String("\n"+s+"\n\n", 2) +} + +// Sub-update functions + +// Update loop for the first view where you're choosing a task. +func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "ctrl+c", "q": - m.quitting = true + case "j", "down": + m.Choice++ + if m.Choice > 3 { + m.Choice = 3 + } + case "k", "up": + m.Choice-- + if m.Choice < 0 { + m.Choice = 0 + } + case "enter": + m.Chosen = true + return m, frame() + } + + case tickMsg: + if m.Ticks == 0 { + m.Quitting = true return m, tea.Quit } - case clearErrorMsg: - m.err = nil + m.Ticks-- + return m, tick() } - var cmd tea.Cmd - m.filepicker, cmd = m.filepicker.Update(msg) + return m, nil +} - if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { - m.selectedFile = path +// Update loop for the second view after a choice has been made +func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) { + switch msg.(type) { + case frameMsg: + if !m.Loaded { + m.Frames++ + m.Progress = ease.OutBounce(float64(m.Frames) / float64(100)) + if m.Progress >= 1 { + m.Progress = 1 + m.Loaded = true + m.Ticks = 3 + return m, tick() + } + return m, frame() + } + + case tickMsg: + if m.Loaded { + if m.Ticks == 0 { + m.Quitting = true + return m, tea.Quit + } + m.Ticks-- + return m, tick() + } + } + + return m, nil +} + +// Sub-views + +// The first view, where you're choosing a task +func choicesView(m model) string { + c := m.Choice + + tpl := "What to do today?\n\n" + tpl += "%s\n\n" + tpl += "Program quits in %s seconds\n\n" + tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") + + choices := fmt.Sprintf( + "%s\n%s\n%s\n%s", + checkbox("Plant carrots", c == 0), + checkbox("Go to the market", c == 1), + checkbox("Read something", c == 2), + checkbox("See friends", c == 3), + ) + + return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Ticks), "79")) +} + +// The second view, after a task has been chosen +func chosenView(m model) string { + var msg string + + switch m.Choice { + case 0: + msg = fmt.Sprintf("Carrot planting?\n\nCool, we'll need %s and %s...", keyword("libgarden"), keyword("vegeutils")) + case 1: + msg = fmt.Sprintf("A trip to the market?\n\nOkay, then we should install %s and %s...", keyword("marketkit"), keyword("libshopping")) + case 2: + msg = fmt.Sprintf("Reading time?\n\nOkay, cool, then we’ll need a library. Yes, an %s.", keyword("actual library")) + default: + msg = fmt.Sprintf("It’s always good to see friends.\n\nFetching %s and %s...", keyword("social-skills"), keyword("conversationutils")) } - if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { - m.err = errors.New(path + " is not valid.") - m.selectedFile = "" - return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) + label := "Downloading..." + if m.Loaded { + label = fmt.Sprintf("Downloaded. Exiting in %s seconds...", colorFg(strconv.Itoa(m.Ticks), "79")) } - return m, cmd + return msg + "\n\n" + label + "\n" + progressbar(m.Progress) + "%" } -func (m model) View() string { - if m.quitting { - return "" +func checkbox(label string, checked bool) string { + if checked { + return colorFg("[x] "+label, "212") } - var s strings.Builder - s.WriteString("\n ") - if m.err != nil { - s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) - } else if m.selectedFile == "" { - s.WriteString("Pick a file:") - } else { - s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile)) + return fmt.Sprintf("[ ] %s", label) +} + +func progressbar(percent float64) string { + w := float64(progressBarWidth) + + fullSize := int(math.Round(w * percent)) + var fullCells string + for i := 0; i < fullSize; i++ { + fullCells += termenv.String(progressFullChar).Foreground(term.Color(ramp[i])).String() } - s.WriteString("\n\n" + m.filepicker.View() + "\n") - return s.String() + + emptySize := int(w) - fullSize + emptyCells := strings.Repeat(progressEmpty, emptySize) + + return fmt.Sprintf("%s%s %3.0f", fullCells, emptyCells, math.Round(percent*100)) } -func main() { - fp := filepicker.New() - fp.AllowedTypes = []string{".ae"} - fp.CurrentDirectory, _ = os.UserHomeDir() +// Utils + +// Color a string's foreground with the given value. +func colorFg(val, color string) string { + return termenv.String(val).Foreground(term.Color(color)).String() +} + +// Return a function that will colorize the foreground of a given string. +func makeFgStyle(color string) func(string) string { + return termenv.Style{}.Foreground(term.Color(color)).Styled +} + +// Generate a blend of colors. +func makeRamp(colorA, colorB string, steps float64) (s []string) { + cA, _ := colorful.Hex(colorA) + cB, _ := colorful.Hex(colorB) + + for i := 0.0; i < steps; i++ { + c := cA.BlendLuv(cB, i/steps) + s = append(s, colorToHex(c)) + } + return +} + +// Convert a colorful.Color to a hexadecimal format compatible with termenv. +func colorToHex(c colorful.Color) string { + return fmt.Sprintf("#%s%s%s", colorFloatToHex(c.R), colorFloatToHex(c.G), colorFloatToHex(c.B)) +} - m := model{ - filepicker: fp, +// Helper function for converting colors to hex. Assumes a value between 0 and +// 1. +func colorFloatToHex(f float64) (s string) { + s = strconv.FormatInt(int64(f*255), 16) + if len(s) == 1 { + s = "0" + s } - tm, _ := tea.NewProgram(&m, tea.WithOutput(os.Stderr)).Run() - mm := tm.(model) - fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") + return } \ No newline at end of file diff --git a/go.mod b/go.mod index 7979699..b6e8bc1 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 github.com/google/uuid v1.4.0 // indirect github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect diff --git a/go.sum b/go.sum index cd07b62..7d66414 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776 h1:VRIbnDWRmAh5yBdz+J6yFMF5vso1It6vn+WmM/5l7MA= +github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776/go.mod h1:9wvnDu3YOfxzWM9Cst40msBF1C2UdQgDv962oTxSuMs= github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= From 43e0d0e954137fd3f663a22d1ecbd1decb44ce11 Mon Sep 17 00:00:00 2001 From: towhid Date: Sat, 4 Nov 2023 21:53:43 +0600 Subject: [PATCH 3/6] multi-view in progress --- cmd/aesir-cli/main.go | 103 +++++++++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 21 deletions(-) diff --git a/cmd/aesir-cli/main.go b/cmd/aesir-cli/main.go index 3038bfe..53b6c00 100644 --- a/cmd/aesir-cli/main.go +++ b/cmd/aesir-cli/main.go @@ -7,12 +7,15 @@ package main // implementing a progress bar from scratch here. import ( + "errors" "fmt" "math" + "os" "strconv" "strings" "time" + "github.com/charmbracelet/bubbles/filepicker" tea "github.com/charmbracelet/bubbletea" "github.com/fogleman/ease" "github.com/lucasb-eyer/go-colorful" @@ -39,11 +42,21 @@ var ( ) func main() { - initialModel := model{0, false, 10, 0, 0, false, false} - p := tea.NewProgram(initialModel) - if _, err := p.Run(); err != nil { + var tm tea.Model + var err error + fp := filepicker.New() + fp.AllowedTypes = []string{".mod", ".sum", ".go", ".txt", ".md"} + fp.CurrentDirectory, _ = os.UserHomeDir() + // fp.ShowHidden = true + + initialModel := model{fp, "", 0, false, 10, 0, 0, false, false} + + p := tea.NewProgram(&initialModel, tea.WithOutput(os.Stderr)) + if tm, err = p.Run(); err != nil { fmt.Println("could not start program:", err) } + mm := tm.(model) + fmt.Println("\n You selected: " + initialModel.Filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") } type ( @@ -64,17 +77,19 @@ func frame() tea.Cmd { } type model struct { - Choice int - Chosen bool - Ticks int - Frames int - Progress float64 - Loaded bool - Quitting bool + Filepicker filepicker.Model + selectedFile string + Choice int + Chosen bool + Ticks int + Frames int + Progress float64 + Loaded bool + Quitting bool } func (m model) Init() tea.Cmd { - return tick() + return tea.Batch(tick(), m.Filepicker.Init()) } // Main update function. @@ -87,12 +102,30 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } } + // var cmd tea.Cmd + m.Filepicker, _ = m.Filepicker.Update(msg) + // Did the user select a file? + // if didSelect, path := m.Filepicker.DidSelectFile(msg); didSelect { + // // Get the path of the selected file. + // m.selectedFile = path + // } + + // // Did the user select a disabled file? + // // This is only necessary to display an error to the user. + // if didSelect, path := m.Filepicker.DidSelectDisabledFile(msg); didSelect { + // // Let's clear the selectedFile and display an error. + // fmt.Println(errors.New(path + " is not valid.")) + // m.selectedFile = "" + // return m, tea.Batch(cmd, tick()) + // } // Hand off the message and model to the appropriate update function for the // appropriate view based on the current state. if !m.Chosen { return updateChoices(msg, m) } + + // return m, tea.Batch(cmd, tick()) return updateChosen(msg, m) } @@ -114,6 +147,7 @@ func (m model) View() string { // Update loop for the first view where you're choosing a task. func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -141,6 +175,28 @@ func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) { return m, tick() } + if m.selectedFile == "" { + var cmd tea.Cmd + // m.Filepicker, cmd = m.Filepicker.Update(msg) + + // Did the user select a file? + if didSelect, path := m.Filepicker.DidSelectFile(msg); didSelect { + // Get the path of the selected file. + m.selectedFile = path + } + + // Did the user select a disabled file? + // This is only necessary to display an error to the user. + if didSelect, path := m.Filepicker.DidSelectDisabledFile(msg); didSelect { + // Let's clear the selectedFile and display an error. + fmt.Println(errors.New(path + " is not valid.")) + m.selectedFile = "" + return m, tea.Batch(cmd, tick()) + } + + return m, cmd + } + return m, nil } @@ -154,7 +210,7 @@ func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) { if m.Progress >= 1 { m.Progress = 1 m.Loaded = true - m.Ticks = 3 + m.Ticks = 100 return m, tick() } return m, frame() @@ -186,11 +242,9 @@ func choicesView(m model) string { tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") choices := fmt.Sprintf( - "%s\n%s\n%s\n%s", - checkbox("Plant carrots", c == 0), - checkbox("Go to the market", c == 1), - checkbox("Read something", c == 2), - checkbox("See friends", c == 3), + "%s\n%s", + checkbox("Choose an Aesir file", c == 0), + checkbox("Start REPL", c == 1), ) return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Ticks), "79")) @@ -202,11 +256,18 @@ func chosenView(m model) string { switch m.Choice { case 0: - msg = fmt.Sprintf("Carrot planting?\n\nCool, we'll need %s and %s...", keyword("libgarden"), keyword("vegeutils")) + var s strings.Builder + s.WriteString("\n ") + if m.selectedFile == "" { + s.WriteString("Pick a file:") + } else { + s.WriteString("Selected file: " + m.Filepicker.Styles.Selected.Render(m.selectedFile)) + } + s.WriteString("\n\n" + m.Filepicker.View() + "\n") + msg = s.String() + // msg = fmt.Sprintf("Carrot planting?\n\nCool, we'll need %s and %s...", keyword("libgarden"), keyword("vegeutils")) case 1: msg = fmt.Sprintf("A trip to the market?\n\nOkay, then we should install %s and %s...", keyword("marketkit"), keyword("libshopping")) - case 2: - msg = fmt.Sprintf("Reading time?\n\nOkay, cool, then we’ll need a library. Yes, an %s.", keyword("actual library")) default: msg = fmt.Sprintf("It’s always good to see friends.\n\nFetching %s and %s...", keyword("social-skills"), keyword("conversationutils")) } @@ -278,4 +339,4 @@ func colorFloatToHex(f float64) (s string) { s = "0" + s } return -} \ No newline at end of file +} From c651245190191fcdc2d448fdc4ca259c27a85a25 Mon Sep 17 00:00:00 2001 From: towhid Date: Sun, 5 Nov 2023 13:48:08 +0600 Subject: [PATCH 4/6] implemented simple fs and multi-view --- cmd/aesir-cli/main.go | 379 ++++++++++++------------------------------ 1 file changed, 110 insertions(+), 269 deletions(-) diff --git a/cmd/aesir-cli/main.go b/cmd/aesir-cli/main.go index 53b6c00..0a55eaf 100644 --- a/cmd/aesir-cli/main.go +++ b/cmd/aesir-cli/main.go @@ -1,15 +1,8 @@ package main -// An example demonstrating an application with multiple views. -// -// Note that this example was produced before the Bubbles progress component -// was available (github.com/charmbracelet/bubbles/progress) and thus, we're -// implementing a progress bar from scratch here. - import ( "errors" "fmt" - "math" "os" "strconv" "strings" @@ -17,326 +10,174 @@ import ( "github.com/charmbracelet/bubbles/filepicker" tea "github.com/charmbracelet/bubbletea" - "github.com/fogleman/ease" - "github.com/lucasb-eyer/go-colorful" - "github.com/muesli/reflow/indent" "github.com/muesli/termenv" ) -const ( - progressBarWidth = 71 - progressFullChar = "█" - progressEmptyChar = "░" -) - -// General stuff for styling the view var ( - term = termenv.EnvColorProfile() - keyword = makeFgStyle("211") - subtle = makeFgStyle("241") - progressEmpty = subtle(progressEmptyChar) - dot = colorFg(" • ", "236") - - // Gradient colors we'll use for the progress bar - ramp = makeRamp("#B14FFF", "#00FFA3", progressBarWidth) + term = termenv.EnvColorProfile() + subtle = makeFgStyle("241") + dot = colorFg(" • ", "236") ) -func main() { - var tm tea.Model - var err error - fp := filepicker.New() - fp.AllowedTypes = []string{".mod", ".sum", ".go", ".txt", ".md"} - fp.CurrentDirectory, _ = os.UserHomeDir() - // fp.ShowHidden = true - - initialModel := model{fp, "", 0, false, 10, 0, 0, false, false} - - p := tea.NewProgram(&initialModel, tea.WithOutput(os.Stderr)) - if tm, err = p.Run(); err != nil { - fmt.Println("could not start program:", err) - } - mm := tm.(model) - fmt.Println("\n You selected: " + initialModel.Filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") +func makeFgStyle(color string) func(string) string { + return termenv.Style{}.Foreground(term.Color(color)).Styled } -type ( - tickMsg struct{} - frameMsg struct{} -) - -func tick() tea.Cmd { - return tea.Tick(time.Second, func(time.Time) tea.Msg { - return tickMsg{} - }) +func colorFg(val, color string) string { + return termenv.String(val).Foreground(term.Color(color)).String() } -func frame() tea.Cmd { - return tea.Tick(time.Second/60, func(time.Time) tea.Msg { - return frameMsg{} - }) +func checkbox(label string, checked bool) string { + if checked { + return colorFg("[x] "+label, "212") + } + return fmt.Sprintf("[ ] %s", label) } -type model struct { - Filepicker filepicker.Model +type Model struct { + page string + filepicker filepicker.Model selectedFile string Choice int Chosen bool - Ticks int - Frames int - Progress float64 - Loaded bool - Quitting bool -} - -func (m model) Init() tea.Cmd { - return tea.Batch(tick(), m.Filepicker.Init()) + quitting bool + err error } -// Main update function. -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - // Make sure these keys always quit - if msg, ok := msg.(tea.KeyMsg); ok { - k := msg.String() - if k == "q" || k == "esc" || k == "ctrl+c" { - m.Quitting = true - return m, tea.Quit - } - } - // var cmd tea.Cmd - m.Filepicker, _ = m.Filepicker.Update(msg) - // Did the user select a file? - // if didSelect, path := m.Filepicker.DidSelectFile(msg); didSelect { - // // Get the path of the selected file. - // m.selectedFile = path - // } - - // // Did the user select a disabled file? - // // This is only necessary to display an error to the user. - // if didSelect, path := m.Filepicker.DidSelectDisabledFile(msg); didSelect { - // // Let's clear the selectedFile and display an error. - // fmt.Println(errors.New(path + " is not valid.")) - // m.selectedFile = "" - // return m, tea.Batch(cmd, tick()) - // } - - // Hand off the message and model to the appropriate update function for the - // appropriate view based on the current state. - if !m.Chosen { - return updateChoices(msg, m) - } +type clearErrorMsg struct{} - // return m, tea.Batch(cmd, tick()) - return updateChosen(msg, m) +func clearErrorAfter(t time.Duration) tea.Cmd { + return tea.Tick(t, func(_ time.Time) tea.Msg { + return clearErrorMsg{} + }) } -// The main view, which just calls the appropriate sub-view -func (m model) View() string { - var s string - if m.Quitting { - return "\n See you later!\n\n" - } - if !m.Chosen { - s = choicesView(m) - } else { - s = chosenView(m) - } - return indent.String("\n"+s+"\n\n", 2) +func (m Model) Init() tea.Cmd { + return m.filepicker.Init() } -// Sub-update functions - -// Update loop for the first view where you're choosing a task. -func updateChoices(msg tea.Msg, m model) (tea.Model, tea.Cmd) { - +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "j", "down": - m.Choice++ - if m.Choice > 3 { - m.Choice = 3 - } - case "k", "up": - m.Choice-- - if m.Choice < 0 { - m.Choice = 0 - } - case "enter": - m.Chosen = true - return m, frame() - } - - case tickMsg: - if m.Ticks == 0 { - m.Quitting = true + case "ctrl+c", "q": + m.quitting = true return m, tea.Quit + case "enter": + // m.page = "fs" } - m.Ticks-- - return m, tick() + case clearErrorMsg: + m.err = nil } - if m.selectedFile == "" { - var cmd tea.Cmd - // m.Filepicker, cmd = m.Filepicker.Update(msg) + var cmd tea.Cmd + m.filepicker, cmd = m.filepicker.Update(msg) - // Did the user select a file? - if didSelect, path := m.Filepicker.DidSelectFile(msg); didSelect { - // Get the path of the selected file. + switch m.page { + case "fs": + if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { m.selectedFile = path } - // Did the user select a disabled file? - // This is only necessary to display an error to the user. - if didSelect, path := m.Filepicker.DidSelectDisabledFile(msg); didSelect { - // Let's clear the selectedFile and display an error. - fmt.Println(errors.New(path + " is not valid.")) + if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { + m.err = errors.New(path + " is not valid.") m.selectedFile = "" - return m, tea.Batch(cmd, tick()) + return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) } return m, cmd - } - - return m, nil -} - -// Update loop for the second view after a choice has been made -func updateChosen(msg tea.Msg, m model) (tea.Model, tea.Cmd) { - switch msg.(type) { - case frameMsg: - if !m.Loaded { - m.Frames++ - m.Progress = ease.OutBounce(float64(m.Frames) / float64(100)) - if m.Progress >= 1 { - m.Progress = 1 - m.Loaded = true - m.Ticks = 100 - return m, tick() + case "repl": + return m, tea.Quit + default: + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "j", "down": + m.Choice++ + if m.Choice > 1 { + m.Choice = 1 + } + case "k", "up": + m.Choice-- + if m.Choice < 0 { + m.Choice = 0 + } + case "enter": + if m.Choice == 0 { + m.page = "fs" + // m.filepicker.CurrentDirectory = "./" + } + m.Chosen = true + return m, cmd } - return m, frame() - } - case tickMsg: - if m.Loaded { - if m.Ticks == 0 { - m.Quitting = true - return m, tea.Quit - } - m.Ticks-- - return m, tick() } } return m, nil } +func (m Model) View() string { + if m.quitting { + return "" + } -// Sub-views - -// The first view, where you're choosing a task -func choicesView(m model) string { - c := m.Choice - - tpl := "What to do today?\n\n" - tpl += "%s\n\n" - tpl += "Program quits in %s seconds\n\n" - tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") - - choices := fmt.Sprintf( - "%s\n%s", - checkbox("Choose an Aesir file", c == 0), - checkbox("Start REPL", c == 1), - ) - - return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Ticks), "79")) -} - -// The second view, after a task has been chosen -func chosenView(m model) string { - var msg string - - switch m.Choice { - case 0: + switch m.page { + case "fs": var s strings.Builder - s.WriteString("\n ") - if m.selectedFile == "" { - s.WriteString("Pick a file:") - } else { - s.WriteString("Selected file: " + m.Filepicker.Styles.Selected.Render(m.selectedFile)) - } - s.WriteString("\n\n" + m.Filepicker.View() + "\n") - msg = s.String() - // msg = fmt.Sprintf("Carrot planting?\n\nCool, we'll need %s and %s...", keyword("libgarden"), keyword("vegeutils")) - case 1: - msg = fmt.Sprintf("A trip to the market?\n\nOkay, then we should install %s and %s...", keyword("marketkit"), keyword("libshopping")) + s.WriteString("\n ") + if m.err != nil { + s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) + } else if m.selectedFile == "" { + s.WriteString("Pick a file:") + } else { + s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile)) + } + s.WriteString("\n\n" + m.filepicker.View() + "\n") + return s.String() + case "repl": + return "repl view" default: - msg = fmt.Sprintf("It’s always good to see friends.\n\nFetching %s and %s...", keyword("social-skills"), keyword("conversationutils")) - } - label := "Downloading..." - if m.Loaded { - label = fmt.Sprintf("Downloaded. Exiting in %s seconds...", colorFg(strconv.Itoa(m.Ticks), "79")) - } + c := m.Choice - return msg + "\n\n" + label + "\n" + progressbar(m.Progress) + "%" -} + tpl := "What to do today?\n\n" + tpl += "%s\n\n" + tpl += "Program quits in %s seconds\n\n" + tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") -func checkbox(label string, checked bool) string { - if checked { - return colorFg("[x] "+label, "212") - } - return fmt.Sprintf("[ ] %s", label) -} + choices := fmt.Sprintf( + "%s\n%s\n", + checkbox("Open File Picker", c == 0), + checkbox("Open REPL", c == 1), + ) -func progressbar(percent float64) string { - w := float64(progressBarWidth) + return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Choice), "79")) - fullSize := int(math.Round(w * percent)) - var fullCells string - for i := 0; i < fullSize; i++ { - fullCells += termenv.String(progressFullChar).Foreground(term.Color(ramp[i])).String() } - - emptySize := int(w) - fullSize - emptyCells := strings.Repeat(progressEmpty, emptySize) - - return fmt.Sprintf("%s%s %3.0f", fullCells, emptyCells, math.Round(percent*100)) -} - -// Utils - -// Color a string's foreground with the given value. -func colorFg(val, color string) string { - return termenv.String(val).Foreground(term.Color(color)).String() } -// Return a function that will colorize the foreground of a given string. -func makeFgStyle(color string) func(string) string { - return termenv.Style{}.Foreground(term.Color(color)).Styled -} +func main() { + fp := filepicker.New() + fp.AllowedTypes = []string{".ae"} + fp.CurrentDirectory, _ = os.UserHomeDir() -// Generate a blend of colors. -func makeRamp(colorA, colorB string, steps float64) (s []string) { - cA, _ := colorful.Hex(colorA) - cB, _ := colorful.Hex(colorB) + - for i := 0.0; i < steps; i++ { - c := cA.BlendLuv(cB, i/steps) - s = append(s, colorToHex(c)) + m := Model{ + filepicker: fp, } - return -} - -// Convert a colorful.Color to a hexadecimal format compatible with termenv. -func colorToHex(c colorful.Color) string { - return fmt.Sprintf("#%s%s%s", colorFloatToHex(c.R), colorFloatToHex(c.G), colorFloatToHex(c.B)) -} -// Helper function for converting colors to hex. Assumes a value between 0 and -// 1. -func colorFloatToHex(f float64) (s string) { - s = strconv.FormatInt(int64(f*255), 16) - if len(s) == 1 { - s = "0" + s + p := tea.NewProgram( + &m, + tea.WithOutput(os.Stdout), + ) + var tm tea.Model + var err error + if tm, err = p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) } - return + + mm := tm.(Model) + fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") } From 913c9b84fb35546ecb25e858889e560e9e819904 Mon Sep 17 00:00:00 2001 From: towhid Date: Sun, 5 Nov 2023 15:23:26 +0600 Subject: [PATCH 5/6] fixed the file-picker wrong directory issue --- cmd/aesir-cli/main.go | 301 +++++++++++++++++++++--------------------- 1 file changed, 153 insertions(+), 148 deletions(-) diff --git a/cmd/aesir-cli/main.go b/cmd/aesir-cli/main.go index 0a55eaf..6374381 100644 --- a/cmd/aesir-cli/main.go +++ b/cmd/aesir-cli/main.go @@ -1,183 +1,188 @@ package main import ( - "errors" - "fmt" - "os" - "strconv" - "strings" - "time" - - "github.com/charmbracelet/bubbles/filepicker" - tea "github.com/charmbracelet/bubbletea" - "github.com/muesli/termenv" + "errors" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/charmbracelet/bubbles/filepicker" + tea "github.com/charmbracelet/bubbletea" + "github.com/muesli/termenv" ) var ( - term = termenv.EnvColorProfile() - subtle = makeFgStyle("241") - dot = colorFg(" • ", "236") + term = termenv.EnvColorProfile() + subtle = makeFgStyle("241") + dot = colorFg(" • ", "236") ) func makeFgStyle(color string) func(string) string { - return termenv.Style{}.Foreground(term.Color(color)).Styled + return termenv.Style{}.Foreground(term.Color(color)).Styled } func colorFg(val, color string) string { - return termenv.String(val).Foreground(term.Color(color)).String() + return termenv.String(val).Foreground(term.Color(color)).String() } func checkbox(label string, checked bool) string { - if checked { - return colorFg("[x] "+label, "212") - } - return fmt.Sprintf("[ ] %s", label) + if checked { + return colorFg("[x] "+label, "212") + } + return fmt.Sprintf("[ ] %s", label) } type Model struct { - page string - filepicker filepicker.Model - selectedFile string - Choice int - Chosen bool - quitting bool - err error + page string + filepicker filepicker.Model + selectedFile string + Choice int + Chosen bool + quitting bool + err error } type clearErrorMsg struct{} func clearErrorAfter(t time.Duration) tea.Cmd { - return tea.Tick(t, func(_ time.Time) tea.Msg { - return clearErrorMsg{} - }) + return tea.Tick(t, func(_ time.Time) tea.Msg { + return clearErrorMsg{} + }) } func (m Model) Init() tea.Cmd { - return m.filepicker.Init() + return m.filepicker.Init() } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "q": - m.quitting = true - return m, tea.Quit - case "enter": - // m.page = "fs" - } - case clearErrorMsg: - m.err = nil - } - - var cmd tea.Cmd - m.filepicker, cmd = m.filepicker.Update(msg) - - switch m.page { - case "fs": - if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { - m.selectedFile = path - } - - if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { - m.err = errors.New(path + " is not valid.") - m.selectedFile = "" - return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) - } - - return m, cmd - case "repl": - return m, tea.Quit - default: - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "j", "down": - m.Choice++ - if m.Choice > 1 { - m.Choice = 1 - } - case "k", "up": - m.Choice-- - if m.Choice < 0 { - m.Choice = 0 - } - case "enter": - if m.Choice == 0 { - m.page = "fs" - // m.filepicker.CurrentDirectory = "./" - } - m.Chosen = true - return m, cmd - } - - } - } - - return m, nil + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + m.quitting = true + return m, tea.Quit + case "enter": + // m.page = "fs" + } + case tea.WindowSizeMsg: + m.filepicker, cmd = m.filepicker.Update(msg) + case clearErrorMsg: + m.err = nil + default: + m.filepicker, cmd = m.filepicker.Update(msg) + } + + switch m.page { + case "fs": + var fcmd tea.Cmd + m.filepicker, fcmd = m.filepicker.Update(msg) + cmd = tea.Batch(cmd, fcmd) + if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { + m.selectedFile = path + } + + if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { + m.err = errors.New(path + " is not valid.") + m.selectedFile = "" + return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) + } + + return m, cmd + case "repl": + return m, tea.Quit + default: + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "j", "down": + m.Choice++ + if m.Choice > 3 { + m.Choice = 3 + } + case "k", "up": + m.Choice-- + if m.Choice < 0 { + m.Choice = 0 + } + case "enter": + if m.Choice == 0 { + m.page = "fs" + // m.filepicker.CurrentDirectory = "./" + } + m.Chosen = true + return m, cmd + } + } + } + + return m, nil } + func (m Model) View() string { - if m.quitting { - return "" - } - - switch m.page { - case "fs": - var s strings.Builder - s.WriteString("\n ") - if m.err != nil { - s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) - } else if m.selectedFile == "" { - s.WriteString("Pick a file:") - } else { - s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile)) - } - s.WriteString("\n\n" + m.filepicker.View() + "\n") - return s.String() - case "repl": - return "repl view" - default: - - c := m.Choice - - tpl := "What to do today?\n\n" - tpl += "%s\n\n" - tpl += "Program quits in %s seconds\n\n" - tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") - - choices := fmt.Sprintf( - "%s\n%s\n", - checkbox("Open File Picker", c == 0), - checkbox("Open REPL", c == 1), - ) - - return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Choice), "79")) - - } + if m.quitting { + return "" + } + + switch m.page { + case "fs": + var s strings.Builder + s.WriteString("\n ") + if m.err != nil { + s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) + } else if m.selectedFile == "" { + s.WriteString("Pick a file:") + } else { + s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile)) + } + s.WriteString("\n\n" + m.filepicker.View() + "\n") + return s.String() + case "repl": + return "repl view" + default: + + c := m.Choice + + tpl := "What to do today?\n\n" + tpl += "%s\n\n" + tpl += "Program quits in %s seconds\n\n" + tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") + + choices := fmt.Sprintf( + "%s\n%s\n%s\n%s", + checkbox("Plant carrots", c == 0), + checkbox("Go to the market", c == 1), + checkbox("Read something", c == 2), + checkbox("See friends", c == 3), + ) + + return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Choice), "79")) + + } } func main() { - fp := filepicker.New() - fp.AllowedTypes = []string{".ae"} - fp.CurrentDirectory, _ = os.UserHomeDir() - - - - m := Model{ - filepicker: fp, - } - - p := tea.NewProgram( - &m, - tea.WithOutput(os.Stdout), - ) - var tm tea.Model - var err error - if tm, err = p.Run(); err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) - } - - mm := tm.(Model) - fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") + fp := filepicker.New() + fp.AllowedTypes = []string{".ae"} + fp.CurrentDirectory, _ = os.UserHomeDir() + + m := Model{ + filepicker: fp, + } + + p := tea.NewProgram( + &m, + tea.WithOutput(os.Stdout), + ) + var tm tea.Model + var err error + if tm, err = p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } + + mm := tm.(Model) + fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") } From 389ad659d01d176bf332e02c4af0b5f6981e6de4 Mon Sep 17 00:00:00 2001 From: towhid Date: Sun, 5 Nov 2023 18:01:27 +0600 Subject: [PATCH 6/6] implementing importer --- cmd/aesir-cli/main.go | 320 +++++++++++++++++++++------------------- importer/test_script.ae | 17 ++- 2 files changed, 184 insertions(+), 153 deletions(-) diff --git a/cmd/aesir-cli/main.go b/cmd/aesir-cli/main.go index 6374381..f2c37f7 100644 --- a/cmd/aesir-cli/main.go +++ b/cmd/aesir-cli/main.go @@ -1,188 +1,204 @@ package main import ( - "errors" - "fmt" - "os" - "strconv" - "strings" - "time" - - "github.com/charmbracelet/bubbles/filepicker" - tea "github.com/charmbracelet/bubbletea" - "github.com/muesli/termenv" + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" + "time" + + "github.com/charmbracelet/bubbles/filepicker" + tea "github.com/charmbracelet/bubbletea" + "github.com/muesli/termenv" + "github.com/nexentra/aesir/importer" ) var ( - term = termenv.EnvColorProfile() - subtle = makeFgStyle("241") - dot = colorFg(" • ", "236") + term = termenv.EnvColorProfile() + subtle = makeFgStyle("241") + dot = colorFg(" • ", "236") ) func makeFgStyle(color string) func(string) string { - return termenv.Style{}.Foreground(term.Color(color)).Styled + return termenv.Style{}.Foreground(term.Color(color)).Styled } func colorFg(val, color string) string { - return termenv.String(val).Foreground(term.Color(color)).String() + return termenv.String(val).Foreground(term.Color(color)).String() } func checkbox(label string, checked bool) string { - if checked { - return colorFg("[x] "+label, "212") - } - return fmt.Sprintf("[ ] %s", label) + if checked { + return colorFg("[x] "+label, "212") + } + return fmt.Sprintf("[ ] %s", label) } type Model struct { - page string - filepicker filepicker.Model - selectedFile string - Choice int - Chosen bool - quitting bool - err error + page string + filepicker filepicker.Model + selectedFile string + Choice int + Chosen bool + quitting bool + err error } type clearErrorMsg struct{} func clearErrorAfter(t time.Duration) tea.Cmd { - return tea.Tick(t, func(_ time.Time) tea.Msg { - return clearErrorMsg{} - }) + return tea.Tick(t, func(_ time.Time) tea.Msg { + return clearErrorMsg{} + }) } func (m Model) Init() tea.Cmd { - return m.filepicker.Init() + return m.filepicker.Init() } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "q": - m.quitting = true - return m, tea.Quit - case "enter": - // m.page = "fs" - } - case tea.WindowSizeMsg: - m.filepicker, cmd = m.filepicker.Update(msg) - case clearErrorMsg: - m.err = nil - default: - m.filepicker, cmd = m.filepicker.Update(msg) - } - - switch m.page { - case "fs": - var fcmd tea.Cmd - m.filepicker, fcmd = m.filepicker.Update(msg) - cmd = tea.Batch(cmd, fcmd) - if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { - m.selectedFile = path - } - - if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { - m.err = errors.New(path + " is not valid.") - m.selectedFile = "" - return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) - } - - return m, cmd - case "repl": - return m, tea.Quit - default: - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "j", "down": - m.Choice++ - if m.Choice > 3 { - m.Choice = 3 - } - case "k", "up": - m.Choice-- - if m.Choice < 0 { - m.Choice = 0 - } - case "enter": - if m.Choice == 0 { - m.page = "fs" - // m.filepicker.CurrentDirectory = "./" - } - m.Chosen = true - return m, cmd - } - } - } - - return m, nil + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + m.quitting = true + return m, tea.Quit + // case "enter": + // m.page = "fs" + } + case tea.WindowSizeMsg: + m.filepicker, cmd = m.filepicker.Update(msg) + case clearErrorMsg: + m.err = nil + default: + m.filepicker, cmd = m.filepicker.Update(msg) + } + + switch m.page { + case "fs": + var fcmd tea.Cmd + m.filepicker, fcmd = m.filepicker.Update(msg) + cmd = tea.Batch(cmd, fcmd) + if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect { + m.selectedFile = path + } + + if didSelect, path := m.filepicker.DidSelectDisabledFile(msg); didSelect { + m.err = errors.New(path + " is not valid.") + m.selectedFile = "" + return m, tea.Batch(cmd, clearErrorAfter(2*time.Second)) + } + + return m, cmd + case "repl": + return m, tea.Quit + default: + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "j", "down": + m.Choice++ + if m.Choice > 1 { + m.Choice = 0 + } + case "k", "up": + m.Choice-- + if m.Choice < 0 { + m.Choice = 1 + } + case "enter": + if m.Choice == 0 { + m.page = "fs" + // m.filepicker.CurrentDirectory = "./" + } + m.Chosen = true + return m, cmd + } + } + } + + return m, nil } func (m Model) View() string { - if m.quitting { - return "" - } - - switch m.page { - case "fs": - var s strings.Builder - s.WriteString("\n ") - if m.err != nil { - s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) - } else if m.selectedFile == "" { - s.WriteString("Pick a file:") - } else { - s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile)) - } - s.WriteString("\n\n" + m.filepicker.View() + "\n") - return s.String() - case "repl": - return "repl view" - default: - - c := m.Choice - - tpl := "What to do today?\n\n" - tpl += "%s\n\n" - tpl += "Program quits in %s seconds\n\n" - tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") - - choices := fmt.Sprintf( - "%s\n%s\n%s\n%s", - checkbox("Plant carrots", c == 0), - checkbox("Go to the market", c == 1), - checkbox("Read something", c == 2), - checkbox("See friends", c == 3), - ) - - return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Choice), "79")) - - } + if m.quitting { + return "" + } + + if m.selectedFile != "" { + res, err := importer.Importer(string(m.selectedFile)) + if err != nil { + print(err) + } + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + res.Inspect() + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = rescueStdout + + return string(out) + } + + switch m.page { + case "fs": + var s strings.Builder + s.WriteString("\n ") + if m.err != nil { + s.WriteString(m.filepicker.Styles.DisabledFile.Render(m.err.Error())) + } else if m.selectedFile == "" { + s.WriteString("Pick a file:") + } else { + s.WriteString("Selected file: " + m.filepicker.Styles.Selected.Render(m.selectedFile)) + } + s.WriteString("\n\n" + m.filepicker.View() + "\n") + return s.String() + case "repl": + return "repl view" + default: + + c := m.Choice + + tpl := "What to do today?\n\n" + tpl += "%s\n\n" + tpl += "Program quits in %s seconds\n\n" + tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") + + choices := fmt.Sprintf( + "%s\n%s\n", + checkbox("Choose an aesir file", c == 0), + checkbox("Fire up REPL", c == 1), + ) + return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.Choice), "79")) + + } } func main() { - fp := filepicker.New() - fp.AllowedTypes = []string{".ae"} - fp.CurrentDirectory, _ = os.UserHomeDir() - - m := Model{ - filepicker: fp, - } - - p := tea.NewProgram( - &m, - tea.WithOutput(os.Stdout), - ) - var tm tea.Model - var err error - if tm, err = p.Run(); err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) - } - - mm := tm.(Model) - fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") + fp := filepicker.New() + fp.AllowedTypes = []string{".ae"} + fp.CurrentDirectory, _ = os.UserHomeDir() + + m := Model{ + filepicker: fp, + } + + p := tea.NewProgram( + &m, + tea.WithOutput(os.Stdout), + ) + // var tm tea.Model + var err error + if _, err = p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } + + // mm := tm.(Model) + // fmt.Println("\n You selected: " + m.filepicker.Styles.Selected.Render(mm.selectedFile) + "\n") } diff --git a/importer/test_script.ae b/importer/test_script.ae index d975313..3f7dbb5 100644 --- a/importer/test_script.ae +++ b/importer/test_script.ae @@ -16,4 +16,19 @@ print("\n") let a = "hey towhid"; -println( "We received ", len(a), " arguments to our script.\n" ); \ No newline at end of file +println( "We received ", len(a), " arguments to our script." ); + +let b = 0 +let func = fn(b){ +if (b == 10){ + return true +} +else{ + let b = b + 1 + println(b) + func(b) +} + +} + +func(b) \ No newline at end of file