commit 2c136debce5d64a5482c81082350c53b5ae8df22 Author: Cameron Reed Date: Wed Jun 12 17:48:27 2024 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2f0f5e --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024 CameronReed + +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. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..89cba3d --- /dev/null +++ b/Makefile @@ -0,0 +1,99 @@ +# Inputs + +SRC_DIRS := src +INC_DIRS := inc +LIBS := glew glfw3 freetype2 sdbus-c++ + + +# Outputs + +OUT_NAME := timer_overlay +BUILD_DIR := build +OFILE_DIR := $(BUILD_DIR)/objects +PREFIX ?= /usr/bin + + +# Project sources + +LIB_INCLUDES := $(foreach lib, $(LIBS), $(shell pkg-config --cflags-only-I $(lib))) +INCLUDES := $(addprefix -I, $(INC_DIRS)) $(LIB_INCLUDES) +C_SOURCES := $(foreach dir, $(SRC_DIRS), $(wildcard $(dir)/*.c)) +CXX_SOURCES := $(foreach dir, $(SRC_DIRS), $(wildcard $(dir)/*.cpp)) + + +# Intermediary Outputs + +OFILES := $(addprefix $(OFILE_DIR)/, $(notdir $(C_SOURCES:.c=.o) $(CXX_SOURCES:.cpp=.o))) + + +# Compiler flags + +OPT := -O2 +CPPFLAGS := $(INCLUDES) -MMD +CFLAGS := $(OPT) -Wall -Wextra -Wpedantic +CXXFLAGS := -std=c++17 $(OPT) -Wall -Wextra -Wpedantic + + +# Linker flags + +LDFLAGS := $(foreach lib, $(LIBS), $(shell pkg-config --libs-only-L $(lib))) +LDLIBS := $(foreach lib, $(LIBS), $(shell pkg-config --libs-only-l $(lib))) + + +# Dependency files generated by the compiler + +DEPENDS := $(OFILES:.o=.d) + + +# All output directories + +ALL_DIRS := $(BUILD_DIR) $(OFILE_DIR) + + +# Use CC as linker if there are no CXX source files + +ifeq ($(strip $(CXX_SOURCES)),) + LD := $(CC) +else + LD := $(CXX) +endif + + + +.PHONY: all run clean install + +all: $(BUILD_DIR)/$(OUT_NAME) + +run: $(BUILD_DIR)/$(OUT_NAME) + $(BUILD_DIR)/$(OUT_NAME) + +clean: + $(RM) -r $(ALL_DIRS) + +install: $(BUILD_DIR)/$(OUT_NAME) + install -D $(BUILD_DIR)/$(OUT_NAME) $(PREFIX) + + +# Include generated header file dependencies + +-include $(DEPENDS) + + +$(BUILD_DIR)/$(OUT_NAME): $(OFILES) | $(COMPILED_SHADERS) $(TEXTURES) $(MODELS) $(BUILD_DIR) + $(LD) $^ $(LDFLAGS) $(LDLIBS) -o $@ + +$(OFILE_DIR)/%.o: %.c | $(OFILE_DIR) + $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ + +$(OFILE_DIR)/%.o: %.cpp | $(OFILE_DIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@ + + +# Directories + +$(ALL_DIRS): + mkdir -p $@ + + +vpath %.c $(SRC_DIRS) +vpath %.cpp $(SRC_DIRS) diff --git a/README.md b/README.md new file mode 100644 index 0000000..56619ea --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Timer overlay +This is a project to display a timer overlayed on top of the rest of your desktop that is controlled with keyboard shortcuts so you can quickly +start a timer any time you need with only a couple key presses + +This relies on the [GlobalShortcuts XDG desktop portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html), +which is currently only implemented on KDE Plasma. I am considering reworking it to not rely on the portal, but we'll see if I get around to that + + + +# Dependencies +This project relies on [glew](https://github.com/nigels-com/glew) [glfw3](https://www.glfw.org/) [freetype2](https://freetype.org/) and [sdbus-cpp](https://github.com/Kistler-Group/sdbus-cpp). +You will need to install these libraries to be able to build/run this project + +For example, on Arch Linux: +``` +sudo pacman -S glew glfw freetype2 sdbus-cpp +``` + +You will also need to have make and a c++ compiler to build the project +For example, on Arch Linux: (you probably already have these if you are using Arch as they are dependencies of base-devel) +``` +sudo pacman -S make gcc +``` + + +# Building +Once the dependencies have been installed, you can compile the project simply with: +``` +make +``` + + + +# Configuring +In the future, there will be a configuration file to configure the position, size, and font. However, when running under Wayland, +the requested position of the window will not be respected, so to set the position, as well as removing window decorations, and +preventing the window from stealing focus when it is opened, you will need to set window rules. You can see the rules I use below: +![window rules](https://cam123.dev/files/hidden/images/window_rules.png) + + + +# Installing +Installation is likewise very simple, simply run: +``` +sudo make install +``` + +There is also a desktop file provided that you may want to install to /usr/share/applications/, ~/.local/share/applications/, or ~/.config/autostart, +but that is left up to you + + + +# Licensing +The MIT license attached to this project applies to most of the repo, with the exception of one file, ```src/character_utils.cpp```. +This file's core logic was copied from [https://learnopengl.com/In-Practice/Text-Rendering](https://learnopengl.com/In-Practice/Text-Rendering), +and is therefore licensed and copyrighted by its creator, [Joey de Vries](https://twitter.com/JoeyDeVriez) under the CC BY 4.0 license +which you can read about [here](https://creativecommons.org/licenses/by/4.0/) or [here](https://creativecommons.org/licenses/by/4.0/legalcode) + diff --git a/inc/character_utils.h b/inc/character_utils.h new file mode 100644 index 0000000..c4bd07e --- /dev/null +++ b/inc/character_utils.h @@ -0,0 +1,31 @@ +#include +#include +#include + +#include +#include FT_FREETYPE_H + +struct Character { + unsigned int TextureID; // ID handle of the glyph texture + glm::ivec2 Size; // Size of glyph + glm::ivec2 Bearing; // Offset from baseline to left/top of glyph + FT_Pos Advance; // Offset to advance to next glyph +}; + +class Font +{ +public: + Font() = delete; + Font(const char* font); + void RenderText(GLuint VAO, GLuint VBO, GLuint shaderProgram, const char* text, float x, float y, float scale); + +private: + void SetupCharMap(FT_Face& face); + +public: + bool LoadError; + +private: + std::unordered_map m_Characters; +}; + diff --git a/inc/shader_utils.h b/inc/shader_utils.h new file mode 100644 index 0000000..f106acd --- /dev/null +++ b/inc/shader_utils.h @@ -0,0 +1,6 @@ +#include + + +GLuint loadShader(const char* const file_path, GLenum shader_type); +GLuint loadShaderDirect(const char* const shader_src, GLenum shader_type); +GLuint linkShaderProgram(GLuint vert_shader, GLuint frag_shader); diff --git a/inc/shadersrc.h b/inc/shadersrc.h new file mode 100644 index 0000000..ef20e5c --- /dev/null +++ b/inc/shadersrc.h @@ -0,0 +1,2 @@ +extern const char* const shader_text_frag; +extern const char* const shader_text_vert; diff --git a/inc/shortcuts.h b/inc/shortcuts.h new file mode 100644 index 0000000..cb8fa8e --- /dev/null +++ b/inc/shortcuts.h @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include + + +typedef sdbus::Struct> dbus_shortcut_t; +typedef void(*shortcut_callback_t)(void*); +// typedef std::function shortcut_callback_t; + +struct ShortcutCallback { + void* userData; + shortcut_callback_t callback; +}; + +class GlobalShortcuts { +public: + GlobalShortcuts() = delete; + GlobalShortcuts(const char* const tokenPrefix); + void addShortcut(const std::string& id, const std::string& description, const std::string& trigger, shortcut_callback_t callback, void* userData); + int createSession(); + int bindKeys(); + bool alreadyBound(); + std::vector listBinds(); + void listen(); + +private: + std::string AddNumToToken(); + +private: + sdbus::ObjectPath m_SessionPath; + std::string m_TokenPrefix; + std::string m_ConnName; + std::string m_Sender; + std::unique_ptr m_xdgProxy; + + std::vector m_Shortcuts; + std::map m_Callbacks; +}; diff --git a/inc/timer.h b/inc/timer.h new file mode 100644 index 0000000..3e76501 --- /dev/null +++ b/inc/timer.h @@ -0,0 +1,31 @@ +#include +#include + + +struct TimeDuration +{ + int64_t hours; + int64_t minutes; + int64_t seconds; + int64_t milliseconds; + + int64_t minutes_absolute; + int64_t seconds_absolute; + int64_t milliseconds_absolute; + + bool negative; +}; + + +class Timer +{ +public: + Timer(); + TimeDuration GetTimeLeft(); + void AddMinutes(int64_t minutes); + void Clear(); + +private: + std::chrono::system_clock::time_point end_time; +}; + diff --git a/shaders/shader.frag b/shaders/shader.frag new file mode 100644 index 0000000..c40f0aa --- /dev/null +++ b/shaders/shader.frag @@ -0,0 +1,12 @@ +#version 330 core +in vec2 TexCoords; +out vec4 color; + +uniform sampler2D text; +uniform vec3 textColor; + +void main() +{ + vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); + color = vec4(textColor, 1.0) * sampled; +} diff --git a/shaders/shader.vert b/shaders/shader.vert new file mode 100644 index 0000000..2f835da --- /dev/null +++ b/shaders/shader.vert @@ -0,0 +1,11 @@ +#version 330 core +layout (location = 0) in vec4 vertex; // +out vec2 TexCoords; + +uniform mat4 projection; + +void main() +{ + gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); + TexCoords = vertex.zw; +} diff --git a/src/character_utils.cpp b/src/character_utils.cpp new file mode 100644 index 0000000..1fee1e8 --- /dev/null +++ b/src/character_utils.cpp @@ -0,0 +1,137 @@ +// This was copied from https://learnopengl.com/In-Practice/Text-Rendering +// and then modifying it slightly the main modification being wrapping it +// in a class. But the core logic was written by Joey de Vries. This file +// is therefore licensed under the terms of the CC BY 4.0. You can read +// about it at https://creativecommons.org/licenses/by/4.0/ or find the +// full license at https://creativecommons.org/licenses/by/4.0/legalcode +// This is not provided under any warranty +// +// (c) Joey de Vries +// (You can find him on twitter at https://twitter.com/JoeyDeVriez + + +#include +#include + +#include +#include FT_FREETYPE_H + +#include +#include + + +#include "character_utils.h" + + +Font::Font(const char* font) + : LoadError(false), m_Characters() +{ + FT_Library ft; + if (FT_Init_FreeType(&ft)) { + std::cerr << "Error: Failed to initialize FreeType2" << std::endl; + LoadError = true; + return; + } + + std::cout << "Loading font " << font << std::endl; + FT_Face face; + if (FT_New_Face(ft, font, 0, &face)) { + std::cerr << "Error: Failed to load font" << std::endl; + LoadError = true; + return; + } + + SetupCharMap(face); + + FT_Done_Face(face); + FT_Done_FreeType(ft); +} + +void Font::SetupCharMap(FT_Face &face) +{ + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // disable byte-alignment restriction + + FT_Set_Pixel_Sizes(face, 0, 48); + + for (unsigned char c = 0; c < 128; c++) { + // load character glyph + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { + std::cerr << "Error: Failed to load " << std::hex << (uint8_t)c << " character" << std::endl; + continue; + } + // generate texture + GLuint texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + face->glyph->bitmap.width, + face->glyph->bitmap.rows, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + face->glyph->bitmap.buffer); + // set texture options + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // now store character for later use + Character character = { + texture, + glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), + glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), + face->glyph->advance.x + }; + m_Characters.insert(std::pair(c, character)); + } + glBindTexture(GL_TEXTURE_2D, 0); +} + +void Font::RenderText(GLuint VAO, GLuint VBO, GLuint shaderProgram, const char* text, float x, float y, float scale) +{ + // activate corresponding render state + glUseProgram(shaderProgram); + glActiveTexture(GL_TEXTURE0); + glBindVertexArray(VAO); + + // iterate through all characters + for (size_t i = 0; text[i] != '\0'; i++) + { + char c = text[i]; + Character ch = m_Characters[c]; + + float xpos = x + ch.Bearing.x * scale; + float ypos = y - (ch.Size.y - ch.Bearing.y) * scale; + + float w = ch.Size.x * scale; + float h = ch.Size.y * scale; + // update VBO for each character + float vertices[6][4] = { + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos, ypos, 0.0f, 1.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + { xpos + w, ypos + h, 1.0f, 0.0f } + }; + // render glyph texture over quad + glBindTexture(GL_TEXTURE_2D, ch.TextureID); + // update content of VBO memory + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glBindBuffer(GL_ARRAY_BUFFER, 0); + // render quad + glDrawArrays(GL_TRIANGLES, 0, 6); + // now advance cursors for next glyph (note that advance is number of 1/64 pixels) + x += (ch.Advance >> 6) * scale; // bitshift by 6 to get value in pixels (2^6 = 64) + } + + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); +} + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..1e4437e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + + +#include "shader_utils.h" +#include "shadersrc.h" +#include "character_utils.h" +#include "shortcuts.h" +#include "timer.h" + + +void error_callback(int error, const char* description) +{ + std::cerr << "Error " << error << ": " << description << std::endl; +} + +int main() +{ + std::cout << glfwGetVersionString() << std::endl; + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_WAYLAND); + + glfwSetErrorCallback(error_callback); + if (glfwInit() != GLFW_TRUE) { + std::cerr << "Error initializing glfw" << std::endl; + return -1; + } + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + glfwWindowHint(GLFW_FOCUSED, GLFW_FALSE); + glfwWindowHint(GLFW_FOCUS_ON_SHOW, GLFW_FALSE); + glfwWindowHint(GLFW_DECORATED, GLFW_FALSE); + glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE); + glfwWindowHint(GLFW_SCALE_FRAMEBUFFER, GLFW_TRUE); + glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); + glfwWindowHint(GLFW_MOUSE_PASSTHROUGH, GLFW_TRUE); + glfwWindowHint(GLFW_POSITION_X, 200); + glfwWindowHint(GLFW_POSITION_Y, 10); + glfwWindowHintString(GLFW_WAYLAND_APP_ID, "timer-overlay"); + glfwWindowHintString(GLFW_X11_CLASS_NAME, "timer-overlay"); + glfwWindowHintString(GLFW_X11_INSTANCE_NAME, "timer-overlay"); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + + GLFWwindow* window = glfwCreateWindow(250, 50, "Timer Overlay", nullptr, nullptr); + if (window == nullptr) { + std::cout << "Failed to create glfw window" << std::endl; + glfwTerminate(); + return -1; + } + glfwMakeContextCurrent(window); + + glewExperimental = GL_TRUE; + int err = glewInit(); + if (err != GLEW_OK) { + std::cerr << "Failed to initialize GLEW: " << glewGetErrorString(err) << std::endl; + return -1; + } + + glEnable(GL_CULL_FACE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + GLuint vertShader = loadShaderDirect(shader_text_vert, GL_VERTEX_SHADER); + GLuint fragShader = loadShaderDirect(shader_text_frag, GL_FRAGMENT_SHADER); + GLuint shaderProgram = linkShaderProgram(vertShader, fragShader); + glDeleteShader(vertShader); + glDeleteShader(fragShader); + + glm::mat4 projection = glm::ortho(0.0f, 250.0f, 0.0f, 50.0f); + glUseProgram(shaderProgram); + glUniformMatrix4fv(glGetUniformLocation(shaderProgram, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); + + + Font NotoSans("/usr/share/fonts/noto/NotoSans-Regular.ttf"); + if (NotoSans.LoadError) { + return -1; + } + + GLuint VAO, VBO; + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + Timer timer; + GlobalShortcuts shortcuts("timer_overlay"); + + if (shortcuts.createSession() != 0) { + std::cout << "Failed to create shortcuts session" << std::endl; + glfwTerminate(); + } + + shortcuts.addShortcut("time1", "Adds 1 minute to the timer", "ALT+F5", [](void* timer_ptr) + { + ((Timer*)timer_ptr)->AddMinutes(1); + }, &timer); + shortcuts.addShortcut("time5", "Adds 5 minutes to the timer", "ALT+F6", [](void* timer_ptr) + { + ((Timer*)timer_ptr)->AddMinutes(5); + }, &timer); + shortcuts.addShortcut("time15", "Adds 15 minutes to the timer", "ALT+F7", [](void* timer_ptr) + { + ((Timer*)timer_ptr)->AddMinutes(15); + }, &timer); + shortcuts.addShortcut("time60", "Adds 1 hour to the timer", "ALT+F8", [](void* timer_ptr) + { + ((Timer*)timer_ptr)->AddMinutes(60); + }, &timer); + shortcuts.addShortcut("timeclear", "Clears the timer", "ALT+F9", [](void* timer_ptr) + { + ((Timer*)timer_ptr)->Clear(); + }, &timer); + + if (!shortcuts.alreadyBound()) { + std::cout << "Requsting to bind keys" << std::endl; + if (shortcuts.bindKeys() != 0) { + std::cerr << "Failed to bind keys" << std::endl; + return -1; + } + } + + shortcuts.listen(); + + int64_t last_frame_seconds = LONG_MAX; + while (!glfwWindowShouldClose(window)) { + if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { + glfwSetWindowShouldClose(window, GLFW_TRUE); + } + + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + TimeDuration time_diff = timer.GetTimeLeft(); + if (time_diff.seconds == last_frame_seconds && !time_diff.negative) { + continue; + } + + char buf[50]; // max of hhh:mm:ss\0 + if (time_diff.negative) { + memcpy(buf, "0:00", 5); + } else if (time_diff.hours > 999) { + memcpy(buf, "999:59:59", 10); + } else if (time_diff.hours < 1) { + sprintf(buf, "%ld:%02ld", time_diff.minutes, time_diff.seconds); + } else { + sprintf(buf, "%ld:%02ld:%02ld", time_diff.hours, time_diff.minutes, time_diff.seconds); + } + + if (!time_diff.negative || (time_diff.seconds_absolute < 3 && time_diff.milliseconds > 500)) { + NotoSans.RenderText(VAO, VBO, shaderProgram, buf, 0.0f, 10.0f, 1.0f); + } else { + usleep(500); + } + + glfwSwapBuffers(window); + glfwPollEvents(); + } + + glfwTerminate(); +} diff --git a/src/shader_utils.cpp b/src/shader_utils.cpp new file mode 100644 index 0000000..5839d1b --- /dev/null +++ b/src/shader_utils.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include + + +#include "shader_utils.h" + + +GLuint loadShader(const char* const file_path, GLenum shader_type) +{ + GLuint shaderID = glCreateShader(shader_type); + + std::string shaderSrc; + std::ifstream shaderStream(file_path, std::ios::in); + if (shaderStream.is_open()) { + std::stringstream sstr; + sstr << shaderStream.rdbuf(); + shaderSrc = sstr.str(); + + shaderStream.close(); + } else { + std::cerr << "Failed to open shader file " << file_path << std::endl; + return 0; + } + + std::cout << "Compiling shader " << file_path << std::endl; + const char* const p_shaderSrc = shaderSrc.c_str(); + glShaderSource(shaderID, 1, &p_shaderSrc, NULL); + glCompileShader(shaderID); + + GLint result = GL_FALSE; + int infoLogLen; + + glGetShaderiv(shaderID, GL_COMPILE_STATUS, &result); + glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLogLen); + if (infoLogLen > 0) { + char* shaderErrMsg = new char[infoLogLen + 1]; + glGetShaderInfoLog(shaderID, infoLogLen, NULL, shaderErrMsg); + std::cerr << shaderErrMsg << std::endl; + } + + return shaderID; +} + +GLuint loadShaderDirect(const char* const shader_src, GLenum shader_type) +{ + GLuint shaderID = glCreateShader(shader_type); + glShaderSource(shaderID, 1, &shader_src, NULL); + glCompileShader(shaderID); + + GLint result = GL_FALSE; + int infoLogLen; + + glGetShaderiv(shaderID, GL_COMPILE_STATUS, &result); + glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLogLen); + if (infoLogLen > 0) { + char* shaderErrMsg = new char[infoLogLen + 1]; + glGetShaderInfoLog(shaderID, infoLogLen, NULL, shaderErrMsg); + std::cerr << shaderErrMsg << std::endl; + } + + return shaderID; +} +GLuint linkShaderProgram(GLuint vert_shader, GLuint frag_shader) +{ + std::cout << "Linking shader program" << std::endl; + + GLuint programID = glCreateProgram(); + glAttachShader(programID, vert_shader); + glAttachShader(programID, frag_shader); + glLinkProgram(programID); + + GLint result = GL_FALSE; + int infoLogLen; + + glGetProgramiv(programID, GL_LINK_STATUS, &result); + glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLogLen); + if (infoLogLen > 0) { + char* programErrMsg = new char[infoLogLen + 1]; + glGetProgramInfoLog(programID, infoLogLen, NULL, programErrMsg); + std::cerr << programErrMsg << std::endl; + } + + glDetachShader(programID, vert_shader); + glDetachShader(programID, frag_shader); + + return programID; +} + diff --git a/src/shadersrc.cpp b/src/shadersrc.cpp new file mode 100644 index 0000000..c63b784 --- /dev/null +++ b/src/shadersrc.cpp @@ -0,0 +1,26 @@ +extern const char* const shader_text_frag = R"( +#version 330 core +in vec2 TexCoords; +out vec4 color; + +uniform sampler2D text; +uniform vec3 textColor; + +void main() { + color = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); +} +)"; + +extern const char* const shader_text_vert = R"( +#version 330 core +layout (location = 0) in vec4 vertex; +out vec2 TexCoords; + +uniform mat4 projection; + +void main() { + gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); + TexCoords = vertex.zw; +} +)"; + diff --git a/src/shortcuts.cpp b/src/shortcuts.cpp new file mode 100644 index 0000000..07de9eb --- /dev/null +++ b/src/shortcuts.cpp @@ -0,0 +1,354 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shortcuts.h" + + +const char* xdgName = "org.freedesktop.portal.Desktop"; +const char* xdgPath = "/org/freedesktop/portal/desktop"; +const char* shortcutsInterface = "org.freedesktop.portal.GlobalShortcuts"; +const char* requstInterface = "org.freedesktop.portal.Request"; + + + + +GlobalShortcuts::GlobalShortcuts(const char* const tokenPrefix) + : m_TokenPrefix(tokenPrefix), m_Shortcuts() { }; + + +std::string GlobalShortcuts::AddNumToToken() +{ + std::random_device dev; + std::mt19937 rng(dev()); + std::uniform_int_distribution dist(1000,9999); + + return m_TokenPrefix + std::to_string(dist(rng)); +} + +void GlobalShortcuts::addShortcut(const std::string& id, const std::string& description, const std::string& trigger, shortcut_callback_t callback, void* userData) +{ + std::map smap; + smap["description"] = sdbus::Variant(description); + smap["preferred_trigger"] = sdbus::Variant(trigger); + + m_Shortcuts.emplace_back(id, smap); + + m_Callbacks[id] = ShortcutCallback{ userData, callback }; +} + +int GlobalShortcuts::createSession() +{ + bool requestInProgress = false; + int result = 1; + + auto conn = sdbus::createSessionBusConnection(); + m_ConnName = conn->getUniqueName(); + m_Sender = m_ConnName.substr(1); + + + m_xdgProxy = sdbus::createProxy(std::move(conn), xdgName, xdgPath); + + std::map create_session_args; + std::string token = AddNumToToken(); + create_session_args["handle_token"] = sdbus::Variant(token); + create_session_args["session_handle_token"] = sdbus::Variant(m_TokenPrefix); + + for (size_t i = 1; i < m_Sender.length(); i++) { + if (m_Sender[i] == '.') { + m_Sender[i] = '_'; + } + } + std::string expectedRequestPath = "/org/freedesktop/portal/desktop/request/" + m_Sender + "/" + token; + + auto requestProxy = sdbus::createProxy(m_xdgProxy->getConnection(), xdgName, expectedRequestPath); + requestProxy->uponSignal("Response").onInterface(requstInterface) + .call([this, &requestInProgress, &result](uint32_t result_code, std::map res_map) + { + if (result_code == 0) { + this->m_SessionPath = res_map.at("session_handle").get(); + } else { + std::cerr << "Failed to create GlobalShortcuts session" << std::endl; + } + result = result_code; + requestInProgress = false; + }); + requestProxy->finishRegistration(); + + + sdbus::ObjectPath requestPath; + requestInProgress = true; + m_xdgProxy->callMethod("CreateSession").onInterface(shortcutsInterface) + .withArguments(create_session_args).storeResultsTo(requestPath); + + std::unique_ptr requestProxy2; + if (requestPath != expectedRequestPath) { + requestProxy->unregister(); + requestProxy2 = sdbus::createProxy(m_xdgProxy->getConnection(), xdgName, requestPath); + requestProxy2->uponSignal("Response").onInterface(requstInterface) + .call([this, &requestInProgress, &result](uint32_t result_code, std::map res_map) + { + if (result_code == 0) { + this->m_SessionPath = res_map.at("session_handle").get(); + } else { + std::cerr << "Failed to create GlobalShortcuts session" << std::endl; + } + result = result_code; + requestInProgress = false; + }); + requestProxy2->finishRegistration(); + } + + while (requestInProgress) { + usleep(500); + } + + return result; +} + +bool GlobalShortcuts::alreadyBound() +{ + std::vector binds = listBinds(); + if (binds.size() != m_Shortcuts.size()) { + return false; + } + + for (dbus_shortcut_t shortcut: m_Shortcuts) { + const std::string shortcut_id = shortcut.get<0>(); + bool match = false; + for (dbus_shortcut_t bind: binds) { + const std::string bind_id = bind.get<0>(); + if (shortcut_id == bind_id) { + match = true; + break; + } + } + + if (!match) { + return false; + } + } + + return true; +} + +std::vector GlobalShortcuts::listBinds() +{ + std::vector binds; + bool requestInProgress = false; + + std::map list_shortcuts_args; + const std::string token = AddNumToToken(); + list_shortcuts_args["handle_token"] = sdbus::Variant(token); + + sdbus::ObjectPath expectedRequestPath = "/org/freedesktop/portal/desktop/request/" + m_Sender + "/" + token; + + auto requestProxy = sdbus::createProxy(m_xdgProxy->getConnection(), xdgName, expectedRequestPath); + requestProxy->uponSignal("Response").onInterface(requstInterface) + .call([&requestInProgress, &binds](uint32_t result_code, std::map res_map) + { + if (result_code == 0) { + binds = res_map.at("shortcuts").get>(); + // for (dbus_shortcut_t bind: binds) { + // std::cout << "id: " << bind.get<0>(); + // std::cout << ", desc: " << bind.get<1>().at("description").get(); + // std::cout << ", trigger: " << bind.get<1>().at("trigger_description").get(); + // std::cout << std::endl << std::endl; + // } + } else { + std::cerr << "Failed to list shortcuts" << std::endl; + } + requestInProgress = false; + }); + requestProxy->finishRegistration(); + + + sdbus::ObjectPath requestPath; + requestInProgress = true; + m_xdgProxy->callMethod("ListShortcuts").onInterface(shortcutsInterface) + .withArguments(m_SessionPath, list_shortcuts_args).storeResultsTo(requestPath); + + std::unique_ptr requestProxy2; + if (requestPath != expectedRequestPath) { + requestProxy->unregister(); + requestProxy2 = sdbus::createProxy(m_xdgProxy->getConnection(), xdgName, requestPath); + requestProxy2->uponSignal("Response").onInterface(requstInterface) + .call([&requestInProgress, &binds](uint32_t result_code, std::map res_map) + { + if (result_code == 0) { + binds = res_map.at("shortcuts").get>(); + } else { + std::cerr << "Failed to list shortcuts" << std::endl; + } + requestInProgress = false; + }); + requestProxy2->finishRegistration(); + } + + + while (requestInProgress) { + usleep(500); + } + + return binds; +} + +int GlobalShortcuts::bindKeys() +{ + bool requestInProgress = false; + int result = 1; + + // std::cout << "session: " << this->sessionPath.c_str() << std::endl; + + std::map bind_shortcuts_args; + const std::string token = AddNumToToken(); + bind_shortcuts_args["handle_token"] = sdbus::Variant(token); + + sdbus::ObjectPath expectedRequestPath = "/org/freedesktop/portal/desktop/request/" + m_Sender + "/" + token; + + auto requestProxy = sdbus::createProxy(m_xdgProxy->getConnection(), xdgName, expectedRequestPath); + requestProxy->uponSignal("Response").onInterface(requstInterface) + .call([&requestInProgress, &result](uint32_t result_code, std::map res_map){ + (void) res_map; + if (result_code == 0) { + // auto binds = res_map.at("shortcuts").get>(); + // for (auto bind: binds) { + // std::cout << "id: " << bind.get<0>(); + // std::cout << ", desc: " << bind.get<1>().at("description").get(); + // std::cout << ", trigger: " << bind.get<1>().at("trigger_description").get(); + // std::cout << std::endl; + // } + } else { + std::cerr << "Failed to bind shortcuts" << std::endl; + } + result = result_code; + requestInProgress = false; + }); + requestProxy->finishRegistration(); + + sdbus::ObjectPath requestPath; + requestInProgress = true; + m_xdgProxy->callMethod("BindShortcuts").onInterface(shortcutsInterface) + .withArguments(m_SessionPath, m_Shortcuts, "", bind_shortcuts_args).storeResultsTo(requestPath); + + std::unique_ptr requestProxy2; + if (requestPath != expectedRequestPath) { + requestProxy->unregister(); + requestProxy2 = sdbus::createProxy(m_xdgProxy->getConnection(), xdgName, requestPath); + requestProxy2->uponSignal("Response").onInterface(requstInterface) + .call([&requestInProgress, &result](uint32_t result_code, std::map res_map){ + (void) res_map; + if (result_code == 0) { + std::cerr << "Failed to bind shortcuts" << std::endl; + } + result = result_code; + requestInProgress = false; + }); + requestProxy2->finishRegistration(); + } + + while (requestInProgress) { + usleep(500); + } + + return result; +} + +void GlobalShortcuts::listen() +{ + m_xdgProxy->uponSignal("Activated").onInterface(shortcutsInterface) + .call([this](sdbus::ObjectPath session_handle, const std::string& shortcut_id, uint64_t timestamp, std::map options) + { + (void) options; + (void) session_handle; + (void) timestamp; + + // std::cout << "Shortcut activated!" << std::endl; + // std::cout << "session: " << session_handle << ", id: " << shortcut_id << ", time: " << timestamp << std::endl << std::endl; + ShortcutCallback cb = this->m_Callbacks[shortcut_id]; + cb.callback(cb.userData); + }); + m_xdgProxy->finishRegistration(); +} + + +// Very basic example + +//int main() +//{ +// GlobalShortcuts shortcuts; +// +// if (shortcuts.createSession() != 0) { +// std::cout << "Failed to create shortcuts session" << std::endl; +// return -1; +// } +// +// shortcuts.addShortcut("test1", "Prints things", "CTRL+SHIFT+a"); +// shortcuts.addShortcut("test2", "Prints things, but like, different", "CTRL+SHIFT+b"); +// +// if (shortcuts.listBinds().size() == 0) { +// std::cout << "Requsting to bind keys" << std::endl; +// shortcuts.bindKeys(); +// } +// +// shortcuts.listen(); +// +// while (true) {}; +//} +// + +// Code for working with libdbus directly. There isn't a lot of information around on how to use it +// And it is pretty difficult to use + +//void connect() +//{ +// DBusConnection* dbus_conn = nullptr; +// DBusError dbus_error; +// +// dbus_error_init(&dbus_error); +// +// dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_error); +// std::cout << "Connected to DBUS as \"" << dbus_bus_get_unique_name(dbus_conn) << "\"." << std::endl; +// +// // Request connection to GlobalShortcuts portal +// DBusMessage* conn_request_msg = dbus_message_new_method_call("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts", "CreateSession"); +// DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, conn_request_msg, DBUS_TIMEOUT_USE_DEFAULT, &dbus_error); +// +// const char* conn_request_path = nullptr; +// dbus_message_get_args(reply, &dbus_error, DBUS_TYPE_OBJECT_PATH, conn_request_path, DBUS_TYPE_INVALID); +// +// dbus_message_unref(reply); +// dbus_message_unref(conn_request_msg); +// +// std::string rule = "type='signal',path='"; +// rule = rule + conn_request_path + '\''; +// dbus_bus_add_match(dbus_conn, rule.c_str(), &dbus_error); +// while (true) { +// dbus_connection_read_write(dbus_conn, DBUS_TIMEOUT_USE_DEFAULT); +// DBusMessage* msg_in = dbus_connection_pop_message(dbus_conn); +// if (msg_in == nullptr) { +// continue; +// } +// +// if (dbus_message_is_signal(msg_in, "org.freedesktop.portal.Request", "Response")) { +// uint32_t result; +// +// dbus_message_get_args(msg_in, &dbus_error, DBUS_TYPE_UINT32, &result, DBUS_TYPE_ARRAY, , DBUS_TYPE_INVALID); +// +// std::cout << "Global shortcut session creation result: " << result << std::endl; +// } +// +// dbus_message_unref(msg_in); +// } +// +// dbus_connection_unref(dbus_conn); +//} diff --git a/src/timer.cpp b/src/timer.cpp new file mode 100644 index 0000000..7ecd818 --- /dev/null +++ b/src/timer.cpp @@ -0,0 +1,51 @@ +#include +#include + +#include "timer.h" + + +Timer::Timer() +{ + Clear(); +} + +TimeDuration Timer::GetTimeLeft() +{ + auto time_left = end_time - std::chrono::system_clock::now(); + bool negative = false; + + if (time_left.count() < 0) { + time_left *= -1; + negative = true; + } + + return TimeDuration{ + std::chrono::duration_cast(time_left).count(), + std::chrono::duration_cast(time_left).count() % 60, + std::chrono::duration_cast(time_left).count() % 60, + std::chrono::duration_cast(time_left).count() % 1000, + + std::chrono::duration_cast(time_left).count(), + std::chrono::duration_cast(time_left).count(), + std::chrono::duration_cast(time_left).count(), + + negative, + }; +} + +void Timer::AddMinutes(int64_t minutes) +{ + const auto time_left = end_time - std::chrono::system_clock::now(); + + if (time_left.count() <= 0) { + end_time = std::chrono::system_clock::now() + std::chrono::minutes(minutes); + } else { + end_time += std::chrono::minutes(minutes); + } +} + +void Timer::Clear() +{ + end_time = std::chrono::system_clock::now() - std::chrono::seconds(60); +} + diff --git a/timer.desktop b/timer.desktop new file mode 100755 index 0000000..8455d0c --- /dev/null +++ b/timer.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +Categories=Utility; +Comment[en_US]=Timer osd controlled with global shortcuts +Comment=Timer osd controlled with global shortcuts +Exec=timer_overlay +GenericName[en_US]=Overlay +GenericName=Overlay +MimeType= +Name[en_US]=Timer Overlay +Name=Timer Overlay +NoDisplay=false +Path=/usr/bin +SingleMainWindow=true +StartupNotify=false +StartupWMClass=timer-overlay +Terminal=false +TerminalOptions= +Type=Application +X-KDE-SubstituteUID=false +X-KDE-Username=