From 11dc255303fae3b54e7f76d2cc42e904ef4e4a3d Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Tue, 3 Mar 2026 21:19:11 +0000 Subject: [PATCH] it is with great sadness the DOM and event bus go. Its all callbacks now --- tui/internal/controllers/controllers.go | 4 - tui/internal/controllers/deviceController.go | 41 +---- tui/internal/controllers/streaming.go | 13 +- tui/internal/dom/dom.go | 119 ------------ tui/internal/dom/uinode.go | 28 --- tui/internal/events/bus.go | 38 ---- tui/internal/events/helper.go | 7 - tui/internal/views/layoutTool.go | 20 -- .../views/layoutTool_WindowManipulation.go | 172 ------------------ tui/internal/views/mainFlex.go | 50 +++-- tui/internal/views/outputWin.go | 10 - tui/internal/views/stream.go | 114 ++++++------ tui/tui.go | 4 - 13 files changed, 82 insertions(+), 538 deletions(-) delete mode 100644 tui/internal/dom/dom.go delete mode 100644 tui/internal/dom/uinode.go delete mode 100644 tui/internal/events/bus.go delete mode 100644 tui/internal/events/helper.go delete mode 100644 tui/internal/views/layoutTool_WindowManipulation.go diff --git a/tui/internal/controllers/controllers.go b/tui/internal/controllers/controllers.go index c5b270f..d9f85fc 100644 --- a/tui/internal/controllers/controllers.go +++ b/tui/internal/controllers/controllers.go @@ -2,8 +2,6 @@ package controllers import ( - "esdi/tui/internal/dom" - "esdi/tui/internal/events" "fmt" "log/slog" @@ -12,9 +10,7 @@ import ( type Controller struct { Logger *slog.Logger - Bus *events.Bus App *tview.Application - Dom *dom.DOM } func ListFormButtonLabels(form *tview.Form) []string { diff --git a/tui/internal/controllers/deviceController.go b/tui/internal/controllers/deviceController.go index d364724..7033426 100644 --- a/tui/internal/controllers/deviceController.go +++ b/tui/internal/controllers/deviceController.go @@ -25,45 +25,6 @@ func NewDeviceController(base *Controller, devService *serv.CDashService) *Devic StreamStrl: NewStreamingCtrl(), } - // mc.Bus.On(ui.DestroyWindowEv{}, func(e any) { - // mc.Logger.Info(fmt.Sprintf("Called in to destroy win: %d", e.(ui.DestroyWindowEv).ID)) - // mc.CDash.DestroyWindow(e.(ui.DestroyWindowEv).ID) - // - // mc.Bus.Emit(ui.WindowDestroyedEv{ID: e.(ui.DestroyWindowEv).ID}) - // }) - - // mc.Bus.On(ui.MoveWindowEv{}, func(e any) { - // mvData := e.(ui.MoveWindowEv) - // mc.Logger.Debug(fmt.Sprintf("request to move window '%d'", mvData.WindowID)) - // - // newDims, err := mc.CDash.MoveWindow(mvData.WindowID, mvData.Delta) - // if err != nil && err != io.EOF { - // mc.Logger.Debug(fmt.Sprintf("failed to move window '%d' %s", mvData.WindowID, err.Error())) - // return - // } - // - // mc.Bus.Emit(ui.WindowMovedEv{ID: mvData.WindowID, Dims: newDims}) - // }) - - // mc.Bus.On(ui.ResizeWindowEv{}, func(e any) { - // mvData := e.(ui.ResizeWindowEv) - // mc.Logger.Debug(fmt.Sprintf("requested to resize window '%d'", mvData.WindowID)) - // err := mc.CDash.ResizeWindow(mvData.WindowID, mvData.Delta) - // if err != nil && err != io.EOF { - // mc.Logger.Debug(fmt.Sprintf("failed to resize window '%d' %s", mvData.WindowID, err.Error())) - // return - // } - // }) - - // mc.Bus.On(ui.SaveLayoutEv{}, func(e any) { - // mc.CDash.SaveLayout() - // }) - - // mc.Bus.On(ui.ForceRedraw{}, func(e any) { - // // mc.App.QueueUpdateDraw(func() {}) - // mc.App.Draw() - // }) - // mc.Bus.On(ui.StartStreamingReqEv{}, func(e any) { // mc.StreamStrl.Start(mc.Bus) // }) @@ -147,7 +108,7 @@ func (mc *DeviceController) mainUI() error { var err error // Set the main view - mc.DeviceAPIView, err = views.NewDeviceAPIView(mc.Dom) + mc.DeviceAPIView, err = views.NewDeviceAPIView() if err != nil { return err } diff --git a/tui/internal/controllers/streaming.go b/tui/internal/controllers/streaming.go index 424947e..4d9c00f 100644 --- a/tui/internal/controllers/streaming.go +++ b/tui/internal/controllers/streaming.go @@ -9,9 +9,6 @@ import ( "sync" "time" - "esdi/tui/internal/events" - "esdi/tui/internal/ui" - "github.com/ESilva15/goirsdk" ) @@ -42,10 +39,10 @@ func NewStreamingCtrl() *StreamingCtrl { } } -func (sc *StreamingCtrl) Stop(bus *events.Bus) { -} +// func (sc *StreamingCtrl) Stop(bus *events.Bus) { +// } -func (sc *StreamingCtrl) Start(bus *events.Bus) { +func (sc *StreamingCtrl) Start() { // Open the telemetry file file, err := os.Open("/home/esilva/Desktop/projetos/simracing_peripherals/testTelemetry/supercars_indianapolis.ibt") if err != nil { @@ -73,10 +70,10 @@ func (sc *StreamingCtrl) Start(bus *events.Bus) { delta := currentTime.Sub(startTime) - str := sc.Stringified() + fmt.Sprintf("%v - %7d\n%5f - %5f", + _ = sc.Stringified() + fmt.Sprintf("%v - %7d\n%5f - %5f", t.UTC(), counter, delta.Seconds(), delta.Seconds()/float64(counter)) - bus.Emit(ui.StreamDataEv{Str: str}) + // bus.Emit(ui.StreamDataEv{Str: str}) counter++ } }() diff --git a/tui/internal/dom/dom.go b/tui/internal/dom/dom.go deleted file mode 100644 index ffdff7b..0000000 --- a/tui/internal/dom/dom.go +++ /dev/null @@ -1,119 +0,0 @@ -// Package dom will be used to handle the multiple elements of an UI -package dom - -import ( - "fmt" - "io" - - "github.com/rivo/tview" -) - -// ErrElementAlreadyRegistered defines the error scenario of the user trying -// to register an element to the DOM with an already registered ID -type ErrElementAlreadyRegistered struct{} - -func (e *ErrElementAlreadyRegistered) Error() string { - return "an element with that ID has already been registered in the DOM" -} - -type DOM struct { - Root *UINode - elements map[string]*UINode -} - -// NewDOM constructs a new DOM -func NewDOM() *DOM { - return &DOM{ - Root: nil, - elements: make(map[string]*UINode), - } -} - -// Debugging -func (d *DOM) ListRegisteredUINodes(io io.Writer) { - fmt.Printf("Registered Nodes:\n") - for key := range d.elements { - fmt.Fprintf(io, " %s\n", key) - } -} - -// Private -func (d *DOM) idIsRegistered(ID string) bool { - _, ok := d.elements[ID] - return ok -} - -// registerElem register a new element to the DOM -// should verify that the ID doesn't yet exist and thats it so far -func (d *DOM) registerElem(elem *UINode) error { - if d.idIsRegistered(elem.ID) { - return &ErrElementAlreadyRegistered{} - } - - d.elements[elem.ID] = elem - return nil -} - -// Public - -// SetRoot defines the root element for the DOM -func (d *DOM) SetRoot(r *UINode) *DOM { - d.Root = r - return d -} - -// NewUINode creates and registers a UI Node -func (d *DOM) NewUINode(ID string, parent, root tview.Primitive) (*UINode, error) { - newNode := &UINode{ - ID: ID, - Self: root, - Children: make(map[string]*UINode), - } - - err := d.registerElem(newNode) - if err != nil { - return nil, err - } - - return newNode, nil -} - -// GetRoot fetches the root UINode of the DOM -func (d *DOM) GetRoot(r *UINode) *UINode { - return d.Root -} - -// GetRootElem fetches the root primitive of the dom -func (d *DOM) GetRootElem() tview.Primitive { - return d.Root.Self -} - -// GetElemByID returns the primitive of a UINode given its ID -// returns nil if the Node isn't registered -func (d *DOM) GetElemByID(ID string) tview.Primitive { - if d.idIsRegistered(ID) { - return d.elements[ID].Self - } - - return nil -} - -// GetNodeByID returns a UINode given its ID -// returns nil if the Node isn't registered -func (d *DOM) GetNodeByID(ID string) *UINode { - if d.idIsRegistered(ID) { - return d.elements[ID] - } - - return nil -} - -// AppendElem adds a new UINode to a given UINode -func (d *DOM) AppendElem(root *UINode, elem *UINode) error { - return root.AppendItem(elem) -} - -// DeleteElem deletes and element from the DOM -func (d *DOM) DeleteElem(elem *UINode) { - delete(d.elements, elem.ID) -} diff --git a/tui/internal/dom/uinode.go b/tui/internal/dom/uinode.go deleted file mode 100644 index c7bc6f4..0000000 --- a/tui/internal/dom/uinode.go +++ /dev/null @@ -1,28 +0,0 @@ -// Package dom -package dom - -import ( - "github.com/rivo/tview" -) - -type UINode struct { - ID string - Self tview.Primitive - Parent tview.Primitive - Children map[string]*UINode -} - -func newUINode(ID string, parent, root tview.Primitive) *UINode { - return &UINode{ - ID: ID, - Self: root, - Children: make(map[string]*UINode), - } -} - -func (l *UINode) AppendItem(item *UINode) error { - // Its not necessary to verify if this ID is already a child of this Node - // because we do that at the UI level - l.Children[item.ID] = item - return nil -} diff --git a/tui/internal/events/bus.go b/tui/internal/events/bus.go deleted file mode 100644 index c7118cf..0000000 --- a/tui/internal/events/bus.go +++ /dev/null @@ -1,38 +0,0 @@ -// Package events defines a simple event handler system -package events - -import "sync" - -type Handler func(event any) - -type Bus struct { - mu sync.RWMutex - handlers map[string][]Handler -} - -func NewBus() *Bus { - return &Bus{ - handlers: make(map[string][]Handler), - } -} - -func (b *Bus) On(eventType any, h Handler) { - key := typeKey(eventType) - - b.mu.Lock() - defer b.mu.Unlock() - - b.handlers[key] = append(b.handlers[key], h) -} - -func (b *Bus) Emit(event any) { - key := typeKey(event) - - b.mu.RLock() - handlers := append([]Handler{}, b.handlers[key]...) - b.mu.RUnlock() - - for _, h := range handlers { - h(event) - } -} diff --git a/tui/internal/events/helper.go b/tui/internal/events/helper.go deleted file mode 100644 index da54aa5..0000000 --- a/tui/internal/events/helper.go +++ /dev/null @@ -1,7 +0,0 @@ -package events - -import "fmt" - -func typeKey(v any) string { - return fmt.Sprintf("%T", v) -} diff --git a/tui/internal/views/layoutTool.go b/tui/internal/views/layoutTool.go index b665cfa..3378a72 100644 --- a/tui/internal/views/layoutTool.go +++ b/tui/internal/views/layoutTool.go @@ -15,22 +15,6 @@ const ( LayoutToolNewWindowID = "new-window-action-form" ) -// type windowReference struct { -// ID int16 -// Form *dom.UINode -// } - -// func getWindowRefFromNode(node *tview.TreeNode) *windowReference { -// ref := node.GetReference() -// wRef, ok := ref.(*windowReference) -// -// if !ok { -// return nil -// } -// -// return wRef -// } - func FindNodeByID(node *tview.TreeNode, id int16) *tview.TreeNode { if node == nil { return nil @@ -54,15 +38,11 @@ func FindNodeByID(node *tview.TreeNode, id int16) *tview.TreeNode { type LayoutTreeView struct { Tree *tview.TreeView InputCapture func(*tcell.EventKey) *tcell.EventKey - // OnChange func(node *tview.TreeNode) - // OnSelect func(node *tview.TreeNode) } func NewLayoutTreeView() *LayoutTreeView { view := &LayoutTreeView{ InputCapture: blankInputCapture, - // OnChange: blankTreeViewOnChange, - // OnSelect: blankTreeViewOnChange, } view.Tree = tview.NewTreeView() diff --git a/tui/internal/views/layoutTool_WindowManipulation.go b/tui/internal/views/layoutTool_WindowManipulation.go deleted file mode 100644 index 0e524e2..0000000 --- a/tui/internal/views/layoutTool_WindowManipulation.go +++ /dev/null @@ -1,172 +0,0 @@ -package views - -import ( - "fmt" - - helper "esdi/helpers" - "esdi/tui/internal/dom" - "esdi/tui/internal/events" - "esdi/tui/internal/ui" - - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" -) - -type ManipMode int - -const ( - moveMode ManipMode = iota - resizeMode -) - -type windowManipState struct { - Mode ManipMode -} - -func keyToVector(r rune) (helper.Vector, bool) { - var mult uint16 = 1 - - // Is uppercase? For fast movement - if r >= 'A' && r <= 'Z' { - mult = 10 - r += 'a' - 'A' - } - - switch r { - case 'h': - return helper.Vector{DX: -mult, DY: 0}, true - case 'j': - return helper.Vector{DX: 0, DY: mult}, true - case 'k': - return helper.Vector{DX: 0, DY: -mult}, true - case 'l': - return helper.Vector{DX: mult, DY: 0}, true - } - - return helper.Vector{}, false -} - -func handleMovementCapture(bus *events.Bus, id int16, ev *tcell.EventKey) *tcell.EventKey { - vec, ok := keyToVector(ev.Rune()) - if !ok { - return ev - } - - bus.Emit(ui.MoveWindowEv{ - WindowID: id, - Delta: vec, - }) - - return ev -} - -func handleResizeCapture(bus *events.Bus, id int16, ev *tcell.EventKey) *tcell.EventKey { - vec, ok := keyToVector(ev.Rune()) - if !ok { - return ev - } - - bus.Emit(ui.ResizeWindowEv{ - WindowID: id, - Delta: vec, - }) - - return ev -} - -type modeHandler func(*tcell.EventKey) *tcell.EventKey - -func (s *windowManipState) CurrentHandler( - bus *events.Bus, - id int16, -) modeHandler { - switch s.Mode { - case moveMode: - return func(ev *tcell.EventKey) *tcell.EventKey { - return handleMovementCapture(bus, id, ev) - } - case resizeMode: - return func(ev *tcell.EventKey) *tcell.EventKey { - return handleResizeCapture(bus, id, ev) - } - } - - return nil -} - -func windowManipulationEvCapture( - bus *events.Bus, - doc *dom.DOM, - id int16, - state *windowManipState, -) func(ev *tcell.EventKey) *tcell.EventKey { - return func(ev *tcell.EventKey) *tcell.EventKey { - switch ev.Key() { - case tcell.KeyEscape: - bus.Emit(ui.ChangeFocusEv{ - Target: doc.GetElemByID(LayoutToolFlexID), - }) - return ev - } - - bus.Emit(ui.PrintLogEv{ - Log: fmt.Sprintf("-> IDX: %d\n", id), - }) - - // Mode switching - switch ev.Rune() { - case 'r': - state.Mode = resizeMode - bus.Emit(ui.PrintLogEv{Log: "switched to resizeMode"}) - case 'm': - state.Mode = moveMode - bus.Emit(ui.PrintLogEv{Log: "switched to moveMode"}) - } - - // Delegate the current handler input - handler := state.CurrentHandler(bus, id) - if handler != nil { - return handler(ev) - } - - return ev - } -} - -func windowManipulationTool(bus *events.Bus, doc *dom.DOM, idx int16) { - bus.Emit(ui.PrintLogEv{Log: fmt.Sprintf("WID: %d\n", idx)}) - - // Get the ActionPages parent - box := tview.NewBox().SetTitle("MOVE WINDOW TOOL") - box.SetBorder(true) - - var err error - var boxNode *dom.UINode - - // delete the currently existing move-window-box - actionPages := doc.GetElemByID(LayoutToolActionPagesID).(*tview.Pages) - actionPages.RemovePage("move-window-box") - - boxNode = doc.GetNodeByID("move-window-box") - if boxNode != nil { - doc.DeleteElem(boxNode) - } - - boxNode, err = doc.NewUINode( - "move-window-box", - doc.GetElemByID(LayoutToolActionPagesID), - box, - ) - - if err != nil { - return - } - - // Create the state for this tool - state := windowManipState{ - Mode: moveMode, - } - - box.SetInputCapture(windowManipulationEvCapture(bus, doc, idx, &state)) - // AddAndShowPage(actionPages, boxNode, true) -} diff --git a/tui/internal/views/mainFlex.go b/tui/internal/views/mainFlex.go index 118e191..d2115a4 100644 --- a/tui/internal/views/mainFlex.go +++ b/tui/internal/views/mainFlex.go @@ -4,8 +4,6 @@ package views import ( "fmt" - "esdi/tui/internal/dom" - "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -76,48 +74,48 @@ func (dl *DeviceAPIListView) AddItem(name, description string, onSelect func()) dl.List.AddItem(name, description, 0, onSelect) } -func NewDeviceAPIView(doc *dom.DOM) (*DeviceAPIView, error) { +func NewDeviceAPIView() (*DeviceAPIView, error) { // To build the main view we must set the DOM root mainFlex := tview.NewFlex().SetDirection(tview.FlexColumn) - mainFlexUINode, err := doc.NewUINode(MainFlexID, nil, mainFlex) - if err != nil { - return nil, err - } - - doc.SetRoot(mainFlexUINode) + // mainFlexUINode, err := doc.NewUINode(MainFlexID, nil, mainFlex) + // if err != nil { + // return nil, err + // } + // + // doc.SetRoot(mainFlexUINode) deviceAPIList := NewDeviceAPIListView() - apiListWindowUINode, err := doc.NewUINode(DeviceAPIListID, doc.GetRootElem(), - deviceAPIList.List) - if err != nil { - return nil, err - } + // apiListWindowUINode, err := doc.NewUINode(DeviceAPIListID, doc.GetRootElem(), + // deviceAPIList.List) + // if err != nil { + // return nil, err + // } apiToolPages := NewDeviceAPIToolView() - apiToolPagesNode, err := doc.NewUINode(APIToolPagesID, doc.GetElemByID(RightFlexID), - apiToolPages.Pages) - if err != nil { - return nil, err - } + // apiToolPagesNode, err := doc.NewUINode(APIToolPagesID, doc.GetElemByID(RightFlexID), + // apiToolPages.Pages) + // if err != nil { + // return nil, err + // } mainFlex. - AddItem(apiListWindowUINode.Self, 0, 1, false). - AddItem(apiToolPagesNode.Self, 0, 4, false) + AddItem(deviceAPIList.List, 0, 1, false). + AddItem(apiToolPages.Pages, 0, 4, false) // Output window outputWin := NewOutputWinView() - _, err = doc.NewUINode("output-window", nil, outputWin.TextArea) - if err != nil { - return nil, err - } + // _, err = doc.NewUINode("output-window", nil, outputWin.TextArea) + // if err != nil { + // return nil, err + // } // Flex with debug window // -------------------------------------------------------------------------- flexWithOutputWindow := tview.NewFlex().SetDirection(tview.FlexRow) flexWithOutputWindow. AddItem(mainFlex, 0, 5, true). - AddItem(doc.GetElemByID(OutputPaneID), 0, 2, false) + AddItem(outputWin.TextArea, 0, 2, false) // -------------------------------------------------------------------------- return &DeviceAPIView{ diff --git a/tui/internal/views/outputWin.go b/tui/internal/views/outputWin.go index 04f37e4..6e8eaf2 100644 --- a/tui/internal/views/outputWin.go +++ b/tui/internal/views/outputWin.go @@ -4,16 +4,6 @@ import ( "github.com/rivo/tview" ) -// func bindOutputWindowEvents( -// bus *events.Bus, -// output *tview.TextView, -// ) { -// bus.On(ui.PrintLogEv{}, func(e any) { -// le, _ := e.(ui.PrintLogEv) -// fmt.Fprintf(output, le.Log) -// }) -// } - type OutputWinView struct { TextArea *tview.TextView } diff --git a/tui/internal/views/stream.go b/tui/internal/views/stream.go index 669335c..04ab348 100644 --- a/tui/internal/views/stream.go +++ b/tui/internal/views/stream.go @@ -1,15 +1,5 @@ package views -import ( - "esdi/tui/internal/dom" - "esdi/tui/internal/events" - "esdi/tui/internal/ui" - "os" - - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" -) - // Car data lengths const ( SpeedLen = 5 @@ -22,56 +12,56 @@ const ( streamingBoxID = "streaming-box" ) -func streamingWindow(bus *events.Bus, doc *dom.DOM) { - bus.Emit(ui.LogEv{Log: "creating streaming window"}) - - // Get the ActionPages parent - box := tview.NewTextView() - box.SetBorder(true).SetTitle("STREAMING") - box.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { - switch ev.Key() { - case tcell.KeyEsc: - // well, I really need to design the UI (as if lmao) better - os.Exit(0) - } - - return ev - }) - - var err error - var boxNode *dom.UINode - - // delete the currently existing streaming-box - actionPages := doc.GetElemByID(LayoutToolActionPagesID).(*tview.Pages) - actionPages.RemovePage(streamingBoxID) - - // Get the currently exisiting streaming box in the dom so we can delete it - boxNode = doc.GetNodeByID(streamingBoxID) - if boxNode != nil { - doc.DeleteElem(boxNode) - } - - // Register this streaming box as an UINode - boxNode, err = doc.NewUINode( - streamingBoxID, - doc.GetElemByID(LayoutToolActionPagesID), - box, - ) - if err != nil { - return - } - - // Add it to the action pages and view it - // AddAndShowPage(actionPages, boxNode, true) - - bus.Emit(ui.StartStreamingReqEv{}) - - bus.On(ui.StreamDataEv{}, func(e any) { - bus.Emit(ui.RedrawEv{ - Fn: func() { - sd := e.(ui.StreamDataEv) - box.SetText(sd.Str) - }, - }) - }) +func streamingWindow() { + // bus.Emit(ui.LogEv{Log: "creating streaming window"}) + // + // // Get the ActionPages parent + // box := tview.NewTextView() + // box.SetBorder(true).SetTitle("STREAMING") + // box.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { + // switch ev.Key() { + // case tcell.KeyEsc: + // // well, I really need to design the UI (as if lmao) better + // os.Exit(0) + // } + // + // return ev + // }) + // + // var err error + // var boxNode *dom.UINode + // + // // delete the currently existing streaming-box + // actionPages := doc.GetElemByID(LayoutToolActionPagesID).(*tview.Pages) + // actionPages.RemovePage(streamingBoxID) + // + // // Get the currently exisiting streaming box in the dom so we can delete it + // boxNode = doc.GetNodeByID(streamingBoxID) + // if boxNode != nil { + // doc.DeleteElem(boxNode) + // } + // + // // Register this streaming box as an UINode + // boxNode, err = doc.NewUINode( + // streamingBoxID, + // doc.GetElemByID(LayoutToolActionPagesID), + // box, + // ) + // if err != nil { + // return + // } + // + // // Add it to the action pages and view it + // // AddAndShowPage(actionPages, boxNode, true) + // + // bus.Emit(ui.StartStreamingReqEv{}) + // + // bus.On(ui.StreamDataEv{}, func(e any) { + // bus.Emit(ui.RedrawEv{ + // Fn: func() { + // sd := e.(ui.StreamDataEv) + // box.SetText(sd.Str) + // }, + // }) + // }) } diff --git a/tui/tui.go b/tui/tui.go index 4efb5d4..399ca2d 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -3,8 +3,6 @@ package tui import ( "esdi/tui/internal/controllers" - "esdi/tui/internal/dom" - "esdi/tui/internal/events" "esdi/tui/internal/services" "log/slog" @@ -20,9 +18,7 @@ type ControlPanel struct { func NewControlPanel(logger *slog.Logger) *ControlPanel { baseController := &controllers.Controller{ Logger: logger, - Bus: events.NewBus(), App: tview.NewApplication(), - Dom: dom.NewDOM(), } deviceService := services.NewCDashService(logger)