diff --git a/CHANGELOG/v2_3.md b/CHANGELOG/v2_3.md new file mode 100644 index 0000000..3d6dd35 --- /dev/null +++ b/CHANGELOG/v2_3.md @@ -0,0 +1,7 @@ +# CHANGE TYPE: MEDIUM + +-> Added audio file preview (VISUAL) with error handling and testing + +-> Files with no extension and non-executable now show in as bash script file with syntax highlighting by default + +-> Updated arguments for litefm diff --git a/CMakeLists.txt b/CMakeLists.txt index 91ed6e8..74ecc0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,21 +35,23 @@ set(CMAKE_C_STANDARD 99) find_package(Curses REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBARCHIVE REQUIRED libarchive) -# Manually specify YAML include and library paths pkg_check_modules(LIBYAML REQUIRED yaml-0.1) +pkg_check_modules(SDL2 REQUIRED sdl2) +pkg_check_modules(SDL2_MIXER REQUIRED SDL2_mixer) - -# Include directories for ncurses, libarchive, libyaml, and project headers +# Include directories for ncurses, libarchive, libyaml, SDL2, SDL2_mixer, and project headers include_directories(${CURSES_INCLUDE_DIR}) include_directories(${LIBARCHIVE_INCLUDE_DIRS}) include_directories(${LIBYAML_INCLUDE_DIRS}) +include_directories(${SDL2_INCLUDE_DIRS}) +include_directories(${SDL2_MIXER_INCLUDE_DIRS}) include_directories(${CMAKE_SOURCE_DIR}) # Add the executable -add_executable(litefm lfm.c src/cursesutils.c src/filepreview.c src/dircontrol.c src/archivecontrol.c src/clipboard.c src/logging.c src/signalhandling.c src/highlight.c src/hashtable.c src/helpers.c) +add_executable(litefm lfm.c src/cursesutils.c src/filepreview.c src/dircontrol.c src/archivecontrol.c src/clipboard.c src/logging.c src/signalhandling.c src/highlight.c src/hashtable.c src/arg_helpers.c src/musicpreview.c) # Link required libraries -target_link_libraries(litefm ${CURSES_LIBRARIES} ${LIBARCHIVE_LIBRARIES} ${LIBYAML_LIBRARIES}) +target_link_libraries(litefm ${CURSES_LIBRARIES} ${LIBARCHIVE_LIBRARIES} ${LIBYAML_LIBRARIES} ${SDL2_LIBRARIES} ${SDL2_MIXER_LIBRARIES}) # Add additional compiler flags target_compile_options(litefm PRIVATE -Wall -Wextra -Wpedantic) diff --git a/Makefile b/Makefile index c941349..bee701a 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,11 @@ ARCHIVE_INCS = $(shell pkg-config --cflags libarchive) YAML_LIBS = $(shell pkg-config --libs yaml-0.1) YAML_INCS = $(shell pkg-config --cflags yaml-0.1) +SDL2_LIBS = $(shell pkg-config --libs sdl2) +SDL2_INCS = $(shell pkg-config --cflags sdl2) +SDL2_MIXER_LIBS = $(shell pkg-config --libs sdl2_mixer) +SDL2_MIXER_INCS = $(shell pkg-config --cflags sdl2_mixer) + # Source files SRCS = lfm.c \ src/cursesutils.c \ @@ -47,7 +52,8 @@ SRCS = lfm.c \ src/signalhandling.c \ src/highlight.c \ src/hashtable.c \ - src/helpers.c + src/arg_helpers.c \ + src/musicpreview.c # Object files OBJS = $(SRCS:.c=.o) @@ -60,11 +66,11 @@ all: $(TARGET) # Link the executable $(TARGET): $(OBJS) - $(CC) -o $@ $(OBJS) $(CURSES_LIBS) $(ARCHIVE_LIBS) $(YAML_LIBS) + $(CC) -o $@ $(OBJS) $(CURSES_LIBS) $(ARCHIVE_LIBS) $(YAML_LIBS) $(SDL2_LIBS) $(SDL2_MIXER_LIBS) # Compile source files into object files %.o: %.c - $(CC) $(CFLAGS) $(CURSES_INCS) $(ARCHIVE_INCS) $(YAML_INCS) -c $< -o $@ + $(CC) $(CFLAGS) $(CURSES_INCS) $(ARCHIVE_INCS) $(YAML_INCS) $(SDL2_INCS) $(SDL2_MIXER_INCS) -c $< -o $@ # Clean up generated files clean: diff --git a/README.md b/README.md index f07180e..02231cc 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,12 @@ Written in C with NCurses, libyaml, rsync and more! > > Without a nerd font, LiteFM will show some unreadable chars! > -> **2. Have it full screen whenever possible:** +> **2. Dynamic resizing:** > -> Despite having dynamic window scaling, in a lot of -> the outputs the strings may look weird (or not visible at all!) upon resizing. -> Therefore, till I find a way to fix this issue, it is **HIGHLY** -> recommended to use LiteFM at: +> There is dynamic windowing setup in LiteFM. However, > -> -> Full screen of originally created terminal [DE] +> Upon resizing, you will have to press any key for it to show! > -> -> Full terminal size without resizing [WM] -> > **3. Launch LiteFM with `superuser`** > > LiteFM allows for fast file/dir actions, diff --git a/include/helpers.h b/include/arg_helpers.h similarity index 100% rename from include/helpers.h rename to include/arg_helpers.h diff --git a/include/filepreview.h b/include/filepreview.h index 9498736..6969d76 100644 --- a/include/filepreview.h +++ b/include/filepreview.h @@ -59,6 +59,7 @@ const char *determine_file_type(const char *filename); int is_readable_extension(const char *filename); const char *format_file_size(off_t size); int is_image(const char *filename); +int is_audio(const char *filename); void launch_env_var(WINDOW* win, const char *current_path, const char *filename, const char* type); void print_permissions(WINDOW *info_win, struct stat *file_stat); void display_archive_contents(WINDOW *info_win, const char *full_path, const char *file_ext); diff --git a/include/musicpreview.h b/include/musicpreview.h new file mode 100644 index 0000000..0646802 --- /dev/null +++ b/include/musicpreview.h @@ -0,0 +1,16 @@ +#ifndef MUSIC_H +#define MUSIC_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void preview_audio(const char *file_path); + +#endif diff --git a/keywords/sh-keywords.yaml b/keywords/sh-keywords.yaml index 6553282..3d55eaa 100644 --- a/keywords/sh-keywords.yaml +++ b/keywords/sh-keywords.yaml @@ -69,7 +69,6 @@ symbols: - '<' - '>' - '$' - - '`' - '~' - '\\' - '=' @@ -99,3 +98,5 @@ multicomments2: strings: - "\"" - "'" + - "`" + - "\"\"" diff --git a/lfm.c b/lfm.c index c23794b..b945f98 100644 --- a/lfm.c +++ b/lfm.c @@ -46,7 +46,8 @@ #include "include/signalhandling.h" #include "include/highlight.h" #include "include/hashtable.h" -#include "include/helpers.h" +#include "include/arg_helpers.h" +#include "include/musicpreview.h" #define MAX_ITEMS 1024 #define MAX_HISTORY 256 @@ -762,6 +763,17 @@ int main(int argc, char* argv[]) { endwin(); show_version(); return 0; + } else if (argc == 2 && (strcmp(argv[1], "-l") == 0 || strcmp(argv[1], "--log-dir") == 0)) { + endwin(); + printf("Log file at: ~/%s\n", LOG_FILE_RELATIVE_PATH); + return 0; + } else if (argc == 2 && (strcmp(argv[1], "-lc") == 0 || strcmp(argv[1], "--log-clear") == 0)) { + endwin(); + char termbuf[256]; + snprintf(termbuf, 256, "echo '' > ~/%s", LOG_FILE_RELATIVE_PATH); + system(termbuf); + printf("Cleared log for LiteFM.\n"); // will implement this later + return 0; } else { get_current_working_directory(current_path, sizeof(current_path)); } @@ -788,6 +800,7 @@ int main(int argc, char* argv[]) { int find_index = 0; char last_query[NAME_MAX] = ""; bool firstKeyPress = true; + bool firstPlay = true; log_message(LOG_LEVEL_DEBUG, "================ LITEFM INSTANCE STARTED ================="); log_message(LOG_LEVEL_DEBUG, "Entering as USER: %s",cur_user); @@ -892,13 +905,19 @@ int main(int argc, char* argv[]) { highlight = 0; scroll_position = 0; } else { - if ((is_readable_extension(items[highlight].name) || !is_image(items[highlight].name)) && !items[highlight].is_dir) { + if ((is_readable_extension(items[highlight].name) || !is_image(items[highlight].name)) && !items[highlight].is_dir && !is_audio(items[highlight].name)) { firstKeyPress = true; launch_env_var(win, current_path, items[highlight].name, "EDITOR"); /* Since we have set firstKeyPress to true, it will not wgetch(), rather it will just refresh everything back to how it was */ } else if (is_image(items[highlight].name) && !items[highlight].is_dir) { firstKeyPress = true; launch_env_var(win, current_path, items[highlight].name, "VISUAL"); + } else if (is_audio(items[highlight].name) && !items[highlight].is_dir) { + char file_path[MAX_PATH_LENGTH]; + snprintf(file_path, MAX_PATH_LENGTH, "%s/%s", current_path, items[highlight].name); + show_term_message(" [PREVIEW] Previewing audio file. Press q to quit.", 0); + preview_audio(file_path); + show_term_message("", -1); } else { show_term_message("Cannot enter this directory/file.", 1); } @@ -1456,9 +1475,6 @@ int main(int argc, char* argv[]) { } break; } - /*else if (nextch == 'p' && !items[highlight].is_dir) {*/ - /* break;*/ - /*}*/ refreshMainWin(win, info_win, items, item_count, highlight, current_path, show_hidden, scroll_position, height, info_height, info_width, info_starty, info_startx); } while (nextch != 10); diff --git a/src/helpers.c b/src/arg_helpers.c similarity index 99% rename from src/helpers.c rename to src/arg_helpers.c index f254746..b7b354b 100644 --- a/src/helpers.c +++ b/src/arg_helpers.c @@ -1,5 +1,5 @@ -#include "../include/helpers.h" +#include "../include/arg_helpers.h" void show_help() { // Print the header in blue color diff --git a/src/cursesutils.c b/src/cursesutils.c index 3f49f6f..831f7c4 100644 --- a/src/cursesutils.c +++ b/src/cursesutils.c @@ -497,14 +497,19 @@ void displayHelp(WINDOW* main_win) { " Traverse the filesystem ", " Toggle hidden dirs", "", - " Mode 3: Command - Direct command input. [Ex: Go to, /, so on]", + " Mode 3: Preview - For file preview (AUDIO only!)", + " ->Cannot perform any operations while in preview", + " Have to exit the preview before accessing LiteFM", + "", + " Mode 4: Command - Direct command input. [Ex: Go to, /, so on]", "", "", " Author: nots1dd", - " Version: 1.0", + " Version: 2.2", " License: GNU GPL v3", " Thank you for using LiteFM!", " For more information, visit the Github page:", + "", " ->https://github.com/nots1dd/litefm", "", " Press 'q' to exit this help", diff --git a/src/filepreview.c b/src/filepreview.c index 9998278..16a16e0 100644 --- a/src/filepreview.c +++ b/src/filepreview.c @@ -16,6 +16,7 @@ #define MAX_LINE_LENGTH 256 // Define the maximum line length #define MAX_FILE_HEADER_SIZE 18 // Maximum size needed for the longest signature #define BUFFER_SIZE 1024 +#define MAX_FILE_TYPE_LENGTH 256 int singlecommentslen = 0; @@ -35,36 +36,50 @@ FileSignature file_signatures[] = { {NULL, 0, NULL} // End of list marker }; -const char * get_file_extension(const char * filename) { - const char * dot = strrchr(filename, '.'); - if (!dot || dot == filename) return ""; - return dot + 1; +const char *determine_file_type(const char *filename); + +const char *get_file_extension(const char *filename) { + const char *dot = strrchr(filename, '.'); + + // If there's no dot in the filename, check file type + if (dot == NULL) { + const char *file_type = determine_file_type(filename); + if (file_type && strcmp(file_type, "text/x-shellscript") == 0) { + return "sh"; // Return the extension for shell scripts + } + return ""; // No extension found and not a shell script + } + + // Return the extension without the dot + return dot + 1; } const char *determine_file_type(const char *filename) { - FILE *file = fopen(filename, "rb"); - if (!file) { - return NULL; - } + static char file_type[MAX_FILE_TYPE_LENGTH]; + char command[MAX_FILE_TYPE_LENGTH + 50]; - unsigned char header[MAX_FILE_HEADER_SIZE]; - size_t bytes_read = fread(header, 1, MAX_FILE_HEADER_SIZE, file); - fclose(file); + // Construct the command to run the 'file' command with the provided filename + snprintf(command, sizeof(command), "file --brief --mime-type \"%s\"", filename); - if (bytes_read == 0) { - return NULL; + FILE *fp = popen(command, "r"); + if (!fp) { + return "Error"; } - for (int i = 0; file_signatures[i].signature != NULL; i++) { - if (bytes_read >= file_signatures[i].length && - memcmp(header, file_signatures[i].signature, file_signatures[i].length) == 0) { - return file_signatures[i].file_type; - } + // Read the output from the 'file' command + if (fgets(file_type, sizeof(file_type), fp) == NULL) { + pclose(fp); + return "Unknown"; } - return "Unknown"; + // Remove trailing newline character + file_type[strcspn(file_type, "\n")] = '\0'; + + pclose(fp); + return file_type; } + const char* read_lines(const char *filename, size_t max_lines) { FILE *file = fopen(filename, "r"); // Open file in text mode if (file == NULL) { @@ -144,7 +159,7 @@ const char *get_keywords_file(const char *ext) { strcmp(ext, "c") == 0 || strcmp(ext, "cxx") == 0) { snprintf(keywords_file, sizeof(keywords_file), "%s/../keywords/c-keywords.yaml", project_dir); } - // Add more mappings as needed + // Add more mappings as needed else { snprintf(keywords_file, sizeof(keywords_file), "%s/../keywords/%s-keywords.yaml", project_dir, ext); } @@ -245,38 +260,36 @@ void display_file(WINDOW *info_win, const char *filename) { wrefresh(info_win); // Refresh the window to show the content } - int is_readable_extension(const char *filename) { - // List of supported extensions + // List of supported extensions (excluding the dot) const char *supported_extensions[] = { - ".txt", ".sh", ".json", ".conf", ".log", ".md", ".c", ".h", ".hpp", ".cpp", ".cxx", ".cc", ".js", - ".ts", ".jsx", ".java", ".dart", ".asm", ".py", ".rs", ".go", ".pl", ".backup", ".rasi", ".nix", - ".yaml", ".html", ".css", ".scss", NULL + "txt", "sh", "json", "conf", "log", "md", "c", "h", "hpp", "cpp", "cxx", "cc", "js", + "ts", "jsx", "java", "dart", "asm", "py", "rs", "go", "pl", "backup", "rasi", "nix", + "yaml", "html", "css", "scss", NULL }; // Check if the file has a supported extension const char *ext = get_file_extension(filename); if (ext[0] != '\0') { for (int i = 0; supported_extensions[i] != NULL; i++) { - if (strcmp(ext, supported_extensions[i] + 1) == 0) { // +1 to skip the initial dot + if (strcmp(ext, supported_extensions[i]) == 0) { // Compare directly with the extension list return 1; } } } - // Check file type based on header + // Check file type based on MIME type const char *file_type = determine_file_type(filename); if (file_type && ( - strcmp(file_type, "ASCII text") == 0 || - strcmp(file_type, "shell script") == 0 || - strcmp(file_type, "JSON data") == 0 || - strcmp(file_type, "UTF-8 Unicode text") == 0 || - strcmp(file_type, "C source") == 0 || - strcmp(file_type, "C++ source") == 0 || - strcmp(file_type, "Python script") == 0 || - strcmp(file_type, "Java source") == 0 || - strcmp(file_type, "HTML document") == 0 || - strcmp(file_type, "CSS document") == 0 + strcmp(file_type, "text/plain") == 0 || + strcmp(file_type, "text/x-shellscript") == 0 || + strcmp(file_type, "application/json") == 0 || + strcmp(file_type, "text/x-c") == 0 || // C source code + strcmp(file_type, "text/x-c++") == 0 || // C++ source code + strcmp(file_type, "text/x-python") == 0 || // Python script + strcmp(file_type, "text/x-java-source") == 0 ||// Java source code + strcmp(file_type, "text/html") == 0 || + strcmp(file_type, "text/css") == 0 )) { return 1; } @@ -284,7 +297,6 @@ int is_readable_extension(const char *filename) { return 0; } - const char *format_file_size(off_t size) { static char formatted_size[64]; @@ -324,6 +336,29 @@ int is_image(const char *filename) { return 0; } +int is_audio(const char *filename) { + const char *image_extensions[] = {".mp3", ".wav", ".flac", ".opus", ".m4a"}; + size_t num_extensions = sizeof(image_extensions) / sizeof(image_extensions[0]); + + const char *ext = strrchr(filename, '.'); + if (!ext) return 0; + + char ext_lower[16]; + size_t i; + for (i = 0; ext[i] && i < sizeof(ext_lower) - 1; i++) { + ext_lower[i] = tolower((unsigned char)ext[i]); + } + ext_lower[i] = '\0'; + + for (i = 0; i < num_extensions; i++) { + if (strcmp(ext_lower, image_extensions[i]) == 0) { + return 1; + } + } + + return 0; +} + bool is_valid_editor(const char *editor) { for (size_t i = 0; i < strlen(editor); ++i) { diff --git a/src/highlight.c b/src/highlight.c index db976f3..83c964b 100644 --- a/src/highlight.c +++ b/src/highlight.c @@ -3,7 +3,6 @@ #include "../include/cursesutils.h" #include "../include/logging.h" -/*#define TABLE_SIZE 1000*/ int multicomments1_length = 0; int multicomments2_length = 0; @@ -142,6 +141,7 @@ void highlightLine(WINDOW* win, int color_pair, int y, int x, char *buffer) { // Function to highlight code snippet void highlight_code(WINDOW *win, int start_y, int start_x, const char *code, HashTable *keywords, HashTable *singlecomments, HashTable *multicomments1, HashTable *multicomments2, HashTable *strings, HashTable *functions, HashTable *symbols, HashTable *operators, int *singlecommentslen) { + int max_x_coord = getmaxx(win) - 1; const char *cursor = code; char buffer[256]; int in_string = 0; @@ -150,7 +150,7 @@ void highlight_code(WINDOW *win, int start_y, int start_x, const char *code, Has int buffer_index = 0; int x = start_x, y = start_y; - while (*cursor != '\0') { + while (*cursor != '\0' && x != max_x_coord) { // Check for multiline comments first if (hash_table_contains(multicomments1, cursor) && hash_table_contains(multicomments2, cursor + 1)) { in_multiline_comment = 1; diff --git a/src/musicpreview.c b/src/musicpreview.c new file mode 100644 index 0000000..506ed07 --- /dev/null +++ b/src/musicpreview.c @@ -0,0 +1,55 @@ + +#include "../include/musicpreview.h" +#include "../include/logging.h" +#include "../include/signalhandling.h" + +void preview_audio(const char *file_path) { + + // Initialize SDL + if (SDL_Init(SDL_INIT_AUDIO) < 0) { + log_message(LOG_LEVEL_ERROR, "SDL could not initialize! SDL_Error: %s", SDL_GetError()); + exit(EXIT_FAILURE); + } + + // Initialize SDL_mixer + if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { + log_message(LOG_LEVEL_ERROR, "SDL_mixer could not initialize! SDL_mixer Error: %s", Mix_GetError()); + SDL_Quit(); + exit(EXIT_FAILURE); + } + + // Load the music file + Mix_Music *music = Mix_LoadMUS(file_path); + if (music == NULL) { + log_message(LOG_LEVEL_ERROR, "Failed to load music file! SDL_mixer Error: %s", Mix_GetError()); + Mix_CloseAudio(); + SDL_Quit(); + exit(EXIT_FAILURE); + } + + // Play the music + if (Mix_PlayMusic(music, 1) == -1) { + log_message(LOG_LEVEL_ERROR, "SDL_mixer Error: %s", Mix_GetError()); + } else { + log_message(LOG_LEVEL_INFO, "Playing music: %s", file_path); + } + + // Wait until the music finishes + int ch; + while (Mix_PlayingMusic() && ch != 'q') { + ch = getch(); + if (ch == 'q') { + break; + } + SDL_Delay(100); + } + + // Clean up + Mix_FreeMusic(music); + Mix_CloseAudio(); + SDL_Quit(); + + log_message(LOG_LEVEL_INFO, "Exiting SDL..."); + +} +