diff --git a/README.md b/README.md index 8a2fff5..133b943 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,22 @@ 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. 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. + +**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 +``` + ## Usage To install and run clide @@ -126,6 +142,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 diff --git a/build.rs b/build.rs index 1d20876..664dd6e 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,8 @@ fn main() { "qml/ClideProjectView.qml", "qml/ClideEditor.qml", "qml/ClideMenuBar.qml", + "qml/ClideLogger.qml", + "qml/Logger/Logger.qml", ])) // Link Qt's Network library // - Qt Core is always linked @@ -18,6 +20,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(); } diff --git a/icons/kilroy-256.png b/images/kilroy-256.png similarity index 100% rename from icons/kilroy-256.png rename to images/kilroy-256.png diff --git a/qml/ClideAboutWindow.qml b/qml/ClideAboutWindow.qml index 1b7c8a8..6d02ec0 100644 --- a/qml/ClideAboutWindow.qml +++ b/qml/ClideAboutWindow.qml @@ -1,4 +1,6 @@ -// TODO: Header +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// SPDX-License-Identifier: GNU General Public License v3.0 or later import QtQuick import QtQuick.Controls.Basic @@ -29,7 +31,7 @@ ApplicationWindow { anchors.top: parent.top anchors.margins: 20 - source: "../icons/kilroy-256.png" + source: "qrc:/images/kilroy.png" sourceSize.width: 80 sourceSize.height: 80 fillMode: Image.PreserveAspectFit diff --git a/qml/ClideEditor.qml b/qml/ClideEditor.qml index 512b470..8e3558e 100644 --- a/qml/ClideEditor.qml +++ b/qml/ClideEditor.qml @@ -1,8 +1,13 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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 @@ -74,6 +79,9 @@ SplitView { text: parent.index + 1 verticalAlignment: Text.AlignVCenter width: parent.width - indicator.width + background: Rectangle { + color: RustColors.terminal_background + } } // Draw edge along the right side of the line number. Rectangle { @@ -145,19 +153,8 @@ SplitView { } } } - TextArea { + ClideLogger { 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 @@ -203,4 +200,11 @@ SplitView { } } } + + + Component.onCompleted: { + // Show logging is working. + Logger.debug("Debug console ready") + Logger.warn("Warnings show up too") + } } diff --git a/qml/ClideLogger.qml b/qml/ClideLogger.qml new file mode 100644 index 0000000..c7b185b --- /dev/null +++ b/qml/ClideLogger.qml @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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 + +Item { + ListModel { id: model } + + Rectangle { + anchors.fill: parent + color: "#111" + } + + ListView { + id: listView + anchors.fill: parent + model: model + clip: true + + function getLogColor(level) { + switch (level) { + case "INFO": + return RustColors.info_log + break; + case "DEBUG": + return RustColors.debug_log + break; + case "WARN": + return RustColors.warn_log + break; + case "ERROR": + return RustColors.error_log + break; + default: + return RustColors.info_log + break; + } + } + + delegate: Text { + text: `[${level}] ${message}` + font.family: "monospace" + color: listView.getLogColor(level) + } + } + + Connections { + target: Logger + function onLogged(level, message) { + model.append({ level, message }) + } + } +} diff --git a/qml/ClideMenuBar.qml b/qml/ClideMenuBar.qml index 73a6655..0675623 100644 --- a/qml/ClideMenuBar.qml +++ b/qml/ClideMenuBar.qml @@ -1,13 +1,23 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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 + border.color: RustColors.explorer_background + } + // Base settings for each Menu. component ClideMenu : Menu { background: Rectangle { - color: RustColors.menubar + color: RustColors.explorer_background implicitWidth: 100 radius: 2 } @@ -19,7 +29,7 @@ MenuBar { background: Rectangle { color: root.hovered ? RustColors.hovered : RustColors.unhovered - radius: 2.5 + radius: 1.0 } contentItem: IconLabel { color: "black" @@ -28,13 +38,6 @@ MenuBar { } } - // Background for this MenuBar. - background: Rectangle { - color: RustColors.menubar - border.color: RustColors.menubar_border - } - - // // File Menu Action { @@ -75,7 +78,7 @@ MenuBar { MenuSeparator { background: Rectangle { border.color: color - color: RustColors.menubar_border + color: RustColors.explorer_background implicitHeight: 3 implicitWidth: 200 } diff --git a/qml/ClideProjectView.qml b/qml/ClideProjectView.qml index 05d55ef..7ef39f6 100644 --- a/qml/ClideProjectView.qml +++ b/qml/ClideProjectView.qml @@ -1,8 +1,13 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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 @@ -39,18 +44,58 @@ SplitView { SplitView.preferredWidth: 200 SplitView.maximumWidth: 250 - StackLayout { - anchors.fill: parent + ColumnLayout { + spacing: 2 + // TODO: Make a ClideBreadCrumb element to support select parent paths as root + Rectangle { + width: navigationView.width + height: 25 + color: RustColors.explorer_background + Text { + id: breadCrumb + anchors.fill: parent + text: clideTreeView.rootDirectory + color: RustColors.explorer_text + elide: Text.ElideLeft + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: (eventPoint, button) => contextMenu.popup() + } + + Menu { + id: contextMenu + Action { + text: qsTr("Reset root index") + onTriggered: { + Logger.log("Resetting root directory: " + clideTreeView.originalRootDirectory) + clideTreeView.rootDirectory = clideTreeView.originalRootDirectory + } + } + } + } + ClideTreeView { id: clideTreeView - onFileClicked: path => root.projectDir = path + onFileClicked: path => clideEditor.filePath = path + width: navigationView.width + height: navigationView.height // Path to the directory opened in the file explorer. + originalRootDirectory: root.projectDir rootDirectory: root.projectDir + onRootDirectoryChanged: { + Logger.log(clideTreeView.rootDirectory) + breadCrumb.text = clideTreeView.rootDirectory + } } } } ClideEditor { + id: clideEditor SplitView.fillWidth: true // Provide a path to the file currently open in the text editor. diff --git a/qml/ClideTreeView.qml b/qml/ClideTreeView.qml index 76ea446..1ba5db5 100644 --- a/qml/ClideTreeView.qml +++ b/qml/ClideTreeView.qml @@ -1,151 +1,168 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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 { - id: root - color: RustColors.explorer_background +TreeView { + id: fileSystemTreeView + model: FileSystem - required property string rootDirectory + property int lastIndex: -1 + + required property string originalRootDirectory + property string rootDirectory signal fileClicked(string filePath) - TreeView { - id: fileSystemTreeView - anchors.margins: 15 + rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory) + leftMargin: 5 + boundsBehavior: Flickable.StopAtBounds + boundsMovement: Flickable.StopAtBounds + clip: true - property int lastIndex: -1 + // The delegate represents a single entry in the filesystem. + delegate: TreeViewDelegate { + id: treeDelegate + indentation: 12 + implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 + implicitHeight: 25 - model: FileSystem - anchors.fill: parent - boundsBehavior: Flickable.StopAtBounds - boundsMovement: Flickable.StopAtBounds - clip: true + required property int index + required property url filePath + required property string fileName - Component.onCompleted: { - FileSystem.setDirectory(root.rootDirectory) - fileSystemTreeView.expandRecursively(0, -1) + indicator: Image { + id: directoryIcon + + function setSourceImage() { + let folderOpen = "data:image/svg+xml;utf8,"; + let folderClosed = "data:image/svg+xml;utf8,"; + let file = "data:image/svg+xml;utf8,"; + // If the item has children, it's a directory. + if (treeDelegate.hasChildren) { + return treeDelegate.expanded ? + folderOpen : folderClosed; + } 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 } - // The delegate represents a single entry in the filesystem. - delegate: TreeViewDelegate { - id: treeDelegate - indentation: 8 - implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 - implicitHeight: 25 + contentItem: Text { + text: treeDelegate.fileName + color: RustColors.explorer_text + } - required property int index - required property url filePath - required property string fileName + 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 - indicator: Image { - id: directoryIcon - - function setSourceImage() { - let folderOpen = "data:image/svg+xml;utf8,"; - let folderClosed = "data:image/svg+xml;utf8,"; - let file = "data:image/svg+xml;utf8,"; - // If the item has children, it's a directory. - if (treeDelegate.hasChildren) { - return treeDelegate.expanded ? - folderOpen : folderClosed; - } 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("") - } + Behavior on color { + ColorAnimation { + duration: 300 } } } - // Provide our own custom ScrollIndicator for the TreeView. - ScrollIndicator.vertical: ScrollIndicator { - active: true - implicitWidth: 15 + MultiEffect { + id: iconOverlay - contentItem: Rectangle { - implicitWidth: 6 - implicitHeight: 6 + anchors.fill: directoryIcon + source: directoryIcon + colorization: 1.0 + brightness: 1.0 + colorizationColor: { + const isFile = !treeDelegate.hasChildren; + if (isFile) + return Qt.lighter(RustColors.explorer_folder, 2) - color: RustColors.scrollbar - opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 + const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren; + if (isExpandedFolder) + return Qt.darker(RustColors.explorer_folder, 2) + else + return RustColors.explorer_folder + } + } - Behavior on opacity { - OpacityAnimator { - duration: 500 - } + HoverHandler { + id: hoverHandler + acceptedDevices: PointerDevice.Mouse + } + + TapHandler { + acceptedButtons: Qt.LeftButton | Qt.RightButton + onSingleTapped: (eventPoint, button) => { + switch (button) { + case Qt.LeftButton: + fileSystemTreeView.toggleExpanded(treeDelegate.row) + // If this model item doesn't have children, it means it's + // representing a file. + if (!treeDelegate.hasChildren) + fileSystemTreeView.fileClicked(treeDelegate.filePath) + break; + case Qt.RightButton: + contextMenu.popup(); + break; + } + } + } + + Menu { + id: contextMenu + Action { + text: qsTr("Set as root index") + enabled: treeDelegate.hasChildren + onTriggered: { + Logger.debug("Setting new root directory: " + treeDelegate.filePath) + fileSystemTreeView.rootDirectory = treeDelegate.filePath + } + } + Action { + text: qsTr("Reset root index") + onTriggered: { + Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory) + fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory + } + } + } + } + + // Provide our own custom ScrollIndicator for the TreeView. + ScrollIndicator.vertical: ScrollIndicator { + active: true + implicitWidth: 15 + + contentItem: Rectangle { + implicitWidth: 6 + implicitHeight: 6 + + color: RustColors.scrollbar + opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 + + Behavior on opacity { + OpacityAnimator { + duration: 500 } } } diff --git a/qml/Logger/Logger.qml b/qml/Logger/Logger.qml new file mode 100644 index 0000000..5f80b54 --- /dev/null +++ b/qml/Logger/Logger.qml @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// SPDX-License-Identifier: GNU General Public License v3.0 or later + +pragma Singleton +import QtQuick + +QtObject { + signal logged(string level, string message) + + function log(msg) { + console.log(msg) + logged("INFO", msg) + } + + function debug(msg) { + console.log(msg) + logged("DEBUG", msg) + } + + function warn(msg) { + console.warn(msg) + logged("WARN", msg) + } + + function error(msg) { + console.error(msg) + logged("ERROR", msg) + } +} diff --git a/qml/Logger/qmldir b/qml/Logger/qmldir new file mode 100644 index 0000000..019474e --- /dev/null +++ b/qml/Logger/qmldir @@ -0,0 +1 @@ +singleton Logger 1.0 Logger.qml diff --git a/qml/main.qml b/qml/main.qml index fdbbf11..6f71d32 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// SPDX-License-Identifier: GNU General Public License v3.0 or later + import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -15,13 +19,7 @@ ApplicationWindow { required property string appContextPath - menuBar: ClideMenuBar { - } - - Rectangle { - anchors.fill: parent - color: RustColors.gutter - } + menuBar: ClideMenuBar { } MessageDialog { id: errorDialog @@ -29,6 +27,7 @@ ApplicationWindow { title: qsTr("Error") } ClideProjectView { + id: clideProjectView projectDir: appWindow.appContextPath } } diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..abfa83c --- /dev/null +++ b/resources.qrc @@ -0,0 +1,5 @@ + + + images/kilroy-256.png + + \ No newline at end of file diff --git a/src/gui.rs b/src/gui.rs index add9993..48b1e0b 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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}; diff --git a/src/gui/colors.rs b/src/gui/colors.rs index d113109..c7c1fbd 100644 --- a/src/gui/colors.rs +++ b/src/gui/colors.rs @@ -1,4 +1,9 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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,11 @@ 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)] type RustColors = super::RustColorsImpl; } } @@ -60,6 +70,11 @@ 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, } impl Default for RustColorsImpl { @@ -68,7 +83,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 +91,22 @@ 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("#111111").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("#2b2b2b").unwrap(), + terminal_background: QColor::try_from("#111111").unwrap(), + info_log: QColor::try_from("#C4FFFF").unwrap(), + debug_log: QColor::try_from("#55ff99").unwrap(), + warn_log: QColor::try_from("#ffaa00").unwrap(), + error_log: QColor::try_from("#ff5555").unwrap(), } } } diff --git a/src/gui/filesystem.rs b/src/gui/filesystem.rs index 557203b..9d897b7 100644 --- a/src/gui/filesystem.rs +++ b/src/gui/filesystem.rs @@ -1,3 +1,20 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// SPDX-License-Identifier: GNU General Public License v3.0 or later + +use cxx_qt_lib::{QModelIndex, QString}; +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, start_highlighted_html_snippet, +}; +use syntect::parsing::SyntaxSet; +use syntect::util::LinesWithEndings; + #[cxx_qt::bridge] pub mod qobject { unsafe extern "C++" { @@ -17,7 +34,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] @@ -39,22 +55,9 @@ pub mod qobject { } } -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. +// TODO: Implement 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 +65,6 @@ impl Default for FileSystemImpl { fn default() -> Self { Self { file_path: QString::from(file!()), - root_index: Default::default(), } } } @@ -72,42 +74,46 @@ 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); + let (mut output, _bg) = start_highlighted_html_snippet(theme); + 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( + ®ions[..], + IncludeBackground::Yes, + &mut output, + ) + .expect("Failed to insert highlighted html"); + } - append_highlighted_html_for_styled_line( - ®ions[..], - IncludeBackground::Yes, - &mut output, - ) - .expect("Failed to insert highlighted html"); - line.clear(); + output.push_str("\n"); + QString::from(output) + } else { + return QString::default(); } - output.push_str("\n"); - QString::from(output) } // There will never be more than one column. @@ -124,14 +130,13 @@ 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)) } } } diff --git a/src/main.rs b/src/main.rs index 917b90b..c975f90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// SPDX-License-Identifier: GNU General Public License v3.0 or later + use anyhow::{Context, Result, anyhow}; use clap::Parser; use log::{info, trace}; diff --git a/src/tui.rs b/src/tui.rs index d0569c5..2652ca7 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// SPDX-License-Identifier: GNU General Public License v3.0 or later + mod about; mod app; mod component; diff --git a/src/tui/about.rs b/src/tui/about.rs index aa68961..7b53b7b 100644 --- a/src/tui/about.rs +++ b/src/tui/about.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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}; diff --git a/src/tui/app.rs b/src/tui/app.rs index d925d2c..d5569eb 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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}; diff --git a/src/tui/component.rs b/src/tui/component.rs index 2b73ae2..27b3999 100644 --- a/src/tui/component.rs +++ b/src/tui/component.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// SPDX-License-Identifier: GNU General Public License v3.0 or later + #![allow(dead_code, unused_variables)] use crate::tui::component::Focus::Inactive; diff --git a/src/tui/editor.rs b/src/tui/editor.rs index a1f0081..0c20d3e 100644 --- a/src/tui/editor.rs +++ b/src/tui/editor.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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::{ diff --git a/src/tui/editor_tab.rs b/src/tui/editor_tab.rs index 76c2de4..21d8112 100644 --- a/src/tui/editor_tab.rs +++ b/src/tui/editor_tab.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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}; diff --git a/src/tui/explorer.rs b/src/tui/explorer.rs index fbd2063..cf295bb 100644 --- a/src/tui/explorer.rs +++ b/src/tui/explorer.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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; diff --git a/src/tui/logger.rs b/src/tui/logger.rs index c68e441..d88cf68 100644 --- a/src/tui/logger.rs +++ b/src/tui/logger.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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; diff --git a/src/tui/menu_bar.rs b/src/tui/menu_bar.rs index 1de6b76..129d83a 100644 --- a/src/tui/menu_bar.rs +++ b/src/tui/menu_bar.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2026, Shaun Reed +// +// 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,