diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d8ba596566..64eb98dc30 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,13 +1,2 @@ -blank_issues_enabled: false - -contact_links: - - name: Questions or discussions about Midnight Commander - url: https://github.com/MidnightCommander/mc/discussions - about: Please ask for support and answer questions here - - - name: List for Midnight Commander users - url: https://lists.midnight-commander.org/mailman/listinfo/mc - about: If you prefer mailing lists, post your questions to the users list - - name: List for Midnight Commander developers - url: https://lists.midnight-commander.org/mailman/listinfo/mc-devel - about: If you prefer mailing lists, post development-related messages to the developers list +blank_issues_enabled: true +contact_links: [] diff --git a/.github/workflows/issue-label-approve.yml b/.github/workflows/issue-label-approve.yml index ea5b4388eb..419c31a07f 100644 --- a/.github/workflows/issue-label-approve.yml +++ b/.github/workflows/issue-label-approve.yml @@ -24,7 +24,7 @@ jobs: - run: | gh issue edit \ ${{ inputs.issue_number != '' && inputs.issue_number || github.event.issue.number }} \ - --remove-label "state: in review" + --remove-label "state: in review" || true gh issue comment \ ${{ inputs.issue_number != '' && inputs.issue_number || github.event.issue.number }} \ diff --git a/.github/workflows/issue-label-review.yml b/.github/workflows/issue-label-review.yml index 2308e1d971..cf04f4189e 100644 --- a/.github/workflows/issue-label-review.yml +++ b/.github/workflows/issue-label-review.yml @@ -24,7 +24,7 @@ jobs: - run: | gh issue edit \ ${{ inputs.issue_number != '' && inputs.issue_number || github.event.issue.number }} \ - --remove-label "state: approved" + --remove-label "state: approved" || true gh issue comment \ ${{ inputs.issue_number != '' && inputs.issue_number || github.event.issue.number }} \ diff --git a/.github/workflows/pr-label-approve.yml b/.github/workflows/pr-label-approve.yml index a178eb45fa..b4207c21a6 100644 --- a/.github/workflows/pr-label-approve.yml +++ b/.github/workflows/pr-label-approve.yml @@ -31,7 +31,7 @@ jobs: gh pr edit \ ${{ inputs.issue_number != '' && inputs.issue_number || github.event.pull_request.number }} \ - --remove-label "state: in review" + --remove-label "state: in review" || true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} diff --git a/.github/workflows/pr-summary.yml b/.github/workflows/pr-summary.yml new file mode 100644 index 0000000000..70f442d299 --- /dev/null +++ b/.github/workflows/pr-summary.yml @@ -0,0 +1,111 @@ +name: PR summary from commits + +on: + pull_request: + types: [opened, reopened, synchronize, edited] + +permissions: + contents: read + pull-requests: write + +jobs: + summarize: + runs-on: ubuntu-latest + steps: + - name: Update PR body with commit summary + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + + // For PRs from forks, GITHUB_TOKEN may be read-only (depends on repo settings). + // We'll try; if forbidden, the action will fail. + const prNumber = pr.number; + + const markerStart = ""; + const markerEnd = ""; + + // 1) Collect commits + const commits = await github.paginate(github.rest.pulls.listCommits, { + owner, + repo, + pull_number: prNumber, + per_page: 100 + }); + + // 2) Collect changed files (limited to 300 for readability) + const files = await github.paginate(github.rest.pulls.listFiles, { + owner, + repo, + pull_number: prNumber, + per_page: 100 + }); + + const commitLines = commits.map(c => { + const msg = (c.commit?.message || "").split("\n")[0].trim(); + const sha = (c.sha || "").slice(0, 7); + return `- ${msg} (\`${sha}\`)`; + }); + + // Group files by status + const limit = 300; + const trimmedFiles = files.slice(0, limit); + + const byStatus = new Map(); + for (const f of trimmedFiles) { + const st = f.status || "modified"; + if (!byStatus.has(st)) byStatus.set(st, []); + byStatus.get(st).push(f.filename); + } + + const statusOrder = ["added", "modified", "removed", "renamed", "changed"]; + const fileSectionParts = []; + for (const st of statusOrder) { + if (!byStatus.has(st)) continue; + const names = byStatus.get(st); + const shown = names.slice(0, 80); + const extra = names.length - shown.length; + const list = shown.map(n => ` - \`${n}\``).join("\n"); + fileSectionParts.push(`**${st}** (${names.length})\n${list}${extra > 0 ? `\n - ... (+${extra} more)` : ""}`); + } + + const commitCount = commits.length; + const fileCount = files.length; + + const summaryBlock = + `${markerStart}\n` + + `## Auto summary\n\n` + + `**Commits:** ${commitCount} \n` + + `**Files changed:** ${fileCount}\n\n` + + `### Commit messages\n` + + `${commitLines.length ? commitLines.join("\n") : "_No commits found_"}\n\n` + + `### Changed files\n` + + `${fileSectionParts.length ? fileSectionParts.join("\n\n") : "_No files found_"}\n\n` + + `_This block is auto-generated. Edit outside this section._\n` + + `${markerEnd}\n`; + + const currentBody = pr.body || ""; + + const hasMarkers = currentBody.includes(markerStart) && currentBody.includes(markerEnd); + + let newBody; + if (hasMarkers) { + const startIdx = currentBody.indexOf(markerStart); + const endIdx = currentBody.indexOf(markerEnd) + markerEnd.length; + newBody = currentBody.slice(0, startIdx).trimEnd() + "\n\n" + summaryBlock + "\n" + currentBody.slice(endIdx).trimStart(); + } else { + // Prepend summary + newBody = summaryBlock + "\n" + currentBody; + } + + // Avoid unnecessary updates + if (newBody !== currentBody) { + await github.rest.pulls.update({ + owner, + repo, + pull_number: prNumber, + body: newBody + }); + } diff --git a/.github/workflows/process-closed-issues.yml b/.github/workflows/process-closed-issues.yml index e7f8ae818e..f8bcd5c57c 100644 --- a/.github/workflows/process-closed-issues.yml +++ b/.github/workflows/process-closed-issues.yml @@ -22,7 +22,7 @@ jobs: - run: | ISSUE=${{ inputs.issue_number != '' && inputs.issue_number || github.event.issue.number }} - gh issue edit $ISSUE --remove-label "state: in review,state: approved" + gh issue edit $ISSUE --remove-label "state: in review,state: approved" || true if [ "${{ github.event.issue.state_reason }}" = "not_planned" ]; then gh issue edit $ISSUE --remove-milestone diff --git a/.github/workflows/process-closed-prs.yml b/.github/workflows/process-closed-prs.yml index 5a93802bf9..c312613974 100644 --- a/.github/workflows/process-closed-prs.yml +++ b/.github/workflows/process-closed-prs.yml @@ -24,7 +24,7 @@ jobs: steps: - run: | gh pr edit ${{ inputs.pr_number != '' && inputs.pr_number || github.event.pull_request.number }} \ - --remove-label "state: in review,state: approved" + --remove-label "state: in review,state: approved" || true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} diff --git a/configure.ac b/configure.ac index 5bdc626a87..6d7bdb4b8f 100644 --- a/configure.ac +++ b/configure.ac @@ -470,6 +470,20 @@ mc_BACKGROUND mc_EXT2FS_ATTR mc_VFS_CHECKS +AC_ARG_WITH([vfs-plugins-dir], + AS_HELP_STRING([--with-vfs-plugins-dir=DIR], + [Directory for dynamic VFS plugins [LIBDIR/mc/vfs-plugins]]), + [vfs_plugins_dir="$withval"], + [vfs_plugins_dir='${libdir}/mc/vfs-plugins']) +AC_SUBST([vfs_plugins_dir]) + +AC_ARG_WITH([panel-plugins-dir], + AS_HELP_STRING([--with-panel-plugins-dir=DIR], + [Directory for dynamic panel plugins [LIBDIR/mc/panel-plugins]]), + [panel_plugins_dir="$withval"], + [panel_plugins_dir='${libdir}/mc/panel-plugins']) +AC_SUBST([panel_plugins_dir]) + dnl ############################################################################ dnl Directories dnl ############################################################################ @@ -543,9 +557,8 @@ AM_CONDITIONAL(USE_INTERNAL_EDIT, [test x"$use_internal_edit" = xyes ]) AM_CONDITIONAL(USE_ASPELL, [test x"$enable_aspell" = xyes ]) AM_CONDITIONAL(USE_DIFF, [test -n "$use_diff"]) AM_CONDITIONAL(CONS_SAVER, [test -n "$cons_saver"]) -dnl Clarify do we really need GModule -AM_CONDITIONAL([HAVE_GMODULE], [test -n "$g_module_supported" && \ - test x"$textmode_x11_support" = x"yes" -o x"$enable_aspell" = x"yes"]) +dnl Enable GModule when available (used for X11, aspell, and dynamic VFS plugins) +AM_CONDITIONAL([HAVE_GMODULE], [test -n "$g_module_supported"]) AC_ARG_ENABLE([configure-args], AS_HELP_STRING([--enable-configure-args], [Embed ./configure arguments into binaries])) @@ -588,6 +601,12 @@ src/viewer/Makefile src/diffviewer/Makefile src/filemanager/Makefile +src/panel-plugins/Makefile +src/panel-plugins/hello/Makefile +src/panel-plugins/systemd/Makefile +src/panel-plugins/sftp/Makefile +src/panel-plugins/docker/Makefile + src/vfs/Makefile src/vfs/cpio/Makefile diff --git a/doc/INSTALL b/doc/INSTALL index 03aafee477..fdaa68bd84 100644 --- a/doc/INSTALL +++ b/doc/INSTALL @@ -176,7 +176,11 @@ VFS options: `--enable-vfs-sftp' (auto) - Support for SFTP vfs + Support for SFTP vfs. When enabled, SFTP is built as a dynamic + plugin (mc-vfs-sftp.so) installed into the VFS plugins directory + (see --with-vfs-plugins-dir). Requires libssh2 >= 1.2.8. + The main mc binary does not link against libssh2; the plugin + carries that dependency itself. `--enable-vfs-extfs' (on by default) @@ -186,6 +190,16 @@ VFS options: (on by default) Support for sfs +`--with-vfs-plugins-dir=DIR' + Set the directory where Midnight Commander looks for dynamic VFS + plugins at runtime [default=LIBDIR/mc/vfs-plugins]. A dynamic VFS + plugin is a shared object (.so) that exports the mc_vfs_plugin_init + symbol. When MC starts, it scans this directory and loads every + matching plugin, allowing third-party VFS modules (e.g. SMB, WebDAV) + to be added without recompiling MC. GModule (part of GLib) is + required for this feature; if unavailable, dynamic loading is + silently skipped. + Screen library: - - - - - - - - diff --git a/doc/PLUGINS b/doc/PLUGINS new file mode 100644 index 0000000000..d8e3a6b548 --- /dev/null +++ b/doc/PLUGINS @@ -0,0 +1,121 @@ +Dynamic VFS plugins for GNU Midnight Commander +=============================================== + +Since version 4.8.33 Midnight Commander can load VFS modules at runtime +from shared objects (.so). This lets distributions package optional VFS +modules (and their external dependencies) separately from the core mc +binary. + +How it works +------------ + +At startup, after initialising the built-in (static) VFS modules, +mc scans the directory specified by --with-vfs-plugins-dir (default: +${libdir}/mc/vfs-plugins). Every .so file found there is opened with +GModule (dlopen) and probed for the symbol: + + void mc_vfs_plugin_init (void); + +If the symbol is found it is called immediately. The plugin is expected +to register its VFS class via vfs_init_subclass() / vfs_register_class() +-- exactly the same way a statically linked module does. + +The mc binary is linked with -export-dynamic so that plugin code can +call back into mc (vfs_init_subclass, tcp_init, query_dialog, etc.) +without linking those symbols explicitly. + +GModule support (part of GLib) is required. If it is unavailable at +build time, dynamic loading is silently skipped. + + +Current dynamic plugins +----------------------- + + mc-vfs-sftp.so SFTP filesystem (requires libssh2 >= 1.2.8) + + +Candidates for future conversion +--------------------------------- + +The following modules are currently compiled as static libraries and +linked into libmc-vfs.la. They are listed in order of conversion +difficulty. + +Phase 1 -- no code changes needed (zero external symbol references): + + tar tar archives no external deps + cpio cpio archives no external deps + extfs extension filesystem no external deps (uses helper scripts) + sfs single-file fs no external deps + + These modules are fully self-contained: their init functions are only + called from src/vfs/plugins_init.c and no code outside src/vfs/ + references their symbols. Conversion is mechanical: rename the init + function to mc_vfs_plugin_init, switch Makefile.am from noinst to + vfsplugin, and remove the static init call. + +Phase 2 -- minimal refactoring (one exported global variable): + + shell SHELL/SSH filesystem no external deps + + src/setup.c reads/writes shell_directory_timeout. A getter/setter + pair or an mc_config-based approach would decouple it. + +Phase 3 -- moderate refactoring (multiple exported globals): + + ftpfs FTP filesystem no external deps + + src/setup.c and src/filemanager/boxes.c directly access ~11 global + variables (ftpfs_use_netrc, ftpfs_proxy_host, ftpfs_anonymous_passwd, + ftpfs_use_passive_connections, etc.) for the configuration dialog and + settings persistence. These would need to be exposed through a + callback/getter API or moved to mc_config key lookups. + +Not convertible: + + local local filesystem core module, must be first + + Other VFS modules depend on local_close(), local_read(), etc. + It must be initialised before any other VFS class. + + +Writing a new plugin +-------------------- + +1. Create a directory under src/vfs/ (e.g. src/vfs/myfs/). + +2. Implement your VFS class. Use src/vfs/sftpfs/ as a reference. + The key function is: + + void + mc_vfs_plugin_init (void) + { + tcp_init (); /* if network-based */ + vfs_init_subclass (&myfs_subclass, "myfs", flags, "myfs"); + /* assign callbacks to vfs_myfs_ops->open, ->read, ... */ + vfs_register_class (vfs_myfs_ops); + } + +3. In Makefile.am: + + vfsplugindir = $(vfs_plugins_dir) + vfsplugin_LTLIBRARIES = mc-vfs-myfs.la + + mc_vfs_myfs_la_SOURCES = ... + mc_vfs_myfs_la_LDFLAGS = -module -avoid-version + mc_vfs_myfs_la_LIBADD = $(GLIB_LIBS) $(MY_EXTERNAL_LIBS) + + Do NOT add -no-undefined -- the plugin resolves mc symbols at + runtime via -export-dynamic on the mc binary. + +4. If the plugin has an external library dependency, add a PKG_CHECK + in an m4 file but do NOT append to MCLIBS -- the plugin links the + library itself. + +5. Add SUBDIRS += myfs to src/vfs/Makefile.am (guarded by your + AM_CONDITIONAL), but do NOT add to libmc_vfs_la_LIBADD. + +6. Install the resulting .so into $(vfs_plugins_dir) -- this happens + automatically via vfsplugin_LTLIBRARIES. + +7. Test: run mc and verify your prefix is accessible (e.g. cd myfs://). diff --git a/lib/Makefile.am b/lib/Makefile.am index f055b87b76..d69c361652 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -43,6 +43,8 @@ libmc_la_SOURCES = \ global.c global.h \ keybind.c keybind.h \ lock.c lock.h \ + panel-plugin.c panel-plugin.h \ + panel-plugin-loader.c \ serialize.c serialize.h \ shell.c shell.h \ stat-size.h \ @@ -57,7 +59,8 @@ libmc_la_SOURCES += charsets.c charsets.h EXTRA_DIST = \ stdckdint.in.h -AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) +AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) \ + -DMC_PANEL_PLUGINS_DIR=\""$(panel_plugins_dir)"\" libmc_la_LIBADD = \ event/libmcevent.la \ diff --git a/lib/keybind.c b/lib/keybind.c index 3a4550eda8..a9c1432a0a 100644 --- a/lib/keybind.c +++ b/lib/keybind.c @@ -231,6 +231,7 @@ static name_keymap_t command_names[] = { ADD_KEYMAP_NAME (CdParent), ADD_KEYMAP_NAME (CdChild), ADD_KEYMAP_NAME (Panelize), + ADD_KEYMAP_NAME (PanelPlugin), ADD_KEYMAP_NAME (PanelOtherSync), ADD_KEYMAP_NAME (SortNext), ADD_KEYMAP_NAME (SortPrev), @@ -305,6 +306,8 @@ static name_keymap_t command_names[] = { ADD_KEYMAP_NAME (BookmarkFlush), ADD_KEYMAP_NAME (BookmarkNext), ADD_KEYMAP_NAME (BookmarkPrev), + ADD_KEYMAP_NAME (FoldToggle), + ADD_KEYMAP_NAME (UnfoldAll), ADD_KEYMAP_NAME (MarkPageUp), ADD_KEYMAP_NAME (MarkPageDown), ADD_KEYMAP_NAME (MarkToFileBegin), diff --git a/lib/keybind.h b/lib/keybind.h index 28be23559e..607a457cdd 100644 --- a/lib/keybind.h +++ b/lib/keybind.h @@ -190,6 +190,7 @@ enum CK_PanelOtherCd = 200L, CK_PanelOtherCdLink, CK_Panelize, + CK_PanelPlugin, CK_CopySingle, CK_MoveSingle, CK_DeleteSingle, @@ -255,6 +256,9 @@ enum CK_BookmarkFlush, CK_BookmarkNext, CK_BookmarkPrev, + // code folding + CK_FoldToggle, + CK_UnfoldAll, // mark commands CK_MarkColumn, CK_MarkWord, diff --git a/lib/panel-plugin-loader.c b/lib/panel-plugin-loader.c new file mode 100644 index 0000000000..e3f0d3c066 --- /dev/null +++ b/lib/panel-plugin-loader.c @@ -0,0 +1,134 @@ +/* + Dynamic panel plugin loader. + + Copyright (C) 2025 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +/** \file panel-plugin-loader.c + * \brief Source: dynamic panel plugin loader + * + * Scans MC_PANEL_PLUGINS_DIR for shared objects exporting + * mc_panel_plugin_register(), loads them, and registers the returned + * mc_panel_plugin_t descriptor via mc_panel_plugin_add(). + */ + +#include + +#include + +#include "lib/global.h" +#include "lib/panel-plugin.h" + +#ifdef HAVE_GMODULE + +#include + +/*** global variables ****************************************************************************/ + +/*** file scope macro definitions ****************************************************************/ + +/*** file scope type declarations ****************************************************************/ + +/*** file scope variables ************************************************************************/ + +static GPtrArray *panel_plugin_modules = NULL; + +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +void +mc_panel_plugins_load (void) +{ + GDir *dir; + const gchar *filename; + gchar *plugins_dir; + + plugins_dir = g_build_filename (MC_PANEL_PLUGINS_DIR, (char *) NULL); + dir = g_dir_open (plugins_dir, 0, NULL); + if (dir == NULL) + { + g_free (plugins_dir); + return; // no plugins dir — OK, nothing to load + } + + panel_plugin_modules = g_ptr_array_new (); + + while ((filename = g_dir_read_name (dir)) != NULL) + { + GModule *module; + mc_panel_plugin_register_fn register_fn; + const mc_panel_plugin_t *plugin; + gchar *path; + + if (!g_str_has_suffix (filename, "." G_MODULE_SUFFIX)) + continue; + + path = g_build_filename (plugins_dir, filename, (char *) NULL); + module = g_module_open (path, G_MODULE_BIND_LAZY); + if (module == NULL) + { + fprintf (stderr, "Panel plugin %s: %s\n", filename, g_module_error ()); + g_free (path); + continue; + } + + if (!g_module_symbol (module, MC_PANEL_PLUGIN_ENTRY, (gpointer *) ®ister_fn)) + { + fprintf (stderr, "Panel plugin %s: symbol %s not found\n", filename, + MC_PANEL_PLUGIN_ENTRY); + g_module_close (module); + g_free (path); + continue; + } + + plugin = register_fn (); + if (plugin == NULL || !mc_panel_plugin_add (plugin)) + { + fprintf (stderr, "Panel plugin %s: registration failed\n", filename); + g_module_close (module); + g_free (path); + continue; + } + + g_module_make_resident (module); // prevent unload — plugin descriptor lives in .so + g_ptr_array_add (panel_plugin_modules, module); + g_free (path); + } + + g_dir_close (dir); + g_free (plugins_dir); +} + +/* --------------------------------------------------------------------------------------------- */ + +#else /* !HAVE_GMODULE */ + +void +mc_panel_plugins_load (void) +{ + // GModule not available — dynamic panel plugins disabled +} + +/* --------------------------------------------------------------------------------------------- */ + +#endif /* HAVE_GMODULE */ diff --git a/lib/panel-plugin.c b/lib/panel-plugin.c new file mode 100644 index 0000000000..645b50736d --- /dev/null +++ b/lib/panel-plugin.c @@ -0,0 +1,148 @@ +/* + Panel plugin registry. + + Copyright (C) 2025 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +/** \file panel-plugin.c + * \brief Source: panel plugin registry + * + * Maintains a list of registered mc_panel_plugin_t descriptors. + * Plugins are registered via mc_panel_plugin_add() — typically called + * from the dynamic loader (panel-plugin-loader.c). + */ + +#include + +#include +#include + +#include "lib/global.h" +#include "lib/panel-plugin.h" + +/*** global variables ****************************************************************************/ + +/*** file scope macro definitions ****************************************************************/ + +/*** file scope type declarations ****************************************************************/ + +/*** file scope variables ************************************************************************/ + +static GSList *panel_plugin_registry = NULL; + +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +gboolean +mc_panel_plugin_add (const mc_panel_plugin_t *plugin) +{ + if (plugin == NULL) + return FALSE; + + if (plugin->api_version != MC_PANEL_PLUGIN_API_VERSION) + { + fprintf (stderr, "Panel plugin \"%s\": API version %d, expected %d\n", + plugin->name != NULL ? plugin->name : "(null)", plugin->api_version, + MC_PANEL_PLUGIN_API_VERSION); + return FALSE; + } + + if (plugin->name == NULL || plugin->open == NULL || plugin->close == NULL + || plugin->get_items == NULL) + { + fprintf (stderr, "Panel plugin \"%s\": missing required callbacks\n", + plugin->name != NULL ? plugin->name : "(null)"); + return FALSE; + } + + // check for duplicate name + if (mc_panel_plugin_find_by_name (plugin->name) != NULL) + { + fprintf (stderr, "Panel plugin \"%s\": already registered\n", plugin->name); + return FALSE; + } + + panel_plugin_registry = g_slist_append (panel_plugin_registry, (gpointer) plugin); + return TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ + +const GSList * +mc_panel_plugin_list (void) +{ + return panel_plugin_registry; +} + +/* --------------------------------------------------------------------------------------------- */ + +const mc_panel_plugin_t * +mc_panel_plugin_find_by_name (const char *name) +{ + const GSList *iter; + + if (name == NULL) + return NULL; + + for (iter = panel_plugin_registry; iter != NULL; iter = g_slist_next (iter)) + { + const mc_panel_plugin_t *p = (const mc_panel_plugin_t *) iter->data; + + if (strcmp (p->name, name) == 0) + return p; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +const mc_panel_plugin_t * +mc_panel_plugin_find_by_prefix (const char *prefix) +{ + const GSList *iter; + + if (prefix == NULL) + return NULL; + + for (iter = panel_plugin_registry; iter != NULL; iter = g_slist_next (iter)) + { + const mc_panel_plugin_t *p = (const mc_panel_plugin_t *) iter->data; + + if (p->prefix != NULL && strcmp (p->prefix, prefix) == 0) + return p; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +void +mc_panel_plugins_shutdown (void) +{ + g_slist_free (panel_plugin_registry); + panel_plugin_registry = NULL; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/lib/panel-plugin.h b/lib/panel-plugin.h new file mode 100644 index 0000000000..f5145082c9 --- /dev/null +++ b/lib/panel-plugin.h @@ -0,0 +1,99 @@ +/** \file panel-plugin.h + * \brief Header: panel plugin API for third-party panel content providers + */ + +#ifndef MC__PANEL_PLUGIN_H +#define MC__PANEL_PLUGIN_H + +#include "lib/global.h" + +/* + * Note: get_items callback receives a dir_list* (from src/filemanager/dir.h) + * cast to void*. Plugin implementations should include dir.h and cast back. + */ + +/*** typedefs(not structures) and defined constants **********************************************/ + +#define MC_PANEL_PLUGIN_API_VERSION 1 +#define MC_PANEL_PLUGIN_ENTRY "mc_panel_plugin_register" + +/*** enums ***************************************************************************************/ + +typedef enum +{ + MC_PPR_OK = 0, + MC_PPR_FAILED = -1, + MC_PPR_NOT_SUPPORTED = -2 +} mc_pp_result_t; + +typedef enum +{ + MC_PPF_NONE = 0, + MC_PPF_NAVIGATE = 1 << 0, /* handles chdir/".." */ + MC_PPF_GET_FILES = 1 << 1, /* can extract files */ + MC_PPF_DELETE = 1 << 2, /* can delete items */ + MC_PPF_CUSTOM_TITLE = 1 << 3, + MC_PPF_CREATE = 1 << 4 /* supports Shift+F4 (create item) */ +} mc_pp_flags_t; + +/*** structures declarations (and typedefs of structures)*****************************************/ + +/* What mc provides to the plugin */ +typedef struct mc_panel_host_t +{ + void (*refresh) (struct mc_panel_host_t *host); + void (*set_hint) (struct mc_panel_host_t *host, const char *text); + void (*message) (struct mc_panel_host_t *host, int flags, const char *title, const char *text); + void (*close_plugin) (struct mc_panel_host_t *host, const char *dir_path); + int (*get_marked_count) (struct mc_panel_host_t *host); + const GString *(*get_next_marked) (struct mc_panel_host_t *host, int *current); + void *host_data; /* opaque, points to WPanel internally */ +} mc_panel_host_t; + +/* What the plugin provides (callback table) */ +typedef struct mc_panel_plugin_t +{ + int api_version; /* MC_PANEL_PLUGIN_API_VERSION */ + const char *name; /* "docker", "git-log" */ + const char *display_name; /* "Docker containers" */ + const char *proto; /* protocol prefix for panel title, e.g. "HelloWorld" → "HelloWorld:/path" */ + const char *prefix; /* "docker:" or NULL */ + mc_pp_flags_t flags; + + /* Required */ + void *(*open) (mc_panel_host_t *host, const char *open_path); + void (*close) (void *plugin_data); + /* Populate the panel. @list is a dir_list* (from dir.h). + The ".." entry at index 0 is already created by the host; + the plugin must NOT add ".." itself — only real items. */ + mc_pp_result_t (*get_items) (void *plugin_data, void *list /* dir_list* */); + + /* Optional (NULL = not supported) */ + mc_pp_result_t (*chdir) (void *plugin_data, const char *path); + mc_pp_result_t (*enter) (void *plugin_data, const char *fname, const struct stat *st); + mc_pp_result_t (*get_local_copy) (void *plugin_data, const char *fname, char **local_path); + mc_pp_result_t (*delete_items) (void *plugin_data, const char **names, int count); + const char *(*get_title) (void *plugin_data); + mc_pp_result_t (*handle_key) (void *plugin_data, int key); + mc_pp_result_t (*create_item) (void *plugin_data); +} mc_panel_plugin_t; + +typedef const mc_panel_plugin_t *(*mc_panel_plugin_register_fn) (void); + +/*** global variables defined in .c file *********************************************************/ + +/*** declarations of public functions ************************************************************/ + +/* Registry */ +gboolean mc_panel_plugin_add (const mc_panel_plugin_t *plugin); +const GSList *mc_panel_plugin_list (void); +const mc_panel_plugin_t *mc_panel_plugin_find_by_name (const char *name); +const mc_panel_plugin_t *mc_panel_plugin_find_by_prefix (const char *prefix); + +/* Loader */ +void mc_panel_plugins_load (void); +void mc_panel_plugins_shutdown (void); + +/*** inline functions ****************************************************************************/ + +#endif /* MC__PANEL_PLUGIN_H */ diff --git a/lib/stdckdint.h b/lib/stdckdint.h new file mode 100644 index 0000000000..8814d5efe7 --- /dev/null +++ b/lib/stdckdint.h @@ -0,0 +1,36 @@ +/* DO NOT EDIT! GENERATED AUTOMATICALLY! */ +/* stdckdint.h -- checked integer arithmetic + + Copyright 2022-2024 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +#ifndef _GL_STDCKDINT_H +#define _GL_STDCKDINT_H + +#include "intprops-internal.h" + +/* Store into *R the low-order bits of A + B, A - B, A * B, respectively. + Return 1 if the result overflows, 0 otherwise. + A, B, and *R can have any integer type other than char, gboolean, a + bit-precise integer type, or an enumeration type. + + These are like the standard macros introduced in C23, except that + arguments should not have side effects. */ + +#define ckd_add(r, a, b) ((gboolean) _GL_INT_ADD_WRAPV (a, b, r)) +#define ckd_sub(r, a, b) ((gboolean) _GL_INT_SUBTRACT_WRAPV (a, b, r)) +#define ckd_mul(r, a, b) ((gboolean) _GL_INT_MULTIPLY_WRAPV (a, b, r)) + +#endif diff --git a/lib/vfs/Makefile.am b/lib/vfs/Makefile.am index 66fc05c715..0a6c807345 100644 --- a/lib/vfs/Makefile.am +++ b/lib/vfs/Makefile.am @@ -1,6 +1,8 @@ noinst_LTLIBRARIES = libmcvfs.la -AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) +AM_CPPFLAGS = \ + -DMC_VFS_PLUGINS_DIR=\""$(vfs_plugins_dir)"\" \ + $(GLIB_CFLAGS) $(GMODULE_CFLAGS) -I$(top_srcdir) libmcvfs_la_SOURCES = \ direntry.c \ @@ -8,6 +10,7 @@ libmcvfs_la_SOURCES = \ interface.c \ parse_ls_vga.c \ path.c path.h \ + plugin-loader.c \ vfs.c vfs.h \ utilvfs.c utilvfs.h \ xdirentry.h diff --git a/lib/vfs/README b/lib/vfs/README index 036949eba4..dad9a5a218 100644 --- a/lib/vfs/README +++ b/lib/vfs/README @@ -86,15 +86,29 @@ a lot of such code in vfs.c. Hierarchy of classes ==================== -vfs ---- direntry ---- cpio } archives - | | ---- tar } +vfs ---- direntry ---- cpio } archives + | | ---- tar } | | - | | ---- fish } remote systems - | | ---- ftpfs } + | | ---- fish } remote systems + | | ---- ftpfs } + | | ---- sftpfs } (dynamic plugin, loaded at runtime) | |---- extfs ---- extfs archives |---- localfs ---- sfs ---- sfs archives +Dynamic plugins +=============== + +Some VFS modules are built as shared objects (.so) and loaded at +runtime from the VFS plugins directory (see --with-vfs-plugins-dir +configure option). Each plugin exports the mc_vfs_plugin_init symbol +which is called by the plugin loader after dlopen. This lets +distributions package optional VFS modules (and their dependencies) +separately from the core mc binary. + +Currently built as dynamic plugins: + sftpfs - SFTP filesystem (requires libssh2) + Properties of classes ===================== @@ -106,6 +120,7 @@ cpio yes* yes* no yes tar yes* yes* no yes fish no yes yes no ftpfs no yes yes no +sftpfs no yes yes no extfs no no yes yes localfs no no N/A N/A sfs no yes yes N/A diff --git a/lib/vfs/plugin-loader.c b/lib/vfs/plugin-loader.c new file mode 100644 index 0000000000..6533329e0a --- /dev/null +++ b/lib/vfs/plugin-loader.c @@ -0,0 +1,144 @@ +/* + Dynamic VFS plugin loader. + + Copyright (C) 2011-2025 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +/** \file + * \brief Dynamic VFS plugin loader + * + * Scans MC_VFS_PLUGINS_DIR for shared objects exporting mc_vfs_plugin_init(), + * loads them, and calls the init function so the plugin can register its + * vfs_class via vfs_register_class(). + */ + +#include + +#include "lib/global.h" +#include "lib/vfs/vfs.h" + +#ifdef HAVE_GMODULE + +#include +#include + +/*** global variables ****************************************************************************/ + +/*** file scope macro definitions ****************************************************************/ + +/*** file scope type declarations ****************************************************************/ + +/*** file scope variables ************************************************************************/ + +static GPtrArray *dynamic_modules = NULL; + +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +void +vfs_plugins_load_dynamic (void) +{ + GDir *dir; + const gchar *filename; + gchar *plugins_dir; + + plugins_dir = g_build_filename (MC_VFS_PLUGINS_DIR, (char *) NULL); + dir = g_dir_open (plugins_dir, 0, NULL); + if (dir == NULL) + { + g_free (plugins_dir); + return; // no plugins dir -- OK, nothing to load + } + + dynamic_modules = g_ptr_array_new (); + + while ((filename = g_dir_read_name (dir)) != NULL) + { + GModule *module; + mc_vfs_plugin_init_fn init_fn; + gchar *path; + + if (!g_str_has_suffix (filename, "." G_MODULE_SUFFIX)) + continue; + + path = g_build_filename (plugins_dir, filename, (char *) NULL); + module = g_module_open (path, G_MODULE_BIND_LAZY); + if (module == NULL) + { + fprintf (stderr, "VFS plugin %s: %s\n", filename, g_module_error ()); + g_free (path); + continue; + } + + if (!g_module_symbol (module, MC_VFS_PLUGIN_ENTRY, (gpointer *) &init_fn)) + { + fprintf (stderr, "VFS plugin %s: symbol %s not found\n", + filename, MC_VFS_PLUGIN_ENTRY); + g_module_close (module); + g_free (path); + continue; + } + + init_fn (); + g_module_make_resident (module); // prevent unload -- vfs_class lives in .so + g_ptr_array_add (dynamic_modules, module); + g_free (path); + } + + g_dir_close (dir); + g_free (plugins_dir); +} + +/* --------------------------------------------------------------------------------------------- */ + +void +vfs_plugins_unload_dynamic (void) +{ + if (dynamic_modules != NULL) + { + g_ptr_array_free (dynamic_modules, TRUE); + dynamic_modules = NULL; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +#else /* !HAVE_GMODULE */ + +void +vfs_plugins_load_dynamic (void) +{ + // GModule not available -- dynamic VFS plugins disabled +} + +/* --------------------------------------------------------------------------------------------- */ + +void +vfs_plugins_unload_dynamic (void) +{ + // nothing to do +} + +/* --------------------------------------------------------------------------------------------- */ + +#endif /* HAVE_GMODULE */ diff --git a/lib/vfs/vfs.h b/lib/vfs/vfs.h index 5497e06dc7..8e1daf38ab 100644 --- a/lib/vfs/vfs.h +++ b/lib/vfs/vfs.h @@ -89,6 +89,9 @@ typedef void (*fill_names_f) (const char *); typedef void *vfsid; +typedef void (*mc_vfs_plugin_init_fn) (void); +#define MC_VFS_PLUGIN_ENTRY "mc_vfs_plugin_init" + #ifdef HAVE_UTIMENSAT typedef struct timespec mc_timesbuf_t[2]; #else @@ -342,6 +345,10 @@ int mc_mkstemps (vfs_path_t **pname_vpath, const char *prefix, const char *suffi /* Creating temporary files safely */ const char *mc_tmpdir (void); +/* Dynamic VFS plugin loader */ +void vfs_plugins_load_dynamic (void); +void vfs_plugins_unload_dynamic (void); + /*** inline functions ****************************************************************************/ #endif diff --git a/m4.include/vfs/mc-vfs-sftp.m4 b/m4.include/vfs/mc-vfs-sftp.m4 index 581b144b79..7c48c987eb 100644 --- a/m4.include/vfs/mc-vfs-sftp.m4 +++ b/m4.include/vfs/mc-vfs-sftp.m4 @@ -8,7 +8,6 @@ AC_DEFUN([mc_VFS_SFTP], if test x"$found_libssh" = "xyes"; then mc_VFS_ADDNAME([sftp]) AC_DEFINE([ENABLE_VFS_SFTP], [1], [Support for SFTP filesystem]) - MCLIBS="$MCLIBS $LIBSSH_LIBS" enable_vfs_sftp="yes" else if test x"$enable_vfs_sftp" = x"yes"; then diff --git a/misc/skins/README.txt b/misc/skins/README.txt index c6e21fe57a..7091aec7fb 100644 --- a/misc/skins/README.txt +++ b/misc/skins/README.txt @@ -396,7 +396,13 @@ display. The characters in the skin files need to be encoded in UTF-8. Maximize or unmaximize an editor window window-close-char - Close an exitor window + Close an editor window + + fold-open-char + Indicator for an open (expanded) fold in the editor + + fold-close-char + Indicator for a closed (collapsed) fold in the editor Aliases section diff --git a/misc/skins/dark.ini b/misc/skins/dark.ini index afae6cb6cf..44fd1b5f1a 100644 --- a/misc/skins/dark.ini +++ b/misc/skins/dark.ini @@ -161,3 +161,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/darkfar.ini b/misc/skins/darkfar.ini index cd72255f75..b919d9fb94 100644 --- a/misc/skins/darkfar.ini +++ b/misc/skins/darkfar.ini @@ -161,3 +161,5 @@ [widget-editor] window-state-char = ↕ window-close-char = × + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/default.ini b/misc/skins/default.ini index 6c74bbf6de..a8784c80e1 100644 --- a/misc/skins/default.ini +++ b/misc/skins/default.ini @@ -161,3 +161,5 @@ [widget-editor] window-state-char = * window-close-char = X + fold-open-char = v + fold-close-char = > diff --git a/misc/skins/double-lines.ini b/misc/skins/double-lines.ini index 28fa680408..6d0a06a04a 100644 --- a/misc/skins/double-lines.ini +++ b/misc/skins/double-lines.ini @@ -161,3 +161,5 @@ [widget-editor] window-state-char = * window-close-char = X + fold-open-char = v + fold-close-char = > diff --git a/misc/skins/featured-plus.ini b/misc/skins/featured-plus.ini index 1d048e6e03..46885f5c37 100644 --- a/misc/skins/featured-plus.ini +++ b/misc/skins/featured-plus.ini @@ -161,3 +161,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/featured.ini b/misc/skins/featured.ini index 3e07d22f3b..e9bef695cd 100644 --- a/misc/skins/featured.ini +++ b/misc/skins/featured.ini @@ -161,3 +161,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/gotar.ini b/misc/skins/gotar.ini index a4ca95b6b0..4224478a3b 100644 --- a/misc/skins/gotar.ini +++ b/misc/skins/gotar.ini @@ -159,3 +159,5 @@ [widget-editor] window-state-char = * window-close-char = X + fold-open-char = v + fold-close-char = > diff --git a/misc/skins/gray-green-purple256.ini b/misc/skins/gray-green-purple256.ini index 575ec6b0fa..d9c6f1e673 100644 --- a/misc/skins/gray-green-purple256.ini +++ b/misc/skins/gray-green-purple256.ini @@ -167,3 +167,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/gray-orange-blue256.ini b/misc/skins/gray-orange-blue256.ini index 458cc59f19..c57e0e0d12 100644 --- a/misc/skins/gray-orange-blue256.ini +++ b/misc/skins/gray-orange-blue256.ini @@ -167,3 +167,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/julia256.ini b/misc/skins/julia256.ini index 052dd87f86..841ea8aa8c 100644 --- a/misc/skins/julia256.ini +++ b/misc/skins/julia256.ini @@ -166,3 +166,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/julia256root.ini b/misc/skins/julia256root.ini index b25078436e..c6588b6479 100644 --- a/misc/skins/julia256root.ini +++ b/misc/skins/julia256root.ini @@ -166,3 +166,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/mc46.ini b/misc/skins/mc46.ini index 0fa6ff5a4d..6ddaaa2fb0 100644 --- a/misc/skins/mc46.ini +++ b/misc/skins/mc46.ini @@ -152,3 +152,5 @@ [widget-editor] window-state-char = * window-close-char = X + fold-open-char = v + fold-close-char = > diff --git a/misc/skins/modarcon16-defbg-thin.ini b/misc/skins/modarcon16-defbg-thin.ini index 31cc9f7edd..d1f149c060 100644 --- a/misc/skins/modarcon16-defbg-thin.ini +++ b/misc/skins/modarcon16-defbg-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarcon16-defbg.ini b/misc/skins/modarcon16-defbg.ini index 7d90991e19..7055a14b6e 100644 --- a/misc/skins/modarcon16-defbg.ini +++ b/misc/skins/modarcon16-defbg.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarcon16-thin.ini b/misc/skins/modarcon16-thin.ini index 0986454545..3840b44622 100644 --- a/misc/skins/modarcon16-thin.ini +++ b/misc/skins/modarcon16-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarcon16.ini b/misc/skins/modarcon16.ini index 60fc0e1f74..b3c0ca1a3b 100644 --- a/misc/skins/modarcon16.ini +++ b/misc/skins/modarcon16.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarcon16root-defbg-thin.ini b/misc/skins/modarcon16root-defbg-thin.ini index 76ba02063a..5a3c875299 100644 --- a/misc/skins/modarcon16root-defbg-thin.ini +++ b/misc/skins/modarcon16root-defbg-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarcon16root-defbg.ini b/misc/skins/modarcon16root-defbg.ini index 775dd54452..ee5fd54b3c 100644 --- a/misc/skins/modarcon16root-defbg.ini +++ b/misc/skins/modarcon16root-defbg.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarcon16root-thin.ini b/misc/skins/modarcon16root-thin.ini index 0918d2bd3b..c0a667b85b 100644 --- a/misc/skins/modarcon16root-thin.ini +++ b/misc/skins/modarcon16root-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarcon16root.ini b/misc/skins/modarcon16root.ini index 0fc9e3f537..312ddd4d9a 100644 --- a/misc/skins/modarcon16root.ini +++ b/misc/skins/modarcon16root.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256-defbg-thin.ini b/misc/skins/modarin256-defbg-thin.ini index 72281b9b0c..7f99976e66 100644 --- a/misc/skins/modarin256-defbg-thin.ini +++ b/misc/skins/modarin256-defbg-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256-defbg.ini b/misc/skins/modarin256-defbg.ini index bf677bd60b..6055b9b080 100644 --- a/misc/skins/modarin256-defbg.ini +++ b/misc/skins/modarin256-defbg.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256-thin.ini b/misc/skins/modarin256-thin.ini index 3f822f12d2..f73a24994e 100644 --- a/misc/skins/modarin256-thin.ini +++ b/misc/skins/modarin256-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256.ini b/misc/skins/modarin256.ini index 317e3ca2ec..2269edb551 100644 --- a/misc/skins/modarin256.ini +++ b/misc/skins/modarin256.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256root-defbg-thin.ini b/misc/skins/modarin256root-defbg-thin.ini index 73d96d3eda..50c497a7a2 100644 --- a/misc/skins/modarin256root-defbg-thin.ini +++ b/misc/skins/modarin256root-defbg-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256root-defbg.ini b/misc/skins/modarin256root-defbg.ini index 2aa2df2b82..49b2938703 100644 --- a/misc/skins/modarin256root-defbg.ini +++ b/misc/skins/modarin256root-defbg.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256root-thin.ini b/misc/skins/modarin256root-thin.ini index 3efbb1dd6e..bb5c4440e7 100644 --- a/misc/skins/modarin256root-thin.ini +++ b/misc/skins/modarin256root-thin.ini @@ -197,3 +197,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/modarin256root.ini b/misc/skins/modarin256root.ini index 2773dddf77..0728f3f76b 100644 --- a/misc/skins/modarin256root.ini +++ b/misc/skins/modarin256root.ini @@ -199,3 +199,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/nicedark.ini b/misc/skins/nicedark.ini index 1b6d5e1446..eea9617885 100644 --- a/misc/skins/nicedark.ini +++ b/misc/skins/nicedark.ini @@ -161,3 +161,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/sand256.ini b/misc/skins/sand256.ini index 1ee31c9064..60fc2af48b 100644 --- a/misc/skins/sand256.ini +++ b/misc/skins/sand256.ini @@ -163,3 +163,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/seasons-autumn16M.ini b/misc/skins/seasons-autumn16M.ini index 622f0273b3..8dd7a31465 100644 --- a/misc/skins/seasons-autumn16M.ini +++ b/misc/skins/seasons-autumn16M.ini @@ -203,3 +203,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/seasons-spring16M.ini b/misc/skins/seasons-spring16M.ini index ab959c791e..5de23f7c1e 100644 --- a/misc/skins/seasons-spring16M.ini +++ b/misc/skins/seasons-spring16M.ini @@ -203,3 +203,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/seasons-summer16M.ini b/misc/skins/seasons-summer16M.ini index 31c7158aaa..93b7acaeff 100644 --- a/misc/skins/seasons-summer16M.ini +++ b/misc/skins/seasons-summer16M.ini @@ -203,3 +203,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/seasons-winter16M.ini b/misc/skins/seasons-winter16M.ini index 19b1275a1d..eea7650fbc 100644 --- a/misc/skins/seasons-winter16M.ini +++ b/misc/skins/seasons-winter16M.ini @@ -203,3 +203,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/xoria256-thin.ini b/misc/skins/xoria256-thin.ini index c1fabaef8c..8ff7f84925 100644 --- a/misc/skins/xoria256-thin.ini +++ b/misc/skins/xoria256-thin.ini @@ -176,3 +176,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/xoria256.ini b/misc/skins/xoria256.ini index 7772dc983b..75f3c866be 100644 --- a/misc/skins/xoria256.ini +++ b/misc/skins/xoria256.ini @@ -176,3 +176,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/xoria256root-thin.ini b/misc/skins/xoria256root-thin.ini index 73615942b4..82f7f382d5 100644 --- a/misc/skins/xoria256root-thin.ini +++ b/misc/skins/xoria256root-thin.ini @@ -176,3 +176,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/yadt256-defbg.ini b/misc/skins/yadt256-defbg.ini index 43b6aa1721..0ce86b6e4e 100644 --- a/misc/skins/yadt256-defbg.ini +++ b/misc/skins/yadt256-defbg.ini @@ -166,3 +166,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/misc/skins/yadt256.ini b/misc/skins/yadt256.ini index 26b88e3591..a8eec4ed72 100644 --- a/misc/skins/yadt256.ini +++ b/misc/skins/yadt256.ini @@ -165,3 +165,5 @@ [widget-editor] window-state-char = ↕ window-close-char = ✕ + fold-open-char = ▾ + fold-close-char = ▸ diff --git a/src/Makefile.am b/src/Makefile.am index dbad42ad60..c7df3f3ce2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = filemanager man2hlp vfs viewer +SUBDIRS = filemanager man2hlp vfs viewer panel-plugins if USE_INTERNAL_EDIT SUBDIRS += editor @@ -26,13 +26,6 @@ SUBDIRS += consaver AM_CPPFLAGS += -DSAVERDIR=\""$(pkglibexecdir)"\" endif -# required for textconf.c -if ENABLE_VFS -if ENABLE_VFS_SFTP -AM_CPPFLAGS += $(LIBSSH_CFLAGS) -endif -endif - AM_CPPFLAGS += -I$(top_srcdir) $(GLIB_CFLAGS) bin_PROGRAMS = mc @@ -55,6 +48,10 @@ libinternal_la_LIBADD = \ viewer/libmcviewer.la \ $(DIFFLIB) $(EDITLIB) $(SUBSHELLLIB) +if HAVE_GMODULE +mc_LDFLAGS = -export-dynamic +endif + mc_LDADD = \ libinternal.la diff --git a/src/editor/Makefile.am b/src/editor/Makefile.am index 304cb35be9..e98759178d 100644 --- a/src/editor/Makefile.am +++ b/src/editor/Makefile.am @@ -14,6 +14,7 @@ libedit_la_SOURCES = \ editbuffer.c editbuffer.h \ editcmd.c \ editdraw.c \ + fold.c \ editmacros.c editmacros.h \ editmenu.c \ editoptions.c \ diff --git a/src/editor/edit-impl.h b/src/editor/edit-impl.h index d8662b8738..6e6a4910e1 100644 --- a/src/editor/edit-impl.h +++ b/src/editor/edit-impl.h @@ -67,7 +67,7 @@ /* max count stack files */ #define MAX_HISTORY_MOVETO 50 -#define LINE_STATE_WIDTH 8 +#define LINE_STATE_WIDTH 9 #define LB_NAMES (LB_MAC + 1) @@ -110,6 +110,8 @@ extern gboolean search_create_bookmark; extern char *edit_window_state_char; extern char *edit_window_close_char; +extern char *edit_fold_open_char; +extern char *edit_fold_close_char; /*** declarations of public functions ************************************************************/ @@ -225,6 +227,20 @@ void book_mark_dec (WEdit *edit, long line); void book_mark_serialize (WEdit *edit, int color); void book_mark_restore (WEdit *edit, int color); +off_t edit_get_bracket (WEdit *edit, gboolean in_screen, unsigned long furthest_bracket_search); + +struct edit_fold_t *edit_fold_find (WEdit *edit, long line); +gboolean edit_fold_is_hidden (WEdit *edit, long line); +void edit_fold_make (WEdit *edit, long line_start, long line_count); +gboolean edit_fold_remove (WEdit *edit, long line); +long edit_fold_next_visible (WEdit *edit, long line); +long edit_fold_prev_visible (WEdit *edit, long line); +void edit_fold_flush (WEdit *edit); +void edit_fold_inc (WEdit *edit, long line); +void edit_fold_dec (WEdit *edit, long line); +void edit_fold_toggle (WEdit *edit); +int edit_fold_indicator_width (const struct edit_fold_t *fold); + gboolean edit_line_is_blank (WEdit *edit, long line); gboolean is_break_char (char c); void edit_options_dialog (WDialog *h); diff --git a/src/editor/edit.c b/src/editor/edit.c index 4f040db90a..464c6544a4 100644 --- a/src/editor/edit.c +++ b/src/editor/edit.c @@ -884,6 +884,16 @@ edit_cursor_to_eol (WEdit *edit) edit->search_start = edit->buffer.curs1; edit->prev_col = edit_get_col (edit); edit->over_col = 0; + + /* On a fold start line, End should place cursor after the fold indicator */ + if (edit->folds != NULL) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL && edit->buffer.curs_line == fold->line_start) + edit->over_col = edit_fold_indicator_width (fold); + } } /* --------------------------------------------------------------------------------------------- */ @@ -1025,10 +1035,94 @@ edit_right_char_move_cmd (WEdit *edit) else c = edit_buffer_get_current_byte (&edit->buffer); + /* On a fold start line at or past the opening bracket: skip over fold indicator */ + if (edit->folds != NULL) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL && edit->buffer.curs_line == fold->line_start) + { + off_t bol, eol, bracket_off; + + bol = edit_buffer_get_current_bol (&edit->buffer); + eol = edit_buffer_get_current_eol (&edit->buffer); + + /* find the opening bracket by scanning backward from EOL */ + for (bracket_off = eol - 1; bracket_off >= bol; bracket_off--) + { + int ch; + + ch = edit_buffer_get_byte (&edit->buffer, bracket_off); + if (ch == '{' || ch == '[' || ch == '(') + break; + } + + if (bracket_off >= bol && edit->buffer.curs1 >= bracket_off) + { + if (edit_options.cursor_beyond_eol) + { + int fold_width; + + fold_width = edit_fold_indicator_width (fold); + + /* move cursor to EOL if not already there */ + if (edit->buffer.curs1 < eol) + { + edit_cursor_move (edit, eol - edit->buffer.curs1); + edit->over_col = 0; + } + + if (edit->over_col < fold_width) + { + /* jump over fold indicator like over a tab */ + edit->over_col = fold_width; + } + else + { + /* past the fold indicator — keep going right */ + edit->over_col++; + } + return; + } + + /* cursor_beyond_eol off: jump to next visible line */ + { + off_t target; + + target = edit_buffer_get_forward_offset (&edit->buffer, bol, + fold->line_count + 1, 0); + edit_cursor_move (edit, target - edit->buffer.curs1); + edit->over_col = 0; + } + return; + } + } + } + if (edit_options.cursor_beyond_eol && c == '\n') edit->over_col++; else edit_cursor_move (edit, char_length); + + /* Skip over hidden (folded) lines — fallback for other entry points */ + if (edit->folds != NULL && edit_fold_is_hidden (edit, edit->buffer.curs_line)) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL) + { + long delta; + off_t target; + + delta = fold->line_start + fold->line_count + 1 - edit->buffer.curs_line; + target = edit_buffer_get_forward_offset ( + &edit->buffer, edit_buffer_get_current_bol (&edit->buffer), delta, 0); + edit_cursor_move (edit, target - edit->buffer.curs1); + edit->over_col = 0; + } + } } /* --------------------------------------------------------------------------------------------- */ @@ -1050,9 +1144,94 @@ edit_left_char_move_cmd (WEdit *edit) } if (edit_options.cursor_beyond_eol && edit->over_col > 0) + { + /* On a fold start line, jump over the fold indicator in one step */ + if (edit->folds != NULL) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL && edit->buffer.curs_line == fold->line_start) + { + int fold_width; + + fold_width = edit_fold_indicator_width (fold); + if (edit->over_col <= fold_width) + { + off_t bol, eol, bracket_off; + + bol = edit_buffer_get_current_bol (&edit->buffer); + eol = edit_buffer_get_current_eol (&edit->buffer); + + /* find the opening bracket */ + for (bracket_off = eol - 1; bracket_off >= bol; bracket_off--) + { + int ch; + + ch = edit_buffer_get_byte (&edit->buffer, bracket_off); + if (ch == '{' || ch == '[' || ch == '(') + break; + } + + if (bracket_off >= bol) + edit_cursor_move (edit, bracket_off - edit->buffer.curs1); + + edit->over_col = 0; + return; + } + } + } edit->over_col--; + } else edit_cursor_move (edit, -char_length); + + /* Skip over hidden (folded) lines */ + if (edit->folds != NULL && edit_fold_is_hidden (edit, edit->buffer.curs_line)) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL) + { + long delta; + off_t bol, eol; + + /* Jump to fold start line */ + delta = edit->buffer.curs_line - fold->line_start; + bol = edit_buffer_get_backward_offset ( + &edit->buffer, edit_buffer_get_current_bol (&edit->buffer), delta); + eol = edit_buffer_get_eol (&edit->buffer, bol); + + if (edit_options.cursor_beyond_eol) + { + /* cursor_beyond_eol on: land at end of fold indicator */ + edit_cursor_move (edit, eol - edit->buffer.curs1); + edit->over_col = edit_fold_indicator_width (fold); + } + else + { + off_t bracket_off; + + /* cursor_beyond_eol off: land on the opening bracket */ + for (bracket_off = eol - 1; bracket_off >= bol; bracket_off--) + { + int ch; + + ch = edit_buffer_get_byte (&edit->buffer, bracket_off); + if (ch == '{' || ch == '[' || ch == '(') + break; + } + + if (bracket_off >= bol) + edit_cursor_move (edit, bracket_off - edit->buffer.curs1); + else + edit_cursor_move (edit, eol - edit->buffer.curs1); + + edit->over_col = 0; + } + } + } } /* --------------------------------------------------------------------------------------------- */ @@ -1075,6 +1254,8 @@ edit_move_updown (WEdit *edit, long lines, gboolean do_scroll, gboolean directio if (lines > 1) edit->force |= REDRAW_PAGE; + if (edit->folds != NULL) + edit->force |= REDRAW_PAGE; if (do_scroll) { if (direction) @@ -1088,6 +1269,38 @@ edit_move_updown (WEdit *edit, long lines, gboolean do_scroll, gboolean directio edit_cursor_move (edit, p - edit->buffer.curs1); edit_move_to_prev_col (edit, p); + /* skip over folded (hidden) lines */ + if (edit->folds != NULL) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL && edit->buffer.curs_line > fold->line_start + && edit->buffer.curs_line <= fold->line_start + fold->line_count) + { + long target; + + if (direction) + target = fold->line_start; + else + target = fold->line_start + fold->line_count + 1; + + { + long delta = target - edit->buffer.curs_line; + off_t np; + + if (delta > 0) + np = edit_buffer_get_forward_offset ( + &edit->buffer, edit_buffer_get_current_bol (&edit->buffer), delta, 0); + else + np = edit_buffer_get_backward_offset ( + &edit->buffer, edit_buffer_get_current_bol (&edit->buffer), -delta); + edit_cursor_move (edit, np - edit->buffer.curs1); + edit_move_to_prev_col (edit, np); + } + } + } + // search start of current multibyte char (like CJK) if (edit->buffer.curs1 > 0 && edit->buffer.curs1 + 1 < edit->buffer.size && edit_buffer_get_current_byte (&edit->buffer) >= 256) @@ -1517,7 +1730,7 @@ check_and_wrap_line (WEdit *edit) * @return position of the found bracket (-1 if no match) */ -static off_t +off_t edit_get_bracket (WEdit *edit, gboolean in_screen, unsigned long furthest_bracket_search) { const char *const b = "{}{[][()(", *p; @@ -2225,6 +2438,7 @@ edit_clean (WEdit *edit) edit_free_syntax_rules (edit); book_mark_flush (edit, -1); + edit_fold_flush (edit); edit_buffer_clean (&edit->buffer); @@ -2544,6 +2758,7 @@ edit_insert (WEdit *edit, int c) if (c == '\n') { book_mark_inc (edit, edit->buffer.curs_line); + edit_fold_inc (edit, edit->buffer.curs_line); edit->buffer.curs_line++; edit->buffer.lines++; edit->force |= REDRAW_LINE_ABOVE | REDRAW_AFTER_CURSOR; @@ -2579,6 +2794,7 @@ edit_insert_ahead (WEdit *edit, int c) if (c == '\n') { book_mark_inc (edit, edit->buffer.curs_line); + edit_fold_inc (edit, edit->buffer.curs_line); edit->buffer.lines++; edit->force |= REDRAW_AFTER_CURSOR; } @@ -2652,6 +2868,7 @@ edit_delete (WEdit *edit, gboolean byte_delete) if (p == '\n') { book_mark_dec (edit, edit->buffer.curs_line); + edit_fold_dec (edit, edit->buffer.curs_line); edit->buffer.lines--; edit->force |= REDRAW_AFTER_CURSOR; } @@ -2707,6 +2924,7 @@ edit_backspace (WEdit *edit, gboolean byte_delete) if (p == '\n') { book_mark_dec (edit, edit->buffer.curs_line); + edit_fold_dec (edit, edit->buffer.curs_line); edit->buffer.curs_line--; edit->buffer.lines--; edit->force |= REDRAW_AFTER_CURSOR; @@ -2859,7 +3077,31 @@ edit_get_col (const WEdit *edit) void edit_update_curs_row (WEdit *edit) { - edit->curs_row = edit->buffer.curs_line - edit->start_line; + long hidden = 0; + + if (edit->folds != NULL) + { + edit_fold_t *f; + + for (f = edit->folds; f != NULL; f = f->next) + { + long h_start, h_end; + + /* hidden lines are [f->line_start + 1 .. f->line_start + f->line_count] */ + if (f->line_start + f->line_count < edit->start_line) + continue; + if (f->line_start >= edit->buffer.curs_line) + break; + + h_start = MAX (f->line_start + 1, edit->start_line); + h_end = MIN (f->line_start + f->line_count, edit->buffer.curs_line - 1); + + if (h_end >= h_start) + hidden += h_end - h_start + 1; + } + } + + edit->curs_row = edit->buffer.curs_line - edit->start_line - hidden; } /* --------------------------------------------------------------------------------------------- */ @@ -2899,6 +3141,23 @@ edit_scroll_upward (WEdit *edit, long i) edit->force |= REDRAW_PAGE; edit->force &= (0xfff - REDRAW_CHAR_ONLY); } + + /* if start_line landed inside a fold, move to fold start */ + if (edit->folds != NULL) + { + edit_fold_t *f; + + f = edit_fold_find (edit, edit->start_line); + if (f != NULL && edit->start_line > f->line_start) + { + long skip = edit->start_line - f->line_start; + + edit->start_line -= skip; + edit->start_display = + edit_buffer_get_backward_offset (&edit->buffer, edit->start_display, skip); + } + } + edit_update_curs_row (edit); } @@ -2910,6 +3169,20 @@ edit_scroll_downward (WEdit *edit, long i) long lines_below; lines_below = edit->buffer.lines - edit->start_line - (WIDGET (edit)->rect.lines - 1); + + /* Each fold in the viewport takes one screen row but covers line_count+1 buffer lines. + Add back fold->line_count so lines_below doesn't underestimate. */ + if (edit->folds != NULL) + { + edit_fold_t *f; + + for (f = edit->folds; f != NULL; f = f->next) + { + if (f->line_start >= edit->start_line) + lines_below += f->line_count; + } + } + if (lines_below > 0) { if (i > lines_below) @@ -2920,6 +3193,23 @@ edit_scroll_downward (WEdit *edit, long i) edit->force |= REDRAW_PAGE; edit->force &= (0xfff - REDRAW_CHAR_ONLY); } + + /* if start_line landed inside a fold, skip past it */ + if (edit->folds != NULL) + { + edit_fold_t *f; + + f = edit_fold_find (edit, edit->start_line); + if (f != NULL && edit->start_line > f->line_start) + { + long skip = f->line_start + f->line_count + 1 - edit->start_line; + + edit->start_line += skip; + edit->start_display = + edit_buffer_get_forward_offset (&edit->buffer, edit->start_display, skip, 0); + } + } + edit_update_curs_row (edit); } @@ -3420,6 +3710,28 @@ edit_execute_cmd (WEdit *edit, long command, int char_for_insertion) edit->redo_stack_reset = 1; + /* Snap cursor out of hidden (folded) lines — like tab-snap prevents mid-tab cursor */ + if (edit->folds != NULL && edit_fold_is_hidden (edit, edit->buffer.curs_line)) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL) + { + long delta; + off_t bol; + + delta = edit->buffer.curs_line - fold->line_start; + bol = edit_buffer_get_backward_offset ( + &edit->buffer, edit_buffer_get_current_bol (&edit->buffer), delta); + edit_cursor_move (edit, bol - edit->buffer.curs1); + /* position at EOL of fold start line */ + edit_cursor_move (edit, + edit_buffer_get_current_eol (&edit->buffer) - edit->buffer.curs1); + edit->over_col = 0; + } + } + // An ordinary key press if (char_for_insertion >= 0) { @@ -3844,6 +4156,14 @@ edit_execute_cmd (WEdit *edit, long command, int char_for_insertion) } break; + case CK_FoldToggle: + edit_fold_toggle (edit); + break; + case CK_UnfoldAll: + edit_fold_flush (edit); + edit->force |= REDRAW_PAGE; + break; + case CK_Top: case CK_MarkToFileBegin: edit_move_to_top (edit); diff --git a/src/editor/editdraw.c b/src/editor/editdraw.c index d096829428..1bd57aec9a 100644 --- a/src/editor/editdraw.c +++ b/src/editor/editdraw.c @@ -433,6 +433,16 @@ print_to_widget (WEdit *edit, long row, int start_col, int start_col_real, long edit_move (x1 + i - edit_options.line_state_width, y); if (status[i] == '\0') status[i] = ' '; + if (i == LINE_STATE_WIDTH - 1 && status[i] == 'v') + { + tty_print_string (edit_fold_open_char); + continue; + } + if (i == LINE_STATE_WIDTH - 1 && status[i] == '>') + { + tty_print_string (edit_fold_close_char); + continue; + } tty_print_char (status[i]); } } @@ -497,6 +507,8 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co int col, start_col_real; int abn_style; int book_mark = 0; + int brace_depth = 0; + long fold_line_count = 0; char line_stat[LINE_STATE_WIDTH + 1] = "\0"; if (row > w->rect.lines - 1 - EDIT_TEXT_VERTICAL_OFFSET - 2 * (edit->fullscreen != 0 ? 0 : 1)) @@ -530,7 +542,7 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co cur_line = edit->start_line + row; if (cur_line <= edit->buffer.lines) - g_snprintf (line_stat, sizeof (line_stat), "%7ld ", cur_line + 1); + g_snprintf (line_stat, sizeof (line_stat), "%7ld ", cur_line + 1); else { memset (line_stat, ' ', LINE_STATE_WIDTH); @@ -539,6 +551,18 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co if (book_mark_query_color (edit, cur_line, EDITOR_BOOKMARK_COLOR)) g_snprintf (line_stat, 2, "*"); + + if (cur_line <= edit->buffer.lines) + { + struct edit_fold_t *fold; + + fold = edit_fold_find (edit, cur_line); + if (fold != NULL && cur_line == fold->line_start) + { + line_stat[LINE_STATE_WIDTH - 1] = '>'; + fold_line_count = fold->line_count; + } + } } if (col <= -(edit->start_col + 16)) @@ -603,6 +627,11 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co else c = edit_buffer_get_byte (&edit->buffer, q); + if (strchr ("{[(", c) != NULL) + brace_depth++; + else if (strchr ("}])", c) != NULL) + brace_depth--; + // we don't use bg for mc - fg contains both if (book_mark != 0) p->style |= book_mark << 16; @@ -805,8 +834,48 @@ edit_draw_this_line (WEdit *edit, off_t b, long row, long start_col, long end_co } } + if (fold_line_count > 0) + { + char fold_text[64]; + int fi; + int style; + line_s *fp; + + style = EDITOR_LINE_STATE_COLOR << 16; + + /* find the opening bracket and determine the closing one */ + { + char close_bracket = '}'; + + for (fp = p - 1; fp >= line; fp--) + { + if (fp->ch == '{' || fp->ch == '[' || fp->ch == '(') + { + if (fp->ch == '[') + close_bracket = ']'; + else if (fp->ch == '(') + close_bracket = ')'; + fp->style = style; + break; + } + } + + g_snprintf (fold_text, sizeof (fold_text), "...%c", close_bracket); + } + + for (fi = 0; fold_text[fi] != '\0' && p < line + MAX_LINE_LEN - 1; fi++) + { + p->ch = fold_text[fi]; + p->style = style; + p++; + } + } + p->ch = 0; + if (brace_depth > 0 && edit_options.line_state && line_stat[LINE_STATE_WIDTH - 1] != '>') + line_stat[LINE_STATE_WIDTH - 1] = 'v'; + print_to_widget (edit, row, start_col, start_col_real, end_col, line, line_stat, book_mark); } @@ -838,6 +907,11 @@ render_edit_text (WEdit *edit, long start_row, long start_column, long end_row, int y1, x1, y2, x2; int last_line, last_column; + /* When folds are active, always do a full page redraw so that + line numbers and content are rendered correctly. */ + if (edit->folds != NULL) + force |= REDRAW_PAGE; + // draw only visible region last_line = wh->rect.y + wh->rect.lines - 1; @@ -891,13 +965,54 @@ render_edit_text (WEdit *edit, long start_row, long start_column, long end_row, if ((force & REDRAW_PAGE) != 0) { + long current_line; + b = edit_buffer_get_forward_offset (&edit->buffer, edit->start_display, start_row, 0); + current_line = edit->start_line + start_row; for (row = start_row; row <= end_row; row++) { + edit_fold_t *fold; + if (key_pending (edit)) return; - edit_draw_this_line (edit, b, row, start_column, end_column); - b = edit_buffer_get_forward_offset (&edit->buffer, b, 1, 0); + + fold = edit_fold_find (edit, current_line); + if (fold != NULL && current_line == fold->line_start) + { + /* draw fold start line normally (shows '>' indicator) */ + long saved_start_line = edit->start_line; + edit->start_line = current_line - row; + edit_draw_this_line (edit, b, row, start_column, end_column); + edit->start_line = saved_start_line; + + /* skip hidden lines */ + b = edit_buffer_get_forward_offset (&edit->buffer, b, fold->line_count + 1, 0); + current_line += fold->line_count + 1; + } + else if (fold != NULL && current_line > fold->line_start) + { + /* start_line was inside a fold — skip to after it + without consuming a screen row */ + long skip = fold->line_start + fold->line_count + 1 - current_line; + + b = edit_buffer_get_forward_offset (&edit->buffer, b, skip, 0); + current_line += skip; + row--; /* compensate for loop increment */ + } + else + { + /* Temporarily adjust start_line so that edit_draw_this_line + computes the correct line number as (start_line + row). + With folds, the default (start_line + row) is wrong because + folded lines don't occupy screen rows. */ + long saved_start_line = edit->start_line; + edit->start_line = current_line - row; + edit_draw_this_line (edit, b, row, start_column, end_column); + edit->start_line = saved_start_line; + + b = edit_buffer_get_forward_offset (&edit->buffer, b, 1, 0); + current_line++; + } } } else diff --git a/src/editor/editmenu.c b/src/editor/editmenu.c index 5b708f95a1..e38bc05ebf 100644 --- a/src/editor/editmenu.c +++ b/src/editor/editmenu.c @@ -162,6 +162,9 @@ create_command_menu (void) entries = g_list_prepend (entries, menu_separator_new ()); entries = g_list_prepend (entries, menu_entry_new (_ ("Encod&ing..."), CK_SelectCodepage)); entries = g_list_prepend (entries, menu_separator_new ()); + entries = g_list_prepend (entries, menu_entry_new (_ ("&Fold / Unfold block"), CK_FoldToggle)); + entries = g_list_prepend (entries, menu_entry_new (_ ("&Unfold all"), CK_UnfoldAll)); + entries = g_list_prepend (entries, menu_separator_new ()); entries = g_list_prepend (entries, menu_entry_new (_ ("&Refresh screen"), CK_Refresh)); entries = g_list_prepend (entries, menu_separator_new ()); entries = g_list_prepend ( diff --git a/src/editor/editwidget.c b/src/editor/editwidget.c index 84701c5044..203ff9ebdc 100644 --- a/src/editor/editwidget.c +++ b/src/editor/editwidget.c @@ -70,6 +70,8 @@ char *edit_window_state_char = NULL; char *edit_window_close_char = NULL; +char *edit_fold_open_char = NULL; +char *edit_fold_close_char = NULL; /*** file scope macro definitions ****************************************************************/ @@ -100,6 +102,8 @@ edit_dlg_init (void) { edit_window_state_char = mc_skin_get ("widget-editor", "window-state-char", "*"); edit_window_close_char = mc_skin_get ("widget-editor", "window-close-char", "X"); + edit_fold_open_char = mc_skin_get ("widget-editor", "fold-open-char", "v"); + edit_fold_close_char = mc_skin_get ("widget-editor", "fold-close-char", ">"); #ifdef HAVE_ASPELL aspell_init (); @@ -119,6 +123,8 @@ edit_dlg_deinit (void) { g_free (edit_window_state_char); g_free (edit_window_close_char); + g_free (edit_fold_open_char); + g_free (edit_fold_close_char); #ifdef HAVE_ASPELL aspell_clean (); @@ -734,6 +740,64 @@ edit_update_cursor (WEdit *edit, const mouse_event_t *event) else edit_move_to_prev_col (edit, edit_buffer_get_current_bol (&edit->buffer)); + /* On a fold start line, handle clicks relative to fold indicator */ + if (edit->folds != NULL) + { + edit_fold_t *fold; + + fold = edit_fold_find (edit, edit->buffer.curs_line); + if (fold != NULL && edit->buffer.curs_line == fold->line_start) + { + off_t bol, eol, bracket_off; + + bol = edit_buffer_get_current_bol (&edit->buffer); + eol = edit_buffer_get_current_eol (&edit->buffer); + + /* Find the opening bracket by scanning backward from EOL */ + for (bracket_off = eol - 1; bracket_off >= bol; bracket_off--) + { + int ch; + + ch = edit_buffer_get_byte (&edit->buffer, bracket_off); + if (ch == '{' || ch == '[' || ch == '(') + break; + } + + if (bracket_off >= bol) + { + long bracket_col, click_col; + + bracket_col = (long) edit_move_forward3 (edit, bol, 0, bracket_off); + click_col = x - edit->start_col - edit_options.line_state_width; + + if (click_col >= bracket_col) + { + long line_visual_len, fold_visual_end; + + /* Calculate visual end of fold indicator */ + line_visual_len = (long) edit_move_forward3 (edit, bol, 0, eol); + fold_visual_end = line_visual_len + edit_fold_indicator_width (fold); + + /* Snap cursor to the bracket */ + edit_cursor_move (edit, bracket_off - edit->buffer.curs1); + edit->curs_col = bracket_col; + edit->prev_col = bracket_col; + + if (edit_options.cursor_beyond_eol && click_col >= fold_visual_end) + { + /* Click beyond fold indicator — cursor past fold end */ + edit->over_col = click_col - bracket_col; + } + else + { + /* Click on fold indicator — stay at bracket */ + edit->over_col = 0; + } + } + } + } + } + if (event->msg == MSG_MOUSE_CLICK) { edit_mark_cmd (edit, TRUE); // reset @@ -1128,6 +1192,93 @@ edit_mouse_callback (Widget *w, mouse_msg_t msg, mouse_event_t *event) } } + // click in the line-state gutter area — toggle fold + if (edit_options.line_state) + { + int gutter_x; + + gutter_x = event->x - (edit->fullscreen != 0 ? 0 : 1); + if (gutter_x >= 0 && gutter_x < edit_options.line_state_width) + { + int click_y; + long line; + edit_fold_t *fold; + + // map screen row to file line and buffer offset + click_y = event->y - (edit->fullscreen != 0 ? 0 : 1); + { + long target_line; + off_t target_b; + int r; + + target_line = edit->start_line; + target_b = edit->start_display; + for (r = 0; r < click_y; r++) + { + edit_fold_t *f; + + f = edit_fold_find (edit, target_line); + if (f != NULL && target_line == f->line_start) + { + target_b = edit_buffer_get_forward_offset (&edit->buffer, target_b, + f->line_count + 1, 0); + target_line += f->line_count + 1; + } + else + { + target_b = + edit_buffer_get_forward_offset (&edit->buffer, target_b, 1, 0); + target_line++; + } + } + line = target_line; + + // move cursor directly to the target line + edit_cursor_move (edit, target_b - edit->buffer.curs1); + } + + fold = edit_fold_find (edit, line); + + if (fold != NULL && line == fold->line_start) + { + // existing fold — unfold + edit_fold_remove (edit, fold->line_start); + } + else + { + // try to create fold: find { on this line, match } + off_t bol, eol, pos; + + bol = edit_buffer_get_current_bol (&edit->buffer); + eol = edit_buffer_get_current_eol (&edit->buffer); + + for (pos = bol; pos < eol; pos++) + { + if (strchr ("{[(", edit_buffer_get_byte (&edit->buffer, pos)) != NULL) + { + off_t match; + + edit_cursor_move (edit, pos - edit->buffer.curs1); + match = edit_get_bracket (edit, 0, 0); + if (match >= 0) + { + long line2; + + line2 = edit_buffer_count_lines (&edit->buffer, 0, match); + if (line2 > line) + edit_fold_make (edit, line, line2 - line); + } + break; + } + } + } + + edit->force |= REDRAW_PAGE; + edit_total_update (edit); + break; + } + } + MC_FALLTHROUGH; // to start/stop text selection case MSG_MOUSE_UP: diff --git a/src/editor/editwidget.h b/src/editor/editwidget.h index c8569cfb35..ea101d720e 100644 --- a/src/editor/editwidget.h +++ b/src/editor/editwidget.h @@ -30,6 +30,15 @@ struct edit_book_mark_t edit_book_mark_t *prev; }; +typedef struct edit_fold_t edit_fold_t; +struct edit_fold_t +{ + long line_start; // first line of folded region + long line_count; // number of hidden lines below line_start + edit_fold_t *next; + edit_fold_t *prev; +}; + typedef struct edit_syntax_rule_t edit_syntax_rule_t; struct edit_syntax_rule_t { @@ -121,6 +130,9 @@ struct WEdit edit_book_mark_t *book_mark; GArray *serialized_bookmarks; + // code folding + edit_fold_t *folds; + // undo stack and pointers unsigned long undo_stack_pointer; long *undo_stack; diff --git a/src/editor/fold.c b/src/editor/fold.c new file mode 100644 index 0000000000..be5ca6ceb1 --- /dev/null +++ b/src/editor/fold.c @@ -0,0 +1,436 @@ +/* + Editor code folding + + Copyright (C) 2025 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +/** \file + * \brief Source: editor code folding + */ + +#include + +#include +#include + +#include "lib/global.h" + +#include "edit-impl.h" +#include "editwidget.h" + +/* --------------------------------------------------------------------------------------------- */ +/*** global variables ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** file scope macro definitions ****************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** file scope type declarations ****************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** forward declarations (file scope functions) *************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** file scope variables ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/** + * Find the fold that contains the given line. + * + * @param edit editor object + * @param line line number to check + * @return pointer to fold if line is the start line or within fold range, NULL otherwise + */ +edit_fold_t * +edit_fold_find (WEdit *edit, long line) +{ + edit_fold_t *p; + + if (edit->folds == NULL) + return NULL; + + for (p = edit->folds; p != NULL; p = p->next) + { + if (line >= p->line_start && line <= p->line_start + p->line_count) + return p; + if (p->line_start > line) + break; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Check if a line is hidden inside a fold (not the fold start line itself). + * + * @param edit editor object + * @param line line number to check + * @return TRUE if line is hidden + */ +gboolean +edit_fold_is_hidden (WEdit *edit, long line) +{ + edit_fold_t *f; + + f = edit_fold_find (edit, line); + if (f == NULL) + return FALSE; + + return (line > f->line_start && line <= f->line_start + f->line_count); +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Create a new fold region. The first visible line is line_start, + * and line_count lines below it become hidden. + * + * If the new fold overlaps an existing fold, the existing fold is removed first. + * + * @param edit editor object + * @param line_start first line of the fold + * @param line_count number of lines to hide + */ +void +edit_fold_make (WEdit *edit, long line_start, long line_count) +{ + edit_fold_t *p, *q, *new_fold; + + if (line_count <= 0) + return; + + /* remove any folds that overlap with the new region */ + p = edit->folds; + while (p != NULL) + { + q = p->next; + /* overlap: fold [p->line_start, p->line_start + p->line_count] + intersects [line_start, line_start + line_count] */ + if (p->line_start + p->line_count >= line_start && p->line_start <= line_start + line_count) + { + /* remove p */ + if (p->prev != NULL) + p->prev->next = p->next; + else + edit->folds = p->next; + if (p->next != NULL) + p->next->prev = p->prev; + g_free (p); + } + p = q; + } + + /* create and insert new fold in sorted order */ + new_fold = g_new0 (edit_fold_t, 1); + new_fold->line_start = line_start; + new_fold->line_count = line_count; + + if (edit->folds == NULL || edit->folds->line_start > line_start) + { + /* insert at head */ + new_fold->next = edit->folds; + new_fold->prev = NULL; + if (edit->folds != NULL) + edit->folds->prev = new_fold; + edit->folds = new_fold; + } + else + { + /* find insertion point */ + for (p = edit->folds; p->next != NULL && p->next->line_start <= line_start; p = p->next) + ; + new_fold->next = p->next; + new_fold->prev = p; + if (p->next != NULL) + p->next->prev = new_fold; + p->next = new_fold; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Remove the fold that contains the given line. + * + * @param edit editor object + * @param line line number + * @return TRUE if a fold was removed + */ +gboolean +edit_fold_remove (WEdit *edit, long line) +{ + edit_fold_t *f; + + f = edit_fold_find (edit, line); + if (f == NULL) + return FALSE; + + if (f->prev != NULL) + f->prev->next = f->next; + else + edit->folds = f->next; + + if (f->next != NULL) + f->next->prev = f->prev; + + g_free (f); + return TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Get the next visible line number after the given line. + * If the line is a fold start, skip over its hidden lines. + * + * @param edit editor object + * @param line current line number + * @return next visible line number + */ +long +edit_fold_next_visible (WEdit *edit, long line) +{ + edit_fold_t *f; + + f = edit_fold_find (edit, line); + if (f != NULL && line == f->line_start) + return f->line_start + f->line_count + 1; + + /* if inside a fold (shouldn't normally happen for cursor), jump past it */ + if (f != NULL && line > f->line_start) + return f->line_start + f->line_count + 1; + + return line + 1; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Get the previous visible line number before the given line. + * + * @param edit editor object + * @param line current line number + * @return previous visible line number + */ +long +edit_fold_prev_visible (WEdit *edit, long line) +{ + edit_fold_t *f; + + if (line <= 0) + return 0; + + f = edit_fold_find (edit, line - 1); + if (f != NULL && (line - 1) > f->line_start) + return f->line_start; + + return line - 1; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Remove all folds. + * + * @param edit editor object + */ +void +edit_fold_flush (WEdit *edit) +{ + edit_fold_t *p, *q; + + for (p = edit->folds; p != NULL; p = q) + { + q = p->next; + g_free (p); + } + edit->folds = NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Shift fold line numbers down by 1 for all folds after the given line. + * Called when a new line is inserted. + * + * @param edit editor object + * @param line line where insertion happened + */ +void +edit_fold_inc (WEdit *edit, long line) +{ + edit_fold_t *p; + + for (p = edit->folds; p != NULL; p = p->next) + { + if (p->line_start > line) + p->line_start++; + else if (line > p->line_start && line <= p->line_start + p->line_count) + p->line_count++; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Shift fold line numbers up by 1 for all folds after the given line. + * Called when a line is deleted. + * + * @param edit editor object + * @param line line where deletion happened + */ +void +edit_fold_dec (WEdit *edit, long line) +{ + edit_fold_t *p, *q; + + for (p = edit->folds; p != NULL; p = q) + { + q = p->next; + if (p->line_start > line) + p->line_start--; + else if (line > p->line_start && line <= p->line_start + p->line_count) + { + p->line_count--; + if (p->line_count <= 0) + { + /* fold collapsed — remove it */ + if (p->prev != NULL) + p->prev->next = p->next; + else + edit->folds = p->next; + if (p->next != NULL) + p->next->prev = p->prev; + g_free (p); + } + } + } +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Calculate the visual width of the fold indicator text "...} (N lines)". + * + * Uses str_term_width1 for correct i18n/UTF-8 handling. + * + * @param fold fold structure + * @return visual column width of the fold indicator text + */ +int +edit_fold_indicator_width (const struct edit_fold_t *fold) +{ + (void) fold; + /* fold indicator is "...}" — always 4 columns */ + return 4; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Toggle fold at the current cursor line. + * + * If the cursor is on a fold start, unfold it. + * If a selection is active, fold the selected lines. + * Otherwise, find an opening bracket on the line, match it, and fold that range. + * + * @param edit editor object + */ +void +edit_fold_toggle (WEdit *edit) +{ + long line; + edit_fold_t *fold; + + line = edit->buffer.curs_line; + fold = edit_fold_find (edit, line); + + if (fold != NULL && line == fold->line_start) + { + /* existing fold — unfold */ + edit_fold_remove (edit, fold->line_start); + } + else + { + off_t start_mark, end_mark; + + if (eval_marks (edit, &start_mark, &end_mark)) + { + /* selection active — fold selected lines */ + long line1, line2; + + line1 = edit_buffer_count_lines (&edit->buffer, 0, start_mark); + line2 = edit_buffer_count_lines (&edit->buffer, 0, end_mark); + if (line2 > line1) + { + edit_fold_make (edit, line1, line2 - line1); + edit_mark_cmd (edit, TRUE); + } + } + else + { + /* no selection — find { on this line, match } */ + off_t bol, eol, pos; + + bol = edit_buffer_get_current_bol (&edit->buffer); + eol = edit_buffer_get_current_eol (&edit->buffer); + + for (pos = bol; pos < eol; pos++) + { + if (strchr ("{[(", edit_buffer_get_byte (&edit->buffer, pos)) != NULL) + { + off_t match; + + edit_cursor_move (edit, pos - edit->buffer.curs1); + match = edit_get_bracket (edit, 0, 0); + if (match >= 0) + { + long line2; + + line2 = edit_buffer_count_lines (&edit->buffer, 0, match); + if (line2 > line) + { + edit_fold_make (edit, line, line2 - line); + break; + } + } + } + } + } + } + + edit->mark1 = edit->mark2 = edit->buffer.curs1; + edit->force |= REDRAW_PAGE; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/filemanager/cmd.c b/src/filemanager/cmd.c index cb2bc49006..e0b7ce8737 100644 --- a/src/filemanager/cmd.c +++ b/src/filemanager/cmd.c @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -89,6 +90,8 @@ #include "cd.h" #include "ioblksize.h" // IO_BUFSIZE +#include "lib/panel-plugin.h" + #include "cmd.h" // Our definitions /*** global variables ****************************************************************************/ @@ -144,6 +147,36 @@ do_view_cmd (WPanel *panel, gboolean plain_view) cd_error_message (fe->fname->str); vfs_path_free (fname_vpath, TRUE); } + else if (panel->is_plugin_panel && panel->plugin != NULL && panel->plugin_data != NULL) + { + if (panel->plugin->get_local_copy == NULL) + { + message (D_ERROR, MSG_ERROR, _("This plugin does not support file viewing")); + return; + } + + { + char *local_path = NULL; + mc_pp_result_t r; + + r = panel->plugin->get_local_copy (panel->plugin_data, fe->fname->str, &local_path); + if (r != MC_PPR_OK || local_path == NULL) + { + message (D_ERROR, MSG_ERROR, _("Cannot get local copy of %s"), fe->fname->str); + return; + } + + { + vfs_path_t *local_vpath; + + local_vpath = vfs_path_from_str (local_path); + view_file (local_vpath, plain_view, use_internal_view); + vfs_path_free (local_vpath, TRUE); + } + unlink (local_path); + g_free (local_path); + } + } else { vfs_path_t *filename_vpath; @@ -680,6 +713,39 @@ edit_cmd (const WPanel *panel) if (fe == NULL) return; + if (panel->is_plugin_panel && panel->plugin != NULL && panel->plugin_data != NULL) + { + if (panel->plugin->get_local_copy == NULL) + { + message (D_ERROR, MSG_ERROR, _("This plugin does not support file editing")); + return; + } + + { + char *local_path = NULL; + mc_pp_result_t r; + + r = panel->plugin->get_local_copy (panel->plugin_data, fe->fname->str, &local_path); + if (r != MC_PPR_OK || local_path == NULL) + { + message (D_ERROR, MSG_ERROR, _("Cannot get local copy of %s"), fe->fname->str); + return; + } + + { + vfs_path_t *local_vpath; + + local_vpath = vfs_path_from_str (local_path); + if (regex_command (local_vpath, "Edit") == 0) + do_edit (local_vpath); + vfs_path_free (local_vpath, TRUE); + } + unlink (local_path); + g_free (local_path); + } + return; + } + fname = vfs_path_from_str (fe->fname->str); if (regex_command (fname, "Edit") == 0) do_edit (fname); @@ -699,6 +765,39 @@ edit_cmd_force_internal (const WPanel *panel) if (fe == NULL) return; + if (panel->is_plugin_panel && panel->plugin != NULL && panel->plugin_data != NULL) + { + if (panel->plugin->get_local_copy == NULL) + { + message (D_ERROR, MSG_ERROR, _("This plugin does not support file editing")); + return; + } + + { + char *local_path = NULL; + mc_pp_result_t r; + + r = panel->plugin->get_local_copy (panel->plugin_data, fe->fname->str, &local_path); + if (r != MC_PPR_OK || local_path == NULL) + { + message (D_ERROR, MSG_ERROR, _("Cannot get local copy of %s"), fe->fname->str); + return; + } + + { + vfs_path_t *local_vpath; + + local_vpath = vfs_path_from_str (local_path); + if (regex_command (local_vpath, "Edit") == 0) + edit_file_at_line (local_vpath, TRUE, 1); + vfs_path_free (local_vpath, TRUE); + } + unlink (local_path); + g_free (local_path); + } + return; + } + fname = vfs_path_from_str (fe->fname->str); if (regex_command (fname, "Edit") == 0) edit_file_at_line (fname, TRUE, 1); @@ -1427,3 +1526,320 @@ encoding_cmd (void) } /* --------------------------------------------------------------------------------------------- */ + +/** + * Copy a local file using POSIX read/write, preserving permissions. + * + * @param src source path (temp file from plugin) + * @param dest destination path + * @return TRUE on success + */ + +static gboolean +copy_local_file (const char *src, const char *dest) +{ + struct stat st; + int fd_src, fd_dest; + char buf[IO_BUFSIZE]; + ssize_t nread; + gboolean ok = TRUE; + + fd_src = open (src, O_RDONLY); + if (fd_src == -1) + return FALSE; + + if (fstat (fd_src, &st) != 0) + st.st_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + + fd_dest = open (dest, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode & 0777); + if (fd_dest == -1) + { + close (fd_src); + return FALSE; + } + + while ((nread = read (fd_src, buf, sizeof (buf))) > 0) + { + const char *p = buf; + ssize_t remaining = nread; + + while (remaining > 0) + { + ssize_t nw = write (fd_dest, p, remaining); + if (nw == -1) + { + ok = FALSE; + goto done; + } + p += nw; + remaining -= nw; + } + } + + if (nread == -1) + ok = FALSE; + + done: + close (fd_src); + close (fd_dest); + return ok; +} + +/* --------------------------------------------------------------------------------------------- */ + +/** + * Collect names of marked files into a GPtrArray. + * If no files are marked, adds the current file name. + * + * @return newly allocated GPtrArray with g_strdup'd names (caller frees with g_ptr_array_free) + */ + +static GPtrArray * +plugin_panel_collect_names (WPanel *panel) +{ + GPtrArray *names; + + names = g_ptr_array_new_with_free_func (g_free); + + if (panel->marked > 0) + { + int idx = 0; + const GString *fname; + + while ((fname = panel_find_marked_file (panel, &idx)) != NULL) + { + g_ptr_array_add (names, g_strdup (fname->str)); + idx++; + } + } + else + { + const file_entry_t *fe = panel_current_entry (panel); + + if (fe != NULL) + g_ptr_array_add (names, g_strdup (fe->fname->str)); + } + + return names; +} + +/* --------------------------------------------------------------------------------------------- */ + +void +plugin_panel_copy_cmd (WPanel *panel) +{ + char *dest_dir; + const char *default_dest; + GPtrArray *names; + guint i; + + if (panel->plugin == NULL || panel->plugin_data == NULL) + return; + + if (panel->plugin->get_local_copy == NULL) + { + message (D_ERROR, MSG_ERROR, _("This plugin does not support copying files")); + return; + } + + default_dest = vfs_path_as_str (other_panel->cwd_vpath); + dest_dir = + input_expand_dialog (_("Copy"), _("Copy to:"), MC_HISTORY_FM_PLUGIN_COPY, default_dest, + INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_CD); + if (dest_dir == NULL || dest_dir[0] == '\0') + { + g_free (dest_dir); + return; + } + + names = plugin_panel_collect_names (panel); + + for (i = 0; i < names->len; i++) + { + const char *name = (const char *) g_ptr_array_index (names, i); + char *local_path = NULL; + mc_pp_result_t r; + char *dest_path; + + r = panel->plugin->get_local_copy (panel->plugin_data, name, &local_path); + if (r != MC_PPR_OK || local_path == NULL) + { + message (D_ERROR, MSG_ERROR, _("Cannot get local copy of %s"), name); + continue; + } + + dest_path = mc_build_filename (dest_dir, name, (char *) NULL); + + if (!copy_local_file (local_path, dest_path)) + message (D_ERROR, MSG_ERROR, _("Cannot copy %s"), name); + + unlink (local_path); + g_free (local_path); + g_free (dest_path); + } + + g_ptr_array_free (names, TRUE); + g_free (dest_dir); + update_panels (UP_OPTIMIZE, UP_KEEPSEL); +} + +/* --------------------------------------------------------------------------------------------- */ + +void +plugin_panel_delete_cmd (WPanel *panel) +{ + GPtrArray *names; + mc_pp_result_t r; + + if (panel->plugin == NULL || panel->plugin_data == NULL) + return; + + if (panel->plugin->delete_items == NULL) + { + message (D_ERROR, MSG_ERROR, _("This plugin does not support deleting files")); + return; + } + + names = plugin_panel_collect_names (panel); + + if (names->len == 0) + { + g_ptr_array_free (names, TRUE); + return; + } + + /* Confirmation */ + if (confirm_delete) + { + int result; + + if (names->len == 1) + result = query_dialog (_("Delete"), + _("Delete file from plugin panel?"), + D_ERROR, 2, _("&Yes"), _("&No")); + else + result = query_dialog (_("Delete"), + _("Delete tagged files from plugin panel?"), + D_ERROR, 2, _("&Yes"), _("&No")); + + if (result != 0) + { + g_ptr_array_free (names, TRUE); + return; + } + } + + r = panel->plugin->delete_items (panel->plugin_data, + (const char **) names->pdata, (int) names->len); + if (r != MC_PPR_OK) + message (D_ERROR, MSG_ERROR, _("Delete failed")); + + g_ptr_array_free (names, TRUE); + update_panels (UP_OPTIMIZE, UP_KEEPSEL); +} + +/* --------------------------------------------------------------------------------------------- */ + +void +plugin_panel_create_cmd (WPanel *panel) +{ + mc_pp_result_t r; + + if (panel->plugin == NULL || panel->plugin_data == NULL) + return; + + if (panel->plugin->create_item == NULL + || (panel->plugin->flags & MC_PPF_CREATE) == 0) + { + message (D_ERROR, MSG_ERROR, _("This plugin does not support creating items")); + return; + } + + r = panel->plugin->create_item (panel->plugin_data); + if (r == MC_PPR_OK) + update_panels (UP_OPTIMIZE, UP_KEEPSEL); +} + +/* --------------------------------------------------------------------------------------------- */ + +void +plugin_panel_move_cmd (WPanel *panel) +{ + char *dest_dir; + const char *default_dest; + GPtrArray *names; + GPtrArray *moved_names; + guint i; + + if (panel->plugin == NULL || panel->plugin_data == NULL) + return; + + if (panel->plugin->get_local_copy == NULL || panel->plugin->delete_items == NULL) + { + message (D_ERROR, MSG_ERROR, _("This plugin does not support moving files")); + return; + } + + default_dest = vfs_path_as_str (other_panel->cwd_vpath); + dest_dir = + input_expand_dialog (_("Move"), _("Move to:"), MC_HISTORY_FM_PLUGIN_MOVE, default_dest, + INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_CD); + if (dest_dir == NULL || dest_dir[0] == '\0') + { + g_free (dest_dir); + return; + } + + names = plugin_panel_collect_names (panel); + moved_names = g_ptr_array_new_with_free_func (g_free); + + for (i = 0; i < names->len; i++) + { + const char *name = (const char *) g_ptr_array_index (names, i); + char *local_path = NULL; + mc_pp_result_t r; + char *dest_path; + + r = panel->plugin->get_local_copy (panel->plugin_data, name, &local_path); + if (r != MC_PPR_OK || local_path == NULL) + { + message (D_ERROR, MSG_ERROR, _("Cannot get local copy of %s"), name); + continue; + } + + dest_path = mc_build_filename (dest_dir, name, (char *) NULL); + + if (!copy_local_file (local_path, dest_path)) + { + message (D_ERROR, MSG_ERROR, _("Cannot copy %s"), name); + unlink (local_path); + g_free (local_path); + g_free (dest_path); + continue; + } + + unlink (local_path); + g_free (local_path); + g_free (dest_path); + g_ptr_array_add (moved_names, g_strdup (name)); + } + + /* Delete successfully moved files from the plugin */ + if (moved_names->len > 0) + { + mc_pp_result_t r; + + r = panel->plugin->delete_items (panel->plugin_data, + (const char **) moved_names->pdata, + (int) moved_names->len); + if (r != MC_PPR_OK) + message (D_ERROR, MSG_ERROR, _("Move succeeded but delete from plugin failed")); + } + + g_ptr_array_free (names, TRUE); + g_ptr_array_free (moved_names, TRUE); + g_free (dest_dir); + update_panels (UP_OPTIMIZE, UP_KEEPSEL); +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/filemanager/cmd.h b/src/filemanager/cmd.h index 93e95c3d26..4a5dfb7555 100644 --- a/src/filemanager/cmd.h +++ b/src/filemanager/cmd.h @@ -94,6 +94,12 @@ const char *chattr_get_as_str (unsigned long attr); /* find.c */ void find_cmd (WPanel *panel); +/* plugin panel file operations */ +void plugin_panel_copy_cmd (WPanel *panel); +void plugin_panel_move_cmd (WPanel *panel); +void plugin_panel_delete_cmd (WPanel *panel); +void plugin_panel_create_cmd (WPanel *panel); + /* --------------------------------------------------------------------------------------------- */ /*** inline functions ****************************************************************************/ /* --------------------------------------------------------------------------------------------- */ diff --git a/src/filemanager/filemanager.c b/src/filemanager/filemanager.c index 3b3a810f40..c56baf015e 100644 --- a/src/filemanager/filemanager.c +++ b/src/filemanager/filemanager.c @@ -63,6 +63,7 @@ #include "lib/keybind.h" #include "lib/event.h" +#include "lib/panel-plugin.h" #include "tree.h" #include "boxes.h" // sort_box(), tree_box() @@ -196,6 +197,7 @@ create_panel_menu (void) entries = g_list_prepend (entries, menu_entry_new (_ ("&Info"), CK_PanelInfo)); entries = g_list_prepend (entries, menu_entry_new (_ ("&Tree"), CK_PanelTree)); entries = g_list_prepend (entries, menu_entry_new (_ ("Paneli&ze"), CK_Panelize)); + entries = g_list_prepend (entries, menu_entry_new (_ ("Pl&ugin panel..."), CK_PanelPlugin)); entries = g_list_prepend (entries, menu_separator_new ()); entries = g_list_prepend (entries, menu_entry_new (_ ("&Listing format..."), CK_SetupListingFormat)); @@ -875,6 +877,8 @@ create_file_manager (void) group_add_widget (g, the_menubar); init_menu (); + mc_panel_plugins_load (); + create_panels (); group_add_widget (g, get_panel_widget (0)); group_add_widget (g, get_panel_widget (1)); @@ -1097,6 +1101,45 @@ midnight_execute_cmd (Widget *sender, long command) // stop quick search before executing any command send_message (current_panel, NULL, MSG_ACTION, CK_SearchStop, NULL); + /* Block destructive/inapplicable file operations on plugin panels — the listed + entries are virtual and do not exist on the real filesystem. + View and Edit are allowed (the viewer/editor will handle missing files). */ + if (current_panel->is_plugin_panel) + { + switch (command) + { + case CK_Copy: + plugin_panel_copy_cmd (current_panel); + return MSG_HANDLED; + case CK_Move: + plugin_panel_move_cmd (current_panel); + return MSG_HANDLED; + case CK_Delete: + plugin_panel_delete_cmd (current_panel); + return MSG_HANDLED; + case CK_ChangeMode: + case CK_ChangeOwn: + case CK_ChangeOwnAdvanced: +#ifdef ENABLE_EXT2FS_ATTR + case CK_ChangeAttributes: +#endif + case CK_Link: + case CK_LinkSymbolic: + case CK_LinkSymbolicRelative: + case CK_LinkSymbolicEdit: + case CK_DirSize: + case CK_CompareDirs: +#ifdef USE_DIFF_VIEW + case CK_CompareFiles: +#endif + message (D_ERROR, MSG_ERROR, + _ ("This operation is not supported for plugin panels")); + return MSG_HANDLED; + default: + break; + } + } + switch (command) { case CK_About: @@ -1226,6 +1269,9 @@ midnight_execute_cmd (Widget *sender, long command) case CK_Panelize: panel_panelize_restore (); break; + case CK_PanelPlugin: + panel_plugin_select_and_activate (current_panel); + break; case CK_Help: help_cmd (); break; @@ -1564,6 +1610,7 @@ midnight_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void * return midnight_execute_cmd (sender, parm); case MSG_DESTROY: + mc_panel_plugins_shutdown (); panel_deinit (); return MSG_HANDLED; diff --git a/src/filemanager/layout.c b/src/filemanager/layout.c index ff2c9fdad8..af56c58b8e 100644 --- a/src/filemanager/layout.c +++ b/src/filemanager/layout.c @@ -1278,6 +1278,10 @@ swap_panels (void) panelswap (current); panelswap (is_panelized); panelswap (panelized_descr); + panelswap (is_plugin_panel); + panelswap (plugin); + panelswap (plugin_data); + panelswap (plugin_host); panelswap (dir_stat); #undef panelswap diff --git a/src/filemanager/panel.c b/src/filemanager/panel.c index af14fa0f6c..d62535232a 100644 --- a/src/filemanager/panel.c +++ b/src/filemanager/panel.c @@ -52,6 +52,7 @@ #include "lib/widget.h" #include "lib/charsets.h" // get_codepage_id () #include "lib/event.h" +#include "lib/panel-plugin.h" #include "src/setup.h" // For loading/saving panel options #include "src/execute.h" @@ -160,6 +161,8 @@ static const char *string_marked (const file_entry_t *fe, int len); static const char *string_space (const file_entry_t *fe, int len); static const char *string_dot (const file_entry_t *fe, int len); +static void panel_plugin_reload (WPanel *panel); + /*** file scope variables ************************************************************************/ static panel_field_t panel_fields[] = { @@ -271,6 +274,43 @@ panelized_descr_free (panelized_descr_t *p) /* --------------------------------------------------------------------------------------------- */ +static char * +plugin_title_last_component (const char *title) +{ + char *tmp, *base, *ret; + size_t len; + + if (title == NULL || title[0] == '\0') + return NULL; + + tmp = g_strdup (title); + len = strlen (tmp); + + while (len > 1 && tmp[len - 1] == '/') + { + tmp[len - 1] = '\0'; + len--; + } + + base = strrchr (tmp, '/'); + if (base != NULL) + base++; + else + base = tmp; + + if (base[0] == '\0') + { + g_free (tmp); + return NULL; + } + + ret = g_strdup (base); + g_free (tmp); + return ret; +} + +/* --------------------------------------------------------------------------------------------- */ + static void set_colors (const WPanel *panel) { @@ -1258,7 +1298,31 @@ show_dir (const WPanel *panel) tty_setcolor (CORE_NORMAL_COLOR); widget_gotoyx (w, 0, 3); - if (panel->is_panelized) + if (panel->is_plugin_panel && panel->plugin != NULL) + { + const char *title = NULL; + + if (panel->plugin->get_title != NULL && panel->plugin_data != NULL) + title = panel->plugin->get_title (panel->plugin_data); + + if (panel->plugin->proto != NULL) + { + /* format as "Proto:/path" */ + if (title != NULL) + tty_printf (" %s:%s ", panel->plugin->proto, title); + else + tty_printf (" %s:/ ", panel->plugin->proto); + } + else + { + if (title == NULL) + title = panel->plugin->display_name; + if (title == NULL) + title = panel->plugin->name; + tty_printf (" %s ", title); + } + } + else if (panel->is_panelized) tty_printf (" %s ", _ ("Panelize")); else { @@ -1274,9 +1338,12 @@ show_dir (const WPanel *panel) if (panel->active) tty_setcolor (CORE_REVERSE_COLOR); - tmp = panel_correct_path_to_show (panel); - tty_printf (" %s ", str_term_trim (tmp, MIN (MAX (w->rect.cols - 12, 0), w->rect.cols))); - g_free (tmp); + if (!panel->is_plugin_panel) + { + tmp = panel_correct_path_to_show (panel); + tty_printf (" %s ", str_term_trim (tmp, MIN (MAX (w->rect.cols - 12, 0), w->rect.cols))); + g_free (tmp); + } if (!panels_options.show_mini_info) { @@ -2270,6 +2337,47 @@ prev_page (WPanel *panel) static void goto_parent_dir (WPanel *panel) { + if (panel->is_plugin_panel && panel->plugin != NULL && panel->plugin_data != NULL) + { + char *focus_name = NULL; + + if (panel->plugin->get_title != NULL) + { + const char *title = panel->plugin->get_title (panel->plugin_data); + focus_name = plugin_title_last_component (title); + } + + if (panel->plugin->chdir != NULL && (panel->plugin->flags & MC_PPF_NAVIGATE) != 0) + { + mc_pp_result_t r; + + r = panel->plugin->chdir (panel->plugin_data, ".."); + if (r == MC_PPR_OK) + { + panel_plugin_reload (panel); + if (focus_name != NULL) + panel_set_current_by_name (panel, focus_name); + g_free (focus_name); + return; + } + if (r == MC_PPR_NOT_SUPPORTED) + { + g_free (focus_name); + panel_plugin_close (panel); + return; + } + } + else + { + g_free (focus_name); + // no navigation support — close plugin panel + panel_plugin_close (panel); + return; + } + + g_free (focus_name); + } + if (!panel->is_panelized) cd_up_dir (panel); else @@ -2939,10 +3047,65 @@ static inline gboolean do_enter (WPanel *panel) { const file_entry_t *fe; + char *focus_name = NULL; fe = panel_current_entry (panel); + if (fe == NULL) + return FALSE; + + if (panel->is_plugin_panel && panel->plugin != NULL && panel->plugin_data != NULL) + { + if (panel->plugin->get_title != NULL && fe->fname != NULL && fe->fname->str != NULL + && strcmp (fe->fname->str, "..") == 0) + { + const char *title = panel->plugin->get_title (panel->plugin_data); + focus_name = plugin_title_last_component (title); + } + + // let plugin handle enter first + if (panel->plugin->enter != NULL) + { + mc_pp_result_t r; + + r = panel->plugin->enter (panel->plugin_data, fe->fname->str, &fe->st); + if (r == MC_PPR_OK) + { + panel_plugin_reload (panel); + if (focus_name != NULL) + panel_set_current_by_name (panel, focus_name); + g_free (focus_name); + return TRUE; + } + } - return (fe == NULL ? FALSE : do_enter_on_file_entry (panel, fe)); + // try chdir for directories (st_mode == 0 covers ".." from dir_list_init) + if ((S_ISDIR (fe->st.st_mode) || link_isdir (fe) || fe->st.st_mode == 0) + && panel->plugin->chdir != NULL && (panel->plugin->flags & MC_PPF_NAVIGATE) != 0) + { + mc_pp_result_t r; + + r = panel->plugin->chdir (panel->plugin_data, fe->fname->str); + if (r == MC_PPR_OK) + { + panel_plugin_reload (panel); + if (focus_name != NULL) + panel_set_current_by_name (panel, focus_name); + g_free (focus_name); + return TRUE; + } + if (r == MC_PPR_NOT_SUPPORTED) + { + g_free (focus_name); + panel_plugin_close (panel); + return TRUE; + } + } + + g_free (focus_name); + return TRUE; // consume the key even if plugin didn't handle it + } + + return do_enter_on_file_entry (panel, fe); } /* --------------------------------------------------------------------------------------------- */ @@ -3579,7 +3742,10 @@ panel_execute_cmd (WPanel *panel, long command) view_raw_cmd (panel); break; case CK_EditNew: - edit_cmd_new (); + if (panel->is_plugin_panel) + plugin_panel_create_cmd (panel); + else + edit_cmd_new (); break; case CK_MoveSingle: rename_cmd_local (panel); @@ -3724,6 +3890,96 @@ panel_execute_cmd (WPanel *panel, long command) return res; } +/* --------------------------------------------------------------------------------------------- */ +/* Panel plugin host callbacks and support */ +/* --------------------------------------------------------------------------------------------- */ + +static void +host_refresh_impl (mc_panel_host_t *host) +{ + WPanel *panel = (WPanel *) host->host_data; + panel->dirty = TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +host_set_hint_impl (mc_panel_host_t *host, const char *text) +{ + (void) host; + set_hintbar (text); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +host_message_impl (mc_panel_host_t *host, int flags, const char *title, const char *text) +{ + (void) host; + message (flags, title, "%s", text); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +host_close_plugin_impl (mc_panel_host_t *host, const char *dir_path) +{ + WPanel *panel = (WPanel *) host->host_data; + + (void) dir_path; + panel_plugin_close (panel); +} + +/* --------------------------------------------------------------------------------------------- */ + +static int +host_get_marked_count_impl (mc_panel_host_t *host) +{ + WPanel *panel = (WPanel *) host->host_data; + return panel->marked; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const GString * +host_get_next_marked_impl (mc_panel_host_t *host, int *current) +{ + WPanel *panel = (WPanel *) host->host_data; + return panel_get_marked_file (panel, current); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +panel_plugin_reload (WPanel *panel) +{ + gboolean was_dotdot = FALSE; + + if (!panel->is_plugin_panel || panel->plugin == NULL || panel->plugin_data == NULL) + return; + + { + const file_entry_t *fe = panel_current_entry (panel); + if (fe != NULL && fe->fname != NULL && fe->fname->str != NULL && strcmp (fe->fname->str, "..") == 0) + was_dotdot = TRUE; + } + + panel_clean_dir (panel); + // panel_clean_dir resets is_panelized; restore plugin state + panel->is_panelized = TRUE; + panel->is_plugin_panel = TRUE; + + dir_list_init (&panel->dir); + panel->plugin->get_items (panel->plugin_data, &panel->dir); + + panel_re_sort (panel); + + if (was_dotdot && panel->dir.len > 1) + panel_set_current (panel, 1); + + panel->dirty = TRUE; +} + /* --------------------------------------------------------------------------------------------- */ static cb_ret_t @@ -3823,12 +4079,25 @@ panel_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *dat return MSG_HANDLED; case MSG_KEY: + // let plugin try to handle the key first + if (panel->is_plugin_panel && panel->plugin != NULL && panel->plugin_data != NULL + && panel->plugin->handle_key != NULL) + { + if (panel->plugin->handle_key (panel->plugin_data, parm) == MC_PPR_OK) + { + panel_plugin_reload (panel); + return MSG_HANDLED; + } + } return panel_key (panel, parm); case MSG_ACTION: return panel_execute_cmd (panel, parm); case MSG_DESTROY: + // close plugin panel if active + if (panel->is_plugin_panel) + panel_plugin_close (panel); vfs_stamp_path (panel->cwd_vpath); // unsubscribe from "history_load" event mc_event_del (h->event_group, MCEVENT_HISTORY_LOAD, panel_load_history, w); @@ -4176,6 +4445,26 @@ update_one_panel_widget (WPanel *panel, panel_update_flags_t flags, const char * gboolean free_pointer; char *my_current_file = NULL; + if (panel->is_plugin_panel) + { + const file_entry_t *fe; + char *saved_name = NULL; + + if (current_file == UP_KEEPSEL) + { + fe = panel_current_entry (panel); + if (fe != NULL) + saved_name = g_strndup (fe->fname->str, fe->fname->len); + current_file = saved_name; + } + + panel_plugin_reload (panel); + panel_set_current_by_name (panel, current_file); + panel->dirty = TRUE; + g_free (saved_name); + return; + } + if ((flags & UP_RELOAD) != 0) { panel->is_panelized = FALSE; @@ -5387,6 +5676,9 @@ panel_panelize_save (WPanel *panel) dir_list *list = &panel->dir; dir_list *plist; + if (panel->is_plugin_panel) + return; + panel_panelize_change_root (panel, panel->cwd_vpath); plist = &panel->panelized_descr->list; @@ -5462,7 +5754,7 @@ panel_cd (WPanel *panel, const vfs_path_t *new_dir_vpath, enum cd_enum exact) gboolean res; const vfs_path_t *_new_dir_vpath = new_dir_vpath; - if (panel->is_panelized) + if (panel->is_panelized && panel->panelized_descr != NULL) { size_t new_vpath_len; @@ -5488,3 +5780,130 @@ panel_cd (WPanel *panel, const vfs_path_t *new_dir_vpath, enum cd_enum exact) } /* --------------------------------------------------------------------------------------------- */ + +void +panel_plugin_activate (WPanel *panel, const mc_panel_plugin_t *plugin, const char *open_path) +{ + mc_panel_host_t *host; + + if (panel == NULL || plugin == NULL) + return; + + // close any previous plugin + if (panel->is_plugin_panel) + panel_plugin_close (panel); + + // build host interface + host = g_new0 (mc_panel_host_t, 1); + host->refresh = host_refresh_impl; + host->set_hint = host_set_hint_impl; + host->message = host_message_impl; + host->close_plugin = host_close_plugin_impl; + host->get_marked_count = host_get_marked_count_impl; + host->get_next_marked = host_get_next_marked_impl; + host->host_data = panel; + + panel->plugin_data = plugin->open (host, open_path); + if (panel->plugin_data == NULL) + { + g_free (host); + return; + } + + panel->plugin = plugin; + panel->plugin_host = host; + panel->is_plugin_panel = TRUE; + panel->is_panelized = TRUE; + + // populate dir list + panel_clean_dir (panel); + // panel_clean_dir resets flags — restore them + panel->is_panelized = TRUE; + panel->is_plugin_panel = TRUE; + + dir_list_init (&panel->dir); + plugin->get_items (panel->plugin_data, &panel->dir); + + panel_re_sort (panel); + panel->dirty = TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ + +void +panel_plugin_close (WPanel *panel) +{ + if (panel == NULL || !panel->is_plugin_panel) + return; + + if (panel->plugin != NULL && panel->plugin_data != NULL) + panel->plugin->close (panel->plugin_data); + + panel->plugin = NULL; + panel->plugin_data = NULL; + panel->is_plugin_panel = FALSE; + + g_free (panel->plugin_host); + panel->plugin_host = NULL; + + panel->is_panelized = FALSE; + panel_reload (panel); +} + +/* --------------------------------------------------------------------------------------------- */ + +void +panel_plugin_select_and_activate (WPanel *panel) +{ + const GSList *plugins; + const GSList *iter; + Listbox *listbox; + int count = 0; + int result; + + plugins = mc_panel_plugin_list (); + if (plugins == NULL) + { + message (D_ERROR, _ ("Plugin panel"), "%s", _ ("No panel plugins are loaded.")); + return; + } + + listbox = listbox_window_new (12, 40, _ ("Plugin panel"), "[Panel Plugins]"); + + for (iter = plugins; iter != NULL; iter = g_slist_next (iter)) + { + const mc_panel_plugin_t *p = (const mc_panel_plugin_t *) iter->data; + const char *label = p->display_name != NULL ? p->display_name : p->name; + + listbox_add_item (listbox->list, LISTBOX_APPEND_AT_END, 0, label, (void *) p, FALSE); + count++; + } + + if (count == 0) + { + // shouldn't happen since we checked above, but be safe + widget_destroy (WIDGET (listbox->dlg)); + return; + } + + result = listbox_run (listbox); + if (result >= 0) + { + const mc_panel_plugin_t *selected = NULL; + int i = 0; + + for (iter = plugins; iter != NULL; iter = g_slist_next (iter), i++) + { + if (i == result) + { + selected = (const mc_panel_plugin_t *) iter->data; + break; + } + } + + if (selected != NULL) + panel_plugin_activate (panel, selected, NULL); + } +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/filemanager/panel.h b/src/filemanager/panel.h index 05673b69e2..f5a81e5164 100644 --- a/src/filemanager/panel.h +++ b/src/filemanager/panel.h @@ -14,6 +14,7 @@ #include "lib/widget.h" // Widget #include "lib/filehighlight.h" #include "lib/file-entry.h" +#include "lib/panel-plugin.h" #include "dir.h" // dir_list @@ -91,6 +92,11 @@ typedef struct gboolean is_panelized; // Panelization: special mode, can't reload the file list panelized_descr_t *panelized_descr; // Panelization descriptor + gboolean is_plugin_panel; // TRUE when driven by a panel plugin + const mc_panel_plugin_t *plugin; // active plugin descriptor, or NULL + void *plugin_data; // instance handle from plugin->open() + mc_panel_host_t *plugin_host; // host interface given to the plugin + int codepage; // Panel codepage dir_list dir; // Directory contents @@ -200,6 +206,11 @@ void panel_panelize_save (WPanel *panel); void panel_init (void); void panel_deinit (void); +void panel_plugin_activate (WPanel *panel, const mc_panel_plugin_t *plugin, + const char *open_path); +void panel_plugin_close (WPanel *panel); +void panel_plugin_select_and_activate (WPanel *panel); + /* --------------------------------------------------------------------------------------------- */ /*** inline functions ****************************************************************************/ /* --------------------------------------------------------------------------------------------- */ diff --git a/src/history.h b/src/history.h index 23b647222f..2ea8fc22ab 100644 --- a/src/history.h +++ b/src/history.h @@ -25,6 +25,8 @@ #define MC_HISTORY_FM_TREE_COPY "mc.fm.tree-copy" #define MC_HISTORY_FM_TREE_MOVE "mc.fm.tree-move" #define MC_HISTORY_FM_PANELIZE_ADD "mc.fm.panelize.add" +#define MC_HISTORY_FM_PLUGIN_COPY "mc.fm.plugin-copy" +#define MC_HISTORY_FM_PLUGIN_MOVE "mc.fm.plugin-move" #define MC_HISTORY_FM_FILTERED_VIEW "mc.fm.filtered-view" #define MC_HISTORY_FM_PANEL_SELECT ":select_cmd: Select " #define MC_HISTORY_FM_PANEL_UNSELECT ":select_cmd: Unselect " diff --git a/src/keymap.c b/src/keymap.c index 892aa4c7a8..b323c6690a 100644 --- a/src/keymap.c +++ b/src/keymap.c @@ -509,6 +509,8 @@ static const global_keymap_ini_t default_editor_keymap[] = { { "Sort", "alt-t" }, { "Mail", "alt-m" }, { "ExternalCommand", "alt-u" }, + { "FoldToggle", "alt-shift-f" }, + { "UnfoldAll", "alt-shift-u" }, #ifdef HAVE_ASPELL { "SpellCheckCurrentWord", "ctrl-p" }, #endif diff --git a/src/learn.c b/src/learn.c index 3380de3a64..0a66128179 100644 --- a/src/learn.c +++ b/src/learn.c @@ -63,6 +63,7 @@ typedef struct Widget *label; gboolean ok; char *sequence; + int modifiers; } learnkey_t; /*** forward declarations (file scope functions) *************************************************/ @@ -76,6 +77,55 @@ static learnkey_t *learnkeys = NULL; static int learn_total; static int learnok; static gboolean learnchanged = FALSE; +static unsigned long learn_mod_ctrl_id = 0; +static unsigned long learn_mod_meta_id = 0; +static unsigned long learn_mod_shift_id = 0; + +/* --------------------------------------------------------------------------------------------- */ + +static int +learn_selected_modifiers (const WDialog *dlg) +{ + int modifiers = 0; + Widget *w; + + if (dlg == NULL) + return 0; + + w = widget_find_by_id (WIDGET (dlg), learn_mod_ctrl_id); + if (w != NULL && CHECK (w)->state) + modifiers |= KEY_M_CTRL; + + w = widget_find_by_id (WIDGET (dlg), learn_mod_meta_id); + if (w != NULL && CHECK (w)->state) + modifiers |= KEY_M_ALT; + + w = widget_find_by_id (WIDGET (dlg), learn_mod_shift_id); + if (w != NULL && CHECK (w)->state) + modifiers |= KEY_M_SHIFT; + + return modifiers; +} + +/* --------------------------------------------------------------------------------------------- */ + +static char * +learn_build_key_name (const char *base_name, int modifiers) +{ + GString *name; + + name = g_string_sized_new (32); + + if ((modifiers & KEY_M_CTRL) != 0) + g_string_append (name, "ctrl-"); + if ((modifiers & KEY_M_ALT) != 0) + g_string_append (name, "meta-"); + if ((modifiers & KEY_M_SHIFT) != 0) + g_string_append (name, "shift-"); + + g_string_append (name, base_name); + return g_string_free (name, FALSE); +} /* --------------------------------------------------------------------------------------------- */ /*** file scope functions ************************************************************************/ @@ -86,6 +136,8 @@ learn_button (WButton *button, int action) { WDialog *d; char *seq; + int modifiers; + int keycode; (void) button; @@ -109,13 +161,17 @@ learn_button (WButton *button, int action) */ gboolean seq_ok = FALSE; + modifiers = learn_selected_modifiers (DIALOG (WIDGET (button)->owner)); + keycode = key_name_conv_tab[action - B_USER].code | modifiers; + if (strcmp (seq, "\\e") != 0 && strcmp (seq, "\\e\\e") != 0 && strcmp (seq, "^m") != 0 && strcmp (seq, "^i") != 0 && (seq[1] != '\0' || *seq < ' ' || *seq > '~')) { learnchanged = TRUE; learnkeys[action - B_USER].sequence = seq; + learnkeys[action - B_USER].modifiers = modifiers; seq = convert_controls (seq); - seq_ok = define_sequence (key_name_conv_tab[action - B_USER].code, seq, MCKEY_NOACTION); + seq_ok = define_sequence (keycode, seq, MCKEY_NOACTION); } if (!seq_ok) @@ -176,7 +232,7 @@ learn_check_key (int c) for (i = 0; i < learn_total; i++) { - if (key_name_conv_tab[i].code != c || learnkeys[i].ok) + if (((key_name_conv_tab[i].code | learnkeys[i].modifiers) != c) || learnkeys[i].ok) continue; widget_select (learnkeys[i].button); @@ -253,7 +309,7 @@ init_learn (void) WGroup *g; const int dlg_width = 78; - const int dlg_height = 23; + const int dlg_height = 25; // buttons WButton *bt0, *bt1; @@ -262,6 +318,9 @@ init_learn (void) int x, y, i; const key_code_name_t *key; + WCheck *mod_ctrl; + WCheck *mod_meta; + WCheck *mod_shift; #ifdef ENABLE_NLS static gboolean i18n_flag = FALSE; @@ -300,6 +359,7 @@ init_learn (void) learnkeys[i].ok = FALSE; learnkeys[i].sequence = NULL; + learnkeys[i].modifiers = 0; label = _ (key_name_conv_tab[i].longname); padding = 16 - str_term_width1 (label); @@ -320,13 +380,26 @@ init_learn (void) } } + group_add_widget (g, hline_new (dlg_height - 10, -1, -1)); + group_add_widget (g, label_new (dlg_height - 9, 3, _ ("With modifiers"))); + + mod_ctrl = check_new (dlg_height - 9, 22, FALSE, _ ("C&trl")); + mod_meta = check_new (dlg_height - 9, 34, FALSE, _ ("&Meta (Alt)")); + mod_shift = check_new (dlg_height - 9, 54, FALSE, _ ("&Shift")); + learn_mod_ctrl_id = WIDGET (mod_ctrl)->id; + learn_mod_meta_id = WIDGET (mod_meta)->id; + learn_mod_shift_id = WIDGET (mod_shift)->id; + group_add_widget (g, mod_ctrl); + group_add_widget (g, mod_meta); + group_add_widget (g, mod_shift); + group_add_widget (g, hline_new (dlg_height - 8, -1, -1)); group_add_widget ( g, label_new (dlg_height - 7, 5, _ ("Press all the keys mentioned here. After you have done it, check\n" - "which keys are not marked with OK. Press space on the missing\n" - "key, or click with the mouse to define it. Move around with Tab."))); + "which keys are not marked with OK. Press space on the missing key\n" + "or click with the mouse to define it. Move around with Tab."))); group_add_widget (g, hline_new (dlg_height - 4, -1, -1)); // buttons bt0 = button_new (dlg_height - 3, 1, B_ENTER, DEFPUSH_BUTTON, b0, NULL); @@ -369,10 +442,12 @@ learn_save (void) if (learnkeys[i].sequence != NULL) { char *esc_str; + char *key_name; esc_str = str_escape (learnkeys[i].sequence, -1, ";\\", TRUE); - mc_config_set_string_raw_value (mc_global.main_config, section, - key_name_conv_tab[i].name, esc_str); + key_name = learn_build_key_name (key_name_conv_tab[i].name, learnkeys[i].modifiers); + mc_config_set_string_raw_value (mc_global.main_config, section, key_name, esc_str); + g_free (key_name); g_free (esc_str); profile_changed = TRUE; diff --git a/src/main.c b/src/main.c index ce12c3cbee..b961c5e0ef 100644 --- a/src/main.c +++ b/src/main.c @@ -304,6 +304,7 @@ main (int argc, char *argv[]) vfs_expire (TRUE); (void) my_rmdir (tmpdir); + vfs_plugins_done (); vfs_shut (); done_setup (); g_free (saved_other_dir); @@ -445,6 +446,7 @@ main (int argc, char *argv[]) (void) my_rmdir (tmpdir); // Virtual File System shutdown + vfs_plugins_done (); vfs_shut (); flush_extension_file (); // does only free memory diff --git a/src/panel-plugins/Makefile.am b/src/panel-plugins/Makefile.am new file mode 100644 index 0000000000..d635f0b467 --- /dev/null +++ b/src/panel-plugins/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS = systemd + +if ENABLE_VFS_SFTP +SUBDIRS += sftp +endif + +SUBDIRS += docker + +DIST_SUBDIRS = systemd sftp docker diff --git a/src/panel-plugins/docker/Makefile.am b/src/panel-plugins/docker/Makefile.am new file mode 100644 index 0000000000..57dfcfec3f --- /dev/null +++ b/src/panel-plugins/docker/Makefile.am @@ -0,0 +1,7 @@ +panelplugindir = $(panel_plugins_dir) +panelplugin_LTLIBRARIES = mc-panel-docker.la + +mc_panel_docker_la_SOURCES = docker.c +mc_panel_docker_la_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) +mc_panel_docker_la_LDFLAGS = -module -avoid-version +mc_panel_docker_la_LIBADD = $(GLIB_LIBS) diff --git a/src/panel-plugins/docker/docker.c b/src/panel-plugins/docker/docker.c new file mode 100644 index 0000000000..e41d6d1cd8 --- /dev/null +++ b/src/panel-plugins/docker/docker.c @@ -0,0 +1,1250 @@ +/* + Docker resources panel plugin. + + Copyright (C) 2026 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include + +#include +#include +#include +#include +#include + +#include "lib/global.h" +#include "lib/panel-plugin.h" +#include "lib/widget.h" + +#include "src/filemanager/dir.h" + +/*** file scope type declarations ****************************************************************/ + +typedef enum +{ + DOCKER_VIEW_ROOT = 0, + DOCKER_VIEW_CONTAINERS_PROJECTS, + DOCKER_VIEW_CONTAINERS_ITEMS, + DOCKER_VIEW_CONTAINER_DETAILS, + DOCKER_VIEW_IMAGES, + DOCKER_VIEW_VOLUMES, + DOCKER_VIEW_NETWORKS +} docker_view_t; + +typedef struct +{ + char *name; /* display name */ + char *id; /* real object id/name used by docker CLI */ + gboolean is_dir; + off_t size; +} docker_item_t; + +typedef struct +{ + mc_panel_host_t *host; + + docker_view_t view; + GPtrArray *items; /* docker_item_t* for current view */ + + char *root_focus; + char *current_project; + char *current_container_id; + char *current_container_name; + + char *title_buf; +} docker_data_t; + +/*** forward declarations (file scope functions) *************************************************/ + +static void *docker_open (mc_panel_host_t *host, const char *open_path); +static void docker_close (void *plugin_data); +static mc_pp_result_t docker_get_items (void *plugin_data, void *list_ptr); +static mc_pp_result_t docker_chdir (void *plugin_data, const char *path); +static mc_pp_result_t docker_enter (void *plugin_data, const char *name, const struct stat *st); +static mc_pp_result_t docker_get_local_copy (void *plugin_data, const char *fname, char **local_path); +static mc_pp_result_t docker_delete_items (void *plugin_data, const char **names, int count); +static const char *docker_get_title (void *plugin_data); +static mc_pp_result_t docker_create_item (void *plugin_data); + +/*** file scope variables ************************************************************************/ + +static const char docker_daemon_info_file[] = "daemon-info.txt"; +static const char docker_version_file[] = "version.txt"; +static const char docker_inspect_file[] = "inspect.json"; +static const char docker_ungrouped_project[] = "ungrouped"; + +static const mc_panel_plugin_t docker_plugin = { + .api_version = MC_PANEL_PLUGIN_API_VERSION, + .name = "docker", + .display_name = "Docker", + .proto = "docker", + .prefix = NULL, + .flags = + MC_PPF_NAVIGATE | MC_PPF_GET_FILES | MC_PPF_DELETE | MC_PPF_CUSTOM_TITLE | MC_PPF_CREATE, + + .open = docker_open, + .close = docker_close, + .get_items = docker_get_items, + + .chdir = docker_chdir, + .enter = docker_enter, + .get_local_copy = docker_get_local_copy, + .delete_items = docker_delete_items, + .get_title = docker_get_title, + .handle_key = NULL, + .create_item = docker_create_item, +}; + +/*** file scope functions ************************************************************************/ + +static void +add_entry (dir_list *list, const char *name, mode_t mode, off_t size) +{ + struct stat st; + + memset (&st, 0, sizeof (st)); + st.st_mode = mode; + st.st_size = size; + st.st_mtime = time (NULL); + st.st_uid = getuid (); + st.st_gid = getgid (); + st.st_nlink = 1; + + dir_list_append (list, name, &st, S_ISDIR (mode), FALSE); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +docker_item_free (gpointer p) +{ + docker_item_t *item = (docker_item_t *) p; + + g_free (item->name); + g_free (item->id); + g_free (item); +} + +/* --------------------------------------------------------------------------------------------- */ + +static off_t +parse_size_to_bytes (const char *text) +{ + char *endptr = NULL; + double value; + double mult = 1.0; + const char *unit; + + if (text == NULL || *text == '\0') + return 0; + + value = g_ascii_strtod (text, &endptr); + if (endptr == text) + return 0; + + while (*endptr == ' ') + endptr++; + unit = endptr; + + if (g_ascii_strcasecmp (unit, "B") == 0 || *unit == '\0') + mult = 1.0; + else if (g_ascii_strcasecmp (unit, "KB") == 0) + mult = 1000.0; + else if (g_ascii_strcasecmp (unit, "MB") == 0) + mult = 1000.0 * 1000.0; + else if (g_ascii_strcasecmp (unit, "GB") == 0) + mult = 1000.0 * 1000.0 * 1000.0; + else if (g_ascii_strcasecmp (unit, "TB") == 0) + mult = 1000.0 * 1000.0 * 1000.0 * 1000.0; + else if (g_ascii_strcasecmp (unit, "KiB") == 0) + mult = 1024.0; + else if (g_ascii_strcasecmp (unit, "MiB") == 0) + mult = 1024.0 * 1024.0; + else if (g_ascii_strcasecmp (unit, "GiB") == 0) + mult = 1024.0 * 1024.0 * 1024.0; + else if (g_ascii_strcasecmp (unit, "TiB") == 0) + mult = 1024.0 * 1024.0 * 1024.0 * 1024.0; + + return (off_t) (value * mult); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +clear_items (docker_data_t *data) +{ + if (data->items != NULL) + { + g_ptr_array_free (data->items, TRUE); + data->items = NULL; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static const docker_item_t * +find_item_by_name (const docker_data_t *data, const char *name) +{ + guint i; + + if (data->items == NULL) + return NULL; + + for (i = 0; i < data->items->len; i++) + { + const docker_item_t *item = (const docker_item_t *) g_ptr_array_index (data->items, i); + + if (strcmp (item->name, name) == 0) + return item; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +static docker_view_t +view_from_root_path (const char *path) +{ + if (strcmp (path, "containers") == 0) + return DOCKER_VIEW_CONTAINERS_PROJECTS; + if (strcmp (path, "images") == 0) + return DOCKER_VIEW_IMAGES; + if (strcmp (path, "volumes") == 0) + return DOCKER_VIEW_VOLUMES; + if (strcmp (path, "networks") == 0) + return DOCKER_VIEW_NETWORKS; + + return DOCKER_VIEW_ROOT; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +run_cmd (const char *cmd, char **output, char **err_text) +{ + gchar *std_out = NULL; + gchar *std_err = NULL; + gint status = 0; + GError *error = NULL; + gboolean spawned; + gboolean exited_ok; + + spawned = g_spawn_command_line_sync (cmd, &std_out, &std_err, &status, &error); + if (!spawned) + { + if (err_text != NULL) + { + if (error != NULL && error->message != NULL) + *err_text = g_strdup (error->message); + else + *err_text = g_strdup (_ ("Failed to start docker command")); + } + + if (output != NULL) + *output = NULL; + + if (error != NULL) + g_error_free (error); + g_free (std_out); + g_free (std_err); + return FALSE; + } + +#if GLIB_CHECK_VERSION(2, 70, 0) + exited_ok = g_spawn_check_wait_status (status, NULL); +#else + exited_ok = g_spawn_check_exit_status (status, NULL); +#endif + + if (output != NULL) + *output = std_out; + else + g_free (std_out); + + if (err_text != NULL) + *err_text = std_err; + else + g_free (std_err); + + return exited_ok; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +is_ungrouped_project (const char *project) +{ + return (project == NULL || project[0] == '\0' || strcmp (project, docker_ungrouped_project) == 0); +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +project_match (const char *selected_project, const char *project_from_docker) +{ + if (is_ungrouped_project (selected_project)) + return is_ungrouped_project (project_from_docker); + + return (project_from_docker != NULL && strcmp (selected_project, project_from_docker) == 0); +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +docker_load_containers_output (char **output, char **err_text) +{ + return run_cmd ( + "docker ps -a --format '{{.ID}}\\t{{.Names}}\\t{{.Image}}\\t{{.Status}}\\t{{.Label \"com.docker.compose.project\"}}'", + output, err_text); +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +parse_projects_from_containers (const char *output, const char *focused_project) +{ + GPtrArray *items; + GHashTable *seen; + char **lines; + int i; + + items = g_ptr_array_new_with_free_func (docker_item_free); + seen = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + if (output == NULL) + goto done; + + lines = g_strsplit (output, "\n", -1); + + for (i = 0; lines[i] != NULL; i++) + { + char **parts; + int part_count = 0; + const char *project; + char *project_key; + + if (lines[i][0] == '\0') + continue; + + parts = g_strsplit (lines[i], "\t", -1); + while (parts[part_count] != NULL) + part_count++; + + if (part_count < 5) + { + g_strfreev (parts); + continue; + } + + project = parts[4]; + project_key = is_ungrouped_project (project) ? g_strdup (docker_ungrouped_project) + : g_strdup (project); + + if (!g_hash_table_contains (seen, project_key)) + { + docker_item_t *item = g_new0 (docker_item_t, 1); + + item->id = g_strdup (project_key); + item->name = g_strdup (project_key); + item->is_dir = TRUE; + item->size = 0; + + if (focused_project != NULL && strcmp (item->id, focused_project) == 0) + g_ptr_array_insert (items, 0, item); + else + g_ptr_array_add (items, item); + g_hash_table_add (seen, project_key); + } + else + g_free (project_key); + + g_strfreev (parts); + } + + g_strfreev (lines); + +done: + if (!g_hash_table_contains (seen, docker_ungrouped_project)) + { + docker_item_t *item = g_new0 (docker_item_t, 1); + + item->id = g_strdup (docker_ungrouped_project); + item->name = g_strdup (docker_ungrouped_project); + item->is_dir = TRUE; + item->size = 0; + + g_ptr_array_add (items, item); + } + + g_hash_table_destroy (seen); + return items; +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +parse_container_items_from_project (const char *output, const char *project_name, + const char *focused_container_id) +{ + GPtrArray *items; + char **lines; + int i; + + items = g_ptr_array_new_with_free_func (docker_item_free); + + if (output == NULL) + return items; + + lines = g_strsplit (output, "\n", -1); + + for (i = 0; lines[i] != NULL; i++) + { + char **parts; + int part_count = 0; + const char *project; + + if (lines[i][0] == '\0') + continue; + + parts = g_strsplit (lines[i], "\t", -1); + while (parts[part_count] != NULL) + part_count++; + + if (part_count < 5) + { + g_strfreev (parts); + continue; + } + + project = parts[4]; + if (!project_match (project_name, project)) + { + g_strfreev (parts); + continue; + } + + { + docker_item_t *item = g_new0 (docker_item_t, 1); + + item->id = g_strdup (parts[0]); + item->name = g_strdup_printf ("%s (%s) %s", parts[1], parts[2], parts[3]); + item->is_dir = TRUE; + item->size = 0; + + if (focused_container_id != NULL && strcmp (item->id, focused_container_id) == 0) + g_ptr_array_insert (items, 0, item); + else + g_ptr_array_add (items, item); + } + + g_strfreev (parts); + } + + g_strfreev (lines); + return items; +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +parse_generic_list_output (docker_view_t view, const char *output) +{ + GPtrArray *items; + char **lines; + int i; + + items = g_ptr_array_new_with_free_func (docker_item_free); + if (output == NULL) + return items; + + lines = g_strsplit (output, "\n", -1); + + for (i = 0; lines[i] != NULL; i++) + { + docker_item_t *item; + char **parts; + int part_count = 0; + + if (lines[i][0] == '\0') + continue; + + parts = g_strsplit (lines[i], "\t", -1); + while (parts[part_count] != NULL) + part_count++; + + if (part_count < 2) + { + g_strfreev (parts); + continue; + } + + item = g_new0 (docker_item_t, 1); + item->is_dir = FALSE; + item->size = 0; + item->id = g_strdup (parts[0]); + + switch (view) + { + case DOCKER_VIEW_IMAGES: + item->name = g_strdup (parts[1]); + if (part_count >= 3) + item->size = parse_size_to_bytes (parts[2]); + break; + case DOCKER_VIEW_VOLUMES: + if (part_count >= 3) + item->name = g_strdup_printf ("%s (%s)", parts[1], parts[2]); + else + item->name = g_strdup (parts[1]); + break; + case DOCKER_VIEW_NETWORKS: + if (part_count >= 4) + item->name = g_strdup_printf ("%s (%s/%s)", parts[1], parts[2], parts[3]); + else + item->name = g_strdup (parts[1]); + break; + default: + item->name = g_strdup (parts[1]); + break; + } + + g_ptr_array_add (items, item); + g_strfreev (parts); + } + + g_strfreev (lines); + return items; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +reload_items (docker_data_t *data) +{ + char *output = NULL; + char *err_text = NULL; + + clear_items (data); + + switch (data->view) + { + case DOCKER_VIEW_ROOT: + return TRUE; + + case DOCKER_VIEW_CONTAINERS_PROJECTS: + if (!docker_load_containers_output (&output, &err_text)) + goto cmd_failed; + + data->items = parse_projects_from_containers (output, data->current_project); + break; + + case DOCKER_VIEW_CONTAINERS_ITEMS: + if (!docker_load_containers_output (&output, &err_text)) + goto cmd_failed; + + data->items = + parse_container_items_from_project (output, data->current_project, data->current_container_id); + break; + + case DOCKER_VIEW_CONTAINER_DETAILS: + { + docker_item_t *item; + + data->items = g_ptr_array_new_with_free_func (docker_item_free); + + item = g_new0 (docker_item_t, 1); + item->id = g_strdup (docker_inspect_file); + item->name = g_strdup (docker_inspect_file); + item->is_dir = FALSE; + item->size = 0; + g_ptr_array_add (data->items, item); + + break; + } + + case DOCKER_VIEW_IMAGES: + if (!run_cmd ("docker images --format '{{.ID}}\\t{{.Repository}}:{{.Tag}}\\t{{.Size}}'", &output, + &err_text)) + goto cmd_failed; + + data->items = parse_generic_list_output (data->view, output); + break; + + case DOCKER_VIEW_VOLUMES: + if (!run_cmd ("docker volume ls --format '{{.Name}}\\t{{.Name}}\\t{{.Driver}}'", &output, + &err_text)) + goto cmd_failed; + + data->items = parse_generic_list_output (data->view, output); + break; + + case DOCKER_VIEW_NETWORKS: + if (!run_cmd ("docker network ls --format '{{.ID}}\\t{{.Name}}\\t{{.Driver}}\\t{{.Scope}}'", + &output, &err_text)) + goto cmd_failed; + + data->items = parse_generic_list_output (data->view, output); + break; + + default: + data->items = g_ptr_array_new_with_free_func (docker_item_free); + break; + } + + g_free (output); + g_free (err_text); + + if (data->items == NULL) + data->items = g_ptr_array_new_with_free_func (docker_item_free); + + return TRUE; + +cmd_failed: + if (err_text != NULL && err_text[0] != '\0') + message (D_ERROR, MSG_ERROR, "%s", err_text); + + g_free (output); + g_free (err_text); + + data->items = g_ptr_array_new_with_free_func (docker_item_free); + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +write_temp_content (const char *prefix, const char *content, char **local_path) +{ + GError *error = NULL; + int fd; + + fd = g_file_open_tmp (prefix, local_path, &error); + if (fd == -1) + { + if (error != NULL) + g_error_free (error); + return FALSE; + } + + if (content == NULL) + content = ""; + + if (write (fd, content, strlen (content)) == -1) + { + close (fd); + unlink (*local_path); + g_free (*local_path); + *local_path = NULL; + return FALSE; + } + + close (fd); + return TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +show_create_container_dialog (char **image, char **name, char **command, gboolean *detach) +{ + /* clang-format off */ + quick_widget_t quick_widgets[] = { + QUICK_LABELED_INPUT (N_("Image:"), input_label_above, + *image != NULL ? *image : "", "docker-create-image", + image, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Container name:"), input_label_above, + *name != NULL ? *name : "", "docker-create-name", + name, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Command:"), input_label_above, + *command != NULL ? *command : "", "docker-create-cmd", + command, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_CHECKBOX (N_("Run in &detached mode (-d)"), detach, NULL), + QUICK_BUTTONS_OK_CANCEL, + QUICK_END, + }; + /* clang-format on */ + + WRect r = { -1, -1, 0, 56 }; + + quick_dialog_t qdlg = { + .rect = r, + .title = N_ ("Create Docker Container"), + .help = "[Docker Plugin]", + .widgets = quick_widgets, + .callback = NULL, + .mouse_callback = NULL, + }; + + return (quick_dialog (&qdlg) == B_ENTER); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +set_view (docker_data_t *data, docker_view_t new_view) +{ + data->view = new_view; + clear_items (data); +} + +/* --------------------------------------------------------------------------------------------- */ +/* Plugin callbacks */ +/* --------------------------------------------------------------------------------------------- */ + +static void * +docker_open (mc_panel_host_t *host, const char *open_path) +{ + docker_data_t *data; + + (void) open_path; + + data = g_new0 (docker_data_t, 1); + data->host = host; + data->view = DOCKER_VIEW_ROOT; + data->items = NULL; + data->root_focus = NULL; + data->current_project = NULL; + data->current_container_id = NULL; + data->current_container_name = NULL; + data->title_buf = NULL; + + return data; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +docker_close (void *plugin_data) +{ + docker_data_t *data = (docker_data_t *) plugin_data; + + clear_items (data); + g_free (data->root_focus); + g_free (data->current_project); + g_free (data->current_container_id); + g_free (data->current_container_name); + g_free (data->title_buf); + g_free (data); +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +docker_get_items (void *plugin_data, void *list_ptr) +{ + dir_list *list = (dir_list *) list_ptr; + docker_data_t *data = (docker_data_t *) plugin_data; + guint idx; + + if (data->view == DOCKER_VIEW_ROOT) + { + const char *sections[] = { "containers", "images", "volumes", "networks" }; + int sec_i; + + for (sec_i = 0; sec_i < 4; sec_i++) + if (data->root_focus != NULL && strcmp (sections[sec_i], data->root_focus) == 0) + add_entry (list, sections[sec_i], S_IFDIR | 0755, 0); + + for (sec_i = 0; sec_i < 4; sec_i++) + if (data->root_focus == NULL || strcmp (sections[sec_i], data->root_focus) != 0) + add_entry (list, sections[sec_i], S_IFDIR | 0755, 0); + + add_entry (list, docker_daemon_info_file, S_IFREG | 0644, 0); + add_entry (list, docker_version_file, S_IFREG | 0644, 0); + return MC_PPR_OK; + } + + if (data->items == NULL) + reload_items (data); + + if (data->items != NULL) + { + for (idx = 0; idx < data->items->len; idx++) + { + const docker_item_t *item = (const docker_item_t *) g_ptr_array_index (data->items, idx); + mode_t mode = item->is_dir ? (S_IFDIR | 0755) : (S_IFREG | 0644); + + add_entry (list, item->name, mode, item->size); + } + } + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +docker_chdir (void *plugin_data, const char *path) +{ + docker_data_t *data = (docker_data_t *) plugin_data; + + if (strcmp (path, "..") == 0) + { + switch (data->view) + { + case DOCKER_VIEW_ROOT: + return MC_PPR_NOT_SUPPORTED; + + case DOCKER_VIEW_CONTAINERS_PROJECTS: + g_free (data->root_focus); + data->root_focus = g_strdup ("containers"); + set_view (data, DOCKER_VIEW_ROOT); + return MC_PPR_OK; + + case DOCKER_VIEW_IMAGES: + g_free (data->root_focus); + data->root_focus = g_strdup ("images"); + set_view (data, DOCKER_VIEW_ROOT); + return MC_PPR_OK; + + case DOCKER_VIEW_VOLUMES: + g_free (data->root_focus); + data->root_focus = g_strdup ("volumes"); + set_view (data, DOCKER_VIEW_ROOT); + return MC_PPR_OK; + + case DOCKER_VIEW_NETWORKS: + g_free (data->root_focus); + data->root_focus = g_strdup ("networks"); + set_view (data, DOCKER_VIEW_ROOT); + return MC_PPR_OK; + + case DOCKER_VIEW_CONTAINERS_ITEMS: + set_view (data, DOCKER_VIEW_CONTAINERS_PROJECTS); + reload_items (data); + return MC_PPR_OK; + + case DOCKER_VIEW_CONTAINER_DETAILS: + set_view (data, DOCKER_VIEW_CONTAINERS_ITEMS); + reload_items (data); + return MC_PPR_OK; + + default: + set_view (data, DOCKER_VIEW_ROOT); + return MC_PPR_OK; + } + } + + if (data->view == DOCKER_VIEW_ROOT) + { + docker_view_t next = view_from_root_path (path); + + if (next == DOCKER_VIEW_ROOT) + return MC_PPR_FAILED; + + g_free (data->root_focus); + data->root_focus = g_strdup (path); + set_view (data, next); + reload_items (data); + return MC_PPR_OK; + } + + if (data->view == DOCKER_VIEW_CONTAINERS_PROJECTS) + { + const docker_item_t *project = find_item_by_name (data, path); + + if (project == NULL) + return MC_PPR_FAILED; + + g_free (data->current_project); + data->current_project = g_strdup (project->id); + g_free (data->current_container_id); + data->current_container_id = NULL; + g_free (data->current_container_name); + data->current_container_name = NULL; + set_view (data, DOCKER_VIEW_CONTAINERS_ITEMS); + reload_items (data); + return MC_PPR_OK; + } + + if (data->view == DOCKER_VIEW_CONTAINERS_ITEMS) + { + const docker_item_t *container = find_item_by_name (data, path); + + if (container == NULL) + return MC_PPR_FAILED; + + g_free (data->current_container_id); + data->current_container_id = g_strdup (container->id); + + g_free (data->current_container_name); + data->current_container_name = g_strdup (container->name); + + set_view (data, DOCKER_VIEW_CONTAINER_DETAILS); + reload_items (data); + return MC_PPR_OK; + } + + return MC_PPR_FAILED; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +docker_enter (void *plugin_data, const char *name, const struct stat *st) +{ + docker_data_t *data = (docker_data_t *) plugin_data; + + (void) st; + if (data->view == DOCKER_VIEW_ROOT) + { + docker_view_t next = view_from_root_path (name); + + if (next != DOCKER_VIEW_ROOT) + { + g_free (data->root_focus); + data->root_focus = g_strdup (name); + set_view (data, next); + reload_items (data); + return MC_PPR_OK; + } + + if (strcmp (name, docker_daemon_info_file) == 0 || strcmp (name, docker_version_file) == 0) + return MC_PPR_NOT_SUPPORTED; + + return MC_PPR_FAILED; + } + + if (data->view == DOCKER_VIEW_CONTAINERS_PROJECTS || data->view == DOCKER_VIEW_CONTAINERS_ITEMS) + return docker_chdir (plugin_data, name); + + if (data->view == DOCKER_VIEW_CONTAINER_DETAILS) + { + if (strcmp (name, docker_inspect_file) == 0) + return MC_PPR_NOT_SUPPORTED; + + return MC_PPR_FAILED; + } + + if (find_item_by_name (data, name) != NULL) + return MC_PPR_NOT_SUPPORTED; + + return MC_PPR_FAILED; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +docker_get_local_copy (void *plugin_data, const char *fname, char **local_path) +{ + docker_data_t *data = (docker_data_t *) plugin_data; + char *output = NULL; + char *err_text = NULL; + const char *cmd = NULL; + char *cmd_dynamic = NULL; + gboolean ok; + + if (data->view == DOCKER_VIEW_ROOT) + { + if (strcmp (fname, docker_daemon_info_file) == 0) + cmd = "docker info"; + else if (strcmp (fname, docker_version_file) == 0) + cmd = "docker version"; + else + return MC_PPR_FAILED; + } + else if (data->view == DOCKER_VIEW_CONTAINER_DETAILS) + { + char *quoted_id; + + if (data->current_container_id == NULL || strcmp (fname, docker_inspect_file) != 0) + return MC_PPR_FAILED; + + quoted_id = g_shell_quote (data->current_container_id); + cmd_dynamic = g_strdup_printf ("docker inspect %s", quoted_id); + g_free (quoted_id); + cmd = cmd_dynamic; + } + else if (data->view == DOCKER_VIEW_CONTAINERS_ITEMS) + { + const docker_item_t *container = find_item_by_name (data, fname); + char *quoted_id; + + if (container == NULL) + return MC_PPR_FAILED; + + quoted_id = g_shell_quote (container->id); + cmd_dynamic = g_strdup_printf ("docker inspect %s", quoted_id); + g_free (quoted_id); + cmd = cmd_dynamic; + } + else + { + const docker_item_t *item = find_item_by_name (data, fname); + char *quoted_id; + + if (item == NULL) + return MC_PPR_FAILED; + + quoted_id = g_shell_quote (item->id); + cmd_dynamic = g_strdup_printf ("docker inspect %s", quoted_id); + g_free (quoted_id); + cmd = cmd_dynamic; + } + + ok = run_cmd (cmd, &output, &err_text); + g_free (cmd_dynamic); + + if (!ok) + { + if (err_text != NULL && err_text[0] != '\0') + message (D_ERROR, MSG_ERROR, "%s", err_text); + g_free (output); + g_free (err_text); + return MC_PPR_FAILED; + } + + ok = write_temp_content ("mc-pp-docker-XXXXXX", output, local_path); + + g_free (output); + g_free (err_text); + + return ok ? MC_PPR_OK : MC_PPR_FAILED; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +docker_delete_items (void *plugin_data, const char **names, int count) +{ + docker_data_t *data = (docker_data_t *) plugin_data; + int i; + + if (data->view == DOCKER_VIEW_ROOT || data->view == DOCKER_VIEW_CONTAINERS_PROJECTS + || data->view == DOCKER_VIEW_CONTAINER_DETAILS) + return MC_PPR_NOT_SUPPORTED; + + for (i = 0; i < count; i++) + { + const docker_item_t *item; + char *quoted; + char *cmd; + char *output = NULL; + char *err_text = NULL; + + item = find_item_by_name (data, names[i]); + if (item == NULL) + continue; + + quoted = g_shell_quote (item->id); + + switch (data->view) + { + case DOCKER_VIEW_CONTAINERS_ITEMS: + cmd = g_strdup_printf ("docker rm %s", quoted); + break; + case DOCKER_VIEW_IMAGES: + cmd = g_strdup_printf ("docker rmi %s", quoted); + break; + case DOCKER_VIEW_VOLUMES: + cmd = g_strdup_printf ("docker volume rm %s", quoted); + break; + case DOCKER_VIEW_NETWORKS: + cmd = g_strdup_printf ("docker network rm %s", quoted); + break; + default: + g_free (quoted); + return MC_PPR_NOT_SUPPORTED; + } + + if (!run_cmd (cmd, &output, &err_text) && err_text != NULL && err_text[0] != '\0') + message (D_ERROR, MSG_ERROR, "%s", err_text); + + g_free (output); + g_free (err_text); + g_free (cmd); + g_free (quoted); + } + + reload_items (data); + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +docker_get_title (void *plugin_data) +{ + docker_data_t *data = (docker_data_t *) plugin_data; + + g_free (data->title_buf); + + switch (data->view) + { + case DOCKER_VIEW_ROOT: + data->title_buf = g_strdup ("/"); + break; + + case DOCKER_VIEW_CONTAINERS_PROJECTS: + data->title_buf = g_strdup ("/containers"); + break; + + case DOCKER_VIEW_CONTAINERS_ITEMS: + data->title_buf = g_strdup_printf ("/containers/%s", + data->current_project != NULL ? data->current_project + : docker_ungrouped_project); + break; + + case DOCKER_VIEW_CONTAINER_DETAILS: + data->title_buf = g_strdup_printf ("/containers/%s/%s", + data->current_project != NULL ? data->current_project + : docker_ungrouped_project, + data->current_container_name != NULL + ? data->current_container_name + : "container"); + break; + + case DOCKER_VIEW_IMAGES: + data->title_buf = g_strdup ("/images"); + break; + + case DOCKER_VIEW_VOLUMES: + data->title_buf = g_strdup ("/volumes"); + break; + + case DOCKER_VIEW_NETWORKS: + data->title_buf = g_strdup ("/networks"); + break; + + default: + data->title_buf = g_strdup ("/"); + break; + } + + return data->title_buf; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +docker_create_item (void *plugin_data) +{ + docker_data_t *data = (docker_data_t *) plugin_data; + char *image = NULL; + char *name = NULL; + char *command = NULL; + gboolean detach = TRUE; + char *q_image = NULL; + char *q_name = NULL; + char *q_cmd = NULL; + char *q_project = NULL; + char *cmd = NULL; + char *output = NULL; + char *err_text = NULL; + mc_pp_result_t result = MC_PPR_FAILED; + + if (data->view != DOCKER_VIEW_CONTAINERS_ITEMS && data->view != DOCKER_VIEW_CONTAINERS_PROJECTS) + return MC_PPR_NOT_SUPPORTED; + + if (!show_create_container_dialog (&image, &name, &command, &detach)) + goto done; + + if (image == NULL || image[0] == '\0') + goto done; + + q_image = g_shell_quote (image); + + if (name != NULL && name[0] != '\0') + q_name = g_shell_quote (name); + + if (command != NULL && command[0] != '\0') + q_cmd = g_shell_quote (command); + + if (data->view == DOCKER_VIEW_CONTAINERS_ITEMS && data->current_project != NULL + && !is_ungrouped_project (data->current_project)) + q_project = g_shell_quote (data->current_project); + + cmd = g_strdup ("docker run "); + + if (detach) + { + char *tmp = g_strconcat (cmd, "-d ", (char *) NULL); + g_free (cmd); + cmd = tmp; + } + + if (q_name != NULL) + { + char *tmp = g_strconcat (cmd, "--name ", q_name, " ", (char *) NULL); + g_free (cmd); + cmd = tmp; + } + + if (q_project != NULL) + { + char *tmp = g_strconcat (cmd, "--label com.docker.compose.project=", q_project, " ", + (char *) NULL); + g_free (cmd); + cmd = tmp; + } + + { + char *tmp = g_strconcat (cmd, q_image, (char *) NULL); + g_free (cmd); + cmd = tmp; + } + + if (q_cmd != NULL) + { + char *tmp = g_strconcat (cmd, " sh -c ", q_cmd, (char *) NULL); + g_free (cmd); + cmd = tmp; + } + + if (!run_cmd (cmd, &output, &err_text)) + { + if (err_text != NULL && err_text[0] != '\0') + message (D_ERROR, MSG_ERROR, "%s", err_text); + goto done; + } + + reload_items (data); + result = MC_PPR_OK; + +done: + g_free (image); + g_free (name); + g_free (command); + g_free (q_image); + g_free (q_name); + g_free (q_cmd); + g_free (q_project); + g_free (cmd); + g_free (output); + g_free (err_text); + + return result; +} + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +const mc_panel_plugin_t *mc_panel_plugin_register (void); + +const mc_panel_plugin_t * +mc_panel_plugin_register (void) +{ + return &docker_plugin; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/panel-plugins/hello/Makefile.am b/src/panel-plugins/hello/Makefile.am new file mode 100644 index 0000000000..9342389e8a --- /dev/null +++ b/src/panel-plugins/hello/Makefile.am @@ -0,0 +1,7 @@ +panelplugindir = $(panel_plugins_dir) +panelplugin_LTLIBRARIES = mc-panel-hello.la + +mc_panel_hello_la_SOURCES = hello.c +mc_panel_hello_la_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) +mc_panel_hello_la_LDFLAGS = -module -avoid-version +mc_panel_hello_la_LIBADD = $(GLIB_LIBS) diff --git a/src/panel-plugins/hello/hello.c b/src/panel-plugins/hello/hello.c new file mode 100644 index 0000000000..97f67074e4 --- /dev/null +++ b/src/panel-plugins/hello/hello.c @@ -0,0 +1,263 @@ +/* + Hello World panel plugin — demonstrates the panel plugin API. + + Copyright (C) 2025 + Free Software Foundation, Inc. + + Written by: + Ilia Maslakov , 2026. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include + +#include +#include +#include +#include + +#include "lib/global.h" +#include "lib/panel-plugin.h" + +/* dir_list and dir_list_append are in the filemanager dir.h, but we need the + dir_list_append prototype. Since we're a loadable module, we rely on the + host exporting it (mc is built with -export-dynamic when HAVE_GMODULE). */ +#include "src/filemanager/dir.h" + +/*** file scope type declarations ****************************************************************/ + +typedef struct +{ + mc_panel_host_t *host; + gboolean in_subdir; /* TRUE when user entered "subdir" */ + GHashTable *deleted; /* set of "deleted" filenames */ +} hello_data_t; + +/*** forward declarations (file scope functions) *************************************************/ + +static void *hello_open (mc_panel_host_t *host, const char *open_path); +static void hello_close (void *plugin_data); +static mc_pp_result_t hello_get_items (void *plugin_data, void *list_ptr); +static mc_pp_result_t hello_chdir (void *plugin_data, const char *path); +static mc_pp_result_t hello_get_local_copy (void *plugin_data, const char *fname, + char **local_path); +static mc_pp_result_t hello_delete_items (void *plugin_data, const char **names, int count); +static const char *hello_get_title (void *plugin_data); + +/*** file scope variables ************************************************************************/ + +static const mc_panel_plugin_t hello_plugin = { + .api_version = MC_PANEL_PLUGIN_API_VERSION, + .name = "hello", + .display_name = "Hello World Plugin", + .proto = "HelloWorld", + .prefix = NULL, + .flags = MC_PPF_NAVIGATE | MC_PPF_GET_FILES | MC_PPF_DELETE | MC_PPF_CUSTOM_TITLE, + + .open = hello_open, + .close = hello_close, + .get_items = hello_get_items, + + .chdir = hello_chdir, + .enter = NULL, + .get_local_copy = hello_get_local_copy, + .delete_items = hello_delete_items, + .get_title = hello_get_title, + .handle_key = NULL, +}; + +/*** file scope functions ************************************************************************/ + +static void +add_fake_entry (dir_list *list, const char *name, mode_t mode, off_t size) +{ + struct stat st; + + memset (&st, 0, sizeof (st)); + st.st_mode = mode; + st.st_size = size; + st.st_mtime = time (NULL); + st.st_uid = getuid (); + st.st_gid = getgid (); + st.st_nlink = 1; + + dir_list_append (list, name, &st, S_ISDIR (mode), FALSE); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void * +hello_open (mc_panel_host_t *host, const char *open_path) +{ + hello_data_t *data; + + (void) open_path; + + data = g_new0 (hello_data_t, 1); + data->host = host; + data->in_subdir = FALSE; + data->deleted = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + return data; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +hello_close (void *plugin_data) +{ + hello_data_t *data = (hello_data_t *) plugin_data; + + g_hash_table_destroy (data->deleted); + g_free (data); +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +hello_get_items (void *plugin_data, void *list_ptr) +{ + dir_list *list = (dir_list *) list_ptr; + hello_data_t *data = (hello_data_t *) plugin_data; + + /* Note: dir_list_init() already creates the ".." entry at index 0, + so the plugin must NOT add it again. */ + + if (data->in_subdir) + { + if (!g_hash_table_contains (data->deleted, "deep-file.txt")) + add_fake_entry (list, "deep-file.txt", S_IFREG | 0644, 256); + if (!g_hash_table_contains (data->deleted, "another.dat")) + add_fake_entry (list, "another.dat", S_IFREG | 0644, 1024); + } + else + { + if (!g_hash_table_contains (data->deleted, "hello.txt")) + add_fake_entry (list, "hello.txt", S_IFREG | 0644, 42); + if (!g_hash_table_contains (data->deleted, "world.txt")) + add_fake_entry (list, "world.txt", S_IFREG | 0644, 100); + add_fake_entry (list, "subdir", S_IFDIR | 0755, 4096); + } + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +hello_chdir (void *plugin_data, const char *path) +{ + hello_data_t *data = (hello_data_t *) plugin_data; + + if (strcmp (path, "..") == 0) + { + if (data->in_subdir) + { + data->in_subdir = FALSE; + return MC_PPR_OK; + } + return MC_PPR_NOT_SUPPORTED; /* close plugin */ + } + + if (strcmp (path, "subdir") == 0 && !data->in_subdir) + { + data->in_subdir = TRUE; + return MC_PPR_OK; + } + + return MC_PPR_FAILED; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +hello_get_local_copy (void *plugin_data, const char *fname, char **local_path) +{ + GError *error = NULL; + int fd; + const char *content; + + (void) plugin_data; + + if (strcmp (fname, "hello.txt") == 0) + content = "Hello, World!\nThis is content from the Hello World panel plugin.\n"; + else if (strcmp (fname, "world.txt") == 0) + content = "This is world.txt\nAnother virtual file from the plugin.\n"; + else if (strcmp (fname, "deep-file.txt") == 0) + content = "Deep inside the subdir.\n"; + else if (strcmp (fname, "another.dat") == 0) + content = "Binary-ish data from the plugin.\n"; + else + return MC_PPR_FAILED; + + fd = g_file_open_tmp ("mc-pp-XXXXXX", local_path, &error); + if (fd == -1) + { + g_error_free (error); + return MC_PPR_FAILED; + } + + if (write (fd, content, strlen (content)) == -1) + { + close (fd); + unlink (*local_path); + g_free (*local_path); + *local_path = NULL; + return MC_PPR_FAILED; + } + close (fd); + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +hello_delete_items (void *plugin_data, const char **names, int count) +{ + hello_data_t *data = (hello_data_t *) plugin_data; + int i; + + for (i = 0; i < count; i++) + g_hash_table_add (data->deleted, g_strdup (names[i])); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +hello_get_title (void *plugin_data) +{ + hello_data_t *data = (hello_data_t *) plugin_data; + + return data->in_subdir ? "/subdir" : "/"; +} + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* Entry point called by the panel plugin loader */ +const mc_panel_plugin_t *mc_panel_plugin_register (void); + +const mc_panel_plugin_t * +mc_panel_plugin_register (void) +{ + return &hello_plugin; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/panel-plugins/samba/Makefile.am b/src/panel-plugins/samba/Makefile.am new file mode 100644 index 0000000000..19516b264d --- /dev/null +++ b/src/panel-plugins/samba/Makefile.am @@ -0,0 +1,7 @@ +panelplugindir = $(panel_plugins_dir) +panelplugin_LTLIBRARIES = mc-panel-samba.la + +mc_panel_samba_la_SOURCES = samba.c +mc_panel_samba_la_CPPFLAGS = $(GLIB_CFLAGS) $(SMBCLIENT_CFLAGS) -I$(top_srcdir) +mc_panel_samba_la_LDFLAGS = -module -avoid-version +mc_panel_samba_la_LIBADD = $(GLIB_LIBS) $(SMBCLIENT_LIBS) diff --git a/src/panel-plugins/samba/samba.c b/src/panel-plugins/samba/samba.c new file mode 100644 index 0000000000..6c3d2a12e8 --- /dev/null +++ b/src/panel-plugins/samba/samba.c @@ -0,0 +1,1011 @@ +/* + Samba network browser panel plugin (libsmbclient). + + Copyright (C) 2025 + Free Software Foundation, Inc. + + Written by: + Ilia Maslakov , 2026. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "lib/global.h" +#include "lib/panel-plugin.h" +#include "lib/widget.h" + +#include "src/filemanager/dir.h" + +/*** file scope type declarations ****************************************************************/ + +/* A saved connection bookmark */ +typedef struct +{ + char *label; /* display name (ini group key) */ + char *server; + char *share; /* default share/path, may be empty */ + char *username; /* may include DOMAIN\user */ + char *password; + char *workgroup; +} smb_connection_t; + +/* A directory entry (file/dir/share inside a connection) */ +typedef struct +{ + char *name; + unsigned int smbc_type; /* SMBC_FILE_SHARE, SMBC_DIR, SMBC_FILE, etc. */ + struct stat st; +} smb_entry_t; + +typedef struct +{ + mc_panel_host_t *host; + SMBCCTX *ctx; + + /* Navigation state */ + gboolean at_root; /* TRUE = showing saved connections list */ + char *current_url; /* "smb://SERVER/SHARE/path" when not at root */ + GPtrArray *entries; /* smb_entry_t* when browsing; NULL at root */ + + /* Saved connections */ + GPtrArray *connections; /* smb_connection_t* — loaded from ini */ + char *connections_file; /* path to ini file */ + + /* Active connection credentials (for auth callback) */ + char *auth_username; + char *auth_password; + char *auth_workgroup; + + char *title_buf; +} samba_data_t; + +/*** forward declarations (file scope functions) *************************************************/ + +static void *samba_open (mc_panel_host_t *host, const char *open_path); +static void samba_close (void *plugin_data); +static mc_pp_result_t samba_get_items (void *plugin_data, void *list_ptr); +static mc_pp_result_t samba_chdir (void *plugin_data, const char *path); +static mc_pp_result_t samba_enter (void *plugin_data, const char *name, const struct stat *st); +static mc_pp_result_t samba_get_local_copy (void *plugin_data, const char *fname, + char **local_path); +static mc_pp_result_t samba_delete_items (void *plugin_data, const char **names, int count); +static const char *samba_get_title (void *plugin_data); +static mc_pp_result_t samba_create_item (void *plugin_data); + +/*** file scope variables ************************************************************************/ + +static const mc_panel_plugin_t samba_plugin = { + .api_version = MC_PANEL_PLUGIN_API_VERSION, + .name = "samba", + .display_name = "Samba network", + .proto = "smb", + .prefix = NULL, + .flags = + MC_PPF_NAVIGATE | MC_PPF_GET_FILES | MC_PPF_DELETE | MC_PPF_CUSTOM_TITLE | MC_PPF_CREATE, + + .open = samba_open, + .close = samba_close, + .get_items = samba_get_items, + + .chdir = samba_chdir, + .enter = samba_enter, + .get_local_copy = samba_get_local_copy, + .delete_items = samba_delete_items, + .get_title = samba_get_title, + .handle_key = NULL, + .create_item = samba_create_item, +}; + +/*** file scope functions ************************************************************************/ + +static void +add_entry (dir_list *list, const char *name, mode_t mode, off_t size, time_t mtime) +{ + struct stat st; + + memset (&st, 0, sizeof (st)); + st.st_mode = mode; + st.st_size = size; + st.st_mtime = mtime; + st.st_uid = getuid (); + st.st_gid = getgid (); + st.st_nlink = 1; + + dir_list_append (list, name, &st, S_ISDIR (mode), FALSE); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +smb_entry_free (gpointer p) +{ + smb_entry_t *e = (smb_entry_t *) p; + + g_free (e->name); + g_free (e); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +smb_connection_free (gpointer p) +{ + smb_connection_t *c = (smb_connection_t *) p; + + g_free (c->label); + g_free (c->server); + g_free (c->share); + g_free (c->username); + g_free (c->password); + g_free (c->workgroup); + g_free (c); +} + +/* --------------------------------------------------------------------------------------------- */ +/* Connection storage (ini file) */ +/* --------------------------------------------------------------------------------------------- */ + +static char * +get_connections_file_path (void) +{ + return g_build_filename (g_get_user_config_dir (), "mc", "smb-connections.ini", (char *) NULL); +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +load_connections (const char *filepath) +{ + GPtrArray *arr; + GKeyFile *kf; + gchar **groups; + gsize n_groups, i; + + arr = g_ptr_array_new_with_free_func (smb_connection_free); + + kf = g_key_file_new (); + if (!g_key_file_load_from_file (kf, filepath, G_KEY_FILE_NONE, NULL)) + { + g_key_file_free (kf); + return arr; + } + + groups = g_key_file_get_groups (kf, &n_groups); + for (i = 0; i < n_groups; i++) + { + smb_connection_t *conn; + + conn = g_new0 (smb_connection_t, 1); + conn->label = g_strdup (groups[i]); + conn->server = g_key_file_get_string (kf, groups[i], "server", NULL); + conn->share = g_key_file_get_string (kf, groups[i], "share", NULL); + conn->username = g_key_file_get_string (kf, groups[i], "username", NULL); + conn->password = g_key_file_get_string (kf, groups[i], "password", NULL); + conn->workgroup = g_key_file_get_string (kf, groups[i], "workgroup", NULL); + + if (conn->server == NULL || conn->server[0] == '\0') + { + smb_connection_free (conn); + continue; + } + + g_ptr_array_add (arr, conn); + } + + g_strfreev (groups); + g_key_file_free (kf); + return arr; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +save_connections (const char *filepath, GPtrArray *connections) +{ + GKeyFile *kf; + gchar *data; + gsize length; + gboolean ok; + gchar *dir; + guint i; + + kf = g_key_file_new (); + + for (i = 0; i < connections->len; i++) + { + const smb_connection_t *conn = + (const smb_connection_t *) g_ptr_array_index (connections, i); + + g_key_file_set_string (kf, conn->label, "server", conn->server); + if (conn->share != NULL) + g_key_file_set_string (kf, conn->label, "share", conn->share); + if (conn->username != NULL) + g_key_file_set_string (kf, conn->label, "username", conn->username); + if (conn->password != NULL) + g_key_file_set_string (kf, conn->label, "password", conn->password); + if (conn->workgroup != NULL) + g_key_file_set_string (kf, conn->label, "workgroup", conn->workgroup); + } + + data = g_key_file_to_data (kf, &length, NULL); + g_key_file_free (kf); + + if (data == NULL) + return FALSE; + + /* Ensure directory exists */ + dir = g_path_get_dirname (filepath); + g_mkdir_with_parents (dir, 0700); + g_free (dir); + + ok = g_file_set_contents (filepath, data, (gssize) length, NULL); + g_free (data); + return ok; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const smb_connection_t * +find_connection (const samba_data_t *data, const char *label) +{ + guint i; + + for (i = 0; i < data->connections->len; i++) + { + const smb_connection_t *c = + (const smb_connection_t *) g_ptr_array_index (data->connections, i); + + if (strcmp (c->label, label) == 0) + return c; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ +/* libsmbclient helpers */ +/* --------------------------------------------------------------------------------------------- */ + +static void +smb_auth_cb (SMBCCTX *ctx, const char *server, const char *share, + char *wg, int wg_len, char *un, int un_len, char *pw, int pw_len) +{ + samba_data_t *data; + + (void) server; + (void) share; + + data = (samba_data_t *) smbc_getOptionUserData (ctx); + if (data == NULL) + return; + + if (data->auth_workgroup != NULL) + g_strlcpy (wg, data->auth_workgroup, (gsize) wg_len); + if (data->auth_username != NULL) + g_strlcpy (un, data->auth_username, (gsize) un_len); + if (data->auth_password != NULL) + g_strlcpy (pw, data->auth_password, (gsize) pw_len); +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +smb_is_inside_share (const char *url) +{ + const char *p; + int slash_count = 0; + + p = url + 6; /* skip "smb://" */ + while (*p != '\0') + { + if (*p == '/') + slash_count++; + p++; + } + + return (slash_count >= 1); +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +smb_load_entries (samba_data_t *data) +{ + GPtrArray *arr; + int dh; + struct smbc_dirent *dirent; + gboolean inside_share; + + arr = g_ptr_array_new_with_free_func (smb_entry_free); + + smbc_set_context (data->ctx); + dh = smbc_opendir (data->current_url); + if (dh < 0) + return arr; + + inside_share = smb_is_inside_share (data->current_url); + + while ((dirent = smbc_readdir (dh)) != NULL) + { + smb_entry_t *entry; + + if (strcmp (dirent->name, ".") == 0 || strcmp (dirent->name, "..") == 0) + continue; + + /* skip IPC$, printer, comms shares */ + if (dirent->smbc_type == SMBC_IPC_SHARE || dirent->smbc_type == SMBC_PRINTER_SHARE + || dirent->smbc_type == SMBC_COMMS_SHARE) + continue; + + entry = g_new0 (smb_entry_t, 1); + entry->name = g_strdup (dirent->name); + entry->smbc_type = dirent->smbc_type; + + if (inside_share + && (dirent->smbc_type == SMBC_FILE || dirent->smbc_type == SMBC_DIR + || dirent->smbc_type == SMBC_LINK)) + { + char *full_url; + + full_url = g_strdup_printf ("%s/%s", data->current_url, dirent->name); + if (smbc_stat (full_url, &entry->st) != 0) + { + memset (&entry->st, 0, sizeof (entry->st)); + entry->st.st_mtime = time (NULL); + } + g_free (full_url); + } + else + { + memset (&entry->st, 0, sizeof (entry->st)); + entry->st.st_mtime = time (NULL); + } + + g_ptr_array_add (arr, entry); + } + + smbc_closedir (dh); + return arr; +} + +/* --------------------------------------------------------------------------------------------- */ + +static char * +smb_url_up (const char *url) +{ + const char *after_scheme; + const char *last_slash; + + after_scheme = url + 6; /* past "smb://" */ + + if (*after_scheme == '\0') + return NULL; + + last_slash = strrchr (after_scheme, '/'); + if (last_slash == NULL) + return g_strdup ("smb://"); + + return g_strndup (url, (gsize) (last_slash - url)); +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +smb_entry_is_dir (unsigned int smbc_type) +{ + return (smbc_type == SMBC_WORKGROUP || smbc_type == SMBC_SERVER + || smbc_type == SMBC_FILE_SHARE || smbc_type == SMBC_DIR); +} + +/* --------------------------------------------------------------------------------------------- */ + +static const smb_entry_t * +find_entry (const samba_data_t *data, const char *name) +{ + guint i; + + if (data->entries == NULL) + return NULL; + + for (i = 0; i < data->entries->len; i++) + { + const smb_entry_t *e = (const smb_entry_t *) g_ptr_array_index (data->entries, i); + + if (strcmp (e->name, name) == 0) + return e; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +set_auth_from_connection (samba_data_t *data, const smb_connection_t *conn) +{ + g_free (data->auth_username); + g_free (data->auth_password); + g_free (data->auth_workgroup); + + data->auth_username = g_strdup (conn->username); + data->auth_password = g_strdup (conn->password); + data->auth_workgroup = g_strdup (conn->workgroup); +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +enter_connection (samba_data_t *data, const smb_connection_t *conn) +{ + set_auth_from_connection (data, conn); + + g_free (data->current_url); + if (conn->share != NULL && conn->share[0] != '\0') + data->current_url = g_strdup_printf ("smb://%s/%s", conn->server, conn->share); + else + data->current_url = g_strdup_printf ("smb://%s", conn->server); + + if (data->entries != NULL) + g_ptr_array_free (data->entries, TRUE); + + smbc_set_context (data->ctx); + data->entries = smb_load_entries (data); + data->at_root = FALSE; + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ +/* Connection dialog */ +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +show_connection_dialog (char **label, char **server, char **share, + char **username, char **password) +{ + /* clang-format off */ + quick_widget_t quick_widgets[] = { + QUICK_LABELED_INPUT (N_("Name:"), input_label_above, + *label != NULL ? *label : "", "smb-conn-label", + label, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Server:"), input_label_above, + *server != NULL ? *server : "", "smb-conn-server", + server, NULL, FALSE, FALSE, INPUT_COMPLETE_HOSTNAMES), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Share/path:"), input_label_above, + *share != NULL ? *share : "", "smb-conn-share", + share, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Username (DOMAIN\\user):"), input_label_above, + *username != NULL ? *username : "", "smb-conn-user", + username, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Password:"), input_label_above, + *password != NULL ? *password : "", "smb-conn-pass", + password, NULL, TRUE, TRUE, INPUT_COMPLETE_NONE), + QUICK_BUTTONS_OK_CANCEL, + QUICK_END, + }; + /* clang-format on */ + + WRect r = { -1, -1, 0, 56 }; + + quick_dialog_t qdlg = { + .rect = r, + .title = N_("SMB Connection"), + .help = "[Samba Plugin]", + .widgets = quick_widgets, + .callback = NULL, + .mouse_callback = NULL, + }; + + return (quick_dialog (&qdlg) == B_ENTER); +} + +/* --------------------------------------------------------------------------------------------- */ +/* Plugin callbacks */ +/* --------------------------------------------------------------------------------------------- */ + +static void * +samba_open (mc_panel_host_t *host, const char *open_path) +{ + samba_data_t *data; + + (void) open_path; + + data = g_new0 (samba_data_t, 1); + data->host = host; + data->at_root = TRUE; + data->current_url = NULL; + data->entries = NULL; + data->title_buf = NULL; + + data->auth_username = NULL; + data->auth_password = NULL; + data->auth_workgroup = NULL; + + /* Load saved connections */ + data->connections_file = get_connections_file_path (); + data->connections = load_connections (data->connections_file); + + /* Create libsmbclient context */ + data->ctx = smbc_new_context (); + if (data->ctx == NULL) + { + g_ptr_array_free (data->connections, TRUE); + g_free (data->connections_file); + g_free (data); + return NULL; + } + + smbc_setDebug (data->ctx, 0); + smbc_setFunctionAuthDataWithContext (data->ctx, smb_auth_cb); + smbc_setOptionUserData (data->ctx, data); + + if (smbc_init_context (data->ctx) == NULL) + { + smbc_free_context (data->ctx, 0); + g_ptr_array_free (data->connections, TRUE); + g_free (data->connections_file); + g_free (data); + return NULL; + } + + return data; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +samba_close (void *plugin_data) +{ + samba_data_t *data = (samba_data_t *) plugin_data; + + if (data->entries != NULL) + g_ptr_array_free (data->entries, TRUE); + + g_ptr_array_free (data->connections, TRUE); + + if (data->ctx != NULL) + smbc_free_context (data->ctx, 1); + + g_free (data->current_url); + g_free (data->title_buf); + g_free (data->connections_file); + g_free (data->auth_username); + g_free (data->auth_password); + g_free (data->auth_workgroup); + g_free (data); +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +samba_get_items (void *plugin_data, void *list_ptr) +{ + dir_list *list = (dir_list *) list_ptr; + samba_data_t *data = (samba_data_t *) plugin_data; + guint i; + + if (data->at_root) + { + /* Show saved connections as directories */ + for (i = 0; i < data->connections->len; i++) + { + const smb_connection_t *conn = + (const smb_connection_t *) g_ptr_array_index (data->connections, i); + + add_entry (list, conn->label, S_IFDIR | 0755, 0, time (NULL)); + } + return MC_PPR_OK; + } + + /* Inside a connection — show SMB entries */ + if (data->entries != NULL) + { + for (i = 0; i < data->entries->len; i++) + { + const smb_entry_t *e = (const smb_entry_t *) g_ptr_array_index (data->entries, i); + mode_t mode; + off_t size; + time_t mtime; + + if (smb_entry_is_dir (e->smbc_type)) + { + mode = S_IFDIR | 0755; + size = 0; + } + else + { + mode = S_IFREG | 0644; + size = e->st.st_size; + } + + mtime = (e->st.st_mtime != 0) ? e->st.st_mtime : time (NULL); + add_entry (list, e->name, mode, size, mtime); + } + } + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +samba_chdir (void *plugin_data, const char *path) +{ + samba_data_t *data = (samba_data_t *) plugin_data; + + if (strcmp (path, "..") == 0) + { + if (data->at_root) + return MC_PPR_NOT_SUPPORTED; /* close plugin */ + + /* Are we at the connection's base URL (smb://SERVER or smb://SERVER/SHARE)? */ + { + char *parent; + + parent = smb_url_up (data->current_url); + if (parent == NULL) + { + /* At smb:// level — go back to root (connections list) */ + data->at_root = TRUE; + g_free (data->current_url); + data->current_url = NULL; + + if (data->entries != NULL) + { + g_ptr_array_free (data->entries, TRUE); + data->entries = NULL; + } + return MC_PPR_OK; + } + + /* Navigate up within the connection */ + g_free (data->current_url); + data->current_url = parent; + + if (data->entries != NULL) + g_ptr_array_free (data->entries, TRUE); + + smbc_set_context (data->ctx); + data->entries = smb_load_entries (data); + return MC_PPR_OK; + } + } + + /* At root: enter a saved connection */ + if (data->at_root) + { + const smb_connection_t *conn; + + conn = find_connection (data, path); + if (conn == NULL) + return MC_PPR_FAILED; + + return enter_connection (data, conn); + } + + /* Inside connection: navigate into subdir/share */ + { + const smb_entry_t *entry; + char *new_url; + + entry = find_entry (data, path); + if (entry == NULL || !smb_entry_is_dir (entry->smbc_type)) + return MC_PPR_FAILED; + + if (g_str_has_suffix (data->current_url, "/")) + new_url = g_strdup_printf ("%s%s", data->current_url, path); + else + new_url = g_strdup_printf ("%s/%s", data->current_url, path); + + g_free (data->current_url); + data->current_url = new_url; + + if (data->entries != NULL) + g_ptr_array_free (data->entries, TRUE); + + smbc_set_context (data->ctx); + data->entries = smb_load_entries (data); + return MC_PPR_OK; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +samba_enter (void *plugin_data, const char *name, const struct stat *st) +{ + samba_data_t *data = (samba_data_t *) plugin_data; + + (void) st; + + /* At root: enter saved connection */ + if (data->at_root) + { + const smb_connection_t *conn; + + conn = find_connection (data, name); + if (conn == NULL) + return MC_PPR_FAILED; + + return enter_connection (data, conn); + } + + /* Inside connection */ + { + const smb_entry_t *entry; + + entry = find_entry (data, name); + if (entry == NULL) + return MC_PPR_FAILED; + + if (smb_entry_is_dir (entry->smbc_type)) + { + char *new_url; + + if (g_str_has_suffix (data->current_url, "/")) + new_url = g_strdup_printf ("%s%s", data->current_url, name); + else + new_url = g_strdup_printf ("%s/%s", data->current_url, name); + + g_free (data->current_url); + data->current_url = new_url; + + if (data->entries != NULL) + g_ptr_array_free (data->entries, TRUE); + + smbc_set_context (data->ctx); + data->entries = smb_load_entries (data); + return MC_PPR_OK; + } + + /* File — let mc handle via get_local_copy / viewer */ + return MC_PPR_NOT_SUPPORTED; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +samba_get_local_copy (void *plugin_data, const char *fname, char **local_path) +{ + samba_data_t *data = (samba_data_t *) plugin_data; + char *smb_url; + int smb_fd; + int local_fd; + GError *error = NULL; + char buf[8192]; + ssize_t n; + + if (data->at_root || data->current_url == NULL) + return MC_PPR_FAILED; + + smb_url = g_strdup_printf ("%s/%s", data->current_url, fname); + + smbc_set_context (data->ctx); + smb_fd = smbc_open (smb_url, O_RDONLY, 0); + g_free (smb_url); + + if (smb_fd < 0) + return MC_PPR_FAILED; + + local_fd = g_file_open_tmp ("mc-pp-smb-XXXXXX", local_path, &error); + if (local_fd == -1) + { + if (error != NULL) + g_error_free (error); + smbc_close (smb_fd); + return MC_PPR_FAILED; + } + + while ((n = smbc_read (smb_fd, buf, sizeof (buf))) > 0) + { + if (write (local_fd, buf, (size_t) n) != n) + { + close (local_fd); + smbc_close (smb_fd); + unlink (*local_path); + g_free (*local_path); + *local_path = NULL; + return MC_PPR_FAILED; + } + } + + close (local_fd); + smbc_close (smb_fd); + + if (n < 0) + { + unlink (*local_path); + g_free (*local_path); + *local_path = NULL; + return MC_PPR_FAILED; + } + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +samba_delete_items (void *plugin_data, const char **names, int count) +{ + samba_data_t *data = (samba_data_t *) plugin_data; + int i; + + if (data->at_root) + { + /* Delete saved connections */ + for (i = 0; i < count; i++) + { + guint j; + + for (j = 0; j < data->connections->len; j++) + { + const smb_connection_t *conn = + (const smb_connection_t *) g_ptr_array_index (data->connections, j); + + if (strcmp (conn->label, names[i]) == 0) + { + g_ptr_array_remove_index (data->connections, j); + break; + } + } + } + + save_connections (data->connections_file, data->connections); + return MC_PPR_OK; + } + + /* Inside connection: delete files/dirs on SMB */ + smbc_set_context (data->ctx); + + for (i = 0; i < count; i++) + { + const smb_entry_t *entry; + char *url; + + entry = find_entry (data, names[i]); + url = g_strdup_printf ("%s/%s", data->current_url, names[i]); + + if (entry != NULL && entry->smbc_type == SMBC_DIR) + smbc_rmdir (url); + else + smbc_unlink (url); + + g_free (url); + } + + if (data->entries != NULL) + g_ptr_array_free (data->entries, TRUE); + data->entries = smb_load_entries (data); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +samba_get_title (void *plugin_data) +{ + samba_data_t *data = (samba_data_t *) plugin_data; + + g_free (data->title_buf); + + if (data->at_root || data->current_url == NULL) + { + data->title_buf = g_strdup ("/"); + return data->title_buf; + } + + /* Strip "smb:/" to get path for display */ + if (strlen (data->current_url) <= 5) + data->title_buf = g_strdup ("/"); + else + data->title_buf = g_strdup (data->current_url + 5); + + return data->title_buf; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +samba_create_item (void *plugin_data) +{ + samba_data_t *data = (samba_data_t *) plugin_data; + smb_connection_t *conn; + char *label = NULL; + char *server = NULL; + char *share = NULL; + char *username = NULL; + char *password = NULL; + + if (!data->at_root) + return MC_PPR_NOT_SUPPORTED; + + if (!show_connection_dialog (&label, &server, &share, &username, &password)) + { + g_free (label); + g_free (server); + g_free (share); + g_free (username); + g_free (password); + return MC_PPR_FAILED; + } + + if (label == NULL || label[0] == '\0' || server == NULL || server[0] == '\0') + { + g_free (label); + g_free (server); + g_free (share); + g_free (username); + g_free (password); + return MC_PPR_FAILED; + } + + conn = g_new0 (smb_connection_t, 1); + conn->label = label; + conn->server = server; + conn->share = share; + conn->username = username; + conn->password = password; + + /* Extract workgroup from DOMAIN\user if present */ + if (username != NULL) + { + char *backslash; + + backslash = strchr (username, '\\'); + if (backslash != NULL) + { + conn->workgroup = g_strndup (username, (gsize) (backslash - username)); + /* Keep full username — libsmbclient handles DOMAIN\user */ + } + } + + g_ptr_array_add (data->connections, conn); + save_connections (data->connections_file, data->connections); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +const mc_panel_plugin_t *mc_panel_plugin_register (void); + +const mc_panel_plugin_t * +mc_panel_plugin_register (void) +{ + return &samba_plugin; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/panel-plugins/sftp/Makefile.am b/src/panel-plugins/sftp/Makefile.am new file mode 100644 index 0000000000..a36c8a048b --- /dev/null +++ b/src/panel-plugins/sftp/Makefile.am @@ -0,0 +1,7 @@ +panelplugindir = $(panel_plugins_dir) +panelplugin_LTLIBRARIES = mc-panel-sftp.la + +mc_panel_sftp_la_SOURCES = sftp.c +mc_panel_sftp_la_CPPFLAGS = $(GLIB_CFLAGS) $(LIBSSH_CFLAGS) -I$(top_srcdir) +mc_panel_sftp_la_LDFLAGS = -module -avoid-version +mc_panel_sftp_la_LIBADD = $(GLIB_LIBS) $(LIBSSH_LIBS) diff --git a/src/panel-plugins/sftp/sftp.c b/src/panel-plugins/sftp/sftp.c new file mode 100644 index 0000000000..7db1d42ae6 --- /dev/null +++ b/src/panel-plugins/sftp/sftp.c @@ -0,0 +1,1278 @@ +/* + SFTP network browser panel plugin (libssh2). + + Copyright (C) 2026 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#ifdef HAVE_ARPA_INET_H +#include +#endif + +#include +#include + +#include "lib/global.h" +#include "lib/panel-plugin.h" +#include "lib/vfs/utilvfs.h" +#include "lib/widget.h" + +#include "src/filemanager/dir.h" + +/*** file scope type declarations ****************************************************************/ + +typedef struct +{ + char *label; + char *host; + int port; + char *user; + char *path; + char *password; + char *pubkey; + char *privkey; + gboolean use_agent; +} sftp_connection_t; + +typedef struct +{ + char *name; + struct stat st; + gboolean is_dir; +} sftp_entry_t; + +typedef struct +{ + mc_panel_host_t *host; + + gboolean at_root; + char *current_path; + GPtrArray *entries; + + GPtrArray *connections; + char *connections_file; + + sftp_connection_t *active_connection; + + int socket_handle; + LIBSSH2_SESSION *session; + LIBSSH2_SFTP *sftp_session; + + char *title_buf; +} sftp_data_t; + +/*** forward declarations (file scope functions) *************************************************/ + +static void *sftp_open (mc_panel_host_t *host, const char *open_path); +static void sftp_close (void *plugin_data); +static mc_pp_result_t sftp_get_items (void *plugin_data, void *list_ptr); +static mc_pp_result_t sftp_chdir (void *plugin_data, const char *path); +static mc_pp_result_t sftp_enter (void *plugin_data, const char *name, const struct stat *st); +static mc_pp_result_t sftp_get_local_copy (void *plugin_data, const char *fname, char **local_path); +static mc_pp_result_t sftp_delete_items (void *plugin_data, const char **names, int count); +static const char *sftp_get_title (void *plugin_data); +static mc_pp_result_t sftp_create_item (void *plugin_data); +static void sftp_disconnect (sftp_data_t *data); + +/*** file scope variables ************************************************************************/ + +#define SFTP_DEFAULT_PORT 22 + +#ifndef LIBSSH2_INVALID_SOCKET +#define LIBSSH2_INVALID_SOCKET -1 +#endif + +static guint sftp_libssh2_refcount = 0; + +static const mc_panel_plugin_t sftp_plugin = { + .api_version = MC_PANEL_PLUGIN_API_VERSION, + .name = "sftp", + .display_name = "SFTP network", + .proto = "sftp", + .prefix = NULL, + .flags = + MC_PPF_NAVIGATE | MC_PPF_GET_FILES | MC_PPF_DELETE | MC_PPF_CUSTOM_TITLE | MC_PPF_CREATE, + + .open = sftp_open, + .close = sftp_close, + .get_items = sftp_get_items, + + .chdir = sftp_chdir, + .enter = sftp_enter, + .get_local_copy = sftp_get_local_copy, + .delete_items = sftp_delete_items, + .get_title = sftp_get_title, + .handle_key = NULL, + .create_item = sftp_create_item, +}; + +/*** file scope functions ************************************************************************/ + +static void +add_entry (dir_list *list, const char *name, mode_t mode, off_t size, time_t mtime) +{ + struct stat st; + + memset (&st, 0, sizeof (st)); + st.st_mode = mode; + st.st_size = size; + st.st_mtime = mtime; + st.st_uid = getuid (); + st.st_gid = getgid (); + st.st_nlink = 1; + + dir_list_append (list, name, &st, S_ISDIR (mode), FALSE); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +sftp_entry_free (gpointer p) +{ + sftp_entry_t *e = (sftp_entry_t *) p; + + g_free (e->name); + g_free (e); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +sftp_connection_free (gpointer p) +{ + sftp_connection_t *c = (sftp_connection_t *) p; + + g_free (c->label); + g_free (c->host); + g_free (c->user); + g_free (c->path); + g_free (c->password); + g_free (c->pubkey); + g_free (c->privkey); + g_free (c); +} + +/* --------------------------------------------------------------------------------------------- */ + +static char * +get_connections_file_path (void) +{ + return g_build_filename (g_get_user_config_dir (), "mc", "sftp-connections.ini", (char *) NULL); +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +load_connections (const char *filepath) +{ + GPtrArray *arr; + GKeyFile *kf; + gchar **groups; + gsize n_groups, i; + + arr = g_ptr_array_new_with_free_func (sftp_connection_free); + + kf = g_key_file_new (); + if (!g_key_file_load_from_file (kf, filepath, G_KEY_FILE_NONE, NULL)) + { + g_key_file_free (kf); + return arr; + } + + groups = g_key_file_get_groups (kf, &n_groups); + for (i = 0; i < n_groups; i++) + { + sftp_connection_t *conn; + GError *error = NULL; + + conn = g_new0 (sftp_connection_t, 1); + conn->label = g_strdup (groups[i]); + conn->host = g_key_file_get_string (kf, groups[i], "host", NULL); + conn->user = g_key_file_get_string (kf, groups[i], "user", NULL); + conn->path = g_key_file_get_string (kf, groups[i], "path", NULL); + conn->password = g_key_file_get_string (kf, groups[i], "password", NULL); + conn->pubkey = g_key_file_get_string (kf, groups[i], "pubkey", NULL); + conn->privkey = g_key_file_get_string (kf, groups[i], "privkey", NULL); + + conn->port = g_key_file_get_integer (kf, groups[i], "port", &error); + if (error != NULL) + { + g_error_free (error); + conn->port = SFTP_DEFAULT_PORT; + } + + error = NULL; + conn->use_agent = g_key_file_get_boolean (kf, groups[i], "use_agent", &error); + if (error != NULL) + { + g_error_free (error); + conn->use_agent = TRUE; + } + + if (conn->host == NULL || conn->host[0] == '\0') + { + sftp_connection_free (conn); + continue; + } + + if (conn->port <= 0) + conn->port = SFTP_DEFAULT_PORT; + + if (conn->user == NULL || conn->user[0] == '\0') + { + conn->user = vfs_get_local_username (); + if (conn->user == NULL) + conn->user = g_strdup (g_get_user_name ()); + } + + if (conn->path == NULL || conn->path[0] == '\0') + conn->path = g_strdup ("/"); + + g_ptr_array_add (arr, conn); + } + + g_strfreev (groups); + g_key_file_free (kf); + return arr; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +save_connections (const char *filepath, GPtrArray *connections) +{ + GKeyFile *kf; + gchar *data; + gsize length; + gboolean ok; + gchar *dir; + guint i; + + kf = g_key_file_new (); + + for (i = 0; i < connections->len; i++) + { + const sftp_connection_t *conn = + (const sftp_connection_t *) g_ptr_array_index (connections, i); + + g_key_file_set_string (kf, conn->label, "host", conn->host); + g_key_file_set_integer (kf, conn->label, "port", conn->port > 0 ? conn->port : SFTP_DEFAULT_PORT); + + if (conn->user != NULL) + g_key_file_set_string (kf, conn->label, "user", conn->user); + if (conn->path != NULL) + g_key_file_set_string (kf, conn->label, "path", conn->path); + if (conn->password != NULL) + g_key_file_set_string (kf, conn->label, "password", conn->password); + if (conn->pubkey != NULL) + g_key_file_set_string (kf, conn->label, "pubkey", conn->pubkey); + if (conn->privkey != NULL) + g_key_file_set_string (kf, conn->label, "privkey", conn->privkey); + + g_key_file_set_boolean (kf, conn->label, "use_agent", conn->use_agent); + } + + data = g_key_file_to_data (kf, &length, NULL); + g_key_file_free (kf); + + if (data == NULL) + return FALSE; + + dir = g_path_get_dirname (filepath); + g_mkdir_with_parents (dir, 0700); + g_free (dir); + + ok = g_file_set_contents (filepath, data, (gssize) length, NULL); + g_free (data); + + return ok; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const sftp_connection_t * +find_connection (const sftp_data_t *data, const char *label) +{ + guint i; + + for (i = 0; i < data->connections->len; i++) + { + const sftp_connection_t *c = + (const sftp_connection_t *) g_ptr_array_index (data->connections, i); + + if (strcmp (c->label, label) == 0) + return c; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const sftp_entry_t * +find_entry (const sftp_data_t *data, const char *name) +{ + guint i; + + if (data->entries == NULL) + return NULL; + + for (i = 0; i < data->entries->len; i++) + { + const sftp_entry_t *e = (const sftp_entry_t *) g_ptr_array_index (data->entries, i); + + if (strcmp (e->name, name) == 0) + return e; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +static char * +sftp_join_path (const char *base, const char *name) +{ + if (strcmp (base, "/") == 0) + return g_strdup_printf ("/%s", name); + + return g_strdup_printf ("%s/%s", base, name); +} + +/* --------------------------------------------------------------------------------------------- */ + +static char * +sftp_path_up (const char *path) +{ + const char *last; + + if (path == NULL || strcmp (path, "/") == 0) + return NULL; + + last = strrchr (path, '/'); + if (last == NULL || last == path) + return g_strdup ("/"); + + return g_strndup (path, (gsize) (last - path)); +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +sftp_auth_has_method (const char *auth_list, const char *method) +{ + const char *p; + size_t method_len; + + if (auth_list == NULL || method == NULL) + return FALSE; + + p = auth_list; + method_len = strlen (method); + + while (*p != '\0') + { + const char *end; + size_t token_len; + + end = strchr (p, ','); + if (end == NULL) + end = p + strlen (p); + + token_len = (size_t) (end - p); + if (token_len == method_len && strncmp (p, method, method_len) == 0) + return TRUE; + + if (*end == '\0') + break; + p = end + 1; + } + + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static int +sftp_open_socket (const sftp_connection_t *conn) +{ + struct addrinfo hints, *res = NULL, *curr; + int sock = LIBSSH2_INVALID_SOCKET; + char port_buf[BUF_TINY]; + + if (conn->host == NULL || conn->host[0] == '\0') + return LIBSSH2_INVALID_SOCKET; + + memset (&hints, 0, sizeof (hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + g_snprintf (port_buf, sizeof (port_buf), "%d", conn->port > 0 ? conn->port : SFTP_DEFAULT_PORT); + + if (getaddrinfo (conn->host, port_buf, &hints, &res) != 0) + return LIBSSH2_INVALID_SOCKET; + + for (curr = res; curr != NULL; curr = curr->ai_next) + { + sock = socket (curr->ai_family, curr->ai_socktype, curr->ai_protocol); + if (sock < 0) + continue; + + if (connect (sock, curr->ai_addr, curr->ai_addrlen) == 0) + break; + + close (sock); + sock = LIBSSH2_INVALID_SOCKET; + } + + freeaddrinfo (res); + return sock; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +sftp_auth_agent (sftp_data_t *data, const char *user) +{ + LIBSSH2_AGENT *agent; + struct libssh2_agent_publickey *identity = NULL; + int rc; + + agent = libssh2_agent_init (data->session); + if (agent == NULL) + return FALSE; + + rc = libssh2_agent_connect (agent); + if (rc != 0) + { + libssh2_agent_free (agent); + return FALSE; + } + + rc = libssh2_agent_list_identities (agent); + if (rc != 0) + { + libssh2_agent_disconnect (agent); + libssh2_agent_free (agent); + return FALSE; + } + + while (libssh2_agent_get_identity (agent, &identity, identity) == 0) + { + rc = libssh2_agent_userauth (agent, user, identity); + if (rc == 0) + { + libssh2_agent_disconnect (agent); + libssh2_agent_free (agent); + return TRUE; + } + } + + libssh2_agent_disconnect (agent); + libssh2_agent_free (agent); + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +sftp_connect (sftp_data_t *data, sftp_connection_t *conn) +{ + const char *user; + const char *auth_list; + + if (data == NULL || conn == NULL) + return FALSE; + + data->socket_handle = sftp_open_socket (conn); + if (data->socket_handle == LIBSSH2_INVALID_SOCKET) + return FALSE; + + data->session = libssh2_session_init (); + if (data->session == NULL) + goto fail; + + libssh2_session_set_blocking (data->session, 1); + + if (libssh2_session_handshake (data->session, (libssh2_socket_t) data->socket_handle) != 0) + goto fail; + + user = (conn->user != NULL && conn->user[0] != '\0') ? conn->user : g_get_user_name (); + auth_list = libssh2_userauth_list (data->session, user, (unsigned int) strlen (user)); + + if (conn->use_agent && sftp_auth_has_method (auth_list, "publickey")) + { + if (sftp_auth_agent (data, user)) + goto auth_ok; + } + + if (conn->privkey != NULL && conn->privkey[0] != '\0' + && sftp_auth_has_method (auth_list, "publickey")) + { + if (libssh2_userauth_publickey_fromfile (data->session, user, conn->pubkey, conn->privkey, + conn->password) == 0) + goto auth_ok; + } + + if (conn->password != NULL && conn->password[0] != '\0' && sftp_auth_has_method (auth_list, "password")) + { + if (libssh2_userauth_password (data->session, user, conn->password) == 0) + goto auth_ok; + } + + if (sftp_auth_has_method (auth_list, "password")) + { + char *pwd; + char *prompt; + + prompt = g_strdup_printf (_ ("Enter password for %s@%s"), user, conn->host); + pwd = input_dialog (_ ("SFTP password"), prompt, "sftp-password", INPUT_PASSWORD, + INPUT_COMPLETE_NONE); + g_free (prompt); + + if (pwd != NULL && pwd[0] != '\0' + && libssh2_userauth_password (data->session, user, pwd) == 0) + { + g_free (conn->password); + conn->password = pwd; + goto auth_ok; + } + + g_free (pwd); + } + + goto fail; + +auth_ok: + data->sftp_session = libssh2_sftp_init (data->session); + if (data->sftp_session == NULL) + goto fail; + + data->active_connection = conn; + return TRUE; + +fail: + sftp_disconnect (data); + return FALSE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +sftp_disconnect (sftp_data_t *data) +{ + if (data->sftp_session != NULL) + { + libssh2_sftp_shutdown (data->sftp_session); + data->sftp_session = NULL; + } + + if (data->session != NULL) + { + libssh2_session_disconnect (data->session, "Normal Shutdown"); + libssh2_session_free (data->session); + data->session = NULL; + } + + if (data->socket_handle != LIBSSH2_INVALID_SOCKET) + { + close (data->socket_handle); + data->socket_handle = LIBSSH2_INVALID_SOCKET; + } + + data->active_connection = NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +sftp_attr_to_stat (const LIBSSH2_SFTP_ATTRIBUTES *attrs, struct stat *st) +{ + memset (st, 0, sizeof (*st)); + + st->st_uid = getuid (); + st->st_gid = getgid (); + st->st_nlink = 1; + + if ((attrs->flags & LIBSSH2_SFTP_ATTR_UIDGID) != 0) + { + st->st_uid = attrs->uid; + st->st_gid = attrs->gid; + } + + if ((attrs->flags & LIBSSH2_SFTP_ATTR_SIZE) != 0) + st->st_size = (off_t) attrs->filesize; + + if ((attrs->flags & LIBSSH2_SFTP_ATTR_ACMODTIME) != 0) + { + st->st_atime = attrs->atime; + st->st_mtime = attrs->mtime; + st->st_ctime = attrs->mtime; + } + else + { + st->st_mtime = time (NULL); + } + + if ((attrs->flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) != 0) + st->st_mode = attrs->permissions; +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +sftp_load_entries (sftp_data_t *data) +{ + GPtrArray *arr; + LIBSSH2_SFTP_HANDLE *dirh; + + arr = g_ptr_array_new_with_free_func (sftp_entry_free); + + dirh = libssh2_sftp_opendir (data->sftp_session, data->current_path); + if (dirh == NULL) + return arr; + + while (TRUE) + { + char mem[BUF_MEDIUM]; + LIBSSH2_SFTP_ATTRIBUTES attrs; + ssize_t rc; + + rc = libssh2_sftp_readdir_ex (dirh, mem, sizeof (mem), NULL, 0, &attrs); + if (rc <= 0) + break; + + if ((size_t) rc >= sizeof (mem)) + rc = (ssize_t) sizeof (mem) - 1; + mem[rc] = '\0'; + + if (strcmp (mem, ".") == 0 || strcmp (mem, "..") == 0) + continue; + + { + sftp_entry_t *entry; + mode_t mode; + + entry = g_new0 (sftp_entry_t, 1); + entry->name = g_strdup (mem); + sftp_attr_to_stat (&attrs, &entry->st); + + mode = entry->st.st_mode; + if (!S_ISDIR (mode) && !S_ISREG (mode) && !S_ISLNK (mode)) + { + mode = S_IFREG | 0644; + entry->st.st_mode = mode; + } + + entry->is_dir = S_ISDIR (entry->st.st_mode); + + g_ptr_array_add (arr, entry); + } + } + + libssh2_sftp_closedir (dirh); + return arr; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +sftp_reload_entries (sftp_data_t *data) +{ + if (data->entries != NULL) + { + g_ptr_array_free (data->entries, TRUE); + data->entries = NULL; + } + + if (data->at_root) + return TRUE; + + if (data->sftp_session == NULL) + return FALSE; + + data->entries = sftp_load_entries (data); + return TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +show_connection_dialog (char **label, char **host, char **port, char **user, char **path, + char **password, char **pubkey, char **privkey, gboolean *use_agent) +{ + /* clang-format off */ + quick_widget_t quick_widgets[] = { + QUICK_LABELED_INPUT (N_("Name:"), input_label_above, + *label != NULL ? *label : "", "sftp-conn-label", + label, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Host:"), input_label_above, + *host != NULL ? *host : "", "sftp-conn-host", + host, NULL, FALSE, FALSE, INPUT_COMPLETE_HOSTNAMES), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Port:"), input_label_above, + *port != NULL ? *port : "22", "sftp-conn-port", + port, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("User:"), input_label_above, + *user != NULL ? *user : "", "sftp-conn-user", + user, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Remote path:"), input_label_above, + *path != NULL ? *path : "/", "sftp-conn-path", + path, NULL, FALSE, FALSE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Password:"), input_label_above, + *password != NULL ? *password : "", "sftp-conn-pass", + password, NULL, TRUE, TRUE, INPUT_COMPLETE_NONE), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Public key file:"), input_label_above, + *pubkey != NULL ? *pubkey : "", "sftp-conn-pubkey", + pubkey, NULL, FALSE, FALSE, INPUT_COMPLETE_FILENAMES), + QUICK_SEPARATOR (FALSE), + QUICK_LABELED_INPUT (N_("Private key file:"), input_label_above, + *privkey != NULL ? *privkey : "", "sftp-conn-privkey", + privkey, NULL, FALSE, FALSE, INPUT_COMPLETE_FILENAMES), + QUICK_SEPARATOR (FALSE), + QUICK_CHECKBOX (N_("Use SSH &agent"), use_agent, NULL), + QUICK_BUTTONS_OK_CANCEL, + QUICK_END, + }; + /* clang-format on */ + + WRect r = { -1, -1, 0, 56 }; + + quick_dialog_t qdlg = { + .rect = r, + .title = N_ ("SFTP Connection"), + .help = "[SFTP Plugin]", + .widgets = quick_widgets, + .callback = NULL, + .mouse_callback = NULL, + }; + + return (quick_dialog (&qdlg) == B_ENTER); +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +sftp_activate_connection (sftp_data_t *data, sftp_connection_t *conn) +{ + char *path; + + sftp_disconnect (data); + + if (!sftp_connect (data, conn)) + return FALSE; + + path = (conn->path != NULL && conn->path[0] != '\0') ? g_strdup (conn->path) : g_strdup ("/"); + if (path[0] != '/') + { + char *tmp = g_strdup_printf ("/%s", path); + g_free (path); + path = tmp; + } + + g_free (data->current_path); + data->current_path = path; + data->at_root = FALSE; + + sftp_reload_entries (data); + return TRUE; +} + +/* --------------------------------------------------------------------------------------------- */ +/* Plugin callbacks */ +/* --------------------------------------------------------------------------------------------- */ + +static void * +sftp_open (mc_panel_host_t *host, const char *open_path) +{ + sftp_data_t *data; + + (void) open_path; + + if (sftp_libssh2_refcount == 0) + { + if (libssh2_init (0) != 0) + return NULL; + } + sftp_libssh2_refcount++; + + data = g_new0 (sftp_data_t, 1); + data->host = host; + data->at_root = TRUE; + data->current_path = NULL; + data->entries = NULL; + data->title_buf = NULL; + + data->socket_handle = LIBSSH2_INVALID_SOCKET; + data->session = NULL; + data->sftp_session = NULL; + data->active_connection = NULL; + + data->connections_file = get_connections_file_path (); + data->connections = load_connections (data->connections_file); + + return data; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +sftp_close (void *plugin_data) +{ + sftp_data_t *data = (sftp_data_t *) plugin_data; + + sftp_disconnect (data); + + if (data->entries != NULL) + g_ptr_array_free (data->entries, TRUE); + + g_ptr_array_free (data->connections, TRUE); + + g_free (data->current_path); + g_free (data->title_buf); + g_free (data->connections_file); + g_free (data); + + if (sftp_libssh2_refcount > 0) + sftp_libssh2_refcount--; + + if (sftp_libssh2_refcount == 0) + libssh2_exit (); +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +sftp_get_items (void *plugin_data, void *list_ptr) +{ + dir_list *list = (dir_list *) list_ptr; + sftp_data_t *data = (sftp_data_t *) plugin_data; + guint i; + + if (data->at_root) + { + for (i = 0; i < data->connections->len; i++) + { + const sftp_connection_t *conn = + (const sftp_connection_t *) g_ptr_array_index (data->connections, i); + + add_entry (list, conn->label, S_IFDIR | 0755, 0, time (NULL)); + } + return MC_PPR_OK; + } + + if (data->entries != NULL) + { + for (i = 0; i < data->entries->len; i++) + { + const sftp_entry_t *e = (const sftp_entry_t *) g_ptr_array_index (data->entries, i); + mode_t mode; + off_t size; + + mode = e->st.st_mode; + if (!S_ISDIR (mode) && !S_ISREG (mode) && !S_ISLNK (mode)) + mode = S_IFREG | 0644; + + size = S_ISDIR (mode) ? 0 : e->st.st_size; + add_entry (list, e->name, mode, size, e->st.st_mtime != 0 ? e->st.st_mtime : time (NULL)); + } + } + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +sftp_chdir (void *plugin_data, const char *path) +{ + sftp_data_t *data = (sftp_data_t *) plugin_data; + + if (strcmp (path, "..") == 0) + { + if (data->at_root) + return MC_PPR_NOT_SUPPORTED; + + if (data->current_path != NULL && strcmp (data->current_path, "/") == 0) + { + data->at_root = TRUE; + sftp_disconnect (data); + + if (data->entries != NULL) + { + g_ptr_array_free (data->entries, TRUE); + data->entries = NULL; + } + + g_free (data->current_path); + data->current_path = NULL; + return MC_PPR_OK; + } + + { + char *parent = sftp_path_up (data->current_path); + + if (parent == NULL) + { + data->at_root = TRUE; + sftp_disconnect (data); + + if (data->entries != NULL) + { + g_ptr_array_free (data->entries, TRUE); + data->entries = NULL; + } + + g_free (data->current_path); + data->current_path = NULL; + return MC_PPR_OK; + } + + g_free (data->current_path); + data->current_path = parent; + sftp_reload_entries (data); + return MC_PPR_OK; + } + } + + if (data->at_root) + { + sftp_connection_t *conn = (sftp_connection_t *) find_connection (data, path); + + if (conn == NULL) + return MC_PPR_FAILED; + + return sftp_activate_connection (data, conn) ? MC_PPR_OK : MC_PPR_FAILED; + } + + { + const sftp_entry_t *entry; + char *new_path; + + entry = find_entry (data, path); + if (entry == NULL || !entry->is_dir) + return MC_PPR_FAILED; + + new_path = sftp_join_path (data->current_path, path); + + g_free (data->current_path); + data->current_path = new_path; + + sftp_reload_entries (data); + return MC_PPR_OK; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +sftp_enter (void *plugin_data, const char *name, const struct stat *st) +{ + sftp_data_t *data = (sftp_data_t *) plugin_data; + + (void) st; + + if (data->at_root) + { + sftp_connection_t *conn = (sftp_connection_t *) find_connection (data, name); + + if (conn == NULL) + return MC_PPR_FAILED; + + return sftp_activate_connection (data, conn) ? MC_PPR_OK : MC_PPR_FAILED; + } + + { + const sftp_entry_t *entry; + + entry = find_entry (data, name); + if (entry == NULL) + return MC_PPR_FAILED; + + if (entry->is_dir) + { + char *new_path; + + new_path = sftp_join_path (data->current_path, name); + g_free (data->current_path); + data->current_path = new_path; + + sftp_reload_entries (data); + return MC_PPR_OK; + } + } + + return MC_PPR_NOT_SUPPORTED; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +sftp_get_local_copy (void *plugin_data, const char *fname, char **local_path) +{ + sftp_data_t *data = (sftp_data_t *) plugin_data; + LIBSSH2_SFTP_HANDLE *fileh; + char *remote_path; + int local_fd; + GError *error = NULL; + + if (data->at_root || data->sftp_session == NULL || data->current_path == NULL) + return MC_PPR_FAILED; + + remote_path = sftp_join_path (data->current_path, fname); + fileh = libssh2_sftp_open (data->sftp_session, remote_path, LIBSSH2_FXF_READ, 0); + g_free (remote_path); + + if (fileh == NULL) + return MC_PPR_FAILED; + + local_fd = g_file_open_tmp ("mc-pp-sftp-XXXXXX", local_path, &error); + if (local_fd == -1) + { + if (error != NULL) + g_error_free (error); + libssh2_sftp_close (fileh); + return MC_PPR_FAILED; + } + + while (TRUE) + { + char buf[8192]; + ssize_t n; + + n = libssh2_sftp_read (fileh, buf, sizeof (buf)); + if (n == 0) + break; + + if (n < 0 || write (local_fd, buf, (size_t) n) != n) + { + close (local_fd); + libssh2_sftp_close (fileh); + unlink (*local_path); + g_free (*local_path); + *local_path = NULL; + return MC_PPR_FAILED; + } + } + + close (local_fd); + libssh2_sftp_close (fileh); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +sftp_delete_items (void *plugin_data, const char **names, int count) +{ + sftp_data_t *data = (sftp_data_t *) plugin_data; + int i; + gboolean failed = FALSE; + + if (data->at_root) + { + for (i = 0; i < count; i++) + { + guint j; + + for (j = 0; j < data->connections->len; j++) + { + const sftp_connection_t *conn = + (const sftp_connection_t *) g_ptr_array_index (data->connections, j); + + if (strcmp (conn->label, names[i]) == 0) + { + g_ptr_array_remove_index (data->connections, j); + break; + } + } + } + + save_connections (data->connections_file, data->connections); + return MC_PPR_OK; + } + + if (data->sftp_session == NULL) + return MC_PPR_FAILED; + + for (i = 0; i < count; i++) + { + const sftp_entry_t *entry; + char *remote_path; + int rc; + + entry = find_entry (data, names[i]); + if (entry == NULL) + continue; + + remote_path = sftp_join_path (data->current_path, names[i]); + + if (entry->is_dir) + rc = libssh2_sftp_rmdir_ex (data->sftp_session, remote_path, (unsigned int) strlen (remote_path)); + else + rc = libssh2_sftp_unlink_ex (data->sftp_session, remote_path, + (unsigned int) strlen (remote_path)); + + if (rc != 0) + failed = TRUE; + + g_free (remote_path); + } + + sftp_reload_entries (data); + + return failed ? MC_PPR_FAILED : MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +sftp_get_title (void *plugin_data) +{ + sftp_data_t *data = (sftp_data_t *) plugin_data; + + g_free (data->title_buf); + + if (data->at_root || data->active_connection == NULL) + { + data->title_buf = g_strdup ("/"); + return data->title_buf; + } + + data->title_buf = g_strdup_printf ("%s:%s", data->active_connection->host, + data->current_path != NULL ? data->current_path : "/"); + + return data->title_buf; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +sftp_create_item (void *plugin_data) +{ + sftp_data_t *data = (sftp_data_t *) plugin_data; + sftp_connection_t *conn; + + char *label = NULL; + char *host = NULL; + char *port = g_strdup ("22"); + char *user = vfs_get_local_username (); + char *path = g_strdup ("/"); + char *password = NULL; + char *pubkey = NULL; + char *privkey = NULL; + gboolean use_agent = TRUE; + + if (user == NULL) + user = g_strdup (g_get_user_name ()); + + if (!data->at_root) + return MC_PPR_NOT_SUPPORTED; + + if (!show_connection_dialog (&label, &host, &port, &user, &path, &password, &pubkey, &privkey, + &use_agent)) + { + g_free (label); + g_free (host); + g_free (port); + g_free (user); + g_free (path); + g_free (password); + g_free (pubkey); + g_free (privkey); + return MC_PPR_FAILED; + } + + if (label == NULL || label[0] == '\0' || host == NULL || host[0] == '\0') + { + g_free (label); + g_free (host); + g_free (port); + g_free (user); + g_free (path); + g_free (password); + g_free (pubkey); + g_free (privkey); + return MC_PPR_FAILED; + } + + conn = g_new0 (sftp_connection_t, 1); + conn->label = label; + conn->host = host; + conn->port = (port != NULL && port[0] != '\0') ? atoi (port) : SFTP_DEFAULT_PORT; + if (conn->port <= 0) + conn->port = SFTP_DEFAULT_PORT; + + conn->user = user; + conn->path = path; + + conn->password = (password != NULL && password[0] != '\0') ? password : NULL; + if (conn->password == NULL) + g_free (password); + + conn->pubkey = (pubkey != NULL && pubkey[0] != '\0') ? pubkey : NULL; + if (conn->pubkey == NULL) + g_free (pubkey); + + conn->privkey = (privkey != NULL && privkey[0] != '\0') ? privkey : NULL; + if (conn->privkey == NULL) + g_free (privkey); + + conn->use_agent = use_agent; + + g_free (port); + + g_ptr_array_add (data->connections, conn); + save_connections (data->connections_file, data->connections); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +const mc_panel_plugin_t *mc_panel_plugin_register (void); + +const mc_panel_plugin_t * +mc_panel_plugin_register (void) +{ + return &sftp_plugin; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/panel-plugins/systemd/Makefile.am b/src/panel-plugins/systemd/Makefile.am new file mode 100644 index 0000000000..b1c23f6153 --- /dev/null +++ b/src/panel-plugins/systemd/Makefile.am @@ -0,0 +1,7 @@ +panelplugindir = $(panel_plugins_dir) +panelplugin_LTLIBRARIES = mc-panel-systemd.la + +mc_panel_systemd_la_SOURCES = systemd.c +mc_panel_systemd_la_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) +mc_panel_systemd_la_LDFLAGS = -module -avoid-version +mc_panel_systemd_la_LIBADD = $(GLIB_LIBS) diff --git a/src/panel-plugins/systemd/systemd.c b/src/panel-plugins/systemd/systemd.c new file mode 100644 index 0000000000..523c46f199 --- /dev/null +++ b/src/panel-plugins/systemd/systemd.c @@ -0,0 +1,651 @@ +/* + Systemd unit browser panel plugin. + + Copyright (C) 2025 + Free Software Foundation, Inc. + + Written by: + Ilia Maslakov , 2026. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include + +#include +#include +#include +#include + +#include "lib/global.h" +#include "lib/panel-plugin.h" + +#include "src/filemanager/dir.h" + +/*** file scope type declarations ****************************************************************/ + +typedef enum +{ + UNIT_STATE_ACTIVE, + UNIT_STATE_INACTIVE, + UNIT_STATE_FAILED +} unit_state_t; + +typedef struct +{ + char *name; + unit_state_t state; +} systemd_unit_t; + +typedef struct +{ + mc_panel_host_t *host; + char *current_type; /* NULL = root, "services" = inside type dir */ + GPtrArray *units; /* array of systemd_unit_t* */ + char *title_buf; /* owned buffer for get_title() return */ +} systemd_data_t; + +/*** file scope variables ************************************************************************/ + +static const struct +{ + const char *dir_name; + const char *type_arg; +} unit_type_dirs[] = { + { "services", "service" }, + { "timers", "timer" }, + { "sockets", "socket" }, + { "targets", "target" }, + { "mounts", "mount" }, + { "slices", "slice" }, + { "paths", "path" }, + { "scopes", "scope" }, + { "automounts", "automount" }, + { "swaps", "swap" }, +}; + +static const size_t unit_type_dirs_count = G_N_ELEMENTS (unit_type_dirs); + +/*** forward declarations (file scope functions) *************************************************/ + +static void *systemd_open (mc_panel_host_t *host, const char *open_path); +static void systemd_close (void *plugin_data); +static mc_pp_result_t systemd_get_items (void *plugin_data, void *list_ptr); +static mc_pp_result_t systemd_chdir (void *plugin_data, const char *path); +static mc_pp_result_t systemd_enter (void *plugin_data, const char *name, const struct stat *st); +static mc_pp_result_t systemd_get_local_copy (void *plugin_data, const char *fname, + char **local_path); +static mc_pp_result_t systemd_delete_items (void *plugin_data, const char **names, int count); +static const char *systemd_get_title (void *plugin_data); + +static const mc_panel_plugin_t systemd_plugin = { + .api_version = MC_PANEL_PLUGIN_API_VERSION, + .name = "systemd", + .display_name = "Systemd units", + .proto = "systemd", + .prefix = NULL, + .flags = MC_PPF_NAVIGATE | MC_PPF_GET_FILES | MC_PPF_DELETE | MC_PPF_CUSTOM_TITLE, + + .open = systemd_open, + .close = systemd_close, + .get_items = systemd_get_items, + + .chdir = systemd_chdir, + .enter = systemd_enter, + .get_local_copy = systemd_get_local_copy, + .delete_items = systemd_delete_items, + .get_title = systemd_get_title, + .handle_key = NULL, +}; + +/*** file scope functions ************************************************************************/ + +static void +add_entry (dir_list *list, const char *name, mode_t mode, off_t size) +{ + struct stat st; + + memset (&st, 0, sizeof (st)); + st.st_mode = mode; + st.st_size = size; + st.st_mtime = time (NULL); + st.st_uid = getuid (); + st.st_gid = getgid (); + st.st_nlink = 1; + + dir_list_append (list, name, &st, S_ISDIR (mode), FALSE); +} + +/* --------------------------------------------------------------------------------------------- */ + +static mode_t +state_to_mode (unit_state_t state) +{ + switch (state) + { + case UNIT_STATE_ACTIVE: + return S_IFREG | 0755; + case UNIT_STATE_FAILED: + return S_IFREG | 0000; + case UNIT_STATE_INACTIVE: + default: + return S_IFREG | 0644; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +state_to_prefix (unit_state_t state) +{ + switch (state) + { + case UNIT_STATE_ACTIVE: + return "[ACTIVE] "; + case UNIT_STATE_FAILED: + return "[FAILED] "; + case UNIT_STATE_INACTIVE: + default: + return "[INACTIVE] "; + } +} + +/* --------------------------------------------------------------------------------------------- */ + +/* Strip "[STATE] " prefix from display name to get real unit name */ +static const char * +strip_state_prefix (const char *display_name) +{ + const char *p; + + if (display_name[0] != '[') + return display_name; + + p = strchr (display_name, ']'); + if (p == NULL) + return display_name; + + p++; + while (*p == ' ') + p++; + + return p; +} + +/* --------------------------------------------------------------------------------------------- */ + +static gboolean +systemd_run_cmd (const char *cmd, char **output) +{ + gchar *std_out = NULL; + gchar *std_err = NULL; + gint exit_status = 0; + GError *error = NULL; + gboolean ok; + + ok = g_spawn_command_line_sync (cmd, &std_out, &std_err, &exit_status, &error); + + g_free (std_err); + + if (!ok) + { + if (error != NULL) + g_error_free (error); + g_free (std_out); + if (output != NULL) + *output = NULL; + return FALSE; + } + + if (output != NULL) + *output = std_out; + else + g_free (std_out); + + return (exit_status == 0); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +systemd_unit_free (gpointer p) +{ + systemd_unit_t *u = (systemd_unit_t *) p; + + g_free (u->name); + g_free (u); +} + +/* --------------------------------------------------------------------------------------------- */ + +static GPtrArray * +systemd_load_units (const char *type_arg) +{ + GPtrArray *arr; + char *cmd; + char *output = NULL; + char **lines; + int i; + + arr = g_ptr_array_new_with_free_func (systemd_unit_free); + + cmd = g_strdup_printf ("systemctl list-units --type=%s --all --no-legend --no-pager", type_arg); + + if (!systemd_run_cmd (cmd, &output) || output == NULL) + { + g_free (cmd); + g_free (output); + return arr; + } + g_free (cmd); + + lines = g_strsplit (output, "\n", -1); + g_free (output); + + for (i = 0; lines[i] != NULL; i++) + { + systemd_unit_t *unit; + char *line; + char **tokens; + int t; + /* parsed fields */ + const char *unit_name = NULL; + const char *active_state = NULL; + int field_idx; + + line = g_strstrip (lines[i]); + if (line[0] == '\0') + continue; + + /* skip leading bullet character (● or *) */ + if ((guchar) line[0] > 127 || line[0] == '*') + { + /* ● is a multi-byte UTF-8 sequence; skip it */ + if ((guchar) line[0] >= 0xC0) + { + /* skip full UTF-8 character */ + if ((guchar) line[0] >= 0xF0) + line += 4; + else if ((guchar) line[0] >= 0xE0) + line += 3; + else + line += 2; + } + else + line++; + + line = g_strstrip (line); + } + + /* tokenize: unit_name load_state active_state sub_state description... */ + tokens = g_strsplit_set (line, " \t", -1); + + /* collect non-empty tokens */ + field_idx = 0; + for (t = 0; tokens[t] != NULL && field_idx < 3; t++) + { + if (tokens[t][0] == '\0') + continue; + + switch (field_idx) + { + case 0: + unit_name = tokens[t]; + break; + case 2: + active_state = tokens[t]; + break; + default: + break; + } + field_idx++; + } + + if (unit_name == NULL || active_state == NULL) + { + g_strfreev (tokens); + continue; + } + + unit = g_new (systemd_unit_t, 1); + unit->name = g_strdup (unit_name); + + if (strcmp (active_state, "active") == 0) + unit->state = UNIT_STATE_ACTIVE; + else if (strcmp (active_state, "failed") == 0) + unit->state = UNIT_STATE_FAILED; + else + unit->state = UNIT_STATE_INACTIVE; + + g_ptr_array_add (arr, unit); + g_strfreev (tokens); + } + + g_strfreev (lines); + return arr; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +find_type_arg (const char *dir_name) +{ + size_t i; + + for (i = 0; i < unit_type_dirs_count; i++) + if (strcmp (unit_type_dirs[i].dir_name, dir_name) == 0) + return unit_type_dirs[i].type_arg; + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const systemd_unit_t * +find_unit (const systemd_data_t *data, const char *display_name) +{ + guint i; + const char *real_name; + + if (data->units == NULL) + return NULL; + + real_name = strip_state_prefix (display_name); + + for (i = 0; i < data->units->len; i++) + { + const systemd_unit_t *u = (const systemd_unit_t *) g_ptr_array_index (data->units, i); + + if (strcmp (u->name, real_name) == 0) + return u; + } + + return NULL; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +reload_units (systemd_data_t *data) +{ + const char *type_arg; + + if (data->current_type == NULL) + return; + + type_arg = find_type_arg (data->current_type); + if (type_arg == NULL) + return; + + if (data->units != NULL) + g_ptr_array_free (data->units, TRUE); + + data->units = systemd_load_units (type_arg); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void * +systemd_open (mc_panel_host_t *host, const char *open_path) +{ + systemd_data_t *data; + + (void) open_path; + + data = g_new0 (systemd_data_t, 1); + data->host = host; + data->current_type = NULL; + data->units = NULL; + data->title_buf = NULL; + + return data; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +systemd_close (void *plugin_data) +{ + systemd_data_t *data = (systemd_data_t *) plugin_data; + + g_free (data->current_type); + g_free (data->title_buf); + + if (data->units != NULL) + g_ptr_array_free (data->units, TRUE); + + g_free (data); +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +systemd_get_items (void *plugin_data, void *list_ptr) +{ + dir_list *list = (dir_list *) list_ptr; + systemd_data_t *data = (systemd_data_t *) plugin_data; + + if (data->current_type == NULL) + { + /* root view: show type directories */ + size_t i; + + for (i = 0; i < unit_type_dirs_count; i++) + add_entry (list, unit_type_dirs[i].dir_name, S_IFDIR | 0755, 4096); + } + else + { + /* inside a type dir: show units */ + guint i; + + if (data->units != NULL) + { + for (i = 0; i < data->units->len; i++) + { + const systemd_unit_t *u = + (const systemd_unit_t *) g_ptr_array_index (data->units, i); + char *display_name; + + display_name = + g_strdup_printf ("%s%s", state_to_prefix (u->state), u->name); + add_entry (list, display_name, state_to_mode (u->state), 0); + g_free (display_name); + } + } + } + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +systemd_chdir (void *plugin_data, const char *path) +{ + systemd_data_t *data = (systemd_data_t *) plugin_data; + + if (strcmp (path, "..") == 0) + { + if (data->current_type != NULL) + { + /* go back to root */ + g_free (data->current_type); + data->current_type = NULL; + + if (data->units != NULL) + { + g_ptr_array_free (data->units, TRUE); + data->units = NULL; + } + + return MC_PPR_OK; + } + + /* already at root — close plugin */ + return MC_PPR_NOT_SUPPORTED; + } + + /* entering a type directory */ + if (data->current_type == NULL) + { + const char *type_arg; + + type_arg = find_type_arg (path); + if (type_arg == NULL) + return MC_PPR_FAILED; + + data->current_type = g_strdup (path); + data->units = systemd_load_units (type_arg); + + return MC_PPR_OK; + } + + return MC_PPR_FAILED; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +systemd_enter (void *plugin_data, const char *name, const struct stat *st) +{ + systemd_data_t *data = (systemd_data_t *) plugin_data; + const systemd_unit_t *u; + char *cmd; + + (void) st; + + if (data->current_type == NULL) + return MC_PPR_NOT_SUPPORTED; /* directories handled by chdir */ + + u = find_unit (data, name); + if (u == NULL) + return MC_PPR_FAILED; + + if (u->state == UNIT_STATE_ACTIVE) + cmd = g_strdup_printf ("systemctl restart %s", u->name); + else + cmd = g_strdup_printf ("systemctl start %s", u->name); + + systemd_run_cmd (cmd, NULL); + g_free (cmd); + + reload_units (data); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +systemd_get_local_copy (void *plugin_data, const char *fname, char **local_path) +{ + systemd_data_t *data = (systemd_data_t *) plugin_data; + char *cmd; + char *output = NULL; + GError *error = NULL; + int fd; + + if (data->current_type == NULL) + return MC_PPR_FAILED; + + cmd = g_strdup_printf ("systemctl status %s --no-pager", strip_state_prefix (fname)); + systemd_run_cmd (cmd, &output); + g_free (cmd); + + if (output == NULL) + output = g_strdup ("(no status output)\n"); + + fd = g_file_open_tmp ("mc-pp-systemd-XXXXXX", local_path, &error); + if (fd == -1) + { + if (error != NULL) + g_error_free (error); + g_free (output); + return MC_PPR_FAILED; + } + + if (write (fd, output, strlen (output)) == -1) + { + close (fd); + unlink (*local_path); + g_free (*local_path); + *local_path = NULL; + g_free (output); + return MC_PPR_FAILED; + } + + close (fd); + g_free (output); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static mc_pp_result_t +systemd_delete_items (void *plugin_data, const char **names, int count) +{ + systemd_data_t *data = (systemd_data_t *) plugin_data; + int i; + + if (data->current_type == NULL) + return MC_PPR_FAILED; + + for (i = 0; i < count; i++) + { + char *cmd; + + cmd = g_strdup_printf ("systemctl stop %s", strip_state_prefix (names[i])); + systemd_run_cmd (cmd, NULL); + g_free (cmd); + } + + reload_units (data); + + return MC_PPR_OK; +} + +/* --------------------------------------------------------------------------------------------- */ + +static const char * +systemd_get_title (void *plugin_data) +{ + systemd_data_t *data = (systemd_data_t *) plugin_data; + + g_free (data->title_buf); + + if (data->current_type != NULL) + data->title_buf = g_strdup_printf ("/%s", data->current_type); + else + data->title_buf = g_strdup ("/"); + + return data->title_buf; +} + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* Entry point called by the panel plugin loader */ +const mc_panel_plugin_t *mc_panel_plugin_register (void); + +const mc_panel_plugin_t * +mc_panel_plugin_register (void) +{ + return &systemd_plugin; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/textconf.c b/src/textconf.c index 33659d3b3d..3f35110bfd 100644 --- a/src/textconf.c +++ b/src/textconf.c @@ -31,10 +31,6 @@ #include #include // uintmax_t -#if defined(ENABLE_VFS) && defined(ENABLE_VFS_SFTP) -#include -#endif - #include "lib/global.h" #include "lib/fileloc.h" #include "lib/mcconfig.h" @@ -165,11 +161,6 @@ show_version (void) #error "Cannot compile mc without S-Lang or ncurses" #endif -#if defined(ENABLE_VFS) && defined(ENABLE_VFS_SFTP) - printf (_ ("Built with libssh2 %d.%d.%d\n"), LIBSSH2_VERSION_MAJOR, LIBSSH2_VERSION_MINOR, - LIBSSH2_VERSION_PATCH); -#endif - for (i = 0; features[i] != NULL; i++) puts (_ (features[i])); diff --git a/src/vfs/Makefile.am b/src/vfs/Makefile.am index 485eb0b368..ad7803067c 100644 --- a/src/vfs/Makefile.am +++ b/src/vfs/Makefile.am @@ -28,7 +28,6 @@ endif if ENABLE_VFS_SFTP SUBDIRS += sftpfs -libmc_vfs_la_LIBADD += sftpfs/libvfs-sftpfs.la endif if ENABLE_VFS_SFS diff --git a/src/vfs/plugins_init.c b/src/vfs/plugins_init.c index 4534dc666e..4567ec6c0b 100644 --- a/src/vfs/plugins_init.c +++ b/src/vfs/plugins_init.c @@ -54,10 +54,6 @@ #include "ftpfs/ftpfs.h" #endif -#ifdef ENABLE_VFS_SFTP -#include "sftpfs/sftpfs.h" -#endif - #ifdef ENABLE_VFS_SFS #include "sfs/sfs.h" #endif @@ -105,12 +101,19 @@ vfs_plugins_init (void) #ifdef ENABLE_VFS_FTP vfs_init_ftpfs (); #endif -#ifdef ENABLE_VFS_SFTP - vfs_init_sftpfs (); -#endif #ifdef ENABLE_VFS_SHELL vfs_init_shell (); #endif + + vfs_plugins_load_dynamic (); +} + +/* --------------------------------------------------------------------------------------------- */ + +void +vfs_plugins_done (void) +{ + vfs_plugins_unload_dynamic (); } /* --------------------------------------------------------------------------------------------- */ diff --git a/src/vfs/plugins_init.h b/src/vfs/plugins_init.h index d706189747..c7fe6ed711 100644 --- a/src/vfs/plugins_init.h +++ b/src/vfs/plugins_init.h @@ -12,6 +12,7 @@ /*** declarations of public functions ************************************************************/ void vfs_plugins_init (void); +void vfs_plugins_done (void); /*** inline functions ****************************************************************************/ diff --git a/src/vfs/sftpfs/Makefile.am b/src/vfs/sftpfs/Makefile.am index 12905d11f1..8928a02aaf 100644 --- a/src/vfs/sftpfs/Makefile.am +++ b/src/vfs/sftpfs/Makefile.am @@ -1,12 +1,16 @@ AM_CPPFLAGS = $(GLIB_CFLAGS) -I$(top_srcdir) $(LIBSSH_CFLAGS) -noinst_LTLIBRARIES = libvfs-sftpfs.la +vfsplugindir = $(vfs_plugins_dir) +vfsplugin_LTLIBRARIES = mc-vfs-sftp.la -libvfs_sftpfs_la_SOURCES = \ +mc_vfs_sftp_la_SOURCES = \ config_parser.c \ connection.c \ dir.c \ file.c \ internal.c internal.h \ sftpfs.c sftpfs.h + +mc_vfs_sftp_la_LDFLAGS = -module -avoid-version +mc_vfs_sftp_la_LIBADD = $(GLIB_LIBS) $(LIBSSH_LIBS) diff --git a/src/vfs/sftpfs/sftpfs.c b/src/vfs/sftpfs/sftpfs.c index 1689e997d9..4b2cf6645b 100644 --- a/src/vfs/sftpfs/sftpfs.c +++ b/src/vfs/sftpfs/sftpfs.c @@ -810,7 +810,7 @@ sftpfs_cb_dir_load (struct vfs_class *me, struct vfs_s_inode *dir, const char *r */ void -vfs_init_sftpfs (void) +mc_vfs_plugin_init (void) { tcp_init (); diff --git a/src/vfs/sftpfs/sftpfs.h b/src/vfs/sftpfs/sftpfs.h index 663d9cc0c4..20da564cc3 100644 --- a/src/vfs/sftpfs/sftpfs.h +++ b/src/vfs/sftpfs/sftpfs.h @@ -16,7 +16,7 @@ /*** declarations of public functions ************************************************************/ -void vfs_init_sftpfs (void); +void mc_vfs_plugin_init (void); /*** inline functions ****************************************************************************/ diff --git a/tests/src/Makefile.am b/tests/src/Makefile.am index abba5ad1f2..eadee54d75 100644 --- a/tests/src/Makefile.am +++ b/tests/src/Makefile.am @@ -32,6 +32,7 @@ TESTS = \ execute__execute_external_editor_or_viewer \ execute__execute_get_external_cmd_opts_from_config \ file_history \ + learn_modifiers \ usermenu__test_condition \ usermenu__test_expand_format @@ -51,6 +52,9 @@ execute__execute_get_external_cmd_opts_from_config_SOURCES = \ file_history_SOURCES = \ file_history.c +learn_modifiers_SOURCES = \ + learn_modifiers.c + usermenu__test_condition_SOURCES = \ usermenu__test_condition.c diff --git a/tests/src/editor/Makefile.am b/tests/src/editor/Makefile.am index 81f3d9ae37..aaeb004b52 100644 --- a/tests/src/editor/Makefile.am +++ b/tests/src/editor/Makefile.am @@ -18,6 +18,7 @@ EXTRA_DIST = edit_complete_word_cmd_test_data.txt.in TESTS = \ edit_complete_word_cmd \ + edit_fold \ edit_insert_column_of_text \ edit_replace_cmd @@ -26,6 +27,9 @@ check_PROGRAMS = $(TESTS) edit_complete_word_cmd_SOURCES = \ edit_complete_word_cmd.c +edit_fold_SOURCES = \ + edit_fold.c + edit_insert_column_of_text_SOURCES = \ edit_insert_column_of_text.c diff --git a/tests/src/editor/edit_fold.c b/tests/src/editor/edit_fold.c new file mode 100644 index 0000000000..77b91d6325 --- /dev/null +++ b/tests/src/editor/edit_fold.c @@ -0,0 +1,744 @@ +/* + src/editor - tests for edit_fold_*() functions + + Copyright (C) 2025 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/src/editor" + +#include "tests/mctest.h" + +#include "lib/charsets.h" +#include "src/selcodepage.h" + +#include "src/editor/editwidget.h" +#include "src/editor/edit-impl.h" + +static WGroup owner; +static WEdit *test_edit; + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ + WRect r; + + str_init_strings (NULL); + + mc_global.sysconfig_dir = (char *) TEST_SHARE_DIR; + load_codepages_list (); + + edit_options.filesize_threshold = (char *) "64M"; + + rect_init (&r, 0, 0, 24, 80); + test_edit = edit_init (NULL, &r, NULL); + memset (&owner, 0, sizeof (owner)); + group_add_widget (&owner, WIDGET (test_edit)); + + mc_global.source_codepage = 0; + mc_global.display_codepage = 0; + cp_source = "ASCII"; + cp_display = "ASCII"; + + do_set_codepage (0); + edit_set_codeset (test_edit); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ + edit_fold_flush (test_edit); + edit_clean (test_edit); + group_remove_widget (test_edit); + g_free (test_edit); + + free_codepages_list (); + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_make + edit_fold_find */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_make_and_find) +{ + edit_fold_t *f; + + // given + edit_fold_make (test_edit, 10, 5); + + // when + f = edit_fold_find (test_edit, 10); + + // then + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + ck_assert_int_eq (f->line_count, 5); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_make_zero_count_is_noop) +{ + // given + edit_fold_make (test_edit, 10, 0); + + // then + mctest_assert_null (edit_fold_find (test_edit, 10)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_make_negative_count_is_noop) +{ + // given + edit_fold_make (test_edit, 10, -1); + + // then + mctest_assert_null (edit_fold_find (test_edit, 10)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_make_two_non_overlapping) +{ + edit_fold_t *f1, *f2; + + // given + edit_fold_make (test_edit, 10, 3); + edit_fold_make (test_edit, 20, 4); + + // when + f1 = edit_fold_find (test_edit, 10); + f2 = edit_fold_find (test_edit, 20); + + // then + mctest_assert_not_null (f1); + ck_assert_int_eq (f1->line_start, 10); + ck_assert_int_eq (f1->line_count, 3); + + mctest_assert_not_null (f2); + ck_assert_int_eq (f2->line_start, 20); + ck_assert_int_eq (f2->line_count, 4); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_make_overlapping_replaces) +{ + edit_fold_t *f; + + // given: create a fold at line 10 with 5 lines + edit_fold_make (test_edit, 10, 5); + + // when: create overlapping fold that spans lines 8-18 + edit_fold_make (test_edit, 8, 10); + + // then: old fold removed, new fold present + f = edit_fold_find (test_edit, 8); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 8); + ck_assert_int_eq (f->line_count, 10); + + // original fold at line 10 should now be part of the new fold, not a separate one + f = edit_fold_find (test_edit, 10); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 8); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_find_empty_list) +{ + // then + mctest_assert_null (edit_fold_find (test_edit, 10)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_find_within_range) +{ + edit_fold_t *f; + + // given: fold at line 10 hiding 5 lines (10-15) + edit_fold_make (test_edit, 10, 5); + + // when: search for line inside fold + f = edit_fold_find (test_edit, 13); + + // then + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_find_outside_range) +{ + // given + edit_fold_make (test_edit, 10, 5); + + // then: line before fold + mctest_assert_null (edit_fold_find (test_edit, 9)); + // line after fold + mctest_assert_null (edit_fold_find (test_edit, 16)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_is_hidden */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_is_hidden_before_fold) +{ + // given: fold at line 10 hiding 5 lines + edit_fold_make (test_edit, 10, 5); + + // then + mctest_assert_false (edit_fold_is_hidden (test_edit, 9)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_is_hidden_fold_start) +{ + // given + edit_fold_make (test_edit, 10, 5); + + // then: fold start line is NOT hidden (it's the visible summary line) + mctest_assert_false (edit_fold_is_hidden (test_edit, 10)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_is_hidden_inside_fold) +{ + // given + edit_fold_make (test_edit, 10, 5); + + // then: lines 11-15 are hidden + mctest_assert_true (edit_fold_is_hidden (test_edit, 11)); + mctest_assert_true (edit_fold_is_hidden (test_edit, 13)); + mctest_assert_true (edit_fold_is_hidden (test_edit, 15)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_is_hidden_after_fold) +{ + // given + edit_fold_make (test_edit, 10, 5); + + // then + mctest_assert_false (edit_fold_is_hidden (test_edit, 16)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_remove */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_remove_existing) +{ + // given + edit_fold_make (test_edit, 10, 5); + + // when + gboolean result = edit_fold_remove (test_edit, 10); + + // then + mctest_assert_true (result); + mctest_assert_null (edit_fold_find (test_edit, 10)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_remove_empty_list) +{ + // when + gboolean result = edit_fold_remove (test_edit, 10); + + // then + mctest_assert_false (result); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_remove_then_find_returns_null) +{ + // given + edit_fold_make (test_edit, 10, 5); + edit_fold_make (test_edit, 20, 3); + + // when: remove first fold + edit_fold_remove (test_edit, 10); + + // then: first fold gone, second still present + mctest_assert_null (edit_fold_find (test_edit, 10)); + mctest_assert_not_null (edit_fold_find (test_edit, 20)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_next_visible / edit_fold_prev_visible */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_next_visible_no_folds) +{ + // then + ck_assert_int_eq (edit_fold_next_visible (test_edit, 10), 11); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_next_visible_at_fold_start) +{ + // given: fold at line 10 hiding 5 lines + edit_fold_make (test_edit, 10, 5); + + // then: from fold start, skip to line_start + line_count + 1 + ck_assert_int_eq (edit_fold_next_visible (test_edit, 10), 16); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_next_visible_before_fold) +{ + // given + edit_fold_make (test_edit, 10, 5); + + // then: line before fold, normal increment + ck_assert_int_eq (edit_fold_next_visible (test_edit, 9), 10); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_prev_visible_no_folds) +{ + // then + ck_assert_int_eq (edit_fold_prev_visible (test_edit, 10), 9); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_prev_visible_after_fold) +{ + // given: fold at line 10 hiding 5 lines (lines 11-15 hidden) + edit_fold_make (test_edit, 10, 5); + + // then: prev_visible(16) checks fold_find(15). Line 15 is in fold range [10,15]. + // Since 15 > line_start(10), it jumps back to fold start. + ck_assert_int_eq (edit_fold_prev_visible (test_edit, 16), 10); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_prev_visible_at_fold_start) +{ + // given + edit_fold_make (test_edit, 10, 5); + + // then: prev_visible(10) checks fold_find(9), which returns NULL, so returns 9 + ck_assert_int_eq (edit_fold_prev_visible (test_edit, 10), 9); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_prev_visible_at_zero) +{ + // then: should not go below 0 + ck_assert_int_eq (edit_fold_prev_visible (test_edit, 0), 0); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_flush */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_flush) +{ + // given + edit_fold_make (test_edit, 10, 5); + edit_fold_make (test_edit, 20, 3); + edit_fold_make (test_edit, 30, 7); + + // when + edit_fold_flush (test_edit); + + // then: all folds removed + mctest_assert_null (test_edit->folds); + mctest_assert_null (edit_fold_find (test_edit, 10)); + mctest_assert_null (edit_fold_find (test_edit, 20)); + mctest_assert_null (edit_fold_find (test_edit, 30)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_inc / edit_fold_dec */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_inc_before_fold) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines + edit_fold_make (test_edit, 10, 5); + + // when: insert a line before the fold (at line 5) + edit_fold_inc (test_edit, 5); + + // then: fold shifts down by 1 + f = edit_fold_find (test_edit, 11); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 11); + ck_assert_int_eq (f->line_count, 5); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_inc_inside_fold) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines + edit_fold_make (test_edit, 10, 5); + + // when: insert a line inside the fold (at line 12) + edit_fold_inc (test_edit, 12); + + // then: fold grows by 1 + f = edit_fold_find (test_edit, 10); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + ck_assert_int_eq (f->line_count, 6); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_dec_before_fold) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines + edit_fold_make (test_edit, 10, 5); + + // when: delete a line before the fold (at line 5) + edit_fold_dec (test_edit, 5); + + // then: fold shifts up by 1 + f = edit_fold_find (test_edit, 9); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 9); + ck_assert_int_eq (f->line_count, 5); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_dec_inside_fold) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines + edit_fold_make (test_edit, 10, 5); + + // when: delete a line inside the fold (at line 12) + edit_fold_dec (test_edit, 12); + + // then: fold shrinks by 1 + f = edit_fold_find (test_edit, 10); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + ck_assert_int_eq (f->line_count, 4); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_dec_removes_collapsed_fold) +{ + // given: fold at line 10 with 1 hidden line + edit_fold_make (test_edit, 10, 1); + + // when: delete the only hidden line + edit_fold_dec (test_edit, 11); + + // then: fold is removed entirely + mctest_assert_null (edit_fold_find (test_edit, 10)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* sorted insertion order */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_make_maintains_sorted_order) +{ + edit_fold_t *f; + + // given: create folds out of order + edit_fold_make (test_edit, 30, 2); + edit_fold_make (test_edit, 10, 2); + edit_fold_make (test_edit, 20, 2); + + // then: linked list should be in sorted order + f = test_edit->folds; + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + + f = f->next; + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 20); + + f = f->next; + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 30); + + mctest_assert_null (f->next); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_indicator_width */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_indicator_width_returns_4) +{ + edit_fold_t f; + + // given: any fold + memset (&f, 0, sizeof (f)); + f.line_start = 10; + f.line_count = 5; + + // then: indicator "...}" is always 4 columns + ck_assert_int_eq (edit_fold_indicator_width (&f), 4); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_indicator_width_independent_of_line_count) +{ + edit_fold_t f1, f2; + + // given: folds with different line counts + memset (&f1, 0, sizeof (f1)); + f1.line_start = 0; + f1.line_count = 1; + + memset (&f2, 0, sizeof (f2)); + f2.line_start = 0; + f2.line_count = 99999; + + // then: same width regardless of line count + ck_assert_int_eq (edit_fold_indicator_width (&f1), edit_fold_indicator_width (&f2)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_remove from inside fold */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_remove_from_inside) +{ + // given: fold at line 10 hiding 5 lines + edit_fold_make (test_edit, 10, 5); + + // when: remove by a line inside the fold + gboolean result = edit_fold_remove (test_edit, 13); + + // then: fold is removed + mctest_assert_true (result); + mctest_assert_null (edit_fold_find (test_edit, 10)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ +/* edit_fold_inc / edit_fold_dec edge cases */ +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_inc_at_fold_start) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines + edit_fold_make (test_edit, 10, 5); + + // when: insert at the fold start line itself + edit_fold_inc (test_edit, 10); + + // then: insertion at fold start line — fold is unchanged + // (line == line_start is neither "after" nor "inside") + f = edit_fold_find (test_edit, 10); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + ck_assert_int_eq (f->line_count, 5); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_dec_at_fold_start) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines + edit_fold_make (test_edit, 10, 5); + + // when: delete at the fold start line itself + edit_fold_dec (test_edit, 10); + + // then: fold start stays, no change (deletion is AT start, not inside) + f = edit_fold_find (test_edit, 10); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + ck_assert_int_eq (f->line_count, 5); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_inc_after_fold) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines (range 10-15) + edit_fold_make (test_edit, 10, 5); + + // when: insert after the fold (at line 20) + edit_fold_inc (test_edit, 20); + + // then: fold unchanged + f = edit_fold_find (test_edit, 10); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + ck_assert_int_eq (f->line_count, 5); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_fold_dec_after_fold) +{ + edit_fold_t *f; + + // given: fold at line 10 with 5 hidden lines + edit_fold_make (test_edit, 10, 5); + + // when: delete after the fold (at line 20) + edit_fold_dec (test_edit, 20); + + // then: fold unchanged + f = edit_fold_find (test_edit, 10); + mctest_assert_not_null (f); + ck_assert_int_eq (f->line_start, 10); + ck_assert_int_eq (f->line_count, 5); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + tcase_add_checked_fixture (tc_core, setup, teardown); + + // Add new tests here: *************** + // edit_fold_make + edit_fold_find + tcase_add_test (tc_core, test_fold_make_and_find); + tcase_add_test (tc_core, test_fold_make_zero_count_is_noop); + tcase_add_test (tc_core, test_fold_make_negative_count_is_noop); + tcase_add_test (tc_core, test_fold_make_two_non_overlapping); + tcase_add_test (tc_core, test_fold_make_overlapping_replaces); + tcase_add_test (tc_core, test_fold_find_empty_list); + tcase_add_test (tc_core, test_fold_find_within_range); + tcase_add_test (tc_core, test_fold_find_outside_range); + // edit_fold_is_hidden + tcase_add_test (tc_core, test_fold_is_hidden_before_fold); + tcase_add_test (tc_core, test_fold_is_hidden_fold_start); + tcase_add_test (tc_core, test_fold_is_hidden_inside_fold); + tcase_add_test (tc_core, test_fold_is_hidden_after_fold); + // edit_fold_remove + tcase_add_test (tc_core, test_fold_remove_existing); + tcase_add_test (tc_core, test_fold_remove_empty_list); + tcase_add_test (tc_core, test_fold_remove_then_find_returns_null); + // edit_fold_next_visible / edit_fold_prev_visible + tcase_add_test (tc_core, test_fold_next_visible_no_folds); + tcase_add_test (tc_core, test_fold_next_visible_at_fold_start); + tcase_add_test (tc_core, test_fold_next_visible_before_fold); + tcase_add_test (tc_core, test_fold_prev_visible_no_folds); + tcase_add_test (tc_core, test_fold_prev_visible_after_fold); + tcase_add_test (tc_core, test_fold_prev_visible_at_fold_start); + tcase_add_test (tc_core, test_fold_prev_visible_at_zero); + // edit_fold_flush + tcase_add_test (tc_core, test_fold_flush); + // edit_fold_inc / edit_fold_dec + tcase_add_test (tc_core, test_fold_inc_before_fold); + tcase_add_test (tc_core, test_fold_inc_inside_fold); + tcase_add_test (tc_core, test_fold_dec_before_fold); + tcase_add_test (tc_core, test_fold_dec_inside_fold); + tcase_add_test (tc_core, test_fold_dec_removes_collapsed_fold); + // sorted order + tcase_add_test (tc_core, test_fold_make_maintains_sorted_order); + // edit_fold_indicator_width + tcase_add_test (tc_core, test_fold_indicator_width_returns_4); + tcase_add_test (tc_core, test_fold_indicator_width_independent_of_line_count); + // edit_fold_remove from inside + tcase_add_test (tc_core, test_fold_remove_from_inside); + // edit_fold_inc / edit_fold_dec edge cases + tcase_add_test (tc_core, test_fold_inc_at_fold_start); + tcase_add_test (tc_core, test_fold_dec_at_fold_start); + tcase_add_test (tc_core, test_fold_inc_after_fold); + tcase_add_test (tc_core, test_fold_dec_after_fold); + // *********************************** + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/tests/src/learn_modifiers.c b/tests/src/learn_modifiers.c new file mode 100644 index 0000000000..70f1185437 --- /dev/null +++ b/tests/src/learn_modifiers.c @@ -0,0 +1,77 @@ +/* + src/learn - tests for modifier-aware key names + + Copyright (C) 2026 + Free Software Foundation, Inc. +*/ + +#define TEST_SUITE_NAME "/src/learn" + +#include "tests/mctest.h" + +#include "lib/tty/key.h" + +#include "src/learn.c" + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_learn_build_key_name_without_modifiers) +{ + char *name; + + name = learn_build_key_name ("up", 0); + mctest_assert_str_eq (name, "up"); + g_free (name); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_learn_build_key_name_single_modifiers) +{ + char *name; + + name = learn_build_key_name ("f9", KEY_M_CTRL); + mctest_assert_str_eq (name, "ctrl-f9"); + g_free (name); + + name = learn_build_key_name ("right", KEY_M_ALT); + mctest_assert_str_eq (name, "meta-right"); + g_free (name); + + name = learn_build_key_name ("left", KEY_M_SHIFT); + mctest_assert_str_eq (name, "shift-left"); + g_free (name); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_learn_build_key_name_combined_modifiers) +{ + char *name; + + name = learn_build_key_name ("up", KEY_M_CTRL | KEY_M_SHIFT); + mctest_assert_str_eq (name, "ctrl-shift-up"); + g_free (name); + + name = learn_build_key_name ("pgdn", KEY_M_CTRL | KEY_M_ALT | KEY_M_SHIFT); + mctest_assert_str_eq (name, "ctrl-meta-shift-pgdn"); + g_free (name); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + tcase_add_test (tc_core, test_learn_build_key_name_without_modifiers); + tcase_add_test (tc_core, test_learn_build_key_name_single_modifiers); + tcase_add_test (tc_core, test_learn_build_key_name_combined_modifiers); + + return mctest_run_all (tc_core); +} diff --git a/tests/vfs-plugin-test/test-plugin.c b/tests/vfs-plugin-test/test-plugin.c new file mode 100644 index 0000000000..7d880ad9e9 --- /dev/null +++ b/tests/vfs-plugin-test/test-plugin.c @@ -0,0 +1,26 @@ +/* + * Minimal VFS test plugin. + * + * Registers a dummy "testvfs" class that does nothing useful, + * but proves the dynamic loading mechanism works. + */ + +#include + +#include "lib/global.h" +#include "lib/vfs/vfs.h" + +static struct vfs_class test_vfs_class; + +void +mc_vfs_plugin_init (void) +{ + fprintf (stderr, "TEST VFS PLUGIN: mc_vfs_plugin_init() called\n"); + + vfs_init_class (&test_vfs_class, "testvfs", VFSF_UNKNOWN, "test"); + + if (vfs_register_class (&test_vfs_class)) + fprintf (stderr, "TEST VFS PLUGIN: registered successfully with prefix 'test:'\n"); + else + fprintf (stderr, "TEST VFS PLUGIN: registration FAILED\n"); +}