#!/usr/bin/perl use strict; use warnings; use Gtk3 '-init'; use Glib qw(TRUE FALSE); use File::Glob; use FileHandle; use LWP::Simple; use Gtk3::SourceView; use IPC::Open3; use IO::Select; # Declare global variables our $textbuffer; # SourceView buffer for the editor our $textview; # SourceView widget for editor our $statusbar; # Status bar for feedback our $open; # Current file path our $track = 'init'; # Tracks modifications our $shell_buffer; # SourceView buffer for pshell our $shell_view; # SourceView widget for pshell our $shell_scrolled; # Scrolled window for pshell our $shell_pid; # pshell process ID # Base directory setup (reused from your code) my ($ar) = @ARGV; my $basedir; if ($^O ne "Win32") { if ($0 =~ /\//g) { my @hd = split "/", $0; pop @hd; # Remove filename $basedir = join('/', @hd) || "."; } else { $basedir = "."; } } else { $basedir = "."; } # Main window my $window = Gtk3::Window->new('toplevel'); $window->set_title('KPad'); $window->set_default_size(800, 600); $window->signal_connect(destroy => sub { Gtk3->main_quit }); # Main layout my $vbox = Gtk3::Box->new('vertical', 0); $window->add($vbox); # Menu bar my $menubar = Gtk3::MenuBar->new; $vbox->pack_start($menubar, FALSE, FALSE, 0); # File menu my $file_menu = Gtk3::Menu->new; my $file_item = Gtk3::MenuItem->new_with_label('File'); $file_item->set_submenu($file_menu); $menubar->append($file_item); my $new_item = Gtk3::MenuItem->new_with_label('New'); $new_item->signal_connect(activate => sub { $textbuffer->set_text(''); $statusbar->push(0, 'New file'); }); $file_menu->append($new_item); my $open_item = Gtk3::MenuItem->new_with_label('Open'); $open_item->signal_connect(activate => \&open_file); $file_menu->append($open_item); my $save_item = Gtk3::MenuItem->new_with_label('Save'); $save_item->signal_connect(activate => \&save_file); $file_menu->append($save_item); my $save_as_item = Gtk3::MenuItem->new_with_label('Save As'); $save_as_item->signal_connect(activate => \&save_as_file); $file_menu->append($save_as_item); my $exit_item = Gtk3::MenuItem->new_with_label('Exit'); $exit_item->signal_connect(activate => \&tapp); $file_menu->append($exit_item); # Edit menu my $edit_menu = Gtk3::Menu->new; my $edit_item = Gtk3::MenuItem->new_with_label('Edit'); $edit_item->set_submenu($edit_menu); $menubar->append($edit_item); my $undo_item = Gtk3::MenuItem->new_with_label('Undo'); $undo_item->signal_connect(activate => sub { $textbuffer->undo if $textbuffer->can_undo; $statusbar->push(0, 'Undo'); }); $edit_menu->append($undo_item); my $redo_item = Gtk3::MenuItem->new_with_label('Redo'); $redo_item->signal_connect(activate => sub { $textbuffer->redo if $textbuffer->can_redo; $statusbar->push(0, 'Redo'); }); $edit_menu->append($redo_item); my $cut_item = Gtk3::MenuItem->new_with_label('Cut'); $cut_item->signal_connect(activate => sub { $textbuffer->cut_clipboard(Gtk3::Clipboard::get('CLIPBOARD'), TRUE); }); $edit_menu->append($cut_item); my $copy_item = Gtk3::MenuItem->new_with_label('Copy'); $copy_item->signal_connect(activate => sub { $textbuffer->copy_clipboard(Gtk3::Clipboard::get('CLIPBOARD')); }); $edit_menu->append($copy_item); my $paste_item = Gtk3::MenuItem->new_with_label('Paste'); $paste_item->signal_connect(activate => sub { $textbuffer->paste_clipboard(Gtk3::Clipboard::get('CLIPBOARD'), undef, TRUE); }); $edit_menu->append($paste_item); my $select_all_item = Gtk3::MenuItem->new_with_label('Select All'); $select_all_item->signal_connect(activate => sub { $textbuffer->select_range($textbuffer->get_start_iter, $textbuffer->get_end_iter); }); $edit_menu->append($select_all_item); my $find_item = Gtk3::MenuItem->new_with_label('Find'); $find_item->signal_connect(activate => sub { find_and_replace_dialog(1); }); $edit_menu->append($find_item); my $replace_item = Gtk3::MenuItem->new_with_label('Find and Replace'); $replace_item->signal_connect(activate => sub { find_and_replace_dialog(0); }); $edit_menu->append($replace_item); # View menu (wrap options) my $view_menu = Gtk3::Menu->new; my $view_item = Gtk3::MenuItem->new_with_label('View'); $view_item->set_submenu($view_menu); $menubar->append($view_item); my $wrap_menu = Gtk3::Menu->new; my $wrap_item = Gtk3::MenuItem->new_with_label('Wrap'); $wrap_item->set_submenu($wrap_menu); $view_menu->append($wrap_item); my $wrap_word = Gtk3::CheckMenuItem->new_with_label('Word'); $wrap_word->signal_connect(toggled => sub { $textview->set_wrap_mode($wrap_word->get_active ? 'word' : 'none'); }); $wrap_menu->append($wrap_word); my $wrap_char = Gtk3::CheckMenuItem->new_with_label('Char'); $wrap_char->signal_connect(toggled => sub { $textview->set_wrap_mode($wrap_char->get_active ? 'char' : 'none'); }); $wrap_menu->append($wrap_char); my $wrap_none = Gtk3::CheckMenuItem->new_with_label('None'); $wrap_none->signal_connect(toggled => sub { $textview->set_wrap_mode($wrap_none->get_active ? 'none' : 'none'); }); $wrap_menu->append($wrap_none); # Tools menu my $tools_menu = Gtk3::Menu->new; my $tools_item = Gtk3::MenuItem->new_with_label('Tools'); $tools_item->set_submenu($tools_menu); $menubar->append($tools_item); my $goto_line_item = Gtk3::MenuItem->new_with_label('Goto Line'); $goto_line_item->signal_connect(activate => \&goto_line_dialog); $tools_menu->append($goto_line_item); my $which_line_item = Gtk3::MenuItem->new_with_label('Which Line?'); $which_line_item->signal_connect(activate => \&which_line_dialog); $tools_menu->append($which_line_item); my $toggle_shell = Gtk3::MenuItem->new_with_label('Toggle pshell'); $toggle_shell->signal_connect(activate => sub { $shell_scrolled->set_visible(!$shell_scrolled->get_visible); }); $tools_menu->append($toggle_shell); # HTML menu my $html_menu = Gtk3::Menu->new; my $html_item = Gtk3::MenuItem->new_with_label('HTML'); $html_item->set_submenu($html_menu); $menubar->append($html_item); my $fetch_item = Gtk3::MenuItem->new_with_label('Fetch a web resource...'); $fetch_item->signal_connect(activate => \&fetch_html); $html_menu->append($fetch_item); # Macros menu (plugins) my $macros_menu = Gtk3::Menu->new; my $macros_item = Gtk3::MenuItem->new_with_label('Macros'); $macros_item->set_submenu($macros_menu); $menubar->append($macros_item); my $exec_macro_item = Gtk3::MenuItem->new_with_label('Execute Macro'); $exec_macro_item->signal_connect(activate => \&show_plugin_dialog); $macros_menu->append($exec_macro_item); # Help menu my $help_menu = Gtk3::Menu->new; my $help_item = Gtk3::MenuItem->new_with_label('Help'); $help_item->set_submenu($help_menu); $menubar->append($help_item); my $help_topics_item = Gtk3::MenuItem->new_with_label('Help Topics...'); $help_topics_item->signal_connect(activate => sub { my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'info', 'ok', "Help topics for KPAD\nWell, this is a text/file editor mainly meant for scripting and programming use.\nLike notepad but made for the programmer."); $dialog->run; $dialog->destroy; }); $help_menu->append($help_topics_item); my $about_item = Gtk3::MenuItem->new_with_label('About KPAD...'); $about_item->signal_connect(activate => sub { my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'info', 'ok', "kPad\nby Paul Malcher\nVersion 6 Release\n"); $dialog->run; $dialog->destroy; }); $help_menu->append($about_item); # Split pane for editor and pshell my $paned = Gtk3::Paned->new('vertical'); $vbox->pack_start($paned, TRUE, TRUE, 0); # Editor (SourceView) my $scrolled = Gtk3::ScrolledWindow->new(undef, undef); $paned->pack1($scrolled, TRUE, TRUE); $textview = Gtk3::SourceView::View->new; $textview->set_wrap_mode('word'); $textview->set_show_line_numbers(TRUE); $textview->set_auto_indent(TRUE); $scrolled->add($textview); $textbuffer = $textview->get_buffer; my $lang_manager = Gtk3::SourceView::LanguageManager->get_default; my $language = $lang_manager->get_language('perl'); $textbuffer->set_language($language) if $language; $textbuffer->set_highlight_syntax(TRUE); # pshell panel (SourceView) $shell_scrolled = Gtk3::ScrolledWindow->new(undef, undef); $paned->pack2($shell_scrolled, FALSE, TRUE); $shell_view = Gtk3::SourceView::View->new; $shell_view->set_wrap_mode('word'); $shell_view->set_editable(TRUE); $shell_scrolled->add($shell_view); $shell_buffer = $shell_view->get_buffer; my $shell_lang = $lang_manager->get_language('perl'); $shell_buffer->set_language($shell_lang) if $shell_lang; $shell_buffer->set_highlight_syntax(TRUE); $shell_buffer->set_text("pshell:/home/kake26/projects/perl/kpadold> "); # Start pshell my ($wtr, $rdr, $err); my $pshell_path = "$basedir/pshell"; if (!-x $pshell_path) { $statusbar->push(0, "Error: pshell not found or not executable at $pshell_path"); } else { eval { $shell_pid = open3($wtr, $rdr, $err, $pshell_path); 1; } or do { $statusbar->push(0, "Failed to start pshell: $@"); $shell_buffer->set_text("Error: Could not start pshell\n"); }; if ($shell_pid && defined $rdr && defined $err) { # Ensure unbuffered output select($wtr); $| = 1; select(STDOUT); # Non-blocking read for pshell stdout and stderr my $selector = IO::Select->new($rdr, $err); Glib::IO->add_watch(fileno($rdr), ['in', 'hup'], sub { my ($fileno, $condition) = @_; if ($condition & 'in') { my $output; sysread($rdr, $output, 4096); # Increased buffer size if ($output) { $output =~ s/pshell:[^\n>]*> //g; # Strip prompt if included $shell_buffer->insert_at_cursor($output); $shell_view->scroll_to_iter($shell_buffer->get_end_iter, 0, FALSE, 0, 0); } } return FALSE if $condition & 'hup'; return TRUE; }) if defined $rdr; Glib::IO->add_watch(fileno($err), ['in', 'hup'], sub { my ($fileno, $condition) = @_; if ($condition & 'in') { my $output; sysread($err, $output, 4096); if ($output) { $output =~ s/pshell:[^\n>]*> //g; # Strip prompt if included $shell_buffer->insert_at_cursor($output); $shell_view->scroll_to_iter($shell_buffer->get_end_iter, 0, FALSE, 0, 0); } } return FALSE if $condition & 'hup'; return TRUE; }) if defined $err; } } # Handle pshell input $shell_view->signal_connect('key-press-event' => sub { my ($widget, $event) = @_; if ($event->keyval == Gtk3::Gdk::KEY_Return) { my ($start, $end) = $shell_buffer->get_bounds; my $text = $shell_buffer->get_text($start, $end, TRUE); my ($cmd) = $text =~ /pshell:[^\n>]*> (.*?)$/s; if (defined $cmd && defined $wtr) { print $wtr "$cmd\n"; $shell_buffer->insert_at_cursor("\npshell:$ENV{HOME}> "); } return TRUE; } return FALSE; }); # CSS styling for Kubuntu my $provider = Gtk3::CssProvider->new; $provider->load_from_data(' textview { font-family: Monospace; font-size: 12pt; } button { color: black; background: #d3d3d3; } '); Gtk3::StyleContext::add_provider_for_screen( Gtk3::Gdk::Screen::get_default, $provider, Gtk3::STYLE_PROVIDER_PRIORITY_APPLICATION ); # Status bar $statusbar = Gtk3::Statusbar->new; $vbox->pack_start($statusbar, FALSE, FALSE, 0); $textbuffer->signal_connect('mark-set' => sub { my ($buf, $iter, $mark) = @_; if ($mark->get_name eq 'insert') { my $line = $iter->get_line + 1; $statusbar->push(0, "Line: $line"); } }); # Track modifications $textbuffer->signal_connect('modified-changed' => sub { $track = $textbuffer->get_text($textbuffer->get_start_iter, $textbuffer->get_end_iter, TRUE); }); # Load file from ARGV if ($ar) { if (open my $fh, '<', $ar) { local $/; $textbuffer->set_text(<$fh>); close $fh; $statusbar->push(0, "Opened $ar"); $open = $ar; } else { $statusbar->push(0, "Failed to open $ar"); } } # Plugin system my @plugins; my @n; my $pls = 0; while (<*.kpd>) { push @plugins, $_; open my $pin, '<', $_ or next; my @gn = split '::', <$pin>; $n[$pls] = $gn[2] eq 'auto' ? 'auto' : $gn[1]; $pls++; } my $nop = grep { $_ ne 'auto' } @n; # File handling sub open_file { my $dialog = Gtk3::FileChooserDialog->new( 'Open File', $window, 'open', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok' ); $dialog->add_filter(Gtk3::FileFilter->new)->add_pattern('*.pl'); $dialog->add_filter(Gtk3::FileFilter->new)->add_pattern('*'); if ($dialog->run eq 'ok') { my $filename = $dialog->get_filename; if (open my $fh, '<', $filename) { local $/; $textbuffer->set_text(<$fh>); close $fh; $statusbar->push(0, "Opened $filename"); $open = $filename; } else { $statusbar->push(0, "Failed to open $filename"); } } $dialog->destroy; } sub save_file { if ($open) { my ($start, $end) = $textbuffer->get_bounds; my $text = $textbuffer->get_text($start, $end, TRUE); if (open my $fh, '>', $open) { print $fh $text; close $fh; $statusbar->push(0, "Saved $open"); } else { $statusbar->push(0, "Failed to save $open"); } } else { save_as_file(); } } sub save_as_file { my $dialog = Gtk3::FileChooserDialog->new( 'Save File', $window, 'save', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok' ); $dialog->add_filter(Gtk3::FileFilter->new)->add_pattern('*.pl'); $dialog->add_filter(Gtk3::FileFilter->new)->add_pattern('*'); if ($dialog->run eq 'ok') { my $filename = $dialog->get_filename; my ($start, $end) = $textbuffer->get_bounds; my $text = $textbuffer->get_text($start, $end, TRUE); if (open my $fh, '>', $filename) { print $fh $text; close $fh; $statusbar->push(0, "Saved $filename"); $open = $filename; } else { $statusbar->push(0, "Failed to save $filename"); } } $dialog->destroy; } # Find and replace dialog sub find_and_replace_dialog { my ($find_only) = @_; my $dialog = Gtk3::Dialog->new($find_only ? 'Find' : 'Find and Replace', $window, 'modal', 'gtk-ok', 'ok', 'gtk-cancel', 'cancel'); my $find_entry = Gtk3::Entry->new; my $replace_entry = Gtk3::Entry->new; my $find_label = Gtk3::Label->new('Find:'); $dialog->get_content_area->pack_start($find_label, FALSE, FALSE, 0); $dialog->get_content_area->pack_start($find_entry, FALSE, FALSE, 0); unless ($find_only) { my $replace_label = Gtk3::Label->new('Replace with:'); $dialog->get_content_area->pack_start($replace_label, FALSE, FALSE, 0); $dialog->get_content_area->pack_start($replace_entry, FALSE, FALSE, 0); } $dialog->show_all; if ($dialog->run eq 'ok') { my $search = $find_entry->get_text; my $replace = $replace_entry->get_text; my $search_context = Gtk3::SourceView::SearchContext->new($textbuffer); $search_context->set_search_text($search, []); my $iter = $textbuffer->get_start_iter; if (my ($match_start, $match_end) = $search_context->forward($iter)) { $textbuffer->select_range($match_start, $match_end); unless ($find_only) { $search_context->replace($match_start, $match_end, $replace, -1); $statusbar->push(0, "Replaced '$search' with '$replace'"); } else { $statusbar->push(0, "Found '$search'"); } } else { $statusbar->push(0, "Text not found"); } } $dialog->destroy; } # Goto line dialog sub goto_line_dialog { my $dialog = Gtk3::Dialog->new('Goto Line', $window, 'modal', 'gtk-ok', 'ok', 'gtk-cancel', 'cancel'); my $entry = Gtk3::Entry->new; $dialog->get_content_area->pack_start($entry, FALSE, FALSE, 0); $dialog->show_all; if ($dialog->run eq 'ok') { my $line = $entry->get_text; if ($line =~ /^\d+$/) { my $iter = $textbuffer->get_iter_at_line($line - 1); $textbuffer->place_cursor($iter); $textview->scroll_to_iter($iter, 0, TRUE, 0, 0.5); } } $dialog->destroy; } # Which line dialog sub which_line_dialog { my $iter = $textbuffer->get_iter_at_mark($textbuffer->get_insert); my $line = $iter->get_line + 1; my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'info', 'ok', "Current line: $line"); $dialog->run; $dialog->destroy; } # HTML fetch sub fetch_html { my $dialog = Gtk3::Dialog->new('HTML Source Fetch', $window, 'modal', 'gtk-ok', 'ok', 'gtk-cancel', 'cancel'); my $label = Gtk3::Label->new('Fetch what:'); my $entry = Gtk3::Entry->new; $entry->set_text('http://'); $dialog->get_content_area->pack_start($label, FALSE, FALSE, 0); $dialog->get_content_area->pack_start($entry, FALSE, FALSE, 0); $dialog->show_all; if ($dialog->run eq 'ok') { my $url = $entry->get_text; my $contents = get($url); if ($contents) { $textbuffer->set_text($contents); $statusbar->push(0, "Fetched $url"); } else { $statusbar->push(0, "Failed to fetch $url"); } } $dialog->destroy; } # Plugin dialog sub show_plugin_dialog { my $dialog = Gtk3::Dialog->new('Macro Execution Menu', $window, 'modal', 'gtk-close', 'close'); my $label = Gtk3::Label->new('Double Click To Execute Macro'); my $listbox = Gtk3::ListBox->new; $dialog->get_content_area->pack_start($label, FALSE, FALSE, 0); $dialog->get_content_area->pack_start($listbox, TRUE, TRUE, 0); foreach my $name (@n) { next if $name eq 'auto'; my $row = Gtk3::ListBoxRow->new; my $lbl = Gtk3::Label->new($name); $row->add($lbl); $listbox->add($row); } $listbox->signal_connect(row_activated => sub { my ($lb, $row) = @_; my $name = $row->get_child->get_text; eplugin($name); }); $dialog->show_all; $dialog->run; $dialog->destroy; } # Plugin execution sub eplugin { my ($v) = @_; my $fp = 0; while ($n[$fp] ne $v) { $fp++; } $v = $plugins[$fp]; open my $pe, '<', "$basedir/$v" or return; my $tdata = do { local $/; <$pe> }; close $pe; $tdata =~ s/.*?\n//; # Skip first line eval $tdata; if ($@) { my $error = $@; my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'error', 'ok', "Error: $error"); $dialog->run; $dialog->destroy; } } # Auto plugin execution sub aeplugin { for my $i (0..$#n) { next unless $n[$i] eq 'auto'; my $v = $plugins[$i]; open my $pe, '<', "$basedir/$v" or next; my $tdata = do { local $/; <$pe> }; close $pe; $tdata =~ s/.*?\n//; eval $tdata; if ($@) { my $error = $@; my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'error', 'ok', "Error: $error"); $dialog->run; $dialog->destroy; } } } # Shutdown handler sub tapp { if ($track ne 'init') { my $dialog = Gtk3::MessageDialog->new( $window, 'modal', 'question', 'yes-no', 'File contents have changed, save now?!' ); my $response = $dialog->run; $dialog->destroy; if ($response eq 'yes') { save_file(); } } # Kill pshell process kill 'TERM', $shell_pid if $shell_pid; Gtk3->main_quit; } # Run auto plugins my $arun = 0; if ($arun eq '0') { aeplugin(); } # Show and start $window->show_all; Gtk3->main;