// SPDX-FileCopyrightText: 2026, Shaun Reed // // SPDX-License-Identifier: GNU General Public License v3.0 or later use crate::gui::filesystem::qobject::{QAbstractItemModel}; use cxx_qt_lib::{QModelIndex, QString}; use dirs; use log::warn; use std::io::BufRead; use std::{fs}; use std::pin::Pin; 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; #[cxx_qt::bridge] pub mod qobject { // Import Qt Types from C++ unsafe extern "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; include!(); type QSortFilterProxyModel; include!(); type QAbstractItemModel; } // Export QML classes from Rust unsafe extern "RustQt" { #[qobject] #[qml_element] #[base = QSortFilterProxyModel] #[qproperty(*mut FileSystem, inner)] type FileSystemSortProxyModel = super::FileSystemSortProxyModelImpl; #[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; } // Export QSortFilterProxyModel functions from Rust // https://doc.qt.io/qt-6/qsortfilterproxymodel.html unsafe extern "RustQt" { #[inherit] #[cxx_name = "setSourceModel"] unsafe fn set_source_model( self: Pin<&mut FileSystemSortProxyModel>, source: *mut QAbstractItemModel, ); #[cxx_override] #[cxx_name = "filterAcceptsRow"] const fn filter_accepts_row( self: &FileSystemSortProxyModel, source_row: i32, source_parent: &QModelIndex, ) -> bool; #[qinvokable] #[cxx_name = "setDirectory"] fn set_directory(self: Pin<&mut FileSystemSortProxyModel>, path: &QString) -> QModelIndex; } // Custom initialization logic. impl cxx_qt::Initialize for FileSystemSortProxyModel {} // Export QFileSystemModel functions from Rust // https://doc.qt.io/qt-6/qfilesystemmodel.html unsafe extern "RustQt" { #[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; } } pub struct FileSystemSortProxyModelImpl { inner: *mut qobject::FileSystem, } impl Default for FileSystemSortProxyModelImpl { fn default() -> Self { let model = Self { inner: std::ptr::null_mut(), }; model } } impl qobject::FileSystemSortProxyModel { pub const fn filter_accepts_row(&self, _source_row: i32, _source_parent: &QModelIndex) -> bool { false } fn read_file(&self, path: &QString) -> QString { if let Some(inner) = unsafe { self.inner().as_ref() } { let pinned_inner = unsafe { Pin::new_unchecked(inner) }; return pinned_inner.read_file(path) } else { panic!("Can't get inner()") } QString::default() } // There will never be more than one column. fn column_count(&self, _index: &QModelIndex) -> i32 { 1 } fn set_directory(self: Pin<&mut Self>, path: &QString) -> QModelIndex { if let Some(inner) = unsafe { self.inner().as_mut() } { let pinned_inner = unsafe { Pin::new_unchecked(inner) }; return pinned_inner.set_directory(path) } else { panic!("Can't get inner()") } QModelIndex::default() } } impl cxx_qt::Initialize for qobject::FileSystemSortProxyModel { fn initialize(self: core::pin::Pin<&mut Self>) { let mut fs = FileSystemImpl::default(); unsafe { let model: *mut FileSystemImpl = std::ptr::from_mut(&mut fs); let m: *mut QAbstractItemModel = model as *mut QAbstractItemModel; self.set_source_model(m); } } } // 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. 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(); } if !fs::metadata(path.to_string()) .expect(format!("Failed to get file metadata {path:?}").as_str()) .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 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"); line.clear(); } output.push_str("\n"); QString::from(output) } // 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(), )) } } }