diff --git a/Tea Code.app/Contents/Info.plist b/Tea Code.app/Contents/Info.plist new file mode 100644 index 0000000..72b54ba --- /dev/null +++ b/Tea Code.app/Contents/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleExecutable + launcher + + CFBundleIdentifier + com.blurrysquire.teacode + + CFBundleName + teacode + + CFBundleDisplayName + TeaCode + + CFBundleVersion + 1.0 + + CFBundleShortVersionString + 1.0 + + CFBundlePackageType + APPL + + LSMinimumSystemVersion + 14.0 + + CFBundleIconFile + AppIcon + + diff --git a/Tea Code.app/Contents/MacOS/launcher b/Tea Code.app/Contents/MacOS/launcher new file mode 100755 index 0000000..b2484d7 --- /dev/null +++ b/Tea Code.app/Contents/MacOS/launcher @@ -0,0 +1,7 @@ +#!/bin/bash + +# Tea Code launcher + +#set current dir +cd "$(dirname "$0")" +./tea-code diff --git a/Tea Code.app/Contents/MacOS/resources/modified-icon.png b/Tea Code.app/Contents/MacOS/resources/modified-icon.png new file mode 100644 index 0000000..a93df45 Binary files /dev/null and b/Tea Code.app/Contents/MacOS/resources/modified-icon.png differ diff --git a/Tea Code.app/Contents/Resources/AppIcon.icns b/Tea Code.app/Contents/Resources/AppIcon.icns new file mode 100644 index 0000000..b9e20f6 Binary files /dev/null and b/Tea Code.app/Contents/Resources/AppIcon.icns differ diff --git a/package/macos/info.plist b/package/macos/info.plist new file mode 100644 index 0000000..72b54ba --- /dev/null +++ b/package/macos/info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleExecutable + launcher + + CFBundleIdentifier + com.blurrysquire.teacode + + CFBundleName + teacode + + CFBundleDisplayName + TeaCode + + CFBundleVersion + 1.0 + + CFBundleShortVersionString + 1.0 + + CFBundlePackageType + APPL + + LSMinimumSystemVersion + 14.0 + + CFBundleIconFile + AppIcon + + diff --git a/package/macos/launcher b/package/macos/launcher new file mode 100755 index 0000000..b2484d7 --- /dev/null +++ b/package/macos/launcher @@ -0,0 +1,7 @@ +#!/bin/bash + +# Tea Code launcher + +#set current dir +cd "$(dirname "$0")" +./tea-code diff --git a/scripts/make_macosbundle.sh b/scripts/make_macosbundle.sh new file mode 100755 index 0000000..337c042 --- /dev/null +++ b/scripts/make_macosbundle.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +APP_NAME="Tea Code" +APP_BUNDLE="$ROOT_DIR/${APP_NAME}.app" + +rm -rf "$APP_BUNDLE" #if it exists + +mkdir -p "$APP_BUNDLE/Contents/" +mkdir -p "$APP_BUNDLE/Contents/MacOS" +mkdir -p "$APP_BUNDLE/Contents/MacOS/resources" +mkdir -p "$APP_BUNDLE/Contents/Resources" + +cp "$ROOT_DIR/tea-code" "$APP_BUNDLE/Contents/MacOS/tea-code" +cp "$ROOT_DIR/package/macos/launcher" "$APP_BUNDLE/Contents/MacOS/launcher" + +chmod +x "$APP_BUNDLE/Contents/MacOS/tea-code" +chmod +x "$APP_BUNDLE/Contents/MacOS/launcher" + +cp "$ROOT_DIR/package/macos/settings.toml" "$APP_BUNDLE/Contents/MacOS/settings.toml" +cp "$ROOT_DIR/branding/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/AppIcon.icns" +cp "$ROOT_DIR/package/macos/info.plist" "$APP_BUNDLE/Contents/Info.plist" +cp "$ROOT_DIR/resources/modified-icon.png" "$APP_BUNDLE/Contents/MacOS/resources/modified-icon.png" + +echo "Created $APP_BUNDLE" diff --git a/src/main.cpp b/src/main.cpp index 8748b0c..82e1479 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ enum { #include "settings.hpp" #include "file_view.hpp" #include "editor_tabs.hpp" +#include "terminal.hpp" class MainFrame : public wxFrame { public: @@ -48,10 +49,16 @@ class MainFrame : public wxFrame { if (!startup_path.empty()) { this->file_view->PopulateTree(startup_path); } + this->workspace_path = startup_path; + this->editor_splitter = new wxSplitterWindow(this->splitter, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSP_LIVE_UPDATE); + this->editor_tabs = new EditorTabs(this->editor_splitter, wxID_ANY); + this->terminal = new Terminal(this->editor_splitter, wxID_ANY, this->workspace_path); - this->editor_tabs = new EditorTabs(this->splitter, wxID_ANY); + this->editor_splitter->SplitHorizontally(this->editor_tabs, this->terminal, -200); + this->editor_splitter->SetMinimumPaneSize(50); + this->editor_splitter->SetSashGravity(1.0); - this->splitter->SplitVertically(this->file_view, this->editor_tabs, 200); + this->splitter->SplitVertically(this->file_view, this->editor_splitter, 200); this->splitter->SetMinimumPaneSize(50); Bind(wxEVT_MENU, &MainFrame::OnNewFile, this, wxID_NEW); @@ -74,9 +81,11 @@ class MainFrame : public wxFrame { private: wxSplitterWindow* splitter; + wxSplitterWindow* editor_splitter; wxString workspace_path; FileView* file_view; EditorTabs* editor_tabs; + Terminal* terminal; void OnNewFile(wxCommandEvent& event) { this->editor_tabs->NewFile(); @@ -100,7 +109,9 @@ class MainFrame : public wxFrame { if (dir_dialog.ShowModal() == wxID_CANCEL) return; - this->file_view->PopulateTree(dir_dialog.GetPath()); + this->workspace_path = dir_dialog.GetPath(); + this->file_view->PopulateTree(this->workspace_path); + this->terminal->ChangeDirectory(this->workspace_path); } void OnFileViewActivated(wxCommandEvent& event) { diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..500b744 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,219 @@ +#include "terminal.hpp" + +#include +#include + +//TODO: +// input is only after the prompt +// use ptx instead of this emulation + +Terminal::Terminal(wxWindow* parent, wxWindowID id, const wxString& working_directory) + : wxPanel(parent, id), process(nullptr), pid(0), input_start(0), + working_directory(working_directory), applying_programmatic_change(false), poll_timer(this) +{ + this->console = new wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_RICH2 | wxBORDER_NONE); + + // read from settings + wxFont mono_font(12, wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL); + this->console->SetFont(mono_font); + + wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL); + sizer->Add(this->console, 1, wxEXPAND); + this->SetSizer(sizer); + + this->console->Bind(wxEVT_CHAR, &Terminal::OnChar, this); + this->console->Bind(wxEVT_LEFT_UP, &Terminal::OnConsoleClick, this); + this->console->Bind(wxEVT_TEXT, &Terminal::OnTextChanged, this); + Bind(wxEVT_TIMER, &Terminal::OnPollTimer, this, this->poll_timer.GetId()); + Bind(wxEVT_END_PROCESS, &Terminal::OnProcessTerminated, this); + + this->StartShell(); + + this->console->SetFocus(); +} + +Terminal::~Terminal() { + this->poll_timer.Stop(); + + if (this->process != nullptr) { + this->process->Detach(); + wxKill(this->pid, wxSIGTERM); + } +} + +void Terminal::StartShell() { + #ifdef __WXMSW__ + wxString shell_command = "cmd.exe"; + #else + wxString shell_command = "/bin/bash -i"; + #endif + + wxEnvVariableHashMap env; + wxGetEnvMap(&env); + env["TERM"] = "dumb"; + env["PS1"] = "\\w $ "; + + this->process = new wxProcess(this); + this->process->Redirect(); + + wxExecuteEnv execute_env; + execute_env.env = env; + if (!this->working_directory.empty()) { + execute_env.cwd = this->working_directory; + } + + this->pid = wxExecute(shell_command, wxEXEC_ASYNC, this->process, &execute_env); + + if (this->pid == 0) { + delete this->process; + this->process = nullptr; + this->WriteLine("failed to start shell"); + return; + } + + //this->SendCommand("stty -echo"); + + this->poll_timer.Start(100); +} + +void Terminal::SendCommand(const wxString& command) { + if (this->process == nullptr) return; + + wxOutputStream* out_stream = this->process-> GetOutputStream(); + if (out_stream != nullptr) { + wxString line = command + "\n"; + + out_stream->Write(line.mb_str(), line.length()); + } +} + +void Terminal::ChangeDirectory(const wxString& path) { + this->working_directory = path; + this->SendCommand("cd " + path); +} + +void Terminal::MarkLocked() { + this->input_start = this->console->GetLastPosition(); + this->locked_prefix = this->console->GetValue(); +} + +void Terminal::OnChar(wxKeyEvent& event) { + long selection_start, selection_end; + this->console->GetSelection(&selection_start, &selection_end); + + bool crosses_boundary = selection_start != selection_end && selection_start < this->input_start; + + if (crosses_boundary) { + this->console->SetInsertionPointEnd(); + + if (event.GetKeyCode() != WXK_RETURN && event.GetKeyCode() != WXK_BACK && event.GetKeyCode() != WXK_DELETE) { + event.Skip(); + } + + return; + } + + long caret = this->console->GetInsertionPoint(); + + if (event.GetKeyCode() == WXK_RETURN) { + wxString command = this->console-> GetRange(this->input_start, this->console->GetLastPosition()); + this->console->AppendText("\n"); + this->MarkLocked(); + + this->SendCommand(command); + + return; + } + + if ((event.GetKeyCode() == WXK_BACK || event.GetKeyCode() == WXK_DELETE) && caret <= this->input_start) { + return; + } + + if (caret < this->input_start) { + this->console->SetInsertionPointEnd(); + } + + event.Skip(); +} + +void Terminal::OnConsoleClick(wxMouseEvent& event) { + event.Skip(); + this->CallAfter(&Terminal::ClampInsertionPoint); +} + +void Terminal::ClampInsertionPoint() { + if (this->console->GetInsertionPoint() < this->input_start) { + this->console->SetInsertionPointEnd(); + } +} + +void Terminal::OnTextChanged(wxCommandEvent& event) { + event.Skip(); + + if (this->applying_programmatic_change) return; + + wxString current = this->console->GetValue(); + + bool prefix_intact = (long)current.length() >= this->input_start && current.Left(this->input_start) == this->locked_prefix; + + if (!prefix_intact) { + this->applying_programmatic_change = true; + this->console->ChangeValue(this->locked_prefix); + this->console->SetInsertionPointEnd(); + this->applying_programmatic_change = false; + } +} + +void Terminal::ReadStream() { + if (this->process == nullptr) return; + wxInputStream* in_stream = this->process->GetInputStream(); + wxInputStream* err_stream = this->process->GetErrorStream(); + + if (in_stream != nullptr) { + while (this->process->IsInputAvailable()) { + char buffer[4096]; + in_stream->Read(buffer, sizeof(buffer )- 1); + + size_t read_count = in_stream->LastRead(); + + buffer[read_count] = '\0'; + + if (read_count > 0) { + this->console-> AppendText(wxString(buffer, wxConvUTF8)); + this->MarkLocked(); + } + } + } + if (err_stream != nullptr) { + while (this->process->IsErrorAvailable()) { + char buffer[4096]; + err_stream->Read(buffer, sizeof(buffer )- 1); + + size_t read_count = err_stream->LastRead(); + + buffer[read_count] = '\0'; + + if (read_count > 0) { + this->console-> AppendText(wxString(buffer, wxConvUTF8)); + this->MarkLocked(); + } + } + } +} + +void Terminal::OnPollTimer(wxTimerEvent& event) { + this->ReadStream(); +} + +void Terminal::OnProcessTerminated(wxProcessEvent& event) { + this->ReadStream(); + + this->poll_timer.Stop(); + //this->WriteLine("ProcessTerminated"); + this->process = nullptr; +} + +void Terminal::WriteLine(const wxString& text) { + this->console->AppendText(text + "\n"); + this->MarkLocked(); +} diff --git a/src/terminal.hpp b/src/terminal.hpp new file mode 100644 index 0000000..898fe0d --- /dev/null +++ b/src/terminal.hpp @@ -0,0 +1,39 @@ +#ifndef HPP_TERMINAL +#define HPP_TERMINAL + +#include +#include +#include + +class Terminal : public wxPanel { +private: + wxTextCtrl* console; + wxProcess* process; + long pid; + long input_start; + wxString locked_prefix; + wxString working_directory; + bool applying_programmatic_change; + wxTimer poll_timer; + + void StartShell(); + void ReadStream(); + void ClampInsertionPoint(); + void SendCommand(const wxString& command); + void MarkLocked(); + + void OnChar(wxKeyEvent& event); + void OnConsoleClick(wxMouseEvent& event); + void OnTextChanged(wxCommandEvent& event); + void OnPollTimer(wxTimerEvent& event); + void OnProcessTerminated(wxProcessEvent& event); + +public: + Terminal(wxWindow* parent, wxWindowID id, const wxString& working_directory = wxEmptyString); + virtual ~Terminal(); + + void WriteLine(const wxString& text); + void ChangeDirectory(const wxString& path); +}; + +#endif