Improve KDE GUI file and command handling

This commit is contained in:
Levi Neely 2026-07-16 19:42:21 +02:00
parent 483981a9cd
commit 9e0e5cb0f4
6 changed files with 135 additions and 23 deletions

View File

@ -26,6 +26,8 @@ kf5:
install: build
@echo "=== Installing ollie-kde (KF6) ==="
# Remove stale copies from old install path
rm -f /usr/local/bin/ollie-ask /usr/local/bin/ollie-session-here
# Binaries
install -m755 $(BUILD_DIR)/ollie-tray /usr/bin/
install -m755 $(BUILD_DIR)/ollie-gui /usr/bin/
@ -58,8 +60,8 @@ install: build
mkdir -p ~/.local/share/kio/servicemenus
rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop
install -m755 dolphin/ollie-actions.desktop ~/.local/share/kio/servicemenus/
install -m755 dolphin/ollie-ask /usr/local/bin/
install -m755 dolphin/ollie-session-here /usr/local/bin/
install -m755 dolphin/ollie-ask /usr/bin/
install -m755 dolphin/ollie-session-here /usr/bin/
# Finalize
ldconfig
kquitapp6 krunner 2>/dev/null; true
@ -78,7 +80,7 @@ uninstall:
rm -rf /usr/lib64/qt6/qml/org/ollie
kpackagetool6 -g -t Plasma/Applet -r org.ollie.plasmoid 2>/dev/null; true
rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop
rm -f /usr/local/bin/ollie-ask /usr/local/bin/ollie-session-here
rm -f /usr/bin/ollie-ask /usr/bin/ollie-session-here
ldconfig
kquitapp6 krunner 2>/dev/null; true
@echo "=== Uninstall complete ==="
@ -96,8 +98,8 @@ dolphin-install:
mkdir -p ~/.local/share/kio/servicemenus
rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop
install -m755 dolphin/ollie-actions.desktop ~/.local/share/kio/servicemenus/
install -m755 dolphin/ollie-ask /usr/local/bin/
install -m755 dolphin/ollie-session-here /usr/local/bin/
install -m755 dolphin/ollie-ask /usr/bin/
install -m755 dolphin/ollie-session-here /usr/bin/
@echo "Dolphin menus installed. Restart Dolphin or re-open context menu."
plasmoid-install:
@ -156,8 +158,8 @@ kf5-install:
mkdir -p ~/.local/share/kio/servicemenus
rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop
install -m755 dolphin/ollie-actions-kf5.desktop ~/.local/share/kio/servicemenus/ollie-actions.desktop
install -m755 dolphin/ollie-ask /usr/local/bin/
install -m755 dolphin/ollie-session-here /usr/local/bin/
install -m755 dolphin/ollie-ask /usr/bin/
install -m755 dolphin/ollie-session-here /usr/bin/
# Finalize
ldconfig
kquitapp5 krunner 2>/dev/null; true
@ -173,7 +175,7 @@ kf5-uninstall:
rm -rf /usr/lib/x86_64-linux-gnu/qt5/qml/org/ollie
kpackagetool5 -t Plasma/Applet -r org.ollie.plasmoid 2>/dev/null; true
rm -f ~/.local/share/kio/servicemenus/ollie-actions.desktop
rm -f /usr/local/bin/ollie-ask /usr/local/bin/ollie-session-here
rm -f /usr/bin/ollie-ask /usr/bin/ollie-session-here
ldconfig
kquitapp5 krunner 2>/dev/null; true
@echo "=== Uninstall complete (KF5) ==="

View File

@ -1,11 +1,21 @@
#!/bin/sh
# ollie-session-here: start a session in the given directory
# Works from both Dolphin (passes %f) and terminal (uses $PWD).
TARGET="$1"
[ -f "$TARGET" ] && TARGET="$(dirname "$TARGET")"
[ -z "$TARGET" ] && TARGET="$HOME"
[ -z "$TARGET" ] && TARGET="$PWD"
TARGET="$(cd "$TARGET" 2>/dev/null && pwd)" || TARGET="$PWD"
dbus-send --session --dest=org.ollie.SessionManager --type=method_call \
RESULT=$(dbus-send --session --dest=org.ollie.SessionManager --type=method_call --print-reply \
/org/ollie/SessionManager org.ollie.SessionManager.CreateSession \
string:"$TARGET" string:"" string:"" string:"default"
string:"$TARGET" string:"" string:"" string:"default" 2>&1)
notify-send -t 3000 "Ollie" "Session started in $TARGET"
SESSION_ID=$(echo "$RESULT" | grep 'string "' | head -1 | sed 's/.*string "\(.*\)"/\1/')
if [ -t 1 ]; then
# Terminal: print session ID for further use
echo "$SESSION_ID"
else
# Dolphin/non-interactive: desktop notification
notify-send -t 3000 "Ollie" "Session started in $TARGET"
fi

View File

@ -9,6 +9,9 @@
#include <KSyntaxHighlighting/Format>
#include <QGuiApplication>
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
#include <QUrl>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QPalette>
@ -469,7 +472,97 @@ void ChatBlockModel::copyToClipboard(const QString &text) const
QGuiApplication::clipboard()->setText(text);
}
void ChatBlockModel::executeInTerminal(const QString &code, const QString &language) const
QString ChatBlockModel::linkifyFilePaths(const QString &markdown) const
{
// Agent output normally uses absolute paths, optionally followed by :line:column.
// Restrict matching to absolute paths so ordinary prose and relative URLs remain intact.
static const QRegularExpression pathRe(
R"((?<![A-Za-z0-9_:])(`?)(/[^\s`<>\[\]\(\),;!?\:]+)(?::(\d+))?(?::(\d+))?\1)");
// Preserve complete Markdown links; rewriting a path inside their label would
// produce nested links that Qt renders as literal Markdown.
static const QRegularExpression markdownLinkRe(R"(\[[^\]]*\]\([^\)]*\))");
QList<QPair<int, int>> linkRanges;
auto links = markdownLinkRe.globalMatch(markdown);
while (links.hasNext()) {
const QRegularExpressionMatch link = links.next();
linkRanges.append({link.capturedStart(), link.capturedEnd()});
}
QString result;
result.reserve(markdown.size() + 64);
int lastEnd = 0;
auto matches = pathRe.globalMatch(markdown);
while (matches.hasNext()) {
const QRegularExpressionMatch match = matches.next();
const QString path = match.captured(2);
const int start = match.capturedStart();
bool insideMarkdownLink = false;
for (const auto &range : linkRanges) {
if (start >= range.first && start < range.second) {
insideMarkdownLink = true;
break;
}
}
if (insideMarkdownLink)
continue;
result += markdown.mid(lastEnd, start - lastEnd);
QUrl url = QUrl::fromLocalFile(path);
QStringList query;
if (!match.captured(3).isEmpty())
query << QStringLiteral("line=") + match.captured(3);
if (!match.captured(4).isEmpty())
query << QStringLiteral("column=") + match.captured(4);
if (!query.isEmpty())
url.setQuery(query.join(QLatin1Char('&')));
QString label = match.captured(0);
if (!match.captured(1).isEmpty())
label = label.mid(1, label.size() - 2);
result += QStringLiteral("[") + label + QStringLiteral("](")
+ url.toString(QUrl::FullyEncoded) + QStringLiteral(")");
lastEnd = match.capturedEnd();
}
result += markdown.mid(lastEnd);
return result;
}
void ChatBlockModel::openLink(const QString &link) const
{
const QUrl url(link);
if (!url.isLocalFile()) {
QDesktopServices::openUrl(url);
return;
}
const QString path = url.toLocalFile();
int line = 0;
int column = 0;
const QStringList queryItems = url.query().split(QLatin1Char('&'), Qt::SkipEmptyParts);
for (const QString &item : queryItems) {
if (item.startsWith(QLatin1String("line=")))
line = item.mid(5).toInt();
else if (item.startsWith(QLatin1String("column=")))
column = item.mid(7).toInt();
}
const QString kate = QStandardPaths::findExecutable(QStringLiteral("kate"));
if (!kate.isEmpty() && line > 0) {
QStringList args;
args << QStringLiteral("--line") << QString::number(line);
if (column > 0)
args << QStringLiteral("--column") << QString::number(column);
args << path;
if (QProcess::startDetached(kate, args))
return;
}
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
}
bool ChatBlockModel::executeInTerminal(const QString &code, const QString &language,
const QString &workingDirectory) const
{
// Determine interpreter/shebang
QString shebang = QStringLiteral("#!/bin/bash");
@ -507,7 +600,7 @@ void ChatBlockModel::executeInTerminal(const QString &code, const QString &langu
tmp.setFileTemplate(QStringLiteral("/tmp/ollie-exec-XXXXXX") + ext);
tmp.setAutoRemove(false); // keep it so terminal can read it
if (!tmp.open())
return;
return false;
QString script = shebang + QStringLiteral("\n") + code + QStringLiteral("\n");
tmp.write(script.toUtf8());
@ -536,7 +629,9 @@ void ChatBlockModel::executeInTerminal(const QString &code, const QString &langu
args << QStringLiteral("-e") << scriptPath;
}
QProcess::startDetached(exe, args);
return;
const QString cwd = QDir(workingDirectory).exists() ? workingDirectory : QDir::homePath();
if (QProcess::startDetached(exe, args, cwd))
return true;
}
return false;
}

View File

@ -63,7 +63,10 @@ public:
Q_INVOKABLE QString highlightedHtml(const QString &code, const QString &language) const;
Q_INVOKABLE QString prettyJson(const QString &text) const;
Q_INVOKABLE void copyToClipboard(const QString &text) const;
Q_INVOKABLE void executeInTerminal(const QString &code, const QString &language) const;
Q_INVOKABLE QString linkifyFilePaths(const QString &markdown) const;
Q_INVOKABLE void openLink(const QString &link) const;
Q_INVOKABLE bool executeInTerminal(const QString &code, const QString &language,
const QString &workingDirectory = QString()) const;
Q_INVOKABLE bool loadEarlier(); // returns true if more blocks were loaded
signals:

View File

@ -1081,14 +1081,14 @@ ApplicationWindow {
id: proseSubDelegate
Text {
width: parent ? parent.width : implicitWidth
text: subBlock.content || ""
text: chatModel.linkifyFilePaths(subBlock.content || "")
wrapMode: Text.Wrap
textFormat: Text.MarkdownText
color: palette.text
font.pointSize: root.font.pointSize
font.family: root.font.family
lineHeight: 1.3
onLinkActivated: Qt.openUrlExternally(link)
onLinkActivated: chatModel.openLink(link)
}
}
@ -1186,7 +1186,8 @@ ApplicationWindow {
"py", "perl", "ruby", "rb", "lua", "node",
"javascript", "js", "php"].indexOf(lang) >= 0
}
onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language)
onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language,
dbus.activeCwd)
ToolTip.text: "Execute in terminal"
ToolTip.visible: hovered
}

View File

@ -1086,7 +1086,7 @@ ApplicationWindow {
id: proseSubDelegate
Text {
width: parent ? parent.width : implicitWidth
text: subBlock.content || ""
text: chatModel.linkifyFilePaths(subBlock.content || "")
wrapMode: Text.Wrap
textFormat: Text.MarkdownText
color: palette.text
@ -1094,7 +1094,7 @@ ApplicationWindow {
font.family: root.font.family
lineHeight: 1.3
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
chatModel.openLink(link)
}
}
}
@ -1193,7 +1193,8 @@ ApplicationWindow {
"py", "perl", "ruby", "rb", "lua", "node",
"javascript", "js", "php"].indexOf(lang) >= 0
}
onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language)
onClicked: chatModel.executeInTerminal(subBlock.content, subBlock.language,
dbus.activeCwd)
ToolTip.text: "Execute in terminal"
ToolTip.visible: hovered
}