23 Commits

Author SHA1 Message Date
shaunrd0 a4f6f199ec Test.
Build / Build (pull_request) Failing after 50s
2026-02-21 16:16:19 -05:00
shaunrd0 911a29937e Install libgl. 2026-02-21 16:16:06 -05:00
shaunrd0 579826d398 Add badge for build CI. 2026-02-21 16:00:59 -05:00
shaunrd0 3b1f33f055 Fix qt version.
Build / Build (pull_request) Failing after 17m17s
2026-02-21 15:54:40 -05:00
shaunrd0 607dae32fe Checkout.
Build / Build (pull_request) Failing after 3m56s
2026-02-21 15:49:16 -05:00
shaunrd0 bb032e9daf Source rust.
Build / Build (pull_request) Failing after 2m0s
2026-02-21 15:45:43 -05:00
shaunrd0 886a32a9e2 Set QMAKE.
Build / Build (pull_request) Failing after 1m55s
2026-02-21 15:42:55 -05:00
shaunrd0 0c58b6c436 Update arch.
Build / Build (pull_request) Failing after 1m59s
2026-02-21 15:38:05 -05:00
shaunrd0 df3547267b update
Build / Build (pull_request) Failing after 1m31s
2026-02-21 15:34:30 -05:00
shaunrd0 a605c4929e Use aqtinstall directly.
Build / Build (pull_request) Failing after 40s
2026-02-21 15:33:11 -05:00
shaunrd0 a40125416d Python
Build / Build (pull_request) Failing after 57s
2026-02-21 15:28:00 -05:00
shaunrd0 6777a44b3b Remove sudo
Build / Build (pull_request) Failing after 1m9s
2026-02-21 15:25:01 -05:00
shaunrd0 288298ac18 Install python.
Build / Build (pull_request) Failing after 49s
2026-02-21 15:21:53 -05:00
shaunrd0 d461a29ff9 Yes.
Build / Build (push) Failing after 54s
Build / Build (pull_request) Failing after 56s
2026-02-21 15:18:42 -05:00
shaunrd0 bc906cd7f3 Install things in CI.
Build / Build (push) Failing after 1m26s
Build / Build (pull_request) Failing after 16s
2026-02-21 15:15:18 -05:00
shaunrd0 8ddff3fe9e Test gitea CI.
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 23s
2026-02-21 15:06:08 -05:00
shaunrd0 289f94300c Move EntryMeta to libclide. 2026-02-21 14:59:48 -05:00
shaunrd0 d95aa680ff Add missing headers. 2026-02-21 14:56:28 -05:00
shaunrd0 1119b3db9b Add libclide. 2026-02-21 14:41:17 -05:00
shaunrd0 7ad25af13d Ignore .qmlls.ini. 2026-02-21 14:40:22 -05:00
shaunrd0 7d4f23d82a Fix bug showing project name in explorer. 2026-02-21 14:24:44 -05:00
shaunrd0 b4e14f7f27 Fix bug preventing TUI editors from opening.
Also fix bugs building file tree for paths including `../`.
2026-02-21 14:21:28 -05:00
shaunrd0 f6fdd19f73 Add basic GUI support (#17) 2026-02-08 21:25:03 +00:00
14 changed files with 523 additions and 183 deletions
+56
View File
@@ -0,0 +1,56 @@
name: Build
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
jobs:
Build:
name: Build
runs-on: ubuntu-latest
env:
QT_VERSION: 6.7.3
steps:
- uses: actions/checkout@v6
- 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 -y \
build-essential \
cmake \
curl
libgl1-mesa-dev \
python3 \
python3-pip \
- name: Install aqtinstall
run: |
python3 -m pip install aqtinstall
- name: Install Qt
run: |
aqt install-qt linux desktop $QT_VERSION linux_gcc_64
- name: Build clide
env:
QMAKE: ${{ github.workspace }}/${{ env.QT_VERSION }}/gcc_64/bin/qmake
run: |
. "$HOME/.cargo/env"
cargo b --release
- name: Test libclide
run: |
cargo test -p libclide
- name: Test clide
run: |
cargo test
+1
View File
@@ -2,3 +2,4 @@
**/.qtcreator/** **/.qtcreator/**
**/.idea/** **/.idea/**
**/*.autosave/** **/*.autosave/**
**/.qmlls.ini
Generated
+338 -148
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,8 +17,8 @@ tui-tree-widget = "0.24.0"
tui-logger = "0.18.1" tui-logger = "0.18.1"
edtui = "0.11.1" edtui = "0.11.1"
strum = "0.27.2" strum = "0.27.2"
uuid = { version = "1.19.0", features = ["v4"] }
devicons = "0.6.12" devicons = "0.6.12"
libclide = { path = "./libclide" }
[build-dependencies] [build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6. # The link_qt_object_files feature is required for statically linking Qt 6.
+2
View File
@@ -1,5 +1,7 @@
# CLIDE # CLIDE
[![Build](https://git.shaunreed.com/shaunrd0/clide/actions/workflows/build.yaml/badge.svg)](https://git.shaunreed.com/shaunrd0/clide/workflows/build.yml)
CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments. CLIDE is an extendable command-line driven development environment written in Rust using the Qt UI framework that supports both full and headless Linux environments.
The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate. The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate.
+16
View File
@@ -0,0 +1,16 @@
# 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",
]
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "libclide"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.102"
+5
View File
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod entry_meta;
+50
View File
@@ -0,0 +1,50 @@
// 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,
})
}
}
+5
View File
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod fs;
+4
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 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
+4
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
import QtQuick.Controls.Basic import QtQuick.Controls.Basic
+4
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
import QtQuick.Controls.Basic import QtQuick.Controls.Basic
+27 -31
View File
@@ -4,21 +4,21 @@
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState}; use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::trace; use log::{trace};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind}; use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect}; use ratatui::layout::{Alignment, Position, Rect};
use ratatui::prelude::Style; use ratatui::prelude::Style;
use ratatui::style::{Color, Modifier}; use ratatui::style::{Color, Modifier};
use ratatui::widgets::{Block, Borders, StatefulWidget, Widget}; use ratatui::widgets::{Block, Borders, StatefulWidget, Widget};
use std::ffi::OsStr;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tui_tree_widget::{Tree, TreeItem, TreeState}; use tui_tree_widget::{Tree, TreeItem, TreeState};
use libclide::fs::entry_meta::EntryMeta;
#[derive(Debug)] #[derive(Debug)]
pub struct Explorer<'a> { pub struct Explorer<'a> {
pub(crate) root_path: PathBuf, root_path: EntryMeta,
tree_items: TreeItem<'a, String>, tree_items: TreeItem<'a, String>,
tree_state: TreeState<String>, tree_state: TreeState<String>,
pub(crate) component_state: ComponentState, pub(crate) component_state: ComponentState,
@@ -30,7 +30,7 @@ impl<'a> Explorer<'a> {
pub fn new(path: &PathBuf) -> Result<Self> { pub fn new(path: &PathBuf) -> Result<Self> {
trace!(target:Self::ID, "Building {}", Self::ID); trace!(target:Self::ID, "Building {}", Self::ID);
let explorer = Explorer { let explorer = Explorer {
root_path: path.to_owned(), root_path: EntryMeta::new(&path)?,
tree_items: Self::build_tree_from_path(path.to_owned())?, tree_items: Self::build_tree_from_path(path.to_owned())?,
tree_state: TreeState::default(), tree_state: TreeState::default(),
component_state: ComponentState::default().with_help_text(concat!( component_state: ComponentState::default().with_help_text(concat!(
@@ -41,46 +41,46 @@ impl<'a> Explorer<'a> {
Ok(explorer) Ok(explorer)
} }
fn build_tree_from_path(path: PathBuf) -> Result<TreeItem<'static, String>> { /// 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();
let mut children = vec![]; let mut children = vec![];
let clean_path = fs::canonicalize(path)?; let path_meta = EntryMeta::new(path)?;
if let Ok(entries) = fs::read_dir(&clean_path) { if let Ok(entries) = fs::read_dir(&path_meta.abs_path) {
let mut paths = entries let mut paths = entries
.map(|res| res.map(|e| e.path())) .map(|res| res.map(|e| e.path()))
.collect::<Result<Vec<_>, std::io::Error>>() .collect::<Result<Vec<_>, std::io::Error>>()
.context(format!( .context(format!(
"Failed to build vector of paths under directory: {:?}", "Failed to build vector of paths under directory: {:?}",
clean_path &path_meta.abs_path
))?; ))?;
paths.sort(); paths.sort();
for path in paths { for entry_path in paths {
if path.is_dir() { let entry_meta = EntryMeta::new(&entry_path)?;
children.push(Self::build_tree_from_path(path)?); if entry_meta.is_dir {
children.push(Self::build_tree_from_path(&entry_meta.abs_path)?);
} else { } else {
if let Ok(path) = fs::canonicalize(&path) {
let path_str = path.to_string_lossy().to_string();
children.push(TreeItem::new_leaf( children.push(TreeItem::new_leaf(
path_str + uuid::Uuid::new_v4().to_string().as_str(), entry_meta.abs_path.clone(),
path.file_name() entry_meta.file_name.clone(),
.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( TreeItem::new(
clean_path.to_string_lossy().to_string() + uuid::Uuid::new_v4().to_string().as_str(), path_meta.abs_path.clone(),
clean_path path_meta.file_name.clone(),
.file_name()
.context(format!("Failed to get file name from path: {clean_path:?}"))?
.to_string_lossy()
.to_string(),
children, children,
) )
.context(format!("Failed to build tree from path: {clean_path:?}")) .context(format!(
"Failed to build tree from path: {:?}",
path_meta.abs_path
))
} }
pub fn selected(&self) -> Result<String> { pub fn selected(&self) -> Result<String> {
@@ -97,15 +97,11 @@ impl<'a> Explorer<'a> {
impl<'a> Widget for &mut Explorer<'a> { impl<'a> Widget for &mut Explorer<'a> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
if let Ok(tree) = Tree::new(&self.tree_items.children()) { 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( StatefulWidget::render(
tree.block( tree.block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title(file_name.to_string_lossy()) .title(self.root_path.file_name.clone())
.border_style(Style::default().fg(self.component_state.get_active_color())) .border_style(Style::default().fg(self.component_state.get_active_color()))
.title_style(Style::default().fg(Color::Green)) .title_style(Style::default().fg(Color::Green))
.title_alignment(Alignment::Center), .title_alignment(Alignment::Center),