diff --git a/go.mod b/go.mod index 9ce2053..c58c2d9 100644 --- a/go.mod +++ b/go.mod @@ -4,20 +4,34 @@ go 1.22.1 require ( github.com/briandowns/spinner v1.23.1 - github.com/gdamore/tcell/v2 v2.7.4 + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.26.6 + github.com/charmbracelet/lipgloss v0.11.0 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-sqlite3 v1.14.22 ) 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.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.17.0 // indirect - github.com/gdamore/encoding v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // 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/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/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 5027277..0ce164c 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,33 @@ +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/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +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.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= +github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= +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.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/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/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +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.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= -github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= -github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= -github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 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/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= @@ -21,51 +37,34 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 458b89b..d927fe4 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -62,14 +62,14 @@ func startNewChat(app *api.App) { return } - editor, err := NewEditor(app, chatID, nil) + editor, err := NewVimEditor(app, chatID, nil) if err != nil { - fmt.Printf("Error creating editor: %v\n", err) + fmt.Printf("Error creating Vim editor: %v\n", err) return } if err := editor.Run(); err != nil { - fmt.Printf("Error running editor: %v\n", err) + fmt.Printf("Error running Vim editor: %v\n", err) } } @@ -97,14 +97,14 @@ func continuePreviousChat(app *api.App) { return } - editor, err := NewEditor(app, selectedChat.ID, messages) + editor, err := NewVimEditor(app, selectedChat.ID, messages) if err != nil { - fmt.Printf("Error creating editor: %v\n", err) + fmt.Printf("Error creating Vim editor: %v\n", err) return } if err := editor.Run(); err != nil { - fmt.Printf("Error running editor: %v\n", err) + fmt.Printf("Error running Vim editor: %v\n", err) } } diff --git a/internal/cli/editor.go b/internal/cli/editor.go index f49746d..e0eb19b 100644 --- a/internal/cli/editor.go +++ b/internal/cli/editor.go @@ -2,298 +2,150 @@ package cli import ( "fmt" - "log" - "os" - "path/filepath" "time" "github.com/Utility-Gods/gottem/internal/api" "github.com/Utility-Gods/gottem/internal/db" - "github.com/Utility-Gods/gottem/pkg/types" - "github.com/gdamore/tcell/v2" + "github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) -type Editor struct { - screen tcell.Screen - app *api.App - chatID int - messages []db.Message - content []string - cursor struct{ x, y int } - scroll int - status string - apis []types.APIInfo - selectedAPI int - logger *log.Logger -} - -func NewEditor(app *api.App, chatID int, messages []db.Message) (*Editor, error) { - - logDir := filepath.Join("" + "logs") - - logFile, err := os.OpenFile(filepath.Join(logDir, fmt.Sprintf("editor_%d.log", time.Now().Unix())), - os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - return nil, fmt.Errorf("failed to open log file: %w", err) - } - - log.Printf("opened log file", logFile) - - logger := log.New(logFile, "", log.Ldate|log.Ltime|log.Lmicroseconds) - - screen, err := tcell.NewScreen() - if err != nil { - logger.Printf("Error creating screen: %v", err) - return nil, err - } - if err := screen.Init(); err != nil { - logger.Printf("Error initializing screen: %v", err) - return nil, err - } - - e := &Editor{ - screen: screen, - app: app, - chatID: chatID, - messages: messages, - content: []string{""}, - apis: app.GetAvailableAPIs(), - selectedAPI: 0, - logger: logger, - } - e.loadMessages() - e.logger.Println("Editor initialized") - return e, nil -} - -func (e *Editor) loadMessages() { - e.logger.Println("Loading messages") - for _, msg := range e.messages { - e.content = append(e.content, fmt.Sprintf("[%s] %s (%s): %s", - msg.CreatedAt.Format("2006-01-02 15:04:05"), - msg.Role, - msg.APIName, - msg.Content, - )) - e.content = append(e.content, "") - } - e.logger.Printf("Loaded %d messages", len(e.messages)) -} -func (e *Editor) Run() error { - defer e.screen.Fini() - e.logger.Println("Editor running") - - e.status = "Ctrl+E: Send query, Ctrl+A: Select API, Ctrl+Q: Quit and return to main menu" - - for { - e.draw() - e.screen.Show() - - ev := e.screen.PollEvent() - e.logger.Printf("Event received: %T", ev) - switch ev := ev.(type) { - case *tcell.EventKey: - if e.handleKeyEvent(ev) { - e.logger.Println("Editor exiting") - return nil - } - case *tcell.EventResize: - e.screen.Sync() - e.logger.Println("Screen resized") - } - } +type EditorModel struct { + app *api.App + chatID int + messages []db.Message + content string + cursor cursor.Model + textarea textarea.Model + viewport viewport.Model + senderStyle lipgloss.Style + selectedAPI string + err error } -func (e *Editor) handleKeyEvent(ev *tcell.EventKey) bool { - e.logger.Printf("Key event: key=%v rune=%v mod=%v", ev.Key(), ev.Rune(), ev.Modifiers()) - switch ev.Key() { - case tcell.KeyCtrlQ: - e.logger.Println("Quit command received") - e.quitEditor() - return true - case tcell.KeyCtrlE: - e.logger.Println("Send query command received") - e.sendQuery() - case tcell.KeyCtrlA: - e.logger.Println("Select API command received") - e.selectAPI() - case tcell.KeyUp: - e.logger.Println("Cursor moved up") - e.moveCursor(0, -1) - case tcell.KeyDown: - e.logger.Println("Cursor moved down") - e.moveCursor(0, 1) - case tcell.KeyLeft: - e.logger.Println("Cursor moved left") - e.moveCursor(-1, 0) - case tcell.KeyRight: - e.logger.Println("Cursor moved right") - e.moveCursor(1, 0) - case tcell.KeyEnter: - e.logger.Println("New line inserted") - e.insertNewLine() - case tcell.KeyBackspace, tcell.KeyBackspace2: - e.logger.Println("Backspace pressed") - e.backspace() - default: - e.logger.Printf("Character inserted: %c", ev.Rune()) - e.insertChar(ev.Rune()) +func NewEditorModel(app *api.App, chatID int, messages []db.Message) EditorModel { + ta := textarea.New() + ta.Placeholder = "Type your query..." + ta.Focus() + + vp := viewport.New(30, 10) + vp.SetContent(`Welcome to the editor! +Press Ctrl+E to send a query. +Press Ctrl+A to change the API. +Press Ctrl+C to quit.`) + + return EditorModel{ + app: app, + chatID: chatID, + messages: messages, + content: "", + cursor: cursor.New(), + textarea: ta, + viewport: vp, + senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")), + selectedAPI: "c", + err: nil, } - return false } -func (e *Editor) quitEditor() { - e.logger.Println("Quitting editor") - e.status = "Quitting editor. Press any key to return to main menu." - e.draw() - e.screen.Show() - e.screen.PollEvent() // Wait for any key press +func (m EditorModel) Init() tea.Cmd { + return textarea.Blink } -func (e *Editor) draw() { - e.logger.Println("Drawing screen") - e.screen.Clear() - width, height := e.screen.Size() - - for y := 0; y < height-1; y++ { - if y+e.scroll < len(e.content) { - line := e.content[y+e.scroll] - for x, ch := range line { - if x < width { - e.screen.SetContent(x, y, ch, nil, tcell.StyleDefault) +func (m EditorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC: + return m, tea.Quit + + case tea.KeyCtrlE: + query := m.textarea.Value() + if query != "" { + m.content += fmt.Sprintf("\nUser: %s\n", query) + response, err := m.app.HandleQuery(m.selectedAPI, query, m.chatID, m.messages) + if err != nil { + m.err = err + } else { + m.content += fmt.Sprintf("Assistant: %s\n", response) } + m.viewport.SetContent(m.content) + m.messages = append(m.messages, + db.Message{Role: "user", APIName: m.selectedAPI, Content: query, CreatedAt: time.Now()}, + db.Message{Role: "assistant", APIName: m.selectedAPI, Content: response, CreatedAt: time.Now()}, + ) + m.textarea.SetValue("") } - } - } - statusStyle := tcell.StyleDefault.Background(tcell.ColorBlue).Foreground(tcell.ColorWhite) - statusRunes := []rune(e.status) - for x := 0; x < width; x++ { - if x < len(statusRunes) { - e.screen.SetContent(x, height-1, statusRunes[x], nil, statusStyle) - } else { - e.screen.SetContent(x, height-1, ' ', nil, statusStyle) + case tea.KeyCtrlA: + apis := m.app.GetAvailableAPIs() + var apiOptions []string + for _, api := range apis { + apiOptions = append(apiOptions, api.Shortcut) + } + m.selectedAPI = apiOptions[(indexOf(m.selectedAPI, apiOptions)+1)%len(apiOptions)] + m.content += fmt.Sprintf("\nSelected API: %s\n", m.selectedAPI) + m.viewport.SetContent(m.content) } } - e.screen.ShowCursor(e.cursor.x, e.cursor.y-e.scroll) - e.logger.Printf("Screen drawn. Cursor at (%d, %d), scroll at %d", e.cursor.x, e.cursor.y, e.scroll) -} + m.textarea, _ = m.textarea.Update(msg) + m.viewport, _ = m.viewport.Update(msg) + m.cursor, _ = m.cursor.Update(msg) -func (e *Editor) moveCursor(dx, dy int) { - oldX, oldY := e.cursor.x, e.cursor.y - newX, newY := e.cursor.x+dx, e.cursor.y+dy - if newY >= 0 && newY < len(e.content) { - e.cursor.y = newY - if newX >= 0 && newX <= len(e.content[newY]) { - e.cursor.x = newX - } else if newX < 0 { - e.cursor.x = 0 - } else { - e.cursor.x = len(e.content[newY]) - } - } - e.adjustScroll() - e.logger.Printf("Cursor moved from (%d, %d) to (%d, %d)", oldX, oldY, e.cursor.x, e.cursor.y) + return m, tea.Batch(cmds...) } -func (e *Editor) insertNewLine() { - e.logger.Printf("Inserting new line at (%d, %d)", e.cursor.x, e.cursor.y) - newLine := e.content[e.cursor.y][e.cursor.x:] - e.content[e.cursor.y] = e.content[e.cursor.y][:e.cursor.x] - e.content = append(e.content[:e.cursor.y+1], append([]string{newLine}, e.content[e.cursor.y+1:]...)...) - e.cursor.y++ - e.cursor.x = 0 - e.adjustScroll() - e.logger.Printf("New line inserted, cursor now at (%d, %d)", e.cursor.x, e.cursor.y) +func (m EditorModel) View() string { + return fmt.Sprintf( + "%s\n%s\n", + m.viewport.View(), + m.textarea.View(), + ) + "\n\nPress Ctrl+E to send a query.\nPress Ctrl+A to change the API.\nPress Ctrl+C to quit.\n" } -func (e *Editor) backspace() { - e.logger.Printf("Backspace at (%d, %d)", e.cursor.x, e.cursor.y) - if e.cursor.x > 0 { - line := e.content[e.cursor.y] - e.content[e.cursor.y] = line[:e.cursor.x-1] + line[e.cursor.x:] - e.cursor.x-- - } else if e.cursor.y > 0 { - e.cursor.y-- - e.cursor.x = len(e.content[e.cursor.y]) - e.content[e.cursor.y] += e.content[e.cursor.y+1] - e.content = append(e.content[:e.cursor.y+1], e.content[e.cursor.y+2:]...) +func (m EditorModel) Run() error { + p := tea.NewProgram(m) + if err := p.Start(); err != nil { + return fmt.Errorf("failed to start Bubbletea program: %w", err) } - e.logger.Printf("After backspace, cursor at (%d, %d)", e.cursor.x, e.cursor.y) + return nil } -func (e *Editor) insertChar(ch rune) { - e.logger.Printf("Inserting character '%c' at (%d, %d)", ch, e.cursor.x, e.cursor.y) - line := e.content[e.cursor.y] - e.content[e.cursor.y] = line[:e.cursor.x] + string(ch) + line[e.cursor.x:] - e.cursor.x++ - e.logger.Printf("After insertion, cursor at (%d, %d)", e.cursor.x, e.cursor.y) +type Editor struct { + app *api.App + chatID int + messages []db.Message + model EditorModel } -func (e *Editor) adjustScroll() { - e.logger.Printf("Adjusting scroll. Current scroll: %d", e.scroll) - _, height := e.screen.Size() - if e.cursor.y < e.scroll { - e.scroll = e.cursor.y - } else if e.cursor.y >= e.scroll+height-1 { - e.scroll = e.cursor.y - height + 2 - } - e.logger.Printf("Scroll adjusted to %d", e.scroll) +func NewEditor(app *api.App, chatID int, messages []db.Message) (*Editor, error) { + model := NewEditorModel(app, chatID, messages) + return &Editor{ + app: app, + chatID: chatID, + messages: messages, + model: model, + }, nil } -func (e *Editor) sendQuery() { - query := e.content[len(e.content)-1] - apiInfo := e.apis[e.selectedAPI] - - e.logger.Printf("Sending query to API %s: %s", apiInfo.Name, query) - e.status = "Sending query..." - e.draw() - e.screen.Show() - - response, err := e.app.HandleQuery(apiInfo.Shortcut, query, e.chatID, e.messages) - if err != nil { - e.status = fmt.Sprintf("Error: %v", err) - e.logger.Printf("Error sending query: %v", err) - return +func (e *Editor) Run() error { + if err := e.model.Run(); err != nil { + return fmt.Errorf("error running editor: %w", err) } - - newMessage := fmt.Sprintf("[%s] assistant (%s): %s", - time.Now().Format("2006-01-02 15:04:05"), - apiInfo.Name, - response, - ) - e.content = append(e.content, "", newMessage, "") - e.cursor.y = len(e.content) - 1 - e.cursor.x = 0 - - e.messages = append(e.messages, - db.Message{Role: "user", APIName: apiInfo.Name, Content: query, CreatedAt: time.Now()}, - db.Message{Role: "assistant", APIName: apiInfo.Name, Content: response, CreatedAt: time.Now()}, - ) - - e.status = "Query sent and response received. Ctrl+E to send another, Ctrl+A to change API." - e.adjustScroll() - e.logger.Printf("Query sent and response received. Response length: %d", len(response)) + return nil } -func (e *Editor) selectAPI() { - e.logger.Println("Selecting API") - currentAPI := e.apis[e.selectedAPI].Name - e.status = fmt.Sprintf("Current API: %s. Enter number to change (1-%d), or any other key to cancel.", currentAPI, len(e.apis)) - e.draw() - e.screen.Show() - - ev := e.screen.PollEvent() - switch ev := ev.(type) { - case *tcell.EventKey: - if ev.Key() == tcell.KeyRune && ev.Rune() >= '1' && ev.Rune() <= rune('0'+len(e.apis)) { - e.selectedAPI = int(ev.Rune() - '1') - e.status = fmt.Sprintf("API changed to: %s", e.apis[e.selectedAPI].Name) - e.logger.Printf("API changed to: %s", e.apis[e.selectedAPI].Name) - } else { - e.status = fmt.Sprintf("API selection cancelled. Current API: %s", currentAPI) - e.logger.Println("API selection cancelled") +func indexOf(element string, data []string) int { + for k, v := range data { + if element == v { + return k } } + return -1 } diff --git a/internal/cli/vim_editor.go b/internal/cli/vim_editor.go new file mode 100644 index 0000000..26a6da3 --- /dev/null +++ b/internal/cli/vim_editor.go @@ -0,0 +1,207 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os/exec" + "strings" + "time" + + "github.com/Utility-Gods/gottem/internal/api" + "github.com/Utility-Gods/gottem/internal/db" +) + +type VimEditor struct { + app *api.App + chatID int + messages []db.Message + selectedAPI string + servername string +} + +func NewVimEditor(app *api.App, chatID int, messages []db.Message) (*VimEditor, error) { + return &VimEditor{ + app: app, + chatID: chatID, + messages: messages, + selectedAPI: "c", + servername: "gottem_vim", + }, nil +} + +func (e *VimEditor) Run() error { + // Start the Vim server + cmd := exec.Command("vim", "--servername", e.servername) + if err := cmd.Start(); err != nil { + return fmt.Errorf("error starting Vim server: %w", err) + } + + // Send initial messages to Vim + if err := e.sendMessagesToVim(); err != nil { + return fmt.Errorf("error sending messages to Vim: %w", err) + } + + // Define custom key mappings + if err := e.defineKeyMappings(); err != nil { + return fmt.Errorf("error defining key mappings: %w", err) + } + + // Wait for Vim to exit + if err := cmd.Wait(); err != nil { + return fmt.Errorf("error waiting for Vim: %w", err) + } + + return nil +} + +func (e *VimEditor) sendMessagesToVim() error { + content := e.messagesContent() + + // Escape newline characters in the content + content = strings.ReplaceAll(content, "\n", "\\n") + + // Send the content to Vim using the `--remote-send` command + cmd := exec.Command("vim", "--servername", e.servername, "--remote-send", fmt.Sprintf("i%s", content)) + if err := cmd.Run(); err != nil { + return fmt.Errorf("error sending messages to Vim: %w", err) + } + + return nil +} + +func (e *VimEditor) defineKeyMappings() error { + keyMappings := []string{ + "nnoremap e :call SendQuery()", + "nnoremap a :call ChangeAPI()", + "vnoremap e :call SendSelectedQuery()", + } + + for _, mapping := range keyMappings { + cmd := exec.Command("vim", "--servername", e.servername, "--remote-send", fmt.Sprintf(":%s", mapping)) + if err := cmd.Run(); err != nil { + return fmt.Errorf("error defining key mapping: %w", err) + } + } + + return nil +} + +func (e *VimEditor) messagesContent() string { + content := "" + for _, msg := range e.messages { + content += fmt.Sprintf("[%s] %s: %s\n", msg.CreatedAt.Format("2006-01-02 15:04:05"), msg.Role, msg.Content) + } + return content +} + +func (e *VimEditor) sendCommand(cmd string) error { + vimCmd := exec.Command("vim", "--servername", e.servername, "--remote-send", fmt.Sprintf(":%s", cmd)) + return vimCmd.Run() +} + +func (e *VimEditor) sendQuery() error { + // Get the current content of the Vim buffer + content, err := e.getBufferContent() + if err != nil { + return fmt.Errorf("error getting buffer content: %w", err) + } + + // Send the query to the selected API + response, err := e.app.HandleQuery(e.selectedAPI, content, e.chatID, e.messages) + if err != nil { + return fmt.Errorf("error handling query: %w", err) + } + + // Append the response to the Vim buffer + if err := e.appendToBuffer(response); err != nil { + return fmt.Errorf("error appending to buffer: %w", err) + } + + // Update the messages slice with the new query and response + e.messages = append(e.messages, + db.Message{Role: "user", APIName: e.selectedAPI, Content: content, CreatedAt: time.Now()}, + db.Message{Role: "assistant", APIName: e.selectedAPI, Content: response, CreatedAt: time.Now()}, + ) + + return nil +} + +func (e *VimEditor) changeAPI() error { + // Get the available APIs + apis := e.app.GetAvailableAPIs() + + // Create a Vim command to prompt the user to select an API + var options []string + for _, api := range apis { + options = append(options, fmt.Sprintf(`"%s"`, api.Name)) + } + command := fmt.Sprintf("let api = inputlist([%s])", strings.Join(options, ",")) + if err := e.sendCommand(command); err != nil { + return fmt.Errorf("error sending API selection command: %w", err) + } + + // Get the selected API index + var selectedIndex int + if err := e.evalExpression("api", &selectedIndex); err != nil { + return fmt.Errorf("error getting selected API index: %w", err) + } + + // Update the selected API + if selectedIndex >= 0 && selectedIndex < len(apis) { + e.selectedAPI = apis[selectedIndex].Shortcut + } + + return nil +} + +func (e *VimEditor) sendSelectedQuery() error { + // Get the selected text in Vim + var selectedText string + if err := e.evalExpression("@*", &selectedText); err != nil { + return fmt.Errorf("error getting selected text: %w", err) + } + + // Send the selected text as a query to the selected API + response, err := e.app.HandleQuery(e.selectedAPI, selectedText, e.chatID, e.messages) + if err != nil { + return fmt.Errorf("error handling query: %w", err) + } + + // Append the response to the Vim buffer + if err := e.appendToBuffer(response); err != nil { + return fmt.Errorf("error appending to buffer: %w", err) + } + + // Update the messages slice with the new query and response + e.messages = append(e.messages, + db.Message{Role: "user", APIName: e.selectedAPI, Content: selectedText, CreatedAt: time.Now()}, + db.Message{Role: "assistant", APIName: e.selectedAPI, Content: response, CreatedAt: time.Now()}, + ) + + return nil +} + +func (e *VimEditor) getBufferContent() (string, error) { + var content string + if err := e.evalExpression("%", &content); err != nil { + return "", fmt.Errorf("error getting buffer content: %w", err) + } + return content, nil +} + +func (e *VimEditor) appendToBuffer(text string) error { + command := fmt.Sprintf("$put ='%s'", text) + return e.sendCommand(command) +} + +func (e *VimEditor) evalExpression(expr string, result interface{}) error { + vimCmd := exec.Command("vim", "--servername", e.servername, "--remote-expr", expr) + output, err := vimCmd.Output() + if err != nil { + return fmt.Errorf("error evaluating expression: %w", err) + } + if err := json.Unmarshal(output, result); err != nil { + return fmt.Errorf("error unmarshaling result: %w", err) + } + return nil +}