diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index 30e992d4..94ca5e3c 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -13,6 +13,10 @@ on: jobs: build-and-deploy: runs-on: ubuntu-latest + environment: linutil_env + permissions: + contents: write + pull-requests: write steps: - name: Checkout Repository @@ -24,11 +28,17 @@ jobs: run: | echo -e "\n\n$(cat .github/CONTRIBUTING.md)" > 'docs/contributing.md' - - uses: stefanzweifel/git-auto-commit-action@v5 + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 with: - commit_message: Commit Contributing Guidelines - file_pattern: "docs/contributing.md" - add_options: '--force' + commit-message: Update Contributing Guidelines + title: 'docs: Update Contributing Guidelines' + body: 'Automated update of Contributing Guidelines from .github/CONTRIBUTING.md' + branch: update-contributing-guidelines + delete-branch: true + base: main + labels: documentation + token: ${{ secrets.PAT_TOKEN }} if: success() - name: Setup Python diff --git a/.github/workflows/linutil.yml b/.github/workflows/linutil.yml index 1184549b..187f1e54 100644 --- a/.github/workflows/linutil.yml +++ b/.github/workflows/linutil.yml @@ -78,11 +78,3 @@ jobs: env: version: ${{ env.version }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: linutil-artifact - path: build/x86_64-unknown-linux-musl/release/linutil - compression-level: 0 - overwrite: true \ No newline at end of file diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 82bd17f1..4967eeef 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -3,8 +3,8 @@ name: LinUtil Preview on: workflow_dispatch: inputs: - run_id: - description: 'Run ID of LinUtil Release' + tag_name: + description: 'Tag name' required: true workflow_run: workflows: ["LinUtil Release"] @@ -14,29 +14,47 @@ on: jobs: generate_preview: runs-on: ubuntu-latest + environment: linutil_env + permissions: + contents: write + pull-requests: write steps: - - uses: actions/checkout@v4 + - name: Checkout source + uses: actions/checkout@v4 - - name: Set Run ID - run: | - if [ "${{ github.event_name }}" == "workflow_run" ]; then - echo "run_id=${{ github.event.workflow_run.id }}" >> $GITHUB_ENV - else - echo "run_id=${{ github.event.inputs.run_id }}" >> $GITHUB_ENV - fi - - - name: Download build artifacts - uses: actions/download-artifact@v4 + - name: Get tag name ( Workflow Run ) + id: latest_tag + uses: actions/github-script@v7 + if: github.event_name == 'workflow_run' with: - name: linutil-artifact - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ env.run_id }} + script: | + const releases = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 1 + }); + core.setOutput('result', releases.data[0].tag_name); + result-encoding: string + + - name: Set tag name ( Workflow Run ) + if: github.event_name == 'workflow_run' + run: echo "tag_name=${{ steps.latest_tag.outputs.result }}" >> $GITHUB_ENV + + - name: Set tag name ( Workflow Dispatch ) + if: ${{ github.event_name }} == 'workflow_dispatch' + run: echo "tag_name=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV + + - name: Download binary + run: | + curl -LO "https://github.com/${{ github.repository }}/releases/download/${{ env.tag_name }}/linutil" - name: Set env run: | chmod +x linutil - echo "${{ github.workspace }}" >> $GITHUB_PATH + mkdir -p build + mv linutil build/linutil + echo "${{ github.workspace }}/build" >> $GITHUB_PATH - name: Generate preview uses: charmbracelet/vhs-action@v2.1.0 @@ -46,10 +64,15 @@ jobs: - name: Move preview run: mv preview.gif docs/assets/preview.gif - - name: Upload preview - uses: stefanzweifel/git-auto-commit-action@v5 + - name: Create PR + uses: peter-evans/create-pull-request@v7.0.5 with: - commit_message: Preview for ${{ env.run_id }} - file_pattern: "docs/assets/preview.gif" - add_options: "--force" + commit-message: Preview for ${{ env.tag_name }} + file-pattern: "docs/assets/preview.gif" + add-options: "--force" + token: ${{ secrets.PAT_TOKEN }} + branch: feature/preview-${{ env.tag_name }} + title: "Update preview for ${{ env.tag_name }}" + body: | + Automated PR to update preview gif for version ${{ env.tag_name }} if: success() diff --git a/core/tabs/applications-setup/Developer-tools/jetbrains-toolbox.sh b/core/tabs/applications-setup/Developer-tools/jetbrains-toolbox.sh new file mode 100644 index 00000000..79ce360e --- /dev/null +++ b/core/tabs/applications-setup/Developer-tools/jetbrains-toolbox.sh @@ -0,0 +1,48 @@ +#!/bin/sh -e + +. ../../common-script.sh + +manualInstall() { + JETBRAINS_TOOLBOX_DIR="/opt/jetbrains-toolbox" + + case "$ARCH" in + x86_64) ARCHIVE_URL=$(curl -s "https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release" | jq -r ".TBA[0].downloads.linux.link") ;; + aarch64) ARCHIVE_URL=$(curl -s "https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release" | jq -r ".TBA[0].downloads.linuxARM64.link") ;; + esac + + curl -fSL "$ARCHIVE_URL" -o "jetbrains-toolbox.tar.gz" + + if [ -d "$JETBRAINS_TOOLBOX_DIR" ]; then + "$ESCALATION_TOOL" rm -rf "$JETBRAINS_TOOLBOX_DIR" + fi + + "$ESCALATION_TOOL" mkdir -p "$JETBRAINS_TOOLBOX_DIR" + "$ESCALATION_TOOL" tar -xzf "jetbrains-toolbox.tar.gz" -C "$JETBRAINS_TOOLBOX_DIR" --strip-components=1 + "$ESCALATION_TOOL" ln -sf "$JETBRAINS_TOOLBOX_DIR/jetbrains-toolbox" "/usr/bin/jetbrains-toolbox" +} + +installJetBrainsToolBox() { + if ! command_exists jetbrains-toolbox; then + printf "%b\n" "${YELLOW}Installing Jetbrains Toolbox...${RC}" + case "$PACKAGER" in + pacman) + "$AUR_HELPER" -S --needed --noconfirm jetbrains-toolbox + ;; + dnf) + manualInstall + ;; + *) + "$ESCALATION_TOOL" "$PACKAGER" install -y libfuse2 + manualInstall + ;; + esac + printf "%b\n" "${GREEN}Successfully installed Jetbrains Toolbox.${RC}" + else + printf "%b\n" "${GREEN}Jetbrains toolbox is already installed.${RC}" + fi +} + +checkEnv +checkEscalationTool +checkAURHelper +installJetBrainsToolBox diff --git a/core/tabs/applications-setup/tab_data.toml b/core/tabs/applications-setup/tab_data.toml index c102a5ce..44b74464 100644 --- a/core/tabs/applications-setup/tab_data.toml +++ b/core/tabs/applications-setup/tab_data.toml @@ -54,6 +54,12 @@ description = "GitHub Desktop is a user-friendly application that simplifies the script = "Developer-tools/githubdesktop-setup.sh" task_list = "I" +[[data.entries]] +name = "JetBrains Toolbox" +description = "JetBrains Toolbox is a collection of tools and an app that help developers work with JetBrains products." +script = "Developer-tools/jetbrains-toolbox.sh" +task_list = "I" + [[data.entries]] name = "Meld" description = "Meld is a visual diff and merge tool that helps compare files, directories, and version-controlled projects." diff --git a/core/tabs/common-script.sh b/core/tabs/common-script.sh index 5200e227..d2444f3c 100644 --- a/core/tabs/common-script.sh +++ b/core/tabs/common-script.sh @@ -51,6 +51,16 @@ elevated_execution() { fi } +checkArch() { + case "$(uname -m)" in + x86_64 | amd64) ARCH="x86_64" ;; + aarch64 | arm64) ARCH="aarch64" ;; + *) printf "%b\n" "${RED}Unsupported architecture: $(uname -m)${RC}" && exit 1 ;; + esac + + printf "%b\n" "${CYAN}System architecture: ${ARCH}${RC}" +} + checkAURHelper() { ## Check & Install AUR helper if [ "$PACKAGER" = "pacman" ]; then @@ -169,6 +179,7 @@ checkDistro() { } checkEnv() { + checkArch checkEscalationTool checkCommandRequirements "curl groups $ESCALATION_TOOL" checkPackageManager 'nala apt-get dnf pacman zypper' diff --git a/docs/userguide.md b/docs/userguide.md index 46a3f99b..8ad704e6 100644 --- a/docs/userguide.md +++ b/docs/userguide.md @@ -17,6 +17,7 @@ ### Developer Tools - **Github Desktop**: GitHub Desktop is a user-friendly application that simplifies the process of managing Git repositories and interacting with GitHub, providing a graphical interface for tasks like committing, branching, and syncing changes. +- **JetBrains Toolbox**: JetBrains Toolbox is a collection of tools and an app that help developers work with JetBrains products. - **Meld**: Meld is a visual diff and merge tool that helps compare files, directories, and version-controlled projects. - **Neovim**: Neovim is a refactor, and sometimes redactor, in the tradition of Vim. It is not a rewrite but a continuation and extension of Vim. diff --git a/tui/src/filter.rs b/tui/src/filter.rs index cd6063fc..898fee74 100644 --- a/tui/src/filter.rs +++ b/tui/src/filter.rs @@ -4,7 +4,7 @@ use ego_tree::NodeId; use linutil_core::Tab; use ratatui::{ layout::{Position, Rect}, - style::Style, + style::{Color, Style}, text::Span, widgets::{Block, Borders, Paragraph}, Frame, @@ -22,6 +22,7 @@ pub struct Filter { in_search_mode: bool, input_position: usize, items: Vec, + completion_preview: Option, } impl Filter { @@ -31,17 +32,23 @@ impl Filter { in_search_mode: false, input_position: 0, items: vec![], + completion_preview: None, } } + pub fn item_list(&self) -> &[ListEntry] { &self.items } + pub fn activate_search(&mut self) { self.in_search_mode = true; } + pub fn deactivate_search(&mut self) { self.in_search_mode = false; + self.completion_preview = None; } + pub fn update_items(&mut self, tabs: &[Tab], current_tab: usize, node: NodeId) { if self.search_input.is_empty() { let curr = tabs[current_tab].tree.get(node).unwrap(); @@ -78,13 +85,34 @@ impl Filter { } self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name)); } + + self.update_completion_preview(); } + + fn update_completion_preview(&mut self) { + if self.search_input.is_empty() { + self.completion_preview = None; + return; + } + + let input = self.search_input.iter().collect::().to_lowercase(); + self.completion_preview = self.items.iter().find_map(|item| { + let item_name_lower = item.node.name.to_lowercase(); + if item_name_lower.starts_with(&input) { + Some(item_name_lower[input.len()..].to_string()) + } else { + None + } + }); + } + pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) { //Set the search bar text (If empty use the placeholder) let display_text = if !self.in_search_mode && self.search_input.is_empty() { Span::raw("Press / to search") } else { - Span::raw(self.search_input.iter().collect::()) + let input_text = self.search_input.iter().collect::(); + Span::styled(input_text, Style::default().fg(theme.focused_color())) }; let search_color = if self.in_search_mode { @@ -110,11 +138,22 @@ impl Filter { let x = area.x + cursor_position as u16 + 1; let y = area.y + 1; frame.set_cursor_position(Position::new(x, y)); + + if let Some(preview) = &self.completion_preview { + let preview_span = Span::styled(preview, Style::default().fg(Color::DarkGray)); + let preview_paragraph = Paragraph::new(preview_span).style(Style::default()); + let preview_area = Rect::new( + x, + y, + (preview.len() as u16).min(area.width - cursor_position as u16 - 1), + 1, + ); + frame.render_widget(preview_paragraph, preview_area); + } } } // Handles key events. Returns true if search must be exited pub fn handle_key(&mut self, event: &KeyEvent) -> SearchAction { - //Insert user input into the search bar match event.code { KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => { return self.exit_search() @@ -124,10 +163,17 @@ impl Filter { KeyCode::Delete => self.remove_next(), KeyCode::Left => return self.cursor_left(), KeyCode::Right => return self.cursor_right(), + KeyCode::Tab => return self.complete_search(), + KeyCode::Esc => { + self.input_position = 0; + self.search_input.clear(); + self.completion_preview = None; + return SearchAction::Exit; + } KeyCode::Enter => return SearchAction::Exit, - KeyCode::Esc => return self.exit_search(), _ => return SearchAction::None, }; + self.update_completion_preview(); SearchAction::Update } @@ -141,16 +187,19 @@ impl Filter { self.input_position = self.input_position.saturating_sub(1); SearchAction::None } + fn cursor_right(&mut self) -> SearchAction { if self.input_position < self.search_input.len() { self.input_position += 1; } SearchAction::None } + fn insert_char(&mut self, input: char) { self.search_input.insert(self.input_position, input); self.cursor_right(); } + fn remove_previous(&mut self) { let current = self.input_position; if current > 0 { @@ -158,12 +207,25 @@ impl Filter { self.cursor_left(); } } + fn remove_next(&mut self) { let current = self.input_position; if current < self.search_input.len() { self.search_input.remove(current); } } + + fn complete_search(&mut self) -> SearchAction { + if let Some(completion) = self.completion_preview.take() { + self.search_input.extend(completion.chars()); + self.input_position = self.search_input.len(); + self.update_completion_preview(); + SearchAction::Update + } else { + SearchAction::None + } + } + pub fn clear_search(&mut self) { self.search_input.clear(); self.input_position = 0;