8
by hackermans
|
1
|
import curses
|
2
|
import threading
|
3
|
import time
|
4
|
import os
|
5
|
import textwrap
|
6
|
import configparser
|
7
|
import logging
|
8
|
import sys
|
9
|
|
10
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
11
|
|
12
|
config = configparser.ConfigParser()
|
13
|
ini_path = os.path.join(os.path.dirname(__file__), 'arma_terminal_real.ini')
|
14
|
try:
|
15
|
config.read(ini_path)
|
16
|
except Exception as e:
|
17
|
logging.error(f"Error reading configuration: {e}")
|
18
|
sys.exit(1)
|
19
|
|
20
|
try:
|
21
|
CONSOLE_LOG = config.get('Paths', 'console_log')
|
22
|
COMMANDS_FILE = config.get('Paths', 'commands_file')
|
23
|
COMMAND_PREFIX = config.get('Settings', 'command_prefix')
|
24
|
MAX_LOG_LINES = config.getint('Settings', 'max_log_lines', fallback=1000)
|
25
|
except Exception as e:
|
26
|
logging.error(f"Error retrieving config values: {e}")
|
27
|
sys.exit(1)
|
28
|
|
29
|
def tail_log(lines, lock, stop_event):
|
30
|
with open(CONSOLE_LOG, 'r', encoding='utf-8', errors='ignore') as f:
|
31
|
f.seek(0, os.SEEK_END)
|
32
|
while not stop_event.is_set():
|
33
|
chunk = f.readline()
|
34
|
if not chunk:
|
35
|
time.sleep(0.1)
|
36
|
continue
|
37
|
text = chunk.strip()
|
38
|
if text:
|
39
|
with lock:
|
40
|
lines.append(text)
|
41
|
with lock:
|
42
|
if len(lines) > MAX_LOG_LINES:
|
43
|
lines[:] = lines[-MAX_LOG_LINES:]
|
44
|
|
45
|
def draw_scrollbar(win, top_line, total_lines, height):
|
46
|
if total_lines <= height:
|
47
|
return
|
48
|
scroll_height = height - 2
|
49
|
bar_height = max(1, int(scroll_height * (height / total_lines)))
|
50
|
top_pos = int(scroll_height * (top_line / total_lines))
|
51
|
for i in range(scroll_height):
|
52
|
char = '█' if top_pos <= i < top_pos + bar_height else '│'
|
53
|
try:
|
54
|
win.addch(i + 1, win.getmaxyx()[1] - 2, char)
|
55
|
except curses.error:
|
56
|
pass
|
57
|
|
58
|
def draw_screen(stdscr, lines, lock):
|
59
|
curses.curs_set(1)
|
60
|
stdscr.nodelay(True)
|
61
|
|
62
|
height, width = stdscr.getmaxyx()
|
63
|
log_h = height - 3
|
64
|
log_win = curses.newwin(log_h, width, 0, 0)
|
65
|
input_win = curses.newwin(3, width, log_h, 0)
|
66
|
|
67
|
input_str = ""
|
68
|
cursor_x = 0
|
69
|
scroll_offset = 0
|
70
|
history = []
|
71
|
history_index = 0
|
72
|
|
73
|
dragging = False
|
74
|
drag_start_y = None
|
75
|
drag_start_offset = None
|
76
|
|
77
|
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
|
78
|
|
79
|
while True:
|
80
|
with lock:
|
81
|
raw_lines = lines[-MAX_LOG_LINES:]
|
82
|
wrapped = []
|
83
|
for line in raw_lines:
|
84
|
wrapped.extend(textwrap.wrap(line, width - 3) or [''])
|
85
|
|
86
|
visible_lines = log_h - 2
|
87
|
max_offset = max(len(wrapped) - visible_lines, 0)
|
88
|
scroll_offset = min(scroll_offset, max_offset)
|
89
|
|
90
|
start_idx = max(0, len(wrapped) - visible_lines - scroll_offset)
|
91
|
display = wrapped[start_idx:start_idx + visible_lines]
|
92
|
|
93
|
log_win.erase()
|
94
|
log_win.box()
|
95
|
for idx, disp in enumerate(display):
|
96
|
log_win.addnstr(idx + 1, 1, disp, width - 3)
|
97
|
draw_scrollbar(log_win, start_idx, len(wrapped), log_h)
|
98
|
log_win.refresh()
|
99
|
|
100
|
input_win.erase()
|
101
|
input_win.box()
|
102
|
prompt = "> " + input_str
|
103
|
input_win.addnstr(1, 1, prompt, width - 2)
|
104
|
input_win.move(1, 2 + cursor_x)
|
105
|
input_win.refresh()
|
106
|
|
107
|
try:
|
108
|
ch = stdscr.get_wch()
|
109
|
except curses.error:
|
110
|
time.sleep(0.05)
|
111
|
continue
|
112
|
|
113
|
if ch == curses.KEY_RESIZE:
|
114
|
height, width = stdscr.getmaxyx()
|
115
|
log_h = height - 3
|
116
|
stdscr.erase(); stdscr.refresh()
|
117
|
log_win.resize(log_h, width); log_win.mvwin(0, 0)
|
118
|
input_win.resize(3, width); input_win.mvwin(log_h, 0)
|
119
|
continue
|
120
|
|
121
|
if ch == curses.KEY_UP:
|
122
|
if history:
|
123
|
history_index = max(history_index - 1, 0)
|
124
|
input_str = history[history_index]
|
125
|
cursor_x = len(input_str)
|
126
|
continue
|
127
|
if ch == curses.KEY_DOWN:
|
128
|
if history:
|
129
|
history_index = min(history_index + 1, len(history))
|
130
|
input_str = history[history_index] if history_index < len(history) else ""
|
131
|
cursor_x = len(input_str)
|
132
|
continue
|
133
|
|
134
|
if ch == curses.KEY_LEFT:
|
135
|
cursor_x = max(0, cursor_x - 1)
|
136
|
continue
|
137
|
if ch == curses.KEY_RIGHT:
|
138
|
cursor_x = min(len(input_str), cursor_x + 1)
|
139
|
continue
|
140
|
|
141
|
if ch == curses.KEY_PPAGE:
|
142
|
scroll_offset = min(scroll_offset + 3, max_offset)
|
143
|
continue
|
144
|
if ch == curses.KEY_NPAGE:
|
145
|
scroll_offset = max(scroll_offset - 3, 0)
|
146
|
continue
|
147
|
|
148
|
if ch == curses.KEY_END:
|
149
|
scroll_offset = 0
|
150
|
continue
|
151
|
|
152
|
if ch == curses.KEY_MOUSE:
|
153
|
try:
|
154
|
_, mx, my, _, bstate = curses.getmouse()
|
155
|
if bstate & curses.BUTTON1_PRESSED:
|
156
|
if 1 <= my < log_h - 1 and mx == width - 2:
|
157
|
dragging = True
|
158
|
drag_start_y = my
|
159
|
drag_start_offset = scroll_offset
|
160
|
elif bstate & curses.BUTTON1_RELEASED:
|
161
|
dragging = False
|
162
|
elif dragging and drag_start_y is not None:
|
163
|
dy = my - drag_start_y
|
164
|
scroll_offset = min(max(drag_start_offset - dy, 0), max_offset)
|
165
|
elif bstate & curses.BUTTON4_PRESSED:
|
166
|
scroll_offset = min(scroll_offset + 1, max_offset)
|
167
|
elif bstate & curses.BUTTON5_PRESSED:
|
168
|
scroll_offset = max(scroll_offset - 1, 0)
|
169
|
except Exception:
|
170
|
pass
|
171
|
continue
|
172
|
|
173
|
if isinstance(ch, str) and ch.isprintable():
|
174
|
input_str = input_str[:cursor_x] + ch + input_str[cursor_x:]
|
175
|
cursor_x += 1
|
176
|
elif ch in (curses.KEY_BACKSPACE, '\b', '\x7f'):
|
177
|
if cursor_x > 0:
|
178
|
input_str = input_str[:cursor_x - 1] + input_str[cursor_x:]
|
179
|
cursor_x -= 1
|
180
|
elif ch == curses.KEY_DC:
|
181
|
if cursor_x < len(input_str):
|
182
|
input_str = input_str[:cursor_x] + input_str[cursor_x + 1:]
|
183
|
elif ch == '\n':
|
184
|
cmd = input_str.strip()
|
185
|
input_str = ""
|
186
|
cursor_x = 0
|
187
|
if cmd:
|
188
|
history.append(cmd)
|
189
|
history_index = len(history)
|
190
|
if cmd.lower() in ("exit", "quit"):
|
191
|
return
|
192
|
full_cmd = f"{COMMAND_PREFIX} {cmd}" if COMMAND_PREFIX else cmd
|
193
|
try:
|
194
|
with open(COMMANDS_FILE, 'a', encoding='utf-8') as f:
|
195
|
f.write(full_cmd + "\n")
|
196
|
f.flush()
|
197
|
os.fsync(f.fileno())
|
198
|
except Exception as e:
|
199
|
with lock:
|
200
|
lines.append(f"→ ERROR writing command: {e}")
|
201
|
else:
|
202
|
with lock:
|
203
|
lines.append(f"→ SENT: {full_cmd}")
|
204
|
elif ch in (curses.KEY_EXIT, '\x1b'):
|
205
|
return
|
206
|
|
207
|
def main(stdscr):
|
208
|
lines = []
|
209
|
lock = threading.Lock()
|
210
|
stop_event = threading.Event()
|
211
|
|
212
|
t = threading.Thread(target=tail_log, args=(lines, lock, stop_event), daemon=True)
|
213
|
t.start()
|
214
|
|
215
|
try:
|
216
|
draw_screen(stdscr, lines, lock)
|
217
|
finally:
|
218
|
stop_event.set()
|
219
|
t.join(0.1)
|
220
|
|
221
|
if __name__ == "__main__":
|
222
|
import curses
|
223
|
curses.wrapper(main)
|