1#include <lldb/API/LLDB.h>
   2
   3#include <iostream>
   4#include <string>
   5#include <chrono>
   6#include <fstream>
   7#include <vector>
   8#include <algorithm>
   9#include <fcntl.h>
  10#include <unistd.h>
  11#include <iomanip>
  12#include <ctime>
  13#include <sstream>
  14
  15#define TB_IMPL
  16#include "termbox2.h"
  17
  18using namespace lldb;
  19
  20struct LayoutConfig {
  21	int status_height = 1;
  22	int watch_height = 15;
  23	int sidebar_width = 50;
  24	int log_height = 5;
  25} layout_config;
  26
  27// https://unicodeplus.com
  28const uint32_t SCROLLBAR_THUMB = 0x2593; // Dark shade
  29const uint32_t SCROLLBAR_LINE = 0x2502;  // Vertical line
  30const uint32_t BREAKPOINT_CIRCLE = 0x25B6; // Filled triangle
  31
  32enum InputMode {
  33	INPUT_MODE_NORMAL,
  34	INPUT_MODE_BREAKPOINT,
  35	INPUT_MODE_VARIABLE,
  36	INPUT_MODE_WATCH,
  37	INPUT_MODE_HELP
  38};
  39
  40struct LLDBGuard {
  41	LLDBGuard() { SBDebugger::Initialize(); }
  42	~LLDBGuard() { SBDebugger::Terminate(); }
  43};
  44
  45struct TermboxGuard {
  46	TermboxGuard() { tb_init(); }
  47	~TermboxGuard() { tb_shutdown(); }
  48};
  49
  50struct SourceCache {
  51	std::string path;
  52	std::vector<std::string> lines;
  53
  54	const std::vector<std::string>& get_lines(const std::string& fullpath) {
  55		if (path != fullpath) {
  56			path = fullpath;
  57			lines.clear();
  58			std::ifstream file(fullpath);
  59			std::string line;
  60			while (std::getline(file, line)) {
  61				lines.push_back(line);
  62			}
  63		}
  64		return lines;
  65	}
  66};
  67
  68struct VarLine {
  69	std::string text;
  70	int indent;
  71	int prefix_start;
  72	int prefix_end;
  73};
  74
  75std::string get_timestamp() {
  76	auto now = std::chrono::system_clock::now();
  77	std::time_t now_c = std::chrono::system_clock::to_time_t(now);
  78	std::tm now_tm = *std::localtime(&now_c);
  79	std::stringstream ss;
  80	ss << std::put_time(&now_tm, "%H:%M:%S");
  81	return ss.str();
  82}
  83
  84void log_msg(std::vector<std::string>& log_buffer, const std::string& msg) {
  85	log_buffer.push_back(get_timestamp() + " " + msg);
  86}
  87
  88void draw_text(int x, int y, uint16_t fg, uint16_t bg, const std::string& text) {
  89	for (char c : text) {
  90		tb_set_cell(x++, y, c, fg, bg);
  91	}
  92}
  93
  94void draw_box(int x, int y, int w, int h, const std::string& title) {
  95	// Corners
  96	tb_set_cell(x, y, 0x250C, TB_DEFAULT, TB_DEFAULT);
  97	tb_set_cell(x + w - 1, y, 0x2510, TB_DEFAULT, TB_DEFAULT);
  98	tb_set_cell(x, y + h - 1, 0x2514, TB_DEFAULT, TB_DEFAULT);
  99	tb_set_cell(x + w - 1, y + h - 1, 0x2518, TB_DEFAULT, TB_DEFAULT);
 100
 101	// Borders
 102	for (int i = 1; i < w - 1; ++i) {
 103		tb_set_cell(x + i, y, 0x2500, TB_DEFAULT, TB_DEFAULT);
 104		tb_set_cell(x + i, y + h - 1, 0x2500, TB_DEFAULT, TB_DEFAULT);
 105	}
 106	for (int i = 1; i < h - 1; ++i) {
 107		tb_set_cell(x, y + i, 0x2502, TB_DEFAULT, TB_DEFAULT);
 108		tb_set_cell(x + w - 1, y + i, 0x2502, TB_DEFAULT, TB_DEFAULT);
 109	}
 110
 111	if (!title.empty()) {
 112		draw_text(x + 2, y, TB_BOLD | TB_GREEN, TB_DEFAULT, " " + title + " ");
 113	}
 114}
 115
 116char get_type_char(SBType type) {
 117	if (!type.IsValid()) return '?';
 118
 119	// Resolve typedefs to their underlying canonical type
 120	type = type.GetCanonicalType();
 121
 122	if (type.IsPointerType()) return 'p';
 123	if (type.IsReferenceType()) return '&';
 124	if (type.IsArrayType()) return 'a';
 125
 126	BasicType basic_type = type.GetBasicType();
 127	switch (basic_type) {
 128		case eBasicTypeInt:
 129		case eBasicTypeUnsignedInt:
 130			return 'i';
 131		case eBasicTypeChar:
 132		case eBasicTypeUnsignedChar:
 133			return 'c';
 134		case eBasicTypeFloat:
 135			return 'f';
 136		case eBasicTypeDouble:
 137			return 'd';
 138		case eBasicTypeBool:
 139			return 'b';
 140		case eBasicTypeLong:
 141		case eBasicTypeUnsignedLong:
 142		case eBasicTypeLongLong:
 143		case eBasicTypeUnsignedLongLong:
 144			return 'l';
 145		case eBasicTypeShort:
 146		case eBasicTypeUnsignedShort:
 147			return 's';
 148		case eBasicTypeVoid:
 149			return 'v';
 150		default:
 151			break;
 152	}
 153
 154	TypeClass type_class = type.GetTypeClass();
 155	if (type_class & eTypeClassStruct) return 's';
 156	if (type_class & eTypeClassClass) return 'c';
 157	if (type_class & eTypeClassEnumeration) return 'e';
 158
 159	const char* name = type.GetName();
 160	if (name && *name) return name[0];
 161
 162	return '?';
 163}
 164
 165void collect_variables_recursive(SBValue val, int indent, std::vector<VarLine>& lines, int width, const std::string& name_override = "") {
 166	if (indent > 3) return;
 167
 168	std::string original_name = name_override.empty() ? (val.GetName() ? val.GetName() : "") : name_override;
 169	char type_char = get_type_char(val.GetType());
 170	std::string prefix = std::string("(") + type_char + ") ";
 171
 172	std::string val_str = val.GetValue() ? val.GetValue() : "";
 173	std::string summary_str = val.GetSummary() ? val.GetSummary() : "";
 174	std::string value;
 175
 176	if (!val.IsValid()) value = "(invalid)";
 177	else {
 178		if (!val_str.empty() && !summary_str.empty()) value = val_str + " " + summary_str;
 179		else if (!val_str.empty()) value = val_str;
 180		else if (!summary_str.empty()) value = summary_str;
 181	}
 182
 183	std::string indent_str(indent * 2, ' ');
 184	std::string content = original_name;
 185	if (!value.empty()) content += " = " + value;
 186
 187	std::string line_text = indent_str + prefix + content;
 188	if ((int)line_text.length() > width) line_text = line_text.substr(0, width - 3) + "...";
 189
 190	VarLine vl;
 191	vl.text = line_text;
 192	vl.indent = indent;
 193	vl.prefix_start = indent * 2;
 194	vl.prefix_end = vl.prefix_start + 4; // length of "(x) "
 195	lines.push_back(vl);
 196
 197	if (val.GetNumChildren() > 0) {
 198		uint32_t n = val.GetNumChildren();
 199		for (uint32_t i = 0; i < n; ++i) {
 200			collect_variables_recursive(val.GetChildAtIndex(i), indent + 1, lines, width);
 201		}
 202	}
 203}
 204
 205void format_variable_log(SBValue val, std::vector<std::string>& log_buffer, int indent, const std::string& name_override = "") {
 206	if (indent > 3) return;
 207
 208	std::string name = name_override.empty() ? (val.GetName() ? val.GetName() : "") : name_override;
 209	char type_char = get_type_char(val.GetType());
 210
 211	std::string val_str = val.GetValue() ? val.GetValue() : "";
 212	std::string summary_str = val.GetSummary() ? val.GetSummary() : "";
 213	std::string value;
 214
 215	if (!val.IsValid()) value = "(invalid)";
 216	else {
 217		if (!val_str.empty() && !summary_str.empty()) value = val_str + " " + summary_str;
 218		else if (!val_str.empty()) value = val_str;
 219		else if (!summary_str.empty()) value = summary_str;
 220	}
 221
 222	std::string indent_str(indent * 2, ' ');
 223	std::string line = get_timestamp() + " " + indent_str + "(" + type_char + ") " + name;
 224	if (!value.empty()) line += " = " + value;
 225
 226	log_buffer.push_back(line);
 227
 228	if (val.GetNumChildren() > 0) {
 229		uint32_t n = val.GetNumChildren();
 230		for (uint32_t i = 0; i < n; ++i) {
 231			format_variable_log(val.GetChildAtIndex(i), log_buffer, indent + 1);
 232		}
 233	}
 234}
 235
 236void draw_variables_view(SBFrame &frame, int x, int y, int w, int h, int scroll_offset) {
 237	draw_box(x, y, w, h, "Locals");
 238
 239	// Content area
 240	int cx = x + 1;
 241	int cy = y + 1;
 242	int ch = h - 2;
 243	int cw = w - 2;
 244
 245	if (!frame.IsValid()) {
 246		draw_text(cx, cy, TB_RED, TB_DEFAULT, "No frame selected.");
 247		return;
 248	}
 249
 250	std::vector<VarLine> lines;
 251	SBValueList vars = frame.GetVariables(true, true, false, true);
 252	for (uint32_t i = 0; i < vars.GetSize(); ++i) {
 253		collect_variables_recursive(vars.GetValueAtIndex(i), 0, lines, cw);
 254	}
 255
 256	int total_lines = (int)lines.size();
 257	int display_count = std::min(total_lines, ch);
 258
 259	for (int i = 0; i < display_count; ++i) {
 260		int line_idx = scroll_offset + i;
 261		if (line_idx < 0 || line_idx >= total_lines) continue;
 262
 263		const VarLine& vl = lines[line_idx];
 264		for (int j = 0; j < (int)vl.text.length() && j < cw; ++j) {
 265			uint16_t fg = TB_DEFAULT;
 266			if (j >= vl.prefix_start && j < vl.prefix_end) {
 267				fg = TB_BLACK | TB_BOLD;
 268			}
 269			tb_set_cell(cx + j, cy + i, vl.text[j], fg, TB_DEFAULT);
 270		}
 271	}
 272
 273	// Draw scrollbar
 274	if (total_lines > ch) {
 275		int thumb_height = std::max(1, (ch * ch) / total_lines);
 276		int max_scroll = total_lines - ch;
 277		double scroll_percent = (double)scroll_offset / (double)max_scroll;
 278		int thumb_pos = (ch - thumb_height) * scroll_percent;
 279
 280		for (int i = 0; i < ch; ++i) {
 281			uint32_t cell_char = SCROLLBAR_LINE;
 282			uint16_t fg = TB_DEFAULT;
 283			if (i >= thumb_pos && i < thumb_pos + thumb_height) {
 284				cell_char = SCROLLBAR_THUMB;
 285				fg = TB_WHITE;
 286			}
 287			tb_set_cell(x + w - 1, cy + i, cell_char, fg, TB_DEFAULT);
 288		}
 289	}
 290}
 291
 292std::string get_breakpoint_name(SBBreakpoint bp) {
 293	if (!bp.IsValid()) return "???";
 294
 295	std::string name = "???";
 296	if (bp.GetNumLocations() > 0) {
 297		SBBreakpointLocation loc = bp.GetLocationAtIndex(0);
 298		SBAddress addr = loc.GetAddress();
 299
 300		SBFunction func = addr.GetFunction();
 301		if (func.IsValid()) {
 302			const char* n = func.GetName();
 303			if (n) name = n;
 304		} else {
 305			SBSymbol sym = addr.GetSymbol();
 306			if (sym.IsValid()) {
 307				const char* n = sym.GetName();
 308				if (n) name = n;
 309			}
 310		}
 311
 312		SBLineEntry line_entry = addr.GetLineEntry();
 313		if (line_entry.IsValid()) {
 314			std::string file_name;
 315			SBFileSpec fs = line_entry.GetFileSpec();
 316			if (fs.IsValid()) file_name = fs.GetFilename();
 317
 318			if (name == "???") {
 319				if (!file_name.empty()) name = file_name + ":" + std::to_string(line_entry.GetLine());
 320			} else {
 321				if (!file_name.empty()) name += " (" + file_name + ":" + std::to_string(line_entry.GetLine()) + ")";
 322			}
 323		}
 324	}
 325	return name;
 326}
 327
 328void draw_source_view(SBFrame &frame, int x, int y, int w, int h, SourceCache& cache, int scroll_offset) {
 329	draw_box(x, y, w, h, "Source");
 330
 331	int cx = x + 1;
 332	int cy = y + 1;
 333	int ch = h - 2;
 334	int cw = w - 2;
 335
 336	if (!frame.IsValid()) {
 337		draw_text(cx, cy, TB_RED, TB_DEFAULT, "No frame selected.");
 338		return;
 339	}
 340
 341	SBLineEntry line_entry = frame.GetLineEntry();
 342	if (!line_entry.IsValid()) {
 343		draw_text(cx, cy, TB_RED, TB_DEFAULT, "No line entry info.");
 344		return;
 345	}
 346
 347	SBFileSpec file_spec = line_entry.GetFileSpec();
 348	if (!file_spec.IsValid()) return;
 349
 350	// Construct full path
 351	std::string fullpath;
 352	if (file_spec.GetDirectory()) {
 353		fullpath = std::string(file_spec.GetDirectory()) + "/" + file_spec.GetFilename();
 354	} else {
 355		fullpath = file_spec.GetFilename();
 356	}
 357
 358	SBAddress addr = frame.GetPCAddress();
 359	SBTarget target = frame.GetThread().GetProcess().GetTarget();
 360
 361	const std::vector<std::string>& lines = cache.get_lines(fullpath);
 362	if (lines.empty() && !std::ifstream(fullpath).good()) {
 363		draw_text(cx, cy, TB_RED | TB_BOLD, TB_DEFAULT, "Could not open source: " + fullpath);
 364
 365		SBFunction func = frame.GetFunction();
 366		std::string func_name = func.IsValid() ? func.GetName() : "???";
 367
 368		char addr_buf[64];
 369		snprintf(addr_buf, sizeof(addr_buf), "At address: 0x%lx", (unsigned long)addr.GetLoadAddress(target));
 370
 371		draw_text(cx, cy + 2, TB_WHITE, TB_DEFAULT, "Function: " + func_name);
 372		draw_text(cx, cy + 3, TB_WHITE, TB_DEFAULT, addr_buf);
 373		draw_text(cx, cy + 5, TB_YELLOW, TB_DEFAULT, "Press 'n' (Step Over) or 'o' (Step Out) to return to your code.");
 374
 375		// Disassembly fallback
 376		SBInstructionList instructions = target.ReadInstructions(addr, (uint32_t)(ch - 8));
 377		if (instructions.IsValid()) {
 378			for (uint32_t i = 0; i < instructions.GetSize() && (int)i < ch - 8; ++i) {
 379				SBInstruction insn = instructions.GetInstructionAtIndex(i);
 380				std::string dis = insn.GetMnemonic(target);
 381				dis += " ";
 382				dis += insn.GetOperands(target);
 383
 384				uint16_t fg = (insn.GetAddress() == addr) ? TB_WHITE | TB_BOLD : TB_DEFAULT;
 385				uint16_t bg = (insn.GetAddress() == addr) ? TB_BLUE : TB_DEFAULT;
 386
 387				char insn_addr_buf[32];
 388				snprintf(insn_addr_buf, sizeof(insn_addr_buf), "0x%lx: ", (unsigned long)insn.GetAddress().GetLoadAddress(target));
 389
 390				draw_text(cx, cy + 7 + i, fg, bg, std::string(insn_addr_buf) + dis);
 391			}
 392		}
 393		return;
 394	}
 395
 396	// Get breakpoints for this file
 397	std::vector<uint32_t> bp_lines;
 398	uint32_t num_breakpoints = target.GetNumBreakpoints();
 399	for (uint32_t i = 0; i < num_breakpoints; ++i) {
 400		SBBreakpoint bp = target.GetBreakpointAtIndex(i);
 401		uint32_t num_locs = bp.GetNumLocations();
 402		for (uint32_t j = 0; j < num_locs; ++j) {
 403			SBBreakpointLocation loc = bp.GetLocationAtIndex(j);
 404			SBLineEntry le = loc.GetAddress().GetLineEntry();
 405			if (le.IsValid()) {
 406				SBFileSpec fs = le.GetFileSpec();
 407				if (fs.IsValid()) {
 408					std::string bp_path;
 409					if (fs.GetDirectory()) {
 410						bp_path = std::string(fs.GetDirectory()) + "/" + fs.GetFilename();
 411					} else {
 412						bp_path = fs.GetFilename();
 413					}
 414					if (bp_path == fullpath) {
 415						bp_lines.push_back(le.GetLine());
 416					}
 417				}
 418			}
 419		}
 420	}
 421
 422	int total_lines = (int)lines.size();
 423	int current_line = line_entry.GetLine();
 424	for (int i = 0; i < ch; ++i) {
 425		int line_idx = scroll_offset + i + 1;
 426		if (line_idx > total_lines) break;
 427
 428		std::string src = lines[line_idx - 1];
 429		// Handle basic tab expansion (simple version)
 430		std::string expanded;
 431		for (char c : src) {
 432			if (c == '\t') expanded += "    ";
 433			else expanded += c;
 434		}
 435		src = expanded;
 436
 437		bool is_current = (line_idx == current_line);
 438		bool has_breakpoint = std::find(bp_lines.begin(), bp_lines.end(), (uint32_t)line_idx) != bp_lines.end();
 439
 440		char buf[32];
 441		snprintf(buf, sizeof(buf), "%4d ", line_idx);
 442		std::string num_str(buf);
 443
 444		uint16_t bg = is_current ? TB_BLUE : TB_DEFAULT;
 445		uint16_t fg = is_current ? TB_WHITE | TB_BOLD : TB_DEFAULT;
 446
 447		// Draw breakpoint indicator
 448		if (has_breakpoint) {
 449			tb_set_cell(cx, cy + i, BREAKPOINT_CIRCLE, TB_RED | TB_BOLD, bg);
 450		} else {
 451			tb_set_cell(cx, cy + i, ' ', fg, bg);
 452		}
 453
 454		draw_text(cx + 1, cy + i, fg, bg, num_str);
 455
 456		int src_max_len = cw - (int)num_str.length() - 1;
 457		if ((int)src.length() > src_max_len) {
 458			src = src.substr(0, src_max_len);
 459		}
 460		draw_text(cx + 1 + num_str.length(), cy + i, fg, bg, src);
 461
 462		if (is_current) {
 463			for (int k = cx + 1 + num_str.length() + src.length(); k < cx + cw; ++k) {
 464				tb_set_cell(k, cy + i, ' ', fg, bg);
 465			}
 466		}
 467	}
 468
 469	// Draw scrollbar
 470	if (total_lines > ch) {
 471		int thumb_height = std::max(1, (ch * ch) / total_lines);
 472		int max_scroll = total_lines - ch;
 473		double scroll_percent = (double)scroll_offset / (double)max_scroll;
 474		int thumb_pos = (ch - thumb_height) * scroll_percent;
 475
 476		for (int i = 0; i < ch; ++i) {
 477			uint32_t cell_char = SCROLLBAR_LINE;
 478			uint16_t fg = TB_DEFAULT;
 479			if (i >= thumb_pos && i < thumb_pos + thumb_height) {
 480				cell_char = SCROLLBAR_THUMB;
 481				fg = TB_WHITE;
 482			}
 483			tb_set_cell(x + w - 1, cy + i, cell_char, fg, TB_DEFAULT);
 484		}
 485	}
 486}
 487
 488void draw_breakpoints_view(SBTarget& target, int x, int y, int w, int h) {
 489	draw_box(x, y, w, h, "Breakpoints");
 490	int cx = x + 1;
 491	int cy = y + 1;
 492	int mh = h - 2;
 493
 494	if (!target.IsValid()) return;
 495
 496	int num_bps = target.GetNumBreakpoints();
 497	for (int i = 0; i < num_bps && i < mh; ++i) {
 498		SBBreakpoint bp = target.GetBreakpointAtIndex(i);
 499		std::string name = get_breakpoint_name(bp);
 500
 501		char buf[128];
 502		snprintf(buf, sizeof(buf), "%d: %s", bp.GetID(), name.c_str());
 503		std::string line = buf;
 504		draw_text(cx, cy + i, TB_DEFAULT, TB_DEFAULT, line);
 505	}
 506}
 507
 508void draw_watch_view(SBFrame& frame, int x, int y, int w, int h, const std::vector<std::string>& expressions, int scroll_offset) {
 509	draw_box(x, y, w, h, "Watch");
 510	int cx = x + 1;
 511	int cy = y + 1;
 512	int ch = h - 2;
 513	int cw = w - 2;
 514
 515	if (expressions.empty()) {
 516		draw_text(cx, cy, TB_DEFAULT, TB_DEFAULT, "No watch expressions.");
 517		return;
 518	}
 519
 520	std::vector<VarLine> lines;
 521	for (const auto& expr : expressions) {
 522		SBValue val = frame.EvaluateExpression(expr.c_str());
 523		if (val.IsValid() && !val.GetError().Fail()) {
 524			collect_variables_recursive(val, 0, lines, cw, expr);
 525		} else {
 526			VarLine vl;
 527			vl.text = expr + " = (error)";
 528			if (val.GetError().GetCString()) {
 529				vl.text += ": " + std::string(val.GetError().GetCString());
 530			}
 531			if ((int)vl.text.length() > cw) vl.text = vl.text.substr(0, cw - 3) + "...";
 532			vl.indent = 0;
 533			vl.prefix_start = 0;
 534			vl.prefix_end = 0;
 535			lines.push_back(vl);
 536		}
 537	}
 538
 539	int total_lines = (int)lines.size();
 540	int display_count = std::min(total_lines, ch);
 541
 542	for (int i = 0; i < display_count; ++i) {
 543		int line_idx = scroll_offset + i;
 544		if (line_idx < 0 || line_idx >= total_lines) continue;
 545
 546		const VarLine& vl = lines[line_idx];
 547		for (int j = 0; j < (int)vl.text.length() && j < cw; ++j) {
 548			uint16_t fg = TB_DEFAULT;
 549			if (j >= vl.prefix_start && j < vl.prefix_end) {
 550				fg = TB_BLACK | TB_BOLD;
 551			}
 552			tb_set_cell(cx + j, cy + i, vl.text[j], fg, TB_DEFAULT);
 553		}
 554	}
 555
 556	// Draw scrollbar
 557	if (total_lines > ch) {
 558		int thumb_height = std::max(1, (ch * ch) / total_lines);
 559		int max_scroll = total_lines - ch;
 560		double scroll_percent = (double)scroll_offset / (double)max_scroll;
 561		int thumb_pos = (ch - thumb_height) * scroll_percent;
 562
 563		for (int i = 0; i < ch; ++i) {
 564			uint32_t cell_char = SCROLLBAR_LINE;
 565			uint16_t fg = TB_DEFAULT;
 566			if (i >= thumb_pos && i < thumb_pos + thumb_height) {
 567				cell_char = SCROLLBAR_THUMB;
 568				fg = TB_WHITE;
 569			}
 570			tb_set_cell(x + w - 1, cy + i, cell_char, fg, TB_DEFAULT);
 571		}
 572	}
 573}
 574
 575SBBreakpoint create_breakpoint(SBTarget& target, const std::string& input) {
 576	SBBreakpoint bp;
 577	size_t colon_pos = input.rfind(':');
 578
 579	if (colon_pos != std::string::npos && colon_pos < input.length() - 1) {
 580		std::string line_str = input.substr(colon_pos + 1);
 581		bool is_number = !line_str.empty() && std::all_of(line_str.begin(), line_str.end(), ::isdigit);
 582
 583		if (is_number) {
 584			std::string filename = input.substr(0, colon_pos);
 585			uint32_t line_no = (uint32_t)std::stoi(line_str);
 586			bp = target.BreakpointCreateByLocation(filename.c_str(), line_no);
 587			if (bp.IsValid() && bp.GetNumLocations() > 0) return bp;
 588		}
 589	}
 590
 591	return target.BreakpointCreateByName(input.c_str());
 592}
 593
 594void draw_log_view(int x, int y, int w, int h, const std::vector<std::string>& log_buffer, InputMode mode, const std::string& input_buffer, int scroll_offset) {
 595	bool input_mode = (mode == INPUT_MODE_BREAKPOINT || mode == INPUT_MODE_VARIABLE || mode == INPUT_MODE_WATCH);
 596	std::string title = input_mode ? "Input (Esc to Cancel)" : "Logs";
 597	if (!input_mode && scroll_offset > 0) {
 598		title += " (Scrolled up: " + std::to_string(scroll_offset) + ")";
 599	}
 600	draw_box(x, y, w, h, title);
 601
 602	int cx = x + 1;
 603	int cy = y + 1;
 604	int ch = h - 2;
 605	int cw = w - 2;
 606
 607	if (input_mode) {
 608		std::string prompt;
 609		if (mode == INPUT_MODE_BREAKPOINT) prompt = "Add Breakpoint: ";
 610		else if (mode == INPUT_MODE_VARIABLE) prompt = "Print Variable: ";
 611		else if (mode == INPUT_MODE_WATCH) prompt = "Watch Variable: ";
 612
 613		prompt += input_buffer;
 614		if ((int)prompt.length() > cw) prompt = prompt.substr(prompt.length() - cw);
 615		draw_text(cx, cy, TB_WHITE | TB_BOLD, TB_DEFAULT, prompt);
 616		tb_set_cell(cx + prompt.length(), cy, '_', TB_WHITE | TB_BOLD | TB_REVERSE, TB_DEFAULT);
 617	} else {
 618		int total_logs = log_buffer.size();
 619		int display_count = std::min(total_logs, ch);
 620
 621		for (int i = 0; i < display_count; ++i) {
 622			int log_idx = total_logs - display_count - scroll_offset + i;
 623			if (log_idx < 0 || log_idx >= total_logs) continue;
 624
 625			const std::string& msg = log_buffer[log_idx];
 626			std::string disp = msg;
 627			if ((int)disp.length() > cw) disp = disp.substr(0, cw);
 628			draw_text(cx, cy + i, TB_DEFAULT, TB_DEFAULT, disp);
 629		}
 630
 631		// Draw scrollbar
 632		if (total_logs > ch) {
 633			int thumb_height = std::max(1, (ch * ch) / total_logs);
 634			int max_scroll = total_logs - ch;
 635			double scroll_percent = (double)scroll_offset / (double)max_scroll;
 636			int thumb_pos = (ch - thumb_height) * (1.0 - scroll_percent);
 637
 638			for (int i = 0; i < ch; ++i) {
 639				uint32_t cell_char = SCROLLBAR_LINE;
 640				uint16_t fg = TB_DEFAULT;
 641				if (i >= thumb_pos && i < thumb_pos + thumb_height) {
 642					cell_char = SCROLLBAR_THUMB;
 643					fg = TB_WHITE;
 644				}
 645				tb_set_cell(x + w - 1, cy + i, cell_char, fg, TB_DEFAULT);
 646			}
 647		}
 648	}
 649}
 650
 651void draw_help_view(int width, int height) {
 652	int w = 60;
 653	int h = 18;
 654	int x = (width - w) / 2;
 655	int y = (height - h) / 2;
 656
 657	// Fill background
 658	for (int i = 0; i < h; ++i) {
 659		for (int j = 0; j < w; ++j) {
 660			tb_set_cell(x + j, y + i, ' ', TB_DEFAULT, TB_DEFAULT);
 661		}
 662	}
 663
 664	draw_box(x, y, w, h, "Help / Keybinds");
 665
 666	int ty = y + 2;
 667	int tx = x + 3;
 668
 669	auto d = [&](const std::string& key, const std::string& desc) {
 670		draw_text(tx, ty, TB_YELLOW | TB_BOLD, TB_DEFAULT, key);
 671		draw_text(tx + 13, ty, TB_DEFAULT, TB_DEFAULT, desc);
 672		ty++;
 673	};
 674
 675	d("r", "Run / Launch program");
 676	d("b", "Add breakpoint (file:line or func)");
 677	d("p", "Print variable / Evaluate expr");
 678	d("w", "Add watch expression");
 679	d("n", "Step Over (next line)");
 680	d("s", "Step Into (into function)");
 681	d("o", "Step Out (to caller)");
 682	d("c", "Continue execution");
 683	d("h", "Toggle help window");
 684	d("q", "Quit debugger");
 685	d("Esc", "Cancel input / Close help");
 686	d("Ctrl+Arrows", "Resize layout");
 687	d("Mouse Wheel", "Scroll active window");
 688
 689	draw_text(x + (w - 24) / 2, y + h - 2, TB_BLACK, TB_WHITE, " Press any key to close ");
 690}
 691
 692void draw_status_bar(SBProcess &process, InputMode mode, int width, int height) {
 693	std::string state_str = "Status: ";
 694	if (!process.IsValid()) {
 695		state_str += "Not Running";
 696	} else {
 697		StateType state = process.GetState();
 698		if (state == eStateStopped) state_str += "Stopped";
 699		else if (state == eStateRunning) state_str += "Running";
 700		else if (state == eStateExited) state_str += "Exited";
 701		else state_str += "Unknown";
 702	}
 703
 704	state_str += (mode == INPUT_MODE_NORMAL)
 705		? " | r=Run, b=Add bp, p=Print, w=Watch, n=Step, s=Step In, o=Step Out, c=Cont, h=Help, q=Quit"
 706		: (mode == INPUT_MODE_HELP ? " | Press any key to close help" : " | Enter=Confirm, Esc=Cancel");
 707
 708	for (int x = 0; x < width; ++x) {
 709		tb_set_cell(x, height - 1, ' ', TB_BLACK, TB_WHITE);
 710	}
 711
 712	draw_text(1, height - 1, TB_BLACK, TB_WHITE, state_str);
 713}
 714
 715SBProcess launch_target(SBTarget& target, const std::string& target_path, const std::vector<std::string>& debuggee_args, const std::vector<std::string>& target_env, std::vector<std::string>& log_buffer) {
 716	if (target.GetNumBreakpoints() == 0) {
 717		SBBreakpoint bp = target.BreakpointCreateByName("main");
 718		if (bp.IsValid() && bp.GetNumLocations() > 0) {
 719			log_msg(log_buffer, "No breakpoints. Added breakpoint at 'main'");
 720		} else {
 721			log_msg(log_buffer, "No breakpoints. Failed to add breakpoint at 'main'");
 722		}
 723	}
 724	log_msg(log_buffer, "Launching...");
 725
 726	std::vector<const char*> launch_argv;
 727	launch_argv.push_back(target_path.c_str());
 728	for (const auto& arg : debuggee_args) {
 729		launch_argv.push_back(arg.c_str());
 730	}
 731	launch_argv.push_back(nullptr);
 732
 733	std::vector<const char*> launch_env;
 734	for (const auto& env : target_env) {
 735		launch_env.push_back(env.c_str());
 736	}
 737	launch_env.push_back(nullptr);
 738
 739	SBLaunchInfo launch_info(launch_argv.data());
 740	launch_info.SetEnvironmentEntries(launch_env.data(), true);
 741	launch_info.SetWorkingDirectory(".");
 742
 743	SBError error;
 744	SBProcess process = target.Launch(launch_info, error);
 745
 746	if (!process.IsValid() || error.Fail()) {
 747		std::string err_msg = "Launch failed";
 748		if (error.GetCString()) {
 749			err_msg += ": ";
 750			err_msg += error.GetCString();
 751		}
 752		log_msg(log_buffer, err_msg);
 753	} else {
 754		log_msg(log_buffer, "Launched");
 755	}
 756	return process;
 757}
 758
 759int main(int argc, char** argv) {
 760	std::vector<std::string> target_env;
 761	std::vector<std::string> startup_breakpoints;
 762	std::vector<std::string> debuggee_args;
 763	std::string target_path;
 764	bool auto_run = false;
 765
 766	for (int i = 1; i < argc; ++i) {
 767		std::string arg = argv[i];
 768		if (arg == "-e" && i + 1 < argc) {
 769			target_env.push_back(argv[++i]);
 770		} else if (arg == "-b" && i + 1 < argc) {
 771			startup_breakpoints.push_back(argv[++i]);
 772		} else if (arg == "-run") {
 773			auto_run = true;
 774		} else if (arg == "-h" || arg == "--help") {
 775			std::cout << "Usage: " << argv[0] << " [options] <target_executable> [-- arg1 arg2 ...]\n\n"
 776					  << "Options:\n"
 777					  << "  -e KEY=VALUE      Set environment variable\n"
 778					  << "  -b BREAKPOINT     Set startup breakpoint (name or file:line)\n"
 779					  << "  -run              Automatically run the target on startup\n"
 780					  << "  -h, --help        Show this help message\n";
 781			return 0;
 782		} else if (arg == "--") {
 783			for (int j = i + 1; j < argc; ++j) {
 784				debuggee_args.push_back(argv[j]);
 785			}
 786			break;
 787		} else if (target_path.empty()) {
 788			target_path = arg;
 789		} else {
 790			debuggee_args.push_back(arg);
 791		}
 792	}
 793
 794	if (target_path.empty()) {
 795		std::cerr << "Usage: " << argv[0] << " [-e KEY=VALUE] [-b BREAKPOINT] [-run] ... <target_executable> [-- arg1 arg2 ...]\n";
 796		return 1;
 797	}
 798
 799	int log_fd = open("tdbg.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
 800	if (log_fd != -1) {
 801		dup2(log_fd, STDERR_FILENO);
 802		close(log_fd);
 803	}
 804
 805	LLDBGuard lldb_guard;
 806	SBDebugger debugger = SBDebugger::Create();
 807	debugger.SetAsync(false); 
 808
 809	SBTarget target = debugger.CreateTarget(target_path.c_str());
 810	if (!target.IsValid()) {
 811		std::cerr << "Failed to create target for " << target_path << "\n";
 812		return 1;
 813	}
 814
 815	std::vector<std::string> log_buffer;
 816	for (const auto& bp_spec : startup_breakpoints) {
 817		SBBreakpoint bp = create_breakpoint(target, bp_spec);
 818		if (bp.IsValid() && bp.GetNumLocations() > 0) {
 819			log_msg(log_buffer, "Set startup breakpoint: " + bp_spec);
 820		} else {
 821			log_msg(log_buffer, "Failed to set startup breakpoint: " + bp_spec);
 822		}
 823	}
 824
 825	SBProcess process;
 826	if (auto_run) {
 827		process = launch_target(target, target_path, debuggee_args, target_env, log_buffer);
 828	}
 829
 830	SBThread thread;
 831	TermboxGuard tb_guard;
 832
 833	bool running = true;
 834	InputMode mode = INPUT_MODE_NORMAL;
 835	std::string input_buffer;
 836	std::string current_source_filename;
 837	std::vector<std::string> watch_expressions;
 838	int log_scroll_offset = 0;
 839	int locals_scroll_offset = 0;
 840	int watch_scroll_offset = 0;
 841	int source_scroll_offset = 0;
 842	uint64_t last_pc = 0;
 843	SourceCache source_cache;
 844	log_buffer.push_back("Debugger started. Press 'b' to add breakpoint, 'r' to run.");
 845
 846	tb_set_input_mode(TB_INPUT_ESC | TB_INPUT_MOUSE);
 847
 848	while (running) {
 849		tb_clear();
 850
 851		int width = tb_width();
 852		int height = tb_height();
 853		int main_window_height = height - layout_config.log_height - layout_config.status_height;
 854		int split_x = width - layout_config.sidebar_width;
 855		int locals_window_height = main_window_height - layout_config.watch_height;
 856
 857		SBFrame frame;
 858		if (process.IsValid() && process.GetState() != eStateExited) {
 859			thread = process.GetSelectedThread();
 860			if (thread.IsValid()) {
 861				frame = thread.GetSelectedFrame();
 862				if (frame.IsValid()) {
 863					uint64_t current_pc = frame.GetPC();
 864					if (current_pc != last_pc) {
 865						last_pc = current_pc;
 866						SBLineEntry le = frame.GetLineEntry();
 867						if (le.IsValid()) {
 868							std::string fullpath;
 869							if (le.GetFileSpec().GetDirectory()) {
 870								fullpath = std::string(le.GetFileSpec().GetDirectory()) + "/" + le.GetFileSpec().GetFilename();
 871								current_source_filename = le.GetFileSpec().GetFilename();
 872							} else {
 873								fullpath = le.GetFileSpec().GetFilename();
 874								current_source_filename = fullpath;
 875							}
 876							const std::vector<std::string>& lines = source_cache.get_lines(fullpath);
 877							int total_lines = (int)lines.size();
 878							int ch = main_window_height - 2;
 879							source_scroll_offset = std::max(0, (int)le.GetLine() - ch / 2 - 1);
 880							if (source_scroll_offset + ch > total_lines) {
 881								source_scroll_offset = std::max(0, total_lines - ch);
 882							}
 883						}
 884					}
 885				}
 886			}
 887		}
 888
 889		draw_source_view(frame, 0, 0, split_x, main_window_height, source_cache, source_scroll_offset);
 890		draw_variables_view(frame, split_x, 0, layout_config.sidebar_width, locals_window_height, locals_scroll_offset);
 891		draw_watch_view(frame, split_x, locals_window_height, layout_config.sidebar_width, layout_config.watch_height, watch_expressions, watch_scroll_offset);
 892		draw_log_view(0, main_window_height, split_x, layout_config.log_height, log_buffer, mode, input_buffer, log_scroll_offset);
 893		draw_breakpoints_view(target, split_x, main_window_height, layout_config.sidebar_width, layout_config.log_height);
 894		draw_status_bar(process, mode, width, height);
 895
 896		if (mode == INPUT_MODE_HELP) {
 897			draw_help_view(width, height);
 898		}
 899
 900		tb_present();
 901
 902		struct tb_event ev;
 903		if (tb_poll_event(&ev) == 0) {
 904			if (ev.type == TB_EVENT_KEY) {
 905				if (mode == INPUT_MODE_NORMAL) {
 906					if (ev.ch == 'q') {
 907						running = false;
 908					} else if (ev.ch == 'r') {
 909						if (!process.IsValid() || process.GetState() == eStateExited) {
 910							process = launch_target(target, target_path, debuggee_args, target_env, log_buffer);
 911						} else {
 912							log_msg(log_buffer, "Already running");
 913						}
 914					} else if (ev.ch == 'b') {
 915						mode = INPUT_MODE_BREAKPOINT;
 916						if (!current_source_filename.empty()) {
 917							input_buffer = current_source_filename + ":";
 918						} else {
 919							input_buffer.clear();
 920						}
 921					} else if (ev.ch == 'p') {
 922						mode = INPUT_MODE_VARIABLE;
 923						input_buffer.clear();
 924					} else if (ev.ch == 'w') {
 925						mode = INPUT_MODE_WATCH;
 926						input_buffer.clear();
 927					} else if (ev.ch == 'h') {
 928						mode = INPUT_MODE_HELP;
 929					} else {
 930						if (process.IsValid() && process.GetState() == eStateStopped) {
 931							switch (ev.ch) {
 932								case 'n': if (thread.IsValid()) thread.StepOver(); break;
 933								case 's': if (thread.IsValid()) thread.StepInto(); break;
 934								case 'o': if (thread.IsValid()) thread.StepOut(); break;
 935								case 'c': process.Continue(); break;
 936							}
 937						}
 938
 939						if (ev.key == TB_KEY_ARROW_LEFT && (ev.mod & TB_MOD_CTRL)) {
 940							layout_config.sidebar_width = std::min(width - 20, layout_config.sidebar_width + 2);
 941						} else if (ev.key == TB_KEY_ARROW_RIGHT && (ev.mod & TB_MOD_CTRL)) {
 942							layout_config.sidebar_width = std::max(20, layout_config.sidebar_width - 2);
 943						} else if (ev.key == TB_KEY_ARROW_UP && (ev.mod & TB_MOD_CTRL)) {
 944							layout_config.log_height = std::min(height - 10, layout_config.log_height + 1);
 945						} else if (ev.key == TB_KEY_ARROW_DOWN && (ev.mod & TB_MOD_CTRL)) {
 946							layout_config.log_height = std::max(5, layout_config.log_height - 1);
 947						}
 948					}
 949				} else if (mode == INPUT_MODE_BREAKPOINT || mode == INPUT_MODE_VARIABLE || mode == INPUT_MODE_WATCH) {
 950					if (ev.key == TB_KEY_ESC) {
 951						mode = INPUT_MODE_NORMAL;
 952						input_buffer.clear();
 953					} else if (ev.key == TB_KEY_ENTER) {
 954						if (!input_buffer.empty()) {
 955							if (mode == INPUT_MODE_BREAKPOINT) {
 956								SBBreakpoint bp = create_breakpoint(target, input_buffer);
 957								if (bp.IsValid() && bp.GetNumLocations() > 0) {
 958									log_msg(log_buffer, "Breakpoint added: " + input_buffer);
 959								} else {
 960									log_msg(log_buffer, "Failed/Invalid breakpoint: " + input_buffer);
 961								}
 962							} else if (mode == INPUT_MODE_VARIABLE) {
 963								if (!frame.IsValid()) {
 964									log_msg(log_buffer, "Error: No stack frame available to evaluate '" + input_buffer + "'");
 965								} else {
 966									SBValue val = frame.EvaluateExpression(input_buffer.c_str());
 967									if (val.IsValid() && !val.GetError().Fail()) {
 968										format_variable_log(val, log_buffer, 0, input_buffer);
 969									} else {
 970										std::string err = "Error evaluating '" + input_buffer + "'";
 971										if (val.GetError().GetCString()) {
 972											err += ": ";
 973											err += val.GetError().GetCString();
 974										}
 975										log_msg(log_buffer, err);
 976									}
 977								}
 978							} else if (mode == INPUT_MODE_WATCH) {
 979								watch_expressions.push_back(input_buffer);
 980								log_msg(log_buffer, "Added to watch: " + input_buffer);
 981							}
 982						}
 983						mode = INPUT_MODE_NORMAL;
 984						input_buffer.clear();
 985					} else if (ev.key == TB_KEY_BACKSPACE || ev.key == TB_KEY_BACKSPACE2) {
 986						if (!input_buffer.empty()) input_buffer.pop_back();
 987					} else if (ev.ch != 0) {
 988						input_buffer += (char)ev.ch;
 989					}
 990				} else if (mode == INPUT_MODE_HELP) {
 991					mode = INPUT_MODE_NORMAL;
 992				}
 993			} else if (ev.type == TB_EVENT_MOUSE) {
 994				int main_window_height = tb_height() - layout_config.log_height - layout_config.status_height;
 995
 996				// Log window scrolling
 997				int log_start_y = main_window_height;
 998				int log_end_y = tb_height() - layout_config.status_height;
 999				if (ev.x < split_x && ev.y >= log_start_y && ev.y < log_end_y) {
1000					if (ev.key == TB_KEY_MOUSE_WHEEL_UP) {
1001						int max_scroll = std::max(0, (int)log_buffer.size() - (layout_config.log_height - 2));
1002						if (log_scroll_offset < max_scroll) {
1003							log_scroll_offset++;
1004						}
1005					} else if (ev.key == TB_KEY_MOUSE_WHEEL_DOWN) {
1006						if (log_scroll_offset > 0) {
1007							log_scroll_offset--;
1008						}
1009					}
1010				}
1011
1012				// Source window scrolling
1013				if (ev.x < split_x && ev.y < main_window_height) {
1014					SBLineEntry le = frame.GetLineEntry();
1015					if (le.IsValid()) {
1016						std::string fullpath;
1017						if (le.GetFileSpec().GetDirectory()) {
1018							fullpath = std::string(le.GetFileSpec().GetDirectory()) + "/" + le.GetFileSpec().GetFilename();
1019						} else {
1020							fullpath = le.GetFileSpec().GetFilename();
1021						}
1022						const std::vector<std::string>& lines = source_cache.get_lines(fullpath);
1023						int total_lines = (int)lines.size();
1024						int ch = main_window_height - 2;
1025						int max_scroll = std::max(0, total_lines - ch);
1026
1027						if (ev.key == TB_KEY_MOUSE_WHEEL_UP) {
1028							if (source_scroll_offset > 0) {
1029								source_scroll_offset--;
1030							}
1031						} else if (ev.key == TB_KEY_MOUSE_WHEEL_DOWN) {
1032							if (source_scroll_offset < max_scroll) {
1033								source_scroll_offset++;
1034							}
1035						}
1036					}
1037				}
1038
1039				// Locals window scrolling
1040				int split_x = tb_width() - layout_config.sidebar_width;
1041				int locals_window_height = main_window_height - layout_config.watch_height;
1042				if (ev.x >= split_x && ev.y < locals_window_height) {
1043					std::vector<VarLine> lines;
1044					if (frame.IsValid()) {
1045						SBValueList vars = frame.GetVariables(true, true, false, true);
1046						for (uint32_t i = 0; i < vars.GetSize(); ++i) {
1047							collect_variables_recursive(vars.GetValueAtIndex(i), 0, lines, layout_config.sidebar_width - 2);
1048						}
1049					}
1050					int max_scroll = std::max(0, (int)lines.size() - (locals_window_height - 2));
1051
1052					if (ev.key == TB_KEY_MOUSE_WHEEL_UP) {
1053						if (locals_scroll_offset > 0) {
1054							locals_scroll_offset--;
1055						}
1056					} else if (ev.key == TB_KEY_MOUSE_WHEEL_DOWN) {
1057						if (locals_scroll_offset < max_scroll) {
1058							locals_scroll_offset++;
1059						}
1060					}
1061				}
1062
1063				// Watch window scrolling
1064				if (ev.x >= split_x && ev.y >= locals_window_height && ev.y < main_window_height) {
1065					std::vector<VarLine> lines;
1066					for (const auto& expr : watch_expressions) {
1067						SBValue val = frame.EvaluateExpression(expr.c_str());
1068						if (val.IsValid() && !val.GetError().Fail()) {
1069							collect_variables_recursive(val, 0, lines, layout_config.sidebar_width - 2, expr);
1070						} else {
1071							VarLine vl;
1072							vl.text = expr + " = (error)";
1073							lines.push_back(vl);
1074						}
1075					}
1076					int max_scroll = std::max(0, (int)lines.size() - (layout_config.watch_height - 2));
1077
1078					if (ev.key == TB_KEY_MOUSE_WHEEL_UP) {
1079						if (watch_scroll_offset > 0) {
1080							watch_scroll_offset--;
1081						}
1082					} else if (ev.key == TB_KEY_MOUSE_WHEEL_DOWN) {
1083						if (watch_scroll_offset < max_scroll) {
1084							watch_scroll_offset++;
1085						}
1086					}
1087				}
1088			}
1089		}
1090	}
1091
1092	return 0;
1093}