├── .gitignore ├── src ├── config.h.in ├── error.h ├── file.h ├── edit.h ├── error.c ├── wedit.h ├── sync.h ├── stream.h ├── task.h ├── sync.c ├── task.c ├── edit.c ├── wedit.c ├── stream.c ├── file.c └── ctodo.c ├── LICENSE ├── cmake └── FindReadline.cmake ├── CMakeLists.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeCache.txt 2 | CMakeFiles 3 | Makefile 4 | cmake_install.cmake 5 | install_manifest.txt 6 | -------------------------------------------------------------------------------- /src/config.h.in: -------------------------------------------------------------------------------- 1 | #cmakedefine CTODO_VERSION "@CTODO_VERSION@" 2 | #cmakedefine SYNC_ENABLE 3 | #cmakedefine READLINE_ENABLE 4 | -------------------------------------------------------------------------------- /src/error.h: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | /* Simple error handling. */ 8 | #ifndef ERROR_H 9 | #define ERROR_H 10 | 11 | /* Set an error message (see printf()). */ 12 | void error(const char *format, ...); 13 | /* Whether an error has been set. */ 14 | int has_error(); 15 | /* Unset the error message. */ 16 | void unset_error(); 17 | /* Get the last error message. */ 18 | char *get_last_error(); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/file.h: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | /* Provides functions for saving/loading tasks from files and strings. */ 8 | #ifndef FILE_H 9 | #define FILE_H 10 | 11 | #include 12 | 13 | #include "task.h" 14 | 15 | /* Load list from file. */ 16 | TODOLIST *load_todolist(char *filename); 17 | /* Save list to file. */ 18 | int save_todolist(TODOLIST *todolist, char *filename); 19 | 20 | /* Parse list from string. */ 21 | TODOLIST *parse_todolist(char *source, size_t length); 22 | /* Convert a list back to a string. */ 23 | char *stringify_todolist(TODOLIST *todolist); 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /src/edit.h: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2017 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | /* Provides line editor used when editing tasks, title, etc. */ 8 | #ifndef EDIT_H 9 | #define EDIT_H 10 | 11 | /* Key command description */ 12 | typedef struct { 13 | char *key; 14 | char *func; 15 | } COMMAND; 16 | 17 | /* Print a list of commands at the bottom of the screen. */ 18 | void print_commands(COMMAND *commands, int y, int width, int height); 19 | /* Open an editor with the given prompt and return the result. */ 20 | char *get_input(char *prompt); 21 | /* Open an editor with the given prompt and content and return the result. */ 22 | char *get_input_edit(char *prompt, char *buffer); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /src/error.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include "stdarg.h" 8 | 9 | #include "error.h" 10 | #include "stream.h" 11 | 12 | char *error_string = NULL; 13 | 14 | void error(const char *format, ...) { 15 | va_list va; 16 | char *old = error_string; 17 | va_start(va, format); 18 | error_string = string_vprintf(format, va); 19 | va_end(va); 20 | if (old) { 21 | free(old); 22 | } 23 | } 24 | 25 | int has_error() { 26 | return error_string != NULL; 27 | } 28 | 29 | void unset_error() { 30 | if (error_string) { 31 | free(error_string); 32 | error_string = NULL; 33 | } 34 | } 35 | 36 | char *get_last_error() { 37 | return error_string; 38 | } 39 | -------------------------------------------------------------------------------- /src/wedit.h: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2017 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | /* Provides unicode readline-based line editor for editing tasks, title, etc. 8 | * 9 | * Is enabled by default since version 1.3. Can be disabled by running cmake 10 | * with -DREADLINE_ENABLE=OFF. */ 11 | #ifndef WEDIT_H 12 | #define WEDIT_H 13 | 14 | /* Key command description */ 15 | typedef struct { 16 | char *key; 17 | char *func; 18 | } COMMAND; 19 | 20 | /* Print a list of commands at the bottom of the screen. */ 21 | void print_commands(COMMAND *commands, int y, int width, int height); 22 | /* Open an editor with the given prompt and return the result. */ 23 | char *get_input(const char *prompt); 24 | /* Open an editor with the given prompt and content and return the result. */ 25 | char *get_input_edit(const char *prompt, const char *buffer); 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /src/sync.h: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | /* Implements an experimental HTTP sync feature. 8 | * 9 | * Use by adding the following options to a ctodo file: 10 | * # autosync=1 origin=https://my-server/path 11 | * 12 | * ctodo downloads a list of tasks with a GET-request, and uploads a list of 13 | * tasks with a PUT-request. 14 | * 15 | * Can be enabled by running cmake with -DSYNC_ENABLE=ON, the default is 16 | * currently OFF. May change in later versions. */ 17 | #ifndef SYNC_H 18 | #define SYNC_H 19 | 20 | #include "task.h" 21 | 22 | /* Download a ctodo file. */ 23 | TODOLIST *pull_todolist(char *origin); 24 | /* Upload a ctodo file. */ 25 | int push_todolist(TODOLIST *todolist, char *origin); 26 | 27 | /* Merge two ctodo files (NOT IMPLEMENTED). */ 28 | int merge_todolist(TODOLIST *mine, TODOLIST *theirs); 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 Niels Sonnich Poulsen (http://nielssp.dk) 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /cmake/FindReadline.cmake: -------------------------------------------------------------------------------- 1 | # from http://websvn.kde.org/trunk/KDE/kdeedu/cmake/modules/FindReadline.cmake 2 | # http://websvn.kde.org/trunk/KDE/kdeedu/cmake/modules/COPYING-CMAKE-SCRIPTS 3 | # --> BSD licensed 4 | # 5 | # GNU Readline library finder 6 | if(READLINE_INCLUDE_DIR AND READLINE_LIBRARY) 7 | set(READLINE_FOUND TRUE) 8 | else(READLINE_INCLUDE_DIR AND READLINE_LIBRARY) 9 | FIND_PATH(READLINE_INCLUDE_DIR readline/readline.h 10 | /usr/include/readline 11 | ) 12 | 13 | # 2008-04-22 The next clause used to read like this: 14 | # 15 | # FIND_LIBRARY(READLINE_LIBRARY NAMES readline) 16 | # FIND_LIBRARY(NCURSES_LIBRARY NAMES ncurses ) 17 | # include(FindPackageHandleStandardArgs) 18 | # FIND_PACKAGE_HANDLE_STANDARD_ARGS(Readline DEFAULT_MSG NCURSES_LIBRARY READLINE_INCLUDE_DIR READLINE_LIBRARY ) 19 | # 20 | # I was advised to modify it such that it will find an ncurses library if 21 | # required, but not if one was explicitly given, that is, it allows the 22 | # default to be overridden. PH 23 | 24 | FIND_LIBRARY(READLINE_LIBRARY NAMES readline) 25 | include(FindPackageHandleStandardArgs) 26 | FIND_PACKAGE_HANDLE_STANDARD_ARGS(Readline DEFAULT_MSG READLINE_INCLUDE_DIR READLINE_LIBRARY ) 27 | 28 | MARK_AS_ADVANCED(READLINE_INCLUDE_DIR READLINE_LIBRARY) 29 | endif(READLINE_INCLUDE_DIR AND READLINE_LIBRARY) 30 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(ctodo) 2 | set(CTODO_VERSION "1.3") 3 | 4 | cmake_minimum_required(VERSION 2.6) 5 | 6 | set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) 7 | 8 | option(SYNC_ENABLE "Enable experimental sync feature" OFF) 9 | option(READLINE_ENABLE "Enable readline" ON) 10 | configure_file(src/config.h.in src/config.h) 11 | 12 | find_package(Curses REQUIRED) 13 | include_directories("$(CURSES_INCLUDE_DIR)") 14 | 15 | if(SYNC_ENABLE) 16 | find_package(CURL REQUIRED) 17 | include_directories("$(CURL_INCLUDE_DIR)") 18 | endif() 19 | if(READLINE_ENABLE) 20 | find_package(Readline REQUIRED) 21 | include_directories("$(READLINE_INCLUDE_DIR)") 22 | endif() 23 | 24 | include_directories(${CMAKE_BINARY_DIR}/src src) 25 | 26 | file(GLOB SRC_LIST src/*.c) 27 | 28 | if(NOT SYNC_ENABLE) 29 | list(REMOVE_ITEM SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/sync.c) 30 | endif() 31 | if(READLINE_ENABLE) 32 | list(REMOVE_ITEM SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/edit.c) 33 | else() 34 | list(REMOVE_ITEM SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/wedit.c) 35 | endif() 36 | 37 | add_executable(ctodo ${SRC_LIST}) 38 | 39 | target_link_libraries(ctodo ${CURSES_LIBRARIES}) 40 | if(SYNC_ENABLE) 41 | target_link_libraries(ctodo ${CURL_LIBRARIES}) 42 | endif() 43 | if(READLINE_ENABLE) 44 | target_link_libraries(ctodo ${READLINE_LIBRARY}) 45 | endif() 46 | 47 | install(TARGETS ctodo DESTINATION bin) 48 | -------------------------------------------------------------------------------- /src/stream.h: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | /* A simple stream wrapper that allows ctodo to save/load from both files and 8 | * strings. Also contains some utilities for working with strings and buffers. */ 9 | #ifndef STREAM_H 10 | #define STREAM_H 11 | 12 | #include 13 | #include 14 | 15 | /* Stream type */ 16 | typedef struct STREAM STREAM; 17 | 18 | /* Open a file as a stream (see fopen()). */ 19 | STREAM *stream_file(const char *filename, const char *mode); 20 | /* Open a buffer as a stream. */ 21 | STREAM *stream_buffer(char *buffer, size_t length); 22 | /* Get buffer content (only for buffer streams). */ 23 | char *stream_get_content(STREAM *stream); 24 | /* Get buffer size (only for buffer streams). */ 25 | size_t stream_get_size(STREAM *stream); 26 | /* Close stream. */ 27 | void stream_close(STREAM *stream); 28 | 29 | /* Read from a stream (see fread()). */ 30 | size_t stream_read(void *ptr, size_t size, size_t nmemb, STREAM *stream); 31 | /* Read a character (see fgetc()). */ 32 | int stream_getc(STREAM *input); 33 | /* Push a character (see ungetc()). */ 34 | void stream_ungetc(int c, STREAM *input); 35 | /* Check for EOF (see feof()). */ 36 | int stream_eof(STREAM *input); 37 | /* Write a character (see fputc()). */ 38 | int stream_putc(int c, STREAM *output); 39 | /* Print to stream (see vfprintf()) */ 40 | int stream_vprintf(STREAM *output, const char *format, va_list va); 41 | /* Print to stream (see fprintf()) */ 42 | int stream_printf(STREAM *output, const char *format, ...); 43 | 44 | /* Like vsprintf(), but automatically creates a large enough buffer. */ 45 | char *string_vprintf(const char *format, va_list va); 46 | /* Like sprintf(), but automatically creates a large enough buffer. */ 47 | char *string_printf(const char *format, ...); 48 | 49 | /* Resize a buffer. */ 50 | char *resize_buffer(char *buffer, size_t oldsize, size_t newsize); 51 | 52 | #endif 53 | -------------------------------------------------------------------------------- /src/task.h: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | /* Provides task and todo list data structures, and utilities for manipulating 8 | * them. */ 9 | #ifndef TASK_H 10 | #define TASK_H 11 | 12 | /* A task. Part of doubly linked list. */ 13 | typedef struct TASK { 14 | char *message; /* Task text */ 15 | int done; /* Done (1) or not done (0). */ 16 | int priority; /* Unused. */ 17 | struct TASK *next; /* Next task in list. */ 18 | struct TASK *prev; /* Previous task in list. */ 19 | } TASK; 20 | 21 | /* An option. Part of singly linked list. */ 22 | typedef struct OPTION { 23 | char *key; /* Option key, left side of '='. */ 24 | char *value; /* Option value, right side of '='. */ 25 | struct OPTION *next; /* Next option in list. */ 26 | } OPTION; 27 | 28 | /* A list of tasks and options. */ 29 | typedef struct { 30 | char *title; /* List title. */ 31 | TASK *first; /* First task in list. */ 32 | TASK *last; /* Last task in list. */ 33 | OPTION *first_option; /* First option in list. */ 34 | OPTION *last_option; /* Last option in list. */ 35 | } TODOLIST; 36 | 37 | /* Delete a list and all associated tasks and options. */ 38 | void delete_todolist(TODOLIST *todolist); 39 | 40 | /* Remove a task from a list and delete it. */ 41 | void delete_task(TASK *delete, TODOLIST *list); 42 | /* Add a task to the end of a list. */ 43 | void add_task(TODOLIST *list, char *message, int done, int priority); 44 | /* Insert a task before another one. */ 45 | void insert_task(TODOLIST *list, TASK *next, char *message, int done, int priority); 46 | /* Move a task up. */ 47 | void move_task_up(TODOLIST *list, TASK *task); 48 | /* Move a task down. */ 49 | void move_task_down(TODOLIST *list, TASK *task); 50 | 51 | /* Get the value of an option. */ 52 | char *get_option(TODOLIST *todolist, const char *key); 53 | /* Get a copy of the value of an option, i.e. the value persists even if the 54 | * list is later deleted. Must be freed manually. */ 55 | char *copy_option(TODOLIST *todolist, const char *key); 56 | /* Get binary value of an option (0 if not set or "0", 1 otherwise). */ 57 | int get_option_bit(TODOLIST *todolist, const char *key); 58 | /* Set value of an option. */ 59 | void set_option(TODOLIST *list, const char *key, const char *value); 60 | /* Get binary value of an option. */ 61 | void set_option_bit(TODOLIST *todolist, const char *key, int bit); 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /src/sync.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "sync.h" 12 | #include "file.h" 13 | #include "stream.h" 14 | #include "error.h" 15 | 16 | char *downloaded_file = NULL; 17 | size_t downloaded_file_size = 0; 18 | 19 | // TODO: implement stream_write and replace this 20 | size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) { 21 | size_t data_size = size * nmemb; 22 | if (downloaded_file) { 23 | downloaded_file = resize_buffer(downloaded_file, downloaded_file_size, downloaded_file_size + data_size); 24 | } 25 | else { 26 | downloaded_file = (char *)malloc(data_size); 27 | downloaded_file_size = 0; 28 | } 29 | if (!downloaded_file) { 30 | return 0; 31 | } 32 | memcpy(downloaded_file + downloaded_file_size, ptr, data_size); 33 | downloaded_file_size += data_size; 34 | return data_size; 35 | } 36 | 37 | TODOLIST *pull_todolist(char *origin) { 38 | TODOLIST *list = NULL; 39 | CURL *curl; 40 | CURLcode res; 41 | long http_status = 0; 42 | 43 | curl_global_init(CURL_GLOBAL_ALL); 44 | 45 | curl = curl_easy_init(); 46 | if (curl) { 47 | curl_easy_setopt(curl, CURLOPT_URL, origin); 48 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &write_callback); 49 | 50 | res = curl_easy_perform(curl); 51 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status); 52 | if (res != CURLE_OK) { 53 | error("Error: %s", curl_easy_strerror(res)); 54 | } 55 | else if (http_status != 200) { 56 | error("Server returned %d", http_status); 57 | } 58 | else { 59 | list = parse_todolist(downloaded_file, downloaded_file_size); 60 | } 61 | if (downloaded_file) { 62 | free(downloaded_file); 63 | downloaded_file = NULL; 64 | } 65 | 66 | curl_easy_cleanup(curl); 67 | } 68 | curl_global_cleanup(); 69 | return list; 70 | } 71 | 72 | int push_todolist(TODOLIST *todolist, char *origin) { 73 | CURL *curl; 74 | CURLcode res; 75 | char *content; 76 | STREAM *stream; 77 | long http_status = 0; 78 | int status = 1; 79 | 80 | curl_global_init(CURL_GLOBAL_ALL); 81 | 82 | curl = curl_easy_init(); 83 | if (curl) { 84 | content = stringify_todolist(todolist); 85 | stream = stream_buffer(content, strlen(content)); 86 | curl_easy_setopt(curl, CURLOPT_URL, origin); 87 | curl_easy_setopt(curl, CURLOPT_UPLOAD, 1); 88 | curl_easy_setopt(curl, CURLOPT_READDATA, stream); 89 | curl_easy_setopt(curl, CURLOPT_READFUNCTION, &stream_read); 90 | 91 | res = curl_easy_perform(curl); 92 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status); 93 | if (res != CURLE_OK) { 94 | error("Error: %s", curl_easy_strerror(res)); 95 | status = 0; 96 | } 97 | else if (http_status != 200) { 98 | error("Server returned %d", http_status); 99 | status = 0; 100 | } 101 | 102 | stream_close(stream); 103 | free(content); 104 | 105 | curl_easy_cleanup(curl); 106 | } 107 | curl_global_cleanup(); 108 | return status; 109 | } 110 | 111 | int merge_todolist(TODOLIST *mine, TODOLIST *theirs) { 112 | return 1; 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ctodo 2 | A simple ncurses-based task list manager 3 | 4 | Website: http://ctodo.apakoh.dk 5 | 6 | ![Screenshot.](http://ctodo.apakoh.dk/screenshot.png) 7 | 8 | ## License 9 | Copyright (C) 2012 Niels Sonnich Poulsen (http://nielssp.dk) 10 | 11 | Permission is hereby granted, free of charge, to any person 12 | obtaining a copy of this software and associated documentation 13 | files (the "Software"), to deal in the Software without 14 | restriction, including without limitation the rights to use, 15 | copy, modify, merge, publish, distribute, sublicense, and/or 16 | sell copies of the Software, and to permit persons to whom the 17 | Software is furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be 20 | included in all copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 29 | OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ## Requirements 32 | ### Linux 33 | * `ncurses` 34 | * A C-compiler like `gcc` 35 | * `cmake` (not actually necessary) 36 | 37 | ### Windows 38 | * Windows XP or newer 39 | 40 | ## Installation 41 | ### Linux 42 | * Run cmake: 43 | 44 | cmake . 45 | 46 | * Compile: 47 | 48 | make 49 | 50 | * Install: 51 | 52 | make install 53 | 54 | ### Windows 55 | Run the installer, e.g. `ctodo-1.1-setup.exe`. 56 | 57 | ## Usage 58 | ### Linux 59 | Run from a console using: 60 | 61 | ctodo 62 | 63 | This will create a `todo.txt` file in the current 64 | directory or open the file if it exists. 65 | 66 | Alternatively you can run ctodo using: 67 | 68 | ctodo someotherfile.txt 69 | 70 | Which will open/create `someotherfile.txt` in the current directory. 71 | 72 | ### Windows 73 | Right click in any directory (or on the desktop) and open the `New`-menu. If ctodo was installed 74 | using the installer, the option `Task List` should be available. Click on it. 75 | 76 | This will create an empty task list which you can open with ctodo by double clicking on it 77 | (if you installed using the installer). 78 | 79 | ## Commands 80 | * Navigation: 81 | 82 | Use UP and DOWN arrows to move between tasks 83 | (or J and K). 84 | 85 | PAGE UP moves 5 tasks up and PAGE DOWN moves 86 | 5 tasks down. 87 | 88 | HOME and g moves to the top of the list and 89 | END and G moves to the bottom. 90 | 91 | * Managing tasks: 92 | 93 | SPACE and ENTER checks/unchecks the selected task. 94 | 95 | D and DELETE deletes the selected task. 96 | 97 | SHIFT-UP and M moves the selected task up and SHIFT-DOWN and SHIFT-M 98 | moves the selected task down. 99 | 100 | Press E to edit the description of the selected task. 101 | 102 | Press SHIFT-E to delete the current task description 103 | and create a new one. 104 | 105 | Press N, SHIFT-A or INSERT to create a new 106 | task at the bottom of the list. 107 | 108 | Press SHIFT-I to insert a new task at the top of the list. 109 | 110 | Press I to insert a new task before the selected task, and 111 | A to insert a new task after the selected task. 112 | 113 | * In input-mode: 114 | 115 | Press ENTER to save the string. 116 | 117 | Press CTRL-C or CTRL-D to cancel. 118 | 119 | * Managing the task list: 120 | 121 | Press T to edit the title of the task list. 122 | 123 | Press SHIFT-T to delete the current title and create a new one. 124 | 125 | Press S to save the list. 126 | 127 | Press R to reload the list (discard unsaved data). 128 | 129 | Press Q to save the list and quit. 130 | -------------------------------------------------------------------------------- /src/task.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | #include "task.h" 11 | 12 | void delete_task(TASK *delete, TODOLIST *list) { 13 | if (delete->prev) { 14 | delete->prev->next = delete->next; 15 | } 16 | else { 17 | list->first = delete->next; 18 | } 19 | if (delete->next) { 20 | delete->next->prev = delete->prev; 21 | } 22 | else { 23 | list->last = delete->prev; 24 | } 25 | free(delete->message); 26 | free(delete); 27 | } 28 | 29 | void add_task(TODOLIST *list, char *message, int done, int priority) { 30 | TASK *task = (TASK *)malloc(sizeof(TASK)); 31 | if (!task) { 32 | return; 33 | } 34 | task->message = message; 35 | task->done = done; 36 | task->priority = priority; 37 | task->next = NULL; 38 | task->prev = list->last; 39 | if (!list->first) { 40 | list->first = task; 41 | } 42 | else { 43 | list->last->next = task; 44 | } 45 | list->last = task; 46 | } 47 | 48 | void insert_task(TODOLIST *list, TASK *next, char *message, int done, int priority) { 49 | TASK *task = (TASK *)malloc(sizeof(TASK)); 50 | if (!task) { 51 | return; 52 | } 53 | task->message = message; 54 | task->done = done; 55 | task->priority = priority; 56 | task->next = next; 57 | task->prev = next->prev; 58 | if (list->first == next) { 59 | list->first = task; 60 | } 61 | else { 62 | next->prev->next = task; 63 | } 64 | next->prev = task; 65 | } 66 | 67 | void move_task_up(TODOLIST *list, TASK *task) { 68 | if (task->prev) { 69 | TASK *prev = task->prev; 70 | task->prev = prev->prev; 71 | if (task->prev) { 72 | task->prev->next = task; 73 | } 74 | else { 75 | list->first = task; 76 | } 77 | prev->prev = task; 78 | prev->next = task->next; 79 | if (prev->next) { 80 | prev->next->prev = prev; 81 | } 82 | else { 83 | list->last = prev; 84 | } 85 | task->next = prev; 86 | } 87 | } 88 | 89 | void move_task_down(TODOLIST *list, TASK *task) { 90 | if (task->next) { 91 | TASK *next = task->next; 92 | task->next = next->next; 93 | if (task->next) { 94 | task->next->prev = task; 95 | } 96 | else { 97 | list->last = task; 98 | } 99 | next->next = task; 100 | next->prev = task->prev; 101 | if (next->prev) { 102 | next->prev->next = next; 103 | } 104 | else { 105 | list->first = next; 106 | } 107 | task->prev = next; 108 | } 109 | } 110 | 111 | char *get_option(TODOLIST *todolist, const char *key) { 112 | OPTION *opt = todolist->first_option; 113 | while (opt) { 114 | if (strcmp(key, opt->key) == 0) { 115 | return opt->value; 116 | } 117 | opt = opt->next; 118 | } 119 | return NULL; 120 | } 121 | 122 | char *copy_option(TODOLIST *todolist, const char *key) { 123 | char *copy = NULL; 124 | char *value = get_option(todolist, key); 125 | if (value) { 126 | copy = (char *)malloc(strlen(value) + 1); 127 | strcpy(copy, value); 128 | } 129 | return copy; 130 | } 131 | 132 | int get_option_bit(TODOLIST *todolist, const char *key) { 133 | char *value = get_option(todolist, key); 134 | if (value == NULL) { 135 | return 0; 136 | } 137 | return *value != '0'; 138 | } 139 | 140 | void set_option(TODOLIST *list, const char *key, const char *value) { 141 | OPTION *opt = list->first_option; 142 | while (opt) { 143 | if (strcmp(key, opt->key) == 0) { 144 | free(opt->value); 145 | opt->value = (char *)malloc(strlen(value) + 1); 146 | strcpy(opt->value, value); 147 | return; 148 | } 149 | opt = opt->next; 150 | } 151 | opt = (OPTION *)malloc(sizeof(OPTION)); 152 | if (!opt) { 153 | return; 154 | } 155 | opt->key = (char *)malloc(strlen(key) + 1); 156 | strcpy(opt->key, key); 157 | opt->value = (char *)malloc(strlen(value) + 1); 158 | strcpy(opt->value, value); 159 | opt->next = NULL; 160 | if (!list->first_option) { 161 | list->first_option = opt; 162 | } 163 | else { 164 | list->last_option->next = opt; 165 | } 166 | list->last_option = opt; 167 | } 168 | 169 | void set_option_bit(TODOLIST *todolist, const char *key, int bit) { 170 | char *value = (char *)malloc(2); 171 | value[0] = bit ? '1' : '0'; 172 | value[1] = '\0'; 173 | set_option(todolist, key, value); 174 | } 175 | 176 | void delete_todolist(TODOLIST *todolist) { 177 | TASK *task = todolist->first; 178 | OPTION *opt = todolist->first_option; 179 | TASK *temp = NULL; 180 | OPTION *temp_opt = NULL; 181 | while (task) { 182 | temp = task; 183 | task = task->next; 184 | free(temp->message); 185 | free(temp); 186 | } 187 | while (opt) { 188 | temp_opt = opt; 189 | opt = opt->next; 190 | free(temp_opt->key); 191 | free(temp_opt->value); 192 | free(temp_opt); 193 | } 194 | free(todolist->title); 195 | free(todolist); 196 | } 197 | 198 | -------------------------------------------------------------------------------- /src/edit.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2017 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | #include "edit.h" 11 | #include "error.h" 12 | #include "stream.h" 13 | 14 | COMMAND edit_commands[] = { 15 | {"^C", "Cancel"}, 16 | {NULL, NULL} 17 | }; 18 | 19 | void print_commands(COMMAND *commands, int y, int width, int height) { 20 | size_t kwidth; 21 | int i; 22 | int show = width / 11; 23 | int cwidth = width / show; 24 | for (i = 0; i < height; i++) { 25 | move(y + i, 0); 26 | clrtoeol(); 27 | } 28 | for (i = 0; commands[i].key != NULL && i < show; i++) { 29 | attron(A_REVERSE); 30 | kwidth = strlen(commands[i].key); 31 | mvprintw(y + i % height, i / height * cwidth, "%s", commands[i].key); 32 | attroff(A_REVERSE); 33 | mvprintw(y + i % height, i / height * cwidth + kwidth, " %s", commands[i].func); 34 | } 35 | } 36 | 37 | char *get_input(char *prompt) { 38 | return get_input_edit(prompt, NULL); 39 | } 40 | 41 | char *get_input_edit(char *prompt, char *buffer) { 42 | int rows, cols, i, j, ch, bc = 0, cc; 43 | int lineoffset, linesize, buffersize; 44 | int bufferlines = 1, cursorpos = 0; 45 | char *newbuffer = NULL; 46 | getmaxyx(stdscr, rows, cols); 47 | print_commands(edit_commands, rows - 1, cols, 1); 48 | lineoffset = strlen(prompt) + 1; 49 | linesize = cols - lineoffset; 50 | buffersize = linesize * bufferlines; 51 | if (!buffer) { 52 | buffer = (char *)malloc(buffersize); 53 | } 54 | else { 55 | bufferlines = strlen(buffer) / linesize + 1; 56 | buffersize = linesize * bufferlines; 57 | newbuffer = (char *)malloc(buffersize); 58 | if (!newbuffer) { 59 | error("Could not increase size of buffer"); 60 | return NULL; 61 | } 62 | bc = strlen(buffer); 63 | memcpy(newbuffer, buffer, bc); 64 | buffer = newbuffer; 65 | cursorpos = bc; 66 | } 67 | if (!buffer) { 68 | error("Could not create buffer"); 69 | return NULL; 70 | } 71 | attron(A_REVERSE); 72 | curs_set(2); 73 | while (1) { 74 | mvprintw(rows - 1 - bufferlines, 0, "%s:", prompt); 75 | cc = 0; 76 | for (i = 0; i < cols - lineoffset; i++, cc++) { 77 | if (cc < bc) { 78 | addch(buffer[cc]); 79 | } 80 | else { 81 | addch(' '); 82 | } 83 | } 84 | for (i = 1; i < bufferlines; i++) { 85 | for (j = 0; j < cols; j++) { 86 | if (j >= lineoffset) { 87 | if (cc < bc) { 88 | addch(buffer[cc++]); 89 | continue; 90 | } 91 | cc++; 92 | } 93 | addch(' '); 94 | } 95 | } 96 | move(rows - 1 - bufferlines + cursorpos / linesize, lineoffset + (cursorpos % linesize)); 97 | refresh(); 98 | ch = getch(); 99 | if (ch == 10) { /* Enter */ 100 | break; 101 | } 102 | if (ch == 4 || ch == 3 || ch == 27) { /* ^D , ^C or ESC */ 103 | buffer[0] = 0; 104 | break; 105 | } 106 | switch (ch) { 107 | case 2: /* ^B */ 108 | case KEY_LEFT: 109 | if (cursorpos > 0) 110 | cursorpos--; 111 | break; 112 | case 6: /* ^F */ 113 | case KEY_RIGHT: 114 | if (cursorpos < bc) 115 | cursorpos++; 116 | break; 117 | case 1: /* ^A */ 118 | case 262: /* home */ 119 | cursorpos = 0; 120 | break; 121 | case 5: /* ^E */ 122 | case 360: /* end */ 123 | cursorpos = bc; 124 | break; 125 | case 8: /* backspace */ 126 | case 127: /* also backspace */ 127 | case 263: /* also backspace (in xterm?) */ 128 | if (cursorpos > 0) { 129 | cursorpos--; 130 | memcpy(buffer + cursorpos, buffer + cursorpos + 1, bc - cursorpos - 1); 131 | if (bc > 0) 132 | bc--; 133 | } 134 | break; 135 | case 330: /* delete */ 136 | if (cursorpos < bc) { 137 | memcpy(buffer + cursorpos, buffer + cursorpos + 1, bc - cursorpos - 1); 138 | if (bc > 0) 139 | bc--; 140 | } 141 | break; 142 | default: 143 | if (ch > 31 && ch < 127) { 144 | if (cursorpos == bc) { 145 | buffer[bc++] = ch; 146 | } 147 | else { 148 | memcpy(buffer + cursorpos + 1, buffer + cursorpos, bc - cursorpos); 149 | buffer[cursorpos] = ch; 150 | bc++; 151 | } 152 | if (bc >= buffersize) { 153 | bufferlines++; 154 | newbuffer = resize_buffer(buffer, buffersize, buffersize + linesize); 155 | if (!newbuffer) { 156 | free(buffer); 157 | error("Could not increase size of buffer"); 158 | return NULL; 159 | } 160 | buffer = newbuffer; 161 | buffersize += linesize; 162 | } 163 | cursorpos++; 164 | } 165 | break; 166 | } 167 | } 168 | buffer[bc] = 0; 169 | curs_set(0); 170 | attroff(A_REVERSE); 171 | return buffer; 172 | } 173 | -------------------------------------------------------------------------------- /src/wedit.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2017 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "wedit.h" 14 | #include "error.h" 15 | #include "stream.h" 16 | 17 | int input_available = 0; 18 | unsigned char next_input; 19 | size_t prompt_l; 20 | int old_lines = 0; 21 | int stop = 0; 22 | char *input_buffer = NULL; 23 | 24 | COMMAND edit_commands[] = { 25 | {"^C", "Cancel"}, 26 | {NULL, NULL} 27 | }; 28 | 29 | void print_commands(COMMAND *commands, int y, int width, int height) { 30 | size_t kwidth; 31 | int i; 32 | int show = width / 11; 33 | int cwidth = width / show; 34 | for (i = 0; i < height; i++) { 35 | move(y + i, 0); 36 | clrtoeol(); 37 | } 38 | for (i = 0; commands[i].key != NULL && i < show; i++) { 39 | attron(A_REVERSE); 40 | kwidth = strlen(commands[i].key); 41 | mvprintw(y + i % height, i / height * cwidth, "%s", commands[i].key); 42 | attroff(A_REVERSE); 43 | mvprintw(y + i % height, i / height * cwidth + kwidth, " %s", commands[i].func); 44 | } 45 | } 46 | 47 | char *get_input(const char *prompt) { 48 | return get_input_edit(prompt, NULL); 49 | } 50 | 51 | /* inspired by https://github.com/ulfalizer/readline-and-ncurses */ 52 | 53 | int has_input() { 54 | return input_available; 55 | } 56 | 57 | int pop_input(FILE *f) { 58 | input_available = 0; 59 | return next_input; 60 | } 61 | 62 | void push_input(char c) { 63 | next_input = c; 64 | input_available = 1; 65 | rl_callback_read_char(); 66 | } 67 | 68 | size_t strnlen(const char *str, size_t max) { 69 | size_t i; 70 | for(i = 0; i < max && str[i]; i++); 71 | return i; 72 | } 73 | 74 | size_t display_width(const char *str, size_t n, size_t line_width, size_t cursor_point, size_t *cursor_pos) { 75 | mbstate_t shift_state; 76 | wchar_t wc; 77 | int cursor_set = 0; 78 | size_t wc_len; 79 | size_t width = prompt_l + 2; 80 | WINDOW *win = newwin(1, 10, 0, 0); 81 | memset(&shift_state, '\0', sizeof shift_state); 82 | for (size_t i = 0; i < n; i += wc_len) { 83 | if (!cursor_set && i >= cursor_point) { 84 | *cursor_pos = width; 85 | cursor_set = 1; 86 | } 87 | wc_len = mbrtowc(&wc, str + i, MB_CUR_MAX, &shift_state); 88 | if (wc_len == (size_t)-1 || wc_len == (size_t)-2) { 89 | width += strnlen(str + i, n - i); 90 | wc_len = 0; 91 | } 92 | if (!wc_len) { 93 | break; 94 | } 95 | if (wc == '\t') { 96 | width += 8 - width % line_width % 8; 97 | } 98 | else if (iswcntrl(wc)) { 99 | width += 2; 100 | } 101 | else { 102 | /* finds the display width of the character by letting ncurses print it */ 103 | mvwprintw(win, 0, 0, "%C", wc); 104 | int x, y; 105 | getyx(win, y, x); 106 | width += x; 107 | (void)y; 108 | } 109 | } 110 | delwin(win); 111 | if (!cursor_set) { 112 | *cursor_pos = width; 113 | } 114 | return width; 115 | } 116 | 117 | void show_buffer() { 118 | int rows, cols; 119 | getmaxyx(stdscr, rows, cols); 120 | size_t pos; 121 | size_t buffer_size = display_width(rl_line_buffer, rl_end, cols, rl_point, &pos); 122 | int lines = buffer_size / cols + 1; 123 | if (lines < old_lines) { 124 | for (int i = lines; i < old_lines; i++) { 125 | move(rows - i - 2, 0); 126 | clrtoeol(); 127 | } 128 | } 129 | old_lines = lines; 130 | int cursor_row = pos / cols; 131 | int cursor_col = pos % cols; 132 | int top = rows - lines - 1; 133 | print_commands(edit_commands, rows - 1, cols, 1); 134 | attron(A_REVERSE); 135 | for (int i = 0; i < lines; i++) { 136 | for (int j = 0; j < cols; j++) { 137 | mvprintw(top + i, j, " "); 138 | } 139 | } 140 | mvprintw(top, 0, "%s: %s", rl_display_prompt, rl_line_buffer); 141 | move(top + cursor_row, cursor_col); 142 | attroff(A_REVERSE); 143 | } 144 | 145 | void callback(char *line) { 146 | if (line == NULL) { 147 | input_buffer = malloc(1); 148 | input_buffer[0] = 0; 149 | } 150 | else { 151 | input_buffer = line; 152 | } 153 | stop = 1; 154 | } 155 | 156 | char *get_input_edit(const char *prompt, const char *buffer) { 157 | prompt_l = strlen(prompt); 158 | rl_bind_key('\t', rl_insert); /* disables completion */ 159 | /* TODO: disable history as well? */ 160 | rl_catch_signals = 0; 161 | rl_catch_sigwinch = 0; 162 | rl_deprep_term_function = NULL; 163 | rl_prep_term_function = NULL; 164 | rl_change_environment = 0; 165 | rl_getc_function = pop_input; 166 | rl_input_available_hook = has_input; 167 | rl_redisplay_function = show_buffer; 168 | rl_callback_handler_install(prompt, callback); 169 | 170 | if (buffer) { 171 | while (*buffer) { 172 | push_input(*(buffer++)); 173 | } 174 | } 175 | 176 | curs_set(2); 177 | keypad(stdscr, 0); 178 | stop = 0; 179 | input_buffer = NULL; 180 | while (!stop) { 181 | int c = wgetch(stdscr); 182 | switch (c) { 183 | case KEY_RESIZE: 184 | show_buffer(); 185 | break; 186 | case 3: /* ^C */ 187 | case 4: /* ^D */ 188 | input_buffer = malloc(1); 189 | input_buffer[0] = 0; 190 | stop = 1; 191 | break; 192 | case 27: /* ESC */ 193 | /* TODO: the old editor would close on ESC, however when the keypad is 194 | * disabled (necessary when forwarding key presses to ncurses), an ESC 195 | * character precedes any escape sequence (e.g. arrow keys etc.). */ 196 | default: 197 | push_input(c); 198 | } 199 | } 200 | keypad(stdscr, 1); 201 | curs_set(0); 202 | rl_callback_handler_remove(); 203 | return input_buffer; 204 | } 205 | -------------------------------------------------------------------------------- /src/stream.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "stream.h" 13 | 14 | #define STREAM_TYPE_FILE 1 15 | #define STREAM_TYPE_BUFFER 2 16 | 17 | struct STREAM { 18 | int type; 19 | size_t length; 20 | size_t pos; 21 | void *obj; 22 | }; 23 | 24 | STREAM *stream_file(const char *filename, const char *mode) { 25 | FILE *file = fopen(filename, mode); 26 | STREAM *stream = NULL; 27 | if (!file) { 28 | return NULL; 29 | } 30 | stream = (STREAM *)malloc(sizeof(STREAM)); 31 | stream->type = STREAM_TYPE_FILE; 32 | stream->obj = file; 33 | return stream; 34 | } 35 | 36 | STREAM *stream_buffer(char *buffer, size_t length) { 37 | STREAM *stream = (STREAM *)malloc(sizeof(STREAM)); 38 | stream->type = STREAM_TYPE_BUFFER; 39 | stream->obj = buffer; 40 | stream->length = length; 41 | stream->pos = 0; 42 | return stream; 43 | } 44 | 45 | char *stream_get_content(STREAM *stream) { 46 | switch (stream->type) { 47 | case STREAM_TYPE_FILE: 48 | return NULL; 49 | case STREAM_TYPE_BUFFER: 50 | return stream->obj; 51 | default: 52 | return NULL; 53 | } 54 | } 55 | 56 | size_t stream_get_size(STREAM *stream) { 57 | switch (stream->type) { 58 | case STREAM_TYPE_FILE: 59 | return 0; 60 | case STREAM_TYPE_BUFFER: 61 | return stream->length; 62 | default: 63 | return 0; 64 | } 65 | } 66 | 67 | void stream_close(STREAM *stream) { 68 | switch (stream->type) { 69 | case STREAM_TYPE_FILE: 70 | fclose(stream->obj); 71 | break; 72 | case STREAM_TYPE_BUFFER: 73 | break; 74 | } 75 | free(stream); 76 | } 77 | 78 | char *resize_buffer(char *buffer, size_t oldsize, size_t newsize) { 79 | char *new = NULL; 80 | if (newsize < oldsize) { 81 | return NULL; 82 | } 83 | new = (char *)malloc(newsize); 84 | if (!new) { 85 | return NULL; 86 | } 87 | memcpy(new, buffer, oldsize); 88 | free(buffer); 89 | return new; 90 | } 91 | 92 | size_t stream_read(void *ptr, size_t size, size_t nmemb, STREAM *input) { 93 | size_t bytes, remaining; 94 | switch (input->type) { 95 | case STREAM_TYPE_FILE: 96 | return fread(ptr, size, nmemb, input->obj); 97 | case STREAM_TYPE_BUFFER: 98 | bytes = size * nmemb; 99 | remaining = input->length - input->pos; 100 | if (remaining <= 0) { 101 | return 0; 102 | } 103 | else if (remaining < bytes) { 104 | char *src = (char *)input->obj + input->pos; 105 | memcpy(ptr, src, remaining); 106 | input->pos += remaining; 107 | return remaining; 108 | } 109 | else { 110 | char *src = (char *)input->obj + input->pos; 111 | memcpy(ptr, src, bytes); 112 | input->pos += bytes; 113 | return bytes; 114 | } 115 | default: 116 | return 0; 117 | } 118 | } 119 | 120 | int stream_getc(STREAM *input) { 121 | switch (input->type) { 122 | case STREAM_TYPE_FILE: 123 | return fgetc(input->obj); 124 | case STREAM_TYPE_BUFFER: 125 | if (input->pos >= input->length) { 126 | return EOF; 127 | } 128 | char *buffer = (char *)input->obj; 129 | return buffer[input->pos++]; 130 | default: 131 | return EOF; 132 | } 133 | } 134 | 135 | void stream_ungetc(int c, STREAM *input) { 136 | switch (input->type) { 137 | case STREAM_TYPE_FILE: 138 | ungetc(c, input->obj); 139 | break; 140 | case STREAM_TYPE_BUFFER: 141 | if (input->pos > input->length) { 142 | input->pos = input->length; 143 | } 144 | char *buffer = (char *)input->obj; 145 | buffer[--input->pos] = (char) c; 146 | break; 147 | } 148 | } 149 | 150 | int stream_eof(STREAM *input) { 151 | switch (input->type) { 152 | case STREAM_TYPE_FILE: 153 | return feof((FILE *)input->obj); 154 | case STREAM_TYPE_BUFFER: 155 | return input->pos >= input->length; 156 | default: 157 | return 1; 158 | } 159 | } 160 | 161 | int stream_putc(int c, STREAM *output) { 162 | unsigned char ch = (unsigned char)c; 163 | switch (output->type) { 164 | case STREAM_TYPE_FILE: 165 | return fputc(c, output->obj); 166 | case STREAM_TYPE_BUFFER: 167 | if (output->pos >= output->length) { 168 | output->obj = resize_buffer(output->obj, output->length, output->length + 100); 169 | output->length += 100; 170 | } 171 | ((char *)output->obj)[output->pos++] = ch; 172 | return ch; 173 | default: 174 | return EOF; 175 | } 176 | } 177 | 178 | int stream_vprintf(STREAM *output, const char *format, va_list va) { 179 | int status = 0, n; 180 | va_list va2; 181 | size_t size; 182 | switch (output->type) { 183 | case STREAM_TYPE_FILE: 184 | va_copy(va2, va); 185 | status = vfprintf(output->obj, format, va2); 186 | va_end(va2); 187 | break; 188 | case STREAM_TYPE_BUFFER: 189 | size = output->length - output->pos; 190 | if (size <= 0) { 191 | output->obj = resize_buffer(output->obj, output->length, output->length + 100); 192 | output->length += 100; 193 | } 194 | while (1) { 195 | char *dest = (char *)output->obj + output->pos; 196 | va_copy(va2, va); 197 | n = vsnprintf(dest, size, format, va2); 198 | va_end(va2); 199 | if (n < 0) { 200 | return 0; 201 | } 202 | if (n < size) { 203 | output->pos += n; 204 | break; 205 | } 206 | size = n + 1; 207 | output->obj = resize_buffer(output->obj, output->length, size + output->pos); 208 | output->length = size + output->pos; 209 | } 210 | break; 211 | } 212 | return status; 213 | } 214 | 215 | int stream_printf(STREAM *output, const char *format, ...) { 216 | va_list va; 217 | int status; 218 | va_start(va, format); 219 | status = stream_vprintf(output, format, va); 220 | va_end(va); 221 | return status; 222 | } 223 | 224 | char *string_vprintf(const char *format, va_list va) { 225 | size_t size = 32; 226 | char *buffer = (char *)malloc(size); 227 | va_list va2; 228 | STREAM *stream = stream_buffer(buffer, size); 229 | va_copy(va2, va); 230 | stream_vprintf(stream, format, va2); 231 | va_end(va2); 232 | buffer = stream_get_content(stream); 233 | stream_close(stream); 234 | return buffer; 235 | } 236 | 237 | char *string_printf(const char *format, ...) { 238 | char *result = NULL; 239 | va_list va; 240 | va_start(va, format); 241 | result = string_vprintf(format, va); 242 | va_end(va); 243 | return result; 244 | } 245 | -------------------------------------------------------------------------------- /src/file.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "file.h" 14 | #include "stream.h" 15 | #include "error.h" 16 | 17 | void skip_whitespace(STREAM *file) { 18 | int c; 19 | while ((c = stream_getc(file)) != EOF && isspace(c)) { 20 | } 21 | stream_ungetc(c, file); 22 | } 23 | 24 | void skip_horizontal_whitespace(STREAM *file) { 25 | int c; 26 | while ((c = stream_getc(file)) != EOF && isspace(c) && c != '\n') { 27 | } 28 | stream_ungetc(c, file); 29 | } 30 | 31 | void skip_line(STREAM *file) { 32 | int c; 33 | while ((c = stream_getc(file)) != EOF && c != '\n') { 34 | } 35 | stream_ungetc(c, file); 36 | } 37 | 38 | /* Read until the end of the line or EOF. */ 39 | char *read_string(STREAM *file) { 40 | int buffersize = 10; 41 | int c, i = 0; 42 | char *newbuffer = NULL; 43 | char *buffer = (char *)malloc(buffersize); 44 | while ((c = stream_getc(file)) != EOF && c != '\n') { 45 | buffer[i++] = c; 46 | if (i >= buffersize) { 47 | newbuffer = resize_buffer(buffer, buffersize, buffersize + buffersize); 48 | if (!newbuffer) { 49 | free(buffer); 50 | return NULL; 51 | } 52 | buffer = newbuffer; 53 | buffersize += buffersize; 54 | } 55 | } 56 | buffer[i] = 0; 57 | return buffer; 58 | } 59 | 60 | /* Read until the next space, '=', EOL, or EOF. Escape using backslash. */ 61 | char *read_option_string(STREAM *file) { 62 | int buffersize = 10; 63 | int c, i = 0; 64 | char *newbuffer = NULL; 65 | char *buffer = (char *)malloc(buffersize); 66 | while ((c = stream_getc(file)) != EOF && c != '\n' && c != '=' && c > ' ') { 67 | if (c == '\\') { 68 | c = stream_getc(file); 69 | if (c == EOF) { 70 | break; 71 | } 72 | } 73 | buffer[i++] = c; 74 | if (i >= buffersize) { 75 | newbuffer = resize_buffer(buffer, buffersize, buffersize + buffersize); 76 | if (!newbuffer) { 77 | free(buffer); 78 | return NULL; 79 | } 80 | buffer = newbuffer; 81 | buffersize += buffersize; 82 | } 83 | } 84 | stream_ungetc(c, file); 85 | buffer[i] = 0; 86 | return buffer; 87 | } 88 | 89 | void read_next_option(STREAM *file, TODOLIST *list) { 90 | char c; 91 | char *key = NULL; 92 | char *value = NULL; 93 | while (1) { 94 | skip_horizontal_whitespace(file); 95 | key = read_option_string(file); 96 | if (key[0] == '\0') { 97 | free(key); 98 | skip_line(file); 99 | return; 100 | } 101 | skip_horizontal_whitespace(file); 102 | c = stream_getc(file); 103 | if (c != '=') { 104 | stream_ungetc(c, file); 105 | set_option_bit(list, key, 1); 106 | continue; 107 | } 108 | skip_horizontal_whitespace(file); 109 | value = read_option_string(file); 110 | set_option(list, key, value); 111 | free(key); 112 | free(value); 113 | } 114 | } 115 | 116 | void read_next_task(STREAM *file, TODOLIST *list) { 117 | char c; 118 | char *message = NULL; 119 | int done = 0; 120 | int priority = 0; 121 | skip_whitespace(file); 122 | c = stream_getc(file); 123 | if (c != '[') { 124 | if (c == '#') { 125 | read_next_option(file, list); 126 | skip_line(file); 127 | return; 128 | } 129 | stream_ungetc(c, file); 130 | skip_line(file); 131 | return; 132 | } 133 | c = stream_getc(file); 134 | switch (c) { 135 | case 'X': 136 | case 'x': 137 | done = 1; 138 | break; 139 | case ' ': 140 | done = 0; 141 | break; 142 | default: 143 | stream_ungetc(c, file); 144 | skip_line(file); 145 | return; 146 | } 147 | c = stream_getc(file); 148 | if (c != ']') { 149 | stream_ungetc(c, file); 150 | skip_line(file); 151 | return; 152 | } 153 | skip_whitespace(file); 154 | message = read_string(file); 155 | add_task(list, message, done, priority); 156 | } 157 | 158 | TODOLIST *read_todolist(STREAM *input) { 159 | TODOLIST *list = (TODOLIST *)malloc(sizeof(TODOLIST)); 160 | if (!list) { 161 | error("Could not allocate memory"); 162 | return NULL; 163 | } 164 | list->first = NULL; 165 | list->last = NULL; 166 | list->first_option = NULL; 167 | list->last_option = NULL; 168 | list->title = read_string(input); 169 | while (!stream_eof(input)) { 170 | read_next_task(input, list); 171 | } 172 | return list; 173 | } 174 | 175 | int touch_file(char *filename) { 176 | FILE *file = fopen(filename, "w+"); 177 | if (!file) { 178 | return 0; 179 | } 180 | fclose(file); 181 | return 1; 182 | } 183 | 184 | TODOLIST *load_todolist(char *filename) { 185 | TODOLIST *list = NULL; 186 | STREAM *file = stream_file(filename, "r"); 187 | if (!file) { 188 | if (touch_file(filename)) { 189 | return load_todolist(filename); 190 | } 191 | else { 192 | error("%s", strerror(errno)); 193 | return NULL; 194 | } 195 | } 196 | list = read_todolist(file); 197 | stream_close(file); 198 | return list; 199 | } 200 | 201 | TODOLIST *parse_todolist(char *source, size_t length) { 202 | TODOLIST *list = NULL; 203 | STREAM *input = stream_buffer(source, length); 204 | list = read_todolist(input); 205 | stream_close(input); 206 | return list; 207 | } 208 | 209 | size_t escape_string(STREAM *output, const char *str) { 210 | size_t l = 0; 211 | char c; 212 | while ((c = *str) != '\0') { 213 | if (c == ' ' || c == '\\' || c == '\n' || c == '=') { 214 | stream_putc('\\', output); 215 | } 216 | stream_putc(c, output); 217 | str++; 218 | } 219 | return l; 220 | } 221 | 222 | void write_todolist(STREAM *output, TODOLIST *todolist) { 223 | OPTION *opt = NULL; 224 | TASK *task = NULL; 225 | int width = 0; 226 | stream_printf(output, "%s\n", todolist->title); 227 | task = todolist->first; 228 | while (task) { 229 | stream_printf(output, "[%c] %s\n", 230 | task->done ? 'X' : ' ', task->message); 231 | task = task->next; 232 | } 233 | opt = todolist->first_option; 234 | if (opt) { 235 | width += stream_printf(output, "#"); 236 | while (opt) { 237 | if (width >= 80) { 238 | stream_printf(output, "\n#"); 239 | width = 1; 240 | } 241 | width += 2; 242 | stream_putc(' ', output); 243 | width += escape_string(output, opt->key); 244 | stream_putc('=', output); 245 | width += escape_string(output, opt->value); 246 | opt = opt->next; 247 | } 248 | stream_printf(output, "\n"); 249 | } 250 | } 251 | 252 | char *stringify_todolist(TODOLIST *todolist) { 253 | size_t size = 100; 254 | char *str = (char *)malloc(size); 255 | STREAM *output = stream_buffer(str, size); 256 | write_todolist(output, todolist); 257 | stream_printf(output, "\0"); 258 | str = stream_get_content(output); 259 | stream_close(output); 260 | return str; 261 | } 262 | 263 | int save_todolist(TODOLIST *todolist, char *filename) { 264 | STREAM *file = stream_file(filename, "w"); 265 | if (!file) { 266 | error("%s", strerror(errno)); 267 | return 0; 268 | } 269 | write_todolist(file, todolist); 270 | stream_close(file); 271 | return 1; 272 | } 273 | -------------------------------------------------------------------------------- /src/ctodo.c: -------------------------------------------------------------------------------- 1 | /* ctodo 2 | * Copyright (c) 2016 Niels Sonnich Poulsen (http://nielssp.dk) 3 | * Licensed under the MIT license. 4 | * See the LICENSE file or http://opensource.org/licenses/MIT for more information. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "config.h" 16 | #include "task.h" 17 | #include "file.h" 18 | #include "stream.h" 19 | #include "error.h" 20 | 21 | #ifdef READLINE_ENABLE 22 | #include "wedit.h" 23 | #else 24 | #include "edit.h" 25 | #endif 26 | 27 | #ifdef SYNC_ENABLE 28 | #include "sync.h" 29 | #endif 30 | 31 | #define STATUS_SAVED "Saved" 32 | #define STATUS_UNSAVED "Unsaved" 33 | 34 | COMMAND main_commands[] = { 35 | {"Q", "Quit"}, 36 | {"S", "Save"}, 37 | {"R", "Reload"}, 38 | {"N", "New"}, 39 | {"E", "Edit"}, 40 | {"D", "Delete"}, 41 | {"T", "Title"}, 42 | {NULL, NULL} 43 | }; 44 | 45 | int print_multiline(int top, int offset, const char *str, size_t max_width) { 46 | mbstate_t shift_state; 47 | wchar_t wc; 48 | size_t x = offset; 49 | size_t y = top; 50 | size_t wc_len; 51 | size_t n = strlen(str); 52 | memset(&shift_state, '\0', sizeof shift_state); 53 | for (size_t i = 0; i < n; i += wc_len) { 54 | wc_len = mbrtowc(&wc, str + i, MB_CUR_MAX, &shift_state); 55 | if (!wc_len || wc_len == (size_t)-1 || wc_len == (size_t)-2) { 56 | break; 57 | } 58 | mvprintw(y, x, "%C", wc); 59 | size_t new_x, new_y; 60 | getyx(stdscr, new_y, new_x); 61 | if (new_y > y) { 62 | y = new_y; 63 | if (new_x > 0) { 64 | move(new_y, 0); 65 | clrtoeol(); 66 | mvprintw(y, offset, "%C", wc); 67 | } 68 | x = offset + new_x; 69 | } 70 | else if (new_x - offset > max_width) { 71 | move(y, x); 72 | clrtoeol(); 73 | mvprintw(y + 1, offset, "%C", wc); 74 | getyx(stdscr, y, x); 75 | } 76 | else { 77 | x = new_x; 78 | } 79 | } 80 | if (x > offset || y == top) { 81 | return y - top + 1; 82 | } 83 | return y - top; 84 | } 85 | 86 | void print_message(char *format, ...) { 87 | int rows, cols, len, x; 88 | char *message; 89 | va_list va; 90 | va_start(va, format); 91 | message = string_vprintf(format, va); 92 | va_end(va); 93 | getmaxyx(stdscr, rows, cols); 94 | len = strlen(message); 95 | x = cols / 2 - (len + 4) / 2; 96 | if (x < 0) x = 0; 97 | move(rows - 2, 0); 98 | clrtoeol(); 99 | attron(A_REVERSE); 100 | mvprintw(rows - 2, x, "[ %s ]", message); 101 | free(message); 102 | attroff(A_REVERSE); 103 | } 104 | 105 | void print_bar(char *status, int rows, int cols, int tasks) { 106 | int i, x; 107 | print_commands(main_commands, rows - 1, cols, 1); 108 | attron(A_REVERSE); 109 | for (i = 0; i < cols; i++) { 110 | mvprintw(0, i, " "); 111 | } 112 | mvprintw(0, 2, "ctodo %s", CTODO_VERSION); 113 | x = cols / 2 - strlen(status) / 2; 114 | mvprintw(0, x, "%s", status); 115 | mvprintw(0, cols - 18, "%10d %stask%s", 116 | tasks, 117 | tasks == 1 ? " " : "", 118 | tasks == 1 ? "" : "s"); 119 | attroff(A_REVERSE); 120 | } 121 | 122 | void fatal_error() { 123 | mvprintw(2, 4, "%s", get_last_error()); 124 | refresh(); 125 | getch(); 126 | endwin(); 127 | exit(1); 128 | } 129 | 130 | int main(int argc, char *argv[]) { 131 | char *filename = "todo.txt"; 132 | char *input_text = NULL; 133 | char *origin = NULL; 134 | char *status = STATUS_SAVED; 135 | int rows, cols, ch, y, highlight = 0, i = 0, 136 | orows, ocols, top = 0, bottom = 0, full = 0; 137 | TASK *task = NULL; 138 | TASK *selected = NULL; 139 | TODOLIST *todolist = NULL; 140 | char *file_version = NULL; 141 | 142 | setlocale(LC_ALL, ""); 143 | initscr(); 144 | clear(); 145 | curs_set(0); 146 | noecho(); 147 | 148 | if (argc > 1) { 149 | filename = argv[1]; 150 | } 151 | todolist = load_todolist(filename); 152 | 153 | mvprintw(0, 0, " ctodo %s", CTODO_VERSION); 154 | 155 | if (!todolist) { 156 | error("Could not open %s: %s", filename, get_last_error()); 157 | fatal_error(); 158 | } 159 | 160 | file_version = copy_option(todolist, "version"); 161 | if (file_version) { 162 | /* TODO: compare versions */ 163 | free(file_version); 164 | } 165 | else { 166 | set_option(todolist, "version", CTODO_VERSION); 167 | } 168 | 169 | #ifdef SYNC_ENABLE 170 | if (get_option_bit(todolist, "autosync")) { 171 | origin = copy_option(todolist, "origin"); 172 | if (origin) { 173 | getmaxyx(stdscr, rows, cols); 174 | mvprintw(2, 5, "Synchronizing tasks..."); 175 | mvprintw(4, 5, "Downloading:"); 176 | print_multiline(5, 5, origin, cols - 9); 177 | refresh(); 178 | /* TODO: merge */ 179 | TODOLIST *new = pull_todolist(origin); 180 | clear(); 181 | if (new) { 182 | delete_todolist(todolist); 183 | todolist = new; 184 | } 185 | else { 186 | print_message("Synchronization failed: %s", get_last_error()); 187 | } 188 | } 189 | } 190 | #endif 191 | 192 | raw(); 193 | keypad(stdscr, 1); 194 | while (1) { 195 | orows = rows; 196 | ocols = cols; 197 | getmaxyx(stdscr, rows, cols); 198 | if (orows != rows || ocols != cols) { 199 | clear(); 200 | } 201 | y = 2; 202 | y += print_multiline(y, 4, todolist->title, cols - 8) + 1; 203 | task = todolist->first; 204 | if (highlight >= i) highlight = i - 1; 205 | if (highlight < 0) highlight = 0; 206 | if (highlight < top) top = highlight; 207 | if (highlight > bottom) top += highlight - bottom; 208 | i = 0; 209 | selected = NULL; 210 | if (top != 0) { 211 | mvprintw(y - 1, 2, " * "); 212 | } 213 | while (task) { 214 | if (i >= top && y < rows - 3) { 215 | if (highlight == i) { 216 | attron(A_REVERSE); 217 | selected = task; 218 | } 219 | mvprintw(y, 2, "[%c]", task->done ? 'X' : ' '); 220 | y += print_multiline(y, 6, task->message, cols - 10); 221 | if (highlight == i) { 222 | attroff(A_REVERSE); 223 | } 224 | bottom = i; 225 | } 226 | i++; 227 | task = task->next; 228 | } 229 | full = y >= rows - 3; 230 | if (bottom != i - 1) { 231 | mvprintw(y++, 2, " * "); 232 | } 233 | print_bar(status, rows, cols, i); 234 | refresh(); 235 | ch = getch(); 236 | 237 | switch (ch) { 238 | case 'k': 239 | case 'K': 240 | case KEY_UP: 241 | highlight--; 242 | if (highlight < top) 243 | clear(); 244 | break; 245 | case 'j': 246 | case 'J': 247 | case KEY_DOWN: 248 | highlight++; 249 | if (highlight > bottom) 250 | clear(); 251 | break; 252 | case 21: /* ^U */ 253 | case 339: /* page up */ 254 | highlight -= 5; 255 | if (highlight < top) 256 | clear(); 257 | break; 258 | case 4: /* ^D */ 259 | case 338: /* page down */ 260 | highlight += 5; 261 | if (highlight > bottom) 262 | clear(); 263 | break; 264 | case 'g': 265 | case 262: /* home */ 266 | highlight = 0; 267 | clear(); 268 | break; 269 | case 'G': 270 | case 360: /* end */ 271 | highlight = i - 1; 272 | clear(); 273 | break; 274 | case 'm': 275 | case 337: /* S-Up */ 276 | if (selected) { 277 | move_task_up(todolist, selected); 278 | highlight--; 279 | status = STATUS_UNSAVED; 280 | clear(); 281 | } 282 | break; 283 | case 'M': 284 | case 336: /* S-Down */ 285 | if (selected) { 286 | move_task_down(todolist, selected); 287 | highlight++; 288 | status = STATUS_UNSAVED; 289 | clear(); 290 | } 291 | break; 292 | #ifdef SYNC_ENABLE 293 | case 'z': /* TODO */ 294 | if (origin) { 295 | print_message("Synchronizing tasks..."); 296 | refresh(); 297 | if (!push_todolist(todolist, origin)) 298 | print_message("Synchronization failed: %s", get_last_error()); 299 | else 300 | print_message("Synchronization complete!"); 301 | } 302 | break; 303 | #endif 304 | case 'R': 305 | case 'r': 306 | i = 0; 307 | TODOLIST *new = load_todolist(filename); 308 | if (!new) { 309 | print_message("Could not load %s: %s", filename, get_last_error()); 310 | } 311 | else { 312 | delete_todolist(todolist); 313 | todolist = new; 314 | status = STATUS_SAVED; 315 | clear(); 316 | print_message("Reloaded"); 317 | } 318 | break; 319 | case 'S': 320 | case 's': 321 | if (save_todolist(todolist, filename)) { 322 | print_message("Saved"); 323 | status = STATUS_SAVED; 324 | } 325 | else { 326 | print_message("Could not save %s: %s", filename, get_last_error()); 327 | status = STATUS_UNSAVED; 328 | } 329 | break; 330 | case 'D': 331 | case 'd': 332 | case '-': 333 | case 330: /* del */ 334 | i--; 335 | if (selected) { 336 | delete_task(selected, todolist); 337 | } 338 | status = STATUS_UNSAVED; 339 | clear(); 340 | break; 341 | case 'i': 342 | input_text = get_input("Insert task"); 343 | if (!input_text) 344 | fatal_error(); 345 | if (input_text[0]) { 346 | if (selected) { 347 | insert_task(todolist, selected, input_text, 0, 0); 348 | } 349 | else { 350 | add_task(todolist, input_text, 0, 0); 351 | } 352 | status = STATUS_UNSAVED; 353 | i++; 354 | } 355 | else { 356 | free(input_text); 357 | } 358 | clear(); 359 | break; 360 | case 'I': 361 | input_text = get_input("Insert task"); 362 | if (!input_text) 363 | fatal_error(); 364 | if (input_text[0]) { 365 | if (todolist->first) { 366 | insert_task(todolist, todolist->first, input_text, 0, 0); 367 | } 368 | else { 369 | add_task(todolist, input_text, 0, 0); 370 | } 371 | status = STATUS_UNSAVED; 372 | highlight = 0; 373 | i++; 374 | } 375 | else { 376 | free(input_text); 377 | } 378 | clear(); 379 | break; 380 | case 'a': 381 | input_text = get_input("Append task"); 382 | if (!input_text) 383 | fatal_error(); 384 | if (input_text[0]) { 385 | if (selected && selected->next) { 386 | insert_task(todolist, selected->next, input_text, 0, 0); 387 | } 388 | else { 389 | add_task(todolist, input_text, 0, 0); 390 | } 391 | status = STATUS_UNSAVED; 392 | if (full && bottom == highlight && bottom < i) { 393 | top++; 394 | } 395 | bottom++; 396 | highlight++; 397 | i++; 398 | } 399 | else { 400 | free(input_text); 401 | } 402 | clear(); 403 | break; 404 | case 'A': 405 | case 'N': 406 | case 'n': 407 | case '+': 408 | case 331: /* ins */ 409 | input_text = get_input("Append task"); 410 | if (!input_text) 411 | fatal_error(); 412 | if (input_text[0]) { 413 | add_task(todolist, input_text, 0, 0); 414 | status = STATUS_UNSAVED; 415 | if (full && bottom < i) { 416 | top++; 417 | } 418 | highlight = i; 419 | bottom++; 420 | i++; 421 | } 422 | else { 423 | free(input_text); 424 | } 425 | clear(); 426 | break; 427 | case 'c': 428 | case 'E': 429 | if (!selected) { 430 | break; 431 | } 432 | input_text = get_input("Change task"); 433 | if (!input_text) 434 | fatal_error(); 435 | if (input_text[0]) { 436 | free(selected->message); 437 | selected->message = input_text; 438 | status = STATUS_UNSAVED; 439 | } 440 | else { 441 | free(input_text); 442 | } 443 | clear(); 444 | break; 445 | case 'e': 446 | if (!selected) { 447 | break; 448 | } 449 | input_text = get_input_edit("Edit task", selected->message); 450 | if (!input_text) 451 | fatal_error(); 452 | if (input_text[0]) { 453 | free(selected->message); 454 | selected->message = input_text; 455 | status = STATUS_UNSAVED; 456 | } 457 | else { 458 | free(input_text); 459 | } 460 | clear(); 461 | break; 462 | case 'T': 463 | input_text = get_input("Change title"); 464 | if (!input_text) 465 | fatal_error(); 466 | if (input_text[0]) { 467 | free(todolist->title); 468 | todolist->title = input_text; 469 | status = STATUS_UNSAVED; 470 | } 471 | else { 472 | free(input_text); 473 | } 474 | clear(); 475 | break; 476 | case 't': 477 | input_text = get_input_edit("Edit title", todolist->title); 478 | if (!input_text) 479 | fatal_error(); 480 | if (input_text[0]) { 481 | free(todolist->title); 482 | todolist->title = input_text; 483 | status = STATUS_UNSAVED; 484 | } 485 | else { 486 | free(input_text); 487 | } 488 | clear(); 489 | break; 490 | case ' ': 491 | case 10: /* enter */ 492 | if (!selected) { 493 | break; 494 | } 495 | selected->done ^= 1; 496 | status = STATUS_UNSAVED; 497 | break; 498 | default: 499 | if (isgraph(ch)) 500 | print_message("Unbound key: %c", ch); 501 | else if (ch < ' ') 502 | print_message("Unbound key: ^%c", '@' + ch); 503 | else 504 | print_message("Unbound key: (%d)", ch); 505 | break; 506 | } 507 | 508 | if (ch == 'q' || ch == 'Q') { 509 | if (save_todolist(todolist, filename) || ch == 'Q') { 510 | break; 511 | } 512 | else { 513 | print_message("Could not save %s: %s", filename, get_last_error()); 514 | } 515 | } 516 | } 517 | delete_todolist(todolist); 518 | endwin(); 519 | return 0; 520 | } 521 | --------------------------------------------------------------------------------