41 Commits

Author SHA1 Message Date
e3bf484a5d Remove TODO 2026-02-08 16:21:52 -05:00
bedd510b0a Update GUI screenshot. 2026-02-08 15:46:17 -05:00
a30ea2b457 Remove TODOs. 2026-02-08 15:37:24 -05:00
52ceda5b06 Reorganize files. 2026-02-08 15:29:34 -05:00
a37fd74d32 Fix spacing. 2026-02-08 15:25:21 -05:00
6ed4ca11f4 Fix width for file tree selection. 2026-02-08 15:21:06 -05:00
41fab2847d Update GUI screenshot in README. 2026-02-08 15:10:06 -05:00
c76529b3cb Fix first line background.
Added a comment if the background in desired in the future.
2026-02-08 15:02:28 -05:00
5f5b1a4d39 Add frame around the application view. 2026-02-08 14:52:57 -05:00
4bb4ce1882 Clean up GUI. 2026-02-08 14:26:26 -05:00
344efc0042 Clean up ClideHandle. 2026-02-08 10:08:47 -05:00
a63acb18fc WIP 2026-02-08 00:03:50 -05:00
f918d65888 Add clickable bread crumbs.
Clicking a parent path changes the root project directory.
2026-02-07 21:18:59 -05:00
aa42ec6072 Get file explorer icon based on file type.
Fixes #15
2026-02-07 18:41:51 -05:00
6f2a655497 Clean up context menus. 2026-02-07 12:59:44 -05:00
c0f38b531d Add arrow to show expanded folder. 2026-02-07 12:43:15 -05:00
4aad91416e Fix selection flickering when expanding folder. 2026-02-07 11:16:57 -05:00
ff1b5ab2e6 Hide handles when not in use. 2026-02-07 10:51:37 -05:00
491087a6c1 Add environment variables to config.toml 2026-02-07 10:37:05 -05:00
c170b3b20d WIP 2026-02-06 22:10:20 -05:00
125041f469 Add ClideEditorView. 2026-02-06 19:36:14 -05:00
67bf82d0cb Add qmllint.ini 2026-02-06 18:59:38 -05:00
39377b32f0 WIP 2026-02-06 18:59:02 -05:00
176efb97b7 Clean up ClideScrollBar. 2026-02-04 19:52:05 -05:00
755066d847 Clean up ClideHandle. 2026-02-04 19:29:52 -05:00
773d7818b5 Auto scroll logger. 2026-02-04 18:35:43 -05:00
7e58e3ee03 Split basic components for reuse. 2026-02-02 23:05:53 -05:00
0f50577d78 Fix scrollbar animations. 2026-02-02 18:21:28 -05:00
29024e3999 Format with qmlformat. 2026-02-02 18:08:37 -05:00
5af09485a3 Add trace logs. 2026-02-02 18:01:53 -05:00
e5b91eaed8 Add basic debug logger. 2026-02-01 20:23:23 -05:00
be383869b2 Add context menu on breadcrumbs.
The only option is to reset the root directory.
2026-02-01 19:10:15 -05:00
b9eee50e52 Fix menu bar colors. 2026-02-01 18:42:21 -05:00
4cc43916cb Update breadcrumbs when root directory changes. 2026-02-01 18:23:44 -05:00
a5bed9ed2c Fix panic when loading bad text in the GUI. 2026-02-01 17:15:21 -05:00
0fac2b71ab Fix TreeView nesting for ColumnLayout. 2026-02-01 15:35:40 -05:00
db2f878018 Fix about page image loading. 2026-02-01 13:44:43 -05:00
048d40eb83 Fix TreeView root index.
Parent folders will now be hidden based on the root folder selection in
the GUI.
2026-02-01 13:07:27 -05:00
c70bba16e4 Revert "Try to use QSortFilterProxyModel via cxx-qt."
This reverts commit 325cf285fc.
2026-02-01 13:07:10 -05:00
325cf285fc Try to use QSortFilterProxyModel via cxx-qt.
This may be easier if I just wrap a QAbstractItemModel in C++ and use
cxx-qt to pull it into QML? That way I could do C++ pointer things in
C++ and rust things in rust.
2026-01-31 21:47:19 -05:00
aa8590cd5c Add license headers. 2026-01-31 08:02:16 -05:00
13 changed files with 181 additions and 491 deletions

View File

@@ -1,28 +0,0 @@
name: Build
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
jobs:
Build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- name: Install Python
run: |
apt update - y
apt install python3 -y
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: '6.7.3'

3
.gitignore vendored
View File

@@ -1,5 +1,4 @@
**/target/**
**/.qtcreator/**
**/.idea/**
**/*.autosave/**
**/.qmlls.ini
**/*.autosave/**

482
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,8 @@ tui-tree-widget = "0.24.0"
tui-logger = "0.18.1"
edtui = "0.11.1"
strum = "0.27.2"
uuid = { version = "1.19.0", features = ["v4"] }
devicons = "0.6.12"
libclide = { path = "./libclide" }
[build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6.

16
libclide/Cargo.lock generated
View File

@@ -1,16 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "libclide"
version = "0.1.0"
dependencies = [
"anyhow",
]

View File

@@ -1,7 +0,0 @@
[package]
name = "libclide"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.102"

View File

@@ -1,5 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod entry_meta;

View File

@@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
#[derive(Debug)]
pub struct EntryMeta {
pub abs_path: String,
pub file_name: String,
pub is_dir: bool,
}
impl EntryMeta {
/// Normalizes a path, returning an absolute from the root of the filesystem.
/// Does not resolve symlinks and extracts `./` or `../` segments.
fn normalize<P: AsRef<Path>>(p: P) -> PathBuf {
let path = p.as_ref();
let mut buf = PathBuf::new();
for comp in path.components() {
match comp {
std::path::Component::ParentDir => {
buf.pop();
}
std::path::Component::CurDir => {}
_ => buf.push(comp),
}
}
buf
}
pub fn new<P: AsRef<Path>>(p: P) -> Result<Self> {
let path = p.as_ref();
let is_dir = path.is_dir();
let abs_path = Self::normalize(&path).to_string_lossy().to_string();
let file_name = Path::new(&abs_path)
.file_name()
.context(format!("Failed to get file name for path: {abs_path:?}"))?
.to_string_lossy()
.to_string();
Ok(EntryMeta {
abs_path,
file_name,
is_dir,
})
}
}

View File

@@ -1,5 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod fs;

View File

@@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

View File

@@ -1,7 +1,3 @@
// 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

View File

@@ -1,7 +1,3 @@
// 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

View File

@@ -4,21 +4,21 @@
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail};
use log::{trace};
use log::trace;
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect};
use ratatui::prelude::Style;
use ratatui::style::{Color, Modifier};
use ratatui::widgets::{Block, Borders, StatefulWidget, Widget};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use tui_tree_widget::{Tree, TreeItem, TreeState};
use libclide::fs::entry_meta::EntryMeta;
#[derive(Debug)]
pub struct Explorer<'a> {
root_path: EntryMeta,
pub(crate) root_path: PathBuf,
tree_items: TreeItem<'a, String>,
tree_state: TreeState<String>,
pub(crate) component_state: ComponentState,
@@ -30,7 +30,7 @@ impl<'a> Explorer<'a> {
pub fn new(path: &PathBuf) -> Result<Self> {
trace!(target:Self::ID, "Building {}", Self::ID);
let explorer = Explorer {
root_path: EntryMeta::new(&path)?,
root_path: path.to_owned(),
tree_items: Self::build_tree_from_path(path.to_owned())?,
tree_state: TreeState::default(),
component_state: ComponentState::default().with_help_text(concat!(
@@ -41,46 +41,46 @@ impl<'a> Explorer<'a> {
Ok(explorer)
}
/// Builds the file tree from a path using recursion.
/// The identifiers used for the TreeItems are normalized. Symlinks are not resolved.
/// Resolving symlinks would cause collisions on the TreeItem unique identifiers within the set.
fn build_tree_from_path<P: AsRef<Path>>(p: P) -> Result<TreeItem<'static, String>> {
let path = p.as_ref();
fn build_tree_from_path(path: PathBuf) -> Result<TreeItem<'static, String>> {
let mut children = vec![];
let path_meta = EntryMeta::new(path)?;
if let Ok(entries) = fs::read_dir(&path_meta.abs_path) {
let clean_path = fs::canonicalize(path)?;
if let Ok(entries) = fs::read_dir(&clean_path) {
let mut paths = entries
.map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>()
.context(format!(
"Failed to build vector of paths under directory: {:?}",
&path_meta.abs_path
clean_path
))?;
paths.sort();
for entry_path in paths {
let entry_meta = EntryMeta::new(&entry_path)?;
if entry_meta.is_dir {
children.push(Self::build_tree_from_path(&entry_meta.abs_path)?);
for path in paths {
if path.is_dir() {
children.push(Self::build_tree_from_path(path)?);
} else {
children.push(TreeItem::new_leaf(
entry_meta.abs_path.clone(),
entry_meta.file_name.clone(),
));
if let Ok(path) = fs::canonicalize(&path) {
let path_str = path.to_string_lossy().to_string();
children.push(TreeItem::new_leaf(
path_str + uuid::Uuid::new_v4().to_string().as_str(),
path.file_name()
.context("Failed to get file name from path.")?
.to_string_lossy()
.to_string(),
));
}
}
}
}
// Note: The first argument is a unique identifier, where no 2 TreeItems may share the same.
// For a file tree this is fine because we shouldn't list the same object twice.
TreeItem::new(
path_meta.abs_path.clone(),
path_meta.file_name.clone(),
clean_path.to_string_lossy().to_string() + uuid::Uuid::new_v4().to_string().as_str(),
clean_path
.file_name()
.context(format!("Failed to get file name from path: {clean_path:?}"))?
.to_string_lossy()
.to_string(),
children,
)
.context(format!(
"Failed to build tree from path: {:?}",
path_meta.abs_path
))
.context(format!("Failed to build tree from path: {clean_path:?}"))
}
pub fn selected(&self) -> Result<String> {
@@ -97,11 +97,15 @@ impl<'a> Explorer<'a> {
impl<'a> Widget for &mut Explorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Ok(tree) = Tree::new(&self.tree_items.children()) {
let file_name = self
.root_path
.file_name()
.unwrap_or_else(|| OsStr::new("Unknown"));
StatefulWidget::render(
tree.block(
Block::default()
.borders(Borders::ALL)
.title(self.root_path.file_name.clone())
.title(file_name.to_string_lossy())
.border_style(Style::default().fg(self.component_state.get_active_color()))
.title_style(Style::default().fg(Color::Green))
.title_alignment(Alignment::Center),