1 Commits
Author SHA1 Message Date
NotAlexNoyleandClaude Opus 4.8 e3b229c464 Add MIT License
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 14:17:33 -04:00
29 changed files with 274 additions and 3520 deletions

No files matched your search

+15 -15
View File
@@ -21,7 +21,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential pkg-config \
libgtk-3-dev libwebkit2gtk-4.1-dev
libgtk-3-dev
- name: Build
run: make -j
@@ -29,8 +29,8 @@ jobs:
- name: ABI check (GLib)
run: |
set -euo pipefail
if nm -D seemd | grep -q ' g_string_free_and_steal$'; then
echo "Error: seemd links against g_string_free_and_steal (too-new GLib ABI for jammy/mint 21.x)" >&2
if nm -D viewmd | grep -q ' g_string_free_and_steal$'; then
echo "Error: viewmd links against g_string_free_and_steal (too-new GLib ABI for jammy/mint 21.x)" >&2
exit 1
fi
@@ -41,7 +41,7 @@ jobs:
ARCH="$(dpkg --print-architecture)"
MAINTAINER="${GITHUB_REPOSITORY_OWNER} <noreply@github.com>"
PKGDIR="dist/deb/seemd_${VERSION}_${ARCH}"
PKGDIR="dist/deb/viewmd_${VERSION}_${ARCH}"
rm -rf "$PKGDIR"
mkdir -p "$PKGDIR/DEBIAN"
@@ -57,7 +57,7 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: seemd-deb
name: viewmd-deb
path: dist/deb/*.deb
rpm:
@@ -68,7 +68,7 @@ jobs:
run: |
dnf -y install \
git make gcc rpm-build \
gtk3-devel webkit2gtk4.1-devel pkgconf-pkg-config
gtk3-devel pkgconf-pkg-config
- uses: actions/checkout@v4
with:
@@ -87,7 +87,7 @@ jobs:
TAG="${{ github.event.release.tag_name || github.ref_name }}"
VERSION="${TAG#v}"
mkdir -p dist
git archive --format=tar.gz --prefix="seemd-${VERSION}/" -o "dist/seemd-${VERSION}.tar.gz" HEAD
git archive --format=tar.gz --prefix="viewmd-${VERSION}/" -o "dist/viewmd-${VERSION}.tar.gz" HEAD
- name: Build .rpm
run: |
@@ -98,25 +98,25 @@ jobs:
TOPDIR="$PWD/dist/rpmbuild"
mkdir -p "$TOPDIR"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
cp "dist/seemd-${VERSION}.tar.gz" "$TOPDIR/SOURCES/"
cp "dist/viewmd-${VERSION}.tar.gz" "$TOPDIR/SOURCES/"
sed \
-e "s/@VERSION@/${VERSION}/g" \
-e "s|@URL@|${URL}|g" \
packaging/rpm/seemd.spec.in > "$TOPDIR/SPECS/seemd.spec"
packaging/rpm/viewmd.spec.in > "$TOPDIR/SPECS/viewmd.spec"
rpmbuild \
--define "_topdir ${TOPDIR}" \
-bb "$TOPDIR/SPECS/seemd.spec"
-bb "$TOPDIR/SPECS/viewmd.spec"
mkdir -p dist/rpm
# Only publish the main runtime RPM (exclude -debuginfo/-debugsource).
find "$TOPDIR/RPMS" -type f -name 'seemd-[0-9]*.rpm' -exec cp -v -t dist/rpm {} +
find "$TOPDIR/RPMS" -type f -name 'viewmd-[0-9]*.rpm' -exec cp -v -t dist/rpm {} +
- uses: actions/upload-artifact@v4
with:
name: seemd-rpm
path: dist/rpm/seemd-[0-9]*.rpm
name: viewmd-rpm
path: dist/rpm/viewmd-[0-9]*.rpm
release-assets:
runs-on: ubuntu-latest
@@ -126,7 +126,7 @@ jobs:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
pattern: seemd-*
pattern: viewmd-*
merge-multiple: true
path: dist
@@ -135,4 +135,4 @@ jobs:
with:
files: |
dist/*.deb
dist/seemd-[0-9]*.rpm
dist/viewmd-[0-9]*.rpm
+7 -8
View File
@@ -1,7 +1,6 @@
CC = gcc
PKGS = gtk+-3.0 webkit2gtk-4.1
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags $(PKGS)`
LDFLAGS = `pkg-config --libs $(PKGS)`
CFLAGS = -Wall -Wextra -O2 -g `pkg-config --cflags gtk+-3.0`
LDFLAGS = `pkg-config --libs gtk+-3.0`
SRCDIR = src
OBJDIR = obj
@@ -9,7 +8,7 @@ BINDIR = .
SOURCES = $(wildcard $(SRCDIR)/*.c)
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(OBJDIR)/md4c.o
TARGET = $(BINDIR)/seemd
TARGET = $(BINDIR)/viewmd
PREFIX ?= /usr/local
DESTDIR ?=
@@ -38,12 +37,12 @@ clean:
rm -rf $(OBJDIR) $(TARGET)
install: $(TARGET)
install -Dm755 $(TARGET) $(DESTDIR)$(bindir)/seemd
install -Dm644 assets/seemd.desktop $(DESTDIR)$(applicationsdir)/seemd.desktop
install -Dm755 $(TARGET) $(DESTDIR)$(bindir)/viewmd
install -Dm644 assets/viewmd.desktop $(DESTDIR)$(applicationsdir)/viewmd.desktop
uninstall:
rm -f $(DESTDIR)$(bindir)/seemd
rm -f $(DESTDIR)$(applicationsdir)/seemd.desktop
rm -f $(DESTDIR)$(bindir)/viewmd
rm -f $(DESTDIR)$(applicationsdir)/viewmd.desktop
# Header dependencies
$(OBJDIR)/main.o: $(SRCDIR)/app.h $(SRCDIR)/window.h
+54 -23
View File
@@ -1,19 +1,18 @@
# 👁 SeeMD
# ViewMD
A lightweight GTK 3 markdown viewer and editor for desktop Linux. It is ideal as your default app for opening `.md` files. Uses the excellent library [md4c](https://github.com/mity/md4c) for markdown parsing and WebKitGTK for GitHub-style preview rendering. This project is a fork of [ViewMD](https://github.com/rabfulton/ViewMD) by NotAlexNoyle.
A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your default app for opening `.md` files. Uses the excellent library [md4c](https://github.com/mity/md4c) for markdown parsing. This project is sort of a companion app/fork of my other markdown project [TrayMD](https://github.com/rabfulton/TrayMD).
![SeeMD](assets/seemd.png)
![ViewMD](assets/screenshot.png)
## Features
- **GitHub-style preview** - Rendered Markdown and inline HTML are displayed through WebKitGTK with GitHub-like markdown styling
- **Read-only preview** - Rendered Markdown stays non-editable until source mode is enabled
- **Source editing mode** - Toggle between rendered preview and editable Markdown
- **Minimal UI** - Clean toolbar with open, reload, edit, save, and settings buttons
- **Lightweight** - Pure C with GTK and WebKitGTK, fast startup
- **Native GTK viewer** - No webviews
- **Read-only rendering** - Focused on viewing markdown files
- **Minimal UI** - Clean toolbar with open and settings buttons
- **Lightweight** - Pure C, no web technologies, fast startup
- **Hyperlink support** - Left click opens links and internal anchors
- **Document search** - `Ctrl+F` with next/previous match navigation
- **Local image support** - Local images keep natural size unless they need to fit the document window
- **Local image support** - Local images are resized to fit the the document window
## Supported Markdown
@@ -31,19 +30,38 @@ A lightweight GTK 3 markdown viewer and editor for desktop Linux. It is ideal as
| `[text](url)` | Link |
| `~~strike~~` | Strikethrough |
| `\| table \| row \|` | Markdown tables |
| `<strong>HTML</strong>` | GitHub-style inline and block HTML rendered in preview mode |
| `---` | Horizontal rule |
Fenced code blocks are syntax-highlighted GitHub-style in the preview for the languages `c`, `java`, `javascript`, `kotlin`, `python`, and `bash`, covering keywords, strings, numbers, and comments.
Code blocks currently support a beta version of keyword highlighting for fenced languages `c`, `java`, and `python`.
## Installation
### Arch Linux (AUR)
https://aur.archlinux.org/packages/viewmd
### Debian/Ubuntu (.deb)
Install a built `.deb` package with:
```bash
sudo apt install ./viewmd_*.deb
```
### Fedora/RHEL (.rpm)
Install a built `.rpm` package with:
```bash
sudo dnf install ./viewmd-*.rpm
```
## Usage
Run `seemd` to start the application.
Run `viewmd` to start the application.
- **Open button**: Open a markdown document
- **Reload button**: Reload the currently open document from disk
- **Edit button**: Toggle editable Markdown source mode (`Ctrl+E`)
- **Save button**: Save the current Markdown source (`Ctrl+S`)
- **Settings button**: Adjust theme, fonts, and markdown accent colors
### Find in Document
@@ -55,11 +73,11 @@ Run `seemd` to start the application.
### Set as Default `.md` Viewer
After installing, associate markdown MIME types with `seemd.desktop`:
After installing, associate markdown MIME types with `viewmd.desktop`:
```bash
xdg-mime default seemd.desktop text/markdown
xdg-mime default seemd.desktop text/x-markdown
xdg-mime default viewmd.desktop text/markdown
xdg-mime default viewmd.desktop text/x-markdown
```
Verify the current default:
@@ -83,8 +101,8 @@ sudo make install
```
This installs:
- Binary to `/usr/local/bin/seemd`
- Desktop file to `/usr/local/share/applications/seemd.desktop`
- Binary to `/usr/local/bin/viewmd`
- Desktop file to `/usr/local/share/applications/viewmd.desktop`
### Uninstallation
@@ -95,19 +113,32 @@ sudo make uninstall
### Arch Linux
```bash
sudo pacman -S gtk3 webkit2gtk-4.1
sudo pacman -S gtk3
```
### Ubuntu/Debian
```bash
sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev
sudo apt install libgtk-3-dev
```
### Fedora
```bash
sudo dnf install gtk3-devel webkit2gtk4.1-devel
sudo dnf install gtk3-devel
```
## Packaging Templates
- Arch: `packaging/arch/PKGBUILD`
- Debian: `packaging/deb/control.in`
- RPM: `packaging/rpm/viewmd.spec.in`
## License
Released under the [MIT License](LICENSE).
MIT License
## Other Useful Projects
- TrayMD is an app for taking notes in markdown with live editing [TrayMD](https://github.com/rabfulton/TrayMD)
- Preditor is an image viewer with a similar philosophy to viewmd [preditor](https://github.com/rabfulton/preditor)
- Try my AI panel plugin for XFCE [XFCE Ask](https://github.com/rabfulton/xfce-ask)
- For a feature complete AI application try out [ChatGTK](https://github.com/rabfulton/ChatGTK)
- A lightweight speech to text implementation [Auriscribe](https://github.com/rabfulton/Auriscribe)
- A lightweight local movie database and browser [ReelVault](https://github.com/rabfulton/ReelVault)
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

@@ -1,7 +1,7 @@
[Desktop Entry]
Name=SeeMD
Name=ViewMD
Comment=Lightweight markdown viewer
Exec=seemd %F
Exec=viewmd %F
Icon=accessories-text-editor
Terminal=false
Type=Application
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+3 -3
View File
@@ -1,11 +1,11 @@
pkgname=seemd
pkgname=viewmd
pkgver=0.r0.g0000000
pkgrel=1
pkgdesc="Lightweight GTK3 markdown viewer"
arch=('x86_64')
url="https://github.com/NotAlexNoyle/SeeMD"
url="https://github.com/rabfulton/ViewMD"
license=('MIT')
depends=('gtk3' 'webkit2gtk-4.1')
depends=('gtk3')
makedepends=('git' 'gcc' 'make' 'pkgconf')
source=("$pkgname::git+$url.git")
sha256sums=('SKIP')
+4 -4
View File
@@ -1,9 +1,9 @@
Package: seemd
Package: viewmd
Version: @VERSION@
Section: utils
Priority: optional
Architecture: @ARCH@
Maintainer: @MAINTAINER@
Depends: libgtk-3-0, libwebkit2gtk-4.1-0
Description: SeeMD - lightweight GTK3 markdown viewer
SeeMD is a lightweight GTK3 markdown viewer with GitHub-style WebKit preview rendering.
Depends: libgtk-3-0
Description: ViewMD - lightweight GTK3 markdown viewer
ViewMD is a lightweight native GTK3 markdown viewer.
@@ -1,26 +1,24 @@
Name: seemd
Name: viewmd
Version: @VERSION@
Release: 1%{?dist}
Summary: Lightweight GTK3 markdown viewer
License: MIT
URL: @URL@
Source0: seemd-%{version}.tar.gz
Source0: viewmd-%{version}.tar.gz
BuildRequires: gcc
BuildRequires: make
BuildRequires: pkgconfig
BuildRequires: gtk3-devel
BuildRequires: webkit2gtk4.1-devel
Requires: gtk3
Requires: webkit2gtk4.1
%description
SeeMD is a lightweight GTK3 markdown viewer with GitHub-style WebKit preview rendering.
ViewMD is a lightweight native GTK3 markdown viewer.
%prep
%autosetup -n seemd-%{version}
%autosetup -n viewmd-%{version}
%build
make %{?_smp_mflags}
@@ -30,9 +28,9 @@ rm -rf %{buildroot}
make install DESTDIR=%{buildroot} PREFIX=%{_prefix}
%files
%{_bindir}/seemd
%{_datadir}/applications/seemd.desktop
%{_bindir}/viewmd
%{_datadir}/applications/viewmd.desktop
%changelog
* Thu Jan 01 1970 SeeMD Packager <noreply@example.com> - @VERSION@-1
* Thu Jan 01 1970 ViewMD Packager <noreply@example.com> - @VERSION@-1
- Initial package
BIN
View File
Binary file not shown.
+5 -44
View File
@@ -27,7 +27,7 @@ MarkydApp *markyd_app_new(void) {
flags = (GApplicationFlags)(flags | G_APPLICATION_NON_UNIQUE |
G_APPLICATION_HANDLES_OPEN);
self->gtk_app = gtk_application_new("org.seemd.app", flags);
self->gtk_app = gtk_application_new("org.viewmd.app", flags);
self->current_file_path = NULL;
g_signal_connect(self->gtk_app, "activate", G_CALLBACK(on_activate), self);
@@ -69,10 +69,10 @@ static void markyd_app_update_window_title(MarkydApp *self) {
if (self->current_file_path && self->current_file_path[0] != '\0') {
gchar *base = g_path_get_basename(self->current_file_path);
title = g_strdup_printf("SeeMD - %s", base);
title = g_strdup_printf("ViewMD - %s", base);
g_free(base);
} else {
title = g_strdup("SeeMD");
title = g_strdup("ViewMD");
}
gtk_window_set_title(GTK_WINDOW(self->window->window), title);
@@ -111,7 +111,7 @@ static void on_open(GtkApplication *gtk_app, GFile **files, gint n_files,
}
if (!opened && n_files > 0) {
g_printerr("SeeMD: unable to open provided file(s)\n");
g_printerr("ViewMD: unable to open provided file(s)\n");
}
markyd_window_show(self->window);
@@ -126,7 +126,7 @@ static void markyd_app_ensure_window(MarkydApp *self) {
self->editor = self->window->editor;
markyd_editor_set_content(self->editor,
"# SeeMD\n\nUse the Open button to load a markdown document.");
"# ViewMD\n\nUse the Open button to load a markdown document.");
markyd_app_update_window_title(self);
}
@@ -156,45 +156,6 @@ gboolean markyd_app_open_file(MarkydApp *self, const gchar *path) {
return TRUE;
}
gboolean markyd_app_save_file(MarkydApp *self, const gchar *path) {
gchar *content;
GError *error = NULL;
if (!self || !self->editor || !path || path[0] == '\0') {
return FALSE;
}
content = markyd_editor_get_content(self->editor);
if (!g_file_set_contents(path, content ? content : "", -1, &error)) {
if (error) {
g_printerr("Failed to save markdown file '%s': %s\n", path, error->message);
g_error_free(error);
}
g_free(content);
return FALSE;
}
g_free(content);
g_free(self->current_file_path);
self->current_file_path = g_strdup(path);
markyd_app_update_window_title(self);
return TRUE;
}
gboolean markyd_app_save_current_file(MarkydApp *self) {
gchar *path;
gboolean ok;
if (!self || !self->current_file_path || self->current_file_path[0] == '\0') {
return FALSE;
}
path = g_strdup(self->current_file_path);
ok = markyd_app_save_file(self, path);
g_free(path);
return ok;
}
const gchar *markyd_app_get_current_path(MarkydApp *self) {
if (!self) {
return NULL;
-2
View File
@@ -25,8 +25,6 @@ int markyd_app_run(MarkydApp *app, int argc, char **argv);
/* Document management */
gboolean markyd_app_open_file(MarkydApp *app, const gchar *path);
gboolean markyd_app_save_file(MarkydApp *app, const gchar *path);
gboolean markyd_app_save_current_file(MarkydApp *app);
/* Utility */
const gchar *markyd_app_get_current_path(MarkydApp *app);
+6 -218
View File
@@ -76,84 +76,6 @@ static const MarkydKeywordGroup py_groups[] = {
{MARKYD_TAG_CODE_KW_C, py_kw_group_c, G_N_ELEMENTS(py_kw_group_c)},
};
static const gchar *const js_kw_group_a[] = {
"await", "break", "case", "catch", "continue", "default",
"do", "else", "finally", "for", "if", "in",
"instanceof", "of", "return", "switch", "throw", "try",
"while", "with", "yield",
};
static const gchar *const js_kw_group_b[] = {
"async", "class", "const", "debugger", "delete", "export",
"extends","function","import", "let", "new", "static",
"super", "this", "typeof", "var", "void",
};
static const gchar *const js_kw_group_c[] = {
"false", "Infinity", "NaN", "null", "true", "undefined",
};
static const MarkydKeywordGroup js_groups[] = {
{MARKYD_TAG_CODE_KW_A, js_kw_group_a, G_N_ELEMENTS(js_kw_group_a)},
{MARKYD_TAG_CODE_KW_B, js_kw_group_b, G_N_ELEMENTS(js_kw_group_b)},
{MARKYD_TAG_CODE_KW_C, js_kw_group_c, G_N_ELEMENTS(js_kw_group_c)},
};
static const gchar *const kotlin_kw_group_a[] = {
"break", "catch", "continue", "do", "else", "finally",
"for", "if", "in", "is", "return","throw",
"try", "when", "while",
};
static const gchar *const kotlin_kw_group_b[] = {
"abstract", "actual", "annotation", "as", "by",
"class", "companion", "const", "constructor","crossinline",
"data", "enum", "expect", "external", "final",
"fun", "import", "infix", "init", "inline",
"inner", "interface", "internal", "lateinit", "noinline",
"object", "open", "operator", "out", "override",
"package", "private", "protected", "public", "reified",
"sealed", "super", "suspend", "tailrec", "this",
"typealias", "val", "var", "vararg", "where",
};
static const gchar *const kotlin_kw_group_c[] = {
"false", "null", "true", "Any", "Boolean", "Byte", "Char",
"Double","Float","Int", "Long", "Nothing", "Short", "String",
"Unit",
};
static const MarkydKeywordGroup kotlin_groups[] = {
{MARKYD_TAG_CODE_KW_A, kotlin_kw_group_a, G_N_ELEMENTS(kotlin_kw_group_a)},
{MARKYD_TAG_CODE_KW_B, kotlin_kw_group_b, G_N_ELEMENTS(kotlin_kw_group_b)},
{MARKYD_TAG_CODE_KW_C, kotlin_kw_group_c, G_N_ELEMENTS(kotlin_kw_group_c)},
};
static const gchar *const bash_kw_group_a[] = {
"case", "do", "done", "elif", "else", "esac", "fi",
"for", "function","if", "in", "select","then", "time",
"until","while",
};
static const gchar *const bash_kw_group_b[] = {
"alias", "bg", "break", "cd", "command", "continue",
"declare", "echo", "eval", "exec", "exit", "export",
"getopts", "hash", "jobs", "kill", "let", "local",
"printf", "pwd", "read", "readonly","return", "set",
"shift", "source", "test", "trap", "type", "typeset",
"ulimit", "umask", "unalias", "unset", "wait",
};
static const gchar *const bash_kw_group_c[] = {
"false", "true",
};
static const MarkydKeywordGroup bash_groups[] = {
{MARKYD_TAG_CODE_KW_A, bash_kw_group_a, G_N_ELEMENTS(bash_kw_group_a)},
{MARKYD_TAG_CODE_KW_B, bash_kw_group_b, G_N_ELEMENTS(bash_kw_group_b)},
{MARKYD_TAG_CODE_KW_C, bash_kw_group_c, G_N_ELEMENTS(bash_kw_group_c)},
};
static gboolean is_ascii_identifier_char(gchar c) {
return (c == '_') || g_ascii_isalnum((guchar)c);
}
@@ -632,43 +554,23 @@ static void scan_line_c_like(const MarkydLanguageHighlight *language,
}
if (in_block_comment) {
gint start_char_index = char_index;
while (*p) {
if (p[0] == '*' && p[1] == '/') {
p += 2;
char_index += 2;
in_block_comment = FALSE;
break;
}
advance_utf8_char(&p, &char_index);
if (p[0] == '*' && p[1] == '/') {
p += 2;
char_index += 2;
in_block_comment = FALSE;
continue;
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
advance_utf8_char(&p, &char_index);
continue;
}
if (p[0] == '/' && p[1] == '/') {
gint start_char_index = char_index;
while (*p) {
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
break;
}
if (p[0] == '/' && p[1] == '*') {
gint start_char_index = char_index;
p += 2;
char_index += 2;
in_block_comment = TRUE;
while (*p) {
if (p[0] == '*' && p[1] == '/') {
p += 2;
char_index += 2;
in_block_comment = FALSE;
break;
}
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
continue;
}
if (allow_java_text_blocks && starts_with_triple_quote(p)) {
@@ -802,11 +704,6 @@ static void scan_line_python(const MarkydLanguageHighlight *language,
}
if (p[0] == '#') {
gint start_char_index = char_index;
while (*p) {
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
break;
}
@@ -930,120 +827,11 @@ static void scan_line_python(const MarkydLanguageHighlight *language,
}
}
/* Bash: '#' comments (position-sensitive), single/double-quoted strings, and
* keyword/builtin identifiers. No multi-line state is tracked. */
static void scan_line_bash(const MarkydLanguageHighlight *language,
const gchar *line, MarkydCodeScanState *state,
MarkydCodeTokenCallback on_token,
gpointer user_data) {
const gchar *p;
gint char_index = 0;
gchar prev = '\0';
if (!language || !line || !state || !on_token) {
return;
}
p = line;
while (*p) {
/* A '#' starts a comment only at the start of a word (line start or after
* whitespace), so tokens like "$#" and "a#b" are left alone. */
if (p[0] == '#' && (p == line || prev == ' ' || prev == '\t')) {
gint start_char_index = char_index;
while (*p) {
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_COMMENT, user_data);
break;
}
if (p[0] == '\'') {
gint start_char_index = char_index;
p++;
char_index++;
while (*p && p[0] != '\'') {
advance_utf8_char(&p, &char_index);
}
if (*p == '\'') {
p++;
char_index++;
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
prev = '\'';
continue;
}
if (p[0] == '"') {
gint start_char_index = char_index;
p++;
char_index++;
while (*p) {
if (p[0] == '\\' && p[1]) {
p++;
char_index++;
advance_utf8_char(&p, &char_index);
continue;
}
if (p[0] == '"') {
p++;
char_index++;
break;
}
advance_utf8_char(&p, &char_index);
}
on_token(start_char_index, char_index, MARKYD_TAG_CODE_LITERAL, user_data);
prev = '"';
continue;
}
{
gunichar c = g_utf8_get_char(p);
const gchar *next = g_utf8_next_char(p);
if (is_identifier_start(c)) {
const gchar *token_start = p;
gint start_char_index = char_index;
p = next;
char_index++;
while (*p) {
gunichar tc = g_utf8_get_char(p);
const gchar *tnext = g_utf8_next_char(p);
if (!is_identifier_char(tc)) {
break;
}
p = tnext;
char_index++;
}
gsize token_len = (gsize)(p - token_start);
const gchar *tag_name =
lookup_keyword_tag(language, token_start, token_len);
if (tag_name) {
on_token(start_char_index, char_index, tag_name, user_data);
}
prev = 'a';
continue;
}
}
prev = *p;
advance_utf8_char(&p, &char_index);
}
}
static const MarkydLanguageHighlight languages[] = {
{"c", c_groups, G_N_ELEMENTS(c_groups), scan_line_c},
{"java", java_groups, G_N_ELEMENTS(java_groups), scan_line_java},
{"python", py_groups, G_N_ELEMENTS(py_groups), scan_line_python},
{"py", py_groups, G_N_ELEMENTS(py_groups), scan_line_python},
{"javascript", js_groups, G_N_ELEMENTS(js_groups), scan_line_c},
{"js", js_groups, G_N_ELEMENTS(js_groups), scan_line_c},
{"kotlin", kotlin_groups, G_N_ELEMENTS(kotlin_groups), scan_line_java},
{"kt", kotlin_groups, G_N_ELEMENTS(kotlin_groups), scan_line_java},
{"bash", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
{"sh", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
{"shell", bash_groups, G_N_ELEMENTS(bash_groups), scan_line_bash},
};
const MarkydLanguageHighlight *
-1
View File
@@ -7,7 +7,6 @@
#define MARKYD_TAG_CODE_KW_B "code_kw_b"
#define MARKYD_TAG_CODE_KW_C "code_kw_c"
#define MARKYD_TAG_CODE_LITERAL "code_literal"
#define MARKYD_TAG_CODE_COMMENT "code_comment"
typedef struct _MarkydKeywordGroup {
const gchar *tag_name;
+1 -1
View File
@@ -50,7 +50,7 @@ void config_free(MarkydConfig *cfg) {
const gchar *config_get_path(void) {
if (!config_path) {
const gchar *config_dir = g_get_user_config_dir();
gchar *new_app_dir = g_build_filename(config_dir, "seemd", NULL);
gchar *new_app_dir = g_build_filename(config_dir, "viewmd", NULL);
g_mkdir_with_parents(new_app_dir, 0755);
config_path = g_build_filename(new_app_dir, "config.ini", NULL);
+47 -511
View File
@@ -2,7 +2,6 @@
#include "app.h"
#include "markdown.h"
#include <string.h>
#include <webkit2/webkit2.h>
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event,
gpointer user_data);
@@ -19,15 +18,6 @@ static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
gchar **out_path);
static void on_text_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation,
gpointer user_data);
static void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data);
static void sync_source_from_buffer(MarkydEditor *self);
static void show_source_buffer(MarkydEditor *self);
static void render_preview(MarkydEditor *self);
static gchar *get_preview_base_uri(MarkydEditor *self);
static gboolean on_preview_decide_policy(WebKitWebView *web_view,
WebKitPolicyDecision *decision,
WebKitPolicyDecisionType type,
gpointer user_data);
static gchar *get_url_from_iter_tags(GtkTextIter *iter) {
GSList *tags;
@@ -41,7 +31,7 @@ static gchar *get_url_from_iter_tags(GtkTextIter *iter) {
for (GSList *node = tags; node != NULL; node = node->next) {
GtkTextTag *tag = GTK_TEXT_TAG(node->data);
const gchar *stored =
(const gchar *)g_object_get_data(G_OBJECT(tag), SEEMD_LINK_URL_DATA);
(const gchar *)g_object_get_data(G_OBJECT(tag), VIEWMD_LINK_URL_DATA);
if (stored && stored[0] != '\0') {
url = g_strdup(stored);
break;
@@ -120,43 +110,24 @@ static gboolean scroll_to_markdown_anchor(MarkydEditor *self,
return TRUE;
}
static void set_editor_mouse_cursor(MarkydEditor *self, gboolean over_link) {
GdkWindow *win;
GdkDisplay *display;
GdkCursor *cursor;
if (!self || !self->text_view) {
return;
}
win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
static void set_link_cursor(MarkydEditor *self, gboolean active) {
GdkWindow *win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
if (!win) {
return;
}
display = gdk_window_get_display(win);
cursor = gdk_cursor_new_from_name(display, over_link ? "pointer" : "text");
if (!cursor) {
cursor = gdk_cursor_new_for_display(display, over_link ? GDK_HAND2 : GDK_XTERM);
}
gdk_window_set_cursor(win, cursor);
if (cursor) {
g_object_unref(cursor);
}
}
static void clear_editor_mouse_cursor(MarkydEditor *self) {
GdkWindow *win;
if (!self || !self->text_view) {
return;
}
win = gtk_text_view_get_window(GTK_TEXT_VIEW(self->text_view),
GTK_TEXT_WINDOW_TEXT);
if (win) {
if (active) {
GdkDisplay *display = gdk_window_get_display(win);
GdkCursor *cursor = gdk_cursor_new_from_name(display, "pointer");
if (!cursor) {
cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
}
gdk_window_set_cursor(win, cursor);
if (cursor) {
g_object_unref(cursor);
}
} else {
gdk_window_set_cursor(win, NULL);
}
}
@@ -165,11 +136,6 @@ static void apply_markdown(MarkydEditor *self) {
if (!self) {
return;
}
if (self->edit_mode) {
return;
}
render_preview(self);
self->updating_tags = TRUE;
markdown_apply_tags(self->buffer,
@@ -224,66 +190,16 @@ static gboolean resolve_image_source_path(MarkydEditor *self, const gchar *src,
static gint get_image_max_width(MarkydEditor *self) {
GtkAllocation alloc;
GdkRectangle visible = {0, 0, 0, 0};
GtkTextView *view;
gint width;
if (!self || !self->text_view) {
return 0;
}
view = GTK_TEXT_VIEW(self->text_view);
gtk_text_view_get_visible_rect(view, &visible);
width = visible.width;
if (width <= 0) {
gtk_widget_get_allocation(self->text_view, &alloc);
width = alloc.width;
}
width -= gtk_text_view_get_left_margin(view);
width -= gtk_text_view_get_right_margin(view);
return MAX(width, 0);
}
static void compute_image_display_size(gint orig_width, gint orig_height,
gint requested_width,
gint requested_height, gint max_width,
gint *out_width, gint *out_height) {
gint width = orig_width;
gint height = orig_height;
if (orig_width <= 0 || orig_height <= 0) {
width = MAX(requested_width, 1);
height = MAX(requested_height, 1);
} else if (requested_width > 0 && requested_height > 0) {
width = requested_width;
height = requested_height;
} else if (requested_width > 0) {
width = requested_width;
height = (gint)(((gdouble)orig_height * (gdouble)width) /
(gdouble)orig_width + 0.5);
} else if (requested_height > 0) {
height = requested_height;
width = (gint)(((gdouble)orig_width * (gdouble)height) /
(gdouble)orig_height + 0.5);
}
width = MAX(width, 1);
height = MAX(height, 1);
if (max_width > 0 && width > max_width) {
height = (gint)(((gdouble)height * (gdouble)max_width) /
(gdouble)width + 0.5);
width = max_width;
}
if (out_width) {
*out_width = MAX(width, 1);
}
if (out_height) {
*out_height = MAX(height, 1);
}
gtk_widget_get_allocation(self->text_view, &alloc);
width = alloc.width - gtk_text_view_get_left_margin(GTK_TEXT_VIEW(self->text_view)) -
gtk_text_view_get_right_margin(GTK_TEXT_VIEW(self->text_view)) - 24;
return MAX(width, 64);
}
static void scale_image_widget(GtkWidget *widget, gint max_width) {
@@ -291,17 +207,13 @@ static void scale_image_widget(GtkWidget *widget, gint max_width) {
GdkPixbuf *orig;
gint ow;
gint oh;
gint requested_width;
gint requested_height;
gint display_width;
gint display_height;
if (!widget) {
if (!widget || max_width <= 0) {
return;
}
image = g_object_get_data(G_OBJECT(widget), "seemd-image-widget-child");
orig = g_object_get_data(G_OBJECT(widget), "seemd-image-orig-pixbuf");
image = g_object_get_data(G_OBJECT(widget), "viewmd-image-widget-child");
orig = g_object_get_data(G_OBJECT(widget), "viewmd-image-orig-pixbuf");
if (!GTK_IS_IMAGE(image) || !GDK_IS_PIXBUF(orig)) {
return;
}
@@ -312,23 +224,16 @@ static void scale_image_widget(GtkWidget *widget, gint max_width) {
return;
}
requested_width = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(widget), "seemd-image-requested-width"));
requested_height = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(widget), "seemd-image-requested-height"));
compute_image_display_size(ow, oh, requested_width, requested_height, max_width,
&display_width, &display_height);
if (display_width == ow && display_height == oh) {
gtk_image_set_from_pixbuf(GTK_IMAGE(image), orig);
} else {
GdkPixbuf *scaled = gdk_pixbuf_scale_simple(
orig, display_width, display_height, GDK_INTERP_BILINEAR);
gtk_image_set_from_pixbuf(GTK_IMAGE(image), scaled ? scaled : orig);
if (ow > max_width) {
gint nh = (gint)(((gdouble)oh * (gdouble)max_width) / (gdouble)ow);
GdkPixbuf *scaled =
gdk_pixbuf_scale_simple(orig, max_width, MAX(nh, 1), GDK_INTERP_BILINEAR);
gtk_image_set_from_pixbuf(GTK_IMAGE(image), scaled);
if (scaled) {
g_object_unref(scaled);
}
} else {
gtk_image_set_from_pixbuf(GTK_IMAGE(image), orig);
}
}
@@ -346,9 +251,9 @@ static void refresh_image_widget_scales(MarkydEditor *self) {
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_ANCHOR_DATA) != NULL) {
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_ANCHOR_DATA) != NULL) {
GtkWidget *image_widget =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDGET_DATA);
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_WIDGET_DATA);
if (image_widget) {
scale_image_widget(image_widget, max_width);
}
@@ -371,18 +276,14 @@ static void render_image_widgets(MarkydEditor *self) {
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_ANCHOR_DATA) != NULL) {
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_ANCHOR_DATA) != NULL) {
GtkWidget *image_widget =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDGET_DATA);
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_WIDGET_DATA);
if (!image_widget) {
const gchar *src =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_SRC_DATA);
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_SRC_DATA);
const gchar *alt =
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_ALT_DATA);
gint requested_width = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDTH_DATA));
gint requested_height = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(anchor), SEEMD_IMAGE_HEIGHT_DATA));
g_object_get_data(G_OBJECT(anchor), VIEWMD_IMAGE_ALT_DATA);
gchar *path = NULL;
if (resolve_image_source_path(self, src, &path)) {
@@ -394,14 +295,10 @@ static void render_image_widgets(MarkydEditor *self) {
gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
gtk_widget_set_halign(event_box, GTK_ALIGN_START);
gtk_container_add(GTK_CONTAINER(event_box), image);
g_object_set_data(G_OBJECT(event_box), "seemd-image-widget-child",
g_object_set_data(G_OBJECT(event_box), "viewmd-image-widget-child",
image);
g_object_set_data_full(G_OBJECT(event_box), "seemd-image-orig-pixbuf",
g_object_set_data_full(G_OBJECT(event_box), "viewmd-image-orig-pixbuf",
orig, g_object_unref);
g_object_set_data(G_OBJECT(event_box), "seemd-image-requested-width",
GINT_TO_POINTER(requested_width));
g_object_set_data(G_OBJECT(event_box), "seemd-image-requested-height",
GINT_TO_POINTER(requested_height));
scale_image_widget(event_box, max_width);
image_widget = event_box;
} else if (err) {
@@ -420,7 +317,7 @@ static void render_image_widgets(MarkydEditor *self) {
gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(self->text_view),
image_widget, anchor);
gtk_widget_show_all(image_widget);
g_object_set_data(G_OBJECT(anchor), SEEMD_IMAGE_WIDGET_DATA, image_widget);
g_object_set_data(G_OBJECT(anchor), VIEWMD_IMAGE_WIDGET_DATA, image_widget);
g_free(path);
} else {
scale_image_widget(image_widget, max_width);
@@ -442,16 +339,16 @@ static void render_table_widgets(MarkydEditor *self) {
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_ANCHOR_DATA) != NULL) {
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_ANCHOR_DATA) != NULL) {
GtkWidget *table =
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_WIDGET_DATA);
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_WIDGET_DATA);
if (!table) {
table = markdown_create_table_widget(anchor);
if (table) {
gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(self->text_view), table,
anchor);
gtk_widget_show_all(table);
g_object_set_data(G_OBJECT(anchor), SEEMD_TABLE_WIDGET_DATA, table);
g_object_set_data(G_OBJECT(anchor), VIEWMD_TABLE_WIDGET_DATA, table);
}
}
}
@@ -470,274 +367,20 @@ static void schedule_markdown_apply(MarkydEditor *self) {
if (!self || self->updating_tags || self->markdown_idle_id != 0) {
return;
}
if (self->edit_mode) {
return;
}
self->markdown_idle_id =
g_idle_add_full(G_PRIORITY_LOW, apply_markdown_idle, self, NULL);
}
static void sync_source_from_buffer(MarkydEditor *self) {
GtkTextIter start;
GtkTextIter end;
gchar *text;
if (!self || !self->buffer) {
return;
}
gtk_text_buffer_get_bounds(self->buffer, &start, &end);
text = gtk_text_buffer_get_text(self->buffer, &start, &end, FALSE);
g_free(self->source_content);
self->source_content = text ? text : g_strdup("");
}
static void show_source_buffer(MarkydEditor *self) {
if (!self || !self->buffer) {
return;
}
self->updating_tags = TRUE;
gtk_text_buffer_set_text(self->buffer,
self->source_content ? self->source_content : "", -1);
self->updating_tags = FALSE;
}
static void on_buffer_changed(GtkTextBuffer *buffer, gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
(void)buffer;
if (!self || self->updating_tags || !self->edit_mode) {
return;
}
sync_source_from_buffer(self);
}
static gchar *get_preview_base_uri(MarkydEditor *self) {
const gchar *current_path;
gchar *dir;
GFile *dir_file;
gchar *uri;
gchar *base_uri;
current_path = self && self->app ? markyd_app_get_current_path(self->app) : NULL;
if (current_path && current_path[0] != '\0') {
dir = g_path_get_dirname(current_path);
} else {
dir = g_get_current_dir();
}
dir_file = g_file_new_for_path(dir);
uri = g_file_get_uri(dir_file);
base_uri = g_str_has_suffix(uri, "/") ? g_strdup(uri) : g_strconcat(uri, "/", NULL);
g_free(uri);
g_object_unref(dir_file);
g_free(dir);
return base_uri;
}
static void render_preview(MarkydEditor *self) {
gchar *base_uri;
gchar *html;
if (!self || !self->preview_view) {
return;
}
base_uri = get_preview_base_uri(self);
html = markdown_render_github_html(self->source_content ? self->source_content : "",
base_uri);
g_free(self->preview_base_uri);
self->preview_base_uri = g_strdup(base_uri);
webkit_web_view_load_html(WEBKIT_WEB_VIEW(self->preview_view), html, base_uri);
g_free(html);
g_free(base_uri);
}
static gboolean preview_uri_is_same_document(MarkydEditor *self,
const gchar *uri) {
const gchar *fragment;
if (!uri || uri[0] == '\0') {
return TRUE;
}
if (uri[0] == '#') {
return TRUE;
}
if (g_str_has_prefix(uri, "about:")) {
return TRUE;
}
fragment = strchr(uri, '#');
if (fragment && self && self->preview_base_uri &&
g_str_has_prefix(uri, self->preview_base_uri)) {
return TRUE;
}
return FALSE;
}
static gboolean path_is_markdown_file(const gchar *path) {
gchar *lower;
gboolean result;
if (!path) {
return FALSE;
}
lower = g_ascii_strdown(path, -1);
result = g_str_has_suffix(lower, ".md") ||
g_str_has_suffix(lower, ".markdown");
g_free(lower);
return result;
}
static gboolean open_local_markdown_link(MarkydEditor *self, const gchar *uri) {
gchar *path;
gchar *fragment;
gboolean opened = FALSE;
if (!self || !self->app || !uri || !g_str_has_prefix(uri, "file://")) {
return FALSE;
}
path = g_filename_from_uri(uri, NULL, NULL);
if (!path) {
return FALSE;
}
fragment = strchr(path, '#');
if (fragment) {
*fragment = '\0';
}
if (path_is_markdown_file(path) && g_file_test(path, G_FILE_TEST_EXISTS)) {
opened = markyd_app_open_file(self->app, path);
}
g_free(path);
return opened;
}
static void open_preview_uri_externally(GtkWidget *widget, const gchar *uri) {
GtkWidget *toplevel;
GError *error = NULL;
if (!uri || uri[0] == '\0') {
return;
}
toplevel = gtk_widget_get_toplevel(widget);
if (!gtk_show_uri_on_window(GTK_IS_WINDOW(toplevel) ? GTK_WINDOW(toplevel)
: NULL,
uri, GDK_CURRENT_TIME, &error)) {
if (error) {
g_printerr("Failed to open link '%s': %s\n", uri, error->message);
g_clear_error(&error);
}
}
}
static gboolean on_preview_decide_policy(WebKitWebView *web_view,
WebKitPolicyDecision *decision,
WebKitPolicyDecisionType type,
gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
WebKitNavigationPolicyDecision *navigation_decision;
WebKitNavigationAction *action;
WebKitURIRequest *request;
const gchar *uri;
if (type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
return FALSE;
}
navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION(decision);
action = webkit_navigation_policy_decision_get_navigation_action(
navigation_decision);
if (!action ||
webkit_navigation_action_get_navigation_type(action) !=
WEBKIT_NAVIGATION_TYPE_LINK_CLICKED) {
return FALSE;
}
request = webkit_navigation_action_get_request(action);
uri = request ? webkit_uri_request_get_uri(request) : NULL;
if (preview_uri_is_same_document(self, uri)) {
return FALSE;
}
if (open_local_markdown_link(self, uri)) {
webkit_policy_decision_ignore(decision);
return TRUE;
}
open_preview_uri_externally(GTK_WIDGET(web_view), uri);
webkit_policy_decision_ignore(decision);
return TRUE;
}
void markyd_editor_refresh(MarkydEditor *self) {
if (!self) {
return;
}
if (self->edit_mode) {
return;
}
schedule_markdown_apply(self);
}
void markyd_editor_set_edit_mode(MarkydEditor *self, gboolean edit_mode) {
if (!self || self->edit_mode == edit_mode) {
return;
}
if (edit_mode) {
if (self->markdown_idle_id != 0) {
g_source_remove(self->markdown_idle_id);
self->markdown_idle_id = 0;
}
self->edit_mode = TRUE;
show_source_buffer(self);
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), TRUE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), TRUE);
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "source");
gtk_widget_grab_focus(self->text_view);
} else {
sync_source_from_buffer(self);
self->edit_mode = FALSE;
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "preview");
schedule_markdown_apply(self);
}
}
gboolean markyd_editor_get_edit_mode(MarkydEditor *self) {
return self ? self->edit_mode : FALSE;
}
void markyd_editor_refresh(MarkydEditor *self) { schedule_markdown_apply(self); }
MarkydEditor *markyd_editor_new(MarkydApp *app) {
MarkydEditor *self = g_new0(MarkydEditor, 1);
WebKitSettings *settings;
self->app = app;
self->source_content = g_strdup("");
self->updating_tags = FALSE;
self->markdown_idle_id = 0;
self->edit_mode = FALSE;
self->preview_base_uri = NULL;
self->widget = gtk_stack_new();
gtk_stack_set_transition_type(GTK_STACK(self->widget),
GTK_STACK_TRANSITION_TYPE_NONE);
gtk_widget_set_hexpand(self->widget, TRUE);
gtk_widget_set_vexpand(self->widget, TRUE);
self->text_view = gtk_text_view_new();
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(self->text_view),
@@ -749,28 +392,8 @@ MarkydEditor *markyd_editor_new(MarkydApp *app) {
gtk_text_view_set_editable(GTK_TEXT_VIEW(self->text_view), FALSE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(self->text_view), FALSE);
self->source_scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->source_scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(self->source_scroll), self->text_view);
self->preview_view = webkit_web_view_new();
gtk_widget_set_hexpand(self->preview_view, TRUE);
gtk_widget_set_vexpand(self->preview_view, TRUE);
settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(self->preview_view));
webkit_settings_set_enable_javascript(settings, TRUE);
webkit_settings_set_enable_javascript_markup(settings, FALSE);
webkit_settings_set_auto_load_images(settings, TRUE);
g_signal_connect(self->preview_view, "decide-policy",
G_CALLBACK(on_preview_decide_policy), self);
gtk_stack_add_named(GTK_STACK(self->widget), self->preview_view, "preview");
gtk_stack_add_named(GTK_STACK(self->widget), self->source_scroll, "source");
gtk_stack_set_visible_child_name(GTK_STACK(self->widget), "preview");
self->buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self->text_view));
markdown_init_tags(self->buffer);
g_signal_connect(self->buffer, "changed", G_CALLBACK(on_buffer_changed), self);
gtk_widget_add_events(self->text_view, GDK_POINTER_MOTION_MASK |
GDK_LEAVE_NOTIFY_MASK |
@@ -804,7 +427,6 @@ void markyd_editor_free(MarkydEditor *self) {
self->markdown_idle_id = 0;
}
g_free(self->source_content);
g_free(self->preview_base_uri);
g_free(self);
}
@@ -815,42 +437,24 @@ void markyd_editor_set_content(MarkydEditor *self, const gchar *content) {
g_free(self->source_content);
self->source_content = g_strdup(content ? content : "");
if (self->edit_mode) {
show_source_buffer(self);
} else {
schedule_markdown_apply(self);
}
schedule_markdown_apply(self);
}
gchar *markyd_editor_get_content(MarkydEditor *self) {
if (!self) {
return g_strdup("");
}
if (self->edit_mode) {
sync_source_from_buffer(self);
}
return g_strdup(self->source_content ? self->source_content : "");
}
GtkWidget *markyd_editor_get_widget(MarkydEditor *self) {
return self ? self->widget : NULL;
return self->text_view;
}
void markyd_editor_focus(MarkydEditor *self) {
GtkTextMark *insert_mark;
if (!self) {
return;
}
if (!self->edit_mode) {
if (self->preview_view) {
gtk_widget_grab_focus(self->preview_view);
}
return;
}
if (!self->text_view || !self->buffer) {
if (!self || !self->text_view || !self->buffer) {
return;
}
@@ -862,74 +466,6 @@ void markyd_editor_focus(MarkydEditor *self) {
}
}
void markyd_editor_scroll_top(MarkydEditor *self) {
GtkTextIter start;
if (!self) {
return;
}
if (!self->edit_mode && self->preview_view) {
webkit_web_view_evaluate_javascript(WEBKIT_WEB_VIEW(self->preview_view),
"window.scrollTo(0, 0);", -1, NULL,
NULL, NULL, NULL, NULL);
return;
}
if (!self->buffer || !self->text_view) {
return;
}
gtk_text_buffer_get_start_iter(self->buffer, &start);
gtk_text_buffer_place_cursor(self->buffer, &start);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(self->text_view), &start, 0.0,
FALSE, 0.0, 0.0);
}
void markyd_editor_find(MarkydEditor *self, const gchar *query) {
WebKitFindController *controller;
if (!self || self->edit_mode || !self->preview_view || !query ||
query[0] == '\0') {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
webkit_find_controller_search(controller, query,
WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE |
WEBKIT_FIND_OPTIONS_WRAP_AROUND,
G_MAXUINT);
}
void markyd_editor_find_next(MarkydEditor *self, gboolean previous) {
WebKitFindController *controller;
if (!self || self->edit_mode || !self->preview_view) {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
if (previous) {
webkit_find_controller_search_previous(controller);
} else {
webkit_find_controller_search_next(controller);
}
}
void markyd_editor_find_clear(MarkydEditor *self) {
WebKitFindController *controller;
if (!self || !self->preview_view) {
return;
}
controller = webkit_web_view_get_find_controller(
WEBKIT_WEB_VIEW(self->preview_view));
webkit_find_controller_search_finish(controller);
}
static gboolean on_button_release(GtkWidget *widget, GdkEventButton *event,
gpointer user_data) {
MarkydEditor *self = (MarkydEditor *)user_data;
@@ -997,7 +533,7 @@ static gboolean on_motion_notify(GtkWidget *widget, GdkEventMotion *event,
(gint)event->y, &bx, &by);
gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, bx, by);
set_editor_mouse_cursor(self, get_link_url_at_iter(self->buffer, &iter, &url));
set_link_cursor(self, get_link_url_at_iter(self->buffer, &iter, &url));
g_free(url);
return FALSE;
}
@@ -1007,6 +543,6 @@ static gboolean on_leave_notify(GtkWidget *widget, GdkEventCrossing *event,
MarkydEditor *self = (MarkydEditor *)user_data;
(void)widget;
(void)event;
clear_editor_mouse_cursor(self);
set_link_cursor(self, FALSE);
return FALSE;
}
-18
View File
@@ -6,10 +6,7 @@
typedef struct _MarkydApp MarkydApp;
typedef struct _MarkydEditor {
GtkWidget *widget;
GtkWidget *source_scroll;
GtkWidget *text_view;
GtkWidget *preview_view;
GtkTextBuffer *buffer;
MarkydApp *app;
@@ -21,11 +18,6 @@ typedef struct _MarkydEditor {
/* Coalesce markdown re-rendering to idle to avoid invalidating GTK iterators. */
guint markdown_idle_id;
/* TRUE when the buffer is showing editable markdown source instead of preview. */
gboolean edit_mode;
gchar *preview_base_uri;
} MarkydEditor;
/* Lifecycle */
@@ -36,19 +28,9 @@ void markyd_editor_free(MarkydEditor *editor);
void markyd_editor_set_content(MarkydEditor *editor, const gchar *content);
gchar *markyd_editor_get_content(MarkydEditor *editor);
/* Source editing mode */
void markyd_editor_set_edit_mode(MarkydEditor *editor, gboolean edit_mode);
gboolean markyd_editor_get_edit_mode(MarkydEditor *editor);
/* Widget access */
GtkWidget *markyd_editor_get_widget(MarkydEditor *editor);
void markyd_editor_focus(MarkydEditor *editor);
void markyd_editor_scroll_top(MarkydEditor *editor);
/* Preview search */
void markyd_editor_find(MarkydEditor *editor, const gchar *query);
void markyd_editor_find_next(MarkydEditor *editor, gboolean previous);
void markyd_editor_find_clear(MarkydEditor *editor);
/* Force a refresh of markdown styling/rendering (e.g., after settings change). */
void markyd_editor_refresh(MarkydEditor *editor);
+45 -2264
View File
File diff suppressed because it is too large. Load diff
+17 -22
View File
@@ -4,7 +4,7 @@
#include <gtk/gtk.h>
/* GObject data key used to mark hrule child anchors inserted into the buffer. */
#define TRAYMD_HRULE_ANCHOR_DATA "seemd-hr-anchor"
#define TRAYMD_HRULE_ANCHOR_DATA "viewmd-hr-anchor"
/* Initialize markdown tags on a text buffer */
void markdown_init_tags(GtkTextBuffer *buffer);
@@ -13,44 +13,42 @@ void markdown_init_tags(GtkTextBuffer *buffer);
void markdown_update_accent_tags(GtkTextBuffer *buffer);
/* Data key set on per-link metadata tags to store resolved URL/href. */
#define SEEMD_LINK_URL_DATA "seemd-link-url"
#define VIEWMD_LINK_URL_DATA "viewmd-link-url"
/* Prefix for named text marks used as internal heading anchors. */
#define SEEMD_ANCHOR_MARK_PREFIX "seemd-anchor-"
#define VIEWMD_ANCHOR_MARK_PREFIX "viewmd-anchor-"
/* GObject data key used to mark table child anchors with parsed table data. */
#define SEEMD_TABLE_ANCHOR_DATA "seemd-table-anchor"
#define VIEWMD_TABLE_ANCHOR_DATA "viewmd-table-anchor"
/* GObject data key set on table anchors for hidden searchable index metadata. */
#define SEEMD_TABLE_SEARCH_INDEX_DATA "seemd-table-search-index"
#define VIEWMD_TABLE_SEARCH_INDEX_DATA "viewmd-table-search-index"
/* GObject data key set on table anchors for attached table widget instance. */
#define SEEMD_TABLE_WIDGET_DATA "seemd-table-widget"
#define VIEWMD_TABLE_WIDGET_DATA "viewmd-table-widget"
/* GObject data keys set on each table cell widget. */
#define SEEMD_TABLE_CELL_ROW_DATA "seemd-table-cell-row"
#define SEEMD_TABLE_CELL_COL_DATA "seemd-table-cell-col"
#define VIEWMD_TABLE_CELL_ROW_DATA "viewmd-table-cell-row"
#define VIEWMD_TABLE_CELL_COL_DATA "viewmd-table-cell-col"
/* CSS classes for table search highlight states. */
#define SEEMD_TABLE_CELL_MATCH_CLASS "seemd-table-cell-match"
#define SEEMD_TABLE_CELL_CURRENT_CLASS "seemd-table-cell-current"
#define VIEWMD_TABLE_CELL_MATCH_CLASS "viewmd-table-cell-match"
#define VIEWMD_TABLE_CELL_CURRENT_CLASS "viewmd-table-cell-current"
/* GObject data keys for image anchors and metadata. */
#define SEEMD_IMAGE_ANCHOR_DATA "seemd-image-anchor"
#define SEEMD_IMAGE_SRC_DATA "seemd-image-src"
#define SEEMD_IMAGE_ALT_DATA "seemd-image-alt"
#define SEEMD_IMAGE_WIDGET_DATA "seemd-image-widget"
#define SEEMD_IMAGE_WIDTH_DATA "seemd-image-width"
#define SEEMD_IMAGE_HEIGHT_DATA "seemd-image-height"
#define VIEWMD_IMAGE_ANCHOR_DATA "viewmd-image-anchor"
#define VIEWMD_IMAGE_SRC_DATA "viewmd-image-src"
#define VIEWMD_IMAGE_ALT_DATA "viewmd-image-alt"
#define VIEWMD_IMAGE_WIDGET_DATA "viewmd-image-widget"
typedef struct {
gint row;
gint col;
gint start_offset;
gint end_offset;
} SeemdTableSearchCellRange;
} ViewmdTableSearchCellRange;
typedef struct {
gint start_offset;
gint end_offset;
GArray *cells; /* SeemdTableSearchCellRange */
} SeemdTableSearchIndex;
GArray *cells; /* ViewmdTableSearchCellRange */
} ViewmdTableSearchIndex;
/* Normalize heading/link text into anchor slug form. Caller owns result. */
gchar *markdown_normalize_anchor_slug(const gchar *text);
@@ -61,9 +59,6 @@ gchar *markdown_anchor_mark_name(const gchar *fragment);
/* Render markdown source into the buffer and apply markdown styling. */
void markdown_apply_tags(GtkTextBuffer *buffer, const gchar *source);
/* Render markdown source to a complete GitHub-style HTML document. */
gchar *markdown_render_github_html(const gchar *source, const gchar *base_uri);
/* Build a GTK widget for a table anchor, or NULL if not a table anchor. */
GtkWidget *markdown_create_table_widget(GtkTextChildAnchor *anchor);
+61 -373
View File
@@ -12,16 +12,12 @@ typedef struct {
gint table_col;
} SearchMatch;
#define TAG_SEARCH_MATCH "seemd_search_match"
#define TAG_SEARCH_CURRENT "seemd_search_current"
#define TOOLBAR_LABEL_DATA "seemd-toolbar-label"
#define TAG_SEARCH_MATCH "viewmd_search_match"
#define TAG_SEARCH_CURRENT "viewmd_search_current"
static void on_open_clicked(GtkButton *button, gpointer user_data);
static void on_refresh_clicked(GtkButton *button, gpointer user_data);
static void on_edit_clicked(GtkButton *button, gpointer user_data);
static void on_save_clicked(GtkButton *button, gpointer user_data);
static void on_settings_clicked(GtkButton *button, gpointer user_data);
static void on_about_clicked(GtkButton *button, gpointer user_data);
static void on_search_changed(GtkEditable *editable, gpointer user_data);
static void on_search_prev_clicked(GtkButton *button, gpointer user_data);
static void on_search_next_clicked(GtkButton *button, gpointer user_data);
@@ -66,16 +62,9 @@ static gchar *rgba_to_hex(const GdkRGBA *rgba);
static void init_color_button(GtkColorButton *btn, const gchar *color_str);
static void on_color_set(GtkColorButton *btn, gpointer user_data);
static GtkWidget *create_settings_dialog(MarkydApp *app);
static gboolean prompt_save_as(MarkydWindow *self);
static void update_edit_button_state(MarkydWindow *self);
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode);
static GtkWidget *create_toolbar_button(const gchar *icon_name, const gchar *label,
const gchar *tooltip);
static void set_toolbar_button_label(GtkWidget *button, const gchar *label);
static void restore_editor_focus(MarkydWindow *self);
static gboolean geometry_debug_enabled(void) {
const gchar *v = g_getenv("SEEMD_DEBUG_GEOMETRY");
const gchar *v = g_getenv("VIEWMD_DEBUG_GEOMETRY");
if (!v) {
v = g_getenv("TRAYMD_DEBUG_GEOMETRY");
}
@@ -139,8 +128,6 @@ static void clear_search_matches(MarkydWindow *self) {
return;
}
markyd_editor_find_clear(self->editor);
gtk_text_buffer_get_bounds(self->editor->buffer, &start, &end);
gtk_text_buffer_remove_tag_by_name(self->editor->buffer, TAG_SEARCH_MATCH,
&start, &end);
@@ -174,15 +161,15 @@ static void set_table_cell_highlight(GtkWidget *cell, gboolean match,
style = gtk_widget_get_style_context(cell);
if (match) {
gtk_style_context_add_class(style, SEEMD_TABLE_CELL_MATCH_CLASS);
gtk_style_context_add_class(style, VIEWMD_TABLE_CELL_MATCH_CLASS);
} else {
gtk_style_context_remove_class(style, SEEMD_TABLE_CELL_MATCH_CLASS);
gtk_style_context_remove_class(style, VIEWMD_TABLE_CELL_MATCH_CLASS);
}
if (current) {
gtk_style_context_add_class(style, SEEMD_TABLE_CELL_CURRENT_CLASS);
gtk_style_context_add_class(style, VIEWMD_TABLE_CELL_CURRENT_CLASS);
} else {
gtk_style_context_remove_class(style, SEEMD_TABLE_CELL_CURRENT_CLASS);
gtk_style_context_remove_class(style, VIEWMD_TABLE_CELL_CURRENT_CLASS);
}
}
@@ -247,9 +234,9 @@ static GtkWidget *lookup_table_cell_widget(GtkWidget *table_widget, gint row,
for (GList *g = grid_children; g != NULL; g = g->next) {
GtkWidget *cell = GTK_WIDGET(g->data);
gint cell_row = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(cell), SEEMD_TABLE_CELL_ROW_DATA));
g_object_get_data(G_OBJECT(cell), VIEWMD_TABLE_CELL_ROW_DATA));
gint cell_col = GPOINTER_TO_INT(
g_object_get_data(G_OBJECT(cell), SEEMD_TABLE_CELL_COL_DATA));
g_object_get_data(G_OBJECT(cell), VIEWMD_TABLE_CELL_COL_DATA));
if (cell_row == row && cell_col == col) {
found = cell;
break;
@@ -275,9 +262,9 @@ static void clear_table_search_highlight(MarkydWindow *self, gboolean clear_matc
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor &&
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_ANCHOR_DATA) != NULL) {
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_ANCHOR_DATA) != NULL) {
GtkWidget *table_widget =
g_object_get_data(G_OBJECT(anchor), SEEMD_TABLE_WIDGET_DATA);
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_WIDGET_DATA);
if (table_widget && GTK_IS_CONTAINER(table_widget)) {
GList *wrapper_children =
gtk_container_get_children(GTK_CONTAINER(table_widget));
@@ -291,11 +278,11 @@ static void clear_table_search_highlight(MarkydWindow *self, gboolean clear_matc
GtkWidget *cell = GTK_WIDGET(g->data);
if (clear_match) {
gtk_style_context_remove_class(gtk_widget_get_style_context(cell),
SEEMD_TABLE_CELL_MATCH_CLASS);
VIEWMD_TABLE_CELL_MATCH_CLASS);
}
if (clear_current) {
gtk_style_context_remove_class(gtk_widget_get_style_context(cell),
SEEMD_TABLE_CELL_CURRENT_CLASS);
VIEWMD_TABLE_CELL_CURRENT_CLASS);
}
}
g_list_free(grid_children);
@@ -333,8 +320,8 @@ static gboolean resolve_table_match_location(MarkydWindow *self, gint start_offs
while (!gtk_text_iter_equal(&iter, &end)) {
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
if (anchor) {
SeemdTableSearchIndex *index = g_object_get_data(
G_OBJECT(anchor), SEEMD_TABLE_SEARCH_INDEX_DATA);
ViewmdTableSearchIndex *index = g_object_get_data(
G_OBJECT(anchor), VIEWMD_TABLE_SEARCH_INDEX_DATA);
if (index && start_offset < index->end_offset &&
end_offset > index->start_offset) {
if (out_anchor) {
@@ -342,10 +329,10 @@ static gboolean resolve_table_match_location(MarkydWindow *self, gint start_offs
}
if (index->cells) {
SeemdTableSearchCellRange *overlap_cell = NULL;
ViewmdTableSearchCellRange *overlap_cell = NULL;
for (guint i = 0; i < index->cells->len; i++) {
SeemdTableSearchCellRange *cell =
&g_array_index(index->cells, SeemdTableSearchCellRange, i);
ViewmdTableSearchCellRange *cell =
&g_array_index(index->cells, ViewmdTableSearchCellRange, i);
if (start_offset >= cell->start_offset && start_offset < cell->end_offset) {
if (out_row) {
*out_row = cell->row;
@@ -396,7 +383,7 @@ static void apply_table_search_match_highlight(MarkydWindow *self) {
}
table_widget =
g_object_get_data(G_OBJECT(match->table_anchor), SEEMD_TABLE_WIDGET_DATA);
g_object_get_data(G_OBJECT(match->table_anchor), VIEWMD_TABLE_WIDGET_DATA);
cell = lookup_table_cell_widget(table_widget, match->table_row, match->table_col);
if (cell) {
set_table_cell_highlight(cell, TRUE, FALSE);
@@ -428,7 +415,7 @@ static void jump_to_search_match(MarkydWindow *self, gint index,
match = &g_array_index(self->search_matches, SearchMatch, index);
if (match->table_anchor && match->table_row >= 0 && match->table_col >= 0) {
GtkWidget *table_widget =
g_object_get_data(G_OBJECT(match->table_anchor), SEEMD_TABLE_WIDGET_DATA);
g_object_get_data(G_OBJECT(match->table_anchor), VIEWMD_TABLE_WIDGET_DATA);
GtkWidget *cell =
lookup_table_cell_widget(table_widget, match->table_row, match->table_col);
if (cell) {
@@ -449,7 +436,7 @@ static void jump_to_search_match(MarkydWindow *self, gint index,
gboolean scrolled_to_cell = FALSE;
if (match->table_row >= 0 && match->table_col >= 0) {
GtkWidget *table_widget = g_object_get_data(G_OBJECT(match->table_anchor),
SEEMD_TABLE_WIDGET_DATA);
VIEWMD_TABLE_WIDGET_DATA);
GtkWidget *cell = lookup_table_cell_widget(table_widget, match->table_row,
match->table_col);
if (cell) {
@@ -493,14 +480,6 @@ static void update_search_matches(MarkydWindow *self) {
return;
}
if (!markyd_editor_get_edit_mode(self->editor)) {
markyd_editor_find(self->editor, query);
gtk_widget_set_sensitive(self->btn_search_prev, TRUE);
gtk_widget_set_sensitive(self->btn_search_next, TRUE);
gtk_label_set_text(GTK_LABEL(self->lbl_search_status), "Preview search");
return;
}
ensure_search_tags(self);
gtk_text_buffer_get_start_iter(self->editor->buffer, &iter);
gtk_text_buffer_get_end_iter(self->editor->buffer, &end);
@@ -569,11 +548,6 @@ static void on_search_prev_clicked(GtkButton *button, gpointer user_data) {
(void)button;
if (!self || !self->search_matches || self->search_matches->len == 0) {
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
markyd_editor_find_next(self->editor, TRUE);
}
return;
}
@@ -591,11 +565,6 @@ static void on_search_next_clicked(GtkButton *button, gpointer user_data) {
(void)button;
if (!self || !self->search_matches || self->search_matches->len == 0) {
if (self && self->editor && !markyd_editor_get_edit_mode(self->editor) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
markyd_editor_find_next(self->editor, FALSE);
}
return;
}
@@ -655,121 +624,16 @@ static void on_editor_buffer_changed(GtkTextBuffer *buffer, gpointer user_data)
update_search_matches(self);
}
static GtkWidget *create_toolbar_button_internal(const gchar *icon_name,
const gchar *label,
const gchar *tooltip,
gboolean toggle) {
GtkWidget *button;
GtkWidget *box;
GtkWidget *image;
GtkWidget *label_widget;
button = toggle ? gtk_toggle_button_new() : gtk_button_new();
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON);
label_widget = gtk_label_new(label);
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
gtk_widget_set_valign(box, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(box), label_widget, FALSE, FALSE, 0);
gtk_container_add(GTK_CONTAINER(button), box);
gtk_widget_set_focus_on_click(button, FALSE);
if (tooltip) {
gtk_widget_set_tooltip_text(button, tooltip);
}
g_object_set_data(G_OBJECT(button), TOOLBAR_LABEL_DATA, label_widget);
return button;
}
static GtkWidget *create_toolbar_button(const gchar *icon_name, const gchar *label,
const gchar *tooltip) {
return create_toolbar_button_internal(icon_name, label, tooltip, FALSE);
}
static void set_toolbar_button_label(GtkWidget *button, const gchar *label) {
GtkWidget *label_widget;
if (!button || !label) {
return;
}
label_widget = g_object_get_data(G_OBJECT(button), TOOLBAR_LABEL_DATA);
if (GTK_IS_LABEL(label_widget)) {
gtk_label_set_text(GTK_LABEL(label_widget), label);
}
}
static void restore_editor_focus(MarkydWindow *self) {
if (self && self->editor) {
markyd_editor_focus(self->editor);
}
}
static void on_about_popover_closed(GtkPopover *popover, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
restore_editor_focus(self);
gtk_widget_destroy(GTK_WIDGET(popover));
}
static void on_about_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
GtkWidget *popover;
GtkWidget *box;
GtkWidget *title;
GtkWidget *summary;
GtkWidget *details;
popover = gtk_popover_new(GTK_WIDGET(button));
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
gtk_container_set_border_width(GTK_CONTAINER(popover), 12);
g_signal_connect(popover, "closed", G_CALLBACK(on_about_popover_closed), self);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_size_request(box, 300, -1);
gtk_container_add(GTK_CONTAINER(popover), box);
title = gtk_label_new(NULL);
gtk_label_set_markup(
GTK_LABEL(title),
"<a href=\"https://github.com/NotAlexNoyle/SeeMD\"><b>SeeMD</b></a>");
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
gtk_box_pack_start(GTK_BOX(box), title, FALSE, FALSE, 0);
summary = gtk_label_new("A lightweight GTK markdown viewer and editor.");
gtk_label_set_xalign(GTK_LABEL(summary), 0.0f);
gtk_label_set_line_wrap(GTK_LABEL(summary), TRUE);
gtk_box_pack_start(GTK_BOX(box), summary, FALSE, FALSE, 0);
details = gtk_label_new(NULL);
gtk_label_set_markup(
GTK_LABEL(details),
"This is a fork of "
"<a href=\"https://github.com/rabfulton/ViewMD\">ViewMD</a> by "
"NotAlexNoyle, with additional features, including GitHub-style preview "
"rendering, HTML-in-Markdown support, editing, saving, toolbar labels, "
"and improved image sizing.");
gtk_label_set_xalign(GTK_LABEL(details), 0.0f);
gtk_label_set_line_wrap(GTK_LABEL(details), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(details), 42);
gtk_box_pack_start(GTK_BOX(box), details, FALSE, FALSE, 0);
gtk_widget_show_all(popover);
gtk_popover_popup(GTK_POPOVER(popover));
}
MarkydWindow *markyd_window_new(MarkydApp *app) {
MarkydWindow *self = g_new0(MarkydWindow, 1);
GtkWidget *toolbar_buttons;
GtkWidget *left_buttons;
GtkWidget *main_box;
GtkWidget *search_box;
self->app = app;
self->window = gtk_application_window_new(app->gtk_app);
gtk_window_set_title(GTK_WINDOW(self->window), "SeeMD");
gtk_window_set_title(GTK_WINDOW(self->window), "ViewMD");
gtk_window_set_default_size(GTK_WINDOW(self->window), config->window_width,
config->window_height);
@@ -780,7 +644,7 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
}
if (geometry_debug_enabled()) {
g_printerr("SeeMD geometry init: x=%d y=%d w=%d h=%d maximized=%d\n",
g_printerr("ViewMD geometry init: x=%d y=%d w=%d h=%d maximized=%d\n",
config->window_x, config->window_y, config->window_width,
config->window_height, config->window_maximized);
}
@@ -796,63 +660,40 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
g_signal_connect(self->window, "window-state-event",
G_CALLBACK(on_window_state_event), self);
self->lbl_title = gtk_button_new_from_icon_name("view-reveal-symbolic",
GTK_ICON_SIZE_BUTTON);
gtk_button_set_relief(GTK_BUTTON(self->lbl_title), GTK_RELIEF_NORMAL);
gtk_widget_set_valign(self->lbl_title, GTK_ALIGN_CENTER);
gtk_widget_set_focus_on_click(self->lbl_title, FALSE);
gtk_widget_set_tooltip_text(self->lbl_title, "About SeeMD");
gtk_widget_set_halign(self->lbl_title, GTK_ALIGN_END);
gtk_widget_set_margin_start(self->lbl_title, 12);
gtk_widget_set_margin_end(self->lbl_title, 12);
g_signal_connect(self->lbl_title, "clicked", G_CALLBACK(on_about_clicked),
self);
self->header_bar = gtk_header_bar_new();
gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(self->header_bar), TRUE);
gtk_window_set_titlebar(GTK_WINDOW(self->window), self->header_bar);
self->lbl_title = gtk_label_new("ViewMD");
gtk_widget_set_halign(self->lbl_title, GTK_ALIGN_CENTER);
gtk_header_bar_set_custom_title(GTK_HEADER_BAR(self->header_bar),
self->lbl_title);
self->btn_open =
create_toolbar_button("document-open-symbolic", "Open",
"Open Markdown Document");
gtk_button_new_from_icon_name("document-open-symbolic", GTK_ICON_SIZE_BUTTON);
gtk_widget_set_tooltip_text(self->btn_open, "Open Markdown Document");
g_signal_connect(self->btn_open, "clicked", G_CALLBACK(on_open_clicked), self);
self->btn_refresh =
create_toolbar_button("view-refresh-symbolic", "Reload",
"Reload Current Document");
gtk_button_new_from_icon_name("view-refresh-symbolic", GTK_ICON_SIZE_BUTTON);
gtk_widget_set_tooltip_text(self->btn_refresh, "Reload Current Document");
g_signal_connect(self->btn_refresh, "clicked", G_CALLBACK(on_refresh_clicked),
self);
self->btn_edit =
create_toolbar_button("accessories-text-editor-symbolic", "Edit",
"Edit Markdown Source");
g_signal_connect(self->btn_edit, "clicked", G_CALLBACK(on_edit_clicked), self);
self->btn_save =
create_toolbar_button("document-save-symbolic", "Save",
"Save Markdown Document");
g_signal_connect(self->btn_save, "clicked", G_CALLBACK(on_save_clicked), self);
self->btn_settings =
create_toolbar_button("emblem-system-symbolic", "Settings", "Settings");
gtk_button_new_from_icon_name("emblem-system-symbolic", GTK_ICON_SIZE_BUTTON);
gtk_widget_set_tooltip_text(self->btn_settings, "Settings");
g_signal_connect(self->btn_settings, "clicked", G_CALLBACK(on_settings_clicked),
self);
toolbar_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_halign(toolbar_buttons, GTK_ALIGN_CENTER);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_open, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_refresh, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_edit, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_save, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(toolbar_buttons), self->btn_settings, FALSE, FALSE, 0);
/* Plain in-window toolbar (no GtkHeaderBar/CSD) so the window manager draws
* server-side decorations with full window controls (e.g. KDE/KWin). */
self->header_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_style_context_add_class(gtk_widget_get_style_context(self->header_bar),
"toolbar");
gtk_box_set_center_widget(GTK_BOX(self->header_bar), toolbar_buttons);
gtk_box_pack_end(GTK_BOX(self->header_bar), self->lbl_title, FALSE, FALSE, 0);
left_buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_open, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_refresh, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(left_buttons), self->btn_settings, FALSE, FALSE, 0);
gtk_header_bar_pack_start(GTK_HEADER_BAR(self->header_bar), left_buttons);
main_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(self->window), main_box);
gtk_box_pack_start(GTK_BOX(main_box), self->header_bar, FALSE, FALSE, 0);
self->search_revealer = gtk_revealer_new();
gtk_revealer_set_transition_type(GTK_REVEALER(self->search_revealer),
@@ -896,10 +737,14 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
gtk_widget_set_halign(self->lbl_search_status, GTK_ALIGN_END);
gtk_box_pack_start(GTK_BOX(search_box), self->lbl_search_status, FALSE, FALSE, 0);
self->scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_box_pack_start(GTK_BOX(main_box), self->scroll, TRUE, TRUE, 0);
self->editor = markyd_editor_new(app);
self->scroll = self->editor->source_scroll;
gtk_box_pack_start(GTK_BOX(main_box), markyd_editor_get_widget(self->editor),
TRUE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(self->scroll),
markyd_editor_get_widget(self->editor));
self->search_matches = g_array_new(FALSE, FALSE, sizeof(SearchMatch));
self->search_current_index = -1;
g_signal_connect(self->editor->buffer, "changed",
@@ -910,10 +755,6 @@ MarkydWindow *markyd_window_new(MarkydApp *app) {
gtk_widget_show_all(self->window);
/* Focus the document instead of letting the first toolbar button (Open)
* grab default keyboard focus. */
markyd_editor_focus(self->editor);
return self;
}
@@ -995,28 +836,28 @@ void markyd_window_apply_css(MarkydWindow *self) {
"window {"
" background-color: %s;"
"}"
".seemd-table-cell {"
".viewmd-table-cell {"
" background-color: %s;"
" border-style: solid;"
" border-width: 1px;"
" border-color: %s;"
"}"
".seemd-table-header-cell {"
".viewmd-table-header-cell {"
" background-color: %s;"
"}"
".seemd-table-cell label {"
".viewmd-table-cell label {"
" color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_MATCH_CLASS " {"
".viewmd-table-cell." VIEWMD_TABLE_CELL_MATCH_CLASS " {"
" background-color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_MATCH_CLASS " label {"
".viewmd-table-cell." VIEWMD_TABLE_CELL_MATCH_CLASS " label {"
" color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_CURRENT_CLASS " {"
".viewmd-table-cell." VIEWMD_TABLE_CELL_CURRENT_CLASS " {"
" background-color: %s;"
"}"
".seemd-table-cell." SEEMD_TABLE_CELL_CURRENT_CLASS " label {"
".viewmd-table-cell." VIEWMD_TABLE_CELL_CURRENT_CLASS " label {"
" color: %s;"
"}",
config->font_family, config->font_size, bg, fg, fg, bg, fg, fg, sel_bg, bg,
@@ -1077,94 +918,6 @@ gboolean markyd_window_is_visible(MarkydWindow *self) {
return gtk_widget_get_visible(self->window);
}
static void update_edit_button_state(MarkydWindow *self) {
gboolean editing;
if (!self || !self->editor || !self->btn_edit) {
return;
}
editing = markyd_editor_get_edit_mode(self->editor);
set_toolbar_button_label(self->btn_edit, editing ? "Preview" : "Edit");
gtk_widget_set_tooltip_text(self->btn_edit,
editing ? "Preview Markdown" : "Edit Markdown Source");
}
static void set_edit_mode(MarkydWindow *self, gboolean edit_mode) {
if (!self || !self->editor) {
return;
}
markyd_editor_set_edit_mode(self->editor, edit_mode);
update_edit_button_state(self);
markyd_editor_focus(self->editor);
if (self->search_revealer &&
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer)) &&
self->search_entry &&
gtk_entry_get_text_length(GTK_ENTRY(self->search_entry)) > 0) {
update_search_matches(self);
}
}
static gboolean prompt_save_as(MarkydWindow *self) {
GtkWidget *dialog;
GtkFileFilter *md_filter;
gint response;
gboolean saved = FALSE;
if (!self || !self->window) {
return FALSE;
}
dialog = gtk_file_chooser_dialog_new(
"Save Markdown Document", GTK_WINDOW(self->window),
GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save",
GTK_RESPONSE_ACCEPT, NULL);
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "Markdown files (*.md, *.markdown)");
gtk_file_filter_add_pattern(md_filter, "*.md");
gtk_file_filter_add_pattern(md_filter, "*.markdown");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
md_filter = gtk_file_filter_new();
gtk_file_filter_set_name(md_filter, "All files");
gtk_file_filter_add_pattern(md_filter, "*");
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), md_filter);
{
const gchar *current = markyd_app_get_current_path(self->app);
if (current && current[0] != '\0') {
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog), current);
} else {
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "untitled.md");
}
}
response = gtk_dialog_run(GTK_DIALOG(dialog));
if (response == GTK_RESPONSE_ACCEPT) {
gchar *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
if (path) {
saved = markyd_app_save_file(self->app, path);
if (!saved) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE, "Failed to save document");
gtk_message_dialog_format_secondary_text(
GTK_MESSAGE_DIALOG(error_dialog), "%s", path);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
g_free(path);
}
}
gtk_widget_destroy(dialog);
return saved;
}
static void on_open_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
GtkWidget *dialog;
@@ -1208,7 +961,6 @@ static void on_open_clicked(GtkButton *button, gpointer user_data) {
}
gtk_widget_destroy(dialog);
restore_editor_focus(self);
}
static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
@@ -1220,7 +972,6 @@ static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
current = markyd_app_get_current_path(self->app);
if (!current || current[0] == '\0') {
restore_editor_focus(self);
return;
}
@@ -1236,45 +987,6 @@ static void on_refresh_clicked(GtkButton *button, gpointer user_data) {
gtk_widget_destroy(error_dialog);
}
g_free(path);
restore_editor_focus(self);
}
static void on_edit_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
(void)button;
if (!self || !self->editor) {
return;
}
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
}
static void on_save_clicked(GtkButton *button, gpointer user_data) {
MarkydWindow *self = (MarkydWindow *)user_data;
const gchar *current;
(void)button;
if (!self || !self->app) {
return;
}
current = markyd_app_get_current_path(self->app);
if (!current || current[0] == '\0') {
prompt_save_as(self);
restore_editor_focus(self);
return;
}
if (!markyd_app_save_current_file(self->app)) {
GtkWidget *error_dialog = gtk_message_dialog_new(
GTK_WINDOW(self->window), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Failed to save document");
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(error_dialog),
"%s", current);
gtk_dialog_run(GTK_DIALOG(error_dialog));
gtk_widget_destroy(error_dialog);
}
restore_editor_focus(self);
}
static void on_settings_clicked(GtkButton *button, gpointer user_data) {
@@ -1295,7 +1007,6 @@ static void on_settings_clicked(GtkButton *button, gpointer user_data) {
}
gtk_widget_destroy(dialog);
restore_editor_focus(self);
}
static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
@@ -1312,18 +1023,6 @@ static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
return TRUE;
}
if ((event->state & GDK_CONTROL_MASK) != 0 &&
(event->keyval == GDK_KEY_e || event->keyval == GDK_KEY_E)) {
set_edit_mode(self, !markyd_editor_get_edit_mode(self->editor));
return TRUE;
}
if ((event->state & GDK_CONTROL_MASK) != 0 &&
(event->keyval == GDK_KEY_s || event->keyval == GDK_KEY_S)) {
on_save_clicked(NULL, self);
return TRUE;
}
if (event->keyval == GDK_KEY_Escape &&
self->search_revealer &&
gtk_revealer_get_reveal_child(GTK_REVEALER(self->search_revealer))) {
@@ -1331,23 +1030,12 @@ static gboolean on_key_press_event(GtkWidget *widget, GdkEventKey *event,
return TRUE;
}
if (event->keyval == GDK_KEY_Escape && self->editor &&
markyd_editor_get_edit_mode(self->editor)) {
set_edit_mode(self, FALSE);
return TRUE;
}
if (event->keyval == GDK_KEY_Escape) {
gtk_window_close(GTK_WINDOW(widget));
return TRUE;
}
if (event->keyval == GDK_KEY_Home || event->keyval == GDK_KEY_KP_Home) {
if (self->editor && !markyd_editor_get_edit_mode(self->editor)) {
markyd_editor_scroll_top(self->editor);
return TRUE;
}
if (self->editor && self->editor->buffer && self->editor->text_view) {
GtkTextIter start;
GtkAdjustment *hadj = NULL;
@@ -1417,15 +1105,15 @@ static gboolean on_configure_event(GtkWidget *widget, GdkEventConfigure *event,
if (geometry_debug_enabled()) {
if (have_extents) {
g_printerr(
"SeeMD configure: event=%dx%d gtk=%dx%d saved=%dx%d frame=%dx%d at (%d,%d)\n",
"ViewMD configure: event=%dx%d gtk=%dx%d saved=%dx%d frame=%dx%d at (%d,%d)\n",
event->width, event->height, gtk_w, gtk_h, width, height,
frame_extents.width, frame_extents.height, x, y);
} else {
g_printerr("SeeMD configure: event=%dx%d gtk=%dx%d saved=%dx%d at (%d,%d)\n",
g_printerr("ViewMD configure: event=%dx%d gtk=%dx%d saved=%dx%d at (%d,%d)\n",
event->width, event->height, gtk_w, gtk_h, width, height, x,
y);
}
g_printerr("SeeMD saved: x=%d y=%d w=%d h=%d\n", config->window_x,
g_printerr("ViewMD saved: x=%d y=%d w=%d h=%d\n", config->window_x,
config->window_y, config->window_width, config->window_height);
}
}
@@ -1500,7 +1188,7 @@ static GtkWidget *create_settings_dialog(MarkydApp *app) {
gint row = 0;
dialog = gtk_dialog_new_with_buttons(
"SeeMD Settings", GTK_WINDOW(app->window->window),
"ViewMD Settings", GTK_WINDOW(app->window->window),
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, "_Cancel",
GTK_RESPONSE_CANCEL, "_Apply", GTK_RESPONSE_APPLY, NULL);
-2
View File
@@ -11,8 +11,6 @@ typedef struct _MarkydWindow {
GtkWidget *header_bar;
GtkWidget *btn_open;
GtkWidget *btn_refresh;
GtkWidget *btn_edit;
GtkWidget *btn_save;
GtkWidget *btn_settings;
GtkWidget *search_revealer;
GtkWidget *search_entry;