43 Commits

Author SHA1 Message Date
shaunrd0 177a4bc432 Fix clide lints. 2026-02-21 19:03:18 -05:00
shaunrd0 5f4391cb82 Fix libclide lints. 2026-02-21 18:38:07 -05:00
shaunrd0 2273c0156e Clean up CI. 2026-02-21 18:30:26 -05:00
shaunrd0 417f01b527 remove [env] 2026-02-21 18:24:18 -05:00
shaunrd0 d776602fe8 Remove debug. 2026-02-21 18:20:51 -05:00
shaunrd0 e2ddadd952 Don't force in cargo.toml 2026-02-21 18:20:29 -05:00
shaunrd0 61c7f59237 debug 2026-02-21 18:16:56 -05:00
shaunrd0 1e63eabd46 Set qmake? 2026-02-21 18:12:45 -05:00
shaunrd0 ac83b3e30f Don't set QMAKE. 2026-02-21 18:04:53 -05:00
shaunrd0 a42ad73a57 Add setup action. 2026-02-21 18:01:51 -05:00
shaunrd0 1ec13aa43a Move env. 2026-02-21 17:52:38 -05:00
shaunrd0 0a3f095080 Install qt using action. 2026-02-21 17:51:09 -05:00
shaunrd0 6474c5b6bd Reuse steps. 2026-02-21 17:49:42 -05:00
shaunrd0 717ea70895 Fix qmake. 2026-02-21 17:37:46 -05:00
shaunrd0 8eada4bbee Update push. 2026-02-21 17:35:04 -05:00
shaunrd0 0ebd45ae15 Move CI to github.
Build / Build (pull_request) Failing after 9s
Why not, it's free.
2026-02-21 17:32:44 -05:00
shaunrd0 14e7514cc1 Merge jobs.
Build / Build (pull_request) Failing after 31m19s
2026-02-21 17:25:58 -05:00
shaunrd0 b3bb13fa33 22.04
Build / Build (pull_request) Failing after 10s
Build / Test (pull_request) Failing after 4s
2026-02-21 17:24:36 -05:00
shaunrd0 ad95056376 fix path.
Build / Build (pull_request) Failing after 4s
Build / Test (pull_request) Failing after 4s
2026-02-21 17:23:22 -05:00
shaunrd0 384fa51b6e Test with ubuntu:latest.
Build / Build (pull_request) Failing after 6s
Build / Test (pull_request) Failing after 3s
ubuntu:24.04 was not defined in the runner's docker-compose.yaml server side.
2026-02-21 17:21:40 -05:00
shaunrd0 fc2a44740f Use ubuntu-latest.
Build / Build (pull_request) Failing after 4m36s
Build / Test (pull_request) Failing after 3s
2026-02-21 17:17:52 -05:00
shaunrd0 f609aa02db Update env.
Build / Build (pull_request) Failing after 5s
Build / Test (pull_request) Failing after 5s
2026-02-21 17:12:07 -05:00
shaunrd0 fdb4f0db0b Test named volume.
Build / Build (pull_request) Failing after 10s
Build / Test (pull_request) Failing after 4s
2026-02-21 16:53:03 -05:00
shaunrd0 2d5e721a79 Update environment.
Build / Build (pull_request) Failing after 7s
2026-02-21 16:52:42 -05:00
shaunrd0 2e55ba1a4b Source rust things.
Build / Build (pull_request) Failing after 19m32s
2026-02-21 16:40:34 -05:00
shaunrd0 6b9e3b1b40 Update name. 2026-02-21 16:26:18 -05:00
shaunrd0 bdb126cab5 Fix apt install.
Build / Build (pull_request) Failing after 17m52s
2026-02-21 16:22:10 -05:00
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
13 changed files with 141 additions and 61 deletions
-4
View File
@@ -1,6 +1,2 @@
[build]
rustflags = [ "-C", "link-arg=-fuse-ld=lld", ]
[env]
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"
+20
View File
@@ -0,0 +1,20 @@
name: "Setup Qt"
description: "Install clide dependencies"
inputs:
qt-version:
description: "Qt version to install"
required: true
runs:
using: "composite"
steps:
- name: Install apt packages
run: |
sudo apt update -y
sudo apt install -y build-essential cmake curl libgl1-mesa-dev python3 python3-pip
shell: bash
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ inputs.qt-version }}
+67
View File
@@ -0,0 +1,67 @@
name: Build
on:
push:
branches:
- '**'
tags:
- 'v*'
pull_request:
env:
QT_VERSION: 6.7.3
jobs:
Build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Build libclide
run: |
cargo b -p libclide --release
- name: Build clide
run: |
cargo b --release
Test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Test libclide
run: |
cargo test -p libclide
- name: Test clide
run: |
cargo test
Lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Qt
uses: ./.github/actions/setup-qt
with:
qt-version: ${{ env.QT_VERSION }}
- name: Lint libclide
run: |
cargo clippy --manifest-path libclide/Cargo.toml -- -D warnings
- name: Lint clide
run: |
cargo clippy -- -D warnings
+2
View File
@@ -1,5 +1,7 @@
# 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.
The GUI is written in QML compiled through Rust using the cxx-qt crate, while the TUI was implemented using the ratatui crate.
+1 -1
View File
@@ -1,7 +1,7 @@
use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() {
CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[
CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files([
"qml/ClideApplicationView.qml",
"qml/ClideEditorView.qml",
"qml/ClideExplorerView.qml",
+1 -1
View File
@@ -35,7 +35,7 @@ impl EntryMeta {
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 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:?}"))?
+4 -4
View File
@@ -76,7 +76,7 @@ impl qobject::FileSystem {
return QString::default();
}
let meta = fs::metadata(path.to_string())
.expect(format!("Failed to get file metadata {path:?}").as_str());
.unwrap_or_else(|_| panic!("Failed to get file metadata {path:?}"));
if !meta.is_file() {
warn!(target:"FileSystem", "Attempted to open file {path:?} that is not a valid file");
return QString::default();
@@ -114,7 +114,7 @@ impl qobject::FileSystem {
output.push_str("</pre>\n");
QString::from(output)
} else {
return QString::default();
QString::default()
}
}
@@ -126,7 +126,7 @@ impl qobject::FileSystem {
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())
.unwrap_or_else(|_| panic!("Failed to get metadata for path {path:?}"))
.is_dir()
{
self.set_root_path(path)
@@ -147,7 +147,7 @@ impl qobject::FileSystem {
if Path::new(&str).is_dir() {
// Ensures directories are given a folder icon and not mistakenly resolved to a language.
// For example, a directory named `cpp` would otherwise return a C++ icon.
return QString::from(FileIcon::from("dir/").to_string())
return QString::from(FileIcon::from("dir/").to_string());
}
let icon = FileIcon::from(str);
QString::from(icon.to_string())
+1 -1
View File
@@ -82,7 +82,7 @@ fn main() -> Result<()> {
RunMode::Gui => {
trace!(target:"main()", "Starting GUI in a new process");
Command::new(std::env::current_exe()?)
.args(&["--gui", app_context.path.to_str().unwrap()])
.args(["--gui", app_context.path.to_str().unwrap()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null())
+2 -2
View File
@@ -68,8 +68,8 @@ impl Widget for About {
.map(|l| Line::from(Span::raw(*l)))
.collect();
Clear::default().render(kilroy_rect, buf);
Clear::default().render(chunks[1], buf);
Clear.render(kilroy_rect, buf);
Clear.render(chunks[1], buf);
Paragraph::new(about_lines)
.block(
Block::default()
+23 -24
View File
@@ -3,7 +3,6 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use crate::tui::about::About;
use crate::tui::app::AppComponent::{AppEditor, AppExplorer, AppLogger};
use crate::tui::component::{Action, Component, Focus, FocusState, Visibility, VisibleState};
use crate::tui::editor_tab::EditorTab;
use crate::tui::explorer::Explorer;
@@ -26,7 +25,7 @@ use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AppComponent {
AppEditor,
Editor,
AppExplorer,
AppLogger,
AppMenuBar,
@@ -51,7 +50,7 @@ impl<'a> App<'a> {
explorer: Explorer::new(&root_path)?,
logger: Logger::new(),
menu_bar: MenuBar::new(),
last_active: AppEditor,
last_active: AppComponent::Editor,
about: false,
};
Ok(app)
@@ -87,7 +86,7 @@ impl<'a> App<'a> {
fn draw_bottom_status(&self, area: Rect, buf: &mut Buffer) {
// Determine help text from the most recently focused component.
let help = match self.last_active {
AppEditor => match self.editor_tab.current_editor() {
AppComponent::Editor => match self.editor_tab.current_editor() {
Some(editor) => editor.component_state.help_text.clone(),
None => {
if !self.editor_tab.is_empty() {
@@ -96,9 +95,9 @@ impl<'a> App<'a> {
"Failed to get current Editor while getting widget help text".to_string()
}
},
AppExplorer => self.explorer.component_state.help_text.clone(),
AppLogger => self.logger.component_state.help_text.clone(),
AppMenuBar => self.menu_bar.component_state.help_text.clone(),
AppComponent::AppExplorer => self.explorer.component_state.help_text.clone(),
AppComponent::AppLogger => self.logger.component_state.help_text.clone(),
AppComponent::AppMenuBar => self.menu_bar.component_state.help_text.clone(),
};
Paragraph::new(
concat!(
@@ -132,15 +131,15 @@ impl<'a> App<'a> {
info!(target:Self::ID, "Changing widget focus to {:?}", focus);
self.clear_focus();
match focus {
AppEditor => match self.editor_tab.current_editor_mut() {
AppComponent::Editor => match self.editor_tab.current_editor_mut() {
None => {
error!(target:Self::ID, "Failed to get current Editor while changing focus")
}
Some(editor) => editor.component_state.set_focus(Focus::Active),
},
AppExplorer => self.explorer.component_state.set_focus(Focus::Active),
AppLogger => self.logger.component_state.set_focus(Focus::Active),
AppMenuBar => self.menu_bar.component_state.set_focus(Focus::Active),
AppComponent::AppExplorer => self.explorer.component_state.set_focus(Focus::Active),
AppComponent::AppLogger => self.logger.component_state.set_focus(Focus::Active),
AppComponent::AppMenuBar => self.menu_bar.component_state.set_focus(Focus::Active),
}
self.last_active = focus;
}
@@ -255,21 +254,21 @@ impl<'a> Component for App<'a> {
}
// Handle events for all components.
let action = match self.last_active {
AppEditor => self.editor_tab.handle_event(event.clone())?,
AppExplorer => self.explorer.handle_event(event.clone())?,
AppLogger => self.logger.handle_event(event.clone())?,
AppComponent::Editor => self.editor_tab.handle_event(event.clone())?,
AppComponent::AppExplorer => self.explorer.handle_event(event.clone())?,
AppComponent::AppLogger => self.logger.handle_event(event.clone())?,
AppMenuBar => self.menu_bar.handle_event(event.clone())?,
};
// Components should always handle mouse events for click interaction.
if let Some(mouse) = event.as_mouse_event() {
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
if let Some(editor) = self.editor_tab.current_editor_mut() {
editor.handle_mouse_events(mouse)?;
}
self.explorer.handle_mouse_events(mouse)?;
self.logger.handle_mouse_events(mouse)?;
if let Some(mouse) = event.as_mouse_event()
&& mouse.kind == MouseEventKind::Down(MouseButton::Left)
{
if let Some(editor) = self.editor_tab.current_editor_mut() {
editor.handle_mouse_events(mouse)?;
}
self.explorer.handle_mouse_events(mouse)?;
self.logger.handle_mouse_events(mouse)?;
}
// Handle actions returned from widgets that may need context on other widgets or app state.
@@ -349,7 +348,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press,
state: _state,
} => {
self.change_focus(AppExplorer);
self.change_focus(AppComponent::AppExplorer);
Ok(Action::Handled)
}
KeyEvent {
@@ -358,7 +357,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press,
state: _state,
} => {
self.change_focus(AppEditor);
self.change_focus(AppComponent::Editor);
Ok(Action::Handled)
}
KeyEvent {
@@ -367,7 +366,7 @@ impl<'a> Component for App<'a> {
kind: KeyEventKind::Press,
state: _state,
} => {
self.change_focus(AppLogger);
self.change_focus(AppComponent::AppLogger);
Ok(Action::Handled)
}
KeyEvent {
+2 -3
View File
@@ -115,9 +115,8 @@ impl Component for Editor {
fn handle_event(&mut self, event: Event) -> Result<Action> {
if let Some(key_event) = event.as_key_event() {
// Handle events here that should not be passed on to the vim emulation handler.
match self.handle_key_events(key_event)? {
Action::Handled => return Ok(Action::Handled),
_ => {}
if let Action::Handled = self.handle_key_events(key_event)? {
return Ok(Action::Handled)
}
}
self.event_handler.on_event(event, &mut self.state);
+15 -17
View File
@@ -4,7 +4,8 @@
use crate::tui::component::{Action, Component, ComponentState, Focus, FocusState};
use anyhow::{Context, Result, bail};
use log::{trace};
use libclide::fs::entry_meta::EntryMeta;
use log::trace;
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{Event, KeyCode, KeyEvent, MouseEvent, MouseEventKind};
use ratatui::layout::{Alignment, Position, Rect};
@@ -14,7 +15,6 @@ use ratatui::widgets::{Block, Borders, StatefulWidget, Widget};
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> {
@@ -30,8 +30,8 @@ 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)?,
tree_items: Self::build_tree_from_path(path.to_owned())?,
root_path: EntryMeta::new(path)?,
tree_items: Self::build_tree_from_path(path)?,
tree_state: TreeState::default(),
component_state: ComponentState::default().with_help_text(concat!(
"(↑/k)/(↓/j): Select item | ←/h: Close folder | →/l: Open folder |",
@@ -96,7 +96,7 @@ 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()) {
if let Ok(tree) = Tree::new(self.tree_items.children()) {
StatefulWidget::render(
tree.block(
Block::default()
@@ -130,23 +130,21 @@ impl<'a> Component for Explorer<'a> {
_ => {}
}
}
if let Some(mouse_event) = event.as_mouse_event() {
match self.handle_mouse_events(mouse_event)? {
Action::Handled => return Ok(Action::Handled),
_ => {}
}
if let Some(mouse_event) = event.as_mouse_event()
&& let Action::Handled = self.handle_mouse_events(mouse_event)?
{
return Ok(Action::Handled);
}
Ok(Action::Pass)
}
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Action> {
if key.code == KeyCode::Enter {
if let Ok(selected) = self.selected() {
if Path::new(&selected).is_file() {
return Ok(Action::OpenTab);
}
}
// Otherwise fall through and handle Enter in the next match case.
if key.code == KeyCode::Enter
&& let Ok(selected) = self.selected()
&& Path::new(&selected).is_file()
{
// Open a tab if the selected item is a file.
return Ok(Action::OpenTab);
}
let changed = match key.code {
+3 -4
View File
@@ -131,7 +131,7 @@ impl MenuBar {
opened: MenuBarItem,
) {
let popup_area = Self::rect_under_option(title_bar_anchor, area, 27, 10);
Clear::default().render(popup_area, buf);
Clear.render(popup_area, buf);
let options = opened.options().iter().map(|i| ListItem::new(i.id()));
StatefulWidget::render(
List::new(options)
@@ -150,15 +150,14 @@ impl MenuBar {
}
fn rect_under_option(anchor: Rect, area: Rect, width: u16, height: u16) -> Rect {
let rect = Rect {
Rect {
x: anchor.x,
y: anchor.y + anchor.height,
width: width.min(area.width),
height,
};
}
// TODO: X offset for item option? It's fine as-is, but it might look nicer.
// trace!(target:Self::ID, "Building Rect under MenuBar popup {}", rect);
rect
}
}