├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake └── FindPDCurses.cmake └── src ├── action.cpp ├── action.hpp ├── caret.hpp ├── editor.cpp ├── editor.hpp ├── fileEditor.cpp ├── fileEditor.hpp └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | build/ 3 | *.txt 4 | !CMakeLists.txt 5 | Makefile 6 | GPATH 7 | GRTAGS 8 | GTAGS -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists.txt 2 | cmake_minimum_required(VERSION 3.16) 3 | 4 | project("yate") 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | if(NOT CMAKE_GENERATOR) 9 | set(CMAKE_GENERATOR "Unix Makefiles") 10 | endif() 11 | 12 | if (WIN32) 13 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libgcc -static-libstdc++ -static") 14 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++ -static") 15 | endif() 16 | 17 | if(NOT CMAKE_BUILD_TYPE) 18 | set(CMAKE_BUILD_TYPE Release) 19 | endif() 20 | 21 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra") 22 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g") 23 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") 24 | 25 | file(GLOB MY_SOURCES "src/*.cpp" "src/*.hpp" "src/*.h") 26 | add_executable("${PROJECT_NAME}" ${MY_SOURCES}) 27 | 28 | if (WIN32) 29 | add_definitions(-DYATE_WINDOWS) 30 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) 31 | find_package(PDCurses REQUIRED) 32 | target_link_libraries(${PROJECT_NAME} PRIVATE PDCurses) 33 | else() 34 | find_package(Curses REQUIRED Curses) 35 | target_link_libraries(${PROJECT_NAME} PRIVATE ${CURSES_LIBRARIES}) 36 | endif() 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marat Isaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/badge/License-MIT-informational.svg)](https://github.com/xyl1t/Yate/blob/master/LICENSE) 2 | [![](https://img.shields.io/badge/Dependency-C%2B%2B17-critical)](https://en.cppreference.com/w/cpp/compiler_support/17) 3 | [![](https://img.shields.io/badge/Dependency-CMake-critical)](https://cmake.org/) 4 | [![](https://img.shields.io/badge/Dependency-ncurses-critical)](https://invisible-island.net/ncurses/) 5 | # Yate 6 | Yate stands for "Yet another text editor" 7 | It's an extremely simple text editor for the terminal. 8 | WIP 9 | # Features 10 | Besides being able to edit text with yate, it also has following features 11 | * undo/redo 12 | Press `ctrl+u` for undo, and `ctrl+r` for redo. 13 | * search 14 | Press `ctrl+f` (f as in "find"), if there is more than one match you can move to the next match by clicking the up arrow or move to the previous match by clicking the down arrow. Press `enter` to confirm or `ESC`/`ctrl+c` to cancel. 15 | * saving a file by pressing `ctrl+s` 16 | * movement shortcuts 17 | Besides the usual arrow keys, keys such as `Page up`, `Page down`, `Home` and `End` also work as expected, but on top of that, you can also press `ctrl+x` to jump the the next word and `ctrl+z` to jump to the previous. If you don't want to move your cursor but just want to scroll right or left, you can do that with `ctrl+k` and `ctrl+l`. 18 | * Warning the user on an attempt to close a modified file 19 | # Building 20 | Dependencies: 21 | * A C++17 compatible compiler 22 | * The ncurses library (or PDCurses if you are on Windows) 23 | * [Cmake](https://cmake.org/) 24 | ```bash 25 | mkdir build 26 | cd build 27 | cmake -G "Unix Makefiles" .. 28 | make 29 | ./yate 30 | ``` 31 | In order to close yate, press `ctrl+c` 32 | # Options 33 | ``` 34 | ./yate [file][-t][-r][-c][-h] 35 | ``` 36 | # License 37 | This project is licensed under the [MIT License](https://github.com/xyl1t/Yate/blob/master/LICENSE) 38 | -------------------------------------------------------------------------------- /cmake/FindPDCurses.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # 2014/Jun/25 Jeongbin Park added this file; to support Windows platform. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | 10 | find_path(PDCURSES_INCLUDE_DIR pdcurses.h) 11 | find_library(PDCURSES_LIBRARY pdcurses) 12 | 13 | set(PDCURSES_LIBRARIES ${PDCURSES_LIBRARY}) 14 | 15 | include(FindPackageHandleStandardArgs) 16 | find_package_handle_standard_args(PDCurses DEFAULT_MSG PDCURSES_LIBRARY PDCURSES_LIBRARIES PDCURSES_INCLUDE_DIR) 17 | 18 | mark_as_advanced(PDCURSES_LIBRARY PDCURSES_LIBRARIES PDCURSES_INCLUDE_DIR) -------------------------------------------------------------------------------- /src/action.cpp: -------------------------------------------------------------------------------- 1 | #include "action.hpp" 2 | #include "editor.hpp" 3 | #include 4 | 5 | Action::Action(const Action& other) 6 | : actionType(other.actionType), 7 | action(other.action), 8 | x(other.x), y(other.y), 9 | undoAction(other.undoAction), 10 | doAction(other.doAction) { 11 | } 12 | 13 | Action::Action(ActionType actionType, int action, int x, int y, std::function undoAction, std::function doAction) 14 | : actionType(actionType), 15 | action(action), 16 | x(x), y(y), 17 | undoAction(undoAction), 18 | doAction(doAction) { 19 | } 20 | 21 | void Action::operator()() { 22 | doAction(); 23 | } -------------------------------------------------------------------------------- /src/action.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ACTION_HPP 2 | #define ACTION_HPP 3 | 4 | #if defined(YATE_WINDOWS) 5 | #include "pdcurses.h" 6 | #undef KEY_BACKSPACE 7 | #define KEY_BACKSPACE 8 8 | 9 | #undef KEY_DL 10 | #define KEY_DL 60490 11 | 12 | #undef KEY_UP 13 | #define KEY_UP 60419 14 | #undef KEY_DOWN 15 | #define KEY_DOWN 60418 16 | #undef KEY_LEFT 17 | #define KEY_LEFT 60420 18 | #undef KEY_RIGHT 19 | #define KEY_RIGHT 60421 20 | #undef KEY_HOME 21 | #define KEY_HOME 60422 22 | #undef KEY_END 23 | #define KEY_END 60518 24 | #undef KEY_PPAGE 25 | #define KEY_PPAGE 60499 26 | #undef KEY_NPAGE 27 | #define KEY_NPAGE 60498 28 | 29 | #undef KEY_ENTER 30 | #define KEY_ENTER 13 31 | #else 32 | #include 33 | #endif 34 | #include 35 | class Editor; 36 | 37 | enum class ActionType { 38 | None = 0, 39 | Input, 40 | NewLine, 41 | DeletionL, 42 | DeletionR, 43 | }; 44 | 45 | struct Action { 46 | ActionType actionType; 47 | int action; 48 | int x; 49 | int y; 50 | std::function undoAction; 51 | std::function doAction; 52 | 53 | Action(const Action& other); 54 | Action(ActionType actionType, int action, int x, int y, std::function undoAction, std::function doAction); 55 | 56 | void operator()(); 57 | 58 | static inline bool isInput(int action) { 59 | return (isCharInput(action) || action == 32 || action == KEY_STAB || action == 9 || action == KEY_ENTER || action == 10); 60 | } 61 | static inline bool isCharInput(int action) { 62 | return (action > 32 && action < 127); 63 | } 64 | static inline bool isMovement(int action) { 65 | return (action == KEY_PPAGE || action == KEY_NPAGE || action == 11 || action == 12 || action == KEY_UP || action == KEY_DOWN || action == KEY_LEFT || action == KEY_RIGHT || action == 5 || action == KEY_END || action == KEY_HOME || action == 1 || action == 25 || action == 26 || action == 24); 66 | } 67 | static inline bool isDeletion(int action) { 68 | return action == 127 || action == KEY_BACKSPACE || action == 330 || action == KEY_DL; 69 | } 70 | static inline bool isNewLine(int action) { 71 | return action == 10 || action == KEY_ENTER; 72 | } 73 | static inline bool isEqual(int action1, int action2) { 74 | return (isInput(action1) == isInput(action2)) || (isMovement(action1) == isMovement(action2)) || (isDeletion(action1) == isDeletion(action2)) || (isNewLine(action1) == isNewLine(action2)); 75 | } 76 | }; 77 | 78 | namespace Actions { 79 | inline const Action separator { 80 | ActionType::None, 0, 0, 0, nullptr, nullptr 81 | }; 82 | } 83 | 84 | #endif -------------------------------------------------------------------------------- /src/caret.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CARET_HPP 2 | #define CARET_HPP 3 | 4 | struct Caret { 5 | int x; 6 | int y; 7 | int savedX = 0; 8 | }; 9 | 10 | #endif -------------------------------------------------------------------------------- /src/editor.cpp: -------------------------------------------------------------------------------- 1 | #include "editor.hpp" 2 | #include 3 | #include 4 | #include 5 | // Color pairs defines: 6 | #define PAIR_STANDARD 1 7 | #define PAIR_ERROR 2 8 | #define PAIR_WARNING 3 9 | #define PAIR_INFO 4 10 | 11 | 12 | Editor::Editor(const std::string& filePath, int tabSize, bool autoIndent) 13 | : alive(true), 14 | file(filePath), 15 | TAB_SIZE(tabSize), 16 | caret(), 17 | scrollX(0), 18 | scrollY(0), 19 | customStatusText(false), 20 | undo{}, 21 | redo{}, 22 | autoIndent(autoIndent) 23 | { 24 | initColorPairs(); 25 | resetStatus(); 26 | if (!file.hasWritePermission()) { 27 | setStatus(" File \'" + file.getFullFilename() + "\' doesn't have write permissions. ", PAIR_WARNING); 28 | } 29 | if (!file.getInfoMessage().empty()) { 30 | setStatus((std::string)(" " + file.getInfoMessage() + " "), PAIR_WARNING); 31 | } 32 | customStatusText = false; 33 | } 34 | 35 | bool Editor::close(bool force) { 36 | // NOTE: Maybe instead of exiting without saving, ask the user if he wants to save 37 | if(file.hasFileContentChanged() && !force) { 38 | std::string status {" Exit without saving? [Y/N] "}; 39 | setStatus(status, PAIR_WARNING); 40 | draw(); 41 | int input{getch()}; 42 | if(input == 'y' || input == 'Y') { 43 | this->alive = false; 44 | file.close(); 45 | return true; 46 | } 47 | resetStatus(); 48 | draw(); 49 | return false; 50 | } 51 | this->alive = false; 52 | file.close(); 53 | return true; 54 | } 55 | 56 | void Editor::draw() { 57 | clear(); 58 | for (int lineNr = scrollY; lineNr < scrollY + getTextEditorHeight() && lineNr < file.linesAmount(); lineNr++) { 59 | std::string line { file.getLine(lineNr) }; 60 | int min = getTextEditorWidth(); 61 | int virtualCol = 0; 62 | 63 | move(lineNr - scrollY, 0); 64 | printw("%3d ", lineNr + 1); 65 | for (char ch : line) { 66 | if(ch == '\t') { 67 | const int tabSize = TAB_SIZE - (virtualCol) % TAB_SIZE; 68 | for(int original = virtualCol; virtualCol < original + tabSize; virtualCol++) { 69 | if(virtualCol >= scrollX && virtualCol - scrollX < min) { 70 | printw(" "); 71 | } 72 | } 73 | } else { 74 | if(virtualCol >= scrollX && virtualCol - scrollX < min) { 75 | printw("%c", ch); 76 | } 77 | virtualCol++; 78 | } 79 | } 80 | } 81 | 82 | drawStatus(); 83 | 84 | refresh(); 85 | } 86 | void Editor::drawStatus() { 87 | // turn on and set color for status 88 | attron(A_STANDOUT); 89 | attron(COLOR_PAIR(this->colorPair)); 90 | 91 | // print status at bottom of screen 92 | this->statusText.resize(getmaxx(stdscr), ' '); 93 | mvprintw(getmaxy(stdscr) - 1, 0, "%s", this->statusText.c_str()); 94 | 95 | attroff(COLOR_PAIR(this->colorPair)); 96 | // Reset color pair: 97 | this->colorPair = PAIR_STANDARD; 98 | attroff(A_STANDOUT); 99 | 100 | move(caret.y - scrollY, caret.x - scrollX + 4); 101 | } 102 | 103 | int Editor::getInput() { 104 | prevAction = currentAction; 105 | currentAction = getch(); 106 | static int accumulation = 0; 107 | if (currentAction == -1) { // HACK: when does -1 actually come? 108 | accumulation++; 109 | if (accumulation > 1000) { 110 | close(true); 111 | return currentAction; 112 | } 113 | } 114 | 115 | auto actionCount = undo.size() + redo.size(); 116 | 117 | if(Action::isInput(currentAction)) { 118 | if (Action::isNewLine(currentAction) && IsAutoIndentEnabled()) { 119 | auto chars = getCharsBeforeFirstCharacter(); 120 | put(currentAction); 121 | 122 | for (auto currentAction : chars) { 123 | put(currentAction); 124 | } 125 | } else { 126 | put(currentAction); 127 | } 128 | 129 | if(!file.hasWritePermission()) { 130 | setStatus(" Warning: File \'" + file.getFullFilename() + "\' doesn't have write permissions. ", PAIR_WARNING); 131 | } 132 | } 133 | else 134 | { 135 | switch(currentAction) 136 | { 137 | case 21: // CTRL+U 138 | if(!undo.empty() && undo.top().actionType == Actions::separator.actionType) { 139 | redo.emplace(undo.top()); 140 | undo.pop(); 141 | } 142 | while(!undo.empty()) { 143 | Action act{undo.top()}; 144 | if(act.actionType != Actions::separator.actionType) { 145 | act.undoAction(); 146 | redo.emplace(act); 147 | } 148 | undo.pop(); 149 | if (undo.empty() || undo.top().actionType == Actions::separator.actionType) { 150 | break; 151 | } 152 | } 153 | break; 154 | 155 | case 18: // CTRL+R 156 | if(!redo.empty() && redo.top().actionType == Actions::separator.actionType) { 157 | undo.emplace(redo.top()); 158 | redo.pop(); 159 | } 160 | while(!redo.empty()) { 161 | Action act{redo.top()}; 162 | if(act.actionType != Actions::separator.actionType) { 163 | act.doAction(); 164 | undo.emplace(act); 165 | } 166 | redo.pop(); 167 | if (redo.empty() || redo.top().actionType == Actions::separator.actionType) { 168 | break; 169 | } 170 | } 171 | break; 172 | 173 | case 11: // CTRL+K 174 | scrollLeft(); 175 | break; 176 | case 12: // CTRL+L 177 | scrollRight(); 178 | break; 179 | 180 | case KEY_PPAGE: 181 | setCaretLocation(caret.x, caret.y - (getTextEditorHeight() - 1)); 182 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 183 | undo.push(Actions::separator); 184 | break; 185 | case KEY_NPAGE: 186 | setCaretLocation(caret.x, caret.y + (getTextEditorHeight() - 1)); 187 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 188 | undo.push(Actions::separator); 189 | break; 190 | case KEY_UP: 191 | moveUp(); 192 | break; 193 | case KEY_DOWN: 194 | moveDown(); 195 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 196 | undo.push(Actions::separator); 197 | break; 198 | case KEY_LEFT: 199 | moveLeft(); 200 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 201 | undo.push(Actions::separator); 202 | break; 203 | case KEY_RIGHT: 204 | moveRight(); 205 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 206 | undo.push(Actions::separator); 207 | break; 208 | case 5: 209 | case KEY_END: 210 | moveEndOfLine(); 211 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 212 | undo.push(Actions::separator); 213 | break; 214 | case KEY_HOME: 215 | case 1: 216 | if(caret.x != getCharsCountBeforeFirstCharacter(-1) || caret.x == 0) 217 | moveToFirstCharacter(); 218 | else 219 | moveBeginningOfLine(); 220 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 221 | undo.push(Actions::separator); 222 | break; 223 | case 25: // CTRL+Y (for qwertz layout) 224 | case 26: // CTRL+Z (for qwerty layout) 225 | moveBeginningOfText(); 226 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 227 | undo.push(Actions::separator); 228 | break; 229 | case 24: // CTRL+X 230 | moveEndOfText(); 231 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType) 232 | undo.push(Actions::separator); 233 | break; 234 | 235 | case KEY_BACKSPACE: 236 | case 127: 237 | deleteCharL(); 238 | break; 239 | case 330: 240 | case KEY_DL: 241 | deleteCharR(); 242 | break; 243 | 244 | case 6: // CTRL+F 245 | find(); 246 | break; 247 | 248 | case 19: 249 | saveFile(); 250 | break; 251 | case 3: 252 | close(); 253 | break; 254 | } 255 | } 256 | if(!(currentAction == 11 || currentAction == 12)) 257 | scrollToCaret(); 258 | 259 | // if the undo stack changed, that means something changed in the file, therefor flush the redo stack 260 | if (undo.size() + redo.size() != actionCount) { 261 | while (!redo.empty()) redo.pop(); 262 | } 263 | 264 | // Reset status on user input if no custom status was applied, if there is a custom status, let it display first and then reset 265 | if(!customStatusText) { 266 | resetStatus(); 267 | } 268 | customStatusText = false; 269 | 270 | #ifndef NDEBUG 271 | setStatus(this->statusText + "\tinput: " + std::to_string(currentAction), this->colorPair); 272 | customStatusText = false; 273 | #endif 274 | 275 | return currentAction; 276 | } 277 | 278 | void Editor::put(int ch, bool record) { 279 | ActionType actType = ActionType::Input; 280 | int beforeX = caret.x; 281 | int beforeY = caret.y; 282 | 283 | if(ch != KEY_ENTER && ch != 10) { 284 | file.put((char)ch); 285 | moveRight(); 286 | } else { 287 | newLine(); 288 | actType = ActionType::NewLine; 289 | } 290 | 291 | if (record) { 292 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType && undo.top().actionType != ActionType::Input && !IsAutoIndentEnabled()) { 293 | undo.emplace(Actions::separator); 294 | } 295 | 296 | undo.emplace((Action) { 297 | actType, 298 | ch, caret.x, caret.y, 299 | [this, x = caret.x, y = caret.y] { 300 | setCaretLocation(x, y); 301 | deleteCharL(false); 302 | }, 303 | [this, act = ch, x = beforeX, y = beforeY] { 304 | setCaretLocation(x, y); 305 | put(static_cast(act), false); 306 | } 307 | }); 308 | 309 | if((ch == ' ' || ch == '\n') && !IsAutoIndentEnabled()) { 310 | undo.emplace(Actions::separator); 311 | } 312 | } 313 | } 314 | void Editor::newLine() { 315 | file.newLine(); 316 | moveDown(); 317 | setCaretLocation(0, caret.y, true); 318 | } 319 | void Editor::deleteCharL(bool record) { 320 | try { 321 | char c{}; 322 | if(caret.x == 0) { 323 | moveLeft(); 324 | c = '\n'; 325 | file.del(true); 326 | } else { 327 | c = file.getLine(caret.y)[getFileCaretColumn(caret.x - 1)]; 328 | file.del(false); 329 | moveLeft(); 330 | } 331 | if(file.getCaretY() - scrollY < 0) { 332 | scrollY--; 333 | } 334 | caret.savedX = caret.x; 335 | 336 | if (record) { 337 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType && undo.top().actionType != ActionType::DeletionL) { 338 | undo.emplace(Actions::separator); 339 | } 340 | 341 | undo.emplace((Action) { 342 | ActionType::DeletionL, 343 | currentAction, caret.x, caret.y, 344 | [this, c, x = caret.x, y = caret.y] { 345 | setCaretLocation(x, y); 346 | put(c, false); 347 | }, 348 | [this, c, x = caret.x, y = caret.y] { 349 | if(c == '\t') { 350 | setCaretLocation(x, y); 351 | deleteCharR(false); 352 | } else { 353 | if(c != '\n') 354 | setCaretLocation(x + 1, y); 355 | else 356 | setCaretLocation(0, y + 1); 357 | deleteCharL(false); 358 | } 359 | } 360 | }); 361 | 362 | if (c == '\n') { 363 | undo.emplace(Actions::separator); 364 | } 365 | } 366 | } catch(std::string e) { 367 | setStatus(e, PAIR_ERROR); 368 | } 369 | 370 | } 371 | void Editor::deleteCharR(bool record) { 372 | try { 373 | char c = file.getLine(caret.y)[getFileCaretColumn()]; 374 | if(caret.x == getVirtualLineLength()) c = '\n'; 375 | 376 | file.del(true); 377 | caret.savedX = caret.x; 378 | 379 | if (record) { 380 | if(!undo.empty() && undo.top().actionType != Actions::separator.actionType && undo.top().actionType != ActionType::DeletionR) { 381 | undo.emplace(Actions::separator); 382 | } 383 | 384 | undo.emplace((Action){ 385 | ActionType::DeletionR, 386 | currentAction, caret.x, caret.y, 387 | [this, c, x = caret.x, y = caret.y] { 388 | setCaretLocation(x, y); 389 | put(c, false); 390 | setCaretLocation(x, y); 391 | }, 392 | [this, x = caret.x, y = caret.y] { 393 | setCaretLocation(x, y); 394 | deleteCharR(false); 395 | } 396 | }); 397 | 398 | if (c == '\n') { 399 | undo.emplace(Actions::separator); 400 | } 401 | } 402 | } catch(std::string e) { 403 | setStatus(e, PAIR_ERROR); 404 | } 405 | } 406 | 407 | void Editor::moveUp() { 408 | if(caret.y - 1 >= 0) { 409 | caret.y--; 410 | file.moveUp(); 411 | if (getVirtualLineLength() < caret.savedX) { 412 | caret.x = getVirtualLineLength(); 413 | file.setCaretLocation(file.getLineSize(), file.getCaretY()); 414 | } else { 415 | file.setCaretLocation(getFileCaretColumn(caret.savedX), file.getCaretY()); 416 | caret.x = getVirtualCaretColumn(file.getCaretX(), file.getCaretY()); 417 | } 418 | } else { 419 | caret.y = 0; 420 | caret.x = caret.savedX = 0; 421 | file.setCaretLocation(0, 0); 422 | } 423 | scrollToCaret(); 424 | } 425 | void Editor::moveDown() { 426 | if(caret.y + 1 < file.linesAmount()) { 427 | caret.y++; 428 | file.moveDown(); 429 | if (getVirtualLineLength() < caret.savedX) { 430 | caret.x = getVirtualLineLength(); 431 | file.setCaretLocation(file.getLineSize(), file.getCaretY()); 432 | } else { 433 | file.setCaretLocation(getFileCaretColumn(caret.savedX), file.getCaretY()); 434 | caret.x = getVirtualCaretColumn(file.getCaretX(), file.getCaretY()); 435 | } 436 | } else { 437 | file.setCaretLocation(file.getLineSize(), file.linesAmount() - 1); 438 | caret.y = file.linesAmount() - 1; 439 | caret.x = caret.savedX = getVirtualCaretColumn(file.getLineSize(), file.linesAmount() - 1); 440 | } 441 | scrollToCaret(); 442 | } 443 | void Editor::moveRight() { 444 | int prev = caret.x; 445 | if (caret.x < getVirtualCaretColumn(file.getLineSize(caret.y), caret.y)) { 446 | file.moveRight(); 447 | caret.x = caret.savedX = getVirtualCaretColumn(getFileCaretColumn(), caret.y); 448 | } else if (caret.y < file.linesAmount() - 1) { 449 | moveDown(); 450 | setScrollH(0); 451 | caret.x = caret.savedX = 0; 452 | file.setCaretLocation(0, file.getCaretY()); 453 | } 454 | 455 | if(caret.x - scrollX + 1 > getTextEditorWidth()) 456 | scrollRight(caret.x - prev); // NOTE: calculating difference in case if there is a tab 457 | } 458 | void Editor::moveLeft() { 459 | int prev = caret.x; 460 | if(caret.x > 0) { 461 | file.moveLeft(); 462 | caret.x = caret.savedX = getVirtualCaretColumn(getFileCaretColumn(), caret.y); 463 | } else if(caret.y > 0) { 464 | moveUp(); 465 | int virtualX = getVirtualCaretColumn(file.getLineSize(caret.y), caret.y); 466 | setScrollH(virtualX - getTextEditorWidth() + 2); 467 | caret.x = caret.savedX = virtualX; 468 | file.setCaretLocation(file.getLineSize(caret.y), file.getCaretY()); 469 | } 470 | 471 | if(caret.x - scrollX < 0) 472 | scrollLeft(prev - caret.x); // NOTE: calculating difference in case if there is a tab 473 | } 474 | 475 | void Editor::moveBeginningOfLine() { 476 | setCaretLocation(0, caret.y); 477 | } 478 | void Editor::moveToFirstCharacter() { 479 | setCaretLocation(getCharsCountBeforeFirstCharacter(-1), caret.y); 480 | } 481 | void Editor::moveEndOfLine() { 482 | setCaretLocation(getVirtualLineLength(), caret.y); 483 | } 484 | void Editor::moveBeginningOfText() { 485 | if(caret.x == 0 && caret.y == 0) return; 486 | 487 | moveLeft(); 488 | while(file.getLine()[file.getCaretX() - 1] != ' ' && file.getLine()[file.getCaretX() - 1] != '\t' && file.getCaretX() != 0) { 489 | moveLeft(); 490 | } 491 | } 492 | void Editor::moveEndOfText() { 493 | if(caret.x == getVirtualLineLength() && caret.y == file.linesAmount()) return; 494 | 495 | moveRight(); 496 | while(file.getLine()[file.getCaretX()] != ' ' && file.getLine()[file.getCaretX()] != '\t' && file.getCaretX() != file.getLineSize()) { 497 | moveRight(); 498 | } 499 | } 500 | 501 | std::string Editor::getInputInStatus(std::string statusText, int colorPair, const std::string& preset) { 502 | std::string status {statusText}; 503 | std::string output{preset}; 504 | setStatus((std::string{status + output + " "}).c_str(), colorPair); 505 | drawStatus(); 506 | int input{}; 507 | Caret statusCaret{(int)preset.size(), 0}; 508 | move(getmaxy(stdscr) - 1, statusCaret.x + status.length()); 509 | while (true) { 510 | input = getch(); 511 | if(input == KEY_ENTER || input == 10) break; 512 | if(input >= 32 && input < 127) { 513 | output.insert(statusCaret.x, 1, (char)input); 514 | statusCaret.x++; 515 | } 516 | if(input == KEY_RIGHT) { 517 | if(statusCaret.x < (int)output.length()) statusCaret.x++; 518 | } 519 | if(input == KEY_LEFT) { 520 | if(statusCaret.x > 0) statusCaret.x--; 521 | } 522 | if((input == 127 || input == KEY_BACKSPACE) && !output.empty()) { // BACKSPACE 523 | output.erase(statusCaret.x - 1, 1); 524 | if(statusCaret.x > 0) statusCaret.x--; 525 | } 526 | if(input == 330 && !output.empty()) { // DEL 527 | output.erase(statusCaret.x, 1); 528 | } 529 | if(input == 27 || input == 3) { // ESCAPE or ctrl+c 530 | resetStatus(); 531 | drawStatus(); 532 | return "\0"; 533 | } 534 | setStatus((std::string{status + output + " "}).c_str(), colorPair); 535 | drawStatus(); 536 | move(getmaxy(stdscr) - 1, statusCaret.x + status.length()); 537 | } 538 | resetStatus(); 539 | drawStatus(); 540 | return output; 541 | } 542 | 543 | void Editor::find() { 544 | std::string newWord{getInputInStatus(" Find: ", PAIR_INFO)}; 545 | std::string word{}; 546 | 547 | Caret current = this->caret; 548 | int currentScrollX = this->scrollX; 549 | int currentScrollY = this->scrollY; 550 | std::vector occurrences{}; 551 | int occurrenceCount{}; 552 | 553 | while(!newWord.empty()) { 554 | if(newWord != word) { 555 | word = newWord; 556 | occurrenceCount = -1; 557 | occurrences.clear(); occurrences.shrink_to_fit(); 558 | 559 | for (int i = 0; i < file.linesAmount(); i++) { 560 | const std::string& line = file.getLine(i); 561 | auto pos = line.find(word); 562 | 563 | if(pos != line.npos) { 564 | occurrences.emplace_back((Caret){getVirtualCaretColumn(pos, i), i}); 565 | if (occurrenceCount == -1 && occurrences[occurrences.size()-1].y >= current.y && (occurrences[occurrences.size()-1].x >= current.x || occurrences[occurrences.size()-1].y > current.y)) { 566 | occurrenceCount = occurrences.size()-1; 567 | 568 | } 569 | } 570 | } 571 | if(occurrenceCount == -1) { 572 | occurrenceCount = occurrences.size() - 1; 573 | } 574 | } 575 | 576 | if (!occurrences.empty()) { 577 | scrollX = 0; 578 | setCaretLocation(occurrences[occurrenceCount].x + newWord.size(), occurrences[occurrenceCount].y); 579 | draw(); 580 | } 581 | 582 | for (int j = 0; j < (int)occurrences.size(); j++) { 583 | const auto& oc = occurrences[j]; 584 | 585 | int onScreenPos = oc.x + 4; 586 | if(oc.y >= scrollY && oc.y < scrollY + getTextEditorHeight() && onScreenPos >= scrollX && onScreenPos < scrollX + getTextEditorWidth()) { 587 | move(oc.y - scrollY, oc.x + 4 - scrollX); 588 | if(j == occurrenceCount) 589 | attron(COLOR_PAIR(PAIR_INFO)); 590 | else 591 | attron(A_STANDOUT); 592 | printw(word.c_str()); 593 | if(j == occurrenceCount) 594 | attroff(COLOR_PAIR(PAIR_INFO)); 595 | else 596 | attroff(A_STANDOUT); 597 | } 598 | } 599 | 600 | std::string status {" Find (" + std::to_string(occurrences.size()) + " occurrences found): " }; 601 | int input{}; 602 | Caret statusCaret{(int)newWord.size(), 0}; 603 | setStatus((std::string{status + newWord + " "}).c_str(), PAIR_INFO); 604 | drawStatus(); 605 | 606 | move(getmaxy(stdscr) - 1, statusCaret.x + status.length()); 607 | while (true) { 608 | input = getch(); 609 | if(input == KEY_ENTER || input == 10) { 610 | resetStatus(); 611 | drawStatus(); 612 | return; 613 | } 614 | if(input >= 32 && input < 127) { 615 | newWord.insert(statusCaret.x, 1, (char)input); 616 | statusCaret.x++; 617 | } 618 | if(input == KEY_RIGHT) { 619 | if(statusCaret.x < (int)newWord.length()) statusCaret.x++; 620 | } 621 | if(input == KEY_LEFT) { 622 | if(statusCaret.x > 0) statusCaret.x--; 623 | } 624 | if((input == 127 || input == KEY_BACKSPACE) && !newWord.empty()) { // BACKSPACE 625 | newWord.erase(statusCaret.x - 1, 1); 626 | if(statusCaret.x > 0) statusCaret.x--; 627 | } 628 | if((input == 330 || input == KEY_DL) && !newWord.empty()) { // DEL 629 | newWord.erase(statusCaret.x, 1); 630 | } 631 | if(input == 27 || input == 3) { // ESCAPE or ctrl+c 632 | resetStatus(); 633 | drawStatus(); 634 | scrollX = currentScrollX; 635 | scrollY = currentScrollY; 636 | setCaretLocation(current.x, current.y); 637 | return; 638 | } 639 | if(input == KEY_UP) { 640 | occurrenceCount--; 641 | break; 642 | } 643 | if(input == KEY_DOWN) { 644 | occurrenceCount++; 645 | break; 646 | } 647 | 648 | setStatus((std::string{status + newWord + " "}).c_str(), PAIR_INFO); 649 | drawStatus(); 650 | move(getmaxy(stdscr) - 1, statusCaret.x + status.length()); 651 | } 652 | if(occurrenceCount < 0) 653 | occurrenceCount = occurrences.size() - 1; 654 | if(occurrenceCount >= (int)occurrences.size()) 655 | occurrenceCount = 0; 656 | } 657 | } 658 | void Editor::saveFile() { 659 | if(file.getPath() != "") { 660 | if (file.hasWritePermission()) { 661 | file.save(); 662 | setStatus(" File \'" + file.getFullFilename() + "\' has been saved. ", PAIR_INFO); 663 | } else { 664 | setStatus(" File \'" + file.getFullFilename() + "\' doesn't have write permissions. ", PAIR_ERROR); 665 | } 666 | } else { 667 | std::string fileName = getInputInStatus(" Specify file name: ", PAIR_INFO); 668 | 669 | if(fileName.empty()) { 670 | setStatus(" File not saved because no name specified ", PAIR_WARNING); 671 | } else { 672 | file.saveAs(fileName); 673 | setStatus(" File \'" + file.getFullFilename() + "\' has been saved. ", PAIR_INFO); 674 | } 675 | } 676 | } 677 | 678 | void Editor::setStatus(const std::string& message) { 679 | setStatus(message, PAIR_STANDARD); 680 | } 681 | void Editor::setStatus(const std::string& message, int colorPair) { 682 | this->statusText = message; 683 | this->colorPair = colorPair; 684 | customStatusText = true; 685 | } 686 | void Editor::resetStatus() { 687 | char buffer[256]; 688 | std::string s{}; 689 | #ifndef NDEBUG 690 | sprintf( 691 | buffer, 692 | " File: %s | c.x %2d, c.y %2d ", 693 | file.getFullFilename().c_str(), 694 | caret.x, caret.y 695 | ); 696 | s = buffer; 697 | 698 | std::stack u = undo; 699 | std::stack r = redo; 700 | 701 | s += "["; 702 | while (!u.empty()) { 703 | Action act = u.top(); 704 | if(act.action == ' ') { 705 | s += '_'; 706 | } else if(act.action == 0) { 707 | s += '|'; 708 | } else if(act.action == '\n') { 709 | s += "\\n"; 710 | } else if(act.action == '\t') { 711 | s += "\\t"; 712 | } else if(act.actionType == ActionType::DeletionL || act.actionType == ActionType::DeletionR) { 713 | s += "\\d"; 714 | } else { 715 | s += act.action; 716 | } 717 | u.pop(); 718 | } 719 | s += "]"; 720 | s += " ["; 721 | while (!r.empty()) { 722 | Action act = r.top(); 723 | if(act.action == ' ') { 724 | s += '_'; 725 | } else if(act.action == 0) { 726 | s += '|'; 727 | } else if(act.action == '\n') { 728 | s += "\\n"; 729 | } else if(act.action == '\t') { 730 | s += "\\t"; 731 | } else if(act.actionType == ActionType::DeletionL || act.actionType == ActionType::DeletionR) { 732 | s += "\\d"; 733 | } else { 734 | s += act.action; 735 | } 736 | r.pop(); 737 | } 738 | s += "]"; 739 | #else 740 | sprintf( 741 | buffer, 742 | " File: %s", 743 | file.getFullFilename() != "" ? file.getFullFilename().c_str() : "not specified" 744 | ); 745 | std::string left {buffer}; 746 | sprintf( 747 | buffer, 748 | "Row: %2d, Col: %2d ", 749 | caret.y + 1, caret.x + 1 750 | ); 751 | std::string right {buffer}; 752 | s = left; 753 | s.resize(getmaxx(stdscr), ' '); 754 | s.insert(s.length() - right.size(), right); 755 | #endif 756 | setStatus(s); 757 | } 758 | 759 | // Defines color pairs 760 | void Editor::initColorPairs() const { 761 | init_pair(PAIR_ERROR, COLOR_RED, COLOR_WHITE); 762 | init_pair(PAIR_STANDARD, COLOR_WHITE, COLOR_BLACK); 763 | init_pair(PAIR_WARNING, COLOR_RED, COLOR_WHITE); 764 | init_pair(PAIR_INFO, COLOR_WHITE, COLOR_BLUE); 765 | } 766 | -------------------------------------------------------------------------------- /src/editor.hpp: -------------------------------------------------------------------------------- 1 | #ifndef EDITOR_HPP 2 | #define EDITOR_HPP 3 | 4 | #include "fileEditor.hpp" 5 | #include "action.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if defined(YATE_WINDOWS) 14 | #include "pdcurses.h" 15 | #undef KEY_BACKSPACE 16 | #define KEY_BACKSPACE 8 17 | #undef KEY_DL 18 | 19 | #define KEY_DL 60490 20 | 21 | #undef KEY_UP 22 | #define KEY_UP 60419 23 | #undef KEY_DOWN 24 | #define KEY_DOWN 60418 25 | #undef KEY_LEFT 26 | #define KEY_LEFT 60420 27 | #undef KEY_RIGHT 28 | #define KEY_RIGHT 60421 29 | #undef KEY_HOME 30 | #define KEY_HOME 60422 31 | #undef KEY_END 32 | #define KEY_END 60518 33 | #undef KEY_PPAGE 34 | #define KEY_PPAGE 60499 35 | #undef KEY_NPAGE 36 | #define KEY_NPAGE 60498 37 | 38 | #undef KEY_ENTER 39 | #define KEY_ENTER 13 40 | #else 41 | #include 42 | #endif 43 | 44 | class Editor { 45 | public: 46 | Editor(const std::string& filePath, int tabSize = 4, bool autoIndent = true); 47 | 48 | bool close(bool force = false); 49 | 50 | void draw(); 51 | void drawStatus(); 52 | int getInput(); 53 | 54 | inline void setScrollH(int val) { 55 | int max = 0; 56 | for (int lineNr = scrollY; lineNr < scrollY + getTextEditorHeight() && lineNr < file.linesAmount(); lineNr++) { 57 | int val = getVirtualLineLength(lineNr); 58 | if(val > max) { 59 | max = val; 60 | } 61 | } 62 | scrollX = std::clamp(val, 0, max - 1); 63 | } 64 | inline void setScrollV(int val) { 65 | scrollY = std::clamp(val, 0, file.linesAmount() - 1); 66 | } 67 | inline void scrollH(int amount) { setScrollH(scrollX + amount); } 68 | inline void scrollV(int amount) { setScrollV(scrollY + amount); } 69 | inline void scrollUp(int amount = 1) { scrollV(-amount); } 70 | inline void scrollDown(int amount = 1) { scrollV(amount); } 71 | inline void scrollRight(int amount = 1) { scrollH(amount); } 72 | inline void scrollLeft(int amount = 1) { scrollH(-amount); } 73 | 74 | void put(int ch, bool record = true); 75 | void deleteCharL(bool record = true); 76 | void deleteCharR(bool record = true); 77 | void newLine(); 78 | 79 | void moveUp(); 80 | void moveDown(); 81 | void moveLeft(); 82 | void moveRight(); 83 | void moveBeginningOfLine(); 84 | void moveToFirstCharacter(); 85 | void moveEndOfLine(); 86 | void moveBeginningOfText(); 87 | void moveEndOfText(); 88 | 89 | void find(); 90 | void saveFile(); 91 | 92 | void setStatus(const std::string& message); 93 | void setStatus(const std::string& message, int colorPair); 94 | void resetStatus(); 95 | 96 | void initColorPairs() const; 97 | void applyColorPairToStatusBar(int colorPair); 98 | 99 | inline bool isAlive() const { 100 | return alive; 101 | } 102 | inline int getOnScreenCursorX() const { 103 | return getcurx(stdscr); 104 | } 105 | inline int getOnScreenCursorY() const { 106 | return getcury(stdscr); 107 | } 108 | inline int getTextEditorWidth() const { 109 | return getmaxx(stdscr) - 4; 110 | } 111 | inline int getTextEditorHeight() const { 112 | return getmaxy(stdscr) - 2; 113 | } 114 | inline void setCaretLocation(int x, int y, bool resetSavedX = false) { 115 | caret.y = std::clamp(y, 0, (int)file.linesAmount() - 1); 116 | if(x == caret.x) { 117 | if (getVirtualLineLength() < caret.savedX) { 118 | caret.x = getVirtualLineLength(); 119 | if (resetSavedX) caret.savedX = x; 120 | file.setCaretLocation(file.getLineSize(), caret.y); 121 | } else { 122 | file.setCaretLocation(getFileCaretColumn(caret.savedX), caret.y); 123 | caret.x = getVirtualCaretColumn(file.getCaretX(), caret.y); 124 | } 125 | } else { 126 | x = std::clamp(x, 0, (int)getVirtualLineLength(caret.y)); 127 | file.setCaretLocation(getFileCaretColumn(x), caret.y); 128 | caret.x = caret.savedX = getVirtualCaretColumn(file.getCaretX(), file.getCaretY()); 129 | } 130 | 131 | scrollToCaret(); 132 | } 133 | inline void scrollToCaret() { 134 | if(caret.x < scrollX) { 135 | scrollLeft((scrollX) - (caret.x)); 136 | } 137 | if(caret.x > getTextEditorWidth() - 1 + scrollX) { 138 | scrollRight((caret.x) - (getTextEditorWidth() - 1 + scrollX)); 139 | } 140 | if(caret.y < scrollY) { 141 | scrollUp((scrollY) - (caret.y)); 142 | } 143 | if(caret.y > getTextEditorHeight() - 1 + scrollY) { 144 | scrollDown((caret.y) - (getTextEditorHeight() - 1 + scrollY)); 145 | } 146 | } 147 | inline bool IsAutoIndentEnabled() { return autoIndent; } 148 | inline void EnableAutoIndent() { autoIndent = true; } 149 | inline void DisableAutoIndent() { autoIndent = false; } 150 | 151 | 152 | private: 153 | bool alive; 154 | FileEditor file; 155 | const int TAB_SIZE; 156 | 157 | Caret caret; 158 | int scrollX; 159 | int scrollY; 160 | 161 | std::string statusText; 162 | bool customStatusText{false}; 163 | 164 | // Color control variable: 165 | int colorPair{1}; 166 | 167 | std::stack undo; 168 | std::stack redo; 169 | 170 | int currentAction{}; 171 | int prevAction{}; 172 | 173 | bool autoIndent{true}; 174 | 175 | std::string getInputInStatus(std::string statusText, int colorPair, const std::string& preset = ""); 176 | 177 | inline char getCharAtCaret() { 178 | return file.getLine(caret.y)[getFileCaretColumn() - 1]; 179 | } 180 | 181 | inline int getVirtualCaretColumnToCaret() { 182 | return getVirtualCaretColumnToCaret(file.getCaretY()); 183 | } 184 | 185 | inline int getVirtualCaretColumnToCaret(int row) { 186 | int size{}; 187 | const std::string& line = file.getLine(row); 188 | for (int col = 0; col < file.getCaretX(); col++) { 189 | if(line[col] != '\t') { 190 | size++; 191 | } else { 192 | size += TAB_SIZE - (size) % TAB_SIZE; 193 | } 194 | } 195 | return size; 196 | } 197 | 198 | inline int getVirtualLineLength() { 199 | return getVirtualLineLength(caret.y); 200 | } 201 | inline int getVirtualLineLength(int y) { 202 | return getVirtualCaretColumn(file.getLineSize(y), y); 203 | } 204 | inline int getVirtualCaretColumn(int x, int y) { 205 | int size{}; 206 | const std::string& line = file.getLine(y); 207 | for (int col = 0; col < x; col++) { 208 | if(line[col] != '\t') { 209 | size++; 210 | } else { 211 | size += TAB_SIZE - (size) % TAB_SIZE; 212 | } 213 | } 214 | return size; 215 | } 216 | 217 | inline int getFileCaretColumn() { 218 | return getFileCaretColumn(getVirtualCaretColumnToCaret()); 219 | } 220 | inline int getFileCaretColumn(int virtualColumn) { 221 | return getFileCaretColumn(virtualColumn, caret.y); 222 | } 223 | inline int getFileCaretColumn(int virtualColumn, int y) { 224 | int size{}; 225 | const std::string& line = file.getLine(y); 226 | for (int col = 0; col < virtualColumn;) { 227 | if(line[size] == '\t') { 228 | col += TAB_SIZE - (col) % TAB_SIZE; 229 | } else { 230 | col++; 231 | } 232 | if(col <= virtualColumn) { 233 | size++; 234 | } 235 | } 236 | return size; 237 | } 238 | inline int getCharsCountBeforeFirstCharacter() { 239 | return getCharsCountBeforeFirstCharacter(caret.x); 240 | } 241 | inline int getCharsCountBeforeFirstCharacter(int x) { 242 | const std::string& line = file.getLine(caret.y); 243 | int fileX; 244 | if (x > -1) fileX = getFileCaretColumn(x); 245 | else fileX = line.size(); 246 | int len = 0; 247 | for (int col = 0; col < fileX; col++) { 248 | if (line[col] == ' ') { 249 | len++; 250 | } else if (line[col] == '\t') { 251 | len += TAB_SIZE - (len) % TAB_SIZE; 252 | } else { 253 | return len; 254 | } 255 | } 256 | return len; 257 | } 258 | inline std::vector getCharsBeforeFirstCharacter() { 259 | int fileX = getFileCaretColumn(); 260 | const std::string& line = file.getLine(caret.y); 261 | std::vector chars; 262 | for (int col = 0; col < fileX; col++) { 263 | if (line[col] == ' ' || line[col] == '\t') { 264 | chars.push_back(line[col]); 265 | } else { 266 | return chars; 267 | } 268 | } 269 | return chars; 270 | } 271 | }; 272 | 273 | #endif 274 | -------------------------------------------------------------------------------- /src/fileEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "fileEditor.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | namespace fs = std::filesystem; 8 | 9 | #ifdef YATE_WINDOWS 10 | #include "pdcurses.h" 11 | #include "Windows.h" 12 | #endif 13 | 14 | #if defined(__linux__) || defined(__APPLE__) 15 | #include 16 | #include 17 | #include 18 | #endif 19 | 20 | FileEditor::FileEditor(const std::string& path) 21 | : caret{}, 22 | path{path}, 23 | fullFilename {}, 24 | filename {}, 25 | extension {}, 26 | lines{}, 27 | writePermission{true}, 28 | infoMessage{} { 29 | 30 | if (path != "" && fs::is_regular_file(path)) { 31 | setPath(path); 32 | 33 | fs::perms active_perms = fs::status(path).permissions(); 34 | #if defined(__linux__) || defined(__APPLE__) 35 | struct stat file_stat; 36 | stat(path.c_str(), &file_stat); 37 | 38 | uid_t current_uid = getuid(); 39 | gid_t current_gid = getgid(); 40 | 41 | if (!(((active_perms & fs::perms::owner_read) != fs::perms::none && file_stat.st_uid == current_uid) || 42 | ((active_perms & fs::perms::group_read) != fs::perms::none && file_stat.st_gid == current_gid) || 43 | ((active_perms & fs::perms::others_read) != fs::perms::none))) { 44 | endwin(); 45 | std::cout << "Can't edit " << fullFilename << " not enough permissions.\n"; 46 | exit(1); 47 | } 48 | this->writePermission = ((active_perms & fs::perms::owner_write) != fs::perms::none && file_stat.st_uid == current_uid) || 49 | ((active_perms & fs::perms::group_write) != fs::perms::none && file_stat.st_gid == current_gid) || 50 | ((active_perms & fs::perms::others_write) != fs::perms::none); 51 | #else 52 | if (!(((active_perms & fs::perms::owner_read) != fs::perms::none) || 53 | ((active_perms & fs::perms::group_read) != fs::perms::none) || 54 | ((active_perms & fs::perms::others_read) != fs::perms::none))) { 55 | endwin(); 56 | std::cout << "Can't edit " << fullFilename << " not enough permissions.\n"; 57 | exit(1); 58 | } 59 | this->writePermission = ((active_perms & fs::perms::owner_write) != fs::perms::none) || 60 | ((active_perms & fs::perms::group_write) != fs::perms::none) || 61 | ((active_perms & fs::perms::others_write) != fs::perms::none); 62 | #endif 63 | 64 | std::ifstream file {path}; 65 | if (!file.good() && file.bad()) { 66 | endwin(); 67 | std::cout << "Error occured while trying to open " << fullFilename << ".\n"; 68 | #ifndef NDEBUG 69 | std::cerr << "Error bits are: " 70 | << "\nfailbit: " << file.fail() 71 | << "\neofbit: " << file.eof() 72 | << "\nbadbit: " << file.bad() << std::endl; 73 | #endif 74 | exit(1); 75 | } else if(!file.good()) { 76 | writePermission = true; 77 | lines.push_back(""); 78 | } 79 | else { 80 | lines.push_back(""); 81 | while (file) { 82 | int ch = file.get(); 83 | if (ch == -1) break; 84 | 85 | if (ch == '\n' || ch == 10) { 86 | lines.push_back(""); 87 | } else { 88 | lines[lines.size()-1].push_back(ch); 89 | } 90 | } 91 | } 92 | } else { 93 | if(!path.empty() && !fs::is_regular_file(path)) { 94 | infoMessage = " Path is not a file "; 95 | } 96 | this->path = ""; 97 | writePermission = true; 98 | lines.push_back(""); 99 | } 100 | } 101 | 102 | void FileEditor::newLine() { 103 | std::string& current = lines[caret.y]; 104 | std::string rest = current.substr(caret.x, getLineSize(caret.y)); 105 | current.erase(caret.x, getLineSize(caret.y)); 106 | lines.insert(lines.begin() + caret.y + 1, rest); 107 | } 108 | void FileEditor::del(bool right) { 109 | if ((caret.y == 0 && caret.x == 0) && linesAmount() <= 1 && getLineSize(caret.y) <= 1) { 110 | throw std::string(" No char to delete. "); 111 | } 112 | if (!(right) && caret.x == 0 && caret.y == 0) { 113 | throw std::string(" No char to delete. "); 114 | } 115 | if (right && caret.x == getLineSize() && caret.y == linesAmount() - 1) { 116 | throw std::string(" No char to delete. "); 117 | } 118 | 119 | if(right) { 120 | if(caret.x == getLineSize(caret.y)) { 121 | int lineNr = caret.y; 122 | std::string line = lines[lineNr + 1]; 123 | lines.erase(lines.begin() + lineNr + 1); 124 | lines[lineNr].append(line); 125 | } 126 | else { 127 | lines[caret.y].erase(lines[caret.y].begin() + (caret.x)); 128 | } 129 | } 130 | else { 131 | if (caret.x == 0 && caret.y == 0 && linesAmount() <= 1) { 132 | throw std::string(" No char to delete. "); 133 | } 134 | if(caret.x == 0) { 135 | int lineNr = caret.y; 136 | std::string line = lines[lineNr]; 137 | lines.erase(lines.begin() + lineNr); 138 | lines[lineNr - 1].append(line); 139 | } 140 | else { 141 | lines[caret.y].erase(lines[caret.y].begin() + (caret.x - 1)); 142 | } 143 | } 144 | } 145 | 146 | void FileEditor::save() { 147 | if (path.empty()) throw std::logic_error("Cannot save file with empty path, use saveAs() instead."); 148 | std::ofstream file { path }; 149 | for (int i = 0; i < (int)lines.size() - 1; i++) { 150 | file << lines[i] << "\n"; 151 | } 152 | file << lines[lines.size() - 1]; 153 | // if (lines[lines.size() - 1].size() != 0) 154 | // file << '\n'; 155 | } 156 | void FileEditor::saveAs(const std::string& path) { 157 | setPath(path); 158 | if (path.empty()) throw std::logic_error("Cannot save file with empty path, use saveAs() instead."); 159 | std::ofstream file { path }; 160 | for (int i = 0; i < (int)lines.size() - 1; i++) { 161 | file << lines[i] << "\n"; 162 | } 163 | file << lines[lines.size() - 1]; 164 | } 165 | 166 | void FileEditor::setPath(const std::string& _path) { 167 | fs::path temp = fs::path{_path}; 168 | this->path = fs::absolute(temp).string(); 169 | for (int i = path.size() - 1; i >= 0; i--) { 170 | if(path[i] == '/' || path[i] == '\\') break; 171 | fullFilename += path[i]; 172 | } 173 | std::reverse(fullFilename.begin(), fullFilename.end()); 174 | filename = fullFilename.substr(0, fullFilename.find('.')); 175 | if (fullFilename.find('.') != std::string::npos) { 176 | extension = fullFilename.substr(filename.size() + 1); 177 | } else { 178 | extension = ""; 179 | } 180 | } 181 | 182 | void FileEditor::close() { 183 | lines.clear(); 184 | lines.shrink_to_fit(); 185 | } 186 | -------------------------------------------------------------------------------- /src/fileEditor.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FILE_EDITOR_HPP 2 | #define FILE_EDITOR_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "caret.hpp" 12 | 13 | class FileEditor { 14 | public: 15 | FileEditor(const std::string& path); 16 | 17 | inline void setCaretLocation(int x, int y) { 18 | caret.y = std::clamp(y, 0, (int)lines.size() - 1); 19 | caret.x = std::clamp(x, 0, (int)lines[caret.y].size()); 20 | } 21 | inline void moveCaret(int x, int y) { 22 | setCaretLocation(caret.x + x, caret.y + y); 23 | } 24 | 25 | inline void moveUp() { 26 | moveCaret(0, -1); 27 | } 28 | inline void moveDown() { 29 | moveCaret(0, 1); 30 | } 31 | inline void moveLeft(){ 32 | moveCaret(-1, 0); 33 | } 34 | inline void moveRight(){ 35 | moveCaret(1, 0); 36 | } 37 | 38 | inline void put(char ch) { 39 | lines[caret.y].insert(caret.x, 1, ch); 40 | } 41 | inline void put(const std::string& str) { 42 | lines[caret.y].insert(caret.x, str); 43 | setCaretLocation(caret.x + str.size(), caret.y); 44 | } 45 | void del(bool right); 46 | 47 | void newLine(); 48 | 49 | bool hasFileContentChanged() { 50 | if(!hasWritePermission()) { 51 | return false; 52 | } 53 | if(path == "") { 54 | if (lines.size() > 1 || !lines[0].empty()) return true; 55 | else return false; 56 | } 57 | 58 | std::ifstream file {path}; 59 | unsigned int x{}; 60 | unsigned int y{}; 61 | while (file) { 62 | 63 | int ch = file.get(); 64 | if (ch == -1) { y++; break; } 65 | 66 | if (ch == '\n' || ch == 10) { 67 | y++; 68 | x = 0; 69 | } else if (lines[y][x] != (char)ch) { 70 | return true; 71 | } else { 72 | x++; 73 | } 74 | if (y > lines.size() || x > lines[y].size()) { 75 | return true; 76 | } 77 | } 78 | 79 | if (lines.size() > y) return true; 80 | 81 | return false; 82 | #if 0 83 | size_t row = 0; 84 | while(file) { 85 | std::string line{""}; 86 | std::getline(file, line); 87 | if(row == 1 && lines.size() == 1) break; 88 | if(line.length() != lines[row].length()) return true; 89 | for(size_t i = 0; i < line.length(); i++) { 90 | if(line[i] != lines[row][i]) return true; 91 | } 92 | row++; 93 | if(row > lines.size()) return true; 94 | } 95 | if(row != lines.size()) return true; 96 | 97 | return false; 98 | #endif 99 | } 100 | 101 | inline const std::string& getLine() const { 102 | return lines[caret.y]; 103 | } 104 | inline const std::string& getLine(size_t lineNr) const { 105 | return lines[lineNr]; 106 | } 107 | inline int getLineSize() const { 108 | return lines[caret.y].size(); 109 | } 110 | inline int getLineSize(size_t lineNr) const { 111 | return lines[lineNr].size(); 112 | } 113 | inline const std::string& getPath() const { 114 | return path; 115 | } 116 | inline const std::string& getFullFilename() const { 117 | return fullFilename; 118 | } 119 | inline const std::string& getFilename() const { 120 | return filename; 121 | } 122 | inline const std::string& getFileExtension() const { 123 | return extension; 124 | } 125 | inline int linesAmount() const { 126 | return lines.size(); 127 | } 128 | 129 | inline int getCaretX() const { 130 | return caret.x; 131 | } 132 | inline int getCaretY() const { 133 | return caret.y; 134 | } 135 | inline const Caret& getCarret() const { 136 | return this->caret; 137 | } 138 | inline const std::string& getInfoMessage() const { 139 | return infoMessage; 140 | } 141 | 142 | void save(); 143 | void saveAs(const std::string& path); 144 | void close(); 145 | 146 | inline bool hasWritePermission() const { 147 | return writePermission; 148 | } 149 | 150 | 151 | private: 152 | Caret caret; 153 | std::string path; 154 | std::string fullFilename; 155 | std::string filename; 156 | std::string extension; 157 | std::vector lines; 158 | bool writePermission; 159 | std::string infoMessage; 160 | 161 | void setPath(const std::string& _path); 162 | }; 163 | 164 | #endif 165 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // TODO: Redefine key macros 2 | #if defined(YATE_WINDOWS) 3 | #include "pdcurses.h" 4 | #include "Windows.h" 5 | #else 6 | #include 7 | #endif 8 | 9 | #include 10 | #include "fileEditor.hpp" 11 | #include "editor.hpp" 12 | #include 13 | 14 | 15 | // Yate: Yet Another Text Editor 16 | // Originally created by Xylit (@Xyl1t) 17 | // Contributors: @Niki4Tap for revoking the project and working on it 18 | // @EntireTwix for making small, but important changes to CMakeLists.txt 19 | // @DCubix (Diego) for getting yate run for the first time on Windows. 20 | 21 | // TODO: 22 | // * Word highlighting 23 | // * Basic keyword highlighting 24 | // * Begin and end parenthesis 25 | // * Custom profile with format: 26 | // extension: 27 | // : 28 | // * Options 29 | // --path-to-profile specify the path to .yateprofile 30 | // * Check for permissions on windows 31 | 32 | int main(int argc, char** argv) { 33 | 34 | #ifndef NDEBUG 35 | std::ofstream ofs("log.txt"); 36 | std::clog.rdbuf(ofs.rdbuf()); 37 | std::clog << "Date: " << __DATE__ << std::endl; 38 | #endif 39 | 40 | #if defined(YATE_WINDOWS) && NDEBUG 41 | FreeConsole(); 42 | #endif 43 | 44 | std::string path {}; 45 | int terminalWidth = 0; 46 | int terminalHeight = 0; 47 | int tabSize = 4; 48 | bool autoIndent = true; 49 | for(int i = 1; i < argc; i++) { 50 | std::string arg = argv[i]; 51 | std::stringstream argStream {argv[i]}; 52 | auto match = [&](std::string_view s) { return (arg.rfind(s.data(), 0) == 0); }; 53 | 54 | if(match("-t") || match("--tab-size")) { 55 | std::stringstream argVal {argv[i + 1]}; 56 | argVal >> tabSize; 57 | i++; 58 | } else if (match("-r") || match("--rows")) { 59 | std::stringstream argVal {argv[i + 1]}; 60 | argVal >> terminalWidth; 61 | i++; 62 | } else if (match("-a") || match("--disable-auto-indent")) { 63 | autoIndent = false; 64 | i++; 65 | } else if (match("-h") || match("--help")) { 66 | std::cout << R"STR(Usage: yate [file] [options] 67 | All possible options are: 68 | -t , --tab-size 69 | sets the tab size to the specified size 70 | -a, --disable-auto-indent 71 | disables auto indentation 72 | -r , --rows 73 | amount of rows in terminal (only for windows) 74 | -c , --cols 75 | amount of columns in terminal (only for windows) 76 | -h, --help 77 | shows this help screen 78 | 79 | Yate key bindings: 80 | Main: 81 | ctrl+s: save file 82 | ctrl+c: exit the program 83 | Movement: 84 | arrow keys: move one letter up/down/left/right 85 | page-up: move caret up by screen size 86 | page-down: move caret down by screen size 87 | home: put caret at end the beginning of the line 88 | end: put caret at the end of the line 89 | ctrl+x: put caret to beginning of word 90 | ctrl+z: put caret to end of word 91 | ctrl+k: scroll screen left 92 | ctrl+l: scroll screen right 93 | Misc: 94 | ctrl+f: find word 95 | up-arrow: go to previous match 96 | down-arrow: go to next match 97 | enter: confirm caret location 98 | esc, ctrl+c: cancel 99 | ctrl+u: undo 100 | ctrl+d: redo 101 | )STR"; 102 | return 0; 103 | } else { 104 | argStream >> path; 105 | } 106 | } 107 | initscr(); 108 | start_color(); 109 | raw(); 110 | refresh(); 111 | noecho(); 112 | resize_term(terminalHeight, terminalWidth); 113 | keypad(stdscr, true); 114 | #if defined(YATE_WINDOWS) 115 | SetWindowTextA(GetActiveWindow(),"Yate"); // set window title 116 | #else 117 | set_escdelay(0); 118 | #endif 119 | 120 | 121 | Editor editor { path, tabSize, autoIndent }; 122 | int action; 123 | 124 | while(editor.isAlive()) { 125 | editor.draw(); 126 | action = editor.getInput(); 127 | } 128 | 129 | endwin(); 130 | 131 | if (action == -1) { 132 | std::cout << "Error reading input (stdin was -1 for a very long time)\nNOTE: piping doesn't work\n"; 133 | } 134 | 135 | return 0; 136 | } 137 | --------------------------------------------------------------------------------