Compare commits

...

57 Commits

Author SHA1 Message Date
557a4affa6 add -O3 to cflags 2025-09-18 01:55:12 +02:00
3b99b87c22 fix index oob segfaults 2025-04-28 02:36:14 +02:00
f40b4001c7 remove github workflows 2024-11-10 01:22:58 +01:00
143feaac83 make ui color configurable through a header file
Some checks failed
Build Todd / build-linux (push) Has been cancelled
Build Todd / build-mac (push) Has been cancelled
Build Todd / build-windows (push) Has been cancelled
2024-11-10 01:21:00 +01:00
972bd28b6f add usage 2024-11-10 01:20:57 +01:00
8421e3ee01 add ncurses to requirements 2024-11-10 01:20:55 +01:00
bda1c18a52 change package name 2024-11-10 01:20:54 +01:00
37cd828c21 add msystem 2024-11-10 01:20:51 +01:00
c746cad281 try out different command name 2024-11-10 01:20:49 +01:00
0abfa4af0f change to correct package name 2024-11-10 01:20:46 +01:00
ceede3001a add windows build again 2024-11-10 01:20:44 +01:00
29eeef56b8 add dirty check 2024-11-10 01:20:42 +01:00
e6203f70a9 remove windows build step 2024-11-10 01:20:41 +01:00
d8a3fe0d7a add windows build step 2024-11-10 01:20:39 +01:00
7c060419f4 add mac build step 2024-11-10 01:20:38 +01:00
c3b1797bd9 upload artifact 2024-11-10 01:20:37 +01:00
e9d8988ef2 fix mvwprintw string literal error 2024-11-10 01:20:35 +01:00
476428b7de remove setup gcc 2024-11-10 01:20:34 +01:00
58eda93cbb remove cache apt pkgs 2024-11-10 01:20:32 +01:00
21a6c077fd set version to string 2024-11-10 01:20:31 +01:00
2e0f7501d7 add other steps 2024-11-10 01:20:29 +01:00
3809b5b96a generate workflow file 2024-11-10 01:20:28 +01:00
79a313a03e start debugging workflow file 2024-11-10 01:20:26 +01:00
c3dee6a8ef copy workflow into correct directory 2024-11-10 01:20:25 +01:00
20d6e4a8b1 add github build action 2024-11-10 01:20:23 +01:00
65d591a455 take height into account 2024-11-10 01:20:22 +01:00
65b5e45cba add view command 2024-11-10 01:20:21 +01:00
e3f807bd7f add licenses 2024-11-10 01:20:19 +01:00
9bd8817797 add proper scroll 2024-11-10 01:20:18 +01:00
e8e0babfaa load file on startup if it exists 2024-11-10 01:20:17 +01:00
3159a34c9a remove unused command 2024-11-10 01:20:15 +01:00
1267fc54bb add install makefile command 2024-11-10 01:20:14 +01:00
613071c8f5 print key bindings on the footer 2024-11-10 01:20:12 +01:00
0cf3febd2b add save file path to the header 2024-11-10 01:20:11 +01:00
76396a3022 store the default file in the home directory dotfile 2024-11-10 01:20:09 +01:00
e900ffc0d7 add todo count in the bottom bar 2024-11-10 01:20:08 +01:00
dd148396f1 add space key for marking todos 2024-11-10 01:20:06 +01:00
71939366c7 allow arrow input 2024-11-10 01:20:05 +01:00
a5734d1323 add a semi-functional TUI 2024-11-10 01:20:03 +01:00
14f177e3f9 fix build errors 2024-11-10 01:20:02 +01:00
881d13b546 add cflags 2024-11-10 01:20:00 +01:00
a852107b62 update makefile 2024-11-10 01:19:59 +01:00
3acbe3eb33 update readme 2024-11-10 01:19:57 +01:00
672998b17b format the code 2024-11-10 01:19:56 +01:00
01e6a6064d free todos after quit 2024-11-10 01:19:54 +01:00
d0ec4b758b update readme 2024-11-10 01:19:52 +01:00
b27047e7d1 implement loading from file 2024-11-10 01:19:51 +01:00
71e935c8ad use stb like a normal person (totally didnt waste an hour) 2024-11-10 01:19:49 +01:00
44a4374fd1 remove llist 2024-11-10 01:19:48 +01:00
76c620c142 steal a linked list implementation 2024-11-10 01:19:46 +01:00
b49b728a4c add todo serialization 2024-11-10 01:19:44 +01:00
bb0c5c173a add makefile 2024-11-10 01:19:43 +01:00
b476873ede add ideas 2024-11-10 01:19:41 +01:00
78a190b28f add readme 2024-11-10 01:19:39 +01:00
21dd4c988e add remove command 2024-11-10 01:19:38 +01:00
e875f6fab3 separate files and fix newline bug 2024-11-10 01:19:36 +01:00
06a0b782cf make stuff work 2024-11-10 01:19:34 +01:00
12 changed files with 2531 additions and 4 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
todd todd
*.o
*.todd

27
3rd-party/NCURSES-LICENSE.txt vendored Normal file
View File

@@ -0,0 +1,27 @@
-------------------------------------------------------------------------------
-- Copyright (c) 1998-2004,2006 Free Software Foundation, Inc. --
-- --
-- Permission is hereby granted, free of charge, to any person obtaining a --
-- copy of this software and associated documentation files (the --
-- "Software"), to deal in the Software without restriction, including --
-- without limitation the rights to use, copy, modify, merge, publish, --
-- distribute, distribute with modifications, sublicense, and/or sell copies --
-- of the Software, and to permit persons to whom the Software is furnished --
-- to do so, subject to the following conditions: --
-- --
-- The above copyright notice and this permission notice shall be included --
-- in all copies or substantial portions of the Software. --
-- --
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS --
-- OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF --
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN --
-- NO EVENT SHALL THE ABOVE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, --
-- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR --
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE --
-- USE OR OTHER DEALINGS IN THE SOFTWARE. --
-- --
-- Except as contained in this notice, the name(s) of the above copyright --
-- holders shall not be used in advertising or otherwise to promote the --
-- sale, use or other dealings in this Software without prior written --
-- authorization. --
-------------------------------------------------------------------------------

16
3rd-party/STB-DS-LICENSE.txt vendored Normal file
View File

@@ -0,0 +1,16 @@
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1895
3rd-party/stb-ds.h vendored Normal file

File diff suppressed because it is too large Load Diff

22
LICENSE.txt Normal file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2024 Zvonimir Rudinski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

23
Makefile Normal file
View File

@@ -0,0 +1,23 @@
.DEFAULT_GOAL := all
CC=gcc
CFLAGS=-Wall -Wextra -Werror -pedantic -std=c99 -O3
todo.o:
$(CC) $(CFLAGS) -c engine/todo.c -o todo.o
main.o:
$(CC) $(CFLAGS) -c main.c -o main.o
clean:
rm -f *.o
rm -f todd
rm /usr/local/bin/todd || true
todd: todo.o main.o
$(CC) $(CFLAGS) todo.o main.o -lncurses -o todd
all: todd
install: todd
cp todd /usr/local/bin/todd

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# Todd
`todd` is a to-do app written in C.
## Requirements
- GCC
- GNU Make
- ncurses development libraries
## Building
To build `todd` all you need to do is run `make`.
## Usage
```sh
./todd <file.todd>
```
## 3rd Party
Todd uses the following dependencies:
- stb_ds - https://github.com/nothings/stb
- ncurses - https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/index.html

View File

@@ -1 +0,0 @@
cc main.c -o todd

15
color.h Normal file
View File

@@ -0,0 +1,15 @@
#ifndef COLOR_H
#define COLOR_H
#include <ncurses.h>
#define BORDERS_PRIMARY COLOR_BLACK
#define BORDERS_SECONDARY COLOR_WHITE
#define DEFAULT_PRIMARY COLOR_WHITE
#define DEFAULT_SECONDARY COLOR_BLACK
#define COMPLETED_PRIMARY COLOR_BLACK
#define COMPLETED_SECONDARY COLOR_YELLOW
#endif

56
engine/todo.c Normal file
View File

@@ -0,0 +1,56 @@
#include "todo.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Memory management
void todo_free_item(TodoItem *item) { free(item->title); }
// Item operations
TodoItem todo_create_item(char *title) {
TodoItem item;
item.title_length = strlen(title);
item.title = malloc(item.title_length + 1);
strcpy(item.title, title);
item.completed = false;
return item;
}
void todo_mark_item(TodoItem *item, bool_t completed) {
item->completed = completed;
}
void todo_print_item(TodoItem *item) {
printf("%s - ", item->title);
if (item->completed) {
printf("Completed\n");
} else {
printf("Not Completed\n");
}
}
// Serialization
char *todo_item_serialize(TodoItem *item, int *buffer_size_out) {
// Layout of the serialized item:
// title_length - 1 byte
// title - title_length bytes
// completed - 1 byte
unsigned char title_length = item->title_length;
unsigned char completed_length = sizeof(item->completed);
int buffer_size = sizeof(unsigned char) + title_length + completed_length;
char *buffer = malloc(buffer_size);
// copy title length
memcpy(buffer, &title_length, sizeof(unsigned char));
// copy title
memcpy(buffer + sizeof(unsigned char), item->title, title_length);
// copy completed
memcpy(buffer + sizeof(unsigned char) + title_length, &item->completed,
completed_length);
*buffer_size_out = buffer_size;
return buffer;
}

25
engine/todo.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef TODO_H
#define TODO_H
#define TODO_MAX_TITLE_LENGTH 255
#define true 1
#define false 0
typedef unsigned char bool_t;
typedef struct {
unsigned char title_length;
char *title;
bool_t completed;
} TodoItem;
// Memory management
void todo_free_item(TodoItem *item);
// Item operations
TodoItem todo_create_item(char *title);
void todo_mark_item(TodoItem *item, bool_t completed);
void todo_print_item(TodoItem *item);
// Serialization
char *todo_item_serialize(TodoItem *item, int *buffer_size_out);
#endif

437
main.c
View File

@@ -1,7 +1,438 @@
#include "engine/todo.h"
#include "color.h"
#include <limits.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pwd.h>
int main(int argc, char **argv) { #define MAX_TODOS UINT_MAX
printf("Hello, world!\n"); #define MAX_PATH_LENGTH 1024
#define STB_DS_IMPLEMENTATION
return 0; #define BORDERS_PAIR 1
#define DEFAULT_PAIR 2
#define COMPLETED_PAIR 3
#include "3rd-party/stb-ds.h"
#define KEY_BINDINGS " [a]dd [v]view [m]ark [d]elete [w]rite [l]oad [q]uit"
bool dirty = false;
char SAVE_FILE[MAX_PATH_LENGTH] = "";
TodoItem *todos = NULL;
enum Command {
ADD = 'a',
VIEW = 'v',
MARK = 'm',
REMOVE = 'd',
QUIT = 'q',
WRITE_TO_FILE = 'w',
LOAD_FROM_FILE = 'l',
UP = 'k',
DOWN = 'j',
UP_ARROW = KEY_UP,
DOWN_ARROW = KEY_DOWN,
SPACE = ' '
};
void clear_todos(void) {
for (int i = 0; i < arrlen(todos); i++) {
todo_free_item(&todos[i]);
}
arrfree(todos);
}
void initialize_curses(int *width, int *height) {
initscr();
curs_set(0);
int tw, th;
getmaxyx(stdscr, th, tw);
*width = tw;
*height = th;
start_color();
init_pair(BORDERS_PAIR, BORDERS_PRIMARY, BORDERS_SECONDARY);
init_pair(DEFAULT_PAIR, DEFAULT_PRIMARY, DEFAULT_SECONDARY);
init_pair(COMPLETED_PAIR, COMPLETED_PRIMARY, COMPLETED_SECONDARY);
// turn off echoing of keys, and enter cbreak mode,
// where no buffering is performed on keyboard input
cbreak();
noecho();
keypad(stdscr, TRUE);
}
void draw_header(int width) {
attron(COLOR_PAIR(BORDERS_PAIR));
move(0, 0);
addstr(" Todd - ");
addstr(SAVE_FILE);
for (int i = strlen(" Todd - ") + strlen(SAVE_FILE); i < width - 1; i++) {
addch(' ');
}
}
void draw_footer(int width, int height) {
move(height - 1, 0);
addch(' ');
int todos_count = arrlen(todos);
int marked_count = 0;
for (int i = 0; i < todos_count; i++) {
if (todos[i].completed) {
marked_count++;
}
}
// get the length of the string
int length = snprintf(NULL, 0, "%d/%d", marked_count, todos_count);
// print the string
printw("%d/%d", marked_count, todos_count);
// print the rest of the line but leave space for the key bindings
for (unsigned long i = length; i < width - strlen(KEY_BINDINGS) - 1; i++) {
addch(' ');
}
// print the key bindings
printw(KEY_BINDINGS);
attroff(COLOR_PAIR(BORDERS_PAIR));
}
char alert(const char *message, int terminal_width, int terminal_height) {
int window_height = 5;
int window_width = terminal_width - 4;
int window_y = terminal_height / 2 - 1;
int window_x = 2;
int message_x = (terminal_width - 4 - strlen(message)) / 2;
int message_y = 2;
// create a new window
WINDOW *alert_window = newwin(window_height, window_width, window_y, window_x);
// draw a border around the window
box(alert_window, 0, 0);
// print the message in the middle of the window
mvwprintw(alert_window, message_y, message_x, "%s", message);
// refresh the window
wrefresh(alert_window);
// wait for a key press
char key = getch();
// delete the window
delwin(alert_window);
return key;
}
void input_string(int terminal_width, int terminal_height, char *buffer, int buffer_size) {
int window_height = 5;
int window_width = terminal_width - 4;
int window_y = terminal_height / 2 - 1;
int window_x = 2;
int message_x = strlen("Enter todo title: ") + 1;
int message_y = 2;
curs_set(1);
// create a new window
WINDOW *input_window = newwin(window_height, window_width, window_y, window_x);
// draw a border around the window
box(input_window, 0, 0);
// print the message in the middle of the window
mvwprintw(input_window, message_y, message_x, "Enter todo title: ");
// refresh the window
wrefresh(input_window);
// wait for a key press
echo();
mvwgetnstr(input_window, message_y, message_x + strlen("Enter todo title: "), buffer, buffer_size);
noecho();
// delete the window
delwin(input_window);
curs_set(0);
}
void display_todos(int width, int height, int current_line) {
int current_y = 1;
int current_index = 0;
const char *padding = "...";
unsigned long title_length = 0;
unsigned long padding_length = strlen(padding);
char *title = (char *)malloc(width);
attron(COLOR_PAIR(DEFAULT_PAIR));
// calculate the offset
if (height - 4 <= current_line) {
// ignore the first n todos
current_index = current_line - (height - 4);
}
// while we have space to print todos
while (current_y < height - 2) {
// check if we're out of bounds
if (current_index >= arrlen(todos)) {
break;
}
// copy the title to the buffer
title_length = strlen(todos[current_index].title);
strncpy(title, todos[current_index].title, width - padding_length - 1);
// append the padding if needed
if (title_length > strlen(title)) {
strcat(title, padding);
}
// move to the correct line
move(current_y, 0);
// if the current todo is completed, print it as green
if (todos[current_index].completed) {
attroff(COLOR_PAIR(DEFAULT_PAIR));
attron(COLOR_PAIR(COMPLETED_PAIR));
}
// highlight the current line
if (current_index == current_line) {
attron(A_UNDERLINE);
}
printw("%d. %s", current_index + 1, title);
// increment the current y and index
current_y++;
current_index++;
attroff(A_UNDERLINE);
attroff(COLOR_PAIR(COMPLETED_PAIR));
attron(COLOR_PAIR(DEFAULT_PAIR));
}
free(title);
}
// Command handlers
void add_command_handler(int width, int height) {
char title[TODO_MAX_TITLE_LENGTH];
input_string(width, height, title, TODO_MAX_TITLE_LENGTH);
TodoItem item = todo_create_item(title);
arrput(todos, item);
// set the dirty flag
dirty = true;
}
void view_command_handler(int width, int height, int index) {
// check if the index is out of bounds
if (index < 0 || index >= arrlen(todos)) {
return;
}
TodoItem item = todos[index];
// create an alert in the middle of the screen
alert(item.title, width, height);
}
void mark_command_handler(int index) {
// check if the index is out of bounds
if (index < 0 || index >= arrlen(todos)) {
return;
}
TodoItem item = todos[index];
todo_mark_item(&item, !item.completed);
todos[index] = item;
// set the dirty flag
dirty = true;
}
void remove_command_handler(int index) {
// check if the index is out of bounds
if (index < 0 || index >= arrlen(todos)) {
return;
}
arrdel(todos, index);
// set the dirty flag
dirty = true;
}
void write_to_file_handler(void) {
FILE *file = fopen(SAVE_FILE, "wb");
int bs = 0;
int todos_count = arrlen(todos);
// write a magic value "TODD" to the file
fwrite("TODD", sizeof(char), 4, file);
// write the number of todos to the file
fwrite(&todos_count, sizeof(int), 1, file);
// write each todo to the file
for (int i = 0; i < todos_count; i++) {
char *buffer = todo_item_serialize(&todos[i], &bs);
fwrite(buffer, sizeof(char), bs, file);
free(buffer);
}
fclose(file);
// reset the dirty flag
dirty = false;
}
void load_from_file_handler(void) {
// clear the current todos
clear_todos();
FILE *file = fopen(SAVE_FILE, "rb");
// if unable to open the file, return
if (file == NULL) {
return;
}
char magic[5] = {'\0'};
int todos_count = 0;
// check the "TODD" (without the \0) magic value
fread(magic, sizeof(char), 4, file);
if (strcmp(magic, "TODD") != 0) {
printf("Invalid file format\nExpected TODD, got %s\n", magic);
return;
}
// read the number of todos
fread(&todos_count, sizeof(int), 1, file);
// read each todo
for (int i = 0; i < todos_count; i++) {
// read the title length
unsigned char title_length = 0;
fread(&title_length, sizeof(unsigned char), 1, file);
// read the title
char *title = malloc(title_length + 1);
fread(title, sizeof(char), title_length, file);
title[title_length] = '\0';
// read the completed flag
bool_t completed = false;
fread(&completed, sizeof(bool_t), 1, file);
// create the todo item
TodoItem item = todo_create_item(title);
// mark the item as completed if needed
if (completed) {
todo_mark_item(&item, true);
}
// add the item to the list
arrput(todos, item);
}
// reset the dirty flag
dirty = false;
}
int main(int argc, char **argv) {
int terminal_width = 0;
int terminal_height = 0;
int key = 0;
int running = 1;
int current_line = 0;
// set the save file path if provided
if (argc > 1 && strlen(argv[1]) < MAX_PATH_LENGTH) {
strncpy(SAVE_FILE, argv[1], MAX_PATH_LENGTH);
} else {
// load the default save file which is "~/.todos.todd"
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
// check if we have enough space to store the path
assert(strlen(homedir) + strlen("/.todos.todd") < MAX_PATH_LENGTH);
// create the path
snprintf(SAVE_FILE, MAX_PATH_LENGTH, "%s/.todos.todd", homedir);
}
// if file exists, load it
load_from_file_handler();
initialize_curses(&terminal_width, &terminal_height);
while (running) {
clear();
draw_header(terminal_width);
draw_footer(terminal_width, terminal_height);
display_todos(terminal_width, terminal_height, current_line);
refresh();
// wait for a key press
key = getch();
// handle the key press
switch (key) {
case LOAD_FROM_FILE:
load_from_file_handler();
alert("Loaded!", terminal_width, terminal_height);
break;
case WRITE_TO_FILE:
write_to_file_handler();
alert("Saved!", terminal_width, terminal_height);
break;
case ADD:
add_command_handler(terminal_width, terminal_height);
break;
case VIEW:
view_command_handler(terminal_width, terminal_height, current_line);
break;
case MARK:
case SPACE:
mark_command_handler(current_line);
break;
case REMOVE:
remove_command_handler(current_line);
if (current_line >= arrlen(todos)) {
current_line = arrlen(todos) - 1;
}
break;
case QUIT:
if (dirty) {
key = alert("You have unsaved changes. Save changes? [y/n]", terminal_width, terminal_height);
if (key == 'y') {
write_to_file_handler();
}
}
running = 0;
break;
case UP:
case UP_ARROW:
current_line--;
if (current_line < 0) {
current_line = 0;
}
break;
case DOWN:
case DOWN_ARROW:
current_line++;
if (current_line >= arrlen(todos)) {
current_line = arrlen(todos) - 1;
}
break;
}
}
endwin();
return 0;
} }