refactor(office): generalize DataTable layout and document TUI table skill
Unify project/user flex column layout into layoutFlexTable with per-subject policy in model. Add office-tui-table skill and AGENTS notes; fix CI go mod tidy drift and test matrix Go version to match go.mod 1.25. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
name: office-tui-table
|
||||||
|
description: >-
|
||||||
|
Build and fix Bubble Tea list tables in cmd/office (DataTable, column layout,
|
||||||
|
lipgloss cell rendering, pane width). Use when changing office center-pane
|
||||||
|
tables, column widths, ANSI styling, or list toolbar alignment.
|
||||||
|
---
|
||||||
|
# office TUI table (`cmd/office/ui`)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
| Piece | File | Role |
|
||||||
|
|-------|------|------|
|
||||||
|
| `DataTable` | `table.go` | cursor, sort, viewport, header/row render |
|
||||||
|
| Column layout | `table_layout.go` | flex vs horizontal-scroll policies |
|
||||||
|
| Flex policy | `model/table_layout.go` | per-subject flex column key + min width |
|
||||||
|
| Cell render | `table_render.go` | bubbles/table pattern + ANSI-safe padding |
|
||||||
|
| Subject row style | `table_projects.go` | optional per-subject cell styles |
|
||||||
|
| Pane sizing | `scroll.go` | `paneContentWidth`, `paneLipglossWidth` |
|
||||||
|
| Scrollbar pad | `scrollbar.go` | `padANSIWidth` for styled lines |
|
||||||
|
|
||||||
|
## Layout policies
|
||||||
|
|
||||||
|
1. **Flex (all columns visible)** — register in `model.TableFlexLayoutFor`:
|
||||||
|
- fixed columns keep `TableColumn.Width`
|
||||||
|
- one `FlexColumnKey` (e.g. `title`, `email`) absorbs remainder
|
||||||
|
- `layoutFlexTable` shrinks fixed cols down to `minFixedColumnWidth` before flex min
|
||||||
|
|
||||||
|
2. **Scrolling (default)** — `layoutScrollingTable`:
|
||||||
|
- horizontal `colScroll`; subset of columns visible
|
||||||
|
- `distributeColumnWidths` fills pane width
|
||||||
|
|
||||||
|
Add a new flex subject: one entry in `TableFlexLayoutFor` only — do **not** copy `layoutProjectTable`/`layoutUserTable` per subject.
|
||||||
|
|
||||||
|
## Cell rendering (required pattern)
|
||||||
|
|
||||||
|
Never `style.Width(w).Padding(0,1).Render(text)` on styled cells — padding doubles display width and breaks `JoinHorizontal`.
|
||||||
|
|
||||||
|
Use `renderTableCell` from `table_render.go` (matches `charmbracelet/bubbles/table`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
renderTableCell(outerStyle, plainText, columnWidth)
|
||||||
|
```
|
||||||
|
|
||||||
|
- truncate **plain** text with `runewidth.Truncate` inside `renderTableCell`
|
||||||
|
- inner box: `Width(innerW).Inline(true)` where `innerW = width - outer.GetHorizontalFrameSize()`
|
||||||
|
- outer style carries padding, background, foreground
|
||||||
|
|
||||||
|
## ANSI rules
|
||||||
|
|
||||||
|
- **Never** `runewidth.Truncate` / `padDisplayWidth` on strings that already contain lipgloss ANSI codes — corrupts escapes and shows garbage like lone `ID`.
|
||||||
|
- Use `padANSIWidth` (`charmbracelet/x/ansi`) for styled lines (headers, toolbar, scrollbar rows).
|
||||||
|
- Do not wrap an already full-width styled header in another `lipgloss.Width()` — pad once.
|
||||||
|
|
||||||
|
## Pane width
|
||||||
|
|
||||||
|
Lipgloss bordered panes render **2 cells wider** than `Style.Width`:
|
||||||
|
|
||||||
|
- `paneLipglossWidth(rendered) = rendered - 2` for `.Width()` on pane style
|
||||||
|
- `paneContentWidth(rendered) = rendered - 4` for table/viewport inner size (border + padding)
|
||||||
|
|
||||||
|
Pass `paneContentWidth(pw.List)` to `DataTable.SetSize` and list toolbar.
|
||||||
|
|
||||||
|
## Tests to add when touching tables
|
||||||
|
|
||||||
|
- `layoutFlexTable` width sum equals pane width
|
||||||
|
- `TestProjectTableHeaderFitsPaneWidth` / per-line width at 68, 40, 30
|
||||||
|
- `TestPadANSIWidthPreservesStyledLine`
|
||||||
|
- `TestThreePaneRenderedWidthMatchesTerminal` top border + table lines ≤ inner width
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Per-subject duplicate layout files (`table_foo.go` with copy-pasted shrink loops)
|
||||||
|
- Skipping truncation on selected rows (causes multi-line rows in viewport)
|
||||||
|
- Using `bubbles/table` widget directly — office needs column cursor, multi-select, subject-specific row styles
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go: ["1.22", "stable"]
|
go: ["1.25", "stable"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Canonical Go client for OnlyOffice Workspace (Projects + Calendar + CRM) and the
|
|||||||
- `calendar.go`, `projects.go`, `projects_files.go`, `tasks.go`, `tasks_files.go`, `users.go`, `contacts.go`, `opportunities.go`, `cases.go`, `crm_tasks.go`, `apps.go` — one file per subject (or per subject facet), each registers in `init()`.
|
- `calendar.go`, `projects.go`, `projects_files.go`, `tasks.go`, `tasks_files.go`, `users.go`, `contacts.go`, `opportunities.go`, `cases.go`, `crm_tasks.go`, `apps.go` — one file per subject (or per subject facet), each registers in `init()`.
|
||||||
- CLI-only deps (`spf13/cobra`, `joho/godotenv`) stay out of the library.
|
- CLI-only deps (`spf13/cobra`, `joho/godotenv`) stay out of the library.
|
||||||
- **TUI — `cmd/office/` as `package main`.** Bubble Tea three-pane browser (module tree, selectable list, markdown preview). Reuses `cmd/internal/bootstrap` for env/auth and the root `onlyoffice` library for all API calls. UI logic in `cmd/office/ui/`; preview/formatting in `cmd/office/preview/`; list loaders in `cmd/office/fetch/`.
|
- **TUI — `cmd/office/` as `package main`.** Bubble Tea three-pane browser (module tree, selectable list, markdown preview). Reuses `cmd/internal/bootstrap` for env/auth and the root `onlyoffice` library for all API calls. UI logic in `cmd/office/ui/`; preview/formatting in `cmd/office/preview/`; list loaders in `cmd/office/fetch/`.
|
||||||
|
- **List table (`DataTable`)** — `cmd/office/ui/table*.go`. Column layout policies live in `cmd/office/model/table_layout.go` (`TableFlexLayoutFor`); cell rendering uses the bubbles/table inline pattern in `table_render.go` (`renderTableCell`, `padANSIWidth`). See `.cursor/skills/office-tui-table/SKILL.md` before changing center-pane tables.
|
||||||
- **Shared bootstrap — `cmd/internal/bootstrap/`.** `LoadEnv()` + `NewClient(ctx)` extracted from `oo`; both binaries import it.
|
- **Shared bootstrap — `cmd/internal/bootstrap/`.** `LoadEnv()` + `NewClient(ctx)` extracted from `oo`; both binaries import it.
|
||||||
- **Applications sync — `cmd/oo/applications/`.** README→CRM bridge, CV-specific; kept under `cmd/oo/` so it's clear it's internal to the binary, not a library feature.
|
- **Applications sync — `cmd/oo/applications/`.** README→CRM bridge, CV-specific; kept under `cmd/oo/` so it's clear it's internal to the binary, not a library feature.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// TableFlexLayout describes a list table where every column stays visible and
|
||||||
|
// one flex column absorbs leftover pane width.
|
||||||
|
type TableFlexLayout struct {
|
||||||
|
FlexColumnKey string
|
||||||
|
MinFlexWidth int
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableFlexLayoutFor returns a flex layout policy for subjects that show all columns.
|
||||||
|
func TableFlexLayoutFor(subject Subject) (TableFlexLayout, bool) {
|
||||||
|
switch subject {
|
||||||
|
case SubjectProjects:
|
||||||
|
return TableFlexLayout{FlexColumnKey: "title", MinFlexWidth: 12}, true
|
||||||
|
case SubjectUsers:
|
||||||
|
return TableFlexLayout{FlexColumnKey: "email", MinFlexWidth: 16}, true
|
||||||
|
default:
|
||||||
|
return TableFlexLayout{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTableFlexLayoutFor(t *testing.T) {
|
||||||
|
proj, ok := TableFlexLayoutFor(SubjectProjects)
|
||||||
|
if !ok || proj.FlexColumnKey != "title" || proj.MinFlexWidth != 12 {
|
||||||
|
t.Fatalf("projects layout=%+v ok=%v", proj, ok)
|
||||||
|
}
|
||||||
|
users, ok := TableFlexLayoutFor(SubjectUsers)
|
||||||
|
if !ok || users.FlexColumnKey != "email" {
|
||||||
|
t.Fatalf("users layout=%+v", users)
|
||||||
|
}
|
||||||
|
_, ok = TableFlexLayoutFor(SubjectTasks)
|
||||||
|
if ok {
|
||||||
|
t.Fatal("tasks should use scrolling layout")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,11 @@ func TestUserFormFieldCount(t *testing.T) {
|
|||||||
|
|
||||||
func TestLayoutUserTableEmailAbsorbsWidth(t *testing.T) {
|
func TestLayoutUserTableEmailAbsorbsWidth(t *testing.T) {
|
||||||
cols := model.BuildColumns(model.SubjectUsers, nil)
|
cols := model.BuildColumns(model.SubjectUsers, nil)
|
||||||
lay := layoutUserTable(cols, 100)
|
flex, ok := model.TableFlexLayoutFor(model.SubjectUsers)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected flex layout")
|
||||||
|
}
|
||||||
|
lay := layoutFlexTable(cols, 100, flex.FlexColumnKey, flex.MinFlexWidth)
|
||||||
sum := 0
|
sum := 0
|
||||||
emailW := 0
|
emailW := 0
|
||||||
for _, i := range lay.indices {
|
for _, i := range lay.indices {
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ func (t *DataTable) styleCell(row, col int, colKey string, selected bool, item m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *DataTable) ensureColVisible() {
|
func (t *DataTable) ensureColVisible() {
|
||||||
if t.spec.Subject == model.SubjectProjects || t.spec.Subject == model.SubjectUsers {
|
if flex, ok := model.TableFlexLayoutFor(t.spec.Subject); ok && flex.FlexColumnKey != "" {
|
||||||
t.colScroll = 0
|
t.colScroll = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,75 @@ func (t *DataTable) computeLayout() columnLayout {
|
|||||||
if len(t.columns) == 0 || contentW <= 0 {
|
if len(t.columns) == 0 || contentW <= 0 {
|
||||||
return columnLayout{widths: map[int]int{}}
|
return columnLayout{widths: map[int]int{}}
|
||||||
}
|
}
|
||||||
if t.spec.Subject == model.SubjectProjects {
|
if flex, ok := model.TableFlexLayoutFor(t.spec.Subject); ok {
|
||||||
return layoutProjectTable(t.columns, contentW)
|
return layoutFlexTable(t.columns, contentW, flex.FlexColumnKey, flex.MinFlexWidth)
|
||||||
}
|
|
||||||
if t.spec.Subject == model.SubjectUsers {
|
|
||||||
return layoutUserTable(t.columns, contentW)
|
|
||||||
}
|
}
|
||||||
return layoutScrollingTable(t.columns, t.colScroll, contentW)
|
return layoutScrollingTable(t.columns, t.colScroll, contentW)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minFixedColumnWidth = 3
|
||||||
|
|
||||||
|
// layoutFlexTable shows every column; flexKey absorbs leftover width.
|
||||||
|
func layoutFlexTable(cols []model.TableColumn, totalW int, flexKey string, minFlexWidth int) columnLayout {
|
||||||
|
indices := make([]int, len(cols))
|
||||||
|
for i := range cols {
|
||||||
|
indices[i] = i
|
||||||
|
}
|
||||||
|
widths := make(map[int]int, len(cols))
|
||||||
|
|
||||||
|
flexIdx := -1
|
||||||
|
fixed := 0
|
||||||
|
for i, col := range cols {
|
||||||
|
if col.Key == flexKey {
|
||||||
|
flexIdx = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
widths[i] = col.Width
|
||||||
|
fixed += widths[i]
|
||||||
|
}
|
||||||
|
if flexIdx < 0 {
|
||||||
|
return layoutScrollingTable(cols, 0, totalW)
|
||||||
|
}
|
||||||
|
|
||||||
|
flexW := totalW - fixed
|
||||||
|
if flexW < minFlexWidth {
|
||||||
|
shrinkFixedColumns(widths, indices, flexIdx, fixed+minFlexWidth-totalW)
|
||||||
|
fixed = 0
|
||||||
|
for _, i := range indices {
|
||||||
|
if i != flexIdx {
|
||||||
|
fixed += widths[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flexW = totalW - fixed
|
||||||
|
if flexW < minFlexWidth {
|
||||||
|
flexW = minFlexWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
widths[flexIdx] = flexW
|
||||||
|
normalizeWidthSum(widths, indices, totalW, flexIdx)
|
||||||
|
return columnLayout{indices: indices, widths: widths}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shrinkFixedColumns(widths map[int]int, indices []int, flexIdx, need int) {
|
||||||
|
for need > 0 {
|
||||||
|
changed := false
|
||||||
|
for _, i := range indices {
|
||||||
|
if i == flexIdx || widths[i] <= minFixedColumnWidth {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
widths[i]--
|
||||||
|
need--
|
||||||
|
changed = true
|
||||||
|
if need == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func layoutScrollingTable(cols []model.TableColumn, colScroll, contentW int) columnLayout {
|
func layoutScrollingTable(cols []model.TableColumn, colScroll, contentW int) columnLayout {
|
||||||
indices := pickVisibleColumnIndices(cols, colScroll, contentW)
|
indices := pickVisibleColumnIndices(cols, colScroll, contentW)
|
||||||
widths := make(map[int]int, len(indices))
|
widths := make(map[int]int, len(indices))
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLayoutFlexTableAllProjectColumnsVisible(t *testing.T) {
|
||||||
|
cols := model.BuildColumns(model.SubjectProjects, nil)
|
||||||
|
flex, ok := model.TableFlexLayoutFor(model.SubjectProjects)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected flex layout for projects")
|
||||||
|
}
|
||||||
|
lay := layoutFlexTable(cols, 80, flex.FlexColumnKey, flex.MinFlexWidth)
|
||||||
|
if len(lay.indices) != 7 {
|
||||||
|
t.Fatalf("got %d visible columns, want 7", len(lay.indices))
|
||||||
|
}
|
||||||
|
sum := 0
|
||||||
|
for _, i := range lay.indices {
|
||||||
|
sum += lay.widths[i]
|
||||||
|
}
|
||||||
|
if sum != 80 {
|
||||||
|
t.Fatalf("width sum=%d want 80", sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLayoutFlexTableUserEmailAbsorbsWidth(t *testing.T) {
|
||||||
|
cols := model.BuildColumns(model.SubjectUsers, nil)
|
||||||
|
flex, ok := model.TableFlexLayoutFor(model.SubjectUsers)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected flex layout for users")
|
||||||
|
}
|
||||||
|
lay := layoutFlexTable(cols, 100, flex.FlexColumnKey, flex.MinFlexWidth)
|
||||||
|
sum := 0
|
||||||
|
emailW := 0
|
||||||
|
for _, i := range lay.indices {
|
||||||
|
sum += lay.widths[i]
|
||||||
|
if cols[i].Key == "email" {
|
||||||
|
emailW = lay.widths[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sum != 100 {
|
||||||
|
t.Fatalf("sum=%d want 100", sum)
|
||||||
|
}
|
||||||
|
if emailW < 20 {
|
||||||
|
t.Fatalf("email width=%d should absorb remainder", emailW)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,72 +5,6 @@ import (
|
|||||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
projectMinTitleWidth = 12
|
|
||||||
projectMinFixedWidth = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
// layoutProjectTable shows every project column; the title column absorbs leftover width.
|
|
||||||
func layoutProjectTable(cols []model.TableColumn, totalW int) columnLayout {
|
|
||||||
indices := make([]int, len(cols))
|
|
||||||
for i := range cols {
|
|
||||||
indices[i] = i
|
|
||||||
}
|
|
||||||
widths := make(map[int]int, len(cols))
|
|
||||||
|
|
||||||
titleIdx := -1
|
|
||||||
fixed := 0
|
|
||||||
for i, col := range cols {
|
|
||||||
if col.Key == "title" {
|
|
||||||
titleIdx = i
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
widths[i] = col.Width
|
|
||||||
fixed += widths[i]
|
|
||||||
}
|
|
||||||
if titleIdx < 0 {
|
|
||||||
return layoutScrollingTable(cols, 0, totalW)
|
|
||||||
}
|
|
||||||
|
|
||||||
titleW := totalW - fixed
|
|
||||||
if titleW < projectMinTitleWidth {
|
|
||||||
shrinkProjectFixedColumns(widths, indices, titleIdx, fixed+projectMinTitleWidth-totalW)
|
|
||||||
fixed = 0
|
|
||||||
for _, i := range indices {
|
|
||||||
if i != titleIdx {
|
|
||||||
fixed += widths[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
titleW = totalW - fixed
|
|
||||||
if titleW < projectMinTitleWidth {
|
|
||||||
titleW = projectMinTitleWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
widths[titleIdx] = titleW
|
|
||||||
normalizeWidthSum(widths, indices, totalW, titleIdx)
|
|
||||||
return columnLayout{indices: indices, widths: widths}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shrinkProjectFixedColumns(widths map[int]int, indices []int, titleIdx, need int) {
|
|
||||||
for need > 0 {
|
|
||||||
changed := false
|
|
||||||
for _, i := range indices {
|
|
||||||
if i == titleIdx || widths[i] <= projectMinFixedWidth {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
widths[i]--
|
|
||||||
need--
|
|
||||||
changed = true
|
|
||||||
if need == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !changed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *DataTable) projectBaseStyle(item model.Item) lipgloss.Style {
|
func (t *DataTable) projectBaseStyle(item model.Item) lipgloss.Style {
|
||||||
if model.ProjectIsOpen(item.Raw) {
|
if model.ProjectIsOpen(item.Raw) {
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/eslider/go-onlyoffice/cmd/office/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLayoutProjectTableAllColumnsVisible(t *testing.T) {
|
|
||||||
cols := model.BuildColumns(model.SubjectProjects, nil)
|
|
||||||
lay := layoutProjectTable(cols, 80)
|
|
||||||
if len(lay.indices) != 7 {
|
|
||||||
t.Fatalf("got %d visible columns, want 7", len(lay.indices))
|
|
||||||
}
|
|
||||||
sum := 0
|
|
||||||
for _, i := range lay.indices {
|
|
||||||
sum += lay.widths[i]
|
|
||||||
}
|
|
||||||
if sum != 80 {
|
|
||||||
t.Fatalf("width sum=%d want 80", sum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -94,7 +94,11 @@ func TestDistributeColumnWidthsFillsTotal(t *testing.T) {
|
|||||||
|
|
||||||
func TestProjectTableTitleAbsorbsWidth(t *testing.T) {
|
func TestProjectTableTitleAbsorbsWidth(t *testing.T) {
|
||||||
cols := model.BuildColumns(model.SubjectProjects, nil)
|
cols := model.BuildColumns(model.SubjectProjects, nil)
|
||||||
lay := layoutProjectTable(cols, 120)
|
flex, ok := model.TableFlexLayoutFor(model.SubjectProjects)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected flex layout")
|
||||||
|
}
|
||||||
|
lay := layoutFlexTable(cols, 120, flex.FlexColumnKey, flex.MinFlexWidth)
|
||||||
if len(lay.indices) != len(cols) {
|
if len(lay.indices) != len(cols) {
|
||||||
t.Fatalf("expected all %d columns visible, got %d", len(cols), len(lay.indices))
|
t.Fatalf("expected all %d columns visible, got %d", len(cols), len(lay.indices))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package ui
|
|
||||||
|
|
||||||
import "github.com/eslider/go-onlyoffice/cmd/office/model"
|
|
||||||
|
|
||||||
const (
|
|
||||||
userMinEmailWidth = 16
|
|
||||||
userMinFixedWidth = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
// layoutUserTable shows every user column; email absorbs leftover width.
|
|
||||||
func layoutUserTable(cols []model.TableColumn, totalW int) columnLayout {
|
|
||||||
indices := make([]int, len(cols))
|
|
||||||
for i := range cols {
|
|
||||||
indices[i] = i
|
|
||||||
}
|
|
||||||
widths := make(map[int]int, len(cols))
|
|
||||||
|
|
||||||
emailIdx := -1
|
|
||||||
fixed := 0
|
|
||||||
for i, col := range cols {
|
|
||||||
if col.Key == "email" {
|
|
||||||
emailIdx = i
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
widths[i] = col.Width
|
|
||||||
fixed += widths[i]
|
|
||||||
}
|
|
||||||
if emailIdx < 0 {
|
|
||||||
return layoutScrollingTable(cols, 0, totalW)
|
|
||||||
}
|
|
||||||
|
|
||||||
emailW := totalW - fixed
|
|
||||||
if emailW < userMinEmailWidth {
|
|
||||||
shrinkUserFixedColumns(widths, indices, emailIdx, fixed+userMinEmailWidth-totalW)
|
|
||||||
fixed = 0
|
|
||||||
for _, i := range indices {
|
|
||||||
if i != emailIdx {
|
|
||||||
fixed += widths[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emailW = totalW - fixed
|
|
||||||
if emailW < userMinEmailWidth {
|
|
||||||
emailW = userMinEmailWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
widths[emailIdx] = emailW
|
|
||||||
normalizeWidthSum(widths, indices, totalW, emailIdx)
|
|
||||||
return columnLayout{indices: indices, widths: widths}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shrinkUserFixedColumns(widths map[int]int, indices []int, emailIdx, need int) {
|
|
||||||
for need > 0 {
|
|
||||||
changed := false
|
|
||||||
for _, i := range indices {
|
|
||||||
if i == emailIdx || widths[i] <= userMinFixedWidth {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
widths[i]--
|
|
||||||
need--
|
|
||||||
changed = true
|
|
||||||
if need == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !changed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,18 +8,21 @@ require (
|
|||||||
github.com/charmbracelet/bubbletea v0.25.0
|
github.com/charmbracelet/bubbletea v0.25.0
|
||||||
github.com/charmbracelet/glamour v0.8.0
|
github.com/charmbracelet/glamour v0.8.0
|
||||||
github.com/charmbracelet/lipgloss v0.12.1
|
github.com/charmbracelet/lipgloss v0.12.1
|
||||||
|
github.com/charmbracelet/x/ansi v0.1.4
|
||||||
github.com/eslider/go-xls/v2 v2.1.0
|
github.com/eslider/go-xls/v2 v2.1.0
|
||||||
github.com/google/go-querystring v1.2.0
|
github.com/google/go-querystring v1.2.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/mattn/go-runewidth v0.0.15
|
||||||
|
github.com/muesli/termenv v0.16.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/JohannesKaufmann/dom v0.3.1 // indirect
|
github.com/JohannesKaufmann/dom v0.3.1 // indirect
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
|
||||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
@@ -27,12 +30,10 @@ require (
|
|||||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/reflow v0.3.0 // indirect
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
github.com/yuin/goldmark v1.8.2 // indirect
|
github.com/yuin/goldmark v1.8.2 // indirect
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46
|
|||||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
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 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||||
|
|||||||
Reference in New Issue
Block a user