diff --git a/src/client/layout.ini b/src/client/layout.ini index f945832..6f95f8d 100644 --- a/src/client/layout.ini +++ b/src/client/layout.ini @@ -1,26 +1,26 @@ [Window][Sessions [Table View]] Pos=10,43 -Size=1477,281 +Size=1477,357 Collapsed=0 DockId=0x00000003,0 [Window][Listeners] -Pos=10,326 -Size=944,663 +Pos=10,402 +Size=1888,587 Collapsed=0 DockId=0x00000007,0 [Window][Eventlog] Pos=1489,43 -Size=409,281 +Size=409,357 Collapsed=0 DockId=0x00000004,0 [Window][Dear ImGui Demo] -Pos=1489,43 -Size=409,281 +Pos=10,402 +Size=1888,587 Collapsed=0 -DockId=0x00000004,1 +DockId=0x00000007,1 [Window][Dockspace] Pos=0,0 @@ -45,14 +45,14 @@ Size=400,400 Collapsed=0 [Window][[G1H2I3J5] diana@WORKSTATION-04] -Pos=10,326 -Size=944,663 +Pos=10,402 +Size=1888,587 Collapsed=0 DockId=0x00000007,1 [Window][[DEADBEEF] alice@DESKTOP-01] -Pos=10,326 -Size=1888,663 +Pos=10,402 +Size=1888,587 Collapsed=0 DockId=0x00000007,1 @@ -77,6 +77,43 @@ Pos=119,266 Size=1717,576 Collapsed=0 +[Window][Same title as another window##1] +Pos=274,278 +Size=754,103 +Collapsed=1 + +[Window][Same title as another window##2] +Pos=100,200 +Size=754,103 +Collapsed=0 +DockId=0x00000009,1 + +[Window][###AnimatedTitle] +Pos=100,200 +Size=754,103 +Collapsed=0 +DockId=0x00000009,0 + +[Window][Delete?] +Pos=696,412 +Size=516,175 +Collapsed=0 + +[Window][Stacked 1] +Pos=588,335 +Size=669,457 +Collapsed=0 + +[Window][StartListener] +Pos=753,446 +Size=76,76 +Collapsed=0 + +[Window][Start Listener] +Pos=704,387 +Size=500,225 +Collapsed=0 + [Table][0x32886A44,8] Column 0 Weight=0.6465 Column 1 Weight=0.9697 @@ -91,14 +128,21 @@ Column 7 Weight=1.4546 RefScale=27 Column 0 Sort=0v +[Table][0x064A67CC,4] +Column 0 Weight=1.2081 +Column 1 Weight=1.3299 +Column 2 Weight=0.4873 +Column 3 Weight=0.9746 + [Docking][Data] +DockNode ID=0x00000009 Pos=100,200 Size=754,103 Selected=0x64D005CF DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y - DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,281 Split=X + DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,357 Split=X DockNode ID=0x00000003 Parent=0x00000001 SizeRef=613,159 CentralNode=1 Selected=0x61E02D75 - DockNode ID=0x00000004 Parent=0x00000001 SizeRef=409,159 Selected=0x5E5F7166 - DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,663 Split=X Selected=0x8D780333 + DockNode ID=0x00000004 Parent=0x00000001 SizeRef=409,159 Selected=0x0FA43D88 + DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,587 Split=X Selected=0x8D780333 DockNode ID=0x00000005 Parent=0x00000002 SizeRef=944,663 Split=X Selected=0x8D780333 - DockNode ID=0x00000007 Parent=0x00000005 SizeRef=944,663 Selected=0x8D780333 + DockNode ID=0x00000007 Parent=0x00000005 SizeRef=944,663 Selected=0x6BE22050 DockNode ID=0x00000008 Parent=0x00000005 SizeRef=942,663 Selected=0x4AD091E6 DockNode ID=0x00000006 Parent=0x00000002 SizeRef=942,663 Selected=0x65D642C0 diff --git a/src/client/views/listeners.nim b/src/client/views/listeners.nim index d98395d..2520dc4 100644 --- a/src/client/views/listeners.nim +++ b/src/client/views/listeners.nim @@ -1,36 +1,122 @@ -import times +import strutils import imguin/[cimgui, glfw_opengl, simple] import ../utils/appImGui -import ../../common/[types] +import ../../common/[types, utils] +import ./modals/startListener type ListenersTableComponent = ref object of RootObj title: string listeners: seq[Listener] + selection: ptr ImGuiSelectionBasicStorage + startListenerModal: ListenerModalComponent let exampleListeners: seq[Listener] = @[ - Listener( - listenerId: "L1234567", - address: "192.168.1.1", - port: 8080, - protocol: HTTP - ), - Listener( - listenerId: "L7654321", - address: "10.0.0.2", - port: 443, - protocol: HTTP - ) + Listener( + listenerId: "L1234567", + address: "192.168.1.1", + port: 8080, + protocol: HTTP + ), + Listener( + listenerId: "L7654321", + address: "10.0.0.2", + port: 443, + protocol: HTTP + ) ] proc ListenersTable*(title: string): ListenersTableComponent = result = new ListenersTableComponent result.title = title result.listeners = exampleListeners + result.selection = ImGuiSelectionBasicStorage_ImGuiSelectionBasicStorage() + result.startListenerModal = ListenerModal() proc draw*(component: ListenersTableComponent, showComponent: ptr bool) = - igSetNextWindowSize(vec2(800, 600), ImGuiCond_Once.int32) igBegin(component.title, showComponent, 0) defer: igEnd() - igText("Listeners") + let textSpacing = igGetStyle().ItemSpacing.x + + # Listener creation modal + if igButton("Start Listener", vec2(0.0f, 0.0f)): + igOpenPopup_str("Start Listener", ImGui_PopupFlags_None.int32) + + let listener = component.startListenerModal.draw() + if listener != nil: + # TODO: Start listener + component.listeners.add(listener) + + #[ + Listener table + ]# + let tableFlags = ( + ImGuiTableFlags_Resizable.int32 or + ImGuiTableFlags_Reorderable.int32 or + ImGuiTableFlags_Hideable.int32 or + ImGuiTableFlags_HighlightHoveredColumn.int32 or + ImGuiTableFlags_RowBg.int32 or + ImGuiTableFlags_BordersV.int32 or + ImGuiTableFlags_BordersH.int32 or + ImGuiTableFlags_ScrollY.int32 or + ImGuiTableFlags_ScrollX.int32 or + ImGuiTableFlags_NoBordersInBodyUntilResize.int32 or + ImGui_TableFlags_SizingStretchProp.int32 + ) + + let cols: int32 = 4 + if igBeginTable("Listeners", cols, tableFlags, vec2(0.0f, 0.0f), 0.0f): + + igTableSetupColumn("ListenerID", ImGuiTableColumnFlags_NoReorder.int32 or ImGuiTableColumnFlags_NoHide.int32, 0.0f, 0) + igTableSetupColumn("Address", ImGuiTableColumnFlags_None.int32, 0.0f, 0) + igTableSetupColumn("Port", ImGuiTableColumnFlags_None.int32, 0.0f, 0) + igTableSetupColumn("Protocol", ImGuiTableColumnFlags_None.int32, 0.0f, 0) + + igTableSetupScrollFreeze(0, 1) + igTableHeadersRow() + + var multiSelectIO = igBeginMultiSelect(ImGuiMultiSelectFlags_ClearOnEscape.int32 or ImGuiMultiSelectFlags_BoxSelect1d.int32, component.selection[].Size, int32(component.listeners.len())) + ImGuiSelectionBasicStorage_ApplyRequests(component.selection, multiSelectIO) + + for row in 0 ..< component.listeners.len(): + + igTableNextRow(ImGuiTableRowFlags_None.int32, 0.0f) + let listener = component.listeners[row] + + if igTableSetColumnIndex(0): + # Enable multi-select functionality + igSetNextItemSelectionUserData(row) + var isSelected = ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](row)) + discard igSelectable_Bool(listener.listenerId, isSelected, ImGuiSelectableFlags_SpanAllColumns.int32, vec2(0.0f, 0.0f)) + + if igTableSetColumnIndex(1): + igText(listener.address) + if igTableSetColumnIndex(2): + igText($listener.port) + if igTableSetColumnIndex(3): + igText($listener.protocol) + + # Handle right-click context menu + # Right-clicking the table header to hide/show columns or reset the layout is only possible when no sessions are selected + if component.selection[].Size > 0 and igBeginPopupContextWindow("TableContextMenu", ImGui_PopupFlags_MouseButtonRight.int32): + + if igMenuItem("Stop", nil, false, true): + # Update agents table with only non-selected ones + var newListeners: seq[Listener] = @[] + for i in 0 ..< component.listeners.len(): + if not ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)): + newListeners.add(component.listeners[i]) + + # TODO: Stop/kill listener + + component.listeners = newListeners + ImGuiSelectionBasicStorage_Clear(component.selection) + igCloseCurrentPopup() + + igEndPopup() + + multiSelectIO = igEndMultiSelect() + ImGuiSelectionBasicStorage_ApplyRequests(component.selection, multiSelectIO) + + igEndTable() \ No newline at end of file diff --git a/src/client/views/modals/buildAgent.nim b/src/client/views/modals/buildAgent.nim new file mode 100644 index 0000000..e69de29 diff --git a/src/client/views/modals/startListener.nim b/src/client/views/modals/startListener.nim new file mode 100644 index 0000000..b919a1c --- /dev/null +++ b/src/client/views/modals/startListener.nim @@ -0,0 +1,97 @@ +import strutils +import imguin/[cimgui, glfw_opengl, simple] +import ../../utils/appImGui +import ../../../common/[types, utils] + +const DEFAULT_PORT = 8080'u16 + +type + ListenerModalComponent* = ref object of RootObj + address: array[256, char] + port: uint16 + protocol: int32 + protocols: seq[string] + +proc getProtocols(): seq[string] = + for p in Protocol.low .. Protocol.high: + result.add($p) + +proc ListenerModal*(): ListenerModalComponent = + result = new ListenerModalComponent + zeroMem(addr result.address[0], 256) + result.port = DEFAULT_PORT + result.protocol = 0 + result.protocols = getProtocols() + +proc resetModalValues(component: ListenerModalComponent) = + zeroMem(addr component.address[0], 256) + component.port = DEFAULT_PORT + component.protocol = 0 + +proc draw*(component: ListenerModalComponent): Listener = + let textSpacing = igGetStyle().ItemSpacing.x + + # Center modal + let vp = igGetMainViewport() + var center: ImVec2 + ImGuiViewport_GetCenter(addr center, vp) + igSetNextWindowPos(center, ImGuiCond_Appearing.int32, vec2(0.5f, 0.5f)) + + let modalWidth = max(500.0f, vp.Size.x * 0.25) + igSetNextWindowSize(vec2(modalWidth, 0.0f), ImGuiCond_Always.int32) + + var show = true + let windowFlags = ImGuiWindowFlags_None.int32 # or ImGuiWindowFlags_NoMove.int32 + if igBeginPopupModal("Start Listener", addr show, windowFlags): + defer: igEndPopup() + + var availableSize: ImVec2 + igGetContentRegionAvail(addr availableSize) + + # Listener address + igText("Host: ") + igSameLine(0.0f, textSpacing) + igGetContentRegionAvail(addr availableSize) + igSetNextItemWidth(availableSize.x) + igInputTextWithHint("##InputAddress", "127.0.0.1", addr component.address[0], 256, ImGui_InputTextFlags_CharsNoBlank.int32, nil, nil) + + # Listener port + let step: uint16 = 1 + igText("Port: ") + igSameLine(0.0f, textSpacing) + igSetNextItemWidth(availableSize.x) + igInputScalar("##InputPort", ImGuiDataType_U16.int32, addr component.port, addr step, nil, "%hu", ImGui_InputTextFlags_CharsDecimal.int32) + + # Listener protocol dropdown selection + igText("Protocol: ") + igSameLine(0.0f, textSpacing) + igSetNextItemWidth(availableSize.x) + igCombo_Str("##InputProtocol", addr component.protocol, (component.protocols.join("\0") & "\0").cstring , component.protocols.len().int32) + + igGetContentRegionAvail(addr availableSize) + + igDummy(vec2(0.0f, 10.0f)) + igSeparator() + igDummy(vec2(0.0f, 10.0f)) + + # Only enabled the start button when valid values have been entered + igBeginDisabled(($(addr component.address[0]) == "") or (component.port <= 0)) + + if igButton("Start", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)): + + result = Listener( + listenerId: generateUUID(), + address: $(addr component.address[0]), + port: int(component.port), + protocol: cast[Protocol](component.protocol) + ) + component.resetModalValues() + igCloseCurrentPopup() + + igEndDisabled() + igSameLine(0.0f, textSpacing) + + if igButton("Close", vec2(availableSize.x * 0.5 - textSpacing * 0.5, 0.0f)): + + component.resetModalValues() + igCloseCurrentPopup() \ No newline at end of file diff --git a/src/client/views/sessions.nim b/src/client/views/sessions.nim index eb260dd..c57e686 100644 --- a/src/client/views/sessions.nim +++ b/src/client/views/sessions.nim @@ -107,8 +107,6 @@ proc interact(component: SessionsTableComponent) = proc draw*(component: SessionsTableComponent, showComponent: ptr bool) = - igSetNextWindowSize(vec2(800, 600), ImGuiCond_Once.int32) - igBegin(component.title, showComponent, 0) defer: igEnd()