Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

All notable changes to gknowledge. Dates are release dates.

## Unreleased

**Dossiers**
- Projects become **dossiers**. **New Dossier** asks only for a name: the dossier is a draft you can work in right away, and it gets a folder when you save it (**Save…**, Ctrl+S) or close the window. By default it is saved in `~/Documents/Dossiers/<name>` (the localised Documents folder); you can pick another folder. Closing with a draft open offers to save it or keep it as a draft for next time.
- A saved dossier shows only what a person needs: a `<name>.dossier` launcher file (double-click opens it in gknowledge), `sources/`, `knowledge/` and `notes/`. The database, caches and settings move to a hidden `.dossier/` folder, excluded from git by a generated `.gitignore`.
- Projects created with 0.3 or earlier open unchanged.

**App**
- Preferences: detect a local Ollama server and pick one of its models (thanks @AdityaWagh06, #46).

## 0.3.0 — 2026-10-19

**Wiki**
Expand Down
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ DATADIR := $(PREFIX)/share
APPSDIR := $(DATADIR)/applications
ICONDIR := $(DATADIR)/icons/hicolor/scalable/apps
METAINFODIR := $(DATADIR)/metainfo
MIMEDIR := $(DATADIR)/mime/packages

INSTALL := install

Expand Down Expand Up @@ -103,10 +104,14 @@ install:
$(INSTALL) -m 644 data/icons/hicolor/scalable/apps/$(APP_ID).svg $(DESTDIR)$(ICONDIR)/$(APP_ID).svg
$(INSTALL) -d $(DESTDIR)$(METAINFODIR)
$(INSTALL) -m 644 data/$(APP_ID).metainfo.xml $(DESTDIR)$(METAINFODIR)/$(APP_ID).metainfo.xml
# .dossier launcher files open in gknowledge.
$(INSTALL) -d $(DESTDIR)$(MIMEDIR)
$(INSTALL) -m 644 data/$(APP_ID).mime.xml $(DESTDIR)$(MIMEDIR)/$(APP_ID).xml
@# Refresh caches only for a live install; packages leave this to dpkg triggers.
@if [ -z "$(DESTDIR)" ]; then \
update-desktop-database $(APPSDIR) 2>/dev/null || true; \
gtk-update-icon-cache -f -t $(DATADIR)/icons/hicolor 2>/dev/null || true; \
update-mime-database $(DATADIR)/mime 2>/dev/null || true; \
fi
@echo ">> Done. Make sure $(BINDIR) is on your PATH, then run: gknowledge"

Expand All @@ -116,10 +121,12 @@ uninstall:
rm -f $(DESTDIR)$(APPSDIR)/$(APP_ID).desktop
rm -f $(DESTDIR)$(ICONDIR)/$(APP_ID).svg
rm -f $(DESTDIR)$(METAINFODIR)/$(APP_ID).metainfo.xml
rm -f $(DESTDIR)$(MIMEDIR)/$(APP_ID).xml
@# A stale icon cache would still point at the removed icon and leave a blank one.
@if [ -z "$(DESTDIR)" ]; then \
update-desktop-database $(APPSDIR) 2>/dev/null || true; \
gtk-update-icon-cache -f -t $(DATADIR)/icons/hicolor 2>/dev/null || true; \
update-mime-database $(DATADIR)/mime 2>/dev/null || true; \
fi
@echo ">> Uninstalled gknowledge from $(PREFIX)"

Expand Down
6 changes: 4 additions & 2 deletions build-aux/flatpak/io.github.openapi.Gknowledge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ finish-args:
- --socket=wayland
- --socket=fallback-x11
- --device=dri
# No broad filesystem access: projects are chosen through Gtk.FileDialog,
# which goes through the document portal inside the sandbox.
# No broad filesystem access: new dossiers are saved by default in
# ~/Documents/Dossiers (localised Documents folder); any other folder is
# chosen through Gtk.FileDialog and the document portal.
- --filesystem=xdg-documents/Dossiers:create

modules:
# PDF import; pure Python, installed from the wheel without network access.
Expand Down
2 changes: 1 addition & 1 deletion data/io.github.openapi.Gknowledge.desktop
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Terminal=false
Categories=GNOME;GTK;Office;
Keywords=knowledge;wiki;notes;llm;research;rag;
StartupNotify=true
MimeType=text/markdown;text/plain;application/pdf;text/html;
MimeType=application/x-dossier;text/markdown;text/plain;application/pdf;text/html;
StartupWMClass=io.github.openapi.Gknowledge
SingleMainWindow=true
X-Flatpak-RenamedFrom=io.gknowledge.Gknowledge.desktop;
12 changes: 12 additions & 0 deletions data/io.github.openapi.Gknowledge.mime.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The launcher file at the top of every dossier: double-click opens it. -->
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-dossier">
<comment>Dossier</comment>
<comment xml:lang="it">Dossier</comment>
<sub-class-of type="application/json"/>
<generic-icon name="x-office-document"/>
<icon name="io.github.openapi.Gknowledge"/>
<glob pattern="*.dossier"/>
</mime-type>
</mime-info>
77 changes: 44 additions & 33 deletions gknowledge/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from gi.repository import Adw, Gdk, Gio, Gtk # noqa: E402

from . import APP_ID, APP_NAME, __version__, config
from .core.project import Project
from .core.project import Project, default_home
from .ui.window import MainWindow


Expand All @@ -23,8 +23,10 @@ def __init__(self):
self._pending_open: str | None = None
self._pending_import: list[str] = []

self._add_action("new-project", self._on_new_project, "<primary>n")
self._add_action("open-project", self._on_open_project, "<primary>o")
self._add_action("new-dossier", self._on_new_dossier, "<primary>n")
self._add_action("open-dossier", self._on_open_dossier, "<primary>o")
self._add_action("save-dossier", self._on_save_dossier, "<primary>s")
self.lookup_action("save-dossier").set_enabled(False)
self._add_action("preferences", self._on_preferences, "<primary>comma")
self._add_action("about", self._on_about)
self._add_action("quit", self._on_quit, "<primary>q")
Expand All @@ -46,8 +48,8 @@ def do_activate(self):
self.window = MainWindow(self)
self.window.present()
if self._pending_open:
self._open_path(self._pending_open)
self._pending_open = None
path, self._pending_open = self._pending_open, None
self.window.confirm_leave(lambda: self._open_path(path))
else:
self._maybe_open_last()
# Files handed over by the file manager ("Open With") are imported
Expand All @@ -57,11 +59,11 @@ def do_activate(self):
if self.window.project:
self.window._do_import(paths)
else:
self.window.toast("Open or create a project first, then import the files.")
self.window.toast("Open or create a dossier first, then import the files.")

def do_open(self, files, n_files, hint):
# `gknowledge /path/to/project` opens a project; other files are
# imported as sources into the current (or last used) project.
# `gknowledge /path/to/dossier` (the folder or its .dossier launcher)
# opens it; other files are imported as sources into the current one.
for gfile in files[:n_files]:
path = gfile.get_path()
if not path:
Expand Down Expand Up @@ -91,47 +93,54 @@ def _maybe_open_last(self):
def _open_path(self, path: str):
try:
project = Project.open(path)
path = str(project.root)
except Exception as exc: # noqa: BLE001
self.window.toast(f"Could not open project: {exc}")
self.window.toast(f"Could not open the dossier: {exc}")
return
config.add_recent_project(str(Path(path).resolve()))
self.window.set_project(project)

def _on_new_project(self, *_a):
dialog = Gtk.FileDialog(title="Choose a folder for the new project")

def done(dlg, res):
try:
folder = dlg.select_folder_finish(res)
except Exception:
return
path = folder.get_path()
def _on_new_dossier(self, *_a):
def create(name: str):
try:
project = Project.create(path, name=Path(path).name)
project = Project.create_draft(name)
except Exception as exc: # noqa: BLE001
self.window.toast(f"Could not create project: {exc}")
self.window.toast(f"Could not create the dossier: {exc}")
return
config.add_recent_project(str(Path(path).resolve()))
config.add_recent_project(str(project.root.resolve()))
self.window.set_project(project)
self.window.toast(f"Created project “{project.name}”")

dialog.select_folder(self.window, None, done)

def _on_open_project(self, *_a):
dialog = Gtk.FileDialog(title="Open a gknowledge project folder")
self.window.confirm_leave(lambda: self.window.ask_dossier_name(create))

def _on_open_dossier(self, *_a):
dialog = Gtk.FileDialog(title="Open a Dossier")
filters = Gio.ListStore.new(Gtk.FileFilter)
dossiers = Gtk.FileFilter(name="Dossiers")
dossiers.add_pattern("*.dossier")
dossiers.add_pattern("gknowledge.json") # created before 0.4
filters.append(dossiers)
dialog.set_filters(filters)
dialog.set_default_filter(dossiers)
home = default_home()
if home.is_dir():
dialog.set_initial_folder(Gio.File.new_for_path(str(home)))

def done(dlg, res):
try:
folder = dlg.select_folder_finish(res)
gfile = dlg.open_finish(res)
except Exception:
return
path = folder.get_path()
path = gfile.get_path()
if not Project.is_project(path):
self.window.toast("That folder is not a gknowledge project.")
self.window.toast("That file does not open a dossier.")
return
self._open_path(path)

dialog.select_folder(self.window, None, done)
self.window.confirm_leave(lambda: dialog.open(self.window, None, done))

def _on_save_dossier(self, *_a):
if self.window:
self.window.save_draft()

def _on_preferences(self, *_a):
if self.window:
Expand All @@ -154,9 +163,11 @@ def _on_about(self, *_a):
about.present()

def _on_quit(self, *_a):
if self.window and self.window.project:
self.window.project.close()
self.quit()
# Closing the window asks about an unsaved draft, then ends the app.
if self.window:
self.window.close()
else:
self.quit()


def main(argv=None) -> int:
Expand Down
8 changes: 8 additions & 0 deletions gknowledge/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,11 @@ def add_recent_project(path: str) -> None:
recent.insert(0, path)
state["recent"] = recent[:10]
save_state(state)


def replace_recent_project(old: str, new: str) -> None:
"""A draft was saved elsewhere: its recent entry follows it."""
state = load_state()
state["recent"] = [p for p in state.get("recent", []) if p not in (old, new)]
save_state(state)
add_recent_project(new)
2 changes: 1 addition & 1 deletion gknowledge/core/extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def analyze(self, text: str) -> dict:

# -- step 2 --------------------------------------------------------------
def _mentions_path(self):
return self.project.root / "cache" / "mentions.json"
return self.project.cache_dir / "mentions.json"

def _load_mentions(self) -> dict:
try:
Expand Down
Loading
Loading