yast/yast-yast2

View on GitHub
library/desktop/src/clients/menu.rb

Summary

Maintainability
D
2 days
Test Coverage
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File:  clients/menu.ycp
# Module:  yast2
# Summary:  NCurses Control Center
# Authors:  Michal Svec <msvec@suse.cz>
#
# $Id$
#
# Provides a list of available yast2 modules. This module is inteded for use
# with ncurses, for X the yast2 control center should be used.

require "shellwords"

module Yast
  class MenuClient < Client
    def main
      Yast.import "UI"

      textdomain "base"

      Yast.import "Desktop"
      Yast.import "Directory"
      Yast.import "FileUtils"
      Yast.import "Label"
      Yast.import "Popup"

      @Groups = {}
      @Modules = {}
      @root = false

      @restart_file = Ops.add(Directory.vardir, "/restart_menu")
      # file existing if yast2-online-update wants to be restarted
      @restart_you = Ops.add(Directory.vardir, "/selected_patches.ycp")

      DisplaySplash()

      @Values = [
        "GenericName",
        # not required: "Comment",
        "X-SuSE-YaST-Argument",
        "X-SuSE-YaST-Call",
        "X-SuSE-YaST-Group",
        "X-SuSE-YaST-SortKey",
        "X-SuSE-YaST-RootOnly",
        "X-SuSE-YaST-WSL",
        "Hidden"
      ]

      # always load the desktop files from the system root
      if WFM.scr_chrooted?
        # revert back the chrooting
        handle = Yast::WFM.SCROpen("chroot=/:scr", false)
        Yast::WFM.SCRSetDefault(handle)
      end

      Desktop.Read(@Values)
      @Groups = deep_copy(Desktop.Groups)
      @Modules = deep_copy(Desktop.Modules)
      Builtins.y2debug("Groups=%1", @Groups)
      Builtins.y2debug("Modules=%1", @Modules)

      @non_root_modules = []

      # create the list of modules available to non-root users
      Builtins.foreach(
        Convert.convert(@Modules, from: "map", to: "map <string, map>")
      ) do |name, params|
        @non_root_modules = Builtins.add(@non_root_modules, name) if Ops.get_string(params, "X-SuSE-YaST-RootOnly", "false") != "true"
      end
      Builtins.y2debug("non-root modules: %1", @non_root_modules)

      SCR.Execute(path(".target.remove"), @restart_file) if FileUtils.Exists(@restart_file)

      UI.CloseDialog

      OpenMenu()

      @GroupList = Desktop.GroupList

      # precache groups (#38363)
      @groups = Builtins.maplist(@GroupList) { |gr| Ops.get_string(gr, [0, 0]) }
      Builtins.y2debug("groups=%1", @groups)

      @modules = Builtins.listmap(@groups) do |gr|
        all_modules = Desktop.ModuleList(gr)
        # filter out root-only stuff if the user is not root (#246015)
        if !@root
          all_modules = Builtins.filter(all_modules) do |t|
            Builtins.contains(@non_root_modules, Ops.get_string(t, [0, 0], ""))
          end
        end
        { gr => all_modules }
      end
      Builtins.y2debug("modules=%1", @modules)

      @first = Ops.get(@groups, 0)
      Builtins.y2debug("first=%1", @first)

      # do not show groups containing no modules to the user (#309452)
      @GroupList = Builtins.filter(@GroupList) do |t|
        group = Ops.get_string(t, [0, 0], "")
        Ops.get(@modules, group) != []
      end

      # GroupList = [`item (`id ("Software"), "Software"), ...]
      UI.ReplaceWidget(
        Id(:groups_rep),
        SelectionBox(
          Id(:groups),
          Opt(:notify, :immediate, :keyEvents),
          "",
          @GroupList
        )
      )
      Builtins.y2debug("GroupList=%1", @GroupList)

      ReplaceModuleList(@first)
      UI.SetFocus(Id(:groups))

      loop do
        @event = UI.WaitForEvent
        @eventid = Ops.get(@event, "ID")
        # y2debug too constly: y2debug("event=%1", event);

        if Ops.is_symbol?(@eventid)
          if @eventid == :groups &&
              Ops.get_string(@event, "EventReason", "") == "SelectionChanged"
            @id = Convert.to_string(UI.QueryWidget(Id(:groups), :CurrentItem))
            # ReplaceModuleList(id);
            UI.ReplaceWidget(
              Id(:progs_rep),
              SelectionBox(
                Id(:progs),
                Opt(:notify, :keyEvents),
                "",
                Ops.get(@modules, @id, [])
              )
            )
            next
          elsif (@eventid == :progs || @eventid == :run) &&
              Ops.get_string(@event, "EventReason", "") == "Activated"
            @program = Convert.to_string(
              UI.QueryWidget(Id(:progs), :CurrentItem)
            )
            break if Launch(@program)
          elsif @eventid == :groups &&
              Ops.get_string(@event, "EventReason", "") == "Activated"
            UI.SetFocus(Id(:progs))
          elsif @eventid == :help
            ShowNcursesHelp()
          elsif @eventid == :quit || @eventid == :cancel
            break
          else
            Builtins.y2warning("Event or widget ID not handled: %1", @event)
          end
        elsif Ops.is_string?(@eventid)
          if Ops.get_symbol(@event, "FocusWidgetID", :none) == :groups &&
              @eventid == "CursorRight"
            UI.SetFocus(Id(:progs))
          elsif Ops.get_symbol(@event, "FocusWidgetID", :none) == :progs &&
              @eventid == "CursorLeft"
            UI.SetFocus(Id(:groups))
          else
            Builtins.y2warning("Event or widget ID not handled: %1", @event)
          end
        else
          Builtins.y2warning("Event or widget ID not handled: %1", @event)
        end
      end

      UI.CloseDialog

      # EOF

      nil
    end

    def DisplaySplash
      UI.OpenDialog(
        Opt(:defaultsize),
        VBox(
          VStretch(),
          # Message shown while loading modules information
          Label(_("Loading modules, please wait ...")),
          VStretch()
        )
      )

      nil
    end

    def OpenMenu
      # check if user is root (#246015)
      output = Convert.to_map(
        SCR.Execute(path(".target.bash_output"), "/usr/bin/id --user")
      )
      @root = Ops.get_string(output, "stdout", "") == "0\n"

      UI.OpenDialog(
        Opt(:defaultsize),
        VBox(
          HBox(
            HSpacing(1),
            Frame(
              "",
              VBox(
                VSpacing(0.4),
                HBox(
                  HSpacing(2),
                  # Heading for NCurses Control Center
                  HCenter(Heading(_("YaST Control Center"))),
                  HSpacing(2)
                ),
                VSpacing(0.4)
              )
            ),
            HSpacing(1.5)
          ),
          VSpacing(1.0),
          HBox(
            HSpacing(1),
            HWeight(
              30,
              ReplacePoint(
                Id(:groups_rep),
                SelectionBox(
                  Id(:groups),
                  Opt(:notify, :immediate, :keyEvents),
                  "",
                  []
                )
              )
            ),
            HSpacing(1),
            HWeight(
              70,
              ReplacePoint(
                Id(:progs_rep),
                SelectionBox(Id(:progs), Opt(:notify, :keyEvents), "", [])
              )
            ),
            HSpacing(1)
          ),
          VSpacing(1.0),
          HBox(
            HSpacing(1),
            PushButton(Id(:help), Opt(:key_F1), Label.HelpButton),
            HStretch(),
            PushButton(Id(:run), Opt(:defaultButton), _("Run")),
            PushButton(Id(:quit), Opt(:key_F9, :cancelButton), Label.QuitButton),
            HSpacing(1)
          ),
          VSpacing(1)
        )
      )

      # show popup when running as non-root
      if !@root
        Popup.Notify(
          _(
            "YaST2 Control Center is not running as root.\nYou can only see modules that do not require root privileges."
          )
        )
      end

      nil
    end

    # @return [Boolean] true if control center is to be resatrted
    def Launch(modul)
      function = Ops.get_string(@Modules, [modul, "X-SuSE-YaST-Call"], "")
      argument = Ops.get_string(@Modules, [modul, "X-SuSE-YaST-Argument"], "")
      Builtins.y2debug("Calling: %1 (%2)", function, argument)

      display_info = UI.GetDisplayInfo
      textmode = Ops.get_boolean(display_info, "TextMode", false)

      if function != ""
        cmd = ""
        ret = nil

        # prevent shell injection when passing argument. But on other hand do
        # not pass empty argument which makes module think it is CLI (bsc#1121425)
        argument = argument.empty? ? "" : argument.shellescape

        # Use UI::RunInTerminal in text-mode only (#237332)
        if textmode
          cmd = Builtins.sformat("/sbin/yast %1 %2 >&2", function.shellescape, argument)
          ret = UI.RunInTerminal(cmd)
        else
          cmd = Builtins.sformat("/sbin/yast2 %1 %2 >&2", function.shellescape, argument)
          ret = SCR.Execute(path(".target.bash"), cmd)
        end
        Builtins.y2milestone("Got %1 from %2", ret, cmd)

        if function == "online_update" && ret != :cancel && ret != :abort &&
            FileUtils.Exists(@restart_you)
          Builtins.y2milestone("yast needs to be restarted - exiting...")
          SCR.Execute(
            path(".target.bash"),
            Builtins.sformat("/usr/bin/touch %1", @restart_file.shellescape)
          )
          return true
        end
      end
      false
    end

    def ShowNcursesHelp
      # NCurses (textmode) Control Center headline
      headline = _("Controlling YaST ncurses with the Keyboard")

      # NCurses Control Center help 1/
      help = _(
        "<p>1) <i>General</i><br>\n" \
        "Navigate through the dialog elements with [TAB] to go to\n" \
        "the next element and [SHIFT] (or [ALT]) + [TAB] to move backwards.\n" \
        "Select or activate elements with [SPACE] or [ENTER].\n" \
        "Some elements use arrow keys (e.g., to scroll in lists).</p>"
      ) +
        # NCurses Control Center help 2/10
        _(
          "<p>Tree navigation is also done by arrow keys. To open or close a " \
          "branch use [SPACE]. For modules showing a tree (might look like a list) " \
          "of configuration items on the left side use [ENTER] to get corresponding " \
          "dialog on the right.</p>"
        ) +
        # NCurses Control Center help 3/10
        _(
          "<p>Buttons are equipped with shortcut keys (the highlighted\nletter). Use [ALT] and the letter to activate the button.</p>"
        ) +
        # NCurses Control Center help 4/10
        _(
          "<p>Press [ESC] to close selection pop-ups (e.g., from\nmenu buttons) without choosing anything.</p>\n"
        ) +
        # NCurses Control Center help 5/10
        _(
          "<p>2) <i>Substitution of Keystrokes</i><br>\n" \
          "<p>Because the environment can affect the use of the keyboard,\n" \
          "there is more than one way to navigate the dialog pages.\n" \
          "If [TAB] and [SHIFT] (or [ALT]) + [TAB] do not work,\n" \
          "move focus forward with [CTRL] + [F] and backward with [CTRL] + [B].</p>"
        ) +
        # NCurses Control Center help 6/10
        _(
          "<p>If [ALT] + [letter] does not work,\n" \
          "try [ESC] + [letter]. Example: [ESC] + [H] for [ALT] + [H].\n" \
          "[ESC] + [TAB] is also a substitute for [ALT] + [TAB].</p>"
        ) +
        # NCurses Control Center help 7/10
        _(
          "<p>3) <i>Function Keys</i><br>\n" \
          "F keys provide a quick access to main functions. " \
          "The function key bindings for the current dialog are " \
          "shown in the bottom line.</p>"
        ) +
        # NCurses Control Center help 8/10
        _("<p>The F keys are usually connected to a certain action:</p>") +
        # NCurses Control Center help 9/10
        _(
          "F1  = Help<br>\n" \
          "F2  = Info or Description<br>\n" \
          "F3  = Add<br>\n" \
          "F4  = Edit or Configure<br>\n" \
          "F5  = Delete<br>\n" \
          "F6  = Test<br>\n" \
          "F7  = Expert or Advanced<br>\n" \
          "F8  = Back<br>\n" \
          "F9  = Abort or Cancel<br>\n" \
          "F10 = OK, Next, Finish, or Accept<br>"
        ) +
        # NCurses Control Center help 10/10
        _("<p>In some environments, all or some\nF keys are not available.</p>")

      Popup.LongText(headline, RichText(help), 60, 20)

      nil
    end

    def ReplaceModuleList(group)
      # y2debug too costly: y2debug("group=%1", group);
      UI.ReplaceWidget(
        Id(:progs_rep),
        SelectionBox(
          Id(:progs),
          Opt(:notify, :keyEvents),
          "",
          Ops.get(@modules, group, [])
        )
      )

      nil
    end
  end
end

Yast::MenuClient.new.main