├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── STYLE.md ├── TODO.md ├── assets ├── basil_8x8.bmp ├── default_7x16.bmp └── trinity_12x18.bmp ├── doc ├── README.md └── config.md ├── img └── screenshot.png ├── source ├── app.c ├── app.h ├── assets.c ├── assets.h ├── assets │ ├── basil_8x8.h │ ├── default_7x16.h │ └── trinity_12x18.h ├── clipboard.h ├── components.h ├── config.c ├── config.h ├── constants.h ├── escapes.c ├── escapes.h ├── input.c ├── input.h ├── main.c ├── noch.c ├── safe.c ├── safe.h ├── sdl.c ├── sdl.h ├── terminal.c ├── terminal.h ├── textScreen.c ├── textScreen.h ├── types.h ├── util.c └── util.h └── todo.json /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | yterm 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/noch"] 2 | path = lib/noch 3 | url = https://github.com/lordoftrident/noch 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 yeti0904 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = $(wildcard source/*.c) 2 | DEPS = $(wildcard source/*.h) 3 | OBJ = $(addsuffix .o,$(subst source/,bin/,$(basename $(SRC)))) 4 | LIBS = -lSDL3 5 | FLAGS = -std=c99 -Wall -Wextra -pedantic -g -I./lib 6 | EMBEDDED = $(addsuffix .h,$(subst assets/,source/assets/,$(basename $(wildcard assets/*)))) 7 | 8 | ifeq ($(MODE), release) 9 | FLAGS += -Ofast -s 10 | else 11 | FLAGS += -g -Og 12 | endif 13 | 14 | compile: $(EMBEDDED) ./bin $(OBJ) $(SRC) $(DEPS) 15 | $(CC) $(OBJ) $(LIBS) -o yterm 16 | 17 | ./bin: 18 | mkdir -p bin 19 | 20 | source/assets/%.h: assets/%.bmp ./source/assets 21 | xxd -i $< > $@ 22 | 23 | ./source/assets: 24 | mkdir -p source/assets 25 | 26 | bin/%.o: source/%.c $(DEPS) 27 | $(CC) -c $< $(FLAGS) -o $@ 28 | 29 | clean: 30 | rm bin/*.o yterm 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

yterm

2 |

3 | 4 | License 5 | 6 | 7 | Issues 8 | 9 | 10 | GitHub pull requests 11 | 12 |


13 |

14 | 15 | A terminal emulator written in C, based on [eduterm](https://www.uninformativ.de/blog/postings/2018-02-24/0/POSTING-en.html) 16 | 17 | Far from complete, any help is appreciated 18 | 19 | If you want to use it maybe wait until it's usable (which it currently isn't) 20 | 21 | 22 | 23 | ## Keybinds 24 | - control + shift + t - new tab 25 | - control + shift + page up - previous tab 26 | - control + shift + page down - next tab 27 | 28 | ## Build 29 | ``` 30 | make 31 | ``` 32 | 33 | ### Dependencies 34 | - SDL3 35 | -------------------------------------------------------------------------------- /STYLE.md: -------------------------------------------------------------------------------- 1 | # yterm style guide 2 | 3 | ## Function calls 4 | This is how a function call must be formatted: 5 | ``` 6 | myfunc(arg1, arg2); 7 | ``` 8 | - no space between the name and the ( 9 | - space after commas 10 | 11 | ## Include structure 12 | #### Order of includes in components.h 13 | 1. standard libraries 14 | 2. 3rd party libraries 15 | 16 | These includes should only be in components.h 17 | 18 | Includes from the yterm source code can be in any other file 19 | 20 | Imports must be ordered based on the length of the text 21 | 22 | ## Pointer definitions 23 | ``` 24 | int* b; 25 | ``` 26 | The pointer must be on the left side 27 | 28 | ## Statements 29 | ``` 30 | if (...) { 31 | 32 | } 33 | else { 34 | 35 | } 36 | ``` 37 | - } must be on a line on its own 38 | - { must be on the line with the statement 39 | - the else should be on a seperate line from the previous } 40 | 41 | ## Naming 42 | - camelCase for variables 43 | - PascalCase for functions 44 | - PascalCase for structs/enums/typedefs etc 45 | - camelCase for file names 46 | 47 | ## Function definitions 48 | ``` 49 | void myfunc() { 50 | 51 | } 52 | ``` 53 | 54 | ## Comments 55 | - use `//` for single linecomments 56 | 57 | ## Line length 58 | - Limited to 80 characters 59 | - If lines are too long with paranthesis, split like this: 60 | ``` 61 | ... ( 62 | ... 63 | ) 64 | ... 65 | ``` 66 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | - Add line wrapping 3 | -------------------------------------------------------------------------------- /assets/basil_8x8.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MESYETI/yterm/c3b4c2567e981683739cd30fe31a01fc26643f80/assets/basil_8x8.bmp -------------------------------------------------------------------------------- /assets/default_7x16.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MESYETI/yterm/c3b4c2567e981683739cd30fe31a01fc26643f80/assets/default_7x16.bmp -------------------------------------------------------------------------------- /assets/trinity_12x18.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MESYETI/yterm/c3b4c2567e981683739cd30fe31a01fc26643f80/assets/trinity_12x18.bmp -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # doc 2 | Some documentation for yterm 3 | 4 | - [config](/doc/config.md) - info on configuring yterm 5 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | # yterm config 2 | yterm config is stored in `~/.config/yterm` 3 | 4 | All config is stored as json 5 | 6 | ## config.json 7 | This is the main config file for yterm, it contains these properties: 8 | - `theme`: contains the name of the theme being used 9 | 10 | ## Theme files 11 | Theme files are stored in `~/.config/yterm/themes` 12 | 13 | They contain these colours that you can set: 14 | - `black` 15 | - `red` 16 | - `green` 17 | - `yellow` 18 | - `blue` 19 | - `magenta` 20 | - `cyan` 21 | - `white` 22 | - `grey` 23 | - `bright_red` 24 | - `bright_green` 25 | - `bright_yellow` 26 | - `bright_blue` 27 | - `bright_magenta` 28 | - `bright_cyan` 29 | - `bright_white` 30 | - `fg` (the default foreground colour) 31 | - `bg` (the default background colour) 32 | -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MESYETI/yterm/c3b4c2567e981683739cd30fe31a01fc26643f80/img/screenshot.png -------------------------------------------------------------------------------- /source/app.c: -------------------------------------------------------------------------------- 1 | #include "app.h" 2 | #include "sdl.h" 3 | #include "safe.h" 4 | #include "input.h" 5 | #include "config.h" 6 | #include "constants.h" 7 | 8 | void App_Init(App* app, char** env) { 9 | app->running = true; 10 | app->env = env; 11 | 12 | // init config 13 | app->config.interpretEscapes = true; 14 | app->config.cursorEnabled = true; 15 | 16 | // init video 17 | app->video = Video_Init(); 18 | SDL_StartTextInput(app->video.window); 19 | 20 | // init colourscheme and font 21 | LoadConfig(&app->colours, &app->video); 22 | 23 | // init tabs 24 | app->tabs = NULL; 25 | app->tabAmount = 0; 26 | app->currentTab = 0; 27 | App_AddTab(app); 28 | 29 | // init screen 30 | app->screen = TextScreen_New( 31 | app->video.windowSize.x / app->video.charWidth, 32 | app->video.windowSize.y / app->video.charHeight 33 | ); 34 | app->screen.colours = &app->colours; 35 | } 36 | 37 | void App_Free(App* app) { 38 | // free screen 39 | TextScreen_Free(&app->screen); 40 | 41 | // free video 42 | Video_Free(&app->video); 43 | 44 | // free tabs 45 | for (size_t i = 0; i < app->tabAmount; ++ i) { 46 | Terminal_Free(&app->tabs[i]); 47 | } 48 | free(app->tabs); 49 | } 50 | 51 | void App_AddTab(App* app) { 52 | if (app->tabs == NULL) { 53 | app->tabs = SafeMalloc(sizeof(Terminal)); 54 | } 55 | else { 56 | app->tabs = SafeRealloc( 57 | app->tabs, (app->tabAmount + 1) * sizeof(Terminal) 58 | ); 59 | } 60 | 61 | Terminal_Init(&app->tabs[app->tabAmount], app->env, App_GetUsableArea(app)); 62 | 63 | app->tabs[app->tabAmount].buffer.colours = &app->colours; 64 | app->tabs[app->tabAmount].config = &app->config; 65 | 66 | ++ app->tabAmount; 67 | } 68 | 69 | Terminal* App_CurrentTab(App* app) { 70 | return &app->tabs[app->currentTab]; 71 | } 72 | 73 | Vec2 App_GetUsableArea(App* app) { 74 | return (Vec2) { 75 | app->video.windowSize.x / app->video.charWidth, 76 | (app->video.windowSize.y / app->video.charHeight) - 1 77 | }; 78 | } 79 | 80 | void App_UpdateTitle(App* app) { 81 | SDL_SetWindowTitle(app->video.window, App_CurrentTab(app)->title); 82 | } 83 | 84 | void App_Update(App* app) { 85 | SDL_Event e; 86 | 87 | while (SDL_PollEvent(&e)) { 88 | switch (e.type) { 89 | case SDL_EVENT_QUIT: { 90 | app->running = false; 91 | continue; 92 | } 93 | case SDL_EVENT_KEY_DOWN: 94 | case SDL_EVENT_TEXT_INPUT: { 95 | if (e.type == SDL_EVENT_KEY_DOWN) { 96 | const bool* keys = SDL_GetKeyboardState(NULL); 97 | 98 | if (!ShiftPressed(keys) || !keys[SDL_SCANCODE_LCTRL]) { 99 | goto doInput; 100 | } 101 | 102 | if ( 103 | (e.key.scancode == SDL_SCANCODE_LCTRL) || 104 | (e.key.scancode == SDL_SCANCODE_LSHIFT) || 105 | (e.key.scancode == SDL_SCANCODE_RSHIFT) 106 | ) { 107 | goto doInput; 108 | } 109 | 110 | switch (e.key.scancode) { 111 | case SDL_SCANCODE_T: { 112 | App_AddTab(app); 113 | app->currentTab = app->tabAmount - 1; 114 | break; 115 | } 116 | case SDL_SCANCODE_PAGEUP: { 117 | if (app->currentTab == 0) { 118 | app->currentTab = app->tabAmount - 1; 119 | } 120 | else { 121 | -- app->currentTab; 122 | } 123 | break; 124 | } 125 | case SDL_SCANCODE_PAGEDOWN: { 126 | ++ app->currentTab; 127 | 128 | if (app->currentTab >= app->tabAmount) { 129 | app->currentTab = 0; 130 | } 131 | break; 132 | } 133 | default: break; 134 | } 135 | 136 | break; 137 | } 138 | 139 | doInput: 140 | HandleInputEvent(&e, App_CurrentTab(app)); 141 | break; 142 | } 143 | case SDL_EVENT_WINDOW_RESIZED: { 144 | app->video.windowSize.x = e.window.data1; 145 | app->video.windowSize.y = e.window.data2; 146 | 147 | Vec2 newSize = { 148 | e.window.data1 / app->video.charWidth, 149 | e.window.data2 / app->video.charHeight 150 | }; 151 | TextScreen_Resize(&app->screen, newSize); 152 | 153 | Vec2 newTabSize = App_GetUsableArea(app); 154 | 155 | for (size_t i = 0; i < app->tabAmount; ++ i) { 156 | TextScreen_Resize(&app->tabs[i].buffer, newTabSize); 157 | } 158 | 159 | SetTerminalSize(App_CurrentTab(app)); 160 | break; 161 | } 162 | } 163 | } 164 | 165 | for (size_t i = 0; i < app->tabAmount; ++ i) { 166 | char* tabTitle = app->tabs[i].title; 167 | 168 | Terminal_Update(&app->tabs[i]); 169 | 170 | if (app->tabs[i].title != tabTitle) { 171 | App_UpdateTitle(app); 172 | } 173 | } 174 | 175 | app->screen.attr.attr |= ATTR_COLOUR_BG; 176 | app->screen.attr.bg = COLOUR_GREY; 177 | app->screen.attr.fg = COLOUR_BRIGHT_WHITE; 178 | TextScreen_HLine(&app->screen, (Vec2) {0, 0}, app->screen.size.x, ' '); 179 | 180 | char status[256]; 181 | sprintf( 182 | status, "%s - [%d/%d]", 183 | APP_NAME, (int) app->currentTab + 1, (int) app->tabAmount 184 | ); 185 | 186 | app->screen.cursor = (Vec2) {0, 0}; 187 | TextScreen_PutString(&app->screen, status); 188 | 189 | app->screen.cursor = App_CurrentTab(app)->buffer.cursor; 190 | ++ app->screen.cursor.y; 191 | TextScreen_Blit(&app->screen, &App_CurrentTab(app)->buffer, (Vec2) {0, 1}); 192 | 193 | TextScreen_Render(&app->screen, &app->video, app->config.cursorEnabled); 194 | SDL_RenderPresent(app->video.renderer); 195 | } 196 | -------------------------------------------------------------------------------- /source/app.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_APP_H 2 | #define YTERM_APP_H 3 | 4 | #include "components.h" 5 | #include "textScreen.h" 6 | #include "terminal.h" 7 | 8 | typedef struct App { 9 | bool running; 10 | Video video; 11 | Terminal* tabs; 12 | size_t tabAmount; 13 | size_t currentTab; 14 | ColourScheme colours; 15 | TerminalConfig config; 16 | TextScreen screen; 17 | char** env; 18 | } App; 19 | 20 | void App_Init(App* app, char** env); 21 | void App_Free(App* app); 22 | void App_AddTab(App* app); 23 | Terminal* App_CurrentTab(App* app); 24 | Vec2 App_GetUsableArea(App* app); 25 | void App_UpdateTitle(App* app); 26 | void App_Update(App* app); 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /source/assets.c: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | #include "assets.h" 3 | #include "config.h" 4 | 5 | // assets 6 | #include "assets/basil_8x8.h" 7 | #include "assets/default_7x16.h" 8 | #include "assets/trinity_12x18.h" 9 | 10 | typedef struct { 11 | const char* fileName; 12 | const uint8_t* contents; 13 | size_t size; 14 | } Asset; 15 | 16 | void WriteAssets(void) { 17 | Asset assets[] = { 18 | { 19 | .fileName = "fonts/basil_8x8.bmp", 20 | .contents = assets_basil_8x8_bmp, 21 | .size = assets_basil_8x8_bmp_len 22 | }, 23 | { 24 | .fileName = "fonts/default_7x16.bmp", 25 | .contents = assets_default_7x16_bmp, 26 | .size = assets_default_7x16_bmp_len 27 | }, 28 | { 29 | .fileName = "fonts/trinity_12x18.bmp", 30 | .contents = assets_trinity_12x18_bmp, 31 | .size = assets_trinity_12x18_bmp_len 32 | } 33 | }; 34 | 35 | for (size_t i = 0; i < ARRAY_LEN(assets); ++ i) { 36 | Asset* asset = &assets[i]; 37 | 38 | char assetPath[2048]; 39 | strcpy(assetPath, GetConfigPath()); 40 | strcat(assetPath, asset->fileName); 41 | 42 | FILE* file = fopen(assetPath, "wb"); 43 | 44 | if (file == NULL) { 45 | FATAL("Failed to open file '%s': %s\n", assetPath, strerror(errno)); 46 | } 47 | 48 | fwrite(asset->contents, 1, asset->size, file); 49 | fclose(file); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /source/assets.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_ASSETS_H 2 | #define YTERM_ASSETS_H 3 | 4 | void WriteAssets(void); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /source/clipboard.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_CLIPBOARD_H 2 | #define YTERM_CLIPBOARD_H 3 | 4 | 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /source/components.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_COMPONENTS_H 2 | #define YTERM_COMPONENTS_H 3 | 4 | #define _XOPEN_SOURCE 600 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | typedef unsigned char uchar; 26 | typedef SDL_Color SDL_Colour; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /source/config.c: -------------------------------------------------------------------------------- 1 | #include "assets.h" 2 | #include "config.h" 3 | 4 | static const char* defaultConfig[] = { 5 | "{", 6 | "\t\"theme\": \"default\",", 7 | "\t\"font\": \"basil_8x8\"", 8 | "}", 9 | }; 10 | 11 | static const char* defaultTheme[] = { 12 | "{", 13 | "\t\"black\": \"212830\",", 14 | "\t\"red\": \"C54133\",", 15 | "\t\"green\": \"27AE60\",", 16 | "\t\"yellow\": \"EDB20A\",", 17 | "\t\"blue\": \"2479D0\",", 18 | "\t\"magenta\": \"7D3EA0\",", 19 | "\t\"cyan\": \"1D8579\",", 20 | "\t\"white\": \"C9CCCD\",", 21 | "\t\"grey\": \"2F3943\",", 22 | "\t\"bright_red\": \"E74C3C\",", 23 | "\t\"bright_green\": \"2ECC71\",", 24 | "\t\"bright_yellow\": \"F1C40F\",", 25 | "\t\"bright_blue\": \"3498DB\",", 26 | "\t\"bright_magenta\": \"9B59B6\",", 27 | "\t\"bright_cyan\": \"2AA198\",", 28 | "\t\"bright_white\": \"ECF0F1\",", 29 | "\t\"fg\": \"C9CCCD\",", 30 | "\t\"bg\": \"161C24\"", 31 | "}", 32 | }; 33 | 34 | static void WriteStringArrayToFile(const char **array, size_t size, const char *path) { 35 | FILE *file = fopen(path, "w"); 36 | if (file == NULL) { 37 | FATAL("Could not write file \"%s\"", path); 38 | } 39 | 40 | for (size_t i = 0; i < size; ++ i) { 41 | fprintf(file, "%s\n", array[i]); 42 | } 43 | 44 | fclose(file); 45 | } 46 | 47 | static json_t* ParseJson(const char* path) { 48 | size_t row, col; 49 | json_t* json = json_from_file(path, &row, &col); 50 | if (json == NULL) { 51 | if (noch_get_err() == NOCH_ERR_FOPEN) { 52 | FATAL("Could not open file \"%s\"", path); 53 | } 54 | else if (noch_get_err() == NOCH_ERR_PARSER) { 55 | FATAL("%s:%zu:%zu: %s", path, row, col, noch_get_err_msg()); 56 | } 57 | else { 58 | FATAL("%s: %s", path, noch_get_err_msg()); 59 | } 60 | } 61 | 62 | return json; 63 | } 64 | 65 | static const char* JsonGetString(json_t* json, const char* key, const char* path) { 66 | json_t* strJson = json_obj_at(json, key); 67 | if (strJson == NULL) { 68 | FATAL("%s: Key \"%s\" is missing", path, key); 69 | } 70 | else if (strJson->type != JSON_STR) { 71 | FATAL("%s: Key \"%s\" expected to be a string", path, key); 72 | } 73 | 74 | return strJson->as.str.buf; 75 | } 76 | 77 | static const char *colourToString[] = { 78 | [COLOUR_BRIGHT_WHITE] = "bright_white", 79 | [COLOUR_BRIGHT_CYAN] = "bright_cyan", 80 | [COLOUR_BRIGHT_MAGENTA] = "bright_magenta", 81 | [COLOUR_BRIGHT_BLUE] = "bright_blue", 82 | [COLOUR_BRIGHT_YELLOW] = "bright_yellow", 83 | [COLOUR_BRIGHT_GREEN] = "bright_green", 84 | [COLOUR_BRIGHT_RED] = "bright_red", 85 | [COLOUR_GREY] = "grey", 86 | [COLOUR_WHITE] = "white", 87 | [COLOUR_CYAN] = "cyan", 88 | [COLOUR_MAGENTA] = "magenta", 89 | [COLOUR_BLUE] = "blue", 90 | [COLOUR_YELLOW] = "yellow", 91 | [COLOUR_GREEN] = "green", 92 | [COLOUR_RED] = "red", 93 | [COLOUR_BLACK] = "black", 94 | }; 95 | 96 | const char* GetConfigPath(void) { 97 | static char path[2048]; 98 | 99 | const char* home = getenv("HOME"); 100 | if (home == NULL) { 101 | FATAL("Failed to get the HOME environment variable"); 102 | } 103 | 104 | strcpy(path, home); 105 | const char* temp = CONFIG_FOLDER; 106 | strcat(path, temp); 107 | 108 | return path; 109 | } 110 | 111 | static void CreateConfig(void) { 112 | const char* path = GetConfigPath(); 113 | 114 | if (access(path, F_OK) != 0) { 115 | if (mkdir(path, 0777) != 0) { 116 | FATAL("Failed to create config folder \"%s\"", path); 117 | } 118 | } 119 | 120 | char configPath[2048]; 121 | strcpy(configPath, path); 122 | strcat(configPath, CONFIG_FILE); 123 | 124 | if (access(configPath, F_OK) != 0) { 125 | WriteStringArrayToFile(defaultConfig, ARRAY_LEN(defaultConfig), configPath); 126 | } 127 | 128 | char defaultThemePath[2048]; 129 | strcpy(defaultThemePath, path); 130 | strcat(defaultThemePath, THEME_FOLDER); 131 | 132 | if (access(defaultThemePath, F_OK) != 0) { 133 | if (mkdir(defaultThemePath, 0777) != 0) { 134 | FATAL("Could not create directory \"%s\"", defaultThemePath); 135 | } 136 | } 137 | 138 | strcat(defaultThemePath, "default.json"); 139 | 140 | if (access(defaultThemePath, F_OK) != 0) { 141 | WriteStringArrayToFile( 142 | defaultTheme, ARRAY_LEN(defaultTheme), defaultThemePath 143 | ); 144 | } 145 | 146 | char fontsPath[2048]; 147 | strcpy(fontsPath, path); 148 | strcat(fontsPath, FONTS_FOLDER); 149 | 150 | if (access(fontsPath, F_OK) != 0) { 151 | if (mkdir(fontsPath, 0777) != 0) { 152 | FATAL("Could not create directory \"%s\"", fontsPath); 153 | } 154 | } 155 | 156 | WriteAssets(); 157 | } 158 | 159 | void LoadConfig(ColourScheme* colourScheme, Video* video) { 160 | assert(colourScheme != NULL); 161 | 162 | CreateConfig(); 163 | const char* path = GetConfigPath(); 164 | 165 | char configPath[2048]; 166 | strcpy(configPath, path); 167 | strcat(configPath, CONFIG_FILE); 168 | 169 | json_t* json = ParseJson(configPath); 170 | const char* themeName = JsonGetString(json, "theme", configPath); 171 | 172 | // THEME 173 | char themePath[2048]; 174 | strcpy(themePath, path); 175 | strcat(themePath, THEME_FOLDER); 176 | strcat(themePath, themeName); 177 | strcat(themePath, ".json"); 178 | 179 | json_t* themeJson = ParseJson(themePath); 180 | 181 | #define LOAD_COLOUR(KEY, PTR) \ 182 | do { \ 183 | if (!HexToColour(JsonGetString( \ 184 | themeJson, KEY, themePath), PTR \ 185 | )) { \ 186 | FATAL("%s: Invalid hex provided for colour \"%s\"", themePath, KEY); \ 187 | } \ 188 | } while (0) 189 | 190 | for (size_t i = 0; i < ARRAY_LEN(colourToString); ++ i) { 191 | LOAD_COLOUR(colourToString[i], colourScheme->colour16 + i); 192 | } 193 | 194 | LOAD_COLOUR("fg", &colourScheme->fg); 195 | LOAD_COLOUR("bg", &colourScheme->bg); 196 | 197 | #undef LOAD_COLOUR 198 | 199 | // FONT 200 | char fontPath[2048]; 201 | strcpy(fontPath, path); 202 | strcat(fontPath, FONTS_FOLDER); 203 | strcat(fontPath, JsonGetString(json, "font", configPath)); 204 | strcat(fontPath, ".bmp"); 205 | 206 | Video_FreeFont(video); 207 | Video_OpenFont(video, fontPath); 208 | 209 | json_destroy(themeJson); 210 | json_destroy(json); 211 | } 212 | -------------------------------------------------------------------------------- /source/config.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_CONFIG_H 2 | #define YTERM_CONFIG_H 3 | 4 | #include "sdl.h" 5 | #include "util.h" 6 | #include "components.h" 7 | #include "textScreen.h" 8 | 9 | #define CONFIG_FOLDER "/.config/yterm/" 10 | #define CONFIG_FILE "config.json" 11 | #define THEME_FOLDER "themes/" 12 | #define FONTS_FOLDER "fonts/" 13 | 14 | const char* GetConfigPath(void); 15 | void LoadConfig(ColourScheme* colourScheme, Video* video); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /source/constants.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_CONSTANTS_H 2 | #define YTERM_CONSTANTS_H 3 | 4 | #define APP_NAME "yterm" 5 | #define APP_VERSION "dev version" 6 | #define APP_AUTHOR "yeti0904" 7 | #define APP_DESC "The terminal emulator of the 27th century" 8 | #define TERM "xterm-16color" 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /source/escapes.c: -------------------------------------------------------------------------------- 1 | #include "safe.h" 2 | #include "util.h" 3 | #include "escapes.h" 4 | 5 | static bool NextByte(Terminal* terminal, char* byte) { 6 | fd_set readable; 7 | char in; 8 | 9 | FD_ZERO(&readable); 10 | FD_SET(terminal->pty.master, &readable); 11 | 12 | struct timeval selectTimeout; 13 | selectTimeout.tv_sec = 1; 14 | selectTimeout.tv_usec = 0; 15 | 16 | if ( 17 | select( 18 | terminal->pty.master + 1, &readable, NULL, NULL, &selectTimeout 19 | ) == -1 20 | ) { 21 | perror("select"); 22 | exit(1); 23 | } 24 | 25 | if (!FD_ISSET(terminal->pty.master, &readable)) { 26 | return false; 27 | } 28 | 29 | if (read(terminal->pty.master, &in, 1) <= 0) { 30 | return false; 31 | } 32 | 33 | *byte = in; 34 | return true; 35 | } 36 | 37 | static uint8_t FGColour(int colour) { 38 | if (colour >= 30 && colour <= 37) { 39 | return colour - 30; 40 | } 41 | else if (colour >= 90 && colour <= 97) { 42 | return colour - 90 + COLOUR_GREY; 43 | } 44 | else { 45 | return (uint8_t) -1; 46 | } 47 | } 48 | 49 | static uint8_t BGColour(int colour) { 50 | if (colour < 40) { 51 | return (uint8_t) -1; 52 | } 53 | 54 | return FGColour(colour - 10); 55 | } 56 | 57 | #define NEXT_BYTE() if (!NextByte(terminal, &in)) {return;} 58 | 59 | static void RunCommand(Terminal* terminal, char cmd, int* args, size_t argsCount) { 60 | // TODO: error checking 61 | switch (cmd) { 62 | // cursor commands 63 | case 'H': 64 | case 'f': { // move cursor home/move cursor to 65 | if (argsCount == 0) { 66 | terminal->buffer.cursor = (Vec2) {0, 0}; 67 | break; 68 | } 69 | 70 | terminal->buffer.cursor.x = args[1] - 1; 71 | terminal->buffer.cursor.y = args[0] - 1; 72 | break; 73 | } 74 | case 'A': { // move cursor up 75 | terminal->buffer.cursor.y -= args[0]; 76 | 77 | if (terminal->buffer.cursor.y < 0) { 78 | terminal->buffer.cursor.y = 0; 79 | } 80 | break; 81 | } 82 | case 'B': { // move cursor down 83 | terminal->buffer.cursor.y += args[0]; 84 | 85 | if (terminal->buffer.cursor.y >= terminal->buffer.size.y) { 86 | int difference = 87 | terminal->buffer.cursor.y - terminal->buffer.size.y; 88 | 89 | TextScreen_ScrollDown(&terminal->buffer, difference + 1); 90 | } 91 | break; 92 | } 93 | case 'C': { // move cursor right 94 | terminal->buffer.cursor.x += args[0]; 95 | 96 | if (terminal->buffer.cursor.x >= terminal->buffer.size.x) { 97 | terminal->buffer.cursor.x = terminal->buffer.size.x - 1; 98 | } 99 | break; 100 | } 101 | case 'D': { // move cursor left 102 | terminal->buffer.cursor.x -= args[0]; 103 | 104 | if (terminal->buffer.cursor.x < 0) { 105 | terminal->buffer.cursor.x = 0; 106 | } 107 | break; 108 | } 109 | // erase commands 110 | case 'J': { 111 | Cell emptyCell = CellByCharacter(' '); 112 | 113 | if ((argsCount == 0) || (args[0] == 0)) { // until end of line 114 | for ( 115 | int i = terminal->buffer.cursor.x; 116 | i < terminal->buffer.size.x; ++ i 117 | ) { 118 | TextScreen_SetCharacter( 119 | &terminal->buffer, i, terminal->buffer.cursor.y, 120 | emptyCell 121 | ); 122 | } 123 | } 124 | else if (args[0] == 2) { // full screen 125 | size_t size = terminal->buffer.size.x * terminal->buffer.size.y; 126 | 127 | for (size_t i = 0; i < size; ++ i) { 128 | terminal->buffer.cells[i] = emptyCell; 129 | } 130 | 131 | terminal->buffer.cursor = (Vec2) {0, 0}; 132 | } 133 | break; 134 | } 135 | // colours/graphics commands 136 | case 'm': { 137 | for (size_t i = 0; i < argsCount; ++ i) { 138 | switch (args[i]) { 139 | case 0: { // reset all modes 140 | terminal->buffer.attr = NewAttr(0, 0, ATTR_NONE); 141 | break; 142 | } 143 | case 1: { // set bold 144 | terminal->buffer.attr.attr |= ATTR_BOLD; 145 | break; 146 | } 147 | case 22: { // reset bold and dim 148 | terminal->buffer.attr.attr &= ~ATTR_BOLD; 149 | terminal->buffer.attr.attr &= ~ATTR_DIM; 150 | break; 151 | } 152 | case 2: { // set dim 153 | terminal->buffer.attr.attr |= ATTR_DIM; 154 | break; 155 | } 156 | case 3: { // set italic 157 | terminal->buffer.attr.attr |= ATTR_ITALIC; 158 | break; 159 | } 160 | case 23: { // reset italic 161 | terminal->buffer.attr.attr &= ~ATTR_ITALIC; 162 | break; 163 | } 164 | case 4: { // set underline 165 | terminal->buffer.attr.attr |= ATTR_UNDERLINE; 166 | break; 167 | } 168 | case 24: { // reset underline 169 | terminal->buffer.attr.attr &= ~ATTR_UNDERLINE; 170 | break; 171 | } 172 | case 5: { // set blinking 173 | terminal->buffer.attr.attr |= ATTR_BLINKING; 174 | break; 175 | } 176 | case 25: { // reset blinking 177 | terminal->buffer.attr.attr &= ~ATTR_BLINKING; 178 | break; 179 | } 180 | case 7: { // set reverse 181 | terminal->buffer.attr.attr |= ATTR_REVERSE; 182 | break; 183 | } 184 | case 27: { // reset reverse 185 | terminal->buffer.attr.attr &= ~ATTR_REVERSE; 186 | break; 187 | } 188 | case 8: { // set hidden 189 | terminal->buffer.attr.attr |= ATTR_HIDDEN; 190 | break; 191 | } 192 | case 28: { // reset hidden 193 | terminal->buffer.attr.attr &= ~ATTR_HIDDEN; 194 | break; 195 | } 196 | case 9: { // set strikethrough 197 | terminal->buffer.attr.attr |= ATTR_STRIKE; 198 | break; 199 | } 200 | case 29: { // reset strikethrough 201 | terminal->buffer.attr.attr &= ~ATTR_STRIKE; 202 | break; 203 | } 204 | case 39: { // reset fg 205 | terminal->buffer.attr.attr &= ~ATTR_COLOUR_FG; 206 | break; 207 | } 208 | case 49: { // reset bg 209 | terminal->buffer.attr.attr &= ~ATTR_COLOUR_BG; 210 | break; 211 | } 212 | default: { 213 | uint8_t colour; 214 | if ((colour = FGColour(args[i])) != (uint8_t) -1) { 215 | terminal->buffer.attr.attr |= ATTR_COLOUR_FG; 216 | terminal->buffer.attr.fg = colour; 217 | } 218 | else if ((colour = BGColour(args[i])) != (uint8_t) -1) { 219 | terminal->buffer.attr.attr |= ATTR_COLOUR_BG; 220 | terminal->buffer.attr.bg = colour; 221 | } 222 | } 223 | } 224 | } 225 | break; 226 | } 227 | default: { 228 | printf("Running command %c: ", cmd); 229 | 230 | for (size_t i = 0; i < argsCount; ++ i) { 231 | printf("%d ", args[i]); 232 | } 233 | putchar('\n'); 234 | } 235 | } 236 | } 237 | 238 | 239 | void HandleEscape(Terminal* terminal) { 240 | char in; 241 | char* readStr; 242 | 243 | NEXT_BYTE(); 244 | 245 | if (in == '[') { // CSI 246 | NEXT_BYTE(); 247 | 248 | if (isdigit(in)) { // command 249 | // TODO: maybe i should make this not a static array 250 | // TODO: lol this is an exploit 251 | int args[256]; 252 | size_t argsCount = 0; 253 | 254 | readStr = SafeMalloc(1); 255 | readStr[0] = 0; 256 | 257 | bool readingArgs = true; 258 | 259 | while (readingArgs) { 260 | readStr = SafeRealloc(readStr, strlen(readStr) + 2); 261 | strncat(readStr, &in, 1); 262 | NEXT_BYTE(); 263 | 264 | if (!isdigit(in)) { 265 | args[argsCount] = atoi(readStr); 266 | ++ argsCount; 267 | free(readStr); 268 | readStr = SafeMalloc(1); 269 | readStr[0] = 0; 270 | 271 | if (in != ';') { 272 | readingArgs = false; 273 | RunCommand(terminal, in, args, argsCount); 274 | } 275 | else { 276 | NEXT_BYTE(); 277 | } 278 | } 279 | } 280 | 281 | free(readStr); 282 | } 283 | else if (in == '?') { 284 | readStr = SafeMalloc(1); 285 | readStr[0] = 0; 286 | 287 | NEXT_BYTE(); 288 | while ((in != 'h') && (in != 'l')) { 289 | readStr = SafeRealloc(readStr, strlen(readStr) + 2); 290 | strncat(readStr, &in, 1); 291 | 292 | NEXT_BYTE(); 293 | } 294 | 295 | int option = atoi(readStr); 296 | free(readStr); 297 | 298 | switch (option) { 299 | case 25: { 300 | terminal->config->cursorEnabled = in == 'h'; 301 | break; 302 | } 303 | default: { 304 | printf("Set option %d to %c\n", option, in); 305 | } 306 | } 307 | } 308 | } 309 | else if (in == ']') { 310 | NEXT_BYTE(); 311 | if (in != '0') { 312 | return; 313 | } 314 | 315 | NEXT_BYTE(); 316 | if (in != ';') { 317 | return; 318 | } 319 | 320 | readStr = SafeMalloc(1); 321 | *readStr = 0; 322 | 323 | NEXT_BYTE(); 324 | 325 | while (in != 7) { 326 | readStr = SafeRealloc(readStr, strlen(readStr) + 2); 327 | strncat(readStr, &in, 1); 328 | 329 | NEXT_BYTE(); 330 | } 331 | 332 | free(terminal->title); 333 | terminal->title = readStr; 334 | 335 | printf("Title set to %s\n", readStr); 336 | } 337 | } 338 | 339 | #undef NEXT_BYTE 340 | -------------------------------------------------------------------------------- /source/escapes.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_ESCAPES_H 2 | #define YTERM_ESCAPES_H 3 | 4 | #include "terminal.h" 5 | #include "components.h" 6 | 7 | void HandleEscape(Terminal* terminal); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /source/input.c: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | #include "input.h" 3 | 4 | bool ShiftPressed(const bool* keys) { 5 | return (keys[SDL_SCANCODE_LSHIFT] || keys[SDL_SCANCODE_RSHIFT])? true : false; 6 | } 7 | 8 | static char* KeyToSequence(SDL_Scancode key) { 9 | const bool* keys = SDL_GetKeyboardState(NULL); 10 | 11 | if (keys[SDL_SCANCODE_LCTRL] && ShiftPressed(keys)) { 12 | switch (key) { 13 | case SDL_SCANCODE_BACKSPACE: return DupString("\x08"); 14 | case SDL_SCANCODE_RETURN: return DupString("\n"); 15 | case SDL_SCANCODE_F1: return DupString("\x1b[1;6P"); 16 | case SDL_SCANCODE_F2: return DupString("\x1b[1;6Q"); 17 | case SDL_SCANCODE_F3: return DupString("\x1bOR"); 18 | case SDL_SCANCODE_F4: return DupString("\x1b[1;6S"); 19 | case SDL_SCANCODE_F5: return DupString("\x1b[15;6~"); 20 | case SDL_SCANCODE_F6: return DupString("\x1b[17;6~"); 21 | case SDL_SCANCODE_F7: return DupString("\x1b[18;6~"); 22 | case SDL_SCANCODE_F8: return DupString("\x1b[19;6~"); 23 | case SDL_SCANCODE_F9: return DupString("\x1b[20;6~"); 24 | case SDL_SCANCODE_F10: return DupString("\x1b[21;6~"); 25 | case SDL_SCANCODE_F11: return DupString("\x1b[23;6~"); 26 | case SDL_SCANCODE_F12: return DupString("\x1b[24;6~"); 27 | case SDL_SCANCODE_UP: return DupString("\x1b[1;6A"); 28 | case SDL_SCANCODE_DOWN: return DupString("\x1b[1;6B"); 29 | case SDL_SCANCODE_RIGHT: return DupString("\x1b[1;6C"); 30 | case SDL_SCANCODE_LEFT: return DupString("\x1b[1;6D"); 31 | default: return NULL; 32 | } 33 | } 34 | else if (keys[SDL_SCANCODE_LCTRL]) { 35 | switch (key) { 36 | case SDL_SCANCODE_BACKSPACE: return DupString("\x7F"); 37 | case SDL_SCANCODE_RETURN: return DupString("\n"); 38 | case SDL_SCANCODE_F1: return DupString(""); // lxterminal does nothing for these? 39 | case SDL_SCANCODE_F2: return DupString(""); 40 | case SDL_SCANCODE_F3: return DupString(""); 41 | case SDL_SCANCODE_F4: return DupString(""); 42 | case SDL_SCANCODE_F5: return DupString("\x1b[15;5~"); 43 | case SDL_SCANCODE_F6: return DupString("\x1b[17;5~"); 44 | case SDL_SCANCODE_F7: return DupString("\x1b[18;5~"); 45 | case SDL_SCANCODE_F8: return DupString("\x1b[19;5~"); 46 | case SDL_SCANCODE_F9: return DupString("\x1b[20;5~"); 47 | case SDL_SCANCODE_F10: return DupString("\x1b[21;5~"); 48 | case SDL_SCANCODE_F11: return DupString("\x1b[23;5~"); 49 | case SDL_SCANCODE_F12: return DupString("\x1b[24;5~"); 50 | case SDL_SCANCODE_A: return CharToString('A' & 0x1F); 51 | case SDL_SCANCODE_B: return CharToString('B' & 0x1F); 52 | case SDL_SCANCODE_C: return CharToString('C' & 0x1F); 53 | case SDL_SCANCODE_D: return CharToString('D' & 0x1F); 54 | case SDL_SCANCODE_E: return CharToString('E' & 0x1F); 55 | case SDL_SCANCODE_F: return CharToString('F' & 0x1F); 56 | case SDL_SCANCODE_G: return CharToString('G' & 0x1F); 57 | case SDL_SCANCODE_H: return CharToString('H' & 0x1F); 58 | case SDL_SCANCODE_I: return CharToString('I' & 0x1F); 59 | case SDL_SCANCODE_J: return CharToString('J' & 0x1F); 60 | case SDL_SCANCODE_K: return CharToString('K' & 0x1F); 61 | case SDL_SCANCODE_L: return CharToString('L' & 0x1F); 62 | case SDL_SCANCODE_M: return CharToString('M' & 0x1F); 63 | case SDL_SCANCODE_N: return CharToString('N' & 0x1F); 64 | case SDL_SCANCODE_O: return CharToString('O' & 0x1F); 65 | case SDL_SCANCODE_P: return CharToString('P' & 0x1F); 66 | case SDL_SCANCODE_Q: return CharToString('Q' & 0x1F); 67 | case SDL_SCANCODE_R: return CharToString('R' & 0x1F); 68 | case SDL_SCANCODE_S: return CharToString('S' & 0x1F); 69 | case SDL_SCANCODE_T: return CharToString('T' & 0x1F); 70 | case SDL_SCANCODE_U: return CharToString('U' & 0x1F); 71 | case SDL_SCANCODE_V: return CharToString('V' & 0x1F); 72 | case SDL_SCANCODE_W: return CharToString('W' & 0x1F); 73 | case SDL_SCANCODE_X: return CharToString('X' & 0x1F); 74 | case SDL_SCANCODE_Y: return CharToString('Y' & 0x1F); 75 | case SDL_SCANCODE_Z: return CharToString('Z' & 0x1F); 76 | case SDL_SCANCODE_UP: return DupString("\x1b[1;5A"); 77 | case SDL_SCANCODE_DOWN: return DupString("\x1b[1;5B"); 78 | case SDL_SCANCODE_RIGHT: return DupString("\x1b[1;5C"); 79 | case SDL_SCANCODE_LEFT: return DupString("\x1b[1;5D"); 80 | default: return NULL; 81 | } 82 | } 83 | else if (ShiftPressed(keys)) { 84 | switch (key) { 85 | case SDL_SCANCODE_BACKSPACE: return DupString("\x7F"); 86 | case SDL_SCANCODE_RETURN: return DupString("\n"); 87 | case SDL_SCANCODE_F1: return DupString("\x1b[1;2P"); 88 | case SDL_SCANCODE_F2: return DupString("\x1b[1;2Q"); 89 | case SDL_SCANCODE_F3: return DupString("\x1b[1;2R"); 90 | case SDL_SCANCODE_F4: return DupString("\x1b[1;2S"); 91 | case SDL_SCANCODE_F5: return DupString("\x1b[15;2~"); 92 | case SDL_SCANCODE_F6: return DupString("\x1b[17;2~"); 93 | case SDL_SCANCODE_F7: return DupString("\x1b[18;2~"); 94 | case SDL_SCANCODE_F8: return DupString("\x1b[19;2~"); 95 | case SDL_SCANCODE_F9: return DupString("\x1b[20;2~"); 96 | case SDL_SCANCODE_F10: return DupString("\x1b[21;2~"); 97 | case SDL_SCANCODE_F11: return DupString("\x1b[23;2~"); 98 | case SDL_SCANCODE_F12: return DupString("\x1b[24;2~"); 99 | case SDL_SCANCODE_UP: return DupString("\x1b[1;2A"); 100 | case SDL_SCANCODE_DOWN: return DupString("\x1b[1;2B"); 101 | case SDL_SCANCODE_RIGHT: return DupString("\x1b[1;2C"); 102 | case SDL_SCANCODE_LEFT: return DupString("\x1b[1;2D"); 103 | default: return NULL; 104 | } 105 | } 106 | else { 107 | switch (key) { 108 | case SDL_SCANCODE_BACKSPACE: return DupString("\x08"); 109 | case SDL_SCANCODE_RETURN: return DupString("\n"); 110 | case SDL_SCANCODE_F1: return DupString("\x1bOP"); 111 | case SDL_SCANCODE_F2: return DupString("\x1bOQ"); 112 | case SDL_SCANCODE_F3: return DupString("\x1bOR"); 113 | case SDL_SCANCODE_F4: return DupString("\x1bOS"); 114 | case SDL_SCANCODE_F5: return DupString("\x1b[15~"); 115 | case SDL_SCANCODE_F6: return DupString("\x1b[17~"); 116 | case SDL_SCANCODE_F7: return DupString("\x1b[18~"); 117 | case SDL_SCANCODE_F8: return DupString("\x1b[19~"); 118 | case SDL_SCANCODE_F9: return DupString("\x1b[20~"); 119 | case SDL_SCANCODE_F10: return DupString("\x1b[21~"); 120 | case SDL_SCANCODE_F11: return DupString("\x1b[23~"); 121 | case SDL_SCANCODE_F12: return DupString("\x1b[24~"); 122 | case SDL_SCANCODE_UP: return DupString("\x1b[A"); 123 | case SDL_SCANCODE_DOWN: return DupString("\x1b[B"); 124 | case SDL_SCANCODE_RIGHT: return DupString("\x1b[C"); 125 | case SDL_SCANCODE_LEFT: return DupString("\x1b[D"); 126 | default: return NULL; 127 | } 128 | } 129 | } 130 | 131 | void HandleInputEvent(SDL_Event* e, Terminal* terminal) { 132 | switch (e->type) { 133 | case SDL_EVENT_TEXT_INPUT: { 134 | write(terminal->pty.master, e->text.text, strlen(e->text.text)); 135 | break; 136 | } 137 | case SDL_EVENT_KEY_DOWN: { 138 | char* sequence = KeyToSequence(e->key.scancode); 139 | 140 | if (sequence == NULL) { 141 | break; 142 | } 143 | 144 | write(terminal->pty.master, sequence, strlen(sequence)); 145 | free(sequence); 146 | break; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /source/input.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_INPUT_H 2 | #define YTERM_INPUT_H 3 | 4 | #include "terminal.h" 5 | #include "components.h" 6 | 7 | bool ShiftPressed(const bool* keys); 8 | void HandleInputEvent(SDL_Event* e, Terminal* terminal); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /source/main.c: -------------------------------------------------------------------------------- 1 | #include "app.h" 2 | #include "sdl.h" 3 | #include "safe.h" 4 | #include "util.h" 5 | #include "input.h" 6 | #include "terminal.h" 7 | #include "constants.h" 8 | #include "components.h" 9 | #include "config.h" 10 | 11 | const char* disclaimer[] = { 12 | "Copyright (c) 2023 yeti0904", 13 | "", 14 | "Permission is hereby granted, free of charge, to any person obtaining a copy", 15 | "of this software and associated documentation files (the \"Software\"), to deal", 16 | "in the Software without restriction, including without limitation the rights", 17 | "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", 18 | "copies of the Software, and to permit persons to whom the Software is", 19 | "furnished to do so, subject to the following conditions:", 20 | "", 21 | "The above copyright notice and this permission notice shall be included in all", 22 | "copies or substantial portions of the Software." 23 | }; 24 | 25 | const char* usages[] = { 26 | "[OPTIONS]", 27 | }; 28 | 29 | const char* execPath; 30 | 31 | static void Usage(FILE* file) { 32 | args_usage_fprint(file, execPath, usages, ARRAY_LEN(usages), APP_DESC, true); 33 | } 34 | 35 | static void ParseFlags(args_t* args, TerminalConfig* config) { 36 | bool flagHelp = false; 37 | bool flagVersion = false; 38 | bool flagNoEscape = false; 39 | 40 | flag_bool("h", "help", "Show the usage", &flagHelp); 41 | flag_bool("v", "version", "Show the version", &flagVersion); 42 | flag_bool( 43 | "e", "no-escape", "Disables interpreting escape sequences", &flagNoEscape 44 | ); 45 | 46 | size_t where; 47 | bool extra; 48 | if (args_parse_flags(args, &where, NULL, &extra) != 0) { 49 | FATAL("Error: '%s': %s\n", args->v[where], noch_get_err_msg()); 50 | } 51 | else if (extra) { 52 | FATAL("Error: '%s': Unexpected argument\n", args->v[where]); 53 | } 54 | 55 | if (flagHelp) { 56 | Usage(stdout); 57 | exit(0); 58 | } 59 | 60 | if (flagVersion) { 61 | printf("%s %s by %s\n", APP_NAME, APP_VERSION, APP_AUTHOR); 62 | 63 | for (size_t i = 0; i < ARRAY_LEN(disclaimer); ++ i) { 64 | puts(disclaimer[i]); 65 | } 66 | 67 | exit(0); 68 | } 69 | 70 | if (flagNoEscape) { 71 | config->interpretEscapes = false; 72 | } 73 | } 74 | 75 | int main(int argc, const char** argv, char** env) { 76 | App app; 77 | App_Init(&app, env); 78 | 79 | args_t args = args_new(argc, argv); 80 | execPath = args_shift(&args); 81 | ParseFlags(&args, &app.config); 82 | 83 | while (app.running) { 84 | App_Update(&app); 85 | } 86 | 87 | App_Free(&app); 88 | } 89 | -------------------------------------------------------------------------------- /source/noch.c: -------------------------------------------------------------------------------- 1 | #include "safe.h" 2 | 3 | #define NOCH_ALLOC(SIZE) SafeMalloc (SIZE) 4 | #define NOCH_REALLOC(PTR, SIZE) SafeRealloc(PTR, SIZE) 5 | #define NOCH_FREE(PTR) free (PTR) 6 | 7 | #include 8 | #include 9 | -------------------------------------------------------------------------------- /source/safe.c: -------------------------------------------------------------------------------- 1 | #include "components.h" 2 | #include "safe.h" 3 | #include "util.h" 4 | 5 | void* SafeMalloc(size_t size) { 6 | void* ret = malloc(size); 7 | 8 | if (ret == NULL) { 9 | FATAL("Malloc failed"); 10 | } 11 | 12 | return ret; 13 | } 14 | 15 | void* SafeCalloc(size_t size, size_t memberSize) { 16 | void* ret = calloc(size, memberSize); 17 | 18 | if (ret == NULL) { 19 | FATAL("Calloc failed"); 20 | } 21 | 22 | return ret; 23 | } 24 | 25 | void* SafeRealloc(void* ptr, size_t size) { 26 | void* ret = realloc(ptr, size); 27 | 28 | if (ret == NULL) { 29 | FATAL("Realloc failed"); 30 | } 31 | 32 | return ret; 33 | } 34 | -------------------------------------------------------------------------------- /source/safe.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_SAFE_H 2 | #define YTERM_SAFE_H 3 | 4 | #include 5 | 6 | void* SafeMalloc(size_t size); 7 | void* SafeCalloc(size_t size, size_t memberSize); 8 | void* SafeRealloc(void* ptr, size_t size); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /source/sdl.c: -------------------------------------------------------------------------------- 1 | #include "sdl.h" 2 | #include "util.h" 3 | #include "constants.h" 4 | 5 | Video Video_Init(void) { 6 | Video ret; 7 | 8 | // initialise SDL2 9 | if (!SDL_Init(SDL_INIT_VIDEO)) { 10 | FATAL("Failed to initialise SDL2"); 11 | } 12 | 13 | ret.windowSize = (Vec2) {720, 400}; 14 | 15 | ret.window = SDL_CreateWindow( 16 | APP_NAME, ret.windowSize.x, ret.windowSize.y, SDL_WINDOW_RESIZABLE 17 | ); 18 | 19 | if (ret.window == NULL) { 20 | FATAL("Failed to create window"); 21 | } 22 | 23 | ret.renderer = SDL_CreateRenderer(ret.window, NULL); 24 | 25 | if (ret.renderer == NULL) { 26 | FATAL("Failed to create renderer"); 27 | } 28 | 29 | printf("Renderer: %s\n", SDL_GetRendererName(ret.renderer)); 30 | 31 | ret.font = NULL; 32 | 33 | return ret; 34 | } 35 | 36 | void Video_FreeFont(Video* video) { 37 | SDL_DestroyTexture(video->font); 38 | } 39 | 40 | void Video_Free(Video* video) { 41 | Video_FreeFont(video); 42 | 43 | SDL_DestroyWindow(video->window); 44 | SDL_DestroyRenderer(video->renderer); 45 | SDL_Quit(); 46 | } 47 | 48 | void Video_OpenFont(Video* video, char* path) { 49 | SDL_Surface* surface = SDL_LoadBMP(path); 50 | 51 | if (surface == NULL) { 52 | Error("Failed to open font '%s': %s", path, SDL_GetError()); 53 | } 54 | 55 | video->font = SDL_CreateTextureFromSurface(video->renderer, surface); 56 | 57 | if (video->font == NULL) { 58 | FATAL("Failed to open font"); // TODO: make a window for this and reset 59 | } 60 | 61 | video->charWidth = surface->w / 16; 62 | video->charHeight = surface->h / 16; 63 | 64 | printf("Loaded %dx%d font\n", video->charWidth, video->charHeight); 65 | 66 | SDL_DestroySurface(surface); 67 | 68 | SDL_SetWindowMinimumSize( 69 | video->window, 21 * video->charWidth, 3 * video->charHeight 70 | ); 71 | } 72 | 73 | void Video_DrawCharacter(Video* video, int x, int y, uchar ch, SDL_Colour colour) { 74 | if (ch == ' ') { 75 | return; 76 | } 77 | 78 | float fontWFloat; 79 | SDL_GetTextureSize(video->font, &fontWFloat, NULL); // TODO: is this slow? 80 | int fontW = (int) fontWFloat; 81 | 82 | SDL_SetTextureColorMod(video->font, colour.r, colour.g, colour.b); 83 | 84 | SDL_FRect src = { 85 | (float) ((((int) ch) % 16) * video->charWidth), 86 | (float) ((((int) ch) / 16) * video->charHeight), 87 | (float) video->charWidth, (float) video->charHeight 88 | }; 89 | SDL_FRect dest = { 90 | (float) x, (float) y, (float) video->charWidth, (float) video->charHeight 91 | }; 92 | 93 | SDL_RenderTexture(video->renderer, video->font, &src, &dest); 94 | } 95 | -------------------------------------------------------------------------------- /source/sdl.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_SDL_H 2 | #define YTERM_SDL_H 3 | 4 | #include "components.h" 5 | #include "types.h" 6 | 7 | typedef struct Video { 8 | SDL_Window* window; 9 | SDL_Renderer* renderer; 10 | SDL_Texture* font; 11 | int charWidth; 12 | int charHeight; 13 | Vec2 windowSize; 14 | } Video; 15 | 16 | Video Video_Init(void); 17 | void Video_Free(Video* video); 18 | void Video_OpenFont(Video* video, char* path); 19 | void Video_FreeFont(Video* video); 20 | void Video_DrawCharacter(Video* video, int x, int y, uchar ch, SDL_Color colour); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /source/terminal.c: -------------------------------------------------------------------------------- 1 | #include "safe.h" 2 | #include "util.h" 3 | #include "escapes.h" 4 | #include "terminal.h" 5 | #include "constants.h" 6 | #include "components.h" 7 | 8 | void Terminal_Init(Terminal* terminal, char** env, Vec2 size) { 9 | terminal->buffer = TextScreen_New(size.x, size.y); 10 | terminal->title = DupString(APP_NAME); 11 | 12 | PtPair(&terminal->pty); 13 | SetTerminalSize(terminal); 14 | Spawn(&terminal->pty, env); 15 | } 16 | 17 | void Terminal_Free(Terminal* terminal) { 18 | free(terminal->title); 19 | TextScreen_Free(&terminal->buffer); 20 | } 21 | 22 | void Terminal_Update(Terminal* terminal) { 23 | // check for stuff in stdout 24 | fd_set readable; 25 | unsigned char in; 26 | 27 | FD_ZERO(&readable); 28 | FD_SET(terminal->pty.master, &readable); 29 | 30 | struct timeval selectTimeout; 31 | selectTimeout.tv_sec = 0; 32 | selectTimeout.tv_usec = 1; 33 | 34 | if ( 35 | select( 36 | terminal->pty.master + 1, &readable, NULL, NULL, &selectTimeout 37 | ) == -1 38 | ) { 39 | perror("select"); 40 | exit(1); 41 | } 42 | 43 | size_t bytesRead = 0; 44 | 45 | while (FD_ISSET(terminal->pty.master, &readable)) { 46 | if (read(terminal->pty.master, &in, 1) <= 0) { 47 | /* This is not necessarily an error but also happens 48 | * when the child exits normally. */ 49 | fprintf(stderr, "Nothing to read from child: "); 50 | perror(NULL); 51 | exit(1); 52 | } 53 | 54 | ++ bytesRead; 55 | 56 | switch (in) { 57 | case '\x1b': { 58 | if (terminal->config->interpretEscapes) { 59 | HandleEscape(terminal); 60 | break; 61 | } 62 | } // fall through 63 | default: { 64 | TextScreen_PutCharacter(&terminal->buffer, in); 65 | } 66 | } 67 | 68 | if (bytesRead > 1024) { 69 | break; 70 | } 71 | 72 | if ( 73 | select( 74 | terminal->pty.master + 1, &readable, NULL, NULL, &selectTimeout 75 | ) == -1 76 | ) { 77 | perror("select"); 78 | exit(1); 79 | } 80 | } 81 | } 82 | 83 | // this code is most likely to survive my purge of eduterm's X11 code 84 | void SetTerminalSize(Terminal* terminal) { 85 | struct winsize ws = { 86 | .ws_col = terminal->buffer.size.x, 87 | .ws_row = terminal->buffer.size.y, 88 | }; 89 | 90 | /* This is the very same ioctl that normal programs use to query the 91 | * window size. Normal programs are actually able to do this, too, 92 | * but it makes little sense: Setting the size has no effect on the 93 | * PTY driver in the kernel (it just keeps a record of it) or the 94 | * terminal emulator. IIUC, all that's happening is that subsequent 95 | * ioctls will report the new size -- until another ioctl sets a new 96 | * size. 97 | * 98 | * I didn't see any response to ioctls of normal programs in any of 99 | * the popular terminals (XTerm, VTE, st). They are not informed by 100 | * the kernel when a normal program issues an ioctl like that. 101 | * 102 | * On the other hand, if we were to issue this ioctl during runtime 103 | * and the size actually changed, child programs would get a 104 | * SIGWINCH. */ 105 | if (ioctl(terminal->pty.master, TIOCSWINSZ, &ws) == -1) { 106 | perror("ioctl(TIOCSWINSZ)"); 107 | exit(1); 108 | } 109 | } 110 | 111 | void PtPair(Pty* pty) { 112 | char *slave_name; 113 | 114 | /* Opens the PTY master device. This is the file descriptor that 115 | * we're reading from and writing to in our terminal emulator. 116 | * 117 | * We're going for BSD-style management of the controlling terminal: 118 | * Don't try to change anything now (O_NOCTTY), we'll issue an 119 | * ioctl() later on. */ 120 | pty->master = posix_openpt(O_RDWR | O_NOCTTY); 121 | if (pty->master == -1) { 122 | perror("posix_openpt"); 123 | exit(1); 124 | } 125 | 126 | /* grantpt() and unlockpt() are housekeeping functions that have to 127 | * be called before we can open the slave FD. Refer to the manpages 128 | * on what they do. */ 129 | if (grantpt(pty->master) == -1) { 130 | perror("grantpt"); 131 | exit(1); 132 | } 133 | 134 | if (unlockpt(pty->master) == -1) { 135 | perror("grantpt"); 136 | exit(1); 137 | } 138 | 139 | /* Up until now, we only have the master FD. We also need a file 140 | * descriptor for our child process. We get it by asking for the 141 | * actual path in /dev/pts which we then open using a regular 142 | * open(). So, unlike pipe(), you don't get two corresponding file 143 | * descriptors in one go. */ 144 | 145 | slave_name = ptsname(pty->master); 146 | if (slave_name == NULL) { 147 | perror("ptsname"); 148 | exit(1); 149 | } 150 | 151 | pty->slave = open(slave_name, O_RDWR | O_NOCTTY); 152 | if (pty->slave == -1) { 153 | perror("open(slave_name)"); 154 | exit(1); 155 | } 156 | } 157 | 158 | void Spawn(Pty* pty, char** env) { 159 | pid_t p; 160 | //char* env[] = {"TERM=xterm-16color", NULL}; 161 | 162 | size_t envSize = 0; 163 | for (size_t i = 0; env[i] != NULL; ++ i) { 164 | ++ envSize; 165 | } 166 | char** envArray = (char**) SafeMalloc((envSize + 1) * sizeof(char*)); 167 | 168 | for (size_t i = 0; i <= envSize; ++ i) { 169 | envArray[i] = env[i]; 170 | } 171 | 172 | bool termSet = false; 173 | for (size_t i = 0; i < envSize; ++ i) { 174 | if (StringStartsWith(envArray[i], "TERM=")) { 175 | envArray[i] = "TERM=" TERM; 176 | termSet = true; 177 | } 178 | } 179 | 180 | if (!termSet) { 181 | envArray = (char**) SafeRealloc(envArray, (envSize + 2) * sizeof(char**)); 182 | 183 | envArray[envSize + 1] = NULL; 184 | envArray[envSize] = "TERM=" TERM; 185 | } 186 | 187 | p = fork(); 188 | if (p == 0) { 189 | close(pty->master); 190 | 191 | /* Create a new session and make our terminal this process' 192 | * controlling terminal. The shell that we'll spawn in a second 193 | * will inherit the status of session leader. */ 194 | setsid(); 195 | if (ioctl(pty->slave, TIOCSCTTY, NULL) == -1) { 196 | perror("ioctl(TIOCSCTTY)"); 197 | exit(1); 198 | } 199 | 200 | dup2(pty->slave, 0); 201 | dup2(pty->slave, 1); 202 | dup2(pty->slave, 2); 203 | close(pty->slave); 204 | 205 | char* command = GetUserShell(); 206 | 207 | //execle(SHELL, "-" SHELL, (char*) NULL, envArray); 208 | execle(command, command, (char*) NULL, envArray); 209 | free(envArray); 210 | exit(1); // TODO: make this close the tab or something 211 | } 212 | else if (p > 0) { 213 | free(envArray); 214 | close(pty->slave); 215 | return; 216 | } 217 | 218 | perror("fork"); 219 | exit(1); 220 | } 221 | -------------------------------------------------------------------------------- /source/terminal.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_TERMINAL_H 2 | #define YTERM_TERMINAL_H 3 | 4 | #include "components.h" 5 | #include "sdl.h" 6 | #include "textScreen.h" 7 | 8 | typedef struct Pty { // no clue what this could be 9 | int master, slave; 10 | } Pty; 11 | 12 | typedef struct TerminalConfig { 13 | bool interpretEscapes; 14 | bool cursorEnabled; 15 | } TerminalConfig; 16 | 17 | typedef struct Terminal { // basically a tab 18 | Pty pty; 19 | TextScreen buffer; 20 | TerminalConfig* config; 21 | char* title; 22 | } Terminal; 23 | 24 | // Terminal 25 | void Terminal_Init(Terminal* terminal, char** env, Vec2 size); 26 | void Terminal_Free(Terminal* terminal); 27 | void Terminal_Update(Terminal* terminal); 28 | 29 | // Pty 30 | void SetTerminalSize(Terminal* terminal); 31 | void PtPair(Pty* pty); 32 | void Spawn(Pty* pty, char** env); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /source/textScreen.c: -------------------------------------------------------------------------------- 1 | #include "safe.h" 2 | #include "util.h" 3 | #include "textScreen.h" 4 | 5 | Cell NewCell(char ch, uint8_t fg, uint8_t bg, uint16_t attr) { 6 | Cell ret; 7 | ret.ch = ch; 8 | ret.attr.fg = fg; 9 | ret.attr.bg = bg; 10 | ret.attr.attr = attr; 11 | return ret; 12 | } 13 | 14 | Cell CellByCharacter(char ch) { 15 | return NewCell(ch, 0, 0, ATTR_NONE); 16 | } 17 | 18 | AttrInfo NewAttr(uint8_t fg, uint8_t bg, uint16_t attr) { 19 | Cell cell = NewCell(0, fg, bg, attr); 20 | return cell.attr; 21 | } 22 | 23 | TextScreen TextScreen_New(uint32_t w, uint32_t h) { 24 | TextScreen ret; 25 | 26 | ret.cells = SafeMalloc(w * h * sizeof(Cell)); 27 | ret.size = (Vec2) {.x = w, .y = h}; 28 | ret.cursor = (Vec2) {.x = 0, .y = 0}; 29 | 30 | for (size_t i = 0; i < w * h; ++ i) { 31 | ret.cells[i] = CellByCharacter(' '); 32 | } 33 | 34 | ret.attr = NewAttr(0, 0, ATTR_NONE); 35 | 36 | return ret; 37 | } 38 | 39 | void TextScreen_Free(TextScreen* text) { 40 | free(text->cells); 41 | } 42 | 43 | Cell TextScreen_GetCharacter(TextScreen* text, int x, int y) { 44 | return text->cells[(y * text->size.x) + x]; 45 | } 46 | 47 | void TextScreen_SetCharacter(TextScreen* text, int x, int y, Cell cell) { 48 | if ( 49 | (x < 0) || 50 | (y < 0) || 51 | (x >= text->size.x) || 52 | (y >= text->size.y) 53 | ) { 54 | return; 55 | } 56 | 57 | text->cells[(y * text->size.x) + x] = cell; 58 | } 59 | 60 | void TextScreen_ScrollDown(TextScreen* text, int lines) { 61 | for (int i = 0; i < lines; ++ i) { 62 | size_t remainingLength = (text->size.x * text->size.y) - text->size.x; 63 | 64 | memmove( 65 | text->cells, text->cells + text->size.x, 66 | remainingLength * sizeof(Cell) 67 | ); 68 | 69 | for (int i = 0; i < text->size.x; ++ i) { 70 | text->cells[remainingLength + i] = CellByCharacter(' '); 71 | } 72 | 73 | -- text->cursor.y; 74 | } 75 | } 76 | 77 | void TextScreen_PutCharacter(TextScreen* text, char ch) { 78 | switch (ch) { 79 | case '\n': { 80 | ++ text->cursor.y; 81 | text->cursor.x = 0; 82 | 83 | if (text->cursor.y >= text->size.y) { 84 | TextScreen_ScrollDown(text, 1); 85 | } 86 | break; 87 | } 88 | case '\r': { 89 | text->cursor.x = 0; 90 | break; 91 | } 92 | case '\t': { 93 | int oldX = text->cursor.x; 94 | 95 | text->cursor.x += 4 - text->cursor.x % 4; 96 | 97 | for (int i = oldX; i <= text->cursor.x; ++ i) { 98 | TextScreen_SetCharacter( 99 | text, i, text->cursor.y, 100 | NewCell(' ', text->attr.fg, text->attr.bg, text->attr.attr) 101 | ); 102 | } 103 | break; 104 | } 105 | case 0x07: { // bell 106 | puts("Ding!"); 107 | break; 108 | } 109 | case 0x08: { // backspace 110 | -- text->cursor.x; 111 | 112 | if (text->cursor.x < 0) { 113 | text->cursor.x = 0; 114 | } 115 | 116 | TextScreen_SetCharacter( 117 | text, text->cursor.x, text->cursor.y, 118 | NewCell(' ', text->attr.fg, text->attr.bg, text->attr.attr) 119 | ); 120 | break; 121 | } 122 | default: { 123 | TextScreen_SetCharacter( 124 | text, text->cursor.x, text->cursor.y, 125 | NewCell(ch, text->attr.fg, text->attr.bg, text->attr.attr) 126 | ); 127 | ++ text->cursor.x; 128 | break; 129 | } 130 | } 131 | 132 | // wrap 133 | if (text->cursor.x >= text->size.x) { 134 | text->cursor.x = 0; 135 | ++ text->cursor.y; 136 | } 137 | } 138 | 139 | void TextScreen_PutString(TextScreen* text, char* str) { 140 | for (size_t i = 0; i < strlen(str); ++ i) { 141 | TextScreen_PutCharacter(text, str[i]); 142 | } 143 | } 144 | 145 | void TextScreen_SetAttribute(TextScreen* text, uint16_t attr, bool on) { 146 | if (on) { 147 | text->attr.attr |= attr; 148 | } 149 | else { 150 | text->attr.attr &= ~attr; 151 | } 152 | } 153 | 154 | void TextScreen_HLine(TextScreen* text, Vec2 pos, int len, char ch) { 155 | text->cursor.y = pos.y; 156 | for (int x = pos.x; x < pos.x + len; ++ x) { 157 | text->cursor.x = x + pos.x; 158 | TextScreen_PutCharacter(text, ch); 159 | } 160 | } 161 | 162 | void TextScreen_Resize(TextScreen* text, Vec2 newSize) { 163 | size_t size = newSize.x * newSize.y * sizeof(Cell); 164 | Cell* newCells = (Cell*) SafeMalloc(size); 165 | 166 | for (size_t i = 0; i < size / sizeof(Cell); ++ i) { 167 | newCells[i] = CellByCharacter(' '); 168 | } 169 | 170 | for (int y = 0; y < text->size.y; ++ y) { 171 | if (y >= newSize.y) { 172 | break; 173 | } 174 | 175 | for (int x = 0; x < text->size.x; ++ x) { 176 | if (x >= newSize.x) { 177 | break; 178 | } 179 | 180 | Cell cell = TextScreen_GetCharacter(text, x, y); 181 | 182 | newCells[(y * newSize.x) + x] = cell; 183 | } 184 | } 185 | 186 | text->size = newSize; 187 | free(text->cells); 188 | text->cells = newCells; 189 | } 190 | 191 | void TextScreen_Blit(TextScreen* text, TextScreen* src, Vec2 pos) { 192 | for (int x = 0; x < src->size.x; ++ x) { 193 | for (int y = 0; y < src->size.y; ++ y) { 194 | Cell cell = TextScreen_GetCharacter(src, x, y); 195 | TextScreen_SetCharacter(text, x + pos.x, y + pos.y, cell); 196 | } 197 | } 198 | } 199 | 200 | void TextScreen_Render(TextScreen* text, Video* video, bool showCursor) { 201 | SDL_SetRenderDrawColor( 202 | video->renderer, 203 | text->colours->bg.r, text->colours->bg.g, text->colours->bg.b, 255 204 | ); 205 | SDL_RenderClear(video->renderer); 206 | 207 | for (int y = 0; y < text->size.y; ++ y) { 208 | for (int x = 0; x < text->size.x; ++ x) { 209 | SDL_FRect rect; 210 | rect.x = (float) (x * video->charWidth); 211 | rect.y = (float) (y * video->charHeight); 212 | rect.w = (float) (video->charWidth); 213 | rect.h = (float) (video->charHeight); 214 | 215 | Cell cell = TextScreen_GetCharacter(text, x, y); 216 | 217 | SDL_Color fg = text->colours->fg; 218 | SDL_Color bg = text->colours->bg; 219 | 220 | if (cell.attr.attr & ATTR_COLOUR_FG) { 221 | fg = text->colours->colour16[cell.attr.fg]; 222 | } 223 | if (cell.attr.attr & ATTR_COLOUR_BG) { 224 | bg = text->colours->colour16[cell.attr.bg]; 225 | } 226 | 227 | if (showCursor && (x == text->cursor.x) && (y == text->cursor.y)) { 228 | SDL_Color temp = fg; 229 | fg = bg; 230 | bg = temp; 231 | } 232 | 233 | if (cell.attr.attr & ATTR_REVERSE) { 234 | SDL_Color temp = fg; 235 | fg = bg; 236 | bg = temp; 237 | } 238 | 239 | SDL_SetRenderDrawColor(video->renderer, bg.r, bg.g, bg.b, bg.a); 240 | SDL_RenderFillRect(video->renderer, &rect); 241 | 242 | Video_DrawCharacter(video, rect.x, rect.y, cell.ch, fg); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /source/textScreen.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_TEXTSCREEN_H 2 | #define YTERM_TEXTSCREEN_H 3 | 4 | #include "sdl.h" 5 | #include "types.h" 6 | #include "components.h" 7 | 8 | enum { // colours 9 | COLOUR_BLACK = 0, 10 | COLOUR_RED, 11 | COLOUR_GREEN, 12 | COLOUR_YELLOW, 13 | COLOUR_BLUE, 14 | COLOUR_MAGENTA, 15 | COLOUR_CYAN, 16 | COLOUR_WHITE, 17 | COLOUR_GREY, 18 | COLOUR_BRIGHT_RED, 19 | COLOUR_BRIGHT_GREEN, 20 | COLOUR_BRIGHT_YELLOW, 21 | COLOUR_BRIGHT_BLUE, 22 | COLOUR_BRIGHT_MAGENTA, 23 | COLOUR_BRIGHT_CYAN, 24 | COLOUR_BRIGHT_WHITE 25 | }; 26 | 27 | enum { // attributes 28 | ATTR_NONE = 0, 29 | ATTR_COLOUR_FG = 1, 30 | ATTR_COLOUR_BG = 2, 31 | ATTR_BOLD = 4, 32 | ATTR_DIM = 8, 33 | ATTR_ITALIC = 16, 34 | ATTR_UNDERLINE = 32, 35 | ATTR_BLINKING = 64, 36 | ATTR_REVERSE = 128, 37 | ATTR_HIDDEN = 256, 38 | ATTR_STRIKE = 512 39 | }; 40 | 41 | typedef struct ColourScheme { 42 | SDL_Color fg; 43 | SDL_Color bg; 44 | 45 | SDL_Color colour16[16]; 46 | } ColourScheme; 47 | 48 | typedef struct AttrInfo { 49 | uint8_t fg; 50 | uint8_t bg; 51 | uint16_t attr; 52 | } AttrInfo; 53 | 54 | typedef struct Cell { 55 | unsigned char ch; 56 | AttrInfo attr; 57 | } Cell; 58 | 59 | Cell NewCell(char ch, uint8_t fg, uint8_t bg, uint16_t attr); 60 | Cell CellByCharacter(char ch); 61 | AttrInfo NewAttr(uint8_t fg, uint8_t bg, uint16_t attr); 62 | 63 | typedef struct TextScreen { 64 | Cell* cells; 65 | Vec2 size; 66 | Vec2 cursor; 67 | ColourScheme* colours; 68 | AttrInfo attr; 69 | } TextScreen; 70 | 71 | TextScreen TextScreen_New(uint32_t w, uint32_t h); 72 | void TextScreen_Free(TextScreen* text); 73 | Cell TextScreen_GetCharacter(TextScreen* text, int x, int y); 74 | void TextScreen_SetCharacter(TextScreen* text, int x, int y, Cell cell); 75 | void TextScreen_ScrollDown(TextScreen* text, int lines); 76 | void TextScreen_PutCharacter(TextScreen* text, char ch); 77 | void TextScreen_PutString(TextScreen* text, char* str); 78 | void TextScreen_SetAttribute(TextScreen* text, uint16_t attr, bool on); 79 | void TextScreen_HLine(TextScreen* text, Vec2 pos, int len, char ch); 80 | void TextScreen_Resize(TextScreen* text, Vec2 newSize); 81 | void TextScreen_Blit(TextScreen* text, TextScreen* src, Vec2 pos); 82 | void TextScreen_Render(TextScreen* text, Video* video, bool showCursor); 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /source/types.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_TYPES_H 2 | #define YTERM_TYPES_H 3 | 4 | typedef struct Vec2 { 5 | int x; 6 | int y; 7 | } Vec2; 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /source/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "util.h" 4 | #include "safe.h" 5 | 6 | void DumpSequence(char* seq) { 7 | for (size_t i = 0; i < strlen(seq); ++ i) { 8 | switch (seq[i]) { 9 | case '\x1b': { 10 | fputs("\\e", stdout); 11 | break; 12 | } 13 | default: { 14 | putchar(seq[i]); 15 | } 16 | } 17 | } 18 | putchar('\n'); 19 | } 20 | 21 | bool StringStartsWith(char* str, char* with) { 22 | if (strlen(with) > strlen(str)) { 23 | return false; 24 | } 25 | 26 | return memcmp(str, with, strlen(with)) == 0; 27 | } 28 | 29 | bool StringIsNumeric(char* str) { 30 | for (size_t i = 0; i < strlen(str); ++ i) { 31 | if (!isdigit(str[i])) { 32 | return false; 33 | } 34 | } 35 | 36 | return true; 37 | } 38 | 39 | char* DupString(char* str) { 40 | char* ret = SafeMalloc(strlen(str) + 1); 41 | strcpy(ret, str); 42 | return ret; 43 | } 44 | 45 | char* CharToString(char ch) { 46 | char ret[] = {ch, 0}; 47 | return DupString(ret); 48 | } 49 | 50 | bool HexToColour(const char* colour, SDL_Color* ret) { 51 | assert(ret != NULL); 52 | 53 | char* ptr; 54 | uint32_t colour32 = (uint32_t) strtol(colour, &ptr, 16); 55 | if (*ptr != '\0') 56 | return false; 57 | 58 | ret->r = (colour32 >> 16) & 0xFF; 59 | ret->g = (colour32 >> 8) & 0xFF; 60 | ret->b = colour32 & 0xFF; 61 | ret->a = 255; 62 | 63 | return true; 64 | } 65 | 66 | char* GetUserShell(void) { 67 | struct passwd* data = getpwuid(geteuid()); 68 | 69 | if (data == NULL) { 70 | perror("getpwuid"); 71 | FATAL("Failed to get passwd data"); 72 | } 73 | 74 | return data->pw_shell; 75 | } 76 | void Log(const char* format, ...) { // most of this is taken from vsprintf(3) 77 | int n = 0; 78 | size_t size = 0; 79 | char* ret = NULL; 80 | va_list ap; 81 | 82 | // Determine required size 83 | va_start(ap, format); 84 | n = vsnprintf(ret, size, format, ap); 85 | va_end(ap); 86 | 87 | if (n < 0) { 88 | return; 89 | } 90 | 91 | size = n + 1; // One extra byte for '\0' 92 | ret = (char*) SafeMalloc(size); 93 | if (ret == NULL) { 94 | return; 95 | } 96 | 97 | va_start(ap, format); 98 | n = vsnprintf(ret, size, format, ap); 99 | va_end(ap); 100 | 101 | if (n < 0) { 102 | free(ret); 103 | return; 104 | } 105 | 106 | time_t rawTime; 107 | struct tm* tm; 108 | 109 | time(&rawTime); 110 | tm = localtime(&rawTime); 111 | 112 | printf("[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec, ret); 113 | free(ret); 114 | } 115 | 116 | void Error(const char* format, ...) { // most of this is taken from vsprintf(3) 117 | int n = 0; 118 | size_t size = 0; 119 | char* ret = NULL; 120 | va_list ap; 121 | 122 | // Determine required size 123 | va_start(ap, format); 124 | n = vsnprintf(ret, size, format, ap); 125 | va_end(ap); 126 | 127 | if (n < 0) { 128 | return; 129 | } 130 | 131 | size = n + 1; // One extra byte for '\0' 132 | ret = (char*) SafeMalloc(size); 133 | if (ret == NULL) { 134 | return; 135 | } 136 | 137 | va_start(ap, format); 138 | n = vsnprintf(ret, size, format, ap); 139 | va_end(ap); 140 | 141 | if (n < 0) { 142 | free(ret); 143 | return; 144 | } 145 | 146 | time_t rawTime; 147 | struct tm* tm; 148 | 149 | time(&rawTime); 150 | tm = localtime(&rawTime); 151 | 152 | printf("[%.2d:%.2d:%.2d] %s\n", tm->tm_hour, tm->tm_min, tm->tm_sec, ret); 153 | 154 | SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", ret, NULL); 155 | exit(1); 156 | } 157 | -------------------------------------------------------------------------------- /source/util.h: -------------------------------------------------------------------------------- 1 | #ifndef YTERM_UTIL_H 2 | #define YTERM_UTIL_H 3 | 4 | #include "components.h" 5 | 6 | #define ARRAY_LEN(x) (sizeof(x) / sizeof(*(x))) 7 | #define FATAL(...) \ 8 | ( \ 9 | fprintf(stderr, "%s:%d: ", __FILE__, __LINE__), \ 10 | fprintf(stderr, __VA_ARGS__), \ 11 | fprintf(stderr, "\n"), \ 12 | exit(EXIT_FAILURE) \ 13 | ) 14 | 15 | void DumpSequence(char* seq); 16 | bool StringStartsWith(char* str, char* with); 17 | bool StringIsNumeric(char* str); 18 | char* DupString(char* str); 19 | char* CharToString(char ch); 20 | bool HexToColour(const char *colour, SDL_Color* ret); 21 | char* GetUserShell(void); 22 | void Log(const char* format, ...); 23 | void Error(const char* format, ...); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /todo.json: -------------------------------------------------------------------------------- 1 | [] 2 | --------------------------------------------------------------------------------