From a27f41a6010f2bfa90bb6cfd92a472cc385bb2fa Mon Sep 17 00:00:00 2001 From: VetheonGames Date: Wed, 26 Jul 2023 16:31:20 -0600 Subject: [PATCH] feat: Introduce colored text windows and enhanced positioning This commit introduces a significant enhancement to the DynamicCursesInput gem, enabling the creation of colored text windows within the terminal. The new `DynamicCursesInput::ColorWindow` class provides methods to add colored text to the window and handle user input. Notable changes in this commit include: - Implementation of colored windows with customizable color schemes and text content. - Automatic centering of text within the window when 'x' is set to 'center.' - Fine-tuning of centered positioning to shift back by 12 cells for improved visual layout. - Removal of Fiber reliance and adoption of instance methods to resolve positioning issues. - Compatibility fixes for different terminal environments to ensure consistent behavior. Additionally, bug fixes have been made to resolve issues related to text misalignment and window positioning. This feature-rich update aims to improve the user experience and offer better control over window positioning and color schemes. We hope these enhancements will be beneficial to our users. Feedback and suggestions are always welcome! --- .gitignore | 1 + CHANGELOG.md | 29 ++++++++- Gemfile | 10 +-- Gemfile.lock | 38 +++++------ dynamic_curses_input.gemspec | 40 ++++++------ lib/dynamic_curses_input.rb | 76 ++++++++++++++++++++-- lib/dynamic_curses_input/color_window.rb | 77 +++++++++++++++++++++++ lib/dynamic_curses_input/input_handler.rb | 11 +++- lib/dynamic_curses_input/version.rb | 2 +- 9 files changed, 226 insertions(+), 58 deletions(-) create mode 100644 lib/dynamic_curses_input/color_window.rb diff --git a/.gitignore b/.gitignore index b04a8c8..3bd6c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ # rspec failure tracking .rspec_status +buildgem.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 26c2516..edb91ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ -## [Unreleased] +## [Release] -## [0.1.0] - 2023-06-07 +## [1.0.0] - 2023-06-07 - Initial release + +## [Release] + +## [1.1.0] - 2023-07-26 + +- Features and Enhancements: + + - Added support for printing colored windows with customized positions and color schemes. + - Introduced the DynamicCursesInput::ColorWindow class, which allows creating colored text windows within the terminal. + - The ColorWindow class provides methods to add colored text to the window and handle user input. + - Implemented automatic centering of text within the window when x is set to 'center'. + - Adjusted the centered position to shift back by 12 cells for better visual layout when necessary. + - Removed reliance on Fibers and replaced it with instance methods to resolve issues with positioning. + - Handled compatibility issues with different terminal environments to ensure consistent behavior. + - Refactored the code to eliminate unnecessary checks for IRB, enabling smooth execution in various contexts. + - Improved the debug log functionality for easier debugging and troubleshooting. + +- Bug Fixes: + + - Fixed the issue causing text to be misaligned or misplaced in certain terminal environments. + - Resolved a bug where the window position was not being updated correctly in some cases. + +- Other Changes: + + - Removed redundant and unused code snippets to improve code cleanliness and maintainability. diff --git a/Gemfile b/Gemfile index 9690e88..f93f9fd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,14 @@ # frozen_string_literal: true -source "https://rubygems.org" +source 'https://rubygems.org' # Specify your gem's dependencies in dynamic_curses_input.gemspec gemspec -gem "rake" +gem 'rake' -gem "rubocop" +gem 'rubocop' -gem "curses" +gem 'curses' + +gem 'reline' diff --git a/Gemfile.lock b/Gemfile.lock index 2f928b3..8bcd22e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,44 +1,38 @@ PATH remote: . specs: - dynamic_curses_input (1.0.0) + dynamic_curses_input (1.0.1.1) curses + reline GEM remote: https://rubygems.org/ specs: ast (2.4.2) curses (1.4.4) - diff-lcs (1.5.0) + io-console (0.6.0) json (2.6.3) + language_server-protocol (3.17.0.3) parallel (1.23.0) - parser (3.2.2.1) + parser (3.2.2.3) ast (~> 2.4.1) + racc + racc (1.7.1) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.8.0) + regexp_parser (2.8.1) + reline (0.3.6) + io-console (~> 0.5) rexml (3.2.5) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.5) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.0) - rubocop (1.52.0) + rubocop (1.55.0) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.29.0) @@ -53,8 +47,8 @@ DEPENDENCIES curses dynamic_curses_input! rake - rspec + reline rubocop BUNDLED WITH - 2.4.13 + 2.4.16 diff --git a/dynamic_curses_input.gemspec b/dynamic_curses_input.gemspec index eaf4275..acc3220 100644 --- a/dynamic_curses_input.gemspec +++ b/dynamic_curses_input.gemspec @@ -1,14 +1,14 @@ # frozen_string_literal: true -require_relative "lib/dynamic_curses_input/version" +require_relative 'lib/dynamic_curses_input/version' Gem::Specification.new do |spec| - spec.name = "dynamic_curses_input" + spec.name = 'dynamic_curses_input' spec.version = DynamicCursesInput::VERSION - spec.authors = ["VetheonGames"] - spec.email = ["vetheon@pixelatedstudios.net"] + spec.authors = ['VetheonGames'] + spec.email = ['vetheon@pixelatedstudios.net'] - spec.summary = "A simple library for making Curses TUI input more dynamic and user-friendly" + spec.summary = 'A simple library for making Curses TUI input more dynamic and user-friendly' spec.description = "Dynamic Curses Input is a highly simple, yet powerful gem that allows simple implementation of dynamic typing in curses TUI menus built in Ruby. For example, one can't simply use their arrow keys to navigate and edit inputs in Cursese TUI menus without adding a bunch of extra code to your @@ -17,23 +17,23 @@ Gem::Specification.new do |spec| allowing the special keys to work as the average user would expect. IE: When you press the left arrow key, the cursor moves to the left and allows you to delete a character you entered that isn't the last character you entered." - spec.homepage = "https://github.com/Pixelated-Studios/dynamic_curses_input" - spec.license = "MIT" - spec.required_ruby_version = "3.2.2" + spec.homepage = 'https://github.com/Pixelated-Studios/dynamic_curses_input' + spec.license = 'MIT' + spec.required_ruby_version = '3.2.2' - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://github.com/Pixelated-Studios/dynamic_curses_input" - spec.metadata["changelog_uri"] = "https://github.com/Pixelated-Studios/dynamic_curses_input/blob/main/CHANGELOG.md" + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = 'https://github.com/Pixelated-Studios/dynamic_curses_input' + spec.metadata['changelog_uri'] = 'https://github.com/Pixelated-Studios/dynamic_curses_input/blob/main/CHANGELOG.md' # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(__dir__) do - `git ls-files -z`.split("\x0").reject do |f| - f.start_with?("spec", ".rspec") || (File.expand_path(f) == __FILE__) - end - end - spec.require_paths = ["lib"] + spec.files = Dir.glob('{bin,lib,sig}/**/*') + Dir.glob('*').reject { |f| f.start_with?('spec', '.rspec', 'dynamic_curses_input.gemspec') } + spec.files << 'LICENSE.txt' + spec.files << 'README.md' + spec.files << 'dynamic_curses_input.gemspec' - spec.add_dependency "curses" - spec.add_development_dependency "rubocop" + spec.require_paths = ['lib'] + + spec.add_dependency 'curses' + spec.add_dependency 'reline' + spec.add_development_dependency 'rubocop' end diff --git a/lib/dynamic_curses_input.rb b/lib/dynamic_curses_input.rb index cf4c0b0..5257de0 100644 --- a/lib/dynamic_curses_input.rb +++ b/lib/dynamic_curses_input.rb @@ -2,8 +2,10 @@ # lib/dynamic_curses_input.rb -require_relative "dynamic_curses_input/version" -require_relative "dynamic_curses_input/input_handler" +require 'readline' # Add the Readline module +require_relative 'dynamic_curses_input/version' +require_relative 'dynamic_curses_input/input_handler' +require_relative 'dynamic_curses_input/color_window' # The module entrypoint for our Gem module DynamicCursesInput @@ -13,12 +15,74 @@ module DynamicCursesInput InputHandler.catch_input(echo) end - def self.ask_question(question, echo) + def self.ask_question(color = 'white', question, x: 'center', input: true, echo: nil) Curses.clear - Curses.setpos(1, 0) - Curses.addstr(question) + ColorWindow.add_color_window(color, question, y:, x:, input:, echo:) + end + + def self.print_color_window(color, text, y_value: nil, x: 'center', input: nil, echo: true) + case x + when 'center' + terminal_size = `stty size`.split.map(&:to_i) + y_value = terminal_size[0] / 2 + x_value = terminal_size[1] / 2 - text.length / 2 # Adjust x-coordinate to center the window + x_value -= 12 if x_value > 1 + # the above line shifts the X value of the cell coords back by 12 cells if we are trying to center the window + # we have to do this because math gets kind of approximate when we convert pixel ratios to character cell coords + when 'left' + y_value = Curses.lines / 2 + x_value = 0 + when 'right' + y_value = Curses.lines / 2 + x_value = Curses.cols - text.length + when 'left_center' + y_value = Curses.lines / 4 + x_value = 0 + when 'right_center' + y_value = Curses.lines / 4 + x_value = Curses.cols - text.length + else + y_value, x_value = x.split('px').map(&:to_i) + end + + # Initialize curses and get the terminal size + Curses.init_screen + Curses.start_color Curses.refresh - catch_input(echo) + + # Set up Readline for proper terminal settings + setup_readline + + ColorWindow.new(echo, x_value, y_value).add_color_window(color, text, x_value, y_value, input:, echo:) + end + + class << self + private + + def process_print_color_window_args(args) + case args.size + when 2 + ['white', args[0], args[1], 'center', nil, true] + when 3 + ['white', args[0], args[1], args[2], nil, true] + when 4 + ['white', args[0], args[1], args[2], args[3], true] + when 5 + [args[0], args[1], args[2], args[3], args[4]] + else + raise ArgumentError, 'print_color_window accepts 2 to 5 arguments: color, text, [position], [input], [echo]' + end + end + end + + def self.setup_readline + # Set up Readline for proper terminal settings + Readline.emacs_editing_mode + # the above line sets Readline to emacs_editing_mode so that terminals behave like they're supposed to + Readline.completion_append_character = ' ' + # we remove Readlines thing where it adds a space to the end of tab completes because it breaks the Curses cursor + Readline.completion_proc = proc { |_s| [] } + # here we basically are disabling tab completion all together. That's because for some reason it breaks the cursor end end diff --git a/lib/dynamic_curses_input/color_window.rb b/lib/dynamic_curses_input/color_window.rb new file mode 100644 index 0000000..1d22975 --- /dev/null +++ b/lib/dynamic_curses_input/color_window.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# lib/dynamic_curses_input/color_window.rb + +require 'curses' +require_relative 'input_handler' + +module DynamicCursesInput + # Class for creating a colored window + class ColorWindow + # Initialize instance variables and setup curses + def initialize(echo, x, y) + @echo = echo # Determines whether input should be echoed to the screen + setup_curses_color # Setup curses + @x = x + @y = y + # Define color pairs + Curses.init_pair(1, Curses::COLOR_BLACK, Curses::COLOR_BLACK) + Curses.init_pair(2, Curses::COLOR_BLUE, Curses::COLOR_BLACK) + Curses.init_pair(3, Curses::COLOR_GREEN, Curses::COLOR_BLACK) + Curses.init_pair(4, Curses::COLOR_CYAN, Curses::COLOR_BLACK) + Curses.init_pair(5, Curses::COLOR_RED, Curses::COLOR_BLACK) + Curses.init_pair(6, Curses::COLOR_MAGENTA, Curses::COLOR_BLACK) + Curses.init_pair(7, Curses::COLOR_YELLOW, Curses::COLOR_BLACK) # Brown is usually represented as yellow + Curses.init_pair(8, Curses::COLOR_WHITE, Curses::COLOR_BLACK) + end + + # Method that adds colored text to the window + def add_color_window(color, text, x, y, input: nil, echo: true) + # Map color names to color pair numbers + color_map = { + 'black' => 1, + 'blue' => 2, + 'green' => 3, + 'cyan' => 4, + 'red' => 5, + 'magenta' => 6, + 'brown' => 7, # Brown is usually represented as yellow in terminal colors + 'white' => 8 + } + + # Get the color pair number for the specified color + color_pair = color_map[color.downcase] + + # Set the cursor position if both x and y are specified + if x && y + set_position(y, x) + elsif x.nil? && y.nil? + # If both x and y are not specified, raise an ArgumentError + raise ArgumentError, 'Both x and y coordinates must be specified for printing the color window.' + end + + # Print the text in the specified color + Curses.attron(Curses.color_pair(color_pair)) + Curses.addstr(text) + Curses.attroff(Curses.color_pair(color_pair)) + + # If an input is specified, take input from the user + InputHandler.catch_input(echo) if input + + Curses.refresh + end + + private + + # Setup curses + def setup_curses_color + Curses.init_screen + Curses.start_color + end + + # Set cursor position manually on the X and Y axis + def set_position(y, x) + Curses.setpos(y, x) + end + end +end diff --git a/lib/dynamic_curses_input/input_handler.rb b/lib/dynamic_curses_input/input_handler.rb index 08b1859..a7eac7d 100644 --- a/lib/dynamic_curses_input/input_handler.rb +++ b/lib/dynamic_curses_input/input_handler.rb @@ -2,7 +2,7 @@ # lib/dynamic_curses_input/input_handler.rb -require "curses" +require 'curses' module DynamicCursesInput # our main class for handling input @@ -15,7 +15,7 @@ module DynamicCursesInput # Initialize instance variables and setup curses def initialize(echo) @echo = echo # Determines whether input should be echoed to the screen - @input = "" # Stores the input string + @input = '' # Stores the input string @cursor_pos = 0 # Stores the current cursor position @initial_y = Curses.stdscr.cury # Stores the initial y-coordinate of the cursor @initial_x = Curses.stdscr.curx # Stores the initial x-coordinate of the cursor @@ -84,7 +84,7 @@ module DynamicCursesInput # Redraw the input string def redraw_input Curses.setpos(@initial_y, @initial_x) # Move cursor to initial position - Curses.addstr(" " * (Curses.cols - @initial_x)) # Clear line + Curses.addstr(' ' * (Curses.cols - @initial_x)) # Clear line Curses.setpos(@initial_y, @initial_x) # Move cursor to initial position Curses.addstr(@input) if @echo # Draw input string if @echo is true Curses.setpos(@initial_y, @initial_x + @cursor_pos) # Move cursor to current position @@ -102,6 +102,11 @@ module DynamicCursesInput def self.right(cursor_pos, length) cursor_pos == length ? cursor_pos : cursor_pos + 1 end + + # Set cursor position + def self.set_position(y, x) + Curses.setpos(y, x) + end end # Class for deleting characters diff --git a/lib/dynamic_curses_input/version.rb b/lib/dynamic_curses_input/version.rb index bd266b5..19fd94b 100644 --- a/lib/dynamic_curses_input/version.rb +++ b/lib/dynamic_curses_input/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module DynamicCursesInput - VERSION = "1.0.0" + VERSION = '1.1.0' end