Manual pages: mcmcdiffmceditmcview

root/src/filemanager/chattr.c

/* [previous][next][first][last][top][bottom][index][help]  */

DEFINITIONS

This source file includes following definitions.
  1. chattr_is_modifiable
  2. chattr_fill_str
  3. fileattrtext_fill
  4. fileattrtext_callback
  5. fileattrtext_new
  6. chattr_draw_select
  7. chattr_toggle_select
  8. chattrboxes_draw_scrollbar
  9. chattrboxes_draw
  10. chattrboxes_rename
  11. checkboxes_save_state
  12. chattrboxes_down
  13. chattrboxes_page_down
  14. chattrboxes_end
  15. chattrboxes_up
  16. chattrboxes_page_up
  17. chattrboxes_home
  18. chattrboxes_execute_cmd
  19. chattrboxes_key
  20. chattrboxes_callback
  21. chattrboxes_handle_mouse_event
  22. chattrboxes_mouse_callback
  23. chattrboxes_new
  24. chattr_init
  25. chattr_dlg_create
  26. chattr_done
  27. try_chattr
  28. do_chattr
  29. chattr_apply_mask
  30. chattr_cmd
  31. chattr_get_as_str

   1 /*
   2    Chattr command -- for the Midnight Commander
   3 
   4    Copyright (C) 2020-2025
   5    Free Software Foundation, Inc.
   6 
   7    Written by:
   8    Andrew Borodin <aborodin@vmail.ru>, 2020-2023
   9 
  10    This file is part of the Midnight Commander.
  11 
  12    The Midnight Commander is free software: you can redistribute it
  13    and/or modify it under the terms of the GNU General Public License as
  14    published by the Free Software Foundation, either version 3 of the License,
  15    or (at your option) any later version.
  16 
  17    The Midnight Commander is distributed in the hope that it will be useful,
  18    but WITHOUT ANY WARRANTY; without even the implied warranty of
  19    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  20    GNU General Public License for more details.
  21 
  22    You should have received a copy of the GNU General Public License
  23    along with this program.  If not, see <https://www.gnu.org/licenses/>.
  24  */
  25 
  26 /** \file chattr.c
  27  *  \brief Source: chattr command
  28  */
  29 
  30 /* TODO: change attributes recursively (ticket #3109) */
  31 
  32 #include <config.h>
  33 
  34 #include <errno.h>
  35 #include <sys/types.h>
  36 #include <sys/stat.h>
  37 
  38 #include <ext2fs/ext2_fs.h>
  39 
  40 #include "lib/global.h"
  41 
  42 #include "lib/tty/tty.h"    // tty_print*()
  43 #include "lib/tty/color.h"  // tty_setcolor()
  44 #include "lib/skin.h"       // COLOR_NORMAL, DISABLED_COLOR
  45 #include "lib/vfs/vfs.h"
  46 #include "lib/widget.h"
  47 #include "lib/util.h"  // x_basename()
  48 
  49 #include "src/keymap.h"  // chattr_map
  50 #include "src/util.h"    // file_error_message()
  51 
  52 #include "cmd.h"  // chattr_cmd(), chattr_get_as_str()
  53 
  54 /*** global variables ****************************************************************************/
  55 
  56 /*** file scope macro definitions ****************************************************************/
  57 
  58 #define B_MARKED       B_USER
  59 #define B_SETALL       (B_USER + 1)
  60 #define B_SETMRK       (B_USER + 2)
  61 #define B_CLRMRK       (B_USER + 3)
  62 
  63 #define BUTTONS        6
  64 
  65 #define CHATTRBOXES(x) ((WChattrBoxes *) (x))
  66 
  67 /*** file scope type declarations ****************************************************************/
  68 
  69 typedef struct WFileAttrText WFileAttrText;
  70 
  71 struct WFileAttrText
  72 {
  73     Widget widget;  // base class
  74 
  75     char *filename;
  76     int filename_width;  // cached width of file name
  77     char attrs[32 + 1];  // 32 bits in attributes (unsigned long)
  78 };
  79 
  80 typedef struct WChattrBoxes WChattrBoxes;
  81 
  82 struct WChattrBoxes
  83 {
  84     WGroup base;  // base class
  85 
  86     int pos;  // The current checkbox selected
  87     int top;  // The first flag displayed
  88 };
  89 
  90 /*** forward declarations (file scope functions) *************************************************/
  91 
  92 /*** file scope variables ************************************************************************/
  93 
  94 /* see /usr/include/ext2fs/ext2_fs.h
  95  *
  96  * EXT2_SECRM_FL            0x00000001 -- Secure deletion
  97  * EXT2_UNRM_FL             0x00000002 -- Undelete
  98  * EXT2_COMPR_FL            0x00000004 -- Compress file
  99  * EXT2_SYNC_FL             0x00000008 -- Synchronous updates
 100  * EXT2_IMMUTABLE_FL        0x00000010 -- Immutable file
 101  * EXT2_APPEND_FL           0x00000020 -- writes to file may only append
 102  * EXT2_NODUMP_FL           0x00000040 -- do not dump file
 103  * EXT2_NOATIME_FL          0x00000080 -- do not update atime
 104  * * Reserved for compression usage...
 105  * EXT2_DIRTY_FL            0x00000100
 106  * EXT2_COMPRBLK_FL         0x00000200 -- One or more compressed clusters
 107  * EXT2_NOCOMPR_FL          0x00000400 -- Access raw compressed data
 108  * * nb: was previously EXT2_ECOMPR_FL
 109  * EXT4_ENCRYPT_FL          0x00000800 -- encrypted inode
 110  * * End compression flags --- maybe not all used
 111  * EXT2_BTREE_FL            0x00001000 -- btree format dir
 112  * EXT2_INDEX_FL            0x00001000 -- hash-indexed directory
 113  * EXT2_IMAGIC_FL           0x00002000
 114  * EXT3_JOURNAL_DATA_FL     0x00004000 -- file data should be journaled
 115  * EXT2_NOTAIL_FL           0x00008000 -- file tail should not be merged
 116  * EXT2_DIRSYNC_FL          0x00010000 -- Synchronous directory modifications
 117  * EXT2_TOPDIR_FL           0x00020000 -- Top of directory hierarchies
 118  * EXT4_HUGE_FILE_FL        0x00040000 -- Set to each huge file
 119  * EXT4_EXTENTS_FL          0x00080000 -- Inode uses extents
 120  * EXT4_VERITY_FL           0x00100000 -- Verity protected inode
 121  * EXT4_EA_INODE_FL         0x00200000 -- Inode used for large EA
 122  * EXT4_EOFBLOCKS_FL        0x00400000 was here, unused
 123  * FS_NOCOW_FL              0x00800000 -- Do not cow file
 124  * EXT4_SNAPFILE_FL         0x01000000 -- Inode is a snapshot
 125  * FS_DAX_FL                0x02000000 -- Inode is DAX
 126  * EXT4_SNAPFILE_DELETED_FL 0x04000000 -- Snapshot is being deleted
 127  * EXT4_SNAPFILE_SHRUNK_FL  0x08000000 -- Snapshot shrink has completed
 128  * EXT4_INLINE_DATA_FL      0x10000000 -- Inode has inline data
 129  * EXT4_PROJINHERIT_FL      0x20000000 -- Create with parents projid
 130  * EXT4_CASEFOLD_FL         0x40000000 -- Casefolded file
 131  *                          0x80000000 -- unused yet
 132  */
 133 
 134 static struct
 135 {
 136     unsigned long flags;
 137     char attr;
 138     const char *text;
 139     gboolean selected;
 140     gboolean state;  // state of checkboxes
 141 } check_attr[] = {
 142     { EXT2_SECRM_FL, 's', N_ ("Secure deletion"), FALSE, FALSE },
 143     { EXT2_UNRM_FL, 'u', N_ ("Undelete"), FALSE, FALSE },
 144     { EXT2_SYNC_FL, 'S', N_ ("Synchronous updates"), FALSE, FALSE },
 145     { EXT2_DIRSYNC_FL, 'D', N_ ("Synchronous directory updates"), FALSE, FALSE },
 146     { EXT2_IMMUTABLE_FL, 'i', N_ ("Immutable"), FALSE, FALSE },
 147     { EXT2_APPEND_FL, 'a', N_ ("Append only"), FALSE, FALSE },
 148     { EXT2_NODUMP_FL, 'd', N_ ("No dump"), FALSE, FALSE },
 149     { EXT2_NOATIME_FL, 'A', N_ ("No update atime"), FALSE, FALSE },
 150     { EXT2_COMPR_FL, 'c', N_ ("Compress"), FALSE, FALSE },
 151 #ifdef EXT2_COMPRBLK_FL
 152     /* removed in v1.43-WIP-2015-05-18
 153        ext2fsprogs 4a05268cf86f7138c78d80a53f7e162f32128a3d 2015-04-12 */
 154     { EXT2_COMPRBLK_FL, 'B', N_ ("Compressed clusters"), FALSE, FALSE },
 155 #endif
 156 #ifdef EXT2_DIRTY_FL
 157     /* removed in v1.43-WIP-2015-05-18
 158        ext2fsprogs 4a05268cf86f7138c78d80a53f7e162f32128a3d 2015-04-12 */
 159     { EXT2_DIRTY_FL, 'Z', N_ ("Compressed dirty file"), FALSE, FALSE },
 160 #endif
 161 #ifdef EXT2_NOCOMPR_FL
 162     /* removed in v1.43-WIP-2015-05-18
 163        ext2fsprogs 4a05268cf86f7138c78d80a53f7e162f32128a3d 2015-04-12 */
 164     { EXT2_NOCOMPR_FL, 'X', N_ ("Compression raw access"), FALSE, FALSE },
 165 #endif
 166 #ifdef EXT4_ENCRYPT_FL
 167     { EXT4_ENCRYPT_FL, 'E', N_ ("Encrypted inode"), FALSE, FALSE },
 168 #endif
 169     { EXT3_JOURNAL_DATA_FL, 'j', N_ ("Journaled data"), FALSE, FALSE },
 170     { EXT2_INDEX_FL, 'I', N_ ("Indexed directory"), FALSE, FALSE },
 171     { EXT2_NOTAIL_FL, 't', N_ ("No tail merging"), FALSE, FALSE },
 172     { EXT2_TOPDIR_FL, 'T', N_ ("Top of directory hierarchies"), FALSE, FALSE },
 173     { EXT4_EXTENTS_FL, 'e', N_ ("Inode uses extents"), FALSE, FALSE },
 174 #ifdef EXT4_HUGE_FILE_FL
 175     /* removed in v1.43.9
 176        ext2fsprogs 4825daeb0228e556444d199274b08c499ac3706c 2018-02-06 */
 177     { EXT4_HUGE_FILE_FL, 'h', N_ ("Huge_file"), FALSE, FALSE },
 178 #endif
 179     { FS_NOCOW_FL, 'C', N_ ("No COW"), FALSE, FALSE },
 180 #ifdef FS_DAX_FL
 181     /* added in v1.45.7
 182        ext2fsprogs 1dd48bc23c3776df76459aff0c7723fff850ea45 2020-07-28 */
 183     { FS_DAX_FL, 'x', N_ ("Direct access for files"), FALSE, FALSE },
 184 #endif
 185 #ifdef EXT4_CASEFOLD_FL
 186     /* added in v1.45.0
 187        ext2fsprogs 1378bb6515e98a27f0f5c220381d49d20544204e 2018-12-01 */
 188     { EXT4_CASEFOLD_FL, 'F', N_ ("Casefolded file"), FALSE, FALSE },
 189 #endif
 190 #ifdef EXT4_INLINE_DATA_FL
 191     { EXT4_INLINE_DATA_FL, 'N', N_ ("Inode has inline data"), FALSE, FALSE },
 192 #endif
 193 #ifdef EXT4_PROJINHERIT_FL
 194     /* added in v1.43-WIP-2016-05-12
 195        ext2fsprogs e1cec4464bdaf93ea609de43c5cdeb6a1f553483 2016-03-07
 196                    97d7e2fdb2ebec70c3124c1a6370d28ec02efad0 2016-05-09 */
 197     { EXT4_PROJINHERIT_FL, 'P', N_ ("Project hierarchy"), FALSE, FALSE },
 198 #endif
 199 #ifdef EXT4_VERITY_FL
 200     /* added in v1.44.4
 201        ext2fsprogs faae7aa00df0abe7c6151fc4947aa6501b981ee1 2018-08-14
 202        v1.44.5
 203        ext2fsprogs 7e5a95e3d59719361661086ec7188ca6e674f139 2018-08-21 */
 204     { EXT4_VERITY_FL, 'V', N_ ("Verity protected inode"), FALSE, FALSE },
 205 #endif
 206 };
 207 
 208 /* number of attributes */
 209 static const size_t check_attr_num = G_N_ELEMENTS (check_attr);
 210 
 211 /* modifiable attribute numbers */
 212 static int check_attr_mod[32];
 213 static int check_attr_mod_num = 0;  // 0..31
 214 
 215 /* maximum width of attribute text */
 216 static int check_attr_width = 0;
 217 
 218 static struct
 219 {
 220     int ret_cmd;
 221     button_flags_t flags;
 222     const char *text;
 223     Widget *button;
 224 } chattr_but[BUTTONS] = {
 225     /* 0 */ { B_SETALL, NORMAL_BUTTON, N_ ("Set &all"), NULL },
 226     /* 1 */ { B_MARKED, NORMAL_BUTTON, N_ ("&Marked all"), NULL },
 227     /* 2 */ { B_SETMRK, NORMAL_BUTTON, N_ ("S&et marked"), NULL },
 228     /* 3 */ { B_CLRMRK, NORMAL_BUTTON, N_ ("C&lear marked"), NULL },
 229     /* 4 */ { B_ENTER, DEFPUSH_BUTTON, N_ ("&Set"), NULL },
 230     /* 5 */ { B_CANCEL, NORMAL_BUTTON, N_ ("&Cancel"), NULL },
 231 };
 232 
 233 static gboolean flags_changed;
 234 static int current_file;
 235 static gboolean ignore_all;
 236 
 237 static unsigned long and_mask, or_mask, flags;
 238 
 239 static WFileAttrText *file_attr;
 240 
 241 /* x-coord of widget in the dialog */
 242 static const int wx = 3;
 243 
 244 /* --------------------------------------------------------------------------------------------- */
 245 /*** file scope functions ************************************************************************/
 246 /* --------------------------------------------------------------------------------------------- */
 247 
 248 static inline gboolean
 249 chattr_is_modifiable (size_t i)
     /* [previous][next][first][last][top][bottom][index][help]  */
 250 {
 251     return ((check_attr[i].flags & EXT2_FL_USER_MODIFIABLE) != 0);
 252 }
 253 
 254 /* --------------------------------------------------------------------------------------------- */
 255 
 256 static void
 257 chattr_fill_str (unsigned long attr, char *str)
     /* [previous][next][first][last][top][bottom][index][help]  */
 258 {
 259     size_t i;
 260 
 261     for (i = 0; i < check_attr_num; i++)
 262         str[i] = (attr & check_attr[i].flags) != 0 ? check_attr[i].attr : '-';
 263 
 264     str[check_attr_num] = '\0';
 265 }
 266 
 267 /* --------------------------------------------------------------------------------------------- */
 268 
 269 static void
 270 fileattrtext_fill (WFileAttrText *fat, unsigned long attr)
     /* [previous][next][first][last][top][bottom][index][help]  */
 271 {
 272     chattr_fill_str (attr, fat->attrs);
 273     widget_draw (WIDGET (fat));
 274 }
 275 
 276 /* --------------------------------------------------------------------------------------------- */
 277 
 278 static cb_ret_t
 279 fileattrtext_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
     /* [previous][next][first][last][top][bottom][index][help]  */
 280 {
 281     WFileAttrText *fat = (WFileAttrText *) w;
 282 
 283     switch (msg)
 284     {
 285     case MSG_DRAW:
 286     {
 287         int color = COLOR_NORMAL;
 288         size_t i;
 289 
 290         tty_setcolor (color);
 291 
 292         if (w->rect.cols > fat->filename_width)
 293         {
 294             widget_gotoyx (w, 0, (w->rect.cols - fat->filename_width) / 2);
 295             tty_print_string (fat->filename);
 296         }
 297         else
 298         {
 299             widget_gotoyx (w, 0, 0);
 300             tty_print_string (str_trunc (fat->filename, w->rect.cols));
 301         }
 302 
 303         // hope that w->cols is greater than check_attr_num
 304         widget_gotoyx (w, 1, (w->rect.cols - check_attr_num) / 2);
 305         for (i = 0; i < check_attr_num; i++)
 306         {
 307             // Do not set new color for each symbol. Try to use previous color.
 308             if (chattr_is_modifiable (i))
 309             {
 310                 if (color == DISABLED_COLOR)
 311                 {
 312                     color = COLOR_NORMAL;
 313                     tty_setcolor (color);
 314                 }
 315             }
 316             else
 317             {
 318                 if (color != DISABLED_COLOR)
 319                 {
 320                     color = DISABLED_COLOR;
 321                     tty_setcolor (color);
 322                 }
 323             }
 324 
 325             tty_print_char (fat->attrs[i]);
 326         }
 327         return MSG_HANDLED;
 328     }
 329 
 330     case MSG_RESIZE:
 331     {
 332         const WRect *wo = &CONST_WIDGET (w->owner)->rect;
 333 
 334         widget_default_callback (w, sender, msg, parm, data);
 335         // initially file name may be wider than screen
 336         if (fat->filename_width > wo->cols - wx * 2)
 337         {
 338             w->rect.x = wo->x + wx;
 339             w->rect.cols = wo->cols - wx * 2;
 340         }
 341         return MSG_HANDLED;
 342     }
 343 
 344     case MSG_DESTROY:
 345         g_free (fat->filename);
 346         return MSG_HANDLED;
 347 
 348     default:
 349         return widget_default_callback (w, sender, msg, parm, data);
 350     }
 351 }
 352 
 353 /* --------------------------------------------------------------------------------------------- */
 354 
 355 static WFileAttrText *
 356 fileattrtext_new (int y, int x, const char *filename, unsigned long attr)
     /* [previous][next][first][last][top][bottom][index][help]  */
 357 {
 358     WRect r = { y, x, 2, 1 };
 359     WFileAttrText *fat;
 360     int width;
 361 
 362     width = str_term_width1 (filename);
 363     r.cols = MAX (width, (int) check_attr_num);
 364 
 365     fat = g_new (WFileAttrText, 1);
 366     widget_init (WIDGET (fat), &r, fileattrtext_callback, NULL);
 367 
 368     fat->filename = g_strdup (filename);
 369     fat->filename_width = width;
 370     fileattrtext_fill (fat, attr);
 371 
 372     return fat;
 373 }
 374 
 375 /* --------------------------------------------------------------------------------------------- */
 376 
 377 static void
 378 chattr_draw_select (const Widget *w, gboolean selected)
     /* [previous][next][first][last][top][bottom][index][help]  */
 379 {
 380     widget_gotoyx (w, 0, -1);
 381     tty_print_char (selected ? '*' : ' ');
 382     widget_gotoyx (w, 0, 1);
 383 }
 384 
 385 /* --------------------------------------------------------------------------------------------- */
 386 
 387 static void
 388 chattr_toggle_select (const WChattrBoxes *cb, int Id)
     /* [previous][next][first][last][top][bottom][index][help]  */
 389 {
 390     Widget *w;
 391 
 392     // find checkbox
 393     w = WIDGET (g_list_nth_data (CONST_GROUP (cb)->widgets, Id - cb->top));
 394 
 395     check_attr[Id].selected = !check_attr[Id].selected;
 396 
 397     tty_setcolor (COLOR_NORMAL);
 398     chattr_draw_select (w, check_attr[Id].selected);
 399 }
 400 
 401 /* --------------------------------------------------------------------------------------------- */
 402 
 403 static inline void
 404 chattrboxes_draw_scrollbar (const WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 405 {
 406     const Widget *w = CONST_WIDGET (cb);
 407     int max_line;
 408     int line;
 409     int i;
 410 
 411     // Are we at the top?
 412     widget_gotoyx (w, 0, w->rect.cols);
 413     if (cb->top == 0)
 414         tty_print_one_vline (TRUE);
 415     else
 416         tty_print_char ('^');
 417 
 418     max_line = w->rect.lines - 1;
 419 
 420     // Are we at the bottom?
 421     widget_gotoyx (w, max_line, w->rect.cols);
 422     if (cb->top + w->rect.lines == check_attr_mod_num || w->rect.lines >= check_attr_mod_num)
 423         tty_print_one_vline (TRUE);
 424     else
 425         tty_print_char ('v');
 426 
 427     // Now draw the nice relative pointer
 428     line = 1 + (cb->pos * (w->rect.lines - 2)) / check_attr_mod_num;
 429 
 430     for (i = 1; i < max_line; i++)
 431     {
 432         widget_gotoyx (w, i, w->rect.cols);
 433         if (i != line)
 434             tty_print_one_vline (TRUE);
 435         else
 436             tty_print_char ('*');
 437     }
 438 }
 439 
 440 /* --------------------------------------------------------------------------------------------- */
 441 
 442 static void
 443 chattrboxes_draw (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 444 {
 445     Widget *w = WIDGET (cb);
 446     int i;
 447     GList *l;
 448     const int *colors;
 449 
 450     colors = widget_get_colors (w);
 451     tty_setcolor (colors[DLG_COLOR_NORMAL]);
 452     tty_fill_region (w->rect.y, w->rect.x - 1, w->rect.lines, w->rect.cols + 1, ' ');
 453 
 454     // redraw checkboxes
 455     group_default_callback (w, NULL, MSG_DRAW, 0, NULL);
 456 
 457     // draw scrollbar
 458     tty_setcolor (colors[DLG_COLOR_NORMAL]);
 459     if (!mc_global.tty.slow_terminal && check_attr_mod_num > w->rect.lines)
 460         chattrboxes_draw_scrollbar (cb);
 461 
 462     // mark selected checkboxes
 463     for (i = cb->top, l = GROUP (cb)->widgets; l != NULL; i++, l = g_list_next (l))
 464         chattr_draw_select (WIDGET (l->data), check_attr[i].selected);
 465 }
 466 
 467 /* --------------------------------------------------------------------------------------------- */
 468 
 469 static void
 470 chattrboxes_rename (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 471 {
 472     Widget *w = WIDGET (cb);
 473     gboolean active;
 474     int i;
 475     GList *l;
 476 
 477     active = widget_get_state (w, WST_ACTIVE);
 478 
 479     // lock the group to avoid redraw of checkboxes individually
 480     if (active)
 481         widget_set_state (w, WST_SUSPENDED, TRUE);
 482 
 483     for (i = cb->top, l = GROUP (cb)->widgets; l != NULL; i++, l = g_list_next (l))
 484     {
 485         WCheck *c = CHECK (l->data);
 486         int m = check_attr_mod[i];
 487         char btext[BUF_SMALL];  // FIXME: are 128 bytes enough?
 488 
 489         g_snprintf (btext, sizeof (btext), "(%c) %s", check_attr[m].attr, check_attr[m].text);
 490         check_set_text (c, btext);
 491         c->state = check_attr[m].state;
 492     }
 493 
 494     // unlock
 495     if (active)
 496         widget_set_state (w, WST_ACTIVE, TRUE);
 497 
 498     widget_draw (w);
 499 }
 500 
 501 /* --------------------------------------------------------------------------------------------- */
 502 
 503 static void
 504 checkboxes_save_state (const WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 505 {
 506     int i;
 507     GList *l;
 508 
 509     for (i = cb->top, l = CONST_GROUP (cb)->widgets; l != NULL; i++, l = g_list_next (l))
 510     {
 511         int m = check_attr_mod[i];
 512 
 513         check_attr[m].state = CHECK (l->data)->state;
 514     }
 515 }
 516 
 517 /* --------------------------------------------------------------------------------------------- */
 518 
 519 static cb_ret_t
 520 chattrboxes_down (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 521 {
 522     if (cb->pos == cb->top + WIDGET (cb)->rect.lines - 1)
 523     {
 524         /* We are on the last checkbox.
 525            Keep this position. */
 526 
 527         if (cb->pos == check_attr_mod_num - 1)
 528             // get out of widget
 529             return MSG_NOT_HANDLED;
 530 
 531         // emulate scroll of checkboxes
 532         checkboxes_save_state (cb);
 533         cb->pos++;
 534         cb->top++;
 535         chattrboxes_rename (cb);
 536     }
 537     else  // cb->pos > cb-top
 538     {
 539         GList *l;
 540 
 541         // select next checkbox
 542         cb->pos++;
 543         l = g_list_next (GROUP (cb)->current);
 544         widget_select (WIDGET (l->data));
 545     }
 546 
 547     return MSG_HANDLED;
 548 }
 549 
 550 /* --------------------------------------------------------------------------------------------- */
 551 
 552 static cb_ret_t
 553 chattrboxes_page_down (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 554 {
 555     WGroup *g = GROUP (cb);
 556     GList *l;
 557 
 558     if (cb->pos == check_attr_mod_num - 1)
 559     {
 560         /* We are on the last checkbox.
 561            Keep this position.
 562            Do nothing. */
 563         l = g_list_last (g->widgets);
 564     }
 565     else
 566     {
 567         int i = WIDGET (cb)->rect.lines;
 568 
 569         checkboxes_save_state (cb);
 570 
 571         if (cb->top > check_attr_mod_num - 2 * i)
 572             i = check_attr_mod_num - i - cb->top;
 573         if (cb->top + i < 0)
 574             i = -cb->top;
 575         if (i == 0)
 576         {
 577             cb->pos = check_attr_mod_num - 1;
 578             cb->top += i;
 579             l = g_list_last (g->widgets);
 580         }
 581         else
 582         {
 583             cb->pos += i;
 584             cb->top += i;
 585             l = g_list_nth (g->widgets, cb->pos - cb->top);
 586         }
 587 
 588         chattrboxes_rename (cb);
 589     }
 590 
 591     widget_select (WIDGET (l->data));
 592 
 593     return MSG_HANDLED;
 594 }
 595 
 596 /* --------------------------------------------------------------------------------------------- */
 597 
 598 static cb_ret_t
 599 chattrboxes_end (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 600 {
 601     GList *l;
 602 
 603     checkboxes_save_state (cb);
 604     cb->pos = check_attr_mod_num - 1;
 605     cb->top = cb->pos - WIDGET (cb)->rect.lines + 1;
 606     l = g_list_last (GROUP (cb)->widgets);
 607     chattrboxes_rename (cb);
 608     widget_select (WIDGET (l->data));
 609 
 610     return MSG_HANDLED;
 611 }
 612 
 613 /* --------------------------------------------------------------------------------------------- */
 614 
 615 static cb_ret_t
 616 chattrboxes_up (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 617 {
 618     if (cb->pos == cb->top)
 619     {
 620         /* We are on the first checkbox.
 621            Keep this position. */
 622 
 623         if (cb->top == 0)
 624             // get out of widget
 625             return MSG_NOT_HANDLED;
 626 
 627         // emulate scroll of checkboxes
 628         checkboxes_save_state (cb);
 629         cb->pos--;
 630         cb->top--;
 631         chattrboxes_rename (cb);
 632     }
 633     else  // cb->pos > cb-top
 634     {
 635         GList *l;
 636 
 637         // select previous checkbox
 638         cb->pos--;
 639         l = g_list_previous (GROUP (cb)->current);
 640         widget_select (WIDGET (l->data));
 641     }
 642 
 643     return MSG_HANDLED;
 644 }
 645 
 646 /* --------------------------------------------------------------------------------------------- */
 647 
 648 static cb_ret_t
 649 chattrboxes_page_up (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 650 {
 651     WGroup *g = GROUP (cb);
 652     GList *l;
 653 
 654     if (cb->pos == 0 && cb->top == 0)
 655     {
 656         /* We are on the first checkbox.
 657            Keep this position.
 658            Do nothing. */
 659         l = g_list_first (g->widgets);
 660     }
 661     else
 662     {
 663         int i = WIDGET (cb)->rect.lines;
 664 
 665         checkboxes_save_state (cb);
 666 
 667         if (cb->top < i)
 668             i = cb->top;
 669         if (i == 0)
 670         {
 671             cb->pos = 0;
 672             cb->top -= i;
 673             l = g_list_first (g->widgets);
 674         }
 675         else
 676         {
 677             cb->pos -= i;
 678             cb->top -= i;
 679             l = g_list_nth (g->widgets, cb->pos - cb->top);
 680         }
 681 
 682         chattrboxes_rename (cb);
 683     }
 684 
 685     widget_select (WIDGET (l->data));
 686 
 687     return MSG_HANDLED;
 688 }
 689 
 690 /* --------------------------------------------------------------------------------------------- */
 691 
 692 static cb_ret_t
 693 chattrboxes_home (WChattrBoxes *cb)
     /* [previous][next][first][last][top][bottom][index][help]  */
 694 {
 695     GList *l;
 696 
 697     checkboxes_save_state (cb);
 698     cb->pos = 0;
 699     cb->top = 0;
 700     l = g_list_first (GROUP (cb)->widgets);
 701     chattrboxes_rename (cb);
 702     widget_select (WIDGET (l->data));
 703 
 704     return MSG_HANDLED;
 705 }
 706 
 707 /* --------------------------------------------------------------------------------------------- */
 708 
 709 static cb_ret_t
 710 chattrboxes_execute_cmd (WChattrBoxes *cb, long command)
     /* [previous][next][first][last][top][bottom][index][help]  */
 711 {
 712     switch (command)
 713     {
 714     case CK_Down:
 715         return chattrboxes_down (cb);
 716 
 717     case CK_PageDown:
 718         return chattrboxes_page_down (cb);
 719 
 720     case CK_Bottom:
 721         return chattrboxes_end (cb);
 722 
 723     case CK_Up:
 724         return chattrboxes_up (cb);
 725 
 726     case CK_PageUp:
 727         return chattrboxes_page_up (cb);
 728 
 729     case CK_Top:
 730         return chattrboxes_home (cb);
 731 
 732     case CK_Mark:
 733     case CK_MarkAndDown:
 734     {
 735         chattr_toggle_select (cb, cb->pos);  // FIXME
 736         if (command == CK_MarkAndDown)
 737             chattrboxes_down (cb);
 738 
 739         return MSG_HANDLED;
 740     }
 741 
 742     default:
 743         return MSG_NOT_HANDLED;
 744     }
 745 }
 746 
 747 /* --------------------------------------------------------------------------------------------- */
 748 
 749 static cb_ret_t
 750 chattrboxes_key (WChattrBoxes *cb, int key)
     /* [previous][next][first][last][top][bottom][index][help]  */
 751 {
 752     long command;
 753 
 754     command = widget_lookup_key (WIDGET (cb), key);
 755     if (command == CK_IgnoreKey)
 756         return MSG_NOT_HANDLED;
 757     return chattrboxes_execute_cmd (cb, command);
 758 }
 759 
 760 /* --------------------------------------------------------------------------------------------- */
 761 
 762 static cb_ret_t
 763 chattrboxes_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
     /* [previous][next][first][last][top][bottom][index][help]  */
 764 {
 765     WChattrBoxes *cb = CHATTRBOXES (w);
 766     WGroup *g = GROUP (w);
 767 
 768     switch (msg)
 769     {
 770     case MSG_DRAW:
 771         chattrboxes_draw (cb);
 772         return MSG_HANDLED;
 773 
 774     case MSG_NOTIFY:
 775     {
 776         // handle checkboxes
 777         int i;
 778 
 779         i = g_list_index (g->widgets, sender);
 780         if (i >= 0)
 781         {
 782             int m;
 783 
 784             i += cb->top;
 785             m = check_attr_mod[i];
 786             flags ^= check_attr[m].flags;
 787             fileattrtext_fill (file_attr, flags);
 788             chattr_toggle_select (cb, i);
 789             flags_changed = TRUE;
 790             return MSG_HANDLED;
 791         }
 792     }
 793         return MSG_NOT_HANDLED;
 794 
 795     case MSG_CHANGED_FOCUS:
 796         // sender is one of chattr checkboxes
 797         if (widget_get_state (sender, WST_FOCUSED))
 798         {
 799             int i;
 800 
 801             i = g_list_index (g->widgets, sender);
 802             cb->pos = cb->top + i;
 803         }
 804         return MSG_HANDLED;
 805 
 806     case MSG_KEY:
 807     {
 808         cb_ret_t ret;
 809 
 810         ret = chattrboxes_key (cb, parm);
 811         if (ret != MSG_HANDLED)
 812             ret = group_default_callback (w, NULL, MSG_KEY, parm, NULL);
 813 
 814         return ret;
 815     }
 816 
 817     case MSG_ACTION:
 818         return chattrboxes_execute_cmd (cb, parm);
 819 
 820     case MSG_DESTROY:
 821         // save all states
 822         checkboxes_save_state (cb);
 823         MC_FALLTHROUGH;
 824 
 825     default:
 826         return group_default_callback (w, sender, msg, parm, data);
 827     }
 828 }
 829 
 830 /* --------------------------------------------------------------------------------------------- */
 831 
 832 static int
 833 chattrboxes_handle_mouse_event (Widget *w, Gpm_Event *event)
     /* [previous][next][first][last][top][bottom][index][help]  */
 834 {
 835     int mou;
 836 
 837     mou = mouse_handle_event (w, event);
 838     if (mou == MOU_UNHANDLED)
 839         mou = group_handle_mouse_event (w, event);
 840 
 841     return mou;
 842 }
 843 
 844 /* --------------------------------------------------------------------------------------------- */
 845 
 846 static void
 847 chattrboxes_mouse_callback (Widget *w, mouse_msg_t msg, mouse_event_t *event)
     /* [previous][next][first][last][top][bottom][index][help]  */
 848 {
 849     WChattrBoxes *cb = CHATTRBOXES (w);
 850 
 851     (void) event;
 852 
 853     switch (msg)
 854     {
 855     case MSG_MOUSE_SCROLL_UP:
 856         chattrboxes_up (cb);
 857         break;
 858 
 859     case MSG_MOUSE_SCROLL_DOWN:
 860         chattrboxes_down (cb);
 861         break;
 862 
 863     default:
 864         // return MOU_UNHANDLED
 865         event->result.abort = TRUE;
 866         break;
 867     }
 868 }
 869 
 870 /* --------------------------------------------------------------------------------------------- */
 871 
 872 static WChattrBoxes *
 873 chattrboxes_new (const WRect *r)
     /* [previous][next][first][last][top][bottom][index][help]  */
 874 {
 875     WChattrBoxes *cb;
 876     Widget *w;
 877     WGroup *cbg;
 878     int i;
 879 
 880     cb = g_new0 (WChattrBoxes, 1);
 881     w = WIDGET (cb);
 882     cbg = GROUP (cb);
 883     group_init (cbg, r, chattrboxes_callback, chattrboxes_mouse_callback);
 884     w->options |= WOP_SELECTABLE | WOP_WANT_CURSOR;
 885     w->mouse_handler = chattrboxes_handle_mouse_event;
 886     w->keymap = chattr_map;
 887 
 888     // create checkboxes
 889     for (i = 0; i < r->lines; i++)
 890     {
 891         int m = check_attr_mod[i];
 892         WCheck *check;
 893 
 894         check = check_new (i, 0, check_attr[m].state, NULL);
 895         group_add_widget (cbg, check);
 896     }
 897 
 898     chattrboxes_rename (cb);
 899 
 900     // select first checkbox
 901     cbg->current = cbg->widgets;
 902 
 903     return cb;
 904 }
 905 
 906 /* --------------------------------------------------------------------------------------------- */
 907 
 908 static void
 909 chattr_init (void)
     /* [previous][next][first][last][top][bottom][index][help]  */
 910 {
 911     static gboolean i18n = FALSE;
 912     size_t i;
 913 
 914     for (i = 0; i < check_attr_num; i++)
 915         check_attr[i].selected = FALSE;
 916 
 917     if (i18n)
 918         return;
 919 
 920     i18n = TRUE;
 921 
 922     for (i = 0; i < check_attr_num; i++)
 923         if (chattr_is_modifiable (i))
 924         {
 925             int width;
 926 
 927 #ifdef ENABLE_NLS
 928             check_attr[i].text = _ (check_attr[i].text);
 929 #endif
 930 
 931             check_attr_mod[check_attr_mod_num++] = i;
 932 
 933             width = 4 + str_term_width1 (check_attr[i].text);  // "(Q) text "
 934             check_attr_width = MAX (check_attr_width, width);
 935         }
 936 
 937     check_attr_width += 1 + 3 + 1;  // mark, [x] and space
 938 
 939 #ifdef ENABLE_NLS
 940     for (i = 0; i < BUTTONS; i++)
 941         chattr_but[i].text = _ (chattr_but[i].text);
 942 #endif
 943 }
 944 
 945 /* --------------------------------------------------------------------------------------------- */
 946 
 947 static WDialog *
 948 chattr_dlg_create (WPanel *panel, const char *fname, unsigned long attr)
     /* [previous][next][first][last][top][bottom][index][help]  */
 949 {
 950     Widget *mw = WIDGET (WIDGET (panel)->owner);
 951     gboolean single_set;
 952     WDialog *ch_dlg;
 953     int lines, cols;
 954     int checkboxes_lines = check_attr_mod_num;
 955     size_t i;
 956     int y;
 957     Widget *dw;
 958     WGroup *dg;
 959     WChattrBoxes *cb;
 960     const int cb_scrollbar_width = 1;
 961     WRect r;
 962 
 963     // prepare to set up checkbox states
 964     for (i = 0; i < check_attr_num; i++)
 965         check_attr[i].state = chattr_is_modifiable (i) && (attr & check_attr[i].flags) != 0;
 966 
 967     cols = check_attr_width + cb_scrollbar_width;
 968 
 969     single_set = (panel->marked < 2);
 970 
 971     lines = 5 + checkboxes_lines + 4;
 972     if (!single_set)
 973         lines += 3;
 974 
 975     if (lines >= mw->rect.lines - 2)
 976     {
 977         int dl;
 978 
 979         dl = lines - (mw->rect.lines - 2);
 980         lines -= dl;
 981         checkboxes_lines -= dl;
 982     }
 983 
 984     ch_dlg = dlg_create (TRUE, 0, 0, lines, cols + wx * 2, WPOS_CENTER, FALSE, dialog_colors,
 985                          dlg_default_callback, NULL, "[Chattr]", _ ("Chattr command"));
 986     dg = GROUP (ch_dlg);
 987     dw = WIDGET (ch_dlg);
 988 
 989     y = 2;
 990     file_attr = fileattrtext_new (y, wx, fname, attr);
 991     group_add_widget_autopos (dg, file_attr, WPOS_KEEP_TOP | WPOS_CENTER_HORZ, NULL);
 992     y += WIDGET (file_attr)->rect.lines;
 993     group_add_widget (dg, hline_new (y++, -1, -1));
 994 
 995     if (cols < WIDGET (file_attr)->rect.cols)
 996     {
 997         r = dw->rect;
 998         cols = WIDGET (file_attr)->rect.cols;
 999         cols = MIN (cols, mw->rect.cols - wx * 2);
1000         r.cols = cols + wx * 2;
1001         r.lines = lines;
1002         widget_set_size_rect (dw, &r);
1003     }
1004 
1005     checkboxes_lines = MIN (check_attr_mod_num, checkboxes_lines);
1006     rect_init (&r, y++, wx, checkboxes_lines > 0 ? checkboxes_lines : 1, cols);
1007     cb = chattrboxes_new (&r);
1008     group_add_widget_autopos (dg, cb, WPOS_KEEP_TOP | WPOS_KEEP_HORZ, NULL);
1009 
1010     y += checkboxes_lines - 1;
1011     cols = 0;
1012 
1013     for (i = single_set ? (BUTTONS - 2) : 0; i < BUTTONS; i++)
1014     {
1015         if (i == 0 || i == BUTTONS - 2)
1016             group_add_widget (dg, hline_new (y++, -1, -1));
1017 
1018         chattr_but[i].button = WIDGET (button_new (y, 1, chattr_but[i].ret_cmd, chattr_but[i].flags,
1019                                                    chattr_but[i].text, NULL));
1020         group_add_widget (dg, chattr_but[i].button);
1021 
1022         const int bw0 = button_get_width (BUTTON (chattr_but[i].button));
1023 
1024         i++;
1025         chattr_but[i].button = WIDGET (button_new (y++, 1, chattr_but[i].ret_cmd,
1026                                                    chattr_but[i].flags, chattr_but[i].text, NULL));
1027         group_add_widget (dg, chattr_but[i].button);
1028 
1029         const int bw1 = button_get_width (BUTTON (chattr_but[i].button));
1030 
1031         // two buttons in a row
1032         cols = MAX (cols, bw0 + 1 + bw1);
1033     }
1034 
1035     // adjust dialog size
1036     cols += 6;
1037     if (cols > dw->rect.cols)
1038     {
1039         r = dw->rect;
1040         r.lines = lines;
1041         r.cols = cols;
1042         widget_set_size_rect (dw, &r);
1043     }
1044 
1045     // dialog center
1046     const int mid = dw->rect.x + dw->rect.cols / 2 + 1;
1047 
1048     // adjust button positions
1049     for (i = single_set ? (BUTTONS - 2) : 0; i < BUTTONS; i++)
1050     {
1051         Widget *b;
1052 
1053         b = chattr_but[i].button;
1054         r = b->rect;
1055         r.x = mid - r.cols;
1056         widget_set_size_rect (b, &r);
1057         i++;
1058         b = chattr_but[i].button;
1059         r = b->rect;
1060         r.x = mid + 1;
1061         widget_set_size_rect (b, &r);
1062     }
1063 
1064     widget_select (WIDGET (cb));
1065 
1066     return ch_dlg;
1067 }
1068 
1069 /* --------------------------------------------------------------------------------------------- */
1070 
1071 static void
1072 chattr_done (gboolean need_update)
     /* [previous][next][first][last][top][bottom][index][help]  */
1073 {
1074     if (need_update)
1075         update_panels (UP_OPTIMIZE, UP_KEEPSEL);
1076     repaint_screen ();
1077 }
1078 
1079 /* --------------------------------------------------------------------------------------------- */
1080 
1081 static gboolean
1082 try_chattr (const vfs_path_t *p, unsigned long m)
     /* [previous][next][first][last][top][bottom][index][help]  */
1083 {
1084     const char *fname = NULL;
1085 
1086     while (mc_fsetflags (p, m) == -1 && !ignore_all)
1087     {
1088         int my_errno = errno;
1089 
1090         if (fname == NULL)
1091             fname = x_basename (vfs_path_as_str (p));
1092 
1093         errno = my_errno;  // restore errno for file_error(
1094 
1095         switch (file_error (NULL, TRUE, _ ("Cannot chattr\n%sn%s"), fname))
1096         {
1097         case FILE_IGNORE:
1098             // try next file
1099             return TRUE;
1100 
1101         case FILE_IGNORE_ALL:
1102             ignore_all = TRUE;
1103             // try next file
1104             return TRUE;
1105 
1106         case FILE_RETRY:
1107             // retry this file
1108             break;
1109 
1110         case FILE_ABORT:
1111         default:
1112             // stop remain files processing
1113             return FALSE;
1114         }
1115     }
1116 
1117     return TRUE;
1118 }
1119 
1120 /* --------------------------------------------------------------------------------------------- */
1121 
1122 static gboolean
1123 do_chattr (WPanel *panel, const vfs_path_t *p, unsigned long m)
     /* [previous][next][first][last][top][bottom][index][help]  */
1124 {
1125     gboolean ret;
1126 
1127     m &= and_mask;
1128     m |= or_mask;
1129 
1130     ret = try_chattr (p, m);
1131 
1132     do_file_mark (panel, current_file, 0);
1133 
1134     return ret;
1135 }
1136 
1137 /* --------------------------------------------------------------------------------------------- */
1138 
1139 static void
1140 chattr_apply_mask (WPanel *panel, vfs_path_t *vpath, unsigned long m)
     /* [previous][next][first][last][top][bottom][index][help]  */
1141 {
1142     gboolean ok;
1143 
1144     if (!do_chattr (panel, vpath, m))
1145         return;
1146 
1147     do
1148     {
1149         const GString *fname;
1150 
1151         fname = panel_find_marked_file (panel, &current_file);
1152         vpath = vfs_path_from_str (fname->str);
1153         ok = (mc_fgetflags (vpath, &m) == 0);
1154 
1155         if (!ok)
1156         {
1157             // if current file was deleted outside mc -- try next file
1158             // decrease panel->marked
1159             do_file_mark (panel, current_file, 0);
1160 
1161             // try next file
1162             ok = TRUE;
1163         }
1164         else
1165         {
1166             flags = m;
1167             ok = do_chattr (panel, vpath, m);
1168             vfs_path_free (vpath, TRUE);
1169         }
1170     }
1171     while (ok && panel->marked != 0);
1172 }
1173 
1174 /* --------------------------------------------------------------------------------------------- */
1175 /*** public functions ****************************************************************************/
1176 /* --------------------------------------------------------------------------------------------- */
1177 
1178 void
1179 chattr_cmd (WPanel *panel)
     /* [previous][next][first][last][top][bottom][index][help]  */
1180 {
1181     gboolean need_update = FALSE;
1182     gboolean end_chattr = FALSE;
1183 
1184     chattr_init ();
1185 
1186     current_file = 0;
1187     ignore_all = FALSE;
1188 
1189     do
1190     {  // do while any files remaining
1191         vfs_path_t *vpath;
1192         WDialog *ch_dlg;
1193         const GString *fname;
1194         size_t i;
1195         int result;
1196 
1197         do_refresh ();
1198 
1199         need_update = FALSE;
1200         end_chattr = FALSE;
1201 
1202         fname = panel_get_marked_file (panel, &current_file);
1203         if (fname == NULL)
1204             break;
1205 
1206         vpath = vfs_path_from_str (fname->str);
1207 
1208         if (mc_fgetflags (vpath, &flags) != 0)
1209         {
1210             file_error_message (_ ("Cannot get ext2 attributes of\n%s"), fname->str);
1211             vfs_path_free (vpath, TRUE);
1212             break;
1213         }
1214 
1215         flags_changed = FALSE;
1216 
1217         ch_dlg = chattr_dlg_create (panel, fname->str, flags);
1218         result = dlg_run (ch_dlg);
1219         widget_destroy (WIDGET (ch_dlg));
1220 
1221         switch (result)
1222         {
1223         case B_CANCEL:
1224             end_chattr = TRUE;
1225             break;
1226 
1227         case B_ENTER:
1228             if (flags_changed)
1229             {
1230                 if (panel->marked <= 1)
1231                 {
1232                     // single or last file
1233                     if (mc_fsetflags (vpath, flags) == -1 && !ignore_all)
1234                         file_error_message (_ ("Cannot chattr\n%s"), fname->str);
1235                     end_chattr = TRUE;
1236                 }
1237                 else if (!try_chattr (vpath, flags))
1238                 {
1239                     // stop multiple files processing
1240                     result = B_CANCEL;
1241                     end_chattr = TRUE;
1242                 }
1243             }
1244 
1245             need_update = TRUE;
1246             break;
1247 
1248         case B_SETALL:
1249         case B_MARKED:
1250             or_mask = 0;
1251             and_mask = ~0;
1252 
1253             for (i = 0; i < check_attr_num; i++)
1254                 if (chattr_is_modifiable (i) && (check_attr[i].selected || result == B_SETALL))
1255                 {
1256                     if (check_attr[i].state)
1257                         or_mask |= check_attr[i].flags;
1258                     else
1259                         and_mask &= ~check_attr[i].flags;
1260                 }
1261 
1262             chattr_apply_mask (panel, vpath, flags);
1263             need_update = TRUE;
1264             end_chattr = TRUE;
1265             break;
1266 
1267         case B_SETMRK:
1268             or_mask = 0;
1269             and_mask = ~0;
1270 
1271             for (i = 0; i < check_attr_num; i++)
1272                 if (chattr_is_modifiable (i) && check_attr[i].selected)
1273                     or_mask |= check_attr[i].flags;
1274 
1275             chattr_apply_mask (panel, vpath, flags);
1276             need_update = TRUE;
1277             end_chattr = TRUE;
1278             break;
1279 
1280         case B_CLRMRK:
1281             or_mask = 0;
1282             and_mask = ~0;
1283 
1284             for (i = 0; i < check_attr_num; i++)
1285                 if (chattr_is_modifiable (i) && check_attr[i].selected)
1286                     and_mask &= ~check_attr[i].flags;
1287 
1288             chattr_apply_mask (panel, vpath, flags);
1289             need_update = TRUE;
1290             end_chattr = TRUE;
1291             break;
1292 
1293         default:
1294             break;
1295         }
1296 
1297         if (panel->marked != 0 && result != B_CANCEL)
1298         {
1299             do_file_mark (panel, current_file, 0);
1300             need_update = TRUE;
1301         }
1302 
1303         vfs_path_free (vpath, TRUE);
1304     }
1305     while (panel->marked != 0 && !end_chattr);
1306 
1307     chattr_done (need_update);
1308 }
1309 
1310 /* --------------------------------------------------------------------------------------------- */
1311 
1312 const char *
1313 chattr_get_as_str (unsigned long attr)
     /* [previous][next][first][last][top][bottom][index][help]  */
1314 {
1315     static char str[32 + 1];  // 32 bits in attributes (unsigned long)
1316 
1317     chattr_fill_str (attr, str);
1318 
1319     return str;
1320 }
1321 
1322 /* --------------------------------------------------------------------------------------------- */

/* [previous][next][first][last][top][bottom][index][help]  */