commit 64dd251dd018883b1d396372c5ad246b7465db7c Author: Josh Ashton Date: Wed Feb 11 13:44:50 2026 -0700 init commit diff --git a/.cache/clangd/index/main.c.8F071D70C859F269.idx b/.cache/clangd/index/main.c.8F071D70C859F269.idx new file mode 100644 index 0000000..5b18974 Binary files /dev/null and b/.cache/clangd/index/main.c.8F071D70C859F269.idx differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03bd412 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..45586f6 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# A Simple Notes Application +`osn` is a Notes application written in C, using GTK bindings, and is meant for use on Linux / Wayland / Hyprland setups and the Spaces ecosystem, more to come soon on that. + +Currently, the default (and only) option is to toggle `osn` when `osn` is ran again. This keeps the Notes in memory, keeping the launch nice and responsive. + +## Build from source +Run the following commands: +``` +git clone https://github.com/weave-space/osnotes +cd osnotes +meson compile -C build +./build/osn +``` + +## TODO +- [ ] Responsive design sizes +- [ ] CLI flags +- [ ] Man page +- [ ] Documentation +- [ ] In-depth testing +- [ ] Customization + +## Customization +There is currently no customization available for OSNotes. + +## Credits +- Google Gemini 3 (Explanation of docs and concepts and debugging.) +- https://docs.gtk.org/gtk4/index.html diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..4498709 --- /dev/null +++ b/meson.build @@ -0,0 +1,12 @@ +project('osnotes', 'c', + version : '0.1', + default_options : ['warning_level=0']) + +# Import the GNOME module to handle GResources +gnome = import('gnome') + +gtk_dep = dependency('gtk4') +libevdev_dep = dependency('libevdev') +layer_shell_dep = dependency('gtk4-layer-shell-0') + +subdir('src') diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..f8631b2 --- /dev/null +++ b/src/main.c @@ -0,0 +1,344 @@ +#include "glib-object.h" +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +typedef enum { TOOL_PEN, TOOL_HIGHLIGHTER, TOOL_ERASER } ToolType; + +typedef struct { + double x, y, pressure; +} Point; + +typedef struct { + GQueue *points; + GdkRGBA color; + double width; + ToolType tool_type; +} Stroke; + +GList *strokes = NULL; +Stroke *current_stroke = NULL; +cairo_surface_t *surface = NULL; + +GtkWidget *canvas = NULL; +GtkWidget *main_window = NULL; + +ToolType current_tool = TOOL_PEN; +int bg_template = 0; + +static void clear_strokes() { + if (!strokes) + return; + + for (GList *l = strokes; l != NULL; l = l->next) { + Stroke *s = (Stroke *)l->data; + if (s->points) { + // Free every Point struct inside the queue + while (!g_queue_is_empty(s->points)) { + g_free(g_queue_pop_head(s->points)); + } + g_queue_free(s->points); + } + g_free(s); + } + g_list_free(strokes); + strokes = NULL; +} + +static void set_cairo_for_stroke(cairo_t *cr, Stroke *s) { + gdk_cairo_set_source_rgba(cr, &s->color); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); + + if (s->tool_type == TOOL_HIGHLIGHTER) { + cairo_set_operator(cr, CAIRO_OPERATOR_MULTIPLY); + } else if (s->tool_type == TOOL_ERASER) { + cairo_set_operator(cr, CAIRO_OPERATOR_DEST_OUT); + } else { + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + } +} + +static void draw_stroke_path(cairo_t *cr, Stroke *s) { + if (!s || !s->points) + return; + + if (g_queue_get_length(s->points) == 1) { + set_cairo_for_stroke(cr, s); + Point *p = (Point *)s->points->head->data; + cairo_arc(cr, p->x, p->y, s->width / 2.0, 0, 2 * G_PI); + cairo_fill(cr); + return; + } + + set_cairo_for_stroke(cr, s); + + GList *l = s->points->head; + Point *p1 = (Point *)l->data; + cairo_move_to(cr, p1->x, p1->y); + + for (l = l->next; l != NULL; l = l->next) { + Point *p2 = (Point *)l->data; + double pressure = (p1->pressure + p2->pressure) / 2.0; + double line_width = s->width; + + if (s->tool_type == TOOL_PEN) { + line_width *= MAX(0.2, pressure * 1.5); + } + + cairo_set_line_width(cr, line_width); + cairo_line_to(cr, p2->x, p2->y); + cairo_stroke(cr); + + cairo_move_to(cr, p2->x, p2->y); + p1 = p2; + } +} + +static void draw_background(cairo_t *cr, int w, int h) { + cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0); + cairo_paint(cr); +} + +static void ensure_surface(GtkWidget *widget) { + int width = gtk_widget_get_width(widget); + int height = gtk_widget_get_height(widget); + + // 1. Prevent 0-sized surface creation (The Portrait fix) + if (width <= 1 || height <= 1) + return; + + if (surface && (cairo_image_surface_get_width(surface) != width || + cairo_image_surface_get_height(surface) != height)) { + + // 2. Optional: Copy old content to new surface so you don't lose notes on + // rotation + cairo_surface_t *old_surface = surface; + surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *cr = cairo_create(surface); + + draw_background(cr, width, height); + + // Re-draw all strokes onto the new size + for (GList *l = strokes; l != NULL; l = l->next) { + draw_stroke_path(cr, (Stroke *)l->data); + } + + cairo_destroy(cr); + cairo_surface_destroy(old_surface); + } + + if (!surface) { + surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + } +} + +void on_draw(GtkDrawingArea *area, cairo_t *cr, int width, int height, + gpointer data) { + draw_background(cr, width, height); + ensure_surface(GTK_WIDGET(area)); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_paint(cr); + if (current_stroke) { + draw_stroke_path(cr, current_stroke); + } +} + +void start_new_stroke(double x, double y, GdkEvent *event) { + current_stroke = g_new0(Stroke, 1); + current_stroke->points = g_queue_new(); + current_stroke->tool_type = current_tool; + + GdkDevice *device = gdk_event_get_device(event); + GdkDeviceTool *tool = gdk_device_get_device_tool(device); + + if (tool) { + GdkDeviceToolType type = gdk_device_tool_get_tool_type(tool); + g_print("DEBUG: Tool Type: %d\n", type); + if (type == GDK_DEVICE_TOOL_TYPE_ERASER) { + current_stroke->tool_type = TOOL_ERASER; + } + } + + GdkModifierType state = gdk_event_get_modifier_state(event); + if (state & GDK_BUTTON3_MASK) { + current_stroke->tool_type = TOOL_HIGHLIGHTER; + g_print("DEBUG: Button 3 Mask Detected\n"); + } + + if (current_stroke->tool_type == TOOL_PEN) { + gdk_rgba_parse(¤t_stroke->color, "black"); + current_stroke->width = 2.0; + } else if (current_stroke->tool_type == TOOL_HIGHLIGHTER) { + gdk_rgba_parse(¤t_stroke->color, "#FFF500"); + current_stroke->color.alpha = 0.4; + current_stroke->width = 20.0; + } else if (current_stroke->tool_type == TOOL_ERASER) { + gdk_rgba_parse(¤t_stroke->color, "#FFFFFF"); + current_stroke->color.alpha = 1.0; + current_stroke->width = 30.0; + } + + Point *p = g_new0(Point, 1); + p->x = x; + p->y = y; + p->pressure = 0.5; + gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &p->pressure); + + g_queue_push_tail(current_stroke->points, p); + gtk_widget_queue_draw(canvas); +} + +static void on_motion(GtkEventControllerMotion *controller, double x, double y, + gpointer user_data) { + GdkEvent *event = + gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(controller)); + GdkModifierType state = gdk_event_get_modifier_state(event); + + gboolean is_drawing = + (state & (GDK_BUTTON1_MASK | GDK_BUTTON2_MASK | GDK_BUTTON3_MASK)); + + if (is_drawing) { + if (!current_stroke) { + start_new_stroke(x, y, event); + } else { + Point *p = g_new0(Point, 1); + p->x = x; + p->y = y; + p->pressure = 0.5; + gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &p->pressure); + g_queue_push_tail(current_stroke->points, p); + gtk_widget_queue_draw(canvas); + } + } else { + if (current_stroke) { + cairo_t *cr = cairo_create(surface); + draw_stroke_path(cr, current_stroke); + cairo_destroy(cr); + strokes = g_list_append(strokes, current_stroke); + current_stroke = NULL; + gtk_widget_queue_draw(canvas); + } + } +} + +static void on_pressed(GtkGestureClick *gesture, int n_press, double x, + double y, gpointer user_data) { + if (current_stroke) + return; + + GdkEvent *event = + gtk_event_controller_get_current_event(GTK_EVENT_CONTROLLER(gesture)); + start_new_stroke(x, y, event); +} + +static void on_released(GtkGestureClick *gesture, int n_press, double x, + double y, gpointer user_data) { + if (current_stroke) { + cairo_t *cr = cairo_create(surface); + draw_stroke_path(cr, current_stroke); + cairo_destroy(cr); + strokes = g_list_append(strokes, current_stroke); + current_stroke = NULL; + } + gtk_widget_queue_draw(canvas); +} + +static void on_tool_changed(GtkToggleButton *btn, gpointer user_data) { + if (!gtk_toggle_button_get_active(btn)) + return; + const char *id = gtk_buildable_get_buildable_id(GTK_BUILDABLE(btn)); + if (g_strcmp0(id, "btn_pen") == 0) + current_tool = TOOL_PEN; + else if (g_strcmp0(id, "btn_highlighter") == 0) + current_tool = TOOL_HIGHLIGHTER; + else if (g_strcmp0(id, "btn_eraser") == 0) + current_tool = TOOL_ERASER; +} + +static void on_template_changed(GtkComboBoxText *combo, gpointer user_data) { + bg_template = gtk_combo_box_get_active(GTK_COMBO_BOX(combo)); + if (surface) { + cairo_surface_destroy(surface); + surface = NULL; + } + gtk_widget_queue_draw(canvas); +} + +static void on_export_response(GtkNativeDialog *d, int r, gpointer u) { + if (r == GTK_RESPONSE_ACCEPT) { + GFile *f = gtk_file_chooser_get_file(GTK_FILE_CHOOSER(d)); + char *p = g_file_get_path(f); + if (surface) + cairo_surface_write_to_png(surface, p); + g_free(p); + g_object_unref(f); + } + g_object_unref(d); +} + +static void action_export(GSimpleAction *a, GVariant *p, gpointer u) { + if (!surface) + return; + GtkFileChooserNative *d = gtk_file_chooser_native_new( + "Export", GTK_WINDOW(main_window), GTK_FILE_CHOOSER_ACTION_SAVE, "Save", + "Cancel"); + g_signal_connect(d, "response", G_CALLBACK(on_export_response), NULL); + gtk_native_dialog_show(GTK_NATIVE_DIALOG(d)); +} + +static void on_save_response(GtkNativeDialog *d, int r, gpointer data) {} + +static void action_save(GSimpleAction *a, GVariant *p, gpointer u) {} + +static void on_load_response(GtkNativeDialog *d, int r, gpointer u) {} + +static void action_load(GSimpleAction *a, GVariant *p, gpointer u) {} + +static void activate(GtkApplication *app, gpointer user_data) { + GtkBuilderCScope *scope = GTK_BUILDER_CSCOPE(gtk_builder_cscope_new()); + gtk_builder_cscope_add_callback_symbol(scope, "on_tool_changed", + G_CALLBACK(on_tool_changed)); + + GtkBuilder *builder = gtk_builder_new(); + gtk_builder_set_scope(builder, GTK_BUILDER_SCOPE(scope)); + g_object_unref(scope); + gtk_builder_add_from_resource(builder, "/org/spaces/osnotes/notes.ui", NULL); + + main_window = GTK_WIDGET(gtk_builder_get_object(builder, "main_window")); + canvas = GTK_WIDGET(gtk_builder_get_object(builder, "canvas")); + + const GActionEntry app_entries[] = { + {"save", action_save, NULL, NULL, NULL}, + {"load", action_load, NULL, NULL, NULL}, + {"export", action_export, NULL, NULL, NULL}}; + g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, + G_N_ELEMENTS(app_entries), app); + + gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(canvas), on_draw, NULL, NULL); + + GtkEventController *motion = gtk_event_controller_motion_new(); + g_signal_connect(motion, "motion", G_CALLBACK(on_motion), NULL); + gtk_widget_add_controller(canvas, motion); + + GtkGesture *click = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(click), 0); + g_signal_connect(click, "pressed", G_CALLBACK(on_pressed), NULL); + g_signal_connect(click, "released", G_CALLBACK(on_released), NULL); + gtk_widget_add_controller(canvas, GTK_EVENT_CONTROLLER(click)); + + gtk_window_set_application(GTK_WINDOW(main_window), app); + gtk_window_present(GTK_WINDOW(main_window)); +} + +int main(int argc, char **argv) { + GtkApplication *app = + gtk_application_new("org.spaces.osnotes", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); + int status = g_application_run(G_APPLICATION(app), argc, argv); + g_object_unref(app); + return status; +} + +#pragma GCC diagnostic pop diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..c6d9d58 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,13 @@ +# Compile the resources into a C file +res = gnome.compile_resources( + 'osnotes-resources', + 'resources.xml', + source_dir: '.', # Look for .ui and .css in the current directory +) + +executable('osn', + 'main.c', + res, # Add the compiled resource file to the sources + dependencies : [gtk_dep, libevdev_dep, layer_shell_dep], + export_dynamic : true, + install : true) diff --git a/src/notes.ui b/src/notes.ui new file mode 100644 index 0000000..bd46443 --- /dev/null +++ b/src/notes.ui @@ -0,0 +1,99 @@ + + + +
+ + + + + + + + + + Export to PNG + app.export + +
+
+ + + OSNotes + + + vertical + + + + FALSE + + + + 6 + + + + + document-edit-symbolic + TRUE + Pen + + + + + + + edit-select-symbolic + Highlighter + btn_pen + + + + + + + edit-clear-symbolic + Eraser + btn_pen + + + + + + + + + open-menu-symbolic + main_menu + + + + + + + + + + + + + + + + + + + + + + TRUE + TRUE + TRUE + + + + + + + +
diff --git a/src/resources.xml b/src/resources.xml new file mode 100644 index 0000000..dde5e5c --- /dev/null +++ b/src/resources.xml @@ -0,0 +1,7 @@ + + + + notes.ui + style.css + + diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..533dfd8 --- /dev/null +++ b/src/style.css @@ -0,0 +1,5 @@ +#canvas_box, +#canvas_box > * { + background-color: black; + color: white; +}