package main import "core:fmt" import "core:log" import "core:os" import "core:strings" import "core:slice" import "core:math" import rl "vendor:raylib" main :: proc() { context.logger = log.create_console_logger(.Debug) defer log.destroy_console_logger(context.logger) args := os.args[1:] if len(args) == 0 { fmt.println("Usage: gui_cat [file2] [file3] ...") return } config, ok := parse_config("config.ini") if !ok { log.fatal("failed to parse config file") return } // Initialize application app := App{ files = make([dynamic]FileBuffer), fonts = make([dynamic]FontConfig), active_tab = 0, current_font_index = 0, line_height = 24, tab_height = 40, window_width = 1200, window_height = 800, line_number_width = 30, mouse_drag_start = -1, is_dragging = false, copied_text = "", config = config } app.text_area_y = app.tab_height + 10 // Initialize Raylib rl.InitWindow(app.window_width, app.window_height, "notepad_squared") rl.SetWindowState(rl.ConfigFlags{.WINDOW_RESIZABLE}) rl.SetWindowMinSize(DEFAULT_WINDOW_MINIMUM_WINDOW_WIDTH, DEFAULT_WINDOW_MINIMUM_WINDOW_HEIGHT) rl.SetTargetFPS(60) // Load fonts load_fonts(&app) // Load files from command line arguments for filename in args { load_file(&app, filename) } if len(app.files) == 0 { fmt.println("No files could be loaded") rl.CloseWindow() return } defer { rl.CloseWindow() delete(app.files) delete(app.fonts) } // Main loop for !rl.WindowShouldClose() { update(&app) draw(&app) } } load_fonts :: proc(app: ^App) { // TODO: Support loading multiple fonts // TODO: Allow searching for a font based on it's name and search fc-list database font := FontConfig{ font = rl.LoadFont(strings.clone_to_cstring(app.config.font)), size = f32(app.config.font_size), name = "Default", } append(&app.fonts, font) app.line_height = app.fonts[app.current_font_index].size + 4 } load_file :: proc(app: ^App, filename: string) { data, ok := os.read_entire_file(filename) if !ok { fmt.printf("Failed to read file: %s\n", filename) return } buffer := FileBuffer{ filename = strings.clone(filename), content = string(data), modified = false, cursor_pos = 0, scroll_y = 0, selection = Selection{start = 0, end = 0, active = false}, } append(&app.files, buffer) } get_wrapped_lines :: proc(content: string, font: ^FontConfig, max_width: f32) -> []WrappedLine { lines := strings.split(content, "\n") defer delete(lines) wrapped_lines := make([dynamic]WrappedLine) character_offset := 0 for line, line_index in lines { if len(line) == 0 { // Empty line append(&wrapped_lines, WrappedLine{ text = "", original_line = line_index, character_offset = character_offset, }) } else { // Wrap long lines current_pos := 0 for current_pos < len(line) { // Find how many characters fit in the width fit_characters := 0 current_width: f32 = 0 for i in current_pos.. max_width && fit_characters > 0 { break } current_width += character_width fit_characters += 1 } // If we couldn't fit even one character, force at least one if fit_characters == 0 { fit_characters = 1 } // Extract the text that fits end_pos := min(current_pos + fit_characters, len(line)) wrapped_text := line[current_pos:end_pos] append(&wrapped_lines, WrappedLine{ text = wrapped_text, original_line = line_index, character_offset = character_offset + current_pos, }) current_pos = end_pos } } character_offset += len(line) + 1 // +1 for newline } return wrapped_lines[:] } get_character_at_position :: proc(app: ^App, mouse_pos: rl.Vector2) -> int { if len(app.files) == 0 || app.active_tab >= len(app.files) do return 0 file := &app.files[app.active_tab] font := &app.fonts[app.current_font_index] content_area_x := 10 + app.line_number_width content_area_y := app.text_area_y content_width := f32(app.window_width - 20) - app.line_number_width - 10 if mouse_pos.x < content_area_x || mouse_pos.y < content_area_y do return 0 wrapped_lines := get_wrapped_lines(file.content, font, content_width) defer delete(wrapped_lines) y_pos := content_area_y - file.scroll_y + 5 for wrapped_line, i in wrapped_lines { if mouse_pos.y >= y_pos && mouse_pos.y < y_pos + app.line_height { // Found the line, now find the character x_offset := mouse_pos.x - content_area_x - 5 if x_offset <= 0 { return wrapped_line.character_offset } for j in 0..=len(wrapped_line.text) { text_slice := wrapped_line.text[:j] text_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(text_slice), font.size, 1).x if x_offset <= text_width { return wrapped_line.character_offset + j } } return wrapped_line.character_offset + len(wrapped_line.text) } y_pos += app.line_height } return len(file.content) } update :: proc(app: ^App) { // Handle window resize if rl.IsWindowResized() { app.window_width = rl.GetScreenWidth() app.window_height = rl.GetScreenHeight() } // Handle tab switching with mouse if rl.IsMouseButtonPressed(.LEFT) { mouse_pos := rl.GetMousePosition() if mouse_pos.y <= app.tab_height { tab_width := f32(app.window_width) / f32(len(app.files)) clicked_tab := int(mouse_pos.x / tab_width) if clicked_tab >= 0 && clicked_tab < len(app.files) { app.active_tab = clicked_tab } } } // Handle text selection with mouse if len(app.files) > 0 && app.active_tab < len(app.files) { current_file := &app.files[app.active_tab] mouse_pos := rl.GetMousePosition() if rl.IsMouseButtonPressed(.LEFT) { if mouse_pos.y > app.text_area_y { character_pos := get_character_at_position(app, mouse_pos) current_file.cursor_pos = character_pos current_file.selection.start = character_pos current_file.selection.end = character_pos current_file.selection.active = false app.mouse_drag_start = character_pos app.is_dragging = true } } if app.is_dragging && rl.IsMouseButtonDown(.LEFT) { if mouse_pos.y > app.text_area_y { character_pos := get_character_at_position(app, mouse_pos) current_file.cursor_pos = character_pos if character_pos != app.mouse_drag_start { current_file.selection.start = min(app.mouse_drag_start, character_pos) current_file.selection.end = max(app.mouse_drag_start, character_pos) current_file.selection.active = true } } } if rl.IsMouseButtonReleased(.LEFT) { app.is_dragging = false } } // Handle keyboard input for active file if len(app.files) > 0 && app.active_tab < len(app.files) { current_file := &app.files[app.active_tab] // Handle text input key := rl.GetCharPressed() for key != 0 { if key >= 32 && key <= 126 { // Printable ASCII if current_file.selection.active { delete_selection(current_file) } insert_character(current_file, rune(key)) } key = rl.GetCharPressed() } // Handle special keys if rl.IsKeyPressed(.ENTER) { if current_file.selection.active { delete_selection(current_file) } insert_character(current_file, '\n') } if rl.IsKeyPressed(.BACKSPACE) { if current_file.selection.active { delete_selection(current_file) } else { delete_character(current_file) } } if rl.IsKeyPressed(.TAB) { if current_file.selection.active { delete_selection(current_file) } insert_character(current_file, '\t') } // Handle cursor movement shift_held := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) if rl.IsKeyPressed(.LEFT) { if !shift_held && current_file.selection.active { current_file.cursor_pos = current_file.selection.start current_file.selection.active = false } else { if !current_file.selection.active && shift_held { current_file.selection.start = current_file.cursor_pos current_file.selection.active = true } if current_file.cursor_pos > 0 { current_file.cursor_pos -= 1 } if shift_held { current_file.selection.end = current_file.cursor_pos } } } if rl.IsKeyPressed(.RIGHT) { if !shift_held && current_file.selection.active { current_file.cursor_pos = current_file.selection.end current_file.selection.active = false } else { if !current_file.selection.active && shift_held { current_file.selection.start = current_file.cursor_pos current_file.selection.active = true } if current_file.cursor_pos < len(current_file.content) { current_file.cursor_pos += 1 } if shift_held { current_file.selection.end = current_file.cursor_pos } } } if rl.IsKeyPressed(.UP) { if !shift_held && current_file.selection.active { current_file.selection.active = false } else if !current_file.selection.active && shift_held { current_file.selection.start = current_file.cursor_pos current_file.selection.active = true } move_cursor_up(current_file) if shift_held { current_file.selection.end = current_file.cursor_pos } } if rl.IsKeyPressed(.DOWN) { if !shift_held && current_file.selection.active { current_file.selection.active = false } else if !current_file.selection.active && shift_held { current_file.selection.start = current_file.cursor_pos current_file.selection.active = true } move_cursor_down(current_file) if shift_held { current_file.selection.end = current_file.cursor_pos } } // Handle scrolling wheel := rl.GetMouseWheelMove() current_file.scroll_y -= wheel * app.line_height * 3 current_file.scroll_y = max(0, current_file.scroll_y) // Handle keyboard shortcuts if rl.IsKeyDown(.LEFT_CONTROL) { if rl.IsKeyPressed(.S) { save_file(current_file) } if rl.IsKeyPressed(.A) { select_all(current_file) } if rl.IsKeyPressed(.C) { copy_selection(app, current_file) } if rl.IsKeyPressed(.V) { paste_text(current_file, app.copied_text) } if rl.IsKeyPressed(.TAB) { app.active_tab = (app.active_tab + 1) % len(app.files) } } // Handle font switching (F1-F9) for i in 0.. 0 && app.active_tab < len(app.files) { draw_file_content(app, &app.files[app.active_tab]) } // Draw status bar draw_status_bar(app) rl.EndDrawing() } draw_tabs :: proc(app: ^App) { if len(app.files) == 0 do return tab_width := f32(app.window_width) / f32(len(app.files)) for i in 0.. 20 { filename = fmt.tprintf("...%s", filename[len(filename)-17:]) } // Add asterisk if modified display_name := filename if app.files[i].modified { display_name = fmt.tprintf("%s*", filename) } font := &app.fonts[app.current_font_index] text_size := rl.MeasureTextEx(font.font, strings.clone_to_cstring(display_name), font.size, 1) text_x := x + tab_width/2 - text_size.x/2 text_y := app.tab_height/2 - text_size.y/2 rl.DrawTextEx( font.font, strings.clone_to_cstring(display_name), {text_x, text_y}, font.size, 1, colours.text ) } } draw_file_content :: proc(app: ^App, file: ^FileBuffer) { font := &app.fonts[app.current_font_index] // Content area (excluding line numbers) content_area := rl.Rectangle{ x = 10 + app.line_number_width, y = app.text_area_y, width = f32(app.window_width - 20) - app.line_number_width, height = f32(app.window_height) - app.text_area_y - 30, } // Line number area line_num_area := rl.Rectangle{ x = 10, y = app.text_area_y, width = app.line_number_width, height = f32(app.window_height) - app.text_area_y - 30, } // Background colours := app.config.colours rl.DrawRectangleRec(line_num_area, colours.line_numbers_background) rl.DrawRectangleLinesEx(line_num_area, 2, colours.line_numbers_background) rl.DrawRectangleRec(content_area, colours.background) rl.DrawRectangleLinesEx(content_area, 2, colours.background) // Get wrapped lines content_width := content_area.width wrapped_lines := get_wrapped_lines(file.content, font, content_width) defer delete(wrapped_lines) // Enable scissor test for scrolling rl.BeginScissorMode( i32(content_area.x), i32(content_area.y), i32(content_area.width), i32(content_area.height) ) // Draw text content and selection y_pos := content_area.y - file.scroll_y + 5 cursor_y := y_pos cursor_x := content_area.x + 5 for wrapped_line, i in wrapped_lines { if y_pos > content_area.y + content_area.height { break } if y_pos + app.line_height > content_area.y { // Draw selection background for this line if file.selection.active { line_start := wrapped_line.character_offset line_end := wrapped_line.character_offset + len(wrapped_line.text) if file.selection.start <= line_end && file.selection.end >= line_start { sel_start_in_line := max(0, file.selection.start - line_start) sel_end_in_line := min(len(wrapped_line.text), file.selection.end - line_start) if sel_start_in_line < sel_end_in_line { before_sel := wrapped_line.text[:sel_start_in_line] sel_x := content_area.x + 5 + rl.MeasureTextEx(font.font, strings.clone_to_cstring(before_sel), font.size, 1).x selected_text := wrapped_line.text[sel_start_in_line:sel_end_in_line] sel_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(selected_text), font.size, 1).x rl.DrawRectangle( i32(sel_x), i32(y_pos), i32(sel_width), i32(app.line_height), app.config.colours.highlight ) } } } // Draw text rl.DrawTextEx( font.font, strings.clone_to_cstring(wrapped_line.text), {content_area.x + 5, y_pos}, font.size, 1, app.config.colours.text ) } // Calculate cursor position if wrapped_line.character_offset <= file.cursor_pos && file.cursor_pos <= wrapped_line.character_offset + len(wrapped_line.text) { cursor_y = y_pos cursor_character_pos := file.cursor_pos - wrapped_line.character_offset cursor_text := wrapped_line.text[:cursor_character_pos] if cursor_character_pos <= len(wrapped_line.text) else wrapped_line.text cursor_x = content_area.x + 5 + rl.MeasureTextEx(font.font, strings.clone_to_cstring(cursor_text), font.size, 1).x } y_pos += app.line_height } // Draw cursor if !file.selection.active && int(rl.GetTime() * 2) % 2 == 0 { // Blinking cursor rl.DrawLine(i32(cursor_x), i32(cursor_y), i32(cursor_x), i32(cursor_y + app.line_height), rl.RED) } rl.EndScissorMode() // Draw line numbers (show original line numbers) rl.BeginScissorMode(i32(line_num_area.x), i32(line_num_area.y), i32(line_num_area.width), i32(line_num_area.height)) y_pos = line_num_area.y - file.scroll_y + 5 current_original_line := -1 for wrapped_line, i in wrapped_lines { if y_pos > line_num_area.y + line_num_area.height { break } if y_pos + app.line_height > line_num_area.y { // Only show line number for the first wrapped line of each original line if wrapped_line.original_line != current_original_line { current_original_line = wrapped_line.original_line line_num_text := fmt.tprintf("%d", wrapped_line.original_line + 1) text_size := rl.MeasureTextEx(font.font, strings.clone_to_cstring(line_num_text), font.size, 1) text_x := line_num_area.x + line_num_area.width - text_size.x - 5 rl.DrawTextEx( font.font, strings.clone_to_cstring(line_num_text), {text_x, y_pos}, font.size, 1, app.config.colours.line_numbers ) } } y_pos += app.line_height } rl.EndScissorMode() } draw_status_bar :: proc(app: ^App) { status_y := app.window_height - 25 rl.DrawRectangle(0, status_y, app.window_width, 25, app.config.colours.status_bar) if len(app.files) > 0 && app.active_tab < len(app.files) { file := &app.files[app.active_tab] font_name := app.fonts[app.current_font_index].name selection_info := "" if file.selection.active { selection_info = fmt.tprintf(" | Selected: %d characters", file.selection.end - file.selection.start) } status_text := fmt.tprintf( "File: %s | Font: %s (F1-F%d) | Cursor: %d%s | Ctrl+S: Save | Ctrl+A: Select All | Ctrl+C/V: Copy/Paste", file.filename, font_name, len(app.fonts), file.cursor_pos, selection_info ) font := &app.fonts[app.current_font_index] rl.DrawTextEx( font.font, strings.clone_to_cstring(status_text), {f32(5), f32(status_y + 5)}, font.size, 1, app.config.colours.status_bar_text ) } } insert_character :: proc(file: ^FileBuffer, character: rune) { if file.cursor_pos <= len(file.content) { before := file.content[:file.cursor_pos] after := file.content[file.cursor_pos:] file.content = fmt.aprintf("%s%c%s", before, character, after) file.cursor_pos += 1 file.modified = true clear_selection(file) } } delete_character :: proc(file: ^FileBuffer) { if file.cursor_pos > 0 { before := file.content[:file.cursor_pos-1] after := file.content[file.cursor_pos:] file.content = fmt.aprintf("%s%s", before, after) file.cursor_pos -= 1 file.modified = true clear_selection(file) } } delete_selection :: proc(file: ^FileBuffer) { if !file.selection.active do return start := min(file.selection.start, file.selection.end) end := max(file.selection.start, file.selection.end) before := file.content[:start] after := file.content[end:] file.content = fmt.aprintf("%s%s", before, after) file.cursor_pos = start file.modified = true clear_selection(file) } clear_selection :: proc(file: ^FileBuffer) { file.selection.active = false file.selection.start = 0 file.selection.end = 0 } select_all :: proc(file: ^FileBuffer) { file.selection.start = 0 file.selection.end = len(file.content) file.selection.active = true file.cursor_pos = len(file.content) } copy_selection :: proc(app: ^App, file: ^FileBuffer) { if !file.selection.active do return start := min(file.selection.start, file.selection.end) end := max(file.selection.start, file.selection.end) app.copied_text = file.content[start:end] // Set clipboard (Raylib function) rl.SetClipboardText(strings.clone_to_cstring(app.copied_text)) } paste_text :: proc(file: ^FileBuffer, text: string) { if file.selection.active { delete_selection(file) } // Get clipboard text clipboard := rl.GetClipboardText() if clipboard != nil { text_to_paste := string(clipboard) before := file.content[:file.cursor_pos] after := file.content[file.cursor_pos:] file.content = fmt.aprintf("%s%s%s", before, text_to_paste, after) file.cursor_pos += len(text_to_paste) file.modified = true } } move_cursor_up :: proc(file: ^FileBuffer) { // For wrapped text, we need to handle cursor movement differently // This is a simplified version - you may want to enhance it further if file.cursor_pos > 0 { // Find the previous newline for i := file.cursor_pos - 1; i >= 0; i -= 1 { if file.content[i] == '\n' { // Move to the previous line prev_newline := -1 for j := i - 1; j >= 0; j -= 1 { if file.content[j] == '\n' { prev_newline = j break } } current_pos_in_line := file.cursor_pos - i - 1 prev_line_length := i - prev_newline - 1 new_pos_in_line := min(current_pos_in_line, prev_line_length) file.cursor_pos = prev_newline + 1 + new_pos_in_line return } } // If no newline found, go to beginning file.cursor_pos = 0 } } move_cursor_down :: proc(file: ^FileBuffer) { // For wrapped text, we need to handle cursor movement differently // This is a simplified version - you may want to enhance it further if file.cursor_pos < len(file.content) { // Find the current line start current_line_start := 0 for i := file.cursor_pos - 1; i >= 0; i -= 1 { if file.content[i] == '\n' { current_line_start = i + 1 break } } // Find the next newline for i := file.cursor_pos; i < len(file.content); i += 1 { if file.content[i] == '\n' { // Move to the next line next_newline := len(file.content) for j := i + 1; j < len(file.content); j += 1 { if file.content[j] == '\n' { next_newline = j break } } current_pos_in_line := file.cursor_pos - current_line_start next_line_length := next_newline - i - 1 new_pos_in_line := min(current_pos_in_line, next_line_length) file.cursor_pos = i + 1 + new_pos_in_line return } } // If no newline found, go to end file.cursor_pos = len(file.content) } } save_file :: proc(file: ^FileBuffer) { success := os.write_entire_file(file.filename, transmute([]u8)file.content) if success { file.modified = false fmt.printf("Saved: %s\n", file.filename) } else { fmt.printf("Failed to save: %s\n", file.filename) } }