From 6188eff8311e51dc2ac4de357fa8a95cc991d2ca Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Sat, 7 Jun 2025 15:08:25 +0200 Subject: [PATCH] wip filepicker --- diff.diff | 124 --------------- go.mod | 10 +- go.sum | 23 +-- .../dialogs/filepicker/filepicker.go | 141 ++++++++++++++++++ .../tui/components/dialogs/filepicker/keys.go | 73 +++++++++ internal/tui/keys.go | 15 +- internal/tui/page/chat/chat.go | 19 ++- internal/tui/page/chat/keys.go | 5 + internal/tui/styles/theme.go | 18 +++ internal/tui/tui.go | 17 +++ 10 files changed, 278 insertions(+), 167 deletions(-) delete mode 100644 diff.diff create mode 100644 internal/tui/components/dialogs/filepicker/filepicker.go create mode 100644 internal/tui/components/dialogs/filepicker/keys.go diff --git a/diff.diff b/diff.diff deleted file mode 100644 index e22ae61ef5e96692b9e0d5dbf4b1ad1b7ea578b0..0000000000000000000000000000000000000000 --- a/diff.diff +++ /dev/null @@ -1,124 +0,0 @@ -diff --git a/.opencode.json b/.opencode.json -index 75e357d..59be1e8 100644 ---- a/.opencode.json -+++ b/.opencode.json -@@ -6,6 +6,6 @@ - } - }, - "tui": { -- "theme": "opencode-dark" -+ "theme": "charm" - } - } -diff --git a/go.mod b/go.mod -index 18ad042..940a8e8 100644 ---- a/go.mod -+++ b/go.mod -@@ -36,6 +36,8 @@ require ( - github.com/stretchr/testify v1.10.0 - ) - -+require github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 // indirect -+ - require ( - cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.13.0 // indirect -diff --git a/go.sum b/go.sum -index f6e08b7..8f347ed 100644 ---- a/go.sum -+++ b/go.sum -@@ -84,6 +84,8 @@ github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa h1:JU05TLAB - github.com/charmbracelet/x/ansi v0.9.3-0.20250516160309-24eee56f89fa/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= - github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME= - github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk= -+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 h1:f6tG7ApqIvXTpgF6MZ+C4Ga7669eiW9BsMkXEjDFHfY= -+github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9/go.mod h1:vr+xCFylsPYq2qSz+n5/jItjcK2/PgrKFMTI7VRR6CI= - github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw= - github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= - github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620 h1:/PN4jqP3ki9NvtHRrYZ9ewCutKZB6DK8frTW+Dj/MWs= -diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go -index 2ee0b04..52c4dae 100644 ---- a/internal/tui/components/chat/chat.go -+++ b/internal/tui/components/chat/chat.go -@@ -95,7 +95,7 @@ func lspsConfigured() string { - func logoBlock() string { - t := theme.CurrentTheme() - return logo.Render(version.Version, true, logo.Opts{ -- FieldColor: t.Accent(), -+ FieldColor: t.Secondary(), - TitleColorA: t.Primary(), - TitleColorB: t.Secondary(), - CharmColor: t.Primary(), -diff --git a/internal/tui/tui.go b/internal/tui/tui.go -index 9e8a62a..3f07956 100644 ---- a/internal/tui/tui.go -+++ b/internal/tui/tui.go -@@ -18,6 +18,7 @@ import ( - "github.com/opencode-ai/opencode/internal/tui/util" - ) - -+// appModel represents the main application model that manages pages, dialogs, and UI state. - type appModel struct { - width, height int - keyMap KeyMap -@@ -35,6 +36,7 @@ type appModel struct { - completions completions.Completions - } - -+// Init initializes the application model and returns initial commands. - func (a appModel) Init() tea.Cmd { - var cmds []tea.Cmd - cmd := a.pages[a.currentPage].Init() -@@ -46,6 +48,7 @@ func (a appModel) Init() tea.Cmd { - return tea.Batch(cmds...) - } - -+// Update handles incoming messages and updates the application state. - func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - var cmd tea.Cmd -@@ -111,6 +114,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - return a, tea.Batch(cmds...) - } - -+// handleWindowResize processes window resize events and updates all components. - func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd { - var cmds []tea.Cmd - msg.Height -= 1 // Make space for the status bar -@@ -134,6 +138,7 @@ func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd { - return tea.Batch(cmds...) - } - -+// handleKeyPressMsg processes keyboard input and routes to appropriate handlers. - func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { - switch { - // completions -@@ -182,11 +187,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { - } - } - --// RegisterCommand adds a command to the command dialog --// func (a *appModel) RegisterCommand(cmd dialog.Command) { --// a.commands = append(a.commands, cmd) --// } -- -+// moveToPage handles navigation between different pages in the application. - func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd { - if a.app.CoderAgent.IsBusy() { - // For now we don't move to any page if the agent is busy -@@ -209,6 +210,7 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd { - return tea.Batch(cmds...) - } - -+// View renders the complete application interface including pages, dialogs, and overlays. - func (a *appModel) View() tea.View { - pageView := a.pages[a.currentPage].View() - components := []string{ -@@ -252,6 +254,7 @@ func (a *appModel) View() tea.View { - return view - } - -+// New creates and initializes a new TUI application model. - func New(app *app.App) tea.Model { - startPage := page.ChatPage - model := &appModel{ diff --git a/go.mod b/go.mod index 560577ca7c5d2eff0384833aecf76e6f327c4102..c4892e8b3616ed276178f7a4c32db9dae48a3439 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,8 @@ require ( github.com/anthropics/anthropic-sdk-go v1.4.0 github.com/aymanbagabas/go-udiff v0.2.0 github.com/bmatcuk/doublestar/v4 v4.8.1 - github.com/catppuccin/go v0.3.0 github.com/charlievieth/fastwalk v1.0.11 - github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f + github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c @@ -23,9 +22,6 @@ require ( github.com/go-logfmt/logfmt v0.6.0 github.com/google/uuid v1.6.0 github.com/mark3labs/mcp-go v0.17.0 - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 - github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.16.0 github.com/ncruces/go-sqlite3 v0.25.0 github.com/openai/openai-go v0.1.0-beta.2 github.com/pressly/goose/v3 v3.24.2 @@ -37,6 +33,8 @@ require ( github.com/stretchr/testify v1.10.0 ) +require github.com/dustin/go-humanize v1.0.1 // indirect + require ( cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.13.0 // indirect @@ -60,7 +58,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/smithy-go v1.20.3 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.3.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect @@ -85,7 +82,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect diff --git a/go.sum b/go.sum index 2c9413322f0bc1043b97f4e238f05cf494b8941a..45bfc062f74dc87fd689169a910d2d3cf151b59f 100644 --- a/go.sum +++ b/go.sum @@ -58,24 +58,18 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= -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/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= -github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8= github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318 h1:f8Q0ybZGxT+St1JfPM7yoz/XFpbmtodcIehaom/9XT8= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250526131538-b3f0c9e42318/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e h1:+3I/1v7vbN0Vf8Tjm3Q0zdLQqjOM/TjQBvoRDQtoAss= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603122936-f1a3fad2b64e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f h1:vvNB+i59Wp3L6gYcpuhfAdNjr4/e6qM/st3ySWfmZnU= -github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250603125125-87aee03b3d4f/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c h1:Dgy7cOR3skvJjGVnhyaixW6ugxVxLtSjRCiMRSdbXSc= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607105053-36addcd3ab8c/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A= +github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174 h1:TlVW+df0rdU/osP0O8DIVS9WFOAzXe3nuiMwJR4n+CA= github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250602154956-43689cfc0174/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= @@ -162,21 +156,14 @@ github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi9 github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= 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-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I= github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -196,7 +183,6 @@ github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJz github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -312,7 +298,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/tui/components/dialogs/filepicker/filepicker.go b/internal/tui/components/dialogs/filepicker/filepicker.go new file mode 100644 index 0000000000000000000000000000000000000000..d80031c6115e6aaed7b071406c129ac7ceecc233 --- /dev/null +++ b/internal/tui/components/dialogs/filepicker/filepicker.go @@ -0,0 +1,141 @@ +package filepicker + +import ( + "os" + "strings" + + "github.com/charmbracelet/bubbles/v2/filepicker" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" + "github.com/opencode-ai/opencode/internal/tui/components/core" + "github.com/opencode-ai/opencode/internal/tui/components/dialogs" + "github.com/opencode-ai/opencode/internal/tui/styles" +) + +const ( + maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB + FilePickerID = "filepicker" + fileSelectionHight = 10 +) + +type FilePicker interface { + dialogs.DialogModel +} + +type filePicker struct { + wWidth int + wHeight int + width int + filepicker filepicker.Model + selectedFile string +} + +func NewFilePickerCmp() FilePicker { + t := styles.CurrentTheme() + fp := filepicker.New() + fp.AllowedTypes = []string{".jpg", ".jpeg", ".png"} + fp.CurrentDirectory, _ = os.UserHomeDir() + fp.ShowPermissions = false + fp.ShowSize = false + fp.AutoHeight = false + fp.Styles = t.S().FilePicker + fp.Cursor = "" + fp.SetHeight(fileSelectionHight) + + return &filePicker{filepicker: fp} +} + +func (m *filePicker) Init() tea.Cmd { + return m.filepicker.Init() +} + +func (m *filePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.wWidth = msg.Width + m.wHeight = msg.Height + m.width = min(70, m.wWidth) + styles := m.filepicker.Styles + styles.Directory = styles.Directory.Width(m.width - 4) + styles.Selected = styles.Selected.PaddingLeft(1).Width(m.width - 4) + styles.DisabledSelected = styles.DisabledSelected.PaddingLeft(1).Width(m.width - 4) + styles.File = styles.File.Width(m.width) + m.filepicker.Styles = styles + return m, nil + } + + 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 + } + + return m, cmd +} + +func (m *filePicker) View() tea.View { + t := styles.CurrentTheme() + + content := lipgloss.JoinVertical( + lipgloss.Left, + t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Add Image", m.width-4)), + m.imagePreview(), + m.filepicker.View(), + ) + return tea.NewView(m.style().Render(content)) +} + +func (m *filePicker) currentImage() string { + for _, ext := range m.filepicker.AllowedTypes { + if strings.HasSuffix(m.filepicker.HighlightedPath(), ext) { + return m.filepicker.HighlightedPath() + } + } + return "" +} + +func (m *filePicker) imagePreview() string { + if m.currentImage() == "" { + return m.imagePreviewStyle().Render() + } + + return "" +} + +func (m *filePicker) imagePreviewStyle() lipgloss.Style { + t := styles.CurrentTheme() + w, h := m.imagePreviewSize() + return t.S().Base. + Width(w). + Height(h). + Margin(1). + Background(t.BgOverlay) +} + +func (m *filePicker) imagePreviewSize() (int, int) { + return m.width - 4, min(20, m.wHeight/2) +} + +func (m *filePicker) style() lipgloss.Style { + t := styles.CurrentTheme() + return t.S().Base. + Width(m.width). + Border(lipgloss.RoundedBorder()). + BorderForeground(t.BorderFocus) +} + +// ID implements FilePicker. +func (m *filePicker) ID() dialogs.DialogID { + return FilePickerID +} + +// Position implements FilePicker. +func (m *filePicker) Position() (int, int) { + row := m.wHeight/4 - 2 // just a bit above the center + col := m.wWidth / 2 + col -= m.width / 2 + return row, col +} diff --git a/internal/tui/components/dialogs/filepicker/keys.go b/internal/tui/components/dialogs/filepicker/keys.go new file mode 100644 index 0000000000000000000000000000000000000000..d5f3a971808ff30e1503e1c4654944b867fd90b0 --- /dev/null +++ b/internal/tui/components/dialogs/filepicker/keys.go @@ -0,0 +1,73 @@ +package filepicker + +import ( + "github.com/charmbracelet/bubbles/v2/key" + "github.com/opencode-ai/opencode/internal/tui/layout" +) + +// KeyMap defines keyboard bindings for dialog management. +type KeyMap struct { + Select, + Down, + Up, + Forward, + Backward, + InsertCWD, + Close key.Binding +} + +func DefaultKeyMap() KeyMap { + return KeyMap{ + Select: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "accept"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("down/j", "move down"), + ), + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("up/k", "move up"), + ), + Forward: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("right/l", "move forward"), + ), + Backward: key.NewBinding( + key.WithKeys("left", "h"), + key.WithHelp("left/h", "move backward"), + ), + InsertCWD: key.NewBinding( + key.WithKeys("i"), + key.WithHelp("i", "manual path input"), + ), + Close: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "close/exit"), + ), + } +} + +// FullHelp implements help.KeyMap. +func (k KeyMap) FullHelp() [][]key.Binding { + m := [][]key.Binding{} + slice := layout.KeyMapToSlice(k) + for i := 0; i < len(slice); i += 4 { + end := min(i+4, len(slice)) + m = append(m, slice[i:end]) + } + return m +} + +// ShortHelp implements help.KeyMap. +func (k KeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + k.InsertCWD, + key.NewBinding( + key.WithHelp("↑↓←→", "navigate"), + ), + k.Select, + k.Close, + } +} diff --git a/internal/tui/keys.go b/internal/tui/keys.go index 96dbab01400f622e8d3e224e3f626f206d4ab68f..4a9d0f81d600e8fb5701cc8555241723b2188d74 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -6,12 +6,11 @@ import ( ) type KeyMap struct { - Logs key.Binding - Quit key.Binding - Help key.Binding - Commands key.Binding - Sessions key.Binding - FilePicker key.Binding + Logs key.Binding + Quit key.Binding + Help key.Binding + Commands key.Binding + Sessions key.Binding } func DefaultKeyMap() KeyMap { @@ -37,10 +36,6 @@ func DefaultKeyMap() KeyMap { key.WithKeys("ctrl+s"), key.WithHelp("ctrl+s", "sessions"), ), - FilePicker: key.NewBinding( - key.WithKeys("ctrl+f"), - key.WithHelp("ctrl+f", "select files to upload"), - ), } } diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index b62dba2c9d62eb107e5c2eb06bd5c23b5e1bbd23..ce5a38a3454d26c77cb5ceb209cd7d41ac216b23 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -24,17 +24,20 @@ type ChatFocusedMsg struct { Focused bool // True if the chat input is focused, false otherwise } -type chatPage struct { - app *app.App +type ( + OpenFilePickerMsg struct{} + chatPage struct { + app *app.App - layout layout.SplitPaneLayout + layout layout.SplitPaneLayout - session session.Session + session session.Session - keyMap KeyMap + keyMap KeyMap - chatFocused bool -} + chatFocused bool + } +) func (p *chatPage) Init() tea.Cmd { cmd := p.layout.Init() @@ -83,6 +86,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { util.CmdHandler(chat.SessionClearedMsg{}), ) + case key.Matches(msg, p.keyMap.FilePicker): + return p, util.CmdHandler(OpenFilePickerMsg{}) case key.Matches(msg, p.keyMap.Tab): logging.Info("Tab key pressed, toggling chat focus") if p.session.ID == "" { diff --git a/internal/tui/page/chat/keys.go b/internal/tui/page/chat/keys.go index d8b151dbd9b3a3f0c20db1f16e51f011c25c4e7f..8441e23b02fd16c70d80ad6258633b3e9756d885 100644 --- a/internal/tui/page/chat/keys.go +++ b/internal/tui/page/chat/keys.go @@ -7,6 +7,7 @@ import ( type KeyMap struct { NewSession key.Binding + FilePicker key.Binding Cancel key.Binding Tab key.Binding } @@ -25,6 +26,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("tab"), key.WithHelp("tab", "change focus"), ), + FilePicker: key.NewBinding( + key.WithKeys("ctrl+f"), + key.WithHelp("ctrl+f", "select files to upload"), + ), } } diff --git a/internal/tui/styles/theme.go b/internal/tui/styles/theme.go index 0bfcc0388c4da1336863445a9524284569c35722..79a56959ca178f34ad974777e095e91285380911 100644 --- a/internal/tui/styles/theme.go +++ b/internal/tui/styles/theme.go @@ -5,6 +5,7 @@ import ( "image/color" "strings" + "github.com/charmbracelet/bubbles/v2/filepicker" "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/textarea" "github.com/charmbracelet/bubbles/v2/textinput" @@ -110,6 +111,9 @@ type Styles struct { // Diff Diff Diff + + // FilePicker + FilePicker filepicker.Styles } func (t *Theme) S() *Styles { @@ -430,6 +434,20 @@ func (t *Theme) buildStyles() *Styles { AddedLineNumberBg: t.GreenDark, RemovedLineNumberBg: t.RedDark, }, + + FilePicker: filepicker.Styles{ + DisabledCursor: base.Foreground(t.FgMuted), + Cursor: base.Foreground(t.FgBase), + Symlink: base.Foreground(t.FgSubtle), + Directory: base.Foreground(t.Primary), + File: base.Foreground(t.FgBase), + DisabledFile: base.Foreground(t.FgMuted), + DisabledSelected: base.Background(t.BgOverlay).Foreground(t.FgMuted), + Permission: base.Foreground(t.FgMuted), + Selected: base.Background(t.Primary).Foreground(t.FgBase), + FileSize: base.Foreground(t.FgMuted), + EmptyDirectory: base.Foreground(t.FgMuted).PaddingLeft(2).SetString("Empty directory"), + }, } } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 71e0ea7dc64b0a1e2ffa094564f8287e440f6452..2477fb7022b3904875d3e4dc31467b9510273dcb 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -14,6 +14,7 @@ import ( "github.com/opencode-ai/opencode/internal/tui/components/core/status" "github.com/opencode-ai/opencode/internal/tui/components/dialogs" "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands" + "github.com/opencode-ai/opencode/internal/tui/components/dialogs/filepicker" "github.com/opencode-ai/opencode/internal/tui/components/dialogs/models" "github.com/opencode-ai/opencode/internal/tui/components/dialogs/quit" "github.com/opencode-ai/opencode/internal/tui/components/dialogs/sessions" @@ -125,12 +126,22 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { Model: sessions.NewSessionDialogCmp(allSessions, a.selectedSessionID), } } + case commands.SwitchModelMsg: return a, util.CmdHandler( dialogs.OpenDialogMsg{ Model: models.NewModelDialogCmp(), }, ) + // File Picker + case chat.OpenFilePickerMsg: + if a.dialog.ActiveDialogId() == filepicker.FilePickerID { + // If the commands dialog is already open, close it + return a, util.CmdHandler(dialogs.CloseDialogMsg{}) + } + return a, util.CmdHandler(dialogs.OpenDialogMsg{ + Model: filepicker.NewFilePickerCmp(), + }) case tea.KeyPressMsg: return a, a.handleKeyPressMsg(msg) } @@ -138,6 +149,11 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.status = s.(status.StatusCmp) updated, cmd := a.pages[a.currentPage].Update(msg) a.pages[a.currentPage] = updated.(util.Model) + if a.dialog.HasDialogs() { + u, dialogCmd := a.dialog.Update(msg) + a.dialog = u.(dialogs.DialogCmp) + cmds = append(cmds, dialogCmd) + } cmds = append(cmds, cmd) return a, tea.Batch(cmds...) } @@ -277,6 +293,7 @@ func (a *appModel) View() tea.View { lipgloss.NewLayer(appView), } if a.dialog.HasDialogs() { + logging.Info("Rendering dialogs") layers = append( layers, a.dialog.GetLayers()...,