Compare commits

..

No commits in common. 'master' and 'add-themes' have entirely different histories.

@ -11,25 +11,25 @@ Add support for saving files - Kind of important that - Done
Add config file - Font and themes - Done Add config file - Font and themes - Done
Show whether a file has been saved or not - Done Show whether a file has been saved or not - Done
However, when undo is implemented this implementation may need to change However, when undo is implemented this implementation may need to change
Line wrapping that wraps on words when possible Line wrapping that wraps on words when possible
Ensure that Home, End, Delete keys work - Done Ensure that Home, End, Delete keys work
Add shortcuts Add shortcuts
Add menu bar - should contain, file, edit and settings Add menu bar (Right click menu)
Need to add a right click menu Need to add a right click menu
Add support for opening files from the editor - drag and drop support and file dialog Add support for opening files from the editor - drag and drop support?
Syntax highlighting Syntax highlighting
## Bugs ## Bugs
Holding a key (backspace or arrow keys) doesn't work as expected - Done Holding a key (backspace or arrow keys) doesn't work as expected
## Screenshots ## Screenshots

@ -8,7 +8,7 @@ DEFAULT_WINDOW_MINIMUM_WINDOW_HEIGHT :: 200
// FONT // FONT
DEFAULT_FONT :: "/usr/share/fonts/TTF/FiraCode-Regular.ttf" DEFAULT_FONT :: "/usr/share/fonts/TTF/FiraCode-Regular.ttf"
DEFAULT_FONT_SIZE :: 18 DEFAULT_FONT_SIZE :: 11
// DELAYS // DELAYS
DEFAULT_DELAY :: 100 * time.Millisecond DEFAULT_DELAY :: 100 * time.Millisecond

@ -1,55 +0,0 @@
package file_dialog
// Save file dialog function
show_save_file_dialog :: proc(
title: string = "Save File",
filters: []File_Filter = {},
default_filename: string = "",
default_directory: string = ""
) -> Result {
when ODIN_OS == .Windows {
return show_save_file_dialog_windows(title, filters, default_filename, default_directory)
} else when ODIN_OS == .Linux {
return show_save_file_dialog_linux(title, filters, default_filename, default_directory)
} else {
return {
success = false,
error_message = "Unsupported platform",
}
}
}
// Main cross-platform function
show_open_file_dialog :: proc(
title: string = "Open File",
filters: []File_Filter = {},
allow_multiple: bool = false
) -> Result {
when ODIN_OS == .Windows {
return show_open_file_dialog_windows(title, filters, allow_multiple)
} else when ODIN_OS == .Linux {
return show_open_file_dialog_linux(title, filters, allow_multiple)
} else {
return {
success = false,
error_message = "Unsupported platform",
}
}
}
// Convenience function to clean up results
destroy_result :: proc(result: ^Result) {
if result.file_path != "" {
delete(result.file_path)
}
if len(result.file_paths) > 0 {
for path in result.file_paths {
delete(path)
}
delete(result.file_paths)
}
if result.error_message != "" {
delete(result.error_message)
}
}

@ -1,77 +0,0 @@
#+build linux
#+feature dynamic-literals
package file_dialog
import "core:log"
import "core:strings"
import os2 "core:os/os2"
SAVE_FILE_COMMANDS :: [][]string{
{"yad", "--file", "--save"}
}
OPEN_SINGLE_FILE_COMMANDS :: [][]string{
{"yad", "--file"}
}
show_save_file_dialog_linux :: proc(title: string, filters: []File_Filter, default_filename: string = "", default_directory: string = "") -> (result: Result) {
result.success = false
process_desc: os2.Process_Desc
for command in SAVE_FILE_COMMANDS {
log.debug("attempting to use", command[0], "to get file path")
process_desc.command = command
state, stdout_bytes, stderr_bytes, err := os2.process_exec(process_desc, context.allocator)
if err == .Not_Exist {
log.debug("couldn't find", command[0])
continue
} else if err != nil {
log.error("error whilst getting filename: ", err)
return
}
stdout := string(stdout_bytes)
if len(stdout) == 0 do result.file_path = strings.clone("")
else if stdout[len(stdout)-1] == '\n' do result.file_path = strings.clone(stdout[:len(stdout)-1])
else do result.file_path = strings.clone(string(stdout))
result.success = true
}
return result
}
show_open_file_dialog_linux :: proc(
title: string,
filters: []File_Filter = {},
allow_multiple: bool = false
) -> (result: Result) {
result.success = false
process_desc: os2.Process_Desc
for command in OPEN_SINGLE_FILE_COMMANDS {
log.debug("attempting to use", command[0], "to get file path")
process_desc.command = command
state, stdout_bytes, stderr_bytes, err := os2.process_exec(process_desc, context.allocator)
if err == .Not_Exist {
log.debug("couldn't find", command[0])
continue
} else if err != nil {
log.error("error whilst getting filename: ", err)
return
}
stdout := string(stdout_bytes)
if len(stdout) == 0 do result.file_path = strings.clone("")
else if stdout[len(stdout)-1] == '\n' do result.file_path = strings.clone(stdout[:len(stdout)-1])
else do result.file_path = strings.clone(string(stdout))
result.success = true
}
return result
}

@ -1,15 +0,0 @@
package file_dialog
// Cross-platform file dialog result
Result :: struct {
success: bool,
file_path: string, // For single selection (backward compatibility)
file_paths: []string, // For multiple selection
error_message: string,
}
// File filter for dialog
File_Filter :: struct {
name: string,
pattern: string,
}

@ -1,267 +0,0 @@
#+build windows
package file_dialog
import "core:strings"
import "core:c"
import "core:fmt"
foreign import comdlg32 "system:comdlg32.lib"
foreign import ole32 "system:ole32.lib"
foreign import shell32 "system:shell32.lib"
OPENFILENAME :: struct {
lStructSize: c.ulong,
hwndOwner: rawptr,
hInstance: rawptr,
lpstrFilter: cstring,
lpstrCustomFilter: cstring,
nMaxCustFilter: c.ulong,
nFilterIndex: c.ulong,
lpstrFile: cstring,
nMaxFile: c.ulong,
lpstrFileTitle: cstring,
nMaxFileTitle: c.ulong,
lpstrInitialDir: cstring,
lpstrTitle: cstring,
Flags: c.ulong,
nFileOffset: c.ushort,
nFileExtension: c.ushort,
lpstrDefExt: cstring,
lCustData: c.ulong_ptr,
lpfnHook: rawptr,
lpTemplateName: cstring,
pvReserved: rawptr,
dwReserved: c.ulong,
FlagsEx: c.ulong,
}
OFN_PATHMUSTEXIST :: 0x00000800
OFN_FILEMUSTEXIST :: 0x00001000
OFN_HIDEREADONLY :: 0x00000004
OFN_ALLOWMULTISELECT :: 0x00000200
@(default_calling_convention="stdcall")
foreign comdlg32 {
GetOpenFileNameA :: proc(lpofn: ^OPENFILENAME) -> c.int ---
GetSaveFileNameA :: proc(lpofn: ^OPENFILENAME) -> c.int ---
}
show_open_file_dialog_windows :: proc(title: string, filters: []File_Filter, allow_multiple: bool) -> File_Dialog_Result {
MAX_PATH :: 260
BUFFER_SIZE :: 32768 // Larger buffer for multiple files
file_buffer: [BUFFER_SIZE]c.char
// Build filter string (format: "Description\0*.ext\0Description2\0*.ext2\0\0")
filter_str := strings.builder_make()
defer strings.builder_destroy(&filter_str)
if len(filters) > 0 {
for filter in filters {
strings.write_string(&filter_str, filter.name)
strings.write_byte(&filter_str, 0)
strings.write_string(&filter_str, filter.pattern)
strings.write_byte(&filter_str, 0)
}
} else {
strings.write_string(&filter_str, "All Files")
strings.write_byte(&filter_str, 0)
strings.write_string(&filter_str, "*.*")
strings.write_byte(&filter_str, 0)
}
strings.write_byte(&filter_str, 0) // Double null terminator
filter_cstr := strings.clone_to_cstring(strings.to_string(filter_str))
defer delete(filter_cstr)
title_cstr := strings.clone_to_cstring(title)
defer delete(title_cstr)
flags := OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY
if allow_multiple {
flags |= OFN_ALLOWMULTISELECT
}
ofn := OPENFILENAME{
lStructSize = size_of(OPENFILENAME),
lpstrFile = &file_buffer[0],
nMaxFile = BUFFER_SIZE,
lpstrFilter = filter_cstr,
nFilterIndex = 1,
lpstrTitle = title_cstr,
Flags = flags,
}
if GetOpenFileNameA(&ofn) != 0 {
buffer_str := string(cstring(&file_buffer[0]))
if allow_multiple {
// Parse multiple files format: "directory\0file1\0file2\0\0"
parts := strings.split(buffer_str, "\x00")
defer delete(parts)
if len(parts) > 1 && parts[1] != "" {
// Multiple files selected
directory := parts[0]
file_paths := make([]string, len(parts) - 1)
for i := 1; i < len(parts); i+=1 {
if parts[i] != "" {
full_path := strings.concatenate({directory, "\\", parts[i]})
file_paths[i-1] = full_path
}
}
return {
success = true,
file_path = file_paths[0], // First file for compatibility
file_paths = file_paths,
}
} else {
// Single file selected (even with multiple selection enabled)
return {
success = true,
file_path = strings.clone(buffer_str),
file_paths = {strings.clone(buffer_str)},
}
}
} else {
// Single file selection
return {
success = true,
file_path = strings.clone(buffer_str),
file_paths = {strings.clone(buffer_str)},
}
}
}
return {
success = false,
error_message = "User cancelled or dialog failed",
}
}
show_save_file_dialog_macos :: proc(title: string, filters: []File_Filter, default_filename: string = "", default_directory: string = "") -> File_Dialog_Result {
panel := NSSavePanel_savePanel()
title_nsstr := NSStringFromCString(strings.clone_to_cstring(title))
NSSavePanel_setTitle(panel, title_nsstr)
// Set default filename if provided
if default_filename != "" {
filename_nsstr := NSStringFromCString(strings.clone_to_cstring(default_filename))
NSSavePanel_setNameFieldStringValue(panel, filename_nsstr)
}
// Set default directory if provided
if default_directory != "" {
dir_nsstr := NSStringFromCString(strings.clone_to_cstring(default_directory))
dir_url := NSURL_fileURLWithPath(dir_nsstr)
if dir_url != nil {
NSSavePanel_setDirectoryURL(panel, dir_url)
}
}
// Note: File filters in NSSavePanel require more complex setup
// This is a simplified version - full implementation would use UTType
response := NSSavePanel_runModal(panel)
if response == NSModalResponseOK {
url := NSSavePanel_URL(panel)
if url != nil {
path_nsstr := NSURL_path(url)
path_cstr := NSString_UTF8String(path_nsstr)
file_path := strings.clone(string(path_cstr))
return {
success = true,
file_path = file_path,
file_paths = {file_path},
}
}
}
return {
success = false,
error_message = "User cancelled or dialog failed",
}
}
show_save_file_dialog_windows :: proc(
title: string,
filters: []File_Filter,
default_filename: string = "",
default_directory: string = ""
) -> File_Dialog_Result {
MAX_PATH :: 260
file_buffer: [MAX_PATH]c.char
// Set default filename if provided
if default_filename != "" {
filename_cstr := strings.clone_to_cstring(default_filename)
defer delete(filename_cstr)
// Copy to buffer (ensure null termination)
filename_len := min(len(default_filename), MAX_PATH - 1)
for i in 0..<filename_len {
file_buffer[i] = c.char(default_filename[i])
}
file_buffer[filename_len] = 0
}
// Build filter string
filter_str := strings.builder_make()
defer strings.builder_destroy(&filter_str)
if len(filters) > 0 {
for filter in filters {
strings.write_string(&filter_str, filter.name)
strings.write_byte(&filter_str, 0)
strings.write_string(&filter_str, filter.pattern)
strings.write_byte(&filter_str, 0)
}
} else {
strings.write_string(&filter_str, "All Files")
strings.write_byte(&filter_str, 0)
strings.write_string(&filter_str, "*.*")
strings.write_byte(&filter_str, 0)
}
strings.write_byte(&filter_str, 0) // Double null terminator
filter_cstr := strings.clone_to_cstring(strings.to_string(filter_str))
defer delete(filter_cstr)
title_cstr := strings.clone_to_cstring(title)
defer delete(title_cstr)
// Default directory (optional)
dir_cstr: cstring = nil
if default_directory != "" {
dir_cstr = strings.clone_to_cstring(default_directory)
defer delete(dir_cstr)
}
ofn := OPENFILENAME{
lStructSize = size_of(OPENFILENAME),
lpstrFile = &file_buffer[0],
nMaxFile = MAX_PATH,
lpstrFilter = filter_cstr,
nFilterIndex = 1,
lpstrTitle = title_cstr,
lpstrInitialDir = dir_cstr,
Flags = OFN_PATHMUSTEXIST | OFN_HIDEREADONLY,
}
if GetSaveFileNameA(&ofn) != 0 {
return {
success = true,
file_path = strings.clone(string(cstring(&file_buffer[0]))),
file_paths = {strings.clone(string(cstring(&file_buffer[0])))},
}
}
return {
success = false,
error_message = "User cancelled or dialog failed",
}
}

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

@ -1,31 +0,0 @@
#+feature dynamic-literals
package main
themes := map[string]Colours {
"gruvbox dark hard" = Colours{
background = Colour{29, 32, 33, 255},
text = Colour{227, 218, 186, 255},
status_bar_text = Colour{133, 153, 0, 255},
line_numbers = Colour{146, 131, 116, 255},
line_numbers_background = Colour{40, 40, 40, 255},
active_tab = Colour{60, 56, 54, 255},
inactive_tab = Colour{50, 48, 47, 255},
tab_border = Colour{0, 218, 0, 255},
cursor = Colour{251, 241, 199, 255},
highlight = Colour{61, 63, 64, 255}
},
"solarized dark" = Colours{
background = Colour{0, 43, 54, 255},
text = Colour{131, 148, 150, 255},
status_bar_text = Colour{133, 153, 0, 255},
line_numbers = Colour{88, 110, 117, 255},
line_numbers_background = Colour{7, 54, 66, 255},
active_tab = Colour{7, 54, 66, 255},
inactive_tab = Colour{0, 43, 54, 255},
tab_border = Colour{88, 110, 117, 255},
cursor = Colour{147, 161, 161, 255},
highlight = Colour{61, 63, 64, 255}
},
}

@ -1,7 +1,5 @@
package main package main
import "core:thread"
import rl "vendor:raylib" import rl "vendor:raylib"
Colour :: rl.Color Colour :: rl.Color
@ -10,31 +8,31 @@ Colour :: rl.Color
FontConfig :: struct { FontConfig :: struct {
font: rl.Font, font: rl.Font,
size: f32, size: f32,
name: string name: string,
} }
// Text selection // Text selection
Selection :: struct { Selection :: struct {
start: int, start: int,
end: int, end: int,
active: bool active: bool,
} }
// Wrapped line information // Wrapped line information
WrappedLine :: struct { WrappedLine :: struct {
text: string, text: string,
original_line: int, original_line: int,
character_offset: int character_offset: int,
} }
// File buffer structure // File buffer structure
File_Buffer :: struct { FileBuffer :: struct {
filename: string, filename: string,
content: string, content: string,
modified: bool, modified: bool,
cursor_pos: int, cursor_pos: int,
scroll_y: f32, scroll_y: f32,
selection: Selection selection: Selection,
} }
Colours :: struct { Colours :: struct {
@ -54,11 +52,12 @@ Colours :: struct {
Config :: struct { Config :: struct {
font: string, font: string,
font_size: int, font_size: int,
colours: Colours colours: Colours,
} }
// Application state
App :: struct { App :: struct {
files: [dynamic]File_Buffer, files: [dynamic]FileBuffer,
active_tab: int, active_tab: int,
fonts: [dynamic]FontConfig, fonts: [dynamic]FontConfig,
current_font_index: int, current_font_index: int,
@ -72,7 +71,4 @@ App :: struct {
is_dragging: bool, is_dragging: bool,
copied_text: string, copied_text: string,
config: Config, config: Config,
thread: ^thread.Thread,
thread_finished: bool,
file_dialog_open: bool
} }

Loading…
Cancel
Save