// 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++" { // Import Qt Types from C++ include!("cxx-qt-lib/qstring.h"); type QString = cxx_qt_lib::QString; include!("cxx-qt-lib/qmodelindex.h"); type QModelIndex = cxx_qt_lib::QModelIndex; include!(); type QFileSystemModel; } unsafe extern "RustQt" { // Export QML Types from Rust #[qobject] #[base = QFileSystemModel] #[qml_element] #[qml_singleton] #[qproperty(QString, file_path, cxx_name = "filePath")] #[qproperty(QModelIndex, root_index, cxx_name = "rootIndex")] type FileSystem = super::FileSystemImpl; #[inherit] #[cxx_name = "setRootPath"] fn set_root_path(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex; #[qinvokable] #[cxx_override] #[cxx_name = "columnCount"] fn column_count(self: &FileSystem, _index: &QModelIndex) -> i32; #[qinvokable] #[cxx_name = "readFile"] fn read_file(self: &FileSystem, path: &QString) -> QString; #[qinvokable] #[cxx_name = "setDirectory"] fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex; } } use cxx_qt_lib::{QModelIndex, QString}; use dirs; use log::warn; use std::fs; use std::io::BufRead; use std::path::Path; use syntect::easy::{HighlightFile, 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; // 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. impl Default for FileSystemImpl { fn default() -> Self { Self { file_path: QString::from(file!()), root_index: Default::default(), } } } impl qobject::FileSystem { fn read_file(&self, path: &QString) -> QString { if path.is_empty() { return QString::default(); } 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 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"); append_highlighted_html_for_styled_line( ®ions[..], IncludeBackground::Yes, &mut output, ) .expect("Failed to insert highlighted html"); } output.push_str("\n"); QString::from(output) } else { return QString::default(); } } // There will never be more than one column. fn column_count(&self, _index: &QModelIndex) -> i32 { 1 } fn set_directory(self: std::pin::Pin<&mut Self>, path: &QString) -> QModelIndex { if !path.is_empty() && fs::metadata(path.to_string()) .expect(format!("Failed to get metadata for path {path:?}").as_str()) .is_dir() { 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(), )) } } }