├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------