Implemented console items window using ImGuiTextSelect after it was implemented into imguin.

This commit is contained in:
Jakob Friedl
2025-09-14 22:55:44 +02:00
parent c6bbef8520
commit ce417db941
4 changed files with 114 additions and 115 deletions

View File

@@ -28,5 +28,5 @@ requires "tiny_sqlite >= 0.2.0"
requires "prologue >= 0.6.6" requires "prologue >= 0.6.6"
requires "winim >= 3.9.4" requires "winim >= 3.9.4"
requires "ptr_math >= 0.3.0" requires "ptr_math >= 0.3.0"
requires "imguin >= 1.92.2.0" requires "imguin >= 1.92.2.1"
requires "zippy >= 0.10.16" requires "zippy >= 0.10.16"

View File

@@ -1,6 +1,6 @@
switch "o", "bin/client" switch "o", "bin/client"
switch "d", "ImColorTextEdit" switch "d", "ImGuiTextSelect"
# Select compiler # Select compiler
var TC = "gcc" var TC = "gcc"

View File

@@ -1,41 +1,41 @@
[Window][Sessions [Table View]] [Window][Sessions [Table View]]
Pos=10,43 Pos=10,43
Size=1477,421 Size=2117,381
Collapsed=0 Collapsed=0
DockId=0x00000003,0 DockId=0x00000003,0
[Window][Listeners] [Window][Listeners]
Pos=10,466 Pos=10,426
Size=1888,523 Size=2528,971
Collapsed=0 Collapsed=0
DockId=0x00000002,0 DockId=0x00000002,0
[Window][Eventlog] [Window][Eventlog]
Pos=1489,43 Pos=2129,43
Size=409,421 Size=409,381
Collapsed=0 Collapsed=0
DockId=0x00000004,0 DockId=0x00000004,0
[Window][Dear ImGui Demo] [Window][Dear ImGui Demo]
Pos=1489,43 Pos=2129,43
Size=409,421 Size=409,381
Collapsed=0 Collapsed=0
DockId=0x00000004,1 DockId=0x00000004,1
[Window][Dockspace] [Window][Dockspace]
Pos=0,0 Pos=0,0
Size=1908,999 Size=2548,1407
Collapsed=0 Collapsed=0
[Window][[FACEDEAD] bob@LAPTOP-02] [Window][[FACEDEAD] bob@LAPTOP-02]
Pos=10,466 Pos=10,426
Size=1888,523 Size=2528,971
Collapsed=0 Collapsed=0
DockId=0x00000002,3 DockId=0x00000002,3
[Window][[C9D8E7F6] charlie@SERVER-03] [Window][[C9D8E7F6] charlie@SERVER-03]
Pos=10,466 Pos=10,426
Size=1888,523 Size=2528,971
Collapsed=0 Collapsed=0
DockId=0x00000002,2 DockId=0x00000002,2
@@ -45,8 +45,8 @@ Size=400,400
Collapsed=0 Collapsed=0
[Window][[G1H2I3J5] diana@WORKSTATION-04] [Window][[G1H2I3J5] diana@WORKSTATION-04]
Pos=10,466 Pos=10,426
Size=1888,523 Size=2528,971
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x00000002,1
@@ -78,23 +78,23 @@ Size=1717,576
Collapsed=0 Collapsed=0
[Table][0x32886A44,8] [Table][0x32886A44,8]
Column 0 Weight=0.6522 Column 0 Weight=0.6513
Column 1 Weight=0.9755 Column 1 Weight=0.9753
Column 2 Weight=0.5541 Column 2 Weight=0.5524
Column 3 Weight=1.0620 Column 3 Weight=1.0605
Column 4 Weight=1.7316 Column 4 Weight=1.7323
Column 5 Weight=1.1486 Column 5 Weight=1.1492
Column 6 Weight=0.3290 Column 6 Weight=0.4263
Column 7 Weight=1.5469 Column 7 Weight=1.4527
[Table][0xB6880529,2] [Table][0xB6880529,2]
RefScale=27 RefScale=27
Column 0 Sort=0v Column 0 Sort=0v
[Docking][Data] [Docking][Data]
DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=1888,946 Split=Y DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=2528,1354 Split=Y
DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,421 Split=X DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,381 Split=X
DockNode ID=0x00000003 Parent=0x00000001 SizeRef=613,159 CentralNode=1 Selected=0x61E02D75 DockNode ID=0x00000003 Parent=0x00000001 SizeRef=613,159 CentralNode=1 Selected=0x61E02D75
DockNode ID=0x00000004 Parent=0x00000001 SizeRef=409,159 Selected=0x5E5F7166 DockNode ID=0x00000004 Parent=0x00000001 SizeRef=409,159 Selected=0x5E5F7166
DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,523 Selected=0x8D780333 DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,971 Selected=0x8D780333

View File

@@ -1,118 +1,117 @@
import strformat, strutils import strformat, strutils, times
import imguin/[cimgui, glfw_opengl, simple] import imguin/[cimgui, glfw_opengl, simple]
import ../utils/appImGui import ../utils/appImGui
import ../../common/[types] import ../../common/[types]
type type
ConsoleItem = ref object
timestamp: DateTime
logType: LogType
text: string
ConsoleItems = ref object
items: seq[string]
ConsoleComponent* = ref object of RootObj ConsoleComponent* = ref object of RootObj
agent: Agent agent: Agent
showConsole*: bool showConsole*: bool
inputBuffer: string inputBuffer: string
consoleEntries: seq[string] consoleItems: ConsoleItems
console: ptr TextEditor textSelect: ptr TextSelect
proc Console*(agent: Agent): ConsoleComponent = proc getNumLines(data: pointer): csize_t {.cdecl.} =
if data.isNil:
return 0
let consoleItems = cast[ConsoleItems](data)
return consoleItems.items.len().csize_t
proc getLineAtIndex(i: csize_t, data: pointer, outLen: ptr csize_t): cstring {.cdecl.} =
if data.isNil:
return nil
let consoleItems = cast[ConsoleItems](data)
let line = consoleItems.items[i].cstring
if not outLen.isNil:
outLen[] = line.len.csize_t
return line
proc Console*(agent: Agent): ConsoleComponent =
result = new ConsoleComponent result = new ConsoleComponent
result.agent = agent result.agent = agent
result.showConsole = true result.showConsole = true
result.console = TextEditor_TextEditor() result.inputBuffer = ""
result.consoleEntries = @[
"a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a","a",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
]
result.console.TextEditor_SetText(result.consoleEntries.join("\n") & '\0') result.consoleItems = new ConsoleItems
result.consoleItems.items = @[]
proc findLongestLength(text: string): float32 = result.textSelect = textselect_create(getLineAtIndex, getNumLines, cast[pointer](result.consoleItems), 0)
var maxWidth = 0.0f
for line in text.splitLines():
let line_cstring = line.cstring
var textSizeOut: ImVec2
igCalcTextSize(addr textSizeOut, line_cstring, nil, false, -1.0f)
if textSizeOut.x > maxWidth:
maxWidth = textSizeOut.x
return maxWidth
proc draw*(component: ConsoleComponent) =
proc draw*(component: ConsoleComponent) =
igBegin(fmt"[{component.agent.agentId}] {component.agent.username}@{component.agent.hostname}", addr component.showConsole, 0) igBegin(fmt"[{component.agent.agentId}] {component.agent.username}@{component.agent.hostname}", addr component.showConsole, 0)
defer: igEnd() defer: igEnd()
#[
Console entries/text section
Problems:
# A InputTextMultiline component is placed within a Child Frame to enable both proper text selection and a horizontal scrollbar
# The only thing missing from this implementation is the ability change the text color and auto-scrolling
# https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129
# https://github.com/ocornut/imgui/issues/950
]#
let footerHeight = igGetStyle().ItemSpacing.y + igGetFrameHeightWithSpacing() # * 2
let buffer = component.consoleEntries.join("\n") & '\0'
# Push styles to hide the Child's background and scrollbar background. #[
igPushStyleColor_Vec4(ImGuiCol_FrameBg.int32, vec4(0.0f, 0.0f, 0.0f, 0.0f)) Console items/text section using ImGuiTextSelect in a child window
igPushStyleColor_Vec4(ImGuiCol_ScrollbarBg.int32, vec4(0.0f, 0.0f, 0.0f, 0.0f)) Supports:
- horizontal+vertical scrolling,
- autoscroll
- colored text
- text selection and copy functionality
if igBeginChild_Str("##Console", vec2(-0.99f, -footerHeight), ImGuiChildFlags_NavFlattened.int32, ImGuiWindowFlags_HorizontalScrollbar.int32): Problems I encountered with other approaches (Multi-line Text Input, TextEditor, ...):
- https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129
# Manually handle horizontal scrolling with the mouse wheel/touchpad - https://github.com/ocornut/imgui/issues/950
let io = igGetIO() Huge thanks to @dinau for implementing ImGuiTextSelect into imguin very rapidly after I requested it.
if io.MouseWheelH != 0: ]#
let scroll_delta = io.MouseWheelH * igGetScrollX() * 0.5 let footerHeight = igGetStyle().ItemSpacing.y + igGetFrameHeightWithSpacing() * 2
igSetScrollX_Float(igGetScrollX() - scroll_delta)
if igGetScrollX() == 0: if igBeginChild_Str("##Console", vec2(-1.0f, -footerHeight), ImGuiChildFlags_NavFlattened.int32, ImGuiWindowFlags_HorizontalScrollbar.int32):
igSetScrollX_Float(1.0f) # This is required to prevent the horizontal scrolling from snapping in
# Retrieve the length of the longes console entry
var width = findLongestLength(buffer)
if width <= io.DisplaySize.x:
width = -1.0f
# Set the Text edit background color and make it visible.
igPushStyleColor_Vec4(ImGuiCol_FrameBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
igPushStyleColor_Vec4(ImGuiCol_ScrollbarBg.int32, vec4(0.1f, 0.1f, 0.1f, 1.0f))
discard igInputTextMultiline("##ConsoleText", buffer, cast[csize_t](buffer.len()), vec2(width, -1.0f), ImGui_InputTextFlags_ReadOnly.int32 or ImGui_InputTextFlags_AllowTabInput.int32, nil, nil)
# Alternative: ImGuiColorTextEdit # Display console items
# component.console.TextEditor_SetReadOnlyEnabled(true) for entry in component.consoleItems.items:
# component.console.TextEditor_SetShowLineNumbersEnabled(false) igTextColored(vec4(0.0f, 1.0f, 1.0f, 1.0f), entry.cstring)
# component.console.TextEditor_Render("##ConsoleEntries", false, vec2(-1, -1), true)
# # Scroll to bottom component.textSelect.textselect_update()
# if igGetScrollY() >= igGetScrollMaxY():
# let lineCount = component.console.TextEditor_GetLineCount() # Auto-scroll to bottom if we're already at the bottom
# component.console.TextEditor_SetCursorPosition(lineCount, 0) if igGetScrollY() >= igGetScrollMaxY():
igSetScrollHereY(1.0f)
igPopStyleColor(2)
igPopStyleColor(2)
igEndChild() igEndChild()
# Buttons for testing the console
if igButton("Add Items", vec2(0.0f, 0.0f)):
for i in 1..10:
component.consoleItems.items.add("Hello world!")
igSameLine(0.0f, 5.0f)
if igButton("Add Long Items", vec2(0.0f, 0.0f)):
for i in 1..3:
component.consoleItems.items.add("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.")
igSameLine(0.0f, 5.0f)
if igButton("Clear", vec2(0.0f, 0.0f)):
component.consoleItems.items.setLen(0)
#[ #[
Input field with prompt indicator Input field with prompt indicator
]# ]#
let promptIndicator = fmt"[{component.agent.agentId}]" igText(fmt"[{component.agent.agentId}]")
var charWidth: ImVec2 let spacing = igGetStyle().ItemSpacing.x
igCalcTextSize(addr charWidth, "A", nil, false, -1.0f) igSameLine(0.0f, spacing)
let promptWidth = charWidth.x * float(promptIndicator.len())
let spacing = igGetStyle().ItemSpacing.x # Calculate available width for input
igTextColored(vec4(1.0f, 1.0f, 1.0f, 1.0f), promptIndicator)
igSameLine(0.0f, spacing)
var availableWidth: ImVec2 var availableWidth: ImVec2
igGetContentRegionAvail(addr availableWidth) igGetContentRegionAvail(addr availableWidth)
igSetNextItemWidth(availableWidth.x) igSetNextItemWidth(availableWidth.x)
let inputFlags = ImGuiInputTextFlags_EnterReturnsTrue.int32 or ImGuiInputTextFlags_EscapeClearsAll.int32 or ImGuiInputTextFlags_CallbackCompletion.int32 or ImGuiInputTextFlags_CallbackHistory.int32 let inputFlags = ImGuiInputTextFlags_EnterReturnsTrue.int32 or ImGuiInputTextFlags_EscapeClearsAll.int32 or ImGuiInputTextFlags_CallbackCompletion.int32 or ImGuiInputTextFlags_CallbackHistory.int32
if igInputText("##Input", component.inputBuffer, 256, inputFlags, nil, nil): if igInputText("##Input", component.inputBuffer, 256, inputFlags, nil, nil):
echo component.inputBuffer discard
#[ #[
Session information (requires footerHeight to be doubled) Session information (optional footer)
]# ]#
# igSeparator() # igSeparator()
# igText(fmt"{component.agent.username}@{component.agent.hostname} [{component.agent.ip}]") # let sessionInfo = fmt"{component.agent.username}@{component.agent.hostname} [{component.agent.ip}]"
# igText(sessionInfo)
igSetItemDefaultFocus() igSetItemDefaultFocus()