detached 19bb708

feat(usecase): persist split pane ratios on manual resize

commit
19bb708202f82adbf85336c264de640856e4d911
author
brice · · 2025-12-26 07:37 UTC
parent
917b52c
signature
unsigned
3 files · +84 -0
M internal/application/usecase/manage_panes.go +49 -0
@@ -260,6 +260,55 @@ func (uc *ManagePanesUseCase) Resize(
260260 return nil
261261 }
262262
263+type SetSplitRatioInput struct {
264+ Workspace *entity.Workspace
265+ SplitNodeID string
266+ Ratio float64
267+ MinPanePercent float64
268+}
269+
270+func (uc *ManagePanesUseCase) SetSplitRatio(ctx context.Context, input SetSplitRatioInput) error {
271+ log := logging.FromContext(ctx)
272+ if uc == nil {
273+ return fmt.Errorf("manage panes use case is nil")
274+ }
275+ if input.Workspace == nil {
276+ return fmt.Errorf("workspace is required")
277+ }
278+ if input.Workspace.Root == nil {
279+ return ErrNothingToResize
280+ }
281+ if input.SplitNodeID == "" {
282+ return fmt.Errorf("split node id is required")
283+ }
284+
285+ var splitNode *entity.PaneNode
286+ input.Workspace.Root.Walk(func(node *entity.PaneNode) bool {
287+ if node.ID == input.SplitNodeID {
288+ splitNode = node
289+ return false
290+ }
291+ return true
292+ })
293+
294+ if splitNode == nil || !splitNode.IsSplit() {
295+ return fmt.Errorf("split node not found: %s", input.SplitNodeID)
296+ }
297+
298+ minRatio := input.MinPanePercent / 100.0
299+ maxRatio := 1.0 - minRatio
300+ oldRatio := splitNode.SplitRatio
301+ splitNode.SplitRatio = clampFloat64(input.Ratio, minRatio, maxRatio)
302+
303+ log.Debug().
304+ Str("split_node_id", input.SplitNodeID).
305+ Float64("old_ratio", oldRatio).
306+ Float64("new_ratio", splitNode.SplitRatio).
307+ Msg("split ratio set")
308+
309+ return nil
310+}
311+
263312 func findSmartResizeDirection(target *entity.PaneNode, growActive bool) ResizeDirection {
264313 splitNode, axis, isStartChild := findNearestSplitForResize(target)
265314 if splitNode == nil {
@@ -260,6 +260,55 @@ func (uc *ManagePanesUseCase) Resize(
260 return nil260 return nil
261 }261 }
262 262
263+type SetSplitRatioInput struct {
264+ Workspace *entity.Workspace
265+ SplitNodeID string
266+ Ratio float64
267+ MinPanePercent float64
268+}
269+
270+func (uc *ManagePanesUseCase) SetSplitRatio(ctx context.Context, input SetSplitRatioInput) error {
271+ log := logging.FromContext(ctx)
272+ if uc == nil {
273+ return fmt.Errorf("manage panes use case is nil")
274+ }
275+ if input.Workspace == nil {
276+ return fmt.Errorf("workspace is required")
277+ }
278+ if input.Workspace.Root == nil {
279+ return ErrNothingToResize
280+ }
281+ if input.SplitNodeID == "" {
282+ return fmt.Errorf("split node id is required")
283+ }
284+
285+ var splitNode *entity.PaneNode
286+ input.Workspace.Root.Walk(func(node *entity.PaneNode) bool {
287+ if node.ID == input.SplitNodeID {
288+ splitNode = node
289+ return false
290+ }
291+ return true
292+ })
293+
294+ if splitNode == nil || !splitNode.IsSplit() {
295+ return fmt.Errorf("split node not found: %s", input.SplitNodeID)
296+ }
297+
298+ minRatio := input.MinPanePercent / 100.0
299+ maxRatio := 1.0 - minRatio
300+ oldRatio := splitNode.SplitRatio
301+ splitNode.SplitRatio = clampFloat64(input.Ratio, minRatio, maxRatio)
302+
303+ log.Debug().
304+ Str("split_node_id", input.SplitNodeID).
305+ Float64("old_ratio", oldRatio).
306+ Float64("new_ratio", splitNode.SplitRatio).
307+ Msg("split ratio set")
308+
309+ return nil
310+}
311+
263 func findSmartResizeDirection(target *entity.PaneNode, growActive bool) ResizeDirection {312 func findSmartResizeDirection(target *entity.PaneNode, growActive bool) ResizeDirection {
264 splitNode, axis, isStartChild := findNearestSplitForResize(target)313 splitNode, axis, isStartChild := findNearestSplitForResize(target)
265 if splitNode == nil {314 if splitNode == nil {
M internal/ui/app.go +6 -0
@@ -1030,6 +1030,12 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
10301030 }
10311031 })
10321032
1033+ wsView.SetOnSplitRatioDragged(func(nodeID string, ratio float64) {
1034+ if a.wsCoord != nil {
1035+ _ = a.wsCoord.SetSplitRatio(ctx, nodeID, ratio)
1036+ }
1037+ })
1038+
10331039 // Store in map
10341040 a.workspaceViews[tab.ID] = wsView
10351041
@@ -1030,6 +1030,12 @@ func (a *App) createWorkspaceViewWithoutAttach(ctx context.Context, tab *entity.
1030 }1030 }
1031 })1031 })
1032 1032
1033+ wsView.SetOnSplitRatioDragged(func(nodeID string, ratio float64) {
1034+ if a.wsCoord != nil {
1035+ _ = a.wsCoord.SetSplitRatio(ctx, nodeID, ratio)
1036+ }
1037+ })
1038+
1033 // Store in map1039 // Store in map
1034 a.workspaceViews[tab.ID] = wsView1040 a.workspaceViews[tab.ID] = wsView
1035 1041
M internal/ui/coordinator/workspace.go +29 -0
@@ -1900,6 +1900,35 @@ func (c *WorkspaceCoordinator) Resize(ctx context.Context, dir usecase.ResizeDir
19001900 return nil
19011901 }
19021902
1903+func (c *WorkspaceCoordinator) SetSplitRatio(ctx context.Context, splitNodeID string, ratio float64) error {
1904+ log := logging.FromContext(ctx)
1905+
1906+ if c.panesUC == nil {
1907+ log.Warn().Msg("panes use case not available")
1908+ return nil
1909+ }
1910+
1911+ ws, _ := c.getActiveWS()
1912+ if ws == nil {
1913+ log.Warn().Msg("no active workspace")
1914+ return nil
1915+ }
1916+
1917+ cfg := config.Get()
1918+ err := c.panesUC.SetSplitRatio(ctx, usecase.SetSplitRatioInput{
1919+ Workspace: ws,
1920+ SplitNodeID: splitNodeID,
1921+ Ratio: ratio,
1922+ MinPanePercent: cfg.Workspace.ResizeMode.MinPanePercent,
1923+ })
1924+ if err != nil {
1925+ return err
1926+ }
1927+
1928+ c.notifyStateChanged()
1929+ return nil
1930+}
1931+
19031932 func (c *WorkspaceCoordinator) updateSplitPositions(wsView *component.WorkspaceView, ws *entity.Workspace) {
19041933 if wsView == nil || ws == nil || ws.Root == nil {
19051934 return
@@ -1900,6 +1900,35 @@ func (c *WorkspaceCoordinator) Resize(ctx context.Context, dir usecase.ResizeDir
1900 return nil1900 return nil
1901 }1901 }
1902 1902
1903+func (c *WorkspaceCoordinator) SetSplitRatio(ctx context.Context, splitNodeID string, ratio float64) error {
1904+ log := logging.FromContext(ctx)
1905+
1906+ if c.panesUC == nil {
1907+ log.Warn().Msg("panes use case not available")
1908+ return nil
1909+ }
1910+
1911+ ws, _ := c.getActiveWS()
1912+ if ws == nil {
1913+ log.Warn().Msg("no active workspace")
1914+ return nil
1915+ }
1916+
1917+ cfg := config.Get()
1918+ err := c.panesUC.SetSplitRatio(ctx, usecase.SetSplitRatioInput{
1919+ Workspace: ws,
1920+ SplitNodeID: splitNodeID,
1921+ Ratio: ratio,
1922+ MinPanePercent: cfg.Workspace.ResizeMode.MinPanePercent,
1923+ })
1924+ if err != nil {
1925+ return err
1926+ }
1927+
1928+ c.notifyStateChanged()
1929+ return nil
1930+}
1931+
1903 func (c *WorkspaceCoordinator) updateSplitPositions(wsView *component.WorkspaceView, ws *entity.Workspace) {1932 func (c *WorkspaceCoordinator) updateSplitPositions(wsView *component.WorkspaceView, ws *entity.Workspace) {
1904 if wsView == nil || ws == nil || ws.Root == nil {1933 if wsView == nil || ws == nil || ws.Root == nil {
1905 return1934 return

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close