5 Commits

Author SHA1 Message Date
3450d6db9b WIP 2026-02-21 21:10:12 -05:00
8ce92b435b Update CI badge. 2026-02-21 20:12:04 -05:00
bc29502ad4 Add workspace dependencies. 2026-02-21 20:08:41 -05:00
4622102d81 Renames. 2026-02-21 19:45:30 -05:00
dc680554dd Set up workspace, add formatting CI. 2026-02-21 19:16:50 -05:00
13 changed files with 147 additions and 58 deletions

View File

@@ -1,4 +1,4 @@
name: Build name: Check
on: on:
push: push:
@@ -61,7 +61,25 @@ jobs:
- name: Lint libclide - name: Lint libclide
run: | run: |
cargo clippy --manifest-path libclide/Cargo.toml -- -D warnings cargo clippy -p libclide -- -D warnings
- name: Lint clide - name: Lint clide
run: | run: |
cargo clippy -- -D warnings cargo clippy -- -D warnings
Format:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Format libclide
run: |
cargo fmt -p libclide --verbose -- --check
- name: Format clide
run: |
cargo fmt --verbose -- --check

1
Cargo.lock generated
View File

@@ -1169,6 +1169,7 @@ name = "libclide"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"strum",
] ]
[[package]] [[package]]

View File

@@ -3,6 +3,17 @@ name = "clide"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[workspace]
resolver = "3"
members = [
".",
"libclide",
]
[workspace.dependencies]
anyhow = "1.0.100"
strum = "0.27.2"
[dependencies] [dependencies]
cxx = "1.0.95" cxx = "1.0.95"
cxx-qt = "0.8.0" cxx-qt = "0.8.0"
@@ -12,13 +23,13 @@ dirs = "6.0.0"
syntect = "5.2.0" syntect = "5.2.0"
clap = { version = "4.5.54", features = ["derive"] } clap = { version = "4.5.54", features = ["derive"] }
ratatui = "0.30.0" ratatui = "0.30.0"
anyhow = "1.0.100"
tui-tree-widget = "0.24.0" 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"
devicons = "0.6.12" devicons = "0.6.12"
libclide = { path = "./libclide" } libclide = { path = "./libclide" }
anyhow = { workspace = true }
strum = { workspace = true }
[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.

View File

@@ -1,6 +1,6 @@
# CLIDE # CLIDE
[![Build](https://git.shaunreed.com/shaunrd0/clide/actions/workflows/build.yaml/badge.svg)](https://git.shaunreed.com/shaunrd0/clide/workflows/build.yml) [![Check](https://github.com/shaunrd0/clide/actions/workflows/check.yaml/badge.svg)](https://github.com/shaunrd0/clide/actions/workflows/check.yaml)
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.

View File

@@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.102" anyhow = { workspace = true }
strum = { workspace = true }

View File

@@ -2,8 +2,8 @@
// //
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug)] #[derive(Debug)]
pub struct EntryMeta { pub struct EntryMeta {

View File

@@ -3,3 +3,4 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
pub mod fs; pub mod fs;
pub mod theme;

5
libclide/src/theme.rs Normal file
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 colors;

View File

@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pub struct Colors {}
impl Colors {
pub const HOVERED: &str = "#303234";
pub const UNHOVERED: &str = "#3c3f41";
pub const PRESSED: &str = "#4b4f51";
pub const MENUBAR: &str = "#262626";
pub const MENUBAR_BORDER: &str = "#575757";
pub const SCROLLBAR: &str = "#4b4f51";
pub const SCROLLBAR_ACTIVE: &str = "#4b4f51";
pub const SCROLLBAR_GUTTER: &str = "#3b3b3b";
pub const LINENUMBER: &str = "#94989b";
pub const ACTIVE: &str = "#a9acb0";
pub const INACTIVE: &str = "#FFF";
pub const EDITOR_BACKGROUND: &str = "#1E1F22";
pub const EDITOR_TEXT: &str = "#acaea3";
pub const EDITOR_HIGHLIGHTED_TEXT: &str = "#ccced3";
pub const EDITOR_HIGHLIGHT: &str = "#ccced3";
pub const GUTTER: &str = "#1e1f22";
pub const EXPLORER_HOVERED: &str = "#4c5053";
pub const EXPLORER_TEXT: &str = "#FFF";
pub const EXPLORER_TEXT_SELECTED: &str = "#262626";
pub const EXPLORER_BACKGROUND: &str = "#1E1F22";
pub const EXPLORER_FOLDER: &str = "#54585b";
pub const EXPLORER_FOLDER_OPEN: &str = "#393B40";
pub const TERMINAL_BACKGROUND: &str = "#111111";
pub const INFO_LOG: &str = "#C4FFFF";
pub const DEBUG_LOG: &str = "#9148AF";
pub const WARN_LOG: &str = "#C4A958";
pub const ERROR_LOG: &str = "#ff5555";
pub const TRACE_LOG: &str = "#ffaa00";
pub fn css_to_u32(css: &str) -> Result<u32, String> {
let hex = css.trim_start_matches('#');
// Expand shorthand #RGB to #RRGGBB
let hex_full = match hex.len() {
3 => hex.chars()
.map(|c| std::iter::repeat(c).take(2).collect::<String>())
.collect::<String>(),
6 => hex.to_string(),
_ => return Err(format!("Invalid hex color length: {}", hex)),
};
// Parse the hex string as u32, masking to ensure the top byte is 0x00.
u32::from_str_radix(&hex_full, 16)
.map(|rgb| rgb & 0x00FF_FFFF)
.map_err(|e| format!("Failed to parse hex: {}", e))
}
}

View File

@@ -2,8 +2,10 @@
// //
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
#[cxx_qt::bridge] use cxx_qt_lib::QColor;
use libclide::theme::colors::Colors;
#[cxx_qt::bridge]
pub mod qobject { pub mod qobject {
unsafe extern "C++" { unsafe extern "C++" {
include!("cxx-qt-lib/qcolor.h"); include!("cxx-qt-lib/qcolor.h");
@@ -46,8 +48,6 @@ pub mod qobject {
} }
} }
use cxx_qt_lib::QColor;
pub struct RustColorsImpl { pub struct RustColorsImpl {
hovered: QColor, hovered: QColor,
unhovered: QColor, unhovered: QColor,
@@ -82,34 +82,34 @@ pub struct RustColorsImpl {
impl Default for RustColorsImpl { impl Default for RustColorsImpl {
fn default() -> Self { fn default() -> Self {
Self { Self {
hovered: QColor::try_from("#303234").unwrap(), hovered: QColor::try_from(Colors::HOVERED).unwrap(),
unhovered: QColor::try_from("#3c3f41").unwrap(), unhovered: QColor::try_from(Colors::UNHOVERED).unwrap(),
pressed: QColor::try_from("#4b4f51").unwrap(), pressed: QColor::try_from(Colors::PRESSED).unwrap(),
menubar: QColor::try_from("#262626").unwrap(), menubar: QColor::try_from(Colors::MENUBAR).unwrap(),
menubar_border: QColor::try_from("#575757").unwrap(), menubar_border: QColor::try_from(Colors::MENUBAR_BORDER).unwrap(),
scrollbar: QColor::try_from("#4b4f51").unwrap(), scrollbar: QColor::try_from(Colors::SCROLLBAR).unwrap(),
scrollbar_active: QColor::try_from("#4b4f51").unwrap(), scrollbar_active: QColor::try_from(Colors::SCROLLBAR_ACTIVE).unwrap(),
scrollbar_gutter: QColor::try_from("#3b3b3b").unwrap(), scrollbar_gutter: QColor::try_from(Colors::SCROLLBAR_GUTTER).unwrap(),
linenumber: QColor::try_from("#94989b").unwrap(), linenumber: QColor::try_from(Colors::LINENUMBER).unwrap(),
active: QColor::try_from("#a9acb0").unwrap(), active: QColor::try_from(Colors::ACTIVE).unwrap(),
inactive: QColor::try_from("#FFF").unwrap(), inactive: QColor::try_from(Colors::INACTIVE).unwrap(),
editor_background: QColor::try_from("#1E1F22").unwrap(), editor_background: QColor::try_from(Colors::EDITOR_BACKGROUND).unwrap(),
editor_text: QColor::try_from("#acaea3").unwrap(), editor_text: QColor::try_from(Colors::EDITOR_TEXT).unwrap(),
editor_highlighted_text: QColor::try_from("#ccced3").unwrap(), editor_highlighted_text: QColor::try_from(Colors::EDITOR_HIGHLIGHTED_TEXT).unwrap(),
editor_highlight: QColor::try_from("#ccced3").unwrap(), editor_highlight: QColor::try_from(Colors::EDITOR_HIGHLIGHT).unwrap(),
gutter: QColor::try_from("#1e1f22").unwrap(), gutter: QColor::try_from(Colors::GUTTER).unwrap(),
explorer_hovered: QColor::try_from("#4c5053").unwrap(), explorer_hovered: QColor::try_from(Colors::EXPLORER_HOVERED).unwrap(),
explorer_text: QColor::try_from("#FFF").unwrap(), explorer_text: QColor::try_from(Colors::EXPLORER_TEXT).unwrap(),
explorer_text_selected: QColor::try_from("#262626").unwrap(), explorer_text_selected: QColor::try_from(Colors::EXPLORER_TEXT_SELECTED).unwrap(),
explorer_background: QColor::try_from("#1E1F22").unwrap(), explorer_background: QColor::try_from(Colors::EXPLORER_BACKGROUND).unwrap(),
explorer_folder: QColor::try_from("#54585b").unwrap(), explorer_folder: QColor::try_from(Colors::EXPLORER_FOLDER).unwrap(),
explorer_folder_open: QColor::try_from("#393B40").unwrap(), explorer_folder_open: QColor::try_from(Colors::EXPLORER_FOLDER_OPEN).unwrap(),
terminal_background: QColor::try_from("#111111").unwrap(), terminal_background: QColor::try_from(Colors::TERMINAL_BACKGROUND).unwrap(),
info_log: QColor::try_from("#C4FFFF").unwrap(), info_log: QColor::try_from(Colors::INFO_LOG).unwrap(),
debug_log: QColor::try_from("#9148AF").unwrap(), debug_log: QColor::try_from(Colors::DEBUG_LOG).unwrap(),
warn_log: QColor::try_from("#C4A958").unwrap(), warn_log: QColor::try_from(Colors::WARN_LOG).unwrap(),
error_log: QColor::try_from("#ff5555").unwrap(), error_log: QColor::try_from(Colors::ERROR_LOG).unwrap(),
trace_log: QColor::try_from("#ffaa00").unwrap(), trace_log: QColor::try_from(Colors::TRACE_LOG).unwrap(),
} }
} }
} }

View File

@@ -8,7 +8,6 @@ use crate::tui::editor_tab::EditorTab;
use crate::tui::explorer::Explorer; use crate::tui::explorer::Explorer;
use crate::tui::logger::Logger; use crate::tui::logger::Logger;
use crate::tui::menu_bar::MenuBar; use crate::tui::menu_bar::MenuBar;
use AppComponent::AppMenuBar;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::{error, info, trace}; use log::{error, info, trace};
use ratatui::DefaultTerminal; use ratatui::DefaultTerminal;
@@ -26,9 +25,9 @@ use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppComponent { pub enum AppComponent {
Editor, Editor,
AppExplorer, Explorer,
AppLogger, Logger,
AppMenuBar, MenuBar,
} }
pub struct App<'a> { pub struct App<'a> {
@@ -95,9 +94,9 @@ impl<'a> App<'a> {
"Failed to get current Editor while getting widget help text".to_string() "Failed to get current Editor while getting widget help text".to_string()
} }
}, },
AppComponent::AppExplorer => self.explorer.component_state.help_text.clone(), AppComponent::Explorer => self.explorer.component_state.help_text.clone(),
AppComponent::AppLogger => self.logger.component_state.help_text.clone(), AppComponent::Logger => self.logger.component_state.help_text.clone(),
AppComponent::AppMenuBar => self.menu_bar.component_state.help_text.clone(), AppComponent::MenuBar => self.menu_bar.component_state.help_text.clone(),
}; };
Paragraph::new( Paragraph::new(
concat!( concat!(
@@ -137,9 +136,9 @@ impl<'a> App<'a> {
} }
Some(editor) => editor.component_state.set_focus(Focus::Active), Some(editor) => editor.component_state.set_focus(Focus::Active),
}, },
AppComponent::AppExplorer => self.explorer.component_state.set_focus(Focus::Active), AppComponent::Explorer => self.explorer.component_state.set_focus(Focus::Active),
AppComponent::AppLogger => self.logger.component_state.set_focus(Focus::Active), AppComponent::Logger => self.logger.component_state.set_focus(Focus::Active),
AppComponent::AppMenuBar => self.menu_bar.component_state.set_focus(Focus::Active), AppComponent::MenuBar => self.menu_bar.component_state.set_focus(Focus::Active),
} }
self.last_active = focus; self.last_active = focus;
} }
@@ -255,9 +254,9 @@ impl<'a> Component for App<'a> {
// Handle events for all components. // Handle events for all components.
let action = match self.last_active { let action = match self.last_active {
AppComponent::Editor => self.editor_tab.handle_event(event.clone())?, AppComponent::Editor => self.editor_tab.handle_event(event.clone())?,
AppComponent::AppExplorer => self.explorer.handle_event(event.clone())?, AppComponent::Explorer => self.explorer.handle_event(event.clone())?,
AppComponent::AppLogger => self.logger.handle_event(event.clone())?, AppComponent::Logger => self.logger.handle_event(event.clone())?,
AppMenuBar => self.menu_bar.handle_event(event.clone())?, AppComponent::MenuBar => self.menu_bar.handle_event(event.clone())?,
}; };
// Components should always handle mouse events for click interaction. // Components should always handle mouse events for click interaction.
@@ -348,7 +347,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppComponent::AppExplorer); self.change_focus(AppComponent::Explorer);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -366,7 +365,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppComponent::AppLogger); self.change_focus(AppComponent::Logger);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {
@@ -375,7 +374,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
state: _state, state: _state,
} => { } => {
self.change_focus(AppMenuBar); self.change_focus(AppComponent::MenuBar);
Ok(Action::Handled) Ok(Action::Handled)
} }
KeyEvent { KeyEvent {

View File

@@ -7,6 +7,7 @@
use crate::tui::component::Focus::Inactive; use crate::tui::component::Focus::Inactive;
use Focus::Active; use Focus::Active;
use anyhow::Result; use anyhow::Result;
use libclide::theme::colors::Colors;
use log::trace; use log::trace;
use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent}; use ratatui::crossterm::event::{Event, KeyEvent, MouseEvent};
use ratatui::style::Color; use ratatui::style::Color;
@@ -98,8 +99,8 @@ pub enum Focus {
impl Focus { impl Focus {
pub(crate) fn get_active_color(&self) -> Color { pub(crate) fn get_active_color(&self) -> Color {
match self { match self {
Active => Color::LightYellow, Active => Color::from_u32(Colors::css_to_u32(Colors::ACTIVE)?),
Inactive => Color::White, Inactive => Color::from_u32(Colors::css_to_u32(Colors::INACTIVE)?),
} }
} }
} }

View File

@@ -116,7 +116,7 @@ impl Component for Editor {
if let Some(key_event) = event.as_key_event() { if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler. // Handle events here that should not be passed on to the vim emulation handler.
if let Action::Handled = self.handle_key_events(key_event)? { if let Action::Handled = self.handle_key_events(key_event)? {
return Ok(Action::Handled) return Ok(Action::Handled);
} }
} }
self.event_handler.on_event(event, &mut self.state); self.event_handler.on_event(event, &mut self.state);