├── .clang-format ├── .github └── workflows │ └── build.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── docs ├── configs.md ├── controls.md ├── example.ninorc ├── img │ ├── nino_v0.0.1.png │ ├── nino_v0.0.2.png │ └── nino_v0.0.3.png └── syntax.md ├── resources ├── README.md ├── bundler.c ├── syntax │ ├── c.json │ ├── cpp.json │ ├── java.json │ ├── json.json │ ├── make.json │ ├── python.json │ ├── rust.json │ └── zig.json └── themes │ ├── dark.nino │ ├── light.nino │ ├── monokai.nino │ ├── ms-dos.nino │ └── solarized.nino └── src ├── action.c ├── action.h ├── config.c ├── config.h ├── defines.h ├── editor.c ├── editor.h ├── file_io.c ├── file_io.h ├── highlight.c ├── highlight.h ├── input.c ├── input.h ├── json.h ├── nino.c ├── os.h ├── os_unix.c ├── os_unix.h ├── os_win32.c ├── os_win32.h ├── output.c ├── output.h ├── prompt.c ├── prompt.h ├── row.c ├── row.h ├── select.c ├── select.h ├── terminal.c ├── terminal.h ├── unicode.c ├── unicode.h ├── utils.c └── utils.h /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | AllowShortIfStatementsOnASingleLine: Never 4 | AllowShortLoopsOnASingleLine: 'false' 5 | BreakBeforeBraces: Attach 6 | IndentWidth: '4' 7 | PointerAlignment: Left 8 | UseTab: Never 9 | 10 | ... 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '.gitignore' 7 | - 'docs/*' 8 | - 'themes/*' 9 | - 'README.md' 10 | - 'LICENSE' 11 | 12 | jobs: 13 | build-linux: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Install dependencies 18 | run: sudo apt-get update && sudo apt-get install -y cmake g++ 19 | - name: Create Build Directory 20 | run: mkdir build && cd build 21 | - name: Configure CMake 22 | run: cmake .. 23 | working-directory: ./build 24 | - name: Build 25 | run: cmake --build . 26 | working-directory: ./build 27 | - name: Upload Linux Binary 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: nino-linux 31 | path: build/nino 32 | 33 | build-windows: 34 | runs-on: windows-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Install dependencies 38 | run: choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' && choco install mingw 39 | - name: Create Build Directory 40 | run: mkdir build && cd build 41 | - name: Configure CMake 42 | run: cmake -G "MinGW Makefiles" .. 43 | working-directory: ./build 44 | - name: Build 45 | run: cmake --build . 46 | working-directory: ./build 47 | - name: Upload Windows Binary 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: nino-windows 51 | path: build/nino.exe 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Codegen 2 | resources/bundle.h 3 | 4 | # Common build directories 5 | /build*/ 6 | 7 | # MacOS Finder files. 8 | .DS_Store 9 | 10 | # CLion work directory 11 | /.idea/ 12 | # CLion build directories 13 | /cmake-build-*/ 14 | 15 | # Visual Studio Code 16 | /.vscode/ 17 | /.cache/ 18 | 19 | # Visual Studio work directory 20 | /.vs/ 21 | # Visual Studio build directory 22 | /out/ 23 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | project(nino VERSION 0.0.6 LANGUAGES C) 4 | 5 | include(GNUInstallDirs) 6 | 7 | set(CMAKE_C_STANDARD 11) 8 | 9 | set(RESOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/resources") 10 | 11 | set (SYNTAX_FILES 12 | ${RESOURCE_DIR}/syntax/c.json 13 | ${RESOURCE_DIR}/syntax/cpp.json 14 | ${RESOURCE_DIR}/syntax/java.json 15 | ${RESOURCE_DIR}/syntax/json.json 16 | ${RESOURCE_DIR}/syntax/make.json 17 | ${RESOURCE_DIR}/syntax/python.json 18 | ${RESOURCE_DIR}/syntax/rust.json 19 | ${RESOURCE_DIR}/syntax/zig.json 20 | ) 21 | 22 | set (BUNDLER_SOURCE "${RESOURCE_DIR}/bundler.c") 23 | 24 | add_executable(bundler ${BUNDLER_SOURCE}) 25 | 26 | set (BUNDLER_BIN $) 27 | set (BUNDLED_FILE "${RESOURCE_DIR}/bundle.h") 28 | 29 | add_custom_command( 30 | OUTPUT ${BUNDLED_FILE} 31 | COMMAND ${BUNDLER_BIN} ${BUNDLED_FILE} ${SYNTAX_FILES} 32 | DEPENDS bundler ${SYNTAX_FILES} 33 | WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} 34 | ) 35 | 36 | set (CORE_SOURCES 37 | src/action.c 38 | src/action.h 39 | src/config.c 40 | src/config.h 41 | src/defines.h 42 | src/editor.c 43 | src/editor.h 44 | src/file_io.c 45 | src/file_io.h 46 | src/highlight.c 47 | src/highlight.h 48 | src/input.c 49 | src/input.h 50 | src/json.h 51 | src/nino.c 52 | src/os.h 53 | src/output.c 54 | src/output.h 55 | src/prompt.c 56 | src/prompt.h 57 | src/row.c 58 | src/row.h 59 | src/select.c 60 | src/select.h 61 | src/terminal.c 62 | src/terminal.h 63 | src/unicode.c 64 | src/unicode.h 65 | src/utils.c 66 | src/utils.h 67 | ) 68 | 69 | if (WIN32) 70 | list(APPEND CORE_SOURCES 71 | src/os_win32.c 72 | src/os_win32.h 73 | ) 74 | else() 75 | list(APPEND CORE_SOURCES 76 | src/os_unix.c 77 | src/os_unix.h 78 | ) 79 | endif() 80 | 81 | add_executable(${PROJECT_NAME} ${CORE_SOURCES} ${BUNDLED_FILE}) 82 | 83 | target_compile_definitions(${PROJECT_NAME} PRIVATE 84 | EDITOR_VERSION="${CMAKE_PROJECT_VERSION}" 85 | ) 86 | 87 | if (MSVC) 88 | target_compile_options(${PROJECT_NAME} PRIVATE /W4 /wd4244 /wd4267 /wd4996) 89 | else() 90 | target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -pedantic) 91 | endif() 92 | 93 | install(TARGETS ${PROJECT_NAME}) 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, evanlin96069 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nino 2 | 3 | [![Build](https://github.com/evanlin96069/nino/actions/workflows/build.yml/badge.svg)](https://github.com/evanlin96069/nino/actions?query=branch%3Amaster) 4 | 5 | ![screenshot](docs/img/nino_v0.0.3.png) 6 | 7 | A small terminal-based text editor written in C. 8 | 9 | Inspired by [kilo](https://github.com/antirez/kilo) 10 | and [snaptoken's Build Your Own Text Editor tutorial](https://viewsourcecode.org/snaptoken/kilo/). 11 | 12 | ## Why? 13 | I'm not used to [Vim](https://www.vim.org/) and I don't like [nano](https://nano-editor.org/) either, so I make my own text editor. (I don't know about [micro](https://micro-editor.github.io/) back then...) 14 | 15 | ## Features 16 | - Basic syntax highlight 17 | - Basic UTF-8 support 18 | - Multiple editor tabs 19 | - Automatic indentation and bracket completion 20 | - Mouse support 21 | - Cut, copy, and paste the selected section 22 | - Multiple undo/redo 23 | - Search with smart case sensitivity 24 | - File explorer 25 | 26 | ## Build 27 | 28 | This project uses CMake for building. Follow these steps to build the project: 29 | 30 | ### Prerequisites 31 | 32 | - CMake (minimum required version, e.g., 3.15) 33 | - A suitable C compiler (GCC, Clang, MSVC, etc.) 34 | 35 | ### Cloning the Repository 36 | 37 | ```bash 38 | git clone https://github.com/evanlin96069/nino.git 39 | cd nino 40 | ``` 41 | 42 | ### Configuring the Build 43 | 44 | Create a build directory and run CMake to configure the project: 45 | 46 | ```bash 47 | mkdir build 48 | cd build 49 | cmake .. 50 | ``` 51 | 52 | ### Building the Project 53 | 54 | To build the project, run: 55 | 56 | ```bash 57 | cmake --build . 58 | ``` 59 | 60 | ### Optional: Installing the Project 61 | 62 | If installation is necessary, you can install the project using: 63 | 64 | ```bash 65 | cmake --install . 66 | ``` 67 | 68 | ## Documentation 69 | - [Configurations](docs/configs.md) 70 | - [Controls](docs/controls.md) 71 | - [Syntax Highlighting](docs/syntax.md) 72 | -------------------------------------------------------------------------------- /docs/configs.md: -------------------------------------------------------------------------------- 1 | # Configurations 2 | Configurations are set by running commands. Use `Ctrl+P` to open the command prompt. 3 | 4 | To run commands on start, create `ninorc` in the configuration directory. 5 | 6 | ## Configuration Directory: 7 | - Linux: `~/.config/nino` 8 | - Windows: `~/.nino` 9 | 10 | ## Execute a Config File 11 | `exec` command executes a config file. 12 | The command will first search for the file in the current directory, then the configuration directory. 13 | 14 | ## Commands and ConVars 15 | | Name | Default | Description | 16 | | - | - | - | 17 | | `tabsize` | 4 | Tab size. | 18 | | `whitespace` | 1 | Use whitespace instead of tab. | 19 | | `autoindent` | 0 | Enable auto indent. | 20 | | `backspace` | 1 | Use hungry backspace. | 21 | | `bracket` | 0 | Use auto bracket completion. | 22 | | `trailing` | 1 | Highlight trailing spaces. | 23 | | `drawspace` | 1 | Render whitespace and tab. | 24 | | `syntax` | 1 | Enable syntax highlight. | 25 | | `helpinfo` | 1 | Show the help information. | 26 | | `ignorecase` | 2 | Use case insensitive search. Set to 2 to use smart case. | 27 | | `mouse` | 1 | Enable mouse mode. | 28 | | `ex_default_width` | 40 | File explorer default width. | 29 | | `ex_show_hidden` | 1 | Show hidden files in the file explorer. | 30 | | `osc52_copy` | 1 | Copy to system clipboard using OSC52. | 31 | | `color` | cmd | Change the color of an element. | 32 | | `exec` | cmd | Execute a config file. | 33 | | `lang` | cmd | Set the syntax highlighting language of the current file. | 34 | | `hldb_load` | cmd | Load a syntax highlighting JSON file. | 35 | | `hldb_reload_all` | cmd | Reload syntax highlighting database. | 36 | | `newline` | cmd | Set the EOL sequence (LF/CRLF). | 37 | | `alias` | cmd | Alias a command. | 38 | | `unalias` | cmd | Remove an alias. | 39 | | `cmd_expand_depth` | 1024 | Max depth for alias expansion. | 40 | | `echo` | cmd | Echo text to console. | 41 | | `clear` | cmd | Clear all console output. | 42 | | `help` | cmd | Find help about a convar/concommand. | 43 | | `find` | cmd | Find concommands with the specified string in their name/help text. | 44 | 45 | ## Color 46 | `color [color]` 47 | 48 | When color code is `000000` it will be transparent. 49 | 50 | ### Default Theme 51 | | Element | Default | 52 | | - | - | 53 | | `bg` | `1e1e1e` | 54 | | `top.fg` | `e5e5e5` | 55 | | `top.bg` | `252525` | 56 | | `top.tabs.fg` | `969696` | 57 | | `top.tabs.bg` | `2d2d2d` | 58 | | `top.select.fg` | `e5e5e5` | 59 | | `top.select.bg` | `575068` | 60 | | `explorer.bg` | `252525` | 61 | | `explorer.select` | `575068` | 62 | | `explorer.directory` | `ecc184` | 63 | | `explorer.file` | `e5e5e5` | 64 | | `explorer.focus` | `2d2d2d` | 65 | | `prompt.fg` | `e5e5e5` | 66 | | `prompt.bg` | `3c3c3c` | 67 | | `status.fg` | `e1dbef` | 68 | | `status.bg` | `575068` | 69 | | `status.lang.fg` | `e1dbef` | 70 | | `status.lang.bg` | `a96b21` | 71 | | `status.pos.fg` | `e1dbef` | 72 | | `status.pos.bg` | `d98a2b` | 73 | | `lineno.fg` | `7f7f7f` | 74 | | `lineno.bg` | `1e1e1e` | 75 | | `cursorline` | `282828` | 76 | | `hl.normal` | `e5e5e5` | 77 | | `hl.comment` | `6a9955` | 78 | | `hl.keyword1` | `c586c0` | 79 | | `hl.keyword2` | `569cd6` | 80 | | `hl.keyword3` | `4ec9b0` | 81 | | `hl.string` | `ce9178` | 82 | | `hl.number` | `b5cea8` | 83 | | `hl.space` | `3f3f3f` | 84 | | `hl.match` | `592e14` | 85 | | `hl.select` | `264f78` | 86 | | `hl.trailing` | `ff6464` | 87 | 88 | ## Example 89 | An [example](example.ninorc) of `ninorc`. 90 | -------------------------------------------------------------------------------- /docs/controls.md: -------------------------------------------------------------------------------- 1 | # Controls 2 | The terminal emulator might have some key binds overlapping with nino, make sure to change them before using. 3 | 4 | ## File 5 | | Action | Keybinding | 6 | | - | - | 7 | | Quit | `Ctrl+Q` | 8 | | Close Tab | `Ctrl+W` | 9 | | Open File | `Ctrl+O` | 10 | | Save | `Ctrl+S` | 11 | | Save All | `Alt+Ctrl+S` | 12 | | Save As | `Ctrl+N` | 13 | | Previous Tab | `Ctrl+[` | 14 | | Next Tab | `Ctrl+]` | 15 | | Focus Explorer | `Ctrl+e` | 16 | | Toggle Explorer | `Ctrl+b` | 17 | 18 | ## Prompt 19 | | Action | Keybinding | 20 | | - | - | 21 | | Prompt | `Ctrl+P` | 22 | 23 | ## Edit 24 | | Action | Keybinding | 25 | | - | - | 26 | | Find | `Ctrl+F` | 27 | | Copy | `Ctrl+C` | 28 | | Paste | `Ctrl+V` | 29 | | Cut | `Ctrl+X` | 30 | | Undo | `Ctrl+Z` | 31 | | Redo | `Ctrl+Y` | 32 | | Copy Line Up | `Shift+Alt+Up` | 33 | | Copy Line Down | `Shift+Alt+Down` | 34 | | Move Line Up | `Alt+Up` | 35 | | Move Line Down | `Alt+Down` | 36 | 37 | ## Navigation 38 | | Action | Keybinding | 39 | | - | - | 40 | | Go To Line | `Ctrl+G` | 41 | | Move Up | `Up` | 42 | | Move Down | `Down` | 43 | | Move Right | `Right` | 44 | | Move Left | `Left` | 45 | | Move Word Right | `Ctrl+Right` | 46 | | Move Word Left | `Ctrl+Left` | 47 | | To Line Start | `Home` | 48 | | To Line End | `End` | 49 | | To File Start | `Ctrl+Home` | 50 | | To File End | `Ctrl+End` | 51 | | To Next Page | `PageUp` | 52 | | To Previous Page | `PageDown` | 53 | | To Next Blank Line | `Ctrl+PageUp` | 54 | | To Previous Blank Line | `Ctrl+PageDown` | 55 | | Scroll Line Up | `Ctrl+Up` | 56 | | Scroll Line Down | `Ctrl+Down` | 57 | 58 | ## Select 59 | | Action | Keybinding | 60 | | - | - | 61 | | Select All | `Ctrl+A` | 62 | | Select Word | `Ctrl+D` | 63 | | Select Move Up | `Shift+Up` | 64 | | Select Move Down | `Shift+Down` | 65 | | Select Move Right | `Shift+Right` | 66 | | Select Move Left | `Shift+Left` | 67 | | Select Move Word Right | `Shift+Ctrl+Right` | 68 | | Select Move Word Left | `Shift+Ctrl+Left` | 69 | | Select To Line Start | `Shift+Home` | 70 | | Select To Line End | `Shift+End` | 71 | | Select To Next Page | `Shift+PageUp` | 72 | | Select To Previous Page | `Shift+PageDown` | 73 | | Select To Next Blank Line | `Shift+Ctrl+PageUp` | 74 | | Select To Previous Blank Line | `Shift+Ctrl+PageDown` | 75 | -------------------------------------------------------------------------------- /docs/example.ninorc: -------------------------------------------------------------------------------- 1 | # cvars 2 | helpinfo 0 3 | autoindent 1 4 | 5 | # toggle autoindent 6 | alias tai "noai" 7 | alias ai "autoindent 1; alias tai noai; echo autoindent on;" 8 | alias noai "autoindent 0; alias tai ai; echo autoindent off;" 9 | -------------------------------------------------------------------------------- /docs/img/nino_v0.0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanlin96069/nino/3ea6c431e47362b9c6f9287c760e6200ed5242ba/docs/img/nino_v0.0.1.png -------------------------------------------------------------------------------- /docs/img/nino_v0.0.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanlin96069/nino/3ea6c431e47362b9c6f9287c760e6200ed5242ba/docs/img/nino_v0.0.2.png -------------------------------------------------------------------------------- /docs/img/nino_v0.0.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanlin96069/nino/3ea6c431e47362b9c6f9287c760e6200ed5242ba/docs/img/nino_v0.0.3.png -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax Highlighting 2 | nino supports simple keywords syntax highlighting. 3 | 4 | Enable syntax highlighting with command `syntax 1`. 5 | 6 | ## Add Syntax Highlighting Data 7 | If you would like to make your own syntax files, 8 | you can put them in: 9 | - Linux: `~/.config/nino/syntax` 10 | - Windows: `~/.nino/syntax` 11 | 12 | ## Syntax Highlighting Data 13 | Syntax highlighting data are stored in JSON files. 14 | 15 | ```JSON 16 | { 17 | "name" : "Example language", 18 | "extensions" : [ 19 | ".extension1", 20 | ".extension2" 21 | ], 22 | "comment": "//", 23 | "multiline-comment": [ 24 | "/*", 25 | "*/" 26 | ], 27 | "keywords1": [ 28 | "for", 29 | "while", 30 | "if", 31 | "else" 32 | ], 33 | "keywords2": [ 34 | "int", 35 | "char", 36 | "float" 37 | ], 38 | "keywords3": [ 39 | "string" 40 | ] 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | # Resource files for nino 2 | 3 | ## syntax 4 | Files in this folder will be bundled in the binary for portability. 5 | If you would like to make your own syntax files, 6 | you can put them in: 7 | - Linux: `~/.config/nino/syntax` 8 | - Windows: `~/.nino/syntax` 9 | 10 | ## themes 11 | These are some example themes. 12 | You can copy them to the configuration directory like normal configuration files. 13 | 14 | Configuration Directory: 15 | - Linux: `~/.config/nino` 16 | - Windows: `~/.nino` 17 | 18 | To apply the theme, run it with the `exec` command. 19 | -------------------------------------------------------------------------------- /resources/bundler.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define ARGS_SHIFT() \ 4 | { \ 5 | argc--; \ 6 | argv++; \ 7 | } \ 8 | while (0) 9 | 10 | int main(int argc, char* argv[]) { 11 | if (argc < 3) { 12 | fprintf(stderr, "Usage: %s files...\n", argv[0]); 13 | return 1; 14 | } 15 | 16 | ARGS_SHIFT(); 17 | 18 | FILE* out = fopen(argv[0], "w"); 19 | if (!out) { 20 | fprintf(stderr, "Failed to open %s to write.\n", argv[1]); 21 | return 1; 22 | } 23 | 24 | ARGS_SHIFT(); 25 | 26 | fprintf(out, "#ifndef BUNDLE_H\n"); 27 | fprintf(out, "#define BUNDLE_H\n\n"); 28 | 29 | for (int i = 0; i < argc; i++) { 30 | FILE* fp = fopen(argv[i], "r"); 31 | if (!fp) { 32 | fprintf(stderr, "Failed to open %s to read.\n", argv[i]); 33 | return 1; 34 | } 35 | 36 | fprintf(out, "const char bundle%d[] = {", i); 37 | 38 | int index = 0; 39 | 40 | int should_read = 1; 41 | while (should_read) { 42 | int byte = fgetc(fp); 43 | if (byte == EOF) { 44 | // Makes it null-terminated 45 | byte = 0; 46 | should_read = 0; 47 | } 48 | 49 | if (index % 10 == 0) { 50 | fprintf(out, "\n "); 51 | } 52 | fprintf(out, "0x%02X, ", byte); 53 | index++; 54 | } 55 | 56 | fprintf(out, "\n};\n\n"); 57 | fclose(fp); 58 | } 59 | 60 | fprintf(out, "const char* bundle[] = {\n"); 61 | for (int i = 0; i < argc; i++) { 62 | fprintf(out, " bundle%d,\n", i); 63 | } 64 | fprintf(out, "};\n\n"); 65 | 66 | fprintf(out, "#endif\n"); 67 | fclose(out); 68 | return 0; 69 | } 70 | -------------------------------------------------------------------------------- /resources/syntax/c.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "C", 3 | "extensions": [ 4 | ".c", 5 | ".h" 6 | ], 7 | "comment": "//", 8 | "multiline-comment": [ 9 | "/*", 10 | "*/" 11 | ], 12 | "keywords1": [ 13 | "break", 14 | "case", 15 | "continue", 16 | "default", 17 | "do", 18 | "else", 19 | "for", 20 | "goto", 21 | "if", 22 | "return", 23 | "switch", 24 | "while", 25 | "#include", 26 | "#define", 27 | "#undef", 28 | "#if", 29 | "#else", 30 | "#elif", 31 | "#endif", 32 | "#", 33 | "#ifdef", 34 | "#ifndef", 35 | "#error", 36 | "#pragma", 37 | "#embed" 38 | ], 39 | "keywords2": [ 40 | "auto", 41 | "char", 42 | "const", 43 | "double", 44 | "enum", 45 | "extern", 46 | "float", 47 | "inline", 48 | "int", 49 | "long", 50 | "register", 51 | "restrict", 52 | "short", 53 | "signed", 54 | "sizeof", 55 | "static", 56 | "struct", 57 | "typedef", 58 | "union", 59 | "unsigned", 60 | "void", 61 | "volatile", 62 | "bool", 63 | "true", 64 | "false", 65 | "stdin", 66 | "stdout", 67 | "stderr", 68 | "NULL", 69 | "__FILE__", 70 | "__LINE__", 71 | "__DATE__", 72 | "__TIME__", 73 | "__TIMESTAMP__" 74 | ], 75 | "keywords3": [ 76 | "int8_t", 77 | "int16_t", 78 | "int32_t", 79 | "int64_t", 80 | "uint8_t", 81 | "uint16_t", 82 | "uint32_t", 83 | "uint64_t", 84 | "intptr_t", 85 | "uintptr_t", 86 | "ptrdiff_t", 87 | "size_t", 88 | "wchar_t" 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /resources/syntax/cpp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "C++", 3 | "extensions": [ 4 | ".cpp", 5 | ".hpp", 6 | ".c++", 7 | ".h++", 8 | ".cc", 9 | ".hh" 10 | ], 11 | "comment": "//", 12 | "multiline-comment": [ 13 | "/*", 14 | "*/" 15 | ], 16 | "keywords1": [ 17 | "break", 18 | "case", 19 | "catch", 20 | "continue", 21 | "co_await", 22 | "co_return", 23 | "co_yield", 24 | "default", 25 | "delete", 26 | "do", 27 | "else", 28 | "for", 29 | "goto", 30 | "if", 31 | "return", 32 | "switch", 33 | "throw", 34 | "try", 35 | "while", 36 | "#include", 37 | "#define", 38 | "#undef", 39 | "#if", 40 | "#else", 41 | "#elif", 42 | "#endif", 43 | "#", 44 | "#ifdef", 45 | "#ifndef", 46 | "#error", 47 | "#pragma" 48 | ], 49 | "keywords2": [ 50 | "asm", 51 | "auto", 52 | "bool", 53 | "char", 54 | "class", 55 | "compl", 56 | "concept", 57 | "const", 58 | "consteval", 59 | "constexpr", 60 | "constinit", 61 | "const_cast", 62 | "double", 63 | "dynamic_cast", 64 | "enum", 65 | "explicit", 66 | "export", 67 | "extern", 68 | "false", 69 | "float", 70 | "friend", 71 | "inline", 72 | "int", 73 | "long", 74 | "mutable", 75 | "namespace", 76 | "new", 77 | "noexcept", 78 | "nullptr", 79 | "operator", 80 | "private", 81 | "protected", 82 | "public", 83 | "register", 84 | "reinterpret_cast", 85 | "restrict", 86 | "short", 87 | "signed", 88 | "sizeof", 89 | "static", 90 | "static_cast", 91 | "struct", 92 | "template", 93 | "this", 94 | "thread_local", 95 | "true", 96 | "typedef", 97 | "union", 98 | "unsigned", 99 | "using", 100 | "virtual", 101 | "void", 102 | "volatile", 103 | "stdin", 104 | "stdout", 105 | "stderr", 106 | "NULL", 107 | "__FILE__", 108 | "__LINE__", 109 | "__DATE__", 110 | "__TIME__", 111 | "__TIMESTAMP__" 112 | ], 113 | "keywords3": [ 114 | "char8_t", 115 | "char16_t", 116 | "char32_t", 117 | "int8_t", 118 | "int16_t", 119 | "int32_t", 120 | "int64_t", 121 | "uint8_t", 122 | "uint16_t", 123 | "uint32_t", 124 | "uint64_t", 125 | "intptr_t", 126 | "uintptr_t", 127 | "ptrdiff_t", 128 | "size_t", 129 | "wchar_t" 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /resources/syntax/java.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Java", 3 | "extensions": [ 4 | ".java" 5 | ], 6 | "comment": "//", 7 | "multiline-comment": [ 8 | "/*", 9 | "*/" 10 | ], 11 | "keywords1": [ 12 | "continue", 13 | "for", 14 | "new", 15 | "switch", 16 | "assert", 17 | "default", 18 | "do", 19 | "if", 20 | "break", 21 | "throw", 22 | "else", 23 | "case", 24 | "return", 25 | "catch", 26 | "try", 27 | "finally", 28 | "while" 29 | ], 30 | "keywords2": [ 31 | "abstract", 32 | "goto", 33 | "package", 34 | "synchronized", 35 | "private", 36 | "this", 37 | "implements", 38 | "protected", 39 | "import", 40 | "public", 41 | "throws", 42 | "enum", 43 | "instanceof", 44 | "transient", 45 | "extends", 46 | "final", 47 | "interface", 48 | "static", 49 | "class", 50 | "strictfp", 51 | "volatile", 52 | "const", 53 | "native", 54 | "super", 55 | "true", 56 | "false", 57 | "null" 58 | ], 59 | "keywords3": [ 60 | "boolean", 61 | "double", 62 | "byte", 63 | "int", 64 | "short", 65 | "char", 66 | "void", 67 | "long", 68 | "float", 69 | "var" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /resources/syntax/json.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JSON", 3 | "extensions": [ 4 | ".json" 5 | ], 6 | "keywords2": [ 7 | "true", 8 | "false", 9 | "null" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /resources/syntax/make.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Makefile", 3 | "extensions": [ 4 | "Makefile", 5 | "makefile", 6 | "GNUmakefile", 7 | ".mak", 8 | ".mk" 9 | ], 10 | "comment": "#", 11 | "keywords1": [ 12 | "define", 13 | "endef", 14 | "undefine", 15 | "ifdef", 16 | "ifndef", 17 | "ifeq", 18 | "ifneq", 19 | "else", 20 | "endif", 21 | "include", 22 | "sinclude", 23 | "override", 24 | "export", 25 | "unexport", 26 | "private", 27 | "vpath", 28 | "-include" 29 | ], 30 | "keywords2": [ 31 | "subst", 32 | "patsubst", 33 | "findstring", 34 | "filter", 35 | "filter-out", 36 | "sort", 37 | "word", 38 | "words", 39 | "wordlist", 40 | "firstword", 41 | "lastword", 42 | "dir", 43 | "notdir", 44 | "suffix", 45 | "basename", 46 | "addsuffix", 47 | "addprefix", 48 | "join", 49 | "wildcard", 50 | "realpath", 51 | "abspath", 52 | "error", 53 | "warning", 54 | "shell", 55 | "origin", 56 | "flavor", 57 | "foreach", 58 | "if", 59 | "or", 60 | "and", 61 | "call", 62 | "eval", 63 | "file", 64 | "value" 65 | ] 66 | } -------------------------------------------------------------------------------- /resources/syntax/python.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python", 3 | "extensions": [ 4 | ".py" 5 | ], 6 | "comment": "#", 7 | "multiline-comment": [ 8 | "'''", 9 | "'''" 10 | ], 11 | "keywords1": [ 12 | "import", 13 | "from", 14 | "with", 15 | "as", 16 | "if", 17 | "else", 18 | "elif", 19 | "for", 20 | "while", 21 | "break", 22 | "continue", 23 | "pass", 24 | "return", 25 | "try", 26 | "except", 27 | "finally", 28 | "raise", 29 | "assert", 30 | "yield", 31 | "del", 32 | "async", 33 | "await", 34 | "match", 35 | "case" 36 | ], 37 | "keywords2": [ 38 | "True", 39 | "False", 40 | "None", 41 | "def", 42 | "class", 43 | "global", 44 | "nonlocal", 45 | "lambda", 46 | "is", 47 | "in", 48 | "and", 49 | "not", 50 | "or", 51 | "self", 52 | "cls", 53 | "NotImplemented" 54 | ], 55 | "keywords3": [ 56 | "bool", 57 | "bytearray", 58 | "bytes", 59 | "classmethod", 60 | "complex", 61 | "dict", 62 | "enumerate", 63 | "filter", 64 | "float", 65 | "frozenset", 66 | "function", 67 | "int", 68 | "list", 69 | "map", 70 | "object", 71 | "property", 72 | "range", 73 | "set", 74 | "slice", 75 | "staticmethod", 76 | "str", 77 | "tuple", 78 | "type", 79 | "zip", 80 | "Exception", 81 | "super", 82 | "BaseException", 83 | "GeneratorExit", 84 | "KeyboardInterrupt", 85 | "SystemExit", 86 | "Exception", 87 | "ArithmeticError", 88 | "FloatingPointError", 89 | "OverflowError", 90 | "ZeroDivisionError", 91 | "AssertionError", 92 | "AttributeError", 93 | "BufferError", 94 | "EOFError", 95 | "ImportError", 96 | "ModuleNotFoundError", 97 | "LookupError", 98 | "IndexError", 99 | "KeyError", 100 | "MemoryError", 101 | "NameError", 102 | "UnboundLocalError", 103 | "OSError", 104 | "BlockingIOError", 105 | "ChildProcessError", 106 | "ConnectionError", 107 | "BrokenPipeError", 108 | "ConnectionAbortedError", 109 | "ConnectionRefusedError", 110 | "ConnectionResetError", 111 | "FileExistsError", 112 | "FileNotFoundError", 113 | "InterruptedError", 114 | "IsADirectoryError", 115 | "NotADirectoryError", 116 | "PermissionError", 117 | "ProcessLookupError", 118 | "TimeoutError", 119 | "ReferenceError", 120 | "RuntimeError", 121 | "NotImplementedError", 122 | "RecursionError", 123 | "StopAsyncIteration", 124 | "StopIteration", 125 | "SyntaxError", 126 | "IndentationError", 127 | "TabError", 128 | "SystemError", 129 | "TypeError", 130 | "ValueError", 131 | "UnicodeError", 132 | "UnicodeDecodeError", 133 | "UnicodeEncodeError", 134 | "UnicodeTranslateError", 135 | "Warning", 136 | "BytesWarning", 137 | "DeprecationWarning", 138 | "FutureWarning", 139 | "ImportWarning", 140 | "PendingDeprecationWarning", 141 | "ResourceWarning", 142 | "RuntimeWarning", 143 | "SyntaxWarning", 144 | "UnicodeWarning", 145 | "UserWarning" 146 | ] 147 | } 148 | -------------------------------------------------------------------------------- /resources/syntax/rust.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust", 3 | "extensions": [ 4 | ".rs" 5 | ], 6 | "comment": "//", 7 | "multiline-comment": [ 8 | "/*", 9 | "*/" 10 | ], 11 | "keywords1": [ 12 | "break", 13 | "continue", 14 | "else", 15 | "for", 16 | "if", 17 | "in", 18 | "loop", 19 | "match", 20 | "return", 21 | "while", 22 | "await" 23 | ], 24 | "keywords2": [ 25 | "as", 26 | "const", 27 | "crate", 28 | "enum", 29 | "extern", 30 | "false", 31 | "fn", 32 | "impl", 33 | "let", 34 | "mod", 35 | "move", 36 | "mut", 37 | "pub", 38 | "ref", 39 | "self", 40 | "Self", 41 | "static", 42 | "struct", 43 | "super", 44 | "trait", 45 | "true", 46 | "type", 47 | "unsafe", 48 | "use", 49 | "where", 50 | "dyn", 51 | "async" 52 | ], 53 | "keywords3": [ 54 | "bool", 55 | "char", 56 | "str", 57 | "i8", 58 | "i16", 59 | "i32", 60 | "i64", 61 | "i128", 62 | "u8", 63 | "u16", 64 | "u32", 65 | "u64", 66 | "u128", 67 | "isize", 68 | "usize", 69 | "f32", 70 | "f64" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /resources/syntax/zig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zig", 3 | "extensions": [ 4 | ".zig" 5 | ], 6 | "comment": "//", 7 | "keywords1": [ 8 | "async", 9 | "await", 10 | "break", 11 | "catch", 12 | "continue", 13 | "defer", 14 | "else", 15 | "errdefer", 16 | "for", 17 | "if", 18 | "inline", 19 | "nosuspend", 20 | "orelse", 21 | "resume", 22 | "return", 23 | "suspend", 24 | "switch", 25 | "try", 26 | "while" 27 | ], 28 | "keywords2": [ 29 | "align", 30 | "allowzero", 31 | "and", 32 | "anyframe", 33 | "anytype", 34 | "asm", 35 | "comptime", 36 | "const", 37 | "enum", 38 | "error", 39 | "export", 40 | "extern", 41 | "fn", 42 | "linksection", 43 | "noalias", 44 | "noinline", 45 | "or", 46 | "packed", 47 | "pub", 48 | "struct", 49 | "test", 50 | "threadlocal", 51 | "union", 52 | "unreachable", 53 | "usingnamespace", 54 | "var", 55 | "true", 56 | "false", 57 | "null", 58 | "undefined" 59 | ], 60 | "keywords3": [ 61 | "i8", 62 | "u8", 63 | "i16", 64 | "u16", 65 | "i32", 66 | "u32", 67 | "i64", 68 | "u64", 69 | "i128", 70 | "u128", 71 | "isize", 72 | "usize", 73 | "c_char", 74 | "c_short", 75 | "c_ushort", 76 | "c_int", 77 | "c_uint", 78 | "c_long", 79 | "c_ulong", 80 | "c_longlong", 81 | "c_ulonglong", 82 | "c_longdouble", 83 | "f16", 84 | "f32", 85 | "f80", 86 | "f128", 87 | "bool", 88 | "anyopaque", 89 | "void", 90 | "noreturn", 91 | "type", 92 | "anyerror", 93 | "comptime_int", 94 | "comptime_float" 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /resources/themes/dark.nino: -------------------------------------------------------------------------------- 1 | color bg 1e1e1e 2 | color top.fg e5e5e5 3 | color top.bg 252525 4 | color top.tabs.fg 969696 5 | color top.tabs.bg 2d2d2d 6 | color top.select.fg e5e5e5 7 | color top.select.bg 575068 8 | color explorer.bg 252525 9 | color explorer.select 575068 10 | color explorer.directory ecc184 11 | color explorer.file e5e5e5 12 | color explorer.focus 2d2d2d 13 | color prompt.fg e5e5e5 14 | color prompt.bg 3c3c3c 15 | color status.fg e1dbef 16 | color status.bg 575068 17 | color status.lang.fg e1dbef 18 | color status.lang.bg a96b21 19 | color status.pos.fg e1dbef 20 | color status.pos.bg d98a2b 21 | color lineno.fg 7f7f7f 22 | color lineno.bg 1e1e1e 23 | color cursorline 282828 24 | color hl.normal e5e5e5 25 | color hl.comment 6a9955 26 | color hl.keyword1 c586c0 27 | color hl.keyword2 569cd6 28 | color hl.keyword3 4ec9b0 29 | color hl.string ce9178 30 | color hl.number b5cea8 31 | color hl.match 592e14 32 | color hl.select 264f78 33 | color hl.trailing ff6464 34 | color hl.space 3f3f3f 35 | -------------------------------------------------------------------------------- /resources/themes/light.nino: -------------------------------------------------------------------------------- 1 | color bg f5f5f5 2 | color top.fg 333333 3 | color top.bg f3f3f3 4 | color top.tabs.fg 555555 5 | color top.tabs.bg d5d5d5 6 | color top.select.fg 333333 7 | color top.select.bg f5f5f5 8 | color explorer.bg f3f3f3 9 | color explorer.select d3dbcd 10 | color explorer.directory 895503 11 | color explorer.file 333333 12 | color explorer.focus ede8ef 13 | color prompt.fg 333333 14 | color prompt.bg ededf5 15 | color status.fg e1dbef 16 | color status.bg 575068 17 | color status.lang.fg e1dbef 18 | color status.lang.bg 634c85 19 | color status.pos.fg e1dbef 20 | color status.pos.bg 4e3c69 21 | color lineno.fg 6d705b 22 | color lineno.bg f5f5f5 23 | color cursorline e4f6d4 24 | color hl.normal 333333 25 | color hl.comment 6a9955 26 | color hl.keyword1 4b69cf 27 | color hl.keyword2 7a3e9d 28 | color hl.keyword3 aa3731 29 | color hl.string 448c27 30 | color hl.number 9c5d27 31 | color hl.match bf9cac 32 | color hl.select bbbbbb 33 | color hl.trailing ff6464 34 | color hl.space 3f3f3f 35 | -------------------------------------------------------------------------------- /resources/themes/monokai.nino: -------------------------------------------------------------------------------- 1 | color bg 272822 2 | color top.fg c5c5c5 3 | color top.bg 1e1f1c 4 | color top.tabs.fg cccccc 5 | color top.tabs.bg 34352f 6 | color top.select.fg ffffff 7 | color top.select.bg 272822 8 | color explorer.bg 1e1f1c 9 | color explorer.select 414339 10 | color explorer.directory ecc184 11 | color explorer.file cccccc 12 | color explorer.focus 272822 13 | color prompt.fg cccccc 14 | color prompt.bg 272822 15 | color status.fg ffffff 16 | color status.bg 414339 17 | color status.lang.fg ffffff 18 | color status.lang.bg fd971f 19 | color status.pos.fg ffffff 20 | color status.pos.bg ac6218 21 | color lineno.fg 90908a 22 | color lineno.bg 272822 23 | color cursorline 2f302b 24 | color hl.normal f8f8f2 25 | color hl.comment 88846f 26 | color hl.keyword1 f92672 27 | color hl.keyword2 66d9ef 28 | color hl.keyword3 a6e22e 29 | color hl.string ffd712 30 | color hl.number ae81ff 31 | color hl.match 673917 32 | color hl.select 515c6a 33 | color hl.trailing ff6464 34 | color hl.space 3f3f3f 35 | -------------------------------------------------------------------------------- /resources/themes/ms-dos.nino: -------------------------------------------------------------------------------- 1 | color bg 0000aa 2 | color top.fg 000001 3 | color top.bg aaaaaa 4 | color top.tabs.fg 000001 5 | color top.tabs.bg aaaaaa 6 | color top.select.fg ffffff 7 | color top.select.bg 000001 8 | color explorer.bg 0000aa 9 | color explorer.select 00aaaa 10 | color explorer.directory ffff55 11 | color explorer.file ffffff 12 | color explorer.focus 0000aa 13 | color prompt.fg 000001 14 | color prompt.bg aaaaaa 15 | color status.fg ffffff 16 | color status.bg 00aaaa 17 | color status.lang.fg ffffff 18 | color status.lang.bg 00aaaa 19 | color status.pos.fg ffffff 20 | color status.pos.bg 00aaaa 21 | color lineno.fg aaaaaa 22 | color lineno.bg 0000aa 23 | color cursorline 000001 24 | color hl.normal ffffff 25 | color hl.comment 00aa00 26 | color hl.keyword1 55ffff 27 | color hl.keyword2 aa0000 28 | color hl.keyword3 55ff55 29 | color hl.string ffff55 30 | color hl.number ffff55 31 | color hl.match aaaaaa 32 | color hl.select aaaaaa 33 | color hl.trailing aa0000 34 | color hl.space 555555 35 | -------------------------------------------------------------------------------- /resources/themes/solarized.nino: -------------------------------------------------------------------------------- 1 | color bg 002b36 2 | color top.fg cccccc 3 | color top.bg 004052 4 | color top.tabs.fg 93a1a1 5 | color top.tabs.bg 003440 6 | color top.select.fg cccccc 7 | color top.select.bg 196e6c 8 | color explorer.bg 00212b 9 | color explorer.select 003440 10 | color explorer.directory ecc184 11 | color explorer.file cccccc 12 | color explorer.focus 1a343c 13 | color prompt.fg cccccc 14 | color prompt.bg 1a343c 15 | color status.fg 999999 16 | color status.bg 00212b 17 | color status.lang.fg cccccc 18 | color status.lang.bg 1a343c 19 | color status.pos.fg cccccc 20 | color status.pos.bg 196e6c 21 | color lineno.fg 999999 22 | color lineno.bg 002b36 23 | color cursorline 073642 24 | color hl.normal 93a1a1 25 | color hl.comment 586e75 26 | color hl.keyword1 859900 27 | color hl.keyword2 93a1a1 28 | color hl.keyword3 cb4b16 29 | color hl.string 2aa198 30 | color hl.number d33682 31 | color hl.match 4d3b24 32 | color hl.select 274642 33 | color hl.trailing ff6464 34 | color hl.space 3f3f3f 35 | -------------------------------------------------------------------------------- /src/action.c: -------------------------------------------------------------------------------- 1 | #include "action.h" 2 | 3 | #include 4 | 5 | #include "editor.h" 6 | #include "terminal.h" 7 | 8 | bool editorUndo(void) { 9 | if (gCurFile->action_current == gCurFile->action_head) 10 | return false; 11 | 12 | switch (gCurFile->action_current->action->type) { 13 | case ACTION_EDIT: { 14 | EditAction* edit = &gCurFile->action_current->action->edit; 15 | editorDeleteText(edit->added_range); 16 | editorPasteText(&edit->deleted_text, edit->deleted_range.start_x, 17 | edit->deleted_range.start_y); 18 | gCurFile->cursor = edit->old_cursor; 19 | } break; 20 | 21 | case ACTION_ATTRI: { 22 | AttributeAction* attri = &gCurFile->action_current->action->attri; 23 | gCurFile->newline = attri->old_newline; 24 | } break; 25 | } 26 | 27 | gCurFile->action_current = gCurFile->action_current->prev; 28 | gCurFile->dirty--; 29 | return true; 30 | } 31 | 32 | bool editorRedo(void) { 33 | if (!gCurFile->action_current->next) 34 | return false; 35 | 36 | gCurFile->action_current = gCurFile->action_current->next; 37 | 38 | switch (gCurFile->action_current->action->type) { 39 | case ACTION_EDIT: { 40 | EditAction* edit = &gCurFile->action_current->action->edit; 41 | editorDeleteText(edit->deleted_range); 42 | editorPasteText(&edit->added_text, edit->added_range.start_x, 43 | edit->added_range.start_y); 44 | gCurFile->cursor = edit->new_cursor; 45 | } break; 46 | 47 | case ACTION_ATTRI: { 48 | AttributeAction* attri = &gCurFile->action_current->action->attri; 49 | gCurFile->newline = attri->new_newline; 50 | } break; 51 | } 52 | 53 | gCurFile->dirty++; 54 | return true; 55 | } 56 | 57 | void editorAppendAction(EditorAction* action) { 58 | if (!action) 59 | return; 60 | 61 | EditorActionList* node = malloc_s(sizeof(EditorActionList)); 62 | node->action = action; 63 | node->next = NULL; 64 | 65 | gCurFile->dirty++; 66 | 67 | editorFreeActionList(gCurFile->action_current->next); 68 | 69 | if (gCurFile->action_current == gCurFile->action_head) { 70 | gCurFile->action_head->next = node; 71 | node->prev = gCurFile->action_head; 72 | gCurFile->action_current = node; 73 | return; 74 | } 75 | 76 | node->prev = gCurFile->action_current; 77 | gCurFile->action_current->next = node; 78 | gCurFile->action_current = gCurFile->action_current->next; 79 | } 80 | 81 | void editorFreeAction(EditorAction* action) { 82 | if (!action) 83 | return; 84 | 85 | if (action->type == ACTION_EDIT) { 86 | editorFreeClipboardContent(&action->edit.deleted_text); 87 | editorFreeClipboardContent(&action->edit.added_text); 88 | } 89 | 90 | free(action); 91 | } 92 | 93 | void editorFreeActionList(EditorActionList* thisptr) { 94 | EditorActionList* temp; 95 | while (thisptr) { 96 | temp = thisptr; 97 | thisptr = thisptr->next; 98 | editorFreeAction(temp->action); 99 | free(temp); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/action.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTION_H 2 | #define ACTION_H 3 | #include 4 | 5 | #include "select.h" 6 | 7 | typedef struct EditorCursor { 8 | int x, y; 9 | bool is_selected; 10 | int select_x; 11 | int select_y; 12 | } EditorCursor; 13 | 14 | typedef struct EditAction { 15 | EditorSelectRange deleted_range; 16 | EditorClipboard deleted_text; 17 | 18 | EditorSelectRange added_range; 19 | EditorClipboard added_text; 20 | 21 | EditorCursor old_cursor; 22 | EditorCursor new_cursor; 23 | } EditAction; 24 | 25 | typedef struct AttributeAction { 26 | int old_newline; 27 | int new_newline; 28 | } AttributeAction; 29 | 30 | typedef enum EditorActionType { 31 | ACTION_EDIT, 32 | ACTION_ATTRI, 33 | } EditorActionType; 34 | 35 | typedef struct EditorAction { 36 | EditorActionType type; 37 | union { 38 | EditAction edit; 39 | AttributeAction attri; 40 | }; 41 | } EditorAction; 42 | 43 | typedef struct EditorActionList { 44 | struct EditorActionList* prev; 45 | struct EditorActionList* next; 46 | EditorAction* action; 47 | } EditorActionList; 48 | 49 | bool editorUndo(void); 50 | bool editorRedo(void); 51 | void editorAppendAction(EditorAction* action); 52 | void editorFreeActionList(EditorActionList* thisptr); 53 | void editorFreeAction(EditorAction* action); 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #include 5 | 6 | #include "defines.h" 7 | #include "utils.h" 8 | 9 | typedef struct EditorConCmd EditorConCmd; 10 | 11 | extern EditorConCmd cvar_tabsize; 12 | extern EditorConCmd cvar_whitespace; 13 | extern EditorConCmd cvar_autoindent; 14 | extern EditorConCmd cvar_backspace; 15 | extern EditorConCmd cvar_bracket; 16 | extern EditorConCmd cvar_trailing; 17 | extern EditorConCmd cvar_drawspace; 18 | extern EditorConCmd cvar_syntax; 19 | extern EditorConCmd cvar_helpinfo; 20 | extern EditorConCmd cvar_ignorecase; 21 | extern EditorConCmd cvar_mouse; 22 | extern EditorConCmd cvar_osc52_copy; 23 | extern EditorConCmd cvar_ex_default_width; 24 | extern EditorConCmd cvar_ex_show_hidden; 25 | 26 | typedef struct EditorColorScheme EditorColorScheme; 27 | extern const EditorColorScheme color_default; 28 | 29 | #define EDITOR_COLOR_COUNT 34 30 | 31 | typedef struct { 32 | const char* label; 33 | Color* color; 34 | } ColorElement; 35 | 36 | extern const ColorElement color_element_map[EDITOR_COLOR_COUNT]; 37 | 38 | #define CONVAR(_name, _help_string, _default_string, _callback) \ 39 | EditorConCmd cvar_##_name = { \ 40 | .name = #_name, \ 41 | .help_string = _help_string, \ 42 | .cvar = {.default_string = _default_string, .callback = _callback}} 43 | 44 | #define CON_COMMAND(_name, _help_string) \ 45 | static void _name##_callback(void); \ 46 | EditorConCmd ccmd_##_name = {.name = #_name, \ 47 | .help_string = _help_string, \ 48 | .callback = _name##_callback}; \ 49 | void _name##_callback(void) 50 | 51 | #define INIT_CONVAR(name) editorInitConVar(&cvar_##name) 52 | #define INIT_CONCOMMAND(name) editorInitConCmd(&ccmd_##name) 53 | 54 | #define CONVAR_GETINT(name) cvar_##name.cvar.int_val 55 | #define CONVAR_GETSTR(name) cvar_##name.cvar.string_val 56 | 57 | #define COMMAND_MAX_ARGC 64 58 | #define COMMAND_MAX_LENGTH 512 59 | 60 | typedef struct EditorConCmdArgs { 61 | int argc; 62 | char* argv[COMMAND_MAX_ARGC]; 63 | } EditorConCmdArgs; 64 | 65 | extern EditorConCmdArgs args; 66 | 67 | typedef void (*CommandCallback)(void); 68 | typedef void (*ConVarCallback)(void); 69 | 70 | typedef struct EditorConVar { 71 | const char* default_string; 72 | char string_val[COMMAND_MAX_LENGTH]; 73 | int int_val; 74 | ConVarCallback callback; 75 | } EditorConVar; 76 | 77 | struct EditorConCmd { 78 | struct EditorConCmd* next; 79 | const char* name; 80 | const char* help_string; 81 | bool has_callback; 82 | union { 83 | CommandCallback callback; 84 | EditorConVar cvar; 85 | }; 86 | }; 87 | 88 | struct EditorColorScheme { 89 | Color bg; 90 | Color top_status[6]; 91 | Color explorer[5]; 92 | Color prompt[2]; 93 | Color status[6]; 94 | Color line_number[2]; 95 | Color cursor_line; 96 | Color highlightFg[HL_FG_COUNT]; 97 | Color highlightBg[HL_BG_COUNT]; 98 | }; 99 | 100 | void editorInitConfig(void); 101 | void editorFreeConfig(void); 102 | bool editorLoadConfig(const char* path); 103 | void editorOpenConfigPrompt(void); 104 | 105 | void editorSetConVar(EditorConVar* thisptr, const char* string_val); 106 | void editorInitConCmd(EditorConCmd* thisptr); 107 | void editorInitConVar(EditorConCmd* thisptr); 108 | EditorConCmd* editorFindCmd(const char* name); 109 | 110 | #endif 111 | -------------------------------------------------------------------------------- /src/defines.h: -------------------------------------------------------------------------------- 1 | #ifndef DEFINES_H 2 | #define DEFINES_H 3 | 4 | #define CTRL_KEY(k) ((k)&0x1F) 5 | #define ALT_KEY(k) ((k) | 0x1B00) 6 | 7 | #define HL_HIGHLIGHT_NUMBERS (1 << 0) 8 | #define HL_HIGHLIGHT_STRINGS (1 << 1) 9 | 10 | enum EditorKey { 11 | UNKNOWN = -1, 12 | ESC = 27, 13 | BACKSPACE = 127, 14 | CHAR_INPUT = 1000, 15 | ARROW_UP, 16 | ARROW_DOWN, 17 | ARROW_LEFT, 18 | ARROW_RIGHT, 19 | DEL_KEY, 20 | HOME_KEY, 21 | END_KEY, 22 | PAGE_UP, 23 | PAGE_DOWN, 24 | SHIFT_UP, 25 | SHIFT_DOWN, 26 | SHIFT_LEFT, 27 | SHIFT_RIGHT, 28 | SHIFT_HOME, 29 | SHIFT_END, 30 | SHIFT_PAGE_UP, 31 | SHIFT_PAGE_DOWN, 32 | ALT_UP, 33 | ALT_DOWN, 34 | SHIFT_ALT_UP, 35 | SHIFT_ALT_DOWN, 36 | CTRL_UP, 37 | CTRL_DOWN, 38 | CTRL_LEFT, 39 | CTRL_RIGHT, 40 | CTRL_HOME, 41 | CTRL_END, 42 | CTRL_PAGE_UP, 43 | CTRL_PAGE_DOWN, 44 | SHIFT_CTRL_UP, 45 | SHIFT_CTRL_DOWN, 46 | SHIFT_CTRL_LEFT, 47 | SHIFT_CTRL_RIGHT, 48 | SHIFT_CTRL_PAGE_UP, 49 | SHIFT_CTRL_PAGE_DOWN, 50 | MOUSE_PRESSED, 51 | MOUSE_RELEASED, 52 | SCROLL_PRESSED, 53 | SCROLL_RELEASED, 54 | MOUSE_MOVE, 55 | WHEEL_UP, 56 | WHEEL_DOWN, 57 | }; 58 | 59 | enum EditorState { 60 | EDIT_MODE = 0, 61 | EXPLORER_MODE, 62 | FIND_MODE, 63 | GOTO_LINE_MODE, 64 | OPEN_FILE_MODE, 65 | CONFIG_MODE, 66 | SAVE_AS_MODE, 67 | }; 68 | 69 | #define HL_FG_MASK 0x0F 70 | #define HL_BG_MASK 0xF0 71 | #define HL_FG_BITS 4 72 | 73 | enum EditorHighlightFg { 74 | HL_NORMAL = 0, 75 | HL_COMMENT, 76 | HL_KEYWORD1, 77 | HL_KEYWORD2, 78 | HL_KEYWORD3, 79 | HL_STRING, 80 | HL_NUMBER, 81 | HL_SPACE, 82 | 83 | HL_FG_COUNT, 84 | }; 85 | 86 | enum EditorHighlightBg { 87 | HL_BG_NORMAL = 0, 88 | HL_BG_MATCH, 89 | HL_BG_SELECT, 90 | HL_BG_TRAILING, 91 | 92 | HL_BG_COUNT, 93 | }; 94 | 95 | enum EditorField { 96 | FIELD_EMPTY, 97 | FIELD_TOP_STATUS, 98 | FIELD_TEXT, 99 | FIELD_LINENO, 100 | FIELD_PROMPT, 101 | FIELD_STATUS, 102 | FIELD_EXPLORER, 103 | FIELD_ERROR, 104 | }; 105 | 106 | #endif 107 | -------------------------------------------------------------------------------- /src/editor.c: -------------------------------------------------------------------------------- 1 | #include "editor.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "defines.h" 7 | #include "highlight.h" 8 | #include "output.h" 9 | #include "prompt.h" 10 | #include "terminal.h" 11 | #include "utils.h" 12 | 13 | Editor gEditor; 14 | EditorFile* gCurFile; 15 | 16 | void editorInit(void) { 17 | memset(&gEditor, 0, sizeof(Editor)); 18 | gEditor.loading = true; 19 | gEditor.state = EDIT_MODE; 20 | gEditor.color_cfg = color_default; 21 | 22 | gEditor.con_front = -1; 23 | 24 | editorInitTerminal(); 25 | editorInitConfig(); 26 | editorInitHLDB(); 27 | 28 | gEditor.explorer.prefered_width = gEditor.explorer.width = 29 | CONVAR_GETINT(ex_default_width); 30 | 31 | // Draw loading 32 | memset(&gEditor.files[0], 0, sizeof(EditorFile)); 33 | gCurFile = &gEditor.files[0]; 34 | editorRefreshScreen(); 35 | } 36 | 37 | void editorFree(void) { 38 | for (int i = 0; i < gEditor.file_count; i++) { 39 | editorFreeFile(&gEditor.files[i]); 40 | } 41 | editorFreeClipboardContent(&gEditor.clipboard); 42 | editorExplorerFree(); 43 | editorFreeHLDB(); 44 | editorFreeConfig(); 45 | } 46 | 47 | void editorInitFile(EditorFile* file) { 48 | memset(file, 0, sizeof(EditorFile)); 49 | file->newline = NL_DEFAULT; 50 | } 51 | 52 | void editorFreeFile(EditorFile* file) { 53 | for (int i = 0; i < file->num_rows; i++) { 54 | editorFreeRow(&file->row[i]); 55 | } 56 | editorFreeActionList(file->action_head); 57 | free(file->row); 58 | free(file->filename); 59 | } 60 | 61 | int editorAddFile(EditorFile* file) { 62 | if (gEditor.file_count >= EDITOR_FILE_MAX_SLOT) { 63 | editorMsg("Already opened too many files!"); 64 | return -1; 65 | } 66 | 67 | EditorFile* current = &gEditor.files[gEditor.file_count]; 68 | 69 | *current = *file; 70 | current->action_head = calloc_s(1, sizeof(EditorActionList)); 71 | current->action_current = current->action_head; 72 | 73 | gEditor.file_count++; 74 | return gEditor.file_count - 1; 75 | } 76 | 77 | void editorRemoveFile(int index) { 78 | if (index < 0 || index > gEditor.file_count) 79 | return; 80 | 81 | EditorFile* file = &gEditor.files[index]; 82 | editorFreeFile(file); 83 | if (file == &gEditor.files[gEditor.file_count]) { 84 | // file is at the end 85 | gEditor.file_count--; 86 | return; 87 | } 88 | memmove(file, &gEditor.files[index + 1], 89 | sizeof(EditorFile) * (gEditor.file_count - index)); 90 | gEditor.file_count--; 91 | } 92 | 93 | void editorChangeToFile(int index) { 94 | if (index < 0 || index >= gEditor.file_count) 95 | return; 96 | gEditor.file_index = index; 97 | gCurFile = &gEditor.files[index]; 98 | 99 | if (gEditor.tab_offset > index || 100 | gEditor.tab_offset + gEditor.tab_displayed <= index) { 101 | gEditor.tab_offset = index; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/editor.h: -------------------------------------------------------------------------------- 1 | #ifndef EDITOR_H 2 | #define EDITOR_H 3 | 4 | #include 5 | 6 | #include "action.h" 7 | #include "config.h" 8 | #include "file_io.h" 9 | #include "os.h" 10 | #include "row.h" 11 | #include "select.h" 12 | 13 | #define EDITOR_FILE_MAX_SLOT 32 14 | 15 | #define EDITOR_CON_COUNT 16 16 | #define EDITOR_CON_LENGTH 255 17 | 18 | #define EDITOR_PROMPT_LENGTH 255 19 | #define EDITOR_RIGHT_PROMPT_LENGTH 32 20 | 21 | typedef struct EditorSyntax EditorSyntax; 22 | 23 | typedef struct EditorFile { 24 | // Cursor position 25 | EditorCursor cursor; 26 | 27 | // Hidden cursor x position 28 | int sx; 29 | 30 | // bracket complete level 31 | int bracket_autocomplete; 32 | 33 | // Editor offsets 34 | int row_offset; 35 | int col_offset; 36 | 37 | // Total line number 38 | int num_rows; 39 | int lineno_width; 40 | 41 | // File info 42 | int dirty; 43 | uint8_t newline; 44 | char* filename; 45 | FileInfo file_info; 46 | 47 | // Text buffers 48 | EditorRow* row; 49 | 50 | // Syntax highlight information 51 | EditorSyntax* syntax; 52 | 53 | // Undo redo 54 | EditorActionList* action_head; 55 | EditorActionList* action_current; 56 | } EditorFile; 57 | 58 | typedef struct Editor { 59 | // Raw screen size 60 | int screen_rows; 61 | int screen_cols; 62 | 63 | // Text field size 64 | int display_rows; 65 | 66 | // Editor mode 67 | bool loading; 68 | int state; 69 | bool mouse_mode; 70 | 71 | // Cursor position for prompt 72 | int px; 73 | 74 | // Copy paste 75 | EditorClipboard clipboard; 76 | 77 | // Color settings 78 | EditorColorScheme color_cfg; 79 | 80 | // ConCmd linked list 81 | EditorConCmd* cvars; 82 | 83 | // Files 84 | EditorFile files[EDITOR_FILE_MAX_SLOT]; 85 | int file_count; 86 | int file_index; 87 | int tab_offset; 88 | int tab_displayed; 89 | 90 | // Syntax highlight 91 | EditorSyntax* HLDB; 92 | 93 | // File explorer 94 | EditorExplorer explorer; 95 | 96 | // Console 97 | int con_front; 98 | int con_rear; 99 | int con_size; 100 | char con_msg[EDITOR_CON_COUNT][EDITOR_CON_LENGTH]; 101 | 102 | // Prompt 103 | char prompt[EDITOR_PROMPT_LENGTH]; 104 | char prompt_right[EDITOR_RIGHT_PROMPT_LENGTH]; 105 | } Editor; 106 | 107 | // Text editor 108 | extern Editor gEditor; 109 | 110 | // Current file 111 | extern EditorFile* gCurFile; 112 | 113 | void editorInit(void); 114 | void editorFree(void); 115 | void editorInitFile(EditorFile* file); 116 | void editorFreeFile(EditorFile* file); 117 | 118 | // Multiple files control 119 | int editorAddFile(EditorFile* file); 120 | void editorRemoveFile(int index); 121 | void editorChangeToFile(int index); 122 | 123 | #endif 124 | -------------------------------------------------------------------------------- /src/file_io.c: -------------------------------------------------------------------------------- 1 | #include "file_io.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "editor.h" 10 | #include "highlight.h" 11 | #include "input.h" 12 | #include "output.h" 13 | #include "prompt.h" 14 | #include "row.h" 15 | 16 | static int isFileOpened(FileInfo info) { 17 | for (int i = 0; i < gEditor.file_count; i++) { 18 | if (areFilesEqual(gEditor.files[i].file_info, info)) { 19 | return i; 20 | } 21 | } 22 | return -1; 23 | } 24 | 25 | static char* editroRowsToString(EditorFile* file, size_t* len) { 26 | size_t total_len = 0; 27 | int nl_len = (file->newline == NL_UNIX) ? 1 : 2; 28 | for (int i = 0; i < file->num_rows; i++) { 29 | total_len += file->row[i].size + nl_len; 30 | } 31 | 32 | // last line no newline 33 | *len = (total_len > 0) ? total_len - nl_len : 0; 34 | 35 | char* buf = malloc_s(total_len); 36 | char* p = buf; 37 | for (int i = 0; i < file->num_rows; i++) { 38 | memcpy(p, file->row[i].data, file->row[i].size); 39 | p += file->row[i].size; 40 | if (i != file->num_rows - 1) { 41 | if (file->newline == NL_DOS) { 42 | *p = '\r'; 43 | p++; 44 | } 45 | *p = '\n'; 46 | p++; 47 | } 48 | } 49 | 50 | return buf; 51 | } 52 | 53 | static void editorExplorerFreeNode(EditorExplorerNode* node) { 54 | if (!node) 55 | return; 56 | 57 | if (node->is_directory) { 58 | for (size_t i = 0; i < node->dir.count; i++) { 59 | editorExplorerFreeNode(node->dir.nodes[i]); 60 | } 61 | 62 | for (size_t i = 0; i < node->file.count; i++) { 63 | editorExplorerFreeNode(node->file.nodes[i]); 64 | } 65 | 66 | free(node->dir.nodes); 67 | free(node->file.nodes); 68 | } 69 | 70 | free(node->filename); 71 | free(node); 72 | } 73 | 74 | bool editorOpen(EditorFile* file, const char* path) { 75 | editorInitFile(file); 76 | 77 | FileType type = getFileType(path); 78 | switch (type) { 79 | case FT_REG: { 80 | FileInfo file_info = getFileInfo(path); 81 | if (file_info.error) { 82 | editorMsg("Can't load \"%s\"! Failed to get file info.", path); 83 | return false; 84 | } 85 | file->file_info = file_info; 86 | int open_index = isFileOpened(file_info); 87 | 88 | if (open_index != -1) { 89 | editorChangeToFile(open_index); 90 | return false; 91 | } 92 | } break; 93 | 94 | case FT_DIR: 95 | if (gEditor.explorer.node) { 96 | editorExplorerFreeNode(gEditor.explorer.node); 97 | } 98 | changeDir(path); 99 | gEditor.explorer.node = editorExplorerCreate("."); 100 | gEditor.explorer.node->is_open = true; 101 | editorExplorerRefresh(); 102 | 103 | gEditor.explorer.offset = 0; 104 | gEditor.explorer.selected_index = 0; 105 | return false; 106 | 107 | case FT_DEV: 108 | editorMsg("Can't load \"%s\"! It's a device file.", path); 109 | return false; 110 | 111 | default: 112 | break; 113 | } 114 | 115 | FILE* fp = openFile(path, "rb"); 116 | if (!fp) { 117 | if (errno != ENOENT) { 118 | editorMsg("Can't load \"%s\"! %s", path, strerror(errno)); 119 | return false; 120 | } 121 | 122 | // file doesn't exist 123 | char parent_dir[EDITOR_PATH_MAX]; 124 | snprintf(parent_dir, sizeof(parent_dir), "%s", path); 125 | getDirName(parent_dir); 126 | if (access(parent_dir, 0) != 0) { // F_OK 127 | editorMsg("Can't create \"%s\"! %s", path, strerror(errno)); 128 | return false; 129 | } 130 | if (access(parent_dir, 2) != 0) { // W_OK 131 | editorMsg("Can't write to \"%s\"! %s", path, strerror(errno)); 132 | return false; 133 | } 134 | } 135 | 136 | const char* full_path = getFullPath(path); 137 | size_t path_len = strlen(full_path) + 1; 138 | free(file->filename); 139 | file->filename = malloc_s(path_len); 140 | memcpy(file->filename, full_path, path_len); 141 | 142 | editorSelectSyntaxHighlight(file); 143 | 144 | file->dirty = 0; 145 | 146 | if (!fp) { 147 | editorInsertRow(file, file->cursor.y, "", 0); 148 | return true; 149 | } 150 | 151 | bool has_end_nl = true; 152 | bool has_cr = false; 153 | size_t at = 0; 154 | size_t cap = 16; 155 | 156 | char* line = NULL; 157 | size_t n = 0; 158 | int64_t len; 159 | 160 | file->row = malloc_s(sizeof(EditorRow) * cap); 161 | 162 | while ((len = getLine(&line, &n, fp)) != -1) { 163 | has_end_nl = false; 164 | while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { 165 | if (line[len - 1] == '\r') 166 | has_cr = true; 167 | has_end_nl = true; 168 | len--; 169 | } 170 | // editorInsertRow but faster 171 | if (at >= cap) { 172 | cap *= 2; 173 | file->row = realloc_s(file->row, sizeof(EditorRow) * cap); 174 | } 175 | file->row[at].size = len; 176 | file->row[at].data = line; 177 | 178 | file->row[at].hl = NULL; 179 | file->row[at].hl_open_comment = 0; 180 | editorUpdateRow(file, &file->row[at]); 181 | 182 | line = NULL; 183 | n = 0; 184 | at++; 185 | } 186 | file->row = realloc_s(file->row, sizeof(EditorRow) * at); 187 | file->num_rows = at; 188 | file->lineno_width = getDigit(file->num_rows) + 2; 189 | 190 | if (has_end_nl) { 191 | editorInsertRow(file, file->num_rows, "", 0); 192 | } 193 | 194 | if (file->num_rows < 2) { 195 | file->newline = NL_DEFAULT; 196 | } else if (has_cr) { 197 | file->newline = NL_DOS; 198 | } else if (file->num_rows) { 199 | file->newline = NL_UNIX; 200 | } 201 | 202 | free(line); 203 | fclose(fp); 204 | 205 | return true; 206 | } 207 | 208 | void editorSave(EditorFile* file, int save_as) { 209 | if (!file->filename || save_as) { 210 | char* path = editorPrompt("Save as: %s", SAVE_AS_MODE, NULL); 211 | if (!path) { 212 | editorMsg("Save aborted."); 213 | return; 214 | } 215 | 216 | // Check path is valid 217 | FILE* fp = openFile(path, "wb"); 218 | if (!fp) { 219 | editorMsg("Can't save \"%s\"! %s", path, strerror(errno)); 220 | return; 221 | } 222 | fclose(fp); 223 | 224 | const char* full_path = getFullPath(path); 225 | size_t path_len = strlen(full_path) + 1; 226 | free(file->filename); 227 | file->filename = malloc_s(path_len); 228 | memcpy(file->filename, full_path, path_len); 229 | 230 | editorSelectSyntaxHighlight(file); 231 | } 232 | 233 | size_t len; 234 | char* buf = editroRowsToString(file, &len); 235 | 236 | FILE* fp = openFile(file->filename, "wb"); 237 | if (fp) { 238 | if (fwrite(buf, sizeof(char), len, fp) == len) { 239 | fclose(fp); 240 | free(buf); 241 | file->dirty = 0; 242 | editorMsg("%d bytes written to disk.", len); 243 | return; 244 | } 245 | fclose(fp); 246 | } 247 | free(buf); 248 | editorMsg("Can't save \"%s\"! %s", file->filename, strerror(errno)); 249 | } 250 | 251 | void editorOpenFilePrompt(void) { 252 | if (gEditor.file_count >= EDITOR_FILE_MAX_SLOT) { 253 | editorMsg("Reached max file slots! Cannot open more files.", 254 | strerror(errno)); 255 | return; 256 | } 257 | 258 | char* path = editorPrompt("Open: %s", OPEN_FILE_MODE, NULL); 259 | if (!path) 260 | return; 261 | 262 | EditorFile file; 263 | if (editorOpen(&file, path)) { 264 | int index = editorAddFile(&file); 265 | gEditor.state = EDIT_MODE; 266 | // hack: refresh screen to update gEditor.tab_displayed 267 | editorRefreshScreen(); 268 | editorChangeToFile(index); 269 | } 270 | 271 | free(path); 272 | } 273 | 274 | static void insertExplorerNode(EditorExplorerNode* node, 275 | EditorExplorerNodeData* data) { 276 | size_t i; 277 | data->nodes = 278 | realloc_s(data->nodes, (data->count + 1) * sizeof(EditorExplorerNode*)); 279 | 280 | for (i = 0; i < data->count; i++) { 281 | if (strcmp(data->nodes[i]->filename, node->filename) > 0) { 282 | memmove(&data->nodes[i + 1], &data->nodes[i], 283 | (data->count - i) * sizeof(EditorExplorerNode*)); 284 | break; 285 | } 286 | } 287 | 288 | data->nodes[i] = node; 289 | data->count++; 290 | } 291 | 292 | EditorExplorerNode* editorExplorerCreate(const char* path) { 293 | EditorExplorerNode* node = malloc_s(sizeof(EditorExplorerNode)); 294 | 295 | int len = strlen(path); 296 | node->filename = malloc_s(len + 1); 297 | snprintf(node->filename, len + 1, "%s", path); 298 | 299 | node->is_directory = (getFileType(path) == FT_DIR); 300 | node->is_open = false; 301 | node->loaded = false; 302 | node->depth = 0; 303 | node->dir.count = 0; 304 | node->dir.nodes = NULL; 305 | node->file.count = 0; 306 | node->file.nodes = NULL; 307 | 308 | return node; 309 | } 310 | 311 | void editorExplorerLoadNode(EditorExplorerNode* node) { 312 | if (!node->is_directory) 313 | return; 314 | 315 | DirIter iter = dirFindFirst(node->filename); 316 | if (iter.error) 317 | return; 318 | 319 | do { 320 | const char* filename = dirGetName(&iter); 321 | if (CONVAR_GETINT(ex_show_hidden) == 0 && filename[0] == '.') 322 | continue; 323 | if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0) 324 | continue; 325 | 326 | char entry_path[EDITOR_PATH_MAX]; 327 | snprintf(entry_path, sizeof(entry_path), PATH_CAT("%s", "%s"), 328 | node->filename, filename); 329 | 330 | EditorExplorerNode* child = editorExplorerCreate(entry_path); 331 | if (!child) 332 | continue; 333 | 334 | child->depth = node->depth + 1; 335 | 336 | if (child->is_directory) { 337 | insertExplorerNode(child, &node->dir); 338 | } else { 339 | insertExplorerNode(child, &node->file); 340 | } 341 | } while (dirNext(&iter)); 342 | dirClose(&iter); 343 | 344 | node->loaded = true; 345 | } 346 | 347 | static void flattenNode(EditorExplorerNode* node) { 348 | if (node != gEditor.explorer.node) 349 | vector_push(gEditor.explorer.flatten, node); 350 | 351 | if (node->is_directory && node->is_open) { 352 | if (!node->loaded) 353 | editorExplorerLoadNode(node); 354 | 355 | for (size_t i = 0; i < node->dir.count; i++) { 356 | flattenNode(node->dir.nodes[i]); 357 | } 358 | 359 | for (size_t i = 0; i < node->file.count; i++) { 360 | flattenNode(node->file.nodes[i]); 361 | } 362 | } 363 | } 364 | 365 | void editorExplorerRefresh(void) { 366 | gEditor.explorer.flatten.size = 0; 367 | gEditor.explorer.flatten.capacity = 0; 368 | free(gEditor.explorer.flatten.data); 369 | flattenNode(gEditor.explorer.node); 370 | vector_shrink(gEditor.explorer.flatten); 371 | } 372 | 373 | void editorExplorerFree(void) { 374 | editorExplorerFreeNode(gEditor.explorer.node); 375 | free(gEditor.explorer.flatten.data); 376 | } 377 | -------------------------------------------------------------------------------- /src/file_io.h: -------------------------------------------------------------------------------- 1 | #ifndef FILE_IO_H 2 | #define FILE_IO_H 3 | 4 | #include 5 | #include 6 | 7 | #include "utils.h" 8 | 9 | typedef struct EditorExplorerNodeData { 10 | struct EditorExplorerNode** nodes; 11 | size_t count; 12 | } EditorExplorerNodeData; 13 | 14 | typedef struct EditorExplorerNode { 15 | char* filename; 16 | bool is_directory; 17 | bool is_open; // Is directory open in the explorer 18 | bool loaded; // Is directory loaded 19 | int depth; 20 | size_t dir_count; 21 | EditorExplorerNodeData dir; 22 | EditorExplorerNodeData file; 23 | } EditorExplorerNode; 24 | 25 | typedef struct EditorExplorer { 26 | int prefered_width; 27 | int width; 28 | int offset; 29 | int selected_index; 30 | EditorExplorerNode* node; // Root node of explorer tree 31 | VECTOR(EditorExplorerNode*) flatten; 32 | } EditorExplorer; 33 | 34 | typedef struct EditorFile EditorFile; 35 | 36 | bool editorOpen(EditorFile* file, const char* filename); 37 | void editorSave(EditorFile* file, int save_as); 38 | void editorOpenFilePrompt(void); 39 | 40 | EditorExplorerNode* editorExplorerCreate(const char* path); 41 | void editorExplorerLoadNode(EditorExplorerNode* node); 42 | void editorExplorerRefresh(void); 43 | void editorExplorerFree(void); 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /src/highlight.c: -------------------------------------------------------------------------------- 1 | #include "highlight.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../resources/bundle.h" 9 | #include "config.h" 10 | #include "os.h" 11 | 12 | #define JSON_IMPLEMENTATION 13 | #define JSON_MALLOC malloc_s 14 | #include "json.h" 15 | 16 | void editorUpdateSyntax(EditorFile* file, EditorRow* row) { 17 | row->hl = realloc_s(row->hl, row->size); 18 | if (row->hl) { 19 | // realloc might returns NULL when row->size == 0 20 | memset(row->hl, HL_NORMAL, row->size); 21 | } 22 | 23 | EditorSyntax* s = file->syntax; 24 | 25 | if (!CONVAR_GETINT(syntax) || !s) 26 | goto update_trailing; 27 | 28 | const char* scs = s->singleline_comment_start; 29 | const char* mcs = s->multiline_comment_start; 30 | const char* mce = s->multiline_comment_end; 31 | 32 | int scs_len = scs ? strlen(scs) : 0; 33 | int mcs_len = mcs ? strlen(mcs) : 0; 34 | int mce_len = mce ? strlen(mce) : 0; 35 | 36 | int prev_sep = 1; 37 | int in_string = 0; 38 | int row_index = (int)(row - file->row); 39 | int in_comment = 40 | (row_index > 0 && file->row[row_index - 1].hl_open_comment); 41 | 42 | int i = 0; 43 | while (i < row->size) { 44 | char c = row->data[i]; 45 | 46 | if (scs_len && !in_string && !in_comment) { 47 | if (strncmp(&row->data[i], scs, scs_len) == 0) { 48 | memset(&row->hl[i], HL_COMMENT, row->size - i); 49 | break; 50 | } 51 | } 52 | 53 | if (mcs_len && mce_len && !in_string) { 54 | if (in_comment) { 55 | row->hl[i] = HL_COMMENT; 56 | if (strncmp(&row->data[i], mce, mce_len) == 0) { 57 | memset(&row->hl[i], HL_COMMENT, mce_len); 58 | i += mce_len; 59 | in_comment = 0; 60 | prev_sep = 1; 61 | } 62 | i++; 63 | continue; 64 | } else if (strncmp(&row->data[i], mcs, mcs_len) == 0) { 65 | memset(&row->hl[i], HL_COMMENT, mcs_len); 66 | i += mcs_len; 67 | in_comment = 1; 68 | continue; 69 | } 70 | } 71 | 72 | if (s->flags & HL_HIGHLIGHT_STRINGS) { 73 | if (in_string) { 74 | row->hl[i] = HL_STRING; 75 | if (c == '\\' && i + 1 < row->size) { 76 | row->hl[i + 1] = HL_STRING; 77 | i += 2; 78 | continue; 79 | } 80 | if (c == in_string) 81 | in_string = 0; 82 | i++; 83 | prev_sep = 1; 84 | continue; 85 | } else if (c == '"' || c == '\'') { 86 | in_string = c; 87 | row->hl[i] = HL_STRING; 88 | i++; 89 | continue; 90 | } 91 | } 92 | 93 | if (s->flags & HL_HIGHLIGHT_NUMBERS) { 94 | if ((isdigit(c) || c == '.') && prev_sep) { 95 | int start = i; 96 | i++; 97 | if (c == '0') { 98 | if (row->data[i] == 'x' || row->data[i] == 'X') { 99 | // hex 100 | i++; 101 | while (isdigit(row->data[i]) || 102 | (row->data[i] >= 'a' && row->data[i] <= 'f') || 103 | (row->data[i] >= 'A' && row->data[i] <= 'F')) { 104 | i++; 105 | } 106 | } else if (row->data[i] >= '0' && row->data[i] <= '7') { 107 | // oct 108 | i++; 109 | while (row->data[i] >= '0' && row->data[i] <= '7') { 110 | i++; 111 | } 112 | } else if (row->data[i] == '.') { 113 | // float 114 | i++; 115 | while (isdigit(row->data[i])) { 116 | i++; 117 | } 118 | } 119 | } else { 120 | while (isdigit(row->data[i])) { 121 | i++; 122 | } 123 | if (c != '.' && row->data[i] == '.') { 124 | i++; 125 | while (isdigit(row->data[i])) { 126 | i++; 127 | } 128 | } 129 | } 130 | if (c == '.' && i - start == 1) 131 | continue; 132 | 133 | if (row->data[i] == 'f' || row->data[i] == 'F') 134 | i++; 135 | if (isSeparator(row->data[i]) || isspace(row->data[i])) 136 | memset(&row->hl[start], HL_NUMBER, i - start); 137 | prev_sep = 0; 138 | continue; 139 | } 140 | } 141 | 142 | if (prev_sep) { 143 | bool found_keyword = false; 144 | for (int kw = 0; kw < 3; kw++) { 145 | for (size_t j = 0; j < s->keywords[kw].size; j++) { 146 | int klen = strlen(s->keywords[kw].data[j]); 147 | int keyword_type = HL_KEYWORD1 + kw; 148 | if (strncmp(&row->data[i], s->keywords[kw].data[j], klen) == 149 | 0 && 150 | isNonIdentifierChar(row->data[i + klen])) { 151 | found_keyword = true; 152 | memset(&row->hl[i], keyword_type, klen); 153 | i += klen; 154 | break; 155 | } 156 | } 157 | if (found_keyword) { 158 | break; 159 | } 160 | } 161 | 162 | if (found_keyword) { 163 | prev_sep = 0; 164 | continue; 165 | } 166 | } 167 | prev_sep = isNonIdentifierChar(c); 168 | i++; 169 | } 170 | int changed = (row->hl_open_comment != in_comment); 171 | row->hl_open_comment = in_comment; 172 | if (changed && row_index + 1 < file->num_rows) 173 | editorUpdateSyntax(file, &file->row[row_index + 1]); 174 | 175 | update_trailing: 176 | for (i = row->size - 1; i >= 0; i--) { 177 | if (row->data[i] == ' ' || row->data[i] == '\t') { 178 | row->hl[i] = HL_BG_TRAILING << HL_FG_BITS; 179 | } else { 180 | break; 181 | } 182 | } 183 | } 184 | 185 | void editorSetSyntaxHighlight(EditorFile* file, EditorSyntax* syntax) { 186 | file->syntax = syntax; 187 | for (int i = 0; i < file->num_rows; i++) { 188 | editorUpdateSyntax(file, &file->row[i]); 189 | } 190 | } 191 | 192 | void editorSelectSyntaxHighlight(EditorFile* file) { 193 | file->syntax = NULL; 194 | if (file->filename == NULL) 195 | return; 196 | 197 | char* ext = strrchr(file->filename, '.'); 198 | 199 | for (EditorSyntax* s = gEditor.HLDB; s; s = s->next) { 200 | for (size_t i = 0; i < s->file_exts.size; i++) { 201 | int is_ext = (s->file_exts.data[i][0] == '.'); 202 | if ((is_ext && ext && strCaseCmp(ext, s->file_exts.data[i]) == 0) || 203 | (!is_ext && strCaseStr(file->filename, s->file_exts.data[i]))) { 204 | editorSetSyntaxHighlight(file, s); 205 | return; 206 | } 207 | } 208 | } 209 | } 210 | 211 | #define ARENA_SIZE (1 << 12) 212 | 213 | static JsonArena hldb_arena; 214 | 215 | static void editorLoadNinoConfigHLDB(void); 216 | static void editorLoadBundledHLDB(void); 217 | 218 | void editorInitHLDB(void) { 219 | json_arena_init(&hldb_arena, ARENA_SIZE); 220 | 221 | editorLoadNinoConfigHLDB(); 222 | editorLoadBundledHLDB(); 223 | 224 | char path[EDITOR_PATH_MAX]; 225 | snprintf(path, sizeof(path), PATH_CAT("%s", CONF_DIR, "syntax"), 226 | getenv(ENV_HOME)); 227 | 228 | DirIter iter = dirFindFirst(path); 229 | if (iter.error) 230 | return; 231 | 232 | do { 233 | const char* filename = dirGetName(&iter); 234 | char file_path[EDITOR_PATH_MAX]; 235 | int len = snprintf(file_path, sizeof(file_path), PATH_CAT("%s", "%s"), 236 | path, filename); 237 | 238 | // This is just to suppress Wformat-truncation 239 | if (len < 0) 240 | continue; 241 | 242 | if (getFileType(file_path) == FT_REG) { 243 | const char* ext = strrchr(filename, '.'); 244 | if (ext && strcmp(ext, ".json") == 0) { 245 | editorLoadHLDB(file_path); 246 | } 247 | } 248 | } while (dirNext(&iter)); 249 | dirClose(&iter); 250 | } 251 | 252 | // Built-in syntax highlighting for nino config 253 | static void editorLoadNinoConfigHLDB(void) { 254 | EditorSyntax* syntax = calloc_s(1, sizeof(EditorSyntax)); 255 | 256 | syntax->file_type = "nino"; 257 | vector_push(syntax->file_exts, "ninorc"); 258 | vector_push(syntax->file_exts, ".nino"); 259 | syntax->singleline_comment_start = "#"; 260 | syntax->multiline_comment_start = NULL; 261 | syntax->multiline_comment_end = NULL; 262 | 263 | EditorConCmd* curr = gEditor.cvars; 264 | while (curr) { 265 | vector_push(syntax->keywords[curr->has_callback ? 0 : 1], curr->name); 266 | curr = curr->next; 267 | } 268 | 269 | for (int i = 0; i < EDITOR_COLOR_COUNT; i++) { 270 | vector_push(syntax->keywords[2], color_element_map[i].label); 271 | } 272 | 273 | syntax->flags = HL_HIGHLIGHT_STRINGS; 274 | 275 | // Add to HLDB 276 | syntax->next = gEditor.HLDB; 277 | gEditor.HLDB = syntax; 278 | } 279 | 280 | static bool editorLoadJsonHLDB(const char* json, EditorSyntax* syntax) { 281 | // Parse json 282 | JsonValue* value = json_parse(json, &hldb_arena); 283 | if (value->type != JSON_OBJECT) { 284 | return false; 285 | } 286 | 287 | // Get data 288 | #define CHECK(boolean) \ 289 | do { \ 290 | if (!(boolean)) \ 291 | return false; \ 292 | } while (0) 293 | 294 | JsonObject* object = value->object; 295 | 296 | JsonValue* name = json_object_find(object, "name"); 297 | CHECK(name && name->type == JSON_STRING); 298 | syntax->file_type = name->string; 299 | 300 | JsonValue* extensions = json_object_find(object, "extensions"); 301 | CHECK(extensions && extensions->type == JSON_ARRAY); 302 | for (size_t i = 0; i < extensions->array->size; i++) { 303 | JsonValue* item = extensions->array->data[i]; 304 | CHECK(item->type == JSON_STRING); 305 | vector_push(syntax->file_exts, item->string); 306 | } 307 | vector_shrink(syntax->file_exts); 308 | 309 | JsonValue* comment = json_object_find(object, "comment"); 310 | if (comment && comment->type != JSON_NULL) { 311 | CHECK(comment->type == JSON_STRING); 312 | syntax->singleline_comment_start = comment->string; 313 | } else { 314 | syntax->singleline_comment_start = NULL; 315 | } 316 | 317 | JsonValue* multi_comment = json_object_find(object, "multiline-comment"); 318 | if (multi_comment && multi_comment->type != JSON_NULL) { 319 | CHECK(multi_comment->type == JSON_ARRAY); 320 | CHECK(multi_comment->array->size == 2); 321 | JsonValue* mcs = multi_comment->array->data[0]; 322 | CHECK(mcs && mcs->type == JSON_STRING); 323 | syntax->multiline_comment_start = mcs->string; 324 | JsonValue* mce = multi_comment->array->data[1]; 325 | CHECK(mce && mce->type == JSON_STRING); 326 | syntax->multiline_comment_end = mce->string; 327 | } else { 328 | syntax->multiline_comment_start = NULL; 329 | syntax->multiline_comment_end = NULL; 330 | } 331 | const char* kw_fields[] = {"keywords1", "keywords2", "keywords3"}; 332 | 333 | for (int i = 0; i < 3; i++) { 334 | JsonValue* keywords = json_object_find(object, kw_fields[i]); 335 | if (keywords && keywords->type != JSON_NULL) { 336 | CHECK(keywords->type == JSON_ARRAY); 337 | for (size_t j = 0; j < keywords->array->size; j++) { 338 | JsonValue* item = keywords->array->data[j]; 339 | CHECK(item->type == JSON_STRING); 340 | vector_push(syntax->keywords[i], item->string); 341 | } 342 | } 343 | vector_shrink(syntax->keywords[i]); 344 | } 345 | 346 | // TODO: Add flags option in json file 347 | syntax->flags = HL_HIGHLIGHT_NUMBERS | HL_HIGHLIGHT_STRINGS; 348 | 349 | return true; 350 | } 351 | 352 | static void editorLoadBundledHLDB(void) { 353 | for (size_t i = 0; i < sizeof(bundle) / sizeof(bundle[0]); i++) { 354 | EditorSyntax* syntax = calloc_s(1, sizeof(EditorSyntax)); 355 | if (editorLoadJsonHLDB(bundle[i], syntax)) { 356 | // Add to HLDB 357 | syntax->next = gEditor.HLDB; 358 | gEditor.HLDB = syntax; 359 | } else { 360 | free(syntax); 361 | } 362 | } 363 | } 364 | 365 | bool editorLoadHLDB(const char* path) { 366 | FILE* fp; 367 | size_t size; 368 | char* buffer; 369 | 370 | // Load file 371 | fp = openFile(path, "rb"); 372 | if (!fp) 373 | return false; 374 | 375 | fseek(fp, 0, SEEK_END); 376 | size = ftell(fp); 377 | fseek(fp, 0, SEEK_SET); 378 | 379 | buffer = calloc_s(1, size + 1); 380 | 381 | if (fread(buffer, size, 1, fp) != 1) { 382 | fclose(fp); 383 | free(buffer); 384 | return false; 385 | } 386 | fclose(fp); 387 | 388 | EditorSyntax* syntax = calloc_s(1, sizeof(EditorSyntax)); 389 | if (editorLoadJsonHLDB(buffer, syntax)) { 390 | // Add to HLDB 391 | syntax->next = gEditor.HLDB; 392 | gEditor.HLDB = syntax; 393 | } else { 394 | free(syntax); 395 | } 396 | 397 | free(buffer); 398 | return true; 399 | } 400 | 401 | void editorFreeHLDB(void) { 402 | EditorSyntax* HLDB = gEditor.HLDB; 403 | while (HLDB) { 404 | EditorSyntax* temp = HLDB; 405 | HLDB = HLDB->next; 406 | free(temp->file_exts.data); 407 | for (size_t i = 0; 408 | i < sizeof(temp->keywords) / sizeof(temp->keywords[0]); i++) { 409 | free(temp->keywords[i].data); 410 | } 411 | free(temp); 412 | } 413 | json_arena_deinit(&hldb_arena); 414 | gEditor.HLDB = NULL; 415 | } 416 | -------------------------------------------------------------------------------- /src/highlight.h: -------------------------------------------------------------------------------- 1 | #ifndef HIGHLIGHT_H 2 | #define HIGHLIGHT_H 3 | 4 | #include "editor.h" 5 | 6 | typedef struct EditorSyntax { 7 | struct EditorSyntax* next; 8 | 9 | const char* file_type; 10 | const char* singleline_comment_start; 11 | const char* multiline_comment_start; 12 | const char* multiline_comment_end; 13 | VECTOR(const char*) file_exts; 14 | VECTOR(const char*) keywords[3]; 15 | int flags; 16 | 17 | struct JsonValue* value; 18 | } EditorSyntax; 19 | 20 | void editorUpdateSyntax(EditorFile* file, EditorRow* row); 21 | void editorSetSyntaxHighlight(EditorFile* file, EditorSyntax* syntax); 22 | void editorSelectSyntaxHighlight(EditorFile* file); 23 | void editorInitHLDB(void); 24 | bool editorLoadHLDB(const char* json_file); 25 | void editorFreeHLDB(void); 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /src/input.h: -------------------------------------------------------------------------------- 1 | #ifndef INPUT_H 2 | #define INPUT_H 3 | 4 | char* editorPrompt(char* prompt, int state, void (*callback)(char*, int)); 5 | void editorMoveCursor(int key); 6 | void editorProcessKeypress(void); 7 | 8 | void editorScrollToCursor(void); 9 | void editorScrollToCursorCenter(void); 10 | void editorScroll(int dist); 11 | 12 | void mousePosToEditorPos(int* x, int* y); 13 | int getMousePosField(int x, int y); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/json.h: -------------------------------------------------------------------------------- 1 | #ifndef JSON_H 2 | #define JSON_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // Arena 9 | 10 | typedef struct JsonArena JsonArena; 11 | typedef struct JsonArenaBlock JsonArenaBlock; 12 | 13 | struct JsonArenaBlock { 14 | uint32_t size; 15 | uint32_t capacity; 16 | void* data; 17 | JsonArenaBlock* next; 18 | }; 19 | 20 | struct JsonArena { 21 | JsonArenaBlock* blocks; 22 | JsonArenaBlock* current_block; 23 | JsonArenaBlock* last_block; 24 | uint32_t first_block_size; 25 | }; 26 | 27 | void json_arena_init(JsonArena* arena, size_t first_block_size); 28 | void json_arena_deinit(JsonArena* arena); 29 | void json_arena_reset(JsonArena* arena); 30 | 31 | void* json_arena_alloc(JsonArena* arena, size_t size); 32 | void* json_arena_realloc(JsonArena* arena, void* ptr, size_t old_size, 33 | size_t size); 34 | 35 | // Parser 36 | 37 | typedef struct JsonValue JsonValue; 38 | typedef struct JsonArray JsonArray; 39 | typedef struct JsonObject JsonObject; 40 | 41 | typedef enum JsonType { 42 | JSON_ERROR, 43 | JSON_NULL, 44 | JSON_BOOLEAN, 45 | JSON_NUMBER, 46 | JSON_STRING, 47 | JSON_ARRAY, 48 | JSON_OBJECT, 49 | } JsonType; 50 | 51 | struct JsonValue { 52 | JsonType type; 53 | union { 54 | double number; 55 | bool boolean; 56 | char* string; 57 | JsonArray* array; 58 | JsonObject* object; 59 | }; 60 | }; 61 | 62 | struct JsonArray { 63 | size_t size; 64 | JsonValue** data; 65 | }; 66 | 67 | struct JsonObject { 68 | struct JsonObject* next; 69 | char* key; 70 | JsonValue* value; 71 | }; 72 | 73 | JsonValue* json_parse(const char* text, JsonArena* arena); 74 | 75 | JsonValue* json_object_find(const JsonObject* object, const char* key); 76 | 77 | #ifdef JSON_IMPLEMENTATION 78 | 79 | #include 80 | #include 81 | #include 82 | #include 83 | 84 | #ifndef JSON_MALLOC 85 | #define JSON_MALLOC malloc 86 | #endif // !JSON_MALLOC 87 | 88 | #ifndef JSON_FREE 89 | #define JSON_FREE free 90 | #endif // !JSON_FREE 91 | 92 | // Arena 93 | 94 | #define JSON_ARENA_ALIGNMENT 16 95 | 96 | void json_arena_init(JsonArena* arena, size_t first_block_size) { 97 | memset(arena, 0, sizeof(JsonArena)); 98 | arena->first_block_size = first_block_size; 99 | } 100 | 101 | void json_arena_deinit(JsonArena* arena) { 102 | JsonArenaBlock* curr = arena->blocks; 103 | while (curr) { 104 | JsonArenaBlock* temp = curr; 105 | curr = curr->next; 106 | JSON_FREE(temp->data); 107 | JSON_FREE(temp); 108 | } 109 | 110 | memset(arena, 0, sizeof(JsonArena)); 111 | } 112 | 113 | void json_arena_reset(JsonArena* arena) { 114 | JsonArenaBlock* curr = arena->blocks; 115 | while (curr) { 116 | curr->size = 0; 117 | curr = curr->next; 118 | } 119 | arena->current_block = arena->blocks; 120 | } 121 | 122 | static inline size_t json__arena_alignment_loss(size_t bytes_allocated, 123 | size_t alignment) { 124 | size_t offset = bytes_allocated & (alignment - 1); 125 | if (offset == 0) 126 | return 0; 127 | return alignment - offset; 128 | } 129 | 130 | static inline size_t json__arena_block_bytes_left(JsonArenaBlock* blk) { 131 | size_t inc = json__arena_alignment_loss(blk->size, JSON_ARENA_ALIGNMENT); 132 | return blk->capacity - (blk->size + inc); 133 | } 134 | 135 | static void json__arena_alloc_new_block(JsonArena* arena, 136 | size_t requested_size) { 137 | size_t allocated_size; 138 | if (!arena->blocks) { 139 | allocated_size = arena->first_block_size; 140 | } else { 141 | allocated_size = arena->last_block->capacity; 142 | } 143 | 144 | if (allocated_size < 1) 145 | allocated_size = 1; 146 | 147 | while (allocated_size < requested_size) { 148 | allocated_size *= 2; 149 | } 150 | 151 | if (allocated_size > UINT32_MAX) 152 | allocated_size = UINT32_MAX; 153 | 154 | JsonArenaBlock* blk = JSON_MALLOC(sizeof(JsonArenaBlock)); 155 | blk->data = JSON_MALLOC(allocated_size); 156 | blk->size = 0; 157 | blk->capacity = allocated_size; 158 | blk->next = NULL; 159 | 160 | if (!arena->blocks) { 161 | arena->blocks = blk; 162 | } else { 163 | arena->last_block->next = blk; 164 | } 165 | arena->last_block = blk; 166 | arena->current_block = blk; 167 | } 168 | 169 | void* json_arena_alloc(JsonArena* arena, size_t size) { 170 | if (size == 0) 171 | return NULL; 172 | 173 | if (!arena->blocks) 174 | json__arena_alloc_new_block(arena, size); 175 | 176 | while (json__arena_block_bytes_left(arena->current_block) < size) { 177 | arena->current_block = arena->current_block->next; 178 | if (!arena->current_block) { 179 | json__arena_alloc_new_block(arena, size); 180 | break; 181 | } 182 | } 183 | 184 | JsonArenaBlock* blk = arena->current_block; 185 | if (blk == NULL) { 186 | return NULL; 187 | } 188 | 189 | size_t inc = json__arena_alignment_loss(blk->size, JSON_ARENA_ALIGNMENT); 190 | void* out = (uint8_t*)blk->data + blk->size + inc; 191 | blk->size += size + inc; 192 | return out; 193 | } 194 | 195 | void* json_arena_realloc(JsonArena* arena, void* ptr, size_t old_size, 196 | size_t size) { 197 | if (old_size >= size) 198 | return ptr; 199 | 200 | if (!ptr || !arena->blocks) 201 | return json_arena_alloc(arena, size); 202 | 203 | JsonArenaBlock* blk = arena->current_block; 204 | uint8_t* prev = (uint8_t*)blk->data + blk->size - old_size; 205 | uint32_t bytes_left = blk->capacity - blk->size + old_size; 206 | void* new_arr; 207 | if (prev == (uint8_t*)ptr && bytes_left >= size) { 208 | blk->size -= old_size; 209 | new_arr = json_arena_alloc(arena, size); 210 | } else { 211 | new_arr = json_arena_alloc(arena, size); 212 | memcpy(new_arr, ptr, old_size); 213 | } 214 | return new_arr; 215 | } 216 | 217 | // JSON 218 | 219 | typedef struct JsonParserState { 220 | JsonArena* arena; 221 | const char* p; 222 | const char* start; 223 | } JsonParserState; 224 | 225 | #define JSON_ERROR_SIZE 64 226 | #define JSON_STRING_SIZE 16 227 | #define JSON_ARRAY_SIZE 16 228 | 229 | typedef enum JsonTokenType { 230 | JSON_TOKEN_EMPTY = 0, 231 | JSON_TOKEN_ERROR, 232 | JSON_TOKEN_NULL, 233 | JSON_TOKEN_BOOLEAN, 234 | JSON_TOKEN_NUMBER, 235 | JSON_TOKEN_STRING, 236 | JSON_TOKEN_LBRACE = '{', 237 | JSON_TOKEN_RBRACE = '}', 238 | JSON_TOKEN_LBRACKET = '[', 239 | JSON_TOKEN_RBRACKET = ']', 240 | JSON_TOKEN_COMMA = ',', 241 | JSON_TOKEN_COLON = ':', 242 | } JsonTokenType; 243 | 244 | typedef struct JsonToken { 245 | JsonTokenType type; 246 | union { 247 | double number; 248 | bool boolean; 249 | char* string; 250 | }; 251 | } JsonToken; 252 | 253 | static JsonToken json__token_error(JsonParserState* state, const char* fmt, 254 | ...) { 255 | JsonToken token; 256 | token.type = JSON_TOKEN_ERROR; 257 | token.string = json_arena_alloc(state->arena, JSON_ERROR_SIZE); 258 | 259 | va_list ap; 260 | va_start(ap, fmt); 261 | vsnprintf(token.string, JSON_ERROR_SIZE, fmt, ap); 262 | va_end(ap); 263 | 264 | return token; 265 | } 266 | 267 | static JsonToken json__next_token(JsonParserState* state, const char* text) { 268 | JsonToken token = {0}; 269 | char c; 270 | 271 | if (text) { 272 | state->start = text; 273 | state->p = text; 274 | } 275 | 276 | if (!state->p) { 277 | return token; 278 | } 279 | 280 | while ((c = *state->p++)) { 281 | if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { 282 | continue; 283 | } else if ((c >= '0' && c <= '9') || c == '-') { 284 | token.type = JSON_TOKEN_NUMBER; 285 | double sign = 1; 286 | double value = 0.0f; 287 | double exp_sign = 10; 288 | double exp = 0.0f; 289 | if (c == '-') { 290 | sign = -1; 291 | c = *state->p++; 292 | if (!(c >= '0' && c <= '9')) { 293 | return json__token_error( 294 | state, "No number after minus sign at position %ld", 295 | state->p - state->start); 296 | } 297 | } 298 | 299 | if (c == '0') { 300 | c = *state->p++; 301 | } else { 302 | while (c >= '0' && c <= '9') { 303 | value *= 10; 304 | value += c - '0'; 305 | c = *state->p++; 306 | } 307 | } 308 | 309 | if (c == '.') { 310 | c = *state->p++; 311 | if (c >= '0' && c <= '9') { 312 | double i = 0.1; 313 | while (c >= '0' && c <= '9') { 314 | value += (c - '0') * i; 315 | i *= 0.1; 316 | c = *state->p++; 317 | } 318 | } else { 319 | return json__token_error( 320 | state, "Unterminated fractional number at position %ld", 321 | state->p - state->start); 322 | } 323 | } 324 | 325 | if (c == 'e' || c == 'E') { 326 | c = *state->p++; 327 | if (c == '-') { 328 | exp_sign = 0.1; 329 | c = *state->p++; 330 | } else if (c == '+') { 331 | c = *state->p++; 332 | } 333 | 334 | if (c >= '0' && c <= '9') { 335 | while (c >= '0' && c <= '9') { 336 | exp *= 10; 337 | exp += c - '0'; 338 | c = *state->p++; 339 | } 340 | } else { 341 | return json__token_error( 342 | state, 343 | "Exponent part is missing a number at position %ld", 344 | state->p - state->start); 345 | } 346 | } 347 | state->p--; 348 | 349 | for (int i = 0; i < exp; i++) { 350 | value *= exp_sign; 351 | } 352 | token.number = sign * value; 353 | 354 | return token; 355 | } else if (c == '"') { 356 | token.type = JSON_TOKEN_STRING; 357 | token.string = json_arena_alloc(state->arena, JSON_STRING_SIZE); 358 | size_t capacity = JSON_STRING_SIZE; 359 | size_t size = 0; 360 | 361 | while ((c = *state->p++) != '"') { 362 | if (c == '\0') { 363 | return json__token_error( 364 | state, "Unterminated string at position %ld", 365 | state->p - state->start); 366 | } 367 | 368 | // TODO: Add UTF-8 support 369 | if (c < 32 || c == 127) { 370 | return json__token_error( 371 | state, 372 | "Bad control character in string literal " 373 | "at position %ld", 374 | state->p - state->start); 375 | } 376 | 377 | if (c == '\\') { 378 | c = *state->p++; 379 | switch (c) { 380 | case '"': 381 | case '\\': 382 | case '/': 383 | break; 384 | case 'b': 385 | c = '\b'; 386 | break; 387 | case 'f': 388 | c = '\f'; 389 | break; 390 | case 'n': 391 | c = '\n'; 392 | break; 393 | case 'r': 394 | c = '\r'; 395 | break; 396 | case 't': 397 | c = '\t'; 398 | break; 399 | // TODO: Add Unicode 400 | case 'u': 401 | return json__token_error( 402 | state, "Unicode escape not implemented"); 403 | default: 404 | return json__token_error( 405 | state, "Bad escaped character at position %ld", 406 | state->p - state->start); 407 | } 408 | } 409 | 410 | if (size + 2 > capacity) { 411 | token.string = json_arena_realloc( 412 | state->arena, token.string, capacity, capacity * 2); 413 | capacity *= 2; 414 | } 415 | 416 | token.string[size] = c; 417 | size++; 418 | } 419 | 420 | token.string[size] = '\0'; 421 | return token; 422 | } else if (strncmp("null", state->p - 1, strlen("null")) == 0) { 423 | state->p += strlen("null") - 1; 424 | token.type = JSON_TOKEN_NULL; 425 | return token; 426 | } else if (strncmp("true", state->p - 1, strlen("true")) == 0) { 427 | state->p += strlen("true") - 1; 428 | token.type = JSON_TOKEN_BOOLEAN; 429 | token.boolean = true; 430 | return token; 431 | } else if (strncmp("false", state->p - 1, strlen("false")) == 0) { 432 | state->p += strlen("false") - 1; 433 | token.type = JSON_TOKEN_BOOLEAN; 434 | token.boolean = false; 435 | return token; 436 | } else { 437 | switch (c) { 438 | case '{': 439 | token.type = JSON_TOKEN_LBRACE; 440 | break; 441 | case '}': 442 | token.type = JSON_TOKEN_RBRACE; 443 | break; 444 | case '[': 445 | token.type = JSON_TOKEN_LBRACKET; 446 | break; 447 | case ']': 448 | token.type = JSON_TOKEN_RBRACKET; 449 | break; 450 | case ',': 451 | token.type = JSON_TOKEN_COMMA; 452 | break; 453 | case ':': 454 | token.type = JSON_TOKEN_COLON; 455 | break; 456 | default: 457 | return json__token_error( 458 | state, "Unexpected token '%c' at position %ld", c, 459 | state->p - state->start); 460 | } 461 | return token; 462 | } 463 | } 464 | 465 | state->start = NULL; 466 | state->p = NULL; 467 | 468 | return token; 469 | } 470 | 471 | static JsonValue* json__error(JsonParserState* state, const char* fmt, ...) { 472 | JsonValue* value = json_arena_alloc(state->arena, sizeof(JsonValue)); 473 | value->type = JSON_ERROR; 474 | value->string = json_arena_alloc(state->arena, JSON_ERROR_SIZE); 475 | 476 | va_list ap; 477 | va_start(ap, fmt); 478 | vsnprintf(value->string, JSON_ERROR_SIZE, fmt, ap); 479 | va_end(ap); 480 | 481 | return value; 482 | } 483 | 484 | static JsonValue* json__parse_array(JsonParserState* state); 485 | 486 | static JsonValue* json__parse_object(JsonParserState* state); 487 | 488 | static JsonValue* json__parse_value(JsonParserState* state, JsonToken token) { 489 | JsonValue* value; 490 | if (token.type == JSON_TOKEN_LBRACE) { 491 | value = json__parse_object(state); 492 | } else if (token.type == JSON_TOKEN_LBRACKET) { 493 | value = json__parse_array(state); 494 | } else { 495 | value = json_arena_alloc(state->arena, sizeof(JsonValue)); 496 | switch (token.type) { 497 | case JSON_TOKEN_NULL: 498 | value->type = JSON_NULL; 499 | break; 500 | case JSON_TOKEN_BOOLEAN: 501 | value->type = JSON_BOOLEAN; 502 | value->boolean = token.boolean; 503 | break; 504 | case JSON_TOKEN_NUMBER: 505 | value->type = JSON_NUMBER; 506 | value->number = token.number; 507 | break; 508 | case JSON_TOKEN_STRING: 509 | value->type = JSON_STRING; 510 | value->string = token.string; 511 | break; 512 | case JSON_TOKEN_ERROR: 513 | value->type = JSON_ERROR; 514 | value->string = token.string; 515 | break; 516 | case JSON_TOKEN_EMPTY: 517 | return json__error(state, "Unexpected end"); 518 | break; 519 | default: 520 | return json__error(state, "Unexpected token '%c'", token.type); 521 | } 522 | } 523 | return value; 524 | } 525 | 526 | static JsonValue* json__parse_object(JsonParserState* state) { 527 | JsonToken token = json__next_token(state, NULL); 528 | 529 | JsonObject head = {0}; 530 | JsonObject* curr = &head; 531 | 532 | // TODO: Detect duplicated keys 533 | while (token.type == JSON_TOKEN_STRING) { 534 | curr->next = json_arena_alloc(state->arena, sizeof(JsonObject)); 535 | curr = curr->next; 536 | curr->next = NULL; 537 | curr->key = token.string; 538 | 539 | token = json__next_token(state, NULL); 540 | if (token.type != JSON_TOKEN_COLON) { 541 | return json__error(state, "Expected ':' after property name"); 542 | } 543 | 544 | token = json__next_token(state, NULL); 545 | curr->value = json__parse_value(state, token); 546 | if (curr->value->type == JSON_ERROR) { 547 | return curr->value; 548 | } 549 | 550 | token = json__next_token(state, NULL); 551 | if (token.type == JSON_TOKEN_RBRACE) { 552 | break; 553 | } 554 | 555 | if (token.type == JSON_TOKEN_COMMA) { 556 | token = json__next_token(state, NULL); 557 | if (token.type != JSON_TOKEN_STRING) { 558 | return json__error(state, 559 | "Expected double-quoted property name"); 560 | } 561 | } else { 562 | return json__error(state, 563 | "Expected ',' or '}' after property value"); 564 | } 565 | } 566 | 567 | if (token.type != JSON_TOKEN_RBRACE) { 568 | return json__error(state, "Expected property name or '}'"); 569 | } 570 | 571 | JsonValue* value = json_arena_alloc(state->arena, sizeof(JsonValue)); 572 | value->type = JSON_OBJECT; 573 | value->object = head.next; 574 | return value; 575 | } 576 | 577 | static JsonValue* json__parse_array(JsonParserState* state) { 578 | JsonToken token = json__next_token(state, NULL); 579 | JsonArray* array = json_arena_alloc(state->arena, sizeof(JsonArray)); 580 | size_t capacity = JSON_ARRAY_SIZE; 581 | array->data = json_arena_alloc(state->arena, sizeof(JsonValue*) * capacity); 582 | array->size = 0; 583 | 584 | while (token.type != JSON_TOKEN_RBRACKET) { 585 | if (array->size > 0) { 586 | if (token.type == JSON_TOKEN_COMMA) { 587 | token = json__next_token(state, NULL); 588 | } else { 589 | return json__error(state, 590 | "Expected ',' or ']' after array element"); 591 | } 592 | } 593 | 594 | JsonValue* data = json__parse_value(state, token); 595 | if (data->type == JSON_ERROR) 596 | return data; 597 | 598 | if (array->size + 1 > capacity) { 599 | size_t new_capacity = capacity * 2; 600 | array->data = json_arena_realloc(state->arena, array->data, 601 | sizeof(JsonValue*) * capacity, 602 | sizeof(JsonValue*) * new_capacity); 603 | capacity = new_capacity; 604 | } 605 | 606 | array->data[array->size] = data; 607 | array->size++; 608 | 609 | token = json__next_token(state, NULL); 610 | } 611 | 612 | JsonValue* value = json_arena_alloc(state->arena, sizeof(JsonValue)); 613 | value->type = JSON_ARRAY; 614 | value->array = array; 615 | return value; 616 | } 617 | 618 | JsonValue* json_parse(const char* text, JsonArena* arena) { 619 | JsonParserState state = {arena, NULL, NULL}; 620 | JsonToken token = json__next_token(&state, text); 621 | JsonValue* value = json__parse_value(&state, token); 622 | if (value->type == JSON_ERROR) 623 | return value; 624 | 625 | // Check if any token left 626 | token = json__next_token(&state, NULL); 627 | if (token.type != JSON_TOKEN_EMPTY) { 628 | return json__error(&state, "Unexpected non-whitespace character"); 629 | } 630 | return value; 631 | } 632 | 633 | JsonValue* json_object_find(const JsonObject* object, const char* key) { 634 | while (object) { 635 | if (strcmp(object->key, key) == 0) { 636 | return object->value; 637 | } 638 | object = object->next; 639 | } 640 | return NULL; 641 | } 642 | 643 | #endif // JSON_IMPLEMENTATION 644 | 645 | #endif // !JSON_H 646 | -------------------------------------------------------------------------------- /src/nino.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "editor.h" 4 | #include "file_io.h" 5 | #include "input.h" 6 | #include "os.h" 7 | #include "output.h" 8 | #include "prompt.h" 9 | #include "row.h" 10 | 11 | int main(int argc, char* argv[]) { 12 | editorInit(); 13 | EditorFile file; 14 | editorInitFile(&file); 15 | 16 | Args cmd_args = argsGet(argc, argv); 17 | 18 | if (cmd_args.count > 1) { 19 | for (int i = 1; i < cmd_args.count; i++) { 20 | if (gEditor.file_count >= EDITOR_FILE_MAX_SLOT) { 21 | editorMsg("Already opened too many files!"); 22 | break; 23 | } 24 | editorInitFile(&file); 25 | if (editorOpen(&file, cmd_args.args[i])) { 26 | editorAddFile(&file); 27 | } 28 | } 29 | } 30 | 31 | argsFree(cmd_args); 32 | 33 | if (gEditor.file_count == 0) { 34 | if (gEditor.explorer.node) { 35 | gEditor.state = EXPLORER_MODE; 36 | } else { 37 | editorAddFile(&file); 38 | editorInsertRow(gCurFile, 0, "", 0); 39 | } 40 | } 41 | 42 | if (gEditor.explorer.node == NULL) { 43 | gEditor.explorer.width = 0; 44 | } 45 | 46 | gEditor.loading = false; 47 | 48 | while (gEditor.file_count || gEditor.explorer.node) { 49 | editorRefreshScreen(); 50 | editorProcessKeypress(); 51 | } 52 | editorFree(); 53 | return 0; 54 | } 55 | -------------------------------------------------------------------------------- /src/os.h: -------------------------------------------------------------------------------- 1 | #ifndef OS_H 2 | #define OS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // File 9 | typedef struct FileInfo FileInfo; 10 | FileInfo getFileInfo(const char* path); 11 | bool areFilesEqual(FileInfo f1, FileInfo f2); 12 | 13 | typedef enum FileType { 14 | FT_INVALID = -1, 15 | FT_REG, 16 | FT_DIR, 17 | FT_DEV, 18 | } FileType; 19 | FileType getFileType(const char* path); 20 | 21 | typedef struct DirIter DirIter; 22 | DirIter dirFindFirst(const char* path); 23 | bool dirNext(DirIter* iter); 24 | void dirClose(DirIter* iter); 25 | const char* dirGetName(const DirIter* iter); 26 | 27 | FILE* openFile(const char* path, const char* mode); 28 | bool changeDir(const char* path); 29 | char* getFullPath(const char* path); 30 | 31 | // Time 32 | int64_t getTime(void); 33 | 34 | // Command line 35 | typedef struct Args { 36 | int count; 37 | char** args; 38 | } Args; 39 | 40 | Args argsGet(int num_args, char** args); 41 | void argsFree(Args args); 42 | 43 | // New Line 44 | #define NL_UNIX 0 45 | #define NL_DOS 1 46 | 47 | #ifdef _WIN32 48 | #define NL_DEFAULT NL_DOS 49 | #include "os_win32.h" 50 | #else 51 | #define NL_DEFAULT NL_UNIX 52 | #include "os_unix.h" 53 | #endif 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/os_unix.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE // realpath 2 | 3 | #include "os_unix.h" 4 | 5 | #include 6 | #include 7 | 8 | #include "os.h" 9 | #include "utils.h" 10 | 11 | FileInfo getFileInfo(const char* path) { 12 | FileInfo info; 13 | info.error = (stat(path, &info.info) == -1); 14 | return info; 15 | } 16 | 17 | bool areFilesEqual(FileInfo f1, FileInfo f2) { 18 | return f1.info.st_ino == f2.info.st_ino; 19 | } 20 | 21 | FileType getFileType(const char* path) { 22 | struct stat info; 23 | if (stat(path, &info) == -1) 24 | return FT_INVALID; 25 | if (S_ISCHR(info.st_mode)) 26 | return FT_DEV; 27 | if (S_ISDIR(info.st_mode)) 28 | return FT_DIR; 29 | if (S_ISREG(info.st_mode)) 30 | return FT_REG; 31 | return FT_INVALID; 32 | } 33 | 34 | DirIter dirFindFirst(const char* path) { 35 | DirIter iter; 36 | iter.dp = opendir(path); 37 | if (iter.dp != NULL) { 38 | iter.entry = readdir(iter.dp); 39 | iter.error = (iter.entry == NULL); 40 | } else { 41 | iter.error = true; 42 | } 43 | return iter; 44 | } 45 | 46 | bool dirNext(DirIter* iter) { 47 | if (iter->error) 48 | return false; 49 | iter->entry = readdir(iter->dp); 50 | return iter->entry != NULL; 51 | } 52 | 53 | void dirClose(DirIter* iter) { 54 | if (iter->error) 55 | return; 56 | closedir(iter->dp); 57 | } 58 | 59 | const char* dirGetName(const DirIter* iter) { 60 | if (iter->error || !iter->entry) 61 | return NULL; 62 | return iter->entry->d_name; 63 | } 64 | 65 | FILE* openFile(const char* path, const char* mode) { return fopen(path, mode); } 66 | 67 | bool changeDir(const char* path) { return chdir(path) == 0; } 68 | 69 | char* getFullPath(const char* path) { 70 | static char resolved_path[EDITOR_PATH_MAX]; 71 | if (realpath(path, resolved_path) == NULL) { 72 | char parent_dir[EDITOR_PATH_MAX]; 73 | char base_name[EDITOR_PATH_MAX]; 74 | 75 | snprintf(parent_dir, sizeof(parent_dir), "%s", path); 76 | snprintf(base_name, sizeof(base_name), "%s", getBaseName(parent_dir)); 77 | getDirName(parent_dir); 78 | if (parent_dir[0] == '\0') { 79 | parent_dir[0] = '.'; 80 | parent_dir[1] = '\0'; 81 | } 82 | 83 | char resolved_parent_dir[EDITOR_PATH_MAX]; 84 | if (realpath(parent_dir, resolved_parent_dir) == NULL) 85 | return NULL; 86 | 87 | int len = snprintf(resolved_path, sizeof(resolved_path), "%s/%s", 88 | resolved_parent_dir, base_name); 89 | // This is just to suppress Wformat-truncation 90 | if (len < 0) 91 | return NULL; 92 | } 93 | return resolved_path; 94 | } 95 | 96 | int64_t getTime(void) { 97 | struct timeval time_val; 98 | gettimeofday(&time_val, NULL); 99 | return time_val.tv_sec * 1000000 + time_val.tv_usec; 100 | } 101 | 102 | Args argsGet(int num_args, char** args) { 103 | return (Args){.count = num_args, .args = args}; 104 | } 105 | 106 | void argsFree(Args args) { UNUSED(args.count); } 107 | -------------------------------------------------------------------------------- /src/os_unix.h: -------------------------------------------------------------------------------- 1 | #ifndef OS_UNIX_H 2 | #define OS_UNIX_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define ENV_HOME "HOME" 10 | #define CONF_DIR ".config/nino" 11 | #define DIR_SEP "/" 12 | 13 | #ifdef __linux__ 14 | // Linux 15 | #include 16 | #define EDITOR_PATH_MAX PATH_MAX 17 | #else 18 | // Other 19 | #define EDITOR_PATH_MAX 4096 20 | #endif 21 | 22 | struct FileInfo { 23 | struct stat info; 24 | 25 | bool error; 26 | }; 27 | 28 | struct DirIter { 29 | DIR* dp; 30 | struct dirent* entry; 31 | 32 | bool error; 33 | }; 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /src/os_win32.c: -------------------------------------------------------------------------------- 1 | #include "os_win32.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "os.h" 7 | #include "utils.h" 8 | 9 | FileInfo getFileInfo(const char* path) { 10 | FileInfo info; 11 | wchar_t w_path[EDITOR_PATH_MAX + 1] = {0}; 12 | MultiByteToWideChar(CP_UTF8, 0, path, -1, w_path, EDITOR_PATH_MAX); 13 | 14 | HANDLE hFile = CreateFileW(w_path, GENERIC_READ, FILE_SHARE_READ, NULL, 15 | OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 16 | if (hFile == INVALID_HANDLE_VALUE) 17 | goto errdefer; 18 | 19 | BOOL result = GetFileInformationByHandle(hFile, &info.info); 20 | if (result == 0) 21 | goto errdefer; 22 | 23 | CloseHandle(hFile); 24 | info.error = false; 25 | return info; 26 | 27 | errdefer: 28 | CloseHandle(hFile); 29 | info.error = true; 30 | return info; 31 | } 32 | 33 | bool areFilesEqual(FileInfo f1, FileInfo f2) { 34 | return (f1.info.dwVolumeSerialNumber == f2.info.dwVolumeSerialNumber && 35 | f1.info.nFileIndexHigh == f2.info.nFileIndexHigh && 36 | f1.info.nFileIndexLow == f2.info.nFileIndexLow); 37 | } 38 | 39 | FileType getFileType(const char* path) { 40 | DWORD attri = GetFileAttributes(path); 41 | if (attri == INVALID_FILE_ATTRIBUTES) 42 | return FT_INVALID; 43 | if (attri & FILE_ATTRIBUTE_DIRECTORY) 44 | return FT_DIR; 45 | return FT_REG; 46 | } 47 | 48 | DirIter dirFindFirst(const char* path) { 49 | DirIter iter; 50 | 51 | wchar_t w_path[EDITOR_PATH_MAX + 1] = {0}; 52 | MultiByteToWideChar(CP_UTF8, 0, path, -1, w_path, EDITOR_PATH_MAX); 53 | 54 | wchar_t entry_path[EDITOR_PATH_MAX]; 55 | swprintf(entry_path, EDITOR_PATH_MAX, L"%ls\\*", w_path); 56 | 57 | iter.handle = FindFirstFileW(entry_path, &iter.find_data); 58 | iter.error = (iter.handle == INVALID_HANDLE_VALUE); 59 | 60 | return iter; 61 | } 62 | 63 | bool dirNext(DirIter* iter) { 64 | if (iter->error) 65 | return false; 66 | return FindNextFileW(iter->handle, &iter->find_data) != 0; 67 | } 68 | 69 | void dirClose(DirIter* iter) { 70 | if (iter->error) 71 | return; 72 | FindClose(iter->handle); 73 | } 74 | 75 | const char* dirGetName(const DirIter* iter) { 76 | static char dir_name[EDITOR_PATH_MAX * 4]; 77 | 78 | if (iter->error) 79 | return NULL; 80 | 81 | WideCharToMultiByte(CP_UTF8, 0, iter->find_data.cFileName, -1, dir_name, 82 | EDITOR_PATH_MAX, NULL, false); 83 | return dir_name; 84 | } 85 | 86 | FILE* openFile(const char* path, const char* mode) { 87 | int size = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0); 88 | wchar_t* w_path = malloc_s(size * sizeof(wchar_t)); 89 | MultiByteToWideChar(CP_UTF8, 0, path, -1, w_path, size); 90 | 91 | size = MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0); 92 | wchar_t* w_mode = malloc_s(size * sizeof(wchar_t)); 93 | MultiByteToWideChar(CP_UTF8, 0, mode, -1, w_mode, size); 94 | 95 | FILE* file = _wfopen(w_path, w_mode); 96 | 97 | free(w_path); 98 | free(w_mode); 99 | 100 | return file; 101 | } 102 | 103 | bool changeDir(const char* path) { return SetCurrentDirectory(path); } 104 | 105 | char* getFullPath(const char* path) { 106 | static char resolved_path[EDITOR_PATH_MAX * 4]; 107 | 108 | int size = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0); 109 | wchar_t* w_path = malloc_s(size * sizeof(wchar_t)); 110 | MultiByteToWideChar(CP_UTF8, 0, path, -1, w_path, size); 111 | 112 | wchar_t w_resolved_path[EDITOR_PATH_MAX]; 113 | GetFullPathNameW(w_path, EDITOR_PATH_MAX, w_resolved_path, NULL); 114 | 115 | WideCharToMultiByte(CP_UTF8, 0, w_resolved_path, -1, resolved_path, 116 | EDITOR_PATH_MAX, NULL, false); 117 | 118 | free(w_path); 119 | 120 | return resolved_path; 121 | } 122 | 123 | int64_t getTime(void) { 124 | static const uint64_t EPOCH = ((uint64_t)116444736000000000ULL); 125 | 126 | SYSTEMTIME system_time; 127 | FILETIME file_time; 128 | uint64_t time; 129 | 130 | GetSystemTime(&system_time); 131 | SystemTimeToFileTime(&system_time, &file_time); 132 | time = ((uint64_t)file_time.dwLowDateTime); 133 | time += ((uint64_t)file_time.dwHighDateTime) << 32; 134 | int64_t sec = ((time - EPOCH) / 10000000); 135 | int64_t usec = (system_time.wMilliseconds * 1000); 136 | return sec * 1000000 + usec; 137 | } 138 | 139 | Args argsGet(int num_args, char** args) { 140 | UNUSED(num_args); 141 | UNUSED(args); 142 | 143 | int argc; 144 | LPWSTR* w_argv = CommandLineToArgvW(GetCommandLineW(), &argc); 145 | if (!w_argv) 146 | PANIC("GetCommandLine"); 147 | 148 | char** utf8_argv = malloc_s(argc * sizeof(char*)); 149 | for (int i = 0; i < argc; i++) { 150 | int size = 151 | WideCharToMultiByte(CP_UTF8, 0, w_argv[i], -1, NULL, 0, NULL, NULL); 152 | utf8_argv[i] = malloc_s(size); 153 | WideCharToMultiByte(CP_UTF8, 0, w_argv[i], -1, utf8_argv[i], size, NULL, 154 | NULL); 155 | } 156 | 157 | return (Args){.count = argc, .args = utf8_argv}; 158 | } 159 | 160 | void argsFree(Args args) { 161 | for (int i = 0; i < args.count; i++) { 162 | free(args.args[i]); 163 | } 164 | free(args.args); 165 | } 166 | -------------------------------------------------------------------------------- /src/os_win32.h: -------------------------------------------------------------------------------- 1 | #ifndef OS_WIN32_H 2 | #define OS_WIN32_H 3 | 4 | #include 5 | 6 | #define WIN32_LEAN_AND_MEAN 7 | #include 8 | 9 | #define ENV_HOME "USERPROFILE" 10 | #define CONF_DIR ".nino" 11 | #define DIR_SEP "\\" 12 | 13 | #define EDITOR_PATH_MAX MAX_PATH 14 | 15 | #include 16 | #ifndef STDIN_FILENO 17 | #define STDIN_FILENO _fileno(stdin) 18 | #endif 19 | 20 | #ifndef STDOUT_FILENO 21 | #define STDOUT_FILENO _fileno(stdout) 22 | #endif 23 | 24 | struct FileInfo { 25 | BY_HANDLE_FILE_INFORMATION info; 26 | 27 | bool error; 28 | }; 29 | 30 | struct DirIter { 31 | HANDLE handle; 32 | WIN32_FIND_DATAW find_data; 33 | 34 | bool error; 35 | }; 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /src/output.c: -------------------------------------------------------------------------------- 1 | #include "output.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "config.h" 10 | #include "defines.h" 11 | #include "editor.h" 12 | #include "highlight.h" 13 | #include "os.h" 14 | #include "prompt.h" 15 | #include "select.h" 16 | #include "unicode.h" 17 | 18 | static void editorDrawTopStatusBar(abuf* ab) { 19 | const char* right_buf = " nino v" EDITOR_VERSION " "; 20 | bool has_more_files = false; 21 | int rlen = strlen(right_buf); 22 | int len = gEditor.explorer.width; 23 | 24 | gotoXY(ab, 1, gEditor.explorer.width + 1); 25 | 26 | setColor(ab, gEditor.color_cfg.top_status[0], 0); 27 | setColor(ab, gEditor.color_cfg.top_status[1], 1); 28 | 29 | if (gEditor.tab_offset != 0) { 30 | abufAppendN(ab, "<", 1); 31 | len++; 32 | } 33 | 34 | gEditor.tab_displayed = 0; 35 | if (gEditor.loading) { 36 | const char* loading_text = "Loading..."; 37 | int loading_text_len = strlen(loading_text); 38 | abufAppendN(ab, loading_text, loading_text_len); 39 | len = loading_text_len; 40 | } else { 41 | for (int i = 0; i < gEditor.file_count; i++) { 42 | if (i < gEditor.tab_offset) 43 | continue; 44 | 45 | const EditorFile* file = &gEditor.files[i]; 46 | 47 | bool is_current = (file == gCurFile); 48 | if (is_current) { 49 | setColor(ab, gEditor.color_cfg.top_status[4], 0); 50 | setColor(ab, gEditor.color_cfg.top_status[5], 1); 51 | } else { 52 | setColor(ab, gEditor.color_cfg.top_status[2], 0); 53 | setColor(ab, gEditor.color_cfg.top_status[3], 1); 54 | } 55 | 56 | char buf[EDITOR_PATH_MAX] = {0}; 57 | const char* filename = 58 | file->filename ? getBaseName(file->filename) : "Untitled"; 59 | int buf_len = snprintf(buf, sizeof(buf), " %s%s ", 60 | file->dirty ? "*" : "", filename); 61 | int tab_width = strUTF8Width(buf); 62 | 63 | if (gEditor.screen_cols - len < tab_width || 64 | (i != gEditor.file_count - 1 && 65 | gEditor.screen_cols - len == tab_width)) { 66 | has_more_files = true; 67 | if (gEditor.tab_displayed == 0) { 68 | // Display at least one tab 69 | // TODO: This is wrong 70 | tab_width = gEditor.screen_cols - len - 1; 71 | buf_len = gEditor.screen_cols - len - 1; 72 | } else { 73 | break; 74 | } 75 | } 76 | 77 | // Not enough space to even show one tab 78 | if (tab_width < 0) 79 | break; 80 | 81 | abufAppendN(ab, buf, buf_len); 82 | len += tab_width; 83 | gEditor.tab_displayed++; 84 | } 85 | } 86 | 87 | setColor(ab, gEditor.color_cfg.top_status[0], 0); 88 | setColor(ab, gEditor.color_cfg.top_status[1], 1); 89 | 90 | if (has_more_files) { 91 | abufAppendN(ab, ">", 1); 92 | len++; 93 | } 94 | 95 | while (len < gEditor.screen_cols) { 96 | if (gEditor.screen_cols - len == rlen) { 97 | abufAppendN(ab, right_buf, rlen); 98 | break; 99 | } else { 100 | abufAppend(ab, " "); 101 | len++; 102 | } 103 | } 104 | } 105 | 106 | static void editorDrawConMsg(abuf* ab) { 107 | if (gEditor.con_size == 0) { 108 | return; 109 | } 110 | 111 | setColor(ab, gEditor.color_cfg.prompt[0], 0); 112 | setColor(ab, gEditor.color_cfg.prompt[1], 1); 113 | 114 | bool should_draw_prompt = 115 | (gEditor.state != EDIT_MODE && gEditor.state != EXPLORER_MODE); 116 | int draw_x = gEditor.screen_rows - gEditor.con_size; 117 | if (should_draw_prompt) { 118 | draw_x--; 119 | } 120 | 121 | int index = gEditor.con_front; 122 | for (int i = 0; i < gEditor.con_size; i++) { 123 | gotoXY(ab, draw_x, 0); 124 | draw_x++; 125 | 126 | const char* buf = gEditor.con_msg[index]; 127 | index = (index + 1) % EDITOR_CON_COUNT; 128 | 129 | int len = strlen(buf); 130 | if (len > gEditor.screen_cols) { 131 | len = gEditor.screen_cols; 132 | } 133 | 134 | abufAppendN(ab, buf, len); 135 | 136 | while (len < gEditor.screen_cols) { 137 | abufAppend(ab, " "); 138 | len++; 139 | } 140 | } 141 | } 142 | 143 | static void editorDrawPrompt(abuf* ab) { 144 | bool should_draw_prompt = 145 | (gEditor.state != EDIT_MODE && gEditor.state != EXPLORER_MODE); 146 | if (!should_draw_prompt) { 147 | return; 148 | } 149 | 150 | setColor(ab, gEditor.color_cfg.prompt[0], 0); 151 | setColor(ab, gEditor.color_cfg.prompt[1], 1); 152 | 153 | gotoXY(ab, gEditor.screen_rows - 1, 0); 154 | 155 | const char* left = gEditor.prompt; 156 | int len = strlen(left); 157 | 158 | const char* right = gEditor.prompt_right; 159 | int rlen = strlen(right); 160 | 161 | if (rlen > gEditor.screen_cols) { 162 | rlen = 0; 163 | } 164 | 165 | if (len + rlen > gEditor.screen_cols) { 166 | len = gEditor.screen_cols - rlen; 167 | } 168 | 169 | abufAppendN(ab, left, len); 170 | 171 | while (len < gEditor.screen_cols) { 172 | if (gEditor.screen_cols - len == rlen) { 173 | abufAppendN(ab, right, rlen); 174 | break; 175 | } else { 176 | abufAppend(ab, " "); 177 | len++; 178 | } 179 | } 180 | } 181 | 182 | static void editorDrawStatusBar(abuf* ab) { 183 | gotoXY(ab, gEditor.screen_rows, 0); 184 | 185 | setColor(ab, gEditor.color_cfg.status[0], 0); 186 | setColor(ab, gEditor.color_cfg.status[1], 1); 187 | 188 | const char* help_str = ""; 189 | const char* help_info[] = { 190 | " ^Q: Quit ^O: Open ^P: Prompt ^S: Save ^F: Find ^G: Goto", 191 | " ^Q: Quit ^O: Open ^P: Prompt", 192 | " ^Q: Cancel Up: back Down: Next", 193 | " ^Q: Cancel", 194 | " ^Q: Cancel", 195 | " ^Q: Cancel", 196 | " ^Q: Cancel", 197 | }; 198 | if (CONVAR_GETINT(helpinfo)) 199 | help_str = help_info[gEditor.state]; 200 | 201 | char lang[16]; 202 | char pos[64]; 203 | int len = strlen(help_str); 204 | int lang_len, pos_len; 205 | int rlen; 206 | if (gEditor.file_count == 0) { 207 | lang_len = 0; 208 | pos_len = 0; 209 | } else { 210 | const char* file_type = 211 | gCurFile->syntax ? gCurFile->syntax->file_type : "Plain Text"; 212 | int row = gCurFile->cursor.y + 1; 213 | int col = editorRowCxToRx(&gCurFile->row[gCurFile->cursor.y], 214 | gCurFile->cursor.x) + 215 | 1; 216 | float line_percent = 0.0f; 217 | const char* nl_type = (gCurFile->newline == NL_UNIX) ? "LF" : "CRLF"; 218 | if (gCurFile->num_rows - 1 > 0) { 219 | line_percent = 220 | (float)gCurFile->row_offset / (gCurFile->num_rows - 1) * 100.0f; 221 | } 222 | 223 | lang_len = snprintf(lang, sizeof(lang), " %s ", file_type); 224 | pos_len = snprintf(pos, sizeof(pos), " %d:%d [%.f%%] <%s> ", row, col, 225 | line_percent, nl_type); 226 | } 227 | 228 | rlen = lang_len + pos_len; 229 | 230 | if (rlen > gEditor.screen_cols) 231 | rlen = 0; 232 | if (len + rlen > gEditor.screen_cols) 233 | len = gEditor.screen_cols - rlen; 234 | 235 | abufAppendN(ab, help_str, len); 236 | 237 | while (len < gEditor.screen_cols) { 238 | if (gEditor.screen_cols - len == rlen) { 239 | setColor(ab, gEditor.color_cfg.status[2], 0); 240 | setColor(ab, gEditor.color_cfg.status[3], 1); 241 | abufAppendN(ab, lang, lang_len); 242 | setColor(ab, gEditor.color_cfg.status[4], 0); 243 | setColor(ab, gEditor.color_cfg.status[5], 1); 244 | abufAppendN(ab, pos, pos_len); 245 | break; 246 | } else { 247 | abufAppend(ab, " "); 248 | len++; 249 | } 250 | } 251 | } 252 | 253 | static void editorDrawRows(abuf* ab) { 254 | setColor(ab, gEditor.color_cfg.bg, 1); 255 | 256 | EditorSelectRange range = {0}; 257 | if (gCurFile->cursor.is_selected) 258 | getSelectStartEnd(&range); 259 | 260 | for (int i = gCurFile->row_offset, s_row = 2; 261 | i < gCurFile->row_offset + gEditor.display_rows; i++, s_row++) { 262 | bool is_row_full = false; 263 | // Move cursor to the beginning of a row 264 | gotoXY(ab, s_row, 1 + gEditor.explorer.width); 265 | 266 | gEditor.color_cfg.highlightBg[HL_BG_NORMAL] = gEditor.color_cfg.bg; 267 | if (i < gCurFile->num_rows) { 268 | char line_number[16]; 269 | if (i == gCurFile->cursor.y) { 270 | if (!gCurFile->cursor.is_selected) { 271 | gEditor.color_cfg.highlightBg[HL_BG_NORMAL] = 272 | gEditor.color_cfg.cursor_line; 273 | } 274 | setColor(ab, gEditor.color_cfg.line_number[1], 0); 275 | setColor(ab, gEditor.color_cfg.line_number[0], 1); 276 | } else { 277 | setColor(ab, gEditor.color_cfg.line_number[0], 0); 278 | setColor(ab, gEditor.color_cfg.line_number[1], 1); 279 | } 280 | 281 | snprintf(line_number, sizeof(line_number), " %*d ", 282 | gCurFile->lineno_width - 2, i + 1); 283 | abufAppend(ab, line_number); 284 | 285 | abufAppend(ab, ANSI_CLEAR); 286 | setColor(ab, gEditor.color_cfg.bg, 1); 287 | 288 | int cols = gEditor.screen_cols - gEditor.explorer.width - 289 | gCurFile->lineno_width; 290 | int col_offset = 291 | editorRowRxToCx(&gCurFile->row[i], gCurFile->col_offset); 292 | int len = gCurFile->row[i].size - col_offset; 293 | len = (len < 0) ? 0 : len; 294 | 295 | int rlen = gCurFile->row[i].rsize - gCurFile->col_offset; 296 | is_row_full = (rlen > cols); 297 | rlen = is_row_full ? cols : rlen; 298 | rlen += gCurFile->col_offset; 299 | 300 | char* c = &gCurFile->row[i].data[col_offset]; 301 | uint8_t* hl = &(gCurFile->row[i].hl[col_offset]); 302 | uint8_t curr_fg = HL_BG_NORMAL; 303 | uint8_t curr_bg = HL_NORMAL; 304 | 305 | setColor(ab, gEditor.color_cfg.highlightFg[curr_fg], 0); 306 | setColor(ab, gEditor.color_cfg.highlightBg[curr_bg], 1); 307 | 308 | int j = 0; 309 | int rx = gCurFile->col_offset; 310 | while (rx < rlen) { 311 | if (iscntrl(c[j]) && c[j] != '\t') { 312 | char sym = (c[j] <= 26) ? '@' + c[j] : '?'; 313 | abufAppend(ab, ANSI_INVERT); 314 | abufAppendN(ab, &sym, 1); 315 | abufAppend(ab, ANSI_CLEAR); 316 | setColor(ab, gEditor.color_cfg.highlightFg[curr_fg], 0); 317 | setColor(ab, gEditor.color_cfg.highlightBg[curr_bg], 1); 318 | 319 | rx++; 320 | j++; 321 | } else { 322 | uint8_t fg = hl[j] & HL_FG_MASK; 323 | uint8_t bg = hl[j] >> HL_FG_BITS; 324 | 325 | if (gCurFile->cursor.is_selected && 326 | isPosSelected(i, j + col_offset, range)) { 327 | bg = HL_BG_SELECT; 328 | } 329 | if (CONVAR_GETINT(drawspace) && 330 | (c[j] == ' ' || c[j] == '\t')) { 331 | fg = HL_SPACE; 332 | } 333 | if (bg == HL_BG_TRAILING && !CONVAR_GETINT(trailing)) { 334 | bg = HL_BG_NORMAL; 335 | } 336 | 337 | // Update color 338 | if (fg != curr_fg) { 339 | curr_fg = fg; 340 | setColor(ab, gEditor.color_cfg.highlightFg[fg], 0); 341 | } 342 | if (bg != curr_bg) { 343 | curr_bg = bg; 344 | setColor(ab, gEditor.color_cfg.highlightBg[bg], 1); 345 | } 346 | 347 | if (c[j] == '\t') { 348 | if (CONVAR_GETINT(drawspace)) { 349 | abufAppend(ab, "|"); 350 | } else { 351 | abufAppend(ab, " "); 352 | } 353 | 354 | rx++; 355 | while (rx % CONVAR_GETINT(tabsize) != 0 && rx < rlen) { 356 | abufAppend(ab, " "); 357 | rx++; 358 | } 359 | j++; 360 | } else if (c[j] == ' ') { 361 | if (CONVAR_GETINT(drawspace)) { 362 | abufAppend(ab, "."); 363 | } else { 364 | abufAppend(ab, " "); 365 | } 366 | rx++; 367 | j++; 368 | } else { 369 | size_t byte_size; 370 | uint32_t unicode = 371 | decodeUTF8(&c[j], len - j, &byte_size); 372 | int width = unicodeWidth(unicode); 373 | if (width >= 0) { 374 | rx += width; 375 | // Make sure double won't exceed the screen 376 | if (rx <= rlen) 377 | abufAppendN(ab, &c[j], byte_size); 378 | } 379 | j += byte_size; 380 | } 381 | } 382 | } 383 | 384 | // Add newline character when selected 385 | if (gCurFile->cursor.is_selected && range.end_y > i && 386 | i >= range.start_y && 387 | gCurFile->row[i].rsize - gCurFile->col_offset < cols) { 388 | setColor(ab, gEditor.color_cfg.highlightBg[HL_BG_SELECT], 1); 389 | abufAppend(ab, " "); 390 | } 391 | setColor(ab, gEditor.color_cfg.highlightBg[HL_BG_NORMAL], 1); 392 | } 393 | if (!is_row_full) 394 | abufAppend(ab, "\x1b[K"); 395 | setColor(ab, gEditor.color_cfg.bg, 1); 396 | } 397 | } 398 | 399 | static void editorDrawFileExplorer(abuf* ab) { 400 | char* explorer_buf = malloc_s(gEditor.explorer.width + 1); 401 | gotoXY(ab, 1, 1); 402 | 403 | setColor(ab, gEditor.color_cfg.explorer[3], 0); 404 | if (gEditor.state == EXPLORER_MODE) 405 | setColor(ab, gEditor.color_cfg.explorer[4], 1); 406 | else 407 | setColor(ab, gEditor.color_cfg.explorer[0], 1); 408 | 409 | snprintf(explorer_buf, gEditor.explorer.width + 1, " EXPLORER%*s", 410 | gEditor.explorer.width, ""); 411 | abufAppendN(ab, explorer_buf, gEditor.explorer.width); 412 | 413 | int lines = gEditor.explorer.flatten.size - gEditor.explorer.offset; 414 | if (lines < 0) { 415 | lines = 0; 416 | } else if (lines > gEditor.display_rows) { 417 | lines = gEditor.display_rows; 418 | } 419 | 420 | for (int i = 0; i < lines; i++) { 421 | gotoXY(ab, i + 2, 1); 422 | 423 | int index = gEditor.explorer.offset + i; 424 | EditorExplorerNode* node = gEditor.explorer.flatten.data[index]; 425 | if (index == gEditor.explorer.selected_index) 426 | setColor(ab, gEditor.color_cfg.explorer[1], 1); 427 | else 428 | setColor(ab, gEditor.color_cfg.explorer[0], 1); 429 | 430 | const char* icon = ""; 431 | if (node->is_directory) { 432 | setColor(ab, gEditor.color_cfg.explorer[2], 0); 433 | icon = node->is_open ? "v " : "> "; 434 | } else { 435 | setColor(ab, gEditor.color_cfg.explorer[3], 0); 436 | } 437 | const char* filename = getBaseName(node->filename); 438 | 439 | snprintf(explorer_buf, gEditor.explorer.width + 1, "%*s%s%s%*s", 440 | node->depth * 2, "", icon, filename, gEditor.explorer.width, 441 | ""); 442 | abufAppendN(ab, explorer_buf, gEditor.explorer.width); 443 | } 444 | 445 | // Draw blank lines 446 | setColor(ab, gEditor.color_cfg.explorer[0], 1); 447 | setColor(ab, gEditor.color_cfg.explorer[3], 0); 448 | 449 | memset(explorer_buf, ' ', gEditor.explorer.width); 450 | 451 | for (int i = 0; i < gEditor.display_rows - lines; i++) { 452 | gotoXY(ab, lines + i + 2, 1); 453 | abufAppendN(ab, explorer_buf, gEditor.explorer.width); 454 | } 455 | 456 | free(explorer_buf); 457 | } 458 | 459 | void editorRefreshScreen(void) { 460 | abuf ab = ABUF_INIT; 461 | 462 | abufAppend(&ab, "\x1b[?25l"); 463 | abufAppend(&ab, "\x1b[H"); 464 | 465 | editorDrawTopStatusBar(&ab); 466 | editorDrawRows(&ab); 467 | editorDrawFileExplorer(&ab); 468 | 469 | editorDrawConMsg(&ab); 470 | editorDrawPrompt(&ab); 471 | 472 | editorDrawStatusBar(&ab); 473 | 474 | bool should_show_cursor = true; 475 | if (gEditor.state == EDIT_MODE) { 476 | int row = (gCurFile->cursor.y - gCurFile->row_offset) + 2; 477 | int col = (editorRowCxToRx(&gCurFile->row[gCurFile->cursor.y], 478 | gCurFile->cursor.x) - 479 | gCurFile->col_offset) + 480 | 1 + gCurFile->lineno_width; 481 | if (row <= 1 || row > gEditor.screen_rows - 1 || col <= 1 || 482 | col > gEditor.screen_cols - gEditor.explorer.width || 483 | row >= gEditor.screen_rows - gEditor.con_size) { 484 | should_show_cursor = false; 485 | } else { 486 | gotoXY(&ab, row, col + gEditor.explorer.width); 487 | } 488 | } else { 489 | // prompt 490 | gotoXY(&ab, gEditor.screen_rows - 1, gEditor.px + 1); 491 | } 492 | 493 | if (gEditor.state == EXPLORER_MODE) { 494 | should_show_cursor = false; 495 | } 496 | 497 | if (should_show_cursor) { 498 | abufAppend(&ab, "\x1b[?25h"); 499 | } else { 500 | abufAppend(&ab, "\x1b[?25l"); 501 | } 502 | 503 | UNUSED(write(STDOUT_FILENO, ab.buf, ab.len)); 504 | abufFree(&ab); 505 | } 506 | -------------------------------------------------------------------------------- /src/output.h: -------------------------------------------------------------------------------- 1 | #ifndef OUTPUT_H 2 | #define OUTPUT_H 3 | 4 | void editorRefreshScreen(void); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /src/prompt.c: -------------------------------------------------------------------------------- 1 | #include "prompt.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "editor.h" 9 | #include "input.h" 10 | #include "output.h" 11 | #include "prompt.h" 12 | #include "terminal.h" 13 | #include "unicode.h" 14 | 15 | void editorMsg(const char* fmt, ...) { 16 | va_list ap; 17 | va_start(ap, fmt); 18 | vsnprintf(gEditor.con_msg[gEditor.con_rear], sizeof(gEditor.con_msg[0]), 19 | fmt, ap); 20 | va_end(ap); 21 | 22 | if (gEditor.con_front == gEditor.con_rear) { 23 | gEditor.con_front = (gEditor.con_front + 1) % EDITOR_CON_COUNT; 24 | gEditor.con_size--; 25 | } else if (gEditor.con_front == -1) { 26 | gEditor.con_front = 0; 27 | } 28 | gEditor.con_size++; 29 | gEditor.con_rear = (gEditor.con_rear + 1) % EDITOR_CON_COUNT; 30 | } 31 | 32 | void editorMsgClear(void) { 33 | gEditor.con_front = -1; 34 | gEditor.con_rear = 0; 35 | gEditor.con_size = 0; 36 | } 37 | 38 | static void editorSetPrompt(const char* fmt, ...) { 39 | va_list ap; 40 | va_start(ap, fmt); 41 | vsnprintf(gEditor.prompt, sizeof(gEditor.prompt), fmt, ap); 42 | va_end(ap); 43 | } 44 | 45 | static void editorSetRightPrompt(const char* fmt, ...) { 46 | va_list ap; 47 | va_start(ap, fmt); 48 | vsnprintf(gEditor.prompt_right, sizeof(gEditor.prompt_right), fmt, ap); 49 | va_end(ap); 50 | } 51 | 52 | #define PROMPT_BUF_INIT_SIZE 64 53 | #define PROMPT_BUF_GROWTH_RATE 2.0f 54 | 55 | char* editorPrompt(char* prompt, int state, void (*callback)(char*, int)) { 56 | int old_state = gEditor.state; 57 | gEditor.state = state; 58 | 59 | size_t bufsize = PROMPT_BUF_INIT_SIZE; 60 | char* buf = malloc_s(bufsize); 61 | 62 | size_t buflen = 0; 63 | buf[0] = '\0'; 64 | 65 | int start = 0; 66 | while (prompt[start] != '\0' && prompt[start] != '%') { 67 | start++; 68 | } 69 | gEditor.px = start; 70 | while (true) { 71 | editorSetPrompt(prompt, buf); 72 | editorRefreshScreen(); 73 | 74 | EditorInput input = editorReadKey(); 75 | int x = input.data.cursor.x; 76 | int y = input.data.cursor.y; 77 | 78 | size_t idx = gEditor.px - start; 79 | switch (input.type) { 80 | case DEL_KEY: 81 | if (idx != buflen) 82 | idx++; 83 | else 84 | break; 85 | // fall through 86 | case CTRL_KEY('h'): 87 | case BACKSPACE: 88 | if (idx != 0) { 89 | memmove(&buf[idx - 1], &buf[idx], buflen - idx + 1); 90 | buflen--; 91 | idx--; 92 | if (callback) 93 | callback(buf, input.type); 94 | } 95 | break; 96 | 97 | case CTRL_KEY('v'): { 98 | if (!gEditor.clipboard.size) 99 | break; 100 | // Only paste the first line 101 | const char* paste_buf = gEditor.clipboard.data[0]; 102 | size_t paste_len = strlen(paste_buf); 103 | if (paste_len == 0) 104 | break; 105 | 106 | if (buflen + paste_len >= bufsize) { 107 | bufsize = buflen + paste_len + 1; 108 | bufsize *= PROMPT_BUF_GROWTH_RATE; 109 | buf = realloc_s(buf, bufsize); 110 | } 111 | memmove(&buf[idx + paste_len], &buf[idx], buflen - idx + 1); 112 | memcpy(&buf[idx], paste_buf, paste_len); 113 | buflen += paste_len; 114 | idx += paste_len; 115 | 116 | if (callback) 117 | callback(buf, input.type); 118 | 119 | break; 120 | } 121 | 122 | case HOME_KEY: 123 | idx = 0; 124 | break; 125 | 126 | case END_KEY: 127 | idx = buflen; 128 | break; 129 | 130 | case ARROW_LEFT: 131 | if (idx != 0) 132 | idx--; 133 | break; 134 | 135 | case ARROW_RIGHT: 136 | if (idx < buflen) 137 | idx++; 138 | break; 139 | 140 | case WHEEL_UP: 141 | editorScroll(-3); 142 | break; 143 | 144 | case WHEEL_DOWN: 145 | editorScroll(3); 146 | break; 147 | 148 | case MOUSE_PRESSED: { 149 | int field = getMousePosField(x, y); 150 | if (field == FIELD_PROMPT) { 151 | if (x >= start) { 152 | size_t cx = x - start; 153 | if (cx < buflen) 154 | idx = cx; 155 | else 156 | idx = buflen; 157 | } 158 | break; 159 | } else if (field == FIELD_TEXT) { 160 | mousePosToEditorPos(&x, &y); 161 | gCurFile->cursor.y = y; 162 | gCurFile->cursor.x = editorRowRxToCx(&gCurFile->row[y], x); 163 | gCurFile->sx = x; 164 | } 165 | } 166 | // fall through 167 | case CTRL_KEY('q'): 168 | case ESC: 169 | editorSetPrompt(""); 170 | gEditor.state = old_state; 171 | if (callback) 172 | callback(buf, input.type); 173 | free(buf); 174 | return NULL; 175 | 176 | case '\r': 177 | if (buflen != 0) { 178 | editorSetPrompt(""); 179 | gEditor.state = old_state; 180 | if (callback) 181 | callback(buf, input.type); 182 | return buf; 183 | } 184 | break; 185 | 186 | case CHAR_INPUT: { 187 | char output[4]; 188 | int len = encodeUTF8(input.data.unicode, output); 189 | if (len == -1) 190 | return buf; 191 | 192 | if (buflen + len >= bufsize) { 193 | bufsize += len; 194 | bufsize *= PROMPT_BUF_GROWTH_RATE; 195 | buf = realloc_s(buf, bufsize); 196 | } 197 | memmove(&buf[idx + len], &buf[idx], buflen - idx + 1); 198 | memcpy(&buf[idx], output, len); 199 | buflen += len; 200 | idx += len; 201 | 202 | // TODO: Support Unicode characters in prompt 203 | 204 | if (callback) 205 | callback(buf, input.data.unicode); 206 | } break; 207 | 208 | default: 209 | if (callback) 210 | callback(buf, input.type); 211 | } 212 | gEditor.px = start + idx; 213 | } 214 | } 215 | 216 | // Goto 217 | 218 | static void editorGotoCallback(char* query, int key) { 219 | if (key == ESC || key == CTRL_KEY('q')) { 220 | return; 221 | } 222 | 223 | editorMsgClear(); 224 | 225 | if (query == NULL || query[0] == '\0') { 226 | return; 227 | } 228 | 229 | int line = strToInt(query); 230 | 231 | if (line < 0) { 232 | line = gCurFile->num_rows + 1 + line; 233 | } 234 | 235 | if (line > 0 && line <= gCurFile->num_rows) { 236 | gCurFile->cursor.x = 0; 237 | gCurFile->sx = 0; 238 | gCurFile->cursor.y = line - 1; 239 | editorScrollToCursorCenter(); 240 | } else { 241 | editorMsg("Type a line number between 1 to %d (negative too).", 242 | gCurFile->num_rows); 243 | } 244 | } 245 | 246 | void editorGotoLine(void) { 247 | char* query = 248 | editorPrompt("Goto line: %s", GOTO_LINE_MODE, editorGotoCallback); 249 | if (query) { 250 | free(query); 251 | } 252 | } 253 | 254 | // Find 255 | 256 | typedef struct FindList { 257 | struct FindList* prev; 258 | struct FindList* next; 259 | int row; 260 | int col; 261 | } FindList; 262 | 263 | static void findListFree(FindList* thisptr) { 264 | FindList* temp; 265 | while (thisptr) { 266 | temp = thisptr; 267 | thisptr = thisptr->next; 268 | free(temp); 269 | } 270 | } 271 | 272 | static void editorFindCallback(char* query, int key) { 273 | static char* prev_query = NULL; 274 | static FindList head = {.prev = NULL, .next = NULL}; 275 | static FindList* match_node = NULL; 276 | 277 | static uint8_t* saved_hl_pos = NULL; 278 | static uint8_t* saved_hl = NULL; 279 | static size_t saved_hl_len = 0; 280 | 281 | static int total = 0; 282 | static int current = 0; 283 | 284 | if (saved_hl && saved_hl_pos) { 285 | memcpy(saved_hl_pos, saved_hl, saved_hl_len); 286 | free(saved_hl); 287 | saved_hl = NULL; 288 | saved_hl_pos = NULL; 289 | saved_hl_len = 0; 290 | } 291 | 292 | // Quit find mode 293 | if (key == ESC || key == CTRL_KEY('q') || key == '\r' || 294 | key == MOUSE_PRESSED) { 295 | if (prev_query) { 296 | free(prev_query); 297 | prev_query = NULL; 298 | } 299 | if (saved_hl) { 300 | free(saved_hl); 301 | saved_hl = NULL; 302 | } 303 | findListFree(head.next); 304 | head.next = NULL; 305 | editorSetRightPrompt(""); 306 | return; 307 | } 308 | 309 | size_t len = strlen(query); 310 | if (len == 0) { 311 | editorSetRightPrompt(""); 312 | return; 313 | } 314 | 315 | FindList* tail_node = NULL; 316 | if (!head.next || !prev_query || strcmp(prev_query, query) != 0) { 317 | // Recompute find list 318 | 319 | total = 0; 320 | current = 0; 321 | 322 | match_node = NULL; 323 | if (prev_query) 324 | free(prev_query); 325 | findListFree(head.next); 326 | head.next = NULL; 327 | 328 | prev_query = malloc_s(len + 1); 329 | memcpy(prev_query, query, len + 1); 330 | prev_query[len] = '\0'; 331 | 332 | FindList* cur = &head; 333 | for (int i = 0; i < gCurFile->num_rows; i++) { 334 | char* match = NULL; 335 | int col = 0; 336 | char* (*search_func)(const char*, const char*) = &strstr; 337 | 338 | if (CONVAR_GETINT(ignorecase) == 1) { 339 | search_func = &strCaseStr; 340 | } else if (CONVAR_GETINT(ignorecase) == 2) { 341 | bool has_upper = false; 342 | for (size_t j = 0; j < len; j++) { 343 | if (isupper(query[j])) { 344 | has_upper = true; 345 | break; 346 | } 347 | } 348 | if (!has_upper) { 349 | search_func = &strCaseStr; 350 | } 351 | } 352 | 353 | while ((match = (*search_func)(&gCurFile->row[i].data[col], 354 | query)) != 0) { 355 | col = match - gCurFile->row[i].data; 356 | FindList* node = malloc_s(sizeof(FindList)); 357 | 358 | node->prev = cur; 359 | node->next = NULL; 360 | node->row = i; 361 | node->col = col; 362 | cur->next = node; 363 | cur = cur->next; 364 | tail_node = cur; 365 | 366 | total++; 367 | if (!match_node) { 368 | current++; 369 | if (((i == gCurFile->cursor.y && 370 | col >= gCurFile->cursor.x) || 371 | i > gCurFile->cursor.y)) { 372 | match_node = cur; 373 | } 374 | } 375 | col += len; 376 | } 377 | } 378 | 379 | if (!head.next) { 380 | editorSetRightPrompt(" No results"); 381 | return; 382 | } 383 | 384 | if (!match_node) 385 | match_node = head.next; 386 | 387 | // Don't go back to head 388 | head.next->prev = tail_node; 389 | } 390 | 391 | if (key == ARROW_DOWN) { 392 | if (match_node->next) { 393 | match_node = match_node->next; 394 | current++; 395 | } else { 396 | match_node = head.next; 397 | current = 1; 398 | } 399 | } else if (key == ARROW_UP) { 400 | match_node = match_node->prev; 401 | if (current == 1) 402 | current = total; 403 | else 404 | current--; 405 | } 406 | editorSetRightPrompt(" %d of %d", current, total); 407 | 408 | gCurFile->cursor.x = match_node->col; 409 | gCurFile->cursor.y = match_node->row; 410 | 411 | editorScrollToCursorCenter(); 412 | 413 | uint8_t* match_pos = &gCurFile->row[match_node->row].hl[match_node->col]; 414 | saved_hl_len = len; 415 | saved_hl_pos = match_pos; 416 | saved_hl = malloc_s(len + 1); 417 | memcpy(saved_hl, match_pos, len); 418 | for (size_t i = 0; i < len; i++) { 419 | match_pos[i] &= ~HL_BG_MASK; 420 | match_pos[i] |= HL_BG_MATCH << HL_FG_BITS; 421 | } 422 | } 423 | 424 | void editorFind(void) { 425 | char* query = editorPrompt("Find: %s", FIND_MODE, editorFindCallback); 426 | if (query) { 427 | free(query); 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/prompt.h: -------------------------------------------------------------------------------- 1 | #ifndef PROMPT_H 2 | #define PROMPT_H 3 | 4 | void editorMsg(const char* fmt, ...); 5 | void editorMsgClear(void); 6 | 7 | char* editorPrompt(char* prompt, int state, void (*callback)(char*, int)); 8 | void editorGotoLine(void); 9 | void editorFind(void); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /src/row.c: -------------------------------------------------------------------------------- 1 | #include "row.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "defines.h" 7 | #include "highlight.h" 8 | #include "unicode.h" 9 | #include "utils.h" 10 | 11 | void editorUpdateRow(EditorFile* file, EditorRow* row) { 12 | row->rsize = editorRowCxToRx(row, row->size); 13 | editorUpdateSyntax(file, row); 14 | } 15 | 16 | void editorInsertRow(EditorFile* file, int at, const char* s, size_t len) { 17 | if (at < 0 || at > file->num_rows) 18 | return; 19 | 20 | file->row = realloc_s(file->row, sizeof(EditorRow) * (file->num_rows + 1)); 21 | memmove(&file->row[at + 1], &file->row[at], 22 | sizeof(EditorRow) * (file->num_rows - at)); 23 | 24 | file->row[at].size = len; 25 | file->row[at].data = malloc_s(len + 1); 26 | memcpy(file->row[at].data, s, len); 27 | file->row[at].data[len] = '\0'; 28 | 29 | file->row[at].hl = NULL; 30 | file->row[at].hl_open_comment = 0; 31 | editorUpdateRow(file, &file->row[at]); 32 | 33 | file->num_rows++; 34 | file->lineno_width = getDigit(file->num_rows) + 2; 35 | } 36 | 37 | void editorFreeRow(EditorRow* row) { 38 | free(row->data); 39 | free(row->hl); 40 | } 41 | 42 | void editorDelRow(EditorFile* file, int at) { 43 | if (at < 0 || at >= file->num_rows) 44 | return; 45 | editorFreeRow(&file->row[at]); 46 | memmove(&file->row[at], &file->row[at + 1], 47 | sizeof(EditorRow) * (file->num_rows - at - 1)); 48 | 49 | file->num_rows--; 50 | file->lineno_width = getDigit(file->num_rows) + 2; 51 | } 52 | 53 | void editorRowInsertChar(EditorFile* file, EditorRow* row, int at, int c) { 54 | if (at < 0 || at > row->size) 55 | at = row->size; 56 | row->data = realloc_s(row->data, row->size + 2); 57 | memmove(&row->data[at + 1], &row->data[at], row->size - at + 1); 58 | row->size++; 59 | row->data[at] = c; 60 | editorUpdateRow(file, row); 61 | } 62 | 63 | void editorRowDelChar(EditorFile* file, EditorRow* row, int at) { 64 | if (at < 0 || at >= row->size) 65 | return; 66 | memmove(&row->data[at], &row->data[at + 1], row->size - at); 67 | row->size--; 68 | editorUpdateRow(file, row); 69 | } 70 | 71 | void editorRowAppendString(EditorFile* file, EditorRow* row, const char* s, 72 | size_t len) { 73 | row->data = realloc_s(row->data, row->size + len + 1); 74 | memcpy(&row->data[row->size], s, len); 75 | row->size += len; 76 | row->data[row->size] = '\0'; 77 | editorUpdateRow(file, row); 78 | } 79 | 80 | void editorInsertChar(int c) { 81 | if (gCurFile->cursor.y == gCurFile->num_rows) { 82 | editorInsertRow(gCurFile, gCurFile->num_rows, "", 0); 83 | } 84 | if (c == '\t' && CONVAR_GETINT(whitespace)) { 85 | int idx = editorRowCxToRx(&gCurFile->row[gCurFile->cursor.y], 86 | gCurFile->cursor.x) + 87 | 1; 88 | editorInsertChar(' '); 89 | while (idx % CONVAR_GETINT(tabsize) != 0) { 90 | editorInsertChar(' '); 91 | idx++; 92 | } 93 | } else { 94 | editorRowInsertChar(gCurFile, &gCurFile->row[gCurFile->cursor.y], 95 | gCurFile->cursor.x, c); 96 | gCurFile->cursor.x++; 97 | } 98 | } 99 | 100 | void editorInsertUnicode(uint32_t unicode) { 101 | char output[4]; 102 | int len = encodeUTF8(unicode, output); 103 | if (len == -1) 104 | return; 105 | 106 | for (int i = 0; i < len; i++) { 107 | editorInsertChar(output[i]); 108 | } 109 | } 110 | 111 | void editorInsertNewline(void) { 112 | int i = 0; 113 | 114 | if (gCurFile->cursor.x == 0) { 115 | editorInsertRow(gCurFile, gCurFile->cursor.y, "", 0); 116 | } else { 117 | editorInsertRow(gCurFile, gCurFile->cursor.y + 1, "", 0); 118 | EditorRow* curr_row = &gCurFile->row[gCurFile->cursor.y]; 119 | EditorRow* new_row = &gCurFile->row[gCurFile->cursor.y + 1]; 120 | if (CONVAR_GETINT(autoindent)) { 121 | while (i < gCurFile->cursor.x && 122 | (curr_row->data[i] == ' ' || curr_row->data[i] == '\t')) 123 | i++; 124 | if (i != 0) 125 | editorRowAppendString(gCurFile, new_row, curr_row->data, i); 126 | if (curr_row->data[gCurFile->cursor.x - 1] == ':' || 127 | (curr_row->data[gCurFile->cursor.x - 1] == '{' && 128 | curr_row->data[gCurFile->cursor.x] != '}')) { 129 | if (CONVAR_GETINT(whitespace)) { 130 | for (int j = 0; j < CONVAR_GETINT(tabsize); j++, i++) 131 | editorRowAppendString(gCurFile, new_row, " ", 1); 132 | } else { 133 | editorRowAppendString(gCurFile, new_row, "\t", 1); 134 | i++; 135 | } 136 | } 137 | } 138 | editorRowAppendString(gCurFile, new_row, 139 | &curr_row->data[gCurFile->cursor.x], 140 | curr_row->size - gCurFile->cursor.x); 141 | curr_row->size = gCurFile->cursor.x; 142 | curr_row->data[curr_row->size] = '\0'; 143 | editorUpdateRow(gCurFile, curr_row); 144 | } 145 | gCurFile->cursor.y++; 146 | gCurFile->cursor.x = i; 147 | gCurFile->sx = editorRowCxToRx(&gCurFile->row[gCurFile->cursor.y], i); 148 | } 149 | 150 | void editorDelChar(void) { 151 | if (gCurFile->cursor.y == gCurFile->num_rows) 152 | return; 153 | if (gCurFile->cursor.x == 0 && gCurFile->cursor.y == 0) 154 | return; 155 | EditorRow* row = &gCurFile->row[gCurFile->cursor.y]; 156 | if (gCurFile->cursor.x > 0) { 157 | editorRowDelChar(gCurFile, row, gCurFile->cursor.x - 1); 158 | gCurFile->cursor.x--; 159 | } else { 160 | gCurFile->cursor.x = gCurFile->row[gCurFile->cursor.y - 1].size; 161 | editorRowAppendString(gCurFile, &gCurFile->row[gCurFile->cursor.y - 1], 162 | row->data, row->size); 163 | editorDelRow(gCurFile, gCurFile->cursor.y); 164 | gCurFile->cursor.y--; 165 | } 166 | gCurFile->sx = 167 | editorRowCxToRx(&gCurFile->row[gCurFile->cursor.y], gCurFile->cursor.x); 168 | } 169 | 170 | int editorRowNextUTF8(EditorRow* row, int cx) { 171 | if (cx < 0) 172 | return 0; 173 | 174 | if (cx >= row->size) 175 | return row->size; 176 | 177 | const char* s = &row->data[cx]; 178 | size_t byte_size; 179 | decodeUTF8(s, row->size - cx, &byte_size); 180 | return cx + byte_size; 181 | } 182 | 183 | int editorRowPreviousUTF8(EditorRow* row, int cx) { 184 | if (cx <= 0) 185 | return 0; 186 | 187 | if (cx > row->size) 188 | return row->size; 189 | 190 | int i = 0; 191 | size_t byte_size = 0; 192 | while (i < cx) { 193 | decodeUTF8(&row->data[i], row->size - i, &byte_size); 194 | i += byte_size; 195 | } 196 | return i - byte_size; 197 | } 198 | 199 | int editorRowCxToRx(const EditorRow* row, int cx) { 200 | int rx = 0; 201 | int i = 0; 202 | while (i < cx) { 203 | size_t byte_size; 204 | uint32_t unicode = decodeUTF8(&row->data[i], row->size - i, &byte_size); 205 | if (unicode == '\t') { 206 | rx += (CONVAR_GETINT(tabsize) - 1) - (rx % CONVAR_GETINT(tabsize)) + 207 | 1; 208 | } else { 209 | int width = unicodeWidth(unicode); 210 | if (width < 0) 211 | width = 1; 212 | rx += width; 213 | } 214 | i += byte_size; 215 | } 216 | return rx; 217 | } 218 | 219 | int editorRowRxToCx(const EditorRow* row, int rx) { 220 | int cur_rx = 0; 221 | int cx = 0; 222 | while (cx < row->size) { 223 | size_t byte_size; 224 | uint32_t unicode = 225 | decodeUTF8(&row->data[cx], row->size - cx, &byte_size); 226 | if (unicode == '\t') { 227 | cur_rx += (CONVAR_GETINT(tabsize) - 1) - 228 | (cur_rx % CONVAR_GETINT(tabsize)) + 1; 229 | } else { 230 | int width = unicodeWidth(unicode); 231 | if (width < 0) 232 | width = 1; 233 | cur_rx += width; 234 | } 235 | if (cur_rx > rx) 236 | return cx; 237 | cx += byte_size; 238 | } 239 | return cx; 240 | } 241 | -------------------------------------------------------------------------------- /src/row.h: -------------------------------------------------------------------------------- 1 | #ifndef ROW_H 2 | #define ROW_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct EditorFile; 9 | typedef struct EditorFile EditorFile; 10 | 11 | typedef struct EditorRow { 12 | int size; 13 | int rsize; 14 | char* data; 15 | uint8_t* hl; 16 | int hl_open_comment; 17 | } EditorRow; 18 | 19 | void editorUpdateRow(EditorFile* file, EditorRow* row); 20 | void editorInsertRow(EditorFile* file, int at, const char* s, size_t len); 21 | void editorFreeRow(EditorRow* row); 22 | void editorDelRow(EditorFile* file, int at); 23 | void editorRowInsertChar(EditorFile* file, EditorRow* row, int at, int c); 24 | void editorRowDelChar(EditorFile* file, EditorRow* row, int at); 25 | void editorRowAppendString(EditorFile* file, EditorRow* row, const char* s, 26 | size_t len); 27 | 28 | // On gCurFile 29 | void editorInsertChar(int c); 30 | void editorInsertUnicode(uint32_t unicode); 31 | void editorInsertNewline(void); 32 | void editorDelChar(void); 33 | 34 | // UTF-8 35 | int editorRowPreviousUTF8(EditorRow* row, int cx); 36 | int editorRowNextUTF8(EditorRow* row, int cx); 37 | 38 | // Cx Rx 39 | int editorRowCxToRx(const EditorRow* row, int cx); 40 | int editorRowRxToCx(const EditorRow* row, int rx); 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /src/select.c: -------------------------------------------------------------------------------- 1 | #include "select.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "config.h" 8 | #include "editor.h" 9 | #include "row.h" 10 | #include "utils.h" 11 | 12 | void getSelectStartEnd(EditorSelectRange* range) { 13 | if (gCurFile->cursor.select_y > gCurFile->cursor.y) { 14 | range->start_x = gCurFile->cursor.x; 15 | range->start_y = gCurFile->cursor.y; 16 | range->end_x = gCurFile->cursor.select_x; 17 | range->end_y = gCurFile->cursor.select_y; 18 | } else if (gCurFile->cursor.select_y < gCurFile->cursor.y) { 19 | range->start_x = gCurFile->cursor.select_x; 20 | range->start_y = gCurFile->cursor.select_y; 21 | range->end_x = gCurFile->cursor.x; 22 | range->end_y = gCurFile->cursor.y; 23 | } else { 24 | // same row 25 | range->start_y = range->end_y = gCurFile->cursor.y; 26 | range->start_x = gCurFile->cursor.select_x > gCurFile->cursor.x 27 | ? gCurFile->cursor.x 28 | : gCurFile->cursor.select_x; 29 | range->end_x = gCurFile->cursor.select_x > gCurFile->cursor.x 30 | ? gCurFile->cursor.select_x 31 | : gCurFile->cursor.x; 32 | } 33 | } 34 | 35 | bool isPosSelected(int row, int col, EditorSelectRange range) { 36 | if (range.start_y < row && row < range.end_y) 37 | return true; 38 | 39 | if (range.start_y == row && range.end_y == row) 40 | return range.start_x <= col && col < range.end_x; 41 | 42 | if (range.start_y == row) 43 | return range.start_x <= col; 44 | 45 | if (range.end_y == row) 46 | return col < range.end_x; 47 | 48 | return false; 49 | } 50 | 51 | void editorDeleteText(EditorSelectRange range) { 52 | if (range.start_x == range.end_x && range.start_y == range.end_y) 53 | return; 54 | 55 | gCurFile->cursor.x = range.end_x; 56 | gCurFile->cursor.y = range.end_y; 57 | 58 | if (range.end_y - range.start_y > 1) { 59 | for (int i = range.start_y + 1; i < range.end_y; i++) { 60 | editorFreeRow(&gCurFile->row[i]); 61 | } 62 | int removed_rows = range.end_y - range.start_y - 1; 63 | memmove(&gCurFile->row[range.start_y + 1], &gCurFile->row[range.end_y], 64 | sizeof(EditorRow) * (gCurFile->num_rows - range.end_y)); 65 | 66 | gCurFile->num_rows -= removed_rows; 67 | gCurFile->cursor.y -= removed_rows; 68 | 69 | gCurFile->lineno_width = getDigit(gCurFile->num_rows) + 2; 70 | } 71 | while (gCurFile->cursor.y != range.start_y || 72 | gCurFile->cursor.x != range.start_x) { 73 | editorDelChar(); 74 | } 75 | } 76 | 77 | void editorCopyText(EditorClipboard* clipboard, EditorSelectRange range) { 78 | if (range.start_x == range.end_x && range.start_y == range.end_y) { 79 | clipboard->size = 0; 80 | clipboard->data = NULL; 81 | return; 82 | } 83 | 84 | clipboard->size = range.end_y - range.start_y + 1; 85 | clipboard->data = malloc_s(sizeof(char*) * clipboard->size); 86 | // Only one line 87 | if (range.start_y == range.end_y) { 88 | clipboard->data[0] = malloc_s(range.end_x - range.start_x + 1); 89 | memcpy(clipboard->data[0], 90 | &gCurFile->row[range.start_y].data[range.start_x], 91 | range.end_x - range.start_x); 92 | clipboard->data[0][range.end_x - range.start_x] = '\0'; 93 | return; 94 | } 95 | 96 | // First line 97 | size_t size = gCurFile->row[range.start_y].size - range.start_x; 98 | clipboard->data[0] = malloc_s(size + 1); 99 | memcpy(clipboard->data[0], 100 | &gCurFile->row[range.start_y].data[range.start_x], size); 101 | clipboard->data[0][size] = '\0'; 102 | 103 | // Middle 104 | for (int i = range.start_y + 1; i < range.end_y; i++) { 105 | size = gCurFile->row[i].size; 106 | clipboard->data[i - range.start_y] = malloc_s(size + 1); 107 | memcpy(clipboard->data[i - range.start_y], gCurFile->row[i].data, size); 108 | clipboard->data[i - range.start_y][size] = '\0'; 109 | } 110 | // Last line 111 | size = range.end_x; 112 | clipboard->data[range.end_y - range.start_y] = malloc_s(size + 1); 113 | memcpy(clipboard->data[range.end_y - range.start_y], 114 | gCurFile->row[range.end_y].data, size); 115 | clipboard->data[range.end_y - range.start_y][size] = '\0'; 116 | } 117 | 118 | void editorPasteText(const EditorClipboard* clipboard, int x, int y) { 119 | if (!clipboard->size) 120 | return; 121 | 122 | gCurFile->cursor.x = x; 123 | gCurFile->cursor.y = y; 124 | 125 | if (clipboard->size == 1) { 126 | EditorRow* row = &gCurFile->row[y]; 127 | char* paste = clipboard->data[0]; 128 | size_t paste_len = strlen(paste); 129 | 130 | row->data = realloc_s(row->data, row->size + paste_len + 1); 131 | memmove(&row->data[x + paste_len], &row->data[x], row->size - x); 132 | memcpy(&row->data[x], paste, paste_len); 133 | row->size += paste_len; 134 | row->data[row->size] = '\0'; 135 | editorUpdateRow(gCurFile, row); 136 | gCurFile->cursor.x += paste_len; 137 | } else { 138 | // First line 139 | int auto_indent = CONVAR_GETINT(autoindent); 140 | CONVAR_GETINT(autoindent) = 0; 141 | editorInsertNewline(); 142 | CONVAR_GETINT(autoindent) = auto_indent; 143 | editorRowAppendString(gCurFile, &gCurFile->row[y], clipboard->data[0], 144 | strlen(clipboard->data[0])); 145 | // Middle 146 | for (size_t i = 1; i < clipboard->size - 1; i++) { 147 | editorInsertRow(gCurFile, y + i, clipboard->data[i], 148 | strlen(clipboard->data[i])); 149 | } 150 | // Last line 151 | EditorRow* row = &gCurFile->row[y + clipboard->size - 1]; 152 | char* paste = clipboard->data[clipboard->size - 1]; 153 | size_t paste_len = strlen(paste); 154 | 155 | row->data = realloc_s(row->data, row->size + paste_len + 1); 156 | memmove(&row->data[paste_len], row->data, row->size); 157 | memcpy(row->data, paste, paste_len); 158 | row->size += paste_len; 159 | row->data[row->size] = '\0'; 160 | editorUpdateRow(gCurFile, row); 161 | 162 | gCurFile->cursor.y = y + clipboard->size - 1; 163 | gCurFile->cursor.x = paste_len; 164 | } 165 | gCurFile->sx = 166 | editorRowCxToRx(&gCurFile->row[gCurFile->cursor.y], gCurFile->cursor.x); 167 | } 168 | 169 | void editorFreeClipboardContent(EditorClipboard* clipboard) { 170 | if (!clipboard || !clipboard->size) 171 | return; 172 | for (size_t i = 0; i < clipboard->size; i++) { 173 | free(clipboard->data[i]); 174 | } 175 | clipboard->size = 0; 176 | free(clipboard->data); 177 | } 178 | 179 | void editorCopyToSysClipboard(EditorClipboard* clipboard) { 180 | if (!CONVAR_GETINT(osc52_copy)) 181 | return; 182 | 183 | if (!clipboard || !clipboard->size) 184 | return; 185 | 186 | abuf ab = ABUF_INIT; 187 | for (size_t i = 0; i < clipboard->size; i++) { 188 | if (i != 0) 189 | abufAppendN(&ab, "\n", 1); 190 | abufAppend(&ab, clipboard->data[i]); 191 | } 192 | 193 | int b64_len = base64EncodeLen(ab.len); 194 | char* b64_buf = malloc_s(b64_len * sizeof(char)); 195 | 196 | b64_len = base64Encode(ab.buf, ab.len, b64_buf); 197 | 198 | bool tmux = (getenv("TMUX") != NULL); 199 | if (tmux) { 200 | fprintf(stdout, "\x1bPtmux;\x1b"); 201 | } 202 | 203 | fprintf(stdout, "\x1b]52;c;%.*s\x07", b64_len, b64_buf); 204 | 205 | if (tmux) { 206 | fprintf(stdout, "\x1b\\"); 207 | } 208 | 209 | fflush(stdout); 210 | 211 | free(b64_buf); 212 | abufFree(&ab); 213 | } 214 | -------------------------------------------------------------------------------- /src/select.h: -------------------------------------------------------------------------------- 1 | #ifndef SELECT_H 2 | #define SELECT_H 3 | 4 | #include 5 | #include 6 | 7 | typedef struct EditorClipboard { 8 | size_t size; 9 | char** data; 10 | } EditorClipboard; 11 | 12 | typedef struct EditorSelectRange { 13 | int start_x; 14 | int start_y; 15 | int end_x; 16 | int end_y; 17 | } EditorSelectRange; 18 | 19 | void getSelectStartEnd(EditorSelectRange* range); 20 | bool isPosSelected(int row, int col, EditorSelectRange range); 21 | 22 | void editorDeleteText(EditorSelectRange range); 23 | void editorCopyText(EditorClipboard* clipboard, EditorSelectRange range); 24 | void editorPasteText(const EditorClipboard* clipboard, int x, int y); 25 | 26 | void editorFreeClipboardContent(EditorClipboard* clipboard); 27 | 28 | void editorCopyToSysClipboard(EditorClipboard* clipboard); 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/terminal.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE // SIGWINCH 2 | 3 | #include "terminal.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "defines.h" 14 | #include "editor.h" 15 | #include "os.h" 16 | #include "output.h" 17 | 18 | #ifdef _WIN32 19 | static HANDLE hStdin = INVALID_HANDLE_VALUE; 20 | static HANDLE hStdout = INVALID_HANDLE_VALUE; 21 | 22 | static DWORD orig_in_mode; 23 | static DWORD orig_out_mode; 24 | #else 25 | #include 26 | #include 27 | 28 | static struct termios orig_termios; 29 | #endif 30 | 31 | static void disableRawMode(void) { 32 | #ifdef _WIN32 33 | SetConsoleMode(hStdin, orig_in_mode); 34 | SetConsoleMode(hStdout, orig_out_mode); 35 | #else 36 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) 37 | PANIC("tcsetattr"); 38 | #endif 39 | } 40 | 41 | static void enableRawMode(void) { 42 | #ifdef _WIN32 43 | if (!SetConsoleCP(CP_UTF8)) 44 | PANIC("SetConsoleCP"); 45 | 46 | if (!SetConsoleOutputCP(CP_UTF8)) 47 | PANIC("SetConsoleOutputCP"); 48 | 49 | DWORD mode = 0; 50 | 51 | if (!GetConsoleMode(hStdin, &mode)) 52 | PANIC("GetConsoleMode(hStdin)"); 53 | orig_in_mode = mode; 54 | mode |= ENABLE_VIRTUAL_TERMINAL_INPUT; 55 | mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); 56 | if (!SetConsoleMode(hStdin, mode)) 57 | PANIC("SetConsoleMode(hStdin)"); 58 | 59 | if (!GetConsoleMode(hStdout, &mode)) 60 | PANIC("GetConsoleMode(hStdout)"); 61 | orig_out_mode = mode; 62 | mode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; 63 | mode &= ~ENABLE_WRAP_AT_EOL_OUTPUT; 64 | if (!SetConsoleMode(hStdout, mode)) 65 | PANIC("SetConsoleMode(hStdout)"); 66 | #else 67 | if (tcgetattr(STDIN_FILENO, &orig_termios) == -1) 68 | PANIC("tcgetattr"); 69 | 70 | struct termios raw = orig_termios; 71 | 72 | raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 73 | raw.c_oflag &= ~(OPOST); 74 | raw.c_cflag |= (CS8); 75 | raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 76 | raw.c_cc[VMIN] = 0; 77 | raw.c_cc[VTIME] = 1; 78 | 79 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) 80 | PANIC("tcsetattr"); 81 | #endif 82 | } 83 | 84 | // Reads a character from the console. 85 | static bool readConsole(uint32_t* unicode) { 86 | #ifdef _WIN32 87 | WCHAR wbuf[1]; 88 | DWORD n = 0; 89 | if (!ReadConsoleW(hStdin, wbuf, 1, &n, NULL) || !n) { 90 | return false; 91 | } 92 | *unicode = (uint32_t)wbuf[0]; 93 | return true; 94 | #else 95 | // Decode UTF-8 96 | 97 | int bytes; 98 | uint8_t first_byte; 99 | 100 | if (read(STDIN_FILENO, &first_byte, 1) != 1) { 101 | return false; 102 | } 103 | 104 | if ((first_byte & 0x80) == 0x00) { 105 | *unicode = (uint32_t)first_byte; 106 | return true; 107 | } 108 | 109 | if ((first_byte & 0xE0) == 0xC0) { 110 | *unicode = (first_byte & 0x1F) << 6; 111 | bytes = 1; 112 | } else if ((first_byte & 0xF0) == 0xE0) { 113 | *unicode = (first_byte & 0x0F) << 12; 114 | bytes = 2; 115 | } else if ((first_byte & 0xF8) == 0xF0) { 116 | *unicode = (first_byte & 0x07) << 18; 117 | bytes = 3; 118 | } else { 119 | return false; 120 | } 121 | 122 | uint8_t buf[3]; 123 | if (read(STDIN_FILENO, buf, bytes) != bytes) { 124 | return false; 125 | } 126 | 127 | int shift = (bytes - 1) * 6; 128 | for (int i = 0; i < bytes; i++) { 129 | if ((buf[i] & 0xC0) != 0x80) { 130 | return false; 131 | } 132 | *unicode |= (buf[i] & 0x3F) << shift; 133 | shift -= 6; 134 | } 135 | 136 | return true; 137 | #endif 138 | } 139 | 140 | typedef struct { 141 | const char* str; 142 | int value; 143 | } StrIntPair; 144 | 145 | static const StrIntPair sequence_lookup[] = { 146 | {"[1~", HOME_KEY}, 147 | // {"[2~", INSERT_KEY}, 148 | {"[3~", DEL_KEY}, 149 | {"[4~", END_KEY}, 150 | {"[5~", PAGE_UP}, 151 | {"[6~", PAGE_DOWN}, 152 | {"[7~", HOME_KEY}, 153 | {"[8~", END_KEY}, 154 | 155 | {"[A", ARROW_UP}, 156 | {"[B", ARROW_DOWN}, 157 | {"[C", ARROW_RIGHT}, 158 | {"[D", ARROW_LEFT}, 159 | {"[F", END_KEY}, 160 | {"[H", HOME_KEY}, 161 | 162 | /* 163 | Code Modifiers 164 | ---------+--------------------------- 165 | 2 | Shift 166 | 3 | Alt 167 | 4 | Shift + Alt 168 | 5 | Control 169 | 6 | Shift + Control 170 | 7 | Alt + Control 171 | 8 | Shift + Alt + Control 172 | 9 | Meta 173 | 10 | Meta + Shift 174 | 11 | Meta + Alt 175 | 12 | Meta + Alt + Shift 176 | 13 | Meta + Ctrl 177 | 14 | Meta + Ctrl + Shift 178 | 15 | Meta + Ctrl + Alt 179 | 16 | Meta + Ctrl + Alt + Shift 180 | ---------+--------------------------- 181 | */ 182 | 183 | // Shift 184 | {"[1;2A", SHIFT_UP}, 185 | {"[1;2B", SHIFT_DOWN}, 186 | {"[1;2C", SHIFT_RIGHT}, 187 | {"[1;2D", SHIFT_LEFT}, 188 | {"[1;2F", SHIFT_END}, 189 | {"[1;2H", SHIFT_HOME}, 190 | 191 | // Alt 192 | {"[1;3A", ALT_UP}, 193 | {"[1;3B", ALT_DOWN}, 194 | 195 | // Shift+Alt 196 | {"[1;4A", SHIFT_ALT_UP}, 197 | {"[1;4B", SHIFT_ALT_DOWN}, 198 | 199 | // Ctrl 200 | {"[1;5A", CTRL_UP}, 201 | {"[1;5B", CTRL_DOWN}, 202 | {"[1;5C", CTRL_RIGHT}, 203 | {"[1;5D", CTRL_LEFT}, 204 | {"[1;5F", CTRL_END}, 205 | {"[1;5H", CTRL_HOME}, 206 | 207 | // Shift+Ctrl 208 | {"[1;6A", SHIFT_CTRL_UP}, 209 | {"[1;6B", SHIFT_CTRL_DOWN}, 210 | {"[1;6C", SHIFT_CTRL_RIGHT}, 211 | {"[1;6D", SHIFT_CTRL_LEFT}, 212 | 213 | // Page UP / Page Down 214 | {"[5;2~", SHIFT_PAGE_UP}, 215 | {"[6;2~", SHIFT_PAGE_DOWN}, 216 | {"[5;5~", CTRL_PAGE_UP}, 217 | {"[6;5~", CTRL_PAGE_DOWN}, 218 | {"[5;6~", SHIFT_CTRL_PAGE_UP}, 219 | {"[6;6~", SHIFT_CTRL_PAGE_DOWN}, 220 | }; 221 | 222 | EditorInput editorReadKey(void) { 223 | uint32_t c; 224 | EditorInput result = {.type = UNKNOWN}; 225 | 226 | #ifdef _WIN32 227 | // TODO: Detect window resize event 228 | resizeWindow(); 229 | #endif 230 | 231 | while (!readConsole(&c)) { 232 | } 233 | 234 | if (c == ESC) { 235 | char seq[16] = {0}; 236 | bool success = false; 237 | if (!readConsole(&c)) { 238 | result.type = ESC; 239 | return result; 240 | } 241 | seq[0] = (char)c; 242 | 243 | if (seq[0] != '[') { 244 | result.type = ALT_KEY(seq[0]); 245 | return result; 246 | } 247 | 248 | for (size_t i = 1; i < sizeof(seq) - 1; i++) { 249 | if (!readConsole(&c)) { 250 | return result; 251 | } 252 | seq[i] = (char)c; 253 | if (isupper(seq[i]) || seq[i] == 'm' || seq[i] == '~') { 254 | success = true; 255 | break; 256 | } 257 | } 258 | 259 | if (!success) { 260 | return result; 261 | } 262 | 263 | // Mouse input 264 | if (seq[1] == '<' && gEditor.mouse_mode) { 265 | int type; 266 | char m; 267 | sscanf(&seq[2], "%d;%d;%d%c", &type, &result.data.cursor.x, 268 | &result.data.cursor.y, &m); 269 | (*&result.data.cursor.x)--; 270 | (*&result.data.cursor.y)--; 271 | 272 | switch (type) { 273 | case 0: 274 | if (m == 'M') { 275 | result.type = MOUSE_PRESSED; 276 | } else if (m == 'm') { 277 | result.type = MOUSE_RELEASED; 278 | } 279 | break; 280 | case 1: 281 | if (m == 'M') { 282 | result.type = SCROLL_PRESSED; 283 | } else if (m == 'm') { 284 | result.type = SCROLL_RELEASED; 285 | } 286 | break; 287 | case 32: 288 | result.type = MOUSE_MOVE; 289 | break; 290 | case 64: 291 | result.type = WHEEL_UP; 292 | break; 293 | case 65: 294 | result.type = WHEEL_DOWN; 295 | break; 296 | default: 297 | break; 298 | } 299 | return result; 300 | } 301 | 302 | for (size_t i = 0; 303 | i < sizeof(sequence_lookup) / sizeof(sequence_lookup[0]); i++) { 304 | if (strcmp(sequence_lookup[i].str, seq) == 0) { 305 | result.type = sequence_lookup[i].value; 306 | return result; 307 | } 308 | } 309 | return result; 310 | } 311 | 312 | if ((c <= 31 || c == BACKSPACE) && c != '\t') { 313 | result.type = c; 314 | return result; 315 | } 316 | 317 | result.type = CHAR_INPUT; 318 | result.data.unicode = c; 319 | 320 | return result; 321 | } 322 | 323 | #ifdef _WIN32 324 | static int getWindowSize(int* rows, int* cols) { 325 | CONSOLE_SCREEN_BUFFER_INFO csbi; 326 | 327 | if (GetConsoleScreenBufferInfo(hStdout, &csbi)) { 328 | *cols = csbi.srWindow.Right - csbi.srWindow.Left + 1; 329 | *rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; 330 | return 0; 331 | } 332 | return -1; 333 | } 334 | #else 335 | static int getCursorPos(int* rows, int* cols) { 336 | char buf[32]; 337 | size_t i = 0; 338 | 339 | if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) 340 | return -1; 341 | 342 | while (i < sizeof(buf) - 1) { 343 | if (read(STDIN_FILENO, &buf[i], 1) != 1) 344 | break; 345 | if (buf[i] == 'R') 346 | break; 347 | i++; 348 | } 349 | buf[i] = '\0'; 350 | 351 | if (buf[0] != '\x1b' || buf[1] != '[') 352 | return -1; 353 | if (sscanf(&buf[2], "%d;%d", rows, cols) != 2) 354 | return -1; 355 | return 0; 356 | } 357 | 358 | static int getWindowSize(int* rows, int* cols) { 359 | struct winsize ws; 360 | if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { 361 | if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) 362 | return -1; 363 | return getCursorPos(rows, cols); 364 | } else { 365 | *cols = ws.ws_col; 366 | *rows = ws.ws_row; 367 | return 0; 368 | } 369 | } 370 | #endif 371 | 372 | static void SIGSEGV_handler(int sig) { 373 | if (sig != SIGSEGV) 374 | return; 375 | terminalExit(); 376 | UNUSED(write(STDOUT_FILENO, "Exit from SIGSEGV_handler\r\n", 27)); 377 | _exit(EXIT_FAILURE); 378 | } 379 | 380 | static void SIGABRT_handler(int sig) { 381 | if (sig != SIGABRT) 382 | return; 383 | terminalExit(); 384 | UNUSED(write(STDOUT_FILENO, "Exit from SIGABRT_handler\r\n", 27)); 385 | _exit(EXIT_FAILURE); 386 | } 387 | 388 | static void enableSwap(void) { 389 | UNUSED(write(STDOUT_FILENO, "\x1b[?1049h\x1b[H", 11)); 390 | } 391 | 392 | static void disableSwap(void) { 393 | UNUSED(write(STDOUT_FILENO, "\x1b[?1049l", 8)); 394 | } 395 | 396 | void enableMouse(void) { 397 | if (!gEditor.mouse_mode && 398 | write(STDOUT_FILENO, "\x1b[?1002h\x1b[?1015h\x1b[?1006h", 24) == 24) 399 | gEditor.mouse_mode = true; 400 | } 401 | 402 | void disableMouse(void) { 403 | if (gEditor.mouse_mode && 404 | write(STDOUT_FILENO, "\x1b[?1002l\x1b[?1015l\x1b[?1006l", 24) == 24) 405 | gEditor.mouse_mode = false; 406 | } 407 | 408 | #ifndef _WIN32 409 | static void SIGWINCH_handler(int sig) { 410 | if (sig != SIGWINCH) 411 | return; 412 | resizeWindow(); 413 | } 414 | #endif 415 | 416 | void resizeWindow(void) { 417 | int rows = 0; 418 | int cols = 0; 419 | 420 | if (getWindowSize(&rows, &cols) == -1) 421 | PANIC("getWindowSize"); 422 | 423 | if (gEditor.screen_rows != rows || gEditor.screen_cols != cols) { 424 | gEditor.screen_rows = rows; 425 | gEditor.screen_cols = cols; 426 | // TODO: Don't hard coding rows 427 | gEditor.display_rows = rows - 2; 428 | 429 | if (!gEditor.loading) 430 | editorRefreshScreen(); 431 | } 432 | } 433 | 434 | void editorInitTerminal(void) { 435 | #ifdef _WIN32 436 | hStdin = GetStdHandle(STD_INPUT_HANDLE); 437 | if (hStdin == INVALID_HANDLE_VALUE) 438 | PANIC("GetStdHandle(STD_INPUT_HANDLE)"); 439 | hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 440 | if (hStdout == INVALID_HANDLE_VALUE) 441 | PANIC("GetStdHandle(STD_OUTPUT_HANDLE)"); 442 | #endif 443 | 444 | enableRawMode(); 445 | enableSwap(); 446 | // Mouse mode default on 447 | enableMouse(); 448 | atexit(terminalExit); 449 | resizeWindow(); 450 | 451 | if (signal(SIGSEGV, SIGSEGV_handler) == SIG_ERR) { 452 | PANIC("SIGSEGV_handler"); 453 | } 454 | 455 | if (signal(SIGABRT, SIGABRT_handler) == SIG_ERR) { 456 | PANIC("SIGABRT_handler"); 457 | } 458 | 459 | #ifndef _WIN32 460 | if (signal(SIGWINCH, SIGWINCH_handler) == SIG_ERR) { 461 | PANIC("SIGWINCH_handler"); 462 | } 463 | #endif 464 | } 465 | 466 | void terminalExit(void) { 467 | disableMouse(); 468 | disableSwap(); 469 | // Show cursor 470 | UNUSED(write(STDOUT_FILENO, "\x1b[?25h", 6)); 471 | disableRawMode(); 472 | } 473 | -------------------------------------------------------------------------------- /src/terminal.h: -------------------------------------------------------------------------------- 1 | #ifndef TERMINAL_H 2 | #define TERMINAL_H 3 | 4 | #include 5 | 6 | typedef struct EditorInput { 7 | int type; 8 | union { 9 | uint32_t unicode; 10 | struct { 11 | int x; 12 | int y; 13 | } cursor; 14 | } data; 15 | } EditorInput; 16 | 17 | void editorInitTerminal(void); 18 | EditorInput editorReadKey(void); 19 | 20 | void enableMouse(void); 21 | void disableMouse(void); 22 | 23 | void resizeWindow(void); 24 | void terminalExit(void); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /src/unicode.c: -------------------------------------------------------------------------------- 1 | #include "unicode.h" 2 | 3 | #include 4 | #include 5 | 6 | struct interval { 7 | uint32_t start, end; 8 | }; 9 | 10 | static const struct interval zero_width[] = { 11 | {0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD}, 12 | {0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5}, 13 | {0x05C7, 0x05C7}, {0x0600, 0x0605}, {0x0610, 0x061A}, 14 | {0x061C, 0x061C}, {0x064B, 0x065F}, {0x0670, 0x0670}, 15 | {0x06D6, 0x06DD}, {0x06DF, 0x06E4}, {0x06E7, 0x06E8}, 16 | {0x06EA, 0x06ED}, {0x070F, 0x070F}, {0x0711, 0x0711}, 17 | {0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3}, 18 | {0x07FD, 0x07FD}, {0x0816, 0x0819}, {0x081B, 0x0823}, 19 | {0x0825, 0x0827}, {0x0829, 0x082D}, {0x0859, 0x085B}, 20 | {0x0890, 0x0891}, {0x0898, 0x089F}, {0x08CA, 0x0902}, 21 | {0x093A, 0x093A}, {0x093C, 0x093C}, {0x0941, 0x0948}, 22 | {0x094D, 0x094D}, {0x0951, 0x0957}, {0x0962, 0x0963}, 23 | {0x0981, 0x0981}, {0x09BC, 0x09BC}, {0x09C1, 0x09C4}, 24 | {0x09CD, 0x09CD}, {0x09E2, 0x09E3}, {0x09FE, 0x09FE}, 25 | {0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, {0x0A41, 0x0A42}, 26 | {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51}, 27 | {0x0A70, 0x0A71}, {0x0A75, 0x0A75}, {0x0A81, 0x0A82}, 28 | {0x0ABC, 0x0ABC}, {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8}, 29 | {0x0ACD, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0AFA, 0x0AFF}, 30 | {0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, {0x0B3F, 0x0B3F}, 31 | {0x0B41, 0x0B44}, {0x0B4D, 0x0B4D}, {0x0B55, 0x0B56}, 32 | {0x0B62, 0x0B63}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0}, 33 | {0x0BCD, 0x0BCD}, {0x0C00, 0x0C00}, {0x0C04, 0x0C04}, 34 | {0x0C3C, 0x0C3C}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48}, 35 | {0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0C62, 0x0C63}, 36 | {0x0C81, 0x0C81}, {0x0CBC, 0x0CBC}, {0x0CBF, 0x0CBF}, 37 | {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, {0x0CE2, 0x0CE3}, 38 | {0x0D00, 0x0D01}, {0x0D3B, 0x0D3C}, {0x0D41, 0x0D44}, 39 | {0x0D4D, 0x0D4D}, {0x0D62, 0x0D63}, {0x0D81, 0x0D81}, 40 | {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6}, 41 | {0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E}, 42 | {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EBC}, {0x0EC8, 0x0ECD}, 43 | {0x0F18, 0x0F19}, {0x0F35, 0x0F35}, {0x0F37, 0x0F37}, 44 | {0x0F39, 0x0F39}, {0x0F71, 0x0F7E}, {0x0F80, 0x0F84}, 45 | {0x0F86, 0x0F87}, {0x0F8D, 0x0F97}, {0x0F99, 0x0FBC}, 46 | {0x0FC6, 0x0FC6}, {0x102D, 0x1030}, {0x1032, 0x1037}, 47 | {0x1039, 0x103A}, {0x103D, 0x103E}, {0x1058, 0x1059}, 48 | {0x105E, 0x1060}, {0x1071, 0x1074}, {0x1082, 0x1082}, 49 | {0x1085, 0x1086}, {0x108D, 0x108D}, {0x109D, 0x109D}, 50 | {0x1160, 0x11FF}, {0x135D, 0x135F}, {0x1712, 0x1714}, 51 | {0x1732, 0x1733}, {0x1752, 0x1753}, {0x1772, 0x1773}, 52 | {0x17B4, 0x17B5}, {0x17B7, 0x17BD}, {0x17C6, 0x17C6}, 53 | {0x17C9, 0x17D3}, {0x17DD, 0x17DD}, {0x180B, 0x180F}, 54 | {0x1885, 0x1886}, {0x18A9, 0x18A9}, {0x1920, 0x1922}, 55 | {0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193B}, 56 | {0x1A17, 0x1A18}, {0x1A1B, 0x1A1B}, {0x1A56, 0x1A56}, 57 | {0x1A58, 0x1A5E}, {0x1A60, 0x1A60}, {0x1A62, 0x1A62}, 58 | {0x1A65, 0x1A6C}, {0x1A73, 0x1A7C}, {0x1A7F, 0x1A7F}, 59 | {0x1AB0, 0x1ACE}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34}, 60 | {0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, 61 | {0x1B6B, 0x1B73}, {0x1B80, 0x1B81}, {0x1BA2, 0x1BA5}, 62 | {0x1BA8, 0x1BA9}, {0x1BAB, 0x1BAD}, {0x1BE6, 0x1BE6}, 63 | {0x1BE8, 0x1BE9}, {0x1BED, 0x1BED}, {0x1BEF, 0x1BF1}, 64 | {0x1C2C, 0x1C33}, {0x1C36, 0x1C37}, {0x1CD0, 0x1CD2}, 65 | {0x1CD4, 0x1CE0}, {0x1CE2, 0x1CE8}, {0x1CED, 0x1CED}, 66 | {0x1CF4, 0x1CF4}, {0x1CF8, 0x1CF9}, {0x1DC0, 0x1DFF}, 67 | {0x200B, 0x200F}, {0x202A, 0x202E}, {0x2060, 0x2064}, 68 | {0x2066, 0x206F}, {0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, 69 | {0x2D7F, 0x2D7F}, {0x2DE0, 0x2DFF}, {0x302A, 0x302D}, 70 | {0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D}, 71 | {0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA802, 0xA802}, 72 | {0xA806, 0xA806}, {0xA80B, 0xA80B}, {0xA825, 0xA826}, 73 | {0xA82C, 0xA82C}, {0xA8C4, 0xA8C5}, {0xA8E0, 0xA8F1}, 74 | {0xA8FF, 0xA8FF}, {0xA926, 0xA92D}, {0xA947, 0xA951}, 75 | {0xA980, 0xA982}, {0xA9B3, 0xA9B3}, {0xA9B6, 0xA9B9}, 76 | {0xA9BC, 0xA9BD}, {0xA9E5, 0xA9E5}, {0xAA29, 0xAA2E}, 77 | {0xAA31, 0xAA32}, {0xAA35, 0xAA36}, {0xAA43, 0xAA43}, 78 | {0xAA4C, 0xAA4C}, {0xAA7C, 0xAA7C}, {0xAAB0, 0xAAB0}, 79 | {0xAAB2, 0xAAB4}, {0xAAB7, 0xAAB8}, {0xAABE, 0xAABF}, 80 | {0xAAC1, 0xAAC1}, {0xAAEC, 0xAAED}, {0xAAF6, 0xAAF6}, 81 | {0xABE5, 0xABE5}, {0xABE8, 0xABE8}, {0xABED, 0xABED}, 82 | {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, {0xFE20, 0xFE2F}, 83 | {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0x101FD, 0x101FD}, 84 | {0x102E0, 0x102E0}, {0x10376, 0x1037A}, {0x10A01, 0x10A03}, 85 | {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, 86 | {0x10A3F, 0x10A3F}, {0x10AE5, 0x10AE6}, {0x10D24, 0x10D27}, 87 | {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50}, {0x10F82, 0x10F85}, 88 | {0x11001, 0x11001}, {0x11038, 0x11046}, {0x11070, 0x11070}, 89 | {0x11073, 0x11074}, {0x1107F, 0x11081}, {0x110B3, 0x110B6}, 90 | {0x110B9, 0x110BA}, {0x110BD, 0x110BD}, {0x110C2, 0x110C2}, 91 | {0x110CD, 0x110CD}, {0x11100, 0x11102}, {0x11127, 0x1112B}, 92 | {0x1112D, 0x11134}, {0x11173, 0x11173}, {0x11180, 0x11181}, 93 | {0x111B6, 0x111BE}, {0x111C9, 0x111CC}, {0x111CF, 0x111CF}, 94 | {0x1122F, 0x11231}, {0x11234, 0x11234}, {0x11236, 0x11237}, 95 | {0x1123E, 0x1123E}, {0x112DF, 0x112DF}, {0x112E3, 0x112EA}, 96 | {0x11300, 0x11301}, {0x1133B, 0x1133C}, {0x11340, 0x11340}, 97 | {0x11366, 0x1136C}, {0x11370, 0x11374}, {0x11438, 0x1143F}, 98 | {0x11442, 0x11444}, {0x11446, 0x11446}, {0x1145E, 0x1145E}, 99 | {0x114B3, 0x114B8}, {0x114BA, 0x114BA}, {0x114BF, 0x114C0}, 100 | {0x114C2, 0x114C3}, {0x115B2, 0x115B5}, {0x115BC, 0x115BD}, 101 | {0x115BF, 0x115C0}, {0x115DC, 0x115DD}, {0x11633, 0x1163A}, 102 | {0x1163D, 0x1163D}, {0x1163F, 0x11640}, {0x116AB, 0x116AB}, 103 | {0x116AD, 0x116AD}, {0x116B0, 0x116B5}, {0x116B7, 0x116B7}, 104 | {0x1171D, 0x1171F}, {0x11722, 0x11725}, {0x11727, 0x1172B}, 105 | {0x1182F, 0x11837}, {0x11839, 0x1183A}, {0x1193B, 0x1193C}, 106 | {0x1193E, 0x1193E}, {0x11943, 0x11943}, {0x119D4, 0x119D7}, 107 | {0x119DA, 0x119DB}, {0x119E0, 0x119E0}, {0x11A01, 0x11A0A}, 108 | {0x11A33, 0x11A38}, {0x11A3B, 0x11A3E}, {0x11A47, 0x11A47}, 109 | {0x11A51, 0x11A56}, {0x11A59, 0x11A5B}, {0x11A8A, 0x11A96}, 110 | {0x11A98, 0x11A99}, {0x11C30, 0x11C36}, {0x11C38, 0x11C3D}, 111 | {0x11C3F, 0x11C3F}, {0x11C92, 0x11CA7}, {0x11CAA, 0x11CB0}, 112 | {0x11CB2, 0x11CB3}, {0x11CB5, 0x11CB6}, {0x11D31, 0x11D36}, 113 | {0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, {0x11D3F, 0x11D45}, 114 | {0x11D47, 0x11D47}, {0x11D90, 0x11D91}, {0x11D95, 0x11D95}, 115 | {0x11D97, 0x11D97}, {0x11EF3, 0x11EF4}, {0x13430, 0x13438}, 116 | {0x16AF0, 0x16AF4}, {0x16B30, 0x16B36}, {0x16F4F, 0x16F4F}, 117 | {0x16F8F, 0x16F92}, {0x16FE4, 0x16FE4}, {0x1BC9D, 0x1BC9E}, 118 | {0x1BCA0, 0x1BCA3}, {0x1CF00, 0x1CF2D}, {0x1CF30, 0x1CF46}, 119 | {0x1D167, 0x1D169}, {0x1D173, 0x1D182}, {0x1D185, 0x1D18B}, 120 | {0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1DA00, 0x1DA36}, 121 | {0x1DA3B, 0x1DA6C}, {0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84}, 122 | {0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006}, 123 | {0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, 124 | {0x1E026, 0x1E02A}, {0x1E130, 0x1E136}, {0x1E2AE, 0x1E2AE}, 125 | {0x1E2EC, 0x1E2EF}, {0x1E8D0, 0x1E8D6}, {0x1E944, 0x1E94A}, 126 | {0xE0001, 0xE0001}, {0xE0020, 0xE007F}, {0xE0100, 0xE01EF}, 127 | }; 128 | 129 | static const struct interval double_width[] = { 130 | {0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A}, 131 | {0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3}, 132 | {0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653}, 133 | {0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1}, 134 | {0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5}, 135 | {0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA}, 136 | {0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA}, 137 | {0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B}, 138 | {0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E}, 139 | {0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797}, 140 | {0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C}, 141 | {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99}, 142 | {0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB}, 143 | {0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF}, 144 | {0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31E3}, 145 | {0x31F0, 0x321E}, {0x3220, 0x3247}, {0x3250, 0x4DBF}, 146 | {0x4E00, 0xA48C}, {0xA490, 0xA4C6}, {0xA960, 0xA97C}, 147 | {0xAC00, 0xD7A3}, {0xF900, 0xFAFF}, {0xFE10, 0xFE19}, 148 | {0xFE30, 0xFE52}, {0xFE54, 0xFE66}, {0xFE68, 0xFE6B}, 149 | {0xFF01, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4}, 150 | {0x16FF0, 0x16FF1}, {0x17000, 0x187F7}, {0x18800, 0x18CD5}, 151 | {0x18D00, 0x18D08}, {0x1AFF0, 0x1AFF3}, {0x1AFF5, 0x1AFFB}, 152 | {0x1AFFD, 0x1AFFE}, {0x1B000, 0x1B122}, {0x1B150, 0x1B152}, 153 | {0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1F004, 0x1F004}, 154 | {0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A}, 155 | {0x1F200, 0x1F202}, {0x1F210, 0x1F23B}, {0x1F240, 0x1F248}, 156 | {0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320}, 157 | {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, 158 | {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, 159 | {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, 160 | {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, 161 | {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, 162 | {0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, 163 | {0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D7}, 164 | {0x1F6DD, 0x1F6DF}, {0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6FC}, 165 | {0x1F7E0, 0x1F7EB}, {0x1F7F0, 0x1F7F0}, {0x1F90C, 0x1F93A}, 166 | {0x1F93C, 0x1F945}, {0x1F947, 0x1F9FF}, {0x1FA70, 0x1FA74}, 167 | {0x1FA78, 0x1FA7C}, {0x1FA80, 0x1FA86}, {0x1FA90, 0x1FAAC}, 168 | {0x1FAB0, 0x1FABA}, {0x1FAC0, 0x1FAC5}, {0x1FAD0, 0x1FAD9}, 169 | {0x1FAE0, 0x1FAE7}, {0x1FAF0, 0x1FAF6}, {0x20000, 0x2FFFD}, 170 | {0x30000, 0x3FFFD}, 171 | }; 172 | 173 | static bool inTable(uint32_t ucs, const struct interval *table, int max) { 174 | int min = 0; 175 | if (ucs < table[0].start || ucs > table[max].end) 176 | return false; 177 | while (max >= min) { 178 | int mid = (min + max) / 2; 179 | if (ucs > table[mid].end) 180 | min = mid + 1; 181 | else if (ucs < table[mid].start) 182 | max = mid - 1; 183 | else 184 | return true; 185 | } 186 | return false; 187 | } 188 | 189 | int unicodeWidth(uint32_t ucs) { 190 | if (ucs == 0) 191 | return 0; 192 | 193 | if (ucs < 32 || (ucs >= 0x7F && ucs < 0xA0)) 194 | return -1; 195 | 196 | if (inTable(ucs, double_width, 197 | sizeof(double_width) / sizeof(double_width[0]) - 1)) 198 | return 2; 199 | if (inTable(ucs, zero_width, 200 | sizeof(zero_width) / sizeof(zero_width[0]) - 1)) 201 | return 0; 202 | return 1; 203 | } 204 | 205 | int encodeUTF8(unsigned int code_point, char output[4]) { 206 | if (code_point <= 0x7F) { 207 | output[0] = (char)code_point; 208 | return 1; 209 | } else if (code_point <= 0x07FF) { 210 | output[0] = 0xC0 | (code_point >> 6); 211 | output[1] = 0x80 | (code_point & 0x3F); 212 | return 2; 213 | } else if (code_point <= 0xFFFF) { 214 | output[0] = 0xE0 | (code_point >> 12); 215 | output[1] = 0x80 | ((code_point >> 6) & 0x3F); 216 | output[2] = 0x80 | (code_point & 0x3F); 217 | return 3; 218 | } else if (code_point <= 0x10FFFF) { 219 | output[0] = 0xF0 | (code_point >> 18); 220 | output[1] = 0x80 | ((code_point >> 12) & 0x3F); 221 | output[2] = 0x80 | ((code_point >> 6) & 0x3F); 222 | output[3] = 0x80 | (code_point & 0x3F); 223 | return 4; 224 | } 225 | return -1; 226 | } 227 | 228 | uint32_t decodeUTF8(const char *str, size_t len, size_t *byte_size) { 229 | if (len == 0) { 230 | *byte_size = 0; 231 | return 0xFFFD; 232 | } 233 | 234 | uint8_t first_byte = str[0]; 235 | uint32_t result; 236 | size_t bytes; 237 | 238 | if ((first_byte & 0x80) == 0x00) { 239 | *byte_size = 1; 240 | return (uint32_t)first_byte; 241 | } 242 | 243 | if ((first_byte & 0xE0) == 0xC0) { 244 | result = (first_byte & 0x1F) << 6; 245 | bytes = 2; 246 | } else if ((first_byte & 0xF0) == 0xE0) { 247 | result = (first_byte & 0x0F) << 12; 248 | bytes = 3; 249 | } else if ((first_byte & 0xF8) == 0xF0) { 250 | result = (first_byte & 0x07) << 18; 251 | bytes = 4; 252 | } else { 253 | *byte_size = 1; 254 | return 0xFFFD; 255 | } 256 | 257 | int shift = (bytes - 2) * 6; 258 | for (size_t i = 1; i < bytes; i++) { 259 | if (i >= len || (str[i] & 0xC0) != 0x80) { 260 | *byte_size = i; 261 | return 0xFFFD; 262 | } 263 | result |= (str[i] & 0x3F) << shift; 264 | shift -= 6; 265 | } 266 | *byte_size = bytes; 267 | return result; 268 | } 269 | 270 | int strUTF8Width(const char *str) { 271 | const char *p = str; 272 | int width = 0; 273 | size_t len = strlen(str); 274 | 275 | while (*p != '\0') { 276 | size_t byte_size; 277 | width += unicodeWidth(decodeUTF8(p, len, &byte_size)); 278 | p += byte_size; 279 | len -= byte_size; 280 | } 281 | return width; 282 | } 283 | -------------------------------------------------------------------------------- /src/unicode.h: -------------------------------------------------------------------------------- 1 | #ifndef UNICODE_H 2 | #define UNICODE_H 3 | 4 | #include 5 | #include 6 | 7 | int encodeUTF8(unsigned int code_point, char output[4]); 8 | uint32_t decodeUTF8(const char* str, size_t len, size_t* byte_size); 9 | int unicodeWidth(uint32_t ucs); 10 | int strUTF8Width(const char* str); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "defines.h" 10 | #include "editor.h" 11 | #include "terminal.h" 12 | 13 | void panic(const char *file, int line, const char *s) { 14 | terminalExit(); 15 | fprintf(stderr, "Error at %s: %d: %s\r\n", file, line, s); 16 | exit(EXIT_FAILURE); 17 | } 18 | 19 | void *_malloc_s(const char *file, int line, size_t size) { 20 | void *ptr = malloc(size); 21 | if (!ptr && size != 0) 22 | panic(file, line, "malloc"); 23 | 24 | return ptr; 25 | } 26 | 27 | void *_calloc_s(const char *file, int line, size_t n, size_t size) { 28 | void *ptr = calloc(n, size); 29 | if (!ptr && size != 0) 30 | panic(file, line, "calloc"); 31 | 32 | return ptr; 33 | } 34 | 35 | void *_realloc_s(const char *file, int line, void *ptr, size_t size) { 36 | ptr = realloc(ptr, size); 37 | if (!ptr && size != 0) 38 | panic(file, line, "realloc"); 39 | return ptr; 40 | } 41 | 42 | void _vector_make_room(_Vector *_vec, size_t item_size) { 43 | if (!_vec->capacity) { 44 | _vec->data = malloc_s(item_size * VECTOR_MIN_CAPACITY); 45 | _vec->capacity = VECTOR_MIN_CAPACITY; 46 | } 47 | if (_vec->size >= _vec->capacity) { 48 | _vec->capacity *= VECTOR_EXTEND_RATE; 49 | _vec->data = realloc_s(_vec->data, _vec->capacity * item_size); 50 | } 51 | } 52 | 53 | void abufAppend(abuf *ab, const char *s) { abufAppendN(ab, s, strlen(s)); } 54 | 55 | void abufAppendN(abuf *ab, const char *s, size_t n) { 56 | if (n == 0) 57 | return; 58 | 59 | if (ab->len + n > ab->capacity) { 60 | ab->capacity += n; 61 | ab->capacity *= ABUF_GROWTH_RATE; 62 | char *new = realloc_s(ab->buf, ab->capacity); 63 | ab->buf = new; 64 | } 65 | 66 | memcpy(&ab->buf[ab->len], s, n); 67 | ab->len += n; 68 | } 69 | 70 | void abufFree(abuf *ab) { free(ab->buf); } 71 | 72 | static int isValidColor(const char *color) { 73 | if (strlen(color) != 6) 74 | return 0; 75 | for (int i = 0; i < 6; i++) { 76 | if (!(('0' <= color[i]) || (color[i] <= '9') || ('A' <= color[i]) || 77 | (color[i] <= 'F') || ('a' <= color[i]) || (color[i] <= 'f'))) 78 | return 0; 79 | } 80 | return 1; 81 | } 82 | 83 | Color strToColor(const char *color) { 84 | Color result = {0, 0, 0}; 85 | if (!isValidColor(color)) 86 | return result; 87 | 88 | int shift = 16; 89 | unsigned int hex = strtoul(color, NULL, 16); 90 | result.r = (hex >> shift) & 0xFF; 91 | shift -= 8; 92 | result.g = (hex >> shift) & 0xFF; 93 | shift -= 8; 94 | result.b = (hex >> shift) & 0xFF; 95 | return result; 96 | } 97 | 98 | void setColor(abuf *ab, Color color, int is_bg) { 99 | char buf[32]; 100 | int len; 101 | if (color.r == 0 && color.g == 0 && color.b == 0 && is_bg) { 102 | len = snprintf(buf, sizeof(buf), "%s", ANSI_DEFAULT_BG); 103 | } else { 104 | len = snprintf(buf, sizeof(buf), "\x1b[%d;2;%d;%d;%dm", is_bg ? 48 : 38, 105 | color.r, color.g, color.b); 106 | } 107 | abufAppendN(ab, buf, len); 108 | } 109 | 110 | void gotoXY(abuf *ab, int x, int y) { 111 | char buf[32]; 112 | int len = snprintf(buf, sizeof(buf), "\x1b[%d;%dH", x, y); 113 | abufAppendN(ab, buf, len); 114 | } 115 | 116 | int colorToStr(Color color, char buf[8]) { 117 | return snprintf(buf, 8, "%02x%02x%02x", color.r, color.g, color.b); 118 | } 119 | 120 | int isSeparator(int c) { 121 | return strchr("`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?", c) != NULL; 122 | } 123 | 124 | int isNonSeparator(int c) { return !isSeparator(c); } 125 | 126 | int isNonIdentifierChar(int c) { 127 | return isspace(c) || c == '\0' || isSeparator(c); 128 | } 129 | 130 | int isIdentifierChar(int c) { return !isNonIdentifierChar(c); } 131 | 132 | int isNonSpace(int c) { return !isspace(c); } 133 | 134 | int getDigit(int n) { 135 | if (n < 10) 136 | return 1; 137 | if (n < 100) 138 | return 2; 139 | if (n < 1000) 140 | return 3; 141 | if (n < 10000000) { 142 | if (n < 1000000) { 143 | if (n < 10000) 144 | return 4; 145 | return 5 + (n >= 100000); 146 | } 147 | return 7; 148 | } 149 | if (n < 1000000000) 150 | return 8 + (n >= 100000000); 151 | return 10; 152 | } 153 | 154 | char *getBaseName(char *path) { 155 | char *file = path + strlen(path); 156 | for (; file > path; file--) { 157 | if (*file == '/' 158 | #ifdef _WIN32 159 | || *file == '\\' 160 | #endif 161 | ) { 162 | file++; 163 | break; 164 | } 165 | } 166 | return file; 167 | } 168 | 169 | char *getDirName(char *path) { 170 | char *name = getBaseName(path); 171 | if (name == path) { 172 | name = path; 173 | *name = '.'; 174 | name++; 175 | } else { 176 | path--; 177 | } 178 | *name = '\0'; 179 | return path; 180 | } 181 | 182 | // if path doesn't have a .EXT, append extension 183 | void addDefaultExtension(char *path, const char *extension, int path_length) { 184 | char *src = path + strlen(path) - 1; 185 | 186 | while (!(*src == '/' 187 | #ifdef _WIN32 188 | || *src == '\\' 189 | #endif 190 | ) && 191 | src > path) { 192 | if (*src == '.') { 193 | return; 194 | } 195 | src--; 196 | } 197 | 198 | strncat(path, extension, path_length); 199 | } 200 | 201 | int64_t getLine(char **lineptr, size_t *n, FILE *stream) { 202 | char *buf = NULL; 203 | size_t capacity; 204 | int64_t size = 0; 205 | int c; 206 | const size_t buf_size = 128; 207 | 208 | if (!lineptr || !stream || !n) 209 | return -1; 210 | 211 | buf = *lineptr; 212 | capacity = *n; 213 | 214 | c = fgetc(stream); 215 | if (c == EOF) 216 | return -1; 217 | 218 | if (!buf) { 219 | buf = malloc_s(buf_size); 220 | capacity = buf_size; 221 | } 222 | 223 | while (c != EOF) { 224 | if ((size_t)size > (capacity - 1)) { 225 | capacity += buf_size; 226 | buf = realloc_s(buf, capacity); 227 | } 228 | buf[size++] = c; 229 | 230 | if (c == '\n') 231 | break; 232 | 233 | c = fgetc(stream); 234 | } 235 | 236 | buf[size] = '\0'; 237 | *lineptr = buf; 238 | *n = capacity; 239 | 240 | return size; 241 | } 242 | 243 | int strCaseCmp(const char *s1, const char *s2) { 244 | if (s1 == s2) 245 | return 0; 246 | 247 | int result; 248 | while ((result = tolower(*s1) - tolower(*s2)) == 0) { 249 | if (*s1 == '\0') 250 | break; 251 | s1++; 252 | s2++; 253 | } 254 | return result; 255 | } 256 | 257 | char *strCaseStr(const char *str, const char *sub_str) { 258 | // O(n*m), but should be ok 259 | if (*sub_str == '\0') 260 | return (char *)str; 261 | 262 | while (*str != '\0') { 263 | const char *s = str; 264 | const char *sub = sub_str; 265 | while (tolower(*s) == tolower(*sub)) { 266 | s++; 267 | sub++; 268 | if (*sub == '\0') { 269 | return (char *)str; 270 | } 271 | } 272 | str++; 273 | } 274 | 275 | return NULL; 276 | } 277 | 278 | int strToInt(const char *str) { 279 | if (!str) { 280 | return 0; 281 | } 282 | 283 | // Skip front spaces 284 | while (*str == ' ' || *str == '\t') { 285 | str++; 286 | } 287 | 288 | int sign = 1; 289 | if (*str == '+' || *str == '-') { 290 | sign = (*str++ == '-') ? -1 : 1; 291 | } 292 | 293 | int result = 0; 294 | while (*str >= '0' && *str <= '9') { 295 | if (result > INT_MAX / 10 || 296 | (result == INT_MAX / 10 && (*str - '0') > INT_MAX % 10)) { 297 | // Overflow 298 | return (sign == -1) ? INT_MIN : INT_MAX; 299 | } 300 | 301 | result = result * 10 + (*str - '0'); 302 | str++; 303 | } 304 | 305 | result = sign * result; 306 | 307 | // Skip trailing spaces 308 | while (*str != '\0') { 309 | if (*str != ' ' && *str != '\t') { 310 | return 0; 311 | } 312 | str++; 313 | } 314 | 315 | return result; 316 | } 317 | 318 | // https://opensource.apple.com/source/QuickTimeStreamingServer/QuickTimeStreamingServer-452/CommonUtilitiesLib/base64.c 319 | 320 | static const char basis_64[] = 321 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 322 | 323 | int base64Encode(const char *string, int len, char *output) { 324 | int i; 325 | char *p = output; 326 | 327 | for (i = 0; i < len - 2; i += 3) { 328 | *p++ = basis_64[(string[i] >> 2) & 0x3F]; 329 | *p++ = basis_64[((string[i] & 0x3) << 4) | 330 | ((int)(string[i + 1] & 0xF0) >> 4)]; 331 | *p++ = basis_64[((string[i + 1] & 0xF) << 2) | 332 | ((int)(string[i + 2] & 0xC0) >> 6)]; 333 | *p++ = basis_64[string[i + 2] & 0x3F]; 334 | } 335 | 336 | if (i < len) { 337 | *p++ = basis_64[(string[i] >> 2) & 0x3F]; 338 | if (i == (len - 1)) { 339 | *p++ = basis_64[((string[i] & 0x3) << 4)]; 340 | *p++ = '='; 341 | } else { 342 | *p++ = basis_64[((string[i] & 0x3) << 4) | 343 | ((int)(string[i + 1] & 0xF0) >> 4)]; 344 | *p++ = basis_64[((string[i + 1] & 0xF) << 2)]; 345 | } 346 | *p++ = '='; 347 | } 348 | 349 | *p++ = '\0'; 350 | return p - output; 351 | } 352 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // Macros 9 | #define _DO02(m, sep, x, y) m(x) sep m(y) 10 | #define _DO03(m, sep, x, y, z) \ 11 | m(x) sep m(y) \ 12 | sep m(z) 13 | 14 | #define _DO_N(x01, x02, x03, N, ...) _DO##N 15 | #define _MAP(m, sep, ...) _DO_N(__VA_ARGS__, 03, 02, 01)(m, sep, __VA_ARGS__) 16 | 17 | #define _NOP(s) s 18 | 19 | #define PATH_CAT(...) _MAP(_NOP, DIR_SEP, __VA_ARGS__) 20 | 21 | #define UNUSED(x) (void)!(x) 22 | 23 | // Panic 24 | #define PANIC(s) panic(__FILE__, __LINE__, s) 25 | void panic(const char* file, int line, const char* s); 26 | 27 | // ANSI escape sequences 28 | #define ANSI_CLEAR "\x1b[m" 29 | #define ANSI_UNDERLINE "\x1b[4m" 30 | #define ANSI_NOT_UNDERLINE "\x1b[24m" 31 | #define ANSI_INVERT "\x1b[7m" 32 | #define ANSI_NOT_INVERT "\x1b[27m" 33 | #define ANSI_DEFAULT_FG "\x1b[39m" 34 | #define ANSI_DEFAULT_BG "\x1b[49m" 35 | 36 | // Allocate 37 | #define malloc_s(size) _malloc_s(__FILE__, __LINE__, size) 38 | #define calloc_s(n, size) _calloc_s(__FILE__, __LINE__, n, size) 39 | #define realloc_s(ptr, size) _realloc_s(__FILE__, __LINE__, ptr, size) 40 | 41 | void* _malloc_s(const char* file, int line, size_t size); 42 | void* _calloc_s(const char* file, int line, size_t n, size_t size); 43 | void* _realloc_s(const char* file, int line, void* ptr, size_t size); 44 | 45 | // Vector 46 | #define VECTOR_MIN_CAPACITY 16 47 | #define VECTOR_EXTEND_RATE 1.5 48 | 49 | #define VECTOR(type) \ 50 | struct { \ 51 | size_t size; \ 52 | size_t capacity; \ 53 | type* data; \ 54 | } 55 | 56 | typedef struct { 57 | size_t size; 58 | size_t capacity; 59 | void* data; 60 | } _Vector; 61 | 62 | void _vector_make_room(_Vector* _vec, size_t item_size); 63 | 64 | #define vector_push(vec, val) \ 65 | do { \ 66 | _vector_make_room((_Vector*)&(vec), sizeof(val)); \ 67 | (vec).data[(vec).size++] = (val); \ 68 | } while (0) 69 | 70 | #define vector_pop(vec) ((vec).data[--(vec).size]) 71 | 72 | #define vector_shrink(vec) \ 73 | do { \ 74 | (vec).data = realloc_s(vec.data, sizeof(*vec.data) * vec.size); \ 75 | } while (0) 76 | 77 | // Abuf 78 | #define ABUF_GROWTH_RATE 1.5f 79 | #define ABUF_INIT \ 80 | { NULL, 0, 0 } 81 | 82 | typedef struct { 83 | char* buf; 84 | size_t len; 85 | size_t capacity; 86 | } abuf; 87 | 88 | void abufAppend(abuf* ab, const char* s); 89 | void abufAppendN(abuf* ab, const char* s, size_t n); 90 | void abufFree(abuf* ab); 91 | 92 | // Color 93 | typedef struct Color { 94 | int r, g, b; 95 | } Color; 96 | 97 | Color strToColor(const char* color); 98 | int colorToStr(Color color, char buf[8]); 99 | void setColor(abuf* ab, Color color, int is_bg); 100 | 101 | // Separator 102 | typedef int (*IsCharFunc)(int c); 103 | int isSeparator(int c); 104 | int isNonSeparator(int c); 105 | int isNonIdentifierChar(int c); 106 | int isIdentifierChar(int c); 107 | int isNonSpace(int c); 108 | 109 | // File 110 | char* getBaseName(char* path); 111 | char* getDirName(char* path); 112 | void addDefaultExtension(char* path, const char* extension, int path_length); 113 | 114 | // Misc 115 | void gotoXY(abuf* ab, int x, int y); 116 | int getDigit(int n); 117 | 118 | // String 119 | int64_t getLine(char** lineptr, size_t* n, FILE* stream); 120 | int strCaseCmp(const char* s1, const char* s2); 121 | char* strCaseStr(const char* str, const char* sub_str); 122 | int strToInt(const char* str); 123 | 124 | // Base64 125 | static inline int base64EncodeLen(int len) { return ((len + 2) / 3 * 4) + 1; } 126 | 127 | int base64Encode(const char* string, int len, char* output); 128 | 129 | #endif 130 | --------------------------------------------------------------------------------