From b00f2fa4e197d73459554bda0a3ed485ffb4d64d Mon Sep 17 00:00:00 2001 From: dneaves Date: Wed, 7 Oct 2026 19:07:21 -0400 Subject: [PATCH] Split, move, and close panes --- glyph.cabal | 4 + lib/Glyph/Config/Dhall/Action.hs | 8 ++ lib/Glyph/Config/Dhall/Examples/Glyph.hs | 47 +++++++- lib/Glyph/Event.hs | 31 ++++-- lib/Glyph/State.hs | 16 ++- lib/Glyph/State/Pane.hs | 15 ++- lib/Glyph/State/Pane/Editor.hs | 0 lib/Glyph/State/Pane/Notification.hs | 0 lib/Glyph/State/Pane/Terminal.hs | 0 lib/Glyph/Update.hs | 30 ++++++ lib/Glyph/View/Data/Rect.hs | 25 ----- lib/Glyph/View/Element/Base.hs | 2 +- lib/Glyph/View/Element/Pane.hs | 113 ++++++++++++++++++-- lib/Glyph/View/Element/Pane/Blank.hs | 18 ++++ lib/Glyph/View/Element/Pane/Editor.hs | 17 +++ lib/Glyph/View/Element/Pane/Notification.hs | 17 +++ lib/Glyph/View/Element/Pane/Terminal.hs | 18 ++++ test/GlyphTest/State/Pane.hs | 10 +- 18 files changed, 313 insertions(+), 58 deletions(-) delete mode 100644 lib/Glyph/State/Pane/Editor.hs delete mode 100644 lib/Glyph/State/Pane/Notification.hs delete mode 100644 lib/Glyph/State/Pane/Terminal.hs create mode 100644 lib/Glyph/View/Element/Pane/Blank.hs create mode 100644 lib/Glyph/View/Element/Pane/Editor.hs create mode 100644 lib/Glyph/View/Element/Pane/Notification.hs create mode 100644 lib/Glyph/View/Element/Pane/Terminal.hs diff --git a/glyph.cabal b/glyph.cabal index 960c9b1..57774d0 100644 --- a/glyph.cabal +++ b/glyph.cabal @@ -81,6 +81,10 @@ library lib Glyph.View.Element.Base Glyph.View.Element.BottomBar Glyph.View.Element.Pane + Glyph.View.Element.Pane.Blank + Glyph.View.Element.Pane.Editor + Glyph.View.Element.Pane.Notification + Glyph.View.Element.Pane.Terminal Glyph.View.Id Glyph.View.Style Prelude diff --git a/lib/Glyph/Config/Dhall/Action.hs b/lib/Glyph/Config/Dhall/Action.hs index ce36b01..d8a6628 100644 --- a/lib/Glyph/Config/Dhall/Action.hs +++ b/lib/Glyph/Config/Dhall/Action.hs @@ -16,6 +16,10 @@ data Action | QuitAndAbandon | QuitAndSave | SelectRowsDown (Input Natural) + | PanesSplitOneOverOther + | PanesSplitSideBySide + | PanesCloseFocused + | PanesChangeFocused (Input Natural) deriving (Dhall.FromDhall, Eq, Generic, Show) ---------------------------------------------------------------------------------------------------- @@ -31,6 +35,10 @@ dhallAction = | QuitAndAbandon | QuitAndSave | SelectRowsDown : Input Natural + | PanesSplitOneOverOther + | PanesSplitSideBySide + | PanesCloseFocused + | PanesChangeFocused : Input Natural > """ diff --git a/lib/Glyph/Config/Dhall/Examples/Glyph.hs b/lib/Glyph/Config/Dhall/Examples/Glyph.hs index 67e51b3..409a50a 100644 --- a/lib/Glyph/Config/Dhall/Examples/Glyph.hs +++ b/lib/Glyph/Config/Dhall/Examples/Glyph.hs @@ -6,8 +6,9 @@ import qualified Data.Map.Strict as Map import qualified Data.Set as Set import Glyph.Config.Dhall.Action ( Action (..) ) +import Glyph.Config.Dhall.Input ( Input (..) ) import Glyph.Key.Chord ( Chord (..) ) -import Glyph.Key.Edge ( Edge (..) ) +import Glyph.Key.Edge ( CaptureType (..), Edge (..) ) import Glyph.Key.Trie ( KeyTrie (..) ) import Graphics.Vty.Input.Events ( Key (..), Modifier (..) ) @@ -51,13 +52,53 @@ exampleModal = ( Literal <| KChar 'a' , Branch <| Map.fromList - [(Literal <| KEnter, Leaf QuitAndAbandon)] + [(Literal KEnter, Leaf QuitAndAbandon)] ) , ( Literal <| KChar 's' , Branch <| Map.fromList - [(Literal <| KEnter, Leaf QuitAndSave)] + [(Literal KEnter, Leaf QuitAndSave)] + ) + ] + ) + , + ( Literal <| KChar 'p' + , Branch <| + Map.fromList + [ + ( Literal <| KChar 'v' + , Branch <| + Map.fromList + [(Literal KEnter, Leaf PanesSplitOneOverOther)] + ) + , + ( Literal <| KChar 'h' + , Branch <| + Map.fromList + [(Literal KEnter, Leaf PanesSplitSideBySide)] + ) + , + ( Literal <| KChar 'c' + , Branch <| + Map.fromList + [(Literal KEnter, Leaf PanesCloseFocused)] + ) + , + ( Literal <| KChar 'f' + , Branch <| + Map.fromList + [ + ( Capture CaptureNum + , Branch <| + Map.fromList + [ + ( Literal KEnter + , Leaf <| PanesChangeFocused FromCapture + ) + ] + ) + ] ) ] ) diff --git a/lib/Glyph/Event.hs b/lib/Glyph/Event.hs index 809b408..a1870aa 100644 --- a/lib/Glyph/Event.hs +++ b/lib/Glyph/Event.hs @@ -4,26 +4,36 @@ module Glyph.Event where import qualified Data.List as List -import Glyph.Class.All ( All (..) ) import Glyph.Config.Dhall.Action ( Action ) import qualified Glyph.Config.Dhall.Action as Action +import Glyph.Config.Dhall.Input ( Input (..) ) import Glyph.Key.Trie ( Captured (..) ) +import Glyph.State.Pane ( PaneId (..) ) ---------------------------------------------------------------------------------------------------- -data Event = Quit | EnterModalEntry - deriving (All, Bounded, Enum, Eq, Ord, Show) +data Event + = InsertError Text + | InsertWarning Text + | InsertInfo Text + | Quit + | EnterModalEntry + | SplitPaneOneOverOther + | SplitPaneSideBySide + | CloseFocusedPane + | ChangeFocusedPane PaneId + deriving (Eq, Ord, Show) --------------------------------------------------------------------------------------------------- planEvents :: Action -> Natural -> List Captured -> List Event planEvents action count captured = let numTimes = fromEnum count - in List.replicate numTimes (fromAction action) + in List.replicate numTimes (fromAction action count captured) |> List.concat -fromAction :: Action -> List Event -fromAction = \case +fromAction :: Action -> Natural -> List Captured -> List Event +fromAction action count captured = case action of Action.NoOp -> [] Action.Help -> [] Action.QuitAndSave -> [Quit] @@ -31,3 +41,12 @@ fromAction = \case Action.StartModal -> [EnterModalEntry] Action.StartCount -> [] Action.SelectRowsDown _ -> [] + Action.PanesSplitOneOverOther -> [SplitPaneOneOverOther] + Action.PanesSplitSideBySide -> [SplitPaneSideBySide] + Action.PanesCloseFocused -> [CloseFocusedPane] + Action.PanesChangeFocused input -> case input of + Predetermined i -> [ChangeFocusedPane <| PaneId i] + FromCount -> [ChangeFocusedPane <| PaneId count] + FromCapture -> case captured of + [CapturedNum n] -> [ChangeFocusedPane <| PaneId n] + _ -> [InsertError "Wrong capture type collected"] diff --git a/lib/Glyph/State.hs b/lib/Glyph/State.hs index 708fb5e..d174372 100644 --- a/lib/Glyph/State.hs +++ b/lib/Glyph/State.hs @@ -33,8 +33,8 @@ data State , status :: Text -- More specific notices on status , notices :: List Notice - -- I'm in pane - , paneState :: PaneState + -- I'm in a state of pane + , panes :: PaneState } deriving (Eq, Show) @@ -52,7 +52,7 @@ blankState = , input = InputState.idle , status = "" , notices = [] - , paneState = PaneState.init + , panes = PaneState.init } initialState @@ -69,12 +69,20 @@ initialState chordKT modalKT notices' = , input = InputState.idle , status = "" , notices = notices' - , paneState = PaneState.init + , panes = PaneState.init } +---------------------------------------------------------------------------------------------------- + updateInputState :: State -> InputState -> State updateInputState state input' = state{input = input'} +updatePaneState :: State -> PaneState -> State +updatePaneState state panes' = state{panes = panes'} + +appendNotice :: State -> Notice -> State +appendNotice state notice = state{notices = state.notices <> [notice]} + updateBrickState :: State -> Brick.EventM Id State () updateBrickState state = -- discard the state in Brick and use the one we supply instead, diff --git a/lib/Glyph/State/Pane.hs b/lib/Glyph/State/Pane.hs index e4d2b82..f1506a6 100644 --- a/lib/Glyph/State/Pane.hs +++ b/lib/Glyph/State/Pane.hs @@ -10,8 +10,11 @@ newtype PaneId = PaneId { id :: Natural } deriving (Eq, Ord, Show) -newtype BufferId - = BufferId { id :: Natural } +data Buffer + = Buffer + { id :: Natural + , filePath :: FilePath + } deriving (Eq, Ord, Show) newtype TTYId @@ -20,7 +23,7 @@ newtype TTYId data PaneType = Blank - | Editor BufferId + | Editor Buffer | Terminal TTYId | Notifications deriving (Eq, Show) @@ -92,6 +95,12 @@ getNextStateFocused focused layout = Nothing -> nullId x : _ -> x +setFocus :: PaneId -> PaneState -> PaneState +setFocus paneId' paneState = + if List.elem paneId' <| layoutIds paneState.layout + then paneState{focused = paneId'} + else paneState + split :: Arrangement -> PaneState -> PaneState split arrangement' paneState = let stepLayout :: PaneLayout -> PaneLayout diff --git a/lib/Glyph/State/Pane/Editor.hs b/lib/Glyph/State/Pane/Editor.hs deleted file mode 100644 index e69de29..0000000 diff --git a/lib/Glyph/State/Pane/Notification.hs b/lib/Glyph/State/Pane/Notification.hs deleted file mode 100644 index e69de29..0000000 diff --git a/lib/Glyph/State/Pane/Terminal.hs b/lib/Glyph/State/Pane/Terminal.hs deleted file mode 100644 index e69de29..0000000 diff --git a/lib/Glyph/Update.hs b/lib/Glyph/Update.hs index 9774002..3c10fdc 100644 --- a/lib/Glyph/Update.hs +++ b/lib/Glyph/Update.hs @@ -22,6 +22,8 @@ import Glyph.State ( State (..) ) import qualified Glyph.State as State import Glyph.State.Input ( InputState (..) ) import qualified Glyph.State.Input as InputState +import qualified Glyph.State.Notice as Notice +import qualified Glyph.State.Pane as PaneState import Glyph.View.Id ( Id ) import Graphics.Vty.Input.Events ( Key (..), Modifier (..) ) @@ -61,6 +63,34 @@ handleAppEvent event = do ModalInput [] |> State.updateInputState state |> State.updateBrickState + Event.SplitPaneOneOverOther -> + PaneState.split PaneState.OneOverOther state.panes + |> State.updatePaneState state + |> State.updateBrickState + Event.SplitPaneSideBySide -> + PaneState.split PaneState.SideBySide state.panes + |> State.updatePaneState state + |> State.updateBrickState + Event.CloseFocusedPane -> + PaneState.close state.panes + |> State.updatePaneState state + |> State.updateBrickState + Event.ChangeFocusedPane paneId' -> + PaneState.setFocus paneId' state.panes + |> State.updatePaneState state + |> State.updateBrickState + Event.InsertError text -> + Notice.Error text + |> State.appendNotice state + |> State.updateBrickState + Event.InsertWarning text -> + Notice.Warn text + |> State.appendNotice state + |> State.updateBrickState + Event.InsertInfo text -> + Notice.Info text + |> State.appendNotice state + |> State.updateBrickState handleKeys :: Key -> List Modifier -> Brick.EventM Id State () handleKeys key modifiers = do diff --git a/lib/Glyph/View/Data/Rect.hs b/lib/Glyph/View/Data/Rect.hs index e7960e7..e8588d8 100644 --- a/lib/Glyph/View/Data/Rect.hs +++ b/lib/Glyph/View/Data/Rect.hs @@ -2,14 +2,6 @@ module Glyph.View.Data.Rect where ---------------------------------------------------------------------------------------------------- -import GHC.Real ( floor, toRational ) - -import Glyph.State.Pane - ( Arrangement (..) - , Pane (..) - , PaneLayout (..) - ) - ---------------------------------------------------------------------------------------------------- data Rect @@ -20,20 +12,3 @@ data Rect , height :: Int } deriving (Eq, Show) - -layoutRects :: Rect -> PaneLayout -> List (Pane, Rect) -layoutRects upperArea = \case - Single pane' -> - [(pane', upperArea)] - Split SideBySide ratio first second -> - let firstWidth = floor <| (toRational upperArea.width - 1) * ratio - secondWidth = upperArea.width - 1 - firstWidth - secondLeft = upperArea.left + firstWidth - in layoutRects (Rect upperArea.left upperArea.top firstWidth upperArea.height) first - <> layoutRects (Rect secondLeft upperArea.top secondWidth upperArea.height) second - Split OneOverOther ratio first second -> - let firstHeight = floor <| (toRational upperArea.height - 1) * ratio - secondHeight = upperArea.height - 1 - firstHeight - secondTop = upperArea.top + firstHeight - in layoutRects (Rect upperArea.left upperArea.top upperArea.width firstHeight) first - <> layoutRects (Rect upperArea.left secondTop upperArea.width secondHeight) second diff --git a/lib/Glyph/View/Element/Base.hs b/lib/Glyph/View/Element/Base.hs index ba609f9..c2c8fed 100644 --- a/lib/Glyph/View/Element/Base.hs +++ b/lib/Glyph/View/Element/Base.hs @@ -14,7 +14,7 @@ import Glyph.View.Id ( Id ) baseView :: State -> [Brick.Widget Id] baseView state = [ Brick.vBox - [ Pane.pane state + [ Pane.viewPanes state , BB.bottomBar state ] ] diff --git a/lib/Glyph/View/Element/Pane.hs b/lib/Glyph/View/Element/Pane.hs index 0656fd4..fda0d99 100644 --- a/lib/Glyph/View/Element/Pane.hs +++ b/lib/Glyph/View/Element/Pane.hs @@ -3,20 +3,111 @@ module Glyph.View.Element.Pane where ---------------------------------------------------------------------------------------------------- import qualified Brick -import qualified Brick.Widgets.Border as Border -import qualified Brick.Widgets.Center as Center +import qualified Brick.Widgets.Border as Border +import qualified Brick.Widgets.Border.Style as BorderStyle -import Glyph.State ( State (..) ) -import Glyph.View.Id ( Id ) -import qualified Glyph.View.Style as Style +import qualified Data.Text as Text + +import GHC.Real ( floor, toRational ) + +import Glyph.State ( State (..) ) +import Glyph.State.Pane + ( Arrangement (..) + , Buffer (..) + , Pane (..) + , PaneId (..) + , PaneLayout (..) + , PaneState (..) + , PaneType (..) + , TTYId (..) + ) +import Glyph.View.Data.Rect ( Rect (..) ) +import Glyph.View.Element.Pane.Blank ( blankPane ) +import Glyph.View.Element.Pane.Editor ( editorPane ) +import Glyph.View.Element.Pane.Notification ( notificationPane ) +import Glyph.View.Element.Pane.Terminal ( terminalPane ) +import Glyph.View.Id ( Id (..) ) +import qualified Glyph.View.Style as Style + +import qualified System.FilePath as FP ---------------------------------------------------------------------------------------------------- -pane :: State -> Brick.Widget Id -pane state = - Brick.withBorderStyle Style.ruledBorderStyle - <. Border.border - <| Center.center - <| Brick.txt "Hello, World!" +viewPanes :: State -> Brick.Widget Id +viewPanes state = + Brick.joinBorders + <. Brick.withBorderStyle BorderStyle.unicode + <| viewLayout state (state |> panes |> layout) + +viewLayout :: State -> PaneLayout -> Brick.Widget Id +viewLayout state (Single pane') = + viewPaneWithHeader state pane' +viewLayout state (Split SideBySide ratio first second) = + Brick.hBox + [ Brick.hLimitPercent + (max 1 <| min 99 <| floor <| (ratio * 100) - 1) + <| viewLayout state first + , Border.vBorder + , viewLayout state second + ] +viewLayout state (Split OneOverOther ratio first second) = + Brick.vBox + [ Brick.vLimitPercent + (max 1 <| min 99 <| floor <| (ratio * 100)) + <| viewLayout state first + , viewLayout state second + ] + +viewPaneWithHeader :: State -> Pane -> Brick.Widget Id +viewPaneWithHeader state pane' = + let isFocused = + if state.panes.focused == pane'.paneId + then "~" + else " " + id' = + isFocused + <> showAsText pane'.paneId.id + <> isFocused + label text = + Brick.hBox + [ Brick.hLimit 2 Border.hBorder + , Brick.txt <| "[" <> text <> " ]" + , Border.hBorder + ] + header = case pane'.paneType of + Blank -> + label <| id' <> "| Blank" + Editor buffer -> + label <| id' <> "| " <> (Text.pack <| FP.takeFileName buffer.filePath) + Terminal tty -> + label <| id' <> "| TTY-" <> showAsText tty.id + Notifications -> + label <| id' <> "| Notifications" + + viewPane = + Brick.reportExtent (ViewPane pane'.paneId) <| + case pane'.paneType of + Blank -> blankPane state + Editor _ -> editorPane state + Terminal _ -> terminalPane state + Notifications -> notificationPane state + in Brick.vBox [header, viewPane] ---------------------------------------------------------------------------------------------------- + +layoutPanes :: Rect -> PaneLayout -> List (Pane, Rect) +layoutPanes upperArea = \case + Single pane' -> + [(pane', upperArea)] + Split SideBySide ratio first second -> + let firstWidth = floor <| (toRational upperArea.width - 1) * ratio + secondWidth = upperArea.width - 1 - firstWidth + secondLeft = upperArea.left + firstWidth + in layoutPanes (Rect upperArea.left upperArea.top firstWidth upperArea.height) first + <> layoutPanes (Rect secondLeft upperArea.top secondWidth upperArea.height) second + Split OneOverOther ratio first second -> + let firstHeight = floor <| (toRational upperArea.height - 1) * ratio + secondHeight = upperArea.height - 1 - firstHeight + secondTop = upperArea.top + firstHeight + in layoutPanes (Rect upperArea.left upperArea.top upperArea.width firstHeight) first + <> layoutPanes (Rect upperArea.left secondTop upperArea.width secondHeight) second diff --git a/lib/Glyph/View/Element/Pane/Blank.hs b/lib/Glyph/View/Element/Pane/Blank.hs new file mode 100644 index 0000000..6a7986d --- /dev/null +++ b/lib/Glyph/View/Element/Pane/Blank.hs @@ -0,0 +1,18 @@ +module Glyph.View.Element.Pane.Blank where + +---------------------------------------------------------------------------------------------------- + +import qualified Brick +import qualified Brick.Widgets.Border as Border +import qualified Brick.Widgets.Center as Center + +import Glyph.State ( State (..) ) +import Glyph.View.Id ( Id ) +import qualified Glyph.View.Style as Style + +---------------------------------------------------------------------------------------------------- + +blankPane :: State -> Brick.Widget Id +blankPane state = + Center.center <| + Brick.txt "Hello, World!" diff --git a/lib/Glyph/View/Element/Pane/Editor.hs b/lib/Glyph/View/Element/Pane/Editor.hs new file mode 100644 index 0000000..a14b8c9 --- /dev/null +++ b/lib/Glyph/View/Element/Pane/Editor.hs @@ -0,0 +1,17 @@ +module Glyph.View.Element.Pane.Editor where + +---------------------------------------------------------------------------------------------------- + +import qualified Brick +import qualified Brick.Widgets.Center as Center + +import Glyph.State ( State (..) ) +import Glyph.View.Id ( Id ) +import qualified Glyph.View.Style as Style + +---------------------------------------------------------------------------------------------------- + +editorPane :: State -> Brick.Widget Id +editorPane state = + Center.center <| + Brick.txt "Editor" diff --git a/lib/Glyph/View/Element/Pane/Notification.hs b/lib/Glyph/View/Element/Pane/Notification.hs new file mode 100644 index 0000000..362764c --- /dev/null +++ b/lib/Glyph/View/Element/Pane/Notification.hs @@ -0,0 +1,17 @@ +module Glyph.View.Element.Pane.Notification where + +---------------------------------------------------------------------------------------------------- + +import qualified Brick +import qualified Brick.Widgets.Center as Center + +import Glyph.State ( State (..) ) +import Glyph.View.Id ( Id ) +import qualified Glyph.View.Style as Style + +---------------------------------------------------------------------------------------------------- + +notificationPane :: State -> Brick.Widget Id +notificationPane state = + Center.center <| + Brick.txt "Notifications" diff --git a/lib/Glyph/View/Element/Pane/Terminal.hs b/lib/Glyph/View/Element/Pane/Terminal.hs new file mode 100644 index 0000000..225f0fb --- /dev/null +++ b/lib/Glyph/View/Element/Pane/Terminal.hs @@ -0,0 +1,18 @@ +module Glyph.View.Element.Pane.Terminal where + +---------------------------------------------------------------------------------------------------- + +import qualified Brick +import qualified Brick.Widgets.Border as Border +import qualified Brick.Widgets.Center as Center + +import Glyph.State ( State (..) ) +import Glyph.View.Id ( Id ) +import qualified Glyph.View.Style as Style + +---------------------------------------------------------------------------------------------------- + +terminalPane :: State -> Brick.Widget Id +terminalPane state = + Center.center <| + Brick.txt "Terminal" diff --git a/test/GlyphTest/State/Pane.hs b/test/GlyphTest/State/Pane.hs index 8f32b64..896cf92 100644 --- a/test/GlyphTest/State/Pane.hs +++ b/test/GlyphTest/State/Pane.hs @@ -9,7 +9,7 @@ import GHC.Real ( mod ) import Glyph.State.Pane ( Arrangement (..) - , BufferId (..) + , Buffer (..) , Pane (..) , PaneId (..) , PaneLayout (..) @@ -158,12 +158,12 @@ test = nextId (Pane.split SideBySide twoPanes) `shouldBe` PaneId 4 it "keeps the details of the pane being split" <| - let editing = twoPanes{layout = Split SideBySide 0.5 (Single (Pane (PaneId 1) (Editor <| BufferId 1))) (blank 2)} + let editing = twoPanes{layout = Split SideBySide 0.5 (Single (Pane (PaneId 1) (Editor <| Buffer 1 ""))) (blank 2)} in layout (Pane.split OneOverOther editing) `shouldBe` Split SideBySide 0.5 - (Split OneOverOther 0.5 (Single (Pane (PaneId 1) (Editor <| BufferId 1))) (blank 3)) + (Split OneOverOther 0.5 (Single (Pane (PaneId 1) (Editor <| Buffer 1 ""))) (blank 3)) (blank 2) describe "close" <| do @@ -189,8 +189,8 @@ test = in layout (Pane.close nested) `shouldBe` Split OneOverOther 0.25 (blank 1) (blank 3) it "keeps the details of the surviving panes" <| - let editing = twoPanes{layout = Split SideBySide 0.5 (blank 1) (Single (Pane (PaneId 2) (Editor <| BufferId 1)))} - in layout (Pane.close editing) `shouldBe` Single (Pane (PaneId 2) (Editor <| BufferId 1)) + let editing = twoPanes{layout = Split SideBySide 0.5 (blank 1) (Single (Pane (PaneId 2) (Editor <| Buffer 1 "")))} + in layout (Pane.close editing) `shouldBe` Single (Pane (PaneId 2) (Editor <| Buffer 1 "")) it "can close every pane down to one, one at a time" <| let fivePanes = List.foldl' (\s _ -> Pane.split SideBySide s) Pane.init (List.replicate 4 ()) -- 2.54.0