mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.config
|
||||
import qs.modules.functions
|
||||
import qs.modules.components
|
||||
import qs.services
|
||||
|
||||
FloatingWindow {
|
||||
id: appWin
|
||||
color: Appearance.m3colors.m3background
|
||||
property bool initialChatSelected: false
|
||||
property bool chatsInitialized: false
|
||||
|
||||
function appendMessage(sender, message) {
|
||||
messageModel.append({
|
||||
"sender": sender,
|
||||
"message": message
|
||||
});
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
function updateChatsList(files) {
|
||||
let existing = {
|
||||
};
|
||||
for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true
|
||||
for (let file of files) {
|
||||
let name = file.trim();
|
||||
if (!name.length)
|
||||
continue;
|
||||
|
||||
if (name.endsWith(".txt"))
|
||||
name = name.slice(0, -4);
|
||||
|
||||
if (!existing[name])
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
|
||||
delete existing[name];
|
||||
}
|
||||
// remove chats that no longer exist
|
||||
for (let name in existing) {
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === name) {
|
||||
chatListModel.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ensure default exists
|
||||
let hasDefault = false;
|
||||
for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") {
|
||||
hasDefault = true;
|
||||
}
|
||||
if (!hasDefault) {
|
||||
chatListModel.insert(0, {
|
||||
"name": "default"
|
||||
});
|
||||
FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt");
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
// Always scroll to end after appending
|
||||
chatView.forceLayout();
|
||||
chatView.positionViewAtEnd();
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (userInput.text === "" || Zenith.loading)
|
||||
return ;
|
||||
|
||||
Zenith.pendingInput = userInput.text;
|
||||
appendMessage("You", userInput.text);
|
||||
userInput.text = "";
|
||||
Zenith.loading = true;
|
||||
Zenith.send();
|
||||
}
|
||||
|
||||
function loadChatHistory(chatName) {
|
||||
messageModel.clear();
|
||||
Zenith.loadChat(chatName);
|
||||
}
|
||||
|
||||
function selectDefaultChat() {
|
||||
let defaultIndex = -1;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === "default") {
|
||||
defaultIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (defaultIndex !== -1) {
|
||||
chatSelector.currentIndex = defaultIndex;
|
||||
Zenith.currentChat = "default";
|
||||
loadChatHistory("default");
|
||||
} else if (chatListModel.count > 0) {
|
||||
chatSelector.currentIndex = 0;
|
||||
Zenith.currentChat = chatListModel.get(0).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
}
|
||||
|
||||
visible: Globals.states.intelligenceWindowOpen
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
return ;
|
||||
|
||||
chatsInitialized = false;
|
||||
messageModel.clear();
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openWindow() {
|
||||
Globals.states.intelligenceWindowOpen = true;
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
Globals.states.intelligenceWindowOpen = false;
|
||||
}
|
||||
|
||||
target: "intelligence"
|
||||
}
|
||||
|
||||
ListModel {
|
||||
// { sender: "You" | "AI", message: string }
|
||||
|
||||
id: messageModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: chatListModel
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Intelligence is disabled!"
|
||||
Layout.leftMargin: Metrics.margin(24)
|
||||
font.pixelSize: Metrics.fontSize("huge")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: !Config.runtime.misc.intelligence.enabled
|
||||
text: "Go to the settings to enable intelligence"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: Config.runtime.misc.intelligence.enabled
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: chatSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: chatListModel
|
||||
textRole: "name"
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex < 0)
|
||||
return ;
|
||||
|
||||
let name = chatListModel.get(currentIndex).name;
|
||||
if (name === Zenith.currentChat)
|
||||
return ;
|
||||
|
||||
Zenith.currentChat = name;
|
||||
loadChatHistory(name);
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "add"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
let name = "new-chat-" + chatListModel.count;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.createFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.append({
|
||||
"name": name
|
||||
});
|
||||
chatSelector.currentIndex = chatListModel.count - 1;
|
||||
Zenith.currentChat = name;
|
||||
messageModel.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "edit"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0
|
||||
onClicked: renameDialog.open()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "delete"
|
||||
Layout.preferredWidth: 40
|
||||
enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default"
|
||||
onClicked: {
|
||||
let name = chatSelector.currentText;
|
||||
let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt";
|
||||
FileUtils.removeFile(path, function(success) {
|
||||
if (success) {
|
||||
chatListModel.remove(chatSelector.currentIndex);
|
||||
selectDefaultChat();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledDropDown {
|
||||
id: modelSelector
|
||||
|
||||
Layout.fillWidth: true
|
||||
model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"]
|
||||
currentIndex: 0
|
||||
Layout.preferredHeight: 40
|
||||
onCurrentTextChanged: Zenith.currentModel = currentText
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
icon: "close_fullscreen"
|
||||
Layout.preferredWidth: 40
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "closeWindow"]);
|
||||
Globals.visiblility.sidebarLeft = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainerLow
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
id: chatView
|
||||
|
||||
model: messageModel
|
||||
spacing: Metrics.spacing(8)
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(12)
|
||||
clip: true
|
||||
|
||||
delegate: Item {
|
||||
property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic
|
||||
|
||||
width: chatView.width
|
||||
height: bubble.implicitHeight + 6
|
||||
Component.onCompleted: {
|
||||
chatView.forceLayout();
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Metrics.spacing(8)
|
||||
|
||||
Item {
|
||||
width: sender === "AI" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: bubble
|
||||
|
||||
radius: Metrics.radius("normal")
|
||||
color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh
|
||||
implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8)
|
||||
implicitHeight: textItem.implicitHeight
|
||||
anchors.right: sender === "You" ? parent.right : undefined
|
||||
anchors.left: sender === "AI" ? parent.left : undefined
|
||||
anchors.topMargin: Metrics.margin(2)
|
||||
|
||||
TextEdit {
|
||||
id: textItem
|
||||
|
||||
text: StringUtils.markdownToHtml(message)
|
||||
wrapMode: TextEdit.Wrap
|
||||
textFormat: TextEdit.RichText
|
||||
readOnly: true // make it selectable but not editable
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
color: Appearance.syntaxHighlightingTheme
|
||||
padding: Metrics.padding(8)
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ma
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent);
|
||||
p.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
width: sender === "You" ? 0 : parent.width * 0.2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
height: 50
|
||||
radius: Metrics.radius("normal")
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(6)
|
||||
spacing: Metrics.spacing(10)
|
||||
|
||||
StyledTextField {
|
||||
// Shift+Enter → insert newline
|
||||
// Enter → send message
|
||||
|
||||
id: userInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Type your message..."
|
||||
font.pixelSize: Metrics.iconSize(14)
|
||||
padding: Metrics.spacing(8)
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (event.modifiers & Qt.ShiftModifier)
|
||||
insert("\n");
|
||||
else
|
||||
sendMessage();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Send"
|
||||
enabled: userInput.text.trim().length > 0 && !Zenith.loading
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onClicked: sendMessage()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: renameDialog
|
||||
|
||||
title: "Rename Chat"
|
||||
modal: true
|
||||
visible: false
|
||||
standardButtons: Dialog.NoButton
|
||||
x: (appWin.width - 360) / 2 // center horizontally
|
||||
y: (appWin.height - 160) / 2 // center vertically
|
||||
width: 360
|
||||
height: 200
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Metrics.margin(16)
|
||||
spacing: Metrics.spacing(12)
|
||||
|
||||
StyledText {
|
||||
text: "Enter a new name for the chat"
|
||||
font.pixelSize: Metrics.fontSize(18)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: renameInput
|
||||
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "New name"
|
||||
filled: false
|
||||
highlight: false
|
||||
text: chatSelector.currentText
|
||||
font.pixelSize: Metrics.fontSize(16)
|
||||
Layout.preferredHeight: 45
|
||||
padding: Metrics.padding(8)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Metrics.spacing(12)
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
StyledButton {
|
||||
text: "Cancel"
|
||||
Layout.preferredWidth: 80
|
||||
onClicked: renameDialog.close()
|
||||
}
|
||||
|
||||
StyledButton {
|
||||
text: "Rename"
|
||||
Layout.preferredWidth: 100
|
||||
enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText
|
||||
onClicked: {
|
||||
let oldName = chatSelector.currentText;
|
||||
let newName = renameInput.text.trim();
|
||||
let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt";
|
||||
let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt";
|
||||
FileUtils.renameFile(oldPath, newPath, function(success) {
|
||||
if (success) {
|
||||
chatListModel.set(chatSelector.currentIndex, {
|
||||
"name": newName
|
||||
});
|
||||
Zenith.currentChat = newName;
|
||||
renameDialog.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
header: StyledRect {
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Metrics.radius("normal")
|
||||
border.color: Appearance.colors.colOutline
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Thinking…"
|
||||
visible: Zenith.loading
|
||||
color: Appearance.colors.colSubtext
|
||||
font.pixelSize: Metrics.fontSize(14)
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
leftMargin: Metrics.margin(22)
|
||||
bottomMargin: Metrics.margin(76)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Connections {
|
||||
// only auto-select once
|
||||
function onChatsListed(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
let previousChat = Zenith.currentChat;
|
||||
updateChatsList(lines);
|
||||
// select & load once
|
||||
if (!chatsInitialized) {
|
||||
chatsInitialized = true;
|
||||
let index = -1;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === previousChat) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1 && chatListModel.count > 0)
|
||||
index = 0;
|
||||
|
||||
if (index !== -1) {
|
||||
chatSelector.currentIndex = index;
|
||||
Zenith.currentChat = chatListModel.get(index).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
return ;
|
||||
}
|
||||
// AFTER init: only react if current chat vanished
|
||||
let stillExists = false;
|
||||
for (let i = 0; i < chatListModel.count; i++) {
|
||||
if (chatListModel.get(i).name === Zenith.currentChat) {
|
||||
stillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stillExists && chatListModel.count > 0) {
|
||||
chatSelector.currentIndex = 0;
|
||||
Zenith.currentChat = chatListModel.get(0).name;
|
||||
loadChatHistory(Zenith.currentChat);
|
||||
}
|
||||
}
|
||||
|
||||
function onAiReply(text) {
|
||||
appendMessage("AI", text.slice(5));
|
||||
Zenith.loading = false;
|
||||
}
|
||||
|
||||
function onChatLoaded(text) {
|
||||
let lines = text.split(/\r?\n/);
|
||||
let batch = [];
|
||||
for (let l of lines) {
|
||||
let line = l.trim();
|
||||
if (!line.length)
|
||||
continue;
|
||||
|
||||
let u = line.match(/^\[\d{4}-.*\] User: (.*)$/);
|
||||
let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/);
|
||||
if (u)
|
||||
batch.push({
|
||||
"sender": "You",
|
||||
"message": u[1]
|
||||
});
|
||||
else if (a)
|
||||
batch.push({
|
||||
"sender": "AI",
|
||||
"message": a[1]
|
||||
});
|
||||
else if (batch.length)
|
||||
batch[batch.length - 1].message += "\n" + line;
|
||||
}
|
||||
messageModel.clear();
|
||||
for (let m of batch) messageModel.append(m)
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
target: Zenith
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user