% Copyright (C) 2018-2019 Olivier Boudeville % % This file is part of the Ceylan-Myriad library. % % This library is free software: you can redistribute it and/or modify % it under the terms of the GNU Lesser General Public License or % the GNU General Public License, as they are published by the Free Software % Foundation, either version 3 of these Licenses, or (at your option) % any later version. % You can also redistribute it and/or modify it under the terms of the % Mozilla Public License, version 1.1 or later. % % This library 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 and the GNU General Public License % for more details. % % You should have received a copy of the GNU Lesser General Public % License, of the GNU General Public License and of the Mozilla Public License % along with this library. % If not, see and % . % % Author: Olivier Boudeville [olivier (dot) boudeville (at) esperide (dot) com] % Creation date: Wednesday, May 2, 2018 % This is the second most basic, terminal-based textual interface, with colors, % dialog boxes, etc., based on the 'dialog' or 'whiptail' tools. % % See: % - term_ui_test.erl for the corresponding test % - text_ui.erl for a more basic text interface % - gui.erl for a graphical counterpart % % See also: trace_utils.erl for another kind of output and test-dialog.sh for an % autonomous, standalone test. % -module(term_ui). % Implementation notes: % % In this very specific case, we use the process dictionary to avoid having to % keep around a UI-state variable in all calls. % % We generally do not provide here counterpart functions dealing with an % explicit state instead (too long, for too little interest, and too many arity % clashes). % Note that using this service from within an escript seems to raise issues: % {display_error_reported,255,[]} is returned whenever trying to display a modal % window. % Dialog-specific section. % % These dialogs will take advantage of the current locale (ex: 'OK' vs % 'Accepter'). % % See also: test-dialog.sh for a live demo. % List of known types of dialogs: % % (see also: https://invisible-island.net/dialog/dialog-figures.html) % 'msgbox' dialog: a modal window to be dismissed by hitting Enter % % - ex: LANG= dialog --title "Hello" --msgbox 'Hello world!' 6 20 % (validated after Enter is pressed) % 'yesno' (yes/no) dialog: a modal window offering two possibilities % % - ex: LANG= dialog --title "Message" --yesno "Are you having\n fun?" 6 25 % (exit status: 0 if Yes, 1 otherwise (No or interrupted)) % 'infobox' dialog: a window displayed once % % - ex: LANG= dialog --infobox "Please wait" 10 30 ; sleep 4 % (disappears after sleep) % 'pause' dialog: pauses for a number of seconds % % - ex: LANG= dialog --pause "Pausing" 10 30 4 % (disappears once pause is over) % 'inputbox' dialog: a request for the user to type an echoed string % % - ex: LANG= dialog --inputbox "Enter your name:" 8 40 % (input written to standard error, possibly redirected) % 'passwordbox' dialog: a request for the user to type a non-echoed string % % - ex: LANG= dialog --passwordbox "Enter some password:" 8 40 % (no input echoed, but written to standard error, possibly redirected) % 'textbox' dialog: displays the content of a file % % - ex: LANG= dialog --textbox /etc/profile 22 70 % 'menu' dialog: allows to select one option among a set (quite similar to % radiolist) % % - ex: LANG= dialog --menu "Choose one:" 10 30 3 1 red 2 green 3 blue % (choice index written to standard error, possibly redirected) % 'radiolist' dialog: allows to select one option among a set (quite similar to % menu) % % - ex: LANG= dialog --radiolist "Select CPU type:" 10 40 4 1 386SX off 2 386DX % on 3 486SX off 4 486DX off 2>${result_file} % (choice index written to standard error, possibly redirected) % 'treeview' dialog: allows to select a tree element % % - ex: LANG= dialog --treeview "Select tree element:" 10 40 5 1 a on 1 2 b on 2 % 3 c off 1 4 d on 3 % (element written to standard error, possibly redirected) % 'checklist' dialog: allows to select non-exclusive options, thanks to a set of % radio buttons with defaults % % - ex: LANG= dialog --checklist "Choose toppings:" 10 40 3 1 Cheese on 2 % "Tomato Sauce" on 3 Anchovies off % (choice indexes written to standard error, possibly redirected) % 'calendar' dialog: allows to select a date % % - ex: LANG= dialog --calendar "Select a date:" 10 40 17 5 1977 % (date written to standard error, possibly redirected) % 'timebox' dialog: allows to select a time % % - ex: LANG= dialog --timebox "Select a time:" 10 40 % (time written to standard error, possibly redirected) % 'fselect' dialog: allows to select a file % % - ex: LANG= dialog --fselect / 10 40 % (file path written to standard error, possibly redirected) % 'dselect' dialog: allows to select a directory % % - ex: LANG= dialog --dselect / 10 40 % (directory path written to standard error, possibly redirected) % 'gauge' dialog: display percentage values % % - ex: LANG= dialog --gauge "My gauge:" 10 20 12 % 'tailbox' dialog: % 'tailboxbg' dialog: % 'progressbox' dialog: % 'buildlist' dialog: % 'editbox' dialog: % 'form' dialog: % 'inputmenu' dialog: % 'mixedform' dialog: % 'mixedgauge' dialog: % 'passwordform' dialog: % 'prgbox' dialog: % 'programbox' dialog: % 'rangebox' dialog: % Also useful: % % - 'dialog --print-maxsize' (ex: MaxSize: 35, 123) % - 'dialog --clear' % Basic UI operations. -export([ is_available/0, start/0, start/1, set/1, set/2, unset/1, display/1, display/2, display_numbered_list/2, display_error/1, display_error/2, display_error_numbered_list/2, add_separation/0, ask_yes_no/2, ask_yes_no/3, choose_designated_item/1, choose_designated_item/2, choose_designated_item/3, choose_numbered_item/1, choose_numbered_item/2, choose_numbered_item/3, choose_numbered_item_with_default/2, choose_numbered_item_with_default/3, choose_numbered_item_with_default/4, set_setting/2, set_setting/3, set_settings/1, set_settings/2, unset_setting/1, unset_setting/2, get_setting/1, trace/1, trace/2, clear/0, clear/1, stop/0, stop/1, to_string/0, to_string/1 ]). % The default place where file-based communications are to occur: % (finally not needed) %-define( default_state_path, "/tmp/.myriad-term_ui.state" ). -type dialog_tool() :: 'dialog' | 'whiptail'. % The locale to be used by dialogs: % % - default: the current user one % - none: no locale (defaulting to C) % - a user-specified one % -type dialog_locale() :: 'default' | 'none' | string(). -record( term_ui_state, { %state_filename = ?default_state_path :: file_utils:file_path(), dialog_tool :: dialog_tool(), dialog_tool_path :: file_utils:file_name(), locale = default :: dialog_locale(), % Generally little use of console outputs for this backend: log_console = false :: boolean(), log_file = undefined :: maybe( file_utils:file() ), settings :: setting_table() } ). -type ui_state() :: #term_ui_state{}. % For common, transverse defines: -include("ui.hrl"). % For control codes: -include("term_ui.hrl"). -export_type([ ui_state/0 ]). % The key used by this module to store its state in the process dictionaty: -define( state_key, term_ui_state ). % Tells whether this user-interface backend is available. -spec is_available() -> boolean(). is_available() -> case lookup_dialog_tool() of undefined -> false; % { T, TPath }: _ -> true end. % Starts the UI with default settings. % % Stores the corresponding state in the process dictionary, yet returns as well % that state, for any explicit later operation. % -spec start() -> ui_state(). start() -> start( _Opts=[] ). % Starts the UI with specified settings. % % Stores the corresponding state in the process dictionary, yet returns as well % that state, for any explicit later operation. % -spec start( ui_options() ) -> ui_state(). start( Options ) -> DialogUIState = case lookup_dialog_tool() of undefined -> throw( no_dialog_tool_available ); { Tool, ToolPath } -> init_state_with_dimensions( Tool, ToolPath ) end, start_helper( Options, DialogUIState ). % (helper) start_helper( _Options=[], UIState ) -> case process_dictionary:put( ?ui_name_key, ?MODULE ) of undefined -> ok; UIName -> throw( { ui_already_started, UIName } ) end, %trace_utils:debug_fmt( "Storing following initial UI state: ~s", % [ to_string( UIState ) ] ), % No prior state expected: case process_dictionary:put( ?ui_state_key, UIState ) of undefined -> ok; _ -> throw( term_ui_already_started ) end, UIState; start_helper( _Options=[ log_file | T ], UIState ) -> start_helper( [ { log_file, "ui.log" } | T ], UIState ); start_helper( _Options=[ { log_file, Filename } | T ], UIState ) -> LogFile = file_utils:open( Filename, [ write, exclusive ] ), file_utils:write( LogFile, "Starting term UI.\n" ), NewUIState = UIState#term_ui_state{ log_file=LogFile }, start_helper( T, NewUIState ); start_helper( UnexpectedList, _UIState ) when is_list( UnexpectedList ) -> throw( { unexpected_options, UnexpectedList } ); start_helper( SingleElem, UIState ) -> start_helper( [ SingleElem ], UIState ). % (helper) -spec init_state_with_dimensions( dialog_tool(), file_utils:file_path() ) -> ui_state(). init_state_with_dimensions( Tool=dialog, DialogPath ) -> Cmd = text_utils:join( _Sep=" ", [ DialogPath, "--print-maxsize", get_redirect_string() ] ), %trace_utils:debug_fmt( "Command: '~s'.", [ Cmd ] ), { Env, PortOpts } = get_execution_settings(), % By default we will be using the full terminal space: case system_utils:run_executable( Cmd, Env, _WorkingDir=undefined, PortOpts ) of % Ex: Result="MaxSize: 28, 107" { _ExitStatus=0, _Result="MaxSize: " ++ SizeString } -> % Here, SizeString="28, 107". [ HeightString, " " ++ WidthString ] = text_utils:split( SizeString, [ $, ] ), Height = text_utils:string_to_integer( HeightString ), Width = text_utils:string_to_integer( WidthString ), DimSettings = ?ui_table:new( [ { max_height, Height }, { max_width, Width } ] ), #term_ui_state{ dialog_tool=Tool, dialog_tool_path=DialogPath, settings=DimSettings }; { ExitStatus, Result } -> throw( { max_size_lookup_failure, ExitStatus, Result } ) end. % Sets specified UI setting.% -spec set( ui_setting_key(), ui_setting_value() ) -> void(). set( SettingKey, SettingValue ) -> set( [ { SettingKey, SettingValue } ] ). % Sets specified UI settings. -spec set( [ ui_setting_entry() ] ) -> void(). set( SettingEntries ) -> UIState = #term_ui_state{ settings=SettingTable } = get_state(), NewSettingTable = ?ui_table:add_entries( SettingEntries, SettingTable ), set_state( UIState#term_ui_state{ settings=NewSettingTable } ). % Unsets specified UI setting. -spec unset( [ ui_setting_key() ] | ui_setting_key() ) -> void(). unset( SettingKeys ) when is_list( SettingKeys ) -> UIState = #term_ui_state{ settings=SettingTable } = get_state(), NewSettingTable = ?ui_table:remove_entries( SettingKeys, SettingTable ), set_state( UIState#term_ui_state{ settings=NewSettingTable } ); unset( SettingKey ) -> UIState = #term_ui_state{ settings=SettingTable } = get_state(), NewSettingTable = ?ui_table:remove_entry( SettingKey, SettingTable ), set_state( UIState#term_ui_state{ settings=NewSettingTable } ). % Displays specified text, as a normal message. -spec display( text() ) -> void(). display( Text ) -> % Simplified example: %Cmd = "dialog --msgbox 'Hello!' 8 40", % Single quotes induce no specific issues (as are enclosed in double ones) EscapedText = text_utils:escape_double_quotes( Text ), trace_utils:debug_fmt( "Original text: '~s'; once escaped: '~s'.", [ Text, EscapedText ] ), #term_ui_state{ dialog_tool_path=ToolPath, settings=SettingTable } = get_state(), %trace_utils:debug_fmt( "Dialog path: '~s'.", [ ToolPath ] ), { SettingString, SuffixString } = get_dialog_settings( SettingTable ), %trace_utils:debug_fmt( "Setting string: '~s'.", [ SettingString ] ), %trace_utils:debug_fmt( "Suffix string: '~s'.", [ SuffixString ] ), DialogString = text_utils:format( "--msgbox \"~s\" ~s", [ EscapedText, SuffixString ] ), Cmd = text_utils:join( _Sep=" ", [ ToolPath, SettingString, DialogString ] ), trace_utils:debug_fmt( "term_ui display command: '~s'.", [ Cmd ] ), { Env, PortOpts } = get_execution_settings(), case system_utils:run_executable( Cmd, Env, _WorkingDir=undefined, PortOpts ) of { _ExitStatus=0, _Output="" } -> ok; { _ExitStatus=0, Output } -> trace_utils:debug_fmt( "Display output: '~s'.", [ Output ] ); { ExitStatus, Output } -> throw( { display_error_reported, ExitStatus, Output } ) end. % Displays specified formatted text, as a normal message. -spec display( text_utils:format_string(), [ term() ] ) -> void(). display( FormatString, Values ) -> display( text_utils:format( FormatString, Values ) ). % Displays in-order the items of specified list, as a normal message. -spec display_numbered_list( label(), [ text() ] ) -> void(). display_numbered_list( Label, Lines ) -> LineStrings = text_utils:strings_to_enumerated_string( Lines ), display( Label ++ LineStrings ). % Displays specified text, as an error message. -spec display_error( text() ) -> void(). display_error( Text ) -> % Simplified example: %Cmd = "dialog --infobox 'Error!' 8 40", % Single quotes induce no specific issues (as are enclosed in double ones) EscapedText = text_utils:escape_double_quotes( Text ), trace_utils:debug_fmt( "Original text: '~s'; once escaped: '~s'.", [ Text, EscapedText ] ), #term_ui_state{ dialog_tool_path=ToolPath, settings=SettingTable } = get_state(), %trace_utils:debug_fmt( "Dialog path: '~s'.", [ ToolPath ] ), ErrorSettingTable = ?ui_table:add_entry( 'title', ?red"Error"?normal, SettingTable ), { SettingString, SuffixString } = get_dialog_settings( ErrorSettingTable ), %trace_utils:debug_fmt( "Setting string: '~s'.", [ SettingString ] ), %trace_utils:debug_fmt( "Suffix string: '~s'.", [ SuffixString ] ), % Apparently button colors are ignored: %OKLabel = "--ok-label '"?red" Abort "?normal"'", OKLabel = "--ok-label 'Abort'", DialogString = "--colors " ++ OKLabel ++ text_utils:format( " --msgbox \"~s\" ~s", [ EscapedText, SuffixString ] ), Cmd = text_utils:join( _Sep=" ", [ ToolPath, SettingString, DialogString ] ), trace_utils:debug_fmt( "term_ui display command: '~s'.", [ Cmd ] ), { Env, PortOpts } = get_execution_settings(), case system_utils:run_executable( Cmd, Env, _WorkingDir=undefined, PortOpts ) of { _ExitStatus=0, _Output="" } -> ok; { _ExitStatus=0, Output } -> trace_utils:debug_fmt( "Display output: '~s'.", [ Output ] ); { ExitStatus, Output } -> throw( { display_error_reported, ExitStatus, Output } ) end. % Displays specified formatted text, as an error message. -spec display_error( text_utils:format_string(), [ term() ] ) -> void(). display_error( FormatString, Values ) -> display_error( text_utils:format( FormatString, Values ) ). % Displays in-order the items of specified list, as an error message. -spec display_error_numbered_list( label(), [ text() ] ) -> void(). display_error_numbered_list( Label, Lines ) -> LineStrings = text_utils:strings_to_enumerated_string( Lines ), display_error( Label ++ LineStrings ). % Adds a default separation between previous and next content. -spec add_separation() -> void(). add_separation() -> % Could be a clear. ok. % Displays specified prompt, let the user choose between two options, "yes" and % "no" (with specified default option), and returns that choice. % -spec ask_yes_no( prompt(), binary_choice() ) -> binary_choice(). ask_yes_no( Prompt, BinaryDefault ) -> ask_yes_no( Prompt, BinaryDefault, get_state() ). % Displays specified prompt, let the user choose between two options, "yes" and % "no" (with specified default option), and returns that choice. % -spec ask_yes_no( prompt(), binary_choice(), ui_state() ) -> binary_choice(). ask_yes_no( Prompt, BinaryDefault, #term_ui_state{ dialog_tool_path=ToolPath, settings=SettingTable } ) -> % Ex: dialog --backtitle "AA" --title "BB" --defaultno --yesno "Having\n % fun?" 6 25 % Single quotes induce no specific issues (as are enclosed in double ones) EscapedPrompt = text_utils:escape_double_quotes( Prompt ), DefaultChoiceOpt = case BinaryDefault of yes -> ""; no -> "--defaultno" end, { SettingString, SuffixString } = get_dialog_settings( SettingTable ), DialogString = text_utils:format( "~s --yesno \"~s\" ~s", [ DefaultChoiceOpt, EscapedPrompt, SuffixString ] ), CmdStrings = [ ToolPath, SettingString, DialogString ], %trace_utils:debug_fmt( "CmdStrings = ~p", [ CmdStrings ] ), Cmd = text_utils:join( _Sep=" ", CmdStrings ), { Env, PortOpts } = get_execution_settings(), case system_utils:run_executable( Cmd, Env, _WorkingDir=undefined, PortOpts ) of { _ExitStatus=0, _Result=[] } -> yes; { _ExitStatus=1, _Result=[] } -> no; { ExitStatus, Output } -> throw( { yes_no_choice_failed, ExitStatus, Output } ) end. % Selects, using a default prompt, an item among the specified ones (comprising, % for each, an internal designator and a text), and returns its designator. % % (const) % -spec choose_designated_item( [ choice_element() ] ) -> choice_designator(). choose_designated_item( Choices ) -> Label = text_utils:format( "Select among these ~B choices:", [ length( Choices ) ] ), choose_designated_item( Label, Choices ). % Selects, using specified prompt, an item among the specified ones (comprising, % for each, an internal designator and a text), and returns its designator. % % (const) % -spec choose_designated_item( label(), [ choice_element() ] ) -> choice_designator(). choose_designated_item( Label, Choices ) -> choose_designated_item( Label, Choices, get_state() ). % Selects, based on an explicit state, using the specified label, an item among % the specified ones (comprising, for each, an internal designator and a text), % and returns its designator. % % (const) % -spec choose_designated_item( label(), [ choice_element() ], ui_state() ) -> choice_designator(). choose_designated_item( Label, Choices, #term_ui_state{ dialog_tool_path=ToolPath, settings=SettingTable } ) -> % Ex: dialog --menu "Hello" 0 0 0 1 One 2 Two 3 Three { Designators, Texts } = lists:unzip( Choices ), ChoiceCount = length( Choices ), % We simply tag the choices with a counter (rather than using the designator % atoms): % NumChoices = lists:zip( lists:seq( 1, ChoiceCount ), Texts ), NumStrings = lists:foldl( fun( { Num, Text }, AccStrings ) -> [ text_utils:format( " ~B \"~s\"", [ Num, Text ] ) | AccStrings ] end, _Acc0=[], _List=NumChoices ), { SettingString, _SuffixString } = get_dialog_settings( SettingTable ), AutoSizeString = "0 0", DialogStrings = [ "--menu", "\"" ++ Label ++ "\"", AutoSizeString, _MenuHeight=text_utils:integer_to_string( ChoiceCount ) | lists:reverse( [ get_redirect_string() | NumStrings ] ) ], CmdStrings = [ ToolPath, SettingString | DialogStrings ], %trace_utils:debug_fmt( "CmdStrings = ~p", [ CmdStrings ] ), Cmd = text_utils:join( _Sep=" ", CmdStrings ), { Env, PortOpts } = get_execution_settings(), case system_utils:run_executable( Cmd, Env, _WorkingDir=undefined, PortOpts ) of { _ExitStatus=0, Result } -> ChosenNum = text_utils:string_to_integer( Result ), list_utils:get_element_at( Designators, ChosenNum ); { ExitStatus, Output } -> throw( { choice_failed, ExitStatus, Output } ) end. % Selects, based on an implicit state, using a default label, an item among the % specified ones (specified as direct text, with no specific designator % provided), and returns its index. % -spec choose_numbered_item( [ choice_text() ] ) -> choice_index(). choose_numbered_item( Choices ) -> choose_numbered_item( Choices, get_state() ). % Selects, based on an explicit state, using a default label, an item among the % specified ones (specified as direct text, with no specific designator % provided), and returns its index. % % Selects, based on an implicit state, using the specified label, an item among % the specified ones, and returns its index. % -spec choose_numbered_item( [ choice_text() ], ui_state() ) -> choice_index(); ( label(), [ choice_element() ] ) -> choice_index(). choose_numbered_item( Choices, UIState ) when is_record( UIState, term_ui_state ) -> Label = text_utils:format( "Select among these ~B choices:", [ length( Choices ) ] ), choose_numbered_item( Label, Choices, UIState ); choose_numbered_item( Label, Choices ) -> choose_numbered_item( Label, Choices, get_state() ). % Selects, based on an explicit state, using the specified label, an item among % the specified ones (specified as direct text, with no specific designator % provided), and returns its index. % -spec choose_numbered_item( label(), [ choice_text() ], ui_state() ) -> choice_index(). choose_numbered_item( Label, Choices, UIState ) -> % We could as well have used a radio list, yet a menu is probably a tad % clearer (and selecting the default, initial entry would have no real use % here). % % We reuse choose_designated_item/3 in a hackhish yet very simple way, based % on integer indexes: % ChoiceElements = lists:zip( lists:seq( 1, length( Choices ) ), Choices ), choose_designated_item( Label, ChoiceElements, UIState ). % Selects, based on an implicit state, using a default label, an item among the % specified ones, and returns its index. % -spec choose_numbered_item_with_default( [ choice_element() ], choice_index() ) -> choice_index(). choose_numbered_item_with_default( Choices, DefaultChoiceIndex ) -> choose_numbered_item_with_default( Choices, DefaultChoiceIndex, get_state() ). % Selects, based on an explicit state, using a default label, an item among the % specified ones (specified as direct text, with no specific designator % provided) and returns its index. % % Selects, based on an implicit state, using the specified label and default % item, an item among the specified ones, and returns its index. % -spec choose_numbered_item_with_default( [ choice_element() ], choice_index(), ui_state() ) -> choice_index(); ( label(), [ choice_element() ], maybe( choice_index() ) ) -> choice_index(). choose_numbered_item_with_default( Choices, DefaultChoiceIndex, UIState ) when is_record( UIState, term_ui_state ) -> Label = text_utils:format( "Select among these ~B choices:", [ length( Choices ) ] ), choose_numbered_item_with_default( Label, Choices, DefaultChoiceIndex, UIState ); choose_numbered_item_with_default( Label, Choices, DefaultChoiceIndex ) -> choose_numbered_item_with_default( Label, Choices, DefaultChoiceIndex, get_state() ). % Selects, based on an explicit state, using the specified label and default % item, an item among the specified ones (specified as direct text, with no % specific designator provided), and returns its index. % -spec choose_numbered_item_with_default( label(), [ choice_element() ], maybe( choice_index() ), ui_state() ) -> choice_index(). choose_numbered_item_with_default( _Label, _Choices, _DefaultChoiceIndex, _UIState ) -> % Using radio list rather than menu, for the selectable initial, default % choice: throw( todo ). % For traces, we attempt to do the same as text_ui, yet with a different % ui_state() (hence with no code reuse). % Traces specified message, by displaying it, and possibly logging it, based on % an implicit state. % -spec trace( message() ) -> void(). trace( Message ) -> trace( Message, get_state() ). % Traces specified message, by displaying it, and possibly logging it. % -spec trace( message(), ui_state() ) -> void(); ( text_utils:format_string(), [ term() ] ) -> void(). trace( Message, UIState ) when is_record( UIState, term_ui_state ) -> TraceMessage = "[trace] " ++ Message ++ "\n", case UIState#term_ui_state.log_console of true -> text_ui:display( TraceMessage, UIState ); false -> ok end, case UIState#term_ui_state.log_file of undefined -> ok; LogFile -> text_ui:display( LogFile, TraceMessage, UIState ) end; trace( FormatString, Values ) -> trace( text_utils:format( FormatString, Values ) ). % Clears the interface. -spec clear() -> void(). clear() -> clear( get_state() ). % Clears the interface. -spec clear( ui_state() ) -> void(). clear( #term_ui_state{ dialog_tool_path=ToolPath } ) -> % Simplified example: %Cmd = "dialog --clear", DialogString = "--clear", Cmd = text_utils:join( _Sep=" ", [ ToolPath, DialogString ] ), { Env, PortOpts } = get_execution_settings(), case system_utils:run_executable( Cmd, Env, _WorkingDir=undefined, PortOpts ) of { _ExitStatus=0, _Output="" } -> %trace_utils:debug( "Cleared." ), ok; { _ExitStatus=0, Output } -> trace_utils:debug_fmt( "Display output: '~s'.", [ Output ] ); { ExitStatus, Output } -> throw( { display_error_reported, ExitStatus, Output } ) end. % Stops the UI. -spec stop() -> void(). stop() -> stop( get_state() ). % Stops the UI. -spec stop( ui_state() ) -> void(). stop( UIState=#term_ui_state{ log_file=undefined } ) -> stop_helper( UIState ); stop( UIState=#term_ui_state{ log_file=LogFile } ) -> file_utils:write( LogFile, "Stopping UI.\n" ), file_utils:close( LogFile ), stop_helper( UIState ). % (helper) %stop_helper( #term_ui_state{ state_filename=StateFilename } ) -> stop_helper( _UIState ) -> clear(), %file_utils:remove_file_if_existing( StateFilename ), process_dictionary:remove( ?ui_state_key ). % Helper section. % Tries to find a suitable dialog tool. -spec lookup_dialog_tool() -> maybe( { dialog_tool(), file_utils:file_path() } ). lookup_dialog_tool() -> case executable_utils:lookup_executable( "dialog" ) of false -> % Maybe in the future: %AcceptWhiptail = true, AcceptWhiptail = false, case AcceptWhiptail andalso executable_utils:lookup_executable( "whiptail" ) of false -> undefined; WPath -> { whiptail, WPath } end; DPath -> %trace_utils:debug_fmt( "Dialog path: '~s'.", [ DPath ] ), { dialog, DPath } end. % Sets the current UI state. % % (helper) % -spec set_state( ui_state() ) -> void(). set_state( UIState ) -> %trace_utils:debug_fmt( "Setting as '~s': ~s.", % [ ?ui_state_key, to_string( UIState ) ] ), process_dictionary:put( ?ui_state_key, UIState ). % Returns the current UI state. % % (helper) % -spec get_state() -> ui_state(). get_state() -> case process_dictionary:get( ?ui_state_key ) of undefined -> throw( term_ui_not_started ); UIState -> UIState end. % Returns the command-line options corresponding to specified table: a settings % string, a suffix string (dealing with size and redirection). % -spec get_dialog_settings( setting_table() ) -> { text_utils:ustring(), text_utils:ustring() }. get_dialog_settings( SettingTable ) -> TitleOpt = case ?ui_table:get_value_with_defaults( 'title', _Default=undefined, SettingTable ) of undefined -> ""; Title -> % We prefer having the title surrounded by spaces: text_utils:format( "--title ' ~s '", [ Title ] ) end, BacktitleOpt = case ?ui_table:get_value_with_defaults( 'backtitle', _Default=undefined, SettingTable ) of undefined -> ""; Backtitle -> text_utils:format( "--backtitle '~s'", [ Backtitle ] ) end, SettingsOpts = [ TitleOpt, BacktitleOpt ], SettingsString = text_utils:join( _Separator=" ", SettingsOpts ), % Dialogs look a lot better if not using the maximum dimensions but % requesting auto sizing: %Height = ?ui_table:get_value( 'max_height', SettingTable ), %Width = ?ui_table:get_value( 'max_width', SettingTable ), % Auto: Height = 0, Width = 0, SuffixString = text_utils:format( "~B ~B ~s", [ Height, Width, get_redirect_string() ] ), { SettingsString, SuffixString }. % Returns a string to be used fir I/O redirection in an execution command. -spec get_redirect_string() -> text_utils:ustring(). get_redirect_string() -> % As 'nouse_stdio' will be needed: "2>&4". % Returns the settings suitable for an execution of the backend. -spec get_execution_settings() -> { system_utils:environment(), [ system_utils:port_option() ] }. get_execution_settings() -> Env = system_utils:get_standard_environment(), % Finding this combination was really not obvious: % (and the VM must be run with -noinput only) % PortOpts = [ stream, nouse_stdio, exit_status, eof ], { Env, PortOpts }. % Sets the specified setting to specified value, in the (implicit) UI state. -spec set_setting( ui_setting_key(), ui_setting_value() ) -> void(). set_setting( SettingKey, SettingValue ) -> NewUIState = set_setting( SettingKey, SettingValue, get_state() ), set_state( NewUIState ). % Sets the specified setting to specified value, in the specified UI state. -spec set_setting( ui_setting_key(), ui_setting_value(), ui_state() ) -> ui_state(). set_setting( SettingKey, SettingValue, UIState=#term_ui_state{ settings=SettingTable } ) -> NewSettingTable = ?ui_table:add_entry( SettingKey, SettingValue, SettingTable ), UIState#term_ui_state{ settings=NewSettingTable }. % Sets the specified settings to specified values, in the (implicit) UI state. -spec set_settings( [ ui_setting_entry() ] ) -> void(). set_settings( SettingEntries ) -> NewUIState = set_settings( SettingEntries, get_state() ), set_state( NewUIState ). % Sets the specified settings to specified values, in the specified UI state. -spec set_settings( [ ui_setting_entry() ], ui_state() ) -> ui_state(). set_settings( SettingEntries, UIState=#term_ui_state{ settings=SettingTable } ) -> NewSettingTable = ?ui_table:add_entries( SettingEntries, SettingTable ), UIState#term_ui_state{ settings=NewSettingTable }. % Unsets specified setting, in the (implicit) UI state. -spec unset_setting( ui_setting_key() ) -> void(). unset_setting( SettingKey ) -> NewUIState = unset_setting( SettingKey, get_state() ), set_state( NewUIState ). % Unsets specified setting, in the specified UI state. -spec unset_setting( ui_setting_key(), ui_state()) -> void(). unset_setting( SettingKey, UIState=#term_ui_state{ settings=SettingTable } ) -> NewSettingTable = ?ui_table:add_entry( SettingKey, _SettingValue=undefined, SettingTable ), UIState#term_ui_state{ settings=NewSettingTable }. % Returns the value (if any) associated, in the (implicit) UI state, to the % specified setting. % -spec get_setting( ui_setting_key() ) -> maybe( ui_setting_value() ). get_setting( SettingKey ) -> get_setting( SettingKey, get_state() ). % Returns the value (if any) associated, in the specified UI state, to the % specified setting. % -spec get_setting( ui_setting_key(), ui_state() ) -> maybe( ui_setting_value() ). get_setting( SettingKey, #term_ui_state{ settings=SettingTable } ) -> ?ui_table:get_value_with_defaults( SettingKey, _Default=undefined, SettingTable ). % Returns a textual description of the (implicit) UI state. -spec to_string() -> string(). to_string() -> to_string( get_state() ). % Returns a textual description of the specified UI state. -spec to_string( ui_state() ) -> string(). to_string( #term_ui_state{ %state_filename=StateFilename, dialog_tool=DialogTool, dialog_tool_path=DialogToolPath, locale=Locale, log_console=LogConsole, log_file=LogFile, settings=SettingTable }) -> DialogString = text_utils:format( "~s (found in '~s')", [ DialogTool, DialogToolPath ] ), LocaleString = text_utils:format( "using the ~s locale", [ Locale ] ), ConsoleString = case LogConsole of true -> ""; false -> "not" end, FileString = case LogFile of undefined -> "not using a log file"; _ -> text_utils:format( "using log file '~s'", [ LogFile ] ) end, SettingString = ui:settings_to_string( SettingTable ), %text_utils:format( "term_ui interface, using state file '~s' for tool ~s, " % "~s, ~s writing logs on console, ~s and ~s", text_utils:format( "term_ui interface, using tool ~s, " "~s, ~s writing logs on console, ~s and ~s", [ DialogString, LocaleString, ConsoleString, FileString, SettingString ] ).