@ -2,13 +2,16 @@ package main
import "core:fmt"
import "core:log"
import "core:math"
import "core:os"
import "core:slice"
import "core:strings"
import "core:thread"
import "core:time"
import "core:math"
import rl "vendor:raylib"
import "file_dialog"
main :: proc() {
context.logger = log.create_console_logger(.Debug)
@ -16,11 +19,6 @@ main :: proc() {
args := os.args[1:]
if len(args) == 0 {
fmt.println("Usage: gui_cat <file1> [file2] [file3] ...")
return
}
config, ok := parse_config("config.ini")
if !ok {
log.fatal("failed to parse config file")
@ -29,7 +27,7 @@ main :: proc() {
// Initialize application
app := App{
files = make([dynamic]FileBuffer),
files = make([dynamic]File_ Buffer),
fonts = make([dynamic]FontConfig),
active_tab = 0,
current_font_index = 0,
@ -55,12 +53,16 @@ main :: proc() {
load_fonts(&app)
// Load files from command line arguments
for filename in args {
load_file(&app, filename)
if len(args) > 1 {
for filename in args {
create_file_buffer(&app, filename)
}
} else {
create_file_buffer(&app)
}
if len(app.files) == 0 {
fmt.println("No files could be loaded")
log.error("n o files could be loaded")
rl.CloseWindow()
return
}
@ -73,8 +75,240 @@ main :: proc() {
// Main loop
for !rl.WindowShouldClose() {
update(&app)
draw(&app)
rl.BeginDrawing()
rl.ClearBackground(app.config.colours.background)
for file in app.files {
if app.thread_finished && thread.is_done(app.thread) {
thread.destroy(app.thread)
}
}
// 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
}
}
}
if !app.file_dialog_open {
// 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.IsKeyDown(.ENTER) {
if current_file.selection.active {
delete_selection(current_file)
}
insert_character(current_file, '\n')
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.BACKSPACE) {
if current_file.selection.active {
delete_selection(current_file)
}
delete_character(current_file)
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.TAB) {
if current_file.selection.active {
delete_selection(current_file)
}
insert_character(current_file, '\t')
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.DELETE) {
if current_file.selection.active {
delete_selection(current_file)
}
delete_character(current_file, delete_backwards = true)
time.sleep(DEFAULT_DELAY)
}
// Handle cursor movement
shift_held := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)
if rl.IsKeyDown(.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
}
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.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
}
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.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
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.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
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.HOME) {
move_cursor_to_start_of_line(current_file)
}
if rl.IsKeyDown(.END) {
move_cursor_to_end_of_line(current_file)
}
// 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) || rl.IsKeyDown(.RIGHT_CONTROL) {
if rl.IsKeyPressed(.S) {
app.thread = thread.create_and_start_with_poly_data2(&app, current_file, save_file)
}
if rl.IsKeyPressed(.O) {
app.thread = thread.create_and_start_with_poly_data(&app, open_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) {
if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) {
app.active_tab = (app.active_tab - 1) % len(app.files)
} else {
app.active_tab = (app.active_tab + 1) % len(app.files)
}
}
}
// Handle font switching (F1-F9)
for i in 0..<min(9, len(app.fonts)) {
if rl.IsKeyPressed(rl.KeyboardKey(int(rl.KeyboardKey.F1) + i)) {
app.current_font_index = i
app.line_height = app.fonts[app.current_font_index].size + 4
}
}
}
}
// Draw tabs
draw_tabs(&app)
// Draw active file content
if len(app.files) > 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()
}
}
@ -91,23 +325,62 @@ load_fonts :: proc(app: ^App) {
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)
create_file_buffer :: proc(app: ^App, filename: string = "") -> (ok: bool) {
ok = false
buffer: File_Buffer
if len(filename) == 0 {
buffer = File_Buffer{
filename = "",
content = "",
modified = false,
cursor_pos = 0,
scroll_y = 0,
selection = Selection{start = 0, end = 0, active = false},
}
} else {
data, ok := os.read_entire_file(filename)
if !ok {
fmt.printf("Failed to read file: %s\n", filename)
return
}
buffer = File_Buffer{
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)
ok = true
return
}
open_file :: proc(app: ^App) {
app.file_dialog_open = true
result: file_dialog.Result
result = file_dialog.show_open_file_dialog()
app.file_dialog_open = false
if !result.success {
log.error(result.error_message)
return
}
defer file_dialog.destroy_result(&result)
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},
ok := create_file_buffer(app, result.file_path)
if !ok {
log.error("failed ot create file buffer")
return
}
app.active_tab+=1
append(&app.files, buffer)
return
}
get_wrapped_lines :: proc(content: string, font: ^FontConfig, max_width: f32) -> []WrappedLine {
@ -207,231 +480,7 @@ get_character_at_position :: proc(app: ^App, mouse_pos: rl.Vector2) -> int {
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.IsKeyDown(.ENTER) {
if current_file.selection.active {
delete_selection(current_file)
}
insert_character(current_file, '\n')
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.BACKSPACE) {
if current_file.selection.active {
delete_selection(current_file)
}
delete_character(current_file)
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.TAB) {
if current_file.selection.active {
delete_selection(current_file)
}
insert_character(current_file, '\t')
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.DELETE) {
if current_file.selection.active {
delete_selection(current_file)
}
delete_character(current_file, delete_backwards = true)
time.sleep(DEFAULT_DELAY)
}
// Handle cursor movement
shift_held := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)
if rl.IsKeyDown(.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
}
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.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
}
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.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
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.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
}
time.sleep(DEFAULT_DELAY)
}
if rl.IsKeyDown(.HOME) {
move_cursor_to_start_of_line(current_file)
}
if rl.IsKeyDown(.END) {
move_cursor_to_end_of_line(current_file)
}
// 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) || rl.IsKeyDown(.RIGHT_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) {
if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) {
app.active_tab = (app.active_tab - 1) % len(app.files)
} else {
app.active_tab = (app.active_tab + 1) % len(app.files)
}
}
}
// Handle font switching (F1-F9)
for i in 0..<min(9, len(app.fonts)) {
if rl.IsKeyPressed(rl.KeyboardKey(int(rl.KeyboardKey.F1) + i)) {
app.current_font_index = i
app.line_height = app.fonts[app.current_font_index].size + 4
}
}
}
}
draw :: proc(app: ^App) {
rl.BeginDrawing()
rl.ClearBackground(app.config.colours.background)
// Draw tabs
draw_tabs(app)
// Draw active file content
if len(app.files) > 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) {
@ -479,7 +528,7 @@ draw_tabs :: proc(app: ^App) {
}
}
draw_file_content :: proc(app: ^App, file: ^FileBuffer) {
draw_file_content :: proc(app: ^App, file: ^File_ Buffer) {
font := &app.fonts[app.current_font_index]
// Content area (excluding line numbers)
@ -657,7 +706,7 @@ draw_status_bar :: proc(app: ^App) {
}
}
insert_character :: proc(file: ^FileBuffer, character: rune) {
insert_character :: proc(file: ^File_ Buffer, character: rune) {
if file.cursor_pos <= len(file.content) {
before := file.content[:file.cursor_pos]
after := file.content[file.cursor_pos:]
@ -668,7 +717,7 @@ insert_character :: proc(file: ^FileBuffer, character: rune) {
}
}
delete_character :: proc(file: ^FileBuffer, delete_backwards: bool = false) {
delete_character :: proc(file: ^File_ Buffer, delete_backwards: bool = false) {
if file.cursor_pos > 0 && !delete_backwards {
before := file.content[:file.cursor_pos-1]
after := file.content[file.cursor_pos:]
@ -685,7 +734,7 @@ delete_character :: proc(file: ^FileBuffer, delete_backwards: bool = false) {
}
}
delete_selection :: proc(file: ^FileBuffer) {
delete_selection :: proc(file: ^File_ Buffer) {
if !file.selection.active do return
start := min(file.selection.start, file.selection.end)
@ -699,20 +748,20 @@ delete_selection :: proc(file: ^FileBuffer) {
clear_selection(file)
}
clear_selection :: proc(file: ^FileBuffer) {
clear_selection :: proc(file: ^File_ Buffer) {
file.selection.active = false
file.selection.start = 0
file.selection.end = 0
}
select_all :: proc(file: ^FileBuffer) {
select_all :: proc(file: ^File_ Buffer) {
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) {
copy_selection :: proc(app: ^App, file: ^File_ Buffer) {
if !file.selection.active do return
start := min(file.selection.start, file.selection.end)
@ -723,7 +772,7 @@ copy_selection :: proc(app: ^App, file: ^FileBuffer) {
rl.SetClipboardText(strings.clone_to_cstring(app.copied_text))
}
paste_text :: proc(file: ^FileBuffer, text: string) {
paste_text :: proc(file: ^File_ Buffer, text: string) {
if file.selection.active {
delete_selection(file)
}
@ -740,7 +789,7 @@ paste_text :: proc(file: ^FileBuffer, text: string) {
}
}
move_cursor_up :: proc(file: ^FileBuffer) {
move_cursor_up :: proc(file: ^File_ Buffer) {
// For wrapped text, we need to handle cursor movement differently
if file.cursor_pos > 0 {
// Find the previous newline
@ -767,7 +816,7 @@ move_cursor_up :: proc(file: ^FileBuffer) {
}
}
move_cursor_down :: proc(file: ^FileBuffer) {
move_cursor_down :: proc(file: ^File_ Buffer) {
// For wrapped text, we need to handle cursor movement differently
if file.cursor_pos < len(file.content) {
// Find the current line start
@ -803,7 +852,7 @@ move_cursor_down :: proc(file: ^FileBuffer) {
}
}
move_cursor_to_start_of_line :: proc(file: ^FileBuffer) {
move_cursor_to_start_of_line :: proc(file: ^File_ Buffer) {
// For wrapped text, we need to handle cursor movement differently
if file.cursor_pos < len(file.content) {
// Find the current line start
@ -819,7 +868,7 @@ move_cursor_to_start_of_line :: proc(file: ^FileBuffer) {
}
}
move_cursor_to_end_of_line :: proc(file: ^FileBuffer) {
move_cursor_to_end_of_line :: proc(file: ^File_ Buffer) {
// For wrapped text, we need to handle cursor movement differently
if file.cursor_pos < len(file.content) {
// Find the next newline
@ -835,13 +884,26 @@ move_cursor_to_end_of_line :: proc(file: ^FileBuffer) {
}
}
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)
save_file :: proc(app: ^App, file: ^File_Buffer) {
app.thread_finished = false
if len(file.filename) == 0 {
app.file_dialog_open = true
result := file_dialog.show_save_file_dialog()
app.file_dialog_open = false
if !result.success {
log.error(result.error_message)
}
defer file_dialog.destroy_result(&result)
file.filename = strings.clone(result.file_path)
}
ok := os.write_entire_file(file.filename, transmute([]u8)file.content)
file.modified = !ok
if !ok {
log.error("failed to save file, \"", file.filename, "\"", sep="") // TODO: Show popup window that displays this to the user
return
}
app.thread_finished = true
}