Added console history handling with arrow keys.

This commit is contained in:
Jakob Friedl
2025-09-16 22:21:11 +02:00
parent ee397c4fb5
commit 5f1a9979be
4 changed files with 113 additions and 41 deletions

View File

@@ -1,24 +1,24 @@
[Window][Sessions [Table View]] [Window][Sessions [Table View]]
Pos=10,43 Pos=10,43
Size=2117,223 Size=2117,435
Collapsed=0 Collapsed=0
DockId=0x00000003,0 DockId=0x00000003,0
[Window][Listeners] [Window][Listeners]
Pos=10,268 Pos=10,480
Size=2528,1081 Size=2528,869
Collapsed=0 Collapsed=0
DockId=0x00000002,0 DockId=0x00000006,0
[Window][Eventlog] [Window][Eventlog]
Pos=2129,43 Pos=2129,43
Size=409,223 Size=409,435
Collapsed=0 Collapsed=0
DockId=0x00000004,0 DockId=0x00000004,0
[Window][Dear ImGui Demo] [Window][Dear ImGui Demo]
Pos=2129,43 Pos=2129,43
Size=409,223 Size=409,435
Collapsed=0 Collapsed=0
DockId=0x00000004,1 DockId=0x00000004,1
@@ -28,16 +28,16 @@ Size=2548,1359
Collapsed=0 Collapsed=0
[Window][[FACEDEAD] bob@LAPTOP-02] [Window][[FACEDEAD] bob@LAPTOP-02]
Pos=10,268 Pos=10,480
Size=2528,1081 Size=2528,869
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x00000006,1
[Window][[C9D8E7F6] charlie@SERVER-03] [Window][[C9D8E7F6] charlie@SERVER-03]
Pos=10,268 Pos=10,480
Size=2528,1081 Size=1261,869
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x00000005,0
[Window][Debug##Default] [Window][Debug##Default]
Pos=60,60 Pos=60,60
@@ -45,22 +45,22 @@ Size=400,400
Collapsed=0 Collapsed=0
[Window][[G1H2I3J5] diana@WORKSTATION-04] [Window][[G1H2I3J5] diana@WORKSTATION-04]
Pos=10,268 Pos=10,480
Size=2528,1081 Size=2528,869
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x00000006,1
[Window][[DEADBEEF] alice@DESKTOP-01] [Window][[DEADBEEF] alice@DESKTOP-01]
Pos=10,604 Pos=10,604
Size=2848,1081 Size=2848,1081
Collapsed=0 Collapsed=0
DockId=0x00000002,2 DockId=0x00000006,2
[Window][Example: Console] [Window][Example: Console]
Pos=10,466 Pos=10,466
Size=1888,523 Size=1888,523
Collapsed=0 Collapsed=0
DockId=0x00000002,1 DockId=0x00000006,1
[Window][Example: Assets Browser] [Window][Example: Assets Browser]
Pos=60,60 Pos=60,60
@@ -93,8 +93,10 @@ Column 0 Sort=0v
[Docking][Data] [Docking][Data]
DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=2528,1306 Split=Y DockSpace ID=0x85940918 Window=0x260A4489 Pos=10,43 Size=2528,1306 Split=Y
DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,559 Split=X DockNode ID=0x00000001 Parent=0x85940918 SizeRef=1024,435 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,1081 Selected=0x65D642C0 DockNode ID=0x00000002 Parent=0x85940918 SizeRef=1024,869 Split=X Selected=0x8D780333
DockNode ID=0x00000005 Parent=0x00000002 SizeRef=1261,869 Selected=0x65D642C0
DockNode ID=0x00000006 Parent=0x00000002 SizeRef=1265,869 Selected=0x8D780333

View File

@@ -6,12 +6,18 @@ import ../../common/[types]
const MAX_INPUT_LENGTH = 512 const MAX_INPUT_LENGTH = 512
type type
ConsoleComponent* = ref object of RootObj ConsoleComponent* = ref object of RootObj
agent: Agent agent*: Agent
showConsole*: bool showConsole*: bool
inputBuffer: array[MAX_INPUT_LENGTH, char] inputBuffer: array[MAX_INPUT_LENGTH, char]
console: ConsoleItems console*: ConsoleItems
history: seq[string]
historyPosition: int
currentInput: string
textSelect: ptr TextSelect textSelect: ptr TextSelect
#[
Helper functions for text selection
]#
proc getItemText(item: ConsoleItem): cstring = proc getItemText(item: ConsoleItem): cstring =
let timestamp = item.timestamp.format("dd-MM-yyyy HH:mm:ss") let timestamp = item.timestamp.format("dd-MM-yyyy HH:mm:ss")
return fmt"[{timestamp}] {$item.itemType} {item.text}".string return fmt"[{timestamp}] {$item.itemType} {item.text}".string
@@ -36,11 +42,69 @@ proc Console*(agent: Agent): ConsoleComponent =
result.agent = agent result.agent = agent
result.showConsole = true result.showConsole = true
zeroMem(addr result.inputBuffer[0], MAX_INPUT_LENGTH) zeroMem(addr result.inputBuffer[0], MAX_INPUT_LENGTH)
result.console = new ConsoleItems result.console = new ConsoleItems
result.console.items = @[] result.console.items = @[]
result.history = @[]
result.historyPosition = -1
result.currentInput = ""
result.textSelect = textselect_create(getLineAtIndex, getNumLines, cast[pointer](result.console), 0) result.textSelect = textselect_create(getLineAtIndex, getNumLines, cast[pointer](result.console), 0)
#[
Text input callback function for managing console history and autocompletion
]#
proc callback(data: ptr ImGuiInputTextCallbackData): cint {.cdecl.} =
let component = cast[ConsoleComponent](data.UserData)
case data.EventFlag:
of ImGui_InputTextFlags_CallbackHistory.int32:
# Handle command history using arrow-keys
# Store current input
if component.historyPosition == -1:
component.currentInput = $(data.Buf)
let prev = component.historyPosition
# Move to a new console history item
if data.EventKey == ImGuiKey_UpArrow:
if component.history.len() > 0:
if component.historyPosition < 0: # We are at the current input and move to the last item in the console history
component.historyPosition = component.history.len() - 1
else:
component.historyPosition = max(0, component.historyPosition - 1)
elif data.EventKey == ImGuiKey_DownArrow:
if component.historyPosition != -1:
component.historyPosition = min(component.history.len(), component.historyPosition + 1)
if component.historyPosition == component.history.len():
component.historyPosition = -1
# Update the text buffer if another item was selected
if prev != component.historyPosition:
let newText = if component.historyPosition == -1:
component.currentInput
else:
component.history[component.historyPosition]
# Replace text input
data.ImGuiInputTextCallbackData_DeleteChars(0, data.BufTextLen)
data.ImGuiInputTextCallbackData_InsertChars(0, newText.cstring, nil)
# Set the cursor to the end of the updated input text
data.CursorPos = newText.len().cint
data.SelectionStart = newText.len().cint
data.SelectionEnd = newText.len().cint
return 0
of ImGui_InputTextFlags_CallbackCompletion.int32:
# Handle Tab-autocompletion
discard
else: discard
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()
@@ -49,15 +113,16 @@ proc draw*(component: ConsoleComponent) =
#[ #[
Console items/text section using ImGuiTextSelect in a child window Console items/text section using ImGuiTextSelect in a child window
Supports: Features:
- horizontal+vertical scrolling, - Horizontal+vertical scrolling,
- autoscroll - Autoscroll
- colored text - Colored text output
- text selection and copy functionality - Text highlighting, copy/paste
Problems I encountered with other approaches (Multi-line Text Input, TextEditor, ...): Problems I encountered with other approaches (Multi-line Text Input, TextEditor, ...):
- https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129 - https://github.com/ocornut/imgui/issues/383#issuecomment-2080346129
- https://github.com/ocornut/imgui/issues/950 - https://github.com/ocornut/imgui/issues/950
Huge thanks to @dinau for implementing ImGuiTextSelect into imguin very rapidly after I requested it. Huge thanks to @dinau for implementing ImGuiTextSelect into imguin very rapidly after I requested it.
]# ]#
let consolePadding: float = 10.0f let consolePadding: float = 10.0f
@@ -113,8 +178,8 @@ proc draw*(component: ConsoleComponent) =
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_CallbackHistory.int32 or ImGuiInputTextFlags_CallbackCompletion.int32
if igInputText("##Input", addr component.inputBuffer[0], MAX_INPUT_LENGTH, inputFlags, nil, nil): if igInputText("##Input", addr component.inputBuffer[0], MAX_INPUT_LENGTH, inputFlags, callback, cast[pointer](component)):
let command = $(addr component.inputBuffer[0]).cstring let command = $(addr component.inputBuffer[0]).cstring
let commandItem = ConsoleItem( let commandItem = ConsoleItem(
@@ -125,6 +190,11 @@ proc draw*(component: ConsoleComponent) =
component.console.items.add(commandItem) component.console.items.add(commandItem)
# TODO: Handle command execution # TODO: Handle command execution
# console.handleCommand(command)
# Add command to console history
component.history.add(command)
component.historyPosition = -1
zeroMem(addr component.inputBuffer[0], MAX_INPUT_LENGTH) zeroMem(addr component.inputBuffer[0], MAX_INPUT_LENGTH)
focusInput = true focusInput = true

View File

@@ -75,11 +75,11 @@ type
# LOG_WARNING = "[ ! ] " # LOG_WARNING = "[ ! ] "
# LOG_COMMAND = "[ > ] " # LOG_COMMAND = "[ > ] "
# LOG_OUTPUT = "" # LOG_OUTPUT = ""
LOG_INFO = "[INFO] " LOG_INFO = "[INFO]"
LOG_ERROR = "[FAIL] " LOG_ERROR = "[FAIL]"
LOG_SUCCESS = "[DONE] " LOG_SUCCESS = "[DONE]"
LOG_WARNING = "[WARN] " LOG_WARNING = "[WARN]"
LOG_COMMAND = "[>>>>] " LOG_COMMAND = "[>>>>]"
LOG_OUTPUT = "" LOG_OUTPUT = ""
SleepObfuscationTechnique* = enum SleepObfuscationTechnique* = enum

View File

@@ -43,16 +43,16 @@ template writeLine*(cq: Conquest, args: varargs[untyped] = "") =
# Wrapper functions for logging/console output # Wrapper functions for logging/console output
template info*(cq: Conquest, args: varargs[untyped] = "") = template info*(cq: Conquest, args: varargs[untyped] = "") =
cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", $LOG_INFO, resetStyle, args) cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", $LOG_INFO, resetStyle, " ", args)
template error*(cq: Conquest, args: varargs[untyped] = "") = template error*(cq: Conquest, args: varargs[untyped] = "") =
cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", fgRed, $LOG_ERROR, resetStyle, args) cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", fgRed, $LOG_ERROR, resetStyle, " ", args)
template success*(cq: Conquest, args: varargs[untyped] = "") = template success*(cq: Conquest, args: varargs[untyped] = "") =
cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", fgGreen, $LOG_SUCCESS, resetStyle, args) cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", fgGreen, $LOG_SUCCESS, resetStyle, " ", args)
template warning*(cq: Conquest, args: varargs[untyped] = "") = template warning*(cq: Conquest, args: varargs[untyped] = "") =
cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", fgYellow, styleDim, $LOG_WARNING, resetStyle, args) cq.writeLine(fgBlack, styleBright, fmt"[{getTimestamp()}] ", fgYellow, styleDim, $LOG_WARNING, resetStyle, " ", args)
template input*(cq: Conquest, args: varargs[untyped] = "") = template input*(cq: Conquest, args: varargs[untyped] = "") =
if cq.interactAgent != nil: if cq.interactAgent != nil: