Add basic GUI support (#17)

This commit was merged in pull request #17.
This commit is contained in:
2026-02-08 21:25:03 +00:00
parent 2340fd7652
commit f6fdd19f73
45 changed files with 1217 additions and 684 deletions

View File

@@ -1,3 +1,6 @@
[build]
rustflags = [ "-C", "link-arg=-fuse-ld=lld", ]
[env]
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"

33
.qmllint.ini Normal file
View File

@@ -0,0 +1,33 @@
[General]
DisableDefaultImports=false
[Warnings]
AccessSingletonViaObject=warning
AttachedPropertyReuse=disable
BadSignalHandlerParameters=warning
CompilerWarnings=disable
Deprecated=warning
DuplicatePropertyBinding=warning
DuplicatedName=warning
ImportFailure=warning
IncompatibleType=warning
InheritanceCycle=warning
InvalidLintDirective=warning
LintPluginWarnings=disable
MissingProperty=warning
MissingType=warning
MultilineStrings=info
NonListProperty=warning
PrefixedImportType=warning
PropertyAliasCycles=warning
ReadOnlyProperty=warning
RequiredProperty=warning
RestrictedType=warning
TopLevelComponent=warning
UncreatableType=warning
UnqualifiedAccess=warning
UnresolvedType=warning
UnusedImports=info
UseProperFunction=warning
VarUsedBeforeDeclaration=warning
WithStatement=warning

10
Cargo.lock generated
View File

@@ -295,6 +295,7 @@ dependencies = [
"cxx-qt",
"cxx-qt-build",
"cxx-qt-lib",
"devicons",
"dirs",
"edtui",
"log",
@@ -667,6 +668,15 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "devicons"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830e47e2f330cf4fdd5a958dcef921b9523ffc21ab6713aa5e77ba2cce03904b"
dependencies = [
"lazy_static",
]
[[package]]
name = "digest"
version = "0.10.7"

View File

@@ -18,6 +18,7 @@ tui-logger = "0.18.1"
edtui = "0.11.1"
strum = "0.27.2"
uuid = { version = "1.19.0", features = ["v4"] }
devicons = "0.6.12"
[build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6.

View File

@@ -19,6 +19,31 @@ And of course, [Rust](https://www.rust-lang.org/tools/install).
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
This project requires at least Qt 6.7.3 To check your Qt version
```bash
qmake6 -query QT_VERSION
```
Use the [Qt Installer](https://www.qt.io/development/download) to download and install the Qt version of your choice.
If the installer is run with `sudo`, the default install location is `/opt/Qt`, otherwise Qt will be installed into your home directory.
**You must set the QMAKE variable before building clide**. This should be a path to `qmake6` binary installed on your system.
The following export is the default installation path for Qt 6.7 on Ubuntu 24.04
```bash
export QMAKE=$HOME/Qt/6.7.3/gcc_64/bin/qmake6
export LD_LIBRARY_PATH=$HOME/Qt/6.7.3/gcc_64/lib
```
Though environment variables set using `export` will take precedence, these can also be set in [.cargo/config.toml](./.cargo/config.toml) for conveinence
```toml
[env]
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"
```
## Usage
To install and run clide
@@ -82,6 +107,8 @@ cargo run
clide
```
![image](./resources/gui.png)
## Development
It's recommended to use RustRover or Qt Creator for development.
@@ -126,6 +153,7 @@ Some helpful links for reading up on QML if you're just getting started.
* [All QML Controls Types](https://doc.qt.io/qt-6/qtquick-controls-qmlmodule.html)
* [KDAB CXX-Qt Book](https://kdab.github.io/cxx-qt/book/)
* [github.com/KDAB/cxx-qt](https://github.com/KDAB/cxx-qt)
* [QML and C++ Intergration](https://doc.qt.io/qt-6/qtqml-cppintegration-overview.html)
### Plugins

View File

@@ -2,12 +2,21 @@ use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() {
CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[
"qml/main.qml",
"qml/ClideAboutWindow.qml",
"qml/ClideApplicationView.qml",
"qml/ClideEditorView.qml",
"qml/ClideExplorerView.qml",
"qml/ClideTreeView.qml",
"qml/ClideProjectView.qml",
"qml/ClideEditor.qml",
"qml/ClideMenuBar.qml",
"qml/Components/ClideAboutWindow.qml",
"qml/Components/ClideBreadCrumbs.qml",
"qml/Components/ClideEditor.qml",
"qml/Components/ClideHandle.qml",
"qml/Components/ClideLogger.qml",
"qml/Components/ClideMenu.qml",
"qml/Components/ClideMenuBar.qml",
"qml/Components/ClideMenuItem.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Logger/Logger.qml",
"qml/main.qml",
]))
// Link Qt's Network library
// - Qt Core is always linked
@@ -18,6 +27,7 @@ fn main() {
.qt_module("Gui")
.qt_module("Svg")
.qt_module("Xml")
.qrc("./resources.qrc")
.files(["src/gui/colors.rs", "src/gui/filesystem.rs"])
.build();
}

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
SplitView {
id: root
// Path to the directory of the project opened in clide.
required property string projectDir
anchors.fill: parent
// Customized handle to drag between the Navigation and the Editor.
handle: ClideHandle {
hovered: SplitHandle.hovered
pressed: SplitHandle.pressed
}
ClideExplorerView {
SplitView.fillHeight: true
SplitView.preferredWidth: 200
projectDir: root.projectDir
// Open files when clicked in the explorer.
onFileClicked: path => {
Logger.trace("Setting editor path from ClideExplorerView signal: " + path)
clideEditorView.filePath = path;
}
}
ClideEditorView {
id: clideEditorView
SplitView.fillHeight: true
SplitView.fillWidth: true
// Provide a path to the file currently open in the text editor.
// Initialized using the Default trait in Rust QML singleton FileSystem.
filePath: FileSystem.filePath
}
}

View File

@@ -1,206 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
SplitView {
id: root
Layout.fillHeight: true
Layout.fillWidth: true
orientation: Qt.Vertical
// The path to the file to show in the text editor.
// This is updated by a signal caught within ClideProjectView.
// Initialized by the Default trait for the Rust QML singleton FileSystem.
required property string filePath
// Customized handle to drag between the Editor and the Console.
handle: Rectangle {
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
implicitHeight: 8
radius: 2.5
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 400
}
}
}
RowLayout {
// We use a flickable to synchronize the position of the editor and
// the line numbers. This is necessary because the line numbers can
// extend the available height.
Flickable {
id: lineNumbers
Layout.fillHeight: true
Layout.fillWidth: false
// Calculating the width correctly is important as the number grows.
// We need to ensure space required to show N line number digits.
// We use log10 to find how many digits are needed in a line number.
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
// The +10 is additional spacing and can be adjusted.
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
contentY: editorFlickable.contentY
interactive: false
Column {
anchors.fill: parent
topPadding: textArea.topPadding
Repeater {
id: repeatedLineNumbers
// TODO: Bug where text wrapping shows as new line number.
model: textArea.lineCount
// This Item is used for each line number in the gutter.
delegate: Item {
// Calculates the height of each line in the text area.
height: textArea.contentHeight / textArea.lineCount
width: parent.width
required property int index
// Show the line number.
Label {
id: numbers
color: RustColors.linenumber
font: textArea.font
height: parent.height
horizontalAlignment: Text.AlignLeft
text: parent.index + 1
verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width
}
// Draw edge along the right side of the line number.
Rectangle {
id: indicator
anchors.left: numbers.right
color: RustColors.linenumber
height: parent.height
width: 1
}
}
}
}
}
Flickable {
id: editorFlickable
Layout.fillHeight: true
Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds
height: 650
ScrollBar.horizontal: MyScrollBar {
}
ScrollBar.vertical: MyScrollBar {
}
TextArea.flickable: TextArea {
id: textArea
focus: true
persistentSelection: true
antialiasing: true
selectByMouse: true
selectionColor: RustColors.editor_highlight
selectedTextColor: RustColors.editor_highlighted_text
textFormat: Qt.AutoText
wrapMode: TextArea.Wrap
text: FileSystem.readFile(root.filePath)
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
// TODO: Handle saving
// Component.onCompleted: {
// if (Qt.application.arguments.length === 2)
// textDocument.source = "file:" + Qt.application.arguments[1]
// else
// textDocument.source = "qrc:/texteditor.html"
// }
// textDocument.onStatusChanged: {
// // a message lookup table using computed properties:
// // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer
// const statusMessages = {
// [ TextDocument.ReadError ]: qsTr("Failed to load “%1”"),
// [ TextDocument.WriteError ]: qsTr("Failed to save “%1”"),
// [ TextDocument.NonLocalFileError ]: qsTr("Not a local file: “%1”"),
// }
// const err = statusMessages[textDocument.status]
// if (err) {
// errorDialog.text = err.arg(textDocument.source)
// errorDialog.open()
// }
// }
}
FontMetrics {
id: fontMetrics
font: textArea.font
}
}
}
TextArea {
id: areaConsole
height: 100
placeholderText: qsTr("Placeholder for bash terminal.")
placeholderTextColor: "white"
readOnly: true
wrapMode: TextArea.Wrap
background: Rectangle {
color: RustColors.editor_background
implicitHeight: 100
// border.color: control.enabled ? RustColors.active : RustColors.inactive
}
}
// We use an inline component to customize the horizontal and vertical
// scroll-bars. This is convenient when the component is only used in one file.
component MyScrollBar: ScrollBar {
id: scrollBar
// Scroll bar gutter
background: Rectangle {
implicitHeight: scrollBar.interactive ? 8 : 4
implicitWidth: scrollBar.interactive ? 8 : 4
color: RustColors.scrollbar_gutter
// Fade the scrollbar gutter when inactive.
opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.2
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}
// Scroll bar
contentItem: Rectangle {
implicitHeight: scrollBar.interactive ? 8 : 4
implicitWidth: scrollBar.interactive ? 8 : 4
// If we don't need a scrollbar, fallback to the gutter color.
// If the scrollbar is required change it's color based on activity.
color: scrollBar.size < 1.0 ? scrollBar.active ? RustColors.scrollbar_active : RustColors.scrollbar : RustColors.scrollbar_gutter
// Smooth transition between color changes based on the state above.
Behavior on color {
ColorAnimation {
duration: 1000
}
}
// Fade the scrollbar when inactive.
opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.35
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}
}
}

49
qml/ClideEditorView.qml Normal file
View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
id: root
// The path to the file to show in the text editor.
// This is updated by a signal caught within ClideApplicationView.
required property string filePath
clip: true
color: "transparent"
radius: 20
SplitView {
anchors.fill: parent
orientation: Qt.Vertical
spacing: 3
// Customized handle to drag between the Editor and the Console.
handle: ClideHandle {
hovered: SplitHandle.hovered
pressed: SplitHandle.pressed
}
Component.onCompleted: {
// Show logging is working.
Logger.info("Info logs");
Logger.warn("Warning logs");
Logger.debug("Debug logs");
Logger.error("Error logs");
Logger.trace("Trace logs");
}
ClideEditor {
SplitView.preferredHeight: 650
}
ClideLogger {
}
}
}

62
qml/ClideExplorerView.qml Normal file
View File

@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
id: root
required property string projectDir
signal fileClicked(string path)
clip: true
color: RustColors.explorer_background
topLeftRadius: 10
ColumnLayout {
anchors.fill: parent
spacing: 5
ClideBreadCrumbs {
id: breadCrumb
Layout.fillWidth: true
Layout.leftMargin: 15
Layout.rightMargin: 15
Layout.topMargin: 10
path: clideTreeView.rootDirectory
onCrumbClicked: path => {
Logger.trace("Crumb clicked: " + path);
clideTreeView.rootDirectory = path;
}
onResetRoot: {
clideTreeView.rootDirectory = clideTreeView.originalRootDirectory;
}
}
ClideTreeView {
id: clideTreeView
Layout.fillHeight: true
Layout.fillWidth: true
// Path to the directory opened in the file explorer.
originalRootDirectory: root.projectDir
rootDirectory: root.projectDir
// Pass the signal to the parent component using another signal.
onFileClicked: path => root.fileClicked(path)
onRootDirectoryChanged: {
Logger.log("Setting root directory: " + clideTreeView.rootDirectory);
breadCrumb.path = clideTreeView.rootDirectory;
}
}
}
}

View File

@@ -1,186 +0,0 @@
import QtQuick
import QtQuick.Controls
import clide.module 1.0
MenuBar {
// Base settings for each Menu.
component ClideMenu : Menu {
background: Rectangle {
color: RustColors.menubar
implicitWidth: 100
radius: 2
}
}
// Base settings for each MenuItem.
component ClideMenuItem : MenuItem {
id: root
background: Rectangle {
color: root.hovered ? RustColors.hovered : RustColors.unhovered
radius: 2.5
}
contentItem: IconLabel {
color: "black"
font.family: "Helvetica"
text: root.text
}
}
// Background for this MenuBar.
background: Rectangle {
color: RustColors.menubar
border.color: RustColors.menubar_border
}
//
// File Menu
Action {
id: actionNewProject
text: qsTr("&New Project...")
}
Action {
id: actionOpen
text: qsTr("&Open...")
}
Action {
id: actionSave
text: qsTr("&Save")
}
Action {
id: actionExit
text: qsTr("&Exit")
onTriggered: Qt.quit()
}
ClideMenu {
title: qsTr("&File")
ClideMenuItem {
action: actionNewProject
}
ClideMenuItem {
action: actionOpen
onTriggered: FileSystem.setDirectory(FileSystem.filePath)
}
ClideMenuItem {
action: actionSave
}
MenuSeparator {
background: Rectangle {
border.color: color
color: RustColors.menubar_border
implicitHeight: 3
implicitWidth: 200
}
}
ClideMenuItem {
action: actionExit
}
}
//
// Edit Menu
Action {
id: actionUndo
text: qsTr("&Undo")
}
Action {
id: actionRedo
text: qsTr("&Redo")
}
Action {
id: actionCut
text: qsTr("&Cut")
}
Action {
id: actionCopy
text: qsTr("&Copy")
}
Action {
id: actionPaste
text: qsTr("&Paste")
}
ClideMenu {
title: qsTr("&Edit")
ClideMenuItem {
action: actionUndo
}
ClideMenuItem {
action: actionRedo
}
ClideMenuItem {
action: actionCut
}
ClideMenuItem {
action: actionCopy
}
ClideMenuItem {
action: actionPaste
}
}
//
// View Menu
Action {
id: actionToolWindows
text: qsTr("&Tool Windows")
}
Action {
id: actionAppearance
text: qsTr("&Appearance")
}
ClideMenu {
title: qsTr("&View")
ClideMenuItem {
action: actionToolWindows
}
ClideMenuItem {
action: actionAppearance
}
}
//
// Help Menu
ClideAboutWindow {
id: clideAbout
}
Action {
id: actionDocumentation
text: qsTr("&Documentation")
}
Action {
id: actionAbout
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
text: qsTr("&About")
}
ClideMenu {
title: qsTr("&Help")
ClideMenuItem {
action: actionDocumentation
}
ClideMenuItem {
action: actionAbout
}
}
}

View File

@@ -1,60 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
SplitView {
id: root
// Path to the directory of the project opened in clide.
required property string projectDir
Layout.fillHeight: true
Layout.fillWidth: true
anchors.fill: parent
// Customized handle to drag between the Navigation and the Editor.
handle: Rectangle {
id: verticalSplitHandle
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
implicitWidth: 8
radius: 2.5
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 400
}
}
}
Rectangle {
id: navigationView
color: RustColors.explorer_background
SplitView.fillHeight: true
SplitView.minimumWidth: 0
SplitView.preferredWidth: 200
SplitView.maximumWidth: 250
StackLayout {
anchors.fill: parent
ClideTreeView {
id: clideTreeView
onFileClicked: path => root.projectDir = path
// Path to the directory opened in the file explorer.
rootDirectory: root.projectDir
}
}
}
ClideEditor {
SplitView.fillWidth: true
// Provide a path to the file currently open in the text editor.
// Initialized using the Default trait in Rust QML singleton FileSystem.
filePath: FileSystem.filePath
}
}

View File

@@ -1,153 +1,166 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Effects
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
TreeView {
id: root
color: RustColors.explorer_background
required property string rootDirectory
property int lastIndex: -1
required property string originalRootDirectory
property string rootDirectory
property int rootIndent: 25
signal fileClicked(string filePath)
TreeView {
id: fileSystemTreeView
anchors.margins: 15
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.StopAtBounds
clip: true
property int lastIndex: -1
// The model is implemented in filesystem.rs
model: FileSystem
// Set the root directory on the Rust model, returning the QModeIndex to use for the root of the tree view widget.
rootIndex: FileSystem.setDirectory(root.rootDirectory)
model: FileSystem
anchors.fill: parent
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.StopAtBounds
clip: true
// Provide our own custom ScrollIndicator for the TreeView.
ScrollBar.horizontal: ClideScrollBar {
sizeModifier: 3
}
ScrollBar.vertical: ClideScrollBar {
sizeModifier: 3
}
Component.onCompleted: {
FileSystem.setDirectory(root.rootDirectory)
fileSystemTreeView.expandRecursively(0, -1)
// The delegate represents a single entry in the filesystem.
delegate: TreeViewDelegate {
id: treeDelegate
required property string fileName
required property url filePath
required property int index
implicitHeight: 25
implicitWidth: root.width
indentation: 12
background: Rectangle {
color: current ? RustColors.explorer_folder_open : "transparent"
radius: 20
width: root.width
}
// Item name.
contentItem: Text {
anchors.left: itemIcon.right
anchors.leftMargin: 5
color: RustColors.explorer_text
text: treeDelegate.fileName
}
// Item Icon.
indicator: Label {
id: itemIcon
anchors.verticalCenter: parent.verticalCenter
antialiasing: true
enabled: false
focus: false
font.family: localFont.font.family
font.pixelSize: 18
smooth: true
// Get the icon from Rust implementation.
text: root.model.icon(filePath)
x: root.rootIndent + (treeDelegate.depth * treeDelegate.indentation) + (carrotIndicator.visible ? carrotIndicator.width : 0)
}
// The delegate represents a single entry in the filesystem.
delegate: TreeViewDelegate {
id: treeDelegate
indentation: 8
implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
implicitHeight: 25
// Directory carrot indicator.
Label {
id: carrotIndicator
required property int index
required property url filePath
required property string fileName
anchors.verticalCenter: parent.verticalCenter
font.family: localFont.font.family
font.pixelSize: 10
font.weight: localFont.font.weight
text: expanded ? "⮟" : "⮞"
visible: isTreeNode && hasChildren
x: (root.rootIndent - implicitWidth) + (depth * indentation)
}
// Apply colorization effects to the icon for the item.
MultiEffect {
anchors.fill: itemIcon
brightness: 1.0
colorization: 1.0
colorizationColor: {
const isFile = !treeDelegate.hasChildren;
if (isFile)
return Qt.lighter(RustColors.explorer_folder, 2);
const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren;
if (isExpandedFolder)
return Qt.darker(RustColors.explorer_folder, 2);
else
return RustColors.explorer_folder;
}
source: itemIcon
}
HoverHandler {
id: hoverHandler
indicator: Image {
id: directoryIcon
acceptedDevices: PointerDevice.Mouse
}
TapHandler {
acceptedButtons: Qt.LeftButton | Qt.RightButton
function setSourceImage() {
let folderOpen = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z\"/></svg>";
let folderClosed = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z\"/></svg>";
let file = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z\"/></svg>";
// If the item has children, it's a directory.
onSingleTapped: (eventPoint, button) => {
switch (button) {
case Qt.LeftButton:
if (treeDelegate.hasChildren) {
return treeDelegate.expanded ?
folderOpen : folderClosed;
root.toggleExpanded(treeDelegate.row);
} else {
return file
}
}
x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation)
anchors.verticalCenter: parent.verticalCenter
source: setSourceImage()
sourceSize.width: 15
sourceSize.height: 15
fillMode: Image.PreserveAspectFit
smooth: true
antialiasing: true
asynchronous: true
}
contentItem: Text {
text: treeDelegate.fileName
color: RustColors.explorer_text
}
background: Rectangle {
// TODO: Fix flickering from color transition on states here.
color: (treeDelegate.index === fileSystemTreeView.lastIndex)
? RustColors.explorer_text_selected
: (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent")
radius: 2.5
opacity: hoverHandler.hovered ? 0.75 : 1.0
Behavior on color {
ColorAnimation {
duration: 300
}
}
}
HoverHandler {
id: hoverHandler
}
TapHandler {
acceptedButtons: Qt.LeftButton | Qt.RightButton
onSingleTapped: (eventPoint, button) => {
switch (button) {
case Qt.LeftButton:
fileSystemTreeView.toggleExpanded(treeDelegate.row)
fileSystemTreeView.lastIndex = treeDelegate.index
// If this model item doesn't have children, it means it's
// representing a file.
if (!treeDelegate.hasChildren)
root.fileClicked(treeDelegate.filePath)
break;
case Qt.RightButton:
if (treeDelegate.hasChildren)
contextMenu.popup();
break;
}
}
}
Menu {
id: contextMenu
Action {
text: qsTr("Set as root index")
onTriggered: {
console.log("Setting directory: " + treeDelegate.filePath)
FileSystem.setDirectory(treeDelegate.filePath)
}
}
Action {
text: qsTr("Reset root index")
onTriggered: {
FileSystem.setDirectory("")
// If this model item doesn't have children, it means it's representing a file.
root.fileClicked(treeDelegate.filePath);
}
break;
case Qt.RightButton:
contextMenu.popup();
break;
}
}
}
ClideMenu {
id: contextMenu
// Provide our own custom ScrollIndicator for the TreeView.
ScrollIndicator.vertical: ScrollIndicator {
active: true
implicitWidth: 15
ClideMenuItem {
action: Action {
enabled: treeDelegate.hasChildren
text: qsTr("Set root")
contentItem: Rectangle {
implicitWidth: 6
implicitHeight: 6
onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath);
root.rootDirectory = treeDelegate.filePath;
}
}
}
ClideMenuItem {
action: Action {
text: qsTr("Reset root")
color: RustColors.scrollbar
opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0
Behavior on opacity {
OpacityAnimator {
duration: 500
onTriggered: {
Logger.log("Resetting root directory: " + root.originalRootDirectory);
root.rootDirectory = root.originalRootDirectory;
}
}
}
}
}
selectionModel: ItemSelectionModel {
}
FontLoader {
id: localFont
source: "qrc:/fonts/saucecodepro-xlight.ttf"
}
}

View File

@@ -1,21 +1,27 @@
// TODO: Header
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls.Basic
import clide.module 1.0
import Logger 1.0
ApplicationWindow {
id: root
width: 450
height: 350
// Create the window with no frame and keep it on top.
flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
color: RustColors.gutter
// Create the window with no frame and keep it on top.
flags: Qt.Tool | Qt.FramelessWindowHint
height: 350
width: 450
visible: root.active
// Hide the window when it loses focus.
onActiveChanged: {
if (!active) {
Logger.debug("Setting active: " + root.active)
if (!root.active) {
root.visible = false;
}
}
@@ -25,48 +31,38 @@ ApplicationWindow {
id: logo
anchors.left: parent.left
anchors.margins: 20
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 20
source: "../icons/kilroy-256.png"
sourceSize.width: 80
sourceSize.height: 80
fillMode: Image.PreserveAspectFit
smooth: true
antialiasing: true
asynchronous: true
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/images/kilroy.png"
sourceSize.height: 80
sourceSize.width: 80
}
ScrollView {
anchors.top: logo.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: 20
anchors.right: parent.right
anchors.top: logo.bottom
TextArea {
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
text: qsTr("<h3>About CLIDE</h3>"
+ "<p>A simple text editor written in Rust and QML using CXX-Qt.</p>"
+ "<p>Personal website <a href=\"http://shaunreed.com\">shaunreed.com</a></p>"
+ "<p>Project notes <a href=\"http://knoats.com\">knoats.com</a></p>"
+ "<p>This project is developed at <a href=\"http://git.shaunreed.com/shaunrd0/clide\">git.shaunreed.com</a></p>"
+ "<p><a href=\"https://github.com/KDAB/cxx-qt\">KDAB CXX-Qt repository</a></p>"
+ "<p>Copyright (C) 2026 Shaun Reed, all rights reserved.</p>")
color: RustColors.editor_text
wrapMode: Text.WordWrap
readOnly: true
antialiasing: true
background: null
color: RustColors.editor_text
horizontalAlignment: Text.AlignHCenter
readOnly: true
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
text: qsTr("<h3>About CLIDE</h3>" + "<p>A simple text editor written in Rust and QML using CXX-Qt.</p>" + "<p>Personal website <a href=\"http://shaunreed.com\">shaunreed.com</a></p>" + "<p>Project notes <a href=\"http://knoats.com\">knoats.com</a></p>" + "<p>This project is developed at <a href=\"http://git.shaunreed.com/shaunrd0/clide\">git.shaunreed.com</a></p>" + "<p><a href=\"https://github.com/KDAB/cxx-qt\">KDAB CXX-Qt repository</a></p>" + "<p>Copyright (C) 2026 Shaun Reed, all rights reserved.</p>")
textFormat: Text.RichText
wrapMode: Text.WordWrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link)
Qt.openUrlExternally(link);
}
}
}

View File

@@ -0,0 +1,107 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import clide.module 1.0
import Logger 1.0
Rectangle {
id: root
property var fullPaths: []
required property string path
property var segments: []
signal crumbClicked(string path)
signal resetRoot
function rebuildSegments(): string {
let cleaned = path;
if (cleaned.endsWith("/"))
cleaned = cleaned.slice(0, -1);
Logger.trace("Building segments for path: " + cleaned);
segments = ["/"];
fullPaths = ["/"];
let parts = cleaned.split("/");
let current = "";
// We already pushed the root `/` path during initialization, so skip index 0.
for (let i = 1; i < parts.length; ++i) {
current += "/" + parts[i];
Logger.trace("Pushing path: " + parts[i] + " Current: " + current);
segments.push(parts[i]);
fullPaths.push(current);
}
// Update the model used in the Repeater to show the new segments.
repeater.model = segments;
}
color: "transparent"
implicitHeight: breadcrumbRow.implicitHeight
width: parent.width
Component.onCompleted: rebuildSegments()
onPathChanged: rebuildSegments()
Flow {
id: breadcrumbRow
anchors.fill: parent
spacing: 2
width: parent.width
Repeater {
id: repeater
model: root.segments
delegate: Text {
required property int index
required property string modelData
function getText(): string {
if (modelData === "/") {
return modelData;
}
return modelData + "/";
}
// Show blue underlined hyperlink text if the mouse is hovering a segment.
color: mouseArea.containsMouse ? "#2a7fff" : RustColors.explorer_text
font.underline: mouseArea.containsMouse
text: getText()
// Click events for each path segment call signal so the parent can set the file explorer root path.
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Logger.info(index + "] Breadcrumb clicked:" + root.fullPaths[index]);
crumbClicked(root.fullPaths[index]);
}
}
}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onSingleTapped: contextMenu.popup()
}
ClideMenu {
id: contextMenu
ClideMenuItem {
action: Action {
text: qsTr("Reset root")
onTriggered: {
Logger.info("Resetting root directory from ClideBreadCrumbs");
resetRoot();
}
}
}
}
}

View File

@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
color: RustColors.editor_background
RowLayout {
anchors.fill: parent
// We use a flickable to synchronize the position of the editor and
// the line numbers. This is necessary because the line numbers can
// extend the available height.
Flickable {
id: lineNumbers
Layout.fillHeight: true
Layout.fillWidth: false
// Calculating the width correctly is important as the number grows.
// We need to ensure space required to show N line number digits.
// We use log10 to find how many digits are needed in a line number.
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
// The +10 is additional spacing and can be adjusted.
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
contentY: editorFlickable.contentY
interactive: false
Column {
anchors.fill: parent
topPadding: textArea.topPadding
Repeater {
id: repeatedLineNumbers
// TODO: Bug where text wrapping shows as new line number.
model: textArea.lineCount
// This Item is used for each line number in the gutter.
delegate: Item {
required property int index
// Calculates the height of each line in the text area.
height: textArea.contentHeight / textArea.lineCount
width: parent.width
// Show the line number.
Label {
id: numbers
color: RustColors.linenumber
font: textArea.font
height: parent.height
horizontalAlignment: Text.AlignLeft
text: parent.index + 1
verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width
}
// Draw an edge along the right side of the line number.
Rectangle {
id: indicator
anchors.left: numbers.right
color: RustColors.linenumber
height: parent.height
width: 1
}
}
}
}
}
Flickable {
id: editorFlickable
Layout.fillHeight: true
Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds
height: 650
ScrollBar.horizontal: ClideScrollBar {
}
ScrollBar.vertical: ClideScrollBar {
}
TextArea.flickable: TextArea {
id: textArea
antialiasing: true
focus: true
persistentSelection: true
selectByMouse: true
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
text: FileSystem.readFile(root.filePath)
textFormat: Qt.AutoText
wrapMode: TextArea.Wrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
}
FontMetrics {
id: fontMetrics
font: textArea.font
}
}
}
}

View File

@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import clide.module 1.0
Rectangle {
id: root
readonly property color currentColor: {
if (pressed) {
return RustColors.pressed;
} else if (hovered) {
return RustColors.hovered;
} else {
return "transparent";
}
}
required property bool hovered
required property bool pressed
border.color: currentColor
color: currentColor
implicitHeight: 8
implicitWidth: 8
radius: 2.5
opacity: root.hovered ? 1.0 : 0.0
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 500
}
}
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}

View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import clide.module 1.0
import Logger 1.0
Rectangle {
color: RustColors.terminal_background
radius: 10
ListModel {
id: model
}
ListView {
id: listView
function getLogColor(level) {
switch (level) {
case "INFO":
return RustColors.info_log;
case "DEBUG":
return RustColors.debug_log;
case "WARN":
return RustColors.warn_log;
case "ERROR":
return RustColors.error_log;
case "TRACE":
return RustColors.trace_log;
default:
return RustColors.info_log;
}
}
anchors.fill: parent
model: model
delegate: Text {
color: listView.getLogColor(level)
font.family: "monospace"
text: `[${level}] ${message}`
}
onCountChanged: Qt.callLater(positionViewAtEnd)
}
Connections {
function onLogged(level, message) {
model.append({
level,
message
});
}
target: Logger
}
}

View File

@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Controls.Basic
import clide.module 1.0
Menu {
background: Rectangle {
border.color: RustColors.hovered
border.width: 10
color: RustColors.menubar
implicitWidth: 100
radius: 5
}
}

View File

@@ -0,0 +1,153 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import clide.module 1.0
MenuBar {
// Background for this MenuBar.
background: Rectangle {
color: RustColors.menubar
}
//
// File Menu
ClideMenu {
title: qsTr("&File")
ClideMenuItem {
action: Action {
id: actionNewProject
text: qsTr("&New Project...")
}
}
ClideMenuItem {
action: Action {
id: actionOpen
text: qsTr("&Open...")
}
onTriggered: FileSystem.setDirectory(FileSystem.filePath)
}
ClideMenuItem {
action: Action {
id: actionSave
text: qsTr("&Save")
}
}
MenuSeparator {
background: Rectangle {
border.color: color
color: Qt.darker(RustColors.menubar, 1)
implicitHeight: 3
implicitWidth: 200
}
}
ClideMenuItem {
action: Action {
id: actionExit
text: qsTr("&Exit")
onTriggered: Qt.quit()
}
}
}
//
// Edit Menu
ClideMenu {
title: qsTr("&Edit")
ClideMenuItem {
action: Action {
id: actionUndo
text: qsTr("&Undo")
}
}
ClideMenuItem {
action: Action {
id: actionRedo
text: qsTr("&Redo")
}
}
ClideMenuItem {
action: Action {
id: actionCut
text: qsTr("&Cut")
}
}
ClideMenuItem {
action: Action {
id: actionCopy
text: qsTr("&Copy")
}
}
ClideMenuItem {
action: Action {
id: actionPaste
text: qsTr("&Paste")
}
}
}
//
// View Menu
ClideMenu {
title: qsTr("&View")
ClideMenuItem {
action: Action {
id: actionAppearance
text: qsTr("&Appearance")
}
}
ClideMenuItem {
action: Action {
id: actionToolWindows
text: qsTr("&Tool Windows")
}
}
}
//
// Help Menu
ClideAboutWindow {
id: clideAbout
}
ClideMenu {
title: qsTr("&Help")
ClideMenuItem {
action: Action {
id: actionDocumentation
text: qsTr("&Documentation")
}
}
ClideMenuItem {
action: Action {
id: actionAbout
text: qsTr("&About")
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
}
}
}
}

View File

@@ -0,0 +1,18 @@
import QtQuick
import QtQuick.Controls.Basic
import clide.module 1.0
MenuItem {
id: root
background: Rectangle {
color: root.hovered ? RustColors.hovered : RustColors.unhovered
radius: 1.0
}
contentItem: IconLabel {
color: "black"
font.family: "Helvetica"
text: root.text
}
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import clide.module 1.0
ScrollBar {
id: scrollBar
// Height, opacitiy, width
property int h: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
property int sizeModifier: 4
property int w: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
// Scroll bar gutter
background: Rectangle {
id: gutter
color: RustColors.scrollbar_gutter
implicitHeight: scrollBar.h
implicitWidth: scrollBar.w
// Fade the scrollbar gutter when inactive.
opacity: scrollBar.o
radius: 20
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}
// Scroll bar
contentItem: Rectangle {
readonly property color barColor: {
if (scrollBar.size < 1.0) {
// If the scrollbar is required change it's color based on activity.
if (scrollBar.active) {
return RustColors.scrollbar_active;
} else {
return RustColors.scrollbar;
}
} else {
// If we don't need a scrollbar, fallback to the gutter color.
return gutter.color;
}
}
color: barColor
implicitHeight: scrollBar.h
implicitWidth: scrollBar.w
// Fade the scrollbar when inactive.
opacity: scrollBar.o
radius: 20
// Smooth transition between color changes based on the state above.
Behavior on color {
ColorAnimation {
duration: 1000
}
}
Behavior on opacity {
OpacityAnimator {
duration: 1000
}
}
}
}

9
qml/Components/qmldir Normal file
View File

@@ -0,0 +1,9 @@
ClideScrollBar ClideScrollBar.qml
ClideHandle ClideHandle.qml
ClideMenu ClideMenu.qml
ClideMenuItem ClideMenuItem.qml
ClideBreadCrumbs ClideBreadCrumbs.qml
ClideAboutWindow ClideAboutWindow.qml
ClideEditor ClideEditor.qml
ClideLogger ClideLogger.qml
ClideMenuBar ClideMenuBar.qml

35
qml/Logger/Logger.qml Normal file
View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pragma Singleton
import QtQuick
QtObject {
signal logged(string level, string message)
function debug(msg) {
console.log(msg);
logged("DEBUG", msg);
}
function error(msg) {
console.error(msg);
logged("ERROR", msg);
}
function info(msg) {
console.log(msg);
logged("INFO", msg);
}
function log(msg) {
console.log(msg);
logged("INFO", msg);
}
function trace(msg) {
console.log(msg);
logged("TRACE", msg);
}
function warn(msg) {
console.warn(msg);
logged("WARN", msg);
}
}

1
qml/Logger/qmldir Normal file
View File

@@ -0,0 +1 @@
singleton Logger 1.0 Logger.qml

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -8,28 +12,32 @@ import clide.module 1.0
ApplicationWindow {
id: appWindow
required property string appContextPath
height: 800
title: "CLIDE"
title: "Clide"
visible: true
width: 1200
required property string appContextPath
menuBar: ClideMenuBar {
}
Rectangle {
anchors.fill: parent
color: RustColors.gutter
}
color: RustColors.menubar
width: appView.implicitWidth
height: appView.implicitHeight
MessageDialog {
id: errorDialog
ClideApplicationView {
id: appView
projectDir: appWindow.appContextPath
implicitHeight: appWindow.height
implicitWidth: appWindow.width
title: qsTr("Error")
}
ClideProjectView {
projectDir: appWindow.appContextPath
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 20
anchors.topMargin: 10
}
}
}

10
resources.qrc Normal file
View File

@@ -0,0 +1,10 @@
<RCC>
<qresource prefix="/images">
<file alias="kilroy.png">resources/images/kilroy-256.png</file>
</qresource>
<qresource prefix="/fonts">
<file alias="saucecodepro.ttf">resources/SauceCodeProNerdFont-Black.ttf</file>
<file alias="saucecodepro-light.ttf">resources/SauceCodeProNerdFont-Light.ttf</file>
<file alias="saucecodepro-xlight.ttf">resources/SauceCodeProNerdFont-ExtraLight.ttf</file>
</qresource>
</RCC>

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resources/gui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::AppContext;
use anyhow::Result;
use cxx_qt_lib::{QMapPair, QMapPair_QString_QVariant, QString, QVariant};

View File

@@ -1,4 +1,9 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
#[cxx_qt::bridge]
pub mod qobject {
unsafe extern "C++" {
include!("cxx-qt-lib/qcolor.h");
@@ -31,6 +36,12 @@ pub mod qobject {
#[qproperty(QColor, explorer_background)]
#[qproperty(QColor, explorer_folder)]
#[qproperty(QColor, explorer_folder_open)]
#[qproperty(QColor, terminal_background)]
#[qproperty(QColor, info_log)]
#[qproperty(QColor, debug_log)]
#[qproperty(QColor, warn_log)]
#[qproperty(QColor, error_log)]
#[qproperty(QColor, trace_log)]
type RustColors = super::RustColorsImpl;
}
}
@@ -60,6 +71,12 @@ pub struct RustColorsImpl {
explorer_background: QColor,
explorer_folder: QColor,
explorer_folder_open: QColor,
terminal_background: QColor,
info_log: QColor,
debug_log: QColor,
warn_log: QColor,
error_log: QColor,
trace_log: QColor,
}
impl Default for RustColorsImpl {
@@ -68,7 +85,7 @@ impl Default for RustColorsImpl {
hovered: QColor::try_from("#303234").unwrap(),
unhovered: QColor::try_from("#3c3f41").unwrap(),
pressed: QColor::try_from("#4b4f51").unwrap(),
menubar: QColor::try_from("#3c3f41").unwrap(),
menubar: QColor::try_from("#262626").unwrap(),
menubar_border: QColor::try_from("#575757").unwrap(),
scrollbar: QColor::try_from("#4b4f51").unwrap(),
scrollbar_active: QColor::try_from("#4b4f51").unwrap(),
@@ -76,17 +93,23 @@ impl Default for RustColorsImpl {
linenumber: QColor::try_from("#94989b").unwrap(),
active: QColor::try_from("#a9acb0").unwrap(),
inactive: QColor::try_from("#FFF").unwrap(),
editor_background: QColor::try_from("#2b2b2b").unwrap(),
editor_background: QColor::try_from("#1E1F22").unwrap(),
editor_text: QColor::try_from("#acaea3").unwrap(),
editor_highlighted_text: QColor::try_from("#ccced3").unwrap(),
editor_highlight: QColor::try_from("#ccced3").unwrap(),
gutter: QColor::try_from("#1e1f22").unwrap(),
explorer_hovered: QColor::try_from("#4c5053").unwrap(),
explorer_text: QColor::try_from("#3b3b3b").unwrap(),
explorer_text_selected: QColor::try_from("#8b8b8b").unwrap(),
explorer_background: QColor::try_from("#676c70").unwrap(),
explorer_text: QColor::try_from("#FFF").unwrap(),
explorer_text_selected: QColor::try_from("#262626").unwrap(),
explorer_background: QColor::try_from("#1E1F22").unwrap(),
explorer_folder: QColor::try_from("#54585b").unwrap(),
explorer_folder_open: QColor::try_from("#FFF").unwrap(),
explorer_folder_open: QColor::try_from("#393B40").unwrap(),
terminal_background: QColor::try_from("#111111").unwrap(),
info_log: QColor::try_from("#C4FFFF").unwrap(),
debug_log: QColor::try_from("#9148AF").unwrap(),
warn_log: QColor::try_from("#C4A958").unwrap(),
error_log: QColor::try_from("#ff5555").unwrap(),
trace_log: QColor::try_from("#ffaa00").unwrap(),
}
}
}

View File

@@ -1,3 +1,19 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use cxx_qt_lib::{QModelIndex, QString};
use devicons::FileIcon;
use dirs;
use log::warn;
use std::fs;
use std::path::Path;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::html::{IncludeBackground, append_highlighted_html_for_styled_line};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
#[cxx_qt::bridge]
pub mod qobject {
unsafe extern "C++" {
@@ -17,7 +33,6 @@ pub mod qobject {
#[qml_element]
#[qml_singleton]
#[qproperty(QString, file_path, cxx_name = "filePath")]
#[qproperty(QModelIndex, root_index, cxx_name = "rootIndex")]
type FileSystem = super::FileSystemImpl;
#[inherit]
@@ -36,25 +51,14 @@ pub mod qobject {
#[qinvokable]
#[cxx_name = "setDirectory"]
fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex;
#[qinvokable]
fn icon(self: Pin<&mut FileSystem>, path: &QString) -> QString;
}
}
use cxx_qt_lib::{QModelIndex, QString};
use dirs;
use log::warn;
use std::fs;
use std::io::BufRead;
use syntect::easy::HighlightFile;
use syntect::highlighting::ThemeSet;
use syntect::html::{
IncludeBackground, append_highlighted_html_for_styled_line, start_highlighted_html_snippet,
};
use syntect::parsing::SyntaxSet;
// TODO: Impleent a provider for QFileSystemModel::setIconProvider for icons.
pub struct FileSystemImpl {
file_path: QString,
root_index: QModelIndex,
}
// Default is explicit to make the editor open this source file initially.
@@ -62,7 +66,6 @@ impl Default for FileSystemImpl {
fn default() -> Self {
Self {
file_path: QString::from(file!()),
root_index: Default::default(),
}
}
}
@@ -72,42 +75,47 @@ impl qobject::FileSystem {
if path.is_empty() {
return QString::default();
}
if !fs::metadata(path.to_string())
.expect(format!("Failed to get file metadata {path:?}").as_str())
.is_file()
{
let meta = fs::metadata(path.to_string())
.expect(format!("Failed to get file metadata {path:?}").as_str());
if !meta.is_file() {
warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file");
return QString::default();
}
let ss = SyntaxSet::load_defaults_nonewlines();
let ts = ThemeSet::load_defaults();
let theme = &ts.themes["base16-ocean.dark"];
let path_str = path.to_string();
if let Ok(lines) = fs::read_to_string(path_str.as_str()) {
let ss = SyntaxSet::load_defaults_nonewlines();
let ts = ThemeSet::load_defaults();
let theme = &ts.themes["base16-ocean.dark"];
let lang = ss
.find_syntax_by_extension(
Path::new(path_str.as_str())
.extension()
.map(|s| s.to_str())
.unwrap_or_else(|| Some("md"))
.expect("Failed to get file extension"),
)
.unwrap_or_else(|| ss.find_syntax_plain_text());
let mut highlighter = HighlightLines::new(lang, theme);
// If you care about the background, see `start_highlighted_html_snippet(theme);`.
let mut output = String::from("<pre>\n");
for line in LinesWithEndings::from(lines.as_str()) {
let regions = highlighter
.highlight_line(line, &ss)
.expect("Failed to highlight");
let mut highlighter =
HighlightFile::new(path.to_string(), &ss, theme).expect("Failed to create highlighter");
let (mut output, _bg) = start_highlighted_html_snippet(theme);
let mut line = String::new();
while highlighter
.reader
.read_line(&mut line)
.expect("Failed to read file.")
> 0
{
let regions = highlighter
.highlight_lines
.highlight_line(&line, &ss)
.expect("Failed to highlight");
append_highlighted_html_for_styled_line(
&regions[..],
IncludeBackground::No,
&mut output,
)
.expect("Failed to insert highlighted html");
}
append_highlighted_html_for_styled_line(
&regions[..],
IncludeBackground::Yes,
&mut output,
)
.expect("Failed to insert highlighted html");
line.clear();
output.push_str("</pre>\n");
QString::from(output)
} else {
return QString::default();
}
output.push_str("</pre>\n");
QString::from(output)
}
// There will never be more than one column.
@@ -124,14 +132,24 @@ impl qobject::FileSystem {
self.set_root_path(path)
} else {
// If the initial directory can't be opened, attempt to find the home directory.
self.set_root_path(&QString::from(
dirs::home_dir()
.expect("Failed to get home directory")
.as_path()
.to_str()
.unwrap()
.to_string(),
))
let homedir = dirs::home_dir()
.expect("Failed to get home directory")
.as_path()
.to_str()
.unwrap()
.to_string();
self.set_root_path(&QString::from(homedir))
}
}
fn icon(self: std::pin::Pin<&mut Self>, path: &QString) -> QString {
let str = path.to_string();
if Path::new(&str).is_dir() {
// Ensures directories are given a folder icon and not mistakenly resolved to a language.
// For example, a directory named `cpp` would otherwise return a C++ icon.
return QString::from(FileIcon::from("dir/").to_string())
}
let icon = FileIcon::from(str);
QString::from(icon.to_string())
}
}

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use anyhow::{Context, Result, anyhow};
use clap::Parser;
use log::{info, trace};

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
mod about;
mod app;
mod component;

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::text::{Line, Span};

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::about::About;
use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger};
use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState};

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
#![allow(dead_code, unused_variables)]
use crate::tui::component::Focus::Inactive;

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail};
use edtui::{

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::component::{Action, Component, Focus, FocusState};
use crate::tui::editor::Editor;
use anyhow::{Context, Result, anyhow};

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail};
use log::trace;

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use log::{LevelFilter, trace};
use ratatui::buffer::Buffer;

View File

@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::component::{Action, Component, ComponentState, FocusState};
use crate::tui::menu_bar::MenuBarItemOption::{
About, CloseTab, Exit, Reload, Save, ShowHideExplorer, ShowHideLogger,